前回のASP.NET MVC4始めましたの記事から1年ぐらいの間、ずっと ASP.NET MVC4使ってたわけですが、その感想を書いてみます。
Railsでいうところの before_filter がないのが辛いです。URL中、あるいは POSTされた内容のIDからオブジェクトを取得して、なければ404を返す、という ような定型処理を各アクションにいちいち書くことになってしまうので、汎用 の before_filter が欲しいなと思いました。同様にafter_filter、 around_filterも欲しいはず。
ただこれC#で書くとするとぱっと思いつく感じでは
1 [Before("FindUser")]
2 /* こんなところに書かないといけないし、メソッド名を文字列で指定しなければならない */
3 /* 複数指定したい場合はどうする? */
4 public class HogeController : Controller
5 {
6 protected User User { get; set; }
7
8 void FindUser(ActionExecutedContext filterContext /* めんどくさい型 */)
9 {
10 if (/* パラメータとれるの? */)
11 {
12 User = ...;
13 }
14 else
15 {
16 /* 404 返す */
17 /* めんどくさい */
18 }
19 }
20
21 [Before("...")]
22 /* クラスのBeforeに指定したものとここで指定したものとで実行順序が不定 */
23 public ActionResult Show(string id)
24 {
25 return View(User);
26 }
27 }
ということになってしまって、あまりスマートでないし問題がいろいろありそ う。いい方法がないか考え中です。
MVC4はModel Binderという仕組みがあって、アクションメソッドの引数に適当 な型を指定しておくと、アクションが呼ばれた際のパラメータをうまいことオ ブジェクトに詰め替えてくれます。
非常に簡単に書くと次のようなコードになります。
1 /* フレームワークがパラメータをUserクラス詰め替えてくれてくれる */
2 [HttpPost]
3 public ActionResult Update(User user)
4 {
5 if (!ModelState.IsValid)
6 {
7 return View(user);
8 }
9
10 Update(user);
11
12 return RedirectToAction("Show");
13 }
でもこれだと
に意図しない状況になります。
そこで実際には次のようなコードを書くハメになり、非常にだるいです。
1 [HttpPost]
2 public ActionResult Update(string userId, UserUpdateParameter userParam)
3 {
4 /* UserUpdateParameter はほぼ User と同じパラメータを持っている! */
5 /* パラメータや制約が変化した場合の追従がだるい */
6
7 var user = Db.Users.Find(userId); // userIdからユーザを取得
8 if (user == null)
9 {
10 return HttpNotFound(); // できなければ 404
11 }
12
13 if (!ModelState.IsValid) // 文字列長や必須項目等のバリデーション
14 {
15 // 不正なら入力画面に戻る
16 return View(userParam);
17 }
18
19 user.Name = userParam.Name;
20 user.Age = userParam.Age;
21 /* 以下必要なパラメータ分くりかえし!! */
22 /* だるい!!! */
23 /* パラメータ増減したらどうするの? */
24
25 Update(user);
26
27 return RedirectToAction("Show");
28 }
アクション毎にUserUpdateParameterとか作るのは面倒すぎます。 Userクラスとほぼ同じプロパティを持っていて、制約も同じにする必要がある クラスを定義しろと言われても、どこかで絶対にミスします。ここでRailsの attr_accesibleの考え方を参考に、パラメータとして受け取っていいプロパティ についてのみ、[Accessible]をつけることにすると良いのではないかと思います。
1 public class User
2 {
3 [Key]
4 public string UserId { get; set; }
5
6 [StringLength(200)]
7 [Accessible]
8 /* パラメータとしてアクションメソッド受け取ることができる */
9 public string Name { get; set; }
10
11 [Accessible]
12 public int Age { get; set; }
13
14 /* パラメータとしてアクションメソッドで受け取らない */
15 public bool IsAdministrator { get; set; }
16
17 /* パラメータとしてアクションメソッドで受け取らない */
18 public string EncryptedPassword { get; set; }
19 }
その上で、オブジェクト間のプロパティのコピーを行うヘルパークラス Parameter を定義します。
1 var user1 = new User();
2 var user2 = new User { Name = "hoge", Age = 5 };
3
4 // Accessible なプロパティについて、内容をコピーする
5 Parameter.UpdateAttributes(from: user2, to: user1);
6
7 user1.Name // "hoge"
こういうものがあれば、先のUpdateアクションは次のように簡潔に書くことができます。 (前節のBeforeと組み合わせています)
1 [HttpPost]
2 [Before("FindUser")]
3 public ActionResult Update(User param)
4 {
5 if (!ModelState.IsValid) // 文字列長や必須項目等のバリデーション
6 {
7 // 不正なら入力画面に戻る
8 return View(param);
9 }
10
11 /* 必要なパラメータ分くりかえして書かなくても良い */
12 /* FindUserによってUserは取得済み */
13 Parameter.UpdateAttributes(from: param, to: User)
14
15 Update(User);
16
17 return RedirectToAction("Show");
18 }
これで本質的なところだけがアクションメソッド内に書かれている状態になり、割とハッピーです。
えーと……HTTP的にKey=1&Key=2&Key=3と書けるのに、RouteValueDictionaryに 複数の値が入れられないのって、どうみても片手落ちです。これはさすがに今 までのように部品を追加する方法では解決できなくて、フレームワーク側に手 をいれないことにはどうしようもないので、どうにかしてほしいです。
これはURLルーティングの仕組みが違うから難しいのだろうと思いますが、欲しいですね。
もうめんどくさくなってきました。
これはMVC4に限った話ではないのですが、Mを厚くするのをどうすればいいのか わからないので、Cにたくさんコードを書いてしまうことになってしまうのは、 やっぱりよくないんだろうなと思いました。最初のうちはいいんだけど、複数 のコントローラで似たような処理が発生したときに、切り分けをどうするかで 毎回悩むことになってしまいました。
Rails使いましょう。