imani-cの日記

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

WPF - ContentTemplateを自動的に選ぶ

画面上の同じ場所に時により異なる情報を表示することはよくあります。このような時に、複数の表示方法をWPFコードに用意してVisibilityをCollapsedにして切り替えるという方法もありますが、「情報の種類を調べて表示するものを決める」というロジックが必要になりますから、WPFコードだけでは実現できず面倒です。 以下で、情報の種類に応じた表示をWPFコードだけで行う方法を紹介します。

サンプルプログラムの仕様

表示するデータとして、以下の二種類を例にとります。署名と著者名を保持するMyBookInfoクラスと、ペットの名前と体重を保持するMyPetInfoクラスです。

//! 表示する情報を定義するクラス その1:蔵書情報
public class MyBookInfo
{
    public string BookName { get; } = "お小遣い帳の付け方";
    public string Author { get; } = "小遣い節約委員会";
}

//! 表示する情報を定義するクラス その2:ペット情報
public class MyPetInfo
{
    public string Name { get; } = "ポチ";
    public double Weight { get; } = 23.45;
}

この2種類の情報の表示方法をXAMLだけで切り替えます。

Xamlコード

表示用コントロール

表示部分のXAMLコードは、以下の通り、単純です。

<!-- 情報を表示する -->
<StackPanel>
    <ContentControl Content="{Binding InfoToShow}"/>
    <Button Content="Change info" Click="Button_Click" HorizontalAlignment="Center"/>
</StackPanel>

ContentControl.Contentに表示されるInfoToShowは、単純なobject型のプロパティです。

// (C#のコードです)
//! 表示情報。このプロパティの中身が表示される。
public object InfoToShow { get; set; } = new MyBookInfo();

表示方法定義

ContentControlにそれぞれのクラスに応じた表示方法を知らせるには、それぞれのクラスに応じたDataTemplateをリソースに定義します。

<StackPanel.Resources>
    <!-- MyBookInfoの時の表示 -->
    <DataTemplate DataType="{x:Type local:MyBookInfo}">
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="Book information: "/>
            <TextBlock Text="{Binding BookName}"/>
            <TextBlock Text="{Binding Author, StringFormat=({0:s})}"/>
        </StackPanel>
    </DataTemplate>
    <!-- MyPetInfoの時の表示 -->
    <DataTemplate DataType="{x:Type local:MyPetInfo}">
        <StackPanel>
            <TextBlock Text="Pet information"/>
            <TextBlock Text="{Binding Name, StringFormat=Pet name is {0:s}.}"/>
            <TextBlock Text="{Binding Weight, StringFormat=Pet name is {0:0.0}.}"/>
        </StackPanel>
    </DataTemplate>
</StackPanel.Resources>

ContentControlは、ContentにバインドされているInfoToShowプロパティに設定されたオブジェクトの型とDataTemplate.DataTypeで指定された型を照合して、どちらのDataTemplateを使えばよいかを自動的に判別します。InfoToShowプロパティには初期化時にMyBookInfoクラスオブジェクトが入りますから、最初はMyBookInfo用のDataTemplateが採用されて以下のように表示されます。

f:id:imani-c:20220417151626p:plain
MyBookInfoクラスに対応する表示

このサンプルでは、画面上のChangeInfoボタンを押すと、InfoToShowプロパティにMyPetInfoクラスのオブジェクトが代入します。 その時、画面上のContentControlは、新たに代入されたオブジェクトの型に従ってMyPetInfoクラス用のDataTemplateを使って表示を変更します。

f:id:imani-c:20220417151624p:plain
MyPetInfoクラスに対応する表示

このように、表示したいデータの型に対応する表示方法を定義しておくだけで、自動的に適切な表示がなされます。表示を選ぶためのC#コードは、必要ありません。 ContentControlだけではなく、ListBoxItemなどContentControlから派生する要素でも同じ方法が使えますので、様々な種類のデータが混ざったリストを表示するときなどにはとても便利です。

常に特定のDateTemplateを使う

常に特定のDataTemplateを使う場合には、DataTemplateにキーを指定します。

<!-- キーを付けたDataTemplate -->
<DataTemplate DataType="{x:Type local:MyPetInfo}" x:Key="petFormat">
    ...
</DataTemplate

<!-- 使うDataTemplateを指定する -->
<ContentControl Content="{Binding InfoToShow}" ContentTemplate="{StaticResource petFormat}"/>

キーを付けると、DataTypeで指定した型のオブジェクトを表示するときでも自動的には適用されなくなります。必ず明示的にContentTemplateを指定しなければなりません。

サンプルコード

上記で使ったサンプルコードは、DataTemplateに直接かかわらない部分を省略してあります。そのため全体が見通せないかもしれませんので、全コードを掲載しておきます。

なお、このコードは、わかりやすさだけを重視して作ったものです。実際のプログラムでこのようなコーディングをすることは、お勧めできません。

Xaml

<Window x:Class="DataTemplateEtc.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:DataTemplateEtc"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">

    <!-- 情報を表示する -->
    <StackPanel Background="Silver" Margin="20" HorizontalAlignment="Left" VerticalAlignment="Top">
        <StackPanel.Resources>
            <Style TargetType="{x:Type TextBlock}">
                <Setter Property="FontSize" Value="20"/>
            </Style>
            <!-- MyBookInfoの時の表示 -->
            <DataTemplate DataType="{x:Type local:MyBookInfo}">
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="Book information: "/>
                    <TextBlock Text="{Binding BookName}"/>
                    <TextBlock Text="{Binding Author, StringFormat=({0:s})}"/>
                </StackPanel>
            </DataTemplate>
            <!-- MyPetInfoの時の表示 -->
            <DataTemplate DataType="{x:Type local:MyPetInfo}">
                <StackPanel>
                    <TextBlock Text="Pet information"/>
                    <TextBlock Text="{Binding Name, StringFormat=Pet name is {0:s}.}"/>
                    <TextBlock Text="{Binding Weight, StringFormat=Pet name is {0:0.0}.}"/>
                </StackPanel>
            </DataTemplate>
        </StackPanel.Resources>

        <ContentControl Content="{Binding InfoToShow}"/>
        <Button Content="Change info" Click="Button_Click" FontSize="20" HorizontalAlignment="Center"/>
    </StackPanel>
</Window>

コードビハインド

using System.ComponentModel;
using System.Windows;

namespace DataTemplateEtc
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        //! コンストラクタ。自身をDataContextを設定する。
        public MainWindow()
        {
            DataContext = this;
            InitializeComponent();
        }

        //! 表示情報。このプロパティの中身が表示される。
        public object InfoToShow { get; set; } = new MyBookInfo();

        //! ボタンクリックハンドラ。表示する情報を切り替える。
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            // ペットの情報を表示情報にする。
            InfoToShow = new MyPetInfo();

            // 表示情報が切り替えられたことを通知する。
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(InfoToShow)));
        }

        //! INotifyPropertyChangedの実装。
        public event PropertyChangedEventHandler? PropertyChanged;
    }

    //! 表示する情報を定義するクラス その1:蔵書情報
    public class MyBookInfo
    {
        public string BookName { get; } = "お小遣い帳の付け方";
        public string Author { get; } = "小遣い節約委員会";
    }

    //! 表示する情報を定義するクラス その2:ペット情報
    public class MyPetInfo
    {
        public string Name { get; } = "ポチ";
        public double Weight { get; } = 23.45;
    }
}