前回の続き。今回はソート機能の実装です。あ、その前に言っておくと、この TwoDimensionalArrayView<T> クラスはできる限り DataView に近づけたクラス構成にするつもりです。
さて、ソートの実装自体はそんなに難しいことではないでしょう。まず前回の雛形からソート関連の部分をピックアップします。
private List<TwoDimensionalArrayRowView<T>> arrangedViews;
bool IBindingList.SupportsSorting { get { return false; } }
bool IBindingList.IsSorted {
get { throw new NotSupportedException(); }
}
ListSortDirection IBindingList.SortDirection {
get { throw new NotSupportedException(); }
}
PropertyDescriptor IBindingList.SortProperty {
get { throw new NotSupportedException(); }
}
void IBindingList.ApplySort(PropertyDescriptor property,
ListSortDirection direction) {
throw new NotSupportedException();
}
void IBindingList.RemoveSort() {
throw new NotSupportedException();
}
次に DataView のソート関連のメンバをピックアップ。
public bool ApplyDefaultSort { get; set; }
public string Sort { get; set; }
このうち ApplyDefaultSort は、「PrimaryKey を持っている DataTable の Sort が空のとき」に「PrimaryKey を使ってソートする」かどうか、というプロパティなので二次元配列には関係なし、除外。
取り敢えず SupportSorting は true を返させるだけですね。そのほかのメンバの挙動については DataView をちょっと触って参考にしましょう。このとき、DataView は IBindingListView をも実装しており、複数の列を使ったソートをサポートしている点に注意が必要です。
ではまず適当に DataView を作って色々試してみます。
DataTable table = new DataTable("sample");
DataColumn prim = table.Columns.Add("id", typeof(int));
table.Columns.Add("name");
table.PrimaryKey = new DataColumn[]{prim};
for (int i = 0; i < 10; i++) {
table.Rows.Add(new object[]{ i, (i * 11).ToString() });
}
DataView view = new DataView(table);
// IBindingList の明示的実装メンバを確認するため
IBindingList list = view;
// 普通にソート
view.Sort = "id";
Console.WriteLine("{0} {1} {2} {3}", view.Sort,
list.IsSorted, list.SortDirection, list.SortProperty);
// 降順でソート
view.Sort = "id DESC";
Console.WriteLine("{0} {1} {2} {3}", view.Sort,
list.IsSorted, list.SortDirection, list.SortProperty);
// 複数列でソート
view.Sort = "id, name";
Console.WriteLine("{0} {1} {2} {3}", view.Sort,
list.IsSorted, list.SortDirection, list.SortProperty);
// 複数列でソート、両方降順
view.Sort = "id DESC, name DESC";
Console.WriteLine("{0} {1} {2} {3}", view.Sort,
list.IsSorted, list.SortDirection, list.SortProperty);
view.Sort = "id";
// id 列を指す PropertyDescriptor を確保
PropertyDescriptor idDesc = list.SortProperty;
// 単一列のソートに対して RemoveSort してみる
list.RemoveSort();
Console.WriteLine("{0} {1} {2} {3}", view.Sort,
list.IsSorted, list.SortDirection, list.SortProperty);
// 単一列のソートに対して RemoveSort してみる
view.Sort = "id DESC, name ASC";
list.RemoveSort();
Console.WriteLine("{0} {1} {2} {3}", view.Sort,
list.IsSorted, list.SortDirection, list.SortProperty);
// ApplySort を試してみる
list.ApplySort(idDesc, ListSortDirection.Ascending);
Console.WriteLine("{0} {1} {2} {3}", view.Sort,
list.IsSorted, list.SortDirection, list.SortProperty);
// Sort を空にしてみる
view.Sort = null;
Console.WriteLine("{0} {1} {2} {3}", view.Sort,
list.IsSorted, list.SortDirection, list.SortProperty);
結果を検証していきましょう。
単純ソートについては特に問題になることは無いですね。降順ソートも SortDirection が Descending になるだけ。SortProperty は DataColumnPropertyDescriptor なるオブジェクトが返ってきますが、id 列を説明する PropertyDescriptor なんでしょう。
複数列をソートした場合、結果はかなり癖のあるものになっています。IsSorted は true を返しますが、SortDirection は常に Ascending、SortProperty は null を返しています。
RemoveSort はまあ特に気にする結果ではありませんでした。強いて言うなら、ソートされていないときは SortDirection は Ascending を返すこと、それから DataView.Sort プロパティは空の場合は必ず空文字列を返し null を返すことはないってことぐらいでしょうか。Sort プロパティに null を設定しても、取得時には空文字列になります。
ApplySort を行った場合、Sort プロパティで列名が角括弧で囲まれるのがちょっと目に付きますが、これは表現が違うだけなのでどうでもいいですね。ちなみにやる人はいないと思いますが、id という名前のプロパティを持ったクラスを作り、その id の PropertyDescriptor を使って ApplySort しても問題なくソートされます。id の型も問いません。PropertyDescriptor のうち Name プロパティしか参照していない可能性が高いですね。
Sort を空にすると、RemoveSort の呼び出しと同じ結果が返ってきています。
ではこれらの結果に沿うように実装してきます。
まず、ApplySort を実装しましょう。ApplySort は PropertyDescriptor と ListSortDirection の二つを引数に取ります。TwoDimensionalArrayView クラスでは列の記述子にネストクラス TwoDimensionalArrayColumnDescriptor を使用することにしています(前回の雛形を参照)。このクラスは ColumnIndex プロパティを持っており、これで列を特定します。それ以外の PropertyDescriptor が渡されたときは例外を投げておきます。また RemoveSort も同時に実装します。
private ListSortDirection sortDirection;
private TwoDimensionalArrayColumnDescriptor sortColumn;
bool IBindingList.SupportsSorting { get { return true; } }
// ソート済みかどうかは sortColumn が設定済みかどうかで判断する
bool IBindingList.IsSorted {
get { return sortColumn != null; }
}
ListSortDirection IBindingList.SortDirection {
get { return this.sortDirection; }
}
PropertyDescriptor IBindingList.SortProperty {
get { return this.sortColumn; }
}
void IBindingList.ApplySort(PropertyDescriptor property,
ListSortDirection direction) {
if (property == null) throw new ArgumentNullException("property");
TwoDimensionalArrayColumnDescriptor desc
= property as TwoDimensionalArrayColumnDescriptor;
if (desc == null) throw new ArgumentException("property");
if (!Enum.IsDefined(typeof(ListSortDirection), direction)) {
throw new InvalidEnumArgumentException("direction", (int)direction,
typeof(ListSortDirection));
}
ListComparer comparer = new ListComparer(desc.ColumnIndex, direction);
this.arrangedViews.Sort(comparer);
this.sortDirection = direction;
this.sortColumn = desc;
}
private class ListComparer : IComparer<TwoDimensionalArrayRowView<T>> {
private int column;
private bool isAscending;
public ListComparer(int columnIndex, ListSortDirection direction) {
this.column = columnIndex;
this.isAscending = direction == ListSortDirection.Ascending;
}
public int Compare(TwoDimensionalArrayRowView<T> x,
TwoDimensionalArrayRowView<T> y) {
int result = Comparer<T>.Default.Compare(x[column], y[column]);
return this.isAscending ? result : -result;
}
}
void IBindingList.RemoveSort() {
this.arrangedViews.Clear();
this.arrangedViews.AddRange(this.rowViews);
this.sortDirection = ListSortDirection.Ascending;
this.sortColumn = null;
}
ソートには List<T>.Sort を、その比較には Comparer<T>.Default.Compare を使用することにします。
あとは Sort プロパティですね。文法は DataView.Sort と同様。ただし複数列ソートを考慮しないのでコンマによる列記は不許可とします。また二次元配列に列名は存在しないため、列番号で代用することにします。DataView.Sort は基本的に設定したのがそのまま取得されますが、TwoDimensionalArrayView では簡便のため get 要求ごとに組み立てることにします。そのため、設定時に並べ替え順序を指定しなかった場合でも、取得時には列番号の後ろに " ASC" が付属することになります。
public string Sort {
get {
if (this.sortColumn == null) return "";
string dir = this.sortDirection == ListSortDirection.Ascending ? "ASC" : "DESC";
return String.Format("{0} {1}", this.sortColumn.ColumnIndex, dir);
}
set {
if (String.IsNullOrEmpty(value)) {
((IBindingList)this).RemoveSort();
}
else {
ListSortDirection dir = ListSortDirection.Ascending;
// 並びつきの場合は末尾文字を削除
if (value.Length > 5 && value.EndsWith(" DESC")) {
dir = ListSortDirection.Descending;
value = value.Substring(0, value.Length - 5);
}
else if (value.Length > 4 && value.EndsWith(" ASC")) {
value = value.Substring(0, value.Length - 4);
}
int columnIndex;
if (!Int32.TryParse(value, out columnIndex))
throw new ArgumentException("value");
if (columnIndex >= this.columnDescriptors.Length)
throw new ArgumentException("value");
((IBindingList)this).ApplySort(this.columnDescriptors[columnIndex], dir);
}
}
}
"3" とか "2 DESC" みたいに指定できます。
以上でソートの実装は完了です。でもこの TwoDimensionalArrayView を DataSource にして DataGridView を表示した場合、一部正常に動きません。具体的にはコードから Sort プロパティを操作した場合で、このとき DataGridView 上は並び替えが発生しません。次回はこの問題についてです。