Building a Classic Tabbed and Databound Desktop Application with UWP and MVVM

What kind of business applications do you build? Do they have a tabbed user interface? Most of mine do so.

After spiking (=prototyping) the Visual Studio Shell I wanted to go deeper into building a tabbed user interface with UWP, of course databound with MVVM.

As you might know, UWP does not contain a TabControl. But it contains a Pivot-Control that has pretty much of the functionality needed for a classic tabbed UI. So, let’s go with the Pivot and let’s see how far we can get.

The main motivation for this post is this entry on uservoice. Awesome Clint Rutkas from Microsoft raised these great questions:

  1. What is missing in the Pivot?
  2. Is there a great sample of a databound Pivot?

Hey Clint, I haven’t found a great sample. So, I decided to build one that we can use to point out what is missing – from the view of a desktop app developer.

The sample used in this post is available on GitHub:
https://github.com/thomasclaudiushuber/Uwp-Tab-Control-Spike

I leave the question open to you – highly appreciated blog readers – whether my sample is great or not :-), but at least my sample is using the Pivot in such a way how I use the TabControl today in a typical databound MVVM-driven WPF application.

Here is what I’ve implemented in my sample:

  • databound TabControl Pivot (using MVVM)
  • closable tab-items pivot-items
  • more than one type of detail view (DataTemplates)
  • styled the Pivot to make it more look like a classic TabControl

I got all these features implemented. And again, UWP felt like a pretty amazing technology. There was not everything perfect, but let’s look at these “non-perfect”-points later and let’s start with the sample.

Below you see a screenshot of the sample-application. Clicking on “Open new friend” or “Open new book” opens up a new tab and also adds that item to the navigation on the left.

When you click on the little X-button in the tab header, the tab closes… did I say “tab”? Of course I mean pivot-item.

In the ViewModel for a friend I check whether the friend has been changed by the user or not. If the friend has been changed, this popup appears when the user tries to close the tab:

Now let’s look at some code parts that are quite common to TabControl users. In the MainViewModel I’ve defined the properties Details and SelectedDetail like below (Note: this is not the full-blown MainViewModel):

public class MainViewModel : ViewModelBase
{
  private DetailViewModelBase _selectedDetail;

  public ObservableCollection Details { get; }

  public DetailViewModelBase SelectedDetail
  {
    get { return _selectedDetail; }
    set
    {
      _selectedDetail = value;
      OnPropertyChanged();
    }
  }
}

And in XAML, I use the Pivot’s ItemsSource-property and SelectedItem-property to bind to the MainViewModel‘s properties, as you can see below. I also set the HeaderTemplate to define the header with the text and the close button.

<Pivot ItemsSource="{x:Bind ViewModel.Details}" 
        SelectedItem="{x:Bind ViewModel.SelectedDetail,Mode=TwoWay}"
        ItemTemplateSelector="{StaticResource DetailViewTemplateSelector}">
  <Pivot.HeaderTemplate>
    <DataTemplate x:DataType="viewModel:DetailViewModelBase">
      <Grid>
        <Grid.ColumnDefinitions>
          <ColumnDefinition/>
          <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
        <TextBlock Text="{x:Bind Title,Mode=OneWay}"/>
        <Button Grid.Column="1" Content="X" Click="{x:Bind Close}"
                Style="{StaticResource PivotHeaderCloseButton}"/>
      </Grid>
    </DataTemplate>
  </Pivot.HeaderTemplate>
</Pivot>

Now let’s look at the parts that felt not perfect to me (Can a framework be perfect? :))

Some parts didn’t work as I would expect them to work. And some could be improved. But overall, the Pivot is quite powerful and gets pretty close to what I expect from a TabControl. The harder parts I’ve identified where these:

  1. Implicit DataTemplates
  2. Styling Headers
  3. Header Layout
  4. Getting Rid of the Content Transition

Let’s go through those items.

1. Implicit DataTemplates

Implicit DataTemplates don’t exist in UWP. Ok, it’s not fair to blame the Pivot for this, but anyway, it was one of the main issues I had. When you look at the Details-property of the MainViewModel, you can see that the items in that ObservableCollection are of type DetailViewModelBase. Two classes inherit from DetailViewModelBase in my app:

  • FriendDetailViewModel
  • BookDetailViewModel

I have also the corresponding views for these viewModels. The “views” are UserControls:

  • FriendDetailView
  • BookDetailView

Now if we would have implicit DataTemplates, I could do this on my Mainpage:

<Page.Resources>
  <DataTemplate DataType="{x:Type viewModel:FriendDetailViewModel}">
    <view:FriendDetailView/>
  </DataTemplate>
  <DataTemplate DataType="{x:Type viewModel:BookDetailViewModel}">
    <view:BookDetailView/>
  </DataTemplate>
</Page.Resources>

Then the Pivot would automatically grab the correct View for the corresponding ViewModel. Now I would be done!

But unfortunately, UWP does not support implicit DataTemplates. That means you have to implement a subclass of DataTemplateSelector and resolve the correct DataTemplate on your own.

I’ve implemented a class called DetailViewTemplateSelector. It looks as simple as below. In the SelectTemplateCore-method it takes the name of the received item. That item is in case of my application a ViewModel, as I want to use this selector on the Pivot. So the item is either a FriendDetailViewModel or a BookDetailViewModel. After the name of the item was grabbed, the code below looks into the Application’s Resources for a DataTemplate that is stored under a key that matches exactly the ViewModel’s name. The code returns that DataTemplate.

public class DetailViewTemplateSelector:DataTemplateSelector
{
  protected override DataTemplate SelectTemplateCore(object item)
  {
    var viewModelName = item.GetType().Name;
    return App.Current.Resources[viewModelName] as DataTemplate;
  }

  protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
  {
    return SelectTemplateCore(item);
  }
}

In the Application Resources that you can see below I’ve defined two DataTemplates. The x:Key-attribute is set to the name of the corresponding ViewModel, so that these DataTemplates are found by the logic implemented in the DetailViewTemplateSelector above. In addition, I’ve defined the DetailViewTemplateSelector itself as a resource, so that it can be referenced from the Pivot-element with the StaticResource-markup-extension:

<view:DetailViewTemplateSelector x:Key="DetailViewTemplateSelector"/>
<DataTemplate x:Key="FriendDetailViewModel">
  <view:FriendDetailView/>
</DataTemplate>
<DataTemplate x:Key="BookDetailViewModel">
  <view:BookDetailView/>
</DataTemplate>

The Pivot is using the defined DetailViewTemplateSelector to find the correct DataTemplate. All you need to do is to assign the DetailViewTemplateSelector to the ItemTemplateSelector-property of the Pivot:

<Pivot ItemsSource="{x:Bind ViewModel.Details}" 
        SelectedItem="{x:Bind ViewModel.SelectedDetail,Mode=TwoWay}"
        ItemTemplateSelector="{StaticResource DetailViewTemplateSelector}">
  <Pivot.HeaderTemplate>
    ...
  </Pivot.HeaderTemplate>
</Pivot>

Now this works as expected. But it would have been much simpler with implicit DataTemplates. For me, this scenario described here is the main scenario for implicit DataTemplates.

If you’d like to have implicit DataTemplates in UWP, vote for them here on uservoice.

2. Styling

By default, the headers of the Pivot looked like below in my application. The selected item just gets a lighter font, but there’s not the typical background-change

I have overriden some Theme-Resources in my App.xaml-file, as you can see below. These resources are used by the default PivotItemHeader-style.

<ResourceDictionary.ThemeDictionaries>
  <ResourceDictionary x:Key="Default">
    <SolidColorBrush x:Key="PivotHeaderItemBackgroundSelected" Color="#007ACC"/>
    <SolidColorBrush x:Key="PivotHeaderItemBackgroundUnselected" Color="#333333"/>
    <SolidColorBrush x:Key="PivotHeaderItemBackgroundUnselectedPointerOver" Color="#555555"/>

    <!--Without this the header is not aligned with the content-->
    <Thickness x:Key="PivotItemMargin">0</Thickness>
  </ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>

With these resources set, the headers in the Pivot look now like this, and I’m happy:

Now you might ask:

But hey Thomas, how did you find out these keys for the brushes?

If you really want to dive into the details, you can go to

C:\Program Files (x86)\Windows Kits\10\DesignTime\CommonConfiguration\Neutral\UAP\10.0.14393.0\Generic

on your machine and open up the generic.xaml-file in that folder. Search for the PivotItemHeader-style and you’ll find the keys that I’ve used in my App.xaml-file.

Of course, you can adjust more than I did in my sample-app. You can even define a complete new style for the PivotItemHeader. But you need to find out how to do it, and this is what could be simpler from my point of view.

I see different options how this could be improved:

  • with a default style for the Pivot for desktop apps
  • or with a simple way for developers to adjust the header style. I’m thinking of a HeaderStyle-property, analog to the HeaderTemplate-property that is already available on the Pivot

3. Header Layout

The headers in the Pivot are stacked horizontally. If you open up a lot of items in the Pivot, you can’t see all the items any more and headers are cutted:

There are several options that could improve this Header-Layout problem:

  • an overflow-panel with the option to turn it on/off
  • a Mode-property on the Pivot that you set to “desktop” or “universal” (I’m not sure about the name of the second option. In doubt, the Mode-property can be a boolean ;-)) to turn the overflow-panel on. You could combine this Mode-property even with the styling to get the desktop-style look for the Pivot if you set the Mode to “desktop”.
  • wrapping the headers to a new line when there’s not enough horizontal space

I think an overflow-panel and the option to wrap the headers would make sense for a desktop app.

4. Getting Rid of the Content Transition

When you select a Pivot item, there’s a slide-in-animation/-transition for its content. The content slides in from the left.

And now I feel like a beginner. :) I was not able to get rid of this content transition/animation. But I don’t want that animation in my enterprise application. It’s too much for me. I want the content to be there without an intro-animation.

I played around with the Style of the Pivot, but didn’t manage to change this animation. And I’m not the only one: See for example this Stackoverflow-Thread.

I have the feeling that this entrance-animation is baked into the Pivot-class itself, into the code and not accessible through the Style. I guess the code grabs a named transformation from the Pivot-Style and executes an animation on such a transformation. But maybe I’m wrong. If someone knows how to get rid of that entrance animation, please comment.

But anyway, also for this case I would like a simpler way to get rid of the animation. This could be a simple property like “IsContentEntranceAnimationEnabled”.

Summary: The Pivot is quite powerful to build a tabbed UI. There are only some points that are a bit harder:
Styling and the Header-layout. Everything else is great! I’d love to have implicit DataTemplates, but as this post shows, it’s not impossible to live without them.

Thanks for reading,
Thomas

Share this post

Comments (2)

  • Philip Reply

    Great article! I love the idea of being able to have different tabbed pages open.

    One question: how well would this concept work on smaller screens, e.g. phone? If not well, is there an (easy) way to fall back to a UI model that does work well? So, I guess, if the screen isn’t big enough to support tabbed pages, is there a way to just display one? Or does the pivot control “just work” on screens of different sizes anyway?

    March 13, 2017 at 2:32 pm

Leave a Reply

Your email address will not be published. Required fields are marked *

*