String format positive and negative values and conditional color formatting XAML

Viewed 3053

I am looking for a simple way to format my result using the following criteria: If positive, add a plus and show it green, if it's negative add a minus and show it red.

I am half way there, I just don't know what's the easiest way to get the color formatting. Is there a way without using a value converter?

<TextBlock Text="{Binding Path=ActualValue, StringFormat='({0:+0.0;-0.0})'}"></TextBlock>

What's the smartest way to go about this? Thank you.

2 Answers

I don't think you can do this without a converter. Here's one that can do the job for number types (except for char):

[ValueConversion(typeof(int), typeof(Brush))]
[ValueConversion(typeof(double), typeof(Brush))]
[ValueConversion(typeof(byte), typeof(Brush))]
[ValueConversion(typeof(long), typeof(Brush))]
[ValueConversion(typeof(float), typeof(Brush))]
[ValueConversion(typeof(uint), typeof(Brush))]
[ValueConversion(typeof(short), typeof(Brush))]
[ValueConversion(typeof(sbyte), typeof(Brush))]
[ValueConversion(typeof(ushort), typeof(Brush))]
[ValueConversion(typeof(ulong), typeof(Brush))]
[ValueConversion(typeof(decimal), typeof(Brush))]
public class SignToBrushConverter : IValueConverter
{
    private static readonly Brush DefaultNegativeBrush = new SolidColorBrush(Colors.Red);
    private static readonly Brush DefaultPositiveBrush = new SolidColorBrush(Colors.Green);
    private static readonly Brush DefaultZeroBrush = new SolidColorBrush(Colors.Green);

    static SignToBrushConverter()
    {
        DefaultNegativeBrush.Freeze();
        DefaultPositiveBrush.Freeze();
        DefaultZeroBrush.Freeze();
    }

    public Brush NegativeBrush { get; set; }
    public Brush PositiveBrush { get; set; }
    public Brush ZeroBrush { get; set; }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (!IsSupportedType(value)) return DependencyProperty.UnsetValue;

        double doubleValue = System.Convert.ToDouble(value);

        if (doubleValue < 0d) return NegativeBrush ?? DefaultNegativeBrush;
        if (doubleValue > 0d) return PositiveBrush ?? DefaultPositiveBrush;

        return ZeroBrush ?? DefaultZeroBrush;
    }

    private static bool IsSupportedType(object value)
    {
        return value is int || value is double || value is byte || value is long ||
               value is float || value is uint || value is short || value is sbyte || 
               value is ushort || value is ulong || value is decimal;
    }

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

Usage:

<local:SignToBrushConverter x:Key="SignToBrushConverter" />

<TextBlock Text="{Binding Path=ActualValue, StringFormat='({0:+0.0;-0.0})'}" 
           Foreground="{Binding ActualValue, Converter={StaticResource SignToBrushConverter}}" />

Or if you want to override the default colors:

<local:SignToBrushConverter x:Key="SignToBrushConverter" NegativeBrush="Purple" PositiveBrush="DodgerBlue" ZeroBrush="Chocolate" />

The right way is to use data triggers, because in general cases foreground and string format might be not only properties to be changed. And in most cases data triggers are used to change DataTemplate of controls.

Here is code example for your case:

<Window.Resources>

    <local:PositiveConverter x:Key="PositiveConverter"/>

    <Style TargetType="TextBlock" x:Key="NumericTextBlock">
        <Style.Triggers>
            <DataTrigger Binding="{Binding Text, RelativeSource={RelativeSource Self}, Converter={StaticResource PositiveConverter}}" Value="True">
                <Setter Property="Foreground" Value="Green"/>
                <Setter Property="Text" Value="{Binding StringFormat='({0:+0.0})'}"/>
            </DataTrigger>

            <DataTrigger Binding="{Binding Text, RelativeSource={RelativeSource Self}, Converter={StaticResource PositiveConverter}}" Value="False">
                <Setter Property="Foreground" Value="Red"/>
                <Setter Property="Text" Value="{Binding StringFormat='({0:-0.0})'}"/>
            </DataTrigger>
        </Style.Triggers>
    </Style>

</Window.Resources>

<Grid>

    <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">

        <TextBlock Text="{Binding Positive}" Style="{StaticResource NumericTextBlock}"/>

        <TextBlock Text="{Binding Negative}" Style="{StaticResource NumericTextBlock}"/>

    </StackPanel>

</Grid>

And code behind:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        Positive = 4;
        Negative = -7;
        InitializeComponent();
        DataContext = this;
    }

    public double Positive { get; set; }
    public double Negative { get; set; }
}

And finally converter class:

public class PositiveConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var number = 0.0;
        var isNumber = double.TryParse(value.ToString(), out number);

        return isNumber && number >= 0.0;
    }

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