Using AdaptiveTrigger to Build Adaptive UIs in Windows 10

Like a lot of developers, I’ve been watching the development of Windows 10 and digesting all the features Microsoft announced at BUILD. And I’m eager to build apps that target the Universal Windows Platform (UWP). UWP is a new model that allows you to write apps that run on a wide range of Windows devices, including desktop PCs, tablets, phones, Xboxes, IoT devices, and yes, even HoloLens. It’s the future of Windows application development, especially if you care to target the ever-expanding family of devices that run Windows. And there’s no time like the present to start learning about it.

Unlike universal apps in Windows 8, universal apps in Windows 10 truly are universal – at least as far as Windows devices are concerned. In Windows 8, a universal app solution in Visual Studio typically contained one shared project and additional projects, or “heads,” for individual devices. For example, it was common to have one shared project, one Windows project, and one Windows Phone project. Building the solution produced two binaries: one for Windows and one for Windows Phone. While the shared project contained shared code that worked on both devices, you created the UI for the Windows app in the Windows project, and the UI for the phone app in the Windows Phone project.

In Windows 10, a universal-app solution produces ONE binary – an APPX package – that runs on any UWP device. Similarly, a universal-app solution contains just one project. If that project has a MainPage.xaml file, you define the UI in that file for all the devices – and all the screens – the app might run on. That’s why a lot of the talk around building UWP apps is about adaptive UIs. Since a UI that looks good on a phone probably won’t look good on a 30” desktop monitor (and vice versa), it’s incumbent upon UWP developers to learn how to write adaptive UIs.

One of the tools that Microsoft gives us for building adaptive UIs in UWP apps is state triggers. The version of Windows 10 released at BUILD 2015 features one state trigger: a class named AdaptiveTrigger. AdaptiveTrigger has two important properties: MinWindowWidth and MinWindowHeight. You use AdaptiveTrigger in conjunction with Visual State Manager to adapt the UI to screens and windows of various sizes. For example, you might lay out the core parts of the UI horizontally on a desktop screen, but opt for a more vertical layout on a phone. With AdaptiveTrigger and Visual State Manager, you can do that in XAML without writing a single line of (C#) code.

A simple use for AdaptiveTrigger is to change the background color of an element when the screen size (or, for a windowed app, the window size) crosses a specified width threshold. Assume that your page contains a root layout element (perhaps a Grid) named LayoutRoot that spans the width and height of the page, and that you want to set that element’s background color to light yellow or light green, depending on the width of the page. Here’s the Visual State Manager XAML to accomplish that:

<VisualStateManager.VisualStateGroups>
  <VisualStateGroup x:Name="WindowStates">
    <VisualState x:Name="WideState">
      <VisualState.StateTriggers>
        <AdaptiveTrigger MinWindowWidth="800" />
      </VisualState.StateTriggers>
      <VisualState.Setters>
        <Setter Target="LayoutRoot.Background" Value="LightYellow" />
      </VisualState.Setters>                    
    </VisualState>
    <VisualState x:Name="NarrowState">
      <VisualState.StateTriggers>
        <AdaptiveTrigger MinWindowWidth="0" />
      </VisualState.StateTriggers>
      <VisualState.Setters>
        <Setter Target="LayoutRoot.Background" Value="LightGreen" />
      </VisualState.Setters>
    </VisualState>
  </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

Now the background will be light yellow when the width is greater than or equal to 800 pixels, and light green when it’s anything less. You can try it out on a Windows desktop PC. Use the mouse to resize the window, and you’ll see that the background changes in real time when the window width crosses the 800-pixel threshold. As an aside, note the new Setters property on the VisualState element. Rather than use animations to define state changes (something that always felt a little clumsy in previous versions of XAML), you can now use setters to assign values to properties directly. That makes your XAML cleaner and more intuitive.

To show a more practical use for AdaptiveTrigger, I built a sample named AdaptiveTriggerDemo, which you can download from OneDrive. Here’s how it looks running in a window on a desktop PC:

AdaptiveTriggerDemo (1)

And here’s how it looks on my Windows phone:

AdaptiveTriggerDemo (2)

It’s one UI defined in one XAML file (MainPage.xaml), but the UI adapts itself to the screen it’s running on (or the window it’s running in). When the screen/window width is greater than or equal to 800 pixels, the three main components of the layout – the panel containing a recipe image and description, the panel listing recipe ingredients, and the panel containing recipe directions – are positioned side by side. But when the width is less than 800 pixels, the UI is rearranged so that the three panels are stacked on top of one another. (In the screen shot from the phone, you can only see the top panel. But all three panels are contained in a ScrollViewer, so you can scroll down to see the remaining panels.)

Here’s the XAML for the page, complete with the Visual State Manager goo (and the AdaptiveTriggers) near the bottom:

<Page
    x:Class="AdaptiveTriggerDemo.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:AdaptiveTriggerDemo"
    xmlns:comp="using:AdaptiveTriggerDemo.Components"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Page.Resources>
        <comp:ListConverter x:Key="ListConverter" />
    </Page.Resources>

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <StackPanel Grid.Row="0" Margin="24,17,0,28">
            <TextBlock Text="Contoso Cookbook" Foreground="#C33D27" Style="{ThemeResource TitleTextBlockStyle}" Typography.Capitals="SmallCaps"/>
            <TextBlock Text="{Binding Title}" Margin="0,8,0,0" Style="{ThemeResource SubheaderTextBlockStyle}" TextWrapping="WrapWholeWords"/>
        </StackPanel>

        <ScrollViewer Grid.Row="1">
            <Grid Margin="0,-24,24,24">
                <Grid.RowDefinitions>
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                </Grid.RowDefinitions>

                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>

                <StackPanel x:Name="ImagePanel" Margin="24,24,0,0">
                    <Image Source="{Binding ImagePath}" HorizontalAlignment="Left" />
                    <TextBlock Text="{Binding Description}" FontSize="20" FontWeight="Light" TextWrapping="WrapWholeWords" Margin="0,8,0,8" />
                    <StackPanel Orientation="Horizontal">
                        <TextBlock FontSize="24" FontWeight="Light" Foreground="#C33D27" Text="{Binding PrepTime}" Margin="0,0,8,0"/>
                        <TextBlock FontSize="24" FontWeight="Light" Foreground="#C33D27" Text="minutes"/>
                    </StackPanel>
                </StackPanel>
                <StackPanel x:Name="IngredientsPanel" Margin="24,24,0,0">
                    <Rectangle Height="32" Fill="#C33D27" />
                    <TextBlock FontSize="26" Foreground="#C33D27" FontWeight="Light" Text="Ingredients" Margin="0,24,0,8"/>
                    <TextBlock FontSize="16" FontWeight="Light" Text="{Binding Ingredients, Converter={StaticResource ListConverter}}" TextWrapping="Wrap" />
                </StackPanel>
                <StackPanel x:Name="DirectionsPanel" Margin="24,24,0,0">
                    <Rectangle Height="32" Fill="#C33D27" />
                    <TextBlock FontSize="26" Foreground="#C33D27" FontWeight="Light" Text="Directions" Margin="0,24,0,8"/>
                    <TextBlock FontSize="16" FontWeight="Light" Text="{Binding Directions}" TextWrapping="Wrap" />
                </StackPanel>
            </Grid>
        </ScrollViewer>

        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="WindowStates">
                <VisualState x:Name="WideState">
                    <VisualState.StateTriggers>
                        <AdaptiveTrigger MinWindowWidth="800" />
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <Setter Target="IngredientsPanel.(Grid.Row)" Value="0" />
                        <Setter Target="IngredientsPanel.(Grid.Column)" Value="1" />
                        <Setter Target="DirectionsPanel.(Grid.Row)" Value="0" />
                        <Setter Target="DirectionsPanel.(Grid.Column)" Value="2" />
                        <Setter Target="ImagePanel.(Grid.ColumnSpan)" Value="1" />
                        <Setter Target="IngredientsPanel.(Grid.ColumnSpan)" Value="1" />
                        <Setter Target="DirectionsPanel.(Grid.ColumnSpan)" Value="1" />
                    </VisualState.Setters>
                </VisualState>
                <VisualState x:Name="NarrowState">
                    <VisualState.StateTriggers>
                        <AdaptiveTrigger MinWindowWidth="0" />
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <Setter Target="IngredientsPanel.(Grid.Row)" Value="1" />
                        <Setter Target="IngredientsPanel.(Grid.Column)" Value="0" />
                        <Setter Target="DirectionsPanel.(Grid.Row)" Value="2" />
                        <Setter Target="DirectionsPanel.(Grid.Column)" Value="0" />
                        <Setter Target="ImagePanel.(Grid.ColumnSpan)" Value="3" />
                        <Setter Target="IngredientsPanel.(Grid.ColumnSpan)" Value="3" />
                        <Setter Target="DirectionsPanel.(Grid.ColumnSpan)" Value="3" />
                    </VisualState.Setters>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
    </Grid>
</Page>

Here are three important takeaways from the XAML:

  • The three “panels” containing recipe information are StackPanels. Their layout is provided by a Grid control containing three columns of equal width, and three rows. When the screen or window width is 800 pixels or higher, the StackPanels appear side-by-side because they occupy the three columns in the first Grid row. However, when the width is less than 800, the StackPanels are stacked on top of one another in the three rows in the first column. The setters effectively move the StackPanels around by changing their Grid.Row and Grid.Column properties. Since the columns don’t collapse when the StackPanels are removed from them, setters are also provided to manipulate the StackPanels’ Grid.RowSpan and Grid.ColumnSpan properties.
  • Why the parentheses around Grid.Row, Grid.Column, and other StackPanel properties? Because they’re attached properties. Ordinary properties don’t require parentheses in a setter’s Target property; attached properties do.
  • As this example demonstrates, a visual state isn’t limited to one setter. You can include as many setters as you’d like to adapt a view to a given visual state.

AdaptiveTrigger is the only state trigger currently included in Windows 10, but it’s easy to build state triggers of your own. Several are already out there in the wild on GitHub and other places. In a future post, I’ll explain how to build custom state triggers, and also explain why it’s sometimes useful to do so.

We deliver solutions that accelerate the value of Azure.

Ready to experience the full power of Microsoft Azure?

Start Today

Blog Home

Stay Connected

Upcoming Events

All Events