Page view counter

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 3x3 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.
Published Monday, February 16, 2009 7:22 AM by jesseliberty

Comments

# Putting the Silverlight Layout System to Work | The Black Ball

Pingback from  Putting the Silverlight Layout System to Work | The Black Ball

# re: Putting the Silverlight Layout System to Work

Jesse:

this is a Great Article (by the way, all the others are too). I was wondering if I used this new Custom User Control with Expression Blend, could I add UIElements to it using the designer? or do I have to do it programatically?

Thank you very much

Luis Vega,

Monday, February 16, 2009 10:21 AM by luisvegas

# Silverlight Cream for February 16, 2009 -- #519

In this issue: Jesse Liberty, Gerard Leblanc, and Jamie Rodriguez. Shoutouts: The MIX09 blog reports

Monday, February 16, 2009 4:48 PM by Community Blogs

# re: Putting the Silverlight Layout System to Work

you guys are so nieaved. You think that you are helping us out. Look at this posting for example.  The author left out so much. Are we using VB C# C++.  I sure don't have a clue. What the tool to use to build this script.  Whats the extention of the script file.  I sure don't know.

Now I'm not new to coding. I have no problems with C# web applications. Yes I'm very grren behind the ears over silverlight.

If you guys give me the very basics on how to make or create a silverlight application. When I wil start understanding on how to make my own application.

Yes I'm talking to you silvelight.net administators, and authors. Please look at ASP.NET web site. Look at the way they teach.

Monday, February 16, 2009 11:56 PM by MSummers

# re: Putting the Silverlight Layout System to Work

It's all done in Pascal with some Fortan built with Lego's Assembly with a Hasboro compiler from Milton Bradley but the Intel compiler from Hot Wheels is much better. Stay away from MatchBox because thier Sun System is like a Big Wheel except it throws exceptions when the plastic wheels get holes in them from excessice power-sliding. And all that does is throw exceptions. I know, it's junk! Can you believe these guys! I dont know why they did'nt explain all of this. You would think (and boy is that big people thing) you could tell from the pictures or something.

Tuesday, February 17, 2009 1:59 AM by Sal

# Programming with Silverlight, WPF &amp; .NET &raquo; Das Layout Model

Pingback from  Programming with Silverlight, WPF &amp; .NET &raquo; Das Layout Model

Tuesday, February 17, 2009 12:37 PM by Programming with Silverlight, WPF & .NET » Das Layout Model

# Programming with Silverlight, WPF &amp; .NET &raquo; Das Layout Model

Pingback from  Programming with Silverlight, WPF &amp; .NET &raquo; Das Layout Model

Tuesday, February 17, 2009 12:37 PM by Programming with Silverlight, WPF & .NET » Das Layout Model

# re: Putting the Silverlight Layout System to Work

M Summers,

I'm sorry you were frustrated, and I can certainly understand how reading a blog entry like this can be overwhelming if this is your first exposure to Silverlight.

That said, I think you'll find that we do provide very good material for folks new to Silverlight, and of course, it doesn't serve the entire community to *only* focus on getting started; eventually folks want to move on to more advanced material.

Clearly, however, you came across my blog and did not find what you needed, and perhaps I need to do a better job pointing out where to start.

For now, you'll find a good starting point is to click on the link in the left column, under the heading "Essential Links" marked Getting Started.  That will take you to a page I created devoted to helping folks who are totally new to Silverlight (you can just go directly to http://tinyurl.com/c8nvl5

if you prefer).

I'll look into whether there are ways to avoid this kind of confusion in the future. Hang in there, we're here to help.

Tuesday, February 17, 2009 2:53 PM by jesseliberty

# The Layout Model - Jesse Liberty - Silverlight Geek

Pingback from  The Layout Model - Jesse Liberty - Silverlight Geek

Tuesday, February 17, 2009 2:55 PM by The Layout Model - Jesse Liberty - Silverlight Geek

# Silverlight Layout System (SLS) - Jesse Liberty &laquo; vincenthome&#8217;s Tech Clips

Pingback from  Silverlight Layout System (SLS) - Jesse Liberty &laquo; vincenthome&#8217;s Tech Clips

# re: Putting the Silverlight Layout System to Work

well I'm a little slow I guess but I do think the post lost some clarity toward the Middle.  You mention PreArrange() and it is called but you are not clear on which of the functions should be used in it.  I am also not clear on where "item" and "center" are defined.

Maybe in the next post you could clarify this, thanks again for this and other posts.

Wednesday, February 18, 2009 7:28 PM by John.J.Hughes.II

# More About the Layout System -

Pingback from  More About the Layout System -

Saturday, February 21, 2009 7:53 PM by More About the Layout System -

# More About the Layout System -

Pingback from  More About the Layout System -

Saturday, February 21, 2009 8:29 PM by More About the Layout System -

# More About the Layout System

Pingback from  More About the Layout System

Monday, February 23, 2009 8:48 AM by More About the Layout System

# Better Navigation

I was pretty happy with the threading navigation I had introduced… &#160; Just to double check, I asked

Thursday, February 26, 2009 1:19 AM by Jesse Liberty - Silverlight Geek

# Better Navigation

I was pretty happy with the threading navigation I had introduced… &#160; Just to double check, I asked

Thursday, February 26, 2009 2:19 AM by Microsoft Weblogs