Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

Dynamic DataGrid Cell Styling in MVVM Projects

4.33/5 (5 votes)
18 Nov 2019CPOL6 min read 20.3K   1.2K  
Changing DataGrid cell styles on the fly in MVVM projects

Introduction

This little app demonstrates one way to dynamically modify the styling of the cells of a DataGrid based on the content of a cell. One example of dynamic styling: If the value in a cell turns negative, you may want to change the background color of the cell to red. This styling is demonstrated with emphasis on the Model-View-ViewModel (MVVM) pattern in particular. I provide the complete Visual Studio source code and other project files here. A working example of the project can be downloaded from here. In the application, every time the user clicks [CHANGE VALUES], the cells in the DataGrid are filled with new random integers from 1 to 9. The background color of the cells change, depending on the new contents of the cell.

Prerequisites

The solution was built using Visual Studio 2019 Community Edition, version 16.3.9 and .NET 4.7.2. It also needs Expression.Blend.Sdk version 1.0.2, but this SDK is packaged with the project files.

It is also assumed that the reader has a basic understanding of C# WPF projects, and the MVVM pattern.

Using the Code

When you build and run the code, the following window will appear:

Image 1

Note: The integer numbers in the cells are generated by a random generator and will differ each time the code is run, and also each time you click the [CHANGE VALUES] button. I realize the colors are a bit garish, but the purpose is to clearly show how a DataGrid cell's background color can be changed when its content changes. It is not intended to comply with Microsoft's bland guidelines for user interface styling.

The dynamic cell styling is done by a MultiValueConverter. This Converter is part of the project's View in the file named: CellColorConverter.cs. These converters require that an object array be passed to them on which their processing will be based. In our case, this object array will consist of two objects: A DataGridCell and the DataRow in the DataGrid that houses the cell for which the attributes must be set.

Each cell contains a random integer between 1 and 9. These numbers change randomly each time you click [CHANGE VALUES]. The cells' background colors also change as detailed below.

In the above example, the cell's background color is set to the system color Colors.LightGoldenrodYellow for all cells in column "First", regardless of the cell's content. For all cells in columns "Second" to column "Eighth", the background color is set as follows:

If the cell contains a number 1, 2 or 3, the background color is set to system color Colors.LightGreen.

If the cell contains a number 4, 5 or 6, the background color is set to system color Colors.LightSteelBlue.

If the cell contains a number 7, 8 or 9, the background color is set to system color Colors.LightSalmon.

In the case of the cells in column "First", the font size is set to 20 and the font style to italic. This can of course be done by simpler means, but the purpose here is to show how the Converter can effect changes to other attributes such as font settings.

First, we need to know how the Converter is tied into the project:

Take a look in the main XAML file: MainWindow.xaml, under the <Window.Resources> tag. Here, the converter is listed as a resource with the Key: "ColorConverter":

XML
<local:CellColorConverter x:Key="ColorConverter" />

Now look a little lower down where the DataGrid style is defined:

XML
        <Style x:Key="BlueDataGridStyle" TargetType="{x:Type DataGrid}">
            <Setter Property="CellStyle" Value="{DynamicResource BlueDataGridCellStyle}" />

.................................

The cell background color in the BlueDataGridCellStyle is defined as follows:

XML
 <Setter Property="Background">
    <Setter.Value>
        <MultiBinding Converter="{StaticResource ColorConverter}">
            <MultiBinding.Bindings>
                <Binding RelativeSource="{RelativeSource Self}" />
                <Binding Path="Row" />
            </MultiBinding.Bindings>
        </MultiBinding>
    </Setter.Value>
</Setter>

Here, you can see that two objects are passed to the Converter, first the cell itself and then the row that contains the cell:

XML
<Binding RelativeSource="{RelativeSource Self}" />
<Binding Path="Row" />

Note about the cell: The cell is passed to the Converter sans its content. You may be tempted in the Converter to try and and access the cell's content as follows: Cell.Content and this is syntactically correct, but it will always return null. More about this later.

So when the system has to render a cell's background, it will see that it needs to pass the cell and its row to the Converter, and the Converter will return the color to use for the background.

The Converter

It's time to look at the all-important Converter. Any MultiValueConverter must obey the contract specified in the IMultiValueConverter interface. This interface dictates that the Converter code shall have two methods:

C#
object Convert(object[] values, Type targetType, 
               object parameter, System.Globalization.CultureInfo culture)

and:

C#
object[] ConvertBack(object value, Type[] targetType, 
                     object parameter, System.Globalization.CultureInfo culture)

Usually, the ConvertBack method does nothing, but must be provided to satisfy the interface. The first parameter, object[], of the Convert(...) method will consist of the cell being rendered and the DataGrid row that contains the cell. The other parameters of this method are not used here.

Here is the entire Convert(...) method of the Converter:

C#
public object Convert(object[] values, Type targetType,
                      object parameter, System.Globalization.CultureInfo culture)
{
 if (values[0] is DataGridCell cell && values[1] is DataRow row)
 {
  try
  {
   string columnName = (string)cell.Column.Header;
   int content = row.Field<int>(columnName); // Header must be same as column name
   int columnIndex = cell.Column.DisplayIndex;

   if (columnIndex == 0)
   {
    cell.FontStyle = FontStyles.Italic;
    cell.FontWeight = FontWeights.Bold;
    cell.FontSize = 20;
    return new SolidColorBrush(Colors.LightGoldenrodYellow);
   }

   if (content < 4)
   {
    return new SolidColorBrush(Colors.LightGreen);
   }

   if (content > 6)
   {
    return new SolidColorBrush(Colors.LightSalmon);
   }

   return new SolidColorBrush(Colors.LightSteelBlue);
  }
  catch (Exception)
  {
   return new SolidColorBrush(Colors.Black); // Error! An Exception was thrown
  }
 }
 return new SolidColorBrush(Colors.DarkRed); // Error! object[] is invalid.
}

First we need to verify that the object[] passed to the Converter did consist of a DataGridCell and DataRow, and at the same time assign names to the two objects:

if (values[0] is DataGridCell cell && values[1] is DataRow row)

Since we cannot directly access the cell's content, we get it from the row Field corresponding to the cell:

C#
string columnName = (string)cell.Column.Header;
int content = row.Field<int>(columnName); // Header must be same as column name

content now holds the integer in the DataGrid cell that was passed to the Converter.

Note: For this approach to work, the DataGrid column headers must be the same as the column names. This is not usually a problem.

Next, the Converter gets the DisplayIndex of the Column, and if the index is zero (leftmost column), the Converter makes some changes to the cell font and return the system color Colors.LightGoldenrodYellow for the cell background. This is how you set an entire column to have the same background color:

C#
int columnIndex = cell.Column.DisplayIndex;

if (columnIndex == 0)
{
 cell.FontStyle = FontStyles.Italic;
 cell.FontWeight = FontWeights.Bold;
 cell.FontSize = 20;
 return new SolidColorBrush(Colors.LightGoldenrodYellow);
}

Next, with the column index greater than zero, the Converter return Colors.LightGreen for content less than 4, Colors.LightSalmon for content greater than 6 and Colors.LightSteelBlue for values 4, 5 and 6:

C#
if (content < 4)
{
 return new SolidColorBrush(Colors.LightGreen);
}

if (content > 6)
{
 return new SolidColorBrush(Colors.LightSalmon);
}

return new SolidColorBrush(Colors.LightSteelBlue);

DataGrid Binding

How do we get the integers into the cells of the DataGrid? If you look in the ViewModel (MainViewModel.cs), you will see a DataTable named ValuesArray that has columns named the same as the columns of the DataGrid. In the View (MainWindow.xaml), you will see that the window's DataContext is set to MainViewModel:

XML
xmlns:viewmodel="clr-namespace:DataGridProject.ViewModel"

and:

XML
<Window.DataContext>
    <viewmodel:MainViewModel />
</Window.DataContext>

Also, in the definition of the DataGrid in MainWindow.xaml:

XML
ItemsSource="{Binding Path=ValuesArray}"

This binds the values in the DataGrid cells to the values of the corresponding cells of DataTable ValuesArray. In other words, the coupling between the View and the ViewModel is very loose. The ViewModel has no direct knowledge of any control in the View, as is required by the MVVM pattern principles.

Button Binding

Similarly, the two buttons are loosely coulped to methods in the ViewModel through binding. When you click on [CHANGE VALUES], the following method in the ViewModel is executed through binding:

C#
private void ChangeValues()
{
 DataRow tableRow;
 int row;
 Random rnd = new Random();

 ValuesArray.Rows.Clear();

 for (row = 0; row < 16; row++)
 {
  tableRow = ValuesArray.NewRow();

  tableRow.SetField<int>("First", rnd.Next(1, 10));
  tableRow.SetField<int>("Second", rnd.Next(1, 10));
  tableRow.SetField<int>("Third 3", rnd.Next(1, 10));
  tableRow.SetField<int>("Fourth", rnd.Next(1, 10));
  tableRow.SetField<int>("Fifth", rnd.Next(1, 10));
  tableRow.SetField<int>("Sixth", rnd.Next(1, 10));
  tableRow.SetField<int>("Seventh", rnd.Next(1, 10));
  tableRow.SetField<int>("Eighth", rnd.Next(1, 10));

  ValuesArray.Rows.Add(tableRow);
 }
}

The ViewModel saves the integers in the DataTable ValuesArray. It is "unaware" that these values are transferred to the View through binding.

In Conclusion

It took me a while to figure out how to use a MultiValueConverter. My goal was to perform all actions related to the styling of DataGrid cells in the View, and to keep the ViewModel uninvolved in these actions.

I do hope that some of you will find the article of value.

History

  • 18th November, 2019: First version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)