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:
- Set the
AllowDrop
property to true
. - Set the
CanReorderItems
property to true
. - Bind to a data source that supports data modification, or specifically reordering. For example, you could use something like
ObservableCollection
or IList
(Note: Unbound GridView
s also support reordering).
<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
.
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.
public class GridViewEx : GridView
{
public GridViewEx()
{
}
private void GridViewEx_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
{
}
protected virtual void OnDragStarting(DragItemsStartingEventArgs e)
{
}
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.
protected override void OnDragOver(DragEventArgs e)
{
private int GetDragOverIndex(DragEventArgs e)
{
}
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.
protected override async void OnDrop(DragEventArgs e)
{
}
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.
public event EventHandler<BeforeDropItemsEventArgs> BeforeDrop;
protected virtual void OnBeforeDrop(BeforeDropItemsEventArgs e)
{
}
The BeforeDropItemEventArgs
carries important information about the item being dragged so that it can be accessed later in the OnDrop
event.
public sealed class BeforeDropItemsEventArgs : System.ComponentModel.CancelEventArgs
{
public object Item
{
get;
}
public int OldIndex
{
get;
}
public int NewIndex
{
get;
}
public bool RequestCreateNewGroup
{
get;
}
public int OldGroupIndex
{
get;
}
public int NewGroupIndex
{
get;
}
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.
public bool AllowNewGroup
{
get { return (bool)GetValue(AllowNewGroupProperty); }
set { SetValue(AllowNewGroupProperty, value); }
}
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.
<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.
[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:
- Set the
GridViewEx.ItemsPanel
to an instance of VariableSizedWrapGrid
. - 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.
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.
public class MyGridView : GridViewSamples.Controls.GridViewEx
{
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.
<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.
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:
- 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. - 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.
<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.
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.
private void UpdateDataContext()
{
CollectionViewSource source = new CollectionViewSource();
source.Source = _groups;
source.ItemsPath = new PropertyPath("Items");
source.IsSourceGrouped = true;
this.DataContext = source;
}
private void MyGridView_BeforeDrop(object sender, Controls.BeforeDropItemsEventArgs e)
{
if (e.RequestCreateNewGroup)
{
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.
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):
protected override void LoadState(Object navigationParameter,
Dictionary<String, Object> pageState)
{
base.LoadState(navigationParameter, pageState);
if (pageState != null && pageState.Count > 0
&& pageState.ContainsKey("Groups"))
{
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
{
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();
}
protected override void SaveState(Dictionary<String, Object> pageState)
{
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);
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
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)
{
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)