Putting the Silverlight Layout System to Work

In my previous blog entry I described the fundamentals of the Silverlight Layout System (SLS). Today, I’d like to build a simplified version of the custom Carousel control that I create in greater detail in a forthcoming video, and examine the role of the SLS in laying out and animating the objects in the Carousel.

We’ll start by creating a new Silverlight solution, and immediately adding a Silverlight library (Add New Project –>Silverlight Class Library) named SimpleCarouselControl. Within that library we’ll add a single class: CarouselPanel.cs, and we’ll delete Class1.cs that Visual Studio created.

We’re going to animate the carousel programmatically using a DispatchTimer, so be sure to add

using System.Windows.Threading;

at the top of the page. Within the class, let’s add a pair of private member variables,

protected DispatcherTimer timer;
public double ItemSize { get; set; }

The latter will be used to hold the size of the items we’ll be adding to the carousel (and to keep things absurdly simple, we’ll set a single size for all the items).

Attached Dependency Property

We want to enable each child to set its Angle Property, but of course each child won’t have such a property; the angle property only makes sense in the context of a carousel.  This is exactly analogous to allowing each item in a grid to set the grid.row and grid.column and the solution is the same: we create an attached dependency property:

 public static readonly DependencyProperty AngleProperty =
       DependencyProperty.RegisterAttached(
       "Angle",
       typeof( double ),
       typeof( CarouselPanel ),
       null );

If you are familiar with the syntax for registering regular dependency properties, you’ll notice this is identical except that the keyword Property is changed to DependencyProperty.  The first parameter is the name of the Dependency property, the second is its type, the third is the type of its parent, and the fourth is a reference to its metadata (almost always a delegate used as a callback for when the property changes).

We’ll also declare a static get and set method for the DP:

 public static double GetAngle( DependencyObject obj )
 {
    return (double) obj.GetValue( AngleProperty );
 }

 public static void SetAngle( DependencyObject obj, double value )
 {
    obj.SetValue( AngleProperty, value );
 }

 

With the properties in place, we’re ready to implement the methods needed to handle layout (MeasureOverride and ArrangeOverride).  We’ll use a helper method for the latter, which will come in handy in animating the carousel, which, after all, is just repeatedly laying out the controls, changing their angle and then laying them out again.

MeasureOverride

We’ll make our override of MeasureOverride simple. Rather than asking each object for its size, and then deciding on a total size needed, we’ll get the size of the largest item, and then multiply that by the number of items. Quick, sleazy and effective.

protected override Size MeasureOverride( Size availableSize )
{
   double maxSize = ItemSize;
   int numChildren = 0;
   if ( ItemSize == 0.0 )
   {
      foreach ( UIElement element in Children )
      {
         element.Measure( availableSize );
         maxSize = Math.Max( element.DesiredSize.Width, maxSize );
         maxSize = Math.Max( element.DesiredSize.Height, maxSize );
         ++numChildren;
      }
      ItemSize = maxSize;
   }
   return new Size( numChildren * maxSize, numChildren * maxSize );
}

ArrangeOverride checks that the panel has chidlren, and iterating through the children sets each one’s angle property for even spacing around the circle that represents the carousel. (Note that the member variables Width and Height are inherited from FrameworkElement).

protected override System.Windows.Size ArrangeOverride( 
System.Windows.Size finalSize ) { if ( Children.Count == 0 ) return new Size( Width, Height ); for ( int i = 0; i < Children.Count; i++ ) { Children[ i ].SetValue( CarouselPanel.AngleProperty, ( Math.PI * 2 ) * i / Children.Count ); } PreArrange(); return new Size( Width, Height ); }

Setting the Angles

The key to this math is that a circle is 360 degrees or 2pi radians. Thus, you are setting the angle to each child’s fraction of the total number of children times the circumference  number of radians in a circle [corrected 2/21/2009] (e.g., if there are 6 children and this is child 5 you are setting the angle to 5/6 of the circumference. Each of the 6 children will be its fraction of the way around (1/6th, 2/6th etc.).  If there are 8 children, they will be 1/8, 2/8, etc. Sweet.

As an aside, when I first saw this, it was written:

Children[ i ].SetValue( 
         CarouselPanel.AngleProperty, 
          i * ( Math.PI * 2 ) / Children.Count );

The result is identical, but the reasoning is harder to discern.

PreArrange

The helper method PreArrange finds the center of the panel, and from that, the X and Y coordinates of the center.

It then iterates through the children of the panel and uses the Angle of each element to find the distance from the center at which to place the object. It does so by setting a Point (P) as the run (multiplying the sine and cosine by the radius) from the center.

double radians = (double) item.GetValue( CarouselPanel.AngleProperty );

Point p = new Point(
                    ( Math.Cos( radians ) * radiusX ) + center.X,
                    ( Math.Sin( radians ) * radiusY ) + center.Y
                   );

Matrix Transform

In other columns and videos we discuss various transforms that can be made directly on shapes and objects such as scale transforms, skew transforms and so forth.  All of these and more can be made directly using a Matrix transform.

While matrices are powerful and have many applications the Matrix we care about here is called an affine matrix which is used to manipulate a coordinate system on a two dimensional plane.

You’re not going crazy

I’ve chased down pages and pages of documentation, and this is what I’ve found. You are told repeatedly that the Matrix we use is a 3×3 structure in which you can safely ignore the third column. The matrix looks like this

    Ignore this column
M11 M12 0
M21 M22 0
OffsetX OffsetY 1

You are told that the OffsetX and OffsetY represent translation values which their name more or less tells you and you are told that the other four can be used for any kind of transform. Great, which does what?  Aha!  That you are not told.  Most of the documentation teases wonderfully, with sentences like this: “M11, the numeric value in the first row and first column of the matrix. for more information see the M11 property.   You follow that link with eager anticipation where you find an entire page of documentation that tells you that this attribute or property sets or retrieves the first row and first column of the matrix (!). Yikes!

So, because I honestly don’t think it is a corporate secret, here is what they actually do: (The default values are in parentheses)

Secrets Revealed!  
M11  X Scale (1.0) M12  Y Skew (0.0)
M21  X Skew (0.0) M22  Y Scale (1.0)
OffsetX  (0.0) OffsetY  (0.0)

For our carousel we want to scale the object based on where it is on the Y scale as in a two dimensional plane, as it moves towards higher values on the Y scale it should appear to move closer to you and thus appear larger.

To compute that value, we return to the point P we computed earlier (the placement for our object as a distance from the center).  Since we know the distance from the center we need only set the apparent perspective by dividing that distance by the sum of the center and radius values plus a small constant found by the incredibly scientific method of trial and error.

 double scaleMinusRounding = p.Y / ( center.Y + radiusY ) +0.2;

We then ensure that we use the value we just computed or the value 1, whichever is less,

double scaleY = Math.Min( scaleMinusRounding, 1.0 );
double scaleX = Math.Min( scaleMinusRounding, 1.0 );

Note carefully that we set the scaleX adn scaleY to the scaling factor we derived based on the Y axis. Objects appear larger as they approach, but not as they move from side to side, and they appear larger both in height and in breadth.

Using the Matrix to implement the scaling up

With the scale values in hand, we retrieve the MatrixTransform object from each item in the carousel and we create a new Matrix to provide to it. The Matrix constructor takes six values (as you would expect) as shown,

MatrixConstructor

Here’s the complete block of code,

MatrixTransform mt = item.RenderTransform as MatrixTransform;
double scaleMinusRounding = p.Y / ( center.Y + radiusY ) +0.2;
double scaleY = Math.Min( scaleMinusRounding, 1.0 );
double scaleX = Math.Min( scaleMinusRounding, 1.0 );
Matrix mx = new Matrix( scaleX, 0.0, 0.0, scaleY, 0.0, 0.0 );
mt.Matrix = mx;
item.RenderTransform = mt;

All that is left to do is to ensure that the items in front are not only larger, but are on top of the items that are behind, which we do by hacking the zIndex,

int zIndex = (int) ( ( p.Y / base.Height ) * 50 );
item.SetValue( Canvas.ZIndexProperty, zIndex );

We can now compute the bounding rectangle for each item, and call Arrange on the item, passing in that rectangle,

Rect r = new Rect( p.X, p.Y, ItemSize, ItemSize );
item.Arrange( r );

Starting  The Animation

All that is left is to start the animation, which we can do by creating and starting the DispatcherTimer:

public CarouselPanel()
    : base()
{
    Loaded += new RoutedEventHandler( CarouselPanel_Loaded );
}

void CarouselPanel_Loaded( object sender, RoutedEventArgs e )
{
    if ( timer == null )
    {
       timer = new DispatcherTimer();
       timer.Interval = new TimeSpan(0, 0, 0, 0, 10);
       timer.Tick += new EventHandler( timer_Tick );
       timer.Start();
    }
}

The DispatcherTimer’s interval property is set with a TimeSpan, the constructor for the TimeSpan used here takes days, hours, minutes, seconds and milliseconds. We have instructed the timer to fire its Tick event every 10 milliseconds or 100 times per second.

The timer Tick is registered with an event handler and then the Timer is started with the cleverly named Start method (no extra points for guessing how it is stopped).

Each time the event fires, our event handler iterates through the children, and moves each child’s angle by a small increment,

void timer_Tick( object sender, EventArgs e )
{
   foreach ( UIElement uie in Children )
   {
      double current = (double) ( uie.GetValue( CarouselPanel.AngleProperty ) );
      uie.SetValue( CarouselPanel.AngleProperty,
         current + ( .0016 * (2 * Math.PI ) ) );  
   }
   PreArrange();
}

That’s it for the custom control. There is no need to create a default appearance (e.g., generic.xaml) as this control derives from Panel. The next step is to use your new SimpleCarousel in your xaml file to hold and display the items in the Carousel.

 

Page.xaml

The very first thing you’ll need is to make your main project (SimpleCarousel) aware of the control you just created, by adding a reference to the Control Library project,

AddReferenceToCarousel

Once you’ve done this you can add a namespace identifier so that you can add an instance of the control to the page.

xmlns:custom="clr-namespace:SimpleCarouselControl;assembly=SimpleCarouselControl"

 

 

 

From there, you just add the panel as you would any other container,

<custom:CarouselPanel x:Name="cPanel"
                          Width="500"
                          Height="400"
                          Background="Bisque">

</custom:CarouselPanel>

Between the open and close tags you may place as many UI elements as you like,

<custom:CarouselPanel x:Name="cPanel"
                      Width="500"
                      Height="400"
                      Background="Bisque">
  <Ellipse Width="15"
           Height="15"
           Fill="Orange" />
  <Ellipse Width="75"
           Height="40"
           Fill="Blue" />
  <Rectangle Height="60"
             Width="30"
             Stroke="Black"
             StrokeThickness="3" />
  <Ellipse Width="50"
           Height="50"
           Fill="Red" />
  <Rectangle Height="40"
             Width="40"
             Fill="Green" />
  <TextBlock Text="Hello!"
             FontFamily="Georgia"
             FontSize="24" />
  <ListBox Height="70"
           Width="75">
    <ListBoxItem Content="George" />
    <ListBoxItem Content="Paul" />
    <ListBoxItem Content="John" />
    <ListBoxItem Content="Ringo" />
  </ListBox>
</custom:CarouselPanel>

The Sequence Of Events

Page.xaml will load, and your panel will be initialized. Your class will be constructed, and then when Page.xaml loads the Carousel will load firing the CarouselPanel_Loaded event.

As part of loading the page, MeasureOverride and ArrangeOverride are called and initial sizing and placement of each object is accomplished. The Carousel_Loaded event handler also creates the timer, sets its interval and starts it. 

10 milliseconds later the event will fire and be caught by timer_Tick which will iterate through all the Panel’s children, getting their angleProperty and incrementing them slightly. Timer_Tick then calls PreArrange which re-scales each object depending on its position on the y axis and calls arrange on the object, which in turn triggers a call to ArrangeOverride on each object (but not on the panel.

Note that the overrides of MeasureOverride and ArrangeOverride in panel are each called only once; after that the values are scaled and incremented as part of the animation but not as part of the layout system.

Streaming Example

— Begin streaming application

— End streaming application

 

    Previous: The Layout Model       Next: More about Layout


This work is licensed under a Creative Commons Attribution By license.

About Jesse Liberty

Jesse Liberty has three decades of experience writing and delivering software projects and is the author of 2 dozen books and a couple dozen online courses. His latest book, Building APIs with .NET will be released early in 2025. Liberty is a Senior SW Engineer for CNH and he was a Senior Technical Evangelist for Microsoft, a Distinguished Software Engineer for AT&T, a VP for Information Services for Citibank and a Software Architect for PBS. He is a Microsoft MVP.
This entry was posted in z Silverlight Archives. Bookmark the permalink.

3 Responses to Putting the Silverlight Layout System to Work

Comments are closed.