DataGrid 高级用法
大约 10 分钟约 2969 字
DataGrid 高级用法
简介
DataGrid 是 WPF 中最常用的数据展示控件,用于显示和编辑表格数据。掌握 DataGrid 的列定制、行详情、虚拟化、分组排序、导出等高级用法,可以构建功能丰富的数据管理界面,满足企业级应用的数据展示需求。在工业上位机中,DataGrid 常用于设备列表、报警记录、配方参数和巡检报告的展示与编辑。
DataGrid 核心架构
DataGrid
├── Columns(列集合)
│ ├── DataGridTextColumn — 文本列
│ ├── DataGridComboBoxColumn — 下拉选择列
│ ├── DataGridCheckBoxColumn — 复选框列
│ ├── DataGridHyperlinkColumn — 超链接列
│ └── DataGridTemplateColumn — 模板列(最灵活)
├── RowDetailsTemplate — 行详情模板
├── GroupStyle — 分组样式
├── ValidationRules — 验证规则
├── CellStyles / RowStyles — 单元格/行样式
└── VirtualizingPanel — 虚拟化面板数据绑定流程
ObservableCollection<T> → ItemsSource → DataGrid 渲染
↓ 更改 ↓
INotifyPropertyChanged → 单元格刷新
ICollectionView → 分组/排序/过滤特点
实现
列定制
多种列类型
<!-- ========== DataGrid 列类型大全 ========== -->
<DataGrid x:Name="OrderGrid"
AutoGenerateColumns="False"
ItemsSource="{Binding Orders}"
SelectedItem="{Binding SelectedOrder}"
IsReadOnly="False"
CanUserAddRows="False"
CanUserDeleteRows="False"
SelectionMode="Single"
SelectionUnit="FullRow"
AlternatingRowBackground="#F8F8F8"
GridLinesVisibility="Horizontal"
HeadersVisibility="Column"
RowHeight="32"
ColumnHeaderHeight="36"
FontSize="13">
<!-- 1. 文本列 — 只读 -->
<DataGridTextColumn Header="订单号"
Binding="{Binding OrderNo}"
IsReadOnly="True"
Width="120"
ElementStyle="{StaticResource DataGridTextBlockStyle}"/>
<!-- 2. 文本列 — 可编辑 -->
<DataGridTextColumn Header="客户名称"
Binding="{Binding Customer, UpdateSourceTrigger=PropertyChanged}"
Width="*"
ElementStyle="{StaticResource DataGridTextBlockStyle}"
EditingElementStyle="{StaticResource DataGridTextBoxStyle}"/>
<!-- 3. 下拉列 -->
<DataGridComboBoxColumn Header="状态" Width="100"
SelectedItemBinding="{Binding Status, UpdateSourceTrigger=PropertyChanged}">
<DataGridComboBoxColumn.ElementStyle>
<Style TargetType="ComboBox">
<Setter Property="ItemsSource" Value="{Binding Source={StaticResource StatusList}}"/>
</Style>
</DataGridComboBoxColumn.ElementStyle>
<DataGridComboBoxColumn.EditingElementStyle>
<Style TargetType="ComboBox">
<Setter Property="ItemsSource" Value="{Binding Source={StaticResource StatusList}}"/>
</Style>
</DataGridComboBoxColumn.EditingElementStyle>
</DataGridComboBoxColumn>
<!-- 4. 复选框列 -->
<DataGridCheckBoxColumn Header="加急"
Binding="{Binding IsUrgent, UpdateSourceTrigger=PropertyChanged}"
Width="60"
ElementStyle="{StaticResource DataGridCheckBoxStyle}"/>
<!-- 5. 模板列 — 自定义显示和编辑 -->
<DataGridTemplateColumn Header="金额" Width="120">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Amount, StringFormat='¥{0:N2}'}"
Foreground="{Binding Amount, Converter={StaticResource AmountColorConverter}}"
VerticalAlignment="Center" Margin="8,0"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox Text="{Binding Amount, StringFormat='N2', UpdateSourceTrigger=PropertyChanged}"
VerticalContentAlignment="Center" Margin="4,0"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<!-- 6. 日期列 -->
<DataGridTemplateColumn Header="创建时间" Width="150">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding CreatedAt, StringFormat='yyyy-MM-dd HH:mm'}"
VerticalAlignment="Center" Margin="8,0"
Foreground="{DynamicResource SecondaryTextBrush}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<DatePicker SelectedDate="{Binding CreatedAt}"
VerticalContentAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<!-- 7. 按钮列 -->
<DataGridTemplateColumn Header="操作" Width="Auto" MinWidth="160">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="4,0">
<Button Content="编辑" Command="{Binding DataContext.EditCommand,
RelativeSource={RelativeSource AncestorType=DataGrid}}"
Style="{StaticResource SmallButtonStyle}" Margin="0,0,4,0"/>
<Button Content="删除" Command="{Binding DataContext.DeleteCommand,
RelativeSource={RelativeSource AncestorType=DataGrid}}"
Style="{StaticResource DangerButtonStyle}"/>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid>行详情
RowDetailsTemplate
<!-- ========== 行详情模板 ========== -->
<DataGrid ItemsSource="{Binding Orders}" SelectedItem="{Binding SelectedOrder}">
<DataGrid.RowDetailsTemplate>
<DataTemplate>
<Border Background="{DynamicResource SurfaceBrush}"
Padding="16" Margin="4,0,4,8" CornerRadius="6"
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1">
<Grid Margin="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="客户:"
FontWeight="SemiBold" Margin="0,0,10,6"
Foreground="{DynamicResource SecondaryTextBrush}"/>
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Customer}"
Margin="0,0,0,6"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="地址:"
FontWeight="SemiBold" Margin="0,0,10,6"
Foreground="{DynamicResource SecondaryTextBrush}"/>
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Address}"
Margin="0,0,0,6" TextWrapping="Wrap"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="备注:"
FontWeight="SemiBold" Margin="0,0,10,6"
Foreground="{DynamicResource SecondaryTextBrush}"/>
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding Remark}"
TextWrapping="Wrap"/>
<!-- 订单明细子表 -->
<DataGrid Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2"
ItemsSource="{Binding Details}"
AutoGenerateColumns="False" IsReadOnly="True"
Margin="0,8,0,0" MaxHeight="150"
HeadersVisibility="Column" FontSize="12">
<DataGrid.Columns>
<DataGridTextColumn Header="产品" Binding="{Binding Product}" Width="*"/>
<DataGridTextColumn Header="数量" Binding="{Binding Quantity}" Width="80"/>
<DataGridTextColumn Header="单价" Binding="{Binding UnitPrice,
StringFormat='¥{0:N2}'}" Width="100"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Border>
</DataTemplate>
</DataGrid.RowDetailsTemplate>
</DataGrid>分组排序
CollectionView 分组
// ========== DataGrid 分组、排序、过滤 ==========
/// <summary>
/// DataGrid 分组排序 ViewModel
/// </summary>
public class OrderListViewModel : ObservableObject
{
private readonly IOrderService _orderService;
private ICollectionView? _ordersView;
[ObservableProperty]
private string _searchKeyword = "";
[ObservableProperty]
private string _groupByField = "Category";
[ObservableProperty]
private bool _isGrouped = true;
public ICollectionView? OrdersView
{
get => _ordersView;
private set => SetProperty(ref _ordersView, value);
}
public OrderListViewModel(IOrderService orderService)
{
_orderService = orderService;
}
/// <summary>
/// 加载数据并设置视图
/// </summary>
[RelayCommand]
private void LoadData()
{
var orders = _orderService.GetAll();
OrdersView = CollectionViewSource.GetDefaultView(orders);
// 分组
ApplyGrouping();
// 默认排序
OrdersView.SortDescriptions.Add(
new SortDescription("CreatedAt", ListSortDirection.Descending));
// 默认过滤
ApplyFilter();
OrdersView.Refresh();
}
/// <summary>
/// 应用分组
/// </summary>
partial void OnGroupByFieldChanged(string value)
{
ApplyGrouping();
}
private void ApplyGrouping()
{
if (OrdersView == null) return;
OrdersView.GroupDescriptions.Clear();
if (IsGrouped && !string.IsNullOrEmpty(GroupByField))
{
OrdersView.GroupDescriptions.Add(
new PropertyGroupDescription(GroupByField));
}
OrdersView.Refresh();
}
/// <summary>
/// 搜索过滤
/// </summary>
partial void OnSearchKeywordChanged(string value)
{
ApplyFilter();
}
private void ApplyFilter()
{
if (OrdersView == null) return;
var keyword = SearchKeyword.Trim();
OrdersView.Filter = item =>
{
if (string.IsNullOrEmpty(keyword)) return true;
var order = (Order)item;
return order.OrderNo.Contains(keyword, StringComparison.OrdinalIgnoreCase) ||
order.Customer.Contains(keyword, StringComparison.OrdinalIgnoreCase) ||
order.Status.Contains(keyword, StringComparison.OrdinalIgnoreCase);
};
OrdersView.Refresh();
}
/// <summary>
/// 切换排序
/// </summary>
[RelayCommand]
private void ToggleSort(string propertyName)
{
if (OrdersView == null) return;
var existing = OrdersView.SortDescriptions
.FirstOrDefault(s => s.PropertyName == propertyName);
var direction = existing.Direction == ListSortDirection.Ascending
? ListSortDirection.Descending
: ListSortDirection.Ascending;
OrdersView.SortDescriptions.Clear();
OrdersView.SortDescriptions.Add(new SortDescription(propertyName, direction));
}
}分组样式
<!-- ========== 分组样式 ========== -->
<DataGrid ItemsSource="{Binding OrdersView}">
<DataGrid.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<Expander IsExpanded="True" Margin="0,2,0,2">
<Expander.Header>
<DockPanel>
<TextBlock Text="{Binding Name}"
FontWeight="SemiBold" FontSize="14"
DockPanel.Dock="Left" Margin="0,0,8,0"/>
<TextBlock Text="{Binding ItemCount, StringFormat='({0} 条)'}"
Foreground="Gray" FontSize="12"
VerticalAlignment="Center"/>
</DockPanel>
</Expander.Header>
<ItemsPresenter Margin="16,4,0,4"/>
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</DataGrid.GroupStyle>
</DataGrid>数据验证
// ========== DataGrid 数据验证 ==========
/// <summary>
/// 订单模型 — 带 INotifyDataErrorInfo 验证
/// </summary>
public class Order : ObservableObject, INotifyDataErrorInfo
{
private string _orderNo = "";
private string _customer = "";
private decimal _amount;
public string OrderNo
{
get => _orderNo;
set => SetProperty(ref _orderNo, value, ValidateOrderNo);
}
public string Customer
{
get => _customer;
set => SetProperty(ref _customer, value, ValidateCustomer);
}
public decimal Amount
{
get => _amount;
set => SetProperty(ref _amount, value, ValidateAmount);
}
// 验证:订单号不能为空
private IEnumerable<string> ValidateOrderNo(string value)
{
if (string.IsNullOrWhiteSpace(value))
yield return "订单号不能为空";
else if (value.Length > 20)
yield return "订单号不能超过20个字符";
}
// 验证:客户名称不能为空
private IEnumerable<string> ValidateCustomer(string value)
{
if (string.IsNullOrWhiteSpace(value))
yield return "客户名称不能为空";
}
// 验证:金额必须大于0
private IEnumerable<string> ValidateAmount(decimal value)
{
if (value <= 0)
yield return "金额必须大于0";
else if (value > 999999999)
yield return "金额超出范围";
}
// INotifyDataErrorInfo 实现
private readonly Dictionary<string, List<string>> _errors = new();
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
public bool HasErrors => _errors.Count > 0;
public IEnumerable<string> GetErrors(string? propertyName)
{
return propertyName != null && _errors.TryGetValue(propertyName, out var errors)
? errors
: Enumerable.Empty<string>();
}
private void SetErrors(string propertyName, IEnumerable<string> errors)
{
if (errors.Any())
{
_errors[propertyName] = errors.ToList();
}
else
{
_errors.Remove(propertyName);
}
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
OnPropertyChanged(nameof(HasErrors));
}
}虚拟化与性能
大数据量优化
<!-- ========== DataGrid 虚拟化配置 ========== -->
<DataGrid EnableRowVirtualization="True"
EnableColumnVirtualization="True"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling"
VirtualizingPanel.CacheLength="10,20"
VirtualizingPanel.CacheLengthUnit="Item"
ScrollViewer.IsDeferredScrollingEnabled="False"
AutoGenerateColumns="False">
<!-- 启用行虚拟化(默认已启用)-->
<!-- 启用列虚拟化 -->
<!-- Recycling 模式:复用行容器,性能最佳 -->
<!-- CacheLength:预渲染 10 行,缓存 20 行 -->
</DataGrid>分页加载
// ========== DataGrid 分页 ViewModel ==========
/// <summary>
/// 分页 ViewModel — 大数据量分页加载
/// </summary>
public class PaginationViewModel : ObservableObject
{
private readonly IOrderService _orderService;
private int _pageSize = 50;
private int _currentPage = 1;
public ObservableCollection<Order> CurrentPageData { get; } = new();
[ObservableProperty]
private int _totalCount;
[ObservableProperty]
private int _totalPages = 1;
[ObservableProperty]
private int _currentPageDisplay = 1;
public bool HasPreviousPage => CurrentPageDisplay > 1;
public bool HasNextPage => CurrentPageDisplay < TotalPages;
public PaginationViewModel(IOrderService orderService)
{
_orderService = orderService;
}
/// <summary>
/// 加载指定页
/// </summary>
[RelayCommand]
private async Task LoadPageAsync(int page)
{
_currentPage = page;
var data = await _orderService.GetPagedAsync(_currentPage, _pageSize);
CurrentPageData.Clear();
foreach (var item in data.Items)
CurrentPageData.Add(item);
TotalCount = data.TotalCount;
TotalPages = (int)Math.Ceiling((double)TotalCount / _pageSize);
CurrentPageDisplay = _currentPage;
OnPropertyChanged(nameof(HasPreviousPage));
OnPropertyChanged(nameof(HasNextPage));
}
[RelayCommand]
private Task NextPageAsync() => LoadPageAsync(_currentPage + 1);
[RelayCommand]
private Task PrevPageAsync() => LoadPageAsync(Math.Max(1, _currentPage - 1));
[RelayCommand]
private Task FirstPageAsync() => LoadPageAsync(1);
[RelayCommand]
private Task LastPageAsync() => LoadPageAsync(TotalPages);
}数据导出
// ========== DataGrid 数据导出 ==========
/// <summary>
/// DataGrid 导出工具
/// </summary>
public static class DataGridExporter
{
/// <summary>
/// 导出为 CSV
/// </summary>
public static void ExportToCsv<T>(IEnumerable<T> data, string filePath,
string[] headers, Func<T, string[]> rowSelector)
{
using var writer = new StreamWriter(filePath, false, Encoding.UTF8);
writer.WriteLine(string.Join(",", headers));
foreach (var item in data)
{
var values = rowSelector(item)
.Select(v => v.Contains(',') ? $"\"{v}\"" : v);
writer.WriteLine(string.Join(",", values));
}
}
/// <summary>
/// 导出为 Excel(使用 ClosedXML)
/// </summary>
public static void ExportToExcel<T>(IEnumerable<T> data, string filePath,
string[] headers, Func<T, object[]> rowSelector)
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("数据");
// 写入表头
for (int i = 0; i < headers.Length; i++)
worksheet.Cell(1, i + 1).Value = headers[i];
// 写入数据
int row = 2;
foreach (var item in data)
{
var values = rowSelector(item);
for (int col = 0; col < values.Length; col++)
worksheet.Cell(row, col + 1).Value = values[col];
row++;
}
// 自动调整列宽
worksheet.Columns().AdjustToContents();
workbook.SaveAs(filePath);
}
/// <summary>
/// 导出 DataGrid 视觉内容为图片
/// </summary>
public static BitmapSource ExportToImage(DataGrid dataGrid, double dpi = 96)
{
var size = new Size(dataGrid.ActualWidth, dataGrid.ActualHeight);
var renderTarget = new RenderTargetBitmap(
(int)size.Width, (int)size.Height, dpi, dpi, PixelFormats.Pbgra32);
renderTarget.Render(dataGrid);
renderTarget.Freeze();
return renderTarget;
}
}DataGrid 样式定制
<!-- ========== DataGrid 样式定制 ========== -->
<Style TargetType="DataGrid">
<Setter Property="Background" Value="{DynamicResource BackgroundBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource BorderBrush}"/>
<Setter Property="RowBackground" Value="{DynamicResource BackgroundBrush}"/>
<Setter Property="AlternatingRowBackground" Value="{DynamicResource SurfaceBrush}"/>
<Setter Property="HeadersVisibility" Value="Column"/>
<Setter Property="GridLinesVisibility" Value="Horizontal"/>
<Setter Property="AutoGenerateColumns" Value="False"/>
<Setter Property="IsReadOnly" Value="False"/>
<Setter Property="CanUserAddRows" Value="False"/>
<Setter Property="SelectionUnit" Value="FullRow"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="RowHeight" Value="32"/>
<Setter Property="ColumnHeaderHeight" Value="36"/>
</Style>
<!-- 表头样式 -->
<Style TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="{DynamicResource SurfaceBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Padding" Value="8,0"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="SeparatorBrush" Value="{DynamicResource BorderBrush}"/>
</Style>
<!-- 行选中样式 -->
<Style TargetType="DataGridRow">
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="{DynamicResource PrimaryBrush}"/>
<Setter Property="Foreground" Value="White"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource SurfaceVariantBrush}"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- 验证错误样式 -->
<Style TargetType="DataGridCell">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="BorderBrush" Value="Red"/>
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>优点
缺点
总结
DataGrid 高级用法:模板列自定义显示和编辑,RowDetailsTemplate 展开行详情,CollectionView 实现分组排序过滤。性能优化:启用虚拟化(Recycling 模式)+ 分页加载。建议大数据量场景使用分页而非一次性加载全部数据。数据验证使用 INotifyDataErrorInfo 接口。
关键知识点
- DataGridTemplateColumn 最灵活,支持自定义显示和编辑模板。
- RowDetailsTemplate 可以展开显示行的附加信息。
- CollectionView 提供分组、排序和过滤功能。
- VirtualizationMode.Recycling 复用行容器,性能最佳。
- INotifyDataErrorInfo 是 WPF 数据验证的标准接口。
- DataGrid 按钮列的命令绑定需要 RelativeSource 找到 DataContext。
项目落地视角
- 封装通用的 DataGrid 样式资源字典,保持全局风格一致。
- 大数据量使用分页加载,避免一次性加载全部数据。
- 为导出功能封装为通用工具类。
- 数据验证使用 INotifyDataErrorInfo,在模型层实现。
常见误区
- 在 CellTemplate 中直接绑定命令,忘记使用 RelativeSource。
- 大数据量不启用虚拟化,导致滚动卡顿。
- DataGridComboBoxColumn 的 ItemsSource 绑定错误(需要 StaticResource)。
- 忘记设置 AutoGenerateColumns="False" 导致列重复。
- 编辑时没有设置 UpdateSourceTrigger=PropertyChanged 导致输入延迟。
进阶路线
- 学习第三方 DataGrid 库(如 DevExpress、Telerik、Syncfusion)。
- 实现单元格合并、冻结列、汇总行等高级功能。
- 研究 DataGrid 的虚拟化原理和自定义 IRecyclingItemContainerGenerator。
- 实现拖拽列排序和列宽持久化。
适用场景
- 设备列表、报警记录、巡检报告的展示与编辑。
- 配方参数表格编辑。
- 数据查询结果的展示和导出。
落地建议
- 封装通用的 DataGrid 样式,统一表格外观。
- 为常用列类型(状态、金额、日期)定义标准模板。
- 提供搜索、分组、导出等通用功能。
- 分页加载配合后台查询,避免一次性加载大量数据。
排错清单
- 检查 ItemsSource 绑定是否正确(ObservableCollection)。
- 确认 INotifyPropertyChanged 是否正确实现。
- 检查 ComboBox 列的 ItemsSource 是否正确绑定。
- 确认虚拟化是否启用(VirtualizingPanel.IsVirtualizing="True")。
- 检查验证规则是否正确返回错误信息。
复盘问题
- 如果把《DataGrid 高级用法》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《DataGrid 高级用法》最容易在什么规模、什么边界条件下暴露问题?
- 相比默认实现或替代方案,采用《DataGrid 高级用法》最大的收益和代价分别是什么?
