如何在代码定义的通用DataGrid中实现指定相同值单元格合并?
Generic WPF DataGrid Vertical Cell Merging (Code-Based, ItemsSource-Bound)
Got it, let's build a fully generic, code-only solution for merging vertical cells in a WPF DataGrid—no XAML column definitions required, and it works with any ItemsSource. This avoids the limitations of XAML-based approaches you mentioned, making it reusable across different view models and projects.
Core Approach
We'll create an attached behavior to handle cell merging logic. This keeps the logic decoupled from the DataGrid itself, making it easy to enable/disable and reuse. The behavior will:
- Listen for auto-generated columns (since we're not defining them in XAML)
- Compare each cell's value with the cell above it when rows load
- Adjust
RowSpanof the previous cell and hide the current cell if values match - Reset merging state when the
ItemsSourcechanges
Step 1: Create the Cell Merging Helper Class
This class contains the attached property and all merging logic:
using System.Collections.Generic; using System.Windows; using System.Windows.Controls; using System.Windows.Media; public static class DataGridCellMergingHelper { public static readonly DependencyProperty EnableCellMergingProperty = DependencyProperty.RegisterAttached("EnableCellMerging", typeof(bool), typeof(DataGridCellMergingHelper), new PropertyMetadata(false, OnEnableCellMergingChanged)); public static bool GetEnableCellMerging(DependencyObject obj) { return (bool)obj.GetValue(EnableCellMergingProperty); } public static void SetEnableCellMerging(DependencyObject obj, bool value) { obj.SetValue(EnableCellMergingProperty, value); } private static void OnEnableCellMergingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is DataGrid dataGrid) { if ((bool)e.NewValue) { dataGrid.AutoGeneratingColumn += OnAutoGeneratingColumn; dataGrid.LoadingRow += OnLoadingRow; dataGrid.ItemsSourceChanged += OnItemsSourceChanged; } else { dataGrid.AutoGeneratingColumn -= OnAutoGeneratingColumn; dataGrid.LoadingRow -= OnLoadingRow; dataGrid.ItemsSourceChanged -= OnItemsSourceChanged; } } } private static void OnItemsSourceChanged(object sender, DependencyPropertyChangedEventArgs e) { // Reset all merging state when the data source changes if (sender is DataGrid dataGrid) { foreach (var rowItem in dataGrid.Items) { if (dataGrid.ItemContainerGenerator.ContainerFromItem(rowItem) is DataGridRow row) { ResetRowCellMerging(row); } } } } private static void OnAutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e) { // Ensure auto-generated columns use a template that supports merging if (e.Column is DataGridTextColumn textColumn) { var template = new DataTemplate(); var textBlockFactory = new FrameworkElementFactory(typeof(TextBlock)); textBlockFactory.SetBinding(TextBlock.TextProperty, textColumn.Binding); textBlockFactory.SetValue(TextBlock.PaddingProperty, new Thickness(5)); textBlockFactory.SetValue(TextBlock.VerticalAlignmentProperty, VerticalAlignment.Center); template.VisualTree = textBlockFactory; textColumn.CellTemplate = template; } // Add support for other column types (e.g., DataGridCheckBoxColumn) here if needed } private static void OnLoadingRow(object sender, DataGridRowEventArgs e) { if (sender is not DataGrid dataGrid) return; var currentItem = e.Row.Item; var currentRowIndex = dataGrid.Items.IndexOf(currentItem); // Check each column for potential merging with the row above foreach (DataGridColumn column in dataGrid.Columns) { var currentCellValue = GetCellValue(currentItem, column.SortMemberPath); var prevRowIndex = currentRowIndex - 1; if (prevRowIndex >= 0) { var prevItem = dataGrid.Items[prevRowIndex]; var prevCellValue = GetCellValue(prevItem, column.SortMemberPath); if (Equals(currentCellValue, prevCellValue)) { var prevRow = dataGrid.ItemContainerGenerator.ContainerFromIndex(prevRowIndex) as DataGridRow; if (prevRow == null) continue; var prevCell = GetDataGridCell(prevRow, column); var currentCell = GetDataGridCell(e.Row, column); if (prevCell != null && currentCell != null) { // Expand the previous cell's row span Grid.SetRowSpan(prevCell, Grid.GetRowSpan(prevCell) + 1); // Hide the current cell since it's merged currentCell.Visibility = Visibility.Collapsed; } } } } } private static object GetCellValue(object item, string propertyName) { // Use reflection to get the cell's value from the bound item return item.GetType().GetProperty(propertyName)?.GetValue(item); } private static DataGridCell GetDataGridCell(DataGridRow row, DataGridColumn column) { // Helper to find the cell element in the visual tree column.CellWidth = DataGridLength.Auto; row.UpdateLayout(); var cellsPresenter = FindVisualChild<DataGridCellsPresenter>(row); cellsPresenter?.UpdateLayout(); return column.GetCellContent(cellsPresenter)?.Parent as DataGridCell; } private static T FindVisualChild<T>(DependencyObject parent) where T : DependencyObject { // Generic visual tree traversal helper for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++) { var child = VisualTreeHelper.GetChild(parent, i); if (child is T typedChild) return typedChild; var result = FindVisualChild<T>(child); if (result != null) return result; } return null; } private static void ResetRowCellMerging(DataGridRow row) { // Reset row span and visibility for all cells in the row var dataGrid = (DataGrid)row.DataContext; foreach (DataGridColumn column in dataGrid.Columns) { var cell = GetDataGridCell(row, column); if (cell != null) { Grid.SetRowSpan(cell, 1); cell.Visibility = Visibility.Visible; } } } }
Step 2: Use the Helper with a Code-Generated DataGrid
Here's how to create a DataGrid dynamically, bind it to your ItemsSource, and enable merging:
// In your window/user control's code-behind private void SetupMergedDataGrid() { // Create the DataGrid instance var mergedDataGrid = new DataGrid { AutoGenerateColumns = true, // Critical for generic column generation CanUserAddRows = false, CanUserDeleteRows = false, Margin = new Thickness(10) }; // Enable cell merging using our helper DataGridCellMergingHelper.SetEnableCellMerging(mergedDataGrid, true); // Bind to your data source (replace with your own collection) var productData = new List<Product> { new Product { Category = "Electronics", Name = "Laptop", Price = 999.99m }, new Product { Category = "Electronics", Name = "Smartphone", Price = 699.99m }, new Product { Category = "Electronics", Name = "Tablet", Price = 499.99m }, new Product { Category = "Home Appliances", Name = "Blender", Price = 49.99m }, new Product { Category = "Home Appliances", Name = "Toaster", Price = 29.99m } }; mergedDataGrid.ItemsSource = productData; // Add the DataGrid to your layout (e.g., a parent Grid) MainLayoutGrid.Children.Add(mergedDataGrid); } // Example model class (replace with your own data model) public class Product { public string Category { get; set; } public string Name { get; set; } public decimal Price { get; set; } }
Key Notes & Enhancements
- Sorting Requirement: For merging to work correctly, your
ItemsSourceshould be sorted by the columns you want to merge. If not, adjacent cells won't have matching values, and merging won't trigger. - Support for Other Column Types: The current code handles
DataGridTextColumn, but you can extendOnAutoGeneratingColumnto supportDataGridCheckBoxColumnor custom columns by creating appropriate cell templates. - Performance: For large datasets, consider optimizing the visual tree traversal or adding virtualization support (WPF's
DataGridhas built-in virtualization, but merging can affect it—test with your data size). - Custom Merging Logic: If you need more complex merging rules (e.g., ignore case, partial matches), modify the
Equalscheck inOnLoadingRow.
内容的提问来源于stack exchange,提问作者Gonzalo
相关产品推荐
相关产品推荐

