Special Windows 10 issue 2015

Volume 30 Number 11

UI Design - Adaptive Apps for Windows 10

By Clint Rutkas | Windows 2015

With the Universal Windows Platform (UWP) in Windows 10, apps can now run on a variety of device families and automatically scale across the different screen and window sizes, supported by the platform controls. How do these device families support user interaction with your apps, and how do your apps respond and adapt to the device on which it’s running? We explore this and the tools and resources Microsoft provides in the platform, so you don’t have to write and maintain complex code for apps running across different device types.

Let’s start with an exploration of the responsive techniques you can use to optimize the UI for different device families. We’ll then go deeper into how your app can adapt to specific device capabilities.

Before we dive into controls, APIs and code, let’s take a moment to explore the device families we’re talking about. Simply put: A device family is a group of devices in a specific form factor, ranging from IoT devices, smartphones, tablets and desktop PCs, to Xbox game consoles, large-screen Surface Hub devices and even wearables. Apps will work across all these device families, but it’s important to consider the device families it could be used on when designing your apps.

While there are many device families, the UWP is designed such that 85 percent of its APIs are fully accessible to any app, independent of where it runs. What’s more, when looking at the top 1,000 apps, 96.2 percent of all APIs used are accounted for in the base Universal Windows API set. The bulk of functionality is present and available as part of the UWP, with specialized APIs on each device available to further tailor your app.

Welcome Back, Windows

One of the biggest changes in how apps are used in Windows is something you’re already very familiar with: running apps in a window. Windows 8 and Windows 8.1 allow apps to run full-screen, or side-by-side with as many as four apps simultaneously. Windows 10, by contrast, lets the user arrange, resize and position apps in any way he wants. The new approach under Windows 10 gives the user better UI flexibility, but may require some work on your end to optimize for it. The improvements in XAML on Windows 10 introduce a number of ways to implement responsive techniques in your app, so it looks great no matter the screen or window size. Let’s explore three of these approaches.

VisualStateManager In Windows 10 the VisualStateManager class has been expanded with two mechanisms to implement responsive design in your XAML-based apps. The new VisualState.StateTriggers and VisualState.Setters APIs allow you to define visual states that correspond to certain conditions. Visual states can change based on app window height and width, by using the built-in AdaptiveTrigger as the VisualState’s StateTrigger and setting the MinWindowHeight and MinWindowWidth properties. You can also extend Windows.UI.Xaml.State­­TriggerBase to create your own triggers, for instance triggering on device family or input type. Take a look at the code in Figure 1.

Figure 1 Create Custom State Triggers

<Page>
  <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <VisualStateManager.VisualStateGroups>
      <VisualStateGroup>
        <VisualState>
          <VisualState.StateTriggers>
          <!-- VisualState to be triggered when window
            width is >=720 effective pixels. -->
            <AdaptiveTrigger MinWindowWidth="720" />
          </VisualState.StateTriggers>
          <VisualState.Setters>
            <Setter Target="myPanel.Orientation"
                    Value="Horizontal" />
          </VisualState.Setters>
        </VisualState>
      </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
    <StackPanel x:Name="myPanel" Orientation="Vertical">
      <TextBlock Text="This is a block of text. It is text block 1. "
                 Style="{ThemeResource BodyTextBlockStyle}"/>
      <TextBlock Text="This is a block of text. It is text block 2. "
                 Style="{ThemeResource BodyTextBlockStyle}"/>
      <TextBlock Text="This is a block of text. It is text block 3. "
                 Style="{ThemeResource BodyTextBlockStyle}"/>
    </StackPanel>
  </Grid>
</Page>

In the example in Figure 1, the page displays three TextBlock elements stacked on top of each other in its default state. The VisualStateManager has an AdaptiveTrigger defined at a MinWindow­Width of 720, which causes the orientation of the StackPanel to change to Horizontal when the window is at least 720 effective pixels wide. This lets you make use of the extra horizontal screen real estate when users resize the window or go from portrait to landscape mode on a phone or tablet device. Keep in mind, if you define both the width and height property, your trigger will only fire if the app meets both conditions simultaneously. You can explore the State triggers sample on GitHub (wndw.ms/XUneob) to view more scenarios using triggers, including a number of custom triggers.

Relative Panel In the Figure 1 example, a StateTrigger is used to change the Orientation property of a StackPanel. The many container elements in XAML, combined with StateTriggers, let you manipulate your UI in a large number of ways, but they don’t offer a way to easily create a complex, responsive UI where elements are laid out relative to each other. That’s where the new RelativePanel comes in. As shown in Figure 2, you can use a RelativePanel to lay out your elements by expressing spatial relationships between elements. This means that you can easily use the RelativePanel together with AdaptiveTriggers to create a responsive UI where you move elements based on available screen space.

Figure 2 Express Spatial Relationships with RelativePanel

<RelativePanel BorderBrush="Gray" BorderThickness="10">
  <Rectangle x:Name="RedRect" Fill="Red" MinHeight="100" MinWidth="100"/>
  <Rectangle x:Name="BlueRect" Fill="Blue" MinHeight="100" MinWidth="100"
             RelativePanel.RightOf="RedRect" />
  <!-- Width is not set on the green and yellow rectangles.
       It's determined by the RelativePanel properties. -->
  <Rectangle x:Name="GreenRect" Fill="Green"
             MinHeight="100" Margin="0,5,0,0"
             RelativePanel.Below="RedRect"
             RelativePanel.AlignLeftWith="RedRect"
             RelativePanel.AlignRightWith="BlueRect"/>
  <Rectangle Fill="Yellow" MinHeight="100"
             RelativePanel.Below="GreenRect"
             RelativePanel.AlignLeftWith="BlueRect"
             RelativePanel.AlignRightWithPanel="True"/>
</RelativePanel>

As a reminder, the syntax you use with attached properties involves extra parentheses, as shown here:

<VisualStateManager.VisualStateGroups>
  <VisualStateGroup>
    <VisualState>
      <VisualState.StateTriggers>
        <AdaptiveTrigger MinWindowWidth="720" />
      </VisualState.StateTriggers>
      <VisualState.Setters>
        <Setter Target="GreenRect.(RelativePanel.RightOf)"
                Value="BlueRect" />
      </VisualState.Setters>
    </VisualState>

You can check out additional scenarios using RelativePanel in the Responsiveness techniques sample on GitHub (wndw.ms/cbdL0q).

SplitView App window size impacts more than the content displayed on app pages; it may require that navigation elements respond to changes in the size of the window itself. The new SplitView control introduced in Windows 10 is typically used to create a top-level navigation experience that can be adjusted to behave differently according to the size of the app window. Keep in mind that while this is one of the common use cases for the SplitView, it’s not strictly limited to this use. The SplitView is divided into two distinct areas: pane and content.

A number of properties on the control can be used to manipulate presentation. First, DisplayMode specifies how the pane is rendered in relation to the Content area, with four available modes—Overlay, Inline, CompactOverlay and CompactInline. Figure 3 shows examples of the Inline, Overlay and CompactInline modes rendered in an app.

DisplayMode Navigation Elements
Figure 3 DisplayMode Navigation Elements

The PanePlacement property displays the Pane on either the left (default) or right side of the Content area. The OpenPaneLength property specifies the width of the pane when it’s fully expanded (default 320 effective pixels).

Note that the SplitView control does not include a built-in UI element for users to toggle the state of the pane, like the common “hamburger” menu often found in mobile apps. If you want to expose this behavior, you must define this UI element in your app and provide code to toggle the IsPaneOpen property of the SplitView.

Want to explore the full set of features the SplitView offers?  Be sure to check out the XAML navigation menu sample on GitHub (wndw.ms/qAUVr9).

Bringing the Back Button

If you developed apps for earlier versions of Windows Phone, you might be accustomed to every device having a hardware or software back button, allowing users to navigate their way back through your app. For Windows 8 and 8.1, however, you had to create your own UI for back navigation. To make things easier when targeting multiple device families in your Windows 10 app, there’s a way to ensure a consistent back-navigation mechanism for all users. This can help free up some UI space in your apps going forward.

To enable a system back button for your app, even on device families that don’t have a hardware or software back button (such as laptop and desktop PCs), use the AppViewBackButtonVisibility property in the SystemNavigationManager class. Simply get the SystemNavigationManager for the current view and set the back button visibility as shown in the following code:

SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility =
  AppViewBackButtonVisibility.Visible;

The SystemNavigationManager class also exposes a BackRequested event, which fires when the user invokes the system-provided button, gesture or voice command for back navigation. This means that you can handle this single event to consistently perform back navigation in your app across all device families.

Benefits of Continuum

Last, but not least, we’d like to mention one of our personal favorites: Continuum on Windows 10. With Continuum, Windows 10 adjusts your experience to what you want to do and how you want to do it. If your app is running on a 2-in-1 Windows PC, for example, implementing Continuum in your app lets users use touch or a mouse and keyboard to optimize productivity. With the UserInteractionMode property in the UIViewSettings class, your app can determine whether the user is interacting with the view using touch or a mouse and keyboard with just one line of code:

UIViewSettings.GetForCurrentView().UserInteractionMode;
// Returns UserInteractionMode.Mouse or UserInteractionMode.Touch

After detecting the mode of interaction, you can optimize the UI of your app, doing things like increasing or decreasing margins, showing or hiding complex features, and more. Check out the TechNet article, “Windows 10 Apps: Leverage Continuum Feature to Change UI for Mouse/Keyboard Users Using Custom StateTrigger,” by Lee McPherson (wndw.ms/y3gB0J) showing how you can combine the new StateTriggers and the UserInteractionMode to build your own custom Continuum StateTrigger.

Apps Adaptive

Apps that can respond to changes in screen size and orientation are useful, but to achieve compelling, cross-platform functionality, the UWP provides developers with two additional types of adaptive behavior:

  • Version adaptive apps respond to different versions of the UWP by detecting available APIs and resources. For example, you might want your app to use some newer APIs that are only present on devices running the most recent versions of the UWP, while continuing to support customers who haven’t upgraded yet.
  • Platform adaptive apps respond to the unique capabilities available on different device families. So an app may be built to run on all device families, but you might want to use some mobile-specific APIs when it runs on a mobile device like a smartphone.

As noted earlier, with Windows 10 the vast majority of UWP APIs are fully accessible to any app, regardless of the device on which it’s running. Specialized APIs associated with each device family then allow developers to further tailor their apps.

The fundamental idea behind adaptive apps is that your app checks for the functionality (or feature) it needs, and only uses it when it’s available. In the past, an app would check the OS version and then call APIs associated with that version. With Windows 10, your app can check at runtime whether a class, method, property, event or API contract is supported by the current OS. If so, the app then calls the appropriate API. The ApiInformation class located in the Windows.Foun­dation.Metadata namespace contains several static methods (such as IsApiContractPresent, IsEventPresent and IsMethodPresent), which are used to query for APIs. Here’s an example:

using Windows.Foundation.Metadata;
if(ApiInformation.IsTypePresent("Windows.Media.Playlists.Playlist"))
{
  await myAwesomePlaylist.SaveAsAsync( ... );
}

This code does two things. It makes a runtime check for the presence of the Playlist class, then calls the SaveAsAsync method on an instance of the class. Also note the ease of checking for the presence of a type on the current OS, using the IsTypePresent API. In the past, such a check might have required LoadLibrary, GetProcAddress, QueryInterface, Reflection, or use of the “dynamic” keyword and others, depending on language and framework. Also note the strongly typed reference when making the method call. When using either Reflection or “dynamic,” you lose static compile-time diagnostics that would, for example, inform you if you misspelled the method name.

Detecting with API Contracts

At its heart, an API contract is a set of APIs. A hypothetical API contract could represent a set of APIs containing two classes, five interfaces, one structure, two enums and so on. We group logically related types into an API contract. In many ways, an API contract represents a feature—a set of related APIs that together deliver some particular functionality. Every Windows Runtime API from Windows 10 onward is a member of some API contract. The documentation at msdn.com/dn706135 describes the variety of API contracts available. You’ll see that most of them represent a set of functionally related APIs.

The use of API contracts also provides you, the developer, with some additional guarantees; the most important being when a platform implements any API in an API contract, it must implement every API in that contract. In other words, an API contract is an atomic unit, and testing for support of that API contract is equivalent to testing that each and every API in the set is supported. Your app can call any API in the detected API contract without having to check for each API individually.

The largest and most commonly used API contract is the Windows.Foundation.UniversalApiContract. It contains nearly all of the APIs in the Universal Windows Platform. If you wanted to see if the current OS supports the UniversalApiContract, you would write the following code:

if (ApiInformation.IsApiContractPresent(
  "Windows.Foundation.UniversalApiContract"), 1, 0)
{
  // All APIs in the UniversalApiContract version 1.0 are available for use
}

Right now the only extant version of the UniversalApiContract is version 1.0, so this check is slightly silly. But a future update of Windows 10 could introduce additional APIs, yielding a version 2.0 of the UniversalApiContract that includes the new universal APIs. In the future an app that wants to run on all devices, but also wants to use new version 2.0 APIs, could use the following code:

if (ApiInformation.IsApiContractPresent(
  "Windows.Foundation.UniversalApiContract"), 2, 0)
{
  // This device supports all APIs in UniversalApiContract version 2.0
}

If your app only needed to call one single method from version 2.0, it could check for the method directly using IsMethodPresent. In such a case, you can use whichever approach you find the easiest.

There are other API contracts besides the UniversalApiContract. Most represent a feature or set of APIs not universally present on all Windows 10 platforms and instead are present on one or more specific device families. As mentioned earlier, you no longer need to check for a particular type of device and then infer support for an API. Simply check for the set of APIs your app wants to use.

I can now rewrite my original example to check for the presence of the Windows.Media.Playlists.PlaylistsContract, instead of just checking for the present of the Playlist class:

if(ApiInformation.IsApiContractPresent(
  "Windows.Media.Playlists.PlaylistsContract"), 1, 0)
{
  // Now I can use all Playlist APIs
}

Any time your app needs to call an API that isn’t present across all device families, you must add a reference to the appropriate Extension SDK that defines the API. In Visual Studio 2015, go to the Add Reference dialog and open the Extensions tabs. There you can find the three most important extensions: Mobile Extension, Desktop Extension and IoT Extension.

All your app needs to do, however, is check for the presence of the desired API contract and call the appropriate APIs conditionally. There’s no need to worry about the type of device. Now the question is: I need to call the Playlist API but it’s not a universally available API. The documentation (bit.ly/1QkYqky) tells me in which API contract the class is. But which Extension SDKs define it?

As it turns out, the Playlist class is (currently) only available on Desktop devices, not on Mobile, Xbox and other device families. So you must add a reference to the Desktop Extension SDK before any of the prior code compiles.

Visual Studio team member and occasional MSDN Magazine author Lucian Wischik created a tool that can help. It analyzes your app code when it calls into a platform-specific API, verifying that an adaptivity check was done around it. If no check was done, the analyzer reports a warning and provides a handy “quick-fix” to insert the correct check in the code, simply by pressing Ctrl+Dot or clicking on the lightbulb. (See bit.ly/1JdXTeV for more details.) The analyzer can also be installed via NuGet (bit.ly/1KU9ozj).

Let’s wrap up by looking at more complete examples of adaptive coding for Windows 10. First, here’s some code that is not correctly adaptive:

// This code will crash if called from IoT or Mobile
async private Task CreatePlaylist()
{
  StorageFolder storageFolder = KnownFolders.MusicLibrary;
  StorageFile pureRockFile = await storageFolder.CreateFileAsync("myJam.mp3");
  Windows.Media.Playlists.Playlist myAwesomePlaylist =
    new Windows.Media.Playlists.Playlist();
  myAwesomePlaylist.Files.Add(pureRockFile);
  // Code will crash here as this is a Desktop-only call
  await myAwesomePlaylist.SaveAsAsync(KnownFolders.MusicLibrary,
    "My Awesome Playlist", NameCollisionOption.ReplaceExisting);
}

Now let’s see the same code, adding a line that verifies that the optional API is supported on the target device before calling the method. This will prevent runtime crashes. Note that you’ll likely want to take this example further and not display the UI that calls the CreatePlaylist method if your app detects that the playlist functionality isn’t available on the device:

async private Task CreatePlaylist()
{
  StorageFolder storageFolder = KnownFolders.MusicLibrary;
  StorageFile pureRockFile = await storageFolder.CreateFileAsync("myJam.mp3");
  Windows.Media.Playlists.Playlist myAwesomePlaylist =
    new Windows.Media.Playlists.Playlist();
  myAwesomePlaylist.Files.Add(pureRockFile);
  // Now I'm a safe call! Cache this value if this will be queried a lot
  if (Windows.Foundation.Metadata.ApiInformation.IsTypePresent(
    "Windows.Media.Playlists.Playlist"))
  {
      await myAwesomePlaylist.SaveAsAsync(
        KnownFolders.MusicLibrary, "My Awesome Playlist",
        NameCollisionOption.ReplaceExisting);
  }
}

Finally, here’s an example of code that looks to access the dedicated camera button found on many mobile devices:

// Note: Cache the value instead of querying it more than once
bool isHardwareButtonsAPIPresent =
  Windows.Foundation.Metadata.ApiInformation.IsTypePresent(
  "Windows.Phone.UI.Input.HardwareButtons");
if (isHardwareButtonsAPIPresent)
{
  Windows.Phone.UI.Input.HardwareButtons.CameraPressed +=
    HardwareButtons_CameraPressed;
}

Note the detection step. If I were to directly reference the HardwareButtons object for the CameraPressed event while on a desktop PC, without checking that HardwareButtons is present, my app would crash.

There’s a lot going on around responsive UI and adaptive apps in Windows 10. Do you want to learn more? Check out the great talk about API Contracts that Brent Rector gave at the Build 2015 conference (wndw.ms/IgNy0I), and be sure to view the informative Microsoft Virtual Academy video on adaptive code (bit.ly/1OhZWGs) that covers this topic in more detail.


Clint Rutkas is a senior product manager for Windows focusing on the developer platform. He has worked on Halo at 343 Industries and on Channel 9 at Microsoft, and has built some crazy projects using Windows technology, like a computer-controlled disco dance floor, a custom Ford Mustang, t-shirt-shooting robots and more.

Rajen Kishna currently works as a senior product marketing manager on the Windows Platform Developer Marketing team for Microsoft in Redmond, Wash. He previously worked as a consultant and technical evangelist for Microsoft in The Netherlands.

Thanks to the following Microsoft technical experts for reviewing this article: Sam Jarawan, Harini Kannan and Brent Rector