从 ViewModel 手动将 WPF DataGrid 自定义为 select 一行或多行

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

我尝试为 WPF/MVVM 创建一个 DataGrid,它允许从 ViewModel 代码手动 select 一个或多个项目。

像往常一样,DataGrid 应该能够将其 ItemsSource 绑定到 List / ObservableCollection。新的部分是它应该维护另一个可绑定列表,即 SelectedItemsList。添加到此列表的每个项目都应立即 select 编辑到 DataGrid 中。

我在 Whosebug 上找到了 this 解决方案:DataGrid 被扩展为包含 SelectedItemsList 的 属性 / Dependency属性:

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));
}

在 View/XAML 中,此 属性 绑定到 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>

ViewModel 还实现了命令 CmdSelectSomeRows,其中 select 用于测试的一些行。测试应用程序的 ViewModel 如下所示:

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();
}

这有效,但只是部分有效:应用程序启动后,当项目从 ViewModel 添加到 SelectedItemsList 时,它们 not 在 DataGrid 中显示为 selected 行.为了让它工作,我必须首先用鼠标 select 一些行。然后,当我从 ViewModel 添加项目到 SelectedItemsList 时,这些 are 显示 selected – 正如我想要的那样。

我怎样才能做到这一点而不必先用鼠标 select 一些行?

您应该订阅 CustomDataGrid 中的 Loaded 事件并初始化 Grid 的 SelectedItems(因为您从未输入 SelectionChangedEvent,所以 SelectedItemsList 和您的 SelectedItems 之间没有 link数据网格。

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;
    }

并且必须像这样修改 SelectionChanged 事件处理程序:

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

请注意,虽然这会解决您的问题,但这不是真正的同步,因为它只会在开始时从 ViewModel 复制项目。 如果您以后需要更改 ViewModel 中的项目并将其反映在选择中,请告诉我,我将编辑我的答案。


编辑:实现“真实”同步的解决方案

我像您一样创建了一个继承自 DataGrid 的 class。 您将需要添加 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));
    }

您必须提前初始化 DataGridSelectedItems,否则在尝试订阅 collectionChanged 事件时会出现空异常。

/// <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>();

我没有尝试所有的边缘情况,但这应该会给你一个好的开始,我添加了一些关于如何改进它的指导。如果代码的某些部分不清楚,请告诉我,我会尝试添加一些评论。