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にレンダリングしてくれたりするので。