Use a window resource in a DataTemplate with MVVM - c#

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.

Related

Can I add a ListView or a GridView inside HeaderItem of another ListView? [UWP]

I have the following scenario.
<GridView ItemsSource={Binding X} ItemsTemplate={StaticResource Y}/>
<TextBlock Text={Binding Z} />
<ListView ItemsSource={Binding K} ItemsTemplate={StaticResource P}/>
Now the whole thing needs to be scrollable, so I can put them inside a ScrollViewer like this,
<ScrollViewer>
<GridView ItemsSource={Binding X} ItemsTemplate={StaticResource Y}/>
<TextBlock Text={Binding Z} />
<ListView ItemsSource={Binding K} ItemsTemplate={StaticResource P}/>
</ScrollViewer>
But apparently doing that kills the virtualization of ListView, so instead of doing that, I am trying to put the GridView and TextBlock inside the ListViewHeaderItem. So that I can use the scrollviewer of the listview itself without using a separate scrollviewer, enabling the virtualization to take effect.
So, my question is can I put a gridView with a different itemsSource inside the headerItem of the list? So far I have not been able to as the HeaderItem's data context seems to be the data context of the listview's ItemsSource. I have tried the following way.
<ListView ItemsSource={Binding K} ItemsTemplate={StaticResource P}>
<ListView.HeaderTemplate>
<DataTemplate>
<GridView ItemsSource={Binding X} ItemsTemplate={StaticResource Y}/>
<TextBlock Text={Binding Z} />
</DataTemplate>
</ListView.HeaderTemplate>
</ListView>
Thanks for reading.
If you want the HeaderTemplate contents to have a different DataContext to that of the parent list, you can use the ListView.Header property:
<ListView ItemsSource={Binding K} ItemsTemplate={StaticResource P} Header={Binding HDC}>
<ListView.HeaderTemplate>
<DataTemplate>
<GridView ItemsSource={Binding X} ItemsTemplate={StaticResource Y}/>
<TextBlock Text={Binding Z} />
</DataTemplate>
</ListView.HeaderTemplate>
</ListView>
Where the DataContext of the list is like this:
public class ListDataContext {
public IEnumerable K {get;}
public HeaderDataContext HDC {get;}
public class HeaderDataContext {
public IEnumerable X {get;}
public string Z {get;}
}
}
Your code isn't in a format I can readily test, but there are generally two ways to do this:
1) Give the parent window a name e.g. 'x:Name=_this' and then change your GridView binding to ItemsSource={Binding X, ElementName=_this}.
2) Use a DataProxy (see this article for code and description) and change your GridView binding to ItemsSource={Binding Data.X, Source={StaticResource proxy}}.
In my experience both worked with WPF but only the second worked with Xamarin. Haven't had to do this with UWP myself but you'll probably find at least one of them works.

Binding complex data to an ItemsControl

So basically I have an list of an object that contains another List of objects. Lets say I have an List of the object Class. And Class contains a list of Students. Every student has a property Name as a simple string.
So basically what I want is the following:
The user can select a class using a ComboBox.
<ComboBox ItemsSource="{Binding Path=Classes}" DisplayMemberPath="Name" />
That works.
After selecting an Item from that ComboBox, the user should see a list of every student in that class (remember the property Name in Students)
I have created a simple ItemsControl for that purpose.
<ItemsControl ItemsSource="{Binding Classes}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Label Content="Name of the Student">
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
My question is: How do I get access to the students name in my label?
Your view model should have a SelectedClass property, which would be updated by binding it to the ComboBox's SelectedItem property:
<ComboBox ItemsSource="{Binding Classes}"
SelectedItem="{Binding SelectedClass}" .../>
You would then bind the ItemsControl to the Students collection of the selected Class like this:
<ItemsControl ItemsSource="{Binding SelectedClass.Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Note that the view model must implement the INotifyPropertyChanged interface and fire the PropertyChanged event when SelectedClass changes.
In a quick and dirty approach without a SelectedClass view model property, you could also directly access the ComboBox's SelectedItem like this:
<ComboBox x:Name="cbClasses" ItemsSource="{Binding Classes}" ... />
<ItemsControl ItemsSource="{Binding SelectedItem.Students, ElementName=cbClasses}">
...

wpf master detail listviews binding not working

I have three viewmodels:
AccountListViewModel(has a collection of accountviewmodels and a selectedaccount property)
public ObservableCollection<AccountViewModel> AccountList
public AccountViewModel SelectedAccount
AccountViewModel(has a collection of transactionviewmodel)
public ObservableCollection<TransactionViewModel> Transactions
and a TransactionViewModel.
I set the datacontext of the mainwindow to the AccountListViewModel and I have two Usercontrols(AccountListView and TransactionListView) that I set in the mainwindow as content control.
<DataTemplate x:Key="AccountListTemplate" >
<views:AccountListView />
</DataTemplate>
<DataTemplate x:Key="TransactionListTemplate" >
<views:TransactionListView />
</DataTemplate>
<ContentControl Content="{Binding}" ContentTemplate=" {StaticResource AccountListTemplate}" />
<ContentControl Grid.Row="1" Content="{Binding SelectedAccount}" ContentTemplate="{StaticResource TransactionListTemplate}" />
This is the itemssource of the AccountListView usercontrol
<ListView x:Name="AccountList" ItemsSource="{Binding Source={StaticResource GroupedAccounts}}" SelectedItem="{Binding SelectedAccount}" >
and the itemssource for the TransactionListView usercontrol
<ListView ItemsSource="{Binding Transactions}" >
The AccountList is displaying correctly but when I select an account no transactions are displayed although the logic is correct I think.
The Content property of the contentcontrol that holds the TransactionListView is set to SelectedAccount and the TransactionListView itemsource is the Transactions collection so whenever i click an account the transactions collection of that account should be displayed in the second contentcontrol.
Try using SelectedValue instead of SelectedItem and see if that works. You may also have to change your mode to two-way and update your source trigger.
<DataGrid ItemsSource="{Binding Source}"
SelectedValue="{Binding Selected}"
Margin="10">
<DataGrid.Columns>
<DataGridCheckBoxColumn Header="Test Checked" Binding="{Binding S}"/>
</DataGrid.Columns>
</DataGrid>
This is an example of a data grid, but the same concepts apply.

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}"

Escaping a WPF DataContext

I'm very new to MVVM in particular.
I have the following piece of XAML code:
<ListBox x:Name="lsbTriggers" ItemsSource="{Binding SelectedProductPart.TriggerViewModels}">
<ListBox.ItemTemplate>
<HierarchicalDataTemplate >
<ComboBox SelectedItem="{Binding WatchedVariable}"
ItemsSource="{Binding SelectedProductPart.AllVariables}" >
</ComboBox>
</HierarchicalDataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I'm trying to make a list that contains a combobox for each TriggerViewModel.
The combobox's selected item is determined by the TriggerViewModel's WatchedVariable property.
However, I want the ItemsSource of the combobox to be a list of variables provided by the SelectedProductPart object.
I seem to be unable to do this because the datacontext has "zoomed in", if you will, on TriggerViewModels, due to it being the list's ItemsSource.
I've tried creating a new DataContext inside the combobox, but this seems to create two disconnected DataContexts where changing the value of the combobox does not result in a change of TriggerViewModel's WatchedVariable.
Is there a way I can escape the current DataContext so I can get to the SelectedProductPart's AllVariables list?
You have to use a Binding with explicit way specifying its source (via ElementName, Source or RelativeSource). In this case we use RelativeSource. It helps walk up the visual tree and target the source based on its type (AncestorType):
<ListBox x:Name="lsbTriggers" ItemsSource="{Binding SelectedProductPart.TriggerViewModels}">
<ListBox.ItemTemplate>
<HierarchicalDataTemplate >
<ComboBox SelectedItem="{Binding WatchedVariable}"
ItemsSource="{Binding DataContext.SelectedProductPart.AllVariables,
RelativeSource={RelativeSource AncestorType=ListBox}}" >
</ComboBox>
</HierarchicalDataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Note the Path is prepended with DataContext.

Categories