Attached properties are at first very mysterious. As you know from this article, here’s how you might see them in XAML:
<Canvas>
. . .
<Ellipse Style="{StaticResource ellipseStyle}"
Canvas.Left="116" Canvas.Top="92" />
. . .
</Canvas>
That’s straight out of the EllipseChain program.
Canvas.Left and Canvas.Top are attached properties. They are properties defined by Canvas that you set on children of the Canvas.
As I discussed in this article, there is actually nothing in Canvas named Left or Top. When setting these attached properties in code, you use two static methods defined by the Canvas class:
Canvas.SetLeft(ellipse, 116);
Canvas.SetTop(ellipse, 92);
Or you can use the SetValue method defined by DependencyObject and inherited by the Ellipse class to reference the static dependency properties defined by Canvas:
ellipse.SetValue(Canvas.LeftProperty, 116.0);
ellipse.SetValue(Canvas.TopProperty, 92.0);
This is the same SetValue methods that a class calls in a CLR property to set a dependency property.
You now know almost everything you need to define your own attached properties. The project named CanvasCloneDemo contains a class named CanvasClone. The class defines two DependencyProperty fields named LeftProperty and TopProperty:
Example 1. Project: CanvasCloneDemo File: CanvasClone.cs (excerpt)
public class CanvasClone : Panel { public static readonly DependencyProperty LeftProperty = DependencyProperty.RegisterAttached("Left", typeof(double), typeof(CanvasClone), new PropertyMetadata(0.0, OnLeftOrTopPropertyChanged));
public static readonly DependencyProperty TopProperty = DependencyProperty.RegisterAttached("Top", typeof(double), typeof(CanvasClone), new PropertyMetadata(0.0, OnLeftOrTopPropertyChanged)); . . . }
|
But notice the difference: Previously in this article, DependencyProperty objects were created with the static DependencyProperty.Register method. The DependencyObject fields in CanvasClone are created with the only other option, DependencyProperty.RegisterAttached. That makes them attached properties and allows them to be set on classes that did not define them.
Notice that the first argument to the PropertyMetadata constructor is explicitly a double so there won’t be a runtime error because the C# compiler assumes the value is an int.
After defining the DependencyProperty fields, you need static methods to access the attached properties. These method names begin with Set and Get followed by the attached property names, in this case, Left and Top,
Example 2. Project: CanvasCloneDemo File: CanvasClone.cs (excerpt)
public static void SetLeft(DependencyObject obj, double value) { obj.SetValue(LeftProperty, value); }
public static double GetLeft(DependencyObject obj) { return (double)obj.GetValue(LeftProperty); }
public static void SetTop(DependencyObject obj, double value) { obj.SetValue(TopProperty, value); }
public static double GetTop(DependencyObject obj) { return (double)obj.GetValue(TopProperty); }
|
These methods get called either
explicitly from code or implicitly from the XAML parser. The first
argument will be the object on which the attached property is being
set—in other words, the first argument will probably be a child of CanvasClone. The body of the method uses that argument to call SetValue and GetValue on the child. These are the same methods defined by DependencyObject to set and get dependency properties.
When these properties change, there will be a call to the property-changed handler defined in the PropertyMetadata constructor. The signature of this method is the same as a normal property-changed handler for regular dependency properties:
static void OnLeftOrTopPropertyChanged(DependencyObject obj,
DependencyPropertyChangedEventArgs args)
{
. . .
}
Once again, the method is static. However, the first argument is not an object of type CanvasClone. It is a child of the CanvasClone. Or rather, it is probably a child of the CanvasClone. It’s possible to call CanvasClone.SetLeft for an element that isn’t actually a child of the panel, and it’s even possible for CanvasClone.SetLeftOnLeftOrTopPropertyChanged method to be called without any instance of CanvasClone in existence! and the
For this reason, the body of the method needs to use a little bit of caution. It calls the handy static VisualTreeHelper.GetParent method to obtain the parent of the DependencyObject argument and cast it to a CanvasClone:
Example 3. Project: CanvasCloneDemo File: CanvasClone.cs (excerpt)
static void OnLeftOrTopPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) { CanvasClone parent = VisualTreeHelper.GetParent(obj) as CanvasClone;
if (parent != null) parent.InvalidateArrange(); }
|
If the parent of the object that has called CanvasClone.SetLeft or CanvasClone.SetTop is truly a CanvasClone, then the method calls InvalidateArrange on the parent, which is the CanvasClone.
In the general case, when a panel handles a change in one of its attached properties, it will probably call InvalidateMeasure on the panel to initiate a complete recalculation of layout. However, as you can see in the following MeasureOverride method, the total size of CanvasClone doesn’t change with the location of its children:
Example 4. Project: CanvasCloneDemo File: CanvasClone.cs (excerpt)
protected override Size MeasureOverride(Size availableSize) { foreach (UIElement child in Children) child.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
return Size.Empty; }
|
It is part of the paradigm of a Canvas that it always returns zero from MeasureOverride regardless of its children, so CanvasClone does the same. MeasureOverrideMeasure on all its children, or the children will have no size, but it calls Measure with infinite dimensions, forcing the child to assume as small a size as possible. still needs to call
When the panel calls InvalidateArrange on itself, layout jumps right into the arrange pass with a call to ArrangeOverride: This method requires the panel to arrange the children on its surface. Essentially it gives each child a size and a location.
Example 5. Project: CanvasCloneDemo File: CanvasClone.cs (excerpt)
protected override Size ArrangeOverride(Size finalSize) { foreach (UIElement child in Children) child.Arrange(new Rect( new Point(GetLeft(child), GetTop(child)), child.DesiredSize));
return base.ArrangeOverride(finalSize); }
|
ArrangeOverride calls its own static GetLeft and GetTop
methods on each child to determine where the child should be positioned
relative to itself. The size of each child is simply the DesiredSize the child originally calculated in the measure pass.
The XAML file in CanvasCloneDemo is the same as the one in the EllipseChain except that Canvas has been replaced with CanvasClone:
Example 6. Project: CanvasCloneDemo File: MainPage.xaml (excerpt)
<Grid x:Name="ContentPanel" Grid.Row="1"> <local:CanvasClone> <local:CanvasClone.Resources> <Style x:Key="ellipseStyle" TargetType="Ellipse"> <Setter Property="Width" Value="100" /> <Setter Property="Height" Value="100" /> <Setter Property="Stroke" Value="{StaticResource PhoneAccentBrush}" /> <Setter Property="StrokeThickness" Value="10" /> </Style> </local:CanvasClone.Resources>
<Ellipse Style="{StaticResource ellipseStyle}" local:CanvasClone.Left="0" local:CanvasClone.Top="0" />
<Ellipse Style="{StaticResource ellipseStyle}" local:CanvasClone.Left="52" local:CanvasClone.Top="53" />
<Ellipse Style="{StaticResource ellipseStyle}" local:CanvasClone.Left="116" local:CanvasClone.Top="92" />
<Ellipse Style="{StaticResource ellipseStyle}" local:CanvasClone.Left="190" local:CanvasClone.Top="107" />
<Ellipse Style="{StaticResource ellipseStyle}" local:CanvasClone.Left="263" local:CanvasClone.Top="92" />
<Ellipse Style="{StaticResource ellipseStyle}" local:CanvasClone.Left="326" local:CanvasClone.Top="53" />
<Ellipse Style="{StaticResource ellipseStyle}" local:CanvasClone.Left="380" local:CanvasClone.Top="0" /> </local:CanvasClone> </Grid>
|
With much elation, we discover that the display looks the same as the earlier program: