2011年9月6日火曜日

wpf : ComboBoxにEnumをバインドして別の文字列を割り当てる

ComboBoxにEnumをバインドして別の文字列を割り当てる方法を書きます。 参考にしたのはこちらの、一般的なクラスをComboBoxの項目に登録する方法です。

Enumだけを対象にするなら違うやり方もあるようです。

以降、最初に見つけたやり方を元にしたコードについて説明します。

Enumとそれをプロパティとして持ったクラスはこんな感じ。

public enum DrinkEnum
{
    Water,
    Juice,
    Tea,
    Coffee
}

public class AppData
{
    private DrinkEnum _drinkId;
    public DrinkEnum DrinkId
    {
        get
        {
            return _drinkId;
        }
        set
        {
            _drinkId = value;
            MessageBox.Show("DrinkId = " + value);
        }
    }
}

AppDataはサンプル用の、DrinkIdプロパティがあるだけのクラスです。 MessageBox.Showはバインディングの確認用です。

Enumと文字列のペアを格納するクラスを作ります。

using System.Collections.Generic;

namespace ComboBoxEnumTest
{
    public class ComboBoxDrinkItem
    {
        public DrinkEnum Id { get; set; }
        public string ComboBoxDisplay { get; set; }

        public static List<ComboBoxDrinkItem> DrinkItems { get; set; }

        static ComboBoxDrinkItem()
        {
            DrinkItems = new List<ComboBoxDrinkItem>
            {
                new ComboBoxDrinkItem()
                {
                    Id = DrinkEnum.Water,
                    ComboBoxDisplay = "水"
                },
                new ComboBoxDrinkItem()
                {
                    Id = DrinkEnum.Juice,
                    ComboBoxDisplay = "ジュース"
                },
                new ComboBoxDrinkItem()
                {
                    Id = DrinkEnum.Tea,
                    ComboBoxDisplay = "紅茶"
                },
                new ComboBoxDrinkItem()
                {
                    Id = DrinkEnum.Coffee,
                    ComboBoxDisplay = "コーヒー"
                }
            };
            DrinkItems.TrimExcess();
        }
    }
}

ComboBoxDrinkItemがEnumと文字列のペアを格納するクラスです。 Idプロパティに列挙値を、ComboBoxDisplayにComboBoxで表示する文字列を格納します。 staticプロパティのDrinkItemsはペアをまとめて記憶しておくためのリストです。 このサンプルでは後から文字列を変えるようなことはないので、静的コンストラクタで全てのペアを登録しています。 ComboBoxには登録順(List内での順番)に表示されます。 ComboBox表示中に項目を変える場合、使うコレクションクラスはListではなく通知機能があるObservableCollectionの方が良いかもしれません。 (通知機能については未確認)

xamlとxaml.csはこんな感じになります。

<Window x:Class="ComboBoxEnumTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:app="clr-namespace:ComboBoxEnumTest"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel>
        <ComboBox
                IsEditable="False"
                ItemsSource="{
                    Binding Path=DrinkItems,
                    Source={x:Static app:ComboBoxDrinkItem.DrinkItems}
                }"
                SelectedValuePath="Id"
                DisplayMemberPath="ComboBoxDisplay"
                SelectedValue="{Binding DrinkId}"
                />
    </StackPanel>
</Window>
// MainWindow.xaml.cs
using System.Windows;

namespace ComboBoxEnumTest
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new AppData();
        }
    }
}

xamlで自作クラスを使うためにappのネームスペースを登録しています。

ComboBoxのプロパティについて、まず、ユーザーが列挙値以外を入力できないようにIsEditable="False"にします。

ItemsSourceで列挙値と文字列のペアリスト(ComboBoxDrinkItem.DrinkItems)を登録します。 SelectedValuePathとDisplayMemberPathで列挙値と文字列のプロパティ名を指定します。 SelectedValueはバインド対象のAppDataのプロパティです。

ItemsSource、SelectedValuePath、DisplayMemberPathにはComboBoxDrinkItemに関する値を、SelectedValueにはMainWindow.DataContextに登録したバインド対象プロパティを書くのがポイントです。 ComboBoxは「ユーザーが入力したデータのバインド先」と「表示データのバインド先」を別々に設定できるんですね。

ちなみに、ComboBox/ItemsSourceでBinding PathとSourceの両方にDrinkItemsを書いています。 重複しているのは無駄に見えますが、片方を省略することはできませんでした。

説明はこんな感じかな? wpfだとコードが分散しがちなので注意が必要だけど、これは許容範囲な気がします。 ちょっと大仰ですけどね。 データ格納用のAppDataクラスと表示でしか使わないComboBoxDrinkItemクラスを分けれたのが良さげ。 もちろん、コードの分け方やデータ構造によってはこのサンプルのComboBoxDrinkItemにあたるクラスに色んなプロパティやメソッドを書くのもありでしょう。