DataTemplates and Generics
Asked Answered
L

1

1

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.

Louque answered 8/1, 2019 at 13:23 Comment(12)
You don't have any generic types here. You have a concrete type, List<string>. A generic type would be List<T>. You can't use generic types simply because the compiler has no way of knowing what's included in those generic typesInconsequential
And no, MS docs aren't worhtless, nor are all those people saying that you can't use generic types wrong. WPF came out 10 years ago after all, someone would have noticed. Almost all tutorials and docs show binding to generic types anyway, whether it's a List<SomeEntity> or an ObservableCollection<SomeOtherEntity> or something returned by EFInconsequential
Never mind the question should be closed as is, and I didn't downvote. Why do you think you need to specify a type like this? What is the actual problem you want to solve? A data template is typically used to display items in a container, which is why you'll see strings as DataType but never IEnumerable<string>. Containers are rendered by controls derived from ItemsControls which knows about IEnumerable alreadyInconsequential
And finally, you don't even need to specify DataType in a DataTemplate. WPF data binding works with reflection so you don't need to specify the type as long as the object the template binds to has properties that satisfy the binding expressions.Inconsequential
@Panagiotis Kanavos: I think you missed the point here. Besides that, the first paragraph aimed to all those people who said, that I should derive from List<string> and create a non-generic type which closes List<T>. Some example: https://mcmap.net/q/271299/-how-to-reference-a-generic-type-in-the-datatype-attribute-of-a-datatemplate. And if almost all tutorials and docs show binding to generic types anyway, why don't you just answer the question?Louque
@Panagioti The question also makes no sense to me. However, in the example constructed in the question, the DataType property is necessary for automatic selection of a ContentTemplate of the ContentControl.Obligor
@Louque It's entirely unclear why at all you would want to assign a collection instance to the Content property of a ContentControl. That doesn't seem to make much sense. Instead, you should assign the collection to the ItemsSource property of an ItemsControl, and have a DataTemplate (without DataType) as the ItemTemplate of the ItemsControl.Obligor
@Obligor and that would still need an ItemsControl to display the items. Even if some conversion was needed, a converter would do the job. No need to create a markup extensionInconsequential
@Louque what you want is already available. Your assumptions are wrong - you can't use a generic type without specifying the type parameters. Your code doesn't do that anyway. I suspect you should check the tutorials on data binding. For example you don't set the content property directly. That binds the View to the ViewModel. You let data binding expressions bind each attribute to a typeInconsequential
@Louque there's nothing undocumented about type names either. People simply don't do what you tried to do though. Items typically have far more than two properties which makes it a lot easier to use ItemsControl, DataTemplate and bindings directly to property namesInconsequential
@Louque someone else downvoted this question. Everyone else abandoned it. Instead of picking on the only people that tried to help (Clemens and me), consider that the question wasn't clear until it was edited and people don't work in this way anywayInconsequential
@Louque the easy way to bind to any container is to create a ViewModel class with a single property to contain the list or dictionary, and implement INotifyPropertyChanged.Inconsequential
L
4

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 =)

Louque answered 10/1, 2019 at 8:43 Comment(5)
Hey, this method was perfect for my use case, and it worked the first time I ran my program, but next time, without any code changes, it failed with the build error: "All objects added to an IDictionary must have a Key attribute..." on the DataTemplate definition. I've tried building, rebuilding, anything I could think of, but the only way to get rid of the build error is to replace the DataType of the DataTemplate with a normal type :( Any ideas? <DataTemplate DataType="{StaticResource OrderViewDataType}">Singularize
<x:Array Type="{x:Type System:Type}" x:Key="OrderViewDataArray"> <x:Type TypeName="viewData:OrderViewData" /> </x:Array> <helpers:GenericType BaseType="{x:Type TypeName=viewData:TabViewData`1}" InnerTypes="{StaticResource OrderViewDataArray}" x:Key="OrderViewDataType" /> TabViewData<T> where T : IsTab OrderViewData : IsTabSingularize
Okay I "Fixed" it by replacing the DataTemplate DataType assignment with <DataTemplate.DataType> <StaticResource ResourceKey="OrderViewDataType" /> </DataTemplate.DataType> The designer view breaks if I place the carret inside the forementioned code, but I can live with that. Hope someone else can use this :)Singularize
Aaand it's broken again. I was working on other parts of my project, tried to build. Bam: "A key for a dictionary cannot be of type 'System.Windows.StaticResourceExtension'" I don't get it... Sorry for spamming your post.Singularize
I created my own question: #70083242Singularize

© 2022 - 2024 — McMap. All rights reserved.