增加DataGridEx
This commit is contained in:
1 parent
497e9f1c34
commit
499379e20b
22 files changed
+2681
-5
No files matched your search
@@ -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 实现当前页实时筛选并保留视图排序。
|
||||
|
||||
搜索输入框、匹配规则、后台查询、异步取消和跨页全部选择不属于控件内置能力。
|
||||
@@ -0,0 +1,97 @@
|
||||
# DataGridEx 调用与属性
|
||||
|
||||
## 常用接入
|
||||
|
||||
以下绑定属性由调用方 ViewModel 提供,controls 表示 DataGridEx 所在命名空间。
|
||||
|
||||
~~~xml
|
||||
<controls:DataGridEx x:Name="StoreGrid"
|
||||
IsReadOnly="True"
|
||||
ShowCheckBox="True" ShowRowNumber="True"
|
||||
ShowToolbar="True" ShowColumnSettings="True" ShowPagination="True"
|
||||
ItemsSource="{Binding CurrentItems, Mode=OneWay}"
|
||||
SelectedItems="{Binding SelectedStores, Mode=OneWay}"
|
||||
TotalItemCount="{Binding TotalCount, Mode=OneWay}"
|
||||
PageSize="50"
|
||||
PageChangedCommand="{Binding LoadPageCommand, Mode=OneWay}"
|
||||
IsLoading="{Binding LoadingStatus.IsLoading, Mode=OneWay}"
|
||||
LoadingText="{Binding LoadingStatus.Message, Mode=OneWay}">
|
||||
<controls:DataGridEx.ToolbarContent>
|
||||
<Button Content="批量操作" Command="{Binding BatchCommand, Mode=OneWay}"/>
|
||||
</controls:DataGridEx.ToolbarContent>
|
||||
<controls:DataGridEx.Columns>
|
||||
<DataGridTextColumn Header="店铺名称"
|
||||
Binding="{Binding Name, Mode=OneWay}"
|
||||
controls:DataGridEx.CanHideColumn="False"/>
|
||||
<DataGridTextColumn Header="店铺 ID"
|
||||
Binding="{Binding StoreId, Mode=OneWay}"/>
|
||||
</controls:DataGridEx.Columns>
|
||||
</controls:DataGridEx>
|
||||
~~~
|
||||
|
||||
首次数据由调用方装入,不依赖 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
|
||||
<controls:DataGridEx.PaginationContent>
|
||||
<DockPanel>
|
||||
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
|
||||
<Button Content="上一页"
|
||||
Command="{x:Static controls:DataGridEx.PreviousPageCommand}"
|
||||
CommandTarget="{Binding ElementName=StoreGrid, Mode=OneWay}"/>
|
||||
<TextBlock Margin="12,0" VerticalAlignment="Center"
|
||||
Text="{Binding PageSummary, ElementName=StoreGrid, Mode=OneWay}"/>
|
||||
<Button Content="下一页"
|
||||
Command="{x:Static controls:DataGridEx.NextPageCommand}"
|
||||
CommandTarget="{Binding ElementName=StoreGrid, Mode=OneWay}"/>
|
||||
</StackPanel>
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{Binding TotalSummary, ElementName=StoreGrid, Mode=OneWay}"/>
|
||||
</DockPanel>
|
||||
</controls:DataGridEx.PaginationContent>
|
||||
~~~
|
||||
|
||||
PageIndex/PageSize 如需双向绑定,明确 Mode=TwoWay 和 UpdateSourceTrigger=PropertyChanged。隐藏分页栏不会停用分页状态或改变当前数据。
|
||||
|
||||
## 图片列
|
||||
|
||||
~~~xml
|
||||
<controls:DataGridExImageColumn Header="图片" Binding="{Binding ImageUrl, Mode=OneWay}"/>
|
||||
~~~
|
||||
|
||||
图片列使用 NetworkThumbnail,默认 58×58、Stretch=Uniform;可配置 ElementStyle 和 PlaceholderBinding。可见单元格按需加载,回收时取消旧请求,避免旧图片覆盖新行。拖动暂停和缓存由所属表格配置;它们不是表格整体加载动画的开关。
|
||||
|
||||
## 能力边界
|
||||
|
||||
列显示设置不持久化;没有额外的右侧冻结实现。控件支持 ICollectionView 数据源,但搜索输入与查询逻辑由调用方实现,不承担 HTTP 请求、数据库访问或跨页全部选择。
|
||||
@@ -100,8 +100,7 @@ namespace Prism.Commands
|
||||
return;
|
||||
|
||||
IsExecuting = true;
|
||||
await _executeMethod(token)
|
||||
.ConfigureAwait(false);
|
||||
await _executeMethod(token);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
/// <summary>为 true 时抑制空数据提示;可在业务加载数据期间设置。</summary>
|
||||
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); }
|
||||
/// <summary>默认开启;拖动时暂停并清空图片,停止滚动 500ms 后恢复。</summary>
|
||||
public bool PauseImageLoadingWhileDragging { get => (bool)GetValue(PauseImageLoadingWhileDraggingProperty); set => SetValue(PauseImageLoadingWhileDraggingProperty, value); }
|
||||
/// <summary>默认开启图片缓存;设为 false 时不读取或写入缓存。</summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>调用方收到请求后加载对应页,再更新 ItemsSource 和 TotalItemCount。</summary>
|
||||
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("正在加载数据…"));
|
||||
/// <summary>加载时显示动画并禁用表格内容;由调用方控制请求生命周期。</summary>
|
||||
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); }
|
||||
/// <summary>自定义批量按钮区,使用调用方的 DataContext。</summary>
|
||||
public object? ToolbarContent { get => GetValue(ToolbarContentProperty); set => SetValue(ToolbarContentProperty, value); }
|
||||
/// <summary>自定义整个分页栏,继承调用方 DataContext;null 时使用默认分页栏。</summary>
|
||||
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);
|
||||
|
||||
/// <summary>设为 false 的业务列在列设置中固定显示。</summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>带默认样式、行号和行选择勾选框的 DataGrid。</summary>
|
||||
public partial class DataGridEx : DataGrid
|
||||
{
|
||||
public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(
|
||||
nameof(SelectedItems), typeof(IList), typeof(DataGridEx),
|
||||
new FrameworkPropertyMetadata(null, OnSelectedItemsChanged));
|
||||
|
||||
/// <summary>可绑定的选中集合。使用 ObservableCollection 可从 VM 增删选中项;未绑定时返回原生选中集合。</summary>
|
||||
public new IList SelectedItems
|
||||
{
|
||||
get => (IList?)GetValue(SelectedItemsProperty) ?? base.SelectedItems;
|
||||
set => SetValue(SelectedItemsProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>批量恢复性能测试前的选择,只发出一次选择变更。</summary>
|
||||
internal void RestoreSelection(IEnumerable<object> selected)
|
||||
{
|
||||
var available = Items.Cast<object>().ToHashSet();
|
||||
BeginUpdateSelectedItems();
|
||||
try
|
||||
{
|
||||
base.SelectedItems.Clear();
|
||||
foreach (var item in selected)
|
||||
if (available.Contains(item)) base.SelectedItems.Add(item);
|
||||
}
|
||||
finally { EndUpdateSelectedItems(); }
|
||||
}
|
||||
|
||||
private bool synchronizingSelection;
|
||||
|
||||
private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var grid = (DataGridEx)d;
|
||||
if (e.NewValue is IList { IsReadOnly: true } or IList { IsFixedSize: true })
|
||||
throw new ArgumentException("SelectedItems 必须绑定到可增删的集合,例如 ObservableCollection<T>。");
|
||||
if (e.OldValue is INotifyCollectionChanged oldCollection)
|
||||
CollectionChangedEventManager.RemoveHandler(oldCollection, grid.OnBoundSelectionChanged);
|
||||
if (e.NewValue is INotifyCollectionChanged newCollection)
|
||||
CollectionChangedEventManager.AddHandler(newCollection, grid.OnBoundSelectionChanged);
|
||||
grid.ApplyBoundSelection();
|
||||
}
|
||||
|
||||
private void OnBoundSelectionChanged(object? sender, NotifyCollectionChangedEventArgs e) => ApplyBoundSelection();
|
||||
|
||||
private void ApplyBoundSelection()
|
||||
{
|
||||
if (synchronizingSelection || GetValue(SelectedItemsProperty) is not IList boundItems ||
|
||||
ReferenceEquals(boundItems, base.SelectedItems) || SelectionUnit == DataGridSelectionUnit.Cell) return;
|
||||
var available = Items.Cast<object>().ToHashSet();
|
||||
var desired = boundItems.Cast<object>().Where(item => available.Contains(item) &&
|
||||
item != System.Windows.Data.CollectionView.NewItemPlaceholder).Distinct().ToList();
|
||||
if (SelectionMode == DataGridSelectionMode.Single) desired = desired.Take(1).ToList();
|
||||
synchronizingSelection = true;
|
||||
try
|
||||
{
|
||||
var desiredSet = desired.ToHashSet();
|
||||
var current = base.SelectedItems.Cast<object>().ToHashSet();
|
||||
if (SelectionMode == DataGridSelectionMode.Single)
|
||||
SetCurrentValue(SelectedItemProperty, desired.FirstOrDefault());
|
||||
else if (desired.Count == 0) UnselectAll();
|
||||
else
|
||||
{
|
||||
foreach (var item in current)
|
||||
if (!desiredSet.Contains(item)) base.SelectedItems.Remove(item);
|
||||
foreach (var item in desired)
|
||||
if (!current.Contains(item)) base.SelectedItems.Add(item);
|
||||
}
|
||||
}
|
||||
finally { synchronizingSelection = false; }
|
||||
UpdateSelectAllState();
|
||||
}
|
||||
|
||||
private void PublishSelection()
|
||||
{
|
||||
if (synchronizingSelection || GetValue(SelectedItemsProperty) is not IList boundItems ||
|
||||
ReferenceEquals(boundItems, base.SelectedItems)) return;
|
||||
synchronizingSelection = true;
|
||||
try
|
||||
{
|
||||
var current = base.SelectedItems.Cast<object>().ToArray();
|
||||
if (current.Length == 0) boundItems.Clear();
|
||||
else
|
||||
{
|
||||
var currentSet = current.ToHashSet();
|
||||
for (var i = boundItems.Count - 1; i >= 0; i--)
|
||||
if (!currentSet.Contains(boundItems[i]!)) boundItems.RemoveAt(i);
|
||||
var boundSet = boundItems.Cast<object>().ToHashSet();
|
||||
foreach (var item in current)
|
||||
if (boundSet.Add(item)) boundItems.Add(item);
|
||||
}
|
||||
}
|
||||
finally { synchronizingSelection = false; }
|
||||
}
|
||||
|
||||
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
|
||||
{
|
||||
var wasSynchronizing = synchronizingSelection;
|
||||
synchronizingSelection = true;
|
||||
try { base.OnItemsSourceChanged(oldValue, newValue); }
|
||||
finally { synchronizingSelection = wasSynchronizing; }
|
||||
ApplyBoundSelection();
|
||||
}
|
||||
private static readonly DependencyPropertyKey IsSelectionModeActivePropertyKey = DependencyProperty.RegisterReadOnly(
|
||||
nameof(IsSelectionModeActive), typeof(bool), typeof(DataGridEx), new PropertyMetadata(false));
|
||||
public static readonly DependencyProperty IsSelectionModeActiveProperty = IsSelectionModeActivePropertyKey.DependencyProperty;
|
||||
/// <summary>点击行头勾选框后进入批量选择,取消所有选中后退出。</summary>
|
||||
public bool IsSelectionModeActive => (bool)GetValue(IsSelectionModeActiveProperty);
|
||||
public static readonly RoutedUICommand ToggleSelectAllCommand = new("全选或取消全选", nameof(ToggleSelectAllCommand), typeof(DataGridEx));
|
||||
private static readonly DependencyPropertyKey AreAllRowsSelectedPropertyKey = DependencyProperty.RegisterReadOnly(
|
||||
nameof(AreAllRowsSelected), typeof(bool?), typeof(DataGridEx), new PropertyMetadata(false));
|
||||
public static readonly DependencyProperty AreAllRowsSelectedProperty = AreAllRowsSelectedPropertyKey.DependencyProperty;
|
||||
/// <summary>全部选中为 true,部分选中为 null,未选中为 false。</summary>
|
||||
public bool? AreAllRowsSelected => (bool?)GetValue(AreAllRowsSelectedProperty);
|
||||
public static readonly DependencyProperty SelectedRowBackgroundProperty = DependencyProperty.Register(
|
||||
nameof(SelectedRowBackground), typeof(Brush), typeof(DataGridEx),
|
||||
new PropertyMetadata((Brush)new BrushConverter().ConvertFromString("#F3EAF1")!));
|
||||
public static readonly DependencyProperty SelectedRowForegroundProperty = DependencyProperty.Register(
|
||||
nameof(SelectedRowForeground), typeof(Brush), typeof(DataGridEx), new PropertyMetadata(Brushes.Black));
|
||||
public static readonly DependencyProperty ShowCheckBoxProperty = DependencyProperty.Register(
|
||||
nameof(ShowCheckBox), typeof(bool), typeof(DataGridEx), new PropertyMetadata(false, HeaderOptionsChanged));
|
||||
public static readonly DependencyProperty ShowRowNumberProperty = DependencyProperty.Register(
|
||||
nameof(ShowRowNumber), typeof(bool), typeof(DataGridEx), new PropertyMetadata(false, HeaderOptionsChanged));
|
||||
|
||||
public Brush SelectedRowBackground { get => (Brush)GetValue(SelectedRowBackgroundProperty); set => SetValue(SelectedRowBackgroundProperty, value); }
|
||||
public Brush SelectedRowForeground { get => (Brush)GetValue(SelectedRowForegroundProperty); set => SetValue(SelectedRowForegroundProperty, value); }
|
||||
/// <summary>勾选状态与行 IsSelected 同步,可通过 SelectedItems 获取。</summary>
|
||||
public bool ShowCheckBox { get => (bool)GetValue(ShowCheckBoxProperty); set => SetValue(ShowCheckBoxProperty, value); }
|
||||
public bool ShowRowNumber { get => (bool)GetValue(ShowRowNumberProperty); set => SetValue(ShowRowNumberProperty, value); }
|
||||
|
||||
static DataGridEx()
|
||||
{
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(DataGridEx), new FrameworkPropertyMetadata(typeof(DataGridEx)));
|
||||
}
|
||||
|
||||
public DataGridEx()
|
||||
{
|
||||
InitializeImageLoadingPause();
|
||||
InitializeWorkbench();
|
||||
CommandBindings.Add(new CommandBinding(ToggleSelectAllCommand, (_, e) =>
|
||||
{
|
||||
if (AreAllRowsSelected == true) UnselectAll();
|
||||
else
|
||||
{
|
||||
SelectAll();
|
||||
SetValue(IsSelectionModeActivePropertyKey, base.SelectedItems.Count > 0);
|
||||
}
|
||||
e.Handled = true;
|
||||
}, (_, e) =>
|
||||
{
|
||||
e.CanExecute = SelectionMode == DataGridSelectionMode.Extended &&
|
||||
SelectionUnit != DataGridSelectionUnit.Cell && SelectableItemCount > 0;
|
||||
e.Handled = true;
|
||||
}));
|
||||
AddHandler(ButtonBase.ClickEvent, new RoutedEventHandler(OnRowCheckBoxClick), true);
|
||||
LoadingRow += (_, e) => UpdateRowNumber(e.Row);
|
||||
ItemContainerGenerator.ItemsChanged += (_, _) => QueueHeaderUpdate();
|
||||
Loaded += (_, _) => { ApplyBoundSelection(); QueueHeaderUpdate(); };
|
||||
}
|
||||
|
||||
private void OnRowCheckBoxClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (e.OriginalSource is not CheckBox checkBox) return;
|
||||
DependencyObject? parent = checkBox;
|
||||
while (parent != null && parent != this)
|
||||
{
|
||||
if (parent is DataGridRowHeader header &&
|
||||
ItemsControl.ItemsControlFromItemContainer(FindRow(header)) == this)
|
||||
{
|
||||
SetValue(IsSelectionModeActivePropertyKey, base.SelectedItems.Count > 0);
|
||||
return;
|
||||
}
|
||||
parent = VisualTreeHelper.GetParent(parent);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
|
||||
{
|
||||
if (!e.Handled && SelectionUnit == DataGridSelectionUnit.CellOrRowHeader)
|
||||
{
|
||||
for (DependencyObject? source = e.OriginalSource as DependencyObject; source != null && source != this;
|
||||
source = source is FrameworkContentElement content ? content.Parent : VisualTreeHelper.GetParent(source))
|
||||
{
|
||||
if (source is DataGrid)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (source is Border border && FindRow(border) is DataGridRow blankRow &&
|
||||
ItemsControl.ItemsControlFromItemContainer(blankRow) == this &&
|
||||
ReferenceEquals(blankRow.Template.FindName("border", blankRow), border))
|
||||
{
|
||||
SelectRowThroughHeader(blankRow, e);
|
||||
break;
|
||||
} if (source is DataGridCell cell)
|
||||
{
|
||||
if (cell.Column == rowNumberColumn && FindRow(cell) is DataGridRow row)
|
||||
{
|
||||
SelectRowThroughHeader(row, e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!e.Handled && ShowCheckBox && ToggleSelectionOnRowHeaderClick && SelectionUnit != DataGridSelectionUnit.Cell)
|
||||
{
|
||||
for (var source = e.OriginalSource as DependencyObject; source != null && source != this;
|
||||
source = source is FrameworkContentElement content ? content.Parent : VisualTreeHelper.GetParent(source))
|
||||
{
|
||||
// 保留行高拖动和嵌套表格自己的鼠标处理。
|
||||
if (source is Thumb || source is DataGrid) break;
|
||||
if (source is not DataGridRowHeader header) continue;
|
||||
var row = FindRow(header);
|
||||
if (row == null || ItemsControl.ItemsControlFromItemContainer(row) != this ||
|
||||
row.Item == System.Windows.Data.CollectionView.NewItemPlaceholder) break;
|
||||
|
||||
// 在 DataGridRowHeader 默认单选逻辑之前处理整个行头,
|
||||
// 包括勾选框四周的留白;阻止 CheckBox 再次切换造成双重反转。
|
||||
e.Handled = true;
|
||||
row.SetCurrentValue(DataGridRow.IsSelectedProperty, !row.IsSelected);
|
||||
SetValue(IsSelectionModeActivePropertyKey, base.SelectedItems.Count > 0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
base.OnPreviewMouseLeftButtonDown(e);
|
||||
}
|
||||
|
||||
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
|
||||
{
|
||||
if (!e.Handled && SelectionUnit != DataGridSelectionUnit.Cell &&
|
||||
e.OriginalSource is Border border &&
|
||||
FindRow(border) is DataGridRow row &&
|
||||
ItemsControl.ItemsControlFromItemContainer(row) == this &&
|
||||
ReferenceEquals(row.Template.FindName("border", row), border))
|
||||
{
|
||||
SelectRowThroughHeader(row, e);
|
||||
}
|
||||
base.OnMouseLeftButtonDown(e);
|
||||
}
|
||||
|
||||
private static void SelectRowThroughHeader(DataGridRow row, MouseButtonEventArgs e)
|
||||
{
|
||||
// 序号和行尾留白沿用原生行头逻辑,保留 Ctrl/Shift 与选择锚点。
|
||||
if (row.Template.FindName("PART_RowHeader", row) is DataGridRowHeader header)
|
||||
{
|
||||
header.RaiseEvent(new MouseButtonEventArgs(e.MouseDevice, e.Timestamp, e.ChangedButton, e.StylusDevice)
|
||||
{
|
||||
RoutedEvent = Mouse.MouseDownEvent
|
||||
});
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
private static DataGridRow? FindRow(DependencyObject element)
|
||||
{
|
||||
for (DependencyObject? parent = element; parent != null; parent = parent is FrameworkContentElement content ? content.Parent : VisualTreeHelper.GetParent(parent))
|
||||
if (parent is DataGridRow row) return row;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void HeaderOptionsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
=> ((DataGridEx)d).UpdateHeaderOptions();
|
||||
|
||||
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
base.OnItemsChanged(e);
|
||||
QueueHeaderUpdate();
|
||||
UpdateSelectAllState();
|
||||
}
|
||||
|
||||
private int SelectableItemCount => Items.Count - (Items.Contains(System.Windows.Data.CollectionView.NewItemPlaceholder) ? 1 : 0);
|
||||
|
||||
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
|
||||
{
|
||||
PublishSelection();
|
||||
base.OnSelectionChanged(e);
|
||||
UpdateSelectAllState();
|
||||
}
|
||||
|
||||
private void UpdateSelectAllState()
|
||||
{
|
||||
SetValue(AreAllRowsSelectedPropertyKey, base.SelectedItems.Count == 0 ? false :
|
||||
base.SelectedItems.Count == SelectableItemCount ? true : (bool?)null);
|
||||
if (base.SelectedItems.Count == 0) SetValue(IsSelectionModeActivePropertyKey, false);
|
||||
SetValue(SelectionSummaryPropertyKey, base.SelectedItems.Count == 0 ? "选择记录后可批量操作" : $"已选择 {base.SelectedItems.Count} 条记录");
|
||||
CommandManager.InvalidateRequerySuggested();
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
base.OnPropertyChanged(e);
|
||||
if (e.Property == FontSizeProperty || e.Property == FontFamilyProperty ||
|
||||
e.Property == FontStyleProperty || e.Property == FontWeightProperty || e.Property == FontStretchProperty)
|
||||
QueueHeaderUpdate();
|
||||
}
|
||||
|
||||
private bool headerUpdatePending;
|
||||
private void QueueHeaderUpdate()
|
||||
{
|
||||
if (headerUpdatePending) return;
|
||||
headerUpdatePending = true;
|
||||
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
|
||||
{
|
||||
headerUpdatePending = false;
|
||||
foreach (var row in GetRealizedRows(this)) UpdateRowNumber(row);
|
||||
}));
|
||||
}
|
||||
|
||||
private void UpdateRowNumber(DataGridRow row)
|
||||
=> row.SetValue(RowNumberProperty, (PageIndex - 1L) * PageSize + row.GetIndex() + 1);
|
||||
|
||||
private IEnumerable<DataGridRow> GetRealizedRows(DependencyObject parent)
|
||||
{
|
||||
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(parent, i);
|
||||
if (child is DataGridRow row && ItemsControl.ItemsControlFromItemContainer(row) == this) yield return row;
|
||||
else if (child is not DataGrid)
|
||||
foreach (var descendant in GetRealizedRows(child)) yield return descendant;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,875 @@
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:Store.Management.Framework.Controls"
|
||||
xmlns:ext="clr-namespace:Store.Management.Framework.Controls"
|
||||
xmlns:f="clr-namespace:Store.Management.Framework.Controls.Attach;assembly=Store.Management.Framework"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib">
|
||||
|
||||
|
||||
<sys:Double x:Key="DataGridExFontSize">12</sys:Double>
|
||||
<FontFamily x:Key="DataGridExFontFamily">Microsoft YaHei UI, Segoe UI</FontFamily>
|
||||
<BooleanToVisibilityConverter x:Key="DataGridExVisibility"/>
|
||||
<Style x:Key="DataGridExCheckBoxStyle" TargetType="CheckBox" BasedOn="{StaticResource {x:Type CheckBox}}"/>
|
||||
<Style x:Key="DataGridExDefaultCellHeaderStyle" TargetType="DataGridColumnHeader">
|
||||
<Setter Property="UseLayoutRounding" Value="True" />
|
||||
<!--<Setter Property="f:DataGridHelper.ShowBorder" Value="True" />-->
|
||||
|
||||
<Setter Property="Height" Value="{Binding ColumnHeaderHeight, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}" />
|
||||
<Setter Property="Background" Value="#eeeeee" />
|
||||
<Setter Property="BorderThickness" Value="0,0,0,1" />
|
||||
<!-- 只保留底边框,竖线不再用 Border 画 -->
|
||||
<Setter Property="HorizontalContentAlignment" Value="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=HorizontalContentAlignment}" />
|
||||
<Setter Property="Padding" Value="8,0" />
|
||||
<Setter Property="FontSize" Value="{StaticResource DataGridExFontSize}" />
|
||||
<Setter Property="FontFamily" Value="{StaticResource DataGridExFontFamily}" />
|
||||
<Setter Property="BorderBrush" Value="{Binding HorizontalGridLinesBrush, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="DataGridColumnHeader">
|
||||
<Grid>
|
||||
<Border
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
UseLayoutRounding="True">
|
||||
<Grid HorizontalAlignment="Stretch" VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ContentPresenter
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
RecognizesAccessKey="True"
|
||||
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
|
||||
<Grid
|
||||
x:Name="SortArrowGrid"
|
||||
Grid.Column="1"
|
||||
Width="7"
|
||||
Height="15"
|
||||
Margin="10,0,0,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="Collapsed">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<Path
|
||||
x:Name="SortArrowUp"
|
||||
Margin="0,0,0,2"
|
||||
Data="M500 365.76l-244.78 279.7a16 16 0 0 0 12 26.54h489.52a16 16 0 0 0 12-26.54L524 365.76a16 16 0 0 0-24 0z"
|
||||
Fill="#D2D2D2"
|
||||
Stretch="Fill" />
|
||||
<Path
|
||||
x:Name="SortArrowDown"
|
||||
Grid.Row="1"
|
||||
Margin="0,2,0,0"
|
||||
Data="M524 658.24l244.74-279.7a16 16 0 0 0-12-26.54H267.26a16 16 0 0 0-12 26.54L500 658.24a16 16 0 0 0 24 0z"
|
||||
Fill="#D2D2D2"
|
||||
Stretch="Fill" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 关键改动:用固定宽度的 Rectangle 画右侧竖线,永不消失 -->
|
||||
<Rectangle
|
||||
Width="1"
|
||||
HorizontalAlignment="Right"
|
||||
Fill="{TemplateBinding BorderBrush}"
|
||||
SnapsToDevicePixels="False" />
|
||||
|
||||
<Thumb
|
||||
x:Name="PART_LeftHeaderGripper"
|
||||
Width="6"
|
||||
HorizontalAlignment="Left"
|
||||
Background="Transparent"
|
||||
Cursor="SizeWE"
|
||||
FocusVisualStyle="{x:Null}">
|
||||
<Thumb.Style>
|
||||
<Style TargetType="Thumb">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Thumb">
|
||||
<Border Padding="{TemplateBinding Padding}" Background="{TemplateBinding Background}" />
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</Thumb.Style>
|
||||
</Thumb>
|
||||
<Thumb
|
||||
x:Name="PART_RightHeaderGripper"
|
||||
Width="6"
|
||||
HorizontalAlignment="Right"
|
||||
Background="Transparent"
|
||||
Cursor="SizeWE"
|
||||
FocusVisualStyle="{x:Null}">
|
||||
<Thumb.Style>
|
||||
<Style TargetType="Thumb">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Thumb">
|
||||
<Border Padding="{TemplateBinding Padding}" Background="{TemplateBinding Background}" />
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</Thumb.Style>
|
||||
</Thumb>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="SortDirection" Value="Ascending">
|
||||
<Setter TargetName="SortArrowGrid" Property="Visibility" Value="Visible" />
|
||||
<Setter TargetName="SortArrowUp" Property="Fill" Value="#61646D" />
|
||||
<Setter TargetName="SortArrowDown" Property="Fill" Value="#E1E4E8" />
|
||||
</Trigger>
|
||||
<Trigger Property="SortDirection" Value="Descending">
|
||||
<Setter TargetName="SortArrowGrid" Property="Visibility" Value="Visible" />
|
||||
<Setter TargetName="SortArrowUp" Property="Fill" Value="#E1E4E8" />
|
||||
<Setter TargetName="SortArrowDown" Property="Fill" Value="#61646D" />
|
||||
</Trigger>
|
||||
<Trigger Property="SortDirection" Value="{x:Null}">
|
||||
<Setter TargetName="SortArrowGrid" Property="Visibility" Value="Collapsed" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding GridLinesVisibility, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}" Value="None">
|
||||
<Setter Property="Padding" Value="0" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="DataGridExDefaultCellStyle" TargetType="DataGridCell">
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Padding" Value="8,0" />
|
||||
<Setter Property="FontSize" Value="{StaticResource DataGridExFontSize}" />
|
||||
<Setter Property="FontFamily" Value="{StaticResource DataGridExFontFamily}" />
|
||||
<!--<Setter Property="Foreground" Value="{Binding RelativeSource={RelativeSource AncestorType=DataGrid, Mode=FindAncestor}, Path=Foreground, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />-->
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type DataGridCell}">
|
||||
<Grid>
|
||||
<Border
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}">
|
||||
<ContentPresenter
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
|
||||
</Border>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="DataGridExDefaultRowHeaderStyle" TargetType="{x:Type DataGridRowHeader}">
|
||||
<Setter Property="BorderBrush" Value="{Binding BorderBrush, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}" />
|
||||
<Setter Property="BorderThickness" Value="0,0,1,1" />
|
||||
<Setter Property="FontSize" Value="{StaticResource DataGridExFontSize}" />
|
||||
<Setter Property="FontFamily" Value="{StaticResource DataGridExFontFamily}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type DataGridRowHeader}">
|
||||
<Border
|
||||
Background="Transparent"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}">
|
||||
<ContentPresenter
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
TextElement.FontSize="10" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}}}" Value="True">
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
</DataTrigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="DataGridExDefaultRowStyle" TargetType="{x:Type DataGridRow}">
|
||||
<Setter Property="BorderThickness" Value="0,0,1,0" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="FontSize" Value="{StaticResource DataGridExFontSize}" />
|
||||
<Setter Property="FontFamily" Value="{StaticResource DataGridExFontFamily}" />
|
||||
<Setter Property="BorderBrush" Value="{Binding RelativeSource={RelativeSource AncestorType=DataGrid, Mode=FindAncestor}, Path=BorderBrush}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="DataGridRow">
|
||||
<Grid>
|
||||
<Border
|
||||
x:Name="border"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}" />
|
||||
<SelectiveScrollingGrid>
|
||||
<SelectiveScrollingGrid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</SelectiveScrollingGrid.ColumnDefinitions>
|
||||
<SelectiveScrollingGrid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</SelectiveScrollingGrid.RowDefinitions>
|
||||
<DataGridCellsPresenter
|
||||
Grid.Column="1"
|
||||
ItemsPanel="{TemplateBinding ItemsPanel}"
|
||||
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
|
||||
<DataGridDetailsPresenter
|
||||
x:Name="DetailsPresenter"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
SelectiveScrollingGrid.SelectiveScrollingOrientation="{Binding AreRowDetailsFrozen, ConverterParameter={x:Static SelectiveScrollingOrientation.Vertical}, Converter={x:Static DataGrid.RowDetailsScrollingConverter}, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"
|
||||
Visibility="{TemplateBinding DetailsVisibility}" />
|
||||
<DataGridRowHeader x:Name="PART_RowHeader"
|
||||
Grid.RowSpan="2"
|
||||
SelectiveScrollingGrid.SelectiveScrollingOrientation="Vertical"
|
||||
Visibility="{Binding HeadersVisibility, ConverterParameter={x:Static DataGridHeadersVisibility.Row}, Converter={x:Static DataGrid.HeadersVisibilityConverter}, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}" />
|
||||
</SelectiveScrollingGrid>
|
||||
<Grid
|
||||
x:Name="fousIcon"
|
||||
IsHitTestVisible="False"
|
||||
Opacity="0"
|
||||
Visibility="Collapsed">
|
||||
<Path
|
||||
Width="10"
|
||||
Height="10"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
Data="M0,0 L0,0 50,0 50,10 10,10 10,50 0,50 0,52 z"
|
||||
Fill="{DynamicResource PrimaryBrush}"
|
||||
RenderTransformOrigin="0.5,0.5"
|
||||
Stretch="Fill">
|
||||
<Path.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform />
|
||||
<SkewTransform />
|
||||
<RotateTransform Angle="0" />
|
||||
<TranslateTransform />
|
||||
</TransformGroup>
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
<Path
|
||||
Width="10"
|
||||
Height="10"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Data="M0,0 L0,0 50,0 50,10 10,10 10,50 0,50 0,52 z"
|
||||
Fill="{DynamicResource PrimaryBrush}"
|
||||
RenderTransformOrigin="0.5,0.5"
|
||||
Stretch="Fill">
|
||||
<Path.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform />
|
||||
<SkewTransform />
|
||||
<RotateTransform Angle="90" />
|
||||
<TranslateTransform />
|
||||
</TransformGroup>
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
<Path
|
||||
Width="10"
|
||||
Height="10"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Data="M0,0 L0,0 50,0 50,10 10,10 10,50 0,50 0,52 z"
|
||||
Fill="{DynamicResource PrimaryBrush}"
|
||||
RenderTransformOrigin="0.5,0.5"
|
||||
Stretch="Fill">
|
||||
<Path.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform />
|
||||
<SkewTransform />
|
||||
<RotateTransform Angle="180" />
|
||||
<TranslateTransform />
|
||||
</TransformGroup>
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
<Path
|
||||
Width="10"
|
||||
Height="10"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Bottom"
|
||||
Data="M0,0 L0,0 50,0 50,10 10,10 10,50 0,50 0,52 z"
|
||||
Fill="{DynamicResource PrimaryBrush}"
|
||||
RenderTransformOrigin="0.5,0.5"
|
||||
Stretch="Fill">
|
||||
<Path.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform />
|
||||
<SkewTransform />
|
||||
<RotateTransform Angle="-90" />
|
||||
<TranslateTransform />
|
||||
</TransformGroup>
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="fousIcon" Property="Opacity" Value="1" />
|
||||
<!-- 整行变色 -->
|
||||
<Setter TargetName="border" Property="Background" Value="#4BB2FF" />
|
||||
</Trigger>
|
||||
<Trigger Property="DetailsVisibility" Value="Visible">
|
||||
<Setter TargetName="DetailsPresenter" Property="Margin" Value="0,0,1,0" />
|
||||
</Trigger>
|
||||
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="DataGridExDefaultNumberCellHeaderStyle"
|
||||
BasedOn="{StaticResource DataGridExDefaultCellHeaderStyle}"
|
||||
TargetType="DataGridColumnHeader">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Right" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="DataGridExDefaultNumberCellStyle"
|
||||
BasedOn="{StaticResource DataGridExDefaultCellStyle}"
|
||||
TargetType="DataGridCell">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Right" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="DataGridExDefaultDataGridStyle" TargetType="local:DataGridEx">
|
||||
<Style.Resources>
|
||||
<Style x:Key="DataGridExColumnMenu" TargetType="ContextMenu">
|
||||
<Setter Property="Width" Value="240"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="Foreground" Value="#343A40"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ContextMenu">
|
||||
<Border Background="White" BorderBrush="#DDE0E5" BorderThickness="1" CornerRadius="6" Padding="0">
|
||||
<StackPanel KeyboardNavigation.DirectionalNavigation="Cycle">
|
||||
<StackPanel Margin="14,12,14,8">
|
||||
<TextBlock Text="显示的列" FontSize="14" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="勾选后立即生效" Foreground="#858A94" Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
<ScrollViewer MaxHeight="340" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" Margin="6,0,6,8">
|
||||
<ItemsPresenter KeyboardNavigation.DirectionalNavigation="Cycle"/>
|
||||
</ScrollViewer>
|
||||
<Border BorderBrush="#ECEEF1" BorderThickness="0,1,0,0" Padding="12,9" Background="#FAFAFB">
|
||||
<Button x:Name="PART_DoneButton" Content="完成" HorizontalAlignment="Right" Width="64" Height="28"
|
||||
Command="{x:Static ApplicationCommands.Close}"
|
||||
Foreground="White" Background="#98638F" BorderThickness="0">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="ButtonBorder" Background="{TemplateBinding Background}" CornerRadius="4">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True"><Setter TargetName="ButtonBorder" Property="Opacity" Value="0.85"/></Trigger>
|
||||
<Trigger Property="IsKeyboardFocused" Value="True"><Setter TargetName="ButtonBorder" Property="BorderBrush" Value="#343A40"/><Setter TargetName="ButtonBorder" Property="BorderThickness" Value="1"/></Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="ItemContainerStyle">
|
||||
<Setter.Value>
|
||||
<Style TargetType="MenuItem">
|
||||
<Setter Property="Height" Value="30"/>
|
||||
<Setter Property="Padding" Value="8,0"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="MenuItem">
|
||||
<Border x:Name="ItemBorder" Background="Transparent" CornerRadius="4" Padding="{TemplateBinding Padding}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions><ColumnDefinition Width="24"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
|
||||
<Border x:Name="CheckBorder" Width="14" Height="14" HorizontalAlignment="Left" VerticalAlignment="Center"
|
||||
BorderBrush="#C8CDD4" BorderThickness="1" CornerRadius="3" Background="White">
|
||||
<Path x:Name="CheckMark" Data="M 2,6 L 5,9 L 11,3" Stroke="White" StrokeThickness="1.5" Visibility="Collapsed"/>
|
||||
</Border>
|
||||
<ContentPresenter Grid.Column="1" ContentSource="Header" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsHighlighted" Value="True"><Setter TargetName="ItemBorder" Property="Background" Value="#F3F4F6"/></Trigger>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="CheckBorder" Property="Background" Value="#98638F"/>
|
||||
<Setter TargetName="CheckBorder" Property="BorderBrush" Value="#98638F"/>
|
||||
<Setter TargetName="CheckMark" Property="Visibility" Value="Visible"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Foreground" Value="#8A8F98"/>
|
||||
<Setter TargetName="CheckBorder" Property="Background" Value="#D5D8DE"/>
|
||||
<Setter TargetName="CheckBorder" Property="BorderBrush" Value="#D5D8DE"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</Style.Resources>
|
||||
<Setter Property="UseLayoutRounding" Value="True" />
|
||||
<Setter Property="SnapsToDevicePixels" Value="True" />
|
||||
<Setter Property="Background" Value="White" />
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
|
||||
<Setter Property="ColumnHeaderHeight" Value="29" />
|
||||
<Setter Property="MinRowHeight" Value="28" />
|
||||
<Setter Property="CanUserAddRows" Value="False" />
|
||||
<Setter Property="AutoGenerateColumns" Value="False" />
|
||||
<Setter Property="CanUserSortColumns" Value="True" />
|
||||
<Setter Property="CanUserReorderColumns" Value="False" />
|
||||
<Setter Property="AreRowDetailsFrozen" Value="False" />
|
||||
<Setter Property="HeadersVisibility" Value="All" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Foreground" Value="#343A40" />
|
||||
<Setter Property="BorderBrush" Value="#E1E4E8" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="VerticalGridLinesBrush" Value="#E1E4E8" />
|
||||
<Setter Property="HorizontalGridLinesBrush" Value="#E1E4E8" />
|
||||
<Setter Property="RowDetailsVisibilityMode" Value="VisibleWhenSelected" />
|
||||
<Setter Property="ScrollViewer.CanContentScroll" Value="true" />
|
||||
<Setter Property="ScrollViewer.PanningMode" Value="Both" />
|
||||
<Setter Property="SelectionMode" Value="Single" />
|
||||
<Setter Property="FontSize" Value="{StaticResource DataGridExFontSize}" />
|
||||
<Setter Property="FontFamily" Value="{StaticResource DataGridExFontFamily}" />
|
||||
<Setter Property="RowHeaderStyle" Value="{StaticResource DataGridExDefaultRowHeaderStyle}" />
|
||||
<Setter Property="RowStyle" Value="{StaticResource DataGridExDefaultRowStyle}" />
|
||||
<Setter Property="ColumnHeaderStyle" Value="{StaticResource DataGridExDefaultCellHeaderStyle}" />
|
||||
<Setter Property="CellStyle" Value="{StaticResource DataGridExDefaultCellStyle}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type local:DataGridEx}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
|
||||
<!-- 调用方可以提供批量操作内容,默认展示选择提示和列设置。 -->
|
||||
<Border x:Name="PART_ToolbarArea" Grid.Row="0" Background="#FAFAFA" BorderBrush="#E1E4E8" BorderThickness="1,1,1,0" Padding="8,5"
|
||||
Visibility="{Binding ShowToolbar, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay, Converter={StaticResource DataGridExVisibility}}">
|
||||
<DockPanel LastChildFill="True">
|
||||
<Button x:Name="PART_ColumnsButton" DockPanel.Dock="Right" Content="列设置" Padding="10,3" MinHeight="26"
|
||||
Visibility="{Binding ShowColumnSettings, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay, Converter={StaticResource DataGridExVisibility}}"/>
|
||||
<TextBlock DockPanel.Dock="Left" VerticalAlignment="Center" Margin="0,0,12,0"
|
||||
Text="{Binding SelectionSummary, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"/>
|
||||
<ContentPresenter Content="{TemplateBinding ToolbarContent}" VerticalAlignment="Center"/>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
<!-- 原生 DataGrid 滚动与虚拟化宿主。 -->
|
||||
<Border x:Name="PART_TableArea" Grid.Row="1"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
SnapsToDevicePixels="True"
|
||||
UseLayoutRounding="True">
|
||||
<ScrollViewer x:Name="DG_ScrollViewer" Focusable="false">
|
||||
<ScrollViewer.Template>
|
||||
<ControlTemplate TargetType="{x:Type ScrollViewer}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Border
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Width="{Binding CellsPanelHorizontalOffset, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"
|
||||
Background="#eeeeee"
|
||||
BorderBrush="{Binding BorderBrush, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"
|
||||
BorderThickness="0,0,1,1">
|
||||
<CheckBox
|
||||
Width="16"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalContentAlignment="Center"
|
||||
AutomationProperties.Name="全选行"
|
||||
Command="{x:Static ext:DataGridEx.ToggleSelectAllCommand}"
|
||||
CommandTarget="{Binding RelativeSource={RelativeSource AncestorType={x:Type ext:DataGridEx}}}"
|
||||
IsChecked="{Binding AreAllRowsSelected, RelativeSource={RelativeSource AncestorType={x:Type ext:DataGridEx}}, Mode=OneWay}">
|
||||
<CheckBox.Style>
|
||||
<Style TargetType="CheckBox" BasedOn="{StaticResource DataGridExCheckBoxStyle}">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<MultiDataTrigger>
|
||||
<MultiDataTrigger.Conditions>
|
||||
<Condition Binding="{Binding ShowCheckBox, RelativeSource={RelativeSource AncestorType={x:Type ext:DataGridEx}}}" Value="True" />
|
||||
<Condition Binding="{Binding ShowSelectAll, RelativeSource={RelativeSource AncestorType={x:Type ext:DataGridEx}}}" Value="True" />
|
||||
</MultiDataTrigger.Conditions>
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</MultiDataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</CheckBox.Style>
|
||||
</CheckBox>
|
||||
</Border>
|
||||
|
||||
<DataGridColumnHeadersPresenter
|
||||
x:Name="PART_ColumnHeadersPresenter"
|
||||
Grid.Column="1"
|
||||
Grid.ColumnSpan="2"
|
||||
Visibility="{Binding HeadersVisibility, ConverterParameter={x:Static DataGridHeadersVisibility.Column}, Converter={x:Static DataGrid.HeadersVisibilityConverter}, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}" />
|
||||
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2">
|
||||
<ScrollContentPresenter x:Name="PART_ScrollContentPresenter" CanContentScroll="{TemplateBinding CanContentScroll}" />
|
||||
<Grid
|
||||
x:Name="NoDataTipsGrid"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
IsHitTestVisible="False"
|
||||
Visibility="Collapsed">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="60" />
|
||||
<RowDefinition Height="auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Path
|
||||
Width="100"
|
||||
Height="60"
|
||||
Data="M156.662278 699.758173h21.097186A10.444152 10.444152 0 0 1 187.994733 710.202325c0 5.765172-4.490985 10.444152-10.235269 10.444152H156.662278v21.097186A10.444152 10.444152 0 0 1 146.218126 751.978932a10.277045 10.277045 0 0 1-10.444152-10.235269V720.646477H114.676787A10.444152 10.444152 0 0 1 104.441518 710.202325c0-5.765172 4.490985-10.444152 10.235269-10.444152H135.773974v-21.097187A10.444152 10.444152 0 0 1 146.218126 668.425717c5.765172 0 10.444152 4.490985 10.444152 10.235269v21.097187z m1378.628042-83.553215v-21.097186A10.277045 10.277045 0 0 0 1524.846168 584.872503a10.444152 10.444152 0 0 0-10.444152 10.235269v21.097186h-21.097186a10.277045 10.277045 0 0 0-10.235269 10.444152c0 5.598065 4.595427 10.444152 10.235269 10.444152h21.097186v21.097187c0 5.744284 4.67898 10.235269 10.444152 10.235268a10.444152 10.444152 0 0 0 10.444152-10.235268V637.093262h21.097187c5.744284 0 10.235269-4.67898 10.235268-10.444152a10.444152 10.444152 0 0 0-10.235268-10.444152H1535.29032zM776.460024 960.861969H250.596979A20.80475 20.80475 0 0 1 229.77134 939.973665c0-11.530344 9.462402-20.888304 20.825639-20.888303h94.728457A83.010119 83.010119 0 0 1 334.212859 877.413196v-605.96969A83.49055 83.49055 0 0 1 417.849627 187.994733H480.430984V167.001988A83.49055 83.49055 0 0 1 564.067752 83.553215h501.152182A83.448773 83.448773 0 0 1 1148.856702 167.001988v605.969689c0 15.185797-4.052331 29.410732-11.133466 41.672166h115.554096c11.551232 0 20.909192 9.274407 20.909192 20.888304 0 11.530344-9.295295 20.888304-20.888304 20.888304H1002.638576v20.992745c0 15.185797-4.052331 29.410732-11.133466 41.672166h11.196131c11.488567 0 20.825639 9.274407 20.825639 20.888303 0 11.530344-9.462402 20.888304-20.825639 20.888304h-109.893365c9.545955 16.000441 7.478013 36.972297-6.41271 50.863019a41.672166 41.672166 0 0 1-59.072122 0L776.460024 960.861969z m76.367638-41.776607h66.424806c22.977134 0 41.609501-18.59059 41.609501-41.881049V270.461756c0-22.559368-18.047494-40.690416-40.314426-40.690416H416.303892c-22.266932 0-40.314426 18.214601-40.314426 40.690416v606.742557c0 23.123352 18.799473 41.881049 41.588613 41.881049h317.084449l-10.736588-10.757477a41.693054 41.693054 0 0 1-10.861918-40.377091l-19.718558-19.739447A146.259902 146.259902 0 0 1 502.363703 627.693525a146.218126 146.218126 0 0 1 220.517822 190.981761l19.739447 19.739447a41.630389 41.630389 0 0 1 40.377091 10.841029L852.827662 919.085362zM1002.638576 814.643843h62.852906A41.797496 41.797496 0 0 0 1107.080095 772.867236V167.106429c0-23.14424-18.632367-41.776607-41.588613-41.776607H563.775316A41.797496 41.797496 0 0 0 522.207592 167.106429v20.888304h396.794216A83.448773 83.448773 0 0 1 1002.638576 271.443506V814.643843zM266.325872 46.998683h31.123572c8.773088 0 15.875111 6.955805 15.875111 15.666228 0 8.647758-7.102023 15.666228-15.875111 15.666228h-31.123572v31.123572c0 8.773088-6.955805 15.875111-15.666228 15.875111a15.770669 15.770669 0 0 1-15.666228-15.875111V78.331139H203.869844A15.728893 15.728893 0 0 1 187.994733 62.664911c0-8.647758 7.102023-15.666228 15.875111-15.666228h31.123572V15.875111c0-8.773088 6.955805-15.875111 15.666228-15.875111 8.647758 0 15.666228 7.102023 15.666228 15.875111v31.123572zM20.888304 939.973665c0-11.530344 9.462402-20.888304 20.825638-20.888303h125.455152c11.488567 0 20.825639 9.274407 20.825639 20.888303 0 11.530344-9.462402 20.888304-20.825639 20.888304H41.713942A20.80475 20.80475 0 0 1 20.888304 939.973665z m658.733544-135.021995a104.441518 104.441518 0 1 0-147.722083-147.722083 104.441518 104.441518 0 0 0 147.722083 147.722083zM459.542681 313.324555a20.888304 20.888304 0 0 1 20.867415-20.888304H710.202325a20.888304 20.888304 0 1 1 0 41.776608H480.430984A20.825639 20.825639 0 0 1 459.542681 313.324555z m0 104.441518c0-11.530344 9.295295-20.888304 20.742085-20.888303h334.505295c11.44679 0 20.742086 9.274407 20.742086 20.888303 0 11.530344-9.295295 20.888304-20.742086 20.888304H480.284766A20.762974 20.762974 0 0 1 459.542681 417.766073z m0 104.441519c0-11.530344 9.316183-20.888304 20.846527-20.888304h146.301679c11.509455 0 20.846527 9.274407 20.846527 20.888304 0 11.530344-9.316183 20.888304-20.846527 20.888303h-146.301679A20.80475 20.80475 0 0 1 459.542681 522.207592zM62.664911 396.87777a62.664911 62.664911 0 1 1 0-125.329822 62.664911 62.664911 0 0 1 0 125.329822z m0-31.332456a31.332456 31.332456 0 1 0 0-62.664911 31.332456 31.332456 0 0 0 0 62.664911zM1357.739739 271.547948a62.664911 62.664911 0 1 1 0-125.329822 62.664911 62.664911 0 0 1 0 125.329822z m0-31.332456a31.332456 31.332456 0 1 0 0-62.664911 31.332456 31.332456 0 0 0 0 62.664911z"
|
||||
Fill="LightGray"
|
||||
Stretch="Fill" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Margin="10"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
FontSize="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=FontSize, Mode=OneWay}"
|
||||
Foreground="LightGray"
|
||||
Text="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=(f:DataGridHelper.NoDataTips), Mode=OneWay}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<ScrollBar
|
||||
x:Name="PART_VerticalScrollBar"
|
||||
Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
Maximum="{TemplateBinding ScrollableHeight}"
|
||||
Orientation="Vertical"
|
||||
|
||||
ViewportSize="{TemplateBinding ViewportHeight}"
|
||||
Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"
|
||||
Value="{Binding VerticalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" />
|
||||
<Grid Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2">
|
||||
<ScrollBar
|
||||
x:Name="PART_HorizontalScrollBar"
|
||||
Maximum="{TemplateBinding ScrollableWidth}"
|
||||
Orientation="Horizontal"
|
||||
|
||||
ViewportSize="{TemplateBinding ViewportWidth}"
|
||||
Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"
|
||||
Value="{Binding HorizontalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<MultiDataTrigger>
|
||||
<MultiDataTrigger.Conditions>
|
||||
<Condition Binding="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=(f:DataGridHelper.IsActive)}" Value="false" />
|
||||
<Condition Binding="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=HasItems}" Value="false" />
|
||||
</MultiDataTrigger.Conditions>
|
||||
<Setter TargetName="NoDataTipsGrid" Property="Visibility" Value="Visible" />
|
||||
</MultiDataTrigger>
|
||||
<!--<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=(f:DataGridHelper.NoDataTips)}" Value="{x:Null}">
|
||||
<Setter TargetName="NoDataTipsGrid" Property="Visibility" Value="Collapsed" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=(f:DataGridHelper.NoDataTips)}" Value="">
|
||||
<Setter TargetName="NoDataTipsGrid" Property="Visibility" Value="Collapsed" />
|
||||
</DataTrigger>-->
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</ScrollViewer.Template>
|
||||
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
<!-- 分页仅请求数据,不截断调用方的 ItemsSource。 -->
|
||||
<Border x:Name="PART_PaginationArea" Grid.Row="2" Background="#FAFAFA" BorderBrush="#E1E4E8" BorderThickness="1,0,1,1" Padding="8,4"
|
||||
Visibility="{Binding ShowPagination, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay, Converter={StaticResource DataGridExVisibility}}">
|
||||
<Grid>
|
||||
<ContentPresenter x:Name="PART_PaginationContent" Content="{TemplateBinding PaginationContent}"/>
|
||||
<DockPanel x:Name="PART_DefaultPagination" Visibility="Collapsed">
|
||||
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
|
||||
<TextBlock Text="每页" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<ComboBox Width="70" MinHeight="26" IsEditable="False" Margin="0,0,8,0"
|
||||
ItemsSource="{Binding PageSizes, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"
|
||||
SelectedItem="{Binding PageSize, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="首页" Command="{x:Static local:DataGridEx.FirstPageCommand}" Padding="8,2" Margin="0,0,4,0"/>
|
||||
<Button Content="上一页" Command="{x:Static local:DataGridEx.PreviousPageCommand}" Padding="8,2"/>
|
||||
<TextBlock Text="{Binding PageSummary, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}" VerticalAlignment="Center" Margin="10,0"/>
|
||||
<Button Content="下一页" Command="{x:Static local:DataGridEx.NextPageCommand}" Padding="8,2"/>
|
||||
<Button Content="末页" Command="{x:Static local:DataGridEx.LastPageCommand}" Padding="8,2" Margin="4,0,0,0"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding TotalSummary, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}" VerticalAlignment="Center"/>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Border x:Name="PART_LoadingOverlay" Grid.RowSpan="3" Background="#DDFFFFFF" Visibility="Collapsed" Cursor="Wait">
|
||||
<StackPanel Width="180" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<ProgressBar x:Name="PART_LoadingProgress" Height="4" IsIndeterminate="False" Foreground="#98638F" Background="#EDE5EB" BorderThickness="0"/>
|
||||
<TextBlock Text="{TemplateBinding LoadingText}" HorizontalAlignment="Center" Margin="0,12,0,0" Foreground="#626873"
|
||||
AutomationProperties.LiveSetting="Polite"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsLoading" Value="True">
|
||||
<Setter TargetName="PART_LoadingOverlay" Property="Visibility" Value="Visible"/>
|
||||
<Setter TargetName="PART_LoadingProgress" Property="IsIndeterminate" Value="True"/>
|
||||
<Setter TargetName="PART_ToolbarArea" Property="IsEnabled" Value="False"/>
|
||||
<Setter TargetName="PART_TableArea" Property="IsEnabled" Value="False"/>
|
||||
<Setter TargetName="PART_PaginationArea" Property="IsEnabled" Value="False"/>
|
||||
</Trigger>
|
||||
<Trigger Property="PaginationContent" Value="{x:Null}">
|
||||
<Setter TargetName="PART_DefaultPagination" Property="Visibility" Value="Visible"/>
|
||||
<Setter TargetName="PART_PaginationContent" Property="Visibility" Value="Collapsed"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="IsGrouping" Value="true" />
|
||||
<Condition Property="VirtualizingPanel.IsVirtualizingWhenGrouping" Value="false" />
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter Property="ScrollViewer.CanContentScroll" Value="false" />
|
||||
</MultiTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
|
||||
|
||||
<Style x:Key="DataGridExCellStyle" TargetType="DataGridCell">
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Padding" Value="8,0" />
|
||||
<Setter Property="FontSize" Value="{StaticResource DataGridExFontSize}" />
|
||||
<Setter Property="FontFamily" Value="{StaticResource DataGridExFontFamily}" />
|
||||
<!--<Setter Property="Foreground" Value="{Binding RelativeSource={RelativeSource AncestorType=DataGrid, Mode=FindAncestor}, Path=Foreground, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />-->
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type DataGridCell}">
|
||||
<Grid>
|
||||
<Border
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}">
|
||||
<ContentPresenter
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
|
||||
</Border>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="{Binding SelectedRowBackground, RelativeSource={RelativeSource AncestorType={x:Type local:DataGridEx}}}" />
|
||||
<Setter Property="Foreground" Value="{Binding SelectedRowForeground, RelativeSource={RelativeSource AncestorType={x:Type local:DataGridEx}}}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style x:Key="DataGridExRowStyle" TargetType="{x:Type DataGridRow}">
|
||||
<Setter Property="ContextMenu" Value="{Binding RowContextMenu, RelativeSource={RelativeSource AncestorType=local:DataGridEx}}" />
|
||||
<Setter Property="Tag" Value="{Binding DataContext, RelativeSource={RelativeSource AncestorType=local:DataGridEx}}" />
|
||||
<Setter Property="BorderThickness" Value="0,0,1,0" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="FontSize" Value="{StaticResource DataGridExFontSize}" />
|
||||
<Setter Property="FontFamily" Value="{StaticResource DataGridExFontFamily}" />
|
||||
<Setter Property="BorderBrush" Value="{Binding RelativeSource={RelativeSource AncestorType=DataGrid, Mode=FindAncestor}, Path=BorderBrush}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="DataGridRow">
|
||||
<Grid>
|
||||
<Border
|
||||
x:Name="border"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}" />
|
||||
<SelectiveScrollingGrid>
|
||||
<SelectiveScrollingGrid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</SelectiveScrollingGrid.ColumnDefinitions>
|
||||
<SelectiveScrollingGrid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</SelectiveScrollingGrid.RowDefinitions>
|
||||
<DataGridCellsPresenter
|
||||
Grid.Column="1"
|
||||
ItemsPanel="{TemplateBinding ItemsPanel}"
|
||||
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
|
||||
<DataGridDetailsPresenter
|
||||
x:Name="DetailsPresenter"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
SelectiveScrollingGrid.SelectiveScrollingOrientation="{Binding AreRowDetailsFrozen, ConverterParameter={x:Static SelectiveScrollingOrientation.Vertical}, Converter={x:Static DataGrid.RowDetailsScrollingConverter}, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"
|
||||
Visibility="{TemplateBinding DetailsVisibility}" />
|
||||
<DataGridRowHeader x:Name="PART_RowHeader"
|
||||
Grid.RowSpan="2"
|
||||
SelectiveScrollingGrid.SelectiveScrollingOrientation="Vertical"
|
||||
Visibility="{Binding HeadersVisibility, ConverterParameter={x:Static DataGridHeadersVisibility.Row}, Converter={x:Static DataGrid.HeadersVisibilityConverter}, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}" />
|
||||
</SelectiveScrollingGrid>
|
||||
<Grid
|
||||
x:Name="fousIcon"
|
||||
IsHitTestVisible="False"
|
||||
Opacity="0"
|
||||
Visibility="Collapsed">
|
||||
<Path
|
||||
Width="10"
|
||||
Height="10"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
Data="M0,0 L0,0 50,0 50,10 10,10 10,50 0,50 0,52 z"
|
||||
Fill="{DynamicResource PrimaryBrush}"
|
||||
RenderTransformOrigin="0.5,0.5"
|
||||
Stretch="Fill">
|
||||
<Path.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform />
|
||||
<SkewTransform />
|
||||
<RotateTransform Angle="0" />
|
||||
<TranslateTransform />
|
||||
</TransformGroup>
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
<Path
|
||||
Width="10"
|
||||
Height="10"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Data="M0,0 L0,0 50,0 50,10 10,10 10,50 0,50 0,52 z"
|
||||
Fill="{DynamicResource PrimaryBrush}"
|
||||
RenderTransformOrigin="0.5,0.5"
|
||||
Stretch="Fill">
|
||||
<Path.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform />
|
||||
<SkewTransform />
|
||||
<RotateTransform Angle="90" />
|
||||
<TranslateTransform />
|
||||
</TransformGroup>
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
<Path
|
||||
Width="10"
|
||||
Height="10"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Data="M0,0 L0,0 50,0 50,10 10,10 10,50 0,50 0,52 z"
|
||||
Fill="{DynamicResource PrimaryBrush}"
|
||||
RenderTransformOrigin="0.5,0.5"
|
||||
Stretch="Fill">
|
||||
<Path.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform />
|
||||
<SkewTransform />
|
||||
<RotateTransform Angle="180" />
|
||||
<TranslateTransform />
|
||||
</TransformGroup>
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
<Path
|
||||
Width="10"
|
||||
Height="10"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Bottom"
|
||||
Data="M0,0 L0,0 50,0 50,10 10,10 10,50 0,50 0,52 z"
|
||||
Fill="{DynamicResource PrimaryBrush}"
|
||||
RenderTransformOrigin="0.5,0.5"
|
||||
Stretch="Fill">
|
||||
<Path.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform />
|
||||
<SkewTransform />
|
||||
<RotateTransform Angle="-90" />
|
||||
<TranslateTransform />
|
||||
</TransformGroup>
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="fousIcon" Property="Opacity" Value="1" />
|
||||
<Setter Property="Foreground" Value="{Binding SelectedRowForeground, RelativeSource={RelativeSource AncestorType={x:Type local:DataGridEx}}}" />
|
||||
<Setter TargetName="border" Property="Background" Value="{Binding SelectedRowBackground, RelativeSource={RelativeSource AncestorType={x:Type local:DataGridEx}}}" />
|
||||
</Trigger>
|
||||
<Trigger Property="DetailsVisibility" Value="Visible">
|
||||
<Setter TargetName="DetailsPresenter" Property="Margin" Value="0,0,1,0" />
|
||||
</Trigger>
|
||||
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style x:Key="DataGridExRowHeaderStyle" BasedOn="{StaticResource DataGridExDefaultRowHeaderStyle}" TargetType="DataGridRowHeader">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="DataGridRowHeader">
|
||||
<Border Background="Transparent" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="0,0,1,1">
|
||||
<CheckBox Style="{StaticResource DataGridExCheckBoxStyle}" HorizontalAlignment="Center" VerticalAlignment="Center" AutomationProperties.Name="选择行"
|
||||
IsChecked="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=DataGridRow}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="DataGridExNumberCellStyle"
|
||||
BasedOn="{StaticResource DataGridExCellStyle}"
|
||||
TargetType="DataGridCell">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Right" />
|
||||
</Style>
|
||||
|
||||
<Style BasedOn="{StaticResource DataGridExDefaultDataGridStyle}" TargetType="local:DataGridEx">
|
||||
<Setter Property="EnableRowVirtualization" Value="True" />
|
||||
<Setter Property="EnableColumnVirtualization" Value="True" />
|
||||
<Setter Property="VirtualizingPanel.IsVirtualizing" Value="True" />
|
||||
<Setter Property="VirtualizingPanel.VirtualizationMode" Value="Recycling" />
|
||||
<Setter Property="ScrollViewer.CanContentScroll" Value="True" />
|
||||
|
||||
<Setter Property="SelectionMode" Value="Extended" />
|
||||
<Setter Property="SelectionUnit" Value="FullRow" />
|
||||
<Setter Property="RowStyle" Value="{StaticResource DataGridExRowStyle}" />
|
||||
<Setter Property="CellStyle" Value="{StaticResource DataGridExCellStyle}" />
|
||||
<Setter Property="RowHeaderStyle" Value="{StaticResource DataGridExRowHeaderStyle}" />
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,169 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Media;
|
||||
using System.Windows;
|
||||
|
||||
namespace Store.Management.Framework.Controls;
|
||||
|
||||
/// <summary>配套图片列:自动使用所属 DataGridEx 的拖动暂停和图片缓存能力。</summary>
|
||||
public class DataGridExImageColumn : DataGridBoundColumn
|
||||
{
|
||||
private static readonly Style DefaultImageStyle = CreateDefaultStyle();
|
||||
/// <summary>可选的逐行占位图;未设置时显示空白。</summary>
|
||||
public BindingBase? PlaceholderBinding { get; set; }
|
||||
|
||||
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
|
||||
{
|
||||
var image = new NetworkThumbnail { Style = ElementStyle ?? DefaultImageStyle };
|
||||
if (Binding != null) BindingOperations.SetBinding(image, NetworkThumbnail.UrlProperty, Binding);
|
||||
if (PlaceholderBinding != null)
|
||||
BindingOperations.SetBinding(image, NetworkThumbnail.PlaceholderProperty, PlaceholderBinding);
|
||||
return image;
|
||||
}
|
||||
protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem) => GenerateElement(cell, dataItem);
|
||||
private static Style CreateDefaultStyle()
|
||||
{
|
||||
var style = new Style(typeof(NetworkThumbnail));
|
||||
style.Setters.Add(new Setter(FrameworkElement.WidthProperty, 58d));
|
||||
style.Setters.Add(new Setter(FrameworkElement.HeightProperty, 58d));
|
||||
style.Setters.Add(new Setter(System.Windows.Controls.Image.StretchProperty, Stretch.Uniform));
|
||||
style.Seal(); return style;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>仅为已生成的可见单元格加载图片;回收时取消,避免旧图片覆盖新行。</summary>
|
||||
public sealed class NetworkThumbnail : Image
|
||||
{
|
||||
public static readonly DependencyProperty CacheEnabledProperty = DependencyProperty.RegisterAttached("CacheEnabled", typeof(bool), typeof(NetworkThumbnail), new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.Inherits, Changed));
|
||||
public static bool GetCacheEnabled(DependencyObject target) => (bool)target.GetValue(CacheEnabledProperty);
|
||||
public static void SetCacheEnabled(DependencyObject target, bool value) => target.SetValue(CacheEnabledProperty, value);
|
||||
|
||||
public static readonly DependencyProperty IsLoadingSuspendedProperty = DependencyProperty.RegisterAttached(
|
||||
"IsLoadingSuspended", typeof(bool), typeof(NetworkThumbnail),
|
||||
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.Inherits, SuspensionChanged));
|
||||
public static bool GetIsLoadingSuspended(DependencyObject target) => (bool)target.GetValue(IsLoadingSuspendedProperty);
|
||||
public static void SetIsLoadingSuspended(DependencyObject target, bool value) => target.SetValue(IsLoadingSuspendedProperty, value);
|
||||
|
||||
private static void SuspensionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is not NetworkThumbnail image) return;
|
||||
image.CancelLoading();
|
||||
image.Source = null;
|
||||
if (!(bool)e.NewValue) image.QueueRefresh();
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty UrlProperty = DependencyProperty.Register(nameof(Url), typeof(string), typeof(NetworkThumbnail), new PropertyMetadata(null, Changed));
|
||||
public static readonly DependencyProperty PlaceholderProperty = DependencyProperty.Register(nameof(Placeholder), typeof(ImageSource), typeof(NetworkThumbnail), new PropertyMetadata(null, Changed));
|
||||
public string? Url { get => (string?)GetValue(UrlProperty); set => SetValue(UrlProperty, value); }
|
||||
public ImageSource? Placeholder { get => (ImageSource?)GetValue(PlaceholderProperty); set => SetValue(PlaceholderProperty, value); }
|
||||
private CancellationTokenSource? loading;
|
||||
private bool refreshPending;
|
||||
|
||||
public NetworkThumbnail()
|
||||
{
|
||||
Loaded += (_, _) => QueueRefresh();
|
||||
Unloaded += (_, _) => CancelLoading();
|
||||
}
|
||||
private static void Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is not NetworkThumbnail image) return;
|
||||
image.CancelLoading();
|
||||
image.Source = GetIsLoadingSuspended(image) ? null : image.Placeholder;
|
||||
// 回收行时多个绑定会依次变更,等本轮绑定完成后只加载一次新 URL。
|
||||
image.QueueRefresh();
|
||||
}
|
||||
private void QueueRefresh()
|
||||
{
|
||||
if (!IsLoaded || refreshPending) return;
|
||||
refreshPending = true;
|
||||
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Loaded, new Action(() =>
|
||||
{
|
||||
refreshPending = false;
|
||||
if (IsLoaded) StartLoading();
|
||||
}));
|
||||
}
|
||||
private void CancelLoading()
|
||||
{
|
||||
loading?.Cancel(); loading?.Dispose(); loading = null;
|
||||
}
|
||||
private async void StartLoading()
|
||||
{
|
||||
CancelLoading(); ToolTip = null;
|
||||
if (GetIsLoadingSuspended(this)) { Source = null; return; }
|
||||
Source = Placeholder;
|
||||
if (string.IsNullOrWhiteSpace(Url)) return;
|
||||
var request = new CancellationTokenSource(); loading = request;
|
||||
var token = request.Token;
|
||||
try
|
||||
{
|
||||
var bitmap = await ThumbnailLoader.LoadAsync(Url, GetCacheEnabled(this), token);
|
||||
if (!token.IsCancellationRequested && ReferenceEquals(loading, request)) Source = bitmap;
|
||||
}
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!token.IsCancellationRequested && ReferenceEquals(loading, request)) ToolTip = $"图片加载失败:{ex.Message}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static class ThumbnailLoader
|
||||
{
|
||||
private static readonly HttpClient Client = new() { Timeout = TimeSpan.FromSeconds(12) };
|
||||
private static readonly SemaphoreSlim Slots = new(4);
|
||||
private static readonly object CacheLock = new();
|
||||
private static readonly Dictionary<string, BitmapSource> Cache = new();
|
||||
private static readonly Queue<string> CacheOrder = new();
|
||||
private static long started, succeeded, failed, canceled, cacheHits, bytes, totalTicks;
|
||||
private static int active, pending;
|
||||
public static int Pending => Volatile.Read(ref pending);
|
||||
public static string Statistics => $"网络图片:请求 {Interlocked.Read(ref started)} / 成功 {Interlocked.Read(ref succeeded)} / 失败 {Interlocked.Read(ref failed)} / 取消 {Interlocked.Read(ref canceled)} / 缓存命中 {Interlocked.Read(ref cacheHits)} | 活跃 {Volatile.Read(ref active)},待完成 {Pending} | 下载 {Interlocked.Read(ref bytes) / 1024d:N0} KB | 成功平均 {AverageMilliseconds:N0} ms";
|
||||
private static double AverageMilliseconds => Interlocked.Read(ref succeeded) == 0 ? 0 : Interlocked.Read(ref totalTicks) * 1000d / Stopwatch.Frequency / Interlocked.Read(ref succeeded);
|
||||
|
||||
public static async Task<BitmapSource> LoadAsync(string url, bool cacheEnabled, CancellationToken token)
|
||||
{
|
||||
var cacheKey = url;
|
||||
lock (CacheLock)
|
||||
if (cacheEnabled && Cache.TryGetValue(cacheKey, out var cached)) { Interlocked.Increment(ref cacheHits); return cached; }
|
||||
Interlocked.Increment(ref pending);
|
||||
var acquired = false;
|
||||
try
|
||||
{
|
||||
await Slots.WaitAsync(token).ConfigureAwait(false); acquired = true;
|
||||
Interlocked.Increment(ref active); Interlocked.Increment(ref started);
|
||||
var watch = Stopwatch.StartNew();
|
||||
var data = await Client.GetByteArrayAsync(url, token).ConfigureAwait(false);
|
||||
Interlocked.Add(ref bytes, data.Length);
|
||||
var bitmap = await Task.Run(() =>
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
using var stream = new MemoryStream(data);
|
||||
var image = new BitmapImage(); image.BeginInit(); image.CacheOption = BitmapCacheOption.OnLoad;
|
||||
image.DecodePixelWidth = 72; image.StreamSource = stream; image.EndInit(); image.Freeze();
|
||||
return image;
|
||||
}, token).ConfigureAwait(false);
|
||||
token.ThrowIfCancellationRequested();
|
||||
lock (CacheLock)
|
||||
{
|
||||
if (cacheEnabled && !Cache.ContainsKey(cacheKey))
|
||||
{
|
||||
if (Cache.Count >= 128) Cache.Remove(CacheOrder.Dequeue());
|
||||
Cache.Add(cacheKey, bitmap); CacheOrder.Enqueue(cacheKey);
|
||||
}
|
||||
}
|
||||
Interlocked.Increment(ref succeeded); Interlocked.Add(ref totalTicks, watch.ElapsedTicks);
|
||||
return bitmap;
|
||||
}
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested) { Interlocked.Increment(ref canceled); throw; }
|
||||
catch { Interlocked.Increment(ref failed); throw; }
|
||||
finally
|
||||
{
|
||||
if (acquired) { Interlocked.Decrement(ref active); Slots.Release(); }
|
||||
Interlocked.Decrement(ref pending);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<ResourceDictionary Source="/Store.Management.Framework;component/Controls/DataGridEx/DataGridEx.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
@@ -17,6 +17,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Controls\DataGridEx\" />
|
||||
<Folder Include="DependencyInjection\" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="/Store.Management.Framework;component/Controls/DataGridEx/DataGridEx.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
@@ -16,6 +16,8 @@
|
||||
<ResourceDictionary Source="pack://application:,,,/Store.Management.Framework;component/Resources/HandyControlBasic.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/Store.Management.Framework;component/Resources/FrameworkTheme.xaml" />
|
||||
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
|
||||
@@ -25,6 +25,16 @@ public partial class App : FrameworkApplication
|
||||
|
||||
protected override Window CreateShell()
|
||||
{
|
||||
if (Environment.GetCommandLineArgs().Contains("--grid-demo"))
|
||||
{
|
||||
return new Framework.Controls.WindowBase
|
||||
{
|
||||
Title = "DataGridEx 控件测试", Width = 1360, Height = 800,
|
||||
MinWidth = 900, MinHeight = 560,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen,
|
||||
Content = new DataGridTestView()
|
||||
};
|
||||
}
|
||||
return Container.Resolve<MainWindow>();
|
||||
}
|
||||
|
||||
@@ -53,6 +63,7 @@ public partial class App : FrameworkApplication
|
||||
{
|
||||
|
||||
containerRegistry.RegisterDialogWindow<Framework.Controls.DialogWindow>();
|
||||
containerRegistry.RegisterDialog<DataGridTestView, DataGridTestViewModel>();
|
||||
|
||||
ViewModelLocationProvider.Register<MainWindow, MainWindowViewModel>();
|
||||
|
||||
|
||||
@@ -10,5 +10,11 @@
|
||||
MinWidth="720"
|
||||
MinHeight="560"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
<views:FrameworkTestPage />
|
||||
<DockPanel>
|
||||
<!-- 控件测试入口 -->
|
||||
<Button DockPanel.Dock="Top" Content="打开 DataGrid 测试"
|
||||
Command="{Binding OpenDataGridTestCommand, Mode=OneWay}"
|
||||
HorizontalAlignment="Left" Margin="16,12" Padding="16,6" />
|
||||
<views:FrameworkTestPage />
|
||||
</DockPanel>
|
||||
</f:WindowBase>
|
||||
@@ -1,4 +1,7 @@
|
||||
using Prism.Mvvm;
|
||||
using Prism.Commands;
|
||||
using Prism.Services.Dialogs;
|
||||
using Store.Management.Views;
|
||||
using Store.Management.Framework.Controls;
|
||||
using System.Windows;
|
||||
namespace Store.Management;
|
||||
@@ -8,9 +11,12 @@ public partial class MainWindow : WindowBase
|
||||
public MainWindow() => InitializeComponent();
|
||||
}
|
||||
|
||||
public class MainWindowViewModel : BindableBase
|
||||
public class MainWindowViewModel(IDialogService dialogService) : BindableBase
|
||||
{
|
||||
private string _title="TTTT";
|
||||
public DelegateCommand OpenDataGridTestCommand => field ??= new(() =>
|
||||
dialogService.ShowDialog(nameof(DataGridTestView), new DialogParameters(), _ => { }));
|
||||
|
||||
private string _title="TTTT";
|
||||
public string Title
|
||||
{
|
||||
get => _title;
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
<UseWPF>true</UseWPF>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bogus" Version="35.6.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Store.Management.Framework\Store.Management.Framework.csproj" />
|
||||
<ProjectReference Include="..\Store.Management.Api\Store.Management.Api.csproj" />
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
using Bogus;
|
||||
using ConfirmDialogViewModel = MAX.Framework.Controls.ConfirmDialogViewModel;
|
||||
using Prism.Commands;
|
||||
using Prism.Mvvm;
|
||||
using Store.Management.Framework.Controls;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Store.Management.ViewModels;
|
||||
|
||||
/// <summary>固定随机种子的离线样例;不请求真实 API。</summary>
|
||||
public sealed class DataGridTestViewModel : ConfirmDialogViewModel
|
||||
{
|
||||
public Store.Management.Framework.ViewModels.LoadingStatus LoadingStatus { get; } = new();
|
||||
public const int SearchTextLimit = 250;
|
||||
private readonly List<DemoStore> stores;
|
||||
private IReadOnlyList<DemoStore> searchResults = [];
|
||||
private readonly List<DemoStore> pageItems = [];
|
||||
private string localKeyword = string.Empty;
|
||||
private string backendKeyword = string.Empty;
|
||||
private readonly ListCollectionView currentItems;
|
||||
private string localFilter = string.Empty;
|
||||
private int totalCount;
|
||||
private int pageSize = 50;
|
||||
private string message = "Bogus 生成 237 条样例。全选仅作用于当前页,换页清空选择;表头排序作用于当前页。";
|
||||
|
||||
public DataGridTestViewModel()
|
||||
{
|
||||
currentItems = new ListCollectionView(pageItems) { Filter = item => Matches((DemoStore)item, localFilter) };
|
||||
Title = "DataGridEx 控件测试";
|
||||
AllowEnter = false;
|
||||
DateTime reference = new(2026, 10, 28);
|
||||
stores = new Faker<DemoStore>("zh_CN").UseSeed(29)
|
||||
.RuleFor(row => row.Name, faker => faker.Company.CompanyName() + "小店")
|
||||
.RuleFor(row => row.StoreId, faker => "wx" + faker.Random.Hexadecimal(14, string.Empty))
|
||||
.RuleFor(row => row.CreatedAt, faker => reference.AddDays(-faker.Random.Int(30, 180)))
|
||||
.RuleFor(row => row.ExpiresAt, faker => reference.AddDays(faker.Random.Int(-20, 180)))
|
||||
.RuleFor(row => row.Group, faker => faker.PickRandom("生活百货", "服饰鞋包", "食品生鲜"))
|
||||
.RuleFor(row => row.OperatorName, faker => faker.Name.FullName())
|
||||
.RuleFor(row => row.Phone, faker => faker.Phone.PhoneNumber("138****####"))
|
||||
.RuleFor(row => row.Note, faker => faker.PickRandom("重点运营", "新品店铺", "", "待补充店铺资料"))
|
||||
.FinishWith((_, row) => row.Status = row.ExpiresAt < reference ? "已过期" : row.ExpiresAt <= reference.AddDays(7) ? "即将到期" : "正常")
|
||||
.Generate(237);
|
||||
SelectedStores.CollectionChanged += (_, _) =>
|
||||
{
|
||||
RenewCommand.RaiseCanExecuteChanged();
|
||||
InspectSelectionCommand.RaiseCanExecuteChanged();
|
||||
};
|
||||
searchResults = stores;
|
||||
TotalCount = stores.Count;
|
||||
LoadPage(new DataGridPageRequest(1, pageSize));
|
||||
}
|
||||
|
||||
public ObservableCollection<DemoStore> SelectedStores { get; } = [];
|
||||
public ICollectionView CurrentItems => currentItems;
|
||||
public int TotalCount { get => totalCount; private set => SetProperty(ref totalCount, value); }
|
||||
public string Message { get => message; private set => SetProperty(ref message, value); }
|
||||
|
||||
public string LocalKeyword
|
||||
{
|
||||
get => localKeyword;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref localKeyword, value))
|
||||
{
|
||||
FilterCurrentPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string BackendKeyword { get => backendKeyword; set => SetProperty(ref backendKeyword, value); }
|
||||
public string LocalSearchSummary => $"本页显示 {currentItems.Count} / {pageItems.Count} 条";
|
||||
|
||||
public AsyncDelegateCommand SearchCommand => field ??= new(Search);
|
||||
private async Task Search()
|
||||
{
|
||||
string keyword = BackendKeyword.Trim();
|
||||
LoadingStatus.Initialize("正在搜索店铺…");
|
||||
try
|
||||
{
|
||||
// 演示网络等待;正式接入时替换为异步 API 请求。
|
||||
await Task.Delay(2450);
|
||||
searchResults = stores.Where(store => Matches(store, keyword)).ToArray();
|
||||
LocalKeyword = string.Empty;
|
||||
TotalCount = 0;
|
||||
TotalCount = searchResults.Count;
|
||||
LoadPage(new DataGridPageRequest(1, pageSize));
|
||||
Message = $"模拟后台搜索完成,共 {TotalCount} 条;未连接真实 API。";
|
||||
}
|
||||
finally
|
||||
{
|
||||
LoadingStatus.Done();
|
||||
}
|
||||
}
|
||||
|
||||
private void FilterCurrentPage()
|
||||
{
|
||||
SelectedStores.Clear();
|
||||
localFilter = LocalKeyword.Trim();
|
||||
currentItems.Refresh();
|
||||
RaisePropertyChanged(nameof(LocalSearchSummary));
|
||||
}
|
||||
|
||||
private static bool Matches(DemoStore store, string keyword) =>
|
||||
store.Name.Contains(keyword, StringComparison.OrdinalIgnoreCase) ||
|
||||
store.StoreId.Contains(keyword, StringComparison.OrdinalIgnoreCase) ||
|
||||
store.Group.Contains(keyword, StringComparison.OrdinalIgnoreCase) ||
|
||||
store.OperatorName.Contains(keyword, StringComparison.OrdinalIgnoreCase) ||
|
||||
store.Phone.Contains(keyword, StringComparison.OrdinalIgnoreCase) ||
|
||||
store.Status.Contains(keyword, StringComparison.OrdinalIgnoreCase) ||
|
||||
store.Note.Contains(keyword, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public DelegateCommand<DataGridPageRequest> LoadPageCommand => field ??= new(LoadPage);
|
||||
private void LoadPage(DataGridPageRequest request)
|
||||
{
|
||||
pageSize = request.PageSize;
|
||||
SelectedStores.Clear();
|
||||
pageItems.Clear();
|
||||
pageItems.AddRange(searchResults.Skip((request.PageIndex - 1) * request.PageSize).Take(request.PageSize));
|
||||
FilterCurrentPage();
|
||||
}
|
||||
|
||||
public DelegateCommand RenewCommand => field ??= new(Renew, () => SelectedStores.Count > 0);
|
||||
private void Renew()
|
||||
{
|
||||
foreach (DemoStore store in SelectedStores)
|
||||
{
|
||||
store.ExpiresAt = new DateTime(2027, 5, 9);
|
||||
store.Status = "正常";
|
||||
}
|
||||
Message = $"已模拟续期 {SelectedStores.Count} 家店铺,未产生真实费用。";
|
||||
}
|
||||
|
||||
public DelegateCommand InspectSelectionCommand => field ??= new(InspectSelection, () => SelectedStores.Count > 0);
|
||||
private void InspectSelection() => Message = $"选择 {SelectedStores.Count} 条:{string.Join("、", SelectedStores.Take(3).Select(store => store.Name))}";
|
||||
|
||||
public DelegateCommand EmptyCommand => field ??= new(Empty);
|
||||
private void Empty()
|
||||
{
|
||||
searchResults = [];
|
||||
TotalCount = 0;
|
||||
LoadPage(new DataGridPageRequest(1, pageSize));
|
||||
Message = "空数据状态:总数为 0,不能继续翻页。";
|
||||
}
|
||||
|
||||
public DelegateCommand RestoreCommand => field ??= new(Restore);
|
||||
private void Restore()
|
||||
{
|
||||
LocalKeyword = string.Empty;
|
||||
BackendKeyword = string.Empty;
|
||||
// 总数先归零,让控件同步回第一页,再装入恢复的数据。
|
||||
TotalCount = 0;
|
||||
searchResults = stores;
|
||||
TotalCount = stores.Count;
|
||||
LoadPage(new DataGridPageRequest(1, pageSize));
|
||||
Message = "已恢复 237 条样例,列显示设置保持不变。";
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class DemoStore : BindableBase
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string StoreId { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
private DateTime expiresAt;
|
||||
public DateTime ExpiresAt { get => expiresAt; set => SetProperty(ref expiresAt, value); }
|
||||
private string status = string.Empty;
|
||||
public string Status { get => status; set => SetProperty(ref status, value); }
|
||||
public string Group { get; set; } = string.Empty;
|
||||
public string OperatorName { get; set; } = string.Empty;
|
||||
public string Phone { get; set; } = string.Empty;
|
||||
public string Note { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
<UserControl
|
||||
x:Class="Store.Management.Views.DataGridTestView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:dialog="clr-namespace:MAX.Framework.Controls.Attach;assembly=MAX.Framework"
|
||||
xmlns:f="Store.Management.Framework"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="clr-namespace:Store.Management.ViewModels"
|
||||
d:DataContext="{d:DesignInstance Type=vm:DataGridTestViewModel}"
|
||||
d:DesignHeight="760"
|
||||
d:DesignWidth="1280"
|
||||
dialog:DialogWindowProperty.Height="800"
|
||||
dialog:DialogWindowProperty.ResizeMode="CanResizeWithGrip"
|
||||
dialog:DialogWindowProperty.Width="1360"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.DataContext>
|
||||
<vm:DataGridTestViewModel />
|
||||
</UserControl.DataContext>
|
||||
<UserControl.Resources>
|
||||
<SolidColorBrush x:Key="PrimaryBrush" Color="#98638F" />
|
||||
<Style x:Key="DemoButton" TargetType="Button">
|
||||
<Setter Property="Padding" Value="10,3" />
|
||||
<Setter Property="Margin" Value="0,0,6,0" />
|
||||
<Setter Property="MinHeight" Value="26" />
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
<Grid Margin="10">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<!-- 调用方控制开关;切换时不重建表格。 -->
|
||||
<StackPanel Margin="0,0,0,10">
|
||||
<TextBlock
|
||||
Margin="0,0,0,8"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Text="DataGridEx · 店铺数据测试" />
|
||||
<WrapPanel>
|
||||
<CheckBox
|
||||
Margin="0,0,16,0"
|
||||
Content="选择框"
|
||||
IsChecked="{Binding ShowCheckBox, ElementName=StoreGrid, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<CheckBox
|
||||
Margin="0,0,16,0"
|
||||
Content="序号列"
|
||||
IsChecked="{Binding ShowRowNumber, ElementName=StoreGrid, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<CheckBox
|
||||
Margin="0,0,16,0"
|
||||
Content="操作栏"
|
||||
IsChecked="{Binding ShowToolbar, ElementName=StoreGrid, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<CheckBox
|
||||
Margin="0,0,16,0"
|
||||
Content="列设置"
|
||||
IsChecked="{Binding ShowColumnSettings, ElementName=StoreGrid, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<CheckBox
|
||||
Margin="0,0,16,0"
|
||||
Content="分页栏"
|
||||
IsChecked="{Binding ShowPagination, ElementName=StoreGrid, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<Button
|
||||
Command="{Binding EmptyCommand, Mode=OneWay}"
|
||||
Content="空数据"
|
||||
Style="{StaticResource DemoButton}" />
|
||||
<Button
|
||||
Command="{Binding RestoreCommand, Mode=OneWay}"
|
||||
Content="恢复样例"
|
||||
Style="{StaticResource DemoButton}" />
|
||||
</WrapPanel>
|
||||
<!-- 两种搜索:当前页实时过滤、全部样例模拟后台查询。 -->
|
||||
<WrapPanel Margin="0,10,0,0">
|
||||
<StackPanel Margin="0,0,24,6" Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="当前页实时搜索" />
|
||||
<TextBox
|
||||
Width="220"
|
||||
AutomationProperties.Name="当前页实时搜索"
|
||||
MaxLength="{x:Static vm:DataGridTestViewModel.SearchTextLimit}"
|
||||
Text="{Binding LocalKeyword, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
ToolTip="输入即过滤当前页;清空恢复本页数据,不改变分页总数" />
|
||||
<TextBlock
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding LocalSearchSummary, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,0,6" Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="后台搜索(模拟)" />
|
||||
<TextBox
|
||||
Width="220"
|
||||
AutomationProperties.Name="后台搜索关键字"
|
||||
MaxLength="{x:Static vm:DataGridTestViewModel.SearchTextLimit}"
|
||||
Text="{Binding BackendKeyword, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
ToolTip="按名称、ID、分组、子账号、手机号、状态或备注搜索全部样例" />
|
||||
<Button
|
||||
Margin="8,0,0,0"
|
||||
Padding="14,3"
|
||||
Command="{Binding SearchCommand, Mode=OneWay}"
|
||||
Content="搜索" />
|
||||
</StackPanel>
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
<!-- ItemsSource 是当前页;后台分页时只需替换加载命令。 -->
|
||||
<f:DataGridEx
|
||||
x:Name="StoreGrid"
|
||||
Grid.Row="1"
|
||||
Margin="10"
|
||||
d:IsLoading="True"
|
||||
CanUserSortColumns="True"
|
||||
IsLoading="{Binding LoadingStatus.IsLoading, Mode=OneWay}"
|
||||
IsReadOnly="True"
|
||||
ItemsSource="{Binding CurrentItems, Mode=OneWay}"
|
||||
LoadingText="{Binding LoadingStatus.Message, Mode=OneWay}"
|
||||
PageChangedCommand="{Binding LoadPageCommand, Mode=OneWay}"
|
||||
PageSize="50"
|
||||
SelectedItems="{Binding SelectedStores, Mode=OneWay}"
|
||||
SelectionUnit="FullRow"
|
||||
ShowCheckBox="True"
|
||||
ShowColumnSettings="True"
|
||||
ShowPagination="True"
|
||||
ShowRowNumber="True"
|
||||
ShowToolbar="True"
|
||||
TotalItemCount="{Binding TotalCount, Mode=OneWay}">
|
||||
<f:DataGridEx.ToolbarContent>
|
||||
<ItemsControl>
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.Items>
|
||||
<Button Content="abc" />
|
||||
<Button Content="edf" />
|
||||
</ItemsControl.Items>
|
||||
</ItemsControl>
|
||||
<!--<StackPanel Orientation="Horizontal">
|
||||
<Button
|
||||
Command="{Binding RenewCommand, Mode=OneWay}"
|
||||
Content="批量续期(模拟)"
|
||||
Style="{StaticResource DemoButton}" />
|
||||
<Button
|
||||
Command="{Binding InspectSelectionCommand, Mode=OneWay}"
|
||||
Content="查看选择结果"
|
||||
Style="{StaticResource DemoButton}" />
|
||||
</StackPanel>-->
|
||||
</f:DataGridEx.ToolbarContent>
|
||||
<f:DataGridEx.Columns>
|
||||
<DataGridTextColumn
|
||||
f:DataGridEx.CanHideColumn="False"
|
||||
Binding="{Binding Name, Mode=OneWay}"
|
||||
Header="店铺名称" />
|
||||
<DataGridTextColumn
|
||||
Width="Auto"
|
||||
Binding="{Binding StoreId, Mode=OneWay}"
|
||||
Header="店铺 ID" />
|
||||
<DataGridTextColumn
|
||||
Width="Auto"
|
||||
Binding="{Binding CreatedAt, Mode=OneWay, StringFormat=yyyy-MM-dd HH:mm}"
|
||||
Header="添加时间" />
|
||||
<DataGridTextColumn
|
||||
Width="Auto"
|
||||
Binding="{Binding ExpiresAt, Mode=OneWay, StringFormat=yyyy-MM-dd}"
|
||||
Header="到期时间" />
|
||||
<DataGridTextColumn
|
||||
Width="Auto"
|
||||
Binding="{Binding Status, Mode=OneWay}"
|
||||
Header="授权状态" />
|
||||
<DataGridTextColumn
|
||||
Width="Auto"
|
||||
Binding="{Binding Group, Mode=OneWay}"
|
||||
Header="分组" />
|
||||
<DataGridTextColumn
|
||||
Width="Auto"
|
||||
Binding="{Binding OperatorName, Mode=OneWay}"
|
||||
Header="子账号" />
|
||||
<DataGridTextColumn
|
||||
Width="Auto"
|
||||
Binding="{Binding Phone, Mode=OneWay}"
|
||||
Header="子账号手机号" />
|
||||
<DataGridTextColumn
|
||||
Width="Auto"
|
||||
Binding="{Binding Note, Mode=OneWay}"
|
||||
Header="备注" />
|
||||
</f:DataGridEx.Columns>
|
||||
|
||||
</f:DataGridEx>
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Margin="0,8,0,0"
|
||||
Text="{Binding Message, Mode=OneWay}"
|
||||
TextWrapping="Wrap" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Store.Management.Views;
|
||||
|
||||
public partial class DataGridTestView : UserControl
|
||||
{
|
||||
public DataGridTestView() => InitializeComponent();
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using Store.Management.Framework.Controls;
|
||||
using Store.Management.ViewModels;
|
||||
using Store.Management.Views;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
[STAThread]
|
||||
private static int Main()
|
||||
{
|
||||
SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext());
|
||||
Application app = new() { ShutdownMode = ShutdownMode.OnExplicitShutdown };
|
||||
foreach (string source in new[]
|
||||
{
|
||||
"/Store.Management.Framework;component/Resources/HandyControlBasic.xaml",
|
||||
"/HandyControl;component/Themes/SkinDefault.xaml",
|
||||
"/HandyControl;component/Themes/Theme.xaml",
|
||||
"/Store.Management.Framework;component/Resources/FrameworkTheme.xaml"
|
||||
})
|
||||
{
|
||||
app.Resources.MergedDictionaries.Add(new ResourceDictionary { Source = new Uri(source, UriKind.Relative) });
|
||||
}
|
||||
BindingErrors errors = new();
|
||||
PresentationTraceSources.DataBindingSource.Listeners.Add(errors);
|
||||
Window? window = null;
|
||||
try
|
||||
{
|
||||
app.Resources["DialogWindowStyle"] = new Style(typeof(Window));
|
||||
DataGridTestView dialogView = new();
|
||||
DataGridTestViewModel dialogVm = (DataGridTestViewModel)dialogView.DataContext;
|
||||
Store.Management.Framework.Controls.DialogWindow dialog = new()
|
||||
{
|
||||
Content = dialogView, DataContext = dialogVm,
|
||||
Left = -10000, Top = -10000, ShowInTaskbar = false
|
||||
};
|
||||
Check(dialog.Width == 1360 && dialog.Height == 800 && dialog.ResizeMode == ResizeMode.CanResizeWithGrip, "dialog size and resizing");
|
||||
Check(dialogVm.CanCloseDialog() && !dialogVm.AllowEnter && dialogVm.AllowEscape, "dialog keyboard behavior");
|
||||
bool closeRequested = false;
|
||||
dialogVm.RequestClose += _ => closeRequested = true;
|
||||
dialogVm.CancelCommand.Execute(null);
|
||||
Check(closeRequested, "dialog cancellation requests close");
|
||||
dialog.Close();
|
||||
|
||||
DataGridTestView view = new();
|
||||
DataGridTestViewModel vm = (DataGridTestViewModel)view.DataContext;
|
||||
DataGridEx grid = (DataGridEx)view.FindName("StoreGrid");
|
||||
window = new Window { Content = view, Width = 1280, Height = 760, Left = -10000, Top = -10000, ShowInTaskbar = false };
|
||||
window.Show();
|
||||
Pump();
|
||||
Check(grid.BorderThickness.Left == 1 && grid.BorderThickness.Right == 1, "continuous table side borders");
|
||||
Check(vm.TotalCount == 237 && grid.Items.Count == 50 && grid.PageCount == 5, "Bogus data and page count");
|
||||
Check(grid.Columns.Count == 10 && Equals(grid.Columns[0].Header, "序号"), "separate serial column");
|
||||
DataGridRow row = (DataGridRow)grid.ItemContainerGenerator.ContainerFromIndex(0);
|
||||
Check(DataGridEx.GetRowNumber(row) == 1, "first row number");
|
||||
Check(Descendants<TextBlock>(row).Any(text => text.Text == "1"), "row number binding inherits into cell");
|
||||
Check(Descendants<CheckBox>(row).Any(), "row checkbox rendered independently");
|
||||
Check(Descendants<CheckBox>(grid).Any(box => System.Windows.Automation.AutomationProperties.GetName(box) == "全选行"), "header checkbox rendered");
|
||||
|
||||
CheckBox selectAll = Descendants<CheckBox>(grid).Single(box => System.Windows.Automation.AutomationProperties.GetName(box) == "全选行");
|
||||
CheckBox selectRow = Descendants<CheckBox>(row).Single();
|
||||
Check(ReferenceEquals(selectAll.Template, selectRow.Template), "header and row checkbox share the theme template");
|
||||
double headerCenter = selectAll.TranslatePoint(new Point(selectAll.ActualWidth / 2, 0), grid).X;
|
||||
double rowCenter = selectRow.TranslatePoint(new Point(selectRow.ActualWidth / 2, 0), grid).X;
|
||||
Check(Math.Abs(headerCenter - rowCenter) <= 1, "header and row checkbox centers align");
|
||||
DataGridColumnHeader nameHeader = Descendants<DataGridColumnHeader>(grid).Single(header => Equals(header.Content, "店铺名称"));
|
||||
grid.Columns[1].SortDirection = ListSortDirection.Ascending;
|
||||
Pump();
|
||||
FrameworkElement sortArrow = (FrameworkElement)nameHeader.Template.FindName("SortArrowGrid", nameHeader);
|
||||
Check(sortArrow != null && sortArrow.IsVisible, "flat custom header and sort arrow template applied");
|
||||
double arrowRight = sortArrow.TranslatePoint(new Point(sortArrow.ActualWidth, 0), nameHeader).X;
|
||||
Check(Math.Abs(nameHeader.ActualWidth - arrowRight - nameHeader.Padding.Right) <= 1, "sort arrow stays at the far right");
|
||||
grid.Columns[1].SortDirection = null;
|
||||
ScrollViewer viewer = (ScrollViewer)grid.Template.FindName("DG_ScrollViewer", grid);
|
||||
ScrollBar horizontalBar = (ScrollBar)viewer.Template.FindName("PART_HorizontalScrollBar", viewer);
|
||||
Check(Math.Abs(horizontalBar.TranslatePoint(new Point(), viewer).X) <= 1, "horizontal scrollbar starts at the left edge");
|
||||
|
||||
Check(grid.SelectionUnit == DataGridSelectionUnit.CellOrRowHeader, "demo uses cell or row header selection");
|
||||
DataGridCell businessCell = Descendants<DataGridCell>(row).First(cell => cell.Column == grid.Columns[1]);
|
||||
businessCell.RaiseEvent(new System.Windows.Input.MouseButtonEventArgs(System.Windows.Input.Mouse.PrimaryDevice, Environment.TickCount, System.Windows.Input.MouseButton.Left)
|
||||
{
|
||||
RoutedEvent = System.Windows.Input.Mouse.MouseDownEvent
|
||||
});
|
||||
Pump();
|
||||
Check(businessCell.IsSelected && !row.IsSelected && vm.SelectedStores.Count == 0, "business cell selects only a cell");
|
||||
Check(ReferenceEquals(businessCell.Background, grid.SelectedRowBackground), "selected cell has visible highlight");
|
||||
Border cellBackground = Descendants<Border>(businessCell).First();
|
||||
Check(ReferenceEquals(cellBackground.Background, grid.SelectedRowBackground), "cell template renders selected background");
|
||||
grid.SelectedCells.Clear();
|
||||
Pump();
|
||||
Check(!businessCell.IsSelected &&
|
||||
businessCell.Background is SolidColorBrush previousBrush && previousBrush.Color.A == 0,
|
||||
"clearing cell selection removes highlight");
|
||||
DataGridCell numberCell = Descendants<DataGridCell>(row).First(cell => cell.Column == grid.Columns[0]);
|
||||
numberCell.RaiseEvent(new System.Windows.Input.MouseButtonEventArgs(System.Windows.Input.Mouse.PrimaryDevice, Environment.TickCount, System.Windows.Input.MouseButton.Left)
|
||||
{
|
||||
RoutedEvent = System.Windows.Input.Mouse.PreviewMouseDownEvent
|
||||
});
|
||||
Pump();
|
||||
Check(row.IsSelected && vm.SelectedStores.Count == 1, "serial cell selects whole row");
|
||||
vm.SelectedStores.Clear();
|
||||
DataGridRowHeader rowHeader = (DataGridRowHeader)row.Template.FindName("PART_RowHeader", row);
|
||||
rowHeader.RaiseEvent(new System.Windows.Input.MouseButtonEventArgs(System.Windows.Input.Mouse.PrimaryDevice, Environment.TickCount, System.Windows.Input.MouseButton.Left)
|
||||
{
|
||||
RoutedEvent = System.Windows.Input.Mouse.PreviewMouseDownEvent
|
||||
});
|
||||
Check(row.IsSelected && vm.SelectedStores.Count == 1, "checkbox header selects row in mixed mode");
|
||||
vm.SelectedStores.Clear();
|
||||
DataGridLength[] columnWidths = grid.Columns.Select(column => column.Width).ToArray();
|
||||
foreach (DataGridColumn column in grid.Columns) { column.Width = 100; }
|
||||
window.Width = 1800;
|
||||
Pump();
|
||||
Point blankPoint = new(row.ActualWidth - 10, row.ActualHeight / 2);
|
||||
UIElement blankTarget = (UIElement)row.InputHitTest(blankPoint);
|
||||
Check(blankTarget != null, "row trailing space is hit-testable");
|
||||
blankTarget.RaiseEvent(new System.Windows.Input.MouseButtonEventArgs(System.Windows.Input.Mouse.PrimaryDevice, Environment.TickCount, System.Windows.Input.MouseButton.Left)
|
||||
{
|
||||
RoutedEvent = System.Windows.Input.Mouse.PreviewMouseDownEvent
|
||||
});
|
||||
Pump();
|
||||
Check(row.IsSelected && vm.SelectedStores.Contains((DemoStore)row.Item), "click trailing row space selects row");
|
||||
vm.SelectedStores.Clear();
|
||||
grid.ShowCheckBox = false;
|
||||
Pump();
|
||||
blankTarget = (UIElement)row.InputHitTest(new Point(row.ActualWidth - 10, row.ActualHeight / 2));
|
||||
blankTarget.RaiseEvent(new System.Windows.Input.MouseButtonEventArgs(System.Windows.Input.Mouse.PrimaryDevice, Environment.TickCount, System.Windows.Input.MouseButton.Left)
|
||||
{
|
||||
RoutedEvent = System.Windows.Input.Mouse.PreviewMouseDownEvent
|
||||
});
|
||||
Check(row.IsSelected, "trailing space selection works with hidden row checkbox");
|
||||
vm.SelectedStores.Clear();
|
||||
grid.ShowCheckBox = true;
|
||||
for (int index = 0; index < grid.Columns.Count; index++) { grid.Columns[index].Width = columnWidths[index]; }
|
||||
window.Width = 1280;
|
||||
Pump();
|
||||
DataGridEx.ToggleSelectAllCommand.Execute(null, grid);
|
||||
Check(vm.SelectedStores.Count == 50 && grid.AreAllRowsSelected == true, "select all publishes to VM");
|
||||
vm.SelectedStores.RemoveAt(0);
|
||||
Check(grid.SelectedItems.Count == 49 && grid.AreAllRowsSelected == null, "partial selection from VM");
|
||||
vm.SelectedStores.Clear();
|
||||
Check(grid.AreAllRowsSelected == false && grid.SelectionSummary == "选择记录后可批量操作", "clear selection");
|
||||
|
||||
DataGridEx.NextPageCommand.Execute(null, grid);
|
||||
Pump();
|
||||
Check(grid.PageIndex == 2 && grid.Items.Count == 50 && vm.SelectedStores.Count == 0, "next page and clear selection");
|
||||
row = (DataGridRow)grid.ItemContainerGenerator.ContainerFromIndex(0);
|
||||
Check(DataGridEx.GetRowNumber(row) == 51, "page offset for row number");
|
||||
grid.Items.SortDescriptions.Add(new SortDescription(nameof(DemoStore.Name), ListSortDirection.Descending));
|
||||
Pump();
|
||||
row = (DataGridRow)grid.ItemContainerGenerator.ContainerFromIndex(0);
|
||||
Check(DataGridEx.GetRowNumber(row) == 51, "sorting recomputes display positions");
|
||||
grid.ScrollIntoView(grid.Items[49]);
|
||||
Pump();
|
||||
row = (DataGridRow)grid.ItemContainerGenerator.ContainerFromIndex(49);
|
||||
Check(DataGridEx.GetRowNumber(row) == 100, "recycled row number");
|
||||
grid.ScrollIntoView(grid.Items[0]);
|
||||
Pump();
|
||||
|
||||
DataGridEx.LastPageCommand.Execute(null, grid);
|
||||
Pump();
|
||||
Check(grid.PageIndex == 5 && grid.Items.Count == 37 && !DataGridEx.NextPageCommand.CanExecute(null, grid), "last page boundary");
|
||||
grid.PageSize = 20;
|
||||
Pump();
|
||||
Check(grid.PageIndex == 1 && grid.PageCount == 12 && grid.Items.Count == 20, "page size resets to first page");
|
||||
|
||||
Button columns = (Button)grid.Template.FindName("PART_ColumnsButton", grid);
|
||||
columns.RaiseEvent(new RoutedEventArgs(ButtonBase.ClickEvent));
|
||||
Pump();
|
||||
MenuItem[] options = columns.ContextMenu.Items.OfType<MenuItem>().Where(item => item.IsCheckable).ToArray();
|
||||
Check(options.Length == 9 && !options[0].IsEnabled, "fixed name and configurable data columns");
|
||||
options[1].IsChecked = false;
|
||||
options[1].RaiseEvent(new RoutedEventArgs(MenuItem.ClickEvent));
|
||||
Check(grid.Columns[2].Visibility == Visibility.Collapsed, "hide column");
|
||||
options[1].IsChecked = true;
|
||||
options[1].RaiseEvent(new RoutedEventArgs(MenuItem.ClickEvent));
|
||||
Check(grid.Columns[2].Visibility == Visibility.Visible, "show column");
|
||||
Check(columns.ContextMenu.ActualWidth == 240, "column settings uses compact panel width");
|
||||
Button done = (Button)columns.ContextMenu.Template.FindName("PART_DoneButton", columns.ContextMenu);
|
||||
Check(done != null && done.Content.Equals("完成"), "column settings has a footer action");
|
||||
System.Windows.Input.ApplicationCommands.Close.Execute(null, done);
|
||||
Pump();
|
||||
Check(!columns.ContextMenu.IsOpen, "done closes column settings");
|
||||
|
||||
grid.ShowCheckBox = false;
|
||||
grid.ShowRowNumber = false;
|
||||
grid.ShowToolbar = false;
|
||||
grid.ShowPagination = false;
|
||||
Pump();
|
||||
Check(grid.RowHeaderWidth == 0 && grid.Columns[0].Visibility == Visibility.Collapsed, "independent header options");
|
||||
grid.ShowCheckBox = true;
|
||||
grid.ShowRowNumber = true;
|
||||
grid.ShowToolbar = true;
|
||||
grid.ShowPagination = true;
|
||||
Pump();
|
||||
Check(grid.RowHeaderWidth == 32 && grid.Columns[0].Visibility == Visibility.Visible, "restore options");
|
||||
|
||||
FrameworkElement defaultPagination = (FrameworkElement)grid.Template.FindName("PART_DefaultPagination", grid);
|
||||
Check(defaultPagination.IsVisible, "default pagination visible without custom content");
|
||||
Button customNext = new() { Content = "自定义下一页", Command = DataGridEx.NextPageCommand };
|
||||
grid.PaginationContent = customNext;
|
||||
Pump();
|
||||
Check(customNext.IsVisible && !defaultPagination.IsVisible, "custom pagination replaces default");
|
||||
Check(ReferenceEquals(customNext.DataContext, vm), "pagination inherits caller DataContext");
|
||||
int previousPage = grid.PageIndex;
|
||||
DataGridEx.NextPageCommand.Execute(null, customNext);
|
||||
Pump();
|
||||
Check(grid.PageIndex == previousPage + 1, "custom pagination routes page command");
|
||||
grid.ShowPagination = false;
|
||||
Pump();
|
||||
Check(!customNext.IsVisible, "ShowPagination hides custom content");
|
||||
grid.ShowPagination = true;
|
||||
grid.PaginationContent = null;
|
||||
Pump();
|
||||
Check(defaultPagination.IsVisible, "clearing custom content restores default");
|
||||
DataGridEx.FirstPageCommand.Execute(null, grid);
|
||||
|
||||
Button renew = new() { Content = "批量续期(模拟)" };
|
||||
renew.SetBinding(Button.CommandProperty, new System.Windows.Data.Binding(nameof(DataGridTestViewModel.RenewCommand)) { Mode = System.Windows.Data.BindingMode.OneWay });
|
||||
grid.ToolbarContent = renew;
|
||||
Pump();
|
||||
Check(ReferenceEquals(renew.Command, vm.RenewCommand), "toolbar inherits caller DataContext");
|
||||
grid.SelectedItems.Add(grid.Items[0]);
|
||||
vm.RenewCommand.Execute();
|
||||
Check(vm.SelectedStores[0].ExpiresAt == new DateTime(2027, 5, 9), "batch command changes selected record");
|
||||
vm.EmptyCommand.Execute();
|
||||
Pump();
|
||||
Check(grid.Items.Count == 0 && grid.PageIndex == 1 && grid.PageCount == 1 && !DataGridEx.NextPageCommand.CanExecute(null, grid), "empty page boundary");
|
||||
vm.RestoreCommand.Execute();
|
||||
Pump();
|
||||
Check(grid.Items.Count == 20 && grid.TotalItemCount == 237, "restore keeps page size");
|
||||
ICollectionView originalView = vm.CurrentItems;
|
||||
Check(originalView is System.Windows.Data.ListCollectionView, "current page uses independent ListCollectionView");
|
||||
Check(!ReferenceEquals(originalView, System.Windows.Data.CollectionViewSource.GetDefaultView(originalView.SourceCollection)), "view is independent from default collection view");
|
||||
vm.CurrentItems.SortDescriptions.Clear();
|
||||
vm.CurrentItems.SortDescriptions.Add(new SortDescription(nameof(DemoStore.StoreId), ListSortDirection.Descending));
|
||||
DemoStore firstStore = vm.CurrentItems.Cast<DemoStore>().First();
|
||||
vm.SelectedStores.Add(firstStore);
|
||||
vm.LocalKeyword = firstStore.StoreId.ToUpperInvariant();
|
||||
Pump();
|
||||
Check(grid.Items.Count == 1 && vm.CurrentItems.Cast<DemoStore>().First() == firstStore && vm.TotalCount == 237 && vm.SelectedStores.Count == 0, "local search filters only loaded page and clears selection");
|
||||
vm.LocalKeyword = "no-such-store";
|
||||
Pump();
|
||||
Check(grid.Items.Count == 0 && grid.PageCount == 12, "local no matches preserves pagination");
|
||||
vm.LocalKeyword = string.Empty;
|
||||
Pump();
|
||||
Check(grid.Items.Count == 20, "clearing local search restores current page");
|
||||
Check(ReferenceEquals(originalView, vm.CurrentItems) && vm.CurrentItems.SortDescriptions.Count == 1 &&
|
||||
vm.CurrentItems.Cast<DemoStore>().First() == firstStore, "local filter preserves view identity and sorting");
|
||||
|
||||
DataGridEx.NextPageCommand.Execute(null, grid);
|
||||
Pump();
|
||||
DemoStore secondPageStore = vm.CurrentItems.Cast<DemoStore>().First();
|
||||
vm.BackendKeyword = secondPageStore.StoreId;
|
||||
Check(vm.TotalCount == 237 && grid.PageIndex == 2, "typing backend keyword does not search");
|
||||
vm.LocalKeyword = "no-such-store";
|
||||
RunSearch(vm, grid);
|
||||
Pump();
|
||||
Check(vm.LocalKeyword == string.Empty && grid.PageIndex == 1 && vm.TotalCount == 1 && vm.CurrentItems.Cast<DemoStore>().Single() == secondPageStore, "backend query searches all data and resets page/local filter");
|
||||
vm.BackendKeyword = "小店";
|
||||
RunSearch(vm, grid);
|
||||
Pump();
|
||||
Check(vm.TotalCount == 237 && grid.Items.Count == 20, "backend results paginate");
|
||||
DataGridEx.NextPageCommand.Execute(null, grid);
|
||||
Pump();
|
||||
Check(vm.CurrentItems.Cast<DemoStore>().First() == secondPageStore, "backend query results persist across pages");
|
||||
Check(ReferenceEquals(originalView, vm.CurrentItems), "paging and backend search retain independent view");
|
||||
vm.BackendKeyword = "no-such-store";
|
||||
RunSearch(vm, grid);
|
||||
Pump();
|
||||
Check(vm.TotalCount == 0 && grid.Items.Count == 0 && grid.PageIndex == 1, "backend no results");
|
||||
vm.BackendKeyword = string.Empty;
|
||||
RunSearch(vm, grid);
|
||||
Pump();
|
||||
Check(vm.TotalCount == 237 && grid.Items.Count == 20, "empty backend keyword restores all results");
|
||||
Check(errors.Messages.Count == 0, "binding errors: " + string.Join(Environment.NewLine, errors.Messages));
|
||||
Console.WriteLine("PASS: template loading, Bogus data, separate selection/number columns, tri-state selection, VM synchronization, paging, sorting/recycling numbers, column settings, visibility toggles and toolbar commands.");
|
||||
return 0;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine(exception);
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
window?.Close();
|
||||
app.Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
private static void RunSearch(DataGridTestViewModel vm, DataGridEx grid)
|
||||
{
|
||||
Task search = Dispatcher.CurrentDispatcher.Invoke(() => vm.SearchCommand.Execute());
|
||||
Pump();
|
||||
FrameworkElement overlay = (FrameworkElement)grid.Template.FindName("PART_LoadingOverlay", grid);
|
||||
FrameworkElement table = (FrameworkElement)grid.Template.FindName("PART_TableArea", grid);
|
||||
ProgressBar progress = (ProgressBar)grid.Template.FindName("PART_LoadingProgress", grid);
|
||||
Check(vm.LoadingStatus.IsLoading && overlay.IsVisible && progress.IsIndeterminate && !table.IsEnabled, "loading animation and interaction lock");
|
||||
Check(!vm.SearchCommand.CanExecute(), "prevent duplicate searches");
|
||||
Stopwatch timeout = Stopwatch.StartNew();
|
||||
while (!search.IsCompleted && timeout.Elapsed < TimeSpan.FromSeconds(5))
|
||||
{
|
||||
Pump();
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
Check(search.IsCompleted, "search completes");
|
||||
search.GetAwaiter().GetResult();
|
||||
Pump();
|
||||
Check(!overlay.IsVisible && !progress.IsIndeterminate && table.IsEnabled, "loading animation stops and interaction restored");
|
||||
}
|
||||
private static void Pump() => Dispatcher.CurrentDispatcher.Invoke(() => { }, DispatcherPriority.ApplicationIdle);
|
||||
private static void Check(bool condition, string message)
|
||||
{
|
||||
if (!condition)
|
||||
{
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<T> Descendants<T>(DependencyObject parent) where T : DependencyObject
|
||||
{
|
||||
for (int index = 0; index < VisualTreeHelper.GetChildrenCount(parent); index++)
|
||||
{
|
||||
DependencyObject child = VisualTreeHelper.GetChild(parent, index);
|
||||
if (child is T match)
|
||||
{
|
||||
yield return match;
|
||||
}
|
||||
foreach (T descendant in Descendants<T>(child))
|
||||
{
|
||||
yield return descendant;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class BindingErrors : TraceListener
|
||||
{
|
||||
public List<string> Messages { get; } = [];
|
||||
public override void Write(string? message) { }
|
||||
public override void WriteLine(string? message)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(message))
|
||||
{
|
||||
Messages.Add(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0-windows</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../Source/Store.Management/Store.Management.csproj"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in new issue
Block a user