How to get {x:DataType} for a DataTemplate in C# for custom DataTemplateSelector
Asked Answered
K

4

6

I'm writing a custom DataTemplateSelector for a ComboBox control and I'll need to use it to display different DateTemplates for different kind of objects, in both the closed and open modes for the ComboBox.

Here's the DataTemplateSelector I came up with:

public class ComboBoxTypedDataTemplateSelector : DataTemplateSelector
{
    public IEnumerable<DataTemplate> SelectedTemplates { get; set; }

    public IEnumerable<DataTemplate> DropDownTemplates { get; set; }

    protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
    {
        IEnumerable<DataTemplate> source = container.FindParent<ComboBoxItem>() == null
            ? SelectedTemplates // Get the template for the closed mode
            : DropDownTemplates; // Get the template for the open UI mode
        Type type = item.GetType();
        return null; // Some LINQ to get the first DataTemplate in source with the {x:DataType} that equals type
    }
}

public sealed class DataTemplatesCollection : List<DataTemplate> { }

And here's how I'd use it in XAML:

<ComboBox>
    <mvvm:ComboBoxTypedDataTemplateSelector>
        <mvvm:ComboBoxTypedDataTemplateSelector.SelectedTemplates>
            <mvvm:DataTemplatesCollection>
                <DataTemplate x:DataType="models:SomeType">
                    <TextBlock Text="{x:Bind ...}"/>
                </DataTemplate>
                <DataTemplate x:DataType="models:SomeOtherType">
                    <TextBlock Text="{x:Bind ...}"/>
                </DataTemplate>
            </mvvm:DataTemplatesCollection>
        </mvvm:ComboBoxTypedDataTemplateSelector.SelectedTemplates>
        <mvvm:ComboBoxTypedDataTemplateSelector.DropDownTemplates>
            <mvvm:DataTemplatesCollection>
                <DataTemplate x:DataType="models:SomeType">
                    <TextBlock Text="{x:Bind ...}"/>
                </DataTemplate>
                <DataTemplate x:DataType="models:SomeOtherType">
                    <TextBlock Text="{x:Bind ...}"/>
                </DataTemplate>
            </mvvm:DataTemplatesCollection>
        </mvvm:ComboBoxTypedDataTemplateSelector.DropDownTemplates>
    </mvvm:ComboBoxTypedDataTemplateSelector>
</ComboBox>

Now, the only piece of the puzzle I'm missing, I can't figure out how to get that {x:DataType} property in C# (I know it's not actually a real property, but I hope there's a way to retrieve it via code). I need something like that to be able to get the right DataTemplate for each object, from the right templates group. Is there a way I can achieve that?

NOTE: I know I could just write a specific DataTemplateSelector that has the hardcoded names of the templates to return for each item type, and I can use that method as a fallback option. But, I was wondering if it was possible to write a more generic selector with this approach in order to make it more modular and be able to reuse it in the future.

Thanks for your help!

EDIT: following the suggestion by Vincent, I wrote an attached property to store a given Type in a DataTemplate:

public class DataTypeHelper
{
    public static Type GetAttachedDataType(DataTemplate element)
    {
        return (Type)element.GetValue(AttachedDataTypeProperty);
    }

    public static void SetAttachedDataType(DataTemplate element, Type value)
    {
        element.SetValue(AttachedDataTypeProperty, value);
    }

    public static readonly DependencyProperty AttachedDataTypeProperty =
        DependencyProperty.RegisterAttached("AttachedDataType", typeof(Type), typeof(DataTypeHelper), new PropertyMetadata(default(Type)));
}

And I've tried to use it like this:

...
 <DataTemplate x:DataType="someXlmns:SomeClass"
               mvvm:DataTypeHelper.AttachedDataType="someXlmns:SomeClass">
     ...
 </DataTemplate>

But I'm getting a XamlParseException at the line where I set the attached property to my type. I've tried to set that property to "Grid" (just as a test) and it doesn't crash, I don't understand why isn't it working with my custom type.

EDIT #2: looks like the x:Type markup extension is not available in UWP and I couldn't find another way (I don't think it's possible at all) to get a Type instance directly from XAML, so I had to just use the type name in XAML and then compare it to item.GetType().Name in the template selector.

The ability to assign a Type property directly from XAML would have been better as it'd also would have had syntax/spell-check in the XAML designer, but at least this approach works fine.

Katiakatie answered 14/4, 2017 at 12:50 Comment(0)
T
3

You cannot retrieve this value. This is just a hint for the compiler to allow it to generate the appropriate code for the binding.

You can either create a custom attached property to store what you need or use the Name property.

<local:Selector x:Key="selector" >
    <local:Selector.Template1>
        <DataTemplate x:DataType="local:Item" x:Name="template1" >
            <.../>
        </DataTemplate>
    </local:Selector.Template1>
</local:Selector>

Then in the selector implementation

Template1.GetValue(FrameworkElement.NameProperty);
Tradition answered 14/4, 2017 at 13:45 Comment(2)
Hello, thanks for your suggestion! I've updated my question with an implementation of the attached property, could you take a look at that? Will mark this as an answer the second I manage to get this suggestion work in my situation.Katiakatie
Nevermind, I figured it just isn't possible to assign a Type property from XAML on UWP, so I just used a string to compare to the type name in the template selector (see updated question). Thanks again!Katiakatie
E
1

Here's my 2 cents:

[ContentProperty(Name = nameof(Templates))]
public class TypedDataTemplateSelector : DataTemplateSelector
{
  public IList<TypedDataTemplate> Templates { get; } 
    = new ObservableCollection<TypedDataTemplate>();

  public TypedDataTemplateSelector()
  {
    var incc = (INotifyCollectionChanged)Templates;
    incc.CollectionChanged += (sender, e) =>
    {
      if (e?.NewItems.Cast<TypedDataTemplate>()
          .Any(tdt => tdt?.DataType == null || tdt?.Template == null) == true)
        throw new InvalidOperationException("All items must have all properties set.");
    };
  }

  protected override DataTemplate SelectTemplateCore(object item, 
      DependencyObject container)
  {
    if (item == null) return null;
    if (!Templates.Any()) throw new InvalidOperationException("No DataTemplates found.");

    var result =
      Templates.FirstOrDefault(t => t.DataType.IsAssignableFrom(item.GetType()));
    if (result == null)
      throw new ArgumentOutOfRangeException(
        $"Could not find a matching template for type '{item.GetType()}'.");

    return result.Template;
  }
}

[ContentProperty(Name = nameof(Template))]
public class TypedDataTemplate
{
  public Type DataType { get; set; }
  public DataTemplate Template { get; set; }
}

Usage:

<ContentControl Content="{Binding}">
  <ContentControl.ContentTemplateSelector>
    <v:TypedDataTemplateSelector>
      <v:TypedDataTemplate DataType="data:Person">
        <DataTemplate>
          <StackPanel>
            <TextBox Header="First name" Text="{Binding FirstName}" />
            <TextBox Header="Last name" Text="{Binding LastName}" />
          </StackPanel>
        </DataTemplate>
      </v:TypedDataTemplate>
      <v:TypedDataTemplate DataType="data:Company">
        <DataTemplate>
          <StackPanel>
            <TextBox Header="Company name" Text="{Binding CompanyName}" />
          </StackPanel>
        </DataTemplate>
      </v:TypedDataTemplate>
    </v:TypedDataTemplateSelector>
  </ContentControl.ContentTemplateSelector>
</ContentControl>
Euboea answered 15/7, 2019 at 7:8 Comment(0)
P
0

Here's a sample I find that use different DataTemplate depending on the data type. Code:

public class MyDataTemplateSelector : DataTemplateSelector 
{ 
    public Dictionary<Type, DataTemplate> TemplateDictionary { get; set; } 
    protected override DataTemplate SelectTemplateCore(object item) 
    { 
        // select datatemplate depending on item type 
        Type itemType = item.GetType(); 
        if (!TemplateDictionary.Keys.Contains(itemType)) 
        { 
            return null; 
        } 
        return TemplateDictionary[itemType]; 
    } 
    protected override DataTemplate SelectTemplateCore(object item, DependencyObject container) 
    { 
        // select datatemplate depending on item type 
        Type itemType = item.GetType(); 
        if (!TemplateDictionary.Keys.Contains(itemType)) 
        { 
            return null; 
        } 
        return TemplateDictionary[itemType]; 
    } 
}

Here's the link https://code.msdn.microsoft.com/How-to-bind-data-to-04dcf26d. You can download the source code.

Penetralia answered 17/4, 2017 at 6:28 Comment(1)
The problem with this is that it's hard to define dictionaries in XAML.Euboea
S
0

What you can actually do is to Create AttachedProperty and use it on DataTemplate, luckly in UWP DataTemplate inherits from DependecyObject up the tree so we can use AP on it. Example below:

AP definition

public class SomeAttachedProperty : DependencyObject
    {
        public static readonly DependencyProperty TypeProperty =
        DependencyProperty.RegisterAttached(
          "SomeAttachedProperty",
          typeof(string),
          typeof(SomeAttachedProperty),
          new PropertyMetadata(null)
        );
        public static void SetType(DependencyObject element, string value)
        {
            element.SetValue(TypeProperty, value);
        }
        public static string GetType(DependencyObject element)
        {
            return (string)element.GetValue(TypeProperty);
        }
    }

XAML

<DataTemplate x:Key="SomeKey" local:SomeAttachedProperty.Type="SomeValue">

In Code

DataTemplate dataTemplate = resourceDictionary["SomeKey"] as DataTemplate;

    string myValue = dataTemplate.GetValue(SomeAttachedProperty.TypeProperty) as string;
Saber answered 26/10, 2021 at 19:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.