前回の記事 では、TypeTemplateSelector にデータの型ごとに DataTemplate を持たせることで、型ごとのテンプレート選択を実装していました
ただ、この方法だとデータの型が増えるたびに TemplateSelector にプロパティを追加したり、分岐条件を追加する必要があるので、あまり保守性がよくありません
そこでもっと汎用的な TypeTemplateSelector にしてみたいと思います
改修したのは上記の網掛けのもの
DataTemplateSelector の修正は少しだけ
/// <summary> /// DataTemplateSelector の基底クラス /// </summary> public class DataTemplateSelector : BindableObject { /// <summary> /// テンプレートを選択する /// </summary> /// <param name="item">アイテムのデータソース</param> /// <param name="container">アイテムのコンテナ</param> /// <param name="index">アイテムのインデックス</param> /// <returns>選択されたテンプレート</returns> public virtual DataTemplate SelectTemplate(object item, BindableObject container, int? index = null) { return null; } }
BindableObject を継承するようにしただけです
さらに新たに追加した TypeAttachedTemplate
/// <summary> /// 型つき DataTemplate クラス /// </summary> [ContentProperty("Template")] public class TypeAttachedTemplate : BindableObject { #region DataType BindableProperty /// <summary> /// DataType BindableProperty /// </summary> public static readonly BindableProperty DataTypeProperty = BindableProperty.Create<TypeAttachedTemplate, Type>( bp => bp.DataType, typeof(object), BindingMode.OneWay); /// <summary> /// DataType CLR Property /// </summary> public Type DataType { get { return (Type)this.GetValue(DataTypeProperty); } set { this.SetValue(DataTypeProperty, value); } } #endregion //DataType BindableProperty #region Template BindableProperty /// <summary> /// Template BindableProperty /// </summary> public static readonly BindableProperty TemplateProperty = BindableProperty.Create<TypeAttachedTemplate, DataTemplate>( bp => bp.Template, null, BindingMode.OneWay); /// <summary> /// Template CLR Property /// </summary> public DataTemplate Template { get { return (DataTemplate)this.GetValue(TemplateProperty); } set { this.SetValue(TemplateProperty, value); } } #endregion //DataType BindableProperty }
ほんとは 添付プロパティ を使いたかったんですが、Xamarin ではなぜか DataTemplate が BindableObject を継承していないのでできません
というわけでちょっと妥協ですがコンポジションパターンを適用して内部に DataType と Template プロパティを持つようにしました
XAML で記述する際に <TypeAttachedTemplate>
の内部に直接 <DataTemplate>
を書きたいので、Template を BindablePropterty にしてクラス属性 ContentProperty に Template プロパティを指定しています
お次は TypeTemplateSelector
/// <summary> /// データソースの型によってテンプレートを選択する DataTemplateSelector /// </summary> [ContentProperty("Templates")] public class TypeTemplateSelector : DataTemplateSelector { #region DataTemplateCollection BindableProperty /// <summary> /// DataTemplateCollection BindableProperty /// </summary> public static readonly BindableProperty TemplatesProperty = BindableProperty.Create<TypeTemplateSelector, List<TypeAttachedTemplate>>( bp => bp.Templates, new List<TypeAttachedTemplate>(), BindingMode.OneWay); /// <summary> /// 型指定テンプレートのリスト /// </summary> public List<TypeAttachedTemplate> Templates { get { return (List<TypeAttachedTemplate>)this.GetValue(TemplatesProperty); } set { this.SetValue(TemplatesProperty, value); } } #endregion //DataTemplateCollection BindableProperty /// <summary> /// デフォルトのテンプレート /// </summary> public DataTemplate DefaultTemplate { get; set; } /// <summary> /// テンプレートを選択する /// </summary> /// <param name="item">アイテムのデータソース</param> /// <param name="container">アイテムのコンテナ</param> /// <param name="index">アイテムのインデックス</param> /// <returns>選択されたテンプレート</returns> public override DataTemplate SelectTemplate(object item, BindableObject container, int? index = null) { var template = (from t in this.Templates where item.GetType() == t.DataType select t.Template).FirstOrDefault(); return template ?? this.DefaultTemplate; } }
DefaultTemplate だけ残して後のテンプレートは TypeAttachedTemplate のコレクションにしました
こちらも <TypeTemplateSelector>
内に直接 <TypeAttachedTemplate>
を複数並べたいので、Templates を BindablePropterty にしてクラス属性 ContentProperty に Templates プロパティを指定しています
最後はやっぱり XAML
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:b="clr-namespace:XamarinControl.Behaviors;assembly=XamarinControl" xmlns:c="clr-namespace:XamarinControl.Controls;assembly=XamarinControl" xmlns:s="clr-namespace:XamarinControl.Selectors;assembly=XamarinControl" xmlns:vm="clr-namespace:XamarinControl.ViewModels;assembly=XamarinControl" x:Class="XamarinControl.Views.TopPage"> <ContentPage.BindingContext> <vm:TopPageViewModel/> </ContentPage.BindingContext> <c:ItemsControl ItemsSource ="{Binding Items}"> <c:ItemsControl.ItemTemplateSelector> <s:TypeTemplateSelector> <!-- TextItemViewModel の場合 --> <s:TypeAttachedTemplate DataType="{x:Type vm:TextItemViewModel}"> <DataTemplate> <Grid Padding="20,10"> <Label Text="{Binding Title}"/> </Grid> </DataTemplate> </s:TypeAttachedTemplate> <!-- ImageItemViewModel の場合 --> <s:TypeAttachedTemplate DataType="{x:Type vm:ImageItemViewModel}"> <DataTemplate> <Grid Padding="20,10" HeightRequest="80"> <Grid.ColumnDefinitions> <ColumnDefinition Width="80"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Image Grid.Column="0" Source="{Binding Source}" Aspect="AspectFit" HorizontalOptions="Start"/> <Label Grid.Column="1" Text="{Binding Title}"/> </Grid> </DataTemplate> </s:TypeAttachedTemplate> <!-- その他の場合 --> <s:TypeTemplateSelector.DefaultTemplate> <DataTemplate> <Grid Padding="20,10"> <Label Text="{Binding}" TextColor="Red"/> </Grid> </DataTemplate> </s:TypeTemplateSelector.DefaultTemplate> </s:TypeTemplateSelector> </c:ItemsControl.ItemTemplateSelector> </c:ItemsControl> </ContentPage>
あんまり変わってないように見えて実は汎用性がアップしています!
TypeTemplateSelector 内に DataType を指定した TypeAttachedTemplate を並べることでデータ型によるテンプレート分岐を XAML 側だけに集約できてます
なお Type を指定するときはこんな記述になります
さて実行
結果は前回と変わらず、データも変わってないので無事にリファクタリングできましたね