enum を文字列で置き換えて ComboBox に表示する

先週は、DataBindingで大ハマリしてました。データベース知らない→ADO.NET知らない→DataBindingあまり使わないって流れで、DataBindingをよく知らないのが原因。なので、ADO.NET本を借りてきてちょっと勉強中です。以下は WinForm の DataBinding についてです。ASP.NETはまたちょっと違うらしい。

まずは ComboBox (と ListBox) の DataSource, ValueMember, DisplayMember の使い方。しかも ADO.NET なし。こいつらも DataBinding の範疇に入るのかわかりませんが、まあどっちでもいいや。
MSDNライブラリの「ListControlクラス」を見ればサンプルコードがあるので、そちらもどうぞ。リンクしようとしたら「コンテンツが見つかりません」と出た…。Googleキャッシュには確かに残ってるんだけど…。

ComboBox (と ListBox) の DataSource, ValueMember, DisplayMember の使い方

DataSource, ValueMember, DisplayMember は言葉で説明しただけではわかりづらいので、コンボボックスに「ある enum の値を文字列で置き換えて表示/選択したい」っていうシチュエーションで順を追って説明します(よくありますよね?)。

表示/選択したい enum はこれです。

public enum Gender { Male, Female }

でも、実際の表示にはこっちの文字列を使いたい。

public static readonly string[] Gender_ja_JP = { "男性", "女性" };

本当はリソースから読む値とでも、いいほうに解釈しておいてください(^^;
今回は Person クラス(ってのがあると思ってください)にべた書きしました。

で、これをフォーム上のコンボボックス this.genderComboBox に結び付けるにはどうするか?
( Gender.Male, Gender_ja_JP[ 0 ] ) のような enum と文字列を一対一対応させたデータ型(なんでもいい)を作ってやり、そのコレクション(や配列)をコンボボックスの DataSource に入れてやればOKです。
ここでは KeyValuePair を使います。

対応表はこのようになります。

KeyValuePair<string, Gender>[] array = new KeyValuePair<string, Gender>[] {
  new KeyValuePair<string, Gender>( Person.Gender_ja_JP[ 0 ], Gender.Male ),
  new KeyValuePair<string, Gender>( Person.Gender_ja_JP[ 1 ], Gender.Female ) };

これをコンボボックスの DataSource に入れ、DisplayMember と ValueMember で表の読み方を教えてあげればお終いです。

this.genderComboBox.DataSource = array;
this.genderComboBox.ValueMember = "Value";
this.genderComboBox.DisplayMember = "Key";

DisplayMember と ValueMember は、KeyValuePair のパブリックプロパティを表しています。ValueMember が実際の値を表すプロパティDisplayMember が表示に使うプロパティDataSource はその対応表です。
あとは SelectedValue で読んでみてください。SelectedValue は enum の Gender で読み書きできます。文字列のほうではありません。MSDNライブラリによると「ValueMember にプロパティを指定しない場合、SelectedValue はオブジェクトの ToString メソッドの結果を返します。」だそうですので、ご注意を。

データバインディング時の注意

ここまでで対応付けはできたけど、データバインディングと一緒に使うときは注意が必要っぽいです。
エンティティクラスを書いて、いったんコンパイルして、VisualStudioでそのエンティティをオブジェクトデータソースとしてフォームにポトペタするってのが、データバインディングを使うときの普通なやりかたと思います。オブジェクトを詳細に設定して多数のコントロールを一度に生成できますが、この方法でコンボボックスを作るとコンボボックスのTextプロパティにバインドしたコードが生成されるようです。上述の対応付けの方法では、Textプロパティにバインドした場合にはうまく働きません!SelectedValueプロパティにバインドしないといけないようです。
何か手順を間違っているのかもしれませんが、自分で試した範囲ではそうでした。なので、フォームのコンストラクタでInitializeComponent()後に以下のように書いて対処しました。

this.genderComboBox.DataBindings.Clear();
this.genderComboBox.DataBindings.Add(
  new Binding( "SelectedValue", this.personBindingSource, "Gender", true ) );

Clearしないで両方にバインドすると、WinFormを閉じることさえできなくなるので注意。
(追記)デザイナのコンボボックスの右上の三角をクリックして出てくる「データバインド項目を使用する」を使うと、SelectedValueプロパティにバインドします。データソースで詳細をポトペタしてからこれを使うと、TextとSelectedValueの両プロパティにバインドしたコードが生成され、まともに動かないので注意。(追記終わり)

自動化してみる

ここからはおまけ。KeyValuePair[] array を作るところを自動化してみました。enum 作るのも、日本語リソースを用意するのもしょうがないけど、これをくっつけるのは自動化してしまいます。対応関係を明示的に書くってのも良いことだと思うけど、やっぱり面倒なので。

using System;
using System.Collections.Generic;
using System.Reflection;

namespace BindingTest
{
  public static class EnumHelper
  {
    public static Array CreateMappingTable( Type enumType, IList<string> textList )
    {
      try
      {
        Array enumValues = Enum.GetValues( enumType );
        if ( enumValues.Length != textList.Count )
          throw new ArgumentException( "enumType に定義された要素数と textList の要素数が異なります。" );

        Type pairType = typeof( KeyValuePair<,> ).MakeGenericType( new Type[] { typeof( string ), enumType } );
        ConstructorInfo ci = pairType.GetConstructor( new Type[] { typeof( string ), enumType } );

        Array result = Array.CreateInstance( pairType, enumValues.Length );
        int i = 0;
        foreach ( object value in enumValues )
        {
          result.SetValue( ci.Invoke( new object[] { textList[ i ], value } ), i );
          i++;
        }
        return result;
      }
      catch ( ArgumentNullException e )
      {
        throw new ArgumentNullException( "enumType が null です", e );
      }
      catch ( ArgumentException e )
      {
        throw new ArgumentException( "enumType が Enum ではありません", e );
      }
    }
  }
}

また、MakeGenericTypeみたいなマニアックな世界になってしまった。型を後から指定するタイプのコードは面倒ですね…。MakeGenericType は id:siokoshou:20070509#p2 を参考にどうぞ。あとは普段あまり目にしないものが多いけど、簡単なものしか使ってないです。メソッド名がいけてないなぁ…。

これがあれば、↓が

KeyValuePair<string, Gender>[] array = new KeyValuePair<string, Gender>[] {
  new KeyValuePair<string, Gender>( Person.Gender_ja_JP[ 0 ], Gender.Male ),
  new KeyValuePair<string, Gender>( Person.Gender_ja_JP[ 1 ], Gender.Female ) };

this.genderComboBox.DataSource = array;
this.genderComboBox.ValueMember = "Value";
this.genderComboBox.DisplayMember = "Key";

こう書けます。

this.genderComboBox.DataSource = EnumHelper.CreateMappingTable(
  typeof( Gender ), Person.Gender_ja_JP );
this.genderComboBox.ValueMember = "Value";
this.genderComboBox.DisplayMember = "Key";

ラク
enumと対応付ける文字列の要素の順番に気をつけてくださいね。enum中の要素の現れる順ではなく、値順になります。 enum E { a = 0, b = -1 } とした場合、対応する文字列のほうは string[] EText = { "b", "a" }; となります。enumの値が飛んでいても文字列のほうは値順に隙間なく並べてください。

最後に愚痴。DataSource が IDictionary<,> を受けることができればスッキリきれいなのに! IList しか受けれないようで。Dictionary がこれほどしっくりくる使いどころはそうそうないと思うんだけど…。

以上、データバインディングのお勉強してたハズが、なんか違うほうにいってしまった日記でしたw

enum を ComboBox に表示する (文字列で置き換えない)

もうちょっと実験したらまた不思議なことが起きたのでメモ。
enum を直接コンボボックスに表示する場合です。一つ前のエントリとの違いは文字列で置き換えないってところ。こっちが先だろって一人つっこみしつつ…。

表示したいのはこれ。

public enum Gender { Male, Female }

そのままコンボボックスに Male や Female と表示します。フォームにこれを書くだけ。

this.genderComboBox.DataSource = Enum.GetValues( typeof( Gender ) );

ただし!前の文字列で置き換える例では、データバインディングを使うとき、コンボボックスの Text プロパティにバインドすると動かず、SelectedValue プロパティにバインドしました。
今度は逆で、この例では Text プロパティにバインドしないと動きません。

なんだかなぁ。データバインディングはドキュメントも少ないくせにややこしい。