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

2008年8月30日土曜日

ModelBinderが素敵過ぎる

ASP.NET MVC Preview5の続き。

SingingEels : Model Binders in ASP.NET MVC

↑ここでサンプルダウンロードできるけど、DefaultModelBinderを派生させて独自Binderを定義しておくことで、Actionのパラメータをクラスの実体に置き換えることができるというもの。

とにかくダウンロードすればすぐわかるんだけど、少し解説。

まずはBinderのクラスを作成。これはDefaultModelBinderクラスを派生させましょう。 で、ConvertTypeメソッドをオーバーライドして、valueに入ってる値をdestinationTypeに変換してあげる。 例えば、ここのサンプルだとCustomersテーブルのID値をBase64にエンコードしたものをtargetCustomerという名前の QueryStringにしてActionLinkでリンクを生成して、そのリンクをクリックしたときのActionでCustomerクラスの実体が渡されるっていうものになってる。

this.Writer.Write(this.Html.ActionLink<HomeController>(c => c.Details(customer), customer.FullName));

↑これをIndex.aspx内で書いてて、リンク(aタグ)を出力してるんだけど、これの出力結果がたとえばID=1なら1をBase64エンコード('='は'_'に置換)して↓こうなる。

<a href="/Home/Details?targetCustomer=AQAAAA__" >Ivan Buckley</a>

これのアクション定義は↓。

public ActionResult Details(Customer targetCustomer){...}

んだけど、これはつまりデフォルトのルーティングをそのまま使うように変更してidという名前で渡すようにしてみるとですね、↓こうなるわけです。

アクション:public ActionResult Details(Customer id){...} リンク:<a href="/Home/Details/AQAAAA__" >Ivan Buckley</a>

これは、分かりやすくていいですよね。 型付きActionLinkで生成してるからIndex.aspxのほうは変更の必要なし。 わざわざアクションの中でIDからデータを取得するコードを書かなくても、MVCのハンドラがモデルに変換する処理をはさんでくれるというすぐれもの。 ただ、勝手に変換はしてくれないので、Global.asaxでModelBindersにどの型の変換をどのクラスで実行するかを登録しておく。

ModelBinders.Binders.Add(typeof(Customer), new MyCustomerBinder());

もう、楽しくてしょうがないね!

まさかまさかのPreview5

早いタイミングで出てきたのはいいけどPreview5とは。 実際、Controller変ったりViewEngine変ったりしてて大きな変更だからPreviewのままなんだろうね。 ※ViewEngineを作ったりはしんどそうだから、特に興味ないぜ!

img.aspx

ばびゅ~んとインストール。 後先考えずにインストール。 今までのプロジェクト動かないのはRelease Noteみたら書いてるから覚悟はしてたけど、ここまでダメだとは...。しかも、Sourceはまだ公開されてなくて、どうやって追っかけるんですか...。またしても見切り発車で先走り過ぎた。まぁ、いいや。

HtmlHelperが大きく変わって全く動かなくなりますね。 追加されたものとして↓。 HtmlHelper.RenderAction HtmlHelper.RenderRoute HtmlHelper.RenderPartial

今までのRenderUserControlが無くなって、これらが追加されてます。 これはViewDataの継承とかが全然変わるんじゃ...? と、思って簡単なテストプログラムを書いてみました。

まずはPreview 5で新規プロジェクト作成。

1.ModelsにUserViewData.cs(クラス)を追加。

namespace MvcApplication1.Models
{
public class UserViewData
{
  public string UserName { get; set; }
  public string ViewName { get; set; }
  public string Message { get; set; }
}
} 

2.Views/HomeにUsers.aspx(MVC View Content Page)を追加。

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Users.aspx.cs" Inherits="MvcApplication1.Views.Home.Users" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
<h2>ユーザー</h2>

<h3>ページで表示するメッセージ:<% = Html.Encode(ViewData["ViewMessage"]) %></h3>

<%
 foreach(var user in ViewData.Model)
   Html.RenderPartial("~/Views/UserControls/User.ascx", user);
%>

</asp:Content>

コードビハインドで型指定

namespace MvcApplication1.Views.Home
{
 public partial class Users : ViewPage<List<Models.UserViewData>>
 {
 }
}

3.Viewsに"UserControls"フォルダ作成。 4.Views/UserControlsにUser.ascx(MVC View User Control)を追加。

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="User.ascx.cs" Inherits="MvcApplication1.Views.UserControls.User" %>

<h5>コントロールで表示するメッセージ:<% = Html.Encode(ViewData["ViewMessage"]) %></h5>

<dl>
 <dt>ユーザー名</dt><dd><%= Html.Encode(ViewData.Model.UserName) %></dd>
 <dt>表示名</dt><dd><%= Html.Encode(ViewData.Model.ViewName) %></dd>
 <dt>メッセージ</dt><dd><%= Html.Encode(ViewData.Model.Message) %></dd>
</dl>

コードビハインドで型指定

namespace MvcApplication1.Views.UserControls
{
 public partial class User : System.Web.Mvc.ViewUserControl<Models.UserViewData>
 {
 }
}

5.HomeControllerにUsersアクション追加。

    public ActionResult Users()
  {
    ViewData["ViewMessage"] = "今日も雨が降ったり止んだりだね。";
    var users = new List()
    {
      new Models.UserViewData(){UserName = "takehara", ViewName="たけはら", Message="運動不足"},
      new Models.UserViewData(){UserName = "mauri", ViewName="マウリ", Message="ホッケーばっかり"},
      new Models.UserViewData(){UserName = "suzuki", ViewName="すずき", Message="ホッケーのみ"}
    };
 
    return View(users);
  }

6.Home/Index.aspxにリンク作成。

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="MvcApplication1.Views.Home.Index" %>

<asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
   <h2><%= Html.Encode(ViewData["Message"]) %></h2>
   <p>
       To learn more about ASP.NET MVC visit <a href="http://asp.net/mvc" title="ASP.NET MVC Website">http://asp.net/mvc</a>.
   </p>

   <%= Html.ActionLink("ユーザーページ", "Users") %>  
</asp:Content>

で、これを実行した結果の画面が↓これ。 img.aspx2

相変わらず、ViewDataが参照できてないですね。 が!!

RenderPartialには違うオーバーロードがあってですね、User.aspxのコードを以下のように変更。

<%
 foreach(var user in ViewData.Model)
   Html.RenderPartial("~/Views/UserControls/User.ascx", user, ViewData);
%>

すると...。 img.aspx3

あびりーばぼー!!

ViewDataDictionaryの中身をUserControlの中から参照できるようになりました。 今までのRenderUserControlだと型指定してViewDataを渡すとディクショナリの中身は見れなくなってたんだけど、これでそのページで利用したい情報はすべてViewDataDictionaryに入れておくことで、全部のUserControlから参照できるようになりますね。 最高っす!!

ここで、RenderPartialが直接結果を返さないのがミソ。実行はギリギリまで遅らせてTestしやすくってことですな。モック使ってね。

次に変わってんのがHtmlHelper.ActionLink/RouteLinkのオーバーロード。object型の引数の解釈が先にHtmlAttributes。ちゃんとUrlが生成されなくてビビるので気をつけましょう。

今までのコードをなるべくそのまま使うなら↓こんな感じでnull渡しましょう。

Html.RouteLink("リンク", "RouteName", new {val1="1", val2="2"},null)

HtmlHelperはいろいろ変更が多そうだけど、ここら辺押さえておけば以降は大丈夫そう。 次はControllerでの変更点。 ビビるのはBindingHelperExtensionsが無くなってるとこ。 BindingHelperExtensions.UpdateFromが...。これがないとRequest.Formの値をまとめて取り出せないじゃないか。と、みんな同じようなことを思ったみたいでForum確認してみたらちゃんと答えが。 Controller.UpdateModelを使えと。なるほど。確かに取得できる。Request.Formを指定しなくても良くなってたり、TryUpdateModelを使えば例外も起きないっぽい。

さらに今まで、Executeの前後の処理をController.Executeをoverrideしてそこに実装してたけど、これがなんとinternalになって、overrideできなくなりました。 が、しかし、これはちゃんとRelease Noteに書いてるから、すんなりとExecuteCoreのoverrideへ切り替え。でも、Initializeのoverrideでいいような処理だから、こっちにしよ。 ActionFilterAttributeもなんか結構変わってて。

ActionMethod.Name何処にいったんですかね。リフレクションでどうのこうのって書いてるけど、RouteDataから引っ張ってくればいいんですかね? とりあえずはfilterContext.RouteData.Values["action"]でAction名はとれる。 Cancelもなくなってるし、アクションが実行されなかったかどうかはどこでセットすればいいんですかね? どうしよ的な変更箇所が多かったりする中で、RenderPartialヘルパーが実装されてたり、AcceptVerbでRESTfulっぽくアクションを書けたり(PUT/DELETEはどうすんの?_methodで書き換えれるのがいいんだけど.NET Reflectorで確認した限りではHttpMethodをそのまま判定に使ってるっぽいから、自分で書いたRESTfulAttributeクラスからの乗り換えはないかな)、AjaxHelperがSystem.Web.Mvc.Ajaxに移動してたり、よさげなこともあるから(他にも FileResultクラスがあったり)しっかりチェックしていこうと思うところです。 How to use the ASP.NET MVC ModelBinder - Melvyn Harbour ↑ModelBinderの使い方サンプル。

Maarten Balliauw {blog} - Form validation with ASP.NET MVC preview 5 ↑ModelStateを使った入力検証のサンプル。

これViewDataに入れたりして、エラー項目を保持するのを自分で実装しなきゃいけなかったりしてたけど、超いい感じ!

2008年8月29日金曜日

ASP.NET MVCの記事

待望の後編が!! もう一つのASP.NET 「ASP.NET MVC」を知る(後編):CodeZine 一通りの機能説明がされててナイスです!

とりあえず前後編読んでおけば何となくアプリケーション作れるんじゃない? あと、ActionResult(ViewResultとか)?

最近、知ったんだけど"The ASP.NET MVC Information Portal"っていうのができてて、これでもかってくらい外部のサイトとかFeedで集めまくってるのでちょっと楽しいです。

最近はSSLを使えるようにいろいろ試行錯誤。 Vistaだと自己証明証明書とかで簡単にテスト環境作れるのがいいね。 SSL に使用する証明書の構成 コマンドで作成すればもっと融通のきく勝手証明書が作れるのかな~? こっちはうまくできなかったけど。 で、SSLで通信できるようになったのはいいけど、今度はどうやってリンクをSSLにしましょうか、ってところですよ。

Html.ActionLinkやRouteLink、Url.Actionとかで吐き出すURLをどうすればいいんだろかと思って調べてたら、こんなすごいのを発見。 Steve Sanderson’s blog » Blog Archive » Adding HTTPS/SSL support to ASP.NET MVC routing 書いてるとおりにSystem.Web.Routingモジュールを外して、こっちに置き換えて、Global.asaxにRoute登録すると...。すげ~!!かっこよくHTTPSとHTTPが切り替わるように出力されてる。絶対URLと相対URLをうまく切り替えてて賢く動く。

でも、VisualStudioのWebDev.WebServerじゃ、SSLで動かせないからIISに設定し直してテストしてみると~!!なんとまぁ、ちゃんと動かない...。 Problem 1のところに本人も書いてるけど、同じ症状。残念です。"Some gremlin in the routing"だってさ。 でも、これを改善したという猛者がいたりして。 Dmitriy Nagirnyak: Fixing HTTPS Support in ASP.NET MVC Routing

なぬ~!!と速攻でこの修正を加えて試してみたけど、ダメだった...。なんでだろね。ソース追っかける元気が...。いつかきっと確認しときます。 あ、そういえば、SP1 RTMにアセンブリリダイレクトしてないからうまく動かないのかも?まぁ、いっか。 このままじゃ、面倒なことになるなと思ってたところで、代替案。 Troy Goode: SquaredRoot - SSL Links/URLs in MVC

使い方は超簡単。いや、そりゃそうだってなもんで。 こっちはRoutingがどうのこうのじゃなくて拡張メソッドでガリっとURL書き換え。 なので、自分で書き換えたい部分のコードを変更する必要あり。 ちょっと面倒だけど、確実なのでとりあえずはこの方法で進めることにしてみます。 ※Html.ActionLink(...).ToSslLink()とかUrl.Action(...).ToSslUrl()って感じで使います。 このままだと、いったんHTTPになった後にHTTPに戻しにくいので、同じ要領で↓こんなのも用意しときましょう。

    public static string ToHttpUrl(this string text)
    {
      if (Utility.UseSSL)
        return ToFullyQualifiedUrl(text).Replace("https:", "http:");
      else
        return text;
    }

    public static string ToHttpLink(this string text)
    {
      if (Utility.UseSSL)
        return ToFullyQualifiedLink(text).Replace("https:", "http:");
      else
        return text;
    } 

そうそう、最初のナオキさんの記事の最後のページにこのブログが紹介で載ってるんだけど、こういうときにブログのタイトルがこんなだとちょっと恥ずかしい...。

2008年8月9日土曜日

イベントの実行順が面白くて

SQL Server 2008に合わせて.NET Framework 3.5 SP1が見えてるところですね。 ウェイトリフティングの三宅選手がスナッチあげてる時に使ってるタオルがスティッチ。ジャークのときにはサメのキャラを期待。

ASP.NET MVCのControllerではoverride出来るイベント(Onなんちゃら)が6個ありまして。

  • OnAuthorization(承認)
  • OnException(例外)
  • OnActionExecuting(Action実行前)
  • OnActionExecuted(Action実行後)
  • OnResultExecuting(Result実行前)
  • OnResultExecuted(Result実行後)
このActionとResultって何なんですか?って気になるところだったりしませんか? 以下のようなコードをHomeControllerに書くとどんな出力がでるのか試してみるとよく分かります。

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

namespace MvcApplication1.Controllers
{
 [HandleError]
 public class HomeController : Controller
 {
    protected override void OnAuthorization(AuthorizationContext filterContext)
   {
     System.Diagnostics.Debug.WriteLine("OnAuthorization");
     base.OnAuthorization(filterContext);
   }

   protected override void OnException(ExceptionContext filterContext)
   {
     System.Diagnostics.Debug.WriteLine("OnException");
     base.OnException(filterContext);
   }

   protected override void Execute(ControllerContext controllerContext)
   {
     System.Diagnostics.Debug.WriteLine("before Execute");
     base.Execute(controllerContext);
     System.Diagnostics.Debug.WriteLine("after Execute");
   }

   protected override void OnActionExecuting(ActionExecutingContext filterContext)
   {
     System.Diagnostics.Debug.WriteLine("- OnActionExecuting");
     base.OnActionExecuting(filterContext);
   }

   protected override void OnActionExecuted(ActionExecutedContext filterContext)
   {
     System.Diagnostics.Debug.WriteLine("- OnActionExecuted");
     base.OnActionExecuted(filterContext);
   }

   protected override ViewResult View(string viewName, string masterName, object model)
   {
     System.Diagnostics.Debug.WriteLine("-- View");
     return base.View(viewName, masterName, model);
   }

   protected override void OnResultExecuting(ResultExecutingContext filterContext)
   {
     System.Diagnostics.Debug.WriteLine("- OnResultExecuting");
     base.OnResultExecuting(filterContext);
   }

   protected override void OnResultExecuted(ResultExecutedContext filterContext)
   {
     System.Diagnostics.Debug.WriteLine("- OnResultExecuted");
     base.OnResultExecuted(filterContext);
   }

   public ActionResult Index()
   {
      System.Diagnostics.Debug.WriteLine("-- Index action execute");

     ViewData["Title"] = "Home Page";
     ViewData["Message"] = "Welcome to ASP.NET MVC!";

     return View();
   }

   public ActionResult About()
   {
     ViewData["Title"] = "About Page";

     return View();
   }
 }
} 

で、これだけだとちょっと見落としちゃうタイミングがあるので、Views/Home/Index.aspxの先頭にもコード追加。

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="MvcApplication1.Views.Home.Index" %>
<asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">

<% System.Diagnostics.Debug.WriteLine("-- page rendering"); %>

   <h2><%= Html.Encode(ViewData["Message"]) %></h2>
   <p>
       To learn more about ASP.NET MVC visit <a href="http://asp.net/mvc" title="ASP.NET MVC Website">http://asp.net/mvc</a>.
   </p>
</asp:Content>

これで、準備完了。 ※太字が追加したコードです。 実行してみると、デバッグ出力が↓こうなります。

before Execute OnAuthorization - OnActionExecuting -- Index action execute ← ここでアクション実行 -- View - OnActionExecuted - OnResultExecuting -- page rendering ← ここでASPXのレンダリング - OnResultExecuted after Execute

面白いでしょ? ASPX の実行はずいぶん後なんですよね。Viewの実行時にレンダリングされるわけじゃないっていうのが、なるほどなと思わずにはいられない。ちなみに Redirect/RedirectToAction/RedirectToRouteもContent/JsonもViewと同じタイミング。ASPX のレンダリングのタイミングと同じタイミングでResult実行されるのを気をつけておく必要ありです。 ControllerのExecuteの中でこんな順序で処理されてるっていうことが分かれば、いろいろできそうじゃないですか。 で、なんでこんなこと書いてるかというと、TempDataですよ。 この中でTempDataってどういうタイミングで保存復元されるんだろかと。 ソースを追いかけるとController.cs内のprotected internal virtual void Execute(ControllerContext controllerContext)に書かれてますね。 InvokeActionを呼び出す前に、TempData.Load(TempDataProvider)。InvokeAction後にTempData.Save(TempDataProvider)。 と、いうことはControllerのExecuteをoverrideしてbase.Executeの前後でTempDataにデータを入れても意味ないってことですよ(ね?)。 TempDataの出し入れのタイミングを間違えると、入れたのに取り出す時にはnullってことになりかねないので注意が必要です。

ViewDataに比べてあんまり注目されてない気がするTempDataだけど、結構使い道があって(メッセージ出力時や、ViewDataで使うモデルデータに関連するデータを入れたり)するので、積極果敢に攻めの姿勢で使っていこうと思うところですよ。 ※ただしTempDataはシリアライズの問題もあり、LINQ to SQLのモデルをそのまま入れることはできない(StateServerとSQLのSession変数に入れられないのと同じ理由)ので、匿名クラスとかに変換して入れたりします。 それにしても、スナッチ1回目で90kg上げる中国チン選手恐るべし!最終的にミスなしで95kgて...。自重の倍て...。

2008年7月29日火曜日

IsMvcAjaxRequest

ナオキさんのサイトで取り上げられていたので、流行りに乗っかっていこうと思います! ASP.NET MVC Preview 4からAjaxが少しとりいれられてます。

クライアントサイドはMicrosoft Ajax Libraryがベース。 これの使いどころはやっぱり部分更新ですよね。ASP.NET AJAXならUpdatePanelのような動きと言えばわかりやすいかな?

とにかく動かしてみることにしましょう。 まずは、Preview 4のプロジェクトテンプレートで新しいプロジェクトを作成。 そしたら、AccountControllerとHomeContoroller、Views/AccoutとViews/Homeとか出てきます。 Views/Homeの中には最初に表示されるIndex.aspxとAboutページのAbout.aspxが出来てます。 とりあえずHomeControllerのIndexアクションとViews/Home/Index.aspxだけを使って試してみることにします。

初期のIndexアクションは↓。

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

return View();
}

Index.aspxは↓。

<asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
   <h2><%= Html.Encode(ViewData["Message"]) %></h2>
   <p>
       To learn more about ASP.NET MVC visit <a href="http://asp.net/mvc" title="ASP.NET MVC Website">http://asp.net/mvc</a>.
   </p>
</asp:Content>

なんとまぁ、スッキリしたものが出てきますわ。 ※ViewData["Title"]はShared/Site.Masterで使ってますよ。 で、ページにFORMを張り付けてPOSTさせてみよう! IndexアクションでPOSTした値を取得してViewDataに入れようじゃないですか。

public ActionResult Index()
{
 ViewData["Title"] = "Home Page";
 ViewData["Message"] = "Welcome to ASP.NET MVC!";
   
 ViewData["result"] = "";
 if (Request.HttpMethod.ToLower() == "post")
 {
   ViewData["result"] = string.Format("こんにちは、{0} さん!", Request.Form["yourName"]);
 }

 return View();
} 

↓これをIndex.aspxに追加(</p>の後に)。

    <% using (Html.Form("Home", "Index"))
      { %>

      名前は?<%= Html.TextBox("yourName") %>
      <input type="submit" value="ぼたん" />
      <span id="result"><%= ViewData["result"] %></span>
    
   <%} %>

こんな感じですね。POSTしたyourNameをViewData["result"]入れて、それをspanタグ内に表示するものです。ここまではすんなりです。 img.aspx

↑こんな表示になるんで、テキストボックスに適当になんか入れて「ぼたん」押すと、↓こんな感じでボタンの横に表示されます。

img.aspx2 とびっきり普通の処理です。

ここからです! IndexアクションへのPOSTが発生した場合、ブラウザからのものなのかXMLHttpRequestからのものなのかを簡単に判別する方法として、Request.IsMvcAjaxRequest()というのがあるので、それを使うことにします。 なので、Indexアクションを変更。

    public ActionResult Index()
   {
     ViewData["Title"] = "Home Page";
     ViewData["Message"] = "Welcome to ASP.NET MVC!";
   
     ViewData["result"] = "";
     if (Request.HttpMethod.ToLower() == "post")
     {
       ViewData["result"] = string.Format("こんにちは、{0} さん!", Request.Form["yourName"]);
        if (Request.IsMvcAjaxRequest())
         return Content((string)ViewData["result"]);
      }

     return View();
   } 

↑太字の部分が追加したコードです。 差を分かりやすくするのに、Index.aspxには追加でAjax.Formを入れることにします。 なので、Index.aspxの全体は↓。

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="MvcApplication5.Views.Home.Index" %>

<asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
   <script src="/Content/MicrosoftAjax.debug.js" type="text/javascript"></script>
   <script src="/Content/MicrosoftMvcAjax.debug.js" type="text/javascript"></script>
 
   <h2><%= Html.Encode(ViewData["Message"]) %></h2>
   <p>
       To learn more about ASP.NET MVC visit <a href="http://asp.net/mvc" title="ASP.NET MVC Website">http://asp.net/mvc</a>.
   </p>
 
   <% using (Html.Form("Home", "Index"))
      { %>

      名前は?<%= Html.TextBox("yourName") %>
      <input type="submit" value="ぼたん" />
      <span id="result"><%= ViewData["result"] %></span>
    
   <%} %>
 
   <% using (Ajax.Form("Index", new AjaxOptions { UpdateTargetId = "result2" }))
      { %>
    
      名前は?<%= Html.TextBox("yourName") %>
      <input type="submit" value="ぼたん" />
      <span id="result2"><%= ViewData["result"] %></span>
    
   <% } %>
 
</asp:Content>

これを実行すると2つのテキストボックスとボタンが表示されます。 img.aspx3

フォームも2つあるからそりゃそうですね。 最初に上段のテキストボックスとボタンに”さる”と入れて上段のボタンを押します。

img.aspx4

ボタンの横に両方とも”さる”と出ます。 続いて、下段のテキストボックスに”いぬ”と入れて下段のボタンを押します。

img.aspx5 小さすぎて見にくい...。 まぁ、それはいいとして、下段だけが"いぬ"になりましたね。 結果だけを見ると分かりにくいんですけど、実際にコードを書くとですよ、上段のボタンはページ全体をPOSTで取得するのに対し、下段はボタン横のテキストのみAJAXで取得して書き換えてる動きになってるのが確認できると思います。

dotnetConf2015 Japan

https://github.com/takepara/MvcVpl ↑こちらにいろいろ置いときました。 参加してくださった方々の温かい対応に感謝感謝です。