2009年11月18日水曜日

IModelBinderの話

今回はカスタムModelBinderを解説する。ModelBinderとはリクエストパラメータとControllerのActionの引数になっている型を見て、リクエストパラメータがその引数に変換可能であれば値を変換・設定してくれるという何とも素晴らしい機能のことだ。

ModelBinderの動作は下記のようになる。

-Model-
class Clip
{
 public string Description { get; set; }
}

-View-
<%Html.TextBox("Description")%>

-Controller-
pubilc ActionResult AddClip(Clip clip)
{
 var description = clip.Description;   // You can directly retrieve Description here!!
 // code here...
}

上記の例でModelBinderは次の処理を行っている。Controller.AddClipの引数にClipクラスがあり、そのクラスのプロパティにDescriptionがあるのでリクエストパラメータのDescriptionという名前の値をClip.Descriptionに代入している。

と、このようにModelBinderは値の変換、代入という煩雑な処理を一手に引き受けてくれる大変便利でシンプルな構造になっているのが理解できたかと思う。しかし、Viewが複雑になってくると独自にバインド処理を記述したくなる場面がでてくる。その方法をここから解説しよう。

ここではViewから配列情報を格納したJsonを文字列として受け取り、そのJsonをListクラスに変換・代入するという男気あふれる処理を行う。

-Model.cs-
[ModelBinder(typeof(PathModelBinder))]
public class PathModel
{
 public string Title { get; set; }
 public List<PathItemModel> Paths { get; set; }
 public string PathsJson { get; set; }

 public void SetModelValue(ModelStateDictionary modelState)
 {
  modelState.SetModelValue("Title", new ValueProviderResult(this.Title, this.Title, null));
  modelState.SetModelValue("PathsJson", new ValueProviderResult(this.PathsJson, this.PathsJson, null));
 }
}

public class PathModelBinder : IModelBinder
{
 public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
 {            
  var list = JsonConvert.DeserializeObject<List<PathItemModel>>(bindingContext.ValueProvider["PathsJson"].AttemptedValue);
  var model = new PathModel()
  {
   Paths = list,
   Title = bindingContext.ValueProvider["Title"].AttemptedValue,
   Description = bindingContext.ValueProvider["Description"].AttemptedValue,
   PathsJson = bindingContext.ValueProvider["PathsJson"].AttemptedValue
  };
  return model;
 }
}

-Controller.cs-
public ActionResult AddPath(PathModel path)
{
  if (!_service.AddPath(path))
  {
      path.SetModelValue(this.ModelState);
      return View("AddPath", path);
  }
  return RedirectToAction("Index");
}

上記の例では、Controller.AddPathは引数としてPathModelを受け取る。このPathModelはModelBinder属性が指定してあり、その指定でバインド処理をPathModelBinderで行うと明示してある。そのPathModelにはList<PathItemModel>というプロパティがあるのだが、その値はポストされたPathsJsonを解析して生成されるものなのでその処理をPathModelBinderで行っている。実際の処理はPathModelBinderのBindModelを見てもらえば一目瞭然なので省略する。

以上がカスタムModelBinderを使用するために必要な実装だ。どうだろう、かなりシンプルにできているのが実感できたと思う。これでController内に「値の変換を行う」という処理がなくなり、Controller本来の役目であるフローの制御に集中できるようなった。より関心の分離が進んだ状態というわけだ。

最後に一点注意が必要なのがPathModelのようにカスタムModelBinderを使用しているModelをViewへ渡す場合だ。その場合は、PathModelのSetModelValueで行っているようにModelStateへ値を設定してやらないとViewで値の取得ができないので忘れずに実装してほしい。

0 件のコメント:

コメントを投稿