WPF DataGrid 多选绑定 + 强类型命令回调 通用解决方案

前言

在 WPF 开发中,DataGrid 是最常用的列表控件,但原生控件不支持直接绑定 SelectedItems 到 ViewModel ,且多选(Ctrl/Shift)场景下,通过附加属性实现同步时,极易出现 IList 强类型转换异常。

本文提供一套通用、强类型、纯 MVVM 的解决方案,完美实现 DataGrid 多选选中项同步到 ViewModel,并通过依赖属性注册命令回调,无冗余代码、无类型报错、可全局复用。


一、问题背景

  1. 原生限制 :WPF DataGrid 的 SelectedItems 为只读属性,无法直接绑定到 ViewModel 集合;
  2. 类型异常 :附加属性同步选中项时,SelectedItemCollection 无法直接强转为 IList<T>,抛出 InvalidCastException
  3. 业务需求 :需要 Ctrl/Shift 多选,且 ViewModel 接收强类型泛型集合 ,不使用弱类型 object/IList
  4. 架构规范:严格遵循 MVVM 模式,无后台代码耦合。

二、核心技术方案

  1. 附加属性(DependencyProperty) :实现 SelectedItems 双向绑定;
  2. 反射动态泛型转换:自动识别实体类型,避免写死代码,解决类型转换异常;
  3. 命令回调 :通过依赖属性注册 SelectionChanged 命令,将选中项强类型传递给 ViewModel;
  4. 多选支持SelectionMode="Extended" 开启 Ctrl/Shift 标准多选。

三、完整代码实现

1. 通用 DataGrid 辅助类(DataGridHelper)

该类为静态工具类,通过附加属性 实现选中项同步 + 命令回调,全局复用,无需修改

cs 复制代码
using System;
using System.Collections;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

/// <summary>
/// DataGrid 多选选中项绑定 + 选择变更命令回调 通用辅助类
/// 解决:SelectedItems 无法绑定、IList<T> 类型转换异常
/// </summary>
public static class DataGridHelper
{
    #region 选中项集合附加属性(双向同步到ViewModel)
    public static readonly DependencyProperty SelectedItemsProperty =
        DependencyProperty.RegisterAttached(
            "SelectedItems",
            typeof(IList),
            typeof(DataGridHelper),
            new PropertyMetadata(null, OnSelectedItemsChanged));

    public static IList GetSelectedItems(DependencyObject obj) => (IList)obj.GetValue(SelectedItemsProperty);
    public static void SetSelectedItems(DependencyObject obj, IList value) => obj.SetValue(SelectedItemsProperty, value);
    #endregion

    #region 选择变更命令附加属性(回调通知ViewModel)
    public static readonly DependencyProperty SelectionChangedCommandProperty =
        DependencyProperty.RegisterAttached(
            "SelectionChangedCommand",
            typeof(ICommand),
            typeof(DataGridHelper),
            new PropertyMetadata(null));

    public static ICommand GetSelectionChangedCommand(DependencyObject obj) => (ICommand)obj.GetValue(SelectionChangedCommandProperty);
    public static void SetSelectionChangedCommand(DependencyObject obj, ICommand value) => obj.SetValue(SelectionChangedCommandProperty, value);
    #endregion

    #region 绑定变更回调
    private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is not DataGrid dataGrid) return;

        // 避免重复订阅事件
        dataGrid.SelectionChanged -= DataGrid_SelectionChanged;
        dataGrid.SelectionChanged += DataGrid_SelectionChanged;
    }
    #endregion

    #region 选择变更核心逻辑
    private static void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (sender is not DataGrid dataGrid) return;

        // 1. 同步选中项到 ViewModel 集合
        var viewModelList = GetSelectedItems(dataGrid);
        if (viewModelList != null)
        {
            viewModelList.Clear();
            foreach (var item in dataGrid.SelectedItems)
            {
                viewModelList.Add(item);
            }
        }

        // 2. 获取绑定的命令
        var command = GetSelectionChangedCommand(dataGrid);
        if (command == null) return;

        // 3. 核心:反射动态生成强类型泛型集合,解决类型转换异常
        var genericList = CreateGenericList(dataGrid, viewModelList);
        if (command.CanExecute(genericList))
        {
            command.Execute(genericList);
        }
    }
    #endregion

    #region 反射创建泛型集合(自动识别实体类型)
    /// <summary>
    /// 动态创建泛型集合,适配任意实体类型,不写死代码
    /// </summary>
    private static object CreateGenericList(DataGrid dg, IList viewModelList)
    {
        if (viewModelList == null) return null;

        // 获取ViewModel集合的泛型类型(如:MountingInfo)
        Type itemType = viewModelList.GetType().GetGenericArguments()[0];
        // 复制选中项到数组
        Array itemArray = Array.CreateInstance(itemType, dg.SelectedItems.Count);
        dg.SelectedItems.CopyTo(itemArray, 0);

        // 反射调用 ToList<T>() 生成强类型集合
        var toListMethod = typeof(Enumerable)
            .GetMethod(nameof(Enumerable.ToList), BindingFlags.Public | BindingFlags.Static)
            ?.MakeGenericMethod(itemType);

        return toListMethod?.Invoke(null, new object[] { itemArray });
    }
    #endregion
}

2. ViewModel 强类型实现(Prism 框架)

严格使用强类型泛型集合接收选中项,无弱类型、无类型转换报错。

cs 复制代码
using Prism.Commands;
using Prism.Mvvm;
using System.Collections.ObjectModel;
using System.Collections.Generic;

/// <summary>
/// 示例ViewModel,替换为你的业务实体即可
/// </summary>
public class DataGridViewModel : BindableBase
{
    #region 构造函数
    public DataGridViewModel()
    {
        // 初始化命令:强类型接收选中项
        SelectionChangedCommand = new DelegateCommand<IList<MountingInfo>>(OnSelectionChanged);
        
        // 初始化测试数据
        DataList = new ObservableCollection<MountingInfo>
        {
            new MountingInfo { Id = 1, Name = "测试项1" },
            new MountingInfo { Id = 2, Name = "测试项2" },
            new MountingInfo { Id = 3, Name = "测试项3" }
        };
    }
    #endregion

    #region DataGrid 数据源
    private ObservableCollection<MountingInfo> _dataList;
    public ObservableCollection<MountingInfo> DataList
    {
        get => _dataList;
        set => SetProperty(ref _dataList, value);
    }
    #endregion

    #region 多选选中项集合(必须实例化,不可为null)
    private IList<MountingInfo> _selectedItems = new List<MountingInfo>();
    public IList<MountingInfo> SelectedItems
    {
        get => _selectedItems;
        set => SetProperty(ref _selectedItems, value);
    }
    #endregion

    #region 选择变更命令
    public DelegateCommand<IList<MountingInfo>> SelectionChangedCommand { get; }
    #endregion

    #region 命令回调:处理选中项
    /// <summary>
    /// DataGrid 多选变更回调
    /// </summary>
    /// <param name="selectedItems">强类型选中项集合</param>
    private void OnSelectionChanged(IList<MountingInfo> selectedItems)
    {
        // 直接使用强类型集合,无需转换
        foreach (var item in selectedItems)
        {
            // 执行业务逻辑
        }
    }
    #endregion
}

/// <summary>
/// 业务实体类(替换为你的实体)
/// </summary>
public class MountingInfo : BindableBase
{
    private int _id;
    public int Id
    {
        get => _id;
        set => SetProperty(ref _id, value);
    }

    private string _name;
    public string Name
    {
        get => _name;
        set => SetProperty(ref _name, value);
    }
}

3. XAML 绑定配置

XML 复制代码
<Window 
    x:Class="WpfApp.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:你的命名空间">

    <Grid>
        <DataGrid 
            SelectionMode="Extended"  <!-- 开启Ctrl/Shift多选 -->
            ItemsSource="{Binding DataList}"
            <!-- 绑定选中项集合 -->
            local:DataGridHelper.SelectedItems="{Binding SelectedItems}"
            <!-- 绑定选择变更命令 -->
            local:DataGridHelper.SelectionChangedCommand="{Binding SelectionChangedCommand}">
        </DataGrid>
    </Grid>
</Window>

四、方案核心优势

  1. 强类型安全 ViewModel 直接使用 IList<T> 接收参数,无类型转换异常,符合代码规范。

  2. 通用复用 辅助类不写死任何实体类型,所有 DataGrid 控件均可直接使用

  3. 纯 MVVM 架构无后台代码耦合,依赖属性 + 命令实现完全解耦。

  4. 完整多选支持支持单击单选、Ctrl 多选、Shift 区间选择。

  5. 双向同步界面选择 → ViewModel 集合自动更新;代码修改集合 → 界面选中状态同步。


五、常见问题排查

1. OnSelectedItemsChanged 不触发

  • ViewModel 中 SelectedItems 必须实例化= new List<T>()),不能为 null
  • XAML 命名空间与辅助类命名空间一致。

2. 类型转换异常

  • 禁止直接将 SelectedItemCollection 强转为 IList<T>
  • 使用本文反射泛型转换逻辑,自动适配类型。

3. 多选不生效

  • DataGrid 必须设置 SelectionMode="Extended"

4. 命令不执行

  • 检查命令绑定路径是否正确;
  • 命令 CanExecute 返回 true

六、总结

本文方案解决了 WPF DataGrid 多选开发中的三大核心痛点

  1. 原生 SelectedItems 无法绑定;
  2. 多选选中项类型转换异常;
  3. MVVM 模式下命令回调强类型适配。
相关推荐
ZoeJoy83 小时前
机器视觉C# 调用相机:从 USB 摄像头到海康工业相机(WinForms & WPF)
数码相机·c#·wpf
ALex_zry19 小时前
C++高性能日志与监控系统设计
c++·unity·wpf
zhojiew1 天前
使用flink agent框架实现流式情感分析的示例
大数据·flink·wpf
海盗12341 天前
ScottPlot在WPF的基本使用和中文乱码问题
c#·.net·wpf
俄城杜小帅1 天前
C++线程异步和wpf中比较
java·c++·wpf
ZoeJoy81 天前
WPF 从入门到实践:基础、ModernUI 与 MVVM 完全指南
c#·wpf
△曉風殘月〆2 天前
WPF Prism中的MVVM实现
wpf·mvvm
量子物理学2 天前
.NET8 中 WPF与ScottPlot 报表 的完美结合
.net·wpf
△曉風殘月〆2 天前
WPF Prism区域导航功能详解
wpf·mvvm