Visually Located

XAML and GIS

Creating a WrapPanel for your Windows Runtime apps

Recently I saw a friend, Glenn Versweyveld, write a blog about showing a “tags” (eg blog tags) within an app. The blog documents how to create a horizontal list of tags that are not formed into columns and rows. The list would let the items flow naturally. He used something I never would have thought of to accomplish this. He used a RichTextBlock. This was a rather cool idea that again, I would have never thought of.

When I saw the blog I quickly asked why he did not just use a GridView or a WrapGrid/ItemsWrapGrid. His simple reply was that it did not accomplish what he wanted due to the row/column layout..

If you are on “Big Windows” the GridView lays items out into columns and rows, by filling up columns from left to right. If you are on Windows Phone the Grid View also lays items in rows and columns, but it fills up rows first instead of columns.

The right picture shows Big Windows and the left shows phone.

GridView-BigWindowsGridView-Phone

Ok, so GridView is out, how about a ListView and change the ItemsPanel to be an ItemsWrapGrid with Orientation set to Horizontal? Nope, same row/column layout with that as well. Okay, now I see why Glenn went a custom route.

I like “Plug-n-Play” solutions. I like custom controls that I can put into XAML w/o and custom work. So, while I think Glenn’s approach was rather cool, it’s just not Plug-n-Play. To accomplish this wrapping we don't need to create a custom control. We can create a new Panel that we can use for any ItemsControl.

public class WrapPanel : Panel
{
    // 
}

When creating custom panels, there are two methods you must override, MeasureOverride and ArrangeOverride. The MeasureOverride method is where the panel determines how much space it will take up. It does this by asking each element within it to measure itself and then it will return the final size. The ArrangeOverride method is where the panel takes the information from the MeasureOverride and then places each item at X and Y locations.

The MeasureOverride will find the Height of the panel and just assume that the Width is the width it is given.

protected override Size MeasureOverride(Size availableSize)
{
    // Just take up all of the width
    Size finalSize = new Size { Width = availableSize.Width };
    double x = 0;
    double rowHeight = 0d;
    foreach (var child in Children)
    {
        // Tell the child control to determine the size needed
        child.Measure(availableSize);
 
        x += child.DesiredSize.Width;
        if (x > availableSize.Width)
        {
            // this item will start the next row
            x = child.DesiredSize.Width;
 
            // adjust the height of the panel
            finalSize.Height += rowHeight;
            rowHeight = child.DesiredSize.Height;
        }
        else
        {
            // Get the tallest item
            rowHeight = Math.Max(child.DesiredSize.Height, rowHeight);
        }
    }
 
    // Add the final height
    finalSize.Height += rowHeight;
    return finalSize;
}

The ArrangeOverride will place each item at the correct X and Y location based on the size of the elements.

protected override Size ArrangeOverride(Size finalSize)
{
    Rect finalRect = new Rect(0, 0, finalSize.Width, finalSize.Height);
 
    double rowHeight = 0;
    foreach (var child in Children)
    {
        if ((child.DesiredSize.Width + finalRect.X) > finalSize.Width)
        {
            // next row!
            finalRect.X = 0;
            finalRect.Y += rowHeight;
            rowHeight = 0;
        }
        // Place the item
        child.Arrange(new Rect(finalRect.X, finalRect.Y, child.DesiredSize.Width, child.DesiredSize.Height));
 
        // adjust the location for the next items
        finalRect.X += child.DesiredSize.Width;
        rowHeight = Math.Max(child.DesiredSize.Height, rowHeight);
    }
    return finalSize;
}

This panel will now layout items from left to right and any content that doesn’t fit in the row will go to the next row.

Let’s test this out. First we’ll try using an ItemsControl

<ItemsControl ItemsSource="{Binding Items}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <controls:WrapPanel/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Button Background="Red" MinWidth="0" MinHeight="0" Margin="0,0,6,0">
                <TextBlock Text="{Binding}" FontSize="20"/>
            </Button>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

We get the following layout in both Phone and Windows

WrapPanel-BigWindows

If you use a ListView or ListBox you will get a slightly different layout due to the style of the ListViewItem and ListBoxItem. I’ll let you decide how you want to style those.

Why does my ListView scroll to the top when navigating backwards?

I’ve seen a few people asking this question. They have a page which contains a ListView and when an item is selected it navigates to another page. When they navigate backwards the ListView is back up to the top again. This behavior is due to the NavigationCacheMode of the page. By default the page will not cache rendered content when navigating “forward”. So when you navigate back to the page it re-renders the content. When displaying content like a ListView this will cause it to show the top content.

navigation

As with most things, there are a few solutions to this problem. The most common solution is to set the NaivationCacheMode to Enabled or Required.

public ListPage()
{
    this.InitializeComponent();
 
    this.NavigationCacheMode = NavigationCacheMode.Required;
}

These options do the following:

Member Value Description
Disabled 0

The page is never cached and a new instance of the page is created on each visit.

Required 1

The page is cached and the cached instance is reused for every visit regardless of the cache size for the frame.

Enabled 2

The page is cached, but the cached instance is discarded when the size of the cache for the frame is exceeded.

With this property set for the page the page content will not re-render because the rendered state has been cached!

It does get a little annoying to set the property for every page. I like to use a base page class that contains all my navigation stuff and cache mode as well. This makes it much easier to do the basic stuff.

public class AppPage : Page
{
    public AppPage()
    {
        NavigationCacheMode = NavigationCacheMode.Enabled;
 
        // other stuff for navigation
    }
}
 
public partial class ListPage : AppPage
{
    ...
}

Unfortunately this is not a dependency property so you cannot create a base style that sets this property.

A second option is to use the ScrollIntoView method of the ListView. When your page loads, simply scroll your current item into view. This does have the drawback of not being at the exact same spot as before so I do recommend using the NavigationCacheMode.