Eric
Eric

Reputation: 472

Pass Color to IValueConverter

I am making a WPF application using the MVVM design pattern. Part of the app is a signal strength bar. We created it with just a rectangular user control and created a 4 column grid, so all we need to do is change either the background or foreground color of the control.

My idea on how to do this is a simply store Boolean values for each of the 4 sections and use a value converter. However, there are 3 instances of this control, each with a different color. How can I pass the needed color into the converter? I know converters have a parameter argument, but I haven't been able to find any examples using it, so I'm not even sure if the parameter argument is what I'm looking for.

Upvotes: 1

Views: 2680

Answers (2)

Your case may not be best addressed by the method you've chosen (it makes it hard to parameterize the colors of the segments), but your specific question is a good one, so I'll answer it.

As you've found, it's tough to pass anything but a string to ConverterParameter. but you don't have to. If you derive a converter from MarkupExtension, you can assign named and typed properties when you use it, and also not have to create it as a resource (indeed, creating it as a resource would break the thing, since that would be a shared instance and the properties are initialized when it's created). Since the XAML parser knows the types of the properties declared on the class, it will apply the default TypeConverter for Brush, and you'll get the exact same behavior as if you were assigning "PapayaWhip" to "Border.Background" or anything else.

This works with any type, of course, not just Brush.

namespace HollowEarth.Converters
{
    public class BoolBrushConverter : MarkupExtension, IValueConverter
    {
        public Brush TrueBrush { get; set; }
        public Brush FalseBrush { get; set; }

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return System.Convert.ToBoolean(value) ? TrueBrush : FalseBrush;
        }

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

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            return this;
        }
    }
}

Usage:

<TextBox 
    xmlns:hec="clr-namespace:HollowEarth.Converters"
    Foreground="{Binding MyFlagProp, Converter={hec:BoolBrushConverter TrueBrush=YellowGreen, FalseBrush=DodgerBlue}}" 
    />

You could give BoolBrushConverter a constructor that takes parameters, too.

public BoolBrushConverter(Brush tb, Brush fb)
{
    TrueBrush = tb;
    FalseBrush = fb;
}

And in XAML...

<TextBox 
    xmlns:hec="clr-namespace:HollowEarth.Converters"
    Foreground="{Binding MyFlagProp, Converter={hec:BoolBrushConverter YellowGreen, DodgerBlue}}" 
    />

I don't think that's a good fit for this case. But sometimes the semantics are so clear, the property name is unnecessary. {hec:GreaterThan 4.5}, for example.

UPDATE

Here's a complete implementation of a SignalBars control. This has five segments to your four, but you can easily remove one; that's only in the template, and the Value property is a double that could be subdivided any way you like (again, in the template).

SignalBars.cs

using System;
using System.ComponentModel;
using System.Windows.Media;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Markup;

namespace HollowEarth
{
    public class SignalBars : ContentControl
    {
        static SignalBars()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(SignalBars), new FrameworkPropertyMetadata(typeof(SignalBars)));
        }

        #region Value Property
        public double Value
        {
            get { return (double)GetValue(ValueProperty); }
            set { SetValue(ValueProperty, value); }
        }

        public static readonly DependencyProperty ValueProperty =
            DependencyProperty.Register("Value", typeof(double), typeof(SignalBars),
                new PropertyMetadata(0d));
        #endregion Value Property

        #region InactiveBarFillBrush Property
        [Bindable(true)]
        [Category("Appearance")]
        [DefaultValue("White")]
        public Brush InactiveBarFillBrush
        {
            get { return (Brush)GetValue(InactiveBarFillBrushProperty); }
            set { SetValue(InactiveBarFillBrushProperty, value); }
        }

        public static readonly DependencyProperty InactiveBarFillBrushProperty =
            DependencyProperty.Register("InactiveBarFillBrush", typeof(Brush), typeof(SignalBars),
                new FrameworkPropertyMetadata(Brushes.White));
        #endregion InactiveBarFillBrush Property
    }

    public class ComparisonConverter : MarkupExtension, IMultiValueConverter
    {
        public virtual object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            if (values.Length != 2)
            {
                throw new ArgumentException("Exactly two values are expected");
            }

            var d1 = GetDoubleValue(values[0]);
            var d2 = GetDoubleValue(values[1]);

            return Compare(d1, d2);
        }

        /// <summary>
        /// Overload in subclasses to create LesserThan, EqualTo, whatever.
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        protected virtual bool Compare(double a, double b)
        {
            throw new NotImplementedException();
        }

        protected static double GetDoubleValue(Object o)
        {
            if (o == null || o == DependencyProperty.UnsetValue)
            {
                return 0;
            }
            else
            {
                try
                {
                    return System.Convert.ToDouble(o);
                }
                catch (Exception)
                {
                    return 0;
                }
            }
        }

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

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            return this;
        }
    }

    public class GreaterThan : ComparisonConverter
    {
        protected override bool Compare(double a, double b)
        {
            return a > b;
        }
    }
}

Themes\Generic.xaml

<ResourceDictionary 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    >

    <Style 
        xmlns:he="clr-namespace:HollowEarth"
        TargetType="{x:Type he:SignalBars}" 
        >
        <!-- Foreground is the bar borders and the fill for "active" bars -->
        <Setter Property="Foreground" Value="Black" />
        <Setter Property="InactiveBarFillBrush" Value="White" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Control">
                    <ControlTemplate.Resources>
                        <Style TargetType="Rectangle">
                            <Setter Property="Width" Value="4" />
                            <Setter Property="VerticalAlignment" Value="Bottom" />
                            <Setter Property="Stroke" Value="{Binding Foreground, RelativeSource={RelativeSource TemplatedParent}}" />
                            <Setter Property="StrokeThickness" Value="1" />
                            <Setter Property="Fill" Value="{Binding InactiveBarFillBrush, RelativeSource={RelativeSource TemplatedParent}}" />
                            <Setter Property="Margin" Value="0,0,1,0" />
                            <Style.Triggers>
                                <DataTrigger Value="True">
                                    <DataTrigger.Binding>
                                        <MultiBinding Converter="{he:GreaterThan}">
                                            <MultiBinding.Bindings>
                                                <Binding 
                                                    Path="Value" 
                                                    RelativeSource="{RelativeSource TemplatedParent}" 
                                                    />
                                                <Binding 
                                                    Path="Tag" 
                                                    RelativeSource="{RelativeSource Self}" 
                                                    />
                                            </MultiBinding.Bindings>
                                        </MultiBinding>
                                    </DataTrigger.Binding>
                                    <Setter Property="Fill" Value="{Binding Foreground, RelativeSource={RelativeSource TemplatedParent}}" />
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </ControlTemplate.Resources>
                    <ContentControl
                        ContentTemplate="{Binding ContentTemplate, RelativeSource={RelativeSource TemplatedParent}}">
                        <StackPanel 
                            Orientation="Horizontal"
                            SnapsToDevicePixels="True"
                            UseLayoutRounding="True"
                            >
                            <!-- Set Tags to the minimum threshold value for turning the segment "on" -->
                            <!-- Remove one of these to make it four segments. To make them all equal height, remove Height here
                            and set a fixed height in the Rectangle Style above. -->
                            <Rectangle Height="4" Tag="0" />
                            <Rectangle Height="6" Tag="2" />
                            <Rectangle Height="8" Tag="4" />
                            <Rectangle Height="10" Tag="6" />
                            <Rectangle Height="12" Tag="8" />
                        </StackPanel>
                    </ContentControl>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

</ResourceDictionary>

Example XAML:

<StackPanel 
    xmlns:he="clr-namespace:HollowEarth"
    Orientation="Vertical"
    HorizontalAlignment="Left"
    >
    <Slider 
        Minimum="0" 
        Maximum="10" 
        x:Name="SignalSlider" 
        Width="200" 
        SmallChange="1" 
        LargeChange="4" 
        TickFrequency="1" 
        IsSnapToTickEnabled="True" 
        />
    <he:SignalBars 
        HorizontalAlignment="Left"
        Value="{Binding Value, ElementName=SignalSlider}" 
        InactiveBarFillBrush="White" 
        Foreground="DarkRed"
        />
</StackPanel>

Upvotes: 5

mechanic
mechanic

Reputation: 781

Usually you may need a ColorToBrushConverter, but not a BooleanToColor.

I would simply create different styles with triggers for each bar, like

        <Style.Triggers>
            <DataTrigger Binding="{Binding IsOffline}" Value="True">
                <Setter Property="Background" Value="Salmon" />
            </DataTrigger>
            <DataTrigger Binding="{Binding IsPrinting}" Value="True">
                <!--<Setter Property="Background" Value="Honeydew" />-->
                <Setter Property="Background" Value="LightGreen" />
            </DataTrigger>
        </Style.Triggers>

Upvotes: 1

Related Questions