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

 
Hint: For improved responsiveness ensure Javascript is enabled and choose 'Normal' from the Layout dropdown and hit 'Update'.
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 
GeneralUsefulmemberPhilip Lee10 Jan '12 - 20:20 
Suggestionreally goog samplememberBlaiseBraye21 Dec '11 - 22:14 
GeneralMy vote of 5memberBlaiseBraye21 Dec '11 - 22:09 
GeneralUsefulmemberAWdrius12 Aug '09 - 23:54 
GeneralA Nice Articlememberdraiko22 Jan '09 - 22:28 
GeneralRe: A Nice ArticlememberTalyn25 Apr '09 - 10:16 
GeneralRe: A Nice Articlememberadam.cataldo26 Apr '09 - 6:40 
GeneralRe: A Nice ArticlememberJoe Gershgorin26 Apr '09 - 16:20 
GeneralGreat ideamvpJosh Smith19 Jan '09 - 3:22 
GeneralRe: Great ideamemberadam.cataldo19 Jan '09 - 4:55 
GeneralRe: Great ideamvpJosh Smith19 Jan '09 - 4:56 
GeneralI cannot download anythingmemberEgidio18 Jan '09 - 2:45 
GeneralRe: I cannot download anythingmemberadam.cataldo18 Jan '09 - 4:56 
GeneralRe: I cannot download anythingmemberspringy7619 Jan '09 - 0:35 
GeneralRe: I cannot download anythingmemberadam.cataldo19 Jan '09 - 4:51 
GeneralRe: I cannot download anythingmemberspringy7619 Jan '09 - 5:19 
GeneralRe: I cannot download anythingmemberadam.cataldo19 Jan '09 - 5:31 
GeneralRe: I cannot download anythingmemberadam.cataldo19 Jan '09 - 5:36 
AnswerRe: I cannot download anythingmemberspringy7619 Jan '09 - 22:20 
GeneralRe: I cannot download anythingmemberadam.cataldo20 Jan '09 - 4:49 
GeneralMy take on thismvpSacha Barber18 Jan '09 - 0:24 
GeneralRe: My take on thismemberadam.cataldo18 Jan '09 - 4:47 
GeneralRe: My take on thismvpSacha Barber18 Jan '09 - 8:00 

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

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