Introduction : Display Tiled Content with GridView
Windows 8 Start screen introduced a new approach to organize list of items or application navigation area. Microsoft’s GridView
control is a great way to display such tiled content. 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 is a second look at implementation of an extended GridView control, GridViewEx, which removes these limitations. The first attempt was for WinRT platform and provided support for drag and drop functionality with enabled grouping and variable sized items. You can find previous article here: Extending GridView with Drag and Drop for Grouping and Variable Sized Items.
This article describes upgrading GridViewEx
control and sample application from WinRT to Universal Windows Platform (UWP). The most of drag and drop functionality remained the same, so I only describe platform-specific changes and new additions. If you are interested in more details about drag and drop, please read previous article and look at attached code.
Upgrading Existent WinRT application to Universal Windows Platform (UWP)
Creating UWP Application
There are a number of blog posts and msdn articles about migrating existent projects, for example this one. In many cases it would be much easier to start from the new empty UWP project. After that you can decide what parts of the old application you want to use. Our old sample uses LayoutAwarePage
and SuspensionManager
classes which were included into Windows Store project templates. If you have several projects built on these templates, probably you will want to make minimal changes and re-use the rest of code. So, let’s add some old common classes to the new UWP application:
- Common/VisibilityConverter.cs
- Common/LayoutAwarePage.cs
- Common/SuspensionManager.cs
Also, let’s re-use the same data and samples, so add all files from DataModel and Samples folders.
Changes in Layout and Navigation
VisibilityConverter
and SuspensionsManager
classes don’t need any changes to be used in UWP. Let’s look at what is changed in layout and navigation logic.
With new form factors and wide range of supported devices Microsoft deprecated ApplicationViewState
handling. UWP platform supplies other means such as AdaptiveTriggers
to build adaptive layout. So we can safely remove all the code about handling ApplicationViewStates
. Without it LayoutAwarePage
name is not perfect for remaining functionality, but we can leave it as is for the sake of compatibility.
Both in WinRT and UWP Microsoft recommends using the back button in apps that use a hierarchical navigation pattern. In desktop WinRT application you have to add this button to your own xaml. In UWP you can enable title bar back button in desktop mode and use it in the same way as hardware Back button on mobile or tablet devices. You can find UWP back button guidelines here. UWP approach looks very generic and doesn’t require any custom xaml, so related code is a good candidate to include it into base class for different xaml pages. Let’s do it in LayoutAwarePage
class:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
if (IsWindowsPhoneDevice())
{
Windows.Phone.UI.Input.HardwareButtons.BackPressed += HardwareButtons_BackPressed;
}
else
{
var currentView = SystemNavigationManager.GetForCurrentView();
currentView.AppViewBackButtonVisibility = this.Frame != null && this.Frame.CanGoBack ?
AppViewBackButtonVisibility.Visible : AppViewBackButtonVisibility.Collapsed;
currentView.BackRequested += backButton_Tapped;
}
...
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
if (IsWindowsPhoneDevice())
{
Windows.Phone.UI.Input.HardwareButtons.BackPressed -= HardwareButtons_BackPressed;
}
else
{
var currentView = SystemNavigationManager.GetForCurrentView();
currentView.BackRequested -= backButton_Tapped;
}
...
private void HardwareButtons_BackPressed(object sender, BackPressedEventArgs e)
{
if (this.Frame != null && this.Frame.CanGoBack)
{
e.Handled = true;
this.Frame.GoBack();
}
}
private void backButton_Tapped(object sender, BackRequestedEventArgs e)
{
this.GoBack(this, new RoutedEventArgs());
}
Handling device Back button is only included into Windows Mobile Extensions for the UWP, so you need to add this reference to the project. Now all navigation is handled by LayoutAwarePage
and derived pages should not worry about it.
One last small addition to this class is a static method to detect device type at runtime:
public static bool IsWindowsPhoneDevice()
{
if (Windows.Foundation.Metadata.ApiInformation.IsTypePresent("Windows.Phone.UI.Input.HardwareButtons"))
{
return true;
}
return false;
}
Other Platform-related Changes
1. If you want your application look conforming to other Windows 10 apps, use Windows 10 ThemeResources.
2. Microsoft made a number of changes in GridView
control comparing with Windows 8 version https://msdn.microsoft.com/en-us/library/windows/apps/mt188204.aspx#gridview. The main of them is changing default orientation from horizontal to vertical. It requires changes in the control template and default property settings.
3. I haven’t found any notes about this, but apparently the behavior of VariableSizedWrapGrid
panel also changed. In all samples, you should set its Orientation
to Horizontal
, or it will always show all items as a single column.
4. The other critical change I’ve found in VariableSizedWrapGrid
(or maybe in Microsoft’s GridView
implementation) is that it doesn’t set column and row spans according to the item size automatically, as it did in Windows 8. Now it is application responsibility. Here is xaml which worked in Windows 8 and doesn’t work in Windows 10:
<GridView Grid.Row="1" Grid.Column="1" Margin="10" AllowDrop="True" CanReorderItems="True" CanDragItems="True" IsSwipeEnabled="True">
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<VariableSizedWrapGrid/>
</ItemsPanelTemplate>
</GridView.ItemsPanel>
<Rectangle Height="100" Width="200" Fill="Blue" />
<Rectangle Height="100" Width="100" Fill="Red" />
<Rectangle Height="100" Width="100" Fill="Yellow" />
<Rectangle Height="100" Width="100" Fill="Green" />
The best solution I’ve found is to set VariableSizedWrapGrid
attached properties on items and propagate them to ListViewItemPresenter
elements in a custom control derived from the GridView
:
public class GridViewTiled : GridView
{
protected override void PrepareContainerForItemOverride(Windows.UI.Xaml.DependencyObject element, object item)
{
element.SetValue(ContentControl.HorizontalContentAlignmentProperty, HorizontalAlignment.Stretch);
element.SetValue(ContentControl.VerticalContentAlignmentProperty, VerticalAlignment.Stretch);
UIElement el = item as UIElement;
if (el != null)
{
int colSpan = Windows.UI.Xaml.Controls.VariableSizedWrapGrid.GetColumnSpan(el);
int rowSpan = Windows.UI.Xaml.Controls.VariableSizedWrapGrid.GetRowSpan(el);
if (rowSpan > 1)
{
element.SetValue(Windows.UI.Xaml.Controls.VariableSizedWrapGrid.RowSpanProperty, rowSpan);
}
if (colSpan > 1)
{
element.SetValue(Windows.UI.Xaml.Controls.VariableSizedWrapGrid.ColumnSpanProperty, colSpan);
}
}
base.PrepareContainerForItemOverride(element, item);
}
}
Here is xaml working in UWP (Unbound
page from the attached GridView
demo uses it):
<controls:GridViewTiled Grid.Row="1" Grid.Column="1" Margin="10" AllowDrop="True" CanReorderItems="True" CanDragItems="True" >
<controls:GridViewTiled.ItemsPanel>
<ItemsPanelTemplate>
<VariableSizedWrapGrid ItemHeight="100" ItemWidth="100" Orientation="Horizontal"/>
</ItemsPanelTemplate>
</controls:GridViewTiled.ItemsPanel>
<Rectangle VariableSizedWrapGrid.ColumnSpan="2" VariableSizedWrapGrid.RowSpan="2" Fill="Blue" />
<Rectangle Fill="Red" />
<Rectangle Fill="Yellow" />
<Rectangle Fill="Green" />
The NewGroupPlaceholder Control
WinRT version of the GridViewEx
control uses simple border as a placeholder for new groups. It doesn’t allow altering its appearance during drag and drop operations. To make it more user-friendly and highlight drop area when drag over, let’s add the NewGroupPlaceholder
control with very simple logic of switching visual states during dragging over:
The code is very simple; you can find it in attached sample. Default control template uses system accent color to highlight drop area:
<Style TargetType="local:NewGroupPlaceholder">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Margin" Value="8" />
<Setter Property="Height" Value="32" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:NewGroupPlaceholder">
<Border x:Name="root" Background="{TemplateBinding Background}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="DragStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="DragOver">
<Storyboard>
<DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="dragOverElement"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Border x:Name="dragOverElement" Background="{ThemeResource SystemControlHighlightListAccentLowBrush}" Opacity="0"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Changes in the GridViewEx Control
Now let’s look what we should change in the GridViewEx
control to make it working under UWP.
The most of GridViewEx
control code works in UWP exactly in the same way as in WinRT. The only critical thing is a necessity to set DragEventArgs.AcceptedOperation
property in the OnDragOver
override. Apparently, default GridView
implementation in UWP resets this property to None
for non-supported items panels. So, if you don’t set it again in the OnDragOver
override, Drop
event will never happen. Fortunately, it is only one line of code:
protected override void OnDragOver(DragEventArgs e)
{
int newIndex = GetDragOverIndex(e);
if (newIndex >= 0)
{
e.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move;
If you build application now, you will see a number of compiler warnings about obsolete ItemContainerGenerator
methods. It is easy to fix, just use corresponding methods of the ItemsControl
class instead.
To workaround VariableSizedWrapGrid
limitations, add the same PrepareContainerForItemOverride
method as described above.
And the last thing to do is to update default GridViewEx
control style with new vertical orientation and NewGroupPlaceholder
controls. You can find it in generic.xaml file in attached samples.
Make it More Handy
Working on samples for this article I found one more extension point in standard GridView
control. If you need to customize UI elements for individual items, in many cases you should inherit from the GridView
and override PrepareContainerForItemOverride
method. To avoid it, I added new PreparingContainerForItem
event to the GridViewEx control. Event arguments for this event contain both data object and UI container for it, so you can adjust UI element properties as you need, for example:
private void gve_PreparingContainerForItem(object sender, GridViewEx.PreparingContainerForItemEventArgs e)
{
try
{
Item it = e.Item as Item;
if (it != null)
{
e.Element.SetValue(Windows.UI.Xaml.Controls.VariableSizedWrapGrid.ColumnSpanProperty, it.GroupId % 2 + 1);
}
}
catch
{
e.Element.SetValue(Windows.UI.Xaml.Controls.VariableSizedWrapGrid.ColumnSpanProperty, 1);
}
}
And the final point is to pack GridViewEx
control and related reusable classes (NewGroupPlaceholder
and VisibilityConverter
) as a custom control assembly.
Device-specific UI
There are some different ways to make adaptive layout. In this sample, the main thing I wanted to do is changing item size for mobile version. Unfortunately, I haven’t found a way to use adaptive triggers to change VariableSizedWrapGrid
ItemHeight
and ItemWidth
properties used in the GridView
’s ItemsPanelTemplate
. It is possible to include 2 GridView
instances with different settings and alter their visibility depending on screen size; you can try to do that. Personally, I think that using device-specific xaml pages is more correct as it allows application to download device-specific xaml only and so to save some resources. And in this case you are free to make more changes to layout than you could with adaptive triggers. So, let’s create Samples\DeviceFamily-Mobile folder in the project and add there xaml for Bound
, Unbound
and Grouped
samples with slightly different appearance. Grouped
sample shows single GridView
control, so mobile page can alter tile size and some paddings and that’s it.
Bound
and Unbound
samples show 2 GridView
controls side-by-side. On a small screen it looks too crowded and doesn’t show all details well enough. Let’s use Pivot
control to only show one GridView
control at a time and allow navigation between 2 controls. You can find source xaml in attached sample. Note that code-behind file is the same for desktop and mobile pages and you don’t need to do anything special to run one or another xaml, environment will do this for you.
For Customized
sample let’s try a different approach with adding dependency property for tile size:
public double TileSize
{
get { return (double)GetValue(TileSizeProperty); }
set { SetValue(TileSizeProperty, value); }
}
public static readonly DependencyProperty TileSizeProperty =
DependencyProperty.Register(nameof(TileSize), typeof(double), typeof(Customized), new PropertyMetadata(100));
public Customized()
{
if (IsWindowsPhoneDevice())
{
TileSize = 72;
}
this.InitializeComponent();
}
The GridView
or GridViewEx
control parts can bind to this property from xaml:
<GroupStyle.Panel>
<ItemsPanelTemplate>
<VariableSizedWrapGrid ItemHeight="{Binding TileSize, ElementName=pageRoot}"
ItemWidth="{Binding TileSize, ElementName=pageRoot}"
Orientation="Horizontal" MaximumRowsOrColumns="10"/>
</ItemsPanelTemplate>
</GroupStyle.Panel>
Known Issues
1. Mobile Emulators installed with Windows SDK 10.0.10240.0 have a problem with touch interaction. Attached sample works well if you use mouse (regardless of Emulator input settings), but it’s extremely hard to drag something with real touch interaction. The problem is the same with default GridView
settings, so apparently it is something in the Microsoft’s GridView
implementation or emulator issue. Maybe it is the same issue as described here: https://github.com/Microsoft/Windows-universal-samples/issues/131.
2. If you set GridViewEx
to collection of value type objects, you will get System.ExecutionEngineException
at extracting data from DragEventArgs
. If you really need value types, wrap them into some reference type. For example, the WinRT Bound
sample uses collection of integers:
ObservableCollection<int> coll = new ObservableCollection<int>();
for (int i = 1; i <= 31; i++)
{
coll.Add(i);
}
Use items in DataTemplate
:
<DataTemplate x:Key="ItemTemplate">
<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>
In UWP version we can replace it by anonymous type:
ObservableCollection<object> coll = new ObservableCollection<object>();
for (int i = 1; i <= 31; i++)
{
coll.Add(new { Id = i });
}
Updated xaml:
<DataTemplate x:Key="ItemTemplate">
<Border BorderBrush="Aqua" BorderThickness="1" Background="Peru">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="{Binding Id}"/>
<TextBlock Grid.Row="1">item</TextBlock>
</Grid>
</Border>
</DataTemplate>
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 Universal Windows Platform app development.
Attached GridView demo
described above was initially made for research purposes and looks not real. The second sample with live tiles shows images in rotating tiles of different size. To run it you will need to download new ComponentOne's UWP Beta Edition. Thanks a lot to Flickr for not asking for API keys and making it possible:
History
· 9th Oct, 2015: First version.
· 22nd Oct, 2015: Added control assembly, PreparingContainerForItem
event, sample with live tiles.