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.

RadialPanelDemo.zip

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