Search code examples
c#wpfmvvmdatagriddatatrigger

Style DataGridCell background depending on selected row values in a WPF MVVM DataGrid bound to a DataTable


I'm trying to write an application that is collecting settings from a remote endpoint and compares them in a DataGrid. These units can have different number of settings depending on which software version they are running and settings can also have been removed in a newer SW-version, not just added. There are ~500 settings in each unit.

Due to the difference in settings information I can't write a proper class and describe each setting so I'm building a dictionary where the setting names are the keys and the setting parameter value is the value. When the dictionary is done for each unit I add it to an ObservableCollection. This collection is a shared resource between the "settingsCollection" view and the "compareSettings" view and is dependency injected in both.

WPF Datagrid are not really working with dictionaries so I'm building a DataTable that the DataGrid can be bound to. If it would be a non MVVM app I would build my DataGrid from the .xaml.cs file and add all attributes to it here.

I get the visualization to work alright but I would like the cell backgrounds change colours depending on the selected row. If I have two rows like looking like this:

Units Setting 1 Setting 2 Setting 3
Unit 1 1 2 3
Unit 2 2 2 2

and the first row selected, I would like the 2 in the columns for "Setting 1" and "Setting 3" to be red while 2 in column "Setting 2" should be green. If I'm changing the selected row, the backgrounds in the cells should change colour depending if they are the same or not.

I built a fake "settingsCollector" in my viewmodel constructor just make it more easy to work with it.

public class MainWindowViewModel : ObservableObject
{
    private DataTable _settingsdataTable = new DataTable();
    public DataTable SettingsDataTable
    {
        get => _settingsdataTable;
        set {
            if (SettingsDataTable != value) {
                _settingsdataTable = value;
                OnPropertyChanged(nameof(SettingsDataTable));
            }
        }
    }

    private Dictionary<string, string> _selectedUnit = new();
    public Dictionary<string, string> SelectedUnit
    {
        get => _selectedUnit;
        set {
            if (SelectedUnit != value) {
                _selectedUnit = value;
                OnPropertyChanged(nameof(SelectedUnit));
            }
        }
    }

    public ICommand CellSelectionChangedCommand { get; }

    private void CellSelected(object o)
    {
        DataGrid dataGrid = o as DataGrid;
        Dictionary<string, string> newSelectedUnit = new();

        for (int i = 0; i < dataGrid.SelectedCells.Count; i++) {
            DataRowView rowView = dataGrid.SelectedCells[i].Item as DataRowView;
            newSelectedUnit.Add(dataGrid.SelectedCells[i].Column.Header.ToString(), rowView.Row[i].ToString());
        };
        SelectedUnit = newSelectedUnit;
    }

    public MainWindowViewModel()
    {
        ObservableCollection<Dictionary<string, string>> DictionaryCollection = new();

        // Make up some settings from multiple diffrent endpoints
        for (int i = 0; i < 2; i++) {
            Dictionary<string, string> incomingSettings = new();
            incomingSettings.Add($"Setting {i}", $"{i}");
            incomingSettings.Add($"Setting {i + 1}", $"{i}");
            incomingSettings.Add($"Setting {i + 2}", $"{i}");
            incomingSettings.Add($"Setting {i + 3}", $"{i}");
            incomingSettings.Add($"Setting {i + 4}", $"{i}");
            incomingSettings.Add($"Setting {i + 6}", $"{i + 3}");
            //... Could continue forever
            DictionaryCollection.Add(incomingSettings);
        };
        SelectedUnit = DictionaryCollection.First();

        // Go through the settings parameters in the incoming units
        // and add them to a list of all possible existing settings parameters
        List<string> AllDictionaryKeys = new();

        foreach (var item in DictionaryCollection)
        {
            foreach (var key in item.Keys)
            {
                if (!AllDictionaryKeys.Contains(key))
                    AllDictionaryKeys.Add(key);
            }
        }

        // Order keys to get all settings alphabetically.
        AllDictionaryKeys = AllDictionaryKeys.OrderBy(x => x.Length).ThenBy(x => x).ToList();

        // Create the full dataTable
        // Start with adding the headers in the table
        DataTable newDataTable = new();

        foreach (var header in AllDictionaryKeys)
        {
            newDataTable.Columns.Add(header, typeof(string));
        }

        // Add values in the table depending on the header keys
        // If the setting does not exists in the unit, add a "-" instead
        foreach (var item in DictionaryCollection)
        {
            DataRow row = newDataTable.NewRow();

            foreach (var key in AllDictionaryKeys)
            {
                if (!item.ContainsKey(key))
                    row[key] = "-";
                else
                    row[key] = item[key];
            }
            newDataTable.Rows.Add(row);
        }

        // Update the dataTable bound to the view
        SettingsDataTable = newDataTable;

        // RelayCommands from MVVM Toolkit
        CellSelectionChangedCommand = new RelayCommand<object>(o =>
        {
            Debug.WriteLine("CellSelectionChanged");
            CellSelected(o);
        });
    }
}
<UserControl x:Class="WpfApp1.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:viewmodel="clr-namespace:WpfApp1"
        xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
        xmlns:local="clr-namespace:WpfApp1" 
        d:DataContext="{d:DesignInstance Type=viewmodel:MainWindowViewModel}"
        mc:Ignorable="d"
        Background="AntiqueWhite">
    <UserControl.Resources>
        <local:CellBackgroundConverter x:Key="CellBackgroundConverter"/>
        <CollectionViewSource x:Key="MyDataViewSource" Source="{Binding SettingsDataTable.DefaultView}" />
    </UserControl.Resources>
    
    <Grid
        Margin="10"
    >
        <DataGrid 
            x:Name="MyDataGrid"
            CanUserAddRows="True"
            AutoGenerateColumns="True" 
            ItemsSource="{Binding Source={StaticResource MyDataViewSource}}" 
            Margin="5,90,5,5"
            SelectionUnit="FullRow"
            SelectionMode="Single"
            IsReadOnly="True">

            <!--Add interaction triggers-->
            <i:Interaction.Triggers>
                <!--Add event trigger-->
                <i:EventTrigger 
                    EventName="SelectedCellsChanged">
                    <i:InvokeCommandAction 
                        Command="{Binding CellSelectionChangedCommand}"
                        CommandParameter="{Binding ElementName=MyDataGrid}"/>
                </i:EventTrigger>
            </i:Interaction.Triggers>

            <DataGrid.CellStyle>
                <Style TargetType="DataGridCell">
                    <!-- Set the default background color -->
                    <Setter Property="Background" Value="Blue" />
                    <Style.Triggers>
                        <!-- Set the background color for unselected cells in the selected column -->
                        <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type DataGridRow}}, Path=IsSelected}" Value="False">
                            <Setter Property="Background" Value="Red" />
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </DataGrid.CellStyle>
        </DataGrid>
    </Grid>
</UserControl>

I tried to add a multibinding and build a converter but it was triggered too many times. I also tried the aoutoGeneratingColumn event but that doesn't really change the cell background.

Can I somehow change the cell bacground colour depending on the selected row values? If the other cells in the columns have a diffrent value than the selected one, it should be red, otherwise green.


Solution

  • Fixed it like this with the help of the answer of mm8.

                <DataGrid.CellStyle>
                <Style TargetType="DataGridCell" BasedOn="{StaticResource {x:Type DataGridCell}}">
                    <Setter Property="Background">
                        <Setter.Value>
                            <MultiBinding Converter="{StaticResource CellBackgroundConverter}">
                                <Binding Path="SelectedItem" RelativeSource="{RelativeSource AncestorType=DataGrid}" />
                                <Binding Path="." />
                                <Binding Path="Column.DisplayIndex" RelativeSource="{RelativeSource Self}"  />
                            </MultiBinding>
                        </Setter.Value>
                    </Setter>
                </Style>
            </DataGrid.CellStyle>
    

    and the converter looking like this

        public class CellBackgroundConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            if (values[0] == null || values[1] == null || values[2] == null )
            {
                // Handle the case when values are null, indicating FallbackValue
                return new SolidColorBrush(Colors.Transparent);
            }
    
            DataRowView selectedRow = (DataRowView)values[0] ;
            DataRowView currentRow = (DataRowView)values[1];
            Int32 selectedIndex = (Int32)values[2];
                    
            if (selectedRow == currentRow)
                return new SolidColorBrush(Colors.LightBlue);
            else if (selectedIndex == 0)
                return new SolidColorBrush(Colors.Transparent);
    
            // compare the selected item with the current item 
            var currentItem = currentRow.Row.ItemArray[selectedIndex];
            var selectedItem = selectedRow.Row.ItemArray[selectedIndex];
    
            if(currentItem.ToString() == selectedItem.ToString())
                return new SolidColorBrush(Colors.LightGreen);
            else
                return new SolidColorBrush(Colors.Salmon);
        }
    

    So now the DataGrid updates the fields in each column depending on if the value is the same or not as can be seen below. Im getting a binding error with the single selected cell when its looking for it's border brush after sorting the column when clicking the column header but I can live with that.

    Animation of final result