Monday, August 12, 2013

Handling Windows 8 Orientations in Windows 8.1

Windows 8.1 eliminates the concept of a “snapped” or “filled” view and allows apps to run at a variety of sizes. The minimum default size is set to 500 pixels wide, but this can be overridden for legacy apps or apps designed specifically for the narrower resolution. The changes can make migration difficult, however. If you built your app using the built-in LayoutAwarePage class, you’ll find the functionality has changed significantly and there is no automatic trigger for orientation changes that map to visual states.

Ultimately, you will end up migrating your apps to the new system. Realistically, you may have dozens of screens that are expecting to adapt to the screen orientation based on legacy code, and it would be nice to reuse all of the effort that went into these styles. Should you just chuck the code and start from scratch? There may be a way to salvage those old visual states.

I was recently working on converting some of my sample projects from Windows 8 to Windows 8.1 and came across a project that demonstrates the Visual State Manager (VSM). It used a combination of some styled controls and the actual page orientation to do this. First, a quick review of the Windows 8 screen orientations as defined in the basic template:

Default or Full Screen Landscape

This is your app, running full screen, in a typical configuration assuming the tablet or laptop is in a typical landscape orientation.

screendefault

Filled

This is the landscape orientation when your screen is next to another app that is in the snapped (small) mode.

screenfilled

Portrait

This happens when you turn the tablet so it is taller than it is wide, like a sheet of paper. It’s more of a “book” or reading orientation.

screenportrait

Snapped

Finally, you can “snap” your app which places it to the side with limited functionality to run side-by-side with another app.

screensnapped

In Windows 8.1, the concepts of “filled” and “snapped” go away. You can have multiple apps running side-by-side with a minimum (default) resolution of 500 pixels. You are either in landscape or portrait mode, and that’s it. This, of course, doesn’t help us migrate our existing apps that were relying on the other modes.

When working on this particular app, I decided to think about the modes as still existing in Windows 8.1, with new definitions, like this:

  • FullScreenLandscape – landscape and full screen, of course
  • Filled – landscape and not full screen
  • FullScreenPortrait – technically, any type of portrait orientation
  • Snapped – the minimum width (500 pixels)

With this set of definitions in mind, I decided the easiest way to migrate existing pages would be to build something that replicates what the old template used to do. The main thing it did was listen for changes to the layout and update the orientation by calling the visual state manager. It turns out we can do that in Windows 8.1 and do it in a way that doesn’t require us to inherit from a base page or even duplicate code on multiple pages. Instead, we can create an attached property and attach it to the pages we want to be “layout aware.”

The attached property is defined on a static class called OrientationHandler. The HandleOrientation property determines whether or not the legacy behavior should be applied. Set it to true to use it (I can’t imagine why you’d attach it at all if you’re going to set it to false).

public static readonly DependencyProperty HandleOrientationProperty =
    DependencyProperty.RegisterAttached(
        "HandleOrientation",
        typeof(bool),
        typeof(OrientationHandler),
        new PropertyMetadata(false, OnHandleOrientationChanged));

public
 static void SetHandleOrientation(UIElement element, bool value)
{
    element.SetValue(HandleOrientationProperty, value);
}

public
 static bool GetHandleOrientation(UIElement element)
{
    return (bool)element.GetValue(HandleOrientationProperty);
}

Next, a property is used to keep track of the previous state so that it doesn’t keep transitioning when a size change doesn’t result in a new orientation.

public static readonly DependencyProperty LastOrientationProperty =
    DependencyProperty.RegisterAttached(
        "LastOrientation",
        typeof(string),
        typeof(OrientationHandler),
        new PropertyMetadata(string.Empty));

public
 static void SetLastOrientation(UIElement element, string value)
{
    element.SetValue(LastOrientationProperty, value);
}

public
 static string GetLastOrientation(UIElement element)
{
    return (string)element.GetValue(LastOrientationProperty);
}

When the property is attached to a control and set to true, it hooks into several events to evaluate the orientation.

private static void OnHandleOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var control = d as Control;

    if (control == null)
    {
        return;
    }

    control.Loaded += (sender, args) => SetLayout(control);
    control.LayoutUpdated += (sender, args) => SetLayout(control);
    control.SizeChanged += (sender, args) => SetLayout(control);
}

Finally, a method applies the layout algorithm I described above to determine the orientation and, if it is a new orientation, transitions to the appropriate visual state.

private static void SetLayout(Control control)
{
    var orientation = ApplicationView.GetForCurrentView().Orientation;
    string newMode;

    if (orientation == ApplicationViewOrientation.Landscape)
    {
        newMode = ApplicationView.GetForCurrentView().IsFullScreen ? "FullScreenLandscape" : "Filled";
    }
    else
    {
        newMode = Window.Current.Bounds.Width <= 500 ? "Snapped" : "FullScreenPortrait";
    }

    if (newMode == GetLastOrientation(control))
    {
        return;
    }

    VisualStateManager.GoToState(control, newMode, true);
    SetLastOrientation(control, newMode);
}

That’s it – now I can take the existing XAML along with the legacy visual states and plug it into Windows 8.1 by attaching the property like this (see the last line):

<common:Page
   x:Name="pageRoot"
   x:Class="VisualStateExample.MainPage"
    DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   xmlns:local="using:VisualStateExample"
   xmlns:common="using:VisualStateExample.Common"
   xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
   xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
   mc:Ignorable="d"
   local:OrientationHandler.HandleOrientation="True">

You can download both the Windows 8 and Windows 8.1 versions of this code by referencing the source code at this link – the code is from the VisualStateExample project in Chapter 3.