UWP DataTemplates for multiple item types in ListView
Asked Answered
S

2

9

How would I go about implementing this?

Let's say this is my model:

public interface IAnimal
{
     string Name { get; }
}
public class Fish : IAnimal
{
    public string Name { get; set; }
    public int ScalesCount { get; set; }
}
public class Dog : IAnimal
{
    public string Name { get; set; }
    public string CollarManufacturerName { get; set; }
}

public class ViewModel
{
    public ObservableCollection<IAnimal> Animals { get; set; }

    public ViewModel()
    {
        this.Animals = new ObservableCollection<IAnimal>();
        this.Animals.Add(new Fish { Name = "Carl", ScalesCount = 9000 });
        this.Animals.Add(new Dog { Name = "Fifi", CollarManufacturerName = "Macrosoft" });
    }
}

For the sake of the amount of code in this question please assume that INotifyPropertyChanged is implemented where necessary, and that the ViewModel is correctly initialized in the page.

How can I use my own corresponding DataTemplates? In WPF I would just define multiple DataTemplates without an x:Key but with a defined DataType and let the ListView chose which to use based on the type of the item. UWP doesn't like that; the compiler simply states Dictionary Item "DataTemplate" must have a Key attribute. So how do I accomplish my goal?

Current Attempt

My current attempt is to make a custom DataTemplateSelector, which seems rather straight forward.

public class MyDataTemplateSelector: Windows.UI.Xaml.Controls.DataTemplateSelector
{
    public ObservableCollection<TemplateMatch> Matches { get; set; }

    public DataTemplateSelector()
    {
        this.Matches = new ObservableCollection<TemplateMatch>();
    }

    protected override DataTemplate SelectTemplateCore(object item)
    {
        return this.Matches.FirstOrDefault(m => m.TargetType.Equals(item))?.Template;
    }

    protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
    {
        return this.Matches.FirstOrDefault(m => m.TargetType.Equals(item))?.Template;
    }
}

public class TemplateMatch
{
    public Type TargetType { get; set; }
    public DataTemplate Template { get; set; }
}

Define it in XAML like this:

<ListView ItemsSource="{x:Bind ViewModel.Animals}">
    <ListView.ItemTemplateSelector>
        <cmp:MyDataTemplateSelector>
            <cmp:MyDataTemplateSelector.Matches>
                <cmp:TemplateMatch TargetType="model:Dog" Template="{StaticResource DogTemplate}"/>
                <cmp:TemplateMatch TargetType="model:Fish" Template="{StaticResource FishTemplate}"/>
            </cmp:MyDataTemplateSelector.Matches>
        </cmp:MyDataTemplateSelector>
    </ListView.ItemTemplateSelector>
</ListView>

Unfortunately when I run this, an Exception occurs during runtime, stating Failed to create a 'Ui.Components.TemplateMatch' from the text 'model:Dog'. So it seems binding to a Type property is not that easy.

Any help is appreciated!

Please note that I'd like to use a property of type Type, as opposed to string where I would pass the CLR type name and using reflection to invoke the type, mostly because I don't want mixed CLR and XML namespaces appear in XAML. If you can find a way to invoke the type using the XML namespace, I'll gladly take that as an answer.

Stevenage answered 5/9, 2015 at 14:32 Comment(6)
did you try TargetType="{x:Type model:Dog}" since the property is of type "Type" & not string.Newland
I did. UWP does not support the x:Type markup anymore. That would have been the solution for WPF. :)Stevenage
Why not create a conditional statement inside the select template method instead of using TemplateMatch?Burrill
I'd have liked to find a solution which I can re-use. Chosing a specific type inside SelectTemplate violates that idea. That's why I wanted to be able to define the available types in XAML.Stevenage
I'm currently facing the same issue, did you find a solution?Sempach
Sadly no. The only solution I see for now is hard-coding the templates which are available to the selector, instead of defining them as TemplateMatches.Stevenage
H
3

I found workaround. If you able to create instances of these types - you can use it for detecting types:

[ContentProperty(Name = nameof(Matches))]
public class TypeTemplateSelector : DataTemplateSelector
{
    public ObservableCollection<TemplateMatch> Matches { get; set; }
    public TypeTemplateSelector()
    {
        this.Matches = new ObservableCollection<TemplateMatch>();
    }

    protected override DataTemplate SelectTemplateCore(object item)
    {
        return this.Matches.FirstOrDefault(m => m.ItemOfType.GetType().Equals(item.GetType()))?.TemplateContent;
    }

    protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
    {
        return this.Matches.FirstOrDefault(m => m.ItemOfType.GetType().Equals(item.GetType()))?.TemplateContent;
    }
}

[ContentProperty(Name = nameof(ItemOfType))]
public class TemplateMatch
{
    public object ItemOfType { get; set; }
    public DataTemplate TemplateContent { get; set; }
}

XAML:

<controls:TypeTemplateSelector>
    <controls:TemplateMatch TemplateContent="{StaticResource FishTemplate}">
        <models:Fish/>
    </controls:TemplateMatch>
    <controls:TemplateMatch TemplateContent="{StaticResource DogTemplate}">
        <models:Dog/>
    </controls:TemplateMatch>
</controls:TypeTemplateSelector>
Handbill answered 24/10, 2015 at 4:44 Comment(1)
@Nick's answer is similar to this one (@NekitoSP's answer). But I like the way this one compares the types rather than comparing the full name string.Slide
O
1

The clue is in the error message.

Failed to create a 'Ui.Components.TemplateMatch' from the text 'model:Dog'

Note the 'model:Dog' is coming to your selector as text not a type.

Change your TemplateMatch class TargetType property to string instead of type like this:-

public class TemplateMatch
{
    public string TargetType { get; set; }
    public DataTemplate Template { get; set; }
}

Then change your template selector class to read

public class MyDataTemplateSelector : DataTemplateSelector
{
    public ObservableCollection<TemplateMatch> Matches { get; set; }

    public MyDataTemplateSelector()
    {
        Matches = new ObservableCollection<TemplateMatch>();
    }

    protected override DataTemplate SelectTemplateCore(object item)
    {
        return Matches.FirstOrDefault(m => m.TargetType.Equals(item.GetType().ToString()))?.Template;
    }

    protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
    {
        return Matches.FirstOrDefault(m => m.TargetType.Equals(item.GetType().ToString()))?.Template;
    }
}

Finally change your xaml to read

<ListView ItemsSource="{x:Bind ViewModel.Animals}">
    <ListView.ItemTemplateSelector>
        <cmp:MyDataTemplateSelector>
            <cmp:MyDataTemplateSelector.Matches>
                <cmp:TemplateMatch TargetType="YourFullNamespaceNotXamlNamespace.Dog" Template="{StaticResource DogTemplate}"/>
                <cmp:TemplateMatch TargetType="YourFullNamespaceNotXamlNamespace.Fish" Template="{StaticResource FishTemplate}"/>
            </cmp:MyDataTemplateSelector.Matches>
        </cmp:MyDataTemplateSelector>
    </ListView.ItemTemplateSelector>
</ListView>

The point is to forget trying to pass it to your selector as a type, and pass the typename as a string instead (Full namespace not Xaml namespace).

Oaks answered 5/4, 2016 at 14:9 Comment(1)
This is one possible solution, but I'd really like to bind to a property of type Type. You know, like normal DataTemplates do. Besides, when I'm in XAML, using the CLR namespace in one place and using the XML namespace in the other is not really straight forward. I know this is nitpicking on a workaround, but I'd like to find the perfect answer.Stevenage

© 2022 - 2024 — McMap. All rights reserved.