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);
何に使うんだろ...。