In an ever-changing landscape of varying form factors and screen sizes, developers are tasked with tailoring apps for a multitude of different devices. The Windows 10 UWP development environment introduces several new UI features to assist developers in creating dynamically responsive applications across such a wide range of devices. This post will demonstrate the use of the Adaptive Trigger, probably one of the most helpful new features for your UWP toolbox.

Before diving into the Adaptive Trigger, or any responsive features for that matter, it's important to have an understanding of device families - what they are, and more importantly, what they are not.

Device Family Detection

Microsoft defines a device family as a, "set of APIs collected together and given a name and a version number". Every UWP app will have access to the APIs inside the 'Universal' device family because it's inherited to every other device family. For instance, an app running on either a phone or a desktop PC can make use of the Windows.Networking.Connectivity.WwanContract API contract for Internet access. However, an app running on a desktop PC will not have access to the Windows.Phone.PhoneContract API contract.

While it may seem reasonable to identify a specific device family for runtime operations, this approach can be problematic. Given the dynamic nature of hardware and how often new hardware becomes available, it's better to detect the presence of an API contract to perform specific actions. For instance, if an app contains visibility logic for a button (when clicked initiates a phone call) based solely on whether the device is a member of the 'Mobile' device family, this logic could break if the user is using a tablet incapable of completing phone calls. A better approach for this task would be to check if the API to make a phone call exists, like so:

if (ApiInformation.IsApiContractPresent("Windows.ApplicationModel.Calls.CallsPhoneContract", 1, 0)) btnCallCustomerService.Visible = true; else btnCallCustomerService.Visible = false;

Device family detection is a useful feature, but its use is better suited for capturing telemetry information rather than UI manipulation at runtime. And while the above example is a valid operation for all devices, making use of another new feature of the VisualStateManager known as adaptive triggers, would not require us to write any C# code.

Adaptive Triggers in Action

To make the best use of adaptive triggers, be sure to:

  • Identify what screen sizes the application is going to support.
  • Know the states for each screen in the application.

Rather than designing an app to accommodate expected screen sizes for a phone, a tablet or desktop, consider identifying what screen sizes your app should support. Keeping the focus on supported screen sizes eliminates the need to worry about specific devices, which as mentioned previously, can be a treacherous path to navigate.

Spend time thinking about how each screen should be laid out for ALL screen sizes. Sorting menu items in a vertical list, for instance, may make sense for smaller screen sizes, but organizing these same items in two columns might make sense for larger screen sizes. This kind of design pre-planning allows us to take full advantage of the tools available in developing UWP apps.

The adaptive trigger has two important properties, MinWindowWidth and MinWindowHeight. Just as the names suggest, these properties specify the width and height at which the associated VisualState should be applied. Think of them as screen width breakpoints or thresholds.

This simple example uses the MinWindowWidth property to update the element's background color.

For simplicity, the MinWindowWidth property is hard-coded to a specific number value. It's likely that an app will have several to many different visual states and adaptive triggers for different screens. It would follow that we define screen widths inside a ResourceDictionary, and reference them as a StaticResource. This way, a change to supported screen widths only needs to be updated in one place. This will be demonstrated in the example to follow.

A common use of adaptive triggers involves updating the state of another new UWP control known as the SplitView. MSDN describes the SplitView as "a container with two views; one view for the main content and another view that is typically used for navigation commands". For more information on this new control, read the MSDN documentation. The SplitView is also implemented in the native Windows 10 apps, Photos and Groove Music for navigation.

The following screenshots demonstrate how the SplitView state can be changed for different screen sizes.


Fully Collapsed Fully Expanded

Figure 1. Phone (4" screen; width at 240 pixels, 60 DPI)

By default, the SplitView is collapsed and only the hamburger menu is visible (left screen). If the hamburger menu is clicked, the SplitView is expanded (right screen). Given the phone's small screen size, it's a good practice to apply some level of opacity to the SplitView background color brush since it will overlay screen content.

Let's take a look at the state on a larger screen.


Partially Collapsed


Fully Expanded

Figure 2. Tablet (10" screen; width at 650 pixels, 60 DPI)

Since we have more real estate available, we can allow the SplitView to occupy some of that area by default, as seen on the partially collapsed state (left screen) in Figure 2.

For larger screen sizes, collapsing the navigation menu by default isn't necessary since there's ample real estate available.



Figure 3. Desktop (15" screen, width at 1024 pixels, 60 DPI)

With the exception of phone devices, users may adjust the width of the app screen after it's launched. The adaptive triggers will update the UI to reflect changes to the associated visual state. For instance, if the app is launched and its frame width is set to 700 pixels, Figure 2 illustrates the app's visual state upon startup. When the user taps (or clicks) the maximize button yielding a screen width of 1280 pixels, the visual state in Figure 3 is rendered.

For the sake of brevity, only relevant code is provided here:

Click="Menu_Clicked" />

Tag=""

Content="Home" />

Tag=""

Content="Search" />

Tag=""

Content="Settings" />

Tag=""

Content="About" />

The AdaptiveTrigger and Setter tags above showcase the mechanics of the adaptive trigger. The AdaptiveTrigger tag is responsible for what condition must be met. In each visual state, the MinWindowWidth property value dictates the screen width at which the visual state will be applied. The Setter tag is responsible for how the referenced elements are changed for the associated visual state. In the above example, the target control is the MySplitView where the IsPaneOpen and DisplayMode properties are being set.

Custom Triggers

Developers are not limited to the native triggers available for UWP apps. The ability to create our own triggers, known as custom triggers, offers additional control over adaptive layout. In this final example, we'll define a trigger that updates the background color based on the orientation (portrait or landscape) of the device.

Since we need to collect some information about the orientation of the user's device, let's create a service class that will return the device orientation.

public class DeviceInformation

{

public enum Orientations { Portrait, Landscape }

public static Orientations Orientation => DisplayInformation.GetForCurrentView().CurrentOrientation.ToString().Contains("Landscape") ? Orientations.Landscape : Orientations.Portrait;

public static DisplayInformation DisplayInformation =>

DisplayInformation.GetForCurrentView();

}

The first step is to create a class that inherits from the StateTriggerBase class:

public class DeviceTrigger : StateTriggerBase

Next, we'll need to define and register a dependency property that our custom trigger will use as its condition.

public Orientations Orientation

{

get { return (Orientations)GetValue(OrientationProperty); }

set { SetValue(OrientationProperty, value); }

}

public static readonly DependencyProperty OrientationProperty =

DependencyProperty.Register(nameof(Orientation), typeof(Orientations), typeof(DeviceTrigger),

new PropertyMetadata(Orientations.Portrait));

We'll also to hook in a changed event so that the trigger is fired when the orientation changes - even after the app is launched.

private void Initialize()

{

DeviceInformation.DisplayInformation.OrientationChanged += (s, e) => SetTrigger();

}

private void SetTrigger()

{

SetActive(Orientation == DeviceInformation.Orientation);

}

Integrating the custom trigger into the XAML is business as usual. It's referenced like any other trigger inside the VisualStateManager.

Adaptive triggers are a powerful new feature for the Windows development environment. Whether using screen width (or height) triggers, or implementing your own custom trigger, they provide the responsiveness necessary to accommodate many different devices. Coupling triggers with some of the new UWP controls, like the SplitView, asks very little effort and requires no additional C# code. Part II of this series will invite another useful control to the adaptive trigger discussion - the RelativePanel.