Click here to Skip to main content
12,454,477 members (54,064 online)
Click here to Skip to main content
Add your own
alternative version

Tagged as

Stats

7.1K views
302 downloads
14 bookmarked
Posted

VariableSizedWrapGrid for WPF

, 28 May 2015 CPOL
Rate this:
Please Sign up or sign in to vote.
An implementation of VariableSizedWrapGrid for the Windows Desktop.

Introduction

Windows apps often make use of the panel VariableSizedWrapGrid set as the ItemsPanel in a GridView control to create an appealing user interface with variable sized tiles representing data or actions. I recently found myself wishing I had an easy way to create a similar style user interface when developing a WPF desktop application. I wanted a panel compatible with the WinRT VariableSizedWrapGrid to be able to reuse code concepts like for example the HeroGridView but couldn't find one so I decided to write the WPF port myself. This article describes the resulting panel and its use.

This article is written in an attempt to give something back to the community and as a homage to Jerry Nixon who inspired me to start using the VariableSizedWrapGrid in the first place. If you haven't already checked out his blog I recommend that you do so, it contains a lot of good tips and elegant solutions presented in a easy-to-grok fashion.

Background

This article is based on the original post by Jerry Nixon called "Windows 8 Beauty Tip", introducing the VariableSizedGridView and a couple of useful techniques I'll be including in this article:

  • The elegant way to get a set of colors as data to the ItemsControl.
  • Subclassing the ItemsControl to set the RowSpan and ColumnSpan attached properties on the ItemContainer based on model properties.

I'll write this article as a stand-alone piece but focus will be on the panel implementation and the additions I made to the original VariableSizedGridView behavior rather than on how to use the panel. If you want to know more about how to use the VariableSizedGridView I recommend that you check out Jerrys post where it is covered in some detail.

Using the code

The attached source code is a WPF solution created in Visual Studio Community 2015 RC. I have compiled and tested the application on Windows 7 and Windows 10, build 10074. It seems to be working well but comments and suggestions for improvements are always welcome!

This is what the main window looks like when you compile and run the application. It looks "awfully familiar" when compared to the final implementation in Jerrys post if I may say so myself, and to be honest; the goal was to mimic the WinRT panel as close as possible. I have made a couple of improvements though, but more about that later.

The VariableSizedWrapGrid is typically used as an ItemsPanel in an ItemsControl and the simplest possible usage is something like this:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApplication1"
        mc:Ignorable="d"
        Title="MainWindow" Height="500" Width="500">

    <ItemsControl Background="DarkGray">
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <local:VariableSizedWrapGrid/>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <Grid Height="200" Width="200" Background="Black"/>
        <Grid Height="100" Width="100" Background="Aqua"/>
        <Grid Height="100" Width="100" Background="Red"/>
        <Grid Height="200" Width="200" Background="Green"/>
        <Grid Height="100" Width="100" Background="Blue"/>
        <Grid Height="100" Width="100" Background="Red"/>
        <Grid Height="200" Width="200" Background="Green"/>
        <Grid Height="100" Width="100" Background="Blue"/>
        <Grid Height="100" Width="100" Background="Aqua"/>
        <Grid Height="100" Width="100" Background="Red"/>
        <Grid Height="100" Width="100" Background="Black"/>
    </ItemsControl>
</Window>

The XAML above will produce a layout like the one below.

There is nothing really "variable" about the layout above. Just like in a WrapPanel all items are given the same amount of real estate even though the item sizes are different. This is due to the lack of ColumnSpan/RowSpan properties set on the items and a quirk in the VariableSizedWrapGrid behavior which causes the size of the first first item to define the sizes for all succeeding items if the ItemWidth and ItemHeight properties aren't set on the panel.

But, as you can see there are no scrollbars so let's add some styling to the ItemsControl to enable scrolling before doing anything else.

    <ItemsControl Background="DarkGray">
        <ItemsControl.Style>
            <Style TargetType="{x:Type ItemsControl}">
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="{x:Type ItemsControl}">
                            <Border BorderBrush="{TemplateBinding BorderBrush}"
                                    BorderThickness="{TemplateBinding BorderThickness}" 
                                    Background="{TemplateBinding Background}" 
                                    Padding="{TemplateBinding Padding}"
                                    SnapsToDevicePixels="True">
                                <ScrollViewer CanContentScroll="True" 
                                              HorizontalScrollBarVisibility="Auto" 
                                              VerticalScrollBarVisibility="Auto">
                                    <ItemsPresenter 
                                           SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                                </ScrollViewer>
                            </Border>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </ItemsControl.Style>
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <local:VariableSizedWrapGrid/>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <Grid Height="200" Width="200" Background="Black"/>
        <Grid Height="100" Width="100" Background="Aqua"/>
        <Grid Height="100" Width="100" Background="Red"/>
        <Grid Height="200" Width="200" Background="Green"/>
        <Grid Height="100" Width="100" Background="Blue"/>
        <Grid Height="100" Width="100" Background="Red"/>
        <Grid Height="200" Width="200" Background="Green"/>
        <Grid Height="100" Width="100" Background="Blue"/>
        <Grid Height="100" Width="100" Background="Aqua"/>
        <Grid Height="100" Width="100" Background="Red"/>
        <Grid Height="100" Width="100" Background="Black"/>
    </ItemsControl>

Key points in the control template applied to enable scrolling for the ItemsControl are:

  1. Wrap the VariableSizedWrapGrid in a ScrollViewer.
  2. Set the CanContentScroll property to True to have the ScrollViewer pass the actual scrolling on to the VariableSizedWrapGrid. Unless doing this the ScrollViewer will give the panel infinite space which will result in all items being laid out in a single column.
  3. Set the HorizontalScrollBarVisibility/VerticalScrollBarVisibility properties to Auto to make the scrollbars appear when needed.

Next, we'll add some ColumnSpan/RowSpan properties to the items matching the item sizes.

<Grid Height="200" Width="200"
   local:VariableSizedWrapGrid.ColumnSpan="2" local:VariableSizedWrapGrid.RowSpan="2"
   Background="Black"/>
<Grid Height="100" Width="100" Background="Aqua"/>
<Grid Height="100" Width="100" Background="Red"/>
<Grid Height="200" Width="200"
   local:VariableSizedWrapGrid.ColumnSpan="2" local:VariableSizedWrapGrid.RowSpan="2"
   Background="Green"/>
<Grid Height="100" Width="100" Background="Blue"/>
<Grid Height="100" Width="100" Background="Red"/>
<Grid Height="200" Width="200"
   local:VariableSizedWrapGrid.ColumnSpan="2" local:VariableSizedWrapGrid.RowSpan="2"
   Background="Green"/>
<Grid Height="100" Width="100" Background="Blue"/>
<Grid Height="100" Width="100" Background="Aqua"/>
<Grid Height="100" Width="100" Background="Red"/>
<Grid Height="100" Width="100" Background="Black"/>

Now, that's a lot more like it!

 

As an alternative to setting the ColumnSpan/RowSpan properties on the items you could use the first of my additions to the original panel behavior to let the items define their own sizes. That can be done by setting the property LatchItemSize to False on the VariableSizedWrapGrid. When you do that each item will be sized independently. However, the item ordering algorithm of the VariableSizedWrapGrid sometimes calls for unused real estate to be discarded in the column/row we are currently filling. This leads to the following layout. 

Not really what we were expecting!

The reason for the strange layout is that the ColumnSpan property of the black square in the top left corner is 1 (the default value) defining the column width to be the same as the item width (200 DIU). In the previous example the ColumnSpan poroperty was 2, making the column width half the item width (100 DIU) thereby allowing the green square to be placed under the black but in this case it will be placed to the right of the black square instead.

The green square won't fit below the red so it will be moved to the next column to the right. The dark blue square is ordered after the green so it can't be positioned left of it leading to the real estate below the black to be discarded.

To the rescue comes the second innovation I made and added to the VariableSizedWrapGrid behavior. I find "gaps" in the layout unsightly but if you want to use a flexible layout they are hard to avoid with a stock VariableSizedWrapGrid. However, the order of the items can often be adjusted a bit to give a more appealing visual appearance. I added the property StrictItemOrder to the WPF VariableSizedWrapGrid to give the panel some freedom when arranging its items, by setting this property to False you simply allow the panel to rearrange the items to make better use of the available real estate.

When setting StrictItemOrder to False we get the following layout.

And this is what the XAML producing the layout above looks like.

    <ItemsControl Background="DarkGray">
        <ItemsControl.Style>
            <Style TargetType="{x:Type ItemsControl}">
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="{x:Type ItemsControl}">
                            <Border BorderBrush="{TemplateBinding BorderBrush}"
                                    BorderThickness="{TemplateBinding BorderThickness}"
                                    Background="{TemplateBinding Background}"
                                    Padding="{TemplateBinding Padding}"
                                    SnapsToDevicePixels="True">
                                <ScrollViewer CanContentScroll="True"
                                              HorizontalScrollBarVisibility="Auto" 
                                              VerticalScrollBarVisibility="Auto">
                                    <ItemsPresenter 
                                         SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                                </ScrollViewer>
                            </Border>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </ItemsControl.Style>
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <local:VariableSizedWrapGrid LatchItemSize="False" StrictItemOrder="False"/>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <Grid Height="200" Width="200" Background="Black"/>
        <Grid Height="100" Width="100" Background="Aqua"/>
        <Grid Height="100" Width="100" Background="Red"/>
        <Grid Height="200" Width="200" Background="Green"/>
        <Grid Height="100" Width="100" Background="Blue"/>
        <Grid Height="100" Width="100" Background="Red"/>
        <Grid Height="200" Width="200" Background="Green"/>
        <Grid Height="100" Width="100" Background="Blue"/>
        <Grid Height="100" Width="100" Background="Aqua"/>
        <Grid Height="100" Width="100" Background="Red"/>
        <Grid Height="100" Width="100" Background="Black"/>
    </ItemsControl>

That's about all I'm going to write about how to use of the panel, please let me know if you find information is missing and I'll try to update as needed.

The implementation of the example application is done in three classes: VariableSizedWrapGrid, MyGridView and MainWindow. I'll describe these starting with the creation of the sample data and then work my way through the control and end with a description of the panel itself.

The MainWindow code-behind is used to create an IEnumerable of an unnamed type containing properties for Color, Name, Index, ColSpan and RowSpan. This IEnumerable is then set as the DataContext of the MainWindow, each instance of the unnamed type making up a model. Note that the ColSpan and RowSpan properties are set based on the Index by the functions with the respective names.

using System.Linq;
using System.Windows;
using System.Windows.Media;

namespace WpfApplication1
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            var _Colors = typeof(Colors)
                           // using System.Reflection;
                           .GetProperties()
                           .Select((c, i) => new
                           {
                               Color = (Color)c.GetValue(null),
                               Name = c.Name,
                               Index = i,
                               ColSpan = ColSpan(i),
                               RowSpan = RowSpan(i)
                           });

            DataContext = _Colors;
        }

        private object RowSpan(int i)
        {
            if (i == 0)
                return 2;
            if (i == 2)
                return 3;
            if (i == 7)
                return 2;
            if (i == 14)
                return 2;
            return 1;
        }

        private object ColSpan(int i)
        {
            if (i == 0)
                return 2;
            if (i == 6)
                return 3;
            if (i == 14)
                return 2;
            return 1;
        }
    }
}

The next step is to transfer the RowSpan/ColSpan properties in the model objects to the corresponding attached properties (RowSpan/ColumnSpan) on the ItemContainer wrapping the model so the values can be picked up by the VariableSizedWrapGrid. This is done by a custom control called MyGridView.

using System.Windows;
using System.Windows.Controls;

namespace WpfApplication1
{
    public class MyGridView : ItemsControl
    {
        protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
        {
            dynamic model = item;
            try
            {
                element.SetValue(VariableSizedWrapGrid.ColumnSpanProperty, model.ColSpan);
                element.SetValue(VariableSizedWrapGrid.RowSpanProperty, model.RowSpan);
            }
            catch
            {
                element.SetValue(VariableSizedWrapGrid.ColumnSpanProperty, 1);
                element.SetValue(VariableSizedWrapGrid.RowSpanProperty, 1);
            }
            finally
            {
                element.SetValue(VerticalContentAlignmentProperty, VerticalAlignment.Stretch);
                element.SetValue(HorizontalContentAlignmentProperty, HorizontalAlignment.Stretch);
                base.PrepareContainerForItemOverride(element, item);
            }
        }
    }
}

MyGridView is simply an ItemsControl where the method PrepareContainerForItemOverride is overriden to transfer property values from the model to the corresponding attached property on the ItemContainer (element).

I should add that both the MainWindow code-behind and MyGridView were shamelessly stolen from Jerrys post.

Now for the main event: The VariableSizedGridView for WPF is derived from the Panel and implements the IScrollInfo interface to enable logical scrolling.

The major flow of control is as follows:

  • The parent control (typically a ScrollViewer) calls Measue on the VariableSizedGridView which in turn calls the overridden method MeasureOverride. This method runs in two passes; first pass measures all the children and determines the minimum size of the panel real estate. The second pass calculates the needed panel size based on the layout properties and the sizes of the children.
  • Panel size is calculated by calling the method PlaceElement for each of the panels children. This method examines the available real estate and selects a plot based on the plot size allocated for each element. The plot location along with the allocated size of each element is stored in _finalRects for later use by ArrangeOverride. The resulting Rect is also used to determine the total panel size. Once a plot is selected the area used by the element is reserved by a call to ReserveAcreage.
  • Once all children have been measured the parent control calls Arrange which in turn calls the overriden method ArrangeOverride. This method calls ArrangeElement for each child causing each element to be properly placed within the allocated plot using the child alignment properties HorizontalChildrenAlignment and VerticalChildrenAlignment. The current scrolling offset is added to each element to implement the logical scrolling.

Points of Interest

In the initial examples I'm using an ItemsControl with Grid children, this is a special case allowing the attached properties RowSpan and ColumnSpan to be set immediately on the model objects and still be picked up by the VariableSizedWrapGrid. Typically the ItemsControl uses a ContentPresenter as a container wrapping all children but if the child derives from UIElement it won't use a container at all and add the model object to the children collection instead. For the generic case another approach like for example the MyGridView implementation is necessary to transfer the information from the model to the container.

The ScrollViewer sometimes calls Measure twice with different availableSizes. I'm guessing that this is to examine the need for automatic scrollbars. At first that caused some jerking when scrolling under certain circumstances but moving the calls to SetHorizontalOffset and SetVerticalOffset from MeasureOverride to ArrangeOverride cleared that up.

History

2015-05-29 Some polishing after editor comments.
2015-05-28 First version of the article.

License

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

Share

About the Author

Magnus Rindeberg
Software Developer
Sweden Sweden
Magnus holds a MSc degree in Computer Science and Technology from Linköping University, Sweden.

He is curious about all things software and did his first hacks in Basic on a Texas TI-99/a at the tender age of 13, followed by bare-metal demo coding on Commodore 64 and the Amiga before moving on to software development for a living. Magnus has been working as a software developer/architect since 1994 on products ranging from OpenGL 3D graphics programming in C++ to web development using ASP but most of the time he spent developing embedded software for engine control systems.

His programming experience includes most mainstream programming languages and design patterns used on a multitude of platforms in a plethora of development environments. He just can't help exploring exciting new stuff professionally or as a hobby.

Magnus likes writing about himself in third-person singular.

You may also be interested in...

Pro
Pro

Comments and Discussions

 
GeneralWow...grat job Pin
damianom21-Jul-15 4:33
memberdamianom21-Jul-15 4:33 
GeneralRe: Wow...grat job Pin
Magnus Rindeberg21-Jul-15 8:14
professionalMagnus Rindeberg21-Jul-15 8:14 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.160826.1 | Last Updated 28 May 2015
Article Copyright 2015 by Magnus Rindeberg
Everything else Copyright © CodeProject, 1999-2016
Layout: fixed | fluid