UI slow at updating ObservableCollection<T> in TreeView control - c#

Scenario
I have a TreeView that is bound to ObservableCollection<T>. The collection gets modified every time the end-user modifies their filters. When users modify their filters a call to the database is made (takes 1-2ms tops) and the data returned gets parsed to create a hierarchy. I also have some XAML that ensures each TreeViewItem is expanded, which appears to be part of the problem. Keep in mind that I'm only modifying ~200 objects with a max node depth of 3. I would expect this to instant.
Problem
The problem is that whenever filters get modified and the TreeView hierarchy gets changed the UI hangs for ~1 second.
Here is the XAML responsible for create the TreeView hierarchy.
<TreeView VerticalAlignment="Top" ItemsSource="{Binding Hierarchy}" Width="240"
ScrollViewer.VerticalScrollBarVisibility="Auto" Grid.Row="1">
<TreeView.ItemTemplate>
<!-- Hierarchy template -->
<HierarchicalDataTemplate ItemsSource="{Binding Stations}">
<TextBlock Text="{Binding}" />
<!-- Station template -->
<HierarchicalDataTemplate.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Locates}">
<TextBlock Text="{Binding Name}" />
<!-- Locate template -->
<HierarchicalDataTemplate.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding TicketNo}" />
</DataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
And here is the code for updating the list.
public ObservableCollection<HierarchyViewModel> Hierarchy
{
get { return _hierarchy; }
set { _hierarchy = value; }
}
public void UpdateLocates(IList<string> filterIds)
{
_hierarchy.Clear();
// Returns 200 records max
var locates = _locateRepository.GetLocatesWithFilters(filterIds);
var dates = locates.Select(x => x.DueDate);
foreach (var date in dates)
{
var vm = new HierarchyViewModel
{
DueDate = date
};
var groups = locates.Where(x => x.DueDate.Date.Equals(date.Date)).GroupBy(x => x.SentTo);
// Logic ommited for brevity
_hierarchy.Add(vm);
}
}
I also have <Setter Property="IsExpanded" Value="True" /> as a style. I have tried using a BindingList<T> and disabling notifications, but that didn't help.
Any ideas as to why my UI hangs whenever changes are made to the ObservableCollection<T>?
Partial Solution
With what H.B. said and implementing a BackgroundWorker the update is much more fluid.

The problem is probably the foreach loop. Every time you add an object the CollectionChanged event is fired and the tree is rebuilt.
You do not want to use an ObservableCollection if all you do is clear the whole list and replace it with a new one, use a List and fire a PropertyChanged event once the data is fully loaded.
i.e. just bind to a property like this (requires implementation of INotifyPropertyChanged):
private IEnumerable<HierarchyViewModel> _hierarchy = null;
public IEnumerable<HierarchyViewModel> Hierarchy
{
get { return _hierarchy; }
set
{
if (_hierarchy != value)
{
_hierarchy = value;
NotifyPropertyChanged("Hierarchy");
}
}
}
If you set this property bindings will be notified. Here i use the IEnumerable interface so no-one tries to just add items to it (which would not be noticed by the binding). But this is just one suggestion which may or may not work for your specific scenario.
(Also see sixlettervariable's good comment)
Just a side note, this code:
public ObservableCollection<HierarchyViewModel> Hierarchy
{
get { return _hierarchy; }
set { _hierarchy = value; }
}
is bad, you could overwrite the list and the binding would break because there is no PropertyChanged event being fired in the setter.
If you use an ObservableCollection it normally is used like this:
private readonly ObservableCollection<HierarchyViewModel> _hierarchy =
new ObservableCollection<HierarchyViewModel>();
public ObservableCollection<HierarchyViewModel> Hierarchy
{
get { return _hierarchy; }
}

The easiest thing to do is detach the item you are bound to, make all the changes you need to the list, then reattach it.
For example, set the treeviews ItemsSource to NULL/NOTHING, run through your for each, then set the ItemsSource back to _hierarchy. Your adds will be instant.

Related

WPF DataTemplateSelector is not used

I am trying to modify a existing WPF application in a way so that a newer version of some data object can be used alongside the old one. And so i avoid redundant code by extending the existing ViewModel with new fields where the old ones cannot be reused.
public IList<G1VU.PDR> VuG1 { get; set; }
public IList<G2VU.PDR> VuG2 { get; set; }
public PlacesCompound VuP
{
get
{
if (VuG1 != null && VuG2 == null)
{
return new PlacesCompound {
G1 = VuG1,
G2 = null
};
}
if (VuG2 != null && VuG1 == null)
{
return new PlacesCompound {
G1 = null,
G2 = VuG2
};
}
throw new Exception("G1 and G2 data present or no data present");
}
}
VuG1 has existed before and i have added a new property VuG2 for the new data. As you can see these are not the same class so i cannot interchange them. For that reason i've added a property that will return either of the two in a PlacesCompound class, which is just a class with two properties and nothing else.
In the corresponding usercontrol (lets call it ActivitiesView) we have a DataGrid which binds to the ViewModel and somewhere a TabItem that will display a custom UserControl places which binds to VuG1 on the ViewModel. I have copied it and changed it so it will work with VuG2 Data.
And i created a custom DataTemplateSelector which will decide what Template to use based on which variable of PlacesCompound isnt null.
In VUActivitiesResources.xaml i have then declared 2 DataTemplates one for each places UserControl and the DataTemplateSelector.
<activities:VUActivitiesViewDataTemplateSelector x:Key="PlacesTemplateSelector"/>
<DataTemplate x:Key="VuG2Template">
<places:VUPViewG2 DataContext="{Binding VuG2}" HorizontalAlignment="Left"/>
</DataTemplate>
<DataTemplate x:Key="VuG1Template">
<places:VUPViewG1 DataContext="{Binding VuG1}" HorizontalAlignment="Left"/>
</DataTemplate>
VUActivitiesResources.xaml is being referenced in ActivitiesView as UserControl.Resources.
In the ActivitiesView i placed a ItemsControl into the TabItem replacing the custom places UserControl (ive also tried a ListBox instead of a ItemsControl, but neither works)
<TabItem IsEnabled="{Binding PlacesIsVisible}">
...
<ItemsControl
ItemsSource="{Binding VuP}"
ItemTemplateSelector="{StaticResource PlacesTemplateSelector}"></ItemsControl>
</TabItem>
My question: why PlacesTemplateSelector is never used and how do i make it being used? Because right now while debugging i can see that in ViewModel VuP returns a PlacesCompound object correctly but the Selector is never entered. I want one of the two DataTemplates to show up in the TabItem and right now none is showing.
ItemsSource must be a collection (which your PlacesCompound is not), in your case this would be either VuG1 or VuG2. If the two item classes have no common base class, you can still use IEnumerable:
public IEnumerable VuP => (IEnumerable)VuG1 ?? VuG2;
Then, instead of writing a TemplateSelector, just let the WPF DataTemplating mechanism do its work:
<DataTemplate DataType="{x:Type namespace1:PDR}">
<places:VUPViewG2 HorizontalAlignment="Left"/>
</DataTemplate>
<DataTemplate DataType="{x:Type namespace2:PDR}">
<places:VUPViewG1 HorizontalAlignment="Left"/>
</DataTemplate>
<ItemsControl ItemsSource="{Binding VuP}" />

How to Update ComboBox ItemsSource Without Flickering

I am struggling with an update to a ComboBox that previously worked. I originally had its ItemsSource bound to a read-only ObservableCollection<char> property in the ViewModel. When the user instigates changes (which is done with mouse strokes, so dozens of times per second in some cases), the get rebuilds the collection from the Model and returns it.
When I changed to my own object in the ObservableCollection, the ComboBox started flickering during updates. I'm not sure what's going wrong. Here's the code that works, starting with the XAML:
<ComboBox ItemsSource='{Binding FromBins}' SelectedValue='{Binding SelectedFromBin, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}' />
ViewModel:
public ObservableCollection<char> FromBins
{
get
{
ObservableCollection<char> tempBins = new ObservableCollection<char>();
foreach (var item in Map.BinCounts)
{
tempBins.Add(item.Key);
}
return tempBins;
}
}
I simply raise a property change with every mouse movement and the interface works as expected (there is some other logic to ensure the SelectedItem is valid).
To make the interface more useful, I decided to add more information to the ComboBox, using my own class:
public class BinItem : IEquatable<BinItem>
{
public char Bin { get; set; }
public SolidColorBrush BinColor { get; set; }
public string BinColorToolTip { get {...} }
public BinItem( char bin )
{
Bin = bin;
BinColor = new SolidColorBrush(BinColors.GetBinColor(bin));
}
public bool Equals(BinItem other)
{
return other.Bin == Bin ? true : false;
}
}
If I swap char out for BinItem in the working code ViewModel I get flickering as the mouse is moved. Here is the updated XAML:
<ComboBox ItemsSource='{Binding FromBins}' SelectedValue='{Binding SelectedFromBin, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}'>
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" ToolTip='{Binding BinColorToolTip}'>
<Rectangle Fill='{Binding BinColor}' Width='10' Height='10' HorizontalAlignment='Center' VerticalAlignment='Center' Margin='0,0,4,0' Stroke='#FF747474' />
<TextBlock Text="{Binding Bin}" Width='16' />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
I have tried numerous things, including but not limited to:
-Using a List instead of the ObservableCollection but even though the Get fires every time and returns the correct collection of items, the interface does not always update (though the flickering disappears).
-Leaving all possible bins in the items source and adding a Visibility property to the BinItem class that I bound to (couldn't get it to update).
I suspect I am doing something fundamentally wrong, but no amount of searching SO or otherwise has helped thus far. Any help is appreciated.
I was able to solve this using the ideas from Clemens and Chris. Not sure if this is the most elegant solution, but it works as intended with no measurable performance hit.
Instead of replacing the collection with each refresh, I go through the logic of finding out what's changed (with each update there could be an addition AND a removal simultaneously). Code below:
private ObservableCollection<BinItem> _FromBins = new ObservableCollection<BinItem>();
public ObservableCollection<BinItem> FromBins
{
get
{
if (_FromBins.Count > 0)
{
List<char> BinsToRemove = new List<char>();
foreach (var item in _FromBins)
{
if (!Map.BinCounts.ContainsKey(item.Bin))
{
BinsToRemove.Add(item.Bin);
}
}
foreach (var item in BinsToRemove)
{
_FromBins.Remove(new BinItem(item));
}
}
foreach (var item in Map.BinCounts)
{
if (!_FromBins.Contains(new BinItem(item.Key)) && item.Value > 0) {
_FromBins.Add(new BinItem(item.Key));
}
}
return _FromBins;
}
}
Hope this can help someone else too.

Organize Items in DropDownButton?

I've a collection of items inside an ObservableCollection, each item have a specific nation name (that's only a string). This is my collection:
private ObservableCollection<League> _leagues = new ObservableCollection<League>();
public ObservableCollection<League> Leagues
{
get
{
return _leagues;
}
set
{
_leagues = value;
OnPropertyChanged();
}
}
the League model have only a Name and a NationName properties.
The Xaml looks like this:
<Controls:DropDownButton Content="Leagues" x:Name="LeagueMenu"
ItemsSource="{Binding Leagues}"
ItemTemplate="{StaticResource CombinedTemplate}" >
<Controls:DropDownButton.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding NationName}" />
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</Controls:DropDownButton.GroupStyle>
</Controls:DropDownButton>
but I doesn't get any header for the NationName property, the items inside the DropDown are organized without header but as list, so without organization.
I'm trying to get this predisposition.
What am I doing wrong?
Preliminaries
Grouping items in an ItemsControl in WPF (which DropDownButton derives from) is fairly simple, and is accomplished in two steps. First you need to set up the items source by tweaking an ICollectionView associated with the source collection. Then you need to populate the ItemsControl.GroupStyle collection with at least one GroupStyle item - otherwise the items are presented in a plain (non-grouped) manner.
Diagnosis
The main issue you're facing is getting the drop-down to present the items in a grouped manner. Unfortunately, unlike setting up the items source, it is not something that is easily accomplished in case of the DropDownButton control. The reason for that stems from the way the control (or, more precisely, its template) is designed - the drop-down is presented inside a ContextMenu attached to a Button which is part of the template (see MahApps.Metro source code). Now ContextMenu also derives from ItemsControl, and most of its properties are bound to corresponding properties of the templated DropDownButton. That is however not the case for its GroupStyle property, because it's a read-only non-dependency property, and cannot be bound or event styled. That means that even if you add items to DropDownButton.GroupStyle collection, the ContextMenu.GroupStyle collection remains empty, hence the items are presented in non-grouped manner.
Solution (workaround)
The most reliable, yet most cumbersome solution would be to re-template the control and add GroupStyle items directly to the ContextMenu.GroupStyle collection. But I can offer you a much more concise workaround.
First of all, let's deal with the first step - setting up the items source. The easiest way (in my opinion) is to use CollectionViewSource in XAML. In your case it would boil down to something along these lines:
<mah:DropDownButton>
<mah:DropDownButton.Resources>
<CollectionViewSource x:Key="LeaguesViewSource" Source="{Binding Leagues}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="NationName" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</mah:DropDownButton.Resources>
<mah:DropDownButton.ItemsSource>
<Binding Source="{StaticResource LeaguesViewSource}" />
</mah:DropDownButton.ItemsSource>
</mah:DropDownButton>
Now for the main part - the idea is that we'll create a helper class that will contain one attached dependency property that will assign an owner DropDownButton control to the ContextMenu responsible for presenting its items. Upon changing the owner we'll observe its DropDownButton.GroupStyle collection and use ContextMenu.GroupStyleSelector to feed the ContextMenu with items coming from its owner's collection. Here's the code:
public static class DropDownButtonHelper
{
public static readonly DependencyProperty OwnerProperty =
DependencyProperty.RegisterAttached("Owner", typeof(DropDownButton), typeof(DropDownButtonHelper), new PropertyMetadata(OwnerChanged));
public static DropDownButton GetOwner(ContextMenu menu)
{
return (DropDownButton)menu.GetValue(OwnerProperty);
}
public static void SetOwner(ContextMenu menu, DropDownButton value)
{
menu.SetValue(OwnerProperty, value);
}
private static void OwnerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var menu = (ContextMenu)d;
if (e.OldValue != null)
//unsubscribe from the old owner
((DropDownButton)e.OldValue).GroupStyle.CollectionChanged -= menu.OwnerGroupStyleChanged;
if (e.NewValue != null)
{
var button = (DropDownButton)e.NewValue;
//subscribe to new owner
button.GroupStyle.CollectionChanged += menu.OwnerGroupStyleChanged;
menu.GroupStyleSelector = button.SelectGroupStyle;
}
else
menu.GroupStyleSelector = null;
}
private static void OwnerGroupStyleChanged(this ContextMenu menu, object sender, NotifyCollectionChangedEventArgs e)
{
//this method is invoked whenever owners GroupStyle collection is modified,
//so we need to update the GroupStyleSelector
menu.GroupStyleSelector = GetOwner(menu).SelectGroupStyle;
}
private static GroupStyle SelectGroupStyle(this DropDownButton button, CollectionViewGroup group, int level)
{
//we select a proper GroupStyle from the owner's GroupStyle collection
var index = Math.Min(level, button.GroupStyle.Count - 1);
return button.GroupStyle.Any() ? button.GroupStyle[index] : null;
}
}
In order to complete the second step we need to bind the Owner property for the ContextMenu (we'll use DropDownButton.MenuStyle to do that) and add some GroupStyle items to the DropDownButton:
<mah:DropDownButton>
<mah:DropDownButton.MenuStyle>
<Style TargetType="ContextMenu" BasedOn="{StaticResource {x:Type ContextMenu}}">
<Setter Property="local:DropDownButtonHelper.Owner" Value="{Binding RelativeSource={RelativeSource TemplatedParent}}" />
</Style>
</mah:DropDownButton.MenuStyle>
<mah:DropDownButton.GroupStyle>
<GroupStyle />
</mah:DropDownButton.GroupStyle>
</mah:DropDownButton>
This I think should be enough to achieve your goal.
If you check out the other post you've linked to, the answer has it all - in particular you need to bind to a CollectionView, rather than directly to the collection. Then you can set up grouping on the CollectionView.
So, in your case, define the property:
public ICollectionView LeaguesView { get; private set; }
and then after you've created your Leagues Collection, attach the View to your collection, and while you're at it set up the grouping on the view:
LeaguesView = (ListCollectionView)CollectionViewSource.GetDefaultView(Leagues);
LeaguesView.GroupDesriptions.Add(new PropertyGroupDescription("NationName"));
Then, bind your DropDownButton ItemSource to LeaguesView, and change your HeaderTemplate to bind to "Name" - which is the the name of the group:
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</GroupStyle.HeaderTemplate>
You can also use the ItemCount property in there if you want to show how many items there are in the group.

ComboBox selected item binding not showing the initial value - then working OK

I have a weird problem with something simple I suppose.
I have a combobox with two bindings set up - one for ItemsSource and another for SelectedItem.
The selected item is not working on initial startup, but then it works OK. Output does not indicate any binding problems, I have also set up a TextBlock with the same binding to see if it works - and it does.
Here's the code
<ComboBox IsSynchronizedWithCurrentItem="True" IsEditable="False"
Name="ProgramsCollectionComboBox"
SelectedItem="{Binding ElementName=ThisUc,
Path=SelectedProgram}"
ItemsSource="{Binding ElementName=ThisUc,
Path=ProgramsCollection}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Text="{Binding ElementName=ThisUc,
Path=SelectedProgram.Name, Mode=TwoWay}" />
The property:
private Program _selectedProgram;
public Program SelectedProgram
{
get
{
if (_selectedProgram == null)
{
_selectedProgram = new Program(Settings.Default.SelectedProgramPath);
}
return _selectedProgram;
}
set
{
_selectedProgram = value;
Settings.Default.SelectedProgramPath = SelectedProgram.PathProgramFolder;
RaisePropertyChanged("SelectedProgram");
}
}
It saves and reads the settings OK, the initial values is shown in the textblock below the combobox, when I change the selected item, the textblock is updated, the settings are changed and everything works fine - except for the fact that on app startup, selected item is not selected.
Thanks for help!
There are two reasons your initial binding is not working. First, as Jehof has mentioned himself, is the fact that you're setting your SelectedProgram to an item that is not part of the ProgramsCollection.
Furthermore, when you are setting the initial value of your SelectedProgram you are doing so in the getter, where PropertyChanged is not invoked and thus the binding will never be aware of that change. You can either invoke PropertyChanged when initializing it in the getter:
...
get
{
if (_selectedProgram == null)
{
_selectedProgram = _programsCollection?.FirstOrDefault();
RaisePropertyChanged("SelectedProgram");
}
return _selectedProgram;
}
...
Or even better, set the default value on the private field:
private Program _selectedProgram = _programsCollection?.FirstOrDefault();
...
The getter of your property SelectedProgram should return a value of your ProgrammsCollection and not a new instance if it is null.
If the value is not part of the collection that is bound to the combobox it is not displayed.

How to bind an object to a ComboBox, but update a different object when it changes?

I have a Zone object that contains
public int Block {get;set;}
I also have a configuration object which contains minimum and maximum Block values, which are 0 and 2 respectively.
I need to display a ComboBox with the range of valid values, but I need to have the selected value bound to Block.
What's the best way for me to do this?
I've been trying the following:
var blocks = new Dictionary<string, int>();
for (int i = _currentZone.Constraints.Block.Min; i <= _currentZone.Constraints.Block.Max; i++)
{
blocks.Add("Block " + i, i);
}
var blocksCombo = new ComboBoxControl(blocks, GetCurrentBlockValue());
with ComboBoxControl defined as
public ComboBoxControl(Dictionary<string, int> comboItems, int? selectedValue)
{
InitializeComponent();
cboItems.ItemsSource = comboItems;
cboItems.SelectedValue = selectedValue;
}
and the XAML defined as
<Grid>
<ComboBox x:Name="cboItems"
SelectionChanged="combo_SelectionChanged"
Height="25"
SelectedValuePath="Value">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Key}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
When the combo_SelectionChanged event is triggered I manually update the Block value, which isn't ideal.
What I'd like is to be able to set the combo box with the items in the dictionary, but when I change the selected item the value is bound to a different object - the Block. Is this possible?
If so, how can I implement this? If not, is there a better way for me to go about this than what I'm currently doing?
I believe it's as simple as changing you xaml to have...
<ComboBox x:Name="cboItems"
SelectionChanged="combo_SelectionChanged"
Height="25"
SelectedValuePath="Value"
SelectedItem="{Binding Block}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Key}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Assuming the data context is setup correctly, you probably need to set the datacontext of the combobox to your Zone object at some point, maybe pass it along with the constructor...
var blocksCombo = new ComboBoxControl(blocks, GetCurrentBlockValue(), this);
public ComboBoxControl(Dictionary<string, int> comboItems, int? selectedValue, Zone zone)
{
InitializeComponent();
cboItems.ItemsSource = comboItems;
cboItems.SelectedValue = selectedValue;
cboItems.DataContext = zone;
}
edit:
Also I think Henk is right, you might want to change the dictionary to instead be a ObservableCollection of Block. (actually just realized block is just an int, this will probably work as a dictionary)
I hope I understood everything right. You have the combobox and want to bind to one specific zone?
<ComboBox ItemsSource="{Binding ValidValuesList}" ItemStringFormat="Block {0}" SelectedItem="{Binding MyZone.Block}"/>
This binds to
public List<int> ValidValuesList
{
get { return new List<int> { 0, 1, 2 }; }
}
and to
public Zone MyZone { get; set; }
in your usercontrols DataContext.

Categories