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:
- What is missing in the Pivot?
- 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
TabControlPivot (using MVVM) - closable
tab-itemspivot-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:
- Implicit DataTemplates
- Styling Headers
- Header Layout
- 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 theHeaderTemplate
-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
Comments (5)
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?
Hi Philip, thanks for the feedback. The Pivot does resize pretty well. Here’s an overview of it: https://blogs.windows.com/buildingapps/2016/06/27/scaling-your-phone-app-design-to-all-uwp-device-families/ And if this doesn’t work as expected, you can still build an adaptive layout that gives you all the possibilities to adapt and react to a small screen size .
Excellent article Claudius! Really a clever way to work around implicit binding! I watched all your courses in pluralsite and I use implicit binding in the same way you explained it “Building an Enterprise App with WPF, MVVM, and Entity Framework Code First” .
I missed the implicit binding in UWP until I found this article. I tried this code and found an issue when I try to navigate to other page after the pivot items are materialized. When you navigate back to the page all pivot items are cleared and looks empty. Even setting this.NavigationCacheMode = Windows.UI.Xaml.Navigation.NavigationCacheMode.Required, the pivots are cleared when you navigate back to this page. Do you think this is an issue in the ItemTemplateSelector of the pivot or something else? I appreciate a lot if you can help me in this issue.
Hi Mohamad,
thanks for this comment. I think instead of investigating into this issue, we should try to use the newer UWP TabControl instead.
I’m busy for the next weeks, but then I think I could bring up a sample.
Thomas
Thanks for your response Thomas. I tried the new UWP TabControl, but I could not get it works as expected. The tab item contents are rendered as the type name and not the usercontrol view. I posted the issue with a sample on stackoverflow but could not get a solution. Here is the link of the post https://stackoverflow.com/questions/59886701/uwp-tabview-itemtemplateselector-not-working. Thanks in advance