Sunday, July 2, 2017

Published 9:29 AM by with 0 comment

How Do I Change A DataGrid Cell's Background Color Based On Its Value With MVVM In C#?

Another thing I needed to do but couldn't find a great description of through googling...


Problem Statement

I want to have a dynamically generated datagrid that updates cell colors in a 'Grade' column if that column contains a grade of 'D' or 'F'. I want it to adapt to new columns being inserted so that the 'Grade' column is not in a fixed position because my columns are sometimes inserted at runtime and are fully dynamic.

Basic Requirements

  • Need to update a cell's background based on the cell's value and location in the grid
  • No code-behind is used
  • The update should be targeted to only the cells that you care about

Libraries Used

For MVVM in C#, I use Prism. I'm using 6.3 here but nothing I'm using is very new so older versions should work. You can get Prism through nuget...just get Prism.Core.


Note that like usual, this is not a full Prism app and is not taking advantage of everything it provides. This is very targeted to the problem here and is not structured in the way you'd structure a real Prism application.

Preview of Result


Targeting Only the Cells with the Grades

I'll walk through two basic ways to do this:
  • using converters
  • using datatriggers
I prefer the converter approach and will explain why after showing both. In both cases, I will do it by overriding the Datagrid's CellStyle.

Converters

One way to do this is to pass relevant values to a converter. There are a lot of options and the two most obvious to me in this case are:
  1. Make a multi-value converter and pass in the content of the cell and the name of its column header
  2. Use a single-value converter, pass in the cell itself, and parse it in the converter code
I've gone with #1 here.

Our xaml here looks like the following:

<DataGrid.CellStyle>
    <Style TargetType="{x:Type DataGridCell}">
        <Style.Setters>
            <Setter Property="Background">
                <Setter.Value>
                    <MultiBinding Converter="{StaticResource GradeToCellBackgroundConverter}">
                        <Binding Path="Content.Text" RelativeSource="{RelativeSource Self}" />
                        <Binding Path="Column.Header" RelativeSource="{RelativeSource Self}" />
                    </MultiBinding>
                </Setter.Value>
            </Setter>
        </Style.Setters>
    </Style>
</DataGrid.CellStyle>

Since this is a DataGridCell and it's referencing itself, Content.Text returns the text contained in the cell and Column.Header returns the name of the column containing the cell. In the converter itself, we put the logic for converting those two fields into a background:


public object Convert(object[] value, Type targetType, object parameter, CultureInfo culture)
{
    if(value[0].GetType() == typeof(string))
    {
        if (((string)value[1] == "Grade") && (((string)value[0] == "D") || ((string)value[0] == "F")))
        {
            return (SolidColorBrush)(new BrushConverter().ConvertFrom("#EEAAAA")); ;
        }
    }
    return null;
}

Basically...if the field is text, it's value is 'D' or 'F', and the column it belongs to is 'Grade', then return red for the background.

Triggers

You can do something very similar with triggers here. Basically...if column name is 'Grade' and content is 'D' or 'F', set the background to red:

<DataGrid.CellStyle>
    <Style TargetType="{x:Type DataGridCell}">
        <Style.Triggers>
            <MultiDataTrigger>
                <MultiDataTrigger.Conditions>
                    <Condition Binding="{Binding Path=Content.Text, RelativeSource={RelativeSource Self}}" Value="D"/>
                    <Condition Binding="{Binding Path=Column.Header, RelativeSource={RelativeSource Self}}" Value="Grade" />
                </MultiDataTrigger.Conditions>
                <Setter Property="Background" Value="#EEAAAA" />
            </MultiDataTrigger>
            <MultiDataTrigger>
                <MultiDataTrigger.Conditions>
                    <Condition Binding="{Binding Path=Content.Text, RelativeSource={RelativeSource Self}}" Value="F"/>
                    <Condition Binding="{Binding Path=Column.Header, RelativeSource={RelativeSource Self}}" Value="Grade" />
                </MultiDataTrigger.Conditions>
                <Setter Property="Background" Value="#EEAAAA" />
            </MultiDataTrigger>
        </Style.Triggers>
    </Style>
</DataGrid.CellStyle>

There's again multiple options here but this was the most obvious to me. As I noted, I prefer the converter one because it's easier to debug to me, and is cleaner to me when you have a lot of conditions. There's probably also a performance difference, but it shouldn't really matter either way here with a properly virtualized grid.

Other options

As I mentioned, you could do this with a single-value converter. Another obvious way to do it is bind the background color to a ViewModel property that you create for it, but I dislike that method and won't go into it here.

Summary

Using any of the methods described above, you can meet all requirements listed. If you want to confirm that it adapts to dynamic columns, try un-commenting the 'Garbage' property in the model and see that it colors the values in the 'Grades' column. You can also try removing the 'Grade' property to confirm that no fields have a red background.

You can download the source here
      edit

0 comments:

Post a Comment