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 (15) -

  • Joost van Schaik

    4/9/2012 5:08:51 AM |

    "ms-appx://" is wrong. It should be "ms-appx:/". Single slash. Apart from that, thanks, your article was about the only one that correctly showed how to bind a image Wink. And I really like your CAPTCHA.
    Cheerss!

  • Ricardo

    4/9/2012 9:26:02 AM |

    Hi Diederik, I really liked your post. Until now you're the only one with a correct solution to this problem.

    I'm having a problem though, when running in the simulator. When I rotate the simulator, the application crashes. I can´t determine the error in the callstack. Do you have any clue to why this is happening?

    Thanks!

  • tec-goblin

    4/23/2012 11:21:31 PM |

    Hi Diederik,
    This was a very helpful example. I still have some issues, though. I have a Grid as a DataTemplate, without any set Height or Width. It contains two rows, each one containing a TextBlock. The problem is that the length of the text of the second TextBlock is variable.
    In the PrepareContainerForItemOverride I can calculate the needed size and set the dependency properties for column and rowspan accordingly, not much problem there.
    What I want is the Grid in the DataTemplate to stretch to all the available area (in about the same way as you do with your images which do UniformToFill). But it doesn't seem to want to do so Frown. It's based on its contents' size.
    What's weird is that in other places the Grid does stretch as it should. So it's more like the ItemPanelTemplate (the VariableSizedWrapGrid) doing weird things.
    Here's my data template:
    <Grid Background="{Binding Brush}">
                                    <Grid.RowDefinitions>
                                        <RowDefinition Height="30"/>
                                        <RowDefinition Height="*"/>
                                    </Grid.RowDefinitions>
                                    <TextBlock HorizontalAlignment="Left" TextWrapping="NoWrap" Text="{Binding Orderable.ElementName}" Style="{StaticResource HeaderBaseTextStyle}" Margin="0" VerticalAlignment="Top"/>
                                    <TextBlock HorizontalAlignment="Left" TextWrapping="Wrap" Text="{Binding Orderable.CollapsedText}" Grid.Row="1" VerticalAlignment="Top" Style="{StaticResource BodyTextStyle}"/>
                                </Grid>
    Any help would be very appreciated!

  • Mikael Koskinen

    4/25/2012 5:23:35 PM |

    Thank for the post, very helpful.

    Unfortunately this doesn't seem to work if the GridView is grouped. Any ideas how to fix that?

  • tec-goblin

    4/25/2012 11:10:33 PM |

    Another interesting question is how to change these properties dynamically. Imagine we want a selected item to get a bigger column and rowspan. Unfortunately, PrepareContainerForItemOverride is not called automatically on selection. Have you any ideas on how to manually call it?
    (regarding my previous question, I will probably have to play with ItemContainerStyle)

  • tec-goblin

    4/27/2012 7:22:54 AM |

    I found out the solution to my first problem: while GridViewItem seems to be inaccessible for templating, we can change other properties in PrepareContainerForItemOverride. The following two lines make the contents stretch nicely:
    element.SetValue(GridViewItem.VerticalContentAlignmentProperty, VerticalAlignment.Stretch);
    element.SetValue(GridViewItem.HorizontalContentAlignmentProperty, HorizontalAlignment.Stretch);

    Still, the original solution has some issues. You mentioned "DataBinding". But, if I am not mistaken,
    element.SetValue(VariableSizedWrapGrid.ColumnSpanProperty, viewModel.Width);
    acts as a one-time binding. If viewModel.Width changes, the item is not resized automatically, it is notified for the change. The right way would for Microsoft to expose ItemContainerStyle in the same way as for ListView, so we could add our own bindings on the markup of GridViewItem, or replace element.SetValue with something like:
    var fe = (FrameworkElement)element;
    fe.SetBinding(VariableSizedWrapGrid.ColumnSpanProperty, new Binding { Source = "ColumnSpan" });
    I haven't tried it yet, but I definitely will, it's necessary for what I have in mind ;).

  • tec-goblin

    4/27/2012 7:42:21 AM |

    Ok, it's fe.SetBinding(VariableSizedWrapGrid.ColumnSpanProperty, new Binding { Path = new PropertyPath("ColumnSpan") });

  • Andrea Domenichini

    7/28/2012 10:03:05 PM |

    Hi,

    thank you very much, it's a great post!
    I'm trying to add reorder feature and to do that I set CanReorderItems="True" CanDragItems="True" AllowDrop="True".
    The items start dragging but do not drop.
    For a normal GridView I need only to set these properties.

    What about?

  • kzhu

    10/25/2012 9:36:53 PM |

    the code don,t run when have much Item

  • Benny Neugebauer

    6/30/2013 11:31:02 AM |

    This helped me a lot. Thank you so much!!

  • Robert Miller

    1/27/2014 12:13:22 PM |

    Fantastic stuff, really like those articles, keep it rolling Smile

  • Elliot Steir

    2/7/2014 1:58:27 AM |

    Found this on MSN and I’m happy I did. Well written article.

Comments are closed