WPF DataGrid MultiSelect

13,008

Solution 1

I have implemented a two-way data binding for MultiSelector.SelectedItems property using attached behavior pattern.

The following image shows how it works:

MultiSelector.SelectedItems two-way data binding

There are two DataGrids bound to the same model and they share selected items. Left DataGrid is active so selected items are blue and right DataGrid is inactive so selected items are gray.

Following is the sample code how to use it:

MainWindow.xaml

<Window x:Class="WpfApplication.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <local:MainWindowModel/>
    </Window.DataContext>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <DataGrid ItemsSource="{Binding People}" local:MultiSelectorExtension.SelectedItems="{Binding SelectedPeople}" CanUserAddRows="True"/>
        <DataGrid Grid.Column="1" ItemsSource="{Binding People}" local:MultiSelectorExtension.SelectedItems="{Binding SelectedPeople}" CanUserAddRows="True"/>
        <StackPanel Grid.Row="1" Grid.ColumnSpan="2">
            <Button DockPanel.Dock="Top" Content="Select All" Command="{Binding SelectAllCommand}"/>
            <Button DockPanel.Dock="Top" Content="Unselect All" Command="{Binding UnselectAllCommand}"/>
            <Button DockPanel.Dock="Top" Content="Select Next Range" Command="{Binding SelectNextRangeCommand}"/>
        </StackPanel>
    </Grid>
</Window>

Model.cs

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Windows.Input;

namespace WpfApplication
{
    abstract class ObservableObject : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            var handler = this.PropertyChanged;
            if (handler != null)
                handler(this, e);
        }

        protected void Set<T>(ref T field, T value, string propertyName)
        {
            if (!EqualityComparer<T>.Default.Equals(field, value))
            {
                field = value;
                this.OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
            }
        }
    }

    sealed class DelegateCommand : ICommand
    {
        private readonly Action action;

        public DelegateCommand(Action action)
        {
            if (action == null)
                throw new ArgumentNullException("action");

            this.action = action;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public void Execute()
        {
            this.action();
        }

        bool ICommand.CanExecute(object parameter)
        {
            return true;
        }

        void ICommand.Execute(object parameter)
        {
            this.Execute();
        }
    }

    class Person : ObservableObject
    {
        private string name, surname;

        public Person()
        {
        }

        public Person(string name, string surname)
        {
            this.name = name;
            this.surname = surname;
        }

        public string Name
        {
            get { return this.name; }
            set { this.Set(ref this.name, value, "Name"); }
        }

        public string Surname
        {
            get { return this.surname; }
            set { this.Set(ref this.surname, value, "Surname"); }
        }

        public override string ToString()
        {
            return this.name + ' ' + this.surname;
        }
    }

    class MainWindowModel : ObservableObject
    {
        public ObservableCollection<Person> People { get; private set; }

        public SelectedItemCollection<Person> SelectedPeople { get; private set; }

        public DelegateCommand SelectAllCommand { get; private set; }
        public DelegateCommand UnselectAllCommand { get; private set; }
        public DelegateCommand SelectNextRangeCommand { get; private set; }

        public MainWindowModel()
        {
            this.People = new ObservableCollection<Person>(Enumerable.Range(1, 1000).Select(i => new Person("Name " + i, "Surname " + i)));
            this.SelectedPeople = new SelectedItemCollection<Person>();
            for (int i = 0; i < this.People.Count; i += 2)
                this.SelectedPeople.Add(this.People[i]);

            this.SelectAllCommand = new DelegateCommand(() => this.SelectedPeople.Reset(this.People));

            this.UnselectAllCommand = new DelegateCommand(() => this.SelectedPeople.Clear());

            this.SelectNextRangeCommand = new DelegateCommand(() =>
            {
                var index = this.SelectedPeople.Count > 0 ? this.People.IndexOf(this.SelectedPeople[this.SelectedPeople.Count - 1]) + 1 : 0;

                int count = 10;

                this.SelectedPeople.Reset(Enumerable.Range(index, count).Where(i => i < this.People.Count).Select(i => this.People[i]));
            });

            this.SelectedPeople.CollectionChanged += this.OnSelectedPeopleCollectionChanged;
        }

        private void OnSelectedPeopleCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            Debug.WriteLine("Action = {0}, NewItems.Count = {1}, NewStartingIndex = {2}, OldItems.Count = {3}, OldStartingIndex = {4}, Total.Count = {5}", e.Action, e.NewItems != null ? e.NewItems.Count : 0, e.NewStartingIndex, e.OldItems != null ? e.OldItems.Count : 0, e.OldStartingIndex, this.SelectedPeople.Count);
        }
    }

    class SelectedItemCollection<T> : ObservableCollection<T>
    {
        public void Reset(IEnumerable<T> items)
        {
            int oldCount = this.Count;

            this.Items.Clear();
            foreach (var item in items)
                this.Items.Add(item);

            if (!(oldCount == 0 && this.Count == 0))
            {
                this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));

                if (this.Count != oldCount)
                    this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));

                this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
            }
        }
    }
}

Following is the implementation, which unfortunately is not documented. Implementation uses various tricks (via reflection) to reduce the number of changes to underlying collections as much as possible (to suspend excessive collection changed notifications). It is worth noting that if selected items collection in model has Select(IEnumerable) or Select(IEnumerable) method, that method will be used for performing bulk updates (updates which affect more than one item) which offers better performanse if, for example, all items in the DataGrid are selected or un-selected.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Markup;

namespace WpfApplication
{
    static class MultiSelectorExtension
    {
        public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.RegisterAttached("SelectedItems", typeof(IList), typeof(MultiSelectorExtension), new PropertyMetadata(new PropertyChangedCallback(OnSelectedItemsChanged)));

        private static readonly DependencyProperty SelectedItemsBinderProperty = DependencyProperty.RegisterAttached("SelectedItemsBinder", typeof(SelectedItemsBinder), typeof(MultiSelectorExtension));

        [AttachedPropertyBrowsableForType(typeof(MultiSelector))]
        [DependsOn("ItemsSource")]
        public static IList GetSelectedItems(this MultiSelector multiSelector)
        {
            if (multiSelector == null)
                throw new ArgumentNullException("multiSelector");

            return (IList)multiSelector.GetValue(SelectedItemsProperty);
        }

        public static void SetSelectedItems(this MultiSelector multiSelector, IList selectedItems)
        {
            if (multiSelector == null)
                throw new ArgumentNullException("multiSelector");

            multiSelector.SetValue(SelectedItemsProperty, selectedItems);
        }

        private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var multiSelector = d as MultiSelector;

            if (multiSelector == null)
                return;

            var binder = (SelectedItemsBinder)multiSelector.GetValue(SelectedItemsBinderProperty);

            var selectedItems = e.NewValue as IList;

            if (selectedItems != null)
            {
                if (binder == null)
                    binder = new SelectedItemsBinder(multiSelector);

                binder.SelectedItems = selectedItems;
            }
            else if (binder != null)
                binder.Dispose();
        }

        private sealed class SelectedItemsBinder : IDisposable
        {
            private static readonly IList emptyList = new object[0];

            private static readonly Action<MultiSelector> multiSelectorBeginUpdateSelectedItems, multiSelectorEndUpdateSelectedItems;

            private readonly MultiSelector multiSelector;
            private IList selectedItems;
            private IResetter selectedItemsResetter;

            private bool suspendMultiSelectorUpdate, suspendSelectedItemsUpdate;

            static SelectedItemsBinder()
            {
                GetMultiSelectorBeginEndUpdateSelectedItems(out multiSelectorBeginUpdateSelectedItems, out multiSelectorEndUpdateSelectedItems);
            }

            public SelectedItemsBinder(MultiSelector multiSelector)
            {
                this.multiSelector = multiSelector;
                this.multiSelector.SelectionChanged += this.OnMultiSelectorSelectionChanged;
                this.multiSelector.Unloaded += this.OnMultiSelectorUnloaded;
                this.multiSelector.SetValue(SelectedItemsBinderProperty, this);
            }

            public IList SelectedItems
            {
                get { return this.selectedItems; }
                set
                {
                    this.SetSelectedItemsChangedHandler(false);
                    this.selectedItems = value;
                    this.selectedItemsResetter = GetResetter(this.selectedItems.GetType());
                    this.SetSelectedItemsChangedHandler(true);

                    if (this.multiSelector.IsLoaded)
                        this.OnSelectedItemsCollectionChanged(this.selectedItems, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
                    else
                    {
                        RoutedEventHandler multiSelectorLoadedHandler = null;
                        this.multiSelector.Loaded += multiSelectorLoadedHandler = new RoutedEventHandler((sender, e) =>
                        {
                            this.OnSelectedItemsCollectionChanged(this.selectedItems, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
                            this.multiSelector.Loaded -= multiSelectorLoadedHandler;
                        });
                    }
                }
            }

            private int ItemsSourceCount
            {
                get
                {
                    var collection = this.multiSelector.ItemsSource as ICollection;
                    return collection != null ? collection.Count : -1;
                }
            }

            public void Dispose()
            {
                this.multiSelector.ClearValue(SelectedItemsBinderProperty);
                this.multiSelector.Unloaded -= this.OnMultiSelectorUnloaded;
                this.multiSelector.SelectionChanged -= this.OnMultiSelectorSelectionChanged;
                this.SetSelectedItemsChangedHandler(false);
            }

            private void OnSelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
            {
                if (this.suspendMultiSelectorUpdate || e.Action == NotifyCollectionChangedAction.Move)
                    return;

                this.suspendSelectedItemsUpdate = true;

                if (this.selectedItems.Count == 0)
                    this.multiSelector.UnselectAll();
                else if (this.selectedItems.Count == this.ItemsSourceCount)
                    this.multiSelector.SelectAll();
                else if (e.Action != NotifyCollectionChangedAction.Reset && (e.NewItems == null || e.NewItems.Count <= 1) && (e.OldItems == null || e.OldItems.Count <= 1))
                    UpdateList(this.multiSelector.SelectedItems, e.NewItems ?? emptyList, e.OldItems ?? emptyList);
                else
                {
                    if (multiSelectorBeginUpdateSelectedItems != null)
                    {
                        multiSelectorBeginUpdateSelectedItems(this.multiSelector);
                        this.multiSelector.SelectedItems.Clear();
                        UpdateList(this.multiSelector.SelectedItems, this.selectedItems, emptyList);
                        multiSelectorEndUpdateSelectedItems(this.multiSelector);
                    }
                    else
                    {
                        this.multiSelector.UnselectAll();
                        UpdateList(this.multiSelector.SelectedItems, this.selectedItems, emptyList);
                    }
                }

                this.suspendSelectedItemsUpdate = false;
            }

            private void OnMultiSelectorSelectionChanged(object sender, SelectionChangedEventArgs e)
            {
                if (this.suspendSelectedItemsUpdate)
                    return;

                this.suspendMultiSelectorUpdate = true;

                if (e.AddedItems.Count <= 1 && e.RemovedItems.Count <= 1)
                    UpdateList(this.selectedItems, e.AddedItems, e.RemovedItems);
                else
                {
                    if (this.selectedItemsResetter != null)
                        this.selectedItemsResetter.Reset(this.selectedItems, this.multiSelector.SelectedItems.Cast<object>().Where(item => item != CollectionView.NewItemPlaceholder));
                    else
                        UpdateList(this.selectedItems, e.AddedItems, e.RemovedItems);
                }

                this.suspendMultiSelectorUpdate = false;
            }

            private void OnMultiSelectorUnloaded(object sender, RoutedEventArgs e)
            {
                this.Dispose();
            }

            private void SetSelectedItemsChangedHandler(bool add)
            {
                var notifyCollectionChanged = this.selectedItems as INotifyCollectionChanged;
                if (notifyCollectionChanged != null)
                {
                    if (add)
                        notifyCollectionChanged.CollectionChanged += this.OnSelectedItemsCollectionChanged;
                    else
                        notifyCollectionChanged.CollectionChanged -= this.OnSelectedItemsCollectionChanged;
                }
            }

            private static void UpdateList(IList list, IList newItems, IList oldItems)
            {
                int addedCount = 0;

                for (int i = 0; i < oldItems.Count; ++i)
                {
                    var index = list.IndexOf(oldItems[i]);
                    if (index >= 0)
                    {
                        object newItem;
                        if (i < newItems.Count && (newItem = newItems[i]) != CollectionView.NewItemPlaceholder)
                        {
                            list[index] = newItem;
                            ++addedCount;
                        }
                        else
                            list.RemoveAt(index);
                    }
                }

                for (int i = addedCount; i < newItems.Count; ++i)
                {
                    var newItem = newItems[i];
                    if (newItem != CollectionView.NewItemPlaceholder)
                        list.Add(newItem);
                }
            }

            private static void GetMultiSelectorBeginEndUpdateSelectedItems(out Action<MultiSelector> beginUpdateSelectedItems, out Action<MultiSelector> endUpdateSelectedItems)
            {
                try
                {
                    beginUpdateSelectedItems = (Action<MultiSelector>)Delegate.CreateDelegate(typeof(Action<MultiSelector>), typeof(MultiSelector).GetMethod("BeginUpdateSelectedItems", BindingFlags.NonPublic | BindingFlags.Instance));
                    endUpdateSelectedItems = (Action<MultiSelector>)Delegate.CreateDelegate(typeof(Action<MultiSelector>), typeof(MultiSelector).GetMethod("EndUpdateSelectedItems", BindingFlags.NonPublic | BindingFlags.Instance));
                }
                catch
                {
                    beginUpdateSelectedItems = endUpdateSelectedItems = null;
                }
            }

            private static IResetter GetResetter(Type listType)
            {
                try
                {
                    MethodInfo genericReset = null, nonGenericReset = null;
                    Type genericResetItemType = null;
                    foreach (var method in listType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
                    {
                        if (method.Name != "Reset")
                            continue;

                        if (method.ReturnType != typeof(void))
                            continue;

                        var parameters = method.GetParameters();

                        if (parameters.Length != 1)
                            continue;

                        var parameterType = parameters[0].ParameterType;

                        if (parameterType.IsGenericType && parameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
                        {
                            genericResetItemType = parameterType.GetGenericArguments()[0];
                            genericReset = method;
                            break;
                        }
                        else if (parameterType == typeof(IEnumerable))
                            nonGenericReset = method;
                    }

                    if (genericReset != null)
                        return (IResetter)Activator.CreateInstance(typeof(GenericResetter<,>).MakeGenericType(genericReset.DeclaringType, genericResetItemType), genericReset);
                    else if (nonGenericReset != null)
                        return (IResetter)Activator.CreateInstance(typeof(NonGenericResetter<>).MakeGenericType(nonGenericReset.DeclaringType), nonGenericReset);
                    else
                        return null;
                }
                catch
                {
                    return null;
                }
            }

            private interface IResetter
            {
                void Reset(IList list, IEnumerable items);
            }

            private sealed class NonGenericResetter<TTarget> : IResetter
            {
                private readonly Action<TTarget, IEnumerable> reset;

                public NonGenericResetter(MethodInfo method)
                {
                    this.reset = (Action<TTarget, IEnumerable>)Delegate.CreateDelegate(typeof(Action<TTarget, IEnumerable>), method);
                }

                public void Reset(IList list, IEnumerable items)
                {
                    this.reset((TTarget)list, items);
                }
            }

            private sealed class GenericResetter<TTarget, T> : IResetter
            {
                private readonly Action<TTarget, IEnumerable<T>> reset;

                public GenericResetter(MethodInfo method)
                {
                    this.reset = (Action<TTarget, IEnumerable<T>>)Delegate.CreateDelegate(typeof(Action<TTarget, IEnumerable<T>>), method);
                }

                public void Reset(IList list, IEnumerable items)
                {
                    this.reset((TTarget)list, items.Cast<T>());
                }
            }
        }
    }
}

Solution 2

Create a command that fires on the DataGrid's SelectionChanged event, passing in the DataGrid's SelectedItems.

In your ViewModel, have a List of selected objects.

Your SelectionChangedCommand execution method would then update that collection of selected objects.

For example:

In my XAML:

<DataGrid ItemsSource="{Binding Datasets, NotifyOnTargetUpdated=True}" Name="dsDatagrid" SelectionMode="Extended" MouseDoubleClick="ViewDataset">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="SelectionChanged">
            <cmd:EventToCommand Command="{Binding SelectionChangedCommand}" CommandParameter="{Binding ElementName=dsDatagrid, Path=SelectedItems}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</DataGrid>

In my ViewModel:

private List<ObservableDataset> selectedDatasets;

private void SelectionChangedExecuted(object datasets)
{
    this.selectedDatasets = new List<ObservableDataset>((datasets as IList).Cast<ObservableDataset>());
}

EDIT: I'm using MVVMLight.

Share:
13,008
Admin
Author by

Admin

Updated on June 30, 2022

Comments

  • Admin
    Admin almost 2 years

    I have read several posts on this topic but many are from a previous versions of VS or framework. What I am trying to do is selected multiple rows from a dataGrid and return those rows into a bound observable collection.

    I have tried creating a property(of type) and adding it to an observable collection and it works with single records but the code never fires with multiple records.

    Is there a clean way to do this in VS2013 using an MVVM patern?

    Any thoughts would be appreciated.

    <DataGrid x:Name="MainDataGrid" Height="390" Width="720" 
                      VerticalAlignment="Center" CanUserAddRows="False" CanUserDeleteRows="False" AutoGenerateColumns="False"  
                      ItemsSource="{Binding Path=DisplayInDataGrid}"
                      SelectedItem="{Binding Path=DataGridItemSelected}"
                      SelectionMode="Extended"
    
    private ObservableCollection<ScannedItem> _dataGridItemsSelected;
        public ObservableCollection<ScannedItem> DataGridItemsSelected
        {
            get { return _dataGridItemsSelected; }
            set 
            {
                _dataGridItemsSelected = value;
                OnPropertyChanged("DataGridItemsSelected");
    
            }
        }
    
    
        private ScannedItem _dataGridItemSelected;
        public ScannedItem DataGridItemSelected
        {
            get { return _dataGridItemSelected;}
            set
            {
                _dataGridItemSelected = value;
                OnPropertyChanged("DataGridItemSelected");
                EnableButtons();
                LoadSelectedCollection(DataGridItemSelected); 
            }
        }
    
        void LoadSelectedCollection(ScannedItem si)
        {
    
            if (DataGridItemsSelected == null)
            {
                DataGridItemsSelected = new ObservableCollection<ScannedItem>();
            }
    
            DataGridItemsSelected.Add(si);
    
        }