2008年9月3日水曜日

ModelBinderに気をつけねば

前のエントリでModelBinderを使って、ActionのパラメータにDBから取ってきたエンティティモデルをそのまま渡す方法を書いちゃったけど、あれはダメでした。

DataContextをstaticにもつサンプルだったけど、そうだとしてもいつDisposeされるのかなんて分かんないから、試しに実装してみたらケチョンケチョン...。

で、正しい使い方はこちら↓。 How to use the ASP.NET MVC ModelBinder - Melvyn Harbour というのを、昨日ガスリー君のブログでも書かれててホッとした。 ASP.NET MVC Preview 5 and Form Posting Scenarios - ScottGu's Blog

サンプルをチェックしてると、ProductをModelBinderでとってるじゃないかと思えるかもしれないけど、あくまで新規登録時の空エンティティの時にしか使ってないですよね。 で、更新処理の時にはDataContextから取得したProductに対して、ModelUpdateを使って値の書き込み。 この方法だとですね、更新時にはProductBinderのGetValue走らない。サンプルだから両方乗せてるんだろうけど(ModelUpdateとModelBinder)、最初はどんな意味が込められてるのか混乱しちゃった。 ModelUpdateでセットされるデフォルトのエラーメッセージが気に入らなかったらどこで書き換えればいいのかはちょっと分かんなかった。リソースファイルに持ってるのをどうすればいいんだろか。

で、 ProductクラスはLINQ to SQLのクラスなんだけど、これに対してModelStateDictionaryへメッセージを突っ込むコードを書くと、クラスが密結合しすぎちゃうがために、あえてRuleViolationクラスを作って(後でModelStateDisctionaryに入れやすくするために)、 IRuleEntityを実装。

ProductクラスのOnValidateはSubmitOnChangeの時に自動で呼び出してくれるっていうのがミソですね! でも、実際の開発は、たぶんだけどLINQ to SQLのエンティティクラスに対して直接入力しないよね。もう一つ間にViewDataクラスをはさんで、ViewDataにDBから読み込んだ値を入れてFormに表示。更新の時にViewDataに読み込んだ後、エンティティクラスに値をマッピングしていって更新。そんな流れになると思うので、入力検証のサンプルが↓これ。

Maarten Balliauw {blog} - Form validation with ASP.NET MVC preview 5

このサンプルではViewDataに直接値を入れてるけど、ViewDataのクラスを用意してViewPage<UserViewData>でレンダリングをView(model)にするんだと、↓こんな感じになるんじゃないかと思いますがどうですかね。

public class UserViewData { public name {get;set;} public email {get;set;} public message {get;set;} }

Contactアクション(POST)の入力値の取得で

var viewData = new UserViewData(); ModelUpdate(viewData, new[]{"name","email","message"});

って、すれば個々に入力値を取得しなくてもviewDataに埋め込みますわね。 でも、それだと入力検証できませんわね。全部stringだし。 なので、UserViewDataクラスに検証用のメソッドを追加して、それを呼び出すときにModelStateDictionaryを渡すのが簡単でいいんじゃないかと思います。 例えば、↓こんな。

public bool Validate(ModelStateDictionary modelStates)
{
 if (string.IsNullOrEmpty(name))
  modelStates.AddModelError("name",name,"名前入れてね!");
 else if (name.length < 4)
  modelStates.AddModelError("name",name,"4文字以上で名前入れてね!");

return modelStates.IsValid;
}

で、アクションでは↓。

 var viewData = new UserViewData();
 if (TryModelUpdate(viewData, new[]{"name","email","message"})) {
  viewData.Validate(ViewData.ModelState);
 } 

なんかヘンテコなコード...。 ※ModelUpdateも中でModelStateDictionaryにエラー値を入れてくれます。キャストできないとか。 ※Prefixをつけた場合、今までは最後に'.'(ドット)を自分でつけなきゃいけなかったのに、自動でつくようになってちょっと涙目...。気がつくのに時間かかった。

ちなみにUpdateModelのキー名はmodelに持ってる項目だけにすべし。Modelの項目を指定して、Formにない場合はエラーにならないけど、その逆はエラー(FormにあってModelにない)になるので気をつけよう。 ModelState情報はうまく利用すれば、エラーフィールドを強調できる(class属性にinput-validation-errorが自動でつく)ので超便利です。

ModelState はRenderPartial時にViewDataDictionaryを渡さないと(ViewData.Modelだけだとダメ)ユーザーコントロールで取得できないから、入力項目を持つユーザーコントロールのRenderPartial時には問答無用でViewDataも渡すようにするのが吉!

その他気になったところ。

Default option label for DropDownList in ASP.NET MVC Preview 5 - Shiju Varghese's Blog

便利にはなるよね。でも、必須にしなくてもいいじゃないかと、思ってしまうんですよ。 LINQで取り出した、ソースの最初の行に空(ここでいうoptionLabel)のレコードを連結させるコードを書いてたから、それがなくなるのはいいんだけど。ちなみにotionLabelに空文字""を指定すると何も起きないからレンダリング結果は今まで通り。

Maarten Balliauw {blog} - ASP.NET MVC preview 5's AntiForgeryToken helper method and attribute Steve Sanderson’s blog » Blog Archive » Prevent Cross-Site Request Forgery (CSRF) using ASP.NET MVC’s AntiForgeryToken() helper 何に使うのかサッパリわからなかったけど、こうやって使うんだね。CookieとForm(HIDDEN)POSTの値を比較して有効なリクエストか判定。

file download as attachment in latest preview (like this blog post) - ASP.NET Forums

FileResultの簡単な使い方。FileResultはResponse.TransmitFileとかで結果を直接返さずにActionResultとして返すことで、テストしやすくなるよね。 AntiForgeryToken/FileResultともにMicrosoft.Web.Mvcに入ってるデス。

2008年9月1日月曜日

ActionNameとAcceptVerbs

やっと意味がわかりました。

How a Method Becomes An Action

Phil Haackさん(なんかインタビュービデオを見たんだけど、どう見てもモルダー...。ホントはFBIなんじゃん?)とこで細かく書かれてるんだけど、なんていうかさ、英語じゃん?

でも、くじけるわけにはいかねっす。作ってるプロダクトがまったく動かないからね。 今まで(Preview4)は、ControllerやActionの前後でなんかしたかったり、処理をざくっと注入するときはActionFilterAttributeクラスを派生したものを使ってました。 Preview5でも同じクラスはあるんですよ。だけど、これが罠でして。同じ名前のクラスなのにコロッとかわっててね。流石Preview版。

どう変わってるかというと、ActionFilterはもう前後に処理をはさむだけで、その中で別のActionを呼んだり、Actionそのものの実行をキャンセルするのは止めないか的な。 いや、filterContextにはResultプロパティがついてて、そこに結果を入れてしまえばいいんだけど。 例えば↓。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

using System.Web.Mvc;

namespace MvcApplication1
{
  public class StopAttribute : ActionFilterAttribute
  {
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
      filterContext.Result = new ContentResult() { Content="途中で止める" };
    }
  }
} 

こんな感じでStopAttributeをActionFilterAttributeから派生して作っておいて、Home/Indexアクションにセットする。

    [Stop]
    public ActionResult Index()
    {
      ViewData["Title"] = "Home Page";
      ViewData["Message"] = "Welcome to ASP.NET MVC!";

      return View();
    } 

img.aspx

ばっちり中断して、違う結果を返してますね。 これはいいんですよ。別にこれがしたいわけじゃないんですよ。

RESTfulAttributeっていうのをずいぶん前に書いた(進化の過程をウキウキウォッチング)んですよ。 要は、GETの時に呼び出すActionとPOST/PUT/DELETEの時のActionを勝手に切り替えてくれるってもんです。でも、普通にブラウザからだとPUT/DELETEはダメなもんだから、prototype.jsの仕様に合わせて"_method"って名前でメソッド名を送信すると HTTP Methodじゃなくて、そっちをみて判断するようにしたものです(Railsっぽいよね!)。

これを実装するには、HTTP Methodを見て、Actionを切り替えるために元々のActionの実行をキャンセルして、HTTP Methodに合わせたActionを代わりに実効(ActionInvoke)して、その結果を返す必要があります。単純に考えたら filterContext.Resultに変わりになるActionの実行結果を入れてしまえばいいってことになるんだけどさ。もちろんそれも正解(上記サンプルのように実装できるし)。 でも、たぶん設計思想はそうじゃないっぽい。filterContext.Cancelがないし。

そこで出てきたのがActionNameAttributeとAcceptVerbsAttribute。ActionNameAttributeは、単純にAction名を別名に置き換えるもので、AcceptVerbsAttributeはHttpMethodを見て、実行可能な場合のみ(GETだけとかPOSTだけとか)Actionを実行するもの。 でね、それぞれ派生元のクラスがさ、ActionFilterAttributeじゃないんですよ。 ActionNameはAttributeクラス。AcceptVerbsはActionSelectionAttributeクラス。ActionSelectionAttributeクラスっていうのが今までなかったもので、今回追加されたんですね。 これは単純なabstractクラスで、

   public abstract bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo); 

たったこれだけ。

んじゃこのIsValidForRequestは何に使うのかというと、Actionの実行を許す時はtrueを返し、実行させたくないときはfalseを返す。 ウソじゃねっす!!試したっす! なので、こういうクラスが今回追加されてきたってことはですね、Actionの実行そのものと、Actionの実行を許可するかどうかは別々に実装すべきなんじゃないですかっていう設計思想なんじゃないかと。 いや、モルダーPhilさんがどう思ってるのかは知らないけど。 ※AuthorizeAttributeとかは別にActionSelectionAttribute派生じゃないから言い切れるわけじゃないっぺ!

なので、そういうことならそっちに合わせた方がカッコイイんじゃないかと。 作ってみたのがRESTfulVerbs。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Web.Mvc;

namespace MvcApplication1
{
  public class RESTfulVerbsAttribute : ActionSelectionAttribute
  {
    // 有効なHTTP Method(複数可)
    public string HttpMethods { get; set; }

    public RESTfulVerbsAttribute() : this("GET,POST") { }
    public RESTfulVerbsAttribute(string methods)
    {
      HttpMethods = methods;
    }


    public override bool IsValidForRequest(ControllerContext controllerContext, System.Reflection.MethodInfo methodInfo)
    {
      string[] enableHttpMethods = HttpMethods.ToLower().Replace(" ", "").Split(',').Where(s => s.Length > 0).ToArray();

     string httpMethod = controllerContext.HttpContext.ToLower();

      // prototype.js対応
      if (httpMethod == "post")
        httpMethod = context.Request.Form["_method"] ?? httpMethod;

      return enableHttpMethods.Contains(httpMethod.ToLower());
    }
  }
} 

どうやって使うかというと↓。

    [RESTfulVerbs("GET")]
    public ActionResult Index()
    {
      return View();
    }

    [ActionName("Index"),RESTfulVerbs("POST")]
    public ActionResult IndexPost()
    {
      // Indexに対してのPOSTはここで実行
    }

    [ActionName("Index"),RESTfulVerbs("PUT,DELETE")]
    public ActionResult IndexPut()
    {
      // Indexに対してのPUT/DELETEはここで実行
    } 

簡単ね。ActionNameAttributeとのコンボです。 単純なんだけど、こういう形にしちゃうと既存コードの変更箇所が凄い多くなるのが痛い...。ガンバです! でもね、ひとつ前のRESTfulFilterAttributeの実装の時(ASP.NET MVCでRESTful)に思ったんだけど、今回みたいな実装にするっていうことはですよ、リクエスト毎にグルグルとActionNameで指定したActionを探すことになるよね、きっと。それが嫌だしちょっとカッコ悪いと思ったから2個目の実装にしたのに。 そんな負荷は微々たるもんだから気にするんじゃないってことかな?

ちなみにfilterContext.ResultにActionの実行結果を入れる版のRESTfulAttributeは↓こんな感じの変更です。

      Type ctrl = filterContext.Controller.GetType();
      if (actions.ContainsKey(httpMethod) && actions[httpMethod] != "")
      {
          MethodInfo method = ctrl.GetMethod(invokeAction);
          if (method != null)
          
filterContext.Result = ctrl.InvokeMember(invokeAction,
BindingFlags.InvokeMethod, null, filterContext.Controller,
filterContext.ActionParameters.Values.ToArray()) as ActionResult;
      } 

※こっちのほうが修正少なくて楽...。

早くソースが公開されないかな~。 とりあえず、今回実装されてるFilter一覧を見てたらすごく気になるもの発見。 まず、この2つは基本クラス。

  • ActionSelectionAttribute : Attribute public abstract bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo);
  • CustomModelBinderAttribute:Attribute protected CustomModelBinderAttribute();
で、今までもあったものを含めて使えるフィルターがこれ。
  • AcceptVerbsAttribute:ActionSelectionAttribute
  • ActionNameAttribute:Attribute
  • AuthorizeAttributeFilter:Attribute, IAuthorizationFilter
  • HandleErrorAttribute:FilterAttribute, IExceptionFilter
  • ModelBinderAttribute : CustomModelBinderAttribute
  • NonActionAttribute : ActionSelectionAttribute
  • OutputCacheAttribute : ActionFilterAttribute
最後に気になるフィルターがこれ。
  • MethodSelectionAttribute:Attribute public virtual MethodSelectionResult OnMethodSelected(ControllerContext controllerContext, string action, MethodInfo methodInfo); public virtual MethodSelectionResult OnMethodSelecting(ControllerContext controllerContext, string action, MethodInfo methodInfo);
何に使うんだろ...。

2026年06月17日の記事一覧

(全 13 件) Foundry Toolkit for Visual Studio Code 自分のPCで「とりあえず動く」ではなく「一番賢く使える」ローカルLLMを教えてくれる「whichllm」、実測ベンチでランク付け(生成AIクローズアップ) | ...