1
votes

I have a simple WPF application that shows about 30-35,000 rows of data in a datagrid and if a user changes any cell value, that cell should be highlighted by changing its background color. Columns are generated at runtime depending on the data. Given the quantity of the data it holds, virtualized datagrid is a must. Here is my datagrid:

<DataGrid x:Name="dgPropertyData" HeadersVisibility="Column" ColumnHeaderHeight="25" 
                  BorderThickness="0" AutoGenerateColumns="false" 
                  HorizontalGridLinesBrush="#FFC7C5C5" 
                  VerticalGridLinesBrush="#FFC7C5C5" 
                  MinRowHeight="20" CanUserResizeRows="False" 
                  CanUserDeleteRows="False" CanUserReorderColumns="False" 
                  EnableRowVirtualization="true" 
                  AutoGeneratedColumns="dgPropertyData_AutoGeneratedColumns"                      
                  CellEditEnding="dgPropertyData_CellEditEnding">
            <DataGrid.ColumnHeaderStyle>
                <Style TargetType="{x:Type DataGridColumnHeader}">
                    <Setter Property="FontWeight" Value="Bold"/>
                </Style>
            </DataGrid.ColumnHeaderStyle>
        </DataGrid>

I am changing a cell background color inside a code-behind method:

    private void dgPropertyData_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
    {
        string oldValue = (e.Row.Item as BindableDynamicDictionary)[e.Column.Header as string] as string;
        string newValue = (e.EditingElement as System.Windows.Controls.TextBox).Text;

        if ((oldValue != null && oldValue.CompareTo(newValue) != 0) || (oldValue == null && string.IsNullOrEmpty(newValue) == false))
        {         
            (e.Column.GetCellContent(e.Row).Parent as DataGridCell).Background = Brushes.Orange;
            IsCellEdited = true;
        }
    }

My Issue: When I edit a cell the background changes nicely to notify that the value has been changed. Only until.... I scroll the grid view. On scrolling, edited cells get a wipe and wrong cells get painted (if at all).

I have gone through following questions to get some ideas:

  1. Changing color of a single cell in WPF Datagrid
  2. Wpf Datagrid Virtualization Issue when setting cell colors
  3. RowVirtualization cause incorrect background color for rows

But haven't quite found a solution for my purpose. Being very new to WPF world, I get a mind block quite often as how to solve this problem. I understand that the issue of losing the cell background color is because of recycling of the rows and that if I disable the virtualization, the performance hit is unbearable for the amount of data I have.

2

2 Answers

0
votes

Instead of handling the CellEditEnding event in the view you should handle the logic in your row view model, which seems to be a BindableDynamicDictionary in your case.

In the setter of the property whose value is displayed in the cell, you could do the comparison (the one that you currently do in the code-behind of the view) and set an IsCellEdited property to indicate whether the cell has been modified.

In the view you should then use a CellStyle with a DataTrigger that binds to this property and changes the background colour of the cell depending on its value.

This design pattern is known as Model-View-ViewModel (MVVM) and it is the recommended pattern to use when developing XAML based UI applications. There is a reason for this. You'have just ran into one of them. This kind of logic should not be handled in the view and, as you have already noticed, it won't even work if you disable the UI virtualization.

0
votes

As mentioned in my original post, I am still new to WPF and MVVM so I still think in code-behind language a-lot. One day, I will implement it in pure XAML. But until then, I came-up with code-behind solution. I know, a lot of people won't like it, but this is something rather than just being stuck.

I have a dictionary to keep track of edited cells

Dictionary<int, List<int>> editedCells = new Dictionary<int, List<int>>(); // Row No, List<Column No>

I save the edited cell location in this dictionary while changing the background:

    private void dgPropertyData_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
    {
        string oldValue = (e.Row.Item as BindableDynamicDictionary)[e.Column.Header as string] as string;
        string newValue = (e.EditingElement as System.Windows.Controls.TextBox).Text;

        if ((oldValue != null && oldValue.CompareTo(newValue) != 0) || (oldValue == null && string.IsNullOrEmpty(newValue) == false))
        {         
            (e.Column.GetCellContent(e.Row).Parent as DataGridCell).Background = Brushes.Orange;
            IsCellEdited = true;

            // Mark this cell has been edited.
            var columnIndex = e.Column.DisplayIndex;
            var rowIndex = dgPropertyData.Items.IndexOf(e.Row.Item);

            if (editedCells.ContainsKey(rowIndex) == false)
                editedCells.Add(rowIndex, new List<int>());

            editedCells[rowIndex].Add(columnIndex);                                   
        }
    }

When user scrolls the datagrid, some rows will be hidden, some will be shown. When a row is being hidden, I clear the edited cell background, and when shown, I recolor the edited cells.

    private void dgPropertyData_LoadingRow(object sender, DataGridRowEventArgs e)
    {
        var rowIndex = dgPropertyData.Items.IndexOf(e.Row.Item);

        if(editedCells.ContainsKey(rowIndex) == true)
        {
            editedCells[rowIndex].ForEach( columnIndex =>
            {
                var column = dgPropertyData.Columns[columnIndex];

                (column.GetCellContent(e.Row).Parent as DataGridCell).Background = Brushes.Orange;

            });                
        }
    }

    private void dgPropertyData_UnloadingRow(object sender, DataGridRowEventArgs e)
    {
        var rowIndex = dgPropertyData.Items.IndexOf(e.Row.Item);

        if (editedCells.ContainsKey(rowIndex) == true)
        {
            editedCells[rowIndex].ForEach(columnIndex =>
            {
                var column = dgPropertyData.Columns[columnIndex];

                (column.GetCellContent(e.Row).Parent as DataGridCell).ClearValue(DataGridCell.BackgroundProperty);

            });
        }
    }

Works fine for me.