Search code examples
c#wpfivalueconverter

Datagrid ValueConverter based on Min or Max Value


I'm trying to change the colour of a datagrid cell based on if it's the min or max value in the ObservableCollection that the value of the cell is a part of.

Now I have the following datagrid and style template:

<Grid ItemsSource="{Binding myData.myObsCollection}">
    <DataGrid.Resources>

    <Style TargetType="{x:Type DataGridCell}">
        <Setter Property="Foreground">
            <Setter.Value>
                <MultiBinding Converter="{StaticResource myValidConverter}">
                    <Binding Path="mdTime" /> //myObsCollection doesn't work here
                    <Binding Path="mdisValid" />
                </MultiBinding>
            </Setter.Value>
         </Setter>
    </Style>...

The collection, simplified, but consists of the following:

public class myClass : INotifyPropertyChanged
{
    ...
    public ObservableCollection<myData> myObsCollection { get; set; }...

public class myData
{
    public int mdTime { get; set; }
    public bool mdisValid { get; set; }...

This currently changes the value of the cell colour depending on the "mdisValid" bool value, which is fine. I want to extend the behaviour and if the mdTime in the current cell value is the lowest in the entire collection I'm binding the datagrid to then change the colour to something else.

The valueconverter sits in the same namespace as the mainwindow, but not within the MainWindow method. I have the MainWindow databound to a ViewModel and the databinding is as sweet as.

I just cannot figure out how to access the collection from within the valueconverter. I've tried getting the datacontext of the parent window, but nothing seems to work. I've now been reading all about valueconverters for several hours, and in every single example it seems to follow the same basic approach. None of them seem to access data outside of the valueconverter method that's called, and I can't figure a way of passing the entire collection into the converter.

I'm sure it's something simple I've overlooked...

EDIT: In addition to sa_ddam's answer I've now added this code to the Style:

<Binding RelativeSource="{RelativeSource Self}"/>

and this to the converter:

SolidColorBrush _brush = new SolidColorBrush(Colors.White);
var cell = (DataGridCell)values[2];
string columnName = cell.Column.Header.ToString();
switch (columnName)
{
    case "mdTime":
        return collection.Min(x => x.mdTime).Equals(collection[0].mdTime)
            ? new SolidColorBrush(Colors.LimeGreen)
            : _brush;
        break;
    case "mdTime2":
        return collection.Min(x => x.mdTime2).Equals(collection[0].mdTime2)
            ? new SolidColorBrush(Colors.LimeGreen)
            : _brush;
        break;...

The columnName now returns the actual text name of the column the cell is in. I then just run a switch on this for the columns I have.

Just need to now look at the next one of refreshing the table so that the Min colour change only applies on a single row, i.e. if this is the smallest value in the collection then it should be one colour else it's always another. What I get now is when I do an add on the collection the old colour values still apply, as well as the Min query on each of the cells of the new row. Getting there slowly ;-)

EDIT #2: So collection[0] is the first in the collection, obviously, and will always be the last data I add, because I actually Insert into position 0. Checking the Min value against this is perfectly valid for the validation on the current row I've added, however not on any others. The grid actually didn't need refreshing at all, so I wasted time with RelayCommands that actually did nothing. Actually they showed me I had an issue with the logic, so not totally wasted time ;-)

I added this simple command and that returns the actual data of the item in question:

var cellData = cell.DataContext as myData;

Then a small change to the query and I get everything exactly how I want it:

return collection.Min(x => x.mdTime).Equals(cellData.mdTime)

Big thanks to sa_ddam he gave me the first part and then I fumbled my way to where I need to.

I'll leave this up in the hope that it helps others out, as there seems to be a lot of "simple" tutorials on the web, but nothing that covers this complex a set-up. Please let me know if it helps you out...


Solution

  • You can get hold of the ItemSource by binding back to the DataGrid, you could use FindAncestor or simply name your DataGrid and use ElementName in the MultiBinding

    Example: (FindAncestor)

    <DataGrid ItemsSource="{Binding myData.myObsCollection}">
        <DataGrid.Resources>
    
        <Style TargetType="{x:Type DataGridCell}">
            <Setter Property="Foreground">
                <Setter.Value>
                    <MultiBinding Converter="{StaticResource myValidConverter}">
                        <Binding Path="ItemsSource" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType={x:Type DataGrid}}" />                                
                        <Binding Path="mdisValid" />
                    </MultiBinding>
                </Setter.Value>
             </Setter>
        </Style>...
    

    Example: (ElementName)

    <DataGrid x:Name="dataGrid" ItemsSource="{Binding myData.myObsCollection}">
        <DataGrid.Resources>
    
        <Style TargetType="{x:Type DataGridCell}">
            <Setter Property="Foreground">
                <Setter.Value>
                    <MultiBinding Converter="{StaticResource myValidConverter}">
                        <Binding Path="ItemsSource" ElementName="dataGrid" />                                       
                        <Binding Path="mdisValid" />
                    </MultiBinding>
                </Setter.Value>
             </Setter>
        </Style>...
    

    Converter:

    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (values[0] is ObservableCollection<myData> && values[1] is int)
        {
            var collection = (ObservableCollection<myData>)values[0];
            var time = (int)values[1];
    
            return collection.Any() && collection.Max(x => x.mdTime).Equals(time)
                ? new SolidColorBrush(Colors.Red)
                : new SolidColorBrush(Colors.Black);
        }
        return new SolidColorBrush(Colors.Black);
    }
    

    Full Working Example

    Code:

    namespace WpfApplication11
    {
        /// <summary>
        /// Interaction logic for MainWindow.xaml
        /// </summary>
        public partial class MainWindow : Window
        {
            private ObservableCollection<myData> _items = new ObservableCollection<myData>();
    
            public MainWindow()
            {
                InitializeComponent();
                Items.Add(new myData { Title = "Item1", mdTime = 1, mdTime2 =3 });
                Items.Add(new myData { Title = "Item2", mdTime = 6, mdTime2 = 50 });
                Items.Add(new myData { Title = "Item3", mdTime = 45, mdTime2 = 23 });
                Items.Add(new myData { Title = "Item4", mdTime = 3, mdTime2 = 1 });
                Items.Add(new myData { Title = "Item5", mdTime = 8, mdTime2 = 9 });
    
            }
    
            public ObservableCollection<myData> Items
            {
                get { return _items; }
                set { _items = value; }
            }
        }
    
        public class myData
        {
            public string Title { get; set; }
            public int mdTime { get; set; }
            public int mdTime2 { get; set; }
            public bool mdisValid { get; set; }
        }
    
        public class MaxConverter : IMultiValueConverter
        {
            public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
            {
                if (values[0] is ObservableCollection<myData> && values[1] is DataGridCell && values[2] is myData)
                {
                    var collection = (ObservableCollection<myData>)values[0];
                    var columnName = (values[1] as DataGridCell).Column.Header.ToString();
                    var data = values[2] as myData;
    
                    if (columnName.Equals("mdTime") && collection.Max(x => x.mdTime).Equals(data.mdTime))
                    {
                         return new SolidColorBrush(Colors.Red);
                    }
                    else if (columnName.Equals("mdTime2") && collection.Min(x => x.mdTime).Equals(data.mdTime))
                    {
                        return new SolidColorBrush(Colors.Blue);
                    }
                }
                return new SolidColorBrush(Colors.Black);
            }
    
            public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
            {
                throw new NotImplementedException();
            }
        }
    }
    

    Xaml:

       <Window x:Class="WpfApplication11.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:local="clr-namespace:WpfApplication11"
            Title="MainWindow" Height="235" Width="308" Name="UI">
    
        <Grid DataContext="{Binding ElementName=UI}" >
    
            <DataGrid ItemsSource="{Binding Items}">
                <DataGrid.Resources>
                    <local:MaxConverter x:Key="myValidConverter" />
    
                    <Style TargetType="{x:Type DataGridCell}">
                        <Setter Property="Foreground">
                            <Setter.Value>
                                <MultiBinding Converter="{StaticResource myValidConverter}">
                                    <Binding Path="ItemsSource" RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}" />
                                    <Binding RelativeSource="{RelativeSource Self}" />
                                    <Binding />
                                </MultiBinding>
                            </Setter.Value>
                        </Setter>
                    </Style>
                </DataGrid.Resources>
            </DataGrid>
    
        </Grid>
    
    </Window>