diff --git a/.agents/skills/datagridex/SKILL.md b/.agents/skills/datagridex/SKILL.md
new file mode 100644
index 0000000..79682a1
--- /dev/null
+++ b/.agents/skills/datagridex/SKILL.md
@@ -0,0 +1,62 @@
+---
+name: datagridex
+description: DataGridEx WPF 表格控件的能力与用法,涵盖分页、行选择、序号、列显隐、自定义操作栏与分页栏、加载动画、排序和图片列。需要了解或配置 DataGridEx 时使用。
+---
+
+# DataGridEx
+
+DataGridEx 继承 WPF DataGrid,提供以下能力。属性默认值和配置示例见 [references/usage.md](references/usage.md)。
+
+## 行选择与序号
+
+- 行头复选框与独立序号列,可分别显示或隐藏。
+- 表头支持全选、未选和部分选中的三态显示;全选范围是当前 ItemsSource/视图。
+- 点击行尾空白可选中对应行;支持行头点击切换选择、可选的右键选中和行菜单。
+- SelectedItems 支持绑定可增删集合,集合内增删与表格选择双向同步。使用 OneWay 绑定集合引用,无需属性 setter;不支持只读或固定大小集合。
+- 序号自动创建,按当前显示顺序和分页偏移计算,排序与行回收后更新。
+- 选中行背景与前景可配置。
+
+## 分页
+
+- 首页、上一页、下一页、末页、页大小选择、当前页和总记录数。
+- PageIndex、PageSize 支持绑定;翻页通过 PageChangedCommand 发出 DataGridPageRequest(PageIndex, PageSize)。
+- 换页或改变页大小清空选择;改变页大小回到第一页,总数减少时自动调整到有效页码。
+- 调用方提供当前页数据和总数;控件不请求数据,也不再次切分 ItemsSource。
+- ShowPagination 控制分页栏显隐。隐藏时仍保留分页状态;不分页时保持 PageIndex=1。
+
+## 列与排序
+
+- 支持原生 DataGrid 列类型和自定义模板列,列放在 DataGridEx.Columns 中。
+- 内置列设置面板,勾选后立即显示或隐藏列。
+- CanHideColumn=False 可将业务列设为固定显示;复选框与序号使用各自开关。
+- 列显示设置不持久化,没有额外的右侧冻结实现。
+- 支持表头排序,排序图标位于列右侧;默认仅对当前数据源排序。
+- 服务端全量排序由调用方处理 Sorting 事件并重新查询。
+
+## 自定义内容
+
+- ShowToolbar 控制操作栏显隐,ShowColumnSettings 控制列设置按钮。
+- ToolbarContent 自定义操作栏中间区域,保留选择提示和列设置按钮。
+- PaginationContent 替换整个分页栏;未设置或为 null 时使用默认分页栏。
+- 两个内容槽均继承调用方 DataContext,可放置按钮、统计文字和业务布局。
+
+## 加载与空数据
+
+- IsLoading 控制加载遮罩及不确定进度动画,LoadingText 配置提示文字。
+- 加载期间禁用表格、操作栏和分页栏,结束后恢复;不控制表格外部控件。
+- NoDataTips 配置空数据提示,IsActive 可抑制空数据提示,与 IsLoading 分开控制。
+- 加载状态由调用方设置,控件不推断请求开始、完成或失败。
+
+## 图片列与虚拟化
+
+- DataGridExImageColumn 支持图片地址绑定、占位图绑定及 ElementStyle。
+- 默认图片尺寸 58×58,Stretch=Uniform。
+- 可见单元格按需加载图片,行回收时取消旧请求,避免旧图片覆盖新行。
+- 支持图片缓存,以及拖动滚动条期间暂停图片加载、停止后恢复。
+- 启用行/列虚拟化和容器回收。
+
+## 集合视图支持
+
+ItemsSource 可绑定 ICollectionView。调用方可使用独立 ListCollectionView,通过 Filter 和 Refresh 实现当前页实时筛选并保留视图排序。
+
+搜索输入框、匹配规则、后台查询、异步取消和跨页全部选择不属于控件内置能力。
\ No newline at end of file
diff --git a/.agents/skills/datagridex/references/usage.md b/.agents/skills/datagridex/references/usage.md
new file mode 100644
index 0000000..e7ef0f5
--- /dev/null
+++ b/.agents/skills/datagridex/references/usage.md
@@ -0,0 +1,97 @@
+# DataGridEx 调用与属性
+
+## 常用接入
+
+以下绑定属性由调用方 ViewModel 提供,controls 表示 DataGridEx 所在命名空间。
+
+~~~xml
+
+
+
+
+
+
+
+
+
+~~~
+
+首次数据由调用方装入,不依赖 PageChangedCommand 自动启动。默认不自动生成列;数据要使用正确的类型,日期/金额等通过绑定格式化展示。
+
+## 常用属性
+
+| 属性 | 当前默认值 / 约束 |
+| --- | --- |
+| ShowCheckBox / ShowRowNumber | false / false,分别显示选择行头与序号 |
+| ShowSelectAll | true;同时启用 ShowCheckBox 才显示表头全选 |
+| ShowToolbar / ShowColumnSettings / ShowPagination | true;列设置按钮位于操作栏 |
+| ToolbarContent | null;页面 DataContext 下的自定义批量操作内容 |
+| PaginationContent | null;自定义整个分页栏,null 恢复默认栏 |
+| PageIndex / PageSize | 1 / 50;均须大于 0,支持双向绑定 |
+| PageSizes | 20、50、100;自定义集合应包含当前 PageSize |
+| TotalItemCount | 0,不能为负数;应为后台查询总数而非当前页条数 |
+| PageChangedCommand | ICommand,参数 DataGridPageRequest |
+| IsLoading / LoadingText | false / 正在加载数据… |
+| SelectedItems | 原生选中集合;绑定时要求可增删集合 |
+| SelectedRowBackground / SelectedRowForeground | 浅梅紫 / 黑色 |
+| ToggleSelectionOnRowHeaderClick | true;点击行头切换选中 |
+| SelectRowOnRightClick | false;开启后右击未选中行会单选该行 |
+| RowContextMenu | null;行级菜单,行 Tag 可访问页面 DataContext |
+| NoDataTips / IsActive | 复用 DataGridHelper;IsActive=true 抑制空数据提示,不等同于 IsLoading |
+| PauseImageLoadingWhileDragging | true;拖动时暂停图片,停止滚动后延迟恢复 |
+| EnableImageCache | true |
+| CanHideColumn(附加属性) | true;对业务列设 false 可固定显示 |
+
+只读状态:PageCount、PageSummary、TotalSummary、SelectionSummary、AreAllRowsSelected(三态)、IsSelectionModeActive、IsImageLoadingSuspended。不要尝试双向写入只读属性。
+
+
+
+## 自定义分页栏
+
+将下面属性元素放在命名为 StoreGrid 的 DataGridEx 内。内置路由命令包含 FirstPageCommand、PreviousPageCommand、NextPageCommand、LastPageCommand,边界由控件判定。
+
+~~~xml
+
+
+
+
+
+
+
+
+
+
+~~~
+
+PageIndex/PageSize 如需双向绑定,明确 Mode=TwoWay 和 UpdateSourceTrigger=PropertyChanged。隐藏分页栏不会停用分页状态或改变当前数据。
+
+## 图片列
+
+~~~xml
+
+~~~
+
+图片列使用 NetworkThumbnail,默认 58×58、Stretch=Uniform;可配置 ElementStyle 和 PlaceholderBinding。可见单元格按需加载,回收时取消旧请求,避免旧图片覆盖新行。拖动暂停和缓存由所属表格配置;它们不是表格整体加载动画的开关。
+
+## 能力边界
+
+列显示设置不持久化;没有额外的右侧冻结实现。控件支持 ICollectionView 数据源,但搜索输入与查询逻辑由调用方实现,不承担 HTTP 请求、数据库访问或跨页全部选择。
\ No newline at end of file
diff --git a/Source/Core/MAX.Framework/PrismExtension/Commands/AsyncDelegateCommand.cs b/Source/Core/MAX.Framework/PrismExtension/Commands/AsyncDelegateCommand.cs
index 8a7a4e9..34e22d4 100644
--- a/Source/Core/MAX.Framework/PrismExtension/Commands/AsyncDelegateCommand.cs
+++ b/Source/Core/MAX.Framework/PrismExtension/Commands/AsyncDelegateCommand.cs
@@ -100,8 +100,7 @@ namespace Prism.Commands
return;
IsExecuting = true;
- await _executeMethod(token)
- .ConfigureAwait(false);
+ await _executeMethod(token);
}
catch
{
diff --git a/Source/Store.Management.Framework/AssemblyInfo.cs b/Source/Store.Management.Framework/AssemblyInfo.cs
index 2d8313b..471b684 100644
--- a/Source/Store.Management.Framework/AssemblyInfo.cs
+++ b/Source/Store.Management.Framework/AssemblyInfo.cs
@@ -5,4 +5,4 @@
[assembly: XmlnsDefinition("Store.Management.Framework", "Store.Management.Framework.Controls")]
[assembly: XmlnsDefinition("Store.Management.Framework", "Store.Management.Framework.Controls.Attach")]
-[assembly: XmlnsPrefix("Store.Management.Framework", "f")]
\ No newline at end of file
+[assembly: XmlnsPrefix("Store.Management.Framework", "f")]
diff --git a/Source/Store.Management.Framework/Controls/DataGridEx/DataGridEx.ImageLoading.cs b/Source/Store.Management.Framework/Controls/DataGridEx/DataGridEx.ImageLoading.cs
new file mode 100644
index 0000000..6323f69
--- /dev/null
+++ b/Source/Store.Management.Framework/Controls/DataGridEx/DataGridEx.ImageLoading.cs
@@ -0,0 +1,100 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+using System.Windows.Media;
+using System.Windows.Threading;
+
+namespace Store.Management.Framework.Controls;
+
+public partial class DataGridEx
+{
+ private DispatcherTimer? imageResumeTimer;
+ private bool draggingScrollThumb;
+
+ private void InitializeImageLoadingPause()
+ {
+ imageResumeTimer = new DispatcherTimer(DispatcherPriority.Background, Dispatcher)
+ {
+ Interval = TimeSpan.FromMilliseconds(500)
+ };
+ imageResumeTimer.Tick += (_, _) =>
+ {
+ imageResumeTimer.Stop();
+ if (!draggingScrollThumb && IsLoaded) SetImageLoadingSuspended(false);
+ };
+ AddHandler(Thumb.DragStartedEvent, new DragStartedEventHandler(ScrollThumbDragStarted), true);
+ AddHandler(Thumb.DragCompletedEvent, new DragCompletedEventHandler(ScrollThumbDragCompleted), true);
+ AddHandler(ScrollViewer.ScrollChangedEvent, new ScrollChangedEventHandler(ImageScrollChanged), true);
+ Unloaded += (_, _) =>
+ {
+ imageResumeTimer.Stop(); draggingScrollThumb = false;
+ SetImageLoadingSuspended(true);
+ };
+ Loaded += (_, _) => { ApplyImageLoadingOptions(); SetImageLoadingSuspended(false); };
+ ApplyImageLoadingOptions();
+ }
+
+ private void SetImageLoadingSuspended(bool suspended)
+ {
+ SetValue(IsImageLoadingSuspendedPropertyKey, suspended);
+ NetworkThumbnail.SetIsLoadingSuspended(this, suspended);
+ }
+
+ private void ApplyImageLoadingOptions()
+ {
+ NetworkThumbnail.SetCacheEnabled(this, EnableImageCache);
+ if (imageResumeTimer == null) return;
+ if (!PauseImageLoadingWhileDragging)
+ {
+ imageResumeTimer.Stop(); draggingScrollThumb = false; SetImageLoadingSuspended(false);
+ }
+ }
+
+ private bool IsOwnScrollBar(DependencyObject? source)
+ {
+ var foundScrollBar = false;
+ for (var parent = source; parent != null; parent = VisualTreeHelper.GetParent(parent))
+ {
+ if (parent is ScrollBar) foundScrollBar = true;
+ if (parent is DataGrid) return ReferenceEquals(parent, this) && foundScrollBar;
+ }
+ return false;
+ }
+
+ private void ScrollThumbDragStarted(object sender, DragStartedEventArgs e)
+ {
+ if (!PauseImageLoadingWhileDragging || !IsOwnScrollBar(e.OriginalSource as DependencyObject)) return;
+ draggingScrollThumb = true;
+ imageResumeTimer!.Stop();
+ // 继承到可见图片及随后回收生成的新行,立即取消请求并清空图片。
+ SetImageLoadingSuspended(true);
+ }
+
+ private void ScrollThumbDragCompleted(object sender, DragCompletedEventArgs e)
+ {
+ if (!PauseImageLoadingWhileDragging || !IsOwnScrollBar(e.OriginalSource as DependencyObject)) return;
+ draggingScrollThumb = false;
+ RestartImageResumeTimer();
+ }
+
+ private void ImageScrollChanged(object sender, ScrollChangedEventArgs e)
+ {
+ if (!NetworkThumbnail.GetIsLoadingSuspended(this) || draggingScrollThumb ||
+ (e.VerticalChange == 0 && e.HorizontalChange == 0) ||
+ !IsOwnScrollViewer(e.OriginalSource as DependencyObject)) return;
+ RestartImageResumeTimer();
+ }
+
+ private bool IsOwnScrollViewer(DependencyObject? source)
+ {
+ for (var parent = source; parent != null; parent = VisualTreeHelper.GetParent(parent))
+ if (parent is DataGrid) return ReferenceEquals(parent, this);
+ return false;
+ }
+
+ private void RestartImageResumeTimer()
+ {
+ imageResumeTimer!.Stop();
+ imageResumeTimer.Start();
+ }
+}
diff --git a/Source/Store.Management.Framework/Controls/DataGridEx/DataGridEx.Options.cs b/Source/Store.Management.Framework/Controls/DataGridEx/DataGridEx.Options.cs
new file mode 100644
index 0000000..eaf7ef3
--- /dev/null
+++ b/Source/Store.Management.Framework/Controls/DataGridEx/DataGridEx.Options.cs
@@ -0,0 +1,52 @@
+using Store.Management.Framework.Controls.Attach;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace Store.Management.Framework.Controls;
+
+public partial class DataGridEx
+{
+ public static readonly DependencyProperty NoDataTipsProperty = DataGridHelper.NoDataTipsProperty.AddOwner(typeof(DataGridEx));
+ public string NoDataTips { get => (string)GetValue(NoDataTipsProperty); set => SetValue(NoDataTipsProperty, value); }
+ public static readonly DependencyProperty IsActiveProperty = DataGridHelper.IsActiveProperty.AddOwner(typeof(DataGridEx));
+ /// 为 true 时抑制空数据提示;可在业务加载数据期间设置。
+ public bool IsActive { get => (bool)GetValue(IsActiveProperty); set => SetValue(IsActiveProperty, value); }
+
+ public static readonly DependencyProperty RowContextMenuProperty = DependencyProperty.Register(nameof(RowContextMenu), typeof(ContextMenu), typeof(DataGridEx), new PropertyMetadata(null));
+ public ContextMenu? RowContextMenu { get => (ContextMenu?)GetValue(RowContextMenuProperty); set => SetValue(RowContextMenuProperty, value); }
+
+ public static readonly DependencyProperty ShowSelectAllProperty = DependencyProperty.Register(nameof(ShowSelectAll), typeof(bool), typeof(DataGridEx), new PropertyMetadata(true));
+ public static readonly DependencyProperty ToggleSelectionOnRowHeaderClickProperty = DependencyProperty.Register(nameof(ToggleSelectionOnRowHeaderClick), typeof(bool), typeof(DataGridEx), new PropertyMetadata(true));
+ public static readonly DependencyProperty SelectRowOnRightClickProperty = DependencyProperty.Register(nameof(SelectRowOnRightClick), typeof(bool), typeof(DataGridEx), new PropertyMetadata(false));
+ public static readonly DependencyProperty PauseImageLoadingWhileDraggingProperty = DependencyProperty.Register(nameof(PauseImageLoadingWhileDragging), typeof(bool), typeof(DataGridEx), new PropertyMetadata(true, ImageOptionsChanged));
+ public static readonly DependencyProperty EnableImageCacheProperty = DependencyProperty.Register(nameof(EnableImageCache), typeof(bool), typeof(DataGridEx), new PropertyMetadata(true, ImageOptionsChanged));
+ private static readonly DependencyPropertyKey IsImageLoadingSuspendedPropertyKey = DependencyProperty.RegisterReadOnly(nameof(IsImageLoadingSuspended), typeof(bool), typeof(DataGridEx), new PropertyMetadata(false));
+ public static readonly DependencyProperty IsImageLoadingSuspendedProperty = IsImageLoadingSuspendedPropertyKey.DependencyProperty;
+
+ public bool ShowSelectAll { get => (bool)GetValue(ShowSelectAllProperty); set => SetValue(ShowSelectAllProperty, value); }
+ public bool ToggleSelectionOnRowHeaderClick { get => (bool)GetValue(ToggleSelectionOnRowHeaderClickProperty); set => SetValue(ToggleSelectionOnRowHeaderClickProperty, value); }
+ public bool SelectRowOnRightClick { get => (bool)GetValue(SelectRowOnRightClickProperty); set => SetValue(SelectRowOnRightClickProperty, value); }
+ /// 默认开启;拖动时暂停并清空图片,停止滚动 500ms 后恢复。
+ public bool PauseImageLoadingWhileDragging { get => (bool)GetValue(PauseImageLoadingWhileDraggingProperty); set => SetValue(PauseImageLoadingWhileDraggingProperty, value); }
+ /// 默认开启图片缓存;设为 false 时不读取或写入缓存。
+ public bool EnableImageCache { get => (bool)GetValue(EnableImageCacheProperty); set => SetValue(EnableImageCacheProperty, value); }
+ public bool IsImageLoadingSuspended => (bool)GetValue(IsImageLoadingSuspendedProperty);
+
+ private static void ImageOptionsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ => ((DataGridEx)d).ApplyImageLoadingOptions();
+
+ protected override void OnPreviewMouseRightButtonDown(MouseButtonEventArgs e)
+ {
+ if (SelectRowOnRightClick && !e.Handled && SelectionUnit != DataGridSelectionUnit.Cell && e.OriginalSource is DependencyObject source)
+ {
+ var row = FindRow(source);
+ if (row != null && ItemsControl.ItemsControlFromItemContainer(row) == this && !row.IsSelected &&
+ row.Item != System.Windows.Data.CollectionView.NewItemPlaceholder)
+ {
+ UnselectAll(); row.SetCurrentValue(DataGridRow.IsSelectedProperty, true);
+ }
+ }
+ base.OnPreviewMouseRightButtonDown(e);
+ }
+}
diff --git a/Source/Store.Management.Framework/Controls/DataGridEx/DataGridEx.Workbench.cs b/Source/Store.Management.Framework/Controls/DataGridEx/DataGridEx.Workbench.cs
new file mode 100644
index 0000000..1a38114
--- /dev/null
+++ b/Source/Store.Management.Framework/Controls/DataGridEx/DataGridEx.Workbench.cs
@@ -0,0 +1,207 @@
+using System.Collections;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+using System.Windows.Data;
+using System.Windows.Input;
+
+namespace Store.Management.Framework.Controls;
+
+/// 调用方收到请求后加载对应页,再更新 ItemsSource 和 TotalItemCount。
+public sealed record DataGridPageRequest(int PageIndex, int PageSize);
+
+public partial class DataGridEx
+{
+ public static readonly DependencyProperty IsLoadingProperty = DependencyProperty.Register(nameof(IsLoading), typeof(bool), typeof(DataGridEx), new PropertyMetadata(false));
+ public static readonly DependencyProperty LoadingTextProperty = DependencyProperty.Register(nameof(LoadingText), typeof(string), typeof(DataGridEx), new PropertyMetadata("正在加载数据…"));
+ /// 加载时显示动画并禁用表格内容;由调用方控制请求生命周期。
+ public bool IsLoading { get => (bool)GetValue(IsLoadingProperty); set => SetValue(IsLoadingProperty, value); }
+ public string LoadingText { get => (string)GetValue(LoadingTextProperty); set => SetValue(LoadingTextProperty, value); }
+
+ public static readonly DependencyProperty ShowToolbarProperty = DependencyProperty.Register(nameof(ShowToolbar), typeof(bool), typeof(DataGridEx), new PropertyMetadata(true));
+ public static readonly DependencyProperty ShowColumnSettingsProperty = DependencyProperty.Register(nameof(ShowColumnSettings), typeof(bool), typeof(DataGridEx), new PropertyMetadata(true));
+ public static readonly DependencyProperty ShowPaginationProperty = DependencyProperty.Register(nameof(ShowPagination), typeof(bool), typeof(DataGridEx), new PropertyMetadata(true, HeaderOptionsChanged));
+ public static readonly DependencyProperty ToolbarContentProperty = DependencyProperty.Register(nameof(ToolbarContent), typeof(object), typeof(DataGridEx));
+ public static readonly DependencyProperty PaginationContentProperty = DependencyProperty.Register(nameof(PaginationContent), typeof(object), typeof(DataGridEx));
+ public static readonly DependencyProperty PageSizesProperty = DependencyProperty.Register(nameof(PageSizes), typeof(IEnumerable), typeof(DataGridEx), new PropertyMetadata(new[] { 20, 50, 100 }));
+ public static readonly DependencyProperty PageChangedCommandProperty = DependencyProperty.Register(nameof(PageChangedCommand), typeof(ICommand), typeof(DataGridEx));
+ public static readonly DependencyProperty PageIndexProperty = DependencyProperty.Register(nameof(PageIndex), typeof(int), typeof(DataGridEx), new FrameworkPropertyMetadata(1, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, PageOptionsChanged), value => (int)value > 0);
+ public static readonly DependencyProperty PageSizeProperty = DependencyProperty.Register(nameof(PageSize), typeof(int), typeof(DataGridEx), new FrameworkPropertyMetadata(50, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, PageOptionsChanged), value => (int)value > 0);
+ public static readonly DependencyProperty TotalItemCountProperty = DependencyProperty.Register(nameof(TotalItemCount), typeof(int), typeof(DataGridEx), new PropertyMetadata(0, TotalChanged), value => (int)value >= 0);
+
+ public bool ShowToolbar { get => (bool)GetValue(ShowToolbarProperty); set => SetValue(ShowToolbarProperty, value); }
+ public bool ShowColumnSettings { get => (bool)GetValue(ShowColumnSettingsProperty); set => SetValue(ShowColumnSettingsProperty, value); }
+ public bool ShowPagination { get => (bool)GetValue(ShowPaginationProperty); set => SetValue(ShowPaginationProperty, value); }
+ /// 自定义批量按钮区,使用调用方的 DataContext。
+ public object? ToolbarContent { get => GetValue(ToolbarContentProperty); set => SetValue(ToolbarContentProperty, value); }
+ /// 自定义整个分页栏,继承调用方 DataContext;null 时使用默认分页栏。
+ public object? PaginationContent { get => GetValue(PaginationContentProperty); set => SetValue(PaginationContentProperty, value); }
+ public IEnumerable PageSizes { get => (IEnumerable)GetValue(PageSizesProperty); set => SetValue(PageSizesProperty, value); }
+ public ICommand? PageChangedCommand { get => (ICommand?)GetValue(PageChangedCommandProperty); set => SetValue(PageChangedCommandProperty, value); }
+ public int PageIndex { get => (int)GetValue(PageIndexProperty); set => SetValue(PageIndexProperty, value); }
+ public int PageSize { get => (int)GetValue(PageSizeProperty); set => SetValue(PageSizeProperty, value); }
+ public int TotalItemCount { get => (int)GetValue(TotalItemCountProperty); set => SetValue(TotalItemCountProperty, value); }
+
+ private static readonly DependencyPropertyKey PageCountPropertyKey = DependencyProperty.RegisterReadOnly(nameof(PageCount), typeof(int), typeof(DataGridEx), new PropertyMetadata(1));
+ public static readonly DependencyProperty PageCountProperty = PageCountPropertyKey.DependencyProperty;
+ public int PageCount => (int)GetValue(PageCountProperty);
+ private static readonly DependencyPropertyKey PageSummaryPropertyKey = DependencyProperty.RegisterReadOnly(nameof(PageSummary), typeof(string), typeof(DataGridEx), new PropertyMetadata("1 / 1"));
+ public static readonly DependencyProperty PageSummaryProperty = PageSummaryPropertyKey.DependencyProperty;
+ public string PageSummary => (string)GetValue(PageSummaryProperty);
+ private static readonly DependencyPropertyKey TotalSummaryPropertyKey = DependencyProperty.RegisterReadOnly(nameof(TotalSummary), typeof(string), typeof(DataGridEx), new PropertyMetadata("共 0 条记录"));
+ public static readonly DependencyProperty TotalSummaryProperty = TotalSummaryPropertyKey.DependencyProperty;
+ public string TotalSummary => (string)GetValue(TotalSummaryProperty);
+ private static readonly DependencyPropertyKey SelectionSummaryPropertyKey = DependencyProperty.RegisterReadOnly(nameof(SelectionSummary), typeof(string), typeof(DataGridEx), new PropertyMetadata("选择记录后可批量操作"));
+ public static readonly DependencyProperty SelectionSummaryProperty = SelectionSummaryPropertyKey.DependencyProperty;
+ public string SelectionSummary => (string)GetValue(SelectionSummaryProperty);
+
+ /// 设为 false 的业务列在列设置中固定显示。
+ public static readonly DependencyProperty CanHideColumnProperty = DependencyProperty.RegisterAttached("CanHideColumn", typeof(bool), typeof(DataGridEx), new PropertyMetadata(true, CanHideColumnChanged));
+ public static bool GetCanHideColumn(DependencyObject column) => (bool)column.GetValue(CanHideColumnProperty);
+ public static void SetCanHideColumn(DependencyObject column, bool value) => column.SetValue(CanHideColumnProperty, value);
+ private static void CanHideColumnChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (!(bool)e.NewValue && d is DataGridColumn column)
+ {
+ column.SetCurrentValue(DataGridColumn.VisibilityProperty, Visibility.Visible);
+ }
+ }
+
+ public static readonly DependencyProperty RowNumberProperty = DependencyProperty.RegisterAttached("RowNumber", typeof(long), typeof(DataGridEx), new FrameworkPropertyMetadata(0L, FrameworkPropertyMetadataOptions.Inherits));
+ public static long GetRowNumber(DependencyObject target) => (long)target.GetValue(RowNumberProperty);
+ public static void SetRowNumber(DependencyObject target, long value) => target.SetValue(RowNumberProperty, value);
+
+ public static readonly RoutedUICommand FirstPageCommand = new("首页", nameof(FirstPageCommand), typeof(DataGridEx));
+ public static readonly RoutedUICommand PreviousPageCommand = new("上一页", nameof(PreviousPageCommand), typeof(DataGridEx));
+ public static readonly RoutedUICommand NextPageCommand = new("下一页", nameof(NextPageCommand), typeof(DataGridEx));
+ public static readonly RoutedUICommand LastPageCommand = new("末页", nameof(LastPageCommand), typeof(DataGridEx));
+
+ private readonly RowNumberColumn rowNumberColumn = new();
+ private Button? columnsButton;
+ private bool updatingPage;
+
+ private void InitializeWorkbench()
+ {
+ Columns.Add(rowNumberColumn);
+ UpdateHeaderOptions();
+ CommandBindings.Add(new CommandBinding(FirstPageCommand, (_, e) => { SetCurrentValue(PageIndexProperty, 1); e.Handled = true; }, (_, e) => { e.CanExecute = PageIndex > 1; e.Handled = true; }));
+ CommandBindings.Add(new CommandBinding(PreviousPageCommand, (_, e) => { SetCurrentValue(PageIndexProperty, PageIndex - 1); e.Handled = true; }, (_, e) => { e.CanExecute = PageIndex > 1; e.Handled = true; }));
+ CommandBindings.Add(new CommandBinding(NextPageCommand, (_, e) => { SetCurrentValue(PageIndexProperty, PageIndex + 1); e.Handled = true; }, (_, e) => { e.CanExecute = PageIndex < PageCount; e.Handled = true; }));
+ CommandBindings.Add(new CommandBinding(LastPageCommand, (_, e) => { SetCurrentValue(PageIndexProperty, PageCount); e.Handled = true; }, (_, e) => { e.CanExecute = PageIndex < PageCount; e.Handled = true; }));
+ }
+
+ private void UpdateHeaderOptions()
+ {
+ SetCurrentValue(RowHeaderWidthProperty, ShowCheckBox ? 32d : 0d);
+ rowNumberColumn.Visibility = ShowRowNumber ? Visibility.Visible : Visibility.Collapsed;
+ QueueHeaderUpdate();
+ }
+
+ private static void PageOptionsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ DataGridEx grid = (DataGridEx)d;
+ if (grid.updatingPage)
+ {
+ return;
+ }
+ grid.updatingPage = true;
+ try
+ {
+ grid.SetCurrentValue(PageIndexProperty, e.Property == PageSizeProperty ? 1 : Math.Min(grid.PageIndex, grid.CalculatePageCount()));
+ grid.UpdatePageSummary();
+ }
+ finally
+ {
+ grid.updatingPage = false;
+ }
+ grid.UnselectAll();
+ grid.QueueHeaderUpdate();
+ DataGridPageRequest request = new(grid.PageIndex, grid.PageSize);
+ if (grid.PageChangedCommand?.CanExecute(request) == true)
+ {
+ grid.PageChangedCommand.Execute(request);
+ }
+ }
+
+ private static void TotalChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ DataGridEx grid = (DataGridEx)d;
+ grid.UpdatePageSummary();
+ if (grid.PageIndex > grid.PageCount)
+ {
+ grid.SetCurrentValue(PageIndexProperty, grid.PageCount);
+ }
+ }
+
+ private int CalculatePageCount() => Math.Max(1, (int)(((long)TotalItemCount + PageSize - 1) / PageSize));
+
+ private void UpdatePageSummary()
+ {
+ SetValue(PageCountPropertyKey, CalculatePageCount());
+ SetValue(PageSummaryPropertyKey, $"{PageIndex} / {PageCount}");
+ SetValue(TotalSummaryPropertyKey, $"共 {TotalItemCount:N0} 条记录");
+ CommandManager.InvalidateRequerySuggested();
+ }
+
+ public override void OnApplyTemplate()
+ {
+ if (columnsButton != null)
+ {
+ columnsButton.Click -= OpenColumnSettings;
+ }
+ base.OnApplyTemplate();
+ columnsButton = GetTemplateChild("PART_ColumnsButton") as Button;
+ if (columnsButton != null)
+ {
+ columnsButton.Click += OpenColumnSettings;
+ }
+ }
+
+ private void OpenColumnSettings(object sender, RoutedEventArgs e)
+ {
+ ContextMenu menu = new() { PlacementTarget = columnsButton, Placement = PlacementMode.Bottom, VerticalOffset = 6, Style = (Style)FindResource("DataGridExColumnMenu") };
+ menu.CommandBindings.Add(new CommandBinding(ApplicationCommands.Close, (_, _) => menu.IsOpen = false));
+ foreach (DataGridColumn column in Columns.OrderBy(column => column.DisplayIndex))
+ {
+ if (column == rowNumberColumn)
+ {
+ continue;
+ }
+ bool canHide = GetCanHideColumn(column);
+ MenuItem option = new()
+ {
+ Header = $"{column.Header}{(canHide ? string.Empty : "(固定)")}",
+ IsCheckable = true,
+ IsChecked = column.Visibility == Visibility.Visible,
+ IsEnabled = canHide,
+ StaysOpenOnClick = true
+ };
+ option.Click += (_, _) => column.SetCurrentValue(DataGridColumn.VisibilityProperty, option.IsChecked ? Visibility.Visible : Visibility.Collapsed);
+ menu.Items.Add(option);
+ }
+
+ columnsButton!.ContextMenu = menu;
+ menu.IsOpen = true;
+ }
+
+ private sealed class RowNumberColumn : DataGridColumn
+ {
+ public RowNumberColumn()
+ {
+ Header = "序号";
+ Width = 52;
+ IsReadOnly = true;
+ CanUserSort = false;
+ CanUserReorder = false;
+ }
+
+ protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
+ {
+ TextBlock text = new() { HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Center };
+ text.SetBinding(TextBlock.TextProperty, new Binding { Path = new PropertyPath(RowNumberProperty), RelativeSource = RelativeSource.Self, Mode = BindingMode.OneWay });
+ return text;
+ }
+
+ protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem) => GenerateElement(cell, dataItem);
+ }
+}
diff --git a/Source/Store.Management.Framework/Controls/DataGridEx/DataGridEx.cs b/Source/Store.Management.Framework/Controls/DataGridEx/DataGridEx.cs
new file mode 100644
index 0000000..1b0d863
--- /dev/null
+++ b/Source/Store.Management.Framework/Controls/DataGridEx/DataGridEx.cs
@@ -0,0 +1,330 @@
+using System.Collections;
+using System.Collections.Specialized;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Threading;
+
+namespace Store.Management.Framework.Controls;
+
+/// 带默认样式、行号和行选择勾选框的 DataGrid。
+public partial class DataGridEx : DataGrid
+{
+ public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(
+ nameof(SelectedItems), typeof(IList), typeof(DataGridEx),
+ new FrameworkPropertyMetadata(null, OnSelectedItemsChanged));
+
+ /// 可绑定的选中集合。使用 ObservableCollection 可从 VM 增删选中项;未绑定时返回原生选中集合。
+ public new IList SelectedItems
+ {
+ get => (IList?)GetValue(SelectedItemsProperty) ?? base.SelectedItems;
+ set => SetValue(SelectedItemsProperty, value);
+ }
+
+ /// 批量恢复性能测试前的选择,只发出一次选择变更。
+ internal void RestoreSelection(IEnumerable