mahboub_mo
mahboub_mo

Reputation: 3038

How to reference a generic type in the DataType attribute of a DataTemplate?

I have a ViewModel defined like this:

public class LocationTreeViewModel<TTree> : 
    ObservableCollection<TTree>, INotifyPropertyChanged
        TTree : TreeBase<TTree>

I want to reference it in the DataType attribute of a DataTemplate in XAML. How can I do that?

Upvotes: 33

Views: 22842

Answers (13)

Prince Owen
Prince Owen

Reputation: 1505

Mild improvement to a previous answer by Rico-E.

/// <summary>
/// Markup extension for using generic types in xaml
/// Source: https://stackoverflow.com/a/57361997/8058709
/// </summary>
public class GenericType : MarkupExtension
{
    public GenericType() { }

    public GenericType(Type baseType, params Type[] innerTypes)
    {
        BaseType = baseType;
        InnerTypes = innerTypes;
    }

    public Type BaseType { get; set; }
    public Type InnerType
    {
        set
        {
            InnerTypes = [value];
        }
    }

    public Type[] InnerTypes { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return BaseType.MakeGenericType(InnerTypes);
    }
}

This one supports single generic types. So you can simply use the MarkupExtension in your xaml like this:

<ui:GenericType
    x:Key="DefaultEnumPair"
    BaseType="{x:Type dt:EnumPair`1}"
    InnerType="{x:Type sys:Enum}" />

Upvotes: 0

The Don
The Don

Reputation: 353

Going to expand a bit on answers provided @Steven and @ColinE.

If you're going to use a generic definition (i.e. GenericType<> without a generic parameter) then @Steven 's answer will work.

If you're going to use a specific generic parameter (i.e GenericType<GenericParameter>, your best bet is to create a concrete type like @ColinE suggests (i.e. class GenericTypeImpl : GenericType<GenericParameter

Additionally, make sure that your ` between the type and number at the end of the type is a backquote and not an apostrophe, otherwise you'll get a compile-time error. If you're using an American QWERTY keyboard, you can type the backquote by typing Shift + ~ (Tilde).

Upvotes: 0

MartinHoly
MartinHoly

Reputation: 167

Define your generic types in the static class.

public static GenericTypes {
  public static Type TreeViewOfITreeItem => typeof(TreeView<ITreeItem>);
}

And then use it in the DataTemplate.

<DataTemplate DataType="{x:Static mhui:GenericTypes.TreeViewOfITreeItem}">

Upvotes: 1

Scover
Scover

Reputation: 138

Suprisingly, this works properly:

<DataTemplate DataType="GenericClass&lt;TypeArgument1,TypeArgument2&gt;">

Just copy the C# type and replace < by &lt; and > by &gt; (those are XML escape sequences)

Upvotes: 1

Steven
Steven

Reputation: 41

The following solution worked for me:

<DataTemplate>
    <DataTemplate.DataType>
        <x:Type Type="ns:MyGenericClass`1"/>
    </DataTemplate.DataType>
</DataTemplate>

Replace `1 for the amount of generic parameters you have, e.g.:

public class MyGenericClass<TParent, TChild>

would be declared:

<x:Type Type="ns:MyGenericClass`2"/>

Upvotes: 3

Jonathan Tuzman
Jonathan Tuzman

Reputation: 13262

I just implemented a workaround that is certainly less than perfect, and does require a bit of code in your ViewModel (which. because the VM shouldn't know about the view, breaks strict MVVM).

Define your generic type, and then define a class of that type with the lowest-common-ancestor as the type argument:

class GenericClass<T> { }

class Class1 : GenericClass<Apples> { }

class Class2 : GenericClass<Oranges> { }

class WorkaroundClass : GenericClass<Fruit> { }

In your viewmodel you'll need to declare your bound member as the ancestor type, and cast it.

// instead of:
// Apple DisplayFruit => GetGrannySmith();

Fruit DisplayFruit => (Fruit)GetGrannySmith();

In your xaml, you can then bind your data template to the ancestor class:

<DataTemplate DataType="{x:Type WorkaroundClass}"/>

I'm pretty sure that because the Generic parent is, well, generic, you shouldn't run into any actual cases where the differences between your type arguments cause any problems.

Upvotes: 1

JanDotNet
JanDotNet

Reputation: 4016

Late and not exactly the answer to the question (CollinE and Bas already sayed the it is actually not possible)... However, maybe the alternativ solution may be helpful for others:

It is possible to resolve generic types by using a TemplateSelector like that:

TemplateSelector

public class MyTemplateSelector : DataTemplateSelector
{
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        var genericType = typeof(MyGenericType<>);
        var isMyGeneric = item?.GetType().GetGenericTypeDefinition() == genericType;

        return isMyGeneric ? MyTemplate : OtherTemplate;
    }

    public DataTemplate MyTemplate { get; set; }
    public DataTemplate OtherTemplate { get; set; }
}

XAML

<UserControl.Resources>
        <DataTemplate x:Key="MyTemplate">            
                <!-- Set Up MyTemplate -->
        </DataTemplate>
        <DataTemplate x:Key="OtherTemplate">
            <!-- Set Up OtherTemplate -->
        </DataTemplate>
        <local:MyTemplateSelector x:Key="MyTemplateSelector"
                                MyTemplate="{StaticResource MyTemplate}"
                                OtherTemplate="{StaticResource MyTemplate}" />
</UserControl.Resources>

...

<ContentControl ContentTemplateSelector="{StaticResource MyTemplateSelector}" 
                Content="{Binding ViewModel}" />

Upvotes: 2

Rico-E
Rico-E

Reputation: 1845

I know, that I'm a little late to the party, but I want post an answer for all those who may see this question in the future:

It is possible.

You can see the whole code in the answer to this question: DataTemplates and Generics. But since it is quite long, I will just copy the important bits. If you want more details, then look into the referenced question.

  1. You need to write a MarkupExtension which can provide a closed generic type.

    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;
        }
    }
    
  2. Now you can define your type which closes your generic type in xaml, and then use the closed generic type as DataType of an DataTemplate.

    <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>
    
  3. Be happy that the defined DataTemplate gets automatically selected by WPF.

Upvotes: 8

Daniel Crowe
Daniel Crowe

Reputation: 321

The {x:Type} markup extension supports allows generic type arguments to be specified as a comma separated list in parentheses.

Here's an example:

<UserControl x:Class="Test"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:generic="clr-namespace:System.Collections.Generic;assembly=mscorlib"
        xmlns:sys="clr-namespace:System;assembly=mscorlib">
    <UserControl.Resources>
        <DataTemplate DataType="{x:Type generic:List(sys:Int64)}">
            <TextBlock Text="{Binding Count}"/>
        </DataTemplate>
    </UserControl.Resources>
</UserControl>

I am using .Net 4.5 on VS 2015, so your mileage may vary.

Upvotes: -1

Mayur Shah
Mayur Shah

Reputation: 21

Slightly improved version of MarkupExtension, work for classes upto 3 generic parameters.

  public class GenericTypeExtension : MarkupExtension
  {
    public GenericTypeExtension()
    {

    }
    public GenericTypeExtension(string baseTypeName_, Type genericType1_, Type genericType2_, Type genericType3_)
    {
      BaseTypeName = baseTypeName_;
      GenericType1 = genericType1_;
      GenericType2 = genericType2_;
      GenericType3 = genericType3_;
    }
    public string BaseTypeName { get; set; }
    public string BaseTypeAssemblyName { get; set; }
    public Type GenericType1 { get; set; }
    public Type GenericType2 { get; set; }
    public Type GenericType3 { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider_)
    {
      var list = new List<Type>();
      if (GenericType1 != null)
      {
        list.Add(GenericType1);
      }
      if (GenericType2 != null)
      {
        list.Add(GenericType2);
      }
      if (GenericType3 != null)
      {
        list.Add(GenericType3);
      }

      var type = Type.GetType(string.Format("{0}`{1}, {2}", BaseTypeName, list.Count, BaseTypeAssemblyName));
      if (type != null)
      {
        return type.MakeGenericType(list.ToArray());
      }
      return null;
    }

  }

Upvotes: -1

mahboub_mo
mahboub_mo

Reputation: 3038

The only way i could do this is to use MarkupExtensions.

public class GenericType : MarkupExtension
{
     private readonly Type _of;
     public GenericType(Type of)
     {
         _of = of;
     }
     public override object ProvideValue(IServiceProvider serviceProvider)
     {
         return typeof(LocationTreeViewModel<>).MakeGenericType(_of);
     }
}

And to use it i just need to do this:

<DataTemplate DataType="{app:GenericType app:TreeBaseClass}">

Upvotes: -2

Bas
Bas

Reputation: 27085

In XAML 2006 this is not supported. You can, however, roll your own if you want to have this functionality.

This link has a nice tutorial on creating markup extensions.

Usage would be like this:

<Grid xmlns:ext="clr-namespace:CustomMarkupExtensions">
  <TextBlock Text="{ext:GenericType FooLocationTreeViewModel(Of Foo)}" />
</Grid>

You have to choose and implement the syntax though. I suggest the VB notation since it won't interfere like the C# notation does with < and >.

Upvotes: 3

ColinE
ColinE

Reputation: 70122

No, you cannot express a generics type in XAML. You will have to create a concrete type that extends your generic one ...

public class FooLocationTreeViewModel : LocationTreeViewModel<Foo>
{
}

Upvotes: 16

Related Questions