しっぽを追いかけて

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

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

Xamarin でデータの型によってリスト内の表示を切り替えたい(2)

前回の記事 では、TypeTemplateSelector にデータの型ごとに DataTemplate を持たせることで、型ごとのテンプレート選択を実装していました

ただ、この方法だとデータの型が増えるたびに TemplateSelector にプロパティを追加したり、分岐条件を追加する必要があるので、あまり保守性がよくありません

そこでもっと汎用的な TypeTemplateSelector にしてみたいと思います

f:id:matatabi_ux:20150201220515p:plain

改修したのは上記の網掛けのもの

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 を指定するときはこんな記述になります

さて実行

f:id:matatabi_ux:20150201214639p:plain

結果は前回と変わらず、データも変わってないので無事にリファクタリングできましたね