読者です 読者をやめる 読者になる 読者になる

しっぽを追いかけて

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

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

テンプレートセレクタは強力なデザイン実装手法

Windows ランタイムアプリ Windows ストアアプリ XAML C#

XAML を使った UI 構築の実装手法には便利な仕組みがたくさんありますが、自分が最も好きな仕組みは DataTemplateSelector などのテンプレートセレクタです!

え?つい最近こんなネタを見たって?ネタがかぶっただけきっと気のせいです!!

なんでテンプレートセレクタが好きかというと、オブジェクト指向言語でいうポリモーフィズムを UI の世界で体現したような仕組みだからです

f:id:matatabi_ux:20140704005409p:plain

例えばこんな画面のように、先頭だけが他と異なるタイルデザインだったとしても DataTemplateSelector を利用すれば、1つの GridView で ItemsSource に突っ込む ViewModel を先頭だけ変更するだけですっきりと実装できてしまいます

やり方は、まず次のような DataTempleteSelector を記述してビルドします

/// <summary>
/// DataTemplete のコレクション
/// </summary>
public class DataTemplateCollection : List<DataTemplate>
{
}

/// <summary>
/// 型によってテンプレートを切り替えるセレクタ
/// </summary>
[ContentProperty(Name = "Templates")]
public class ContentTypeTemplateSelector : DataTemplateSelector
{
    #region TargetType 添付プロパティ
    /// <summary>
    /// 対象の型を指定するための添付プロパティ
    /// </summary>
    public static readonly DependencyProperty TargetTypeProperty = DependencyProperty.RegisterAttached(
        "TargetType",
        typeof(string),
        typeof(ContentTypeTemplateSelector),
        new PropertyMetadata("ViewModelBase"));

    /// <summary>
    /// 対象の型を設定します
    /// </summary>
    /// <param name="template">テンプレート</param>
    /// <param name="value"></param>
    public static void SetTargetType(DataTemplate template, string value)
    {
        template.SetValue(TargetTypeProperty, value);
    }

    /// <summary>
    /// 対象の型を取得します
    /// </summary>
    /// <param name="template">テンプレート</param>
    /// <returns></returns>
    public static string GetTargetType(DataTemplate template)
    {
        return (string)template.GetValue(TargetTypeProperty);
    }
    #endregion TargetType 添付プロパティ

    /// <summary>
    /// 変換先テンプレート
    /// </summary>
    public DataTemplateCollection Templates { get; set; }

    public ContentTypeTemplateSelector()
    {
        this.Templates = new DataTemplateCollection();
    }

    /// <summary>
    /// アイテムに対応する DataTemplate を返却します
    /// </summary>
    /// <param name="item">アイテム</param>
    /// <returns>DataTemplate</returns>
    protected override DataTemplate SelectTemplateCore(object item)
    {
        return base.SelectTemplateCore(item, null);
    }

    /// <summary>
    /// アイテムに対応する DataTemplate を返却します
    /// </summary>
    /// <param name="item">アイテム</param>
    /// <param name="container">コンテナ</param>
    /// <returns>DataTemplate</returns>
    protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
    {
        var vm = item as ItemContainerViewModel;
        if (vm == null)
        {
            return null;
        }

        var assembly = typeof(ViewModelBase).GetTypeInfo().Assembly;

        return this.Templates.FirstOrDefault(t => ContentTypeTemplateSelector.GetTargetType(t).Equals(vm.ContentType.Name));
    }
}

GridView に設定する ItemsSource は先頭だけ ViewModel を変えておきます

/// <summary>
/// 画面に遷移したときの処理
/// </summary>
/// <param name="navigationParameter">遷移パラメータ</param>
/// <param name="navigationMode">遷移モード</param>
/// <param name="viewModelState">画面状態データ</param>
public override void OnNavigatedTo(object navigationParameter, NavigationMode navigationMode, Dictionary<string, object> viewModelState)
{
    base.OnNavigatedTo(navigationParameter, navigationMode, viewModelState);

    if (this.ViewModel.Items.Count > 0)
    {
        return;
    }

    // 先頭のアイテム追加
    this.ViewModel.Items.Add(
                new ItemContainerViewModel()
                {
                    UniqueId = Guid.NewGuid().ToString(),
                    ContentId = Guid.NewGuid().ToString(),
                    Content = new AddItemViewModel(),
                    ColumnSpan = 1,
                    RowSpan = 4,
                    IsActive = true,
                });

    // 2番目以降のアイテム追加
    int i = 0;
    foreach (var photo in App.AppSettings.Settings.Items)
    {
        var item = PhotoViewModel.Convert(photo);


        if (i < 2 || i == 3)
        {
            this.ViewModel.Items.Add(
                new ItemContainerViewModel()
                {
                    UniqueId = Guid.NewGuid().ToString(),
                    ContentId = item.UniqueId,
                    Content = item,
                    ColumnSpan = 1,
                    RowSpan = 2,
                    IsActive = true,
                });
        }

        if (i == 2)
        {
            this.ViewModel.Items.Add(
                new ItemContainerViewModel()
                {
                    UniqueId = Guid.NewGuid().ToString(),
                    ContentId = item.UniqueId,
                    Content = item,
                    ColumnSpan = 1,
                    RowSpan = 4,
                    IsActive = true,
                });
        }

        if (i > 3)
        {
            this.ViewModel.Items.Add(
                new ItemContainerViewModel()
                {
                    UniqueId = Guid.NewGuid().ToString(),
                    ContentId = item.UniqueId,
                    Content = item,
                    ColumnSpan = 1,
                    RowSpan = 1,
                    IsActive = true,
                });
        }

        i++;
    }
}

あとは作った DataTemplateSelector を GridView の ItemTemplateSelector に設定するだけ

<Page.Resources>
    <!--  このページで表示されるアイテムのコレクション  -->
    <CollectionViewSource x:Name="itemsViewSource" Source="{Binding Items}" />

    <!--  GridView 用 ItemTemplateSelector  -->
    <selector:ContentTypeTemplateSelector x:Key="GridViewItemTemplateSelector">
        <!-- アイテムの中身が AddItemViewModel だった場合のテンプレート -->
        <DataTemplate selector:ContentTypeTemplateSelector.TargetType="AddItemViewModel">
            <Border Background="#20486EC6">
                <Grid Margin="10" Background="#FF486EC6">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="1*" />
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="2*" />
                    </Grid.RowDefinitions>
                    <TextBlock Grid.Row="1"
                                HorizontalAlignment="Center"
                                FontSize="144"
                                Foreground="White"
                                Text="+" />
                    <TextBlock Grid.Row="2"
                                HorizontalAlignment="Center"
                                FontSize="26"
                                Foreground="White"
                                Text="新しい写真を追加"
                                TextAlignment="Left" />
                </Grid>
            </Border>
        </DataTemplate>
        <!-- アイテムの中身が PhotoViewModel だった場合のテンプレート -->
        <DataTemplate selector:ContentTypeTemplateSelector.TargetType="PhotoViewModel">
            <Grid>
                <Image HorizontalAlignment="Center"
                        VerticalAlignment="Top"
                        Source="{Binding Content.ImageUri}"
                        Stretch="UniformToFill" />
            </Grid>
        </DataTemplate>
    </selector:ContentTypeTemplateSelector>

</Page.Resources>

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="140" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <!--  水平スクロール グリッド  -->
        <ctrl:VariableSizedGridView x:Name="itemGridView"
                                    Grid.RowSpan="2"
                                    ItemTemplateSelector="{StaticResource GridViewItemTemplateSelector}"
                                    ItemsSource="{Binding Source={StaticResource itemsViewSource}}"
                                    Padding="116,186,116,0"
                                    SelectionMode="Multiple">
            <GridView.ItemsPanel>
                <ItemsPanelTemplate>
                    <VariableSizedWrapGrid ItemHeight="120"
                                            ItemWidth="300"
                                            Orientation="Vertical" />
                </ItemsPanelTemplate>
            </GridView.ItemsPanel>
            <GridView.ItemContainerStyle>
                <Style TargetType="GridViewItem">
                    <Setter Property="Margin" Value="0,0,20,20" />
                    <Setter Property="HorizontalContentAlignment" Value="Stretch" />
                    <Setter Property="VerticalContentAlignment" Value="Stretch" />
                </Style>
            </GridView.ItemContainerStyle>
        </ctrl:VariableSizedGridView>

        ~ 中略 ~
 </Grid>
</Grid>

f:id:matatabi_ux:20140704005450p:plain

先頭もそれ以外も 1つの GridView 上に配置できるので、選択枠やスクロール、クリックなど統一して振る舞えます

こういったことがスマートにできるのでテンプレートセレクタは結構使いやすいと思うんです