Implicit Data Templates in UWP
Posted: March 25, 2017 Filed under: C#, UWP, XAML | Tags: C#, UWP, XAML 1 CommentIn WPF the use of implicit data templates without an x:Key
makes it easy to associate a template with a particular type of object. You just set the DataType
property of the DataTemplate
to the corresponding type and the template is then applied automatically to all instances of that particular type.
The Universal Windows Platform (UWP) however has no concept of implicit data templates. Each DataTemplate
that you define in a UWP app must have an x:Key
attribute and it must be set to a string
value.
Let’s consider the following sample code where three different types are defined – Apple
, Banana
and Pear
. Each of these classes implement a common interface IFruit
and the view model exposes a collection of fruits that a ListBox
in the view binds to. The details of the currently selected fruit are displayed in a ContentControl
.
public interface IFruit { string Name { get; } } public class Apple : IFruit { public string Name => "Apple"; } public class Banana : IFruit { public string Name => "Banana"; } public class Pear : IFruit { public string Name => "Pear"; } public class ViewModel : INotifyPropertyChanged { public List<IFruit> Fruits { get; } = new List<IFruit>() { new Apple(), new Banana(), new Pear() }; private IFruit _selectedFruit; public IFruit SelectedFruit { get { return _selectedFruit; } set { _selectedFruit = value; NotifyPropertyChanged(); } } public event PropertyChangedEventHandler PropertyChanged; private void NotifyPropertyChanged([CallerMemberName] String propertyName = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); }
<Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition /> </Grid.ColumnDefinitions> <ListBox ItemsSource="{Binding Fruits}" SelectedItem="{Binding SelectedFruit}" DisplayMemberPath="Name" /> <ContentControl Content="{Binding SelectedFruit}" Grid.Column="1" /> </Grid>
In App.xaml
there is a merged resource dictionary that contains a specific DataTemplate
associated with each type of fruit that defines how the fruit is presented to the user on the screen.
<Application x:Class="Mm.Samples.ImplicitDataTemplates.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" RequestedTheme="Light"> <Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="DataTemplates.xaml" /> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources> </Application>
This kind of master/detail experience is a pretty common scenario to implement in a UI application. So how to solve this in a UWP app?
As mentioned before you must set the x:Key
attribute of a DataTemplate
in a UWP app to a string
and this basically means that the DataTemplate
isn’t implicit any more. The closest you get to an implicit DataTemplate
in UWP is to set the x:Key
attribute to a value that can be used to uniquely identify the type to which you want to apply the template. Like for example the name of the type:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <DataTemplate x:Key="Apple"> <TextBlock Text="I am an apple!" Foreground="Red" /> </DataTemplate> <DataTemplate x:Key="Banana"> <TextBlock Text="I am a banana!" Foreground="Yellow" /> </DataTemplate> <DataTemplate x:Key="Pear"> <TextBlock Text="I am a pear!" Foreground="Green" /> </DataTemplate> </ResourceDictionary>
You could then write a converter class that looks up the data template based on the type of the data object:
public class ImplicitDataTemplateConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, string language) { if (value == null || App.Current == null) return null; object dataTemplate; if (App.Current.Resources.TryGetValue(value.GetType().Name, out dataTemplate)) return dataTemplate; return null; } public object ConvertBack(object value, Type targetType, object parameter, string language) { throw new NotSupportedException(); } }
The last thing you need to do is then to bind the ContentTemplate
property of the ContentControl
to the SelectedFruit
property of the view model and use the converter to select the appropriate data template:
<Page x:Class="App1.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:App1" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <Page.DataContext> <local:ViewModel /> </Page.DataContext> <Page.Resources> <local:ImplicitDataTemplateConverter x:Key="ImplicitDataTemplateConverter" /> </Page.Resources> <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition /> </Grid.ColumnDefinitions> <ListBox ItemsSource="{Binding Fruits}" SelectedItem="{Binding SelectedFruit, Mode=TwoWay}" DisplayMemberPath="Name" /> <ContentControl Content="{Binding SelectedFruit}" ContentTemplate="{Binding SelectedFruit, Converter={StaticResource ImplicitDataTemplateConverter}}" Grid.Column="1" /> </Grid> </Page>
Compiled bindings support (x:Bind)
So far so food. Using this workaround, the template is being applied as expected. But what about {x:Bind}
? One of the nicest XAML features of the UWP is the support for compiled bindings. Unlike {Binding}
, the {x:Bind}
markup extension evaluates the binding expressions at compile-time which not only improves the runtime performance of the app but also makes it possible to detect binding errors when you build it.
To be able to use {x:Bind}
in a DataTemplate
you must set the x:DataType
attribute to the type to which the template is supposed to be applied. If you however try to do this in the DataTemplates.xaml
resource dictionary and build the application, you will get a compilation error:
<DataTemplate x:Key="Apple" x:DataType="local:Apple"> <TextBlock Text="I am an apple!" Foreground="Red" /> </DataTemplate>
{x:Bind}
generates code at compile-time and for this to work the XAML file must be associated with a class. This is an easy thing to fix though. You could just add a DataTemplates.xaml.cs
partial code-behind class to the folder where the DataTemplates.xaml
resource dictionary is located. In the constructor of the partial class you should call the InitializeComponent()
method to initialize the generated code:
public partial class DataTemplates : ResourceDictionary { public DataTemplates() { InitializeComponent(); } }
You then set the x:Class
attribute of the ResourceDictionary
element in the XAML file to the partial class name and then the application should build just fine.
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="Mm.Samples.ImplicitDataTemplates.DataTemplates" xmlns:local="using:Mm.Samples.ImplicitDataTemplates"> <DataTemplate x:Key="Apple" x:DataType="local:Apple"> <TextBlock Text="I am an apple!" Foreground="Red" /> </DataTemplate> <DataTemplate x:Key="Banana" x:DataType="local:Banana"> <TextBlock Text="I am a banana!" Foreground="Yellow" /> </DataTemplate> <DataTemplate x:Key="Pear" x:DataType="local:Pear"> <TextBlock Text="I am a pear!" Foreground="Green" /> </DataTemplate> </ResourceDictionary>
In order to be able to use {x:Bind}
in the MainPage XAML you should then modify the code-behind class a bit. The view model needs to be exposed in a strongly typed fashion for the compile-time safety to work. The source of a compiled binding is always the class itself, i.e. the Page
class in this case, rather than the DataContext
of the element. In this case, we could add a ViewModel
property to the MainPage
class that gets set whenever the DataContext
property is set:
public sealed partial class MainPage : Page { public MainPage() { this.InitializeComponent(); DataContextChanged += (s, e) => ViewModel = DataContext as ViewModel; } public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(MainPage), typeof(ViewModel), typeof(ImplicitDataTemplateConverter), new PropertyMetadata(null)); public ViewModel ViewModel { get { return (ViewModel)GetValue(ViewModelProperty); } set { SetValue(ViewModelProperty, value); } } }
And bind to the properties of this using {x:Bind}
in the XAML markup. Remember that the default mode of {x:Bind}
is OneTime
so you need to explicitly set the Mode
property to OneWay
for the Content
and the ContentTemplate
target properties of the ContentPresenter
to get updated when the source properties of the view model are set:
<ContentControl Content="{x:Bind ViewModel.SelectedFruit, Mode=OneWay}" ContentTemplate="{x:Bind ViewModel.SelectedFruit, Mode=OneWay, Converter={StaticResource ImplicitDataTemplateConverter}}" Grid.Column="1" />
Finally, it should be mentioned that this is indeed a workaround to be able to use something similar to implicit data templates in UWP. It is not perfect though. You may for example have several different types with the same type name in different namespaces or assemblies and then you need to come up with another x:Key
naming strategy for the data templates than simply using the short type name. Also there is no good way of determining whether the x:DataType
attribute of data template that the converter looks up actually matches the type of the data object that is passed to the converter at runtime.
So there are certainly pitfalls but the workaround presented in this post should hopefully be applicable and work just fine as-is or with some slight application-specific modifications in the vast majority of scenarios.
This is very clear and very clever. I have just attempted my first UWP app and am discouraged by the lack of implicit data templates. Not being able to use them makes implementing MVVM more difficult in my opinion.