Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A better panel for data binding to a WrapPanel in WPF

0.00/5 (No votes)
18 Jan 2009 1  
An article on a useful extension to the WrapPanel control.

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 has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here