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.
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);
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>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With