ObservableでSortedListなCollectionをつくってみた

2018年11月14日

やること

C#でWindowsアプリを開発するときによく用いるObservableCollectionで、SortedListみたいに常にソートされた状態でBindingに使いたい!ということがあったのでつくってみました。

ソース全体はこっち

SortedなObservableCollectionをつくる

実装する量が一番少なそうな方法を選びたかったので、ObservableCollection<T>を継承する方法を選択しました。
以下の手順で実装していきます。

  1. ObservableCollection<T>を継承するクラスの土台を作成
  2. ObservableCollection<T>のコンストラクタをコピペ
  3. 便利関数 FirstIndexOf(T), LastIndexOf(T) を実装 (4.で使うため)
  4. InsertItem(), MoveItem()をオーバーライド

ObservableCollection<T>は、AddやRemoveなどの操作をオーバーライドして好みのCollectionをつくることができます。便利ヾ(๑╹◡╹)ノ"

1. ObservableCollection\<T>を継承するクラスの作成

クラスObservableSortedCollection<T>をつくっていきます。
ソートできるようにするのが目的なので、TIComparable<T>を実装している前提にすることで、Collectionの実装を楽にできるようにします。

もし比較方法をいろいろ変えたい場合は、SortedListのようにComparerをプロパティとして持つように実装してもよいかもしれません。

今回は手軽さ重視。

using System.Collections.ObjectModel;

public class ObservableSortedCollection<T> :
    ObservableCollection<T> 
    where T: IComparable<T>
{
    // ...
}

2. ObservableSortedCollection\<T>のコンストラクタの実装

ObservableCollection<T>は2つのコンストラクタを持っています。

public ObservableCollection();
public ObservableCollection(IEnumerable<T> collection);

さっそく実装していきますが、2番目のコンストラクタの場合はソートによって順番が入れ替わる可能性があるため一つずつAddしていきます。

public class ObservableSortedCollection<T> : ...
{
    // 1つ目はそのまま
    public ObservableSortedList() : base() { }

    // 2つ目はひとつずつ追加する
    public ObservableSortedList(IEnumerable<T> collection) : base()
    {
        foreach(var item in collection)
        {
            this.Add(item);
        }
    }
}

3. 便利関数 FirstIndexOf(T), LastIndexOf(T) の実装

あとあと、AddやMoveを実装する上でも必要となるIndexを取得する関数をつくっておきます。比較したときに同じ順位のオブジェクトが連続する可能性があるので、幅を持ったFirstとLastのIndexを取得できるようにしておきます。

また、IComparable<T>を使って手軽な実装をします。

public class ObservableSortedCollection<T> : ...
{
    // 同じ順位で最初のインデックス. 見つからないときは-1.
    public int FirstIndexOf(T item)
    {
        // "以上"になる最初のオブジェクトを選ぶ
        return this.IndexOf(this.FirstOrDefault(x => x.CompareTo(item) >= 0));
    }

    // 同じ順位で最後のインデックス. 見つからないときは-1.
    public int LastIndexOf(T item)
    {
        // "以下"になる最後のオブジェクトを選ぶ
        return this.IndexOf(this.LastOrDefault(x => x.CompareTo(item) <= 0));
    }
}

IndexOf()nullも引数に取れるので便利。

4. InsertItem(),MoveItem()をオーバーライドしてSortedにする

今回は、追加と移動のときに適切な場所に挿入されるようにしていきます。(使わない引数は無視・・・)

public class ObservableSortedCollection<T> : ...
{
    // 適切な位置に挿入
    protected override void InsertItem(int _, T item)
    {
        // 後ろにつける
        var index = LastIndexOf(item) + 1;
        base.InsertItem(index, item);
    }

    // 適切な位置に移動
    protected override void MoveItem(int oldIndex, int _)
    {
        // 本来より前にある場合はfirstの手前につける
        var firstIndex = this.FirstIndexOf(this[oldIndex]);
        if(oldIndex < firstIndex)
        {
            base.MoveItem(oldIndex, firstIndex);
        }

        // 本来より後ろにある場合はlastの後ろにつける
        var lastIndex = this.LastIndexOf(this[oldIndex]);
        if (lastIndex < oldIndex)
        {
            base.MoveItem(oldIndex, lastIndex + 1);
        }

        // firstとlastの間にある場合は移動する必要がないので何もしない
    }
}

ObservableCollection<T>ではこの他にも、SetItem(), RemoveItem(), ClearItem()をoverrideすることができます。

おまけ. Tのプロパティが変わって順番が入れ替わる場合

T.CompareTo()の中身によっては、プロパティを変えることで順番が変わってしまうと思います。上記の実装だと途中で変更があったときに順番が正しく入れ替わらないので、変更のときにうまく順番を入れ替えるようにします。

順番が入れ替わる可能性がある箇所に、Moveを入れていきます。

i. SetItem()のときに入れ替える

public class ObservableSortedCollection<T> : ...
{
    // 変更後に移動もする
    protected override void SetItem(int index, T item)
    {
        base.SetItem(index, item);
        base.MoveItem(index, 0);
    }
}

ii. INotifyPropertyChangedで変更をつかむ

Tのプロパティが変わったときにMoveが呼び出されるようにします。Tの条件にINotifyPropertyChangedも加えておきます。

public class ObservableSortedList<T> :
    ObservableCollection<T>
    where T: IComparable<T>, INotifyPropertyChanged
{
    //...
}

PropertyChangedイベントに追加する関数を定義します。

public class ObservableSortedCollection<T> ://...
{
    // Tのプロパティ変更時に呼び出されるようにする関数
    private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if(sender is T item)
        {
            this.MoveItem(IndexOf(item), 0);
        }
    }
}

InsertItem()でイベント変更通知関数を登録します。

public class ObservableSortedCollection<T> ://...
{
    // 挿入時
    protected override void InsertItem(int _, T item)
    {
        var index = LastIndexOf(item) + 1;
        base.InsertItem(index, item);
        item.PropertyChanged += OnPropertyChanged; // イベント変更通知をつかむようにする
    }
}

RemoveItem(), ClearItems()でイベント変更通知関数を登録解除します。

public class ObservableSortedCollection<T> ://...
{
    // 削除
    protected override void RemoveItem(int index)
    {
        this[index].PropertyChanged -= OnPropertyChanged; // イベント変更通知を解除
        base.RemoveItem(index);
    }

    // クリア
    protected override void ClearItems()
    {
        foreach(var item in this)
        {
            item.PropertyChanged -= OnPropertyChanged; // イベント変更通知を解除
        }
        base.ClearItems();
    }
}

SetItem()ではイベント変更通知関数を解除し、登録します。

public class ObservableSortedCollection<T> ://...
{
    // 変更
    protected override void SetItem(int index, T item)
    {
        this[index].PropertyChanged -= OnPropertyChanged; // イベント変更通知を解除
        base.SetItem(index, item);
        base.MoveItem(index, 0);
        item.PropertyChanged += OnPropertyChanged; // イベント変更通知を登録
    }
}

おわりに

果たして楽な実装方法だったのか・・・

未分類

Posted by tanitanin