using System.Collections; using System.Windows.Controls.Primitives; using System.Windows.Data; using System.Windows.Input; using Melskin.Extensions; namespace Melskin.Controls; /// /// NeuDataGrid 类继承自 DataGrid,用于在WPF应用程序中显示和编辑表格数据。它扩展了标准的DataGrid功能,增加了绑定选定项列表以及全选功能。 /// 通过BindableSelectedItems属性可以双向绑定当前选择的项目列表,IsAllSelected属性则允许控制是否所有行被选中。 /// 此外,NeuDataGrid自动为第一列添加了一个复选框,用户可以通过点击该复选框来实现整行的选择或取消选择。 /// public class NeuDataGrid : DataGrid { private bool isSelectionChangeHandled; // 这个标志位用于防止 SelectedItems 和 BindableSelectedItems 之间产生无限循环。 private bool isSyncing; /// /// /// public NeuDataGrid() { this.LoadingRow += NeuDataGrid_LoadingRow; // 订阅选择变化事件 this.SelectionChanged += NeuDataGrid_SelectionChanged; } #region 可绑定的选中项 (BindableSelectedItems) /// /// 代表 NeuDataGrid 控件中 BindableSelectedItems 属性的依赖属性。此属性允许将当前选中的项目列表与视图模型或其他数据源进行双向绑定。 /// 绑定的集合需要实例化才能绑定成功,不能为空引用 /// public static readonly DependencyProperty BindableSelectedItemsProperty = DependencyProperty.Register( nameof(BindableSelectedItems), typeof(IList), typeof(NeuDataGrid), // 注意: 类型已更新 new FrameworkPropertyMetadata( null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnBindableSelectedItemsChanged)); /// /// 获取或设置一个列表,该列表代表当前在 NeuDataGrid 中选中的所有项。此属性支持双向数据绑定, /// 使得可以轻松地在视图模型或其他数据源 与 NeuDataGrid 控件之间同步选定项。 /// /// 类型为 IList 的对象,包含 NeuDataGrid 当前选中的所有项目。 /// /// 通过使用 BindableSelectedItems 属性,开发者能够更方便地管理用户界面中的选择状态,并且可以在后台逻辑中直接访问和操作这些选择, /// 从而实现更加灵活的数据交互体验。当 NeuDataGrid 内部的选择发生变化时,BindableSelectedItems 会自动更新以反映最新的选择; /// 同样地,如果外部修改了 BindableSelectedItems 的值,NeuDataGrid 也会相应地调整其显示状态来匹配新的选择集合。 /// public IList BindableSelectedItems { get => (IList)GetValue(BindableSelectedItemsProperty); set => SetValue(BindableSelectedItemsProperty, value); } // 当ViewModel中的集合首次绑定或被替换时,从ViewModel -> 同步到UI private static void OnBindableSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if(d is NeuDataGrid dataGrid) { dataGrid.SyncToUi(); } } #endregion #region 全选/全不选 (IsAllSelected) /// /// 代表 NeuDataGrid 控件中 IsAllSelected 属性的依赖属性。此属性允许控制数据网格中的所有行是否被选中,通过设置为 true 或 false 来实现全选或取消全选。 /// 当该属性值发生变化时,会触发 OnIsAllSelectedChanged 方法,用于更新数据网格内所有行的选择状态。 /// public static readonly DependencyProperty IsAllSelectedProperty = DependencyProperty.Register( nameof(IsAllSelected), typeof(bool?), typeof(NeuDataGrid), // 注意: 类型已更新 new FrameworkPropertyMetadata(false, OnIsAllSelectedChanged)); /// /// 获取或设置一个布尔值,表示 NeuDataGrid 控件中的所有行是否被选中。此属性允许控制和检查控件内所有行的选择状态。 /// 当设置为 true 时,表示所有行都被选中;当设置为 false 时,表示没有行被选中;当设置为 null 时,表示部分行被选中。 /// public bool? IsAllSelected { get => (bool?)GetValue(IsAllSelectedProperty); set => SetValue(IsAllSelectedProperty, value); } // 当表头CheckBox被点击,IsAllSelected属性改变时触发 private static void OnIsAllSelectedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { // 如果是SelectionChanged方法正在更新IsAllSelected,则直接返回,避免循环 if (d is not NeuDataGrid dataGrid || dataGrid.isSelectionChangeHandled) return; bool? isChecked = (bool?)e.NewValue; if(isChecked.HasValue) { // 1. 设置标志,告诉SelectionChanged事件处理器“是我触发了你,请不要再反过来更新表头状态” dataGrid.isSelectionChangeHandled = true; if(isChecked.Value) dataGrid.SelectAll(); else dataGrid.UnselectAll(); // 2. 解除标志 dataGrid.isSelectionChangeHandled = false; } } #endregion #region 核心事件处理与逻辑 // 当DataGrid的选择项发生任何变化时触发 private void NeuDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e) { // 如果是同步代码正在执行,直接返回 if(isSyncing) return; // 关键修复:无论如何,首先要将UI的选择状态同步到ViewModel! SyncToVm(); // 如果本次SelectionChanged是由OnIsAllSelectedChanged方法(即点击表头)触发的, // 那么就不需要再反过来更新表头状态了,直接返回。 if(isSelectionChangeHandled) return; // --- 仅当用户手动选择/取消选择行时,才执行以下逻辑 --- // 1. 设置标志,告诉OnIsAllSelectedChanged“是我要来更新你了,你收到改变后无需再做任何事” isSelectionChangeHandled = true; // 获取真实的、非占位符的数据项总数 var realItemsCount = this.Items.Cast().Count(item => item != CollectionView.NewItemPlaceholder); // 获取真实的、非占位符的选中项总数 var realSelectedItemsCount = this.SelectedItems .Cast() .Count(item => item != CollectionView.NewItemPlaceholder); if(realItemsCount > 0 && realSelectedItemsCount == realItemsCount) { this.IsAllSelected = true; } else if(realSelectedItemsCount == 0) { this.IsAllSelected = false; } else { this.IsAllSelected = null; } // 2. 解除标志 isSelectionChangeHandled = false; } // 同步:从UI (SelectedItems) -> ViewModel (BindableSelectedItems) private void SyncToVm() { // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if(isSyncing || BindableSelectedItems == null) return; isSyncing = true; BindableSelectedItems.Clear(); foreach(var item in this.SelectedItems) { // 忽略“新行占位符” if(item != CollectionView.NewItemPlaceholder) { BindableSelectedItems.Add(item); } } isSyncing = false; } // 同步:从ViewModel (BindableSelectedItems) -> UI (SelectedItems) private void SyncToUi() { // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if(isSyncing || BindableSelectedItems == null) return; isSyncing = true; this.SelectedItems.Clear(); foreach(var item in BindableSelectedItems) { this.SelectedItems.Add(item); } isSyncing = false; } #endregion #region 控件初始化与UI元素创建 /// protected override void OnInitialized(EventArgs e) { base.OnInitialized(e); var checkBoxColumn = new DataGridTemplateColumn(); // 创建表头模板 var headerFactory = new FrameworkElementFactory(typeof(CheckBox)); var headerBinding = new Binding("IsAllSelected") { Source = this, Mode = BindingMode.TwoWay }; headerFactory.SetBinding(ToggleButton.IsCheckedProperty, headerBinding); headerFactory.SetValue(FrameworkElement.HorizontalAlignmentProperty, HorizontalAlignment.Center); headerFactory.SetValue(FrameworkElement.VerticalAlignmentProperty, VerticalAlignment.Center); checkBoxColumn.HeaderTemplate = new DataTemplate { VisualTree = headerFactory }; // 创建单元格模板 var cellFactory = new FrameworkElementFactory(typeof(CheckBox)); var cellBinding = new Binding("IsSelected") { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(DataGridRow), 1), Mode = BindingMode.TwoWay }; cellFactory.SetBinding(ToggleButton.IsCheckedProperty, cellBinding); cellFactory.SetValue(FrameworkElement.HorizontalAlignmentProperty, HorizontalAlignment.Center); cellFactory.SetValue(FrameworkElement.VerticalAlignmentProperty, VerticalAlignment.Center); cellFactory.AddHandler( PreviewMouseLeftButtonDownEvent, new MouseButtonEventHandler(OnCellCheckBoxPreviewMouseLeftButtonDown)); checkBoxColumn.CellTemplate = new DataTemplate { VisualTree = cellFactory }; this.Columns.Insert(0, checkBoxColumn); } // 处理单元格CheckBox点击冲突 private void OnCellCheckBoxPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (sender is not CheckBox checkBox) return; var row = checkBox.FindParent(); if(row != null && row.Item != CollectionView.NewItemPlaceholder) { row.IsSelected = !row.IsSelected; e.Handled = true; } } // 设置行号 private void NeuDataGrid_LoadingRow(object? sender, DataGridRowEventArgs e) { e.Row.Header = (e.Row.GetIndex() + 1).ToString(); } // 查找父控件的辅助方法 //private static T? FindVisualParent(DependencyObject child) where T : DependencyObject //{ // DependencyObject? parentObject = VisualTreeHelper.GetParent(child); // if(parentObject == null) // return null; // T? parent = parentObject as T; // return parent ?? FindVisualParent(parentObject); //} #endregion }