Search code examples
.netwpfxaml.net-4.6.2

DataTemplates and Generics


I have read nearly a thousand posts explaining that setting a closed generic type as DataType on a DataTemplate does not work, because WPF wouldn't support that. But as a matter of fact, this is just wrong.

I can define the following DataTemplate in my Window.Resources and it will be used when I assign a list of strings to a content control. For example:

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:System="clr-namespace:System;assembly=mscorlib"
        xmlns:Generic="clr-namespace:System.Collections.Generic;assembly=mscorlib"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <DataTemplate DataType="{x:Type TypeName=Generic:List`1[System.String]}">
            <TextBlock Text="Hi List of Strings"
                       FontSize="40"
                       Foreground="Cyan"/>
        </DataTemplate>
    </Window.Resources>
    <Grid>
        <ContentControl x:Name="_contentControl">
        </ContentControl>
    </Grid>
</Window>

and in code-behind:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        _contentControl.Content = new List<string> { "Huhu" };
    }
}

With this setup you will see "Hi List of Strings". For me that's the proof that I can define generic types as DataType. But I want to take it one step further: I'd like to define a Dictionary<string, string> as DataType. But unfortunately, I can't get it to work.

So the question is: How can I define a Dictionary<string, string> as DataType of a DataTemplate?

If you know the answer, you can stop reading. But since it is good practice to show what I already did, I keep writing. What did I do already? At first I went brute-force and tried several combinations similar to:

- DataType="{x:Type TypeName=Generic:Dictionary`2[System.String];[System.String]}"
- DataType="{x:Type TypeName=Generic:Dictionary`2[System.String],[System.String]}"
- DataType="{x:Type TypeName=Generic:Dictionary`2[System.String,System.String]}"

But since none of them worked, I dove into System.Xaml and looked at TypeExtension, GenericTypeNameParser and GenericTypeNameScanner, because I thought that these are the codelines which resolve the type. But looking at the code I realized that ` is an invalid character.

To prove it, I wrote my own MarkupExtension

public class UseTheTypeExtensionsParser : MarkupExtension
{
    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        var a = new TypeExtension("Generic:List`1[[System.String]]");
        var type = a.ProvideValue(serviceProvider);
        return type.ToString();
    }
}

and used it as follows:

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:System="clr-namespace:System;assembly=mscorlib"
        xmlns:Generic="clr-namespace:System.Collections.Generic;assembly=mscorlib"
        xmlns:WpfApp1="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <ContentControl Content="{WpfApp1:UseTheTypeExtensionsParser}"/>
    </Grid>
</Window>

And this threw the exception that the character ` was not expected and that the XAML-type is invalid.

That got me wondering why my first example worked. I think, that on markup-compiling the XAML for WPF, it is not the TypeExtension that is used for resolving the XamlType, but i think that the XamlNamespace is used. Because this class has the MangleGenericTypeName-method which uses the `-character. But I still can't see the code which extracts the type arguments, so I cannot see the correct syntax to specify the type arguments for the Dictionary. This is where I am stuck.

(Needless to say that the Microsoft-Docs are worthless on this topic.)

Edit: Since it seems unclear why I want this, I will explain it: I want the automatic selection of a ContentTemplate of the ContentControl. And of course: my constructed DataTemplate in the example is very simple. But everyone should be able to imagine, that I want different DataTemplates for Lists, for Dictionaries or for simple strings.

I have a ViewModel which has a public object Result { get; } Property. And sometimes, the result is an int, sometimes a string, sometimes a List and so on and so forth. I am binding this Result-property to the Content-Property of a ContentControl. And for all the types mentioned, I wrote different DataTemplates which are automatically selected by WPF. So ints are shown in a Rectangle and Strings are shown in an Ellipse.

After I got all this to work, I want another DataTemplate, but this time for a Dictionary.


Solution

  • I got it to work with the following code:

    Write a MarkupExtension which returns the closed generic type you want as DataType for your DataTemplate (this is not my own. It is somewhere from SO, but I didn't keep the link).

    public class GenericType : MarkupExtension
    {
        public GenericType() { }
    
        public GenericType(Type baseType, params Type[] innerTypes)
        {
            BaseType = baseType;
            InnerTypes = innerTypes;
        }
    
        public Type BaseType { get; set; }
        
        public Type[] InnerTypes { get; set; }
    
        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            Type result = BaseType.MakeGenericType(InnerTypes);
            return result;
        }
    }
    

    Use it as follows:

    <Window x:Class="WpfApp1.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:System="clr-namespace:System;assembly=mscorlib"
            xmlns:Generic="clr-namespace:System.Collections.Generic;assembly=mscorlib"
            xmlns:WpfApp1="clr-namespace:WpfApp1"
            mc:Ignorable="d"
            Title="MainWindow" Height="450" Width="800">
        <Window.Resources>
            <x:Array Type="{x:Type System:Type}" 
                     x:Key="ListWithTwoStringTypes">
                <x:Type TypeName="System:String" />
                <x:Type TypeName="System:String" />
            </x:Array>
    
            <WpfApp1:GenericType BaseType="{x:Type TypeName=Generic:Dictionary`2}" 
                               InnerTypes="{StaticResource ListWithTwoStringTypes}"
                               x:Key="DictionaryStringString" />
    
            <DataTemplate DataType="{StaticResource DictionaryStringString}">
                <TextBlock Text="Hi Dictionary"
                       FontSize="40"
                       Foreground="Cyan"/>
            </DataTemplate>
        </Window.Resources>
        <Grid>
            <ContentControl x:Name="_contentControl"/>
        </Grid>
    </Window>
    

    To see if the DataTemplate is automatically applied, use can write in code-behind:

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            _contentControl.Content = new Dictionary<string, string>();
        }
    }
    

    And you will see your DataTemplate.

    But in my project, I have a dedicated assembly for the styles in which I write all my DataTemplates and ControlTemplates. Usually I have a ResourceDictionary which holds them. But when I want to put my DataTemplate in a ResourceDictionary, the compiler tells me that it would not have a Key.

    This does not work:

    <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                        xmlns:System="clr-namespace:System;assembly=mscorlib"
                        xmlns:DataTemplates="clr-namespace:Dana.Styles.Flat.DataTemplates"
                        xmlns:Generic="clr-namespace:System.Collections.Generic;assembly=mscorlib">
    
        <x:Array Type="{x:Type System:Type}" 
                 x:Key="ListWithTwoStringTypes">
            <x:Type TypeName="System:String" />
            <x:Type TypeName="System:String" />
        </x:Array>
    
        <DataTemplates:GenericType BaseType="{x:Type TypeName=Generic:Dictionary`2}" 
                                   InnerTypes="{StaticResource ListWithTwoStringTypes}"
                                   x:Key="DictionaryStringString" />
    
        <DataTemplate DataType="{StaticResource DictionaryStringString}">
    
            <TextBlock Text="Hi Dictionary"
                       FontSize="40"
                       Foreground="Cyan"/>
        </DataTemplate>
    
    </ResourceDictionary>
    

    As a workaround, I am now defining my DataTemplates in the Resources of a FrameworkElement and add them in code-behind to the Application.Resources.

    This is DictionaryStringString.xaml

    <FrameworkElement x:Class="Dana.Styles.Flat.DataTemplates.DictionaryStringString"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
                 xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                 xmlns:Generic="clr-namespace:System.Collections.Generic;assembly=mscorlib"
                 xmlns:DataTemplates="clr-namespace:Dana.Styles.Flat.DataTemplates"
                 xmlns:System="clr-namespace:System;assembly=mscorlib"
                 mc:Ignorable="d" 
                 d:DesignHeight="450" d:DesignWidth="800">
        <FrameworkElement.Resources>
    
            <x:Array Type="{x:Type System:Type}" 
                     x:Key="ListWithTwoStringTypes">
                <x:Type TypeName="System:String" />
                <x:Type TypeName="System:String" />
            </x:Array>
    
            <DataTemplates:GenericType BaseType="{x:Type TypeName=Generic:Dictionary`2}" 
                                       InnerTypes="{StaticResource ListWithTwoStringTypes}"
                                       x:Key="DictionaryStringString" />
    
            <DataTemplate DataType="{StaticResource DictionaryStringString}">
    
                <TextBlock Text="Hallo Wörterbuch"
                               FontSize="40"
                               Foreground="Cyan"/>Template>
                </ItemsControl>-->
            </DataTemplate>
        </FrameworkElement.Resources>
    </FrameworkElement>
    

    This is DictionaryStringString.xaml.cs:

    public partial class DictionaryStringString
    {
        /// <summary>
        /// Konstruktor
        /// </summary>
        public DictionaryStringString()
        {
            InitializeComponent();
        }
    }
    

    And then, where I initialize my styles I added:

    var _dictionaryStringString = new DictionaryStringString();
    Application.Current.Resources.MergedDictionaries.Add(_dictionaryStringString.Resources);
    

    And now I can define DataTemplates for all closed generic types and get them automatically applied by WPF =)