Search code examples
wpfmvvmdatagridviewmodel

Custom WPF DataGrid to select one or multiple rows manually from ViewModel


I try to create a DataGrid for WPF / MVVM which allows to manually select one ore more items from ViewModel code.

As usual the DataGrid should be able to bind its ItemsSource to a List / ObservableCollection. The new part is that it should maintain another bindable list, the SelectedItemsList. Each item added to this list should immediately be selected in the DataGrid.

I found this solution on Stackoverflow: There the DataGrid is extended to hold a Property / DependencyProperty for the SelectedItemsList:

public class CustomDataGrid : DataGrid
{
  public CustomDataGrid()
  {
    this.SelectionChanged += CustomDataGrid_SelectionChanged;
  }

  private void CustomDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
  {
    this.SelectedItemsList = this.SelectedItems;
  }

  public IList SelectedItemsList
  {
    get { return (IList)GetValue(SelectedItemsListProperty); }
    set { SetValue(SelectedItemsListProperty, value); }
  }

  public static readonly DependencyProperty SelectedItemsListProperty =
      DependencyProperty.Register("SelectedItemsList", 
                                  typeof(IList), 
                                  typeof(CustomDataGrid), 
                                  new PropertyMetadata(null));
}

In the View/XAML this property is bound to the ViewModel:

<Grid>
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="Auto"/>
  </Grid.RowDefinitions>
  <ucc:CustomDataGrid Grid.Row="0" 
                      ItemsSource="{Binding DataGridItems}"
                      SelectionMode="Extended"
                      AlternatingRowBackground="Beige"
                      SelectionUnit="FullRow"
                      IsReadOnly="True"
                      SelectedItemsList="{Binding DataGridSelectedItems, 
                                          Mode=TwoWay,
                                          UpdateSourceTrigger=PropertyChanged}" />

  <Button Grid.Row="1"
        Margin="5"
        HorizontalAlignment="Center"
        Content="Select some rows"
        Command="{Binding CmdSelectSomeRows}"/>

</Grid>

The ViewModel also implements the command CmdSelectSomeRows which selects some rows for testing. The ViewModel of the test application looks like this:

public class CustomDataGridViewModel : ObservableObject
{
  public IList DataGridSelectedItems
  {
    get { return dataGridSelectedItems; }
    set
    {
      dataGridSelectedItems = value;
      OnPropertyChanged(nameof(DataGridSelectedItems));
    }
  }

  public ICommand CmdSelectSomeRows { get; }

  public ObservableCollection<ExamplePersonModel> DataGridItems { get; private set; }



  public CustomDataGridViewModel()
  {
    // Create some example items
    DataGridItems = new ObservableCollection<ExamplePersonModel>();

    for (int i = 0; i < 10; i++)
    {
      DataGridItems.Add(new ExamplePersonModel
      {
        Name = $"Test {i}",
        Age = i * 22
      });
    }

    CmdSelectSomeRows = new RelayCommand(() =>
    {
      if (DataGridSelectedItems == null)
      {
        DataGridSelectedItems = new ObservableCollection<ExamplePersonModel>();
      }
      else
      {
        DataGridSelectedItems.Clear();
      }

      DataGridSelectedItems.Add(DataGridItems[0]);
      DataGridSelectedItems.Add(DataGridItems[1]);
      DataGridSelectedItems.Add(DataGridItems[4]);
      DataGridSelectedItems.Add(DataGridItems[6]);
    }, () => true);
  }
  
  
  
  private IList dataGridSelectedItems = new ArrayList();
}

This works, but only partially: After application start when items are added to the SelectedItemsList from ViewModel, they are not displayed as selected rows in the DataGrid. To get it to work I must first select some rows with the mouse. When I then add items to the SelectedItemsList from ViewModel these are displayed selected – as I want it.

How can I achieve this without having to first select some rows with the mouse?


Solution

  • You should subscribe to the Loaded event in your CustomDataGrid and initialize the SelectedItems of the Grid (since you never entered the SelectionChangedEvent, there is no link between the SelectedItemsList and the SelectedItems of your DataGrid.

    private bool isSelectionInitialization = false;
    
        private void CustomDataGrid_Loaded(object sender, RoutedEventArgs e)
        {
            this.isSelectionInitialization = true;
            foreach (var item in this.SelectedItemsList)
            {
                this.SelectedItems.Clear();
                this.SelectedItems.Add(item);
            }
            this.isSelectionInitialization = false;
        }
    

    and the SelectionChanged event handler has to be modified like this:

    private void CustomDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (!this.isSelectionInitialization)
            {
                this.SelectedItemsList = this.SelectedItems;
            }
            else
            {
                //Initialization from the ViewModel
            }
        }
    

    Note that while this will fix your problem, this won't be a true synchronization as it will only copy the items from the ViewModel at the beginning. If you need to change the items in the ViewModel at a later time and have it reflected in the selection let me know and I will edit my answer.


    Edit: Solution to have a "true" synchronization

    I created a class inheriting from DataGrid like you did. You will need to add the using

    using System;
    using System.Collections;
    using System.Collections.Specialized;
    using System.Windows;
    using System.Windows.Controls;
    
    public class CustomDataGrid : DataGrid
        {
            public CustomDataGrid()
            {
                this.SelectionChanged += CustomDataGrid_SelectionChanged;
                this.Loaded += CustomDataGrid_Loaded;
            }
    
            private void CustomDataGrid_Loaded(object sender, RoutedEventArgs e)
            {
                //Can't do it in the constructor as the bound values won't be initialized
                //If it is expected for the bound collection to be null initially, you could subscribe to the change of the 
                //dependency in order to subscribe to the collectionChanged event on the first non null value
                this.SelectedItemsList.CollectionChanged += SelectedItemsList_CollectionChanged;
                //We call the update in case we have already some items in the VM collection
                this.UpdateUIWithSelectedItemsFromVm();
                
                if(this.SelectedItems.Count != 0)
                {
                    //Otherwise the items won't be as visible unless you change the style (this part is not required)
                    this.Focus();
                }
                else
                {
                    //No focus
                }
            }
    
            private void SelectedItemsList_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
            {
                this.UpdateUIWithSelectedItemsFromVm();
            }
    
            private void UpdateUIWithSelectedItemsFromVm()
            {
                if (!this.isSelectionChangeFromUI)
                {
                    this.isSelectionChangeFromViewModel = true;
                    this.SelectedItems.Clear();
                    if (this.SelectedItemsList == null)
                    {
                        //Nothing to do, we just cleared all the selections
                    }
                    else
                    {
                        if (this.SelectedItemsList is IList iListFromVM)
                            foreach (var item in iListFromVM)
                            {
                                this.SelectedItems.Add(item);
                            }
                    }
                    this.isSelectionChangeFromViewModel = false;
                }
                else
                {
                    //Nothing to do, the change is coming from the SelectionChanged event
                }
            }
    
            private void CustomDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
            {
                //If your collection allow suspension of notifications, it would be a good idea to add a check here in order to use it
                if(!this.isSelectionChangeFromViewModel)
                {
                    this.isSelectionChangeFromUI = true;
                    if (this.SelectedItemsList is IList iListFromVM)
                    {
                        iListFromVM.Clear();
                        foreach (var item in SelectedItems)
                        {
                            iListFromVM.Add(item);
                        }
                    }
                    else
                    {
                        throw new InvalidOperationException("The bound collection must inherit from IList");
                    }
                    this.isSelectionChangeFromUI = false;
                }
                else
                {
                    //Nothing to do, the change is comming from the bound collection so no need to update it
                }
            }
    
            private bool isSelectionChangeFromUI = false;
    
            private bool isSelectionChangeFromViewModel = false;
    
            public INotifyCollectionChanged SelectedItemsList
            {
                get { return (INotifyCollectionChanged)GetValue(SelectedItemsListProperty); }
                set { SetValue(SelectedItemsListProperty, value); }
            }
    
            public static readonly DependencyProperty SelectedItemsListProperty =
                DependencyProperty.Register(nameof(SelectedItemsList),
                                            typeof(INotifyCollectionChanged),
                                            typeof(CustomDataGrid),
                                            new PropertyMetadata(null));
        }
    

    You will have to initialize the DataGridSelectedItems earlier or you there will be a null exception when trying to subscribe to the collectionChanged event.

    /// <summary>
    /// I removed the notify property changed from your example as it probably isn't necessary unless you really intended to create a new Collection at some point instead of just clearing the items
    /// (In this case you will have to adapt the code for the synchronization of CustomDataGrid so that it subscribe to the collectionChanged event of the new collection)
    /// </summary>
    public ObservableCollection<ExamplePersonModel> DataGridSelectedItems { get; set; } = new ObservableCollection<ExamplePersonModel>();
    

    I didn't try all the edge cases but this should give you a good start and I added some directions as to how to improve it. Let me know if some parts of the code aren't clear and I will try to add some comments.