One of the common layouts that you see in Windows Store applications is one that looks a little like the one below;
If I’m data-binding to a set of items that I’ve slotted into my DataContext with something like;
this.DataContext = from i in Enumerable.Range(1, 100) select new { ItemSize = (i == 1) ? 2 : 1, Title = string.Format("Title {0}", i) };
then my first attempt to get this on the screen might be something like;
<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}"> <Grid.RowDefinitions> <RowDefinition Height="0.5*"/> <RowDefinition Height="*"/> <RowDefinition Height="0.5*"/> </Grid.RowDefinitions> <GridView Grid.Row="1" ItemsSource="{Binding}"> </GridView> </Grid>
and that’s going to give me something like;
and so I clearly need an ItemTemplate to display things properly;
<GridView Grid.Row="1" ItemsSource="{Binding}"> <GridView.ItemTemplate> <DataTemplate> <Grid Background="Blue"> <TextBlock Text="{Binding Title}" HorizontalAlignment="Center" VerticalAlignment="Center" TextAlignment="Center" /> </Grid> </DataTemplate> </GridView.ItemTemplate> </GridView>
which only gives me;
where clearly I’ve got some kind of horizontal/vertical alignment problem which might be fixed by altering the container that each item is slotted into;
<GridView Grid.Row="1" ItemsSource="{Binding}"> <GridView.ItemTemplate> <DataTemplate> <Grid Background="Blue"> <TextBlock Text="{Binding Title}" HorizontalAlignment="Center" VerticalAlignment="Center" TextAlignment="Center" /> </Grid> </DataTemplate> </GridView.ItemTemplate> <GridView.ItemContainerStyle> <Style TargetType="GridViewItem"> <Setter Property="HorizontalContentAlignment" Value="Stretch" /> <Setter Property="VerticalContentAlignment" Value="Stretch" /> </Style> </GridView.ItemContainerStyle> </GridView>
and that gives me a slightly better outcome;
but if I want my items to have different possibilities for width/height then I need to switch the ItemsPanel on my control to be a VariableSizedWrapGrid and that panel has properties called ItemWidth and ItemHeight which allows me to set how big I’d like the items;
<GridView Grid.Row="1" ItemsSource="{Binding}"> <GridView.ItemTemplate> <DataTemplate> <Grid Background="Blue"> <TextBlock Text="{Binding Title}" HorizontalAlignment="Center" VerticalAlignment="Center" TextAlignment="Center" /> </Grid> </DataTemplate> </GridView.ItemTemplate> <GridView.ItemContainerStyle> <Style TargetType="GridViewItem"> <Setter Property="HorizontalContentAlignment" Value="Stretch" /> <Setter Property="VerticalContentAlignment" Value="Stretch" /> </Style> </GridView.ItemContainerStyle> <GridView.ItemsPanel> <ItemsPanelTemplate> <VariableSizedWrapGrid ItemHeight="128" ItemWidth="128" /> </ItemsPanelTemplate> </GridView.ItemsPanel> </GridView>
which then gives me;
but how would I get an item to display itself at a different size to any other item? The VariableSizedWrapGrid defines attached properties called ColumnSpan and RowSpan whereby I can define multiples of the ItemHeight and ItemWidth for it to use. Ideally then I’d be able to modify my ItemContainerStyle to be something like;
<GridView.ItemContainerStyle> <Style TargetType="GridViewItem"> <Setter Property="HorizontalContentAlignment" Value="Stretch" /> <Setter Property="VerticalContentAlignment" Value="Stretch" /> <Setter Property="VariableSizedWrapGrid.RowSpan" Value="{Binding ItemSize}" /> <Setter Property="VariableSizedWrapGrid.ColumnSpan" Value="{Binding ItemSize}" /> </Style> </GridView.ItemContainerStyle>
but that doesn’t work. What’s going on? As far as I know, this kind of binding within a style isn’t going to work in a Windows Store app. See this post for reference. That’s a shame as I seem to remember that this is something that was present in Silverlight 5.
Regardless, we can still try and effect things from a code perspective by deriving from GridView and overriding the PrepareContainerForItemOverride function. Here’s a simple version that suits my purposes;
class SpecificGridView : GridView { protected override void PrepareContainerForItemOverride( DependencyObject element, object item) { // being lazy because I bound to an anonymous data type dynamic lateBoundItem = item; int sizeFactor = (int)lateBoundItem.ItemSize; element.SetValue(VariableSizedWrapGrid.ColumnSpanProperty, sizeFactor); element.SetValue(VariableSizedWrapGrid.RowSpanProperty, sizeFactor); base.PrepareContainerForItemOverride(element, item); } }
and if I replace my GridView with a SpecificGridView;
<local:SpecificGridView Grid.Row="1" ItemsSource="{Binding}"> <local:SpecificGridView.ItemTemplate> <DataTemplate> <Grid Background="Blue"> <TextBlock Text="{Binding Title}" HorizontalAlignment="Center" VerticalAlignment="Center" TextAlignment="Center" /> </Grid> </DataTemplate> </local:SpecificGridView.ItemTemplate> <local:SpecificGridView.ItemContainerStyle> <Style TargetType="GridViewItem"> <Setter Property="HorizontalContentAlignment" Value="Stretch" /> <Setter Property="VerticalContentAlignment" Value="Stretch" /> <Setter Property="VariableSizedWrapGrid.RowSpan" Value="{Binding ItemSize}" /> <Setter Property="VariableSizedWrapGrid.ColumnSpan" Value="{Binding ItemSize}" /> </Style> </local:SpecificGridView.ItemContainerStyle> <local:SpecificGridView.ItemsPanel> <ItemsPanelTemplate> <VariableSizedWrapGrid ItemHeight="128" ItemWidth="128" /> </ItemsPanelTemplate> </local:SpecificGridView.ItemsPanel> </local:SpecificGridView>
then I get a display;
It’s a bit disappointing that it’s not possible to get to this stage without resorting to some code but it’s not the end of the world. If I changed my data a little;
this.DataContext = from i in Enumerable.Range(1, 100) select new { ItemSize = (i % 4 == 1) ? 2 : 1, Title = string.Format("Title {0}", i) };
then I get a more mixed display;
The only thing that I don’t really like about this is that it feels like I have to set specific sizes and it’s not at all clear how that would work at different screen sizes. Specifically, my GridView is actually sized as highlighted by the red background below;
so what if I wanted my large items to take all the vertical space and the small items to take 50% (approximately) of the vertical space? What if I don’t want to explicitly set ItemHeight and ItemWidth to hard-code values that won’t work for me on larger screens?
I played with this for quite a while and found it to be harder than I’d initially expected it to be and I found a few of the solutions that I wanted to try with binding didn’t seem to be bindable and in at least one place I wanted to derive from VariableSizedWrapGrid but that was sealed so that I couldn’t do that either.
The simplest thing I managed to come up with was to tweak what I already had such that rather than setting the ItemHeight and ItemWidth on the VariableSizedWrapGrid declaratively, I instead wait until that control loads and then try to figure it out dynamically and attempt to subscribe to future changes such that I can change the item height and width again to accommodate.
That is – I handle the Loaded event on the VariableSizedWrapGrid that I’m using as my ItemsPanel with;
void GridLoaded(object sender, RoutedEventArgs args) { VariableSizedWrapGrid g = (VariableSizedWrapGrid)sender; SizeChangedEventHandler handler = (s, e) => { g.ItemHeight = (g.ActualHeight) / 2.0; g.ItemWidth = g.ItemHeight; }; handler(null, null); g.SizeChanged += handler; }
and that seemed to produce the layout that I’d wanted. Naturally, the 2.0 could be a property that I add to my SpecificGridView class that I’ve derived from GridView.
I daresay that there’s a better way but I haven’t figured it out quite yet