Kristoffer Berge Kristoffer Berge - 29 days ago 14
C# Question

Conditional xaml layout based on class in UWP

I have a data model with inheritance and I want to display the right fields for each subclass in my xaml markup.

public abstract class Model {
public int Id { set; get; }
}
public class ModelOne : Model {
public int Tasks { set; get; }
}
public class ModelTwo : Model {
public DateTime date { set; get; }
}


The data context of my xaml will be a field of type Model. Each model has different fields that i want to display, but the rest of the xaml will be the same, so I hope I can avoid creating two views. I could create a converter that converts class to visibility, but i don't think this would be the best solution. Is there any features in UWP-xaml that could help me achieve this?

Answer

There are a variety of ways to approach this. But for me, the simplest and most logical is to create DataTemplate resources as usual, but have the templates for the more-derived classes use the template for the base class.

For example, given model classes that look like this:

class MainModel
{
    public Model BaseModel { get; set; }
    public ModelOne ModelOne { get; set; }
    public ModelTwo ModelTwo { get; set; }
}

class Model
{
    public int BaseValue { get; set; }
}

class ModelOne : Model
{
    public int OneValue { get; set; }
}

class ModelTwo : Model
{
    public int TwoValue { get; set; }
}

You can write XAML that looks like this:

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

  <Page.DataContext>
    <l:MainModel>
      <l:MainModel.BaseModel>
        <l:Model BaseValue="17"/>
      </l:MainModel.BaseModel>
      <l:MainModel.ModelOne>
        <l:ModelOne BaseValue="19" OneValue="29"/>
      </l:MainModel.ModelOne>
      <l:MainModel.ModelTwo>
        <l:ModelTwo BaseValue="23" TwoValue="37"/>
      </l:MainModel.ModelTwo>
    </l:MainModel>
  </Page.DataContext>

  <Page.Resources>
    <DataTemplate x:Key="baseModelTemplate"  x:DataType="l:Model">
      <TextBlock Text="{Binding BaseValue}"/>
    </DataTemplate>
    <DataTemplate x:Key="modelOneTemplate" x:DataType="l:ModelOne">
      <StackPanel>
        <ContentControl Content="{Binding}" ContentTemplate="{StaticResource baseModelTemplate}"/>
        <TextBlock Text="{Binding OneValue}"/>
      </StackPanel>
    </DataTemplate>
    <DataTemplate x:Key="modelTwoTemplate" x:DataType="l:ModelTwo">
      <StackPanel>
        <ContentControl Content="{Binding}" ContentTemplate="{StaticResource baseModelTemplate}"/>
        <TextBlock Text="{Binding TwoValue}"/>
      </StackPanel>
    </DataTemplate>
  </Page.Resources>

  <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <StackPanel Orientation="Horizontal"
                HorizontalAlignment="Center" VerticalAlignment="Center">
      <ContentControl Content="{Binding BaseModel}" Margin="5"
                    ContentTemplate="{StaticResource baseModelTemplate}"/>
      <ContentControl Content="{Binding ModelOne}" Margin="5"
                    ContentTemplate="{StaticResource modelOneTemplate}"/>
      <ContentControl Content="{Binding ModelTwo}" Margin="5"
                    ContentTemplate="{StaticResource modelTwoTemplate}"/>
    </StackPanel>
  </Grid>
</Page>

The above might be overkill for classes that look literally like the examples in your question. But for more complex view models, this works well. The derived classes can reuse the base class template, but have some control over how that template is presented (by virtue of being able to put the ContentControl wherever is needed in the template).

In addition to allowing reuse of the base class template in any derived class template, this also avoids the need for a single template that includes elements with bindings for all possible view models. Not only would such an approach result in over-weight visual trees at runtime, you'd get lots of binding errors as well, since the hidden elements will still be trying to bind to non-existing properties on the view model.

Reusing the base class template in derived class templates avoids all that, and to me fits better with the general architecture of the view model class inheritances.


Note that this is somewhat different from the way it would be done in WPF:

<DataTemplate DataType="{x:Type lm:Model}">
  <!-- template definition here...for example: -->
  <StackPanel>
    <TextBlock Text="{Binding Id, StringFormat=Id: {0}}"/>
  </StackPanel>
</DataTemplate>

<DataTemplate DataType="{x:Type lm:ModelOne}">
  <!-- template for ModelOne here; a ContentControl as shown below should be placed
       in the as needed for your desired visual appearance. For example,
       here is a template using a StackPanel as the top-level element,
       with the base class template shown as the first item in the panel -->
  <StackPanel>
    <ContentControl Content="{Binding}" Focusable="False">
      <ContentControl.ContentTemplate>
        <StaticResourceExtension>
          <StaticResourceExtension.ResourceKey>
            <DataTemplateKey DataType="{x:Type lm:Model}"/>
          </StaticResourceExtension.ResourceKey>
        </StaticResourceExtension>
      </ContentControl.ContentTemplate>
    </ContentControl>
    <TextBlock Text="{Binding Tasks, StringFormat=Tasks: {0}}"/>
  </StackPanel>
</DataTemplate>


<DataTemplate DataType="{x:Type lm:ModelTwo}">
  <!-- template for ModelTwo here; same as above -->
  <StackPanel>
    <ContentControl Content="{Binding}" Focusable="False">
      <ContentControl.ContentTemplate>
        <StaticResourceExtension>
          <StaticResourceExtension.ResourceKey>
            <DataTemplateKey DataType="{x:Type lm:Model}"/>
          </StaticResourceExtension.ResourceKey>
        </StaticResourceExtension>
      </ContentControl.ContentTemplate>
    </ContentControl>
    <TextBlock Text="{Binding date, StringFormat=date: {0}}"/>
  </StackPanel>
</DataTemplate>

(Where, of course, lm: is whatever your actual XML namespace for your model class types is.)

Unfortunately, it looks like along with many other useful WPF features, UWP (and previously WinRT, Phone, Silverlight, etc.) is missing automatic data template resource key definition and lookup. The WPF example takes advantage of this, even using the model type as the key for the base class data template resource reference.

In UWP, it appears that all data template resources must be given a key explicitly, and be referenced explicitly, either inline to a template property (e.g. ContentTemplate or ItemTemplate), or via a resource reference (e.g. {Static Resource ...}).

The documentation tantalizingly hints at the possibility of using automatic lookup [emphasis mine]:

All resources need to have a key. Usually that key is a string defined with x:Key=”myString”. However, there are a few other ways to specify a key:

  • Style and ControlTemplate require a TargetType, and will use the TargetType as the key if x:Key is not specified. In this case, the key is the actual Type object, not a string. (See examples below)
  • DataTemplate resources that have a TargetType will use the TargetType as the key if x:Key is not specified. In this case, the key is the actual Type object, not a string.
  • x:Name can be used instead of x:Key. However, x:Name also generates a code behind field for the resource. As a result, x:Name is less efficient than x:Key because that field needs to be initialized when the page is loaded.

But the XAML editor and compiler doesn't recognize a DataTemplate.TargetType property, there's no mention of it in the DataTemplate class documentation, x:DataType doesn't avoid the need to still define an x:Key property for the resource, and I don't see a way to use an actual Type reference as the resource key explicitly.

I can only surmise that the documentation page is in fact incorrect. Maybe some lazy tech writer just copy/pasted from the WPF? I don't know.

So the UWP example above goes with simple {StaticResource ...} references coded explicitly where needed.

Of course, another option in UWP is to use DataTemplateSelector, a WPF feature that appears to still be supported in UWP. Here is a related question that includes an example of one way to use a selector: UWP DataTemplates for multiple item types in ListView. Of course, there are many other reasonable ways to initialize and use a DataTemplateSelector. It's basically the fallback when the behaviors automatically supported in XAML don't suffice, and when implementing one, you can do it however makes the most sense to you.

Comments