Silverlight 2.0 has introduced some new layout controls that match some controls from Windows Presentation Foundation. Grid, and StackPanel can handle the most of cases but there are many other cases where they are not useful and we have to return to the Canvas control and handle the resize event to get our custom layout to work.

Many people do not know that in Silverlight 2.0 it is possible to create a custom layout control and use it in XAML markup exactly like a usual layout control. Implementing a custom layout control is easy and require only that we are able to write the layout logic in the 2-pass layout system of Silverlight.

When the Silverlight runtime encounter a Layout control, it start a 2-pass algorithm to define measure and arrangement of the child controls. The first pass work to define the size of each element. During this passage the runtime calls the Measure method and collect the size requested by each control. When the first pass is completed and all the nodes of the XAML document has been visited and measured the runtime start to call the Arrange method where it request each control to do its own layout hierarchically. Every control has to perform layout of his children calling the Arrange method and define the position of every element.

In this sample I will show how to create a custom layout arranging the children in Radial positions. This control will be called RadialPanel and we will have to implement at least one of the two layout passages of the runtime to have it to work.

For a RadialPanel we have also to define two Attached Properties. This properties will contains the Angle and Distance from center for each children element. During the arrangement of control we will use this properties to define the position of the elements calculating it respect the center of the panel using a simple formula. So let's start our RadialPanel control creating the class

   1: public class RadialPanel : Panel
   2: {
   3: }

To create a layout control we have to inherits from the Panel class that is the base class for Canvas, Grid and StackPanel. This class handle the biggest part of the layout system and define two methods that we may override to implements our custom layout rule. Now that we have created the layout control we have to define the dependency properties:

   1: public static readonly DependencyProperty DistanceProperty = 
   2:     DependencyProperty.RegisterAttached("Distance", typeof(double), typeof(RadialPanel), null);
   3: public static readonly DependencyProperty AngleProperty = 
   4:     DependencyProperty.RegisterAttached("Angle", typeof(double), typeof(RadialPanel), null);

These declarations create two Dependency Properties that we register as Attached Properties using the RegisterAttached static method of the DependencyProperty class. This method require us to specify the property name (as we will write in XAML markup), the property type, the type of containing class and if needed a PropertyChangedCallback delegate that the runtime will call when the property changes its value. We have also to create four methods - two Set methods and two Get methods - that will Set and Get the value to and from the UIElement contained in the Panel. This is a simple operation:

   1: /// <summary>
   2: /// Sets the distance.
   3: /// </summary>
   4: /// <param name="element">The element.</param>
   5: /// <param name="distance">The distance.</param>
   6: public static void SetDistance(UIElement element, double distance)
   7: {
   8:     element.SetValue(DistanceProperty, distance);
   9: }
  11: /// <summary>
  12: /// Gets the distance.
  13: /// </summary>
  14: /// <param name="element">The element.</param>
  15: /// <returns></returns>
  16: public static double GetDistance(UIElement element)
  17: {
  18:     return (double)element.GetValue(DistanceProperty);
  19: }
  22: /// <summary>
  23: /// Sets the angle.
  24: /// </summary>
  25: /// <param name="element">The element.</param>
  26: /// <param name="distance">The distance.</param>
  27: public static void SetAngle(UIElement element, double distance)
  28: {
  29:     element.SetValue(AngleProperty, distance);
  30: }
  32: /// <summary>
  33: /// Gets the angle.
  34: /// </summary>
  35: /// <param name="element">The element.</param>
  36: /// <returns></returns>
  37: public static double GetAngle(UIElement element)
  38: {
  39:     return (double)element.GetValue(AngleProperty);
  40: }

Now it is time to implement our custom layout logic. The Panel class expose two methods to override to customize the layout algorithm. These methods are MeasureOverride, called during the first pass and ArrangeOverride called during the second pass. For our RadialPanel to work whe need only to override the ArrangeOverride method because the default implementation of MeasureOverride is perfect for us. Here is the implementation of this method:

   1: /// <summary>
   2: /// When implemented in a derived class, provides the behavior for the "Arrange" pass of Silverlight layout.
   3: /// </summary>
   4: /// <param name="finalSize">The final area within the parent that this element should use to arrange itself and its children.</param>
   5: /// <returns>The actual size used.</returns>
   6: protected override Size ArrangeOverride(Size finalSize)
   7: {
   8:     foreach (UIElement element in this.Children)
   9:     {
  10:         double distance = (double)element.GetValue(DistanceProperty);
  11:         double angle = Math.PI / 180 * (double)element.GetValue(AngleProperty);
  13:         double x = distance * Math.Cos(angle);
  14:         double y = distance * Math.Sin(angle);
  16:         element.Arrange(new Rect(x, y, finalSize.Width, finalSize.Height));
  17:     }
  19:     return base.ArrangeOverride(finalSize);
  20: }

The code is very simple. We loop all the UIElement in the Children collection and for every element found we acquire its angle and distance property. The angle property need to be converted from degree to radians because we have to use this parameter in the trigonometric functions Math.Sin() and Math.Cos(). The next lines calculate the x and y position referred to the panel center. Finally we call the Arrange method on the UIElement passing the rectangle defining position and size granted to the element itself. As size we simply pass the entire area. It is not an our concern the size the element will have. It may fill the entire panel.

Now that the RadialPanel has been completed we need to use it in a XAML markup. The first thing to do is to reference the assembly in the XAML file. It has to be done by inserting the namespace declaration in the UserControl root element of the XAML page:

   1: <UserControl
   2:     x:Class="Elite.Silverlight.Samples.RadialPanelSample"
   3:     xmlns="" 
   4:     xmlns:x="" 
   5:     xmlns:ec="clr-namespace:Elite.Silverlight.Controls;assembly=Elite.Silverlight"
   6:     Width="Auto" Height="Auto">
   8: </UserControl>

In the namespace declaration we have to insert also the "assembly=Elite.Silverlight" part of the declaration otherwise the AttachedProperties will not work. It appears there is an issue in the Silverlight runtime affecting this kind of properties. Now we have to insert the RadialPanel in the markup. It is needed we use the declared namespace "ec" and Visual Studio 2008 will show us a powerful intellisense window.

   1: <UserControl 
   2:     x:Class="Elite.Silverlight.Samples.RadialPanelSample"
   3:     xmlns="" 
   4:     xmlns:x="" 
   5:     xmlns:ec="clr-namespace:Elite.Silverlight.Controls;assembly=Elite.Silverlight"
   6:     Width="Auto" Height="Auto">
   8:     <ec:RadialPanel>
  10:     </ec:RadialPanel>
  12: </UserControl>

Finally we may insert some TextBloks specifing the Angle and distance properties. We have to specify the namespace too using the Attached Properties to have is working.

   1: <UserControl x:Class="Elite.Silverlight.Samples.RadialPanelSample"
   2:     xmlns="" 
   3:     xmlns:x="" 
   4:     xmlns:ec="clr-namespace:Elite.Silverlight.Controls;assembly=Elite.Silverlight"
   5:     Width="Auto" Height="Auto">
   6:     <ec:RadialPanel>
   7:         <TextBlock Text="0" ec:RadialPanel.Distance="100" ec:RadialPanel.Angle="0" Width="50" Height="50" />
   8:         <TextBlock Text="45" ec:RadialPanel.Distance="100" ec:RadialPanel.Angle="45" Width="50" Height="50" />
   9:         <TextBlock Text="90" ec:RadialPanel.Distance="100" ec:RadialPanel.Angle="90" Width="50" Height="50" />
  10:         <TextBlock Text="135" ec:RadialPanel.Distance="100" ec:RadialPanel.Angle="135" Width="50" Height="50" />
  11:         <TextBlock Text="180" ec:RadialPanel.Distance="100" ec:RadialPanel.Angle="180" Width="50" Height="50" />
  12:         <TextBlock Text="225" ec:RadialPanel.Distance="100" ec:RadialPanel.Angle="225" Width="50" Height="50" />
  13:         <TextBlock Text="270" ec:RadialPanel.Distance="100" ec:RadialPanel.Angle="270" Width="50" Height="50" />
  14:         <TextBlock Text="315" ec:RadialPanel.Distance="100" ec:RadialPanel.Angle="315" Width="50" Height="50" />
  15:     </ec:RadialPanel>    
  16: </UserControl>

The example is now complete and it will show eight Textblock arranged in a circle around the center of the layout panel. Each Textblock Text property contains the angle we have specified to show how the panel works.

I think that custom layout will be very powerful. Thanks to this kind of extensibility we can easily fill the gap between Silverlight 2.0 and WPF layout system. Probably is not so hard to implement a DockPanel and a WrapPanel using this tecnique.


The code I shown in this article has been added to the Elite.Silverlight library I've uploaded to codeplex at this address. I think I will add all the working samples to this library because I think that it is useful to have a library exposing this useful controls and code snippets.

Link: (release 1.1.3043.0)

Commenti (4) -

# | | 20.10.2008 - 09.16 infoDose #3 (13th Oct - 17th Oct)

# | Automotive Software | 16.11.2008 - 14.54

This is very useful. Thank you!

# | ASD Blog | 20.12.2008 - 06.30

Radial Panel with Animated Attached Properties

# | Zakanyi Balazs | 31.01.2009 - 00.26

Thanks maybe i will use it.

Aggiungi Commento