前回テスト用のページを作りましたが、ソートするにはurlのパラメータを手で入力しなければなりませんでした。ヘッダのクリックでソートできるようにしたいと思います。

複数フィールドのテスト

色々いじる前に、前回コントローラー内でやった処理と同じ処理をテストに追加しときます。

 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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/// <summary>
        /// create dummy data
        /// </summary>
        /// <param name="num">num of data. must be an even number.</param>
        /// <returns></returns>
        private IQueryable<Article> TestData1(int num) {
            Article[] data = new Article[num];
            for(var i = 0; i < num; i++) {
                /* mixed index. if num=10 then 0,9,2,7,4,5,6,3,8,1 */
                var idx = (i % 2 == 0) ? i: num - i;
                System.Diagnostics.Debug.WriteLine(idx);
                data[i] = new Article() {
                    Title = "Title" + idx.ToString(),
                    Category = "Category" + idx.ToString(),
                    Published = new DateTime(2015, 10, idx+1),
                    Viewcount = idx
                };
            }
            return data.AsQueryable();
        }

        [TestMethod]
        public void MultiSortField_Asc_Test1() {
            var data = this.TestData1(10);
            Article article = null;
            ISortFieldDefinition<Article>[] sort_fields = new[] {   //ソート列定義
                article.DefineSort(a=>a.Title),
                article.DefineSort(a=>a.Viewcount),
                article.DefineSort(a=>a.Published),
            };

            foreach (var field in sort_fields) { //ソート適用
                data = field.AddOrderBy(data, "Title");
            }
            var result = data.ToArray();
            for(var i = 0; i < 10; i++) {
                Assert.AreEqual(i, result[i].Viewcount);
            }
        }

        [TestMethod]
        public void MultiSortField_Desc_Test1() {
            var data = this.TestData1(10);
            Article article = null;
            ISortFieldDefinition<Article>[] sort_fields = new[] {   //ソート列定義
                article.DefineSort(a=>a.Title),
                article.DefineSort(a=>a.Viewcount),
                article.DefineSort(a=>a.Published),
            };

            foreach (var field in sort_fields) { //ソート適用
                data = field.AddOrderBy(data, "Title desc");
            }
            var result = data.ToArray();
            for (var i = 0; i < 10; i++) {
                Assert.AreEqual(9-i, result[i].Viewcount);
            }
        }

複数のソートフィールド定義を管理するクラス

テスト書いてると、ソート適用部分のループ処理は毎回必要ですので、まとめたくなります。

まずは、テストで書きたいように書く。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
[TestMethod]
        public void SortDefinition_Test1() {
            var data = this.TestData1(10);
            Article article = null;
            SortDefinition<Article> sort_def = new SortDefinition<Article>() {
                Fields=new[]{   //ソート列定義
                    article.DefineSort(a=>a.Title),
                    article.DefineSort(a=>a.Viewcount),
                    article.DefineSort(a=>a.Published)
                },
                SortKey = "Title"   //ソート対象
            };
            data= sort_def.AddOrderBy(data); //かつてループだったところ

            var result = data.ToArray();
            for (var i = 0; i < 10; i++) {
                Assert.AreEqual(i, result[i].Viewcount);
            }
        }

実装は簡単で、主にループ部分を抜き出しただけ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class SortDefinition<TModel>
    {
        public IEnumerable<ISortFieldDefinition<TModel>> Fields { get; set; }
        public string SortKey { get; set; }

        public IQueryable<TModel> AddOrderBy(IQueryable<TModel> aQuery) {
            foreach (var field in this.Fields) { //ソート適用
                aQuery = field.AddOrderBy(aQuery, this.SortKey);
            }
            return aQuery;
        }
    }

リンク作成に必要なデータ

Viewにリンクを埋め込む際に必要なデータは、フィールド名とソートキーです。

サンプルでは

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

といった感じで、フィールド毎にViewBagに入れています。

View側では

1
2
3
@Html.ActionLink(
    Html.DisplayNameFor(model => model.Category).ToString(),
    "Sort", new { sort = ViewBag.Category })

というように、ViewBagの値を参照しています。

ViewBagはインテリセンスが効かないのでできれば避けたいものです。

この辺の処理をSortDefinitionとその拡張クラスに追加します。

Viewからはアクションに渡すべき値をフィールド毎に知ることができればOKなので、modelのラムダ式だけ知っていればOKみたいにしたいところです。

そのために、各フィールド定義に持たせていたGetNextSortKeyメソッドをまとめて呼び出せるようにします。

テストで書くとこんな感じ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
[TestMethod]
        public void SortDefinition_GetNextSortKey_Test1() {
            var data = this.TestData1(10);
            Article article = null;
            SortDefinition<Article> sort_def = new SortDefinition<Article>() {
                Fields = new[]{   //ソート列定義
                    article.DefineSort(a=>a.Title),
                    article.DefineSort(a=>a.Viewcount),
                    article.DefineSort(a=>a.Published)
                },
                SortKey = "Title"   //ソート対象
            };

            var keyforlink= sort_def.GetNextSortKey(a=>a.Title);  //これを実装したい
            Assert.AreEqual("Title desc", keyforlink);

            keyforlink = sort_def.GetNextSortKey(a => a.Viewcount);
            Assert.AreEqual("Viewcount", keyforlink);

        }

実装はSortDefinitionをthisとする拡張メソッドにします。

1
2
3
4
5
6
7
8
public static class SortDefinitionExtention
    {
        public static string GetNextSortKey<TModel,TKey>(this SortDefinition<TModel> aSortDef, Expression<Func<TModel, TKey>> aSelector) {
            var targetKey = ((MemberExpression)aSelector.Body).Member.Name;
            var field = aSortDef.Fields.First((def) => def.SortKey == targetKey);
            return field.GetNextSortKey(aSortDef.SortKey);
        }
    }

そしたらSortDefinitionごとViewに引き渡してあげれば何とかなるはずです。

Controller

SortDefinitionを使用するよう、コントローラーを書き換えます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// GET: Articles
        public ActionResult Index(string sort)
        {
            var query = db.Articles.AsQueryable();  //データソース

            Article article = null;
            SortDefinition<Article> sort_def = new SortDefinition<Article>() {
                Fields = new[] {   //ソート列定義
                    article.DefineSort(a=>a.Url),
                    article.DefineSort(a=>a.Title),
                    article.DefineSort(a=>a.Description),
                    article.DefineSort(a=>a.Viewcount),
                    article.DefineSort(a=>a.Published),
                    article.DefineSort(a=>a.Released),
                },
                SortKey=sort
            };

            query=sort_def.AddOrderBy(query);

            ViewBag.SortDef = sort_def;     //リンク作成用にソート定義を渡す
            return View(query.ToList());    //ソート済み結果をViewに返す。
        }

View

Viewでは渡されたSortDefinitionでActionLinkを作ります。

まず、先頭のコードブロックでViewBagの内容を型の付いた変数に格納します。(ついでにusingも)

1
2
3
4
5
6
@using ChimaLib.Models
@using ChimaLibSample.Models
@{
    ViewBag.Title = "Index";
    SortDefinition<Article> SortDef = ViewBag.SortDef; //型付け
}

これでView内でインテリセンスが使えます。

そしたら各ヘッダ部のDisplayNameForをActionLinkに置き換えます。

こんな感じ。

1
2
3
@Html.ActionLink(
                Html.DisplayNameFor(model => model.Url).ToString(),
                "Index", new { sort = SortDef.GetNextSortKey(model=>model.Url) })

ソートキーはSortDefinitionの機能を使って生成、その際にラムダ式が使えるのでインテリセンスOKで、スペルミスには警告が出ます。

実行結果

これでソート機能つきリストができました。

実行するとこんな感じです。

ヘッダ部のフィールド名クリックで、ソート順が変わります。上の絵では「ビュー数」でソートしております。

要するに何がうれしいのか?

長々書いてきましたが、要するに、ControllerとViewがシンプルになりますよ、ってことです。

今回の場合、ライブラリ部分を再利用すれば、個別に実装する部分は

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// GET: Articles
        public ActionResult Index(string sort)
        {
            var query = db.Articles.AsQueryable();  //データソース

            Article article = null;
            SortDefinition<Article> sort_def = new SortDefinition<Article>() {
                Fields = new[] {   //ソート列定義
                    article.DefineSort(a=>a.Url),
                    article.DefineSort(a=>a.Title),
                    article.DefineSort(a=>a.Description),
                    article.DefineSort(a=>a.Viewcount),
                    article.DefineSort(a=>a.Published),
                    article.DefineSort(a=>a.Released),
                },
                SortKey=sort
            };

            query=sort_def.AddOrderBy(query);

            ViewBag.SortDef = sort_def;     //リンク作成用にソート定義を渡す
            return View(query.ToList());    //ソート済み結果をViewに返す。
        }

1
2
3
@Html.ActionLink(
                Html.DisplayNameFor(model => model.Url).ToString(),
                "Index", new { sort = SortDef.GetNextSortKey(model=>model.Url) })

になる訳です。(いろいろ突っ込みどころもあるかと思いますが)

ソート部はできたということで、気が向いたら次回は検索(フィルタリング)機能を追加してみようかと思います。