WPF TreeView: bindings not updating when TreeView is virtualizing - c#

In my WPF app I have a TreeView with a lot of elements, so I turned on virtualization to speed up the rendering, like this:
<TreeView VirtualizingPanel.IsVirtualizing="True">
...
</TreeView>
However, if I do this it seems controls inside the tree items that are data-bound to properties in my Viewm Model stop reacting to the OnPropertyChanged event.
For example, let's assume I have the following template for my items:
<DataTemplate DataType="{x:Type viewModels:MyViewModel}">
<TextBlock Text="{Binding ItemName}" />
</DataTemplate>
and the model is something like this:
public class MyViewModel: INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
private string itemName;
public string ItemName {
get { return itemName; }
set {
if(value != itemName) {
itemName = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ItemName)));
}
}
}
}
The initial binding to the property works correctly and the item name is displayed in the TextBlock, but if the value of the ItemName property changes after the tree view item has already been rendered it will not update in the UI, as if the PropertyChanged event is being completely ignored.
How can I fix this? Note that if I set VirtualizingPanel.IsVirtualizing="False" the problem disappears, so it is definitely caused by the virtualization.

I've found the problem after further testing and debugging, it is not strictly related to the panel virtualization, I guess that was just a condition in which the bug manifests itself.
Basically, in my full code (that I omitted for brevity) the ItemName property is being set from inside an Event Handler. The problem was that my handler was triggered by events that were raised in a background thread somewhere else in the code and thus it did not run on the UI thread. I was under the impression that this wasn't a problem, since I was not manipulating any UI objects in the model (I was just touching my ViewModel object), however apparently if you're not in the UI thread raising the PropertyChanged becomes unreliable.
To solve the problem, I've wrapped the code in my handler in a dispatcher call, like this:
private void MyEvenHandler(object sender, MyEventArgs e)
{
Dispatcher.Invoke(() =>
{
var myViewModel = FindVm(e);
myViewModel.ItemName = "updated";
}
}
Now everything works as expected.

Related

CheckBox Checked event fires before bound collection updates

I have a custom control to show items with checkboxes inside a ComboBox. To realize this, I used a DataTemplate with a CheckBox. The ItemSource of the ComboBox uses a binding to a ObserableCollection<FilterValue> which contains my filter values. FilterValue is a custom class implementing INotifyPropertyChanged. The properties Content and IsChecked of the CheckBox use bindings as well to use the values of my list. This control will be used in Silverlight.
Binding itself works fine, as seen here:
The problem appears when I register the Checked or Unchecked event.
As soon as one of the check boxes changed its state, the event is fired as expected but at this moment, the value in the bound list is still not updated.
What I saw while debugging is that the Checked/Unchecked events are firing before the PropertyChanged event of the FilterValue.
This means that at the time the event is firing, I can't ask the list for all active (checked) filters. What could I do to achieve this?
FilterControl.xaml:
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"
xmlns:local="clr-namespace:Controls" x:Class="Controls.FilterControl"
mc:Ignorable="d"
d:DesignHeight="45" d:DesignWidth="140">
<StackPanel x:Name="LayoutRoot">
<sdk:Label x:Name="LblFilterDescription" Content="-" />
<ComboBox x:Name="Filter" Width="120" ItemsSource="{Binding AvailableFilters, RelativeSource={RelativeSource FindAncestor, AncestorType=local:FilterControl}}">
<ComboBox.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Path=Text}" IsChecked="{Binding Path=IsChecked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Checked="FilterChanged" Unchecked="FilterChanged" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
</UserControl>
FilterControl.xaml.cs:
public partial class FilterControl : UserControl
{
public delegate void FilterChangedHandler(object sender);
public event FilterChangedHandler OnFilterChanged;
public ObservableCollection<FilterValue> AvailableFilters { get; set; }
public List<string> AppliedFilters
{
get
{
return new List<string>(AvailableFilters.Where(filter => filter.IsChecked).Select(filter => filter.Text));
}
}
public FilterControl()
{
InitializeComponent();
AvailableFilters = new ObservableCollection<FilterValue>();
}
public bool AddFilterValue(string filterValue)
{
bool found = false;
foreach (FilterValue f in AvailableFilters)
{
if (f.Text == filterValue)
{
found = true;
break;
}
}
if (!found)
AvailableFilters.Add(new FilterValue() { IsChecked = false, Text = filterValue });
return found;
}
private void FilterChanged(object sender, RoutedEventArgs e)
{
//Here if I check AvailableFilters, the value is not changed yet.
//PropertyChanged allways fires after this, what makes me unable to
//get all currently applied filters (checked items)...
}
}
FilterValue:
public class FilterValue : INotifyPropertyChanged
{
private bool _IsChecked;
private string _Text;
public bool IsChecked
{
get { return _IsChecked; }
set
{
_IsChecked = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("IsChecked"));
}
}
public string Text
{
get { return _Text; }
set
{
_Text = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Text"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
So, as I tried to reproduce this behavior, I realized that this appears to be a behavior that only occurs like that in Silverlight. If you try this example on WPF, the Changed fires after the bound property is updated. So you can just access your AppliedFilters property in the FilterChanged method and it will reflect the actual current situation. On Silverlight though, not so much. Even worse, this behavior didn’t even appear to be consistent to me. I did encounter situations in which the event fired after the property has been updated (resulting in the expected output).
A way to get around this is to clean up your component logic. If you look at it, you are mixing two different concepts: Event-driven UI logic, and clear data binding. Of course, doing it “properly” has multiple effects you likely cannot just ensure in an existing project, but you can at least try to get in the right direction here which should then also solve this issue.
So your logic right now uses data binding to provide the data for the view, and to reflect changes of the displayed items. But you are using events on the item level to perform additional logic depending on the former changes. As we have seen, the order of execution appears not be guaranteed across platforms, so it’s best to avoid having to rely on it.
In this case, you should have your data be the source of truth and make changes in the data tell you when applied filters change. You’re already halfway there by having an ObservableCollection and items that implement INotifyPropertyChanged. Unfortunately, an observable collection will only notify you about changes to the collection but not to changes to the contained items. But there are multiple solutions to expand the collection to also look at the items inside the collection.
This related question covers exactly that topic and there are multiple ideas on how to expand the observable collection for exactly that behavior. In my case, I have used the FullyObservableCollection implementation by Bob Sammers.
All you have to do for that is to change your ObservableCollection<FilterValue> into a FullyObservableCollection<FilterValue> and subscribe to the ItemPropertyChanged event:
AvailableFilters = new FullyObservableCollection<FilterValue>();
AvailableFilters.ItemPropertyChanged += AvailableFilters_ItemPropertyChanged;
In that event handler, you will then correctly see the proper behavior.

DataGrid Items not being updated on ObservableCollection change

I've got a DataGrid , where values shown in the columns aren't always being updated correctly.
Here's the definition:
<uic:DataGridControlEx Grid.Row="1"
ReadOnly="True"
Name="m_dgErgaenzungsfelder"
NavigationBehavior ="RowOnly"
SelectionMode="Extended"
AutoCreateColumns="False"
ItemsSource="{Binding Path=ErgaenzungsfelderEntities}"
SelectionChanged="OnDGSelectionChanged" >
<uic:DataGridControlEx.View>
<xc:TableView ColumnStretchMode="Last"
AllowColumnChooser="False"
VerticalGridLineThickness="0"
UseDefaultHeadersFooters="False"
ShowRowSelectorPane="False">
<xc:TableView.FixedHeaders>
<DataTemplate>
<xc:ColumnManagerRow/>
</DataTemplate>
</xc:TableView.FixedHeaders>
<xc:TableView.Theme>
<xc:Office2007SilverTheme />
</xc:TableView.Theme>
</xc:TableView>
</uic:DataGridControlEx.View>
<uic:DataGridControlEx.Columns>
<xc:Column Title="{LocText FGG1:ErgaenzungsfelderResources:ErgaenzungsfelderViewColumnName}"
FieldName="Name" />
<xc:Column Title="{LocText FGG1:ErgaenzungsfelderResources:ErgaenzungsfelderViewColumnType}"
FieldName="ErgaenzungsfeldType" >
<xc:Column.CellContentTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=., Converter={x:Static converters:ErgaenzungsfeldTypeTotextConverter.Instance}}" />
</DataTemplate>
</xc:Column.CellContentTemplate>
</xc:Column>
<xc:Column Title="{LocText FGG1:ErgaenzungsfelderResources:ErgaenzungsfelderViewColumnAuthor}"
FieldName="Author" />
<xc:Column Title="{LocText FGG1:ErgaenzungsfelderResources:ErgaenzungsfelderViewColumnCreationDate}"
FieldName="CreationDate" />
</uic:DataGridControlEx.Columns>
</uic:DataGridControlEx>
DataGridControlEx exentds the Xceed DataGridControl but doesn't influence binding.
And the code behind with the definition of the ObservableCollection the grid binds to, the constructor initializing the collection early and the method updating the items:
public ObservableCollection<ErgaenzungsfeldEntity> ErgaenzungsfelderEntities { get; private set; }
public ErgaenzungsfelderView() {
ErgaenzungsfelderEntities = new ObservableCollection<ErgaenzungsfeldEntity>();
InitializeComponent();
}
public void ShowErgaenzungsfelder(List<ErgaenzungsfeldEntity> entities) {
ErgaenzungsfelderEntities.Clear();
entities.ForEach(e => ErgaenzungsfelderEntities.Add(e));
//m_dgErgaenzungsfelder.GetBindingExpression(ItemsControl.ItemsSourceProperty).UpdateSource();
}
ErgaenzungsfeldEntity implements INotifyPropertyChanged and does notify property changes for every change e.g.:
public string Name {
get { return m_name; }
set {
m_name = value;
NotifyPropertyChanged("Name");
}
}
When updating a bound item through the GUI, the changes are being reflected correctly all the time. Through the GUI, items aren't being reloaded using the above mentioned ShowErgaenzungsfelder, but the bound item is being passed as a reference.
Issue:
Our service layer can notify events requiring to reload the elements. This will call ShowErgaenzungsfelder. When doing this, added entities will show up in the grid, removed entities will be removed. BUT, modified entities won't reflect the changes for the fields Name and ErgaenzungsfeldType (which are the only properties which can change).
E.g. changing the column sorting will trigger an update of the grid and display the correct values.
For the `ItemsSource, I've tried changing all these properties without success:
ItemsSource="{Binding Path=ErgaenzungsfelderEntities, UpdateSourceTrigger=Explicit, NotifyOnTargetUpdated=True, NotifyOnSourceUpdated=True, Mode=OneWay}"
With
UpdateSourceTrigger=Explicit and un-commented m_dgErgaenzungsfelder.GetBindingExpression(ItemsControl.ItemsSourceProperty).UpdateSource(); in ShowErgaenzungsfelder
UpdateSourceTrigger=PropertyChanged
UpdateSourceTrigger=Default
I'd be grateful for any input.
Some psychic debugging here.
The Events raised by the Service Layer are not running on the GUI Thread, and hence the ShowErgaenzungsfelder function is also not on the GUI Thread, nor are the events raised by changing the collection. WPF will receive these events on the non-GUI Thread and then attempt to update the GUI, but fail as it's doing so not on the GUI Thread and throw an error. WPF's behaviour when it generates an exception is to abort the operation and hide the exception, hence you see nothing. (In Visual Studio, you may see these exceptions in the Output panel; there's an option to show them there.)
To test this, you need to despatch the updates to the GUI Thread. You can do this as follows:
public ObservableCollection<ErgaenzungsfeldEntity> ErgaenzungsfelderEntities { get; private set; }
public ErgaenzungsfelderView() {
ErgaenzungsfelderEntities = new ObservableCollection<ErgaenzungsfeldEntity>();
InitializeComponent();
// This will be called on the GUI thread
this.guiContext = SynchronizationContext.Current;
}
private readonly SynchronizationContext guiContext;
public void ShowErgaenzungsfelder(List<ErgaenzungsfeldEntity> entities) {
this.guiContext.Send(this.ShowErgaenzungsfelderOnGuiThread, entities);
}
private void ShowErgaenzungsfelderOnGuiThread(object state) {
List<ErgaenzungsfeldEntity> entities = state as List<ErgaenzungsfeldEntity>;
ErgaenzungsfelderEntities.Clear();
entities.ForEach(e => ErgaenzungsfelderEntities.Add(e));
}

WPF MVVM firing code based on Tab SelectedValue, rather than SelectedIndex

In WPF with MVVM it's easy to fire some code when the user changes the tab.
<TabControl Margin="0 5 5 5" Background="#66F9F9F9" SelectedIndex="{Binding TabIndex}">
And then in the ViewModel:
private int _tabIndex;
public int TabIndex
{
get { return _tabIndex; }
set
{
if(_tabIndex != value)
{
_tabIndex = value;
OnPropertyChanged("TabIndex");
if(value == 1)
{
//do something
}
}
}
}
But I'm vaguely uncomfortable with this. What if another developer happens along later and adds another tab in the "1" position. If this is application-critical code (which it is), things will break spectacularly.
Danger can be minimized with unit tests, of course. But it made me wonder: is this seen as bad practice? And is there a way of doing this that allows you to refer to the Tab with a string, rather than an int? I tried noodling with binding to the SelectedValue property, but nothing seemed to happen when the tabs were changed.
You could make a behavior for TabItem, listening for changes to the IsSelected dependency property, and raises a Command when the tab is selected. This can be extended to any number of tabs, each which invokes different commands in the viewmodel. You could also supply a command parameter for any optional context:
class TabSelectedBehavior : Behavior<TabItem>
{
public static readonly DependencyProperty SelectedCommandProperty = DependencyProperty.Register("SelectedCommand", typeof(ICommand), typeof(TabSelectedBehavior));
public ICommand SelectedCommand
{
get { return (ICommand)GetValue(SelectedCommandProperty); }
set { SetValue(SelectedCommandProperty, value); }
}
private EventHandler _selectedHandler;
protected override void OnAttached()
{
DependencyPropertyDescriptor dpd = DependencyPropertyDescriptor.FromProperty(TabItem.IsSelectedProperty, typeof(TabItem));
if (dpd != null)
{
_selectedHandler = new EventHandler(AssociatedObject_SelectedChanged);
dpd.AddValueChanged(AssociatedObject, _selectedHandler);
}
base.OnAttached();
}
protected override void OnDetaching()
{
DependencyPropertyDescriptor dpd = DependencyPropertyDescriptor.FromProperty(TabItem.IsSelectedProperty, typeof(TabItem));
if (dpd != null && _selectedHandler != null)
{
dpd.RemoveValueChanged(AssociatedObject, _selectedHandler);
}
base.OnDetaching();
}
void AssociatedObject_SelectedChanged(object sender, EventArgs e)
{
if (AssociatedObject.IsSelected)
{
if (SelectedCommand != null)
{
SelectedCommand.Execute(null);
}
}
}
}
XAML
<TabControl>
<TabItem Header="TabItem1">
<i:Interaction.Behaviors>
<local:TabSelectedBehavior SelectedCommand="{Binding TabSelectedCommand}"/>
</i:Interaction.Behaviors>
</TabItem>
<TabItem Header="TabItem2">
</TabItem>
</TabControl>
In a similar fashion you could also make a behavior for the TabControl, which turns the SelectionChanged event into a command, and pass the Tag object of the selected TabItem as command parameter.
As with all collection controls, the best way to maintain the selected item is to use the SelectedItem property. If you data bind a property of the relevant data type to the TabControl.SelectedItem property, then you'll still be able to tell which tab is selected and select a different one from the view model.
The only problem with this method is that you'll also need to use the TabControl.ItemsSource property to set up the TabItems:
<TabControl ItemsSource="{Binding YourDataItems}" SelectedItem="{Binding YourItem}" />
If you want to try this, then you should know that defining the TabItems can be a little bit confusing. Please refer to the answer from the How to bind items of a TabControl to an observable collection in wpf? question for help with that.

How to reliably detect when an item is scrolled outside of view?

I have a large collection of items bound to a ListBox, with a VirtualizingStackPanel set as its ItemsPanel. As the user scrolls and item containers are created, I do some work to populate the item with data (using a database query). If the user scrolls very rapidly, it builds up a large number of requests that tend to bog things down. What I would like to do is detect when the item is scrolled outside of the viewport, so I can cancel its corresponding request.
Here are the approaches I've tried thus far, and why they have not worked:
Override VirtualizingStackPanel.OnCleanUpVirtualizedItem. The
problem is that this method seems to be called sometime much later
than when the item actually goes off-screen. Cancelling my request
within this method doesn't do much good because it occurs so late.
Turn on container recycling with
VirtualizationMode.Recycling. This event causes the item
container's DataContext to change but the item container itself is
reused. The DataContextChanged event occurs immediately as one
item goes outside of view, so it is good in that regard. The problem
is that container recycling creates a lot of side-effects and, in my
testing, is a little buggy overall. I would prefer not to use it.
Is there are a good lower-level approach, such as hooking into layout events, that can give me a deterministic answer on when an item goes outside of view? Perhaps at the ScrollViewer level?
Here's a rough solution that I think accomplishes what you're looking for. I'm getting the virtualizing stack panel by listening to the loaded event in the XAML. If I were doing this in production code, I might factor this into a reusable attached behavior rather than throwing a bunch of code in the code-behind.
public partial class MainWindow
{
private VirtualizingStackPanel _panel;
public MainWindow()
{
InitializeComponent();
DataContext = new MyViewModel();
}
private IList<ChildViewModel> _snapshot = new List<ChildViewModel>();
private void OnPanelLoaded(object sender, RoutedEventArgs eventArgs)
{
_panel = (VirtualizingStackPanel)sender;
UpdateSnapshot();
_panel.ScrollOwner.ScrollChanged += (s,e) => UpdateSnapshot();
}
private void UpdateSnapshot()
{
var layoutBounds = LayoutInformation.GetLayoutSlot(_panel);
var onScreenChildren =
(from visualChild in _panel.GetChildren()
let childBounds = LayoutInformation.GetLayoutSlot(visualChild)
where layoutBounds.Contains(childBounds) || layoutBounds.IntersectsWith(childBounds)
select visualChild.DataContext).Cast<ChildViewModel>().ToList();
foreach (var removed in _snapshot.Except(onScreenChildren))
{
// TODO: Cancel pending calculations.
Console.WriteLine("{0} was removed.", removed.Value);
}
_snapshot = onScreenChildren;
}
}
Notice that there isn't really a property we can use here to find the on-screen children, so we look at the layout bounds of the parent compared to the children to determine which children are on screen.
The code uses an extension method for getting the visual children of an item in the visual tree, included below:
public static class MyVisualTreeHelpers
{
public static IEnumerable<FrameworkElement> GetChildren(this DependencyObject dependencyObject)
{
var numberOfChildren = VisualTreeHelper.GetChildrenCount(dependencyObject);
return (from index in Enumerable.Range(0, numberOfChildren)
select VisualTreeHelper.GetChild(dependencyObject, index)).Cast<FrameworkElement>();
}
}
This code is using a very basic view model hierarchy I created for the purposes of testing this out. I'll include it just in case it's helpful in understanding the other code:
public class MyViewModel
{
public MyViewModel()
{
Children = new ObservableCollection<ChildViewModel>(GenerateChildren());
}
public ObservableCollection<ChildViewModel> Children { get; set; }
private static IEnumerable<ChildViewModel> GenerateChildren()
{
return from value in Enumerable.Range(1, 1000)
select new ChildViewModel {Value = value};
}
}
public class ChildViewModel
{
public int Value { get; set; }
}
XAML:
<Window x:Class="WpfTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wpfTest="clr-namespace:WpfTest"
Title="MainWindow" Height="500" Width="500">
<ListBox ItemsSource="{Binding Children}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Loaded="OnPanelLoaded" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate DataType="wpfTest:ChildViewModel">
<TextBlock Text="{Binding Value}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Window>
On the viewmodel side you could watch attach and detach from INotifyPropertyChanged event:
public event PropertyChangedEventHandler PropertyChanged
{
add
{
if(this.InternalPropertyChanged == null)
Console.WriteLine("COMING INTO VIEW");
this.InternalPropertyChanged += value;
}
remove
{
this.InternalPropertyChanged -= value;
if(this.InternalPropertyChanged == null)
Console.WriteLine("OUT OF VIEW");
}
}
private event PropertyChangedEventHandler InternalPropertyChanged;
Note: Without VirtualizationMode.Recycling a ListBox may defer the container destruction (and hence the detach) until the user stops scrolling. This can increase memory consumption extensivly, especially if the ItemTemplate is complex (and also won't cancel your DB queries).
You can possibly try to add.
scrollviewer.veriticalscroll = "auto"
That will cause it to only scroll for the amount of items already in the listbox

set SelectedIndex of Combobox in WPF to 0

I am binding a Collection at run time to a Combobox and I would like to set the Index after to 0. I could not find a straight answer to what I want.
_stationNames = new ObservableCollection<string>(_floorUnits.Unit.Select(f => f.Name));
_stationNames.Insert(0, "All");
stationsComboBox.ItemsSource = _stationNames;
stationsComboBox.SelectedIndex = 0;//Doesn;t work
Xaml
<ComboBox x:Name="stationsComboBox" Grid.Row="1" Grid.Column="1" Text="{Binding Name}"
SelectionChanged="StationComboBoxSelectionChanged" VerticalAlignment="Center" Margin="3"
SelectedIndex="0"/>
It sounds like you're trying to use it like you would with WinForms. WPF is a slightly different beast and a lot more powerful regarding bindings.
I recommend reading a bit on MVVM to get the most benefit from WPF. By binding the XAML to a view model class (rather than trying to wire things up in Code-behind) you will find you can accomplish what you want with a lot more flexibility without oodles of code.
For instance: Given the following VM:
public class MyViewModel: INotifyPropertyChanged
{
public ObservableCollection<string> StationNames
{
get;
private set;
}
public Something()
{
StationNames = new ObservableCollection<string>( new [] {_floorUnits.Unit.Select(f=>f.Name)});
StationNames.Insert(0, "All");
}
private string _selectedStationName = null;
public string SelectedStationName
{
get
{
return _selectedStationName;
}
set
{
_selectedStationName = value;
FirePropertyChanged("SelectedStationName");
}
}
private void FirePropertyChanged(string propertyName)
{
if ( PropertyChanged != null )
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
You can set your view's (XAML form) DataContext to an instance of the ViewModel and update your combo box definition to:
<ComboBox x:Name="stationsComboBox" Grid.Row="1" Grid.Column="1"
ItemsSource="{Binding Path=StationNames}" SelectedItem={Binding Path=SelectedStationName} VerticalAlignment="Center" Margin="3"
SelectedIndex="0"/>
From here whenever the combo box selection changes, the VM's SelectedStationName updates to reflect the current selection, and from anywhere in the VM code, setting the VM's SelectedStationName will update the combo's selection. (I.e. implementing a Reset button, etc.)
Normally though, with something like what you've suggested, I would be looking at binding directly to the Units collection. (or VM's derived from units if they themselves can be viewed/edited.) In any case it should give you a bit of a starting point to start researching into WPF bindings.

Categories