RSS

OrientationHelper for Windows Phone and Windows 8

18 Sep

This is a small utility to help with WP7,WP8 and Windows Store app development where there is a need to design your UX in order to support multiple screen sizes and orientations.
(Phew i almost said Metro Style..Wait..Damn it)

It is inspired by ‘LayoutAwarePage’ , an utility class included in the templates for Windows Store Apps.

On the Windows Phone side there are 6 orientation states:

  • Landscape
  • LandscapeLeft
  • LandscapeRight
  • Portrait
  • PortraitUp
  • PortraitDown

On the Windows Store side there are 4 view states :

  • Filled
  • Snapped
  • FullScreenLandscape
  • FullScreenPortrait

Before we go into details here is an example of the utility working on the Phone in Portrait mode :
On the left side we have setup our utility to automatically run some arbitrary scale animations ,defined in XAML, based on the current Orientation or ViewState. There’s nothing sophisticated in the screenshots below. Simply showing off the functionality.

Here you can see another one automatically running on the left side in Landscape mode.

Similarly on the Windows 8 side:

(FullScreenPortrait)

(FullScreenLandscape)

(Filled)

(Snapped)

What the designer needs to do is describe the UI (Xaml) for all screens and for all possible orientations/view states in order to provide a good UX. There is no clear pattern/guidelines on how this should be done although LayoutAwarePage gives us a good ‘clean’ solution by taking advantage of a well known proven XAML solution called the VisualStateManager. We are, after all, trying to tackle switching between multiple visual states so VisualStateManager sounds like a natural fit. And it is.

The basic idea is , similar to creating Custom Controls, to declare all views for all supported orientations/view states inside the same XAML file (what if the UI is composite though?) and use VisualStates, one for each specific orientation/view state, to describe the UI changes needed to transition between View designs. We will then let VisualStateManager do it’s work by asking it programmatically to switch between states when the platform notifies us of a change in orientation/view state.

What the developer needs to do is manually listen for orientation/view state changes for all visible screens
and call VisualStateManager to transition the current screen to the current view state.

This programmatic part of the developer seems like a lot of manual work that could be done automatically.
There’s gonna be a lot of code-behind spread around listening for the same event and dealing manually
with VisualState changing (or manually enabling/disabling controls, running animations etc etc if we didnt
use VisualStateManager)
Unfortunately no one can help the poor designer (developer?) that has to rewrite/redesign the whole UI
again and again. It has to happen.

LayoutAwarePage solves it by creating a Page base class for Metro style apps.
This utility is instead an attached property and supports Pages, user controls and custom controls. Haven’t made a sample with custom controls but it should be the same as user controls.
What if though i want to create a reusable custom control/user control and i want to have IT define how it should look on different orientations/view states instead of forcing the consumer/user of the control to do it.
Sometimes ,me as a user of a control, might need to tweak more stuff “inside” the control to make it look right. Lets say for example that i have a control that defines two buttons side by side ,stretched, that fill my Landscape view across nicely. I would have liked if the control itself switched them to be stacked (one on top of the other) when in Portrait mode. I can’t do that though unless the control supported that. Note that it would be best if the control supported that but didn’t enforce it.I the user would like to have control over when and if the control is allowed to auto-adapt by itself or i will solve it outside or the control. The utility supports this scenario by letting me decide whether or not to set the attached property OrientationHelper.IsActive on the control that enables it’s orientation/view state support if of course the control has defined the appropriate VisualStates.

The solution seemed transferable to Windows Phone as well and so the utility comes in 3 assemblies supporting WP7/WP8/WIN8.

What we need to do in order to use it is
1) set the attached property OrientationHelper.IsActive=”true” on a Page, UserControl or CustomControl
2) Define one VisualStateGroup on the orientation-aware control named “PageOrientationStates” for WP
and “ApplicationViewStates” for Win8.
3) Define one VisualState for each supported Orientation/ViewState. The VisualState name should match exactly the name of the Orientation/ViewState. For example i would name “Filled” the VisualState that is meant to design my UI when in Filled state.

Setting the attached property :
In this case the UserControl set’s it for demo purposes .It’s overridable.

Defining the VisualStates (For Win8) :

Quick tip : VisualStateGroups and VisualStateManager , i found, work *only* when defined/attached on the
RootElement of your Page or UserControl. Which means it wont work if you declare a control inside a Page, declare VisualStates on it and try to call VSM on that specific control.

Here’s the code for the OrientationHelper.

using System;
using System.Collections.Generic;
using System.Linq;

#if WINDOWS_PHONE
using System.Windows;
using System.Windows.Controls;
using Microsoft.Phone.Controls;
using Orientation = Microsoft.Phone.Controls.PageOrientation;
#else
using Windows.UI.Core;
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Orientation = Windows.UI.ViewManagement.ApplicationViewState;
#endif

namespace OrientationHelper
{
    public static class OrientationHelper
    {
#if WINDOWS_PHONE
        private const String VisualStateGroupName = "PageOrientationStates";
#else
        private const String VisualStateGroupName = "ApplicationViewStates";
#endif

        #region IsActive DP

        public static readonly DependencyProperty IsActiveProperty =
            DependencyProperty.RegisterAttached("IsActive", typeof (bool), typeof (OrientationHelper),
                                                new PropertyMetadata(default(bool), OnIsActivePropertyChanged));

        public static void SetIsActive(UIElement element, bool value)
        {
            element.SetValue(IsActiveProperty, value);
        }

        public static bool GetIsActive(UIElement element)
        {
            return (bool) element.GetValue(IsActiveProperty);
        }

        private static void OnIsActivePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var control = d as Control;
            if (control == null)
                return;

            SetupOrientationAwareControl(control, (bool)e.NewValue);

#if WINDOWS_PHONE
            //The control itself can be a page.In every case we need to retry until the containing page is loaded.
            control.Loaded += OnControlLoaded;
#endif

        }

        #endregion

#if WINDOWS_PHONE
        //Note: On the phone side Orientation is Page-specific so we keep the orientation aware controls
        //on the page itself using a Dependency Property
        #region OrientationAwareControls Private DP

        private static readonly DependencyProperty OrientationAwareControlsProperty =
            DependencyProperty.RegisterAttached("OrientationAwareControls", typeof (IList),
                                                typeof (OrientationHelper), null);

        private static void SetOrientationAwareControls(DependencyObject element, IList value)
        {
            element.SetValue(OrientationAwareControlsProperty, value);
        }

        private static IList GetOrientationAwareControls(DependencyObject element)
        {
            return (IList) element.GetValue(OrientationAwareControlsProperty);
        }

        #endregion
#else
        //Note: On the Windows 8 side Orientation is global
        private static IList<WeakReference> orientationAwareControls;
#endif

        private static void SetupOrientationAwareControl(Control control, bool isActive)
        {
#if WINDOWS_PHONE
            var page = FindParentPage(control);
            if (page == null)
                return;
            if (isActive)
            {
                //Add the orientation aware control to the list stored on the Page
                if (GetOrientationAwareControls(page) == null)
                {
                    SetOrientationAwareControls(page, new List());
                    //Start listening for changes
                    page.OrientationChanged += PageOnOrientationChanged;
                }
                if (!GetOrientationAwareControls(page).Contains(control))
                    GetOrientationAwareControls(page).Add(control);
                UpdateOrientationAwareControls(new[] { control }, page.Orientation); //Update this control only

            }
            else
            {
                var orientationAwareControls = GetOrientationAwareControls(page);
                if (orientationAwareControls != null)
                {
                    if (orientationAwareControls.Contains(control))
                    {
                        orientationAwareControls.Remove(control);
                        control.Loaded -= OnControlLoaded;
                    }
                    //Do cleanup for this page if there are no more controls
                    if (!orientationAwareControls.Any())
                    {
                        SetOrientationAwareControls(page, null);
                        page.OrientationChanged -= PageOnOrientationChanged;
                    }
                }
            }
#else
            if (isActive)
            {
                if (orientationAwareControls == null)
                    orientationAwareControls = new List();

                if (!GetOrientationAwareControls().Contains(control))
                {
                    orientationAwareControls.Add(new WeakReference(control));
                    // Note: On the Windows 8 side, probably due to aggressive optimizations,
                    // if the control is not named then when changing orientations/view states any
                    // WeakReference to that control will show it as GCed!
                    // Disclaimer : I ONLY TESTED ON THE SIMULATOR!!
                    if (string.IsNullOrEmpty(control.Name))
                    {
                        control.Name = Guid.NewGuid().ToString();
                    }
                    control.Loaded += OnControlLoaded;

                    if (control.Parent != null)
                    {
                        UpdateOrientationAwareControls(new[] { control }, ApplicationView.Value);
                    }
                }
                if (GetOrientationAwareControls().Any())
                {
                    //TODO: Handle Window changes (Closed)
                    Window.Current.SizeChanged += OnWindowSizeChanged;
                }

            }
            else
            {
                if (GetOrientationAwareControls().Contains(control))
                {
                    var controlWeakRef = orientationAwareControls.Single(weak => control.Equals(weak.Target));
                    orientationAwareControls.Remove(controlWeakRef);
                    control.Loaded -= OnControlLoaded;
                }
                if (!GetOrientationAwareControls().Any())
                {
                    orientationAwareControls = null;
                    Window.Current.SizeChanged -= OnWindowSizeChanged;
                }
            }
#endif
        }

        private static void OnControlLoaded(object sender, RoutedEventArgs routedEventArgs)
        {
            var control = sender as Control;
#if WINDOWS_PHONE
            SetupOrientationAwareControl(control, GetIsActive(control));
#else
            UpdateOrientationAwareControls(new[] { control }, ApplicationView.Value);
#endif
        }

#if WINDOWS_PHONE

        private static PhoneApplicationPage FindParentPage(FrameworkElement el)
        {
            FrameworkElement parent = el;
            while(parent != null)
            {
                if (parent is PhoneApplicationPage)
                    return parent as PhoneApplicationPage;
                parent = parent.Parent as FrameworkElement;
            }
            return null;
        }

        private static void PageOnOrientationChanged(object sender, OrientationChangedEventArgs e)
        {
            var page = sender as PhoneApplicationPage;
            UpdateOrientationAwareControls(GetOrientationAwareControls(page), page.Orientation); //Update all controls
        }
#else
        private static void OnWindowSizeChanged(object sender, WindowSizeChangedEventArgs windowSizeChangedEventArgs)
        {
            UpdateOrientationAwareControls(GetOrientationAwareControls(), ApplicationView.Value);
        }
        private static IEnumerable<Control> GetOrientationAwareControls()
        {
            if(orientationAwareControls == null || !orientationAwareControls.Any())
                return Enumerable.Empty();

            IList<Control> aliveControls = new List<Control>();
            foreach (var controlWeakRef in orientationAwareControls.ToList())
            {
                if (controlWeakRef.IsAlive)
                    aliveControls.Add(controlWeakRef.Target as Control);
                else
                    orientationAwareControls.Remove(controlWeakRef);
            }
            return aliveControls;
        }
#endif

        private static void UpdateOrientationAwareControls(IEnumerable controls, Orientation orientation)
        {
            if (controls == null || !controls.Any())
                return;

            foreach (var control in controls)
            {
                VisualStateManager.GoToState(control, GetVisualStateName(orientation), true);
            }
        }

        private static String GetVisualStateName(Orientation orientation)
        {
            return orientation.ToString();
        }
    }
}

Here’s the whole solution : OrientationHelper.zip

Feel free to comment if you found it useful or found any issue.

Au revoir

 
 

5 responses to “OrientationHelper for Windows Phone and Windows 8

  1. Sadie

    June 4, 2013 at 18:09

    Hi! I wanted to implement this in my app instead of recording each state. But because I do not have those same controls that you do, do I still have to record? Or am I missing something in your instructions?

     
    • Anargyros Tomaras

      June 6, 2013 at 17:39

      Hi Sadie,

      Could you elaborate a little more?
      You are free to specify your own UI and your own storyboards for each state.
      If you don’t want to support a specific state your can leave the VisualState defined but dont specify any storyboard for it.

       
      • Sadie Stokes

        June 6, 2013 at 18:19

        Yes, basically I need to create storyboards for each part of the UI? So record all of those actions in every state change? (if that makes sense)

         
      • Anargyros Tomaras

        June 6, 2013 at 18:31

        Yes you just use VisualStateManager as it was meant to work. Leave empty VisualStates for those Orientations where there’s nothing you can/want to change. For example you might want to switch your layout to a list instead of grid when in Portrait mode. In that case only change the Portait ones to hide the grid element and show the list one. You might need to define the opposite changes in Landscape also.

         
      • Sadie Stokes

        June 6, 2013 at 18:33

        Perfect! Thanks for your help!

         

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
Follow

Get every new post delivered to your Inbox.

%d bloggers like this: