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:
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"
:
<local:CellColorConverter x:Key="ColorConverter" />
Now look a little lower down where the DataGrid
style is defined:
<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:
<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:
<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:
object Convert(object[] values, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
and:
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
:
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);
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);
}
}
return new SolidColorBrush(Colors.DarkRed);
}
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:
string columnName = (string)cell.Column.Header;
int content = row.Field<int>(columnName);
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:
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
:
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
:
xmlns:viewmodel="clr-namespace:DataGridProject.ViewModel"
and:
<Window.DataContext>
<viewmodel:MainViewModel />
</Window.DataContext>
Also, in the definition of the DataGrid
in MainWindow.xaml:
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:
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