2008年02月23日

二次元配列をビュー

多次元配列を私は嫌っていますが、世間的にはそれなりに人気のようです。DataGridViewに2次元配列のレコードを表示させたい と言うスレッドでは、二次元配列を DataGridView の DataSource にできないかという話が出ました。もっとも質問が微妙で、単純にデータソースとして二次元配列を使いたいのか、それとも既にデータソースに DataTable を設定していてそれにデータを追加したいのか判断に迷うところではありましたけど。

二次元配列なぞさっさと捨てるのが幸せになる方法だと思っているのですが、それはそれとして技術的興味はあります。

まず、データソースに使用できるものを調べてみます。DataGridView.DataSource プロパティ の解説によると、以下のいずれかと言うことになります。

  • IList インターフェイス。1 次元配列を含みます。
  • IListSource インターフェイス。DataTable クラス、DataSet クラスなどがあります。
  • IBindingList インターフェイス。BindingList クラスなどがあります。
  • IBindingListView インターフェイス。BindingSource クラスなどがあります。

このうち、後者二つのインターフェイスのメンバを見る限り、これらのインターフェイスで追加されたメンバは追加やソート、フィルタに関するものばかりなので固定長である配列とは縁遠いもののようです。となると前者二つがターゲットですね。

IList は配列があらかじめ実装しているインターフェイスです。ですがわざわざ「1 次元配列を」と特記されてるように、二次元配列は対象外。まあそもそも多次元配列は IList では実際上操作不能ですしね。

IListSource のメンバを見ると、複数のリストが格納されているかどうかと IList を取得するのの二つのみ。多次元配列自体がこれを実装してるならともかく、自前で実装する意味はなさそう。

そもそも DataTable をデータソースにセットした場合どういう挙動になるのでしょうか。DataTable は上のリストにあるとおり IListSource を実装しているクラスですから、データソースの要求元には IListSource.GetList を返すはずです。実際に呼び出してみると(GetList は明示的実装されてるので IListSource にキャストする必要があります)、DataView を返しているのが分かります。

では DataView はどんなインターフェイスを持っているのかと言うと、上のリストで挙げたインターフェイスだけでも IBindingList、IBindingListView、IList となかなか多彩な顔ぶれです。そのほか、気になるものとして ITypedList も実装しているようです。

ここでちょっと話は変わりますが、適当なクラス、例えば ProcessStartInfo のインスタンスを ArrayList 辺りにいくつか放り込んで DataGridView で表示させてみるとどうなるかと言うと、このとき各列にはインスタンスプロパティがそれぞれ割り当てられることになります。Int32 の配列の配列(二次元のジャグ配列)をデータソースにすると、これもそれぞれの要素ごとにプロパティが表示されるようになります。つまり Int32 の配列のプロパティである Length とか SyncRoot とか Rank とかが列になります。

IList をデータソースにした場合、各要素のプロパティが列として扱われると言うことが分かりました。DataView は IList の要素として DataRowView を扱いますが、しかし DataView をデータソースにしても DataRowView のプロパティである Count やら Sort やらは表示されず、普通にテーブルが表示されます。これは何故でしょうか。

と調子よく論を進めてきましたが、ここで穴を一つ発見。ArrayList に DataRowView を突っ込んで表示させた場合も普通にテーブルが表示されます。DataRowView は ICustomTypeDescriptor を実装しており、これで自身の表示内容をプロパティから自身の持つデータに置き換えているんですね。が、まあこれは見なかったことにします。二次元配列の場合は利用しようがないので。

さて、ではいよいよわざとらしく名前を出した ITypedList インターフェイスです。MSDN の項目 には、まず見出しに「バインドに利用できるプロパティがバインド先のオブジェクトのパブリック プロパティと異なる場合に、バインド可能リストのスキーマを検出できるようにします。」とあり、いささか文意を読み取りづらいですがこれっぽい雰囲気を感じますね。解説にも似たようなことが書かれています。やっぱり読み取りづらいですけど。

ITypedList は二つのメンバを持ちます。一つは GetListName でまあこれはどうでもいいですね。もう一つが GetItemProperties です。引数に PropertyDescriptor の配列を受け取り、返値として PropertyDescriptorCollection を返します。つまり、飽くまで公開するのはプロパティであるってのがポイントです。

PropertyDescriptor と言えば以前書いた EnumDisplayName って記事の続編としてちょっといじってみようかと思ったりもしたものですが結局お蔵入りでした。ていうかそもそも何をどう拡張しようとしたのかすら思い出せません。

とりあえず DataView の GetItemProperties を呼び出してみましょう。

// C#
DataTable table = new DataTable("table");
table.Columns.Add("ID", typeof(int));
table.Columns.Add("NAME", typeof(string));
PropertyDescriptorCollection props =
    ((ITypedList)table.DefaultView).GetItemProperties(null);
foreach (PropertyDescriptor prop in props) {
    Console.WriteLine("Name:{0} Property:{1} Component:{2} ({3})",
                      prop.Name, prop.PropertyType,
                      prop.ComponentType, prop.GetType());
}

ID と NAME をそれぞれプロパティとして PropertyDescriptor が公開されているのが確認できます。プロパティの型はそれぞれカラムの型、ComponentType は DataRowView が返されます。GetType() はおまけですが DataColumnPropertyDescriptor なる型であることが分かります。

PropertyDescriptor さえ見つかれば、その GetValue/SetValue でデータを取得・設定することは容易に想像できます。と言うことで DataView からデータを取得してみましょう。

// C#
DataTable table = new DataTable("table");
table.Columns.Add("ID", typeof(int));
table.Columns.Add("NAME", typeof(string));
table.Rows.Add(0, "zero");
table.Rows.Add(1, "one");
PropertyDescriptorCollection props =
    ((ITypedList)table.DefaultView).GetItemProperties(null);
foreach (DataRowView row in table.DefaultView) {
    foreach (PropertyDescriptor prop in props) {
        Console.Write("{0}:{1} ", prop.Name, prop.GetValue(row));
    }
    Console.WriteLine();
}

うまいこと取れることが確認できます。

下準備は整いました。後はこの ITypedList 及び PropertyDescriptor をどう実装するかと言う話になります。

まず前提として、DataGridView に表示するだけではつまりません。固定長と言う制約上、行追加はできないにせよ、編集はしたいところです。もちろん編集結果はもとの二次元配列にも及ぶべきです。対象を Object の二次元配列とすると値型の二次元配列を使うことができませんから(参照型の二次元配列なら共変性があるので Object の二次元配列にキャストできるのですが)、ジェネリックな構造が望まれます。

まず、IList 及び ITypedList を実装するクラスが必要です。これで二次元配列をラップします。実際に使うのは Count プロパティと Item プロパティ(インデクサ)ぐらいですが。この Item プロパティで何を返すかが問題ですね。

PropertyDescriptor の実装クラスについて考える場合、この PropertyDescriptor はなんのデスクリプタなのかというのを考えます。前述の DataView の場合、いずれかの列を記述するものでした。ComponentType は一般に GetValue/SetValue などに渡されるオブジェクト(コンポーネント)の型で、これが DataRowView ですから、まとめると「DataRowView の一つの列を表現する記述子」と言うことになります。

これを二次元配列に敷衍すると、PropertyDescriptor はやはり列を記述するものとなります。ではそのコンポーネントは何かとなると、少々考えさせられます。単純には二次元配列の特定の行を指す一次元配列ですが、二次元配列から任意の行の一次元配列を取り出すとなると要素のコピーと言うことになってしまいます。C/C++ や unsafe な C# ならポインタですんだりしますけど。となるとそのポインタ代わりの「任意の行を指す何か」が必要です。この何かは複雑な機能は必要ありません(ポインタ代わりですし)。元となる二次元配列にアクセスできること、自分が指している行。あとは要求された列に対して自分が指している行と合わせて元となる二次元配列の要素を入出力するだけですね。

そしてこのコンポーネントはそのまま IList の Item プロパティ(インデクサ)が返すオブジェクトにもなります。

以上で解説は大体終了です。それでは実装を見てみましょう。まず、「ポインタ代わりのコンポーネント」から。

// C#
public class TwoDimensionArrayColumnIndexer<T> {
    public TwoDimensionArrayColumnIndexer(T[,] array, int rowIndex) {
        this.array = array;
        this.rowIndex = rowIndex;
    }
    public T this[int index] {
        get { return this.array[this.rowIndex, index]; }
        set { this.array[this.rowIndex, index] = value; }
    }
    private T[,] array;
    private int rowIndex;
}

' VB
Public Class TwoDimensionArrayColumnIndexer(Of T)
    Public Sub New(ByVal array As T(,), ByVal rowIndex As Integer)
        Me.array = array
        Me.rowIndex = rowIndex
    End Sub
    Public Default Property Item(ByVal index As Integer) As T
        Get
            Return Me.array(Me.rowIndex, index)
        End Get
        Set
            Me.array(Me.rowIndex, index) = value
        End Set
    End Property
    Private array As T(,)
    Private rowIndex As Integer
End Class

実に単純ですが。一種のカリー化みたいな?

さて、次に PropertyDescriptor の実装クラスです。基本的に PropertyDescriptor は Public にしないみたいなんで、ラッパクラスにネストされたクラスとして実装します。外側のラッパクラスのほうを後回しにする都合上パーシャルクラスで表現。

// C#
public partial class TwoDimensionArrayView<T> : IList, ITypedList {
    private class ColumnDescriptor : PropertyDescriptor {
        public ColumnDescriptor(int columnIndex)
                : base(columnIndex.ToString(), null) {
            this.columnIndex = columnIndex;
        }
        private int columnIndex;
        public override string DisplayName {
           // 適当に
           get { return string.Format("Index{0}", this.columnIndex); }
        }
        public override Type ComponentType {
           get { return typeof(TwoDimensionArrayColumnIndexer<T>); }
        }
        public override bool IsReadOnly { get { return false; } }
        public override Type PropertyType { get { return typeof(T); } }
        public override bool CanResetValue(object component) {
            return false;
        }
        public override object GetValue(object component) {
            TwoDimensionArrayColumnIndexer<T> indexer
                = component as TwoDimensionArrayColumnIndexer<T>;
            return indexer == null ? default(T) : indexer[this.columnIndex];
        }
        public override void ResetValue(object component) {
            throw new NotSupportedException();
        }
        public override void SetValue(object component, object value) {
            TwoDimensionArrayColumnIndexer<T> indexer
                = component as TwoDimensionArrayColumnIndexer<T>;
            indexer[this.columnIndex] = (T)value;
        }
        public override bool ShouldSerializeValue(object component) {
           return false;
        }
    }
}

' VB
Public Partial Class TwoDimensionArrayView(Of T)
    Private Class ColumnDescriptor
        Inherits PropertyDescriptor
        Public Sub New(ByVal columnIndex As Integer)
            MyBase.New(columnIndex.ToString(), Nothing)
            Me.columnIndex = columnIndex
        End Sub
        Private columnIndex As Integer
        Public Overloads Overrides ReadOnly Property DisplayName() As String
            Get ' 適当に
                Return String.Format("Index{0}", Me.columnIndex)
            End Get
        End Property
        Public Overloads Overrides ReadOnly Property ComponentType() As Type
            Get
                Return GetType(TwoDimensionArrayColumnIndexer(Of T))
            End Get
        End Property
        Public Overloads Overrides ReadOnly Property IsReadOnly() As Boolean
            Get
                Return False
            End Get
        End Property
        Public Overloads Overrides ReadOnly Property PropertyType() As Type
            Get
                Return GetType(T)
            End Get
        End Property
        Public Overloads Overrides Function CanResetValue( _
                ByVal component As Object) As Boolean
            Return False
        End Function
        Public Overloads Overrides Function GetValue( _
                ByVal component As Object) As Object
            Dim indexer As TwoDimensionArrayColumnIndexer(Of T) _
                = TryCast(component, TwoDimensionArrayColumnIndexer(Of T))
            If indexer Is Nothing Then
                Return Nothing
            Else
                Return indexer(Me.columnIndex)
            End If
        End Function
        Public Overloads Overrides Sub ResetValue( _
                ByVal component As Object)
            Throw New NotSupportedException()
        End Sub
        Public Overloads Overrides Sub SetValue( _
                ByVal component As Object, ByVal value As Object)
            Dim indexer As TwoDimensionArrayColumnIndexer(Of T) _
                = TryCast(component, TwoDimensionArrayColumnIndexer(Of T))
            If indexer IsNot Nothing Then
                indexer(Me.columnIndex) = DirectCast(value, T)
            End If
        End Sub
        Public Overloads Overrides Function ShouldSerializeValue( _
                ByVal component As Object) As Boolean
            Return False
        End Function
    End Class
End Class

オーバーライドするメソッドは多めですが複雑ではないですね。ポイントは GetValue と SetValue のみですし。

そして最後が IList 及び ITypedList を実装するラッパクラス。

// C#
public partial class TwoDimensionArrayView<T> : IList, ITypedList {
    public TwoDimensionArrayView(T[,] array) { this.array = array; }
    private T[,] array;
    public int Count { get { return this.array.GetLength(0); } }

#region インデクサ 明示的実装 セッタは無用
    object IList.this[int index] {
        get { return this[index]; }
        set { throw new NotSupportedException(); }
    }
    // 厳密に型指定されたインデクサ
    public TwoDimensionArrayColumnIndexer<T> this[int rowIndex] {
        get { 
            return new TwoDimensionArrayColumnIndexer<T>(this.array,
                                                      rowIndex);
        }
    }
#endregion
#region ITypedList の明示的実装
    PropertyDescriptorCollection ITypedList.GetItemProperties(
            PropertyDescriptor[] listAccessors) {
        PropertyDescriptor[] props
            = new PropertyDescriptor[this.array.GetLength(1)];
        for (int i = 0; i < props.Length; i++) {
            // それぞれの行について記述子を作成。詳細は後述
            props[i] = new ColumnDescriptor(i);
        }
        return new PropertyDescriptorCollection(props);
    }
    string ITypedList.GetListName(PropertyDescriptor[] listAccessors) {
        return string.Empty;
    }
#endregion
#region IList ほかの明示的実装 基本的に例外投げるだけ
    bool IList.IsFixedSize { get { return true; } }
    bool IList.IsReadOnly { get { return false; } }
    bool ICollection.IsSynchronized { get { return false; } }
    object ICollection.SyncRoot { get { return this; } }
    IEnumerator IEnumerable.GetEnumerator() { return null; }

    int IList.Add(object value) { throw new NotSupportedException(); }
    void IList.Clear() { throw new NotSupportedException(); }
    bool IList.Contains(object value) { throw new NotSupportedException(); }
    int IList.IndexOf(object value) { throw new NotSupportedException(); }
    void IList.Insert(int index, object value) {
        throw new NotSupportedException();
    }
    void IList.Remove(object value) { throw new NotSupportedException(); }
    void IList.RemoveAt(int index) { throw new NotSupportedException(); }
    void ICollection.CopyTo(Array array, int start) {
        throw new NotSupportedException();
    }
#endregion
}

' VB
Public Partial Class TwoDimensionArrayView(Of T)
    Implements IList, ITypedList

    Public Sub New(ByVal array As T(,))
        Me.array = array
    End Sub
    Private array As T(,)
    Public ReadOnly Property Count() As Integer Implements IList.Count
        Get
            Return Me.array.GetLength(0)
        End Get
    End Property
#Region "デフォルトプロパティ 明示的実装 セッタは無用"
    Private Property ObjectiveItem(ByVal index As Integer) _
            As Object Implements IList.Item
        Get
            Return Me.Item(index)
        End Get
        Set
            Throw New NotSupportedException()
        End Set
    End Property
    Public Default ReadOnly Property Item(ByVal rowIndex As Integer) _
            As TwoDimensionArrayColumnIndexer(Of T)
        Get
            Return New TwoDimensionArrayColumnIndexer(Of T)(Me.array, _
                                                            rowIndex)
        End Get
    End Property
#End Region
#Region "ITypedList のプライベート実装 基本的に例外投げるだけ"
    Public Function GetItemProperties( _
            ByVal listAccessors As PropertyDescriptor()) _
            As PropertyDescriptorCollection _
            Implements ITypedList.GetItemProperties
        Dim props As PropertyDescriptor() _
            = New PropertyDescriptor(Me.array.GetLength(1) - 1) {}
        For i As Integer = 0 To props.Length - 1
            props(i) = New ColumnDescriptor(i)
        Next
        Return New PropertyDescriptorCollection(props)
    End Function
    Public Function GetListName(ByVal listAccessors As PropertyDescriptor()) _
            As String Implements ITypedList.GetListName
        Return String.Empty
    End Function
#End Region
#Region "IList ほかのプライベート実装"
    Private ReadOnly Property IsFixedSize() As Boolean _
            Implements IList.IsFixedSize
        Get
            Return True
        End Get
    End Property
    Private ReadOnly Property IsReadOnly() As Boolean _
            Implements IList.IsReadOnly
        Get
            Return False
        End Get
    End Property
    Private ReadOnly Property IsSynchronized() As Boolean _
            Implements ICollection.IsSynchronized
        Get
            Return False
        End Get
    End Property
    Private ReadOnly Property SyncRoot() As Object _
            Implements ICollection.SyncRoot
        Get
            Return Me
        End Get
    End Property
    Private Function GetEnumerator() As IEnumerator _
            Implements IEnumerable.GetEnumerator
        Return Nothing
    End Function

    Private Function Add(ByVal value As Object) _
            As Integer Implements IList.Add
        Throw New NotSupportedException()
    End Function
    Private Sub Clear() Implements IList.Clear
        Throw New NotSupportedException()
    End Sub
    Private Function Contains(ByVal value As Object) As Boolean _
            Implements IList.Contains
        Throw New NotSupportedException()
    End Function
    Private Function IndexOf(ByVal value As Object) As Integer _
            Implements IList.IndexOf
        Throw New NotSupportedException()
    End Function
    Private Sub Insert(ByVal index As Integer, ByVal value As Object) _
            Implements IList.Insert
        Throw New NotSupportedException()
    End Sub
    Private Sub Remove(ByVal value As Object) Implements IList.Remove
        Throw New NotSupportedException()
    End Sub
    Private Sub RemoveAt(ByVal index As Integer) Implements IList.RemoveAt
        Throw New NotSupportedException()
    End Sub
    Private Sub CopyTo(ByVal array As Array, ByVal start As Integer) _
            Implements ICollection.CopyTo
        Throw New NotSupportedException()
    End Sub
#End Region
End Class

IList のほとんどのメンバは実装していませんので、あまり見るべきところはありませんね。なお、DataGridView のインデクサ/デフォルトプロパティの引数は column, row の順に並びますが、二次元配列では一般に row, column の順だと思うので今回のクラス群はそういう前提で記述しています。

この実装では GetItemProperties で PropertyDescriptorCollection を作成していますが、コンストラクタで作ってしまってフィールドに置いておいたほうがいいかもしれません。一度作ったら変わらない物ですし。それから、rowIndex 及び columnIndex を引数に取るインデクサ/デフォルトプロパティを用意すればもうちょっと便利になるかもしれません。

使い方は別に難しいものではありませんので省略します。

一つ既知の問題があります。一部の型で型コンバータがうまく動きません。例えば System.Windows.Input.KeyGesture を使った場合、KeyGesture から String への変換でこけてしまいます。でも System.Drawing.Rectangle なら問題ないんですよね。参照型だからかと思ったけど自前で型コンバータを持つ参照型を作って試したら問題なかったりと、詳細はよく分かりません。このときは、DataGridView の CellFormatting イベントで適切に型変換してやると動くようです。あるいはカスタム DataGridViewColumn 及び カスタム DataGridViewCell を作るか。

そういえば以前のエントリで DataGridViewComboBoxColumn を扱ったときもこの辺で苦労したような記憶が。

posted by Hongliang at 09:50| Comment(3) | TrackBack(0) | C# | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
二次元配列の英語は一般的に two-dimension-array じゃなくて two-dimensional-array みたいですよ?
Posted by Hongliang at 2008年02月25日 17:47
通販 ワンピース
Posted by クリスタンルブタン at 2013年07月15日 13:07
時計 omega オメガ ω http://www.advanceshopbossjp.biz/
Posted by オメガ ω at 2013年08月11日 06:38
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。
この記事へのトラックバックURL
http://blog.seesaa.jp/tb/85853119

この記事へのトラックバック

ここ(hongliang.seesaa.net)で公開しているものについて、利用は自由に行って頂いて構いません。改変、再頒布もお好きになさって下さい。利用に対しこちらが何かを要求することはありません。

ただし、公開するものを使用、または参考したことによって何らかの損害等が生じた場合でも、私はいかなる責任も負いません。

あ、こんなのに使ったってコメントを頂ければ嬉しいです。

×

この広告は1年以上新しい記事の投稿がないブログに表示されております。