しっぽを追いかけて

ぐるぐるしながら考えています

Unity と猫の話題が中心   掲載内容は個人の私見であり、所属組織の見解ではありません

UWP でデータの型ごとにテンプレートを決めるセレクタをつくる

UWP になって WPF と同様に DataTemplate に x:DataType プロパティを付けられるようになりました

これはもしかしてデータの型ごとにテンプレートを選択するテンプレートセレクタが作りやすくなったのでは?・・・と期待して試してみることにしました

サンプルソース全体は下記にアップしています

github.com


しかし、そんな甘い話はありませんでした;

DataTemplate には WPF のように DataType プロパティがありません・・・リフレクションで強引に参照しようとしてもだめだったので、これは XAML コンパイラのための目印でしかないようでした

というわけで相変わらず UWP でも次のような実装になりました

/// <summary>
/// Item template selector from data item type
/// </summary>
[ContentProperty(Name = "Templates")]
public class TypeToTemplateSelector : DataTemplateSelector
{
    /// <summary>
    /// Select target templates
    /// </summary>
    public TypeToTemplateCollection Templates { get; private set; } = new TypeToTemplateCollection();

    /// <summary>
    /// Content property path
    /// </summary>
    public string ContentPath { get; set; }

    /// <summary>
    /// Select template from item
    /// </summary>
    /// <param name="item">data item</param>
    /// <param name="container">item container view</param>
    /// <returns>selected item template</returns>
    protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
    {
        if (item == null)
        {
            return default(DataTemplate);
        }

        // Get content by route down view model tree
        var content = item;
        if (!string.IsNullOrEmpty(this.ContentPath))
        {
            var tokens = this.ContentPath.Trim().Split(new string[] { "." }, StringSplitOptions.RemoveEmptyEntries).ToList();
            foreach (var property in tokens)
            {
                content = content.GetType().GetProperty(property).GetValue(content);
            }
        }

        var template = (from t in this.Templates
                        where t.TargetType != null
                        && content.GetType().Equals(t.TargetType)
                        select t.Template).FirstOrDefault();

        if (template != null)
        {
            return template;
        }

        // Select a template without specified 'DataType' attribute for default
        template = (from t in this.Templates
                    where t.TargetType == null
                    select t.Template).FirstOrDefault();

        if (template != null)
        {
            return template;
        }

        return base.SelectTemplateCore(item, container);
    }
}

/// <summary>
/// Type convert to template class
/// </summary>
[ContentProperty(Name = "Template")]
public class TypeToTemplate : DependencyObject
{
    /// <summary>
    /// Data template
    /// </summary>
    public DataTemplate Template { get; set; }

    /// <summary>
    /// Type to DataTemplate
    /// </summary>
    public string DataType { get; set; }

    /// <summary>
    /// Type to DataTemplate(Converted)
    /// </summary>
    public Type TargetType
    {
        get { return this.DataType != null ? Type.GetType(this.DataType) : null; }
    }
}

/// <summary>
/// DataTemplate collection for xaml markup
/// </summary>
public class TypeToTemplateCollection : List<TypeToTemplate>
{
    /// <summary>
    /// Constroctor
    /// </summary>
    public TypeToTemplateCollection() : base()
    {
    }
}

DataTemplate が static メソッドを持ってたりするので、継承ではなくコンポジションクラス TypeToTemplate を用意することにしました

例によって WPF にはある TypeConveter 属性とかがないので、仕方なく DataType プロパティはクラスの完全修飾名(名前空間付きフルネーム)の文字列で指定するように妥協;

こちらのセレクタを利用した XAML のサンプルがこんな感じ

<Page
    x:Class="TypeSelectorSample.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:TypeSelectorSample"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    RequestedTheme="Dark"
    mc:Ignorable="d">
    <Page.DataContext>
        <local:MainPageViewModel/>
    </Page.DataContext>

    <Page.Resources>
        <local:TypeToTemplateSelector x:Key="ColorItemSelector">
            <local:TypeToTemplate DataType="TypeSelectorSample.RedViewModel">
                <DataTemplate x:DataType="local:RedViewModel">
                    <Border Background="#FF993320">
                        <TextBlock Text="Red" 
                                   FontSize="20"
                                   HorizontalAlignment="Center"
                                   VerticalAlignment="Center"/>
                    </Border>
                </DataTemplate>
            </local:TypeToTemplate>
            <local:TypeToTemplate DataType="TypeSelectorSample.GreenViewModel">
                <DataTemplate x:DataType="local:GreenViewModel">
                    <Border Background="#FF209933">
                        <TextBlock Text="Green" 
                                   FontSize="20"
                                   HorizontalAlignment="Center"
                                   VerticalAlignment="Center"/>
                    </Border>
                </DataTemplate>
            </local:TypeToTemplate>
            <local:TypeToTemplate DataType="TypeSelectorSample.BlueViewModel">
                <DataTemplate x:DataType="local:BlueViewModel">
                    <Border Background="#FF203399">
                        <TextBlock Text="Blue" 
                                   FontSize="20"
                                   HorizontalAlignment="Center"
                                   VerticalAlignment="Center"/>
                    </Border>
                </DataTemplate>
            </local:TypeToTemplate>
        </local:TypeToTemplateSelector>
    </Page.Resources>

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <ListView ItemsSource="{Binding Items}"
                  ItemTemplateSelector="{StaticResource ColorItemSelector}"
                  VerticalAlignment="Center"
                  HorizontalAlignment="Center">
            <ListView.ItemContainerStyle>
                <Style TargetType="ListViewItem">
                    <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
                    <Setter Property="VerticalContentAlignment" Value="Stretch"/>
                    <Setter Property="Width" Value="320"/>
                    <Setter Property="Height" Value="48"/>
                    <Setter Property="Margin" Value="0,0,0,10"/>
                </Style>
            </ListView.ItemContainerStyle>
        </ListView>
    </Grid>
</Page>

TypeToTemplateSelector の内部に DataType により対応するクラスを指定している TypeToTemplate タグを複数並べました

画面内では ListView で複数の項目を並べていますが、それぞれの見え方を決めるテンプレートは ListView.ItemTemplateSelector で指定された TypeToTemplateSelector が切り替えています

例えばこの XAML の DataContext である MainPageViewModel を次のように生成すると

public class MainPageViewModel : BindableBase
{
    public ObservableCollection<BindableBase> Items { get; set; } = new ObservableCollection<BindableBase>
    {
        new RedViewModel(),
        new BlueViewModel(),
        new GreenViewModel(),
        new RedViewModel(),
        new BlueViewModel(),
        new GreenViewModel(),
        new GreenViewModel(),
        new BlueViewModel(),
        new RedViewModel(),
    };

    public MainPageViewModel()
    {
    }
}

先ほどの XAML の画面は下記のように表示されました

f:id:matatabi_ux:20150927221726p:plain

直接見え方を切り替えているのは TypeToTemplateSelector なので、例えば 新たに OrangeViewModel なんてデータ型が増えても TypeToTemplate を増えたデータ型に合わせて追加するだけ

ListView には一切影響を与えることなく対応ができてしまうというわけです・・・先頭の項目だけ変えたい、交互に背景を変えたい、最後に追加ボタンを入れたいなどの仕様変更にもデータ側の修正が中心でビュー側は最小限の修正で対応できるようになりますね