ComboBoxに列挙型をバインドして、列挙値に応じたテキストを表示し、選択された項目を取得する方法を説明。カスタムコントロールを使う方法やUWPでの注意点も取り上げる。
WPFアプリやUWPアプリなどでは、データを画面に表示するためにデータバインディングを使うのが一般的だ。TextBoxコントロールやSliderコントロールなど1つの値を表示するだけのコントロールならば、データバインディングは簡単だ。ComboBoxコントロールやListViewコントロールなど、複数の項目を表示してその中からエンドユーザーに選択してもらうようなコントロールでデータバインディングするのは、ちょっと難しくなる。
本稿では、ComboBoxコントロールに列挙型のデータをバインドする方法を紹介する。特定のトピックをすぐに知りたいという方は以下のリンクを活用してほしい。
なお、本稿に掲載したサンプルコードをそのまま試すにはVisual Studio 2017以降が必要である。掲載したサンプルコードに基づいて作成したWPFアプリの例を次の画像に示す。
列挙値と表示文字列のDictionaryコレクションをComboBoxコントロールのItemsSourceプロパティに、データクラス(=バインディングソース)の列挙型プロパティをSelectedValueプロパティにバインドすればよい(次の図)。
なお、列挙値そのもの(上の図ではOne/Two/Three)をそのままドロップダウンに表示するのであれば、Dictionaryコレクションの代わりに、列挙値だけのコレクション(配列やListコレクションなど)を使ってもよい。その場合、WPFではObjectDataProviderクラスを利用すると、XAMLだけでドロップダウンの表示が完結する(この方法はUWPでは利用できない)。
列挙値(上の図ではOne/Two/Three)とドロップダウンリストに表示したい文字列(上の図では選択肢1/選択肢2/選択肢3)は、日本語環境では異なっているのが一般的だ。そこで本稿では、上図のようにDictionaryコレクションを使う汎用的な方法だけを説明する。
列挙型SampleEnum
それでは、上の図をWPFで実装する例を順を追って説明していこう。まずはバインドする列挙型の定義から(次のコード)。
namespace dotNetTips1207CS
{
public enum SampleEnum
{
One = 1, Two, Three,
}
}
Public Enum SampleEnum
One = 1
Two
Three
End Enum
データクラスSampleData
バインディングソースとなるのが、上の図の左側にあるクラスだ(次のコード)。このクラスのEnumValueプロパティを、後ほどComboBoxのSelectedValueプロパティにバインドする。双方向にバインドするため、INotifyPropertyChangedインタフェース(System.ComponentModel名前空間)を実装している。
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace dotNetTips1207CS
{
public abstract class BindableBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
protected void SetProperty<T>(ref T storage, T value,
[CallerMemberName] string propertyName = null)
{
if (object.Equals(storage, value))
return;
storage = value;
OnPropertyChanged(propertyName);
}
}
public class SampleData : BindableBase
{
// 画面とバインドしたい列挙型のプロパティ
private SampleEnum _enumValue;
public SampleEnum EnumValue
{
get => _enumValue;
set => SetProperty(ref _enumValue, value);
}
}
}
Imports System.ComponentModel
Imports System.Runtime.CompilerServices
Public Class BindableBase
Implements INotifyPropertyChanged
Public Event PropertyChanged As PropertyChangedEventHandler _
Implements INotifyPropertyChanged.PropertyChanged
Overridable Sub OnPropertyChanged(propertyName As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
Overridable Sub SetProperty(Of T)(ByRef storage As T, value As T,
<CallerMemberName> Optional propertyName As String = Nothing)
If (Object.Equals(storage, value)) Then
Return
End If
storage = value
OnPropertyChanged(propertyName)
End Sub
End Class
Public Class SampleData
Inherits BindableBase
' 画面とバインドしたい列挙型のプロパティ
Private _enumValue As SampleEnum
Public Property EnumValue As SampleEnum
Get
Return _enumValue
End Get
Set(value As SampleEnum)
SetProperty(_enumValue, value)
End Set
End Property
End Class
列挙値と表示文字列のDictionary
前掲の図の右側にあるDictionaryコレクションは、列挙値とそれに対応する表示文字列のテーブルだ。これは恐らくアプリの他の部分からも利用するであろうから、データクラスとは別の場所に置こう。ここでは次のコードに示すAppCommonDataクラスを作った。
using System.Collections.Generic;
namespace dotNetTips1207CS
{
public class AppCommonData
{
// ComboBoxの一覧に表示するデータ
public Dictionary<SampleEnum, string> SampleEnumNameDictionary { get; }
= new Dictionary<SampleEnum, string>();
public AppCommonData()
{
// 列挙値とその表示文字列のDictionaryを作る
SampleEnumNameDictionary.Add(SampleEnum.One, "選択肢1");
SampleEnumNameDictionary.Add(SampleEnum.Two, "選択肢2");
SampleEnumNameDictionary.Add(SampleEnum.Three, "選択肢3");
}
}
}
Public Class AppCommonData
' ComboBoxの一覧に表示するデータ
Public ReadOnly Property SampleEnumNameDictionary As Dictionary(Of SampleEnum, String) _
= New Dictionary(Of SampleEnum, String)
Public Sub New()
' 列挙値とその表示文字列のDictionaryを作る
SampleEnumNameDictionary.Add(SampleEnum.One, "選択肢1")
SampleEnumNameDictionary.Add(SampleEnum.Two, "選択肢2")
SampleEnumNameDictionary.Add(SampleEnum.Three, "選択肢3")
End Sub
End Class
なお、列挙値をそのまま表示する(前掲の図でいえばOne/Two/Threeと表示する)のであれば、上のコードのコンストラクタを次のコードのように変更する。
public AppCommonData()
{
// 列挙値をそのまま表示する場合
SampleEnum[] enumvalues = Enum.GetValues(typeof(SampleEnum)) as SampleEnum[];
foreach (var e in enumvalues)
SampleEnumNameDictionary.Add(e, e.ToString());
}
Public Sub New()
' 列挙値をそのまま表示する場合
Dim enumvalues() As SampleEnum = [Enum].GetValues(GetType(SampleEnum))
For Each e In enumvalues
SampleEnumNameDictionary.Add(e, e.ToString())
Next
End Sub
ComboBoxとバインドする
以上のパーツを画面のXAMLにまとめよう(次のコード)。これで完成である。ComboBoxコントロールのドロップダウンには[選択肢1][選択肢2][選択肢3]と表示され、いずれかを選択すると(データクラスのプロパティの変更を介して)TextBlockコントロールの表示が列挙値[One][Two][Three]の対応する値に変わる。
<Window x:Class="dotNetTips1207CS.MainWindow"
……省略…… >
<Window.Resources>
<local:SampleData x:Key="Sample" />
<local:AppCommonData x:Key="CommonData" />
</Window.Resources>
<Grid DataContext="{StaticResource Sample}">
<StackPanel ……省略…… >
<TextBlock>選択されているEnum値=<Run
Text="{Binding EnumValue}"/></TextBlock>
<ComboBox ItemsSource="{Binding SampleEnumNameDictionary,
Source={StaticResource CommonData}}"
DisplayMemberPath="Value"
SelectedValue="{Binding EnumValue, Mode=TwoWay}"
SelectedValuePath="Key"
/>
</StackPanel>
</Grid>
</Window>
上のXAMLコードは、ちょっと煩雑だ。ComboBoxコントロールのプロパティを4つも指定しなければならないし、そのうちの2つはデータバインディングだ。ComboBoxコントロールが1つや2つならまだしも、10個も20個もとなったらもう少し楽をしたくなるだろう。
ComboBoxを継承して特定の列挙型専用のカスタムコントロールを作ってしまえば(次のコード)、1つのプロパティにデータバインディングするだけで済む。
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Windows.Controls;
namespace dotNetTips1207CS
{
// SampleEnum列挙型に特化したComboBox
public class SampleEnumComboBox : EnumComboBox<SampleEnum>
{
// 列挙値とその表示文字列をペアにしたDictionaryを返すメソッド
// (このオーバーライドを書かないときは、列挙値そのものが表示文字列になる)
protected override Dictionary<SampleEnum, string> GetDictionary()
=> (new AppCommonData()).SampleEnumNameDictionary;
}
// 汎用的な親クラス
public class EnumComboBox<TEnum> : ComboBox
where TEnum : struct, IComparable, IFormattable, IConvertible
// 注意:where TEnum : Enumとは書けない。
// 仕方がないのでEnumクラスの継承元を列挙して代用する。
{
public EnumComboBox()
{
#if DEBUG
// TEnumが確かにEnum型であることのチェック
if (!typeof(TEnum).GetTypeInfo().IsEnum)
throw new ArgumentException("TEnum must be an enumerated type");
#endif
// 列挙値とその表示文字列をペアにしたDictionaryを得る
Dictionary<TEnum, string> items = GetDictionary();
// 上のDictionaryを、このComboBoxに表示する
this.ItemsSource = items;
this.DisplayMemberPath = "Value"; // 一覧に表示するもの(表示文字列)
this.SelectedValuePath = "Key"; // 選択されたもの(列挙値)
}
// 列挙値と表示文字列のDictionaryを得るメソッド
// この既定の実装は、列挙値そのものを表示文字列とする
// (必要に応じて継承先で上書きする)
protected virtual Dictionary<TEnum, string> GetDictionary()
{
var items = new Dictionary<TEnum, string>();
var values = (TEnum[])Enum.GetValues(typeof(TEnum));
foreach (var v in values)
items.Add(v, Enum.GetName(typeof(TEnum), v));
return items;
}
}
}
Imports System.Reflection
' SampleEnum列挙型に特化したComboBox
Public Class SampleEnumComboBox
Inherits EnumComboBox(Of SampleEnum)
' 列挙値とその表示文字列をペアにしたDictionaryを返すメソッド
' (このオーバーライドを書かないときは、列挙値そのものが表示文字列になる)
Public Overrides Function GetDictionary() As Dictionary(Of SampleEnum, String)
Return (New AppCommonData()).SampleEnumNameDictionary
End Function
End Class
' 汎用的な親クラス
Public Class EnumComboBox(Of TEnum As {Structure, IComparable, IFormattable, IConvertible})
Inherits ComboBox
' 注意:Of TEnum As Enumとは書けない。
' 仕方がないのでEnumクラスの継承元を列挙して代用する。
Public Sub New()
#If DEBUG Then
' TEnumが確かにEnum型であることのチェック
If (Not GetType(TEnum).GetTypeInfo().IsEnum) Then
Throw New ArgumentException("TEnum must be an enumerated type")
End If
#End If
' 列挙値とその表示文字列をペアにしたDictionaryを得る
Dim items As Dictionary(Of TEnum, String) = GetDictionary()
' 上のDictionaryを、このComboBoxに表示する
Me.ItemsSource = items
Me.DisplayMemberPath = "Value" ' 一覧に表示するもの(表示文字列)
Me.SelectedValuePath = "Key" ' 選択されたもの(列挙値)
End Sub
' 列挙値と表示文字列のDictionaryを得るメソッド
' この既定の実装は、列挙値そのものを表示文字列とする
' (必要に応じて継承先で上書きする)
Overridable Function GetDictionary() As Dictionary(Of TEnum, String)
Dim items = New Dictionary(Of TEnum, String)
Dim values = [Enum].GetValues(GetType(TEnum))
For Each v In values
items.Add(v, [Enum].GetName(GetType(TEnum), v))
Next
Return items
End Function
End Class
このカスタムコントロールを使うと、XAMLのコードは簡潔に書ける(次のコード)。
<local:SampleEnumComboBox SelectedValue="{Binding EnumValue, Mode=TwoWay}" />
UWPアプリの場合、バインディングソースの列挙型プロパティをComboBoxコントロールのSelectedValueプロパティにバインドするとうまく動かない。バインディングソースに変更があったとき、一度は正常な値がSelectedValueプロパティにセットされるのだが、なぜか続けて二度目としてnullがセットされてしまうのだ。
この不具合を回避するには、バリューコンバーターを介してSelectedIndexプロパティにバインドするか、または、カスタムコントロール内でSelectedValueプロパティの変化をトラップして対処する。本稿ではその詳細は述べないが、C#のサンプルコードを筆者のGitHubに上げてあるので参考にしていただければ幸いである(バリューコンバーターはEnumToIndexConverterクラスに、カスタムコントロールはEnumComboBoxクラスに実装してある)。
ListViewコントロールやComboBoxコントロールなどは、一般に2つのデータをバインドすることになる。すなわち、一覧表示するデータはItemsSourceプロパティに、エンドユーザーが選択した値はSelectedValueプロパティにバインドする。XAMLの記述が煩雑になるので、同じ選択肢を繰り返し記述する場合は本稿に示したようにカスタムコントロールを作ってしまうのも一案だ。
なおUWPでは、ComboBoxコントロールに列挙型をバインドするときに工夫が必要になる。
利用可能バージョン:.NET Framework 3.5以降
カテゴリ:WPF 処理対象:データバインディング
カテゴリ:WPF/XAML 処理対象:ComboBoxコントロール
使用ライブラリ:ComboBoxコントロール(System.Windows.Controls名前空間)
使用ライブラリ:INotifyPropertyChangedインタフェース(System.ComponentModel名前空間)
関連TIPS:WPF/UWP:ラジオボタンを双方向バインディングするには?[C#/VB]
関連TIPS:WPF/UWP:テキストブロックの一部分だけをデータバインディングするには?[XAML]
関連TIPS:WPF:ラジオボタンの選択をバインディングソースに反映させるには?[C#/VB]
関連TIPS:WPF:DataGridやListViewなどに表示しているデータを別スレッドから変更するには?[C#、VB]
関連TIPS:オプション引数が使えるメソッドを作るには?[C#/VB]
関連TIPS:Caller Info属性で呼び出し元の情報を得るには?[C#/VB]
関連TIPS:構文:nullチェックを簡潔に記述するには?[C# 6.0]
関連TIPS:構文:メソッドやプロパティをラムダ式で簡潔に実装するには?[C# 6.0/7.0]
Copyright© Digital Advantage Corp. All Rights Reserved.