WPF DataGrid 为每个列集添加单独的边框

WPF DataGrid add separate border to every column set

我正在尝试实现每列都有自己的边框的效果,但仍找不到完美的解决方案。

需要这种外观,但这是通过在 3 列网格中放置 3 个边框来实现的,这不灵活,因为网格列和 DataGrid 列的大小是分开调整的

<Window x:Class="WpfApp3.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:WpfApp3" xmlns:usercontrols="clr-namespace:EECC.UserControls"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800">
<Grid Background="LightGray">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>

    <Border Background="White" CornerRadius="5" BorderThickness="1" BorderBrush="Black" Margin="5"/>
    <Border Background="White" CornerRadius="5" BorderThickness="1" BorderBrush="Black" Margin="5" Grid.Column="1"/>
    <Border Background="White" CornerRadius="5" BorderThickness="1" BorderBrush="Black" Margin="5" Grid.Column="2"/>

    <DataGrid ItemsSource="{Binding Items}" ColumnWidth="*" AutoGenerateColumns="True" Padding="10" GridLinesVisibility="None" Background="Transparent" Grid.ColumnSpan="3">
        <DataGrid.Resources>
            <Style TargetType="{x:Type DataGridRow}">
                <Setter Property="Background" Value="Transparent"/>
            </Style>
            <Style TargetType="{x:Type DataGridColumnHeader}">
                <Setter Property="Background" Value="Transparent"/>
            </Style>
        </DataGrid.Resources>
    </DataGrid>
</Grid>

如果您想使用 DataGrid,这不是微不足道的。 “问题”是 DataGrid 使用 Grid 来承载细胞。单元格边框是使用 Grid 的功能绘制的,以显示网格线。您可以隐藏网格线,但仍然有 Grid 控制布局。
创造你想要的差距并不容易。 Grid 的布局系统努力实现可扩展的解决方案。您可以扩展 SelectiveScrollingGridDataGrid 的托管面板)并在布置项目时添加间隙。了解 DataGrid 内部结构,我可以说这是可能的,但不值得付出努力。

另一种解决方案是使用 ListViewGridView 作为主机。这里的问题是 GridView 旨在将行显示为单个项目。您没有机会修改基于列的边距。您只能调整内容。我没有尝试修改 ListView 内部布局元素或覆盖布局算法,但在替代解决方案的上下文中,我也会使用 GridView 来规则 ListView - 但这是可能的.不值得。


解决方案:自定义视图

我能建议的最简单的解决方案是调整数据结构以显示基于数据列。这样你就可以使用水平ListBox。每一项构成一列。每列都实现为垂直 ListBox。您基本上嵌套了 ListBox 个元素。

您必须处理行映射,以便允许跨垂直 ListBox 列选择公共行的单元格。
这可以通过在 CellItem 模型中添加 RowIndex 属性 来轻松实现。

想法是让水平 ListBox 显示 collection 个 ColumnItem 个模型。每个列项模型公开 collection 个 CellItem 个模型。不同列但同一行的CellItem项必须共享相同的CellItem.RowIndex.
作为奖励,这个解决方案非常容易设计。 ListBox 模板与明显更复杂的 DataGrid 或稍复杂的 GridView.

相比几乎没有任何部分

为了展示这个概念不那么混乱,我选择将网格布局实现为 UserControl。为了简单起见,初始化和托管模型和源 collections 的逻辑在这个 UserControl 中实现。我不推荐这个。项目的实例化和托管应该在控制之外,例如,在视图模型中。您应该为控件添加一个 DependencyProperty as ItemsSource 作为内部水平 ListBox.

的数据源

用法示例

<Window>
  <ColumnsView />
</Window>

  1. 首先创建数据结构来填充视图。
    该结构基于 ColumnItem 类型,其中包含 collection 个 CellItem 个项目,其中每个 CellItem 有一个 CellItem.RowIndex
    逻辑上构成一行的不同列的 CellItem 项必须共享相同的 CellItem.RowIndex.

ColumnItem.cs

public class ColumnItem
{
  public ColumnItem(string header, IEnumerable<CellItem> items)
  {
    Header = header;
    this.Items = new ObservableCollection<CellItem>(items);
  }

  public CellItem this[int rowIndex] 
    => this.Items.FirstOrDefault(cellItem => cellItem.RowIndex.Equals(rowIndex));

  public string Header { get; }
  public ObservableCollection<CellItem> Items { get; }
}

CellItem.cs

public class CellItem : INotifyPropertyChanged
{
  public CellItem(int rowIndex, object value)
  {
    this.RowIndex = rowIndex;
    this.Value = value;
  }

  protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "") 
    => this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

  public event PropertyChangedEventHandler PropertyChanged;
  public int RowIndex { get; }

  private object value;
  public object Value
  {
    get => this.value;
    set
    {
      this.value = value;
      OnPropertyChanged();
    }
  }

  private bool isSelected;
  public bool IsSelected
  {
    get => this.isSelected;
    set
    {
      this.isSelected = value;
      OnPropertyChanged();
    }
  }
}
  1. 构建并初始化数据结构。
    在此示例中,这全部在 UserControl 本身中实现,目的是使示例尽可能紧凑。

ColumnsView.xaml.cs

public partial class ColumnsView : UserControl
{
  public ColumnsView()
  {
    InitializeComponent();
    this.DataContext = this;

    InitializeSourceData();
  }

  public InitializeSourceData()
  {
    this.Columns = new ObservableCollection<ColumnItem>();

    for (int columnIndex = 0; columnIndex < 3; columnIndex++)
    {
      var cellItems = new List<CellItem>();
      int asciiChar = 65;

      for (int rowIndex = 0; rowIndex < 10; rowIndex++)
      {
        var cellValue = $"CellItem.RowIndex:{rowIndex}, Value: {(char)asciiChar++}";
        var cellItem = new CellItem(rowIndex, cellValue);
        cellItems.Add(cellItem);
      }

      var columnHeader = $"Column {columnIndex + 1}";
      var columnItem = new ColumnItem(columnHeader, cellItems);
      this.Columns.Add(columnItem);
    }
  }

  private void CellsHostListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
  {
    var cellsHost = sender as Selector;
    var selectedCell = cellsHost.SelectedItem as CellItem;
    SelectCellsOfRow(selectedCell.RowIndex);
  }

  private void SelectCellsOfRow(int selectedRowIndex)
  {
    foreach (ColumnItem columnItem in this.Columns)
    {
      var cellOfRow = columnItem[selectedRowIndex];
      cellOfRow.IsSelected = true;
    }
  }

  private void ColumnGripper_DragStarted(object sender, DragStartedEventArgs e) 
    => this.DragStartX = Mouse.GetPosition(this).X;

  private void ColumnGripper_DragDelta(object sender, DragDeltaEventArgs e)
  {
    if ((sender as DependencyObject).TryFindVisualParentElement(out ListBoxItem listBoxItem))
    {
      double currentMousePositionX = Mouse.GetPosition(this).X;
      listBoxItem.Width = Math.Max(0 , listBoxItem.ActualWidth - (this.DragStartX - currentMousePositionX));
      this.DragStartX = currentMousePositionX;
    }
  }

  public static bool TryFindVisualParentElement<TParent>(DependencyObject child, out TParent resultElement)
    where TParent : DependencyObject
  {
    resultElement = null;

    DependencyObject parentElement = VisualTreeHelper.GetParent(child);

    if (parentElement is TParent parent)
    {
      resultElement = parent;
      return true;
    }

    return parentElement != null 
      ? TryFindVisualParentElement(parentElement, out resultElement) 
      : false;
  }

  public ObservableCollection<ColumnItem> Columns { get; }
  private double DragStartX { get; set; }
}
  1. 使用水平 ListView 创建视图,将其 ColumnItem 源 collection 呈现为垂直 ListBox 元素的列表。

ColumnsView.xaml

<UserControl x:Class="ColumnsView">
  <UserControl.Resources>
    <Style x:Key="ColumnGripperStyle"
         TargetType="{x:Type Thumb}">
      <Setter Property="Margin"
            Value="-2,8" />
      <Setter Property="Width"
            Value="4" />
      <Setter Property="Background"
            Value="Transparent" />
      <Setter Property="Cursor"
            Value="SizeWE" />
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="{x:Type Thumb}">
            <Border Background="{TemplateBinding Background}"
                  Padding="{TemplateBinding Padding}" />
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>
  </UserControl.Resources>

  <!-- Column host. Displays cells of a column. -->
  <ListBox ItemsSource="{Binding Columns}">
    <ListBox.ItemsPanel>
      <ItemsPanelTemplate>
        <VirtualizingStackPanel Orientation="Horizontal" />
      </ItemsPanelTemplate>
    </ListBox.ItemsPanel>

    <ListBox.ItemTemplate>
      <DataTemplate>
        <Border Padding="4" 
                BorderThickness="1" 
                BorderBrush="Black" 
                CornerRadius="8">
          <StackPanel>
            <TextBlock Text="{Binding Header}" />

            <!-- Cell host. Displays cells of a column. -->
            <ListBox ItemsSource="{Binding Items}" 
                     BorderThickness="0" 
                     Height="150"
                     Selector.SelectionChanged="CellsHostListBox_SelectionChanged">
              <ListBox.ItemTemplate>
                <DataTemplate>
                  <TextBlock Text="{Binding Value}" />
                </DataTemplate>
              </ListBox.ItemTemplate>
          
              <ListBox.ItemContainerStyle>
                <Style TargetType="ListBoxItem">
              
                  <!-- Link item container selection to CellItem.IsSelected -->
                  <Setter Property="IsSelected" Value="{Binding IsSelected}" />
                </Style>
              </ListBox.ItemContainerStyle>
            </ListBox>
          </StackPanel>
        </Border>
      </DataTemplate>
    </ListBox.ItemTemplate>

    <ListBox.ItemContainerStyle>
      <Style TargetType="ListBoxItem">
        <Setter Property="Margin" Value="0,0,8,0" /> <!-- Define the column gap -->    
        <Setter Property="Template">
          <Setter.Value>
            <ControlTemplate TargetType="ListBoxItem">
              <Grid>
                <Grid.ColumnDefinitions>
                  <ColumnDefinition />
                  <ColumnDefinition Width="Auto" />
                </Grid.ColumnDefinitions>
                <ContentPresenter />
                <Thumb Grid.Column="1" 
                       Style="{StaticResource ColumnGripperStyle}"
                       DragStarted="ColumnGripper_DragStarted" 
                       DragDelta="ColumnGripper_DragDelta" />
              </Grid>
            </ControlTemplate>
          </Setter.Value>
        </Setter>
      </Style>
    </ListBox.ItemContainerStyle>
  </ListBox>
</UserControl>

改进注意事项

ColumnsView.Columns 属性 应该是 DependencyProperty 以允许将控件用作绑定目标。
列间距也可以是 DependencyProperty of ColumnsView.
通过将显示列 header 的 TextBlock 替换为 Button,您可以轻松添加排序。具有触发排序的活动列,例如从词法上讲,您必须同步其他被动列并根据主动排序列的 CellItem.RowIndex 顺序对它们进行排序。
也许选择扩展 Control 而不是 UserControl
您可以实现 CellItem 以使用泛型类型参数来声明 Cellitem.Value 属性,例如 CellItem<TValue>.
您可以实现 ColumnItem 以使用泛型类型参数来声明 ColumnItem.Items 属性,例如 ColumnItem<TColumn>.
添加一个 ColumnsView.SelectedRow 属性 那个 returns 一个 collection of all CellItem items of all current selected row

只要付出相当多的努力,您就可以通过扩展原生 DataGrid.

来获得所需的外观

自定义 header 和单元格模板应注意间距,并使用适当的背景颜色。 AutoGeneratingColumn 行为需要比 XAML 更容易实现的控制,所以我选择在代码中创建模板,以便能够传递列的 PropertyName.

细心的 reader 会问自己:“列表末尾的边框怎么办?”。没错,我们需要能够将最后一项与所有其他项区分开来,以便能够对其边框进行不同的模板化。

这是通过以下合约完成的:

public interface ICanBeLastItem
{
    bool IsLastItem { get; set; }
}

行 object 需要实现才能正确绘制底部边框。

这在排序时也需要一些自定义逻辑,以更新 IsLastItem 的值。黄色背景的图片显示了 ThirdNumber.

上的排序结果

原生 DataGrid 提供开箱即用的 Sorting 事件,但没有 Sorted 事件。模板结合了自定义事件的需要,导致我从 DataGrid 继承 ColumnView 而不是将其声明为 UserControl.

我将 code-behind 添加到 MainWindow,用于切换背景颜色,但这只是为了说明目的(因为我不喜欢实现 Command 模式)并且与自定义控件无关。

ColumnView是通过绑定配置的。一如既往,请随意扩展。当前的实现期望列是自动生成的。无论哪种情况,都提供了生成模板的代码。

<local:ColumnView ItemsSource="{Binding Items}" Background="LightSteelBlue"/>

演示代码

列视图

public class ColumnView : DataGrid
{
    public ColumnView()
    {
        HeadersVisibility = DataGridHeadersVisibility.Column;
        HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden;

        // Hidden props from base DataGrid
        base.ColumnWidth = new DataGridLength(1, DataGridLengthUnitType.Star);
        base.AutoGenerateColumns = true;
        base.GridLinesVisibility = DataGridGridLinesVisibility.None;

        // Styling
        ColumnHeaderStyle = CreateColumnHeaderStyle();
        CellStyle = CreateCellStyle(this);

        // Event handling
        AutoGeneratingColumn += OnAutoGeneratingColumn;
        Sorting += OnSorting;
        Sorted += OnSorted;
    }

    #region Hidden props

    [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
    public new DataGridLength ColumnWidth
    {
        get => base.ColumnWidth;
        set => new InvalidOperationException($"{nameof(ColumnView)} doesn't allow changing {nameof(ColumnWidth)}.");
    }

    [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
    public new DataGridGridLinesVisibility GridLinesVisibility
    {
        get => base.GridLinesVisibility;
        set => new InvalidOperationException($"{nameof(ColumnView)} doesn't allow changing {nameof(GridLinesVisibility)}.");
    }

    [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
    public new bool AutoGenerateColumns
    {
        get => base.AutoGenerateColumns;
        set => new InvalidOperationException($"{nameof(ColumnView)} doesn't allow changing {nameof(AutoGenerateColumns)}.");
    }

    #endregion Hidden props

    #region Styling

    private static Style CreateColumnHeaderStyle()
        => new Style(typeof(DataGridColumnHeader))
        {
            Setters =
            {
                new Setter(BackgroundProperty, Brushes.Transparent),
                new Setter(HorizontalAlignmentProperty, HorizontalAlignment.Stretch),
                new Setter(HorizontalContentAlignmentProperty, HorizontalAlignment.Stretch)
            }
        };

    private static Style CreateCellStyle(ColumnView columnView)
        => new Style(typeof(DataGridCell))
        {
            Setters =
            {
                new Setter(BorderThicknessProperty, new Thickness(0.0)),
                new Setter(BackgroundProperty, new Binding(nameof(Background)) { Source = columnView})
            }
        };

    #endregion Styling

    #region AutoGeneratingColumn

    // https://whosebug.com/questions/25643765/wpf-datagrid-databind-to-datatable-cell-in-celltemplates-datatemplate
    private static void OnAutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
    {
        if (sender is ColumnView columnView)
        {
            if (e.PropertyName == nameof(ICanBeLastItem.IsLastItem))
            {
                e.Cancel = true;
            }
            else
            {
                var column = new DataGridTemplateColumn
                {
                    CellTemplate = CreateCustomCellTemplate(e.PropertyName),
                    Header = e.Column.Header,
                    HeaderTemplate = CreateCustomHeaderTemplate(columnView, e.PropertyName),
                    HeaderStringFormat = e.Column.HeaderStringFormat,
                    SortMemberPath = e.PropertyName
                };
                e.Column = column;
            }
        }
    }

    private static DataTemplate CreateCustomCellTemplate(string path)
    {
        // Create the data template
        var customTemplate = new DataTemplate();

        // Set up the wrapping border
        var border = new FrameworkElementFactory(typeof(Border));
        border.SetValue(BorderBrushProperty, Brushes.Black);
        border.SetValue(StyleProperty, new Style(typeof(Border))
        {
            Triggers =
            {
                new DataTrigger
                {
                    Binding = new Binding(nameof(DataGridCell.IsSelected)) { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(DataGridCell), 1) },
                    Value = false,
                    Setters =
                    {
                        new Setter(BackgroundProperty, Brushes.White),
                    }
                },
                new DataTrigger
                {
                    Binding = new Binding(nameof(DataGridCell.IsSelected)) { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(DataGridCell), 1) },
                    Value = true,
                    Setters =
                    {
                        new Setter(BackgroundProperty, SystemColors.HighlightBrush),
                    }
                },
                new DataTrigger
                {
                    Binding = new Binding(nameof(ICanBeLastItem.IsLastItem)),
                    Value = false,
                    Setters =
                    {
                        new Setter(MarginProperty, new Thickness(5.0, -1.0, 5.0, -1.0)),
                        new Setter(BorderThicknessProperty, new Thickness(1.0, 0.0, 1.0, 0.0)),
                    }
                },
                new DataTrigger
                {
                    Binding = new Binding(nameof(ICanBeLastItem.IsLastItem)),
                    Value = true,
                    Setters =
                    {
                        new Setter(MarginProperty, new Thickness(5.0, -1.0, 5.0, 0.0)),
                        new Setter(BorderThicknessProperty, new Thickness(1.0, 0.0, 1.0, 1.0)),
                        new Setter(Border.CornerRadiusProperty, new CornerRadius(0.0, 0.0, 5.0, 5.0)),
                        new Setter(Border.PaddingProperty, new Thickness(0.0, 0.0, 0.0, 5.0)),
                    }
                }
            }
        });

        // Set up the TextBlock
        var textBlock = new FrameworkElementFactory(typeof(TextBlock));
        textBlock.SetBinding(TextBlock.TextProperty, new Binding(path));
        textBlock.SetValue(MarginProperty, new Thickness(10.0, 0.0, 5.0, 0.0));

        // Set the visual tree of the data template
        border.AppendChild(textBlock);
        customTemplate.VisualTree = border;

        return customTemplate;
    }

    private static DataTemplate CreateCustomHeaderTemplate(ColumnView columnView, string propName)
    {
        // Create the data template
        var customTemplate = new DataTemplate();

        // Set up the wrapping border
        var border = new FrameworkElementFactory(typeof(Border));
        border.SetValue(MarginProperty, new Thickness(5.0, 0.0, 5.0, 0.0));
        border.SetValue(BackgroundProperty, Brushes.White);
        border.SetValue(BorderBrushProperty, Brushes.Black);
        border.SetValue(BorderThicknessProperty, new Thickness(1.0, 1.0, 1.0, 0.0));
        border.SetValue(Border.CornerRadiusProperty, new CornerRadius(5.0, 5.0, 0.0, 0.0));

        // Set up the TextBlock
        var textBlock = new FrameworkElementFactory(typeof(TextBlock));
        textBlock.SetValue(TextBlock.TextProperty, propName);
        textBlock.SetValue(MarginProperty, new Thickness(5.0));

        // Set the visual tree of the data template
        border.AppendChild(textBlock);
        customTemplate.VisualTree = border;

        return customTemplate;
    }

    #endregion AutoGeneratingColumn

    #region Sorting

    #region Custom Sorted Event

    // https://whosebug.com/questions/9571178/datagrid-is-there-no-sorted-event

    // Create a custom routed event by first registering a RoutedEventID
    // This event uses the bubbling routing strategy
    public static readonly RoutedEvent SortedEvent = EventManager.RegisterRoutedEvent(
        nameof(Sorted), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ColumnView));

    // Provide CLR accessors for the event
    public event RoutedEventHandler Sorted
    {
        add => AddHandler(SortedEvent, value);
        remove => RemoveHandler(SortedEvent, value);
    }

    // This method raises the Sorted event
    private void RaiseSortedEvent()
    {
        var newEventArgs = new RoutedEventArgs(ColumnView.SortedEvent);
        RaiseEvent(newEventArgs);
    }

    protected override void OnSorting(DataGridSortingEventArgs eventArgs)
    {
        base.OnSorting(eventArgs);
        RaiseSortedEvent();
    }

    #endregion Custom Sorted Event

    private static void OnSorting(object sender, DataGridSortingEventArgs e)
    {
        if (sender is DataGrid dataGrid && dataGrid.HasItems)
        {
            if (dataGrid.Items[dataGrid.Items.Count - 1] is ICanBeLastItem lastItem)
            {
                lastItem.IsLastItem = false;
            }
        }
    }

    private static void OnSorted(object sender, RoutedEventArgs e)
    {
        if (sender is DataGrid dataGrid && dataGrid.HasItems)
        {
            if (dataGrid.Items[dataGrid.Items.Count - 1] is ICanBeLastItem lastItem)
            {
                lastItem.IsLastItem = true;
            }
        }
    }

    #endregion Sorting
}

行项目

public class RowItem : INotifyPropertyChanged, ICanBeLastItem
{
    public RowItem(int firstNumber, string secondNumber, double thirdNumber)
    {
        FirstNumber = firstNumber;
        SecondNumber = secondNumber;
        ThirdNumber = thirdNumber;
    }

    public int FirstNumber { get; }
    public string SecondNumber { get; }
    public double ThirdNumber { get; }

    private bool _isLastItem;
    public bool IsLastItem
    {
        get => _isLastItem;
        set
        {
            _isLastItem = value;
            OnPropertyChanged();
        }
    }

    #region INotifyPropertyChanged

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    #endregion INotifyPropertyChanged
}

public interface ICanBeLastItem
{
    bool IsLastItem { get; set; }
}

MainWindow.xaml

<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="30"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Button Content="Switch Background" Click="ButtonBase_OnClick" />
        <local:ColumnView x:Name="columnView" Grid.Row="1" Padding="10"
                          ItemsSource="{Binding Items}"
                          Background="LightSteelBlue"/>
    </Grid>
</Window>

MainWindow.xaml.cs

/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
    {
        if (columnView.Background == Brushes.LightSteelBlue)
        {
            columnView.Background = Brushes.DarkRed;
        }
        else if (columnView.Background == Brushes.DarkRed)
        {
            columnView.Background = Brushes.Green;
        }
        else if (columnView.Background == Brushes.Green)
        {
            columnView.Background = Brushes.Blue;
        }
        else if (columnView.Background == Brushes.Blue)
        {
            columnView.Background = Brushes.Yellow;
        }
        else
        {
            columnView.Background = Brushes.LightSteelBlue;
        }
    }
}

MainViewModel

public class MainViewModel
{
    public MainViewModel()
    {
        Items = InitializeItems(200);
    }

    private ObservableCollection<RowItem> InitializeItems(int numberOfItems)
    {
        var rowItems = new ObservableCollection<RowItem>();
        var random = new Random();
        for (var i = 0; i < numberOfItems; i++)
        {
            var firstNumber = Convert.ToInt32(1000 * random.NextDouble());
            var secondNumber = Convert.ToString(Math.Round(1000 * random.NextDouble()));
            var thirdNumber = Math.Round(1000 * random.NextDouble());
            var rowItem = new RowItem(firstNumber, secondNumber, thirdNumber);
            rowItems.Add(rowItem);
        }

        rowItems[numberOfItems - 1].IsLastItem = true;

        return rowItems;
    }

    public ObservableCollection<RowItem> Items { get; }
}