Creating a WPF ValueConverter for a Brush

Andy picture Andy · Jun 4, 2009 · Viewed 8.6k times · Source

On the Nerd Plus Art blog today, there was a post about creating WPF Resources for arrows, which the author uses frequently. I have a side project that has Back and Forward buttons, so I thought that the Left and Right arrows would work great on those buttons.

I added the LeftArrow and RightArrow Geometries to my application's resources, and then used them as the content of the buttons:

<Application x:Class="Notes.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="Views/MainWindow.xaml">
    <Application.Resources>
        <Geometry x:Key="RightArrow">M0,0 L1,0.5 0,1Z</Geometry>
        <Geometry x:Key="LeftArrow">M0,0.5 L1,1 1,0Z</Geometry>
    </Application.Resources>
</Application>

<Button x:Name="BackButton"
    Padding="5,5,5,5"
    Command="{x:Static n:Commands.GoBackCommand}">
    <Path Data="{StaticResource LeftArrow}" Width="10" Height="8"
        Stretch="Fill" Fill="Black"/>
    </Button>
<Button x:Name="ForwardButton"
    Padding="5,5,5,5"
    Command="{x:Static n:Commands.GoForwardCommand}">
    <Path Data="{StaticResource RightArrow}" Width="10" Height="8"
        Stretch="Fill" Fill="Red" />
</Button>

That worked, except that the arrows were drawn in black regardless of whether the button was enabled or not. So, I created a ValueConverter to go from a bool to a Brush:

class EnabledColorConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter,
        CultureInfo culture)
    {
        bool b = (bool)value;
        return b ? Brushes.Black : Brushes.Gray;
    }

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

(I realize that I should probably use system colors instead of hard coded black and gray, but I just wanted to get this working, first.)

I modified the Fill property of the Path to use my converter (which I created within the application's resources):

<Path Data="{StaticResource LeftArrow}" Width="10" Height="8"
    Stretch="Fill"
    Fill="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=Button}, Path=IsEnabled, Converter={StaticResource EnabledColorConverter}}"/>

Unfortunately, this doesn't work, and I'm not sure why. When I run it, the arrow isn't drawn at all. I checked the Output window in Visual Studio, and no binding errors were displayed. I also verified that the bool is the right value in the converter, based on the whether the button should be enabled or not.

If I change the Path back to a TextBlock (and bind its Foreground property in the same manner as Path.Fill), the text is always drawn in black.

Am I doing something wrong? Why is the Brush returned by my converter not used to render the Path in the button?

Answer

Nicholas Armstrong picture Nicholas Armstrong · Jun 4, 2009

For this kind of UI state updates, try using triggers instead; it'll save you from writing a value converter, and it's a lot shorter to write.

Try this:

<Application x:Class="Notes.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="Views/MainWindow.xaml">
    <Application.Resources>
        <Geometry x:Key="RightArrow">M0,0 L1,0.5 0,1Z</Geometry>
        <Geometry x:Key="LeftArrow">M0,0.5 L1,1 1,0Z</Geometry>

         <!-- Base Arrow Style, with a trigger to toggle it Gray when its parent button is disabled -->
        <Style x:Key="ArrowStyle" TargetType="Path">
            <Setter Property="Width" Value="10"/>
            <Setter Property="Height" Value="8"/>
            <Setter Property="Stretch" Value="Fill"/>
            <Style.Triggers>
                <DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=Button}, Path=IsEnabled}" Value="False">
                    <Setter Property="Fill" Value="Gray"/>
                </DataTrigger>
            </Style.Triggers>
        </Style>

        <!-- Left Arrow Style, with the Left Arrow fill and data -->
        <Style x:Key="LeftArrowStyle" BasedOn="{StaticResource ArrowStyle}" TargetType="Path">
            <Setter Property="Fill" Value="Black"/>
            <Setter Property="Data" Value="{StaticResource LeftArrow}"/>
        </Style>

        <!-- Right Arrow Style, with the Right Arrow fill and data -->
        <Style x:Key="RightArrowStyle" BasedOn="{StaticResource ArrowStyle}" TargetType="Path">
            <Setter Property="Fill" Value="Red"/>
            <Setter Property="Data" Value="{StaticResource RightArrow}"/>
        </Style>
    </Application.Resources>

    <Button x:Name="BackButton" Padding="5,5,5,5" IsEnabled="False">
        <Path Style="{StaticResource LeftArrowStyle}"/>
    </Button>
    <Button x:Name="ForwardButton" Padding="5,5,5,5">
        <Path Style="{StaticResource RightArrowStyle}"/>
    </Button>
</Application>

Then, you set your default fill in LeftArrowStyle and RightArrowStyle, for the left and right arrows, respectively. If you set it on the Path itself, then that value would take precedence and override anything a style or its trigger may do. The base style, ArrowStyle, contains a DataTrigger bound to the parent button - it fires whenever IsEnabled is false, and changes the Path's Fill to Gray.