Sometimes we need some new functionality from a basic control. One way to go about it is to create a custom control and add that functionality. However, sometimes creating a new control is an overkill, especially when no new behaviour is actually needed. Perhaps some new way of presenting or drawing the control is all that’s needed.
The problem is that we may want more properties on the control, but we don’t want or need to actually extend it (i.e. derive from it).
For example, suppose we want to create a button with one of several images depending on its state: enabled, disabled or focused.
Deriving from Button seems too excessive: the images are not changing anything from a behaviour perspective. However, I would like to have new properties, such as NormalImage, DisabledImage and FocusedImage.
Adding properties to an existing class in WPF is possible through attached properties. Attached properties are contextual; they are defined by one class, but may be used by any other class.
Continuing the example, I can look for some already define attached properties that match my intentions. More often than not, I won’t find any.
To facilitate my image button, I can create a new class, define the attached properties by calling DependencyProperty.RegisterAttached and simply look for them in the appropriate class (the Button in this case, when used with a style).
Here’s an example: (you can add an attached property easily using Visual Studio with the propa code snippet – just type propa and hit Tab twice)
public sealed class ImageStates : DependencyObject {
public static ImageSource GetNormalImage(DependencyObject obj) {
return (ImageSource)obj.GetValue(NormalImageProperty);
}
public static void SetNormalImage(DependencyObject obj, ImageSource value) {
obj.SetValue(NormalImageProperty, value);
}
public static readonly DependencyProperty NormalImageProperty =
DependencyProperty.RegisterAttached("NormalImage", typeof(ImageSource), typeof(ImageStates), new UIPropertyMetadata(null));
public static ImageSource GetDisabledImage(DependencyObject obj) {
return (ImageSource)obj.GetValue(DisabledImageProperty);
}
public static void SetDisabledImage(DependencyObject obj, ImageSource value) {
obj.SetValue(DisabledImageProperty, value);
}
public static readonly DependencyProperty DisabledImageProperty =
DependencyProperty.RegisterAttached("DisabledImage", typeof(ImageSource), typeof(ImageStates), new UIPropertyMetadata(null));
public static ImageSource GetFocusedImage(DependencyObject obj) {
return (ImageSource)obj.GetValue(FocusedImageProperty);
}
public static void SetFocusedImage(DependencyObject obj, ImageSource value) {
obj.SetValue(FocusedImageProperty, value);
}
public static readonly DependencyProperty FocusedImageProperty =
DependencyProperty.RegisterAttached("FocusedImage", typeof(ImageSource), typeof(ImageStates), new UIPropertyMetadata(null));
}
I’ve created 3 attached properties in some class acting as a simple logical container for these properties.
Now, I can create some buttons like so:
<StackPanel>
<Button Margin="4" local:ImageStates.NormalImage="book_open.png"
local:ImageStates.DisabledImage="book_red.png"
local:ImageStates.FocusedImage="book_open2.png" />
<Button Margin="4" local:ImageStates.NormalImage="book_open.png" IsEnabled="False"
local:ImageStates.DisabledImage="book_red.png"
local:ImageStates.FocusedImage="book_open2.png" />
<Button Margin="4" local:ImageStates.NormalImage="book_open.png"
local:ImageStates.DisabledImage="book_red.png"
local:ImageStates.FocusedImage="book_open2.png" />
</StackPanel>
The attached properties are used in the correct “context”. These properties are not part of Button, but now are used by it (actually by the style and template that’s applied). The application of the templates can be done like so: (a bit crude)
<Application.Resources>
<Style TargetType="Button">
<Style.Triggers>
<Trigger Property="IsEnabled" Value="True">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Image Stretch="None" Source="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Button}}, Path=(local:ImageStates.NormalImage)}" />
</DataTemplate>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="IsFocused" Value="True">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Image Stretch="None" Source="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Button}}, Path=(local:ImageStates.FocusedImage)}" />
</DataTemplate>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Image Stretch="None" Opacity=".5" Source="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Button}}, Path=(local:ImageStates.DisabledImage)}" />
</DataTemplate>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
</Application.Resources>
Here’s a screenshot of the result (top button is focused, middle is disabled):
The attached ZIP is the complete project. Power to Attached Properties!