Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

WinRT - Custom WrapPanel

4.33/5 (4 votes)
24 Sep 2012CPOL4 min read 36.7K   203  
How to create a custom (wrap) panel control for Windows RT / 8.

Introduction

It all started out pretty usual. With Windows 8 on it's way out I was asked (or rather assigned a task) to create a newspaper app for the company I work for. After sorting out all the interesting stuff (like connecting to API, serialization/deserialization of articles, session management) I've got to the point of presenting the data to the user.

My basic concept was to show the article as a collection of article elements (images, paragraphs, videos, graphs, etc.) in multiple columns of same width. Something similiar to this (random googled image):

Image 1

A WrapPanel would be great to get the job done, but as I found out there isn't any in WinRT (Thanks MACrosoft, as if Vista wasn't enough.

After googling around I've found one Silverlight WrapPanel control ported to WinRT, but it had some issues. For example it couldn't autosize to it's content, so i decided to roll out my own. And since you are reading this even share it out to hopefully save somebody some time.  

Creating the WrapPanel

My first idea was to make a custom panel control which would contain multiple vertical stack panels inside horizontal stack panel, updating content and count of inner panels upon adding and removing items to the control. Final solution became a lot simpler without any need for additional inner panels.

So let's get started.  

First of all we need to create a new panel control as a panels descendant and declare some useful properties:

C#
public class WrapPanel : Panel
{
    //Gets or sets whether elements are stacked vertically or horizontally.
    public Orientation Orientation { get; set; }
 
    //Gets or sets the fixed dimension size of block.
    //Vertical orientation => BlockWidth
    //Horizontal orientation => BlockHeight
    public double BlockSize { get; set; }
 
    //Gets or sets the amount of space in pixels between blocks.
    public double BlockSpacing { get; set; }
}

We will turn these "standard" properties into DependencyProperties later on. That way they can be set in XAML. But first let's take care of sizing and laying out controls in our new panel.

For this we need to override two methods of parent Panel control. Namely MeasureOverride and ArrangeOverride.

I won't go into detail of describing these methods /*that's what we have msdn for*/

http://msdn.microsoft.com/en-us/library/system.windows.frameworkelement.measureoverride.aspx

http://msdn.microsoft.com/en-us/library/system.windows.frameworkelement.arrangeoverride.aspx

What microsoft is basically trying to tell us is:

Use measure override to report back (to the parent control) the amount of space that you are going to use. If the parent control is autosizing to its content it will autosize to this reported size (plus size of other possible controls). Do note that the size you report back will be passed back to you in ArrangeOverride method.

Use arrange override to actually layout child controls within the panel.

For the sake of simplicity I've splitted the Measure/Arrange override methods into separate methods for Vertical and Horizontal orientation. You can merge them together if you like, but i find it more readable this way. Since the code is very much identical, I'll only show the vertical version. 

Measure override

C#
protected override Size MeasureOverride(Size availableSize)
{
    switch (Orientation)
    {
        case Orientation.Horizontal:
            return MeasureOverrideHorizontal(availableSize);
 
        case Orientation.Vertical:
        default:
            return MeasureOverrideVertical(availableSize);
    }
}

And the vertical MeasureOverride:

C#
private Size MeasureOverrideVertical(Size availableSize)
{
    //Create available size for child control
    //In Vertical orientation child control can have a maximum width of BlockSize
    //And it's height can be "unlimited" - I want to know what height would control like to have at given width
    Size childAvailableSize = new Size(BlockSize, double.PositiveInfinity);
 
    //Next, i want to stack my child controls under each other (i call it block), until i reach my available height. 
    //From that point i want to begin another block of controls next to the current one.
            
    int blockCount = 0;
    if (Children.Count > 0) //If i have any child controls, than i will have at least one block.
        blockCount = 1;
 
    var remainingSpace = availableSize.Height; //Set my limit as my available height.
    foreach (var item in Children)    
    {
        item.Measure(childAvailableSize); //Let the child measure itself (result of this will be in item.DesiredSize
        if (item.DesiredSize.Height > remainingSpace) //If there is not enough space for this control
        {
            //Then we will start a new block, but only if the current block is not empty
            //if its empty, then remaining space will be equal to available height.
            if (remainingSpace != availableSize.Height)
            {
                remainingSpace = availableSize.Height;
                blockCount++;   //Reset remaining space and increase block count.
            }
        }
        //In any case, decrease remaining space by desired height of control.
        remainingSpace -= item.DesiredSize.Height;
    }
 
    //Now we need to report back how much size we want,
    //thats number of blocks * their width, plus spaces between blocks
    //And for height, we will take what ever we can get.
    Size desiredSize = new Size();
    if (blockCount > 0)
        desiredSize.Width = (blockCount * BlockSize) + ((blockCount - 1) * BlockSpacing);
    else desiredSize.Width = 0;
    desiredSize.Height = availableSize.Height;
 
    return desiredSize;
}

Arrange Override

C#
protected override Size ArrangeOverride(Size finalSize)
{
    switch (Orientation)
    {
        case Orientation.Horizontal:
            return ArrangeOverrideHorizontal(finalSize);
        case Orientation.Vertical:
        default:
            return ArrangeOverrideVertical(finalSize);
    }
}

And once again the Arrange vertical method:

C#
private Size ArrangeOverrideVertical(Size finalSize)
{
    //Each child control will be placed in rectangle with width of BlockSize
    //and height of child controls desired height.
    //Upper left corner of first controls rectangle will initialy start at 0,0 relative to this control
    //and move down by height of control and more to the left by BlockSize once the block runs out of free space
    double offsetX = 0;
    double offsetY = 0;
    foreach (var item in Children)
    {
        //If item will fit into remaining space, ....
        if ((finalSize.Height - offsetY) < item.DesiredSize.Height)
        {
            if (offsetY != 0) //and the current block is not empty. (same rules as in measureoverride)
            {
                offsetX += BlockSpacing; //We will increse offset from left by the block size
                offsetX += BlockSize;    //and spacing between blocks
                offsetY = 0;             //and finally reset offset from top
            }
        }
        //Create rectangle for child control
        Rect rect = new Rect(new Point(offsetX, offsetY), new Size(BlockSize, item.DesiredSize.Height));
        //And make it arrange within the rectangle, ...
        item.Arrange(rect);
        //Increment the offset by height.
        offsetY += item.DesiredSize.Height;
    }
    return base.ArrangeOverride(finalSize);
}

Making our properties XAML friendly

After this, all that is left is turning our properties into dependency properties. Do note we need to update our layout after changing them. For this purpose we cannot just put update logic into property setters (that would work in runtime only) but we need to register OnChangeHandler in property metadata during property registration. 

Orientation

C#
public Orientation Orientation
{
    get { return (Orientation)GetValue(OrientationProperty); }
    set { SetValue(OrientationProperty, value); }
}
 
public static readonly DependencyProperty OrientationProperty =
    DependencyProperty.Register("Orientation", typeof(Orientation), typeof(WrapPanel), new PropertyMetadata(Orientation.Vertical, OnOrientationPropertyChanged));
 
private static void OnOrientationPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
    (source as WrapPanel).InvalidateMeasure();
}

BlockSize

C#
public double BlockSize
{
    get { return (double)GetValue(BlockSizeProperty); }
    set { SetValue(BlockSizeProperty, value); }
}
 
public static readonly DependencyProperty BlockSizeProperty =
    DependencyProperty.Register("BlockSize", typeof(double), typeof(WrapPanel), new PropertyMetadata(100.0, OnBlockSizePropertyChanged));
 
private static void OnBlockSizePropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
    (source as WrapPanel).InvalidateMeasure();
} 

BlockSpacing

C#
public double BlockSpacing
{
    get { return (double)GetValue(BlockSpacingProperty); }
    set { SetValue(BlockSpacingProperty, value); }
}
 
public static readonly DependencyProperty BlockSpacingProperty =
    DependencyProperty.Register("BlockSpacing", typeof(double), typeof(WrapPanel), new PropertyMetadata(0.0, OnBlockSpacingPropertyChanged));
 
private static void OnBlockSpacingPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
    (source as WrapPanel).InvalidateMeasure();
}

Note: Use invalidate measure for invalidation, since changing for example block size will result in change of wrap panels dimensions. Calling invalidate arrange will result in rearranging of controls but size of wrap panel will remain the same. Controls will be cropped or there will be free space left (whether the size was increased or decreased)

Using the control

After building your project, you should be able to drag and drop this control onto designer surface and use it as any other control.

Most basic usage is for homogeneous controls (for this WrapGrid would suffice) example Xaml and image:

XML
<Grid Name="grid" Width="600" Height="160" Background="{StaticResource ApplicationPageBackgroundThemeBrush}" HorizontalAlignment="Left">
    <local:WrapPanel Background="Brown" VerticalAlignment="Stretch" HorizontalAlignment="Left" MinWidth="100" BlockSize="250" BlockSpacing="10" Orientation="Vertical">
        <Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="Test 1" />
        <Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="Test 2" />
        <Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="Test 3" />
        <Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="Test 4" />
        <Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="Test 5" />
        <Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="Test 6" />
        <Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="Test 7" /> 
    </local:WrapPanel>
</Grid> 

 Image 2

But works nicely with diffrent sizes of controls:  

XML
<Grid Name="grid" Width="550" Height="200" Background="{StaticResource ApplicationPageBackgroundThemeBrush}" HorizontalAlignment="Left">
    <local:WrapPanel Background="Brown" VerticalAlignment="Stretch" HorizontalAlignment="Left" MinWidth="100" BlockSize="150" BlockSpacing="10" Orientation="Vertical">
        <Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="Test 1" />
        <TextBlock FontSize="18" TextWrapping="Wrap" TextAlignment="Justify">Hello world, this is a test of long textblock</TextBlock>
        <Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Height="90" Content="Test 2 - Bigger" />
        <Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="Test 3" />
        <Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="Test 4" />
        <Button HorizontalAlignment="Stretch" Margin="0,22" VerticalAlignment="Stretch" Content="Test 5 - Margin" />
        <Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="Test 6" />
        <Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="Test 7" /> 
    </local:WrapPanel>
</Grid> 

Diffrent size controls on panel 

Note: The black border you are seeing is from underlying grid to make the wrap panels size visible, remove the grids fixed width and it will autosize to our panel.

Further improvements hints 

Well I can think of only one, if you would like to add controls with fixed sizes and make the block autosize to the biggest of them, all you should have to do is remember the biggest width found during the measure pass and make it your BlockSize. But that is not what I needed, so it's up to you.  

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)