Diederik Krols

The XAML Brewer

Databinding to the VariableSizedWrapGrid in Windows 8 Metro

This article describes how to implement data binding and using variable sized cells in a VariableSizedWrapGrid control in a Windows 8 Metro application, Customer Preview release. The VariableSizedWrapGrid control exposes the ColumnSpan and RowSpan attached properties. These allow to specify the number of columns and rows an element should span. He're how the sample project looks like:

According to the official documentation the ColumnSpan and RowSpan values are interpreted by the most immediate parent VariableSizedWrapGrid element from where the value is set. So let's say we have a Thing class (I couldn't find a better name) with a Width property that contains the relative width (i.e. the number of columns to span). Our viewmodel exposes a list of Things that we want to show, and allows binding to the selected Thing:

public class MainPageViewModel : BindableBase
{
    public List<Thing> Things
    {
        get
        {
            List<Thing> results = new List<Thing>();

            results.Add(new Thing() { Name = "Beer", Height = 1, Width = 1, ImagePath = @"/Assets/Images/beer.jpg" });
            results.Add(new Thing() { Name = "Hops", Height = 2, Width = 2, ImagePath = @"/Assets/Images/hops.jpg" });
            results.Add(new Thing() { Name = "Malt", Height = 1, Width = 2, ImagePath = @"/Assets/Images/malt.jpg" });
            results.Add(new Thing() { Name = "Water", Height = 1, Width = 2, ImagePath = @"/Assets/Images/water.jpg" });
            results.Add(new Thing() { Name = "Yeast", Height = 1, Width = 1, ImagePath = @"/Assets/Images/yeast.jpg" });
            results.Add(new Thing() { Name = "Sugar", Height = 2, Width = 2, ImagePath = @"/Assets/Images/sugars.jpg" });
            results.Add(new Thing() { Name = "Herbs", Height = 2, Width = 1, ImagePath = @"/Assets/Images/herbs.jpg" });
            // And so on, and so forth ...

            return results;
        }
    }

    private Thing selectedThing;

    public Thing SelectedThing
    {
        get { return selectedThing; }
        set { this.SetProperty(ref selectedThing, value); }
    }
}

The viewmodel derives from BindableBase, a helper class provided by the new Visual Studio templates that implements property changed notification using the new C# 5 CallerMemberName attribute. The viewmodel is attached to the view in XAML. I'm glad that this finally works! In the Developer Preview we needed to do this in the code behind:

<Page.DataContext>
    <local:MainPageViewModel />
</Page.DataContext>

Now the VariableSizedWrapGrid control is just a Panel, not an ItemsControl, so in a databound scenario we also need a GridView. In theory the next configuration would work. We simply attach the properties to the item data template of the GridView:

<GridView ItemsSource="{Binding Things}">
    <GridView.ItemTemplate>
        <DataTemplate>
            <Grid VariableSizedWrapGrid.ColumnSpan="{Binding Width}"  >
                <!-- Insert Item Data Template Details Here -->
               <!-- ... -->
            </Grid>
        </DataTemplate>
    </GridView.ItemTemplate>
    <GridView.ItemsPanel>
        <ItemsPanelTemplate>
            <VariableSizedWrapGrid />
        </ItemsPanelTemplate>
    <GridView.ItemsPanel>
</GridView>

This does not work. When data binding, each item is wrapped in a GridViewItem control, which apparently swallows the attached properties. So far for the theory. In practice, the only way to make the scenario work is to create a GridView child class and override the PrepareContainerForItemOverride method.

The control class would look like this:

public class VariableGridView : GridView
{
    protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
    {
        var viewModel = item as Thing

        element.SetValue(VariableSizedWrapGrid.ColumnSpanProperty, viewModel.Width);
        element.SetValue(VariableSizedWrapGrid.RowSpanProperty, viewModel.Height);

        base.PrepareContainerForItemOverride(element, item);
    }
}

Of course we're not going to build a specialized GridView class for each model in our application, so it makes sense to define an interface:

public interface IResizable
{
    int Width { get; set; }
    int Height { get; set; }
}

Here's a reusable version of the GridView child. It only depends on the interface:

public class VariableGridView : GridView
{
    protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
    {
        var viewModel = item as IResizable;

        element.SetValue(VariableSizedWrapGrid.ColumnSpanProperty, viewModel.Width);
        element.SetValue(VariableSizedWrapGrid.RowSpanProperty, viewModel.Height);

        base.PrepareContainerForItemOverride(element, item);
    }
}

And here's the Thing, the whole Thing, and nothing but the Thing:

public class Thing : IResizable
{
    public string Name { get; set; }
    public string ImagePath { get; set; }
    public int Width { get; set; }
    public int Height { get; set; }

    public ImageSource Image
    {
        get
        {
            return new BitmapImage(new Uri("ms-appx://" + this.ImagePath));
        }
    }
}

In the main page, we just replace GridView by the custom class:

<local:VariableGridView ItemsSource="{Binding Things}">
    <local:VariableGridView.ItemTemplate>
        <DataTemplate>
            <Grid>
                <!-- Insert Item Data Template Details Here -->
               <!-- ... -->
            </Grid>
        </DataTemplate>
    </local:VariableGridView.ItemTemplate>
    <local:VariableGridView.ItemsPanel>
        <ItemsPanelTemplate>
            <VariableSizedWrapGrid />
        </ItemsPanelTemplate>
    <local:VariableGridView.ItemsPanel>
</local:VariableGridView>

Tadaa !

Here's the source code. It was written with Visual Studio Express 11 Beta, for the Windows 8 Consumer Preview: U2UConsult.Metro.VariableSizedWrapGridSample.zip (951,87 kb)

Enjoy !

 

Comments are closed