Spicing Up Universal Windows Apps with Per-Platform Views and Compiled Data Bindings

Adaptability is one of the hallmarks of the Universal Windows Platform (UWP). Apps that run on devices large and small must be able to adapt their UIs to the screen space available. UWP offers a number of tools for building adaptive UIs, including AdaptiveTrigger, RelativePanel, and controls that are cognizant of the screens they’re running on and willing to adapt themselves to provide the best user experience possible.

But sometimes the best way to optimize the experience on a particular device is to design the UI just for that device. I ran into that while porting Contoso Cookbook from Windows 8.1 to Windows 10. On a large screen, I wanted to use a GridView control to present rows and columns of recipes, starting in the upper-left corner and moving to the right, wrapping as needed to fill the screen with delicious recipes. On a phone, I envisioned a different experience built around a ListView control with a scrolling column of recipes.

Fortunately, UWP anticipates such scenarios and makes it rather easy to tailor UIs to individual devices – specifically, to device families: mobile devices, desktop devices, and so on. While doing the port, I also decided to leverage a new feature of Windows 10 that delivers faster performance and lower memory consumption. That feature is compiled data bindings, and it not only delivers at run-time, it improves the development experience, too.

Contoso Cookbook on a PC

Let’s start by seeing how the app looks when it’s running on a desktop device – that is, a PC. Here’s the opening page:

Cookbook Main Page

The headers at the top – “Chinese,” “French,” and so on – are provided by a Pivot control. Pivot controls were introduced in Windows Phone 7, but have only now, with UWP, made their way to the PC. Initially, Chinese recipes are displayed, but you can tap a header to show recipes of a different type. Selecting a recipe navigates to Contoso’s recipe-detail page:

Cookbook Recipe Page

If that page looks familiar, it’s probably because I recently used it an article on AdaptiveTrigger. If you make the window narrow enough, the content reflows and assumes a vertical rather than horizontal layout. I added a Back button in the top-left corner of the page so you can navigate back to the main page after viewing a recipe.

Here’s the XAML for the main page in MainPage.xaml:

<Page
  x:Class="ContosoCookbook.MainPage"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="using:ContosoCookbook"
  xmlns:data="using:ContosoCookbook.Data"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  mc:Ignorable="d">
    
  <Grid Margin="24" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Pivot ItemsSource="{x:Bind Recipes}" Margin="-12">
      <Pivot.HeaderTemplate>
        <DataTemplate x:DataType="data:RecipeGroup">
          <TextBlock Text="{x:Bind Title}" Style="{ThemeResource SubheaderTextBlockStyle}"
            Foreground="#C33D27" />
        </DataTemplate>
      </Pivot.HeaderTemplate>
      <Pivot.ItemTemplate>
        <DataTemplate x:DataType="data:RecipeGroup">
          <GridView ItemsSource="{x:Bind Recipes}" SelectionMode="None"
            IsItemClickEnabled="True" ItemClick="OnRecipeClicked">
            <GridView.ItemTemplate>
              <DataTemplate x:DataType="data:Recipe">
                <Grid Width="310" Margin="0,20,20,20">
                  <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="*"/>
                  </Grid.RowDefinitions>
                  <Image Source="{x:Bind ImagePath}" Height="180" Stretch="UniformToFill" />
                  <StackPanel Grid.Row="1" Margin="0,8,0,0">
                    <TextBlock Text="{x:Bind Subtitle}" Style="{StaticResource TitleTextBlockStyle}" />
                    <TextBlock Text="{x:Bind Description}" Style="{StaticResource BodyTextBlockStyle}"
                      MaxLines="2" TextTrimming="WordEllipsis" />
                  </StackPanel>
                </Grid>
              </DataTemplate>
            </GridView.ItemTemplate>
          </GridView>
        </DataTemplate>
      </Pivot.ItemTemplate>
    </Pivot>
  </Grid>
</Page>

Right off the bat, you may notice a couple of things in this page that you’re not accustomed to seeing: x:DataType attributes on the DataTemplates, and {x:Bind} expressions where you’d normally see {Binding} expressions. This is compiled data binding, and it’s one of the coolest new features of Windows 10.

Bindings created with {Binding} statements are late-bound. At run-time, Binding objects are created and used to marry data sources to data targets. But binding objects require memory, and they add performance overhead in the same way that late-bound IDispatch interfaces add overhead to COM calls.

Bindings created with {x:Bind} statements are different. They get compiled. Rather than emit code to instantiate and initialize a Binding object, the parser emits code that connects the data target directly to the data source. It’s analogous to the difference between a compiled language and an interpreted language. Compiled bindings perform faster and consume less memory. (In a session on XAML data binding at Microsoft Build last week, Sam Spencer showed numbers quantifying the differences. They were eye-opening.) Moreover, they enable compile-time type checking. If you misspell a property name in a {Binding} statement, you don’t learn about it until run-time, and even then it’s not always obvious where the error is; the binding simply doesn’t work. But misspell a property name in a {x:Bind} statement, and you’ll be notified when you build the project. That alone is worth the price of admission.

There is one requirement for compiled data binding to work: the compiler has to know what type you’re binding against. That’s the purpose of x:DataType attributes on the DataTemplates; without them, the compiler would have no way of type-checking the property names. For {x:Bind} statements outside of data templates, the compiler assumes that the property names provided are properties of the page. In MainPage.xaml.cs, I declared the following property in the page:

private ObservableCollection<RecipeGroup> Recipes; // For compiled data binding

I then overrode OnNavigatedTo and initialized the Recipes property (rather than DataContext) with recipe data:

protected async override void OnNavigatedTo(NavigationEventArgs e)
{
    base.OnNavigatedTo(e);

    if (this.Recipes == null)
        this.Recipes = await RecipeDataSource.GetGroupsAsync();
}

The property declaration allowed the following statement to compile; the property initialization allowed it to work:

<Pivot ItemsSource="{x:Bind Recipes}" Margin="-12">

There a few limitations to compiled data bindings. For example, you can’t use value converters in {x:Bind} statements, because the compiler has no way of knowing what data type a value converter returns. Look in RecipePage.xaml and you’ll notice lots of {x:Bind} statements and one {Binding} statement; the latter uses a value converter to convert an array of strings into a single string, so using a compiled data binding wasn’t an option. Also, while the default binding mode for traditional bindings is OneWay, compiled data bindings default to OneTime. You can use set the binding mode to OneWay or TwoWay if circumstances require it, but doing so decreases performance.

Contoso Cookbook on a Phone

When run on a phone, Contoso Cookbook uses the same compiled data bindings that it does on a PC. It also recasts the UI to provide the best possible experience on a small screen. Here’s how it looks on a Windows phone:

Phones

Rather than use GridView controls as pivot items on the main page, it uses ListView controls. And rather than use the same recipe page shown on the desktop, the app uses a variation of that page tweaked for the small screen.

Most of the talk around adaptive rendering in UWP has to do with making one page look great on a wide range of devices. But UWP also allows you to provide different versions of a page for different device families. It’s not documented very well just yet, but all you have to do is add a folder to the solution for each device family you wish to target with custom UIs, and then add views to those folders. A view is nothing more than a XAML file without a corresponding code-behind file. You add them by right-clicking the folder in Visual Studio, clicking “Add New Item,” and selecting “XAML View.” Contoso Cookbook contains two such views: MainPage.xaml and RecipePage.xaml. Both are located in a folder that I created named DeviceFamily-Mobile:

Cookbook Solution

Crack open the mobile version of MainPage.xaml and you’ll find that it’s similar to the one in the root of the solution, but it replaces GridView with ListView and contains subtle tweaks to the layout, such as a reduced margin around the root grid:

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

  <Grid Margin="8" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Pivot ItemsSource="{x:Bind Recipes}">
      <Pivot.HeaderTemplate>
        <DataTemplate x:DataType="data:RecipeGroup">
          <TextBlock Text="{x:Bind Title}" Style="{ThemeResource SubheaderTextBlockStyle}" Foreground="#C33D27" />
        </DataTemplate>
      </Pivot.HeaderTemplate>
      <Pivot.ItemTemplate>
        <DataTemplate x:DataType="data:RecipeGroup">
          <ListView ItemsSource="{x:Bind Recipes}" SelectionMode="None"
            IsItemClickEnabled="True" ItemClick="OnRecipeClicked" Margin="0,16,0,0">
            <ListView.ItemTemplate>
              <DataTemplate x:DataType="data:Recipe">
                <Grid Margin="-16,8,0,8" Height="100">
                  <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="*" />
                  </Grid.ColumnDefinitions>
                  <Image Source="{x:Bind ImagePath}" />
                  <Grid Grid.Column="1" Margin="16,-4,0,0" VerticalAlignment="Top">
                      <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="*" />
                      </Grid.RowDefinitions>
                      <TextBlock Text="{x:Bind Subtitle}" Style="{StaticResource TitleTextBlockStyle}" />
                      <TextBlock Text="{x:Bind Description}" Style="{StaticResource BodyTextBlockStyle}"
                        Grid.Row="1" TextWrapping="WrapWholeWords" TextTrimming="WordEllipsis" />
                  </Grid>
                </Grid>
              </DataTemplate>
            </ListView.ItemTemplate>
          </ListView>
        </DataTemplate>
      </Pivot.ItemTemplate>
    </Pivot>
  </Grid>
</Page>

It’s important to note that this is merely a view and not a page. The same code-behind file used for MainPage.xaml in the root of the solution is used for this one, too. And there must be fidelity between the two. If MainPage.xaml.cs contains a reference to a control that’s present in the main MainPage.xaml but not in the mobile version, the app will throw an exception on a phone. This is added justification for using the MVVM pattern in your apps, because MVVM pages tend to have little in the way of code-behind anyway.

The Back Button without Extension SDKs

In a previous post, I demonstrated how to respond to presses of the Back button on a phone by installing the Microsoft Mobile Extension SDK and processing HardwareButtons.BackPressed events. I also showed how to use ApiInformation.IsTypePresent to avoid referencing HardwareButtons (and throwing an exception) on non-mobile devices.

Contoso Cookbook uses a different approach to going back to the main page from the recipe page when the phone’s Back button is pressed – one that requires no extension SDKs and no run-time API checks. Here’s the relevant code in RecipePage.xaml.cs:

// Respond to Back-button events
SystemNavigationManager.GetForCurrentView().BackRequested += OnBackRequested;

The event never fires on a non-mobile device, but SystemNavigationManager is present in the core UWP, so it can safely be called on any device. In the future, BackRequested events will fire on desktop devices, too, as well as provide aid in implementing software Back buttons.

Summary

You can download the Visual Studio solution containing Contoso Cookbook and run it yourself if you have the Visual Studio 2015 RC and the Windows 10 tools installed. I’ll be doing a talk on UWP apps at the Software Design & Development conference in London next week. I’d love for you to attend if you’re at the conference – which, by the way, is my favorite overseas programming conference. And if your company is planning to write UWP apps and needs help from Wintellect, let us know. We do a lot of app-dev work for a lot of different customers, and it is always a pleasure to put a new platform to work building real-world solutions.

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