imani-cの日記

WPFスタイルとテンプレートの本をAmazonで出しました。ちょっととっつきにくいこれらについて知りたいお方は、覗いてみてください。「WPF スタイル」で検索すると、トップに出ます。gRPCの本もあります。「gRPC入門」で検索するとすぐ見つかります。

WPF - ContentTemplateSelectorの書き方の工夫と汎用化

目次

リソースにテンプレートセレクタオブジェクトを作る

「WPF - ContentTemplateをデータの値によって自動的に選ぶ」という記事でContentTemplateSelectorについて説明しました。 紹介した書き方は、以下のようなものです。画面定義部分の記述量を最小限にしないと読みづらいXamlではあまり使いたい書き方ではありません。特に、このコードのように構造が生じると、きちんと読んで解釈する必要が生じますので、面倒です。

<ContentControl Content="{Binding InfoToShow}">
    <ContentControl.ContentTemplateSelector>
        <local:PetTemplateSelector/>
    </ContentControl.ContentTemplateSelector>
</ContentControl>

このような書き方ではなく、ContentTemplateSelectorを指定する部分を属性として「テンプレートセレクタはこれ」とシンプルに指定したいものです。

そこで、セレクタをオブジェクトとしてリソース内に宣言する方法を使います。前述の記事のDataTemplate宣言のすぐ下に、以下の行を追加するだけです。

<StackPanel>
    <StackPanel.Resources>
        ... <!-- 小型犬と大型犬のテンプレート宣言は、省略 -->

        <!-- この行を追加:セレクタオブジェクトを作る -->
        <local:PetTemplateSelector x:Key="dogSizeSelector"/>
    </StackPanel.Resources>

    <!-- データ表示:属性でセレクタを指定する -->
    <ContentControl Content="{Binding InfoToShow}"
                    ContentTemplateSelector="{StaticResource dogSizeSelector}" />

    ... <!-- ボタンを省略 -->
</StackPanel>

ContentControlの記述がだいぶ読みやすくなりました。

汎用的なテンプレートセレクタ

ここまでの例で使ったPetTemplateSelectorは、体重でテンプレートを選びました。このように、数値の大小で表示を変えることは多くありそうです。ですから、このテンプレートセレクタを汎用化してみましょう。

PetTemplateSelectorがやっていることは、「引数として渡されたitemオブジェクトのWeightプロパティの値が20.0以上と未満で異なるテンプレートを返す」ということです。これを汎用化し、「引数として渡されたitemオブジェクトの指定されたプロパティの値が指定された閾値以上と未満で、それぞれのために指定されたテンプレートを返す」としてみましょう。

//! 表示用テンプレートを選ぶための汎用的なセレクタークラス。
public class LargerOrEqualTemplateSelector : DataTemplateSelector
{
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        // リフレクションにより、itemから指定されたプロパティの値を取り出す。
        var pi = item.GetType().GetRuntimeProperty(PropertyName);
        IComparable propertyValue = (IComparable)pi.GetValue(item);

        // プロパティを閾値と比較して、適したDataTemplateを返す。
        return propertyValue.CompareTo(Threshold) < 0 ? SmallerTemplate : LargerOrEqualTemplate;
    }

    public string PropertyName { get; set; }                // 値を得るプロパティの名前。
    public IComparable Threshold { get; set; } = 25.0;      // 閾値。
    public DataTemplate LargerOrEqualTemplate { get; set; } // プロパティの値が閾値以上の時のDataTemplate。
    public DataTemplate SmallerTemplate { get; set; }       // プロパティの値が閾値未満の時のDataTemplate。
}

プロパティは、それぞれコメント通りの役割です。これらは、Xamlから設定されます。

関数SelectTemplate()は、リフレクションを利用して、PropertyNameに指定された名前のプロパティの値を引数itemから取り出し、閾値と比べて、適切なDataTemplateを返しています。この関数は、int・string・DateTimeなどIComparableを実装する全ての型のプロパティに対して使えます。

これを利用するためにはセレクタのプロパティに値を与えなければなりませんから、上記のXamlを以下のように変更します。コードが書いてある部分は、すべて変更されています。

<StackPanel>
    <StackPanel.Resources>
        ... <!-- 小型犬と大型犬のテンプレート宣言は、省略 -->

        <!-- セレクタオブジェクトを作る。ここでセレクタのプロパティを設定する。 -->
        <system:Double x:Key="threshold">25.0</system:Double>
        <local:LargerOrEqualTemplateSelector x:Key="generalDogSelector"
                                             SmallerTemplate="{StaticResource SmallDogInfoTemplate}"
                                             LargerOrEqualTemplate="{StaticResource LargeDogInfoTemplate}"
                                             PropertyName="Weight"
                                             Threshold="{StaticResource threshold}"
                                             />
    </StackPanel.Resources>

    <!-- データ表示 -->
    <ContentControl Content="{Binding InfoToShow}"
                    ContentTemplateSelector="{StaticResource generalDogSelector}"/>

    ... <!-- ボタンを省略 -->
</StackPanel>

閾値をわざわざオブジェクトとして宣言していますが、これをセレクタ宣言の中で「Threshold="25.0"」とは書けません。ThresholdプロパティがIComparable型なので、「25.0」という文字列を変換する方法がないからです。Thresholdプロパティには、PropertyNameで指定したプロパティの型と同じ型のオブジェクトを指定する必要があります。