Files
ShrlAlgoToolkit/Melskin/Controls/NeuDataGrid.xaml.cs
2026-02-12 21:29:00 +08:00

270 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using Melskin.Extensions;
namespace Melskin.Controls;
/// <summary>
/// NeuDataGrid 类继承自 DataGrid用于在WPF应用程序中显示和编辑表格数据。它扩展了标准的DataGrid功能增加了绑定选定项列表以及全选功能。
/// 通过BindableSelectedItems属性可以双向绑定当前选择的项目列表IsAllSelected属性则允许控制是否所有行被选中。
/// 此外NeuDataGrid自动为第一列添加了一个复选框用户可以通过点击该复选框来实现整行的选择或取消选择。
/// </summary>
public class NeuDataGrid : DataGrid
{
private bool isSelectionChangeHandled;
// 这个标志位用于防止 SelectedItems 和 BindableSelectedItems 之间产生无限循环。
private bool isSyncing;
/// <summary>
///
/// </summary>
public NeuDataGrid()
{
this.LoadingRow += NeuDataGrid_LoadingRow;
// 订阅选择变化事件
this.SelectionChanged += NeuDataGrid_SelectionChanged;
}
#region (BindableSelectedItems)
/// <summary>
/// 代表 NeuDataGrid 控件中 BindableSelectedItems 属性的依赖属性。此属性允许将当前选中的项目列表与视图模型或其他数据源进行双向绑定。
/// 绑定的集合需要实例化才能绑定成功,不能为空引用
/// </summary>
public static readonly DependencyProperty BindableSelectedItemsProperty =
DependencyProperty.Register(
nameof(BindableSelectedItems),
typeof(IList),
typeof(NeuDataGrid), // 注意: 类型已更新
new FrameworkPropertyMetadata(
null,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnBindableSelectedItemsChanged));
/// <summary>
/// 获取或设置一个列表,该列表代表当前在 NeuDataGrid 中选中的所有项。此属性支持双向数据绑定,
/// 使得可以轻松地在视图模型或其他数据源 与 NeuDataGrid 控件之间同步选定项。
/// </summary>
/// <value>类型为 IList 的对象,包含 NeuDataGrid 当前选中的所有项目。</value>
/// <remarks>
/// 通过使用 BindableSelectedItems 属性,开发者能够更方便地管理用户界面中的选择状态,并且可以在后台逻辑中直接访问和操作这些选择,
/// 从而实现更加灵活的数据交互体验。当 NeuDataGrid 内部的选择发生变化时BindableSelectedItems 会自动更新以反映最新的选择;
/// 同样地,如果外部修改了 BindableSelectedItems 的值NeuDataGrid 也会相应地调整其显示状态来匹配新的选择集合。
/// </remarks>
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)
/// <summary>
/// 代表 NeuDataGrid 控件中 IsAllSelected 属性的依赖属性。此属性允许控制数据网格中的所有行是否被选中,通过设置为 true 或 false 来实现全选或取消全选。
/// 当该属性值发生变化时,会触发 OnIsAllSelectedChanged 方法,用于更新数据网格内所有行的选择状态。
/// </summary>
public static readonly DependencyProperty IsAllSelectedProperty =
DependencyProperty.Register(
nameof(IsAllSelected),
typeof(bool?),
typeof(NeuDataGrid), // 注意: 类型已更新
new FrameworkPropertyMetadata(false, OnIsAllSelectedChanged));
/// <summary>
/// 获取或设置一个布尔值,表示 NeuDataGrid 控件中的所有行是否被选中。此属性允许控制和检查控件内所有行的选择状态。
/// 当设置为 true 时,表示所有行都被选中;当设置为 false 时,表示没有行被选中;当设置为 null 时,表示部分行被选中。
/// </summary>
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<object>().Count(item => item != CollectionView.NewItemPlaceholder);
// 获取真实的、非占位符的选中项总数
var realSelectedItemsCount = this.SelectedItems
.Cast<object>()
.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元素创建
/// <inheritdoc />
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<DataGridRow>();
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<T>(DependencyObject child) where T : DependencyObject
//{
// DependencyObject? parentObject = VisualTreeHelper.GetParent(child);
// if(parentObject == null)
// return null;
// T? parent = parentObject as T;
// return parent ?? FindVisualParent<T>(parentObject);
//}
#endregion
}