技術memo

関数型ゴースト

IComparer<T>と、リフレクションを使用しないSortableなBindingListの実装

リストのソートなんて今時LINQでOrderBy/ThenByが使えるから十分、とはいえそうも行かないのがUI周りですね。

Windows Formsでdata objectを定義してそのListをDataGridViewにDataBindしたら、ヘッダをクリックしてもソートしてくれないんですけど!」

f:id:nenono:20141005191539p:plain

そんなことを言われましても。

ということで今回はその辺の話と、作ったコードです。

ICompareの取り回しがつらい件

リストのソート条件の指定方法は、一般に以下のような方法が考えられると思います。

  1. その要素の型にIComparableを実装させる
  2. そのときのソート条件に応じてIComparerを作成する
  3. そのときのソート条件に応じてComparisonデリゲートを使用する
  4. LINQのOrderByシリーズを使う

ここで、IComparableを使うと、「対象データTの既定の大小関係」を定義することになるので、今回は除外。

また、LINQのOrderByは、「ソートした新しいシーケンスを作成する」ものなので、普段使いには使い勝手がよいのですが、今回は除外。

そうするとIComparerかComparisonかになりますが、これらは実質的にはインターフェースかデリゲートかの違いだけで同じものです。ということで、今回はこれらを使っていきます。

参考 : IComparer(T).Compare メソッド (System.Collections.Generic)

IComparer.Compareメソッド、あるいはComparisonデリゲートは、以下の形をとります

int Compare(
    T x,
    T y
)

ここで、xとyは比較対象だとわかります。では戻り値のintは、上記の説明を見ると次のようになっています

Value 説明
0 より小さい値 x が y より小さい。
0 x と y が等しい。
0 を超える値 x が y より大きくなっています。

ソート基準とするプロパティやフィールドを指定する形式ではないのが面倒くさいですね。正と負がどちらの意味だったかを毎回調べるのも嫌です。

ということで作ったコードが以下です。

まあこれ自体は車輪の再発明ではありますが... neue cc - AnonymousComparer - lambda compare selector for Linq ここでは簡易版なので匿名型対応は無しです。

AppendとReverseメソッドがちょっと便利そうでよい感じです。

/// <summary>
/// IComparer[T]補助関数
/// </summary>
public static class ComparerUtils
{
    /// <summary>
    /// 動的にデリゲートから生成するIComparer[T]実装
    /// </summary>
    /// <typeparam name="T">比較対象の型</typeparam>
    private sealed class DynamicComparer<T> : IComparer<T>
    {
        private readonly Func<T, T, int> compare;

        /// <summary>
        ///  2 つのオブジェクトを比較し、一方が他方より小さいか、等しいか、大きいかを示す値を返します。
        /// </summary>
        /// <param name="x">比較する最初のオブジェクトです。</param>
        /// <param name="y">比較する 2 番目のオブジェクト。</param>
        /// <returns>
        /// x と y の相対的な値を示す符号付き整数 (次の表を参照)。
        /// 値 : 説明
        /// number < 0 : x が y より小さい。
        /// 0 : x と y は等しい。
        /// 0 より大 : x が y より大きい。
        /// </returns>
        public int Compare(T x, T y)
        {
            return compare(x, y);
        }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="compare">比較関数</param>
        public DynamicComparer(Func<T, T, int> compare)
        {
            this.compare = compare;
        }
    }

    /// <summary>
    /// 比較するキーを指定してComparerを生成します。
    /// </summary>
    /// <typeparam name="T">比較対象の型</typeparam>
    /// <typeparam name="TP">キーの型</typeparam>
    /// <param name="selector">比較対象からキーを取得する関数</param>
    /// <param name="comparer">キーのComparer</param>
    /// <returns>Comparer</returns>
    public static IComparer<T> By<T, TP>(Func<T, TP> selector, IComparer<TP> comparer)
    {
        return new DynamicComparer<T>((x, y) => comparer.Compare(selector(x), selector(y)));
    }

    /// <summary>
    /// 比較するキーを指定してComparerを生成します。比較にはComparer[TP].Defaultを使用します
    /// </summary>
    /// <typeparam name="T">比較対象の型</typeparam>
    /// <typeparam name="TP">キーの型</typeparam>
    /// <param name="selector">比較対象からキーを取得する関数</param>
    /// <returns>Comparer</returns>
    public static IComparer<T> By<T, TP>(Func<T, TP> selector)
        where TP : IComparable<TP>
    {
        return By(selector, Comparer<TP>.Default);
    }

    /// <summary>
    /// Comparerでの比較結果が同一だった場合に、第二引数のComparerで比較を行うようなComparerを生成します
    /// </summary>
    /// <typeparam name="T">比較対象の型</typeparam>
    /// <param name="comparer">優先する第一のComparer</param>
    /// <param name="second">第二のComparer</param>
    /// <returns>Comparer</returns>
    public static IComparer<T> Append<T>(this IComparer<T> comparer, IComparer<T> second)
    {
        return new DynamicComparer<T>((x, y) =>
        {
            var first = comparer.Compare(x, y);
            if (first != 0)
            {
                return first;
            }
            return second.Compare(x, y);
        });
    }

    /// <summary>
    /// Comparerの比較順序を逆転させます
    /// </summary>
    /// <typeparam name="T">比較対象の型</typeparam>
    /// <param name="comparer">元となるComparer</param>
    /// <returns>Comparer</returns>
    public static IComparer<T> Reverse<T>(this IComparer<T> comparer)
    {
        return new DynamicComparer<T>((x, y) => comparer.Compare(y, x));
    }
}

BindingListでソートを実装する

参考 : カスタム データ バインド (第 2 部)

ええ、これくらいのこと、標準では提供してくれないんですか。しかも肝心のPropertyComparerの実装が書いてないってどういうことですか。

困りました。仕方が無いので、要件を考えてみます。

  • バインドしたデータTの、表示中のプロパティそれぞれの昇順逆順について、Tのソート条件を生成する
  • それらのソート条件によりソートする
  • 画面に描画する

ここで、ソートするところはSystem.Collections.Generic.List.Sortにでもやらせればいいですね。また、画面に描画するところはDataGridViewにやらせればいいから、後は適切に描画できるようにIBindingListの実装を考えればよい、ということです。

ということでサクっと実装してみます。プロパティ名ごとにIComparerをあらかじめ作って渡しておけば良さそうです。

/// <summary>
/// ソート機能をサポートするBindingListです
/// </summary>
/// <typeparam name="T">リストの要素の型</typeparam>
public sealed class SortableBindingList<T> : BindingList<T>
{
    /// <summary>
    /// キー: プロパティ名 , 値 : プロパティをキーにソートする為のIComparer[T]
    /// </summary>
    private readonly Dictionary<string, IComparer<T>> comparers;

    /// <summary>
    /// 前回ソート方向
    /// </summary>
    private ListSortDirection direction;
    /// <summary>
    /// 前回ソートプロパティ
    /// </summary>
    private PropertyDescriptor property;
    /// <summary>
    /// ソート済みフラグ
    /// </summary>
    private bool isSorted = false;

    private SortableBindingList(List<T> source, Dictionary<string, IComparer<T>> comparers)
        :base(source)
    {
        this.comparers = comparers;
    }

    /// <summary>
    /// SortableBindingListを生成します
    /// </summary>
    /// <param name="source">生成するリストの要素の一覧</param>
    /// <param name="comparers">KeyValuePairのリスト... キー: プロパティ名 , 値 : プロパティをキーにソートする為のIComparer[T]</param>
    /// <returns>生成したSortableBindingList</returns>
    public static SortableBindingList<T> Create(IEnumerable<T> source, IEnumerable<KeyValuePair<string, IComparer<T>>> comparers)
    {
        return new SortableBindingList<T>(source.ToList(), comparers.ToDictionary());
    }

    protected override void ApplySortCore(PropertyDescriptor prop, ListSortDirection direction)
    {
        this.property = prop;
        this.Sort(prop.Name, direction);
    }

    /// <summary>
    /// ソートを実行します
    /// </summary>
    /// <param name="propertyName">基準とするプロパティ名</param>
    /// <param name="direction">ソート方向</param>
    public void Sort(string propertyName, ListSortDirection direction)
    {
        var list = (List<T>)this.Items;
        var c = comparers[propertyName];
        list.Sort(direction == ListSortDirection.Ascending ? c : c.Reverse());
        this.isSorted = true;
        this.direction = direction;
        this.OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
    }

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

    protected override ListSortDirection SortDirectionCore
    {
        get
        {
            return direction;
        }
    }

    protected override PropertyDescriptor SortPropertyCore
    {
        get
        {
            return property;
        }
    }

    protected override bool SupportsSortingCore
    {
        get
        {
            return true;
        }
    }
}

使ってみます。デザイナ側は省略するので適当なdataGridView1を作って列を追加してDataPropertyNameを設定してあげてください。

public partial class Form1: Form
{
    public Form1()
    {
        InitializeComponent();
    }
    private class Record
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public DateTime Birth { set; get; }
    }
    private void Form1_Load(object sender, EventArgs e)
    {
        var source = new[]{
            new Record{Id=1, Name="yamada akihito", Birth= new DateTime(1990, 1,5)},
            new Record{Id=2, Name="sasaki yoshie", Birth=new DateTime(1989, 12, 1)},
            new Record{Id=3, Name="katou takayuki", Birth=new DateTime(1989, 5, 30)},
            new Record{Id=4, Name="tanaka kotarou", Birth=new DateTime(1990, 3, 15)}
        };
        this.dataGridView1.DataSource = SortableBindingList<Record>.Create(source, new[]{
            KVP.Create("Id", ComparerUtils.By((Record r)=>r.Id)),
            KVP.Create("Name", ComparerUtils.By((Record r)=>r.Name)),
            KVP.Create("Birth", ComparerUtils.By((Record r)=>r.Birth))
        });
    }
}

f:id:nenono:20141005192624p:plain

ということで、ソートできました。画像はBirthの降順です。 ソートがある程度型安全にできているのがいい感じじゃないでしょうか。勿論プロパティ名に対応するIComparerをつくり忘れたらエラーになりますが……。

以上でした。以下補足。

KVP.Create等は作ってあったユーティリティ関数です。

/// <summary>
/// KeyValuePairのユーティリティ関数群
/// </summary>
public static class KVP
{
    public static KeyValuePair<TK, TV> Create<TK, TV>(TK key, TV value)
    {
        return new KeyValuePair<TK, TV>(key, value);
    }

    public static Dictionary<TK, TV> ToDictionary<TK, TV>(this IEnumerable<KeyValuePair<TK, TV>> source)
    {
        return source.ToDictionary(x => x.Key, x => x.Value);
    }
}

また記事には入ってないですが、namespacesは以下くらいを開いておけばたぶん大丈夫なはずです。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;

動作バージョンはC# 3.0以降、.NET3.5以降のはず。