UWP DataTemplates for multiple item types in ListV

2019-03-27 10:44发布

问题:

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.

回答1:

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>


回答2:

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