My blog about application development on the .NET platform and Windows®.

Implicit Data Templates in UWP

In 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.

Advertisements


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s