Friday, 14 November 2014

Extending WPF Control Templates with Attached Properties

One of the great things about WPF is that you can completely customise everything about the way a control looks. Most of the time then, creating a custom style is more than sufficient to get your controls looking just how you want them? But what if you need your control to change its appearance based on some criteria. At this point, I’d usually be tempted to create my own custom control. But that isn’t actually always necessary.

For example, I recently wanted to create a peak LED button in an audio application that was monitoring sound levels. The button would show the peak decibel level, and when the sound went above a certain threshold, would go red. To acknowledge the clipping, you could click the button and the it would revert to its default colour. So my button needed a boolean property that would indicate is clipped or not.

image

But how can you add a property to a class without inheriting it? Well in WPF, you can make use of attached properties. You’ve already used them if you’ve set Grid.Row for a control in your XAML. The control itself has no Grid.Row property, but you can associate a grid row value with that control, which enables it to be positioned correctly within the grid.

So we need an attached property that stores whether a peak has been detected or not. Attached properties are similar to dependency properties if you’ve ever created those before. I can never remember how to write them from scratch, but if you have an example handy, you can copy it. You need to inherit from DependencyObject, to register a static DependencyProperty, and to provide static getter and setter methods. Here’s the one I created:

public class PeakHelper : DependencyObject
{
    public static readonly DependencyProperty IsPeakProperty = DependencyProperty.RegisterAttached(
        "IsPeak", typeof (bool), typeof (PeakHelper), new PropertyMetadata(false));


    public static void SetIsPeak(DependencyObject target, Boolean value)
    {
        target.SetValue(IsPeakProperty, value);
    }

    public static bool GetIsPeak(DependencyObject target)
    {
        return (bool)target.GetValue(IsPeakProperty);
    }
}

Now we have our attached property, we can use it in our button template. The regular button template is simply a ContentPresenter inside a Border, and then we use a Trigger to set the border’s background and border colours when our attached property is true. Obviously this is a very simplistic button template otherwise, with no triggers for mouse-over, pressed, or disabled.

<ControlTemplate TargetType="Button" x:Key="PeakButtonControlTemplate" >
    <Border x:Name="PeakBorder" BorderBrush="Gray" 
            BorderThickness="2" Background="LightGray">
        <ContentPresenter HorizontalAlignment="Center">
        </ContentPresenter>        
    </Border>
    <ControlTemplate.Triggers>
        <Trigger Property="local:PeakHelper.IsPeak"
                 Value="True">
            <Setter 
                TargetName="PeakBorder"
                Property="BorderBrush"
                Value="Red"></Setter>
            <Setter 
                TargetName="PeakBorder"
                Property="Background"
                Value="Pink"></Setter>

        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

And that’s all there is to it. We can now use regular MVVM to set the IsPeak attached property to true, which will turn our button red:

<Button Margin="4" 
    Template="{StaticResource PeakButtonControlTemplate}" 
    Content="{Binding MaxVolume}"  
    local:PeakHelper.IsPeak="{Binding IsPeak}" 
    Command="{Binding PeakReset}" 
    Width="40" Height="20" />

No comments: