Using Grids with ItemsControl in XAML

ItemsControl in XAML is a useful control that allows you to bind against multiple items and have them displayed using a repeating template (or templates). It forms the basis of the ListBox and other controls but can be used on its own where extra behaviour such as selection is not needed.

By default the ItemsControl uses a vertically aligned StackPanel as the layout panel for its items but you can override this by providing a template. It’s quite common to override this for example to use a VirtualizingStackPanel but in this instance I wanted to use a Grid for layout.

The Problem

I have a set of items that I want to arrange in a grid, I want the items to size to fill the available space after specifying the number of columns and rows I would like for display. This means I can’t use a WrapPanel but will ideally use a Grid.

The XAML for this would look something like this.

<ItemsControl ItemsSource="{Binding Data}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Grid/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Ellipse
                Grid.Row="{Binding X}"
                Grid.Column="{Binding Y}"
                Fill="Black"
                HorizontalAlignment="Stretch"
                VerticalAlignment="Stretch"/>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

This should let me have a collection called “Data” with items that have X and Y properties that refer to the grid coordinates. The problem is that this doesn’t work. To understand why we need to look at how the Grid and ItemsControl work.

The ContentPresenter

During layout the Grid will look for attached properties Grid.Row and Grid.Column on child elements to place them in the correct cell. However, it only checks immediate children, this is important due to how the ItemsControl works.

When displaying items the ItemsControl will wrap the supplied item and its DataTemplate (if it has one) in a ContentPresenter. This means that if you add Grid.Row and Grid.Column attributes to the item in the DataTemplate then the Grid will not find them since the immediate child is a ContentPresenter, not the item in the DataTemplate.

Dynamic Rows and Columns

The other challenge we have is that the number of rows and columns cannot be databound, we have to specify them explicitly using RowDefinitions and ColumnDefinitions.

The Solution

After a bit of poking around on the internet I found a few solutions to the problem but having worked with the ItemsControl in the past I knew that there was probably a simpler solution than some of the ones I found.

I subclassed the ItemsControl into a GridAwareItemsControl and implemented the GetContainerForItemOverride method. This is the method that normally creates the container for the item, i.e. the ContentPresenter.

In this method I check to see if there is an ItemTemplate (DataTemplate) and then check to see if the content of it has Grid.Row and Grid.Column set. If they do I copy the bindings from them to the ContentPresenter. This code assumes there are bindings and not hardcoded values but hardcoding the values wouldn’t make sense for this.

public class GridAwareItemsControl : ItemsControl
{
    protected override DependencyObject GetContainerForItemOverride()
    {
        ContentPresenter container = (ContentPresenter) base.GetContainerForItemOverride();
        if (ItemTemplate == null)
        {
            return container;
        }

        FrameworkElement content = (FrameworkElement)ItemTemplate.LoadContent();
        BindingExpression rowBinding = content.GetBindingExpression(Grid.RowProperty);
        BindingExpression columnBinding = content.GetBindingExpression(Grid.ColumnProperty);

        if (rowBinding != null)
        {
            container.SetBinding(Grid.RowProperty, rowBinding.ParentBinding);
        }

        if (columnBinding != null)
        {
            container.SetBinding(Grid.ColumnProperty, columnBinding.ParentBinding);
        }

        return container;
    }
}

The second part of the solution, dynamically creating the rows and columns, was solved using a couple of attached properties. These take int values and then programmatically create the RowDefinition and ColumnDefinition items on the Grid.

public class GridAutoLayout
{
    public static int GetNumberOfColumns(DependencyObject obj)
    {
        return (int)obj.GetValue(NumberOfColumnsProperty);
    }

    public static void SetNumberOfColumns(DependencyObject obj, int value)
    {
        obj.SetValue(NumberOfColumnsProperty, value);
    }

    // Using a DependencyProperty as the backing store for NumberOfColumns.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty NumberOfColumnsProperty =
        DependencyProperty.RegisterAttached("NumberOfColumns", typeof(int), typeof(GridAutoLayout), new PropertyMetadata(1, NumberOfColumnsUpdated));

    public static int GetNumberOfRows(DependencyObject obj)
    {
        return (int)obj.GetValue(NumberOfRowsProperty);
    }

    public static void SetNumberOfRows(DependencyObject obj, int value)
    {
        obj.SetValue(NumberOfRowsProperty, value);
    }

    // Using a DependencyProperty as the backing store for NumberOfRows.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty NumberOfRowsProperty =
        DependencyProperty.RegisterAttached("NumberOfRows", typeof(int), typeof(GridAutoLayout), new PropertyMetadata(1, NumberOfRowsUpdated));

    private static void NumberOfRowsUpdated(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        Grid grid = (Grid) d;

        grid.RowDefinitions.Clear();
        for (int i = 0; i < (int)e.NewValue; i++)
        {
            grid.RowDefinitions.Add(new RowDefinition(){Height = new GridLength(1, GridUnitType.Star)});
        }
    }

    private static void NumberOfColumnsUpdated(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        Grid grid = (Grid)d;

        grid.ColumnDefinitions.Clear();
        for (int i = 0; i < (int)e.NewValue; i++)
        {
            grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star) });
        }
    }
}

In all this results in XAML that looks like this and I think is a reasonably simple solution.

<controls:GridAwareItemsControl ItemsSource="{Binding Data}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Grid controls:GridAutoLayout.NumberOfRows="{Binding Rows}"
                  controls:GridAutoLayout.NumberOfColumns="{Binding Columns}">
            </Grid>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Ellipse
                Grid.Row="{Binding X}"
                Grid.Column="{Binding Y}"
                Fill="Black"
                HorizontalAlignment="Stretch"
                VerticalAlignment="Stretch"/>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</controls:GridAwareItemsControl>

8 thoughts on “Using Grids with ItemsControl in XAML

  1. Using Grids with ItemsControl in XAML:

    Is this for WPF? I tried using this and my grid does change the number of rows dynamically but the itemscontrol still does not seem to allow the Ellipse to fall into the grid.

    • Interesting, I only tested it with WinRT not WPF so it’s possible it doesn’t work correctly in that. I’ll have to take a look at it and see what’s going on there.

        • Not really had much time to work on this so I don’t have a solution for WP8 at this point. If I get chance at some point then I’ll take a look at it but I’m not sure when.

  2. How to make this example to select templates using ItemTemplateSelector instead of ItemsControl.ItemTemplate?

  3. May I ask to what object you are binding? Did you create a specific class that is both IEnumberable and exposes a Rows and Columns property?

    • There are two things being bound in this, the Rows and Columns for the Row/Column definitions are bound against properties in the view model that the main view uses as they inherit the DataContext from it’s parents.

      My main VM has 3 properties, Rows, Columns and Data. Data is an Observable collection of EllipseData child view models which have X and Y properties on it.

Comments are closed.