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て...。自重の倍て...。