Andrew Hanlon
Andrew Hanlon

Reputation: 7421

WPF DataGrid (MultiSelector?) is raising SelectedItems CollectionChanged event multiple times

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

Answers (3)

Matt Vukomanovic
Matt Vukomanovic

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

Ajay Kumar
Ajay Kumar

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

Andrew Hanlon
Andrew Hanlon

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

Related Questions