You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在代码定义的通用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:

  1. Listen for auto-generated columns (since we're not defining them in XAML)
  2. Compare each cell's value with the cell above it when rows load
  3. Adjust RowSpan of the previous cell and hide the current cell if values match
  4. Reset merging state when the ItemsSource changes

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 ItemsSource should 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 extend OnAutoGeneratingColumn to support DataGridCheckBoxColumn or custom columns by creating appropriate cell templates.
  • Performance: For large datasets, consider optimizing the visual tree traversal or adding virtualization support (WPF's DataGrid has 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 Equals check in OnLoadingRow.

内容的提问来源于stack exchange,提问作者Gonzalo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:36:27