Reputation: 7421
I'm not sure if this is an issue with the DataGrid control or with MultiSelectors in general, but when I select multiple rows within the grid, the CollectionChanged event is fired for every single row. This makes sense if I'm 'dragging' with my mouse, but it also occurs if I 'shift-click' to select multiple rows or simply click the top-left 'select-all-rows' button.
I have seen on the MultiSelector that there are Begin/EndUpdateSelectedItems methods as well as an IsUpdatingSelectedItems property. Unfortunately my consumer of this collection/event is unaware of its source.
Is there a way to make the DataGrid / SelectedItems collection only send the CollectionChanged notification when updating is finished?
thank you kindly.
Edit: I have found that for the DataGrid the IsUpdatingSelectedItems property is not being set even when changing a large selection.
Edit: I have found that the DataGrid SelectionChanged event is correctly fired only once after the full change. It's unfortunate since this ruins the possibility of simple data binding, but it is a potential workaround if you have control over the consumer of the SelectedItems collection.
Upvotes: 5
Views: 1193
Reputation: 1440
I also found this very annoying that when I was selecting multiple lines at once it would send a changed event for every single item selected. So I have a behaviour that I made... And a BulkObservableCollection which will handle it so that it only sends one event rather than all of them. The code followed by a quick example of using it in XAML is:
using Microsoft.Xaml.Behaviors;
using System.Collections;
using System.Collections.Specialized;
using System.Windows;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.ComponentModel;
using System.Runtime.CompilerServices;
public class MultiSelectMultiSelectorBehavior : Behavior<System.Windows.Controls.Primitives.MultiSelector>
{
public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(nameof(SelectedItems), typeof(INotifyCollectionChanged), typeof(MultiSelectMultiSelectorBehavior), new PropertyMetadata(OnSelectedItemsPropertyChanged));
public INotifyCollectionChanged SelectedItems
{
get { return (INotifyCollectionChanged)GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
private static void OnSelectedItemsPropertyChanged(DependencyObject target, DependencyPropertyChangedEventArgs args)
{
SubscribeBinding(target, args.OldValue as INotifyCollectionChanged, false);
SubscribeBinding(target, args.NewValue as INotifyCollectionChanged, true);
}
private static void SubscribeBinding(DependencyObject target, INotifyCollectionChanged collection, bool subscribe)
{
if (collection == null) return;
if (subscribe)
{
collection.CollectionChanged += ((MultiSelectMultiSelectorBehavior)target).ContextSelectedItems_CollectionChanged;
}
else
{
collection.CollectionChanged -= ((MultiSelectMultiSelectorBehavior)target).ContextSelectedItems_CollectionChanged;
}
}
private void SubscribeGridChanged(bool subscribe)
{
if (subscribe)
{
AssociatedObject.AddHandler(System.Windows.Controls.Primitives.Selector.SelectionChangedEvent, new RoutedEventHandler(Grid_SelectionChangedEvent));
}
else
{
AssociatedObject.RemoveHandler(System.Windows.Controls.Primitives.Selector.SelectionChangedEvent, new RoutedEventHandler(Grid_SelectionChangedEvent));
}
}
protected override void OnAttached()
{
base.OnAttached();
SubscribeGridChanged(true);
}
protected override void OnDetaching()
{
base.OnDetaching();
SubscribeGridChanged(false);
}
private void SubscribeToEvents()
{
SubscribeGridChanged(true);
SubscribeBinding(this, SelectedItems, true);
}
private void UnsubscribeFromEvents()
{
SubscribeGridChanged(false);
SubscribeBinding(this, SelectedItems, false);
}
private void ContextSelectedItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
Transfer(SelectedItems as IList, AssociatedObject.SelectedItems);
}
private void Grid_SelectionChangedEvent(object sender, RoutedEventArgs e)
{
Transfer(AssociatedObject.SelectedItems, SelectedItems as IList);
}
public void Transfer(IList source, IList target)
{
if (source == null || target == null)
return;
UnsubscribeFromEvents();
(target as IBulkChange)?.BeginBulkOperation();
target.Clear();
foreach (var o in source)
{
target.Add(o);
}
(target as IBulkChange)?.EndBulkOperation();
SubscribeToEvents();
}
}
public interface IBulkChange
{
void BeginBulkOperation();
void EndBulkOperation();
}
[DebuggerDisplay("Count = {Count}")]
public class BulkObservableCollection<T> : ObservableCollection<T>, IBulkChange
{
private bool _collectionChangedDuringRangeOperation;
private int _rangeOperationCount;
private ReadOnlyObservableCollection<T> _readOnlyAccessor;
public BulkObservableCollection()
{
}
public BulkObservableCollection(List<T> list)
: base(list)
{
}
public BulkObservableCollection(IEnumerable<T> collection)
: base(collection)
{
}
public void SetCollection(IEnumerable<T> collection)
{
try
{
BeginBulkOperation();
Clear();
AddRange(collection);
}
finally
{
EndBulkOperation();
}
}
public void AddRange(IEnumerable<T> items)
{
if (items != null)
{
try
{
BeginBulkOperation();
foreach (T local in items)
{
Add(local);
}
}
finally
{
EndBulkOperation();
}
}
}
public void RemoveRange(IEnumerable<T> items)
{
if (items != null)
{
try
{
this.BeginBulkOperation();
foreach (T local in items)
{
base.Remove(local);
}
}
finally
{
this.EndBulkOperation();
}
}
}
public ReadOnlyObservableCollection<T> AsReadOnly()
{
if (this._readOnlyAccessor == null)
{
this._readOnlyAccessor = new ReadOnlyObservableCollection<T>(this);
}
return this._readOnlyAccessor;
}
public void BeginBulkOperation()
{
this._rangeOperationCount++;
}
public void EndBulkOperation()
{
if (((this._rangeOperationCount > 0) && (--this._rangeOperationCount == 0)) && this._collectionChangedDuringRangeOperation)
{
InternalEndBulkOperation();
}
}
private void InternalEndBulkOperation()
{
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
this._collectionChangedDuringRangeOperation = false;
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (this._rangeOperationCount == 0)
{
base.OnCollectionChanged(e);
}
else
{
this._collectionChangedDuringRangeOperation = true;
}
}
public List<T> ToList()
{
return new List<T>(this);
}
}
Then the XAML
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
>
<DataGrid ItemsSource="{Binding AllItems}">
<b:Interaction.Behaviors>
<local:MultiSelectMultiSelectorBehavior SelectedItems="{Binding Path=SelectedItems}" />
</b:Interaction.Behaviors>
</DataGrid>
</Window>
Upvotes: 1
Reputation: 19
ViewModel:
private MultiSelector _selectedItems;
Public MultiSelector SelectedItems
{
get {return _selectedItems;
set { _selectedItems=value;}
}
Bind the SelectedItems Property to the SelectedItem of the DataGrid and add System.Windows.Controls.Primitives.MultiSelector
Upvotes: 1
Reputation: 7421
For the sake of completeness, I'll 'answer' my own question. It turns out that the WPF controls in general can not handle anything but a single element change in their CollectionChanged event handlers - meaning that the 'call CollectionChanged for every item' workflow is the right way for the framework in its current form. However, personally I feel that this is a terrible performance issue.
Upvotes: 1