13

Is it possible to do this in a WPF datagrid :

|-------------- A header------------|---------------B Header---------------|

|-----A1Header----|----A2Header-----|-----B1Header-----|-----B2Header------|
|-----A1Data------|----A2 Data------|-----B1 Data------|-----B2 Data-------|
|-----A1Data------|----A2 Data------|-----B1 Data------|-----B2 Data-------|

Thanks.

BionicCode
  • 1
  • 4
  • 28
  • 44
David Brunelle
  • 6,528
  • 11
  • 64
  • 104

3 Answers3

10

This Thread might help you achieve what you're trying to do.

It doesn't get the functionality directly from the DataGrid, but instead the DataGrid is wrapped in a regular Grid, and uses bound columns (with multi-columnspan) to add super headers.

Hopefully there's a nice easy way to do this directly from the DataGrid, but if not, maybe this will be an acceptable workaround for you.

Scott
  • 11,840
  • 6
  • 47
  • 54
4

I can offer three solutions to the problem of grouping columns.

Solution 1

Using conventional grouping via the ICollectionView of the source collection. This groups are vertical, means they share the same columns.


Solution 2

Create a nested data source. The idea is that each column binds to an individual data set that is displayed by a DataGrid that is added to the column's DataGridTemplateColumn. It's a DataGrid for each column group. The disadvantage of this solution is that the constraints for the data structure are very strict. No DataTable supported and no auto generation of columns. The effort increases if column sorting or reordering is allowed. but for simple displaying of a grouped table, this solution is fine enough.

Usage example

enter image description here

MainWindow.xaml

<Window>
  <Window.DataContext>
    <ViewModel />
  </Window.DataContext>

  <!-- Toplevel DataGrid that displays the column group headers -->
  <DataGrid ItemsSource="{Binding Rows}"
            AutoGenerateColumns="False"
            CanUserAddRows="False">

    <!-- The grouped column definitions -->
    <DataGrid.Columns>
      <DataGridTemplateColumn>
        <DataGridTemplateColumn.Header>
          <TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=Items[0].Columns[0].GroupHeader}"></TextBlock>
        </DataGridTemplateColumn.Header>

        <DataGridTemplateColumn.CellTemplate>
          <DataTemplate DataType="{x:Type local:DataGridRowItem}">
            <DataGrid ItemsSource="{Binding Columns[0].TableData}"
                      local:DataGridHelper.IsSynchronizeSelectedRowEnabled="True"
                      local:DataGridHelper.SynchronizeGroupKey="A"
                      RowHeaderWidth="0"
                      BorderThickness="0" />
          </DataTemplate>
        </DataGridTemplateColumn.CellTemplate>
        <DataGridTemplateColumn.CellEditingTemplate>
          <DataTemplate DataType="{x:Type local:DataGridRowItem}">
            <TextBox />
          </DataTemplate>
        </DataGridTemplateColumn.CellEditingTemplate>
      </DataGridTemplateColumn>
      <DataGridTemplateColumn>
        <DataGridTemplateColumn.Header>
          <TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=Items[0].Columns[1].GroupHeader}"></TextBlock>
        </DataGridTemplateColumn.Header>

        <DataGridTemplateColumn.CellTemplate>
          <DataTemplate DataType="{x:Type local:DataGridRowItem}">
            <DataGrid ItemsSource="{Binding Columns[1].TableData}"
                      local:DataGridHelper.IsSynchronizeSelectedRowEnabled="True"
                      local:DataGridHelper.SynchronizeGroupKey="A"
                      RowHeaderWidth="0"
                      BorderThickness="0" />
          </DataTemplate>
        </DataGridTemplateColumn.CellTemplate>
        <DataGridTemplateColumn.CellEditingTemplate>
          <DataTemplate DataType="{x:Type local:DataGridRowItem}">
            <TextBox />
          </DataTemplate>
        </DataGridTemplateColumn.CellEditingTemplate>
      </DataGridTemplateColumn>
    </DataGrid.Columns>
  </DataGrid>
</Window>

It's recommended to disable table modifications via the DataGrid. To prettify the look like centering the group names or overriding the DataGridRow template to add row highlight for the unfocused grids is quite simple.

Implementation example

The data structure for the nested tables:

DataGridRowItem.cs
The root element. A single row item for the top-level DataGrid
that will display the column group headers. A column group for each DataGridColumnGroupItem.

public class DataGridRowItem
{
  public List<DataGridColumnGroupItem> Columns { get; set; }
}

DataGridColumnGroupItem.cs
Each DataGridColumnGroupItem will make a group of columns.

public class DataGridColumnGroupItem
{
  public string GroupHeader { get; set; }
  public List<Appointment> TableData { get; set; }
}

Appointment.cs
The actual data model that is displayed in the group's DataGrid.

public class Appointment
{
  public DateTime Start { get; set; }
  public DateTime End { get; set; }
}

ViewModel.cs

public class TestViewModel : ViewModel
{
  public List<DataGridRowItem> Rows { get; }

  public ViewModel()
  {
    this.GroupingRow = new List<DataGridRowItem>
    {
      // The single row for the grouping top level DataGrid
      new DataGridRowItem()
      {  
        Columns = new List<DataGridColumnGroupItem>()
        {
          // First column group
          new DataGridColumnGroupItem()
          {
            GroupHeader = "Group 1",
            TableData = new List<Appointment>
            {
              new Appointment() { Start = DateTime.Now.AddDays(1), End = DateTime.Now.AddDays(2) },
              new Appointment() { Start = DateTime.Now.AddDays(5), End = DateTime.Now.AddDays(6) }
            }
          },

          // Second column group
          new DataGridColumnGroupItem()
          {
            GroupHeader = "Group 2",
            TableData = new List<Appointment>
            {
              new Appointment() { Start = DateTime.Now.AddDays(3), End = DateTime.Now.AddDays(4) },
              new Appointment() { Start = DateTime.Now.AddDays(7), End = DateTime.Now.AddDays(8) }
            }
          }
        }
      }
    };
  }
}

DataGridHelper.cs
An attached behavior that helps to synchronize the selected row across multiple DataGrid instances. the behavior was originally written for different problems, but can be reused in this scenario too. It allows to create synchronization groups of DataGrid elements.

public class DataGridHelper : DependencyObject
{
  public static object GetSynchronizeGroupKey(DependencyObject attachedElement)
    => (object)attachedElement.GetValue(SynchronizeGroupKeyProperty);
  public static void SetSynchronizeGroupKey(DependencyObject attachedElement, object value)
    => attachedElement.SetValue(SynchronizeGroupKeyProperty, value);

  public static readonly DependencyProperty SynchronizeGroupKeyProperty = DependencyProperty.RegisterAttached(
    "SynchronizeGroupKey",
    typeof(object),
    typeof(DataGridHelper),
    new PropertyMetadata(default(object), OnSynchronizeGroupKeyChanged));

  public static bool GetIsSynchronizeSelectedRowEnabled(DependencyObject attachedElement)
    => (bool)attachedElement.GetValue(IsSynchronizeSelectedRowEnabledProperty);
  public static void SetIsSynchronizeSelectedRowEnabled(DependencyObject attachedElement, bool value)
    => attachedElement.SetValue(IsSynchronizeSelectedRowEnabledProperty, value);

  public static readonly DependencyProperty IsSynchronizeSelectedRowEnabledProperty = DependencyProperty.RegisterAttached(
    "IsSynchronizeSelectedRowEnabled",
    typeof(bool),
    typeof(DataGridHelper),
    new PropertyMetadata(default(bool), OnIsSynchronizeSelectedRowEnabledChanged));

  private static Dictionary<object, IList<WeakReference<DataGrid>>> DataGridTable { get; } = new Dictionary<object, IList<WeakReference<DataGrid>>>();
  private static void OnIsSynchronizeSelectedRowEnabledChanged(DependencyObject attachingElement, DependencyPropertyChangedEventArgs e)
  {
    if (attachingElement is not DataGrid dataGrid)
    {
      throw new ArgumentException($"Attaching element must of type {typeof(DataGrid)}.", nameof(attachingElement));
    }

    if ((bool)e.NewValue)
    {
      RegisterDataGridForSelectedItemSynchronization(dataGrid);
    }
    else
    {
      UnregisterDataGridForSelectedItemSynchronization(dataGrid);
    }
  }

  private static void RegisterDataGridForSelectedItemSynchronization(DataGrid dataGrid)
    => WeakEventManager<DataGrid, SelectionChangedEventArgs>.AddHandler(dataGrid, nameof(DataGrid.SelectionChanged), SynchronizeSelectedItem_OnSelectionChanged);

  private static void UnregisterDataGridForSelectedItemSynchronization(DataGrid dataGrid)
    => WeakEventManager<DataGrid, SelectionChangedEventArgs>.RemoveHandler(dataGrid, nameof(DataGrid.SelectionChanged), SynchronizeSelectedItem_OnSelectionChanged);

  private static void OnSynchronizeGroupKeyChanged(DependencyObject attachingElement, DependencyPropertyChangedEventArgs e)
  {
    if (attachingElement is not DataGrid dataGrid)
    {
      throw new ArgumentException($"Attaching element must of type {typeof(DataGrid)}.", nameof(attachingElement));
    }

    if (e.NewValue == null)
    {
      throw new ArgumentNullException($"{null} is not a valid value for the attached property {nameof(SynchronizeGroupKeyProperty)}.", nameof(e.NewValue));
    }

    if (!DataGridTable.TryGetValue(e.NewValue, out IList<WeakReference<DataGrid>>? dataGridGroup))
    {
      dataGridGroup = new List<WeakReference<DataGrid>>();
      DataGridTable.Add(e.NewValue, dataGridGroup);
    }

    dataGridGroup.Add(new WeakReference<DataGrid>(dataGrid));
  }

  private static void SynchronizeSelectedItem_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
  {
    var synchronizationSourceDataGrid = sender as DataGrid;
    var synchronizationSourceDataGridGroupKey = GetSynchronizeGroupKey(synchronizationSourceDataGrid);
    if (!DataGridTable.TryGetValue(synchronizationSourceDataGridGroupKey, out IList<WeakReference<DataGrid>> dataGridGroup))
    {
      return;
    }

    var selectedIndices = synchronizationSourceDataGrid.SelectedItems
      .Cast<object>()
      .Select(synchronizationSourceDataGrid.Items.IndexOf)
      .ToList();

    foreach (WeakReference<DataGrid> dataGridReference in dataGridGroup)
    {
      if (!dataGridReference.TryGetTarget(out DataGrid dataGrid)
        || dataGrid == synchronizationSourceDataGrid
        || dataGrid.Items.Count == 0)
      {
        continue;
      }

      UnregisterDataGridForSelectedItemSynchronization(dataGrid);
      dataGrid.SelectedItems.Clear();
      foreach (int selectedItemIndex in selectedIndices)
      {
        var selectedItem = dataGrid.Items[selectedItemIndex];
        dataGrid.SelectedItems.Add(selectedItem);
      }

      RegisterDataGridForSelectedItemSynchronization(dataGrid);
    }
  }
}

Solution 3

A more powerful solution is to implement a custom control. This way e.g., reorder/resize columns, add/remove rows and customization are really convenient.
The custom control GroupingDataGrid basically wraps a custom DataGrid into a Grid.
This solution supports auto generation as well as explicit column definitions. The column groups and the individual columns can be resized. Of course, resizing the group header will also resize the column of this group.
The DataGrid, that is hosted by the GroupingDataGrid, can be used without any restrictions:

<GroupingDataGrid>

  <!-- Define a DataGrid as usual -->
  <DataGrid ItemsSource="{Binding DataGridItems}" />
</GroupingDataGrid>

The layout is clean and the definition of column groups (using the GroupDefinition element in the style of Grid.ColumnDefinitions) is quite convenient.
To customize the group headers, define a Style that targets GroupingDataGridHeader (which is a ContenControl).
The GroupingDataGrid is an existing control from my library. I removed some code, mainly customization features like templating, from the sources to keep the post as concise as possible.

Usage example

![enter image description here

<local:GroupingDataGrid>
  <local:GroupingDataGrid.GroupDefinitions>

    <!-- Group from column 0 to 3 -->
    <local:GroupDefinition ColumnSpan="4"
                           Header="Person" />

    <!-- Second group from column 4 to 5 -->
    <local:GroupDefinition Column="4"
                           ColumnSpan="2"
                           Header="Numbers" />

    <!-- Remaining columns are automatically added 
         to a common unnamed group -->
  </local:GroupingDataGrid.GroupDefinitions>

    <!-- Define DataGrid as usual -->
    <DataGrid ItemsSource="{Binding DataGridItems}" />
</local:GroupingDataGrid>

Source code

GroupingDataGrid.cs

[ContentProperty(nameof(GroupingDataGrid.DataGrid))]
public class GroupingDataGrid : Control
{
  public GroupDefinitionCollection GroupDefinitions
  {
    get => (GroupDefinitionCollection)GetValue(GroupDefinitionsProperty);
    set => SetValue(GroupDefinitionsProperty, value);
  }

  public static readonly DependencyProperty GroupDefinitionsProperty = DependencyProperty.Register(
    "GroupDefinitions",
    typeof(GroupDefinitionCollection),
    typeof(GroupingDataGrid),
    new PropertyMetadata(default));

  public DataGrid DataGrid
  {
    get { return (DataGrid)GetValue(DataGridProperty); }
    set { SetValue(DataGridProperty, value); }
  }

  public static readonly DependencyProperty DataGridProperty = DependencyProperty.Register(
    "DataGrid",
    typeof(DataGrid),
    typeof(GroupingDataGrid),
    new PropertyMetadata(default(DataGrid), OnDataGridChanged));

  static GroupingDataGrid()
  {
    DefaultStyleKeyProperty.OverrideMetadata(typeof(GroupingDataGrid), new FrameworkPropertyMetadata(typeof(GroupingDataGrid)));
  }
  private static void OnDataGridChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    => (d as GroupingDataGrid).OnDataGridChanged(e.OldValue as DataGrid, e.NewValue as DataGrid);

  private bool IsDataGridLayoutDirty { get; set; }
  private Grid GroupHost { get; }
  private Dictionary<Thumb, GroupingDataGridHeader> ThumbToGroupingDataGridHeaderTable { get; }
  private Dictionary<GroupDefinition, GroupingDataGridHeader> GroupDefinitionToGroupingDataGridHeaderTable { get; }

  public GroupingDataGrid()
  {
    this.GroupDefinitions = new GroupDefinitionCollection();
    this.ThumbToGroupingDataGridHeaderTable = new Dictionary<Thumb, GroupingDataGridHeader>();
    this.GroupDefinitionToGroupingDataGridHeaderTable = new Dictionary<GroupDefinition, GroupingDataGridHeader>();
    this.GroupHost = new Grid();
    this.GroupHost.RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto });
    this.GroupHost.RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto });
  }

  public override void OnApplyTemplate()
  {
    base.OnApplyTemplate();
    var contentHost = GetTemplateChild("PART_DataGridHost") as ContentPresenter;
    if (contentHost != null)
    {
      contentHost.Content = this.GroupHost;
    }
  }

  protected virtual void OnDataGridChanged(DataGrid oldDataGrid, DataGrid newDataGrid)
  {
    if (oldDataGrid != null)
    {
      this.GroupHost.Children.Remove(oldDataGrid);
      oldDataGrid.ColumnDisplayIndexChanged -= OnColumnOrderChanged;
      oldDataGrid.AutoGeneratedColumns -= OnDataGridAutoGeneratedColumns;
    }

    if (newDataGrid == null)
    {
      return;
    }

    this.IsDataGridLayoutDirty = true;
    this.GroupHost.Children.Add(this.DataGrid);
    newDataGrid.ColumnDisplayIndexChanged += OnColumnOrderChanged;
    if (newDataGrid.AutoGenerateColumns && !newDataGrid.IsLoaded)
    {
      newDataGrid.AutoGeneratedColumns += OnDataGridAutoGeneratedColumns;
    }
    else
    {
      CreateVisualTree();
    }
  }

  private void OnColumnOrderChanged(object? sender, DataGridColumnEventArgs e)
    => CreateVisualTree();

  private void OnDataGridAutoGeneratedColumns(object sender, EventArgs e)
    => CreateVisualTree();

  private void CreateVisualTree()
  {
    CreateGroups();
    if (this.IsDataGridLayoutDirty)
    {
      LayoutDataGrid();
    }
  }

  private void CreateGroups()
  {
    this.ThumbToGroupingDataGridHeaderTable.Clear();
    this.GroupDefinitionToGroupingDataGridHeaderTable.Clear();
    ClearGroupHost();

    AddRowHeaderColumnGroup();

    List<DataGridColumn> sortedColumns = this.DataGrid.Columns
    .OrderBy(column => column.DisplayIndex)
    .ToList();
    int ungroupedColumnCount = sortedColumns.Count - this.GroupDefinitions.Sum(definition => definition.ColumnSpan);
    bool hasUngroupedColumns = ungroupedColumnCount > 0;

    for (int groupIndex = 0; groupIndex < this.GroupDefinitions.Count; groupIndex++)
    {
      GroupDefinition group = this.GroupDefinitions[groupIndex];
      int groupHeaderColumnIndex = groupIndex + 1;

      AddGridColumn();
      AddGroupHeader(group, groupHeaderColumnIndex, sortedColumns);
      if (groupHeaderColumnIndex > 1)
      {
        GroupDefinition previousGroup = this.GroupDefinitions[groupIndex - 1];
        AddColumnGrippers(previousGroup, groupHeaderColumnIndex - 1);
      }
    }

    if (hasUngroupedColumns)
    {
      AddGroupForRemainingColumns();
    }
  }

  private void AddGroupForRemainingColumns()
  {
    AddGridColumn(false);
    AddGroupHeader(null, this.GroupHost.ColumnDefinitions.Count - 1, new List<DataGridColumn>());

    if (this.GroupDefinitions.Any())
    {
      GroupDefinition previousGroup = this.GroupDefinitions.Last();
      AddColumnGrippers(previousGroup, this.GroupDefinitions.Count);
    }
  }

  private void CreateColumnGroupHeaderBinding(IList<DataGridColumn> sortedColumns, GroupingDataGridHeader groupHeaderHost)
  {
    GroupDefinition group = groupHeaderHost.GroupDefinition;
    var groupHeaderWidthMultiBinding = new MultiBinding
    {
      Mode = BindingMode.TwoWay,
      Converter = new DataGridColumnRangeWidthToGroupHeaderWidthConverter(sortedColumns),
      ConverterParameter = group
    };

    for (int columnIndex = group.Column; columnIndex < group.Column + group.ColumnSpan; columnIndex++)
    {
      DataGridColumn column = sortedColumns[columnIndex];
      var widthBinding = new Binding(nameof(DataGridColumn.Width))
      {
        Mode = BindingMode.TwoWay,
        Source = column
      };
      groupHeaderWidthMultiBinding.Bindings.Add(widthBinding);
    }

    groupHeaderHost.SetBinding(WidthProperty, groupHeaderWidthMultiBinding);
  }

  private GroupingDataGridHeader AddGroupHeader(GroupDefinition group, int groupHeaderColumnIndex, List<DataGridColumn> sortedColumns)
  {
    var groupHeaderHost = new GroupingDataGridHeader(group);
    Grid.SetColumn(groupHeaderHost, groupHeaderColumnIndex);
    Grid.SetRow(groupHeaderHost, 0);
    this.GroupHost.Children.Add(groupHeaderHost);
     
    if (group != null)
    {
      this.GroupDefinitionToGroupingDataGridHeaderTable.Add(group, groupHeaderHost);
      if (sortedColumns.Any())
      {
        CreateColumnGroupHeaderBinding(sortedColumns, groupHeaderHost);
      }
    }

    return groupHeaderHost;
  }

  private void AddGridColumn(bool isAutoWidth = true)
  {
    var gridColumnWidth = isAutoWidth 
      ? GridLength.Auto 
      : new GridLength(1, GridUnitType.Star);
    var groupHeaderHostColumnDefinition = new ColumnDefinition() { Width = gridColumnWidth };
    this.GroupHost.ColumnDefinitions.Add(groupHeaderHostColumnDefinition);
  }

  private void AddColumnGrippers(GroupDefinition groupDefinition, int groupHeaderColumnIndex)
  {
    GroupingDataGridHeader groupHeaderHost = this.GroupDefinitionToGroupingDataGridHeaderTable[groupDefinition];
    AddColumnGripper(groupHeaderColumnIndex, groupHeaderHost, true);
    AddColumnGripper(groupHeaderColumnIndex + 1, groupHeaderHost);
  }

  private void AddColumnGripper(int columnIndex, GroupingDataGridHeader groupHeader, bool isLeftColumnGripper = false)
  {
    var columnGripper = new Thumb()
    {
      HorizontalAlignment = isLeftColumnGripper
        ? HorizontalAlignment.Right
        : HorizontalAlignment.Left,
    };

    columnGripper.DragDelta += OnGroupHeaderResizing;
    this.ThumbToGroupingDataGridHeaderTable.Add(columnGripper, groupHeader);
    Grid.SetColumn(columnGripper, columnIndex);
    Grid.SetRow(columnGripper, 0);
    this.GroupHost.Children.Add(columnGripper);
  }

  private void LayoutDataGrid()
  {
    Grid.SetColumnSpan(this.DataGrid, this.GroupHost.ColumnDefinitions.Count);
    Grid.SetRow(this.DataGrid, 1);
    this.IsDataGridLayoutDirty = false;
  }

  private void AddRowHeaderColumnGroup()
  {
    AddGridColumn();
    GroupingDataGridHeader rowHeaderGroupHost = AddGroupHeader(null, 0, new List<DataGridColumn>());
    var rowHeaderWidthBinding = new Binding(nameof(DataGrid.RowHeaderActualWidth))
    {
      Source = this.DataGrid
    };

    rowHeaderGroupHost.SetBinding(WidthProperty, rowHeaderWidthBinding);
  }

  private void ClearGroupHost()
  {
    for (int childIndex = this.GroupHost.Children.Count - 1; childIndex >= 0; childIndex--)
    {
      var child = this.GroupHost.Children[childIndex];
      if (child != this.DataGrid)
      {
        this.GroupHost.Children.Remove(child);
      }
    }
  }

  private void OnGroupHeaderResizing(object sender, DragDeltaEventArgs e)
  {
    var thumb = sender as Thumb;
    if (this.ThumbToGroupingDataGridHeaderTable.TryGetValue(thumb, out GroupingDataGridHeader groupingDataGridHeader))
    {
      groupingDataGridHeader.Width += e.HorizontalChange;
    }
  }
}

GroupingDataGridHeader.cs

public class GroupingDataGridHeader : ContentControl
{
  public GroupDefinition GroupDefinition { get; }

  public GroupingDataGridHeader() : this(new GroupDefinition())
  {      
  }

  public GroupingDataGridHeader(GroupDefinition groupDefinition)
  {
    this.GroupDefinition = groupDefinition;
    this.Content = this.GroupDefinition?.Header ?? string.Empty;
  }

  static GroupingDataGridHeader()
  {
    DefaultStyleKeyProperty.OverrideMetadata(typeof(GroupingDataGridHeader), new FrameworkPropertyMetadata(typeof(GroupingDataGridHeader)));
  }
}

GroupDefinition.cs

public class GroupDefinition : FrameworkContentElement
{
  public int Column
  {
    get => (int)GetValue(ColumnProperty);
    set => SetValue(ColumnProperty, value);
  }

  public static readonly DependencyProperty ColumnProperty = DependencyProperty.Register(
    "Column",
    typeof(int),
    typeof(GroupDefinition),
    new PropertyMetadata(default));

  public int ColumnSpan
  {
    get => (int)GetValue(ColumnSpanProperty);
    set => SetValue(ColumnSpanProperty, value);
  }

  public static readonly DependencyProperty ColumnSpanProperty = DependencyProperty.Register(
    "ColumnSpan",
    typeof(int),
    typeof(GroupDefinition),
    new PropertyMetadata(default));

  public object Header
  {
    get => (object)GetValue(HeaderProperty);
    set => SetValue(HeaderProperty, value);
  }

  public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register(
    "Header",
    typeof(object),
    typeof(GroupDefinition),
    new PropertyMetadata(default));
}

GroupDefinitionCollection.cs

public class GroupDefinitionCollection : Collection<GroupDefinition>
{ }

DataGridColumnRangeWidthToGroupHeaderWidthConverter.cs

public class DataGridColumnRangeWidthToGroupHeaderWidthConverter : IMultiValueConverter
{
  private IList<DataGridColumn> DataGridColumns { get; }

  public DataGridColumnRangeWidthToGroupHeaderWidthConverter(IList<DataGridColumn> dataGridColumns)
  {
    this.DataGridColumns = dataGridColumns;
  }

  public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    => values.Cast<DataGridLength>().Sum(gridLength => gridLength.DisplayValue);

  public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
  {
    var groupDefinition = (GroupDefinition)parameter;
    double currentGroupedColumnsWidth = this.DataGridColumns
      .Skip(groupDefinition.Column)
      .Take(groupDefinition.ColumnSpan)
      .Select(column => column.Width.DisplayValue)
      .Sum();

    var result = new object[groupDefinition.ColumnSpan];
    Array.Fill(result, Binding.DoNothing);
    DataGridColumn lastGroupColumn = this.DataGridColumns[groupDefinition.Column + groupDefinition.ColumnSpan - 1];
    var newColumnWidth = new DataGridLength(lastGroupColumn.Width.DisplayValue + (double)value - currentGroupedColumnsWidth, DataGridLengthUnitType.Pixel);
    result[result.Length - 1] = newColumnWidth;

    return result;
  }
}

Generic.xaml

<ResourceDictionary>
  <Style TargetType="local:GroupingDataGrid">
    <Style.Resources>
      <Style TargetType="Thumb">
        <Setter Property="Width"
                Value="8" />
        <Setter Property="Background"
                Value="Transparent" />
        <Setter Property="Cursor"
                Value="SizeWE" />
        <Setter Property="BorderBrush"
                Value="Transparent" />
        <Setter Property="Template">
          <Setter.Value>
            <ControlTemplate TargetType="{x:Type Thumb}">
              <Border Background="{TemplateBinding Background}"
                      Padding="{TemplateBinding Padding}" />
            </ControlTemplate>
          </Setter.Value>
        </Setter>
      </Style>
    </Style.Resources>

    <Setter Property="BorderThickness"
            Value="0,0,1,0" />
    <Setter Property="BorderBrush"
            Value="Black" />
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="local:GroupingDataGrid">
          <Border BorderThickness="{TemplateBinding BorderThickness}"
                  BorderBrush="{TemplateBinding BorderBrush}"
                  Background="{TemplateBinding Background}"
                  Padding="{TemplateBinding Padding}">
            <ContentPresenter x:Name="PART_DataGridHost" />
          </Border>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>

  <Style TargetType="local:GroupingDataGridHeader">
    <Setter Property="BorderThickness"
            Value="0,0,1,0" />
    <Setter Property="BorderBrush"
            Value="Black" />
    <Setter Property="HorizontalContentAlignment"
            Value="Center" />
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="local:GroupingDataGridHeader">
          <Border BorderThickness="{TemplateBinding BorderThickness}"
                  BorderBrush="{TemplateBinding BorderBrush}"
                  Background="{TemplateBinding Background}"
                  Padding="{TemplateBinding Padding}">
            <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                              VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
          </Border>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</ResourceDictionary>
BionicCode
  • 1
  • 4
  • 28
  • 44
  • I'm missing the GroupingDataGridHeader in this code. Am I blind? Or should I be creating that as part of my implementation to look how I want a column group to look? – Byrel Mitchell Oct 24 '22 at 12:40
  • No, you were right. I mean you have good eyes. Looks like I forgot to post this class. I will post it later when I'm back and then let you know. Sorry. And thank you for telling me. – BionicCode Oct 24 '22 at 13:37
  • No, thank you for such an elegant and extensive solution to the prompt. I need to do something a bit more sophisticated than a lot of the simpler solutions for this problem permit. – Byrel Mitchell Oct 24 '22 at 16:54
  • You are very kind. Thank you very much. I've added the missing *GroupingDataGridHeader.cs*. Let me know if you need support. – BionicCode Oct 24 '22 at 18:32
  • I just got back to this project. I have a datagrid which displays properly, but simply never renders when inside a GroupingDataGrid. Looking at the code, I believe I'm missing any template application to trigger GroupingDataGrid.OnApplyTemplate() (which would connect the DataGrid to the template element 'PART_DataGridHost'; probably a ContentPresenter?) I'm going to play around with trying to build a proper template for it; let me know if you have a sample, or if I'm missing something. – Byrel Mitchell Nov 03 '22 at 12:49
  • I will run the example tomorrow to check what is missing. The posted code is originally from a working example. But obviously I forgot to post some code. – BionicCode Nov 03 '22 at 14:45
  • Yep, I got this working by adding a style setting the template for GroupingDataGrid and GroupingDataGridHeader. The GroupingDataGrid one just needs a ContentPresenter with that name, and the header I used was a fairly standard border+contentcontrol with a dozen or so template bindings for easy reuse. Works great now! – Byrel Mitchell Nov 03 '22 at 17:28
  • @ByrelMitchell I ran the above code and it worked. The default style in generic.xaml (see above) already contains a `ContentPresenter`. Using XAML object notation automatically assigns the `DataGrid` to the `GroupingDataGrid.DataGrid` property: ` ` (see usage example above). Just make sure you have added the `ContentPropertyAttribute` to the `GroupingDataGrid` class declaration like shown in the example above. Copying the *full* example's source code will give you a working control. – BionicCode Nov 05 '22 at 10:47
0

If paying is an option, Telerik radGridView has Column Groups

xvan
  • 4,554
  • 1
  • 22
  • 37