前回テスト用のページを作りましたが、ソートするには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) })
|
になる訳です。(いろいろ突っ込みどころもあるかと思いますが)
ソート部はできたということで、気が向いたら次回は検索(フィルタリング)機能を追加してみようかと思います。