UWP になって WPF と同様に DataTemplate に x:DataType プロパティを付けられるようになりました
これはもしかしてデータの型ごとにテンプレートを選択するテンプレートセレクタが作りやすくなったのでは?・・・と期待して試してみることにしました
※サンプルソース全体は下記にアップしています
しかし、そんな甘い話はありませんでした;
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 の画面は下記のように表示されました
直接見え方を切り替えているのは TypeToTemplateSelector なので、例えば 新たに OrangeViewModel なんてデータ型が増えても TypeToTemplate を増えたデータ型に合わせて追加するだけ
ListView には一切影響を与えることなく対応ができてしまうというわけです・・・先頭の項目だけ変えたい、交互に背景を変えたい、最後に追加ボタンを入れたいなどの仕様変更にもデータ側の修正が中心でビュー側は最小限の修正で対応できるようになりますね