My new favourite Silverlight Panel

In a SIlverlight app I’m building I needed a panel that can display items in a given sort order that is less strict than the row index of a Grid. If multiple elements share the same sort order, fine, the sort together, but they should not overlap. During runtime, elements need to be re-ordered, with some nifty animation on top please.
I hadn’t written a custom panel yet, so I thought it was a good chance to try.

For defining the sort order, I use an attached dependency property. Attached dependency properties are used e.g. by the Grid for defining the row and column of elements. Defining a custom attached dependency property is mostly like defining a normal dependency property, it is registered via DependencyProperty.RegisterAttached. You need to provide a static getter and setter method, and you can define a default value and an optional callback. Using the attached dependency property SortOrder, each element of the panel can specify in which order it should appear, independent of its actual position in the Children collection. BTW: wouldn’t something like attached property make a lot of sense in general purpose programming languages? To me, they seem to make a good compromise between dynamically extensible objects and type-safety.

To implement a custom panel in Silverlight, you need to implement MeasureOverride and ArrangeOverride. MeasureOverride lets you specify how much space the panel is going to demand. ArrangeOverride is used to position the child elements within the panel.

And that was it for the ordered panel! For the animation part, I stumbled across this excellent article by Robby Ingebretsen. He’s created a great base class for animated panels; all I needed to do is derive from it and use its SetElementLocation method to set child element location. The article has been written in the SIlverlight 2 beta 2 timeframe, and the demos currently don’t work, but don’t let that bother you. Robby’s AnimatingPanelBase is an absolute gem, and does work in Silverlight 3 without modification. (To make the demo work, remove MainCanvas.InvalidateMeasure();) from Page.xaml.cs.)

So here’s the code, enjoy!

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using AnimatingCanvas.Controls;

namespace GreenIcicle.Controls
{
  /// <summary>
  /// A panel that behaves like a StackPanel, with two additions: 
  /// it offers a SortOrder to sort elements within it, and uses animation
  /// effects to move elements when their sort order changes.
  /// </summary>
  public class AnimatingOrderedPanel : AnimatingPanelBase
  {

    /// <summary>
    /// Determine the space taken up by this panel.
    /// </summary>
    /// <param name="availableSize">The size that is available for this panel
    /// within the parent element.</param>
    /// <returns>The size that this element is going to require.</returns>
    protected override Size MeasureOverride( Size availableSize )
    {
      // Child elements can be as high as they need, but only as wide as this panel
      Size elementAvailableSize = new Size( availableSize.Width, double.PositiveInfinity );


      double height = 0;
      double width = 0;
      foreach( UIElement element in this.Children )
      {
        element.Measure( elementAvailableSize );
        height += element.DesiredSize.Height;
        if( element.DesiredSize.Width > width )
        {
          width = element.DesiredSize.Width;
        }
      }

      return new Size( width, height );
    }
    
    /// <summary>
    /// Assigns the position of child elements, and returns the ccorrect size of theis control.
    /// </summary>
    /// <param name="finalSize">The size of the control before <see cref="ArrangeOverride"/> is executed.</param>
    /// <returns>he size of this control: the width is not altered. The height is set to the sum of the
    /// height of all elements in the <see cref="Chilren"/> collection.</returns>
    protected override Size ArrangeOverride( Size finalSize )
    {

     
      // Order the child elements by their SortOrder and their index in the Children collection.
      // The sorting by index is for improved stability; Linq does not make promises on the 
      // order of results, methinks.
      int index = 0;
      IList<UIElement> orderedChildren =
        Children
        .Select( element => new
        {
          Element = element,
          SortOrder = element.GetValue( SortOrderProperty ),
          Index = index++
        } )
        .OrderBy( ordered => ordered.Index )
        .OrderBy( ordered => ordered.SortOrder )
        .Select( ordered => ordered.Element )
        .ToList();

      double top = 0;
      // Now set the position of all elements.
      foreach( UIElement element in orderedChildren )
      {

        // Accept the  height from the child elements
        double height = element.DesiredSize.Height;

        // Set the element width to own width
        double width = finalSize.Width;

        // Position the element left corner at the container's left border
        double left = 0;

        // Set the position of the element
        base.SetElementLocation( element, new Rect( left, top, width, height ) );

        // Increase the top position of the next element by the height of the current one
        top += height;        
      }

      // Set the size to the sum of the elements' height
      finalSize.Height = top;

      return finalSize;
    }


    /// <summary>
    /// Gets the value of the attached dependency property <see cref="SortOrderProperty"/>
    /// </summary>
    /// <param name="target">The element onto which the dependency property is applied.</param>
    /// <returns>The value of <see cref="SortOrderProperty"/></returns>
    public static int GetSortOrder( DependencyObject target )
    {
      return (int) target.GetValue( SortOrderProperty );
    }

    /// <summary>
    /// Sets the attached dependency property <see cref="SortOrderProperty"/>
    /// </summary>
    /// <param name="target">The element onto which the dependency property is applied.</param>
    /// <param name="value">The value of <see cref="SortOrderProperty"/></param>
    public static void SetSortOrder( DependencyObject target, int value )
    {
      target.SetValue( SortOrderProperty, value );
    }

    /// <summary>
    /// Attached dependency property Sort order: Sets the order of elements
    /// within the ordered panel. If two elements have the same SortOrder, 
    /// then the order in the <see cref="Children"/> collection decides.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The default value is 0.
    /// </para>
    /// <para>
    /// When the sort order changes, <see cref="SortOrderChanged"/> is called.
    /// </para>
    /// </remarks>
    public static DependencyProperty SortOrderProperty = DependencyProperty.RegisterAttached(
      "SortOrder",
      typeof( int ),
      typeof( AnimatingOrderedPanel ),
      new PropertyMetadata( 0, SortOrderChanged ) );

    /// <summary>
    /// Callback method for changed values of the attached dependency property SortOrder.
    /// </summary>
    /// <param name="sender">The element on which the sort attached dependency property value has been changed</param>
    /// <param name="args">Event arguments that contain the old and new value</param>
    public static void SortOrderChanged( DependencyObject sender, 	DependencyPropertyChangedEventArgs args )
    {
      // Navigate from the element on which the sort order has been changed to the parent panel.
      // Parent is a property of FrameworkElement, so we need to cast...
      FrameworkElement element = sender as FrameworkElement;
      if( element == null )
      {
        return;
      }
      
      // ... and here we've hopefully got our panel.
      AnimatingOrderedPanel panel = element.Parent as AnimatingOrderedPanel;
      if( panel == null )
      {
        return;
      }

       // Invalidating the Arrange-ment will cause OverrideArrange to be called; this methofd re-orders 
       // the elements.
       panel.InvalidateArrange();      
    }
  }
}
Advertisements
About

Christian is a software architect/developer. He lives in Germany, reads a lot, and likes cycling.

Tagged with:
Posted in Coding

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: