Home > Enterprise >  How can I change the DataGrid Foreground color based on user input?
How can I change the DataGrid Foreground color based on user input?

Time:10-16

I have a DataGrid. There are 12 columns in the DataGrid. Each of these columns represents byte values between 0-255. I want to colorize them depending on the ranges that the user enters.

I don't know how I can change these values based on user input. The user can specify different ranges for each column. Below is an application that I implemented manually. How can I bind this app to user login.

Converter

public class DataGridColorConverter : IValueConverter
{
    public object Convert(
        object value, Type targetType,
        object parameter, CultureInfo culture)
    {
        byte data = (byte)value;

        if (data <= 30)
            return 0;
        else if (data <= 60)
            return 1;
        else if (data <= 90)
            return 2;
        else
            return 3;
    }

    public object ConvertBack(
        object value, Type targetType,
        object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Xaml

<UserControl.Resources>
    <my:DataGridColorConverter x:Key="DGCConverter"/>
</UserControl.Resources>

<DataGrid Name="MyDataGrid"
          Grid.Row="1"
          AutoGenerateColumns="False" 
          ItemsSource="{Binding JobsCollectionView , IsAsync=True}"
          VirtualizingStackPanel.VirtualizationMode="Recycling"
          IsReadOnly="True"
          Height="480">
    <DataGrid.Columns>
        <DataGridTextColumn Header="ID" Binding="{Binding ID}"/>
        <DataGridTextColumn Header="RTR" Binding="{Binding RTR}"/>
        <DataGridTextColumn Header="IDE" Binding="{Binding IDE}"/>
        <DataGridTextColumn Header="DLC" Binding="{Binding DLC}"/>
        <DataGridTextColumn Header="BYTE-0" Binding="{Binding Byte0}"/>
        <DataGridTextColumn Header="BYTE-1" Binding="{Binding Byte1}"/>
        <DataGridTextColumn Header="BYTE-2" Binding="{Binding Byte2}"/>
        <DataGridTextColumn Header="BYTE-3" Binding="{Binding Byte3}"/>
        <DataGridTextColumn Header="BYTE-4" Binding="{Binding Byte4}"/>
        <DataGridTextColumn Header="BYTE-5" Binding="{Binding Byte5}"/>
        <DataGridTextColumn Header="BYTE-6" Binding="{Binding Byte6}"/>
        <DataGridTextColumn Header="BYTE-7" Binding="{Binding Byte7}">
            <DataGridTextColumn.ElementStyle>
                <Style TargetType="{x:Type TextBlock}">

                    <Setter Property="Foreground" Value="Green" />
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding Byte7,Converter={StaticResource DGCConverter}}" Value="1">
                            <Setter Property="Foreground" Value="Yellow" />
                        </DataTrigger>
                        <DataTrigger Binding="{Binding Byte7,Converter={StaticResource DGCConverter}}" Value="2">
                            <Setter Property="Foreground" Value="Orange" />
                        </DataTrigger>
                        <DataTrigger Binding="{Binding Byte7,Converter={StaticResource DGCConverter}}" Value="3">
                            <Setter Property="Foreground" Value="Red" />
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </DataGridTextColumn.ElementStyle>
        </DataGridTextColumn>
        <DataGridTextColumn Header="TIME" Binding="{Binding Time, StringFormat=\{0:dd.MM.yy HH:mm:ss\}}"/>
    </DataGrid.Columns>
</DataGrid>

Note : I am using MVVM.

Note 2: I did not share the fields where the user can enter value ranges so that it is not crowded in XAML.

For example, the user wanted Blue between 0-100, Red between 100-150, Green between 150-255.

CodePudding user response:

I don't know how I can change these values based on user input. The user can specify different ranges for each column.

If a user can specify the ranges, e.g. through Sliders or TextBoxes, those values need to be bound in order to react to changes. Let's assume that the brushes that these values map to do not need to be bound (yet). An IValueConverter can only bind a single value. You could instead create an IMultiValueConverter, which can bind multiple values as its name suggests. There are multiple options how a concrete converter could look like, depending on your view, view model and other requirements.

The following is an example of an IMultiValueConverter that allows binding multiple byte properties, where the first is the original data value to be compared and the rest represent the boundaries of each range. The Brushes for these ranges are passed as parameter (a collection) and are not bindable. If you would like them to be bindable, too, they would be passed though the values arrays as well. This converter assumes that all values are bytes and the parameter is a collection of Brushes. Furthermore, there must be distinct boundaries of ranges (no duplicates) and an equal number of Brushes (can be duplicates), otherwise there is no 1:1 mapping (throws an exception). If there are no bound values or no possible mapping, no brush will be returned.

public class DataGridColorConverter : IMultiValueConverter
{
   public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
   {
      if (values == null)
         return Binding.DoNothing;

      var byteValues = values.Cast<byte>().ToList();
      var brushes = ((IEnumerable)parameter).Cast<Brush>().ToList();

      var data = byteValues[0];
      var bounds = byteValues.Skip(1).Distinct().ToList();

      if (bounds.Count != brushes.Count)
         throw new ArgumentException("The number of distinct stops must be equal to the number of stop brushes.");

      foreach (var (boundary, brush) in bounds.Zip(brushes))
      {
         if (data <= boundary)
            return brush;
      }

      return Binding.DoNothing;
   }

   public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
   {
      throw new NotImplementedException();
   }
}

In XAML, you can adapt your style with a MultiBinding using the new converter.

<Style TargetType="{x:Type TextBlock}">
   <Setter Property="Foreground">
      <Setter.Value>
         <MultiBinding Converter="{StaticResource DataGridColorConverter}">
            <MultiBinding.Bindings>
               <!-- ...binds the current data context, your BYTE 7. -->
               <Binding/>
               <!-- ...these bindings may vary depending on the source for binding, view model, text block, ... -->
               <Binding Path="MyBlueRangeEndProperty"/>
               <Binding Path="MyRedRangeEndProperty"/>
               <Binding Path="MyGreenRangeEndProperty"/>
            </MultiBinding.Bindings>
            <MultiBinding.ConverterParameter>
               <!-- ...this array could also be shared (moved to resource and refernce via {StaticResource ...}). -->
               <x:Array Type="{x:Type Brush}">
                  <SolidColorBrush Color="Blue"/>
                  <SolidColorBrush Color="Red"/>
                  <SolidColorBrush Color="Green"/>
               </x:Array>
            </MultiBinding.ConverterParameter>
         </MultiBinding>
      </Setter.Value>
   </Setter>
</Style>

As stated above, there are plenty of options to implement this. This should be sufficient for your use-case and enable you to even share the color array if it is the same for all columns or define each individually.


Update for the comment about compilation errors with Zip. This extension method is not available on all target frameworks. The same applies to the C# language version and tuple deconstruction. However, in these cases you can use a simple for loop instead.

for (var i = 0; i < bounds.Count; i  )
{
   if (data <= bounds[i])
      return brushes[i];
}
  • Related