Custom Panels in Silverlight/WPF Part 1: MeasureOverride

Posted by: Clarity Blogs: ASP.NET, on 21 Aug 2009 | View original | Bookmarked: 0 time(s)

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 panels 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 lets take a quick look at what well be adding to our panel. Ive 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 its 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, its 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 Panel.MeasureOverride method is to measure each UIElement child and use its 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 childs Measure method. If the child doesnt 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 weve wrapped (sizeSoFar.Height) in other words, we know the total size the wrap panel requires so we return this value.

Note that Ive kept this simple by making two assumptions: 1) each child has the same height so we dont 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 youd 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. 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.

Advertisement
Free Agile Project Management Tool from Telerik
TeamPulse Community Edition helps your team effectively capture requirements, manage project plans, assign and track work, and most importantly, be continually connected with each other.
Category: Silverlight | Other Posts: View all posts by this blogger | Report as irrelevant | View bloggers stats | Views: 1531 | Hits: 15

Similar Posts

  • Designer Support for One-Way navigations in Entity Framework 4 more
  • How To: Silverlight grid hierarchy load on demand using MVVM and RIA services more
  • Telerik Releases New Controls for Silverlight 3 and WPF more
  • Scenarios for WS-Passive and OpenID more
  • Customizing the SharePoint ECB with Javascript, Part 1 more
  • Custom Panels in Silverlight/WPF: Introduction more
  • Ruminations on Multi-Tenant Data Architectures more
  • Telerik Launches RadControls for Silverlight 3 for Line-of-Business Application Development more
  • Data Access Guidance more
  • Why Embedded Silverlight Makes Sense more

News Categories

.NET | Agile | Ajax | Architecture | ASP.NET | BizTalk | C# | Certification | Data | DataGrid | DataSet | Debugger | DotNetNuke | Events | GridView | IIS | Indigo | JavaScript | Mobile | Mono | Patterns and Practices | Performance | Podcast | Refactor | Regex | Security | Sharepoint | Silverlight | Smart Client Applications | Software | SQL | VB.NET | Visual Studio | W3 | WCF | WinFx | WPF | WSE | XAML | XLinq | XML | XSD