The blog of dlaa.me

Make life a little easier for designers [WPF Triggers with a little less XAML and a little less code]

When I was writing my video frame grabbing sample, I came up with a potentially useful technique for maintaining the developer/designer separation that's so sought after in the WPF (and Silverlight) world. I'm sure something similar has been discussed before, but I hadn't come across it (and couldn't find anything like it with a quick search), so I thought I'd describe it here for posterity.

Background

The high-level goal is to maintain a separation between how an application works (developer) and how it looks (designer). The conventional approach to this is to follow some kind of model/view pattern where the application exposes various properties that represent its state and the interface binds to those properties to convey that state to the user. In WPF, this is typically done in XAML via Binding or TemplateBinding. These two constructs are very powerful and work quite well. However, it seemed to me that there might be a slightly better way...

The Conventional Way

In my scenario, the application exposes a Boolean DependencyProperty "Processing" which is normally False, but flips to True whenever a video file is being processed. The UI exposes an "Open File" Button which is normally enabled, but should flip to disabled whenever a video file is being processed.

The first instinct is to use a Trigger to apply the style changes to the Button. However, there's a catch - the Trigger needs to be defined on Button itself because the Button properties are being changed, yet the actual Trigger property (the Processing property) is defined on the Window object. I didn't see an obvious way to point the Trigger at the Window from the Button, so I moved on to the next idea which was to use a Binding to attach the Button's IsEnabled property to the Window's Processing property. Depending on whether you prefer ElementName or RelativeSource, the XAML looks like this:

<Button
    IsEnabled="{Binding Processing, ElementName=Window}"
    Content="..."/>
<Button
    IsEnabled="{Binding Processing, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type l:Window1}}}"
    Content="..."/>

Both approaches work, but not perfectly: the IsEnabled state of the Button is backwards because we've got IsEnabled == Processing when what we really want is IsEnabled == !Processing. Fortunately, Bindings support Converters and I've already written about the power of IValueConverter. So the designer goes to the developer and asks for a Converter that will invert the state of a Boolean variable and the developer writes the following code:

/// <summary>
/// Simplest implementation of a Converter to invert the state of a bool.
/// </summary>
/// <remarks>
/// Needs parameter validation and error handling.
/// </remarks>
public class BooleanInversionConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return !((bool)value);
    }
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Now the designer adds an instance of this Converter to the resources of the application:

    <Application.Resources>
        <l:BooleanInversionConverter x:Key="BooleanInversionConverter"/>
    </Application.Resources>

And uses it to achieve the desired effect:

<Button
    IsEnabled="{Binding Processing, ElementName=Window, Converter={StaticResource BooleanInversionConverter}}"
    Content="..."/>
<Button
    IsEnabled="{Binding Processing, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type l:Window1}}, Converter={StaticResource BooleanInversionConverter}}"
    Content="..."/>

And the scenario works. But then the designer decides that the text of the Button should also be italic when it's disabled... And that requires the designer to go back to the developer who then needs to write a BooleanToFontStyleItalicConverter. Or maybe the designer attaches the Binding to the Button's Style property and gets the developer to write some kind of Style toggling Converter. Sheesh, what started out as a simple scenario has gotten annoyingly complicated and involves significant developer/designer coordination to accomplish something the designer should have been able to do independently in the first place.

A Different Way

So that's when I came up with a different idea that would let me use a Trigger after all: an attached DependencyProperty that inherits! Simply by changing the registration of the Processing property from Register to RegisterAttached and adding the FrameworkPropertyMetadataOptions.Inherits flag, we end up with a property on Window that looks to the Button just like a property defined on the Button itself. And suddenly the original idea to use a Trigger becomes possible - and even preferable! - because there's no longer a need for Bindings, Converters, or any of the complication above. Now the XAML looks just like it should and the designer can change things with complete autonomy:

<Button Content="...">
    <Button.Style>
        <Style TargetType="{x:Type Button}">
            <Style.Triggers>
                <Trigger Property="l:Window1.Processing" Value="True">
                    <Setter Property="IsEnabled" Value="False"/>
                    <Setter Property="FontStyle" Value="Italic"/>
                </Trigger>
            </Style.Triggers>
        </Style>
    </Button.Style>
</Button>

Much better.

Another Different Way

I discussed this scenario with my teammate Beatriz Costa briefly (she's one of the WPF data binding experts) and she independently suggested a very similar solution to what I came up with: DataTrigger. Using a DataTrigger instead of a Trigger has the nice benefit that the DependencyProperty no longer needs to be made attached/inheritable; any standard DependencyProperty will do. However, DataTrigger uses a Binding instead of a Property, so we end up back in the world of long-ish ElementName/RelativeSource markup syntax. Here's the XAML for the ElementName form:

<Button Content="...">
    <Button.Style>
        <Style TargetType="{x:Type Button}">
            <Style.Triggers>
                <DataTrigger Binding="{Binding Processing, ElementName=Window}" Value="True">
                    <Setter Property="IsEnabled" Value="False"/>
                    <Setter Property="FontStyle" Value="Italic"/>
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </Button.Style>
</Button>

Just as better - pick whichever feels more comfortable. :)

Summary

Achieving a good developer/designer separation is good for a lot of reasons: flexibility, productivity, encapsulation, etc.. Unfortunately, it's not always as easy as we'd like... So if the approaches outlined here can help move us all just a little bit closer to that goal, then this post has served its purpose!

Tags: WPF