Like pancakes...

the random ramblings begin...
in

August 2009 - Posts

Custom Panels in Silverlight/WPF Part 3: Animation

This is Part 3 of my series of posts dedicated to creating an animating and virtualizing WrapPanel for Silverlight. If you missed the previous posts, I suggest you take a look.

Setup

NumberBox Animation

Let’s make this easy on ourselves by having the child control handle it’s own animation. To do this I’ve added an animation to my NumberBox resources called "slide.”

<Storyboard x:Key="slide">
    <DoubleAnimation To="0" BeginTime="0:0:0" Duration="0:0:0.5"
        Storyboard.TargetName="LayoutRoot"
        Storyboard.TargetProperty="(UIElement.RenderTransform).
            (TransformGroup.Children)[0].(TranslateTransform.X)" />
    <DoubleAnimation To="0" BeginTime="0:0:0" Duration="0:0:0.5"
        Storyboard.TargetName="LayoutRoot"
        Storyboard.TargetProperty="(UIElement.RenderTransform).
            (TransformGroup.Children)[0].(TranslateTransform.Y)" />
</Storyboard>

You’ll notice there isn’t anything special about this Storyboard, it’s a simple TranslateTransform animation. Oh, make sure you’ve got your RenderTransform defined too.

<Grid x:Name="LayoutRoot" Background="White">
    <Grid.RenderTransform>
        <TransformGroup>
            <TranslateTransform X="0" Y="0"/>
        </TransformGroup>
    </Grid.RenderTransform>

I’ve created this animation as a resource for my UserControl, but it could just as easily live in the application level resource dictionary, or be created entirely in code behind. But, now that we’ve got the Storyboard in place, let’s give callers the ability to pick a destination and start the animation. For that I’ve created a public method which takes two parameters to define the translate transform values like this:

public void SlideTo(double x, double y)
{
    Storyboard slideAni = Resources["slide"] as Storyboard;
    (slideAni.Children[0] as DoubleAnimation).To = x;
    (slideAni.Children[1] as DoubleAnimation).To = y;
    slideAni.Begin();
}

ArrangeOverride

Animation

Now that we are going to animate the location of our panel’s children we will need to update the ArrangeOverride method. First of all, even though we are going to animate the location of the child control, the logic required to determine that location does not change. Secondly, we still need to call the UIElement.Arrange method for each control to ensure that it will be shown.

WrapPanel

Our WrapPanel will call the NumberBox.SlideTo method to animate the control. We do not ever set the From values for that animation, necessitating that the NumberBox always be Arranged to the same location and that each transform be relative to that same location. To keep things simple we will arrange every child to (0,0) and then animate the TranslateTransform. That makes our ArrangeOverride code look like this:

protected override Size ArrangeOverride(Size finalSize)
{
    Size sizeSoFar = new Size(0, 0);
    foreach (UIElement child in Children)
    {
        child.Arrange(new Rect(0, 0, child.DesiredSize.Width, child.DesiredSize.Height));
        (child as NumberBox).SlideTo(sizeSoFar.Width, sizeSoFar.Height);

        if (sizeSoFar.Width + child.DesiredSize.Width >= finalSize.Width)
        {
            sizeSoFar.Height += child.DesiredSize.Height;
            sizeSoFar.Width = 0;
        }
        else
        {
            sizeSoFar.Width += child.DesiredSize.Width;
        }
    }
    return finalSize;
}

So, with a simple control-level animation and two changes to our ArrangeOverride we are now animating our WrapPanel! Easy!

Get Microsoft Silverlight

Next Steps

This has been a simplistic example to prove we can animate our custom panel. A more realistic, real-world scenario is using our panel as the ItemsPanelTemplate of an ItemsControl bound to a collection of data objects. For the next post we will move to using an ItemsControl and discuss the need for and benefits of Virtualization.

Custom Panels in Silverlight/WPF Part 2: ArrangeOverride

This is Part 2 of my series of posts dedicated to creating an animating and virtualizing WrapPanel for Silverlight. If you missed the Introduction or Part 1 I suggest you take a look.

Setup

WrapPanel

We’ve already created the WrapPanel class in Part 1, so now all we need to do is add the ArrangeOverride method to the class.

protected override Size ArrangeOverride(Size finalSize)
{
    return base.ArrangeOverride(finalSize);
}

ArrangeOverride

General Info

The ArrangeOverride method takes a Size parameter and returns a Size. The finalSize parameter represents the space allotted to the panel and the Width and Height values range from 0 to positive infinity. The finalSize parameter is determined by the MeasureOverride availableSize parameter and return value using the following process (defined for Width, but equivalent for the Height also).

If the Panel Width, MinWidth and MaxWidth are NaN (i.e. not explicitly set) and the HorizontalAlignment is Center, Left or Right then the Width value is from the MeasureOverride return value. If the Width values are NaN and HorizontalAlignment is Stretch then the Width is the maximum of the MeasureOverride availableSize width and the return value’s width property. If, however, the Width is set, the finalSize width is the maximum of the Width and the return value’s Width.

The ArrangeOverride return value is the size that is then used as the RenderSize. Typically, the finalSize is just returned regardless of the logic in the method.

The guts of the ArrangeOverride method is to physically place each child in the appropriate location. This is accomplished using the UIElement.Arrange(Rect) method. The Rect parameter passed into Arrange defines the X and Y coordinates of the top left corner of the child, relative to the parent, while the Width and Height values define the Width and Height of the area the child element should take up.

In my experience the ArrangeOverride method contains similar logic to the MeasureOverride as you have to iterate through the children and place them, tracking how much space has been used as you go along. The main difference is that UIElement.Measure is called in MeasureOverride and UIElement.Arrange is called in ArrangeOverride.

WrapPanel

For our panel the ArrangeOverride method will simply arrange each child element into the appropriate location using the same wrapping logic described in Part 1. The differences are that we don’t have to track the maximum width and, as mentioned above, we call UIElement.Arrange instead of UIElement.Measure.

protected override Size ArrangeOverride(Size finalSize)
{
    Size sizeSoFar = new Size(0, 0);

    foreach (UIElement child in Children)
    {
        child.Arrange(new Rect(sizeSoFar.Width, sizeSoFar.Height, 
                               child.DesiredSize.Width, child.DesiredSize.Height));

        if (sizeSoFar.Width + child.DesiredSize.Width >= finalSize.Width)
        {
            sizeSoFar.Height += child.DesiredSize.Height;
            sizeSoFar.Width = 0;
        }
        else
        {
            sizeSoFar.Width += child.DesiredSize.Width;
        }
    }

    return finalSize;
}

The same assumptions we made in Part 1 still apply here.

Calling Arrange

So, when does ArrangeOverride get called? Well, every time MeasureOverride is called ArrangeOverride is called after. So we can force an Arrange by calling UIElement.InvalidateMeasure(). However, if we know the measure is accurate and we want to skip what can be a complex process we can call UIElement.InvalidateArrange() directly instead. Also, just as with MeasureOverride, a custom defined DependencyProperty can register such that it includes FrameworkPropertyMetadataOptions.AffectsArrange (WPF only).

Completing the Example

So now our WrapPanel implementation has a MeasureOverride and ArrangeOverride and is complete. We built our test control (NumberBox) in Part 1 – so that’s ready to go. All we need now is to add the WrapPanel to a parent and add some code to add and remove children.

When creating a new Silverlight 3 project in Visual Studio I asked it to create a test page to host the Silverlight at build time. This UserControl is autocreated as Page.xaml and is has straightforward XAML:

<UserControl x:Class="Clarity.Demo.CustomPanel.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:local="clr-namespace:Clarity.Demo.CustomPanel"
    MaxWidth="500" MaxHeight="500">
    <Grid x:Name="LayoutRoot" Background="White">
            <local:WrapPanel x:Name="wrapPanel" />
    </Grid>
</UserControl>

Don’t forget to reference your local namespace in order to get to your WrapPanel control. Next we’ll create a DispatcherTimer to add children to our WrapPanel. I implemented this in the Page constructor, but it a real-life scenario would life be populated via binding or user interaction. I place each new child control at the beginning of the Children collection so you can see each control is arranged to it’s new position with the addition of each subsequent child. Also, for the sake of seeing our example a little cleaner, I clear the Children when we’ve got 50 of them.

public partial class Page : UserControl
{
    private DispatcherTimer _timer = new DispatcherTimer();
    private int _count = 0;

    public Page()
    {
        InitializeComponent();
        PopulatePanel();
    }

    private void PopulatePanel()
    {
        _timer.Interval = TimeSpan.FromSeconds(1);
        _timer.Tick += (sender, args) =>
            {
                if (_count > 50)
                {
                    wrapPanel.Children.Clear();
                    _count = 0;
                }
                wrapPanel.Children.Insert(0, (new NumberBox(_count++)));
            };
        _timer.Start();
    }
}

And this is what it looks like:

Get Microsoft Silverlight

OK, admittedly this is kind of lame. The real fun starts in the next post when we will animate the controls as they rearrange.

Custom Panels in Silverlight/WPF Part 1: MeasureOverride

This is Part 1 of my series of posts dedicated to creating an animating and virtualizing WrapPanel for Silverlight. If you missed the Introduction I suggest you take a look.

Before we write any code for our custom panel we have to decide what the layout of the panel’s children should be. For this example, I have decided to create a WrapPanel. (Yes, I know there is already a WrapPanel in the Silverlight Toolkit, but this one will be cooler, I promise.) We will use the WPF WrapPanel default behavior of laying out items left to right, top to bottom.

Setup

Child Control

Before we get into the MeasureOverride details let’s take a quick look at what we’ll be adding to our panel. I’ve created a Silverlight 3 UserControl called NumberBox which is just a Border and a TextBlock so we can differentiate the children.

<UserControl x:Class="Clarity.Demo.CustomPanel.NumberBox"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Width="50" Height="50">
       <Grid x:Name="LayoutRoot" Background="White">
        <Border x:Name="border" BorderBrush="Red" BorderThickness="2" Margin="5">
            <TextBlock x:Name="text" Text="0" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="16"/>
        </Border>
    </Grid>
</UserControl>
public partial class NumberBox : UserControl
{
    public NumberBox()
    {
        InitializeComponent();
    }

    public NumberBox(int number)
    {
        InitializeComponent();
        text.Text = number.ToString();
    }
}

This is a simple control that we will see put to use in Part 2: ArrangeOverride.

WrapPanel

Next we set up a new class WrapPanel which inherits from Panel and overrides the base MeasureOverride method:

public class WrapPanel : Panel
{
    public WrapPanel() : base () {}

    protected override Size MeasureOverride(Size availableSize)
    {
        return base.MeasureOverride(availableSize);
    }
}

MeasureOverride

General Info

The MeasureOverride method takes a Size parameter and returns a Size. The availableSize parameter represents the space allotted to the panel by it’s parent and values range from 0 to positive infinity for both the Width and Height properties.

If the panel is contained in a specific size you will see availableSize with those values. This applies not just for explicitly set Width and Height values, but also when the MinWidth/MinHeight and MaxWidth/MaxHeight properties are set. If the panel has margins set these are already removed from the availableSize before calling MeasureOverride. If the panel is hosted inside a ScrollViewer or other unlimited range (e.g. a Grid with an “Auto” size dimension) availableSize will have values of positive infinity for the appropriate dimensions.

MeasureOverride also returns a Size. This size may be used as the parameter passed into the ArrangeOverride method, depending on settings (discussed in more detail in Part 2). However, it’s important to note now that you cannot return a value of positive infinity for either dimension of the returned Size. A runtime System.InvalidOperationException will be thrown with message: MeasureOverride of element 'Clarity.Demo.CustomPanel.WrapPanel' should not return PositiveInfinity or NaN as its DesiredSize. You can avoid this by tracking the actual dimensions your child elements require or setting max values.

The true responsibility of the MeasureOverride method is to measure each UIElement child and use it’s DesiredSize property along with the child layout to determine the final size required for the panel (usually the Size that is then returned). Simply by calling UIElement.Measure(Size) on each child sets the DesiredSize property. Then you have to write your own logic to assess how the child fits into the chosen layout.

WrapPanel

For our panel the MeasureOverride method needs to measure each child element, determine if it fits to the right of the previous child, and wrap to the next line down if it does not. Throughout this process we will track our current height and width to know where the next child should be placed, as well as the maximum width reached. Sounds simple, right? Well, for our situation, it is.

protected override Size MeasureOverride(Size availableSize)
{
    Size sizeSoFar = new Size(0, 0);
    double maxWidth = 0.0;

    foreach (UIElement child in Children)
    {
        child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));

        if (sizeSoFar.Width + child.DesiredSize.Width > availableSize.Width)
        {
            sizeSoFar.Height += child.DesiredSize.Height;
            sizeSoFar.Width = 0;
        }
        else
        {
            sizeSoFar.Width += child.DesiredSize.Width;
            maxWidth = Math.Max(sizeSoFar.Width, maxWidth);
        }
    }

    return new Size(maxWidth, sizeSoFar.Height);
}

Because we are creating a WrapPanel I allow each child as much space as it thinks it needs by passing an infinite Size into each child’s Measure method. If the child doesn’t fit on the current row I wrap it to the next row. Once we iterate through all the children we know the largest width of all the rows (maxWidth) and how far down we’ve wrapped (sizeSoFar.Height) – in other words, we know the total size the wrap panel requires – so we return this value.

Note that I’ve kept this simple by making two assumptions: 1) each child has the same height so we don’t have to track a maxHeight separately, and 2) that we will be able to fit at least one child per row.

Calling Measure

Great, so now we can Measure! But when does this happen? Firstly, the runtime will call this when attempting to render the panel. This happens anytime we add or remove children, show or hide, etc. But we can also force a call to MeasureOverride in different ways. One is to call InvalidateMeasure() on the FrameworkElement you’d like to force a Measure on. Another is a little more complex but useful. If your custom element has custom Dependency Properties the properties can be registered with metadata of FrameworkPropertyMetadataOptions.AffectsMeasure (WPF only). Then, when setting the property the control will already know to call Measure.

Next Steps

This gets us partway to creating a WrapPanel in Silverlight. Stay tuned for Part 2 where I discuss the ArrangeOverride and complete the simple example.

Custom Panels in Silverlight/WPF: Introduction

One of the most important concepts in UI design is the ability to organize your UI however you want. The framework panels all have their uses, but don’t allow the type of customization that is sometimes required. In order to create your own panels there are a couple of relatively simple steps that must be taken.

I will spend the next couple of posts looking at what it takes to make a custom Silverlight WrapPanel that is both animated and virtualized. However, before I get into the details, there are a couple of introductory concepts to address.

Firstly, I will be writing this code in Silverlight. Why Silverlight you ask? Isn’t WPF more full-featured you ask? Well, the short answer (at least from where I sit), is Yes. And that is exactly why I am writing this in Silverlight. Because if you can write it in Silverlight, you can write it in WPF, while the opposite isn’t necessarily true.

The next piece of info is that our custom panel will inherit from the base Panel class. This gives us access to the UIElementCollection Children. We could manage a child element collection ourselves, but by using the built-in Children property we don’t have to worry about explicitly adding and removing the UIElements from the visual and logical trees.

It is also necessary to know the order of operations for a rendering panel. A Panel will call into it’s MeasureCore method to determine how much room it needs to render. MeasureCore is sealed and so we can’t touch it, but MeasureOverride is exposed, allowing us to indirectly override the functionality of the MeasureCore method. After measuring, the panel’s ArrangeCore method is called. Again, this method is sealed, but ArrangeOverride is exposed for us to override. This method will actually place all child elements in a location relative to the parent panel. After arranging, the panel executes OnRender to actually “paint” the screen.

We will examine MeasureOverride and ArrangeOverride in more detail in subsequent posts, but nothing I have worked on has required overriding OnRender, so I’ll leave that one alone. Also note: calling Measure will always then call Arrange, which will then always call Render, but there is no guarantee that when Render is called Arrange was called previously, and likewise no guarantee that when Arrange is called Measure was also called.

The final, and potentially most complex, piece of this puzzle is virtualization. Making a panel “virtualizing” essentially means that we are only going to create controls for the elements on screen, while destroying any controls not currently visible. This gives a huge performance boost when working with long lists of items. Again, more detail to come.

It may have already become obvious to some of you, but we will go through the process of creating a custom animating and virtualizing panel in four discrete pieces:

Part 1: MeasureOverride

Part 2: ArrangeOverride

Part 3: Animation

Part 4: Virtualization

Stay tuned…