Click here to Skip to main content
Click here to Skip to main content

A better panel for data binding to a WrapPanel in WPF

By , 18 Jan 2009
 

img4.jpg

Introduction

This article presents an extension to the WPF WrapPanel control that infers aesthetically pleasing ItemWidth and ItemHeight properties. This inference is important in data-binding applications, when you may not know a good value for ItemWidth or ItemHeight to choose at design time.

Background

The System.Windows.Controls.WrapPanel control that comes with WPF positions its child elements sequentially from left to right (or top to bottom), breaking content to the next line at the edge of the containing box. Subsequent ordering happens sequentially from left to right (or top to bottom). The following is the output of using a WrapPanel to display a set of buttons:

img1.jpg

The code used to generate this is straightforward:

<WrapPanel>
    <Button Margin="5,5,5,5">Alpha</Button>
    <Button Margin="5,5,5,5">Bravo</Button>
       
        <!-- ... -->

    <Button Margin="5,5,5,5">Zebra</Button>
</WrapPanel>

The output looks ugly, since each button has a different size. To make the button size uniform, we can set the ItemWidth property:

<WrapPanel ItemWidth="69">
    <Button Margin="5,5,5,5">Alpha</Button>
    <Button Margin="5,5,5,5">Bravo</Button>
       
        <!-- ... -->

    <Button Margin="5,5,5,5">Zebra</Button>
</WrapPanel>

Now, we get a better looking output:

img2.jpg

Picking the right value for ItemWidth requires trial and error. The objective is to make the ItemWidth just big enough to fully display the child control with the largest desired width (the Yesterday Button in this case) without making it any bigger. In this example, if the size is too big, the buttons will look silly. If the size is too small, the text of one or more of the buttons will get cut off.

This problem gets harder when we introduce data binding. When we bind the children of the WrapPanel to a set of data with a corresponding set of data-bound controls, we may not know the maximum child control desired width at design time. To demonstrate this, I first factor the same set of names into the new class TextToDisplay, which I will use to data bind the set of button names:

public class TextToDisplay
{
    public TextToDisplay()
    {
        Text = new ObservableCollection<string>();
        Text.Add("Alpha");
        Text.Add("Bravo");

           //...

        Text.Add("Zebra");
    }

    public ObservableCollection<string> Text
    {
        get;
        private set;
    }
}

This new view code uses data biding to display the Text elements of the TextToDisplay class:

<Window 
    x:Class="UniformWrapPanelExample.WrapPanelWithDataBindingWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:sys="clr-namespace:System;assembly=mscorlib"
    xmlns:local="clr-namespace:UniformWrapPanelExample"
    Title="WrapPanelWithDataBindingWindow" Height="300" Width="300">
    <Window.Resources>
        <local:TextToDisplay x:Key="TextContainer"/>
    </Window.Resources>
    <ItemsControl
        ItemsSource="{Binding Text,
            Source={StaticResource TextContainer}}">
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <WrapPanel ItemWidth="69"/>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <ItemsControl.ItemTemplate>
            <DataTemplate DataType="{x:Type sys:String}">
                <Button
                    Margin="5,5,5,5"
                    Content="{Binding}"/>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</Window>

This new implementation allows someone to change the button collection by simply adding or removing strings from the Text property of the TextToDisplay object. The view code handles translating this string into a button automatically.

With data binding, we can no longer reliably use trial and error to guess the right ItemWidth value. Our assumptions about the text width of the buttons may be invalidated at run time. For example, if you guess that 50 is a good value for ItemWidth, you would end up with some words being cut off in the example set:

img3.jpg

To solve the guess work involved in choosing the right ItemWidth (or ItemHeight) value, I created the UniformWrapPanel. The UnfiormWrapPanel is just a WrapPanel, with a special property IsAutoUniform. When this property is true, the ItemWidth (or ItemHeight) will be automatically set to the largest desired width of the children before layout, causing all the items to receive an aesthetically pleasing uniform layout without any guesswork. The IsAutoUniformProperty is defined below:

public class UniformWrapPanel : WrapPanel
{
    public bool IsAutoUniform
    {
        get { return (bool)GetValue(IsAutoUniformProperty); }
        set { SetValue(IsAutoUniformChanged, value); }
    }

    public static readonly DependencyProperty
        IsAutoUniformProperty = DependencyProperty.Register(
        "IsAutoUniform", typeof(bool), typeof(UniformWrapPanel),
        new FrameworkPropertyMetadata(true, 
            new PropertyChangedCallback(IsAutoUniformChanged)));

    //...
    }
}

This property is true by default; setting it to false will make this panel behave like the standard WrapPanel. Because changing this value may affect layout, the PropertyChangedCallback IsAutoUniformChanged will cause the panel to recompute the layout of its elements whenever the IsAutoUniform property gets changed:

private static void IsAutoUniformChanged(DependencyObject sender,
        DependencyPropertyChangedEventArgs e)
{
    if (sender is UniformWrapPanel)
    {
        ((UniformWrapPanel)sender).InvalidateVisual();
    }
}

To produce the correct behavior, the UniformWrapPanel computes an appropriate ItemWidth (or ItemHeight) property, and then defers to the WrapPanel base class to complete the layout:

protected override Size MeasureOverride(Size availableSize)
{
    if (Children.Count > 0 && IsAutoUniform)
    {
        if (Orientation == Orientation.Horizontal)
        {
            double totalWidth = availableSize.Width;
            ItemWidth = 0.0;
            foreach (UIElement el in Children)
            {
                el.Measure(availableSize);
                Size next = el.DesiredSize;
                if (!(Double.IsInfinity(next.Width) ||
                    Double.IsNaN(next.Width)))
                {
                    ItemWidth = Math.Max(next.Width, ItemWidth);
                }
            }
        }
        else
        {
            double totalHeight = availableSize.Height;
            ItemHeight = 0.0;
            foreach (UIElement el in Children)
            {
                el.Measure(availableSize);
                Size next = el.DesiredSize;
                if (!(Double.IsInfinity(next.Height) ||
                    Double.IsNaN(next.Height)))
                {
                    ItemHeight = Math.Max(next.Height, ItemHeight);
                }
            }
        }
    }
    return base.MeasureOverride(availableSize);
}

Using the Code

To use this new panel, replace the ItemsPanel in the above data binding example with:

<ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
        <local:UniformWrapPanel/>
    </ItemsPanelTemplate>
</ItemsControl.ItemsPanel>

which produces the following output:

img4.jpg

Points of Interest

When I originally made this panel, I made it as an extension to Panel, rather than WrapPanel. In the original version, there was no ItemWidth or ItemHeight property, since these were inferred automatically. This seemed limiting, since it couldn't always be used in place of WrapPanel, so I switched to the above model. Sure enough, the code actually got simpler when I did this :)

History

Any future changes or improvements I make will later be posted here.

License

This article, along with any associated source code and files, is licensed under The MIT License

About the Author

adam.cataldo
United States United States
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralMy vote of 5membervinod.sankuthodi5 Mar '13 - 23:54 
exactly what I was looks for, thank you very much for nice explanation and code
GeneralUsefulmemberPhilip Lee10 Jan '12 - 20:20 
Adam,
 
a useful bit of code, and almost what I was looking for. I wanted the wrap panel items to uniformly fill the entire width of the panel. Here's a small update to your code:
 
    public enum ItemSize
    {
        None,
        Uniform,
        UniformStretchToFit
    }
 
    public class UniformWrapPanel : WrapPanel
    {
        public static readonly DependencyProperty ItemSizeProperty =
            DependencyProperty.Register(
                "ItemSize", 
                typeof (ItemSize), 
                typeof (UniformWrapPanel), 
                new FrameworkPropertyMetadata(
                    default(ItemSize),
                    FrameworkPropertyMetadataOptions.AffectsMeasure,
                    ItemSizeChanged));
 
        private static void ItemSizeChanged(
            DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            var uniformWrapPanel = sender as UniformWrapPanel;
            if (uniformWrapPanel != null)
            {
                if (uniformWrapPanel.Orientation == Orientation.Horizontal)
                {
                    uniformWrapPanel.ItemWidth = double.NaN;
                }
                else
                {
                    uniformWrapPanel.ItemHeight = double.NaN;
                }
            }
        }
 
        public ItemSize ItemSize
        {
            get { return (ItemSize) GetValue(ItemSizeProperty); }
            set { SetValue(ItemSizeProperty, value); }
        }
 
        protected override Size MeasureOverride(Size availableSize)
        {
            var mode = ItemSize;
 
            if (Children.Count > 0 && mode != ItemSize.None)
            {
                bool stretchToFit = mode == ItemSize.UniformStretchToFit;
 
                if (Orientation == Orientation.Horizontal)
                {
                    double totalWidth = availableSize.Width;
 
                    ItemWidth = 0.0;
                    foreach (UIElement el in Children)
                    {
                        el.Measure(availableSize);
                        Size next = el.DesiredSize;
                        if (!(Double.IsInfinity(next.Width) || Double.IsNaN(next.Width)))
                        {
                            ItemWidth = Math.Max(next.Width, ItemWidth);
                        }
                    }
 
                    if (stretchToFit)
                    {
                        if (!double.IsNaN(ItemWidth) && !double.IsInfinity(ItemWidth) && ItemWidth > 0)
                        {
                            var itemsPerRow = (int) (totalWidth/ItemWidth);
                            if (itemsPerRow > 0)
                            {
                                ItemWidth = totalWidth/itemsPerRow;
                            }
                        }
                    }
                }
                else
                {
                    double totalHeight = availableSize.Height;
 
                    ItemHeight = 0.0;
                    foreach (UIElement el in Children)
                    {
                        el.Measure(availableSize);
                        Size next = el.DesiredSize;
                        if (!(Double.IsInfinity(next.Height) || Double.IsNaN(next.Height)))
                        {
                            ItemHeight = Math.Max(next.Height, ItemHeight);
                        }
                    }
 
                    if (stretchToFit)
                    {
                        if (!double.IsNaN(ItemHeight) && !double.IsInfinity(ItemHeight) && ItemHeight > 0)
                        {
                            var itemsPerColumn = (int) (totalHeight/ItemHeight);
                            if (itemsPerColumn > 0)
                            {
                                ItemHeight = totalHeight/itemsPerColumn;
                            }
                        }
                    }
                }
            }
 
            return base.MeasureOverride(availableSize);
        }
    }
 
Regards,
Phil
Suggestionreally goog samplememberBlaiseBraye21 Dec '11 - 22:14 
directly adopted.
I made a small change for silverlight compatibility : use InvalidateMeasure method instead of InvalidateVisual method(this last one does not exist in silverlight)
 
private static void IsAutoUniformChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
   if (sender is UniformWrapPanel)
   {
      ((UniformWrapPanel)sender).InvalidateMeasure();
   }
}
Let's make code sharing our goal...
Blaise Braye

GeneralMy vote of 5memberBlaiseBraye21 Dec '11 - 22:09 
love it, so easy to reuse and perfect simple exemple about customizing panels layout
GeneralUsefulmemberAWdrius12 Aug '09 - 23:54 
Hi, a big 5 for the control, it is really useful and saved me some time (OK, OK, a lot of time (- ). It is surprising that default toolbox contains uniform grid but no uniform wrap panel. On the other hand it allows people like you to do it and at the same time to show others how to create something for their need.
 
Thanks and keep it rolling.
 
Trust is a weakness.

GeneralA Nice Articlememberdraiko22 Jan '09 - 22:28 
A nice and good written article.
I have one question.
Would be the same effect achievved if each item were embedded in a one-column grid
with SharedSizeGroup and the pannel were decorated with IsSharedSizeScope?
(Or i'm missing something?)
GeneralRe: A Nice ArticlememberTalyn25 Apr '09 - 10:16 
Yes, as soon as started reading this article I thought, wait, this does the exact same thing as SharedSizeScope does, which I've been using with WrapPanel for ages with only a few lines of XAML.
 
Here's an example of how to use it with a ListView:
 

<ListView
x:Name="SharedSizeScopeListView"
ItemsPanel="{StaticResource SharedSizeScopeHorizontalWrapPanelItemsPanelTemplate}"
ItemTemplate="{StaticResource SharedSizeGroupListViewItemTemplate}"
/>

 
Setup the WrapPanel with the Grid.IsSharedSizeScope attribute:

<ItemsPanelTemplate x:Key="SharedSizeScopeHorizontalWrapPanelItemsPanelTemplate">
<WrapPanel
Grid.IsSharedSizeScope="True"
Orientation="Horizontal"
HorizontalAlignment="{Binding Path=HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"
VerticalAlignment="{Binding Path=VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"
/>
</ItemsPanelTemplate>

 
The following item template defines that all items will have the same height and width as the highest and widest item in the WrapPanel. These values are dynamic so if an item grows with more content on lets say mouseover, so will all the other items in the wrappanel. The SharedSizeGroup value is any arbitrary string.

<DataTemplate x:Key="SharedSizeGroupListViewItemTemplate">
<Grid Width="Auto" Height="Auto" Margin="0,0,0,0" >
 
<Grid.RowDefinitions>
<RowDefinition SharedSizeGroup="SharedHeight" />
</Grid.RowDefinitions>
 
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="SharedWidth" />
</Grid.ColumnDefinitions>
 
<!-- My Content Goes Here />
</Grid>
</DataTemplate>

 
This works really well with grouping too. The following defines that only items within the same group have the same width and height:

<ListView

x:Name="SharedSizeScopeListView"
ItemsPanel="{StaticResource SharedSizeScopeHorizontalWrapPanelItemsPanelTemplate}"
ItemTemplate="{StaticResource SharedSizeGroupListViewItemTemplate}">
 
<ListView.GroupStyle>
<GroupStyle x:Name="SharedSizeScopeGroupStyle"
HeaderTemplate="{StaticResource SharedSizeScopeGroupHeaderTemplate}">

<GroupStyle.Panel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" CanHorizontallyScroll="False" />
</ItemsPanelTemplate>
</GroupStyle.Panel>
</GroupStyle>
</ListView.GroupStyle>
</ListView>

 

<DataTemplate x:Key="SharedSizeScopeGroupHeaderTemplate">
<Grid ShowGridLines="False"
SnapsToDevicePixels="True"
HorizontalAlignment="Stretch"
Focusable="False"
Margin="0,12,10,0"
>

<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
 
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
 

<!-- Header -->
<TextBlock Margin="10,0,3,7"
Grid.Row="0" Grid.Column="0"
Foreground="Black"
FontWeight="Bold"
TextAlignment="Left"
Text="{Binding Name}"
HorizontalAlignment="Left" />

<Border Grid.Row="0"
Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Margin="0,5,10,0"
SnapsToDevicePixels="True"
Height="2"
BorderThickness="0,2,0,0"
BorderBrush="Black"
/>
</Grid>
</DataTemplate>

GeneralRe: A Nice Articlememberadam.cataldo26 Apr '09 - 6:40 
It's great to know there is an easier way to do this. I'm amazed you were able to figure this out, since MS didn't document this at all. I must say, it seems unintuitive that an attached property of Grid could make an impact on WrapPanel, given that WrapPanel does not inherit from Grid. This makes me wonder what other great WPF features I'm missing out of because they're obscure in the documentation!
 
Thanks,
Adam
GeneralRe: A Nice ArticlememberJoe Gershgorin26 Apr '09 - 16:20 
I picked a better name than Talyn...
 
I have to thank chapter 18 of the Matthew MacDonald Pro WPF book. His book and Dr. WPF's series on itemscontrol covers the majority of what you can do with itemcontrols.
 
I can see the content your provided being potentially handy outside the specific usage shown.
 
Thanks for your contribution,
-Joe
GeneralGreat ideamvpJosh Smith19 Jan '09 - 3:22 
Nice article, too. Thanks for posting it. Got my 5.
 
:josh:
Try Crack![^]
Sleep is overrated.

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

Permalink | Advertise | Privacy | Mobile
Web04 | 2.6.130523.1 | Last Updated 18 Jan 2009
Article Copyright 2009 by adam.cataldo
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid