Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / XAML

Extending GridView with Drag and Drop for Grouping and Variable Sized Items

4.98/5 (23 votes)
9 Oct 2015CPOL11 min read 139.5K   8K  
This article describes the implementation of an extended GridView control that enables drag and drop with grouping and variable sized items.

Introduction

The GridView is a great control that can be used in many different ways to display tiled content in your Windows Store apps. If you’ve looked at any WinRT applications lately, or even Microsoft Partner websites, you’ll recognize the popularity of tiles when it comes to UI design in the Windows World. Tiles provide a simple, sleek way to organize a list of items or navigation areas of your application. Perhaps the greatest example of tiled content is the Windows 8 start screen itself. It displays each of your apps in a sizable tile that can be rearranged and grouped to the users’ desire.

As is typical with native applications, we developers want to emulate the same experience within our own applications. This imitation goes back to the early days of Windows and has been a consistent approach to user interfaces. If you’re trying to emulate the Windows 8 start screen in your own Windows Store application, the GridView control is a great place to start.

The GridView can display variable sized tiles and group them for you, or it can display non-grouped items of the same size with support for drag and drop. Unfortunately, you can’t have everything enabled by default. For instance, you don’t get drag and drop for all items panels. Certain items panels are necessary if you want a mixture of different sized items (i.e., VariableSizedWrapGrid). Also drag and drop is not supported when grouping is enabled.

This article describes the implementation of an extended GridView control, GridViewEx, which removes these limitations. The sample provided enables you to deliver drag and drop in a GridView that has support for grouping and variable sized items.

If you are developing for Universal Windows Platform (UWP) under Windows 10, please use updated version from the new article How to Upgrade Extended GridView from WinRT to Universal Windows Platform (UWP)

Background

First, let’s see how we can enable drag and drop in the simplest scenario. Here, we have a GridView with minimal properties set and a very basic ItemTemplate. To enable drag-and-drop reordering, you need to do three things:

  1. Set the AllowDrop property to true.
  2. Set the CanReorderItems property to true.
  3. Bind to a data source that supports data modification, or specifically reordering. For example, you could use something like ObservableCollection or IList (Note: Unbound GridViews also support reordering).
XML
<GridView ItemsSource="{Binding}" AllowDrop="True" CanReorderItems="True">
    <GridView.ItemTemplate>
        <DataTemplate>
            <Border BorderBrush="Aqua" BorderThickness="1" Background="Peru">
                <Grid Margin="12">
                    <Grid.RowDefinitions>
                        <RowDefinition />
                        <RowDefinition Height="Auto"/>
                    </Grid.RowDefinitions>
                    <TextBlock Text="{Binding}"/>
                    <TextBlock Grid.Row="1">item</TextBlock>
                </Grid>
            </Border>
        </DataTemplate>
    </GridView.ItemTemplate>
</GridView>

You will notice that very easily we have some level of drag-and-drop support in our GridView.

Image 1

As mentioned earlier, there are a couple of major limitations to enabling drag and drop for both bound and unbound scenarios. Specifically, you can’t have grouping enabled or a mix of variable sized items. If you look at the Windows 8 Start Screen, you will notice that there is grouping, differently sized items, and drag and drop. If you’re really trying to emulate this experience, you will want two or three of these features combined. How can we implement all of these features in a GridView? We will need to extend the control to support these other scenarios. Now let’s take a look at the GridViewEx control.

The GridViewEx Control

The GridViewEx control implements drag and drop for cases which are not supported by the regular GridView control:

  • For items panels other than WrapGrid, StackPanel, and VirtualizingStackPanel
  • When grouping is enabled

It also allows adding new groups to the underlying data source if the user drags some item to the left-most or right-most edges of the control.

Dragging Code

Let’s look at the control implementation and how we handle dragging items.

C#
public class GridViewEx : GridView
{
    /// <summary>
    /// Initializes a new instance of the <see cref="GridViewEx"/> control.
    /// </summary>
    public GridViewEx()
    {
        // see attached sample
    }
 
    private void GridViewEx_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
    {
        // see attached sample
    }
 
    /// <summary>
    /// Stores dragged items into DragEventArgs.Data.Properties["Items"] value.
    /// Override this method to set custom drag data if you need to.
    /// </summary>
    protected virtual void OnDragStarting(DragItemsStartingEventArgs e)
    {
        // see attached sample
    }

The control has several fields which store the indices of several active items during the drag/drop process. The OnDragStarting event stores dragged items into the DragEventArgs.Data.Properties[“Items”] value. You would override this method to set custom drag data if you need to.

When the user drags an item, we need to show hints as to where the item will be placed if dropped. The standard GridView handles this by sliding adjacent items out of the way. We will implement this exact behavior ourselves in GridViewEx because we need to account for cases where GridView does not support dropping.

C#
/// <summary>
/// Shows reoder hints while custom dragging.
/// </summary>
protected override void OnDragOver(DragEventArgs e)
{
    // see attached sample }

private int GetDragOverIndex(DragEventArgs e)
{
    // see attached sample 
}

OnDragOver applies reorder hints when an item is dragged over neighboring items. The neighboring items are calculated from the GetIntersectingItems method. There are five possible ReorderHintStates to set depending on the location of each item:

  • NoReorderHint
  • BottomReorderHint
  • TopReorderHint
  • RightReorderHint
  • LeftReorderHint

Dropping Code

Next, let’s look at the code that handles dropping.

We have to override GridView.OnDrop method which is called every time when an end-user drops an item to the new location. Our override handles dropping for any ItemsPanel that the standard GridView does not support dropping.

C#
/// <summary>
/// Handles drag and drop for cases when it is not supported by the Windows.UI.Xaml.Controls.GridView control
/// </summary>
protected override async void OnDrop(DragEventArgs e)
{
    // see attached sample
} 

The OnDrop method includes logic for moving items from one group to another when grouping is enabled, and for new group creation if it is requested by end-user actions.

Adding New Groups

The GridView supports grouping if it is bound to the CollectionViewSource with the IsSourceGrouped property set to true. That means that the grouping logic should be implemented on the data source level and GridView has no access to it. Here, we see that to add new groups during the drag-and-drop operation, we need something more than the standard Drop event. The GridViewEx.BeforeDrop event allows us to handle this situation and supplies more information including the original DragEventArgs data.

The BeforeDrop event occurs before the user performs a drop operation.

C#
/// <summary>
/// Occurs before performing drop operation,
/// </summary>
public event EventHandler<BeforeDropItemsEventArgs> BeforeDrop;
/// <summary>
/// Rises the <see cref="BeforeDrop"/> event.
/// </summary>
/// <param name="e">Event data for the event.</param>
protected virtual void OnBeforeDrop(BeforeDropItemsEventArgs e)
{
    // see attached sample 
} 

The BeforeDropItemEventArgs carries important information about the item being dragged so that it can be accessed later in the OnDrop event.

C#
/// <summary>
/// Provides data for the <see cref="GridViewEx.BeforeDrop"/> event.
/// </summary>
public sealed class BeforeDropItemsEventArgs : System.ComponentModel.CancelEventArgs
{
    /// <summary>
    /// Gets the item which is being dragged.
    /// </summary>
    public object Item
    {
        get;
    }
    /// <summary>
    /// Gets the current item index in the underlying data source.
    /// </summary>
    public int OldIndex
    {
        get;
    }
    /// <summary>
    /// Gets the index in the underlying data source where
    /// the item will be inserted by the drop operation.
    /// </summary>
    public int NewIndex
    {
        get;
    }
    /// <summary>
    /// Gets the bool value determining whether end-user actions requested
    /// creation of the new group in the underlying data source.
    /// This property only makes sense if GridViewEx.IsGrouping property is true.
    /// </summary>
    /// <remarks>
    /// If this property is true, create the new data group and insert it into
    /// the groups collection at the positions, specified by the 
    /// <see cref="BeforeDropItemsEventArgs.NewGroupIndex"/> property value.
    /// Then the <see cref="GridViewEx"/> will insert dragged item
    /// into the newly added group.
    /// </remarks>
    public bool RequestCreateNewGroup
    {
        get;
    }
    /// <summary>
    /// Gets the current item data group index in the underlying data source.
    /// This property only makes sense if GridViewEx.IsGrouping property is true.
    /// </summary>
    public int OldGroupIndex
    {
        get;
    }
    /// <summary>
    /// Gets the data group index in the underlying data source
    /// where the item will be inserted by the drop operation.
    /// This property only makes sense if GridViewEx.IsGrouping property is true.
    /// </summary>
    public int NewGroupIndex
    {
        get;
    }
    /// <summary>
    /// Gets the original <see cref="DragEventArgs"/> data. 
    /// </summary>
    public DragEventArgs DragEventArgs
    {
        get;
    }
} 

The AllowNewGroup property determines whether new groups should be created if the user drags an item to the far edges of the control. This feature is not supported in the standard GridView under any scenario so it’s a nice added benefit of the GridViewEx class.

C#
/// <summary>
/// Gets or sets the value determining whether new group should be created at 
/// dragging the item to the empty space.
/// </summary>
public bool AllowNewGroup
{
    get { return (bool)GetValue(AllowNewGroupProperty); }
    set { SetValue(AllowNewGroupProperty, value); }
}
 
/// <summary>
/// Identifies the <see cref="AllowNewGroup"/> dependency property.
/// </summary>
public static readonly DependencyProperty AllowNewGroupProperty =
        DependencyProperty.Register("AllowNewGroup", typeof(bool), 
        typeof(GridViewEx), new PropertyMetadata(false));

To allow new group creation with drag-and-drop operations, you should set the AllowNewGroup property to true. To handle adding new groups to the data layer, you should handle the GridViewEx.BeforeDrop event. The event arguments help determine the item’s origin and destination. Within the BeforeDrop event handler, you can create the new data group and insert it into the group’s collection at the position specified by the argument’s NewGroupIndex property.

The last thing necessary for adding the new group feature is extending the default GridView control template. We need a filler or placeholder, where the user can drag an item to create a new group. The GridViewEx control template supports adding new groups if the user drags some item to the left-most or right-most edge of control. So, two border elements on either end of the ItemsPresenter are placeholders for the new groups.

The GridViewEx control template from generic.xaml.

XML
<Style TargetType="local:GridViewEx">
    <Setter Property="Padding" Value="0,0,0,10" />
    <Setter Property="IsTabStop" Value="False" />
    <Setter Property="TabNavigation" Value="Once" />
    <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
    <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Disabled"/>
    <Setter Property="ScrollViewer.HorizontalScrollMode" Value="Enabled" />
    <Setter Property="ScrollViewer.IsHorizontalRailEnabled" Value="False" />
    <Setter Property="ScrollViewer.VerticalScrollMode" Value="Disabled" />
    <Setter Property="ScrollViewer.IsVerticalRailEnabled" Value="False" />
    <Setter Property="ScrollViewer.ZoomMode" Value="Disabled" />
    <Setter Property="ScrollViewer.IsDeferredScrollingEnabled" Value="False" />
    <Setter Property="ScrollViewer.BringIntoViewOnFocusChange" Value="True" />
    <Setter Property="IsSwipeEnabled" Value="True" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:GridViewEx">
                <Border BorderBrush="{TemplateBinding BorderBrush}"
                        Background="{TemplateBinding Background}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                    <ScrollViewer x:Name="ScrollViewer"
                            TabNavigation="{TemplateBinding TabNavigation}"
                            HorizontalScrollMode="
                            {TemplateBinding ScrollViewer.HorizontalScrollMode}"
                            HorizontalScrollBarVisibility=
                              "{TemplateBinding 
                              ScrollViewer.HorizontalScrollBarVisibility}"
                            IsHorizontalScrollChainingEnabled=
                              "{TemplateBinding 
                              ScrollViewer.IsHorizontalScrollChainingEnabled}"
                            VerticalScrollMode="
                            {TemplateBinding ScrollViewer.VerticalScrollMode}"
                            VerticalScrollBarVisibility=
                              "{TemplateBinding 
                              ScrollViewer.VerticalScrollBarVisibility}"
                            IsVerticalScrollChainingEnabled=
                              "{TemplateBinding 
                              ScrollViewer.IsVerticalScrollChainingEnabled}"
                            IsHorizontalRailEnabled="
                            {TemplateBinding ScrollViewer.IsHorizontalRailEnabled}"
                            IsVerticalRailEnabled="
                            {TemplateBinding ScrollViewer.IsVerticalRailEnabled}"
                            ZoomMode="{TemplateBinding 
                            ScrollViewer.ZoomMode}"
                            IsDeferredScrollingEnabled="
                            {TemplateBinding ScrollViewer.IsDeferredScrollingEnabled}"
                            BringIntoViewOnFocusChange="
                            {TemplateBinding ScrollViewer.BringIntoViewOnFocusChange}">
                        <StackPanel Orientation="Horizontal">
                            <Border Width="60" 
                            x:Name="NewGroupPlaceHolderFirst" 
                                    Background="Transparent" 
                                    Padding="{TemplateBinding Padding}" 
                                    Visibility="{Binding AllowNewGroup, 
                                    Converter={StaticResource 
                                      VisibilityConverter}, 
                                      RelativeSource={RelativeSource TemplatedParent}}"/>
                            <ItemsPresenter 
                                Header="{TemplateBinding Header}" 
                                HeaderTemplate="{TemplateBinding HeaderTemplate}"
                                HeaderTransitions="{TemplateBinding HeaderTransitions}"
                                Padding="{TemplateBinding Padding}"/>
                            <Border Width="60" 
                            x:Name="NewGroupPlaceHolderLast" 
                                    Background="Transparent" 
                                    Padding="{TemplateBinding Padding}" 
                                    Visibility="{Binding AllowNewGroup, 
                                    Converter={StaticResource 
                                      VisibilityConverter}, 
                                      RelativeSource={RelativeSource TemplatedParent}}"/>
                        </StackPanel>
                    </ScrollViewer>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

These new template parts should be defined in our code too.

C#
[TemplatePart(Name = GridViewEx.NewGroupPlaceHolderFirstName, Type = typeof(FrameworkElement))]
[TemplatePart(Name = GridViewEx.NewGroupPlaceHolderLastName, Type = typeof(FrameworkElement))]
public class GridViewEx : GridView
{
    private const string NewGroupPlaceHolderFirstName = "NewGroupPlaceHolderFirst";
    private FrameworkElement _newGroupPlaceHolderFirst;
 
    private const string NewGroupPlaceHolderLastName = "NewGroupPlaceHolderLast";
    private FrameworkElement _newGroupPlaceHolderLast;
 
    protected override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        _newGroupPlaceHolderFirst = 
          GetTemplateChild(NewGroupPlaceHolderFirstName) as FrameworkElement;
        _newGroupPlaceHolderLast = 
          GetTemplateChild(NewGroupPlaceHolderLastName) as FrameworkElement;
    }

Using and Extending GridViewEx

Right now, we have a drag-and-drop GridView solution that at first appears identical to the standard GridView. Our goal is for it to behave more like the Windows 8 start screen. Let’s discuss how to implement the following features that would otherwise be unsupported:

  • Variable Sized Items
  • Grouping
  • Adding new groups
  • Saving layout across sessions

Variable Sized Items

The Windows 8 start screen shows tiles of various sizes (well, two sizes exactly). If you try to make items of different sizes in a default GridView or GridViewEx, it won’t work. That’s because the GridView uses a WrapGrid as its default ItemsPanel. WrapGrid creates a uniform layout where each item is of the same size. For that reason, Microsoft also includes a VariableSizedWrapGrid, which as the name implies, supports items of different sizes.

The benefit of the GridViewEx control is that you can use VariableSizedWrapGrid and still retain support for drag and drop. To use VariableSizedWrap grid and display items of various sizes, you must do two things:

  1. Set the GridViewEx.ItemsPanel to an instance of VariableSizedWrapGrid.
  2. Override the PrepareContainerForItemOverride method on the GridView. In this method, you set the RowSpan or ColumnSpan properties on the items to indicate its size.

This means we need to extend the GridViewEx control with another control named MyGridView. Why extend GridViewEx rather than simply override PrepareContainerForItemOverride within the GridViewEx class? Because the logic for specifying item size should be in your data model and not within the control itself. That is, if you want to leave GridViewEx as a versatile control for usage in more than one place.

For instance, say you want specific items to appear larger, you would create a property on your data item that returns an integer value higher than 1 and use that to set the RowSpan or ColumnSpan property.

C#
public class Item
{
    public int Id { get; set; }
    public int ItemSize { get; set; }
    /* */
}

Here, when we create each item, we will specify the ItemSize to be either 1 or 2 indicating regular (1) or larger (2). Larger items will have its ColumnSpan property set to two so it occupies the space of two items horizontally. You can also set the RowSpan to make items larger vertically as well.

C#
/// <summary>
/// This class sets VariableSizedWrapGrid.ColumnSpanProperty for GridViewItem controls, 
/// so that every item can have different size in the VariableSizedWrapGrid.
/// </summary>
public class MyGridView : GridViewSamples.Controls.GridViewEx
{
    // set ColumnSpan according to the business logic
    // (maybe some GridViewSamples.Samples.Item or group properties)
    protected override void PrepareContainerForItemOverride(
              Windows.UI.Xaml.DependencyObject element, object item)
    {
        try
        {
            GridViewSamples.Samples.Item it = item as GridViewSamples.Samples.Item;
            if (it != null)
            {
                element.SetValue(
                  Windows.UI.Xaml.Controls.VariableSizedWrapGrid.ColumnSpanProperty, it.ItemSize);
            }
        }
        catch
        {
            element.SetValue(Windows.UI.Xaml.Controls.VariableSizedWrapGrid.ColumnSpanProperty, 1);
        }
        finally
        {
            base.PrepareContainerForItemOverride(element, item);
        }
    }
}

Now let’s create a MyGridView instance and bind it to a collection of items.

XML
<local:MyGridView AllowDrop="True" CanReorderItems="True" 
          CanDragItems="True" IsSwipeEnabled="True"
          ItemsSource="{Binding}" 
          ItemTemplate="{StaticResource ItemTemplate}" >
    <GridView.ItemsPanel>
        <ItemsPanelTemplate>
            <VariableSizedWrapGrid ItemHeight="160" 
            ItemWidth="160" />
        </ItemsPanelTemplate>
    </GridView.ItemsPanel>
    <GridView.ItemContainerStyle>
        <Style TargetType="GridViewItem">
            <Setter Property="HorizontalContentAlignment" 
            Value="Stretch"/>
            <Setter Property="VerticalContentAlignment" 
            Value="Stretch"/>
        </Style>
    </GridView.ItemContainerStyle>
</local:MyGridView>

In the code that initializes the collection, we will set the ItemSize property on certain items (business logic can determine this) to be 2, thus indicating a larger tile that spans two columns.

Image 2

Grouping

Using the GridViewEx control, we can enable grouping and drag and drop together. You can group a GridViewEx control just as you would enable grouping for the standard GridView. In fact, the Grid App template that you’ve probably started your application with uses grouping. You implement grouping by doing two things:

  1. Bind the GridView to a CollectionViewSource with a grouping-enabled data source. Meaning, the data source should contain groups of data such as each item containing a collection of child items. The CollectionViewSource acts as a proxy over the collection class to enable grouping.
  2. Specify a GroupStyle to determine how groups are displayed. GroupStyle includes a HeaderTempate and a Panel that specifies how child items in the group are arranged.

Optionally, you can specify the GroupStyle.ContainerStyle. This modifies the group container appearance. For example, you could add a border around each group. Let’s add grouping to our MyGridView implementation of GridViewEx. Remember, we extended GridViewEx to add business logic for variable sized items.

XML
<local:MyGridView AllowDrop="True" CanReorderItems="True" 
          CanDragItems="True" IsSwipeEnabled="True"
          ItemsSource="{Binding}" 
          ItemTemplate="{StaticResource ItemTemplate}" >
    <GridView.ItemsPanel>
        <ItemsPanelTemplate>
            <VirtualizingStackPanel Orientation="Horizontal"/>
        </ItemsPanelTemplate>
    </GridView.ItemsPanel>
    <GridView.GroupStyle>
        <GroupStyle>
            <GroupStyle.HeaderTemplate>
                <DataTemplate>
                    <Grid Background="LightGray" 
                    Margin="0">
                        <TextBlock Foreground="Black" 
                        Margin="10" 
                                  Style="{StaticResource 
                                  GroupHeaderTextStyle}">
                            <Run Text="{Binding Id}"/>
                            <Run Text=" group"/>
                        </TextBlock>
                    </Grid>
                </DataTemplate>
            </GroupStyle.HeaderTemplate>

            <GroupStyle.ContainerStyle>
                <Style TargetType="GroupItem">
                    <Setter Property="BorderBrush" 
                    Value="DarkGray"/>
                    <Setter Property="BorderThickness" 
                    Value="2"/>
                    <Setter Property="Margin" 
                    Value="3,0"/>
                </Style>
            </GroupStyle.ContainerStyle>

            <GroupStyle.Panel>
                <ItemsPanelTemplate>
                    <VariableSizedWrapGrid ItemHeight="160" 
                    ItemWidth="160" />
                </ItemsPanelTemplate>
            </GroupStyle.Panel>
        </GroupStyle>
    </GridView.GroupStyle>

    <GridView.ItemContainerStyle>
        <Style TargetType="GridViewItem">
            <Setter Property="HorizontalContentAlignment" 
            Value="Stretch"/>
            <Setter Property="VerticalContentAlignment" 
            Value="Stretch"/>
        </Style>
    </GridView.ItemContainerStyle>
</local:MyGridView>

It’s important to note when grouping is enabled and a GroupStyle is specified, the ItemsPanel has a new meaning. We changed ItemsPanel from a VariableSizedWrapGrid to a VirtualizingStackPanel. With grouping, ItemsPanel refers to how the groups are arranged in the GridView. Since we want to support different sized items within each group, we moved our VariableSizedWrapGrid to the GroupStyle.Panel template.

Run the sample and notice that we have grouping, variable sized items, and now drag and drop between groups thanks to the custom GridViewEx control.

Image 3

Adding New Groups

The custom GridViewEx control also added support for adding new groups when the user drags an item to the far left and right edges of the control. To allow new group creation, set the AllowNewGroup property to true. Then to handle adding new groups to the data layer, handle the GridViewEx.BeforeDrop event. The event arguments help determine the item’s origin and destination. Within the BeforeDrop event handler, you can create the new data group and insert it into the groups collection at the position specified by the argument’s NewGroupIndex property. The reason this is left to the developer is because the GridViewEx control knows nothing about your data structure.

C#
/// <summary>
/// Creates new CollectionViewSource and updates page DataContext.
/// </summary>
private void UpdateDataContext()
{
    CollectionViewSource source = new CollectionViewSource();
    source.Source = _groups;
    source.ItemsPath = new PropertyPath("Items");
    source.IsSourceGrouped = true;
    this.DataContext = source;
}
// creates new group in the data source,
// if end-user drags item to the new group placeholder
private void MyGridView_BeforeDrop(object sender, Controls.BeforeDropItemsEventArgs e)
{
    if (e.RequestCreateNewGroup)
    {
        // create new group and re-assign datasource 
        Group group = Group.GetNewGroup();
        if (e.NewGroupIndex == 0)
        {
            _groups.Insert(0, group);
        }
        else
        {
            _groups.Add(group);
        }
        UpdateDataContext();
    }
}

We also can use the Drop event to clear any empty groups.

C#
// removes empty groups (except the last one)
private void MyGridView_Drop(object sender, DragEventArgs e)
{
    bool needReset = false;
    for (int i = _groups.Count - 1; i >= 0; i--)
    {
        if (_groups[i].Items.Count == 0 && _groups.Count > 1)
        {
            _groups.RemoveAt(i);
            needReset = true;
        }
    }
    if (needReset)
    {
        UpdateDataContext();
    }
}

Saving Layout Across Sessions

In Windows 8 application can be suspended or terminated when the user switches away from it. For better user experience, our sample stores current layout when end-user navigates to the other page or when application is deactivated. In this sample, we use the simplest data serialization to JSON string. Depending on your data structure, data size and your needs, you can save data in other format and to the other place. In our case, it would be enough to save underlying business objects collection.

To save page layout, we use overrides for the LayoutAwarePage methods (see comments in code):

C#
/// <summary>
/// Populates the page with content passed during navigation.  Any saved state is also
/// provided when recreating a page from a prior session.
/// </summary>
/// <param name="navigationParameter">The parameter value passed to
/// <see cref="Frame.Navigate(Type, 
/// Object)"/> when this page was initially requested.
/// </param>
/// <param name="pageState"
/// >A dictionary of state preserved by this page during an earlier
/// session.  This will be null the first time a page is visited.</param>
protected override void LoadState(Object navigationParameter, 
	Dictionary<String, Object> pageState)
{
    base.LoadState(navigationParameter, pageState);
    if (pageState != null && pageState.Count > 0 
    && pageState.ContainsKey("Groups"))
    {
        // restore groups and items from the previously serialized state
        System.Runtime.Serialization.Json.DataContractJsonSerializer rootSer = 
        new System.Runtime.Serialization.Json.DataContractJsonSerializer(typeof(List<Group>));
        var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes
        	((string)pageState["Groups"]));
        _groups = (List<Group>)rootSer.ReadObject(stream);
    }
    else
    {
        // if we get here for the first time and don't have
        // serialized content, fill groups and items from scratch
        for (int j = 1; j <= 12; j++)
        {
            Group group = Group.GetNewGroup();
            for (int i = 1; i <= 7 + j % 3; i++)
            {
                group.Items.Add(new Item()
                {
                    Id = i,
                    GroupId = group.Id
                });
            }
            _groups.Add(group);
        }
    }
    UpdateDataContext();
}

/// <summary>
/// Preserves state associated with this page in case the application is suspended or the
/// page is discarded from the navigation cache.  Values must conform to the serialization
/// requirements of <see cref="SuspensionManager.SessionState"/>.
/// </summary>
/// <param name="pageState">
/// An empty dictionary to be populated with serializable state.</param>
protected override void SaveState(Dictionary<String, Object> pageState)
{
    // save groups and items to JSON string so that 
    // it's possible to restore page state later
    base.SaveState(pageState);
    System.Runtime.Serialization.Json.DataContractJsonSerializer rootSer = 
        new System.Runtime.Serialization.Json.DataContractJsonSerializer
        (typeof(List<Group>));
    var stream = new MemoryStream();
    rootSer.WriteObject(stream, _groups);
    string str = System.Text.Encoding.UTF8.GetString(stream.ToArray(), 
    		0, (int)stream.Length);
    pageState.Add("Groups", str);
}

/// <summary>
/// Invoked when this page is about to be displayed in a Frame.
/// </summary>
/// <param name="e">Event data that describes 
/// how this page was reached.  The Parameter
/// property is typically used to configure the page.</param>
protected override void OnNavigatedTo(NavigationEventArgs e)
{
    // restore page state
    var frameState = 
    GridViewSamples.Common.SuspensionManager.SessionStateForFrame(this.Frame);
    if (frameState.ContainsKey("TilePageData"))
    {
        this.LoadState(e.Parameter, 
        (Dictionary<String, Object>)frameState["TilePageData"]);
    }
    else
    {
        this.LoadState(e.Parameter, null);
    }
}

protected override void OnNavigatedFrom(NavigationEventArgs e)
{
    // save page state with "TilePageData" key
    var frameState = 
    GridViewSamples.Common.SuspensionManager.SessionStateForFrame(this.Frame);
    var pageState = new Dictionary<String, Object>();
    this.SaveState(pageState);
    frameState["TilePageData"] = pageState;
}

The above code serializes layout to the page state. Then, our sample application uses SuspensionManager to save this information along with the app state. You can find the full code in the attached samples. If you need more information about saving the app's state, start from MSDN article Manage app lifecycle and state.

Summary

The custom GridViewEx control enables us to combine several useful features of the GridView control. Taking advantage of more features allows us to deliver user experiences that are becoming the new norm when it comes to Windows Store app development.

There’s more we can do to our GridView items to make them behave like the Windows 8 start screen. For instance, the start tiles are “alive” as they rotate through updated content. You can take advantage of third party tile libraries, such as ComponentOne Tiles, and use them within a GridView to deliver animated tiles that flip and rotate to display live data. A second sample with live tiles is attached as well that shows the C1Tile controls in use with the GridViewEx control.

Update History

  • 31st Jan, 2013: First version
  • 26th Mar, 2013: Added the simplest data persistence to store screen layout across sessions
  • 16th May, 2013: Added support for new group placeholders between groups. To enable it, GroupStyle.ContainerStyle style should define custom control template which includes element with NewGroupPlaceHolder name (or uncomment it in attached samples)
  • 2nd Sep, 2015: Samples upgraded to Windows 8.1
  • 9th Oct, 2015: Added link to new UWP version (Windows 10)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)