wpfdatagrid

How to apply alternating row colors to groups in a DataGrid?


Suppose I have a list of visitor object grouped by Nation

Class visitor
{
    String Nation;
    String Name
}
Nation Name
USA Tom
Italy Jim
China Josh
UK Jane
UK Kelly
S. Korea Betty
Japan Cathy
France Steve
France Alex
Cuba Ken

I want my datagrid to have alternating color only when the Nation is different with the previous row. In that case, the above table should be colored as Colored Row

DataGrid.RowStyle>DataTrigger is the first suggestion came to my mind. However, it judges from the property of the entry (visitor in my case) itself instead of the relationship between entries.

My current workaround is to have a custom List which would append a flag based on the logic I mentioned above. The DataTrigger then reads the flag and colors each row based on the bool value.

class FlaggedVisitor
{
    string Nation;
    string Name;
    //True for ColorA false for ColorB
    bool ColorFlag;
}

Ex: List

USA Tom True

Italy Jim False

China Josh True

UK Jane False

UK Kelly False

S.Korea Betty True

However, I'm curious if I can somehow achieve what I want by XAML or some ValueConverter?


Solution

  • First, your data model FlaggedVisitor must implement INotifyPropertyChanged even when the properties are not changing. Every binding source object that does not extend DepenedencyObject (and therefore does not implement its properties as dependency properties) and that participates in a OneWay or TwoWay binding must implement INotifyPropertyChanged.

    Next, group the items. Assuming that you have a collection of type FlaggedVisitor that binds to the DataGrid somewhere, you group by providing a related PropertyGroupDescrition to the underlying ICollectionView that the DataGrid implicitly operates on:

    MainWindow.cs

    class MainWindow : Window
    {
      public ObservableCollection<FlaggedVisitor> FlaggedVisitors { get; }
    
      public MainWindow()
      {
        this.FlaggedVisitors = new ObservableCollectionFlaggedVisitor>();
    
        // Create the groups based on the FlaggedVisitor.Nation value
        var ICollectionView flaggedVisitorsCollectionView = CollectionViewSource.GetDefaultView(this.FlaggedVisitors);
        var nationGroupDescription = new PropertyGroupDescription(nameof(FlaggedVisitor.Nation));
        flaggedVisitorsCollectionView.GroupDescriptions.Add(nationGroupDescription);
    
        InitializeComponent();
      }
    }
    

    Then create a converter that convers the group index to a Brush:

    GroupBackgroundConverter.cs
    This simple implementation supports different alternation counts, e.g. to alternate between three colors.

    [ContentProperty(nameof(AlternationBrushes))]
    public class GroupBackgroundConverter : IValueConverter
    {
      public List<Brush> AlternationBrushes { get; }
      public int AlterantionCount { get; set; }
      private readonly Brush defaultAlterantionBrush;
    
      public GroupBackgroundConverter()
      {
        this.AlternationBrushes = new List<Brush>();
        this.AlterantionCount = 2;
        this.defaultAlterantionBrush = Brushes.LightSteelBlue;
      }
    
      public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
      {
        if (value is not GroupItem groupItem)
        {
          return Binding.DoNothing;
        }
    
        int alternationIndex = ItemsControl.GetAlternationIndex(groupItem);
        return alternationIndex < this.AlternationBrushes.Count
          ? this.AlternationBrushes[alternationIndex]
          : this.AlternationBrushes.FirstOrDefault() ?? this.defaultAlterantionBrush;
      }
    
      public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 
        => throw new NotSupportedException();
    }
    

    Finally, configure the DataGrid to show groups and attach the GroupBackgroundConverter inside the GroupStyle:

    MainWindow.xaml.cs

    <Window x:Name="Root">
      <Window.Resources>
        <GroupBackgroundConverter x:Key="GroupBackgroundConverter"
                                  AlternationCount="2">
          <SolidColorBrush Color="Orange" />
          <SolidColorBrush Color="Yellow" />
        </GroupBackgroundConverter>
      </Window.Resources>
    
      <!-- 
        Set the row background to Transparent so that we can enable 
        the group's background to take precedence.
      -->
      <DataGrid ItemsSource="{Binding ElementName=Root, Mode=OneTime}"
                RowBackground="Transparent">
    
        <!-- Define a headerless group layout -->
        <DataGrid.GroupStyle>
          <GroupStyle AlternationCount="2">
            <GroupStyle.ContainerStyle>
              <Style TargetType="{x:Type GroupItem}">
                <Setter Property="Template">
                  <Setter.Value>
                    <ControlTemplate TargetType="{x:Type GroupItem}">
    
                      <!-- 
                        The Border that actually renders the 
                        background brush based on the GroupBackgroundConverter
                      -->
                      <Border Background="{Binding RelativeSource={RelativeSource AncestorType=GroupItem}, Converter={StaticResource 
                                  GroupBackgroundConverter}}">
                        <ItemsPresenter />
                      </Border>
                    </ControlTemplate>
                  </Setter.Value>
                </Setter>
              </Style>
            </GroupStyle.ContainerStyle>
          </GroupStyle>
        </DataGrid.GroupStyle>
      </DataGrid>
    </Window>