Creating an attached property
An attached property can be used to somehow "enhance" or extend another object. In the case outlined in the previous recipe, Using an attached property, an element was placed at exact coordinates within a Canvas
using the attached Canvas.Left
and Canvas.Top
properties. An attached property is a powerful tool for extending the behavior of any object without the need to inherit from the type of the object. In this task, we'll see this in action.
Getting ready
Make sure Visual Studio is up and running.
How to do it...
We'll create an attached property that would rotate any element it's attached to:
Create a new WPF Application named
CH01.CustomAttached
.Open
MainWindow.xaml
. Add some elements in aCanvas
(replace the defaultGrid
) as follows:<Canvas> <Ellipse Fill="Red" Width="100" Height="60" /> <Rectangle Fill="Blue" Width="80" Height="80" Canvas.Left="100" Canvas.Top="100" /> <Button Content="Hello" Canvas.Left="130" Canvas.Top="30" FontSize="20" /> </Canvas>
Suppose we want to rotate a particular element around its center. We would have to write something like this (example for the
Ellipse
):<Ellipse Fill="Red" Width="100" Height="60" RenderTransformOrigin=".5,.5"> <Ellipse.RenderTransform> <RotateTransform Angle="30" /> </Ellipse.RenderTransform> </Ellipse>
Although this is certainly possible, this makes for a lot of typing. Now imagine doing something similar for other elements. Let's make it shorter by defining and using an attached property.
Add a new class to the project named
RotationManager
.We'll register a new attached property within this class; a property any other object can use. To do that, we'll take advantage of a Visual Studio code snippet,
propa
(similar in concept topropdp
discussed in the task Creating a dependency property in this chapter). Inside the class definition, typepropa
(without the quotes). This is how it should look at this point:Press Tab once, and fill in the property details as follows: the property type should be double, its name should be
Angle
, its owner classRotationManager
, and its default value zero. You'll have to add ausing
statement forSystem.Windows
namespace. The generated code should look as follows (after removing the comment and some formatting):class RotationManager : DependencyObject { public static double GetAngle(DependencyObject obj) { return (double)obj.GetValue(AngleProperty); } public static void SetAngle(DependencyObject obj, double value) { obj.SetValue(AngleProperty, value); } public static readonly DependencyProperty AngleProperty = DependencyProperty.RegisterAttached("Angle", typeof(double), typeof(RotationManager), new UIPropertyMetadata(0.0)); }
Now that we have an attached property definition, let's use it. We'll set it on our various elements. The first step is mapping an XML namespace to our namespace (as we learned in the recipe Creating custom type instances in XAML in this chapter). Open
MainWindow.xaml
and add a mapping on the root element, as in the following code snippet:xmlns:local="clr-namespace:CH01.CustomAttached"
Now let's set the property with various values on the various elements. Here's an example for the
Ellipse
(notice the intellisense popping up to help):<Ellipse Fill="Red" Width="100" Height="60" local:RotationManager.Angle="45"/>
Add similar settings for the
Rectangle
andButton
like as follows:<Rectangle Fill="Blue" Width="80" Height="80" Canvas.Left="100" Canvas.Top="100" local:RotationManager.Angle="30" /> <Button Content="Hello" Canvas.Left="130" Canvas.Top="30" FontSize="20" local:RotationManager.Angle="90"/>
Notice that the designer preview shows no change. If you run the application, nothing happens. And why would anything happen? We declared a property and nothing else. Let's add some behavior logic if the property is actually used. For that, we'll add a property changed handler notification. Go back to
RotationManager.cs
and modify the property registration as follows:public static readonly DependencyProperty AngleProperty = DependencyProperty.RegisterAttached("Angle", typeof(double), typeof(RotationManager), new UIPropertyMetadata(0.0, OnAngleChanged));
The
OnAngleChanged
method will be called for any change in the property value on any object it's applied to. Let's add some simple logic that will rotate the element:private static void OnAngleChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) { var element = obj as UIElement; if(element != null) { element.RenderTransformOrigin = new Point(.5, .5); element.RenderTransform = new RotateTransform( (double)e.NewValue); } }
If we switch back to the designer, we'll see the elements rotated according to the specified angles. If we run the application, we'll see something like this:
There we have it. An easy way to rotate any element by using an attached property.
How it works...
Attached properties are registered similarly to regular dependency properties. In terms of functionality, they are dependency properties. This means they support everything a dependency property supports: data binding, animation, and so on. An attached property can be defined by any class (RotationManager
in our example) and can be applied to any object whose type derives from DependencyObject
.
Simply registering an attached property has no effect on its own. There must be some "extra" code that looks for that property and does something when it's applied or changed. In the example shown, this is done by specifying a property changed handler, called by WPF whenever the property is changed on any object. In the example code, we restrict using a UIElement
-derived type, as this is the first type that supports RenderTransform
and RenderTransformOrigin
. This also shows the weakness of attached properties: it's not possible to know whether specifying the property on some object is beneficial. We could have thrown an exception if the object was not UIElement
-derived to somewhat rectify this (albeit at runtime rather than compile time), but this is not typically employed (although we could have written something with Debug.WriteLine
to indicate this needs attention), as there may be other code that does not consider this an invalid setting.
There's more...
The property change notification scheme is typically used by WPF with attached properties that are defined by panels, such as Canvas
, DockPanel
, and Grid
. Note that the panels only look for the relevant attached properties on their immediate children (and not grandchildren). This is not a limitation of attached properties, it's simply the way these panels work. Although attached properties within panels are common, there are other ways these properties can be used.
One possibility is to use the existence of these property values within styles (a complete treatment of styles is in given Chapter 8) or templates (templates are discussed in Chapter 6 and Chapter 8). For now, think of a style as a grouping of related settings that can be applied as a group to an element. For example, the following style accomplishes roughly the same thing as our property change handler:
<Style TargetType="Button"> <Setter Property="RenderTransformOrigin" Value=".5,.5" /> <Setter Property="RenderTransform"> <Setter.Value> <RotateTransform Angle="{Binding Path=(local:RotationManager.Angle), RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Button}}" /> </Setter.Value> </Setter> </Style>
Of course, this example works with buttons only because of the targeted style (style that works on buttons only), but the result is the same. Note the parentheses around the attached property name. This is essential – otherwise the XAML parser does not understand this to be an attached property; it interprets local:RotationManager
as the property name (and expects Angle
to be a sub-property). Also, leaving out the "Path="
(as is customary in binding expressions), causes the expression to fail (for a similar reason).
Reusing existing attached properties
An attached property is (paradoxically) a detached entity. It has no special affinity to the declaring type. This means we can use an already defined attached property if it's typed appropriately, named appropriately, and has no use in the needed situation. In our example, we need an attached property that is of type double
, has an intuitive enough name (maybe something with "angle" or "rotate"), and is unused in scenarios where the use of our property makes sense.
Clearly, it's not easy finding such a property, but sometimes one may get lucky. For instance, if we elect to go for the attached property ToolTipService.HorizontalOffset
(typed as double
), we can achieve the same effect as previously (with a style setter) without defining a new attached property. This is not a good choice in this case, as an offset is not an angle, and clearly tooltips have nothing to do with rotation. The worse problem here is that there may be a legitimate reason to place that property on a button (to cater offsetting a tooltip), so that reusing for rotation purposes would collide with the tooltip, making only one a winner. Still, the general concept holds – any attached property can be reused.
Attached property reuse is possible in styles, templates (data template and control template), and triggers (within styles and templates).
See also
For background on dependency properties, check out the recipe Creating a dependency property in this chapter.