カスタム データ バインディング、パート 2

 

Michael Weinhardt
www.mikedub.net

2005 年 2 月 6 日

概要:パート 2 では、BindingSource が単一の型をリスト データ ソースに変換できるようにするために BindingList<T> が果たす役割を簡単に見て、カスタム データ バインディング体験を続けます。 また、並べ替えや検索など、 BindingSource に依存するだけでは不十分な状況についても説明します。 このような機能では、さらに一歩進み、追加のサポートを使用して独自の BindingList<T> 実装を作成する必要があります。これについても確認します。 さらに、ゲーム データのシリアル化が追加され、生活が簡単になります。 (14ページ印刷)

メモこの記事では、.NET Framework 2.0 と Visual Studio .NET の両方の 2004 年 12 月の CTP バージョンを対象とします。

winforms02182005_sample.msi ファイルをダウンロードします

私たちはどこにいたのですか?

図 1 に示すように、パート 1 で作成した中国語発音ゲーム データ アプリケーションを思い出してください。その目的は、フラッシュカードスタイルの学習ゲームで使用するデータをキャプチャすることです。

図 1. 中国語発音ゲームデータ入力アプリケーション

任意のゲーム データをキャプチャするためのカスタム Word 型の作成から始めました。

public class Word {
  public Image Character { get; set; }
  public string English { get; set; }
  public string Pinyin { get; set; }
  public byte[] Pronunciation { get; set; }
}

Windows フォーム 2.0 のデータ デザイナーのおかげで、Word型をリスト データ ソースに変換し、DataGridView コントロールにバインドし、1 行のデータ バインディング コードを記述せずに VCR スタイルのナビゲーション サポートを追加することができました。

メモ振り返ると、Visual Studio .NET December CTPクラス Designer機能を使用して、Word型を視覚的に作成し、コーディングを完全に回避できました。

BindingSource は、任意のカスタム型に編集可能なリスト データ ソースを提供するために、System.ComponentModel.Collections 名前空間にある BindingList<T> と呼ばれる小さいが非常に便利なテクノロジに依存しています。

BindingList<T>

Bindinglist<T> は、(System.ComponentModel 名前空間からの) IBindingList インターフェイスの一般的な実装であり、編集をサポートするためにリスト データ ソースのデータ バインディング インフラストラクチャに必要な最小インターフェイスです。 IList は、バインド可能なリスト データ ソースを実装するために必要な最小インターフェイスですが、編集機能は提供されません。これは、ListBox などの編集できないコントロールにバインドされている場合に問題ありません。 ただし、 DataGridView などの完全な編集サポートを持つコントロールにバインドする場合は、リスト データ ソースで編集する機能と、並べ替え、検索、インデックス作成、変更通知などの機能のサポートが必要です。 IBindingListIList から派生し、 を拡張して、このすべてのサポートを提供します。

public interface IBindingList : IList, ICollection, IEnumerable {

  // Editing members
  bool AllowNew { get; } // Is adding new list data items supported?
  bool AllowEdit { get; } // Is editing list data items supports?
  bool AllowRemove { get; } // Is list data item removal supported?
  object AddNew(); // Adds and returns a new list data
  
  // Sorting members
  bool SupportsSorting { get; } // Supported?
  bool IsSorted { get; } // Is the list data source sorted?
  PropertyDescriptor SortProperty { get; } // Current sort column
  ListSortDirection SortDirection { get; } // Current sort direction
  void ApplySort(
    PropertyDescriptor property, 
    ListSortDirection direction); // Sort list by column/direction
  void RemoveSort(); // Revert to an unsorted state

  // Searching members
  bool SupportsSearching { get; } Supported?
  int Find(
    PropertyDescriptor property, 
    object key); // Find a list data item whose specified property 
                 // matches the provided key value
  
  // Indexing members
  void AddIndex(
    PropertyDescriptor property); // Add index to desired column
  void RemoveIndex(
    PropertyDescriptor property); // Remove index from desired column

  // Change notification members
  bool SupportsChangeNotification { get; } // Supported?
  event ListChangedEventHandler ListChanged; // Broadcast list change
}

Bindinglist<T> は、新しい .NET 2.0 ジェネリックサポートを使用して IBindingList を実装し、任意のカスタム型が厳密に型指定された IBindingList リスト データ ソースになるようにします。 厳密な型指定は、特にバインド可能なプロパティのリスト データ ソースの検査に関しては、データ バインド インフラストラクチャの処理を簡略化するため、特に利点があります。

メモ ジェネリックの議論はこの記事の範囲外ですが、最初に、大きな紹介からさらに https://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnvs05/html/csharp_generics.asp 詳しく調べるようお勧めします。

BindingSource で使用されるコードはもう少し複雑ですが、次の例は、結果として得られる BindingList<T> 実装によって、Word型に対してこれを実現する方法を示しています。

BindingList<Word> words = new BindingList<Word>();

DataGridView にバインドすると、BindingList<T> 実装ではリスト変更通知がサポートされるため、バインドされたクライアントにリストの変更をブロードキャストするための既知のデータ バインディング メカニズムを使用するなど、適切なデータ バインディング インフラストラクチャ リスト データ ソースになります。 Bindinglist<T> は実際には、データバインディングインフラストラクチャとデータバインディングインフラストラクチャに統合するためにより多くのことを行いますが、議論は簡単ではありません。 ただし、 で https://msdn2.microsoft.com/library/sywd766d.aspx自由に独自の調査を開始してください。

BindingList<T>: 不足しているもの

DataGridView には、ユーザーが、 BindingList<T> リスト通知のサポートでサポートされている行の追加、更新、削除を行い、制御データ ソースとリスト データ ソースの間の同期性を提供する視覚的なメカニズムが用意されています。 Bindinglist<T>IBindingList を実装します。これは、実装によって提供されるサービス レベルをデータ バインディング インフラストラクチャに通知するインターフェイスです。 ただし、 IBindingList 実装 はインターフェイス 全体 を実装する義務を負いません。したがって、 BindingList<T> は並べ替え、検索、インデックス作成に関しては実装しません。 その理由の 1 つは、真に汎用的な並べ替えと検索アルゴリズムを実装することは非常に困難である可能性があります。特に、異なる型には、並べ替え可能または検索可能ではない可能性があるプロパティが異なる場合があるためです。 どちらの方法でも、これは重要な考慮事項であり、 BindingSource を使用してリスト データ ソースを自動的に作成および管理できるかどうか、または独自の並べ替え、検索、またはインデックス作成コードを記述する必要があるかどうか、または 3 つすべてを組み合わせて記述する必要があるかどうかを判断できます。 これらの実装は DataGridView 自体に組み込むことはできますが、このロジックをカスタム IBindingList 実装に組み込むことは、リスト データ ソースに格納されているカスタム型から派生した微妙な点と微妙な点が移植可能で再利用可能であることを意味します。

中国の発音ゲーム データ マネージャーのようなアプリケーションでは、数百行と数千行のデータが表示される可能性が高く、並べ替えと検索の利点は間違いありません。 この記事の残りの部分では、並べ替え可能な BindingList<T> 実装の構築に焦点を当てています。

並べ替え可能な BindingList T の<作成>

並べ替えを実装するには、次のような IBindingList とその並べ替えメンバーを必要に応じて実装する必要があります。

public interface IBindingList : IList, ICollection, IEnumerable {
  ...  
  // Sorting members
  bool SupportsSorting { get; }
  bool IsSorted { get; }
  PropertyDescriptor SortProperty { get; }
  ListSortDirection SortDirection { get; }
  void ApplySort(
    PropertyDescriptor property, 
    ListSortDirection direction);
  void RemoveSort();
  ...
}

BindingList<T> は並べ替えを提供しませんが、必要な基本的なリスト データ ソースのサポートを提供し、IBindingList の完全な実装を最初から構築するのではなく、開始するのに最適な場所です。 BindingList< から型を派生することもできますがWord>結果はWord型に固有です。 代わりに、追加の手順を実行し、一般的な並べ替え可能なバインド リストを作成できます。

public class SortableBindingList<T> : BindingList<T> {}

結果は、Wordを含む任意の型に並べ替えを提供するために使用できるクラスです。 BindingList<T> は並べ替えを実装していませんが、独自のフックを簡単に統合するために必要なフックを提供します。

コア メンバー

これらのフックは、オーバーライドする一連のプロパティとメソッドで構成されます。これらはすべて、実装する IBindingList メンバーの名前を受け取り、"Core" サフィックスを追加します。 コア並べ替えメンバーのセット全体を次に示します。

public class BindingList<T> : IBindingList, ... {
  // Core sort methods
  protected virtual void ApplySortCore(PropertyDescriptor property, ListSortDirection direction);
  protected virtual void RemoveSortCore();

  // Core sort properties
  protected virtual bool SupportsSortingCore { get; }
  protected virtual bool IsSortedCore { get; }
  protected virtual ListSortDirection SortDirectionCore { get; }
  protected virtual PropertyDescriptor SortPropertyCore { get; }
}

BindingList<T> 実装では、対応するコア メンバーを持つ各 IBindingList メンバーは、単にコア メンバーのラッパーです。 たとえば、 SupportsSorting プロパティは SupportsSortingCore メソッドを 呼び出して実際の作業を行います。

public class BindingList<T> : IBindingList, ... {
  public bool SupportsSorting {
    get { return this.SupportsSortingCore; }
  }
  protected virtual bool SupportsSortingCore {
    get { return false; }
  }
}

ご覧のように、BindingList<T>SupportsSortingCore プロパティは false を返し、並べ替えがサポートされていないことをデータ バインディングワールドに示します。 完全な並べ替えソリューションを実装するには、 IsSortedCoreSortPropertyCoreSortDirectionCoreApplySortCoreRemoveSortCoreSupportsSortingCore など、それぞれの適切な並べ替え方法をオーバーライドする必要があります。

並べ替えはサポートされていますか?

ここまで推測したように、カスタム リスト データ ソースでは、次のように、 SupportsSortingCore をオーバーライドして true を返すことで、並べ替えのサポートをブロードキャストする必要があります。

public class SortableBindingList<T> : BindingList<T> {}
  protected virtual bool SupportsSortingCore {
    get { return true; }
  }
}

あー。 ここまでは簡単でした。 もちろん、話をしたので、歩いて実際に並べ替えアルゴリズムを実装する必要があります。 アルゴリズムという言葉を見るたびに、私は通常フリークアウトし、代わりに私のお気に入りのゲームをロードします。 あなたが私のような場合は、これらの状況で行うことを何でもして、できるだけ早く報告してください。

並べ替えアルゴリズムの適用

並べ替えを実装するには、並べ替えの頭脳信頼が存続して実行される ApplySortCore をオーバーライドする必要があります。 具体的には、 ApplySortCore は単に並べ替えアルゴリズムを実行しますが、プロセスは DataGridView のようなバインドされたコントロールによって駆動されます。 DataGridView での並べ替えは、列ヘッダーがマウスの左ボタンをクリックしたときに開始され、3 段階のプロセスです。 最初のフェーズでは、必要な並べ替えメタデータ (並べ替えの列) と順序が計算されます。これは PropertyDescriptor 列挙値と ListSortDirection 列挙値にそれぞれ変換されます。 2 番目のフェーズでは、必要な PropertyDescriptor 引数と ListSortDirection 引数を渡して、データ ソースに IBindingList.ApplySort の呼び出しで並べ替えを指示する DataGridView が表示されます。 ApplySort が返されると、DataGridView は新しい並べ替え順序を反映するようにそれ自体を再描画します。 カスタム バインド リストで実行する必要がある唯一のジョブは、 ApplySort に渡された並べ替え引数を受け取り、並べ替え自体を行う必要があります。

Bindinglist<T> は、実際には コレクション<T> から派生し、1 つ以上のリスト データ項目を管理するメカニズムを提供します。 コレクション<T> は、並べ替えを実行する必要があるリスト<T> への参照を返す Items プロパティを公開します。 便利なことに、 List<T> は、この目的のために Sort メソッドを公開します。 このメソッドを使用すると、 IComparer 実装を利用して、リスト データ項目ごとに必要な並べ替え比較を実行します。 IComparer を実装する型は、2 つの値を取得する方法を認識し、最初の値が大きい (1 を返す)、最初の値が小さい (-1 を返す)、または両方の値が同じかどうか (0 を返す) かどうかを指定する整数値を返します。 Sort にはいくつかのオーバーロードがあり、それぞれが使用する IComparer を渡す別の方法を提供します。

public class List<T> : IList<T>, ... {
  ...
  // Use system-provided IComparer
  public void Sort();
  // Use custom IComparer<T> object
  public void Sort(IComparer<T> comparer);
  // Creates an internal IComparer shim that calls the Comparison<T> 
  // delegate to perform the sort
  public void Sort(Comparison<T> comparison);
  // Use custom IComparer<T> object to sort a portion of the list
  public void Sort(int index, int count, IComparer<T> comparer);
  ...
}

この記事では、IComparer<T から派生したカスタム PropertyComparer T> クラスを実装します。><

public class PropertyComparer<T> :

System.Collections.Generic.IComparer<T> {

// Constructor

  public PropertyComparer(
    PropertyDescriptor property, ListSortDirection direction) {...}
  // IComparer<T> interface

public int Compare(T xValue, T yValue) {...}

  public bool Equals(T xValue, T yValue) {...}
  public int GetHashCode(T obj) {...}
  ...
}

PropertyComparer は、Rockford Lhotka によって構築された比較アルゴリズムに基づいて構築され、任意の型のジェネリック プロパティ比較子に変換されます。

メモ 比較子の詳細な分析はこの記事の範囲を超えていますが、簡単な議論で十分ですが、ロッキーの素晴らしい記事を読んでコード サンプルを調べるようお勧めします。

PropertyComparer のコンストラクターはPropertyDescriptorListSortDirection 列挙値の 2 つの引数を受け取ります。 これらの引数は比較中に使用されます。つまり、 Compare メソッドが呼び出されたときです。 Compare 自体は 2 つの型インスタンスを受け入れ、両方のインスタンスでコンストラクターの property 引数で指定されたプロパティの値を取得し、それらを比較し、結果を返します。

すべてをまとめると、ApplySortPropertyComparer インスタンスが作成され、コンストラクターに適切な引数が渡され、PropertyComparer への参照が List<T> に渡されます。並べ替え: PropertyComparer のCompare メソッドを、指定したプロパティで目的の並べ替え順序でリストを並べ替えるために必要な回数だけ呼び出します。

public class SortableBindingList<T> : BindingList<T> {
  ...
  protected override void ApplySortCore(
    PropertyDescriptor property, ListSortDirection direction) {
    // Get list to sort
    List<T> items = this.Items as List<T>;

    // Apply and set the sort, if items to sort
    if( items != null ) {
      PropertyComparer<T> pc = 
        new PropertyComparer<T>(property, direction);
      items.Sort(pc);
    }

    // Let bound controls know they should refresh their views
    this.OnListChanged(
      new ListChangedEventArgs(ListChangedType.Reset, -1));
  }
}

ListChange のブロードキャスト

並べ替え後は、バインドされているすべてのコントロールにリスト データ ソースの順序が変更されたことを通知し、新しい順序を視覚的に反映させる必要があります。 上記のコード サンプルに示すように、バインドされたコントロールに通知するには、BindingList<T> から継承された OnListChanged メソッドを呼び出し、適切な ListChangedType 列挙値を渡す必要があります。

this.OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));

Reset の値は、リスト全体が変更されている可能性が高く、並べ替え操作の後に意味があることを意味します。 ListChangedType (System.ComponentMode 名前空間から) には、次に示す他のいくつかの値があります。

public enum ListChangedType {
  ItemAdded = 1, // A list item was added
  ItemChanged = 4, // A list item was changed
  ItemDeleted = 2, // A list item was deleted
  ItemMoved = 3, // A list item moved to a new list position
  PropertyDescriptorAdded = 5, // List schema change (new property)
  PropertyDescriptorChanged = 7, // List schema change (property changed)
  PropertyDescriptorDeleted = 6, // List schema change (property deleted)
  Reset = 0 // List-wide change
}

あなたが気づいたかもしれないことの1つは、コレクション<TやリストTを含む並べ替えに関する BindingList><T>>の内部がジェネリックである方法です。つまり、厳密な型指定は並べ替えインフラストラクチャ全体に浸透し、パフォーマンスの観点からは良い<ことです。

現在並べ替えられていますか?

並べ替えると、onus は IBindingList 実装上にあり、現在の並べ替えを報告します。 IsSortedCore は、これをオーバーライドするためにオーバーライドする必要があるプロパティです。これにより、データ バインディング インフラストラクチャは、実装の並べ替えられた状態を必要に応じて検査できます。 これは、プライベート状態変数を使用して簡単に実現できます。

public class SortableBindingList<T> : BindingList<T> {
  private bool _isSorted;

  protected override void ApplySortCore(
    PropertyDescriptor property, ListSortDirection direction) {

    // Get list to sort
    List<T> items = this.Items as List<T>;

    // Apply and set the sort, if items to sort
    if( items != null ) {
      PropertyComparer<T> pc = new PropertyComparer<T>(property, direction);
      items.Sort(pc);
      isSorted = true;
    }
    else {
      isSorted = false;
    }

    // Let bound controls know they should refresh their views
    this.OnListChanged(
      new ListChangedEventArgs(ListChangedType.Reset, -1));
  }

  protected override bool IsSortedCore {
    get { return _isSorted; }
  }
}

実装が完了したら、サンプルに示すように、 ApplySortCore を更新して、並べ替え時に実装の並べ替えられた状態を true に変更する必要があります。

並べ替えの削除

同様に、並べ替えが削除された場合、並べ替えの状態は最小限で false に設定する必要があります。 このロジックは、 次のように RemoveSortCore メソッドに実装する必要があります。

public class SortableBindingList<T> : BindingList<T> {
  private bool _isSorted;
  ...
  protected override void RemoveSortCore() {
    _isSorted = false;
  }
  ...
}

RemoveSort の実際の使用には、ADO.NET で使用される DataView モデルや DataTable モデルと同様に、基になるリスト データ ソースに対して直接並べ替える代わりに、特殊な並べ替えられたビューを使用する必要があります。 ここでも、詳細については、ロッキーの記事をお読みください。

必要なコア並べ替えメソッドをオーバーライドして実装したので、 DataGridView にバインドする必要がある並べ替え対応のカスタム リスト データ ソースが用意されました。

SortableBindingList<T の使用>

現在、このフォームは、BindingSource コンポーネントにバインドされた DataGridView として、それ自体がWord型にバインドされています。 前の記事の作業により、 DataGridView は、バインドされた BindingSource のおかげで、実行時に適切な列を表示するようにも構成されています。 必要なのは、SortableBindingList<T>BindingSource のデータ ソースとして、Word型に置き換える方法です。

partial class MainForm : Form {
  private void MainForm_Load(object sender, EventArgs e) {
    ...
    SortableBindingList<Word> source = new SortableBindingList<Word>();
    this.wordBindingSource.DataSource = source;
  }
  ...
}

アプリケーションが実行されると、 DataGridView が並べ替え可能になりました (図 3 を参照)。

図 2. 並べ替え可能な DataGridView

DataGridView では、並べ替え結果に、三角形の並べ替えシェブロンなど、予期される追加の UI アクセス権が表示されます。

永続性を得るために支払う

並べ替えは便利ですが、データ管理アプリケーションで実際にデータを読み込んで保存できる方が便利です。 永続化はパート 1 で中断した機能ですが、プロジェクトの bin\debug フォルダーに用意されているファイル gamedata.dat を利用する基本的な Save/Load 実装として組み込まれました。

メモ Chris Sells の ドキュメント中心の管理シリーズ を読み、ドキュメント中心のアプリケーションが表示する必要がある動作の完全な補完について詳しく説明します。

.NET Framework 2.0BindingList<T> の両方に関する興味深いビットは、データをシリアル化および逆シリアル化する実際の動作Word簡単です。 BindingList<T> はシリアル化可能ですが、データ項目の保存と読み込みに関心があります。これは、BindingList<T> の等しくシリアル化可能な Items プロパティを通じて見つかります。 そのため、データ項目のリスト全体を一度に保存して読み込むことができます。 これを移植可能にするために、SortableBindingListLoad メソッドと Save メソッドを実装しました。

public class SortableBindingList<T> : BindingList<T> {
  // NOTE: BindingList<T> is not serializable but List<T> is

  public void Save(string filename) {
    BinaryFormatter formatter = new BinaryFormatter();
    using( FileStream stream = 
      new FileStream(filename, FileMode.Create) ) {
      // Serialize data list items
      formatter.Serialize(stream, (List<T>)this.Items);
    }
  }

  public void Load(string filename) {
    this.ClearItems();
    if( File.Exists(filename) ) {
      BinaryFormatter formatter = new BinaryFormatter();
      using( FileStream stream = 
        new FileStream(filename, FileMode.Open) ) {
        // Deserialize data list items
        ((List<T>)this.Items).AddRange(
          (IEnumerable<T>)formatter.Deserialize(stream));
      }
    }

    // Let bound controls know they should refresh their views
    this.OnListChanged(
      new ListChangedEventArgs(ListChangedType.Reset, -1));
  }
}

シリアル化は簡単ですが、逆シリアル化 (読み込み) には少し追加が必要です。 ショートカットとして、SortableBindingListList<T> インスタンスを読み込む最も簡単な方法は、AddRange メソッドを呼び出す方法ですが、BinaryFormatter逆シリアル化メソッドによって返される IEnumerable<T> 参照が必要でした。 ここでも、 OnListChanged が呼び出され、クリアおよび再読み込みされたリスト データ ソースに応答して完全なリスト リセットが通知されます。

シリアル化ロジックは、シリアル化できる型でのみ機能します。一般に、このような型は SerializableAttribute で装飾されるか、 ISerializable を実装します。 前者は、プロパティ値のみをシリアル化する必要があるが、後者ではより複雑なシリアル化の実装が可能な場合に便利です。 Wordではプロパティ値のみをシリアル化する必要があるため、SerializableAttribute で更新されました。

[Serializable]
public class Word { ... }

一方、SortableBindingList< Word インスタンスは、>LoadSave の両方を呼び出すために参照する必要があります。

partial class MainForm : Form {
  ...
  private void MainForm_Load(object sender, EventArgs e) {
    ...
    // Open game data file
    SortableBindingList<Word> source = new SortableBindingList<Word>();
    source.Load(_filename);
    this.wordBindingSource.DataSource = source;
  }

  private void MainForm_FormClosing(
    object sender, FormClosingEventArgs e) {
    SortableBindingList<Word> list = 
      this.wordBindingSource.List as SortableBindingList<Word>;
    if( list != null ) {
      list.Save(_filename);
    }
  }
  ...
}

これで、Word ベースのリスト データ ソースの読み込みと保存が行われ、DataGridView が自動的に更新されます。

ここはどこですか。

BindingSource がジェネリック BindingList<T> 型に依存して、任意の型を IBindingList 実装に変換する方法について説明しました。 ただし、この実装では、並べ替え、検索、インデックス作成は実装されていません。 ただし、オーバーライドするコア メンバーのセットを通じて、これを行うために必要なフックが提供されます。 基本的な永続化を終える前に、並べ替えのためにこれを行いました。

私も検索を実装することを意図していましたが、今月は部屋を使い果たしました。 しかし、次回のラウンドでは、基本的な検索実装を構築する方法だけでなく、UI に簡単にスロットし、検索プロセス全体を簡略化できるカスタム の Find strip コントロールも構築します。

謝辞

スティーブ・ラスカーとジョー・ステグマンが解説を続けてくれてありがとう。 特に、ジョーは私にスティッキーベータの問題を手伝ってくれて、責任を資産に変えました。SortableBindingList<T> も PropertyComparer<T> も、彼がいなければ T> でなかった<でしょう。 同様に、彼らはあなたにほど役に立たなかったでしょう。

リファレンス

現在、Michael Weinhardt は、Chris Sells と共に共同編集Windows フォームプログラミング 、2nd Edition (Addison Wesley) を Chris Sells と共に作成し、この列を記述するなど、さまざまな .NET 書き込みコミットメントにフルタイムで取り組んでいます。 Michael は一般的に .NET を愛し、特にWindows フォーム。 彼はまた、多くの場合、80年代の音楽の品質を過度に評価していると非難されています。 詳細については 、「www.mikedub.net 」を参照してください。