ASP.NET MVC 5のお勉強にこちらの本を参考にさせていただいております。

必要な内容が体系的に理解できる良書かと思います。

非常にお勧めできる内容です。

が、こちらの中に出てくるサンプルで、ソート可能なグリッド表を作るってのがあるんですが、LINQ説明用のサンプルということもあり、あんまり汎用的に使えるコードではなかったりします。

似たような記事をcodeprojectでも見つけましたが、こちらも再利用するとき大変そうです。。

てことで、もうちょっと再利用できるような形にしてみたいと思います。

参考資料

問題のサンプルはWebでも読めます。感謝!

LINQ:データを並べ替える – orderby句

codeprojectはこちら

Sample for to list, sort, search data and add pagination in ASP.Net MVC 5

プロジェクト

プロジェクトは

クラスライブラリ、単体テスト、サンプルMVCの3つでいこうかと思います。

まずはクラスライブラリ、単体テストを作って、テストファーストで開発して行こうかと思います。

ソートフィールド定義

サンプルを見ますと各フィールドごとに、

が定義されています。こんな感じですね。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
...
 ViewBag.Category  = (sort == "Category" ? "dCategory" : "Category");
...
    case "Category":
      articles = articles.OrderBy(a => a.Category);
      break;
    case "dCategory":
      articles = articles.OrderByDescending(a => a.Category);
      break;
...

サンプルではソートキーとかべた書きですし、同じ文字列が何度も出てきたりしてますが、こいつをもう少し簡単に扱えるようにSortFieldDefinitionクラスを作ります。

ソートキーの生成

ソートキーとフィールド名は同じなことが多いので、ラムダ式を与えれば後は勝手にやってくれるとうれしいような気がします。

てことで、テストはこんな感じ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// <summary>
    /// テスト用データクラス
    /// Class for test data
    /// </summary>
    public class Article
    {
        public string Title { get; set; }
        public string Category { get; set; }
        public DateTime Published { get; set; }
        public long Viewcount { get; set; }
    }

    [TestClass]
    public class SortFieldDefinition
    {
        [TestMethod]
        public void SortKey_Test1()
        {
            var sortdef = new ChimaLib.Models.SortFieldDefinition<Article, string>(obj => obj.Title);
            Assert.AreEqual("Title", sortdef.SortKey);      //キーを抽出する関数からソートキー文字列生成
        }
    }

インプリはこんな感じ。

1
2
3
4
5
6
7
8
public class SortFieldDefinition<TModel, TKey>
    {
        public SortFieldDefinition(Expression<Func<TModel, TKey>> aKeySelector){
            this.SortKey = ((MemberExpression)aKeySelector.Body).Member.Name;
        }

        public string SortKey{ get;  private set; }
    }

キモはラムダ式からメンバ名を取ってきてるところでしょうか。

こちらを参考にさせていただきました。

UI用ソートキー

サンプルではUI上で選択した値によって、次に使うべきソートキーを決定しています。

1
ViewBag.Category  = (sort == "Category" ? "dCategory" : "Category");

この部分。昇順、降順の入れ替えですね。

テストはこんな感じ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[TestMethod]
        public void NextSortKey_Test1()
        {
            var sortdef = new ChimaLib.Models.SortFieldDefinition<Article, string>(obj => obj.Title);

            var next=sortdef.GetNextSortKey("Title");
            Assert.AreEqual("Title desc", next);    //昇順の次は降順

            next = sortdef.GetNextSortKey("Title desc");
            Assert.AreEqual("Title", next);         //降順の次は昇順

            next = sortdef.GetNextSortKey("unknown");
            Assert.AreEqual("Title", next);         //別のフィールドがソートキーの場合は昇順

        }

インプリ。

1
2
3
4
5
6
7
8
private const string DESC_SUFFIX = " desc";
        public string GetNextSortKey(string aCurrentSortKey) {
            if (aCurrentSortKey == this.SortKey) {
                return this.SortKey + DESC_SUFFIX;
            }
            return this.SortKey;

        }

OrderBy/OrderByDescendingの追加

ソートキーとユーザー選択値を元に、クエリ(IQueryable)にOrderByを追加します。

テストでは実際ソートしてみて結果を調べています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
[TestMethod]
        public void AddOrderBy_Test1() {
            var dummy_list = new Article[] {
                new Article() {Title="Z"},
                new Article() {Title="A"},
                new Article() {Title="M"}
            };
            var sortdef = new ChimaLib.Models.SortFieldDefinition<Article, string>(obj => obj.Title);

            var articles = (from a in dummy_list select a).AsQueryable();

            articles = sortdef.AddOrderBy(articles, "Title");  //ソートキー"Title"順に並べ替え
            var result = articles.ToArray();

            Assert.AreEqual("A", result[0].Title);
            Assert.AreEqual("M", result[1].Title);
            Assert.AreEqual("Z", result[2].Title);

        }

        [TestMethod]
        public void AddOrderBy_Test2() {
            var dummy_list = new Article[] {
                new Article() {Title="Z"},
                new Article() {Title="A"},
                new Article() {Title="M"}
            };
            var sortdef = new ChimaLib.Models.SortFieldDefinition<Article, string>(obj => obj.Title);

            var articles = (from a in dummy_list select a).AsQueryable();

            articles = sortdef.AddOrderBy(articles, "Title desc");  //ソートキー"Title"順に並べ替え
            var result = articles.ToArray();

            Assert.AreEqual("Z", result[0].Title);
            Assert.AreEqual("M", result[1].Title);
            Assert.AreEqual("A", result[2].Title);

        }

インプリはこんな感じ。

コンストラクタでKeySelectorラムダ式を保存してなかったので保存するようにして、メソッド内で使用してます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
private Expression<Func<TModel, TKey>> KeySelector { get; set; }

        public SortFieldDefinition(Expression<Func<TModel, TKey>> aKeySelector){
            this.SortKey = ((MemberExpression)aKeySelector.Body).Member.Name;
            this.KeySelector = aKeySelector;
        }
...
        public IQueryable<TModel> AddOrderBy(IQueryable<TModel> aQuery, string aCurrentSortKey) {
            if (aCurrentSortKey == this.SortKey) {
                return aQuery.OrderBy(this.KeySelector);
            } else if(aCurrentSortKey == this.SortKey + DESC_SUFFIX) {
                return aQuery.OrderByDescending(this.KeySelector);
            }
            return aQuery;
        }

ココまでで、一応並べ替えの処理本体ができました。

SortFieldDefinitionのインスタンスをもうちょっと簡単に作れるようにする

SortFieldDefinitionをnewするには

1
var sortdef = new ChimaLib.Models.SortFieldDefinition<Article, string>(obj => obj.Title);

のように書くのが基本ですが、なんか長いですね。特にラムダ式の戻り値の型(string)の部分とか、いちいちめんどくさいです。

こういうときには、拡張メソッドと型推論でさっくり書けるようにできます。

こことかこことか参考になります。

テストを書きます。

1
2
3
4
5
6
7
[TestMethod]
        public void SortDef_New_Test1() {
            Article article=null;
            //理想的には Article.DefineSort(obj => obj.Title); みたいに書きたいけど無理っぽい。
            var sortdef = article.DefineSort(obj => obj.Title);
            Assert.AreEqual("Title", sortdef.SortKey);  //キーを抽出する関数からソートキー文字列生成
        }

拡張メソッドでthisになるオブジェクトが必要なんで、

Article article=null;

してますが、ほんとはクラスの静的メソッド風に呼べるといいんですけどね、できないのかな?

実装はこんな感じです。

1
2
3
4
5
6
public static class SortFieldDefinitionExtention
    {
        public static SortFieldDefinition<TModel,TKey> DefineSort<TModel, TKey>(this TModel aModel, Expression<Func<TModel, TKey>> aKeySelector) {
            return new SortFieldDefinition<TModel, TKey>(aKeySelector);
        }
    }

長くなったので、(たぶん)次回に続きます。

一応ココまでの内容はGitHubで公開しときます。

ブログ書きながらコード書くのって、時間食いますな。