Attached properties - the basics
One of the coolest features in Avalon is the property subsystem. While it might seem difficult for some that Avalon has a property system built on top of the normal CLR properties, the power that this system gives you is incredible and is part of why a lot of scenarios in Avalon require less code than their counterparts in other systems.
One cool thing that the property system in Avalon allows is attached properties. The simplest way to explain an attached property is that it allows the place where the property is defined and the place where it is stored to be completely different classes that know nothing about each other. The only restriction is that the property value can only be stored on DependencyObject objects (which happens to be the base class for most Avalon things).
Why is this so cool? The best example is how DockPanel uses attached properties. When you add elements to a DockPanel, you need to tell the DockPanel on what side to Dock the elements. WinForms uses the Dock property on the Control class for this. But you then pay the tax of having the property on the Control, even though you are not always being docked. The property only makes sense to the docking code.
In Avalon the DockPanel defines an attached property (DockPanel.DockProperty) that tells the DockPanel where to dock a child element. You can set the property on the child element from code:
DockPanel.SetDock(element, Dock.Left);
Or from XAML:
<UIElement DockPanel.Dock="Left" />
The DockPanel reads off the value and uses it during the layout. Because it is using attached properties, the UIElement and associated classes do not need to know about how DockPanel works, and there can be a multitude of properties used by various panels for layout without bloating UIElement with spurious properties.
Defining your own attached properties is easy - I have made a quick sample to show how to do this. The idea behind the sample is to make a panel called RadialPanel that arranges children in a radial pattern, and has an attached property Angle that shows where to place the children.
The first thing that we do is create a new class called RadialPanel (make sure it is public or the XAML cannot find it!). We then add an attached property to the class as such:
public static readonly DependencyProperty AngleProperty = DependencyProperty.RegisterAttached(
"Angle",
typeof(double),
typeof(RadialPanel),
new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsParentArrange));
Here is some explanation about this blob of code (if you work with Avalon a while you end up writing stuff like this a lot):
- We make the DependencyProperty public because other people need to reference it.
- It is static because this object is only really describing the property, not storing it per instance.
- We make it read only, because we don't want people to overwrite it.
- We define the type of the value stored (double) and the type of the owner (RadialPanel).
- The metadata tells Avalon what the default value is, and it also says that changing the value of this property will invalidate arrange on the parent of the object that the value is set on. This is very handy, because we don't need to write a handler for when the property changes.
After we write this code, we write some accessor methods. We need to do this or Avalon will not be able to change the values from XAML. These are just static methods that wrap the Avalon GetValue/SetValue methods:
public static double GetAngle(DependencyObject obj)
{
return (double)obj.GetValue(AngleProperty);
}
public static void SetAngle(DependencyObject obj, double value)
{
obj.SetValue(AngleProperty, value);
}
Now we can implement the measure and arrange code. The measure code is fairly straightforward - we just decide how big each child will be and measure the children like that:
protected override Size MeasureOverride(Size availableSize)
{
// find out the radius of the children (it is 1/3rd of the minor axis of the RadialPanel)
double childRadius = Math.Min(availableSize.Width, availableSize.Height) / 3.0;
// measure each of the children with this size
foreach (UIElement element in this.InternalChildren)
{
element.Measure(new Size(childRadius, childRadius));
}
// tell our layout parent that we used all of the space up
return availableSize;
}
And now we can write the arrange, which is simple as well. Notice that we grab the value of the attached property to figure out where to put the element:
protected override Size ArrangeOverride(Size finalSize)
{
// figure out the size for the children
double childRadius = Math.Min(finalSize.Width, finalSize.Height) / 3.0;
foreach (UIElement element in this.InternalChildren)
{
double angle = GetAngle(element);
// figure out where the child goes in cartesian space. We invert the
// Y value because screen space and normal mathematical cartesian space
// are reversed in the vertical.
Point childPoint = new Point(
Math.Cos(angle * Math.PI / 180.0) * childRadius,
-Math.Sin(angle * Math.PI / 180.0) * childRadius);
// offset to center of the panel
childPoint.X += finalSize.Width / 2;
childPoint.Y += finalSize.Height / 2;
// arrange so that the center of the child is at the point we calculated,
// and the child is of the calculated radius.
element.Arrange(new Rect(
childPoint.X - childRadius / 2.0,
childPoint.Y - childRadius / 2.0,
childRadius,
childRadius));
}
// we used up all of the space
return finalSize;
}
Now the panel is written we can test it out with some simple XAML:
<Window x:Class="RadialPanelDemo.Window1"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:RadialPanelDemo"
Title="RadialPanelDemo" Height="300" Width="300"
>
<local:RadialPanel x:Name="radialPanel">
<Button Content="Button One" local:RadialPanel.Angle="30" Opacity="0.5" />
<Button Content="Button Two" local:RadialPanel.Angle="90" Opacity="0.5" />
<Button Content="Button Three" local:RadialPanel.Angle="120" Opacity="0.5" />
<Button Content="Button Four" local:RadialPanel.Angle="250" Opacity="0.5" />
</local:RadialPanel>
</Window>
We give some transparency to the buttons so that we can see them when they overlap each other. One last bit of fun is showing changes to the property value and showing how the layout updates automatically. In the InitializeComponent of the window, we add an event handler to the buttons:
// whenever one of the buttons is clicked, handle it in the same place
foreach (Button button in radialPanel.Children)
{
button.Click += new RoutedEventHandler(OnButtonClick);
}
And write the handler:
void OnButtonClick(object sender, RoutedEventArgs e)
{
// find out who was clicked
Button button = (Button)e.OriginalSource;
// increment angle by 15 degrees
RadialPanel.SetAngle(button, RadialPanel.GetAngle(button) + 15.0);
}
Now when we click the buttons, they each move 15 degrees around the circle!
This is just the beginning of what attached properties can do. I will try to blog about more advanced usage soon. Please let me know if there is a specific Avalon property system feature that you want an article about.
Update: I uploaded the code for this example.
Comments
Anonymous
August 02, 2006
The color of this page makes it really hard to read.
Suggestion - change the blue and green text to a lighter color.
Or use a lighter background color with darker text. [BenCon - I have played with this but have been slack. I will try to clean it up when I recover from the conference] [BenCon - I have changed to a better (I hope) theme now. Sorry it took so long, and thanks for the feedback]Anonymous
December 09, 2006
The comment has been removed