ComboBoxとDataGridViewComboBoxColumnを同期させる(その3)

オブジェクトとデータバインディングした、ComboBoxとDataGridViewComboBoxColumnの振る舞いに納得がいかないと書いた。以下、順に解決していこうと思う。

  • DataGridViewComboBoxColumn上のComboBoxにデータソースが表示されていない

GenderDTOのメンバであるGendersは、ComboBoxとDataGridViewComboBoxColumn上のComboBoxのデータソースとしてバインドされており、ComboBoxにはバインドされたGendersの内容が表示されるが、DataGridViewComboBoxColumnにはなぜか表示されていない。

データソースに同期した内容が表示されない、というのは普通に考えると信じられないが、本当にそうなのだから仕方が無い。原因を調べるためには、DataGridViewComboBoxColumn上のセルが描画される時にどのような処理がされているかを調べる必要がある。そのためにはDataGridViewComboBoxColumnを継承したクラスを用意して、これぞというメソッドをオーバライドして動作を見る必要があるのだが、それっぽいメソッドはあるだろうか。

ここで少し冷静になろう。DataGridViewコントロールクラスに関しては、非常に多機能なグリッドコントロールであり、今までは外販されているコントロールや内製したコントロールで実現してきた機能の殆どで標準コントロールで解決できるぞ、位の感覚しかなかったので、何時か読もうと思ってダウンロードだけしていた、以下の文書を読んでみることにした。(この前にMSDNは読んだが、いまひとつ私の知りたい情報に辿りつけなかったので)

DataGridView FAQ - jfo's coding (Word文書)

このような素晴らしい文書を書いてくれた先達に感謝しつつ、慣れない英語と格闘する。
しかし、凄いな。MSDNもそうだが、DataGridViewクラスだけで一冊の本になりそうな内容だ。内製した時も思ったけれど、グリッドコントロールはどうしても大規模になってしまう。みんなExcelが悪いんだ。(笑

この中の「2.3.1 How a DataGridViewCell works」の"Formatting for Display"からの二行がキモだ。

Anytime the grid needs to know “how would this cell display” it needs to get its FormattedValue.
DataGridView FAQ - 2.3.1 How a DataGridViewCell works/ Formatting for Display

At the cell level, all of this is controlled via the DataGridViewCell::GetFormattedValue(...) method.
DataGridView FAQ - 2.3.1 How a DataGridViewCell works/ Formatting for Display

どうやら、表示セルの値はDataGridViewCell#GetFormattedValueメソッドの戻り値により制御できるらしい。このメソッドはprotectedだが、オーバライドできるので、独自のDataGridViewComboBoxCellを用意して、デフォルトの実装を置き換えてその動作を見てみることにした。用意したのは以下のクラスだ。

public class GenderItemComboBoxCell : DataGridViewComboBoxCell
{
    protected override object GetFormattedValue(
        object value,
        int rowIndex,
        ref DataGridViewCellStyle cellStyle,
        TypeConverter valueTypeConverter,
        TypeConverter formattedValueTypeConverter,
        DataGridViewDataErrorContexts context)
    {
        return base.GetFormattedValue(
            value, rowIndex, ref cellStyle, valueTypeConverter, formattedValueTypeConverter, context);
    }
}

これをデフォルトの実装と挿げ替えるには、以下のようにセルが作られる前にカラムが持つ、セルのテンプレートを作り直してやればよい。

DataGridViewComboBoxColumn column = new DataGridViewComboBoxColumn();
column.CellTemplate = new GenderItemComboBoxCell();
form.genderDataGridView.Columns.Add(column);

WndProcにWM_PAINTが飛んできた等、結果としてセルに再描画の必要が発生した時には、必ずこのメソッドが呼ばれるらしい。普通に考えるとバインドしているデータソース・オブジェクトが飛んできても良さそうなものだが、実際にはパラメタ"value"にnullがセットされてくるだけだ。
ComboBoxのようにValueMemberとDisplayMemberを持つコントロールの場合、このメソッドの期待される振る舞いは

1.データソースが変更される
2.BindingSourceがデータソースの変更を検知する
3.コンボボックスの再描画が要求される
4.再描画処理は、データソース・オブジェクトのValueMemberを用いてGetFormattedValueメソッドを呼ぶ
5.ValueMemberに対応したDisplayMemberを正しい表示形式で戻す
6.再描画完了

となるはずなので、正しい変更かどうかは別にして、GetFormattedValueメソッドを以下のようにオーバライドしてみた。

protected override object GetFormattedValue(
    object value,
    int rowIndex,
    ref DataGridViewCellStyle cellStyle,
    TypeConverter valueTypeConverter,
    TypeConverter formattedValueTypeConverter,
    DataGridViewDataErrorContexts context)
{
    object current = this.DataGridView.BindingContext[value].Current;
    return base.GetFormattedValue(
        current, rowIndex, ref cellStyle, valueTypeConverter, formattedValueTypeConverter, context);
}

スーパクラスのメソッドに渡すオブジェクトに、データソースのカレントオブジェクトを強制的に与えてやる訳だ。これによりGenderItemComboBoxCellはどのように再描画されるようになっただろうか。

まだおかしい。型名らしき名前がGenderItemComboBoxCell上に描画されているのが解るだろう。型名を表示したい訳ではないのだが.....型名が表示される、なにか見覚えがあるなと思い、データソースになっているGenderItemクラスのToString()メソッドをオーバライドしてみた。

public class GenderItem
{
  〜略〜
    public override string ToString()
    {
        return this.Name;
    }
}

さて、結果はどうだろう。

ビンゴ。どうやらDataGridViewComboBoxCell#GetFormattedValueメソッドのデフォルトの実装は、渡されたオブジェクトのToString()メソッドを呼び出すらしい。

上手くいったのはいいけど、どうしてこういう仕様なんだろう。

ToString()メソッドによるオブジェクトの文字列表現はごく一部を除いてはデバッグのためにしか使わないと考えるのが普通ではなかったか? 少なくともJavaではそうだし、C#でもVisual Studioのデバッガのオプションにはビジュアライザの指定として"常時ToString()を呼び出す"っていう項目がある位だし。