Bind SelectedItem to correct DataContext in WPF - c#

I have a UserControl barView and corresponding ViewModel barViewModel. This ViewModel have property SelectedSomething, which is binded to different ListBoxes in my view.
If I have construction like this, then everything woks fine:
<UserControl DataContext="barViewModel">
<ListBox ItemsSource="{Binding ObservableCollectionWithItems}"
SelectedItem="{Binding SelectedSomething, Mode=TwoWay}">
....
</ListBox>
</UserControl>
In this case my ViewModel have ObservableCollection with items.
Now I want to split my items to groups. I create a separete class for that:
class ItemsGroup
{
private string _Name;
public string Name {...}
private List<Item> _ItemsList;
public List<Item> ItemsList {...}
}
My barViewModel now contains a observalbe collection of ItemsGroup objects. New view for this looks like this:
<UserControl DataContext="barViewModel">
<ItemsControl ItemsSource="{Binding ObservalbeCollectionWithItemsGroup}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type test:ItemsGroup}">
<Expander IsExpanded="False">
<Expander.Header>
<TextBlock Content="{Binding Name}"/>
</Expander.Header>
<ListBox ItemsSource="{Binding ItemsList}" Margin="10"
SelectedItem="{Binding SelectedSomething, Mode=TwoWay}">
...
</ListBox>
</Expander>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
The problem is that SelectedItem of ListBox is binded to the parent and show me this error:
System.Windows.Data Error: 40 : BindingExpression path error: 'SelectedSomething' property not found on 'object' ''ItemsGroup' (HashCode=10335672)'. BindingExpression:Path=SelectedSomething; DataItem='ItemsGroup' (HashCode=10335672); target element is 'ListBox' (Name=''); target property is 'SelectedItem' (type 'Object')
I tried to change SelectedItem to this:
Text="{Binding SelectedSomething,
RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type UserControl}}}"
This will remove error, but my SelectedSomething is still not binded to the ListBox. How can I fix this?

When SelectedSomething is a property in your main view model, and the UserControl's DataContext is set to an instance of that view model, the binding should look like this:
SelectedItem="{Binding DataContext.SelectedSomething,
RelativeSource={RelativeSource AncestorType=UserControl}}"
Note also that it's not necessary to set Mode=TwoWay on the SelectedItem binding, because the property binds two-way by default.

Related

fill ComboBox with items from different ViewModel

I have a ComboBox binded to DataContext SceneViewModel, but I want to fill it with data from an observableCollection from another ViewModel called GearViewModel.
How do I do this? or is this possible.
Here is the xaml
<UserControl x:Class="MoviePrepper.View.SceneView"
DataContext="{Binding SceneViewModel, Source={StaticResource Locator}}">
<Grid>
<ComboBox ItemsSource="{Binding to observableCollection in GearViewModel}}" SelectedItem="{Binding SceneCollectionView/Equipment, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
</UserControl>
You can achieve this using a binding like this:
<ComboBox ItemsSource="{Binding GearViewModel.MyCollection, Source={StaticResource Locator}}"
SelectedItem="{Binding Equipment, UpdateSourceTrigger=PropertyChanged}"/>
Where the ItemsSource property binds to the GearViewModel.MyCollection property in your Locator, and the SelectedItem binds to the SceneViewModel.Equipment (as set by the DataContext of the UserControl).
It is not clear exactly what property you had in mind for binding on the SelectedItem property, so I made some assumptions.
Anywho, that should solve the problem with binding your ItemsSource property to a different view model.

How to bind to a source inside a ListBox different from the ItemsSource already specified

I have a ListBox inside a HubSection, whose Items are bound to a class "players" added to my DefaulViewModel via code behind.
First I simply put a TextBox bound to the property "PlayerName" of my class "players".
Now I would like to add a ComboBox with some items that are NOT part of the class players.
Is it possible ? I thought that definind an ItemsSource in the ComboBox would sort of override the ItemsSource of the ListBox, but nothing displays.
The DataContext of the whole page is defined like so:
DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}"
Then the HubSection is like so:
<HubSection x:Name="HubSec1">
<DataTemplate>
<ListBox x:Name="ListBox1" ItemsSource="{Binding players}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBox Text="{Binding Path=PlayerName, Mode=TwoWay}"/>
<ComboBox ItemsSource="{Binding Path=ListOfElements}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DataTemplate>
</HubSection>
If I define the ComboBox in the same way but outside the ListBox, it will display the string elements of "ListOfElements" properly.
But in this ListBox, the ComboBox is empty. So my guess is that having defined an ItemsSource for the ListBox, it is not possible to override it.
I have tried to define a DataTemplate but was not successful doing so, but it might be the good solution (and I did not proceed properly)
What am I missing ?
Edit :
The ComboBox items is an ObservableCollection. It is not part of the "players" class.
Here is how I added these elements to the DefaultViewModel
DefaultViewModel.Add("players", players);
DefaultViewModel.Add("MyItemsList", ListOfElements);
You can walk up the visual tree and bind to an ancestors datacontext:
{Binding Path=PathToProperty, RelativeSource={RelativeSource AncestorType={x:Type typeOfAncestor}}}
EX:
{Binding Path=ListOfItems, RelativeSource={RelativeSource AncestorType={x:Type ListBox}}}
that should give you the datacontext that the listbox has, so assuming your ListOfItems exists in that data context.
Or you can name your control, and then bind to its datacontext by element name:
{Binding ElementName=mySourceElement,Path=ListOfItems}
It can be a little bit tricky to create a good working binding in Windows Apps. A widely used work around is to use the Tag property.
<ListBox x:Name="ListBox1" ItemsSource="{Binding players}" Margin="0,184,0,0" Tag="{Binding Path=ListOfElements}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBox Text="{Binding Path=PlayerName, Mode=TwoWay}"/>
<TextBox Text="{Binding Path=Tag, ElementName=ListBox1}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
A binding to an element wirh the specific name will work always. And the ListOfElements should be in the scope of the ListBox so you can use the Tag property as a proxy. If you need to bind more than one property, you can also use dummy XAML elements:
<Border Tag="{Binding ...}" Name="dummy1"/>

Binding to a second property

I have two properties in my viewmodel, called Premises and Towns.
I'm binding my ListViewItems to Premises, and in the itemtemplate I want to bind to Towns, but when I use the following XAML it tries to bind to Premises.Towns instead of Towns.
How can I bind to Towns directly?
Viewmodel:
public class MainWindowViewModel
{
public ObservableCollection<Premise> Premises;
public List<Town> Towns;
}
XAML:
<ListView x:Name="PremisesList" Margin="195,35,10,10"
ItemContainerStyle="{StaticResource OverviewListViewItemStyle}"
ItemsSource="{Binding Premises}" HorizontalContentAlignment="Stretch">
And this is what's in my OverviewListViewItemStyle.
<ComboBox ItemsSource="{Binding Towns}" Grid.Row="2" Grid.ColumnSpan="3">
<ComboBox.ItemTemplate>
<DataTemplate>
<ComboBoxItem>
<TextBox Text="{Binding Name}" />
</ComboBoxItem>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
I'd like to be able to select a Town for a Premise via XAML.
You are correct in your assumption. ComboBox looks for Towns in Premise class, which is the class behind each ListViewItem If you want to refer to same context as ListView you need to use RelativeSource binding.
<ComboBox
ItemsSource="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListView}}, Path=DataContext.Towns}"
Grid.Row="2"
Grid.ColumnSpan="3"
DisplayMemberPath="Name"/>
Not related to your problem but you also don't need to specify DataTemplate to display single property. DisplayMemberPath will work as well. If you do specify DataTemplate you don't need to use ComboBoxItem as ComboBox will wrap DataTemplate content in ComboBoxItem so effectively you'll end up with ComboBoxItem inside another ComboBoxItem
You bind the ItemsSource to the Premises property therefore if you bind to the Towns in the OverviewListViewItemStyle the binding engine will look up in the Premise object for a property called Towns.
If you want to select a town for a premises you should tell to the combobox where to look from that property. You can try to set the combobox's datacontext to the main viewmodel with relative source in the binding. Something like that:
ItemsSource="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListView}}, Path=DataContext.Towns}"

Use a window resource in a DataTemplate with MVVM

I have a DataTemplate that is loading a list of ~7000 items in a list for a combobox. Currently the ItemsSource is bound to a property in the data context of the DataTemplate, however this means that for each instance of the DataTemplate the system is loading all 7k objects, which is slowing down the system by quite a bit.
Ideally I want to be able to load the list once and use it for all instances. The obvious solution to me is using a resource defined in the Window.Resources section. However I can't figure out how this should work, and more importantly, how that resource should be populated via the MVVM pattern.
Current code which loads the ItemsSource for each DataTemplate instance
<DataTemplate>
<ComboBox SelectedItem="{Binding SelectedItem}" ItemsSource="{Binding ItemsSource}" />
</DataTemplate>
Attempt at solving the problem:
<Window.Resources>
<ResourceDictionary>
<sys:Object x:Key="ItemItemsSource" />
</ResourceDictionary>
</Window.Resources>
<DataTemplate>
<ComboBox SelectedItem="{Binding SelectedItem}" ItemsSource="{Binding Source={StaticResource ItemItemsSource}}" />
</DataTemplate>
Update
Each DataTemplate has its own DataContext which means each instance of the data template has its own ItemsSource, which will populate at DataContext initialiser.
Update 2
The ideal way in my mind to solve this is to have a property in the DataContext/VM of the Window that they Combobox is bound too. Is this possible? Something like:
public class WindowsViewModel
{
public List<Object> SharedItemSource { get; set; }
}
<DataTemplate>
<ComboBox SelectedItem="{Binding SelectedItem}" ItemsSource="{Binding <Some Binding To SharedItemSource>}" />
</DataTemplate>
Where is the slow down ?
If it is when you show the ComboBox's popup, maybe you can try to use virtualization like this :
<DataTemplate>
<ComboBox SelectedItem="{Binding SelectedItem}" ItemsSource="{Binding ItemsSource}">
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
</ComboBox>
</DataTemplate>
Create a MainViewModel for your window or what ever control all your combobox's are in ,
cs:
public class MainViewModel
{
private List<object> _itemsSource;
public List<object> ItemsSource
{
get { return _itemsSource; }
set { _itemsSource = value; }
}
}
xaml:
<DataTemplate>
<ComboBox SelectedItem="{Binding SelectedItem}"
ItemsSource="{Binding Path=DataContext.ItemsSource,
RelativeSource={RelativeSource AncestorType=ItemsControl}}"/>
</DataTemplate>
If you have property defined in VM, then that will be loading just once when you will be instantiating it and be served as source for all the comboboxes.. not every combobox create its itemsSource.. it just consume it to generate its items.. so whether you put your itemsSource as Resource or in Datacontext is one and the same thing here.

Why is CommandParameter always null?

anybody an idea why CommandParameter is always null?
The class TransactionViewModel has the collection property of TransactionCommands to be displayed in the ItemsControl. The items are of type CommandViewModel.
TransactionBrowserViewModel has the command AddJobForSelectedTransactionCommand. The command to be passed as a parameter the CommandViewModel.
View Snipp:
<ItemsControl Grid.Row="4"
ItemsSource="{Binding TransactionCommands}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<telerik:RadButton Content="{Binding DisplayName}"
CommandParameter="{Binding DataContext, RelativeSource={RelativeSource Self}}"
Command="{Binding ViewModel.AddJobForSelectedTransactionCommand, ElementName=userControl}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Codebehind of UserControl:
[Export]
public partial class TransactionBrowserView : UserControl, IView<TransactionBrowserViewModel>
{
[ImportingConstructor]
public TransactionBrowserView()
{
InitializeComponent();
}
[Import]
public TransactionBrowserViewModel ViewModel
{
get { return (TransactionBrowserViewModel)this.DataContext; }
set { this.DataContext = value; }
}
}
OK, sorry I have found the error.
It is located on the RadButton by Telerik. I have tested the scenario with a default button. Here it works without any problems.
You have set the ComandParameter to the path of the DataContext of the RadButton, but I don't see that you have set anything to that DataContext anywhere.
Look into the Output window for information regarding your Binding errors... it should say something like 'There is no DataContext property on object XXX'.
What are you trying to bind to the CommandParameter property?
Try this binding
<ItemsControl x:Name="transactionList" Grid.Row="4" ItemsSource="{Binding TransactionCommands}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<telerik:RadButton Content="{Binding DisplayName}"
CommandParameter="{Binding SelectedItem, ElementName=transactionList}"
Command="{Binding ViewModel.AddJobForSelectedTransactionCommand, ElementName=userControl}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Give your ItemsControl (like transactionList) and set the binding of the CommandParameter to the SelectedItem of your transactionList.
or does this not do what you want.
<telerik:RadButton Content="{Binding DisplayName}"
CommandParameter="{Binding}"
Command="{Binding ViewModel.AddJobForSelectedTransactionCommand, ElementName=userControl}"/>

Categories