WPF datagrid: converter and StringFormat
Asked Answered
P

3

7

I have a standard (WPF toolkit) data grid. Some of the columns (which are explicitly defined) have to be shown as percentages. Some columns have to be shown in red if the values are below 0. (The two sets of columns are not the same). I tried to implement these requirements using a StringFormat and Style, respectively. My XAML:

<Window xmlns:local="clr-namespace:myNamespace"
        xmlns:tk="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit">
    <Window.Resources>
        <local:ValueConverter x:Key="valueToForeground" />
        <Style TargetType="{x:Type tk:DataGridCell}">
            <Setter Property="Foreground"
                    Value="{Binding RelativeSource={RelativeSource Self}, Path=Content.Text, Converter={StaticResource valueToForeground}}" />
        </Style>
    </Window.Resources>
    <Grid>
        <tk:DataGrid AutoGenerateColumns="False"
                     ItemsSource="{Binding Path=myClass/myProperty}">
            <tk:DataGrid.Columns>
                <tk:DataGridTextColumn Header="A"
                                       Binding="{Binding colA}" />
                <tk:DataGridTextColumn Header="B"
                                       Binding="{Binding colB, StringFormat=\{0:P\}}" />
                <tk:DataGridTextColumn Header="C"
                                       Binding="{Binding colC, StringFormat=\{0:P\}}" />
                <tk:DataGridTextColumn Header="D"
                                       Binding="{Binding colD, StringFormat=\{0:P\}}" />
            </tk:DataGrid.Columns>
        </tk:DataGrid>
    </Grid>
</Window>

And the relevant converter:

namespace myNamespace
{
    public class ValueConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            SolidColorBrush brush = new SolidColorBrush(Colors.Black);

            Double doubleValue = 0.0;
            if (value != null)
            {
                if (Double.TryParse(value.ToString(), out doubleValue))
                {
                    if (doubleValue < 0)
                        brush = new SolidColorBrush(Colors.Red);
                }
            }
            return brush;
        }

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

I think it's all pretty standard, but the problem is that the converter gets the Text value after it's gone through the StringFormat, and at that point it's difficult to parse it correctly (since in reality, not all columns have the same format). If I take out the StringFormats, the converter works fine and the text shows up in red. Am I missing something obvious? Is there an easy way to work around this? The only thing that I can think of right now is moving the formatting into a different converter, and I'm not convinced that would work.

Phyllous answered 29/5, 2012 at 9:7 Comment(0)
S
6

We had a similar situation where we needed a different Path Property for the Binding but otherwise a similar CellStyle for each DataGridColumn. We solved this with a custom MarkupExtension. In your case it would look like this

<tk:DataGrid AutoGenerateColumns="False" 
                ItemsSource="{Binding MyItems}">
    <tk:DataGrid.Columns>
        <tk:DataGridTextColumn Header="A" 
                               Binding="{Binding colA}" />
        <tk:DataGridTextColumn Header="B" 
                               Binding="{Binding colB, StringFormat=\{0:P\}}"
                               CellStyle="{markup:ForegroundCellStyle PropertyName=colB}"/>
        <tk:DataGridTextColumn Header="C" 
                               Binding="{Binding colC, StringFormat=\{0:P\}}"
                               CellStyle="{markup:ForegroundCellStyle PropertyName=colC}"/>
        <tk:DataGridTextColumn Header="D" 
                               Binding="{Binding colD, StringFormat=\{0:P\}}"
                               CellStyle="{markup:ForegroundCellStyle PropertyName=colD}"/>
    </tk:DataGrid.Columns>
</tk:DataGrid>

and then ForegroundCellStyleExtension creates the Style for DataGridCell depending on PropertyName

ForegroundCellStyleExtension

public class ForegroundCellStyleExtension : MarkupExtension
{
    public ForegroundCellStyleExtension() { }
    public ForegroundCellStyleExtension(string propertyName)
    {
        PropertyName = propertyName;
    }

    public string PropertyName
    {
        get;
        set;
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        IProvideValueTarget service = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
        DependencyObject targetObject = service.TargetObject as DependencyObject;
        if (targetObject == null)
        {
            return null;
        }

        Binding foregroundBinding = new Binding
        {
            Path = new PropertyPath(PropertyName),
            Converter = new ValueConverter()
        };
        Style foregroundCellStyle = new Style(typeof(DataGridCell));
        foregroundCellStyle.Setters.Add(new Setter(DataGridCell.ForegroundProperty, foregroundBinding));

        return foregroundCellStyle;
    }
}

Also, if you have some other Setters etc. that you would like to use then they can be included by another parameter to the MarkupExtension.

<Window.Resources>
    <Style x:Key="dataGridCellStyle" TargetType="{x:Type tk:DataGridCell}">
        <Setter Property="Background" Value="Blue"/>
    </Style>
</Window.Resources>
<!-- ... -->
<tk:DataGridTextColumn Header="B" 
                       Binding="{Binding colB, StringFormat=\{0:P\}}"
                       CellStyle="{markup:ForegroundCellStyle colB, {StaticResource dataGridCellStyle}}"/>

And ForegroundCellStyleExtension would then use the second parameter as BasedOn for the DataGridCell Style

ForegroundCellStyleExtension with BasedOn

public class ForegroundCellStyleExtension : MarkupExtension
{
    public ForegroundCellStyleExtension() { }
    public ForegroundCellStyleExtension(string propertyName, Style basedOnCellStyle)
    {
        PropertyName = propertyName;
        BasedOnCellStyle = basedOnCellStyle;
    }

    public string PropertyName
    {
        get;
        set;
    }
    public Style BasedOnCellStyle
    {
        get;
        set;
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        IProvideValueTarget service = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
        DependencyObject targetObject = service.TargetObject as DependencyObject;
        if (targetObject == null)
        {
            return null;
        }

        Binding foregroundBinding = new Binding
        {
            Path = new PropertyPath(PropertyName),
            Converter = new ValueConverter()
        };
        Style foregroundCellStyle = new Style(typeof(DataGridCell), BasedOnCellStyle);
        foregroundCellStyle.Setters.Add(new Setter(DataGridCell.ForegroundProperty, foregroundBinding));

        return foregroundCellStyle;
    }
}
Skimmer answered 29/5, 2012 at 11:56 Comment(2)
this looks like it would work, but I didn't have enough time to test it out. Thanks anyway!Phyllous
this is a much better solution for re-use, and works like a charm! this should be the real answerCastara
S
2

Specify a cell style for each column as follows:

<DataGridTextColumn Header="ColA" Binding="{Binding colA, StringFormat=\{0:P\}}">
    <DataGridTextColumn.CellStyle>
        <Style TargetType="DataGridCell">
            <Setter Property="Foreground" 
                    Value="{Binding colA, Converter={StaticResource valueToForeground}}" />
         </Style>
    </DataGridTextColumn.CellStyle>
</DataGridTextColumn>

<DataGridTextColumn Header="ColB" Binding="{Binding colB, StringFormat=\{0:P\}}">
    <DataGridTextColumn.CellStyle>
        <Style TargetType="DataGridCell">
            <Setter Property="Foreground" 
                    Value="{Binding colB, Converter={StaticResource valueToForeground}}" />
         </Style>
    </DataGridTextColumn.CellStyle>
</DataGridTextColumn>

... 

and modify your converter

public class ValueConverter : IValueConverter
{
    public object Convert(
                object value, Type targetType, object parameter, CultureInfo culture)
    {
        return ((double) value < 0) ? Brushes.Red : Brushes.Black;
    }

    public object ConvertBack(
                object value, Type targetType, object parameter, CultureInfo culture)
    {
        return Binding.DoNothing;
    }
}

enter image description here

Sopher answered 29/5, 2012 at 9:54 Comment(2)
@vlad: it definitely works, I forgot to add the updated value converterSopher
I stand corrected. I made a small change in the XAML: instead of TargetType="DataGridCell" I have TargetType="{x:Type tk:DataGridCell}" I think this is required in .NET 3.5 (since the datagrid comes from the toolkit), but probably not 4.0. Anyway, +1 to you, sir!Phyllous
C
1

The simplest way I figured out is to bind your full item instead of the item/content.text only to your converter. Then you will be able to do what you wanted to do with your cells with need to worry about the item and parameter values.

In your Cell Style:

 <Setter Property="Foreground"
     Value="{Binding Converter={StaticResource valueToForeground}}" />

and in your Converter code:

public object Convert(object value, Type targetType,
    object parameter, System.Globalization.CultureInfo culture)
{
    SolidColorBrush brush = new SolidColorBrush(Colors.Black);    

    Double doubleValue = 0.0;
    if (value != null)
    {
        mydatatype data = value as mydatatype;
        //your logic goes here and also can play here with your dataitem. 
        if (Double.TryParse(data.CollD.ToString(), out doubleValue))
        {
            if (doubleValue < 0)
               brush = new SolidColorBrush(Colors.Red);
        }        
    }
    return brush;
}
Carouse answered 29/5, 2012 at 13:21 Comment(1)
I ended up doing something similar, but keeping the Binding Path to Content.Text. Thanks!Phyllous

© 2022 - 2024 — McMap. All rights reserved.