前言
在 WPF 开发中,DataGrid 是最常用的列表控件,但原生控件不支持直接绑定 SelectedItems 到 ViewModel ,且多选(Ctrl/Shift)场景下,通过附加属性实现同步时,极易出现 IList 强类型转换异常。
本文提供一套通用、强类型、纯 MVVM 的解决方案,完美实现 DataGrid 多选选中项同步到 ViewModel,并通过依赖属性注册命令回调,无冗余代码、无类型报错、可全局复用。
一、问题背景
- 原生限制 :WPF DataGrid 的
SelectedItems为只读属性,无法直接绑定到 ViewModel 集合; - 类型异常 :附加属性同步选中项时,
SelectedItemCollection无法直接强转为IList<T>,抛出InvalidCastException; - 业务需求 :需要 Ctrl/Shift 多选,且 ViewModel 接收强类型泛型集合 ,不使用弱类型
object/IList; - 架构规范:严格遵循 MVVM 模式,无后台代码耦合。
二、核心技术方案
- 附加属性(DependencyProperty) :实现
SelectedItems双向绑定; - 反射动态泛型转换:自动识别实体类型,避免写死代码,解决类型转换异常;
- 命令回调 :通过依赖属性注册
SelectionChanged命令,将选中项强类型传递给 ViewModel; - 多选支持 :
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>
四、方案核心优势
-
强类型安全 ViewModel 直接使用
IList<T>接收参数,无类型转换异常,符合代码规范。 -
通用复用 辅助类不写死任何实体类型,所有 DataGrid 控件均可直接使用。
-
纯 MVVM 架构无后台代码耦合,依赖属性 + 命令实现完全解耦。
-
完整多选支持支持单击单选、Ctrl 多选、Shift 区间选择。
-
双向同步界面选择 → ViewModel 集合自动更新;代码修改集合 → 界面选中状态同步。
五、常见问题排查
1. OnSelectedItemsChanged 不触发
- ViewModel 中
SelectedItems必须实例化 (= new List<T>()),不能为null; - XAML 命名空间与辅助类命名空间一致。
2. 类型转换异常
- 禁止直接将
SelectedItemCollection强转为IList<T>; - 使用本文反射泛型转换逻辑,自动适配类型。
3. 多选不生效
- DataGrid 必须设置
SelectionMode="Extended"。
4. 命令不执行
- 检查命令绑定路径是否正确;
- 命令
CanExecute返回true。
六、总结
本文方案解决了 WPF DataGrid 多选开发中的三大核心痛点:
- 原生
SelectedItems无法绑定; - 多选选中项类型转换异常;
- MVVM 模式下命令回调强类型适配。