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.
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.