【WPF】DataTemplateSelectorクラスを使用して、動的にコントロールの種類を変更する方法【C#】

# 概要
`DataTemplateSelector`クラスを使用方法の一例を記載しました。

# `DataTemplateSelector`クラスとは
同じ種類のオブジェクトに対して、複数の`DataTemplate`を使用したい場合に用いられるクラスです。例えば、Viewに複数のオブジェクトをバインディングする場合、バインディングしたいオブジェクトの種類は一緒ですが、その表示方法は異なるような場合を作成したい、みたいな場面です。

# `DataTemplate`とは
データに関するオブジェクトの視覚的な構造を定義できます。
下記のような`ItemsControl`をもつのコントロールを使用する場合、リストやコンボボックスの中の、一つ一つのアイテムを表示する箇所の見た目を定義する為に`DataTemplate`がよく使用されたりします。
- ComboBox
- ListBox
- ListView

# `ItemTemplate`と`DataTemplate`の違い
同じようなものに、`ItemTemplate`というものがあります。
`ItemTemplate`は、`ListBox`や`ListView`、`ComboBox`のような、複数のアイテムを表示するコントロールで使用されます。コントロール内の各アイテムの見た目を定義するための、`DataTemplate`を指定するために使用されるプロパティのイメージです。
`DataTemplate`は個々のオブジェクトの表示方法を定義するために使用されます。

例えば、以下のように`ListBox.ItemTemplate`の中に`DataTemplate`を定義できます。

```xml
<ListBox Width="300"
         ItemsSource="{Binding hogeList}">
   <ListBox.ItemTemplate>
     <DataTemplate>
       <StackPanel>
         <TextBlock Text="{Binding hogeText}" />
         <TextBlock Text="{Binding hugaText}" />
       </StackPanel>
     </DataTemplate>
   </ListBox.ItemTemplate>
 </ListBox>
```
このような構造により、複数のアイテムを扱うコントロールも、各アイテムの表示方法を定義できます。

また以下のように、`DataTemplate`の中の`ListBox`の`ItemTemplate`の中に、`ListBox`の`DataTemplate`を定義する、みたいなこともできます。
※上部`UserControl`の名前空間の宣言部分は色々と省略しています。

```xml
<UserControl
	x:Class="Hoge.HogeView"
    xmlns:local="clr-namespace:Hoge.Views">
    <UserControl.Resources>
        <ResourceDictionary>
            <!-- チェックボックスのテンプレート -->
            <DataTemplate x:Key="checkBoxTemplate">
                <ListBox
                    ItemsSource="{Binding CheckItemSource}">
                    <ListBox.ItemTemplate>
                        <DataTemplate>
                            <WrapPanel
                                VerticalAlignment="Center"
                                Orientation="Horizontal"
                                ScrollViewer.HorizontalScrollBarVisibility="Disabled">
                                <Grid>
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="20" />
                                        <ColumnDefinition Width="*" />
                                    </Grid.ColumnDefinitions>
                                    <CheckBox
                                        Grid.Column="0"
                                        HorizontalAlignment="Left"
                                        VerticalAlignment="Center"
                                        IsChecked="{Binding IsChecked}"/>
                                    <Label
                                        Grid.Column="1"
                                        VerticalAlignment="Center">
                                        <TextBlock
                                            MinWidth="100"
                                            MaxWidth="200"
                                            Text="{Binding Content}"
                                            TextWrapping="Wrap">
                                        </TextBlock>
                                    </Label>
                                </Grid>
                            </WrapPanel>
                        </DataTemplate>
                    </ListBox.ItemTemplate>
                </ListBox>
            </DataTemplate>
        </ResourceDictionary>
    </UserControl.Resources>
    <Grid>
    <!-- 画面の実装 -->
    </Grid>
</UserControl>
```

上記の`ListBox`は、`CheckBox`と`Label`をひとつのまとまりとして、それを複数水平に並べて表示させるために使用されています。
また、この`DataTemplate`は下記のように使用できます。
※上の「画面の実装」の部分で使用するイメージです。
```xml
<ListBox ItemsSource="{Binding Items}" 
         ItemTemplate="{StaticResource checkBoxTemplate}"/>
```
:::note
`StaticResource`を使用すると、`ResourceDictionary`に定義したリソースにアクセスできます。
:::

## `DataTemplateSelector`の使用方法
上に記載した`DataTemplate`の使用方法をもとに、以下に`DataTemplateSelector`の使用方法の一例を記載します。
View (画面) に縦に項目群 (コントロール群) がずらっと並んでいるイメージで、その項目ひとつひとつはオブジェクトになっています。その項目ひとつひとつは同じオブジェクトを使用 (ここでは`UnitItem`クラス) していますが、Viewに表示されるコントロールは「コンボボックス」「テキストボックス」「チェックボックス」の3種類に分けたい、みたいな内容です。

### View
- `ResourceDictionary`に、コンボボックスとテキストボックス、チェックボックスのテンプレートを記載しています。また、テンプレート選択用クラス`TemplateSelector`に、各テンプレートをセットしています。
- <Grid>以下は実際の画面の部分です。
- `CellTemplateSelector="{StaticResource TemplateSelector}"/>`の箇所が、色々なテンプレートが適用される場所です。

```xml
<UserControl
	x:Class="Hoge.SampleView"
    xmlns:local="clr-namespace:Hoge.Views">
    <UserControl.Resources>
        <ResourceDictionary>
            <!-- コンボボックスのテンプレート -->
             <DataTemplate x:Key="comboBoxTemplate">
                <Border>
                    <ComboBox
						Width="100"
						HorizontalAlignment="Center"
						ItemsSource="{Binding ComboItemSource}"
						SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
                    </ComboBox>
                </Border>
            </DataTemplate>
            <!-- テキストボックスのテンプレート -->
            <DataTemplate x:Key="textBoxTemplate">
                <Border>
                    <TextBox
						Width="100"
						HorizontalAlignment="Center"
                　      Text="{Binding InputText, Mode=TwoWay}"
						TextWrapping="Wrap">
                    </TextBox>
                </Border>
            </DataTemplate>
            <!-- チェックボックスのテンプレート -->
            <DataTemplate x:Key="checkBoxTemplate">
                <ListBox
                    ItemsSource="{Binding CheckItemSource}">
                    <ListBox.ItemTemplate>
                        <DataTemplate>
                            <WrapPanel
                                VerticalAlignment="Center"
                                Orientation="Horizontal"
                                ScrollViewer.HorizontalScrollBarVisibility="Disabled">
                                <Grid>
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="20" />
                                        <ColumnDefinition Width="*" />
                                    </Grid.ColumnDefinitions>
                                    <CheckBox
                                        Grid.Column="0"
                                        HorizontalAlignment="Left"
                                        VerticalAlignment="Center"
                                        IsChecked="{Binding IsChecked}"/>
                                    <Label
                                        Grid.Column="1"
                                        VerticalAlignment="Center">
                                        <TextBlock
                                            MinWidth="100"
                                            MaxWidth="200"
                                            Text="{Binding Content}"
                                            TextWrapping="Wrap">
                                        </TextBlock>
                                    </Label>
                                </Grid>
                            </WrapPanel>
                        </DataTemplate>
                    </ListBox.ItemTemplate>
                </ListBox>
            </DataTemplate>
           <!-- ここで、「TemplateSelector」クラスの各テンプレートのプロパティにテンプレートを設定している -->
           <!-- ここの各テンプレートをセットする記述がないと、「TemplateSelector」クラスの各テンプレートのプロパティはnullになってしまう -->
            <local:TemplateSelector
				x:Key="TemplateSelector"
				ComboTemplate="{StaticResource comboBoxTemplate}"
				TextTemplate="{StaticResource textBoxTemplate}"
  				CheckTemplate="{StaticResource checkBoxTemplate}"/>
        </ResourceDictionary>
    </UserControl.Resources>
    <!-- 画面の実装 -->
    <Grid>
        <TabControl>
            <TabItem>
                <TabItem.Header>
                    <TextBlock Text="サンプルタブ"/>
                </TabItem.Header>
                <DockPanel>
                    <ListView
						ItemsSource="{Binding SampleModel.InputItems}">
                        <ListView.View>
                            <GridView>
                                <GridView.Columns>
                                    <GridViewColumn Width="130" Header="名前">
                                        <GridViewColumn.CellTemplate>
                                            <DataTemplate>
                                                <TextBlock
													Text="{Binding Name}"
													TextWrapping="Wrap" />
                                            </DataTemplate>
                                        </GridViewColumn.CellTemplate>
                                    </GridViewColumn>
                                    <GridViewColumn Width="80" Header="補足">
                                        <GridViewColumn.CellTemplate>
                                            <DataTemplate>
                                                <TextBlock Text="{Binding Remark}" />
                                            </DataTemplate>
                                        </GridViewColumn.CellTemplate>
                                    </GridViewColumn>
                                     <!-- ここのCellTemplateSelectorで、選択されたテンプレートが適用される -->
                                    <GridViewColumn
                                        Width="180"
                                        Header="選択"
                                        CellTemplateSelector="{StaticResource TemplateSelector}"/>
                                </GridView.Columns>
                            </GridView>
                        </ListView.View>
                    </ListView>
                </DockPanel>
            </TabItem>
        </TabControl>
    </Grid>
</UserControl>
```

### コードビハインド
特別な実装はないです。
```C#
    public partial class SampleView : UserControl
    {
        public SampleView()
        {
            InitializeComponent();
        }
    }
```

### テンプレート選択用クラス
`DataTemplateSelector`を継承したクラスを作成し、`SelectTemplate()`メソッドをオーバーライドしたメソッドを作成します。

```C#
namespace Hoge.Views
{
    public class TemplateSelector : DataTemplateSelector
    {
        // xamlの「<local:TemplateSelector～」の部分で、この各テンプレートのプロパティにテンプレートがセットされる
    
        /// <summary>
        /// コンボボックス用テンプレート
        /// </summary>
        public DataTemplate ComboTemplate { get; set; }
        
        /// <summary>
        /// テキストボックス用テンプレート
        /// </summary>
        public DataTemplate TextTemplate { get; set; }

        /// <summary>
        /// チェックボックス用テンプレート
        /// </summary>
        public DataTemplate CheckTemplate { get; set; }

        public override DataTemplate SelectTemplate(object item, DependencyObject container)
        {
            // 第一引数「item」に、データバインディングの対象となるオブジェクトのインスタンスが入ってくる
            
            if (item is UnitItem)
            {
                // ここで、UnitUtemクラスの、あらかじめ値を入れておいたControlTypeプロパティ値を元にして、
                // 分岐してテンプレートを分ける
                switch (((UnitItem)item).ControlType)
                {
                    case ControlType.Combo:
                        // コンボボックスのテンプレート
                        return ComboTemplate;

                    case ControlType.Text:
                       // テキストボックスのテンプレート
                        return TextTemplate;
                        
                    case ControlType.Check:
                       // チェックボックスのテンプレート
                        return CheckTemplate;

                    default:
                        // エラー処理
                }
            }
        }
    }
}
```

### ViewModel

```C#
    public class SampleViewModel : BindableBase
    {
        public SampleModel SampleModel { get; set; } = SampleModel.Instance;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public SampleViewModel(){}
    }
```

### Model (`SampleModel`クラス)
入力項目群をまとめたModelクラスです

```C#
    public class SampleModel : BindableBase
    {
         public static SampleModel Instance { get; set; } = new SampleModel();

        public ObservableCollection<UnitItem> InputItems { get; set; } =
            new ObservableCollection<UnitItem>();
         
        /// <summary>
        ///     コンストラクタ
        /// </summary>
        public SampleModel() {}

        public void Init()
        {
            // InputItemsにデータを入れる処理
            // UnitItemクラスを一つずつ new し、InputItemsに Add していく
            // SelectTemplateメソッドの処理の差異に必要な「ControlType」プロパティの値も入れておく
        }
    }

```

:::note
`ObservableCollection`を使用すると、コレクション内の項目が追加、削除、変更されたときに自動的にUIに通知するので、コレクション内のデータが変更されるたびにUIがすぐに更新されます。
例えば`List<UnitItem>`とするとUIの更新は手動で行う必要がありますので、データバインディングのコードを書く手間が発生してしまいます。
:::

### Model (`UnitItem`クラス)
各入力項目一行分のクラスです

```C#
    /// <summary>
    ///   項目一行分のクラス
    /// </summary>
    public class UnitItem : BindableBase
    {
        /// <summary>
        /// コントロールタイプ
        /// ここの値をもとに、「TemplateSelector」クラスでこのUnitItemがどのテンプレートを採用するか決めている
        /// </summary>
        public ControlType ControlType { get; set; }

        /// <summary>
        ///  名前
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        ///  備考
        /// </summary>
        public string Remark { get; set; }

        /// <summary>
        ///  コンボボックスの選択肢群 ※コンストラクタでデータをセットしておく
        /// </summary>
        public List<string> ComboItemSource {get; }
        
        /// <summary>
        ///  コンボボックスの選択されている値
        /// </summary>
        public string SelectedItem {get; set; }
        
        /// <summary>
        ///  テキストボックスの入力値
        /// </summary>
        public string InputText {get; set; }

        /// <summary>
        ///  チェックボックスの選択肢群 ※コンストラクタでデータをセットしておく
        /// </summary>
        public List<CheckItem> CheckItemSource {get; }
}
```
### `CheckItem`クラス
チェックボックス一項目の中の、各チェックボックスとその表示テキストに関するクラスです

```C#
    /// <summary>
    /// チェックボックス1項目分のクラス
    /// </summary>
    public class CheckItem : BindableBase
    {     
        /// <summary>
        /// チェック状態
        /// </summary>
        public bool IsChecked { get; set; }
        
        /// <summary>
        /// 表示テキスト
        /// </summary>
        public string Content { get; set; }
    }
```

上記の実装で、実際に画面が表示されるタイミングでテンプレート選択用クラスの`SelectTemplate()`メソッドが走り、同じ種類のオブジェクト (UnitItem) を使用していても、「コンボボックス」「テキストボックス」「チェックボックス」の3種類のコントロールが画面に表示できます。

# 終わりに
ユーザーに選択させるコントロールを画面に沢山配置する場合は、意外とあるかと思います。
`DataTemplateSelector`の使用方法を抑えておくと、そのような場合も綺麗に書けそうかと感じました。