Utils/SortableBindingListUtil.cs

/*
 * Copyright INDEX Co.
 */

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

namespace ChatworkBulkSender.Utils
{
    /// <summary>
    /// カスタムソート機能を提供するクラス。
    /// ダブルクリック時にソートを行う場合、SortModeをProgrammaticに設定し、ApplySortを使用してください。
    /// </summary>
    public class SortableBindingListUtil<T> : BindingList<T>,ITypedList
    {
        // 各列のソート状態を格納する配列を用意する
        private Dictionary<string, ListSortDirection> _columnSortDirections = new Dictionary<string,ListSortDirection>();
        private Dictionary<string, ListSortDirection> _defaultSortDirections = new Dictionary<string, ListSortDirection>();

        // 初回ソート済みであるかどうかを表すフラグ
        private bool _isSorted = false;

        // ソート対象プロパティ
        private PropertyDescriptor _sortProperty = null;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="list"></param>
        public SortableBindingListUtil(IList<T> list) : base(list)
        {
            // 基底クラスで初期化を行うので空
        }

        // オーバーライドプロパティ
        // 基底クラス BindingList<T> は、基本ソート機能設定のデフォルトの戻り値が FALSE なので常時有効にする
        protected override bool SupportsSortingCore => true; // ※ 式変形のプロパティ(C#6.0以降の式本体メンバー) プロパティのget {return true} と同等
        protected override bool IsSortedCore => _isSorted;
        protected override PropertyDescriptor SortPropertyCore => _sortProperty;

        // 
        public ListSortDirection SortDirection =>
            _sortProperty != null && _columnSortDirections.ContainsKey(_sortProperty.Name)
            ? _columnSortDirections[_sortProperty.Name]
            : ListSortDirection.Ascending;

        // 外部アクセス可のプロパティ
        public bool IsSorted => base.IsSortedCore;
        public PropertyDescriptor SortProperty => base.SortPropertyCore;
        public ListSortDirection SortDescription => base.SortDirectionCore;

        // 現在のソート順を取得する
        // プロパティが存在すれば、ソート状態を渡す
        protected override ListSortDirection SortDirectionCore => _sortProperty != null &&
                    _columnSortDirections.ContainsKey(_sortProperty.Name) ? _columnSortDirections[_sortProperty.Name] : ListSortDirection.Ascending;

        /// <summary>
        /// プロパティごとのデフォルトソートを設定する。
        /// </summary>
        /// <param name="property"></param>
        /// <returns></returns>
        public ListSortDirection this[PropertyDescriptor property]
        {   
            get => _defaultSortDirections.ContainsKey(property.Name) ? _defaultSortDirections[property.Name] : ListSortDirection.Ascending;

            set => _defaultSortDirections[property.Name] = value;
        }

        /// <summary>
        /// ソート処理を行う。
        /// </summary>
        /// <param name="property"></param>
        /// <param name="direction"></param>
        protected override void ApplySortCore(PropertyDescriptor property, ListSortDirection direction = ListSortDirection.Ascending)
        {
            // ソート順の決定
            direction = DatermineSortDirection(property,direction);

            // プロパティから取得し、List<T>にキャスト(下記でLINQを使用するため)
            var items = Items as List<T>;

            if (items != null && property != null)
            {
                var propertyInfo = typeof(T).GetProperty(property.Name);
                if (propertyInfo == null) { return; }

                // 自然順ソートを用いて、アイテムをソートする
                PerformSort(items,propertyInfo,direction);

                // ソート後の設定状況を更新する
                UpdateSortState(property,direction);
            }
        }

        /// <summary>
        /// ソート順を決定する。
        /// </summary>
        /// <param name="property"></param>
        /// <param name="direction"></param>
        /// <returns></returns>
        private ListSortDirection DatermineSortDirection(PropertyDescriptor property, ListSortDirection direction)
        {
            // ソート済みの列として含まれているか
            if (property != null && _columnSortDirections.ContainsKey(property.Name))
            {
                // 現在のソート設定と並びが一致するか確認する
                if (IsCurrentlySortedBy(property,_columnSortDirections[property.Name]))
                {
                    // 一致していた場合、ソート処理を反転する
                    return _columnSortDirections[property.Name] == ListSortDirection.Ascending
                        ? ListSortDirection.Descending : ListSortDirection.Ascending;
                }
                else
                {
                    // 一致していない場合、設定通りにソートする
                    return _columnSortDirections[property.Name];
                }
            }
            else
            {
                // 初回または新しい列の場合、デフォルト順でソートする
                return this[property];
            }
        }

        /// <summary>
        /// 実際に自然順でのソート処理を行う。
        /// </summary>
        /// <param name="items"></param>
        /// <param name="propertyInfo"></param>
        /// <param name="direction"></param>
        private void PerformSort(List<T> items, PropertyInfo propertyInfo, ListSortDirection direction)
        {
            // 自然順でソートできるネイティブメソッドのUtilクラスの比較子を用意する
            // 降順設定ならTrue
            var comparer = new NaturalStringComparerUtil(direction == ListSortDirection.Descending);

            // 用意した比較子を用いて、並び替える
            var sortedItems = items.OrderBy(x => propertyInfo.GetValue(x), comparer).ToList();

            // リストの更新中は変更通知を無効にする
            RaiseListChangedEvents = false;
            try
            {
                // 既存のアイテムをクリアする
                ClearItems();

                // ソート済みのアイテムを追加する
                sortedItems.ForEach(Add);
            }
            finally
            {
                // 変更通知を有効にする
                RaiseListChangedEvents = true;
            }
        }

        /// <summary>
        /// ソート後にソート状態を更新し、変更を通知する。
        /// </summary>
        /// <param name="property"></param>
        /// <param name="direction"></param>
        private void UpdateSortState(PropertyDescriptor property, ListSortDirection direction)
        {
            _columnSortDirections[property.Name] = direction;
            _isSorted = true;
            _sortProperty = property;

            // リスト全体が変更されたことを通知する
            OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
        }

        /// <summary>
        /// 外部から呼び出し可能なソート実行メソッド。
        /// 列のソート順を判定し、適切なソート順でソートを実行する。
        /// </summary>
        /// <param name="property">列のプロパティを設定してください</param>
        public void ApplySort(PropertyDescriptor property)
        {
            // ソートをする
            ApplySortCore(property);
        }

        public void ApplySort(PropertyDescriptor property, ListSortDirection direction)
        {
            // ソートする
            ApplySortCore(property,direction);
        }



        /// <summary>
        /// 現在プロパティ列の並びが記録されたソート順と一致するか判別する
        /// 他列のソートによって並び順が変化した場合にも対応する
        /// </summary>
        /// <param name="property">判別対象列のプロパティ</param>
        /// <param name="direction">記録されたソート順</param>
        /// <returns></returns>
        private bool IsCurrentlySortedBy(PropertyDescriptor property,ListSortDirection direction)
        {
            // 要素が1個以下(ソート対象なし)の場合、ソート済みとみなす
            if (Items.Count <= 1) { return true; }

            // ソート対象のプロパティをリフレクションで取得する
            var propertyInfo = typeof(T).GetProperty(property.Name);

            if (propertyInfo == null) { return false; }

            // IComparableと文字列の混在、null値を適切に処理するため、自然順のネイティブメソッド比較子を用意する
            var comparer = new NaturalStringComparerUtil();

            // アイテムのプロパティの値を取得する
            var values = Items.Select(item => propertyInfo.GetValue(item)).ToString();

            // 隣接する要素のペアを作成し、現在の並びが記録されたソート順と一致するか判別する
            return Items.Zip(values.Skip(1), (first,second) => 
            {
                int comparison = comparer.Compare(first,second);

                // 前の値 <= 後ろの値 なら昇順、前の値 >= 後ろの値なら降順
                return direction == ListSortDirection.Ascending ? comparison <= 0 : comparison >= 0;

            }).All(x => x); // 全てtrueの時のみtrue
        }

        /// <summary>
        /// ソートを解除する。
        /// </summary>
        protected override void RemoveSortCore()
        {
            _isSorted = false;
            _sortProperty = null;
        }

        /*
         * ITypedList インターフェイスの実装
         * DataGridViewがデータ構造を理解するために使用する
         * 
         * ※ データソースを解析する以下の3つで呼び出される 
         * 
         * ①.データソースを設定した時。
         * ②.AutoGenerateColumnsがTrueの場合
         * ③.DataPropertyNameとの紐付け時
         */

        /// <summary>
        /// プロパティの名前を取得する。
        /// </summary>
        /// <param name="listAccessors"></param>
        /// <returns></returns>
        public string GetListName(PropertyDescriptor[] listAccessors)
        {
            // リストの名前を返す
            return typeof(T).Name;
        }

        /// <summary>
        /// リスト内のアイテムが持つプロパティの情報を取得する。
        /// </summary>
        /// <param name="listAccessors"></param>
        /// <returns></returns>
        public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors)
        {
            // リスト内のプロパティ情報を返す
            return TypeDescriptor.GetProperties(typeof(T));
        }

    }
}