四回目。前回は Sort プロパティを設定したときに DataGridView などに通知する機構を追加しました。今回は要素が変更されたときの通知です。なお、面倒なので以降の記事では TwoDimensionalArrayView を ArrayView と、TwoDimensionalArrayRowView を ArrayRowView と書くことにします。
話しに入る前に、前回の終わりでちらっと触れた AllowEdit を書き換えておきましょう。これで DataGridView で表示しているときに値を編集できるようになります。まあ後からコードで AllowEdit を操作することもできるんですが、一般的には編集可能がデフォルトでしょうし。
// デフォルト値を変更
private bool allowEdit = true;
ListChangedEventArgs で利用される ListChangedType 列挙体には、リスト全体が変更となったことを表す Reset の他に、ItemAdded や ItemDeleted、ItemMoved などが存在しています。固定長である二次元配列には一見縁がなさそうですが、ListChanged イベントの対象となるリストは飽くまで ArrayView、つまり ArrayRowView の並び方が問題なのです。値を変更した結果 ArrayRowView の順序が変更されれば ItemMoved の ListChanged イベントを起こすさせる必要があります。また今回取り扱う予定はありませんが Filter(RowFilter)が適用されているとき(Filter を適用したタイミングでは Reset です)、そのフィルタから外れるように値が変更された場合は ItemDeleted の ListChanged を起こすことになります。
さて、これを導入するに当たって今までの構成では難しい点が出てきます。
まず、値をどこで書き換えるか。今は ArrayView が管理する array を internal に設定し、ArrayRowView のインデクサで array に対し書き換えを行っています。値を持っている人が書き換えない形は少々違和感があります。ListChanged にしても、ArrayRowView から ArrayView に通知する必要があって不自然です。これをどうにか ArrayView の方に持ってきましょう。
// TwoDimensionalArrayView 内
private T[,] array;
public T this[int rowIndex, int columnIndex] {
get {
int orig = this.arrangedViews[rowIndex].OriginalRowIndex;
return this.array[orig, columnIndex];
}
set {
int orig = this.arrangedViews[rowIndex].OriginalRowIndex;
this.array[orig, columnIndex] = value;
// このメソッドは現時点で未定義
this.OnValueChanged();
}
}
こうすれば ArrayView は問題なさそうです。次に ArrayRowView です。
// TwoDimensionalArrayRowView 内
public T this[int columnIndex] {
get {
return this.owner[this.OriginalRowIndex, columnIndex];
}
set {
this.owner[this.OriginalRowIndex, columnIndex] = value;
}
}
ここで手が止まってしまいました。ArrayView のインデクサの rowIndex はソート済みリストの行番号。OriginalRowIndex を渡してしまってはいけません。さりとて ArrayRowView はソート済みリストにおける行番号を持っていません。困りましたね。
これは取り敢えず置いておいて、OnValueChanged メソッドの方を実装することにします。OnValueChanged でやることは、必要なときにソートし直すことと、変更に応じた ListChanged イベントを発生させることです。ソートだけでは増えたり減ったりはしないので、あり得る ListChangedType は ItemMoved か ItemChanged のみになります。
// イベントのではないので protected にはしていない
private void OnValueChanged(int originalRow, int column) {
ListChangedEventArgs e = null;
// 取り敢えず匿名メソッドを使用する
Predicate<TwoDimensionalArrayRowView<T>> finder
= delegate(TwoDimensionalArrayRowView<T> x) {
return x.OriginalRowIndex == originalRow;
};
int oldRowView = this.arrangedViews.Find(finder).OriginalRowInex;
// ソートの必要を確認
if (this.sortColumn != null
&& this.sortColumn.ColumnIndex == columnIndex) {
((IBindingList)this).ApplySort(this.sortColumn, this.sortDirection);
int newRowView = this.arrangedViews.Find(finder).OriginalRowInex;
if (oldRowView != newRowView) {
e = new ListChangedEventArgs(ListChangedType.ItemMoved,
newRowView, oldRowView);
}
}
if (e == null) {
e = new ListChangedEventArgs(ListChangedType.ItemChanged, oldRowView);
}
this.OnListChanged(e);
}
オリジナル二次元配列の行インデックスから arrangedViews 内の ArrayRowView のインデックスを取得するのに匿名メソッドを使って Find を呼び出していますが、これは少々面倒です。二次元配列行インデックスではなく ArrayRowView インスタンスそのものなら IndexOf で単純化できます。Find にせよ IndexOf にせよ、ループで検索するわけでコストが多少不安ですが、まあ要素数がそこまで増えることは少ないでしょうから気にしないでおきましょう。Dictionary<TwoDimensionalArrayRowView<T>, int> で ArrayRowView から ArrayRowView のインデックスを用意する手もありますが、メモリ消費とのトレードオフです。また、ApplySort を使っていますが、このメソッドを直接呼び出すと ListChangedType.Reset の ListChanged イベントが発生したりと色々問題がありますので、ApplySort なども分割します。
// イベントのではないので protected にはしていない
private void OnValueChanged(TwoDimensionalArrayRowView<T> row, int column) {
ListChangedEventArgs e = null;
int oldRowView = this.arrangedViews.IndexOf(row);
if (oldRowView == -1) {
return; // ありえないはず
}
// ソートの必要を確認
if (this.sortColumn != null && this.sortColumn.ColumnIndex == column) {
this.SortRows();
// arrangedViews 内での位置が変わったときのみ ItemMoved を発生
if (row != this.arrangedViews[oldRowView]) {
int newRowView = this.arrangedViews.IndexOf(row);
e = new ListChangedEventArgs(ListChangedType.ItemMoved,
newRowView, oldRowView);
}
}
if (e == null) {
e = new ListChangedEventArgs(ListChangedType.ItemChanged, oldRowView);
}
this.OnListChanged(e);
}
private void SortRows() {
if (this.sortColumn == null) {
this.arrangedViews.Clear();
this.arrangedViews.AddRange(this.rowViews);
}
else {
ListComparer comparer = new ListComparer(this.sortColumn.ColumnIndex,
this.sortDirection);
this.arrangedViews.Sort(comparer);
}
}
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));
}
this.sortDirection = direction;
this.sortColumn = desc;
this.SortRows();
this.OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
}
void IBindingList.RemoveSort() {
this.sortDirection = ListSortDirection.Ascending;
this.sortColumn = null;
this.SortRows();
this.OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
}
OnValueChanged の引数が決まったので、先ほどは棚に上げておいたインデクサの方を対処しましょう。OnValueChanged には ArrayRowView を渡す必要があることを考えると、ArrayView のインデクサに ArrayRowView(と列番号)を取るオーバーロードを用意すれば解決できますね。ArrayRowView のインデクサは自分自身と列番号を owner のインデクサに渡せばいい。このオーバーロードなら public でも違和感がありません。
// TwoDimensionalArrayView 内
public T this[int rowIndex, int columnIndex] {
get {
return this[this.arrangedViews[rowIndex], columnIndex];
}
set {
this[this.arrangedViews[rowIndex], columnIndex] = value;
}
}
public T this[TwoDimensionalArrayRowView<T> row, int columnIndex] {
get {
return this.array[row.OriginalRowIndex, columnIndex];
}
set {
this.array[row.OriginalRowIndex, columnIndex] = value;
this.OnValueChanged(row, columnIndex);
}
}
// TwoDimensionalArrayRowView 内
public T this[int columnIndex] {
get {
return this.owner[this, columnIndex];
}
set {
this.owner[this, columnIndex] = value;
}
}
値の変更は最終的に ArrayView の [ArrayRowView, int] インデクサに集約されるようになり、ここで値変更時の処理を捌くことになります。
ソート系はこんなところでしょうか。次は検索系の実装です。