WPF MMVM navigate to details page by clicking on item [duplicate] - wpf

This question already has answers here:
WPF Binding ListBox Master/Detail
(2 answers)
Closed 4 months ago.
I have an objects, which I am receiving from an API call, I am putting them into a ListView
<ListView Background="#222222"
ScrollViewer.CanContentScroll="False"
ItemsSource="{Binding Assets}"
Style="{StaticResource ListStyle}">
</ListView>
By clicking on the item in the list, I want to get it's values (in order to call an api again on a specific object to get more info), and navigate user to the new ViewModel "details" page where I will output the api response. I can navigate a user to the new ViewModel page by creating a button with command
Command="{Binding HomeViewCommand}//as button parameter
//Actual method in ViewModel
HomeViewCommand = new RelayCommand(o =>
{
CurrentView = HomeVm;
});
But I don't know how to get the values of the item in the list user Clicked. Any help whould be nice

Bind the SelectedItem property of the ListView to a view model property:
<ListView Background="#222222"
ScrollViewer.CanContentScroll="False"
ItemsSource="{Binding Assets}"
SelectedItem="{Binding SelectedAsset}"
Style="{StaticResource ListStyle}">
</ListView>
...and get the values from this one when the command is executed:
public SelectedAsset { get; set; }
...
HomeViewCommand = new RelayCommand(o =>
{
var selectedAsset = this.SelectedAsset;
...
});
The other option would be to pass the currently selected or clicked item to the command as a command parameter using the CommandParameter property.

Related

WPF Dynamic Context Menu with Caliburn.Micro

I am trying to create a dynamic context menu in my Caliburn.Micro based application. Can anyone share an example of an effective way to do that?
So far, I have a very minimal model for each context menu item:
public class ContextMenuUiModel
{
public string Name { get; set; }
}
a property in my view model that presents a list of those menu item models:
BindableCollection<ContextMenuUiModel> m_ContextMenuItems = new BindableCollection<ContextMenuUiModel>
{
new ContextMenuUiModel { Name="Item 1"},
new ContextMenuUiModel { Name="Item 2"},
new ContextMenuUiModel { Name="Item 3"}
};
public BindableCollection<ContextMenuUiModel> ContextMenuItems
{
get {return m_ContextMenuItems;}
}
and, a menu item named for the collection property (based on the menu creation in FreePIE, found via this question and answer)
<TreeView x:Name="ConfigItemTree" VerticalAlignment="Top" ItemsSource="{Binding ConfigTreeRoot}" >
<TreeView.ContextMenu>
<ContextMenu >
<MenuItem x:Name="ContextMenuItems" DisplayMemberPath="Name" />
</ContextMenu>
</TreeView.ContextMenu>
Caliburn.Micro logging reports "No actionable element for get_ContextMenuItems". Also, although Caliburn is noting other named elements for which no property was found (e.g. "Binding Convention Not Applied: Element ConfigItemTree did not match a property."), it is not making a similar statement for ContextMenuItems. So, it seems Caliburn is just not seeing the ContextMenu as an element it could or should deal with.
Maybe the issue is that Caliburn can't see the context menu because it doesn't actually exist until a right click happens (similar to this issue with collapsed elements)?
Ultimately, I would like the context menu's contents to be based on the tree view item that was right clicked, possibly including sub menus and/or disabled items. For a start, though, I'll settle for whatever items I can get.
Bind the ItemsSource property of the ContextMenu to the ContextMenuItems property:
<ContextMenu ItemsSource="{Binding PlacementTarget.DataContext.ContextMenuItems, RelativeSource={RelativeSource Self}}"
DisplayMemberPath="Name" />

Bind Selected Item to comboBox with caliburn micro

I have seen and tested a lot of material about this and I have the problem again. I have a collection of cities bound to the combobox and I want to set the selected item (selected city) when I click the button (for simplicity)
this is my combobox in xaml:
<ComboBox Grid.Column="1"
HorizontalAlignment="Left"
Margin="65,10,0,0"
Height="25"
Name="Cities"
DisplayMemberPath="Name"
IsSynchronizedWithCurrentItem="True"
VerticalAlignment="Top"
Width="71">
</ComboBox>
this is code in corresponding ViewModel:
private City _selectedCity;
public City SelectedCity
{
get { return _selectedCity; }
set
{
_selectedCity = value;
NotifyOfPropertyChange(() => SelectedCity);
MessageBox.Show(SelectedCity.Name);
}
}
When I click the button this line of code is executed:
SelectedCity = CitySecond;
where, CitySecond is one random item from the Cities Collection.
When I click this button the desired city name appears in MessageBox, but nothing changes in combobox. I tested it in Debug mode and everything looks fine, but as it seems comboBox doesn't feels that something has changed.
I have also added the following line of code in combobox xaml definition but nothing changed:
SelectedItem="{Binding SelectedCity}"
What Can I do?
EDIT:
I have created another project, where everything works fine, but here is mystery. Does anything other affects the combobox to work properly?

WPF Xaml GridView SelectionChanged Behavior

I am working with a WPF view with Prism.MVVM which allows our users to edit records.
Originally the record to be edited was selected via ComboBox.
<ComboBox IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Records}"
SelectedItem="{Binding SelectedRecord}"/>
This worked, but users wanted a more efficient way of finding which records had fields which needed updating so we have added a read only DataGrid which they can sort and visually spot which records they are interested in. Next they want to select the record to edit off the grid (but keep the combo box). This is where things go wrong.
Ideally the behavior we are looking for is:
If user selects a record from combo box:
The selected record is loaded in the form
The selected record is shown as selected in the combo box.
The selected record is shown as selected in the grid.
If user selects a record in Grid
single click to select record.
The selected record is loaded in the form
The selected record is shown as selected in the combo box
The selected record is shown as selected in the grid.
Most Successful Attempt
Trigger Command on SelectionChanged event of DataGrid
<DataGrid x:Name="TheDataGrid"
ItemsSource="{Binding Source={StaticResource GridRecords}}"
SelectedItem="DataContext.SelectedRecord, ElementName=LayoutRoot, Mode=OneWay}">
...
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction CommandParameter="{Binding SelectedItem, ElementName=TheDataGrid}"
Command="{Binding DataContext.SelectRecordFromGridCommand, ElementName=LayoutRoot}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
DelegateCommand:
Public ReadOnly Property SelectRecordFromGridCommand As DelegateCommand(Of TheRecordType) = new DelegateCommand(Of TheRecordType)(Sub(r) SelectedRecord = r)
This was attempted with various options for the SelectedItem binding mode.
If the DataGrid SelectedItem binding is removed, We get 1,2,4,5,6, and 7. but selecting the record from the combo box would not show the record as selected in the grid.
If the DataGrid SelectedItem binding is set to OneWay, Selecting a record via the combo box breaks: Setting SelectedRecord triggers the SelectionChanged event in the DataGrid, which uses the value before the event and effectively sets everything back to the original value.
This can be remedied by introducing a sentinal on the Set of the Property in the ViewModel
Private _selectedRecord As TheRecordType
Private _enableRecordSelection As Boolean = true
Public Property SelectedRecord As TheRecordType
Get
Return _selectedRecord
End Get
Set(value As TheRecordType)
If _enableRecordSelection
_enableRecordSelection = false
SetProperty(_selectedRecord , value)
_enableRecordSelection = true
End If
End Set
End Property
This actually works, and we came up with it while writing the question, but feels horribly hacky. My gut is telling me there has to be a better way so I'm still asking:
Is there a clean (preferably xaml only) way to set this up?
The other most successful things we tried:
Straight xaml configuration for the DataGrid with TwoWay binding
<DataGrid x:Name="TheDataGrid"
ItemsSource="{Binding Source={StaticResource GridRecords}}"
SelectedItem="DataContext.SelectedRecord, ElementName=LayoutRoot, Mode=TwoWay}"/>
With this, we satisfy requirements 1 through 6; however when selecting the record through the grid, the previous record is always highlighted instead of the current one.
DataGrid.InputBindings
<DataGrid.InputBindings>
<MouseBinding Gesture="LeftClick"
CommandParameter="{Binding SelectedItem, ElementName=TheDataGrid}"
Command="{Binding DataContext.SelectRecordFromGridCommand, ElementName=LayoutRoot}"/>
</DataGrid.InputBindings>
With no SelectedItem binding, this behaves similarly to the no binding InteractionTrigger on SelectionChanged, except it requires the user to perform multiple mouse actions. A first click selects the row in the grid (actual bold blue selection) The second click triggers the Command.
With a OneWay binding on SelectedItem, this behaves similarly to the straight xaml config, again except needing to click multiple times.
Again to reiterate the question:
Is there a cleaner way to accomplish the 7 requirements than to resort to the sentinal value on the property setter?
According to you ask, I understand that you want to sync the selected item in Datagrid and ComboBox. If I were you, I will use the that two control binding the same object(SelectedRecord). I only familiar with C#, so the code is write in C#. Hope it can help you.
For XAML:
<DataGrid ItemsSource="{Binding Records}"
SelectedValue="{Binding SelectedRecord}" />
<ComboBox Grid.Column="1" ItemsSource="{Binding Records}"
DisplayMemberPath="Id"
SelectedValue="{Binding SelectedRecord}" />
For ViewModel:
public ObservableCollection<Record> Records { get; } = new ObservableCollection<Record>();
public Record SelectedRecord
{
get { return _selectedRecord; }
set
{
_selectedRecord = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public class Record
{
private static int id = 0;
public Record()
{
Id = ++id;
}
public int Id { get; set; }
}

Silverlight 3 data-binding child property doesn't update

I have a Silverlight control that has my root ViewModel object as it's data source. The ViewModel exposes a list of Cards as well as a SelectedCard property which is bound to a drop-down list at the top of the view. I then have a form of sorts at the bottom that displays the properties of the SelectedCard. My XAML appears as (reduced for simplicity):
<StackPanel Orientation="Vertical">
<ComboBox DisplayMemberPath="Name"
ItemsSource="{Binding Path=Cards}"
SelectedItem="{Binding Path=SelectedCard, Mode=TwoWay}"
/>
<TextBlock Text="{Binding Path=SelectedCard.Name}"
/>
<ListBox DisplayMemberPath="Name"
ItemsSource="{Binding Path=SelectedCard.PendingTransactions}"
/>
</StackPanel>
I would expect the TextBlock and ListBox to update whenever I select a new item in the ComboBox, but this is not the case. I'm sure it has to do with the fact that the TextBlock and ListBox are actually bound to properties of the SelectedCard so it is listening for property change notifications for the properties on that object. But, I would have thought that data-binding would be smart enough to recognize that the parent object in the binding expression had changed and update the entire binding.
It bears noting that the PendingTransactions property (bound to the ListBox) is lazy-loaded. So, the first time I select an item in the ComboBox, I do make the async call and load the list and the UI updates to display the information corresponding to the selected item. However, when I reselect an item, the UI doesn't change!
For example, if my original list contains three cards, I select the first card by default. Data-binding does attempt to access the PendingTransactions property on that Card object and updates the ListBox correctly. If I select the second card in the list, the same thing happens and I get the list of PendingTransactions for that card displayed. But, if I select the first card again, nothing changes in my UI! Setting a breakpoint, I am able to confirm that the SelectedCard property is being updated correctly.
How can I make this work???
If you are using Silverlight 3 you will need to use INotifyPropertyChanged.
Example:
public class CardViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<Card> Cards { get; set; }
private Card _selectedCard;
public SelectedCard
{
get
{
return _selectedCard;
}
set
{
if (value != _selectedCard)
{
_selectedCard = value;
NotifyPropertyChanged("SelectedCard");
}
}
}
public CardViewModel()
{
Cards = new ObservableCollection<Card>();
//Populate Cards collection with objects
}
public void NotifyPropertyChanged(string item)
{
if (PropertyChanged!=null)
{
PropertyChanged(this, new PropertyChangedEventArgs(item));
}
}
}
All you would need to do is set this class to your views DataContext and everything should be happy.
A pattern I've been using recently is to bind the data context of a container of detail info to the selected item of the list box. The XAML in your case becomes:
<StackPanel Orientation="Vertical">
<ComboBox x:Name="_lbxCards" <-- new
DisplayMemberPath="Name"
ItemsSource="{Binding Path=Cards}"
SelectedItem="{Binding Path=SelectedCard, Mode=TwoWay}"
/>
<StackPanel DataContext={Binding ElementName=_lbxCards,Path=SelectedItem}> <-- new
<TextBlock Text="{Binding Path=Name}" <-- updated
/>
<ListBox DisplayMemberPath="Name"
ItemsSource="{Binding Path=PendingTransactions}" <-- updated
/>
</StackPanel> <-- new
</StackPanel>
Turns out the problem isn't in the UI at all. The PendingTransactions class lazy-loads its values using a async WCF call to the server. The async pattern uses events to notify the caller that the operation is complete so the data can be parsed into the class. Because each Card has its own instance of the PendingTransactions class and we used a ServiceFactory to manage our WCF proxies, each instance was wiring up their event handler to the same event (we are using a singleton approach for performance reasons - for the time being). So, each instance received the event each time any of the instances triggered the async operation.
This means that the data-binding was working correctly. The PendingTransactions collections were overwriting themselves each time a new Card was viewed. So, it appeared that selecting a previous card did nothing when, in fact, it was selecting the correct object for binding, it was the data that was screwed up and make it look like nothing was changing.
Thanks for the advice and guidance nonetheless!

How to bind an observable collection to Multiple user controls at runtime?

I am stucked at the part where I have to bind a collection to a dynamic usercontrol. Scenario is something like this.
I have a dynamic control, having a expander , datagrid, combobox and textbox, where combox and textbox are inside datagrid. There are already two collections with them. One is binded with combobox and another is binded with datagrid. When the item is changes in combox its respective value is set to its respective textbox, and so on. and this pair of value is then set to the collection binded with datagrid. A user can add multiple items.
Now the main problem is that all these things are happening inside a user control which is added dynamically, that is on button click event. A user can add desired numbers of user controls to the form.
problem is coming in this situtaion. Say I have added 3 controls. Now in 1st one if i add a code to the collection then it gets reflected in the next two controls too, as they are binded with same collection.
So, I want to know is there anyway to regenrate/rename the same collection so that the above condition should not arise.
It's hard to answer your question without seeing the bigger picture, however I have a feeling you are going about this the wrong way. It appears that you are adding instances of your user control directly from code. Instead of doing that, you should create some kind of ItemsControl in your XAML, and in its ItemTemplate have your user control. Bind that ItemsControl to a collection in your view model, and only manipulate that collection.
You should not be referring to visual controls in your view model or code behind. Whenever you find yourself referencing visual elements directly from code, it should raise a warning flag in your mind "Hey! There's a better way than that!"...
Example:
The view model:
public class ViewModel
{
public ObservableCollection<MyDataObject> MyDataObjects { get; set; }
public ViewModel()
{
MyDataObjects = new ObservableCollection<MyDataObject>
{
new MyDataObject { Name="Name1", Value="Value1" },
new MyDataObject { Name="Name2", Value="Value2" }
};
}
}
public class MyDataObject
{
public string Name { get; set; }
public string Value { get; set; }
}
The window XAML fragment containing the list box and the data template:
<Window.Resources>
...
<DataTemplate x:Key="MyDataTemplate">
<local:MyUserControl/>
</DataTemplate>
</Window.Resources>
...
<ListBox ItemsSource="{Binding MyDataObjects}"
ItemTemplate="{StaticResource MyDataTemplate}"
HorizontalContentAlignment="Stretch"/>
The user control:
<UniformGrid Rows="1">
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Value}" HorizontalAlignment="Right"/>
</UniformGrid>

Resources