ASP.NET MVC Tip #49 - Use the LinqBinaryModelBinder in your Edit Actions
ステファン君それは無理じゃない?更新以前にバインドした時点で例外でるじゃん。
と、思ってたのは今は昔。RCでは何の問題もなくバインド出来るみたい。試してみたら、普通に出来た。ずいぶん前に試したときはデータベースコンテキストがスタティックじゃないと例外でたり(ModelBinderに気をつけねば)、そもそも直接DBO使うのはうんぬんかんぬん。綺麗な設計どうのじゃなくて、純粋に少ないコード量でどこまで出来るか、っていうのを考えたら直接使う事もあったりするのかもね。
これまでも使ってたサンプルはDBを使ってなかったので、改めてDBを用意して、LINQ to SQLのモデルを作成。
これを利用するためのコントローラも定義。
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Mvc.Ajax; using Mvc.RC.Models; namespace Mvc.RC.Controllers { public class OnePeaceController : Controller { OnePeaceDataContext _context = null; public OnePeaceController() { _context = new OnePeaceDataContext(); } public ActionResult Index() { return List(); } public ActionResult List() { var people = _context.Persons; return View("List", people); } public ActionResult Details(int id) { var people = _context.Persons; return View(people.First(p => p.Id == id)); } [AcceptVerbs(HttpVerbs.Get)] public ActionResult Edit(int id) { var people = _context.Persons; return View(people.First(p => p.Id == id)); } [AcceptVerbs(HttpVerbs.Post), ValidateInput(false)] public ActionResult Edit(int id, Person person) { try { _context.Persons.Attach(person, true); _context.SubmitChanges(); return RedirectToAction("Index"); } catch (Exception ex) { return View(person); } } } }
List/Detail/Editの各ViewはRCで追加されたAdd Viewコマンドでサラッと作成。こういう時にとても便利ですね。Attachのところでブレークポイントをセットして、まずはこのまま動かしてみたところ、DB使っててもホントに動く。信じてなかったわけじゃないけど、こうもあっさりと動くとちょっと感動する。
↑Listページ。
↑Editページ。
↑普通にバインドされてる様子。
でも、このままステップ実行すると、確かに例外が発生。
エンティティは、バージョン メンバを宣言するか、または更新チェック ポリシーを 含まない場合にのみ、元の状態なしに変更したものをアタッチできます。
System.Exception {System.InvalidOperationException}
と、言うことで、簡単に更新チェックポリシーをオフにしてしまおうかとも思ったけど、それよりちゃんと競合チェックするようにtimestamp型の列をテーブルに追加して動かしてみます。
↑ChangeStampという名前のtimestamp型の列を追加したモデル。
でも、timestamp型はSystem.Data.Linq.Binary型としてクラスが生成されるので、このままだとちゃんとモデルが復元されません。なによりViewにちゃんと出力してないし。まずはEditのViewにChangeStampをhiddenで展開するコードを追加。
<p> <%= Html.Hidden("ChangeStamp", Convert.ToBase64String(Model.ChangeStamp.ToArray())) %> <input type="submit" value="Save" /> </p> </fieldset> <% } %> <div> <%=Html.ActionLink("Back to List", "Index") %> </div> </asp:Content>
submitの手前に入れてます。Base64エンコードしてHiddenフィールドに。このままだとDefaultModelBinderが復元してくれくれないので、Global.asaxのApplication_Start時にASP.NET MVC Futuresに入ってるLinqBinaryModelBinderを登録。
protected void Application_Start() { RegisterRoutes(RouteTable.Routes); ModelBinders.Binders.Add(typeof(System.Data.Linq.Binary), new LinqBinaryModelBinder()); }
もう、何もかもステファンさんのいいなりです。
この状態で動かしてみて、Viewが出力するHTMLソースのChangeStamp部分を確認してみたのが↓これ。
<input id="ChangeStamp" name="ChangeStamp" type="hidden" value="AAAAAAAAB9Q=" />
ちゃんと、エンコードされて出力されてます。あたりまえだっちゅーの。
これを更新するためにsubmitして、ブレークポイントでモデルの中身を確認。ちゃんとLinqBinaryModelBinderでBinary型も復元されてます。そのまま実行を続けても例外は出なくて、テーブルも更新されてました。で、更新された後のViewのCangeStampを確認してみる。
<input id="ChangeStamp" name="ChangeStamp" type="hidden" value="AAAAAAAAB9U=" />
ちゃんと、最初とは違う値が入ってますね~。
と、いうことで、ステファンさんのやったことをそのまま試したみたわけですが、これが出来るって事はLINQ to SQL+スキャッフォールディングで凄く簡単にDBを使ったアプリケーションを作成出来る事になりますね。Repositoryは作るにしても、ViewModelを作らずシンプルなコードで開発することが出来るのでWebForms並の生産性(ViewStateとデータバインディング)を実現できてるんじゃないかと思う次第です。
※ViewStateはModelStateが各inputフィールドの値を保持しつつHTMLにレンダリングしてくれたりするので。