ラベル C# の投稿を表示しています。 すべての投稿を表示
ラベル C# の投稿を表示しています。 すべての投稿を表示

2010年6月18日金曜日

yield break

最近HaackさんとこでChecking For Empty EnumerationsNull Or Empty CoalescingでEmptyなIEnumerableに対する拡張メソッドで盛り上がりを見せてるますね。面白いですよね。

でね、ふと思ったんですけどね、IEnumerable<T>の空イテレータってどう表現するのがオシャレなのかな、と。

  public IEnumerable<T> Empty<T>()
  {
    return Enumerable.Empty<T>();
  }

  public IEnumerable<T> EmptyZero<T>()
  {
    return new T[0];
  }

  public IEnumerable<T> EmptyYield<T>()
  {
    yield break;
  }

んん~。個人的にはyield breakなんだけど。

2010年2月28日日曜日

続Demotterについて

途中まで書いて力尽きたので、続きを。

ValuProviderFactoryについての説明まで進みましたが、そもそものIValueProviderは何なのかというところをすっ飛ばしてます。IValueProvider自体はMVC 1の頃から存在してるものです。GetValueでValueProviderResultとして値を取得するためのインターフェースです。UpdateModelなんかもIValueProviderを渡すオーバーライドがちゃんとあります。FormCollection.ToValueProvider()なんかを渡しますね。

ようは何でもいいからキーに対応する値を渡せばいいんです。基本実装のDictionaryValueProviderは値の実体そのものをキーと共に保持してるものなんですが、キーに対応する値を常に保持しておく必要はなく、GetValueのたびに取得しにいくという実装もアリです。HttpCookieValueProviderの例としてとてもいいサイトが有ります。

Dive Deep Into MVC - IValueProvider - Mehdi Golchin's blog

このサンプルコードではGetValueのたびにRequest.Cookieを参照する作りなのは、値の実体はHttpContextにそもそも保持されてるので、Dictionaryとして二重に保持する必要がないからです。ただ、これだとテストはHttpContextBaseのモック作成から必要になるんで、少し面倒ですけど。

Demotter(MvcPresetner)で作成したのは、PresentationZipValueProviderFactoryでプロジェクトルートにあるやつです。

mvcp

これまで説明したValueProviderFactoryのサンプル実装としてZipファイルをアップロードすると、サーバーサイドで解凍し、Presentationクラス(特にModelBinderの対象となるクラスを限定する縛りはないです)のインスタンスを生成するためにDefaultModelBinderから利用出来るようになっています。

なんで、ここに注目してるのかというと、SRP(Single Responsibility Principal)でテストを簡単にしたいからです。やっぱり楽して作りたいというのがあるからね。Actionのテストってモデルクラスを渡してしまえば、実行コンテキストに依存させなくてもいいじゃないです。DefaultModelBinderは標準機能だからテストなんかしなくていい。そうすると、カスタムValueProviderFactoryだけがHttpContextBaseのMockを使ったテスト対象になるわけです(ViewのテストはWebサーバーで実際に動かしてSeleniumとかでどうぞ)。素晴らしいリファクタリングだと思います。

Zipファイルを解凍するために、外部の依存アセンブリとしてDotNetZip Libraryを利用しています。ZipFileクラスのReadでZipファイルを指定し、ExtractAllで全解凍です。使ってる機能はそれだけ。

PresentationのリポジトリとしてStoragePresentationRepositoryクラスを作ってます。IPresentationRepositoryのファイルストレージ保存用の実装です。なので、ここはデータベースに保存するようなRepositoryを実装すれば、データファイルの保存場所は上位のサービス層(このサンプルではサービス層はなくControllerで直接Repositoryを使ってます)が知る必要はないような作りです。

実ファイルを”~/App_Data”に保存するようにしてるのでServer.MapPathを使う必要があり、Controller.InitializeでIPresentationRepositoryの実装クラスのインスタンスを作成してるので、LinqToSqlPresentationRepositoryを作成したとしても、単純変更はできないのが、手を抜いたところです。RepositoryのResolverというかCreatorを作っておいて、そいつに任せるようにしておく実装であれば依存性を排除できますね。

    private IPresentationRepository _repository;
    public static Func<RequestContext,IPresentationRepository> RepositoryCreator = 
      (requestContext) => new StoragePresentationRepository(
requestContext.HttpContext.Server.MapPath("~/App_Data")
); protected override void Initialize(RequestContext requestContext) { base.Initialize(requestContext); // Serverプロパティなどの参照はInitialize以降じゃないと // できないので気をつけましょう _repository = RepositoryCreator(requestContext); }

たとえば、現状のコードを↑こうしてみるとか。LinqToSqlPresentationRepositoryはRequestContextを必要としないですけど、簡単にするにはこういうのを渡すルールにしておくのもいいんじゃないですかね。Global.asaxなんかで以下のようにCreatorを変えちゃえば、うまくいくはず。

    protected void Application_Start()
    {
      AreaRegistration.RegisterAllAreas();

      RegisterRoutes(RouteTable.Routes);

      ValueProviderFactories.Factories.Add(new PresentationZipValueProviderFactory());

      HomeController.RepositoryCreator = (_) => new LinqToSqlPresentationRepository();

      // カスタムModelBinderを使うなら↓ここで登録忘れずに。
      //ModelBinders.Binders.Add(typeof(Presentation), new PresentationModelBinder());
    }

ここはMVC関係ないところ。今回のサンプルではControllerが生成されるたびに、毎回Repositoryの中でApp_Dataを見てPresentationのインスタンスを取得するので無駄が多いですが、その辺もサンプルということで勘弁してもらえると助かります。

ModelBinderは標準のDefaultModelBinderを使っていて、DataAnnotationModelValidatorがそのまま機能します。カスタムModelBinderでもDataAnnotationを機能させるなら、多分以下のような作り方になると思います。

  /// ValueProviderFactoryを定義しない従来の手法だと、ModelBinderを作成して
  /// 以下のように自分でマップしたモデルに対してValidationを実行することになります。
  public class PresentationModelBinder : DefaultModelBinder
  {
    public override object BindModel(ControllerContext controllerContext, 
ModelBindingContext bindingContext) { // ここでモデルを生成してModelMetadataに入れておくと、 // CreateModelでは生成せずに、OnModelUpdating/OnModelUpdated // を内部で呼び出してくれるようになる。 // でも、この書き方であってるのか自信無いですが...。 var valueResult = bindingContext.ValueProvider.GetValue("Name"); var model = StorageAccessor.Load(
PresentationZipValueProviderFactory.UploadTempPath,
true, valueResult.AttemptedValue); bindingContext.ModelMetadata.Model = model; return base.BindModel(controllerContext, bindingContext); } }

これがあってるのかどうかは自信がないです。BindModelの戻り値にインスタンスをそのまま返すだけではDataAnnotationが効かないので、BindingContext.ModelMetadata.Modelに対象モデルのインスタンスを入れて、後はbase.BindModelに任せてしまう実装です。いいのかな~。ちゃんと動くのは確認してます。

リポジトリから取得できたものをHTMLとして生成するために、Viewにモデルを渡し(Presentationクラスのインスタンス)、後はViewにまるなげです。

スライドとして表示したいデータをMarkdown書式で送信したものを利用するようにしてるので(Stackoverflow.comをまねっこしてみたかった)、Markdown書式からHTMLに変換する必要があります。クライアントでの変換実装としてWMDというのもありますが、今回はサーバーサイドでHTMLに変換するMarkdownSharpを利用しています。2個目の外部依存アセンブリです。Markdownで厳しいところはUL/LIの入れ子が2段までしかできないところ。できる方法があるんだろうか。当たり前ですが、利用目的がスライドじゃないのでしかたないところですね。

スライドの動き自体はカーソルの上下でフェードさせながらの切り替え、左右でアニメーション無しでの切り替えの2種類のみで、リッチなアニメーションは実装してないです。その辺はS5やS6なんかがあるので、そっちに差し替えてもらえればいいかな~、なんて。

そんなこんなで、実行すると↓こんなです。

mvcp2

後は、F11でフルスクリーン表示にしておけば、それっぽく見えます。

あとアップロードしたコンテンツの削除をHttpDelete属性を指定したActionで実装してますが、これだけだとHttpVerbs.DeleteなリクエストじゃないとActionInvokerの対象として選択されないです。一般的なブラウザではGET/POSTしか送信してくれないので困りもの。でも、MVC 2ではHttpVerbsのオーバーライドを簡単にできるように拡張メソッドも用意されてるので、HtmlHelperの拡張メソッドHttpMethodOverrideをForm内で呼び出せば、POSTでもオーバーライド(hiddenに埋め込まれる)されてうまく動くようになります。Railsなんかでも"_method"でHttpVerbsをオーバーライドできるのでそれと同じです。

Viewでは以下のように書いてます。

    <ul>
    <% foreach (var item in Model) { %>
    
    <li>
      <%= Html.RouteLink(item.Name, "Viewer", new { id = item.Name })%>
      &nbsp;-&nbsp;
      <% using (Html.BeginForm("Delete", "Home", new { id = item.Name }, 
FormMethod.Post, new { style = "display:inline;" })) { %> <%= Html.HttpMethodOverride(HttpVerbs.Delete) %> <input type="submit" value="削除" /> <% } %> </li> <% } %> </ul>

簡単ですね。

mvcp3

↑こんなボタン出てきます。なんで、わざわざHTTPメソッドでActionを選択するのかというのはRESTfulなアーキテクチャスタイルの話になるので割愛。ただ、この実装方法であれば、ブラウザ以外からDeleteやPutのリクエストと、ブラウザからの同リクエストを区別するようにActionを書かなくて済むのがいいですよね。もちろんAction名が”Remove”とか”Update”とかでPostで処理をするようにしても、結果は一緒ですけどね。

DemotterことMvcPresenterが何を実装したサンプルなのか、だいたい分かってもらえたでしょうか。これを10分で話すのはさすがに無理ですね。詰め込みすぎました。

Demotterについて

ASP.NET MVC 2になって変更された箇所はとても多いです。MVC 2での新しくなった部分を紹介するサンプルとしてMvcPresenterというのを作成することにしました。Demotterという名前はEdtterへのオマージュ(?)。

MVC 2の新機能のうちマニアにはたまらないだろうなと思って目をつけたのが各種Providerモデルへのリファクタリング部分で、MvcPresenterではそのうちIValueProviderを実装したValueProviderFactoryのカスタム化と言う部分をメインに実装しています。

いきなりそんな話をされても意味がわからないと思うので、順を追って説明していきます(ASP.NET MVCについての基本的な知識は前提です)。

そもそもMVCではポストバックがないので、TextBoxやRadioButtonなどの入力用サーバーコントロールは使用しません。HTMLとしてのinputやtextarea、selectを使用するのみです。そうすると入力値をサーバーサイドで取得するにはどうすればいいかというと以下の3通りあります。

  • Request.Formで取得
  • FormCollection型の変数をAction引数に指定する
  • ModelBinderに任せる

Request.FormとFormCollectionを使用する方法はあまりにも原始的すぎます。入力に対する検証も自分で行う必要があり、とても煩雑なコードになります。

Viewとして以下のようなものがあるとします。

  <% using (Html.BeginForm("FormPost1")) { %>
  
    <% = Html.TextBox("name") %>
    <% = Html.TextBox("age") %>
    <% = Html.CheckBox("isDeveloper") %>
    
    <input type="submit" value="do post" />
    
  <% } %>

これを受け付けるActionとしてRequest.FormやFormCollectionの場合↓こうなります。

    public ActionResult FormPost1()
    {
      var name = Request.Form["name"];
      int age;
      int.TryParse(Request.Form["age"], out age);
      bool isDeveloper;
      bool.TryParse(Request.Form["isDeveloper"], out isDeveloper);

      // 処理

      return View();
    }

    public ActionResult FormPost2(FormCollection form)
    {
      var name = form["name"];
      int age;
      int.TryParse(form["age"], out age);
      bool isDeveloper;
      bool.TryParse(form["isDeveloper"], out isDeveloper);

      // 処理

      return View();
    }

ほとんど同じなんですが、テストする際にRequestなどのコンテキストに依存させないようにするためにFormCollectionを使用するという書き方が存在します。

これに対しModelBinderを利用するスタイルの場合は以下のようになります。

    public ActionResult FormPost3(string name, int age, bool isDeveloper)
    {
      // 処理

      return View();
    }

Action引数に直接入力値が入ってきます。型変換も自動です。変換できないならエラーになるという便利なものです。でも、これだと細かく入力エラーを処理できないです。しかも値が多いとAction引数がとんでもないことになります。

なので以下のようにクラスを定義して、そのクラスのインスタンスをAction引数に取得するというスタイルがオーソドックスな手法となるはずです。

  public class Person
  {
    public string Name { get; set; }
    public int Age { get; set; }
    public bool IsDeveloper { get; set; }
  }

↑これがクラス定義で、↓これがAction。

    public ActionResult FormPost4(Person person)
    {
      // 処理

      return View();
    }

何が違うかは一目瞭然。本来personという仮引数名を使用する場合、Formのname属性にプレフィックスとして"person."とつけておくんですが、そこは自動でプロパティ名とname属性をみて一致するなら埋めてくれます。なので、あえて"person.name"や”person.age”とname属性に指定しなくてもModelBinderは賢いのでなんとかしてくれるんですね。明示的に分けたいときにname属性にプレフィックスを指定する必要があります。

クラスを指定するのも基本型を指定するのもModelBinderにしてみれば同じことです。固有のクラスを使用して、DefaultModelBinderがきちんとインスタンスを生成できないときには独自のModelBinderを作成することになると思いますが、MVC 2ではそういう手法はあまりとらないんじゃないかと思ってます。理由はValueProviderFactoryが指定できるようになったからです。

不思議に思わないですか?Routeに指定した場合でもAction引数に割り当てられるし、FormからPostしても割り当てられる。もちろんQueryStringの場合でも自動でAction引数に値がわたってくるんですよ?データの出所がそれぞれ違うじゃないですか。RouteとQueryStringはURLだから同じだと見ることもできるんですけど。

ここで、もうひとつ忘れてはいけないのがUpdateModelとTryUpdateModelです。これはIValueProviderを指定するか、Formの値を利用するかのどちらかでクラスのインスタンスを生成してくれるんですが、それもValueProviderFactoryを利用することでデータの出所を意識しなくても良くなります。

さっきから"データの出所(でどころ)"という言葉を使ってますが、それってどういう意味かというと、ModelBinderが値を復元する際にどこから値を持ってくるのか?ということです。Request.FormなのかRequest.QueryStringなのか、RouteData.Valueなのか、ですね。じゃーRequest.Cookieから復元させたいときはどうすればいいと思いますか?MVC 1の時はAction内でRequest.Cookieを直接みて自分で変数に割り当てるか、カスタムModelBinderを作成し、そこでRequest.Cookieを参照してモデルに復元させる必要がありました。MVC 2になるとデータの出所をValueProviderFactoryから取得するという仕様になっているので、カスタムなValueProviderFactoryを作成し、Global.asaxでValueProviderFactoriesに追加しておけば、標準のValueProviderFactoryで見つからなかった場合、カスタムValueProviderFactoryから値を取得して、ModelBinderが値(クラスのインスタンスか基本型)を復元してくれます。Cookieから値を取得して復元させたければ、ModelBinderを作成するのではなく、そこはDefaultModelBinderに任せたまま、CookieValueProviderFactoryを作成するとなるでしょう。そうすることでDataAnnotationも有効な状態で値を取得できます(カスタムModelBinderでもできますがそれはまた別の時に)。

ModelBinderのデータの出所(データ取得元)を自分で好きなように指定できるということです。すごいことですよね。ちなみにValueProviderFactoryとして実装しなければいけない唯一のメソッドは

public override IValueProvider GetValueProvider(ControllerContext controllerContext)

です。IValueProviderの基本実装はDictionaryValueProvider<object>で、KeyValueなディクショナリです。ModelBinderの仕組みそのものはMVC 1の時から変わってないので、詳細ははしょりますが(書いたほうがいいですか?)、キーとしてForm要素のname属性やQueryStringのKeyを指定するのを想定して値を取得します。

なのでDictionaryとしてCookieから取得した値を返そうが、JSONをデシリアライズしてキー指定で取得できるようにしたものを返そうがXMLをキー指定で取得できるようにしたとしても、Dictionaryとして取得できるならなんでもいいんです。データの出所だけではなくデータのフォーマットにも依存させなくて済むということです。

JSONとして以下のようなデータがあったとしましょう。

{
  name:'たけはら',
  age:15,
  isDevelopper:true
}

ここから以下のように値が取得できるDictionaryを返すことができれば、ModelBinderは値を復元できるということです。

dict["name"] = "たけはら";
dict["age"] = 15;
dict["isDeveloper"] = true;

おなじ理屈でXMLでもいいですよね。自分で取りやすいスキーマさえ定義しておけばいいので。つまり、ファイルシステムにKeyValueでアクセスできるValueProviderFactoryを作成するなら、Zipでアップロードしたファイルを解凍し、フォルダ構造とファイル名がキーになっていてファイルの実体が取得できるようなものも作成できるわけです。実際にファイルの実体をbyte[]なんかで復元するのはリソースの無駄遣いになるので、ModelBinderが復元するのはファイルのパスにしておくというのが現実的でしょう。MvcPresenterがまさにそのように処理をしています。

ちょっと長くて疲れてきたので、続きは今度にしてもいいですかね。いいですよね。中途半端でさーせん。眠いっす。

2010年2月6日土曜日

動的なRouting登録の素敵な方法

MVC V2 RC2が出てるのに関係ない話です。

Editable Routes Using App_Code

少し前にエントリされてたけど、ずっと放置してたです。そもそもEditable Routesで最初の実装サンプルが出てた話。

何をしてるのかというと、動的なRoute登録をするのにいちいちビルドし直さずに、Routes.cs ファイルをBuildManager.GetCompiledAssemblyで実行時にコンパイル。Routes.cs自体にCacheDependencyでファイルの変更監視をさせておき、csを書き換えた時点で動的にコンパイルしなおす(上記BuildManagerで)。そこから取り出される IRouteRegistrar実装がルーティングを登録するようにしておくことで、Global.asaxにルーティング情報を保持しなくて良くなるし、アプリケーションの機能とルーティング情報を分離して、柔軟な環境にできる(Subtextがそうなってるのかね)んですね。

で、 CacheDependencyでファイルを監視させるときにFileSystemWatcherを中で使ってて、それがFullTrustじゃないと実行できないから不便だねっていうのがこのエントリの本題みたい。解決方法としてはシンプルに~/App_Code下にRoutes.csをおいて、自分では監視しないようにするところ。そうしとけば、MediumTrustでも動くんだよと。なるほど。

~/App_Code配下のファイルなら更新時にAppDomainがシャットダウン(CodeDirChangeOrDirectoryRename)して再起動されるから、その ASP.NETの仕組みに任せとかばいいじゃんと。素敵だす。その場合、コンパイルは勝手にされるから、 BuildManager.GetCompiledAssemblyじゃなくてBuildManager.GetTypeでAppDomainに読み込まれてるTypeを取得(もちろんIRouteRegistrarの実装)して実行すればよろしな流れ。

2010年1月24日日曜日

ControlBuilder.ProcessGeneratedCodeを活用

Angle Bracket Percent : Take your MVC User Controls to the next level

確かにNext Level。

普通、ascxをレンダリングするときにはRenderPartialヘルパーを使うところだけど、 T4MVCとか使ってないとパスがマジックストリングになりますね。T4MVC使ってれば、そんなことにはならないけど、そもそもascx単位の RenderPartial的なヘルパーを用意してしまうほうがいいんじゃないの?という話。

で、それを実現するためにいちいちコントロール毎にヘルパーを定義するのはバカらしいってことで、動的生成ですよ。これもまさに黒魔術。

App_Codeの動的コンパイルに介入(ControlBuilder.ProcessGeneratedCodeのoverride)して、実行時動的に拡張メソッドクラスを作り出してます。CodeSnippetTypeMemberとか初めて見た。

まずは、FileLevelUserControlBuilderを派生させたUserControlHtmlHelperControlBuilderを作成。このクラスが動的生成をおこなうクラスで、ProcessGeneratedCodeをoverrideしてCodeTypeMemberCollectionにヘルパークラスのCodeSnippetTypeMemberを追加してます。動的クラスのコーディングはサンプルと言うことでシンプルな実装ですね。

            dummyClass.Members.Add(new CodeSnippetTypeMember(String.Format(@"
        }}
    }}
    public static class {2}Extensions {{
        public static void Render{2}(this System.Web.Mvc.HtmlHelper htmlHelper{3}) {{
            var uc = new {0}();
    {5}
            uc.RenderView(htmlHelper.ViewContext);
        }}

        public static string {2}(this System.Web.Mvc.HtmlHelper htmlHelper{3}) {{
            return {1}.RenderHelper(htmlHelper.ViewContext, () => Render{2}(htmlHelper{4}));
    ", codeGenUserControlFullTypeName, userControlHtmlHelperFullTypeName, helperMethodBaseName, paramBuilder, callParamBuilder, fieldAssignementBuilder)));

 

続いて、このControlBuilderを使ってくれるようにASP.NETに指示をださなきゃいけないんだけど、それってどこでやってるんでしょうね。ぱっと見、以下のクラスの属性指定?

    [FileLevelControlBuilder(typeof(UserControlHtmlHelperControlBuilder))]
    public class UserControlHtmlHelper : ViewUserControl {
        // Helper method which renders the code in a temporary writer and returns it as a string
        public static string RenderHelper(ViewContext viewContext, Action render) {
            TextWriter oldWriter = viewContext.Writer;
            var tmpWriter = new StringWriter(CultureInfo.CurrentCulture);
            viewContext.Writer = tmpWriter;
            try {
                render();
            }
            finally {
                viewContext.Writer = oldWriter;
            }

            return tmpWriter.ToString();
        }
    }

ここが実行時に勝手に解釈されるようになってるのかな~。動的ヘルパーを生成したいascxのInherits="MvcUserControlHtmlHelpers.UserControlHtmlHelper"という記述が通常と違う部分なのはわかるんだけど。

FileLevelControlBuilderAttribute コンストラクタ (System.Web.UI)

実行時に初めてコードが生成されるから、コーディング時には↓こんな感じでメソッドないぜと怒られる。

cb

実行したところで、VSはそんなコードしらないんだってさ。

cb2

そもそもMVCのViewUserControlクラスがこれと同じようにViewUserControlControlBuilderを使ってるんじゃんね。でも、BaseTypeをうわがいてるくらい(目的はよくわかってないデス)ですね。

追記:ascxなりaspxのベースクラス(ViewPage,ViewUserControl)の派生元(親)クラスを指定してるんでした。そりゃそうだ。

<%@ Control Language="C#" Inherits="MvcUserControlHtmlHelpers.UserControlHtmlHelper" ClassName="Gravatar" %>

<script runat="server">
    // Declare the paramaters that we need the caller the pass to us
    public string Email;
    public int Size;
</script>

<%
    // Build hash of the email address
    // Note: in spite of its name, this API is really just a general MD5 encoder
    string hash = FormsAuthentication.HashPasswordForStoringInConfigFile(Email.ToLower(), "MD5").ToLower();

    // Construct Gravatar URL
    string imageURL = String.Format("http://www.gravatar.com/avatar/{0}.jpg?s={1}&d=wavatar", hash, Size);
%>

<img src="<%= imageURL %>" alt="<%= Email %>" title="<%= Email %>" />

※人さまのコードをこんなにコピペしていいのだろうか...。

上記のApp_Codeに作成されてるGravator.ascxのscript blockから正規表現でpublicなプロパティ定義を取り出して、ヘルパーのパラメータ生成に利用してるところも見逃せない。

T4MVCでの動的生成、Emitによる黒魔術に次いで、新たな黒魔術として覚えておくのもいいんではないでしょうか。

※ASP.NET MVCに限らずASP.NETならすべてに適用できるのがミソですよ!

2010年1月4日月曜日

PostSharp興味深い

PostSharp - Bringing AOP to .NET

AOPフレームワークと言われるんだって。

ボスの大嫌いなRemotingのProxy(.NET Framework 2.0 コア機能解説 ~ 第 1 回 .NET リモーティング ~)でのフックや(A basic proxy for intercepting method calls (Part –1) - Mehfuz's WebLog)、Invokerを必ず呼び出すASP.NET MVCでのFilterAttribute達のどっちかが一般的なのかと思ってたけど、世の中スゴイ事思いつく人たちがいるモンで、ビルド後にILを書き換えてフックポイントを追加してしまえばいいじゃないかという発想で作られてます。まさにEmitにを超える黒魔術。

ドキュメントに書かれてる実装パターン。

int MyMethod(object arg0, int arg1)
{
  OnEntry();
  try
  {
    // Original method body. 

    OnSuccess();
    return returnValue;
  }
  catch ( Exception e )
  {
    OnException();
  }
  finally
  {
    OnExit();
  }
}

Reflectorなんかでビルド後のアセンブリを見ると上記のパターンにそったものが出力されてるのが確認出来ます。

postsharp1 

確かに~、確かに~。

ドキュメントを見つつ、どんなことが出来るのかチラッとコード書いてみたんです。

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

namespace TestPostSharp
{
  class Program
  {
    [ThirdAspect]
    private static int _intFldValue;

    static void Main(string[] args)
    {
      Console.WriteLine("Start program.");

      PrintMessage("Hello, World! - first");
      PrintMessage("Hello, World! - second");
      PrintMessage("Hello, World! - third");
      
      var res = Invocation(100,"まじか!",DateTime.Now);
      Console.WriteLine("res = {0}",res);

      _intFldValue = 100;
      Console.WriteLine("Get field {0}", _intFldValue);

      Console.WriteLine("End program.");
      Console.ReadKey();
    }

    [FirstAspect]
    static void PrintMessage(string message)
    {
      Console.WriteLine(message);
    }

    [SecondAspect]
    static int Invocation(int p1, string p2, DateTime dateTime)
    {
      Console.WriteLine("何かか実行されるようだ");
      Console.WriteLine("{0} - {1} - {2}",p1,p2,dateTime);

      return p1;
    }
  }

  [Serializable]
  public class FirstAspectAttribute:OnMethodBoundaryAspect
  {
    public override void OnEntry(MethodExecutionEventArgs eventArgs)
    {
      Console.WriteLine(eventArgs.Method.Name + " - OnEntry");
      base.OnEntry(eventArgs);
    }

    public override void OnExit(MethodExecutionEventArgs eventArgs)
    {
      Console.WriteLine(eventArgs.Method.Name + " - OnExit");
      base.OnExit(eventArgs);
    }

    public override void OnSuccess(MethodExecutionEventArgs eventArgs)
    {
      Console.WriteLine(eventArgs.Method.Name + " - OnSuccess");
      base.OnSuccess(eventArgs);
    }

    public override void OnException(MethodExecutionEventArgs eventArgs)
    {
      Console.WriteLine(eventArgs.Method.Name + " - OnException");
      base.OnException(eventArgs);
    }

    public override void RuntimeInitialize(System.Reflection.MethodBase method)
    {
      Console.WriteLine(method.Name + " - RuntimeInitialize");
      base.RuntimeInitialize(method);
    }

    public override void CompileTimeInitialize(System.Reflection.MethodBase method)
    {
      Console.WriteLine(method.Name + " - CompileTimeInitialize");
      base.CompileTimeInitialize(method);
    }

    public override bool CompileTimeValidate(System.Reflection.MethodBase method)
    {
      Console.WriteLine(method.Name + " - CompileTimeValidate");
      return base.CompileTimeValidate(method);
    }
  }

  [Serializable]
  public class SecondAspect : OnMethodInvocationAspect
  {
    public override void OnInvocation(MethodInvocationEventArgs eventArgs)
    {
      Console.WriteLine("Calling {0}", eventArgs.Method.Name);

      var args = eventArgs.GetArgumentArray();
      if (args[0].GetType() == typeof(int))
        args[0] = (int) args[0] * 2;

      eventArgs.Proceed(args);
      var res = eventArgs.ReturnValue;
      eventArgs.ReturnValue = (int) res + 100;
    }
  }

  [Serializable]
  public class ThirdAspect : OnFieldAccessAspect
  {
    public override void OnGetValue(FieldAccessEventArgs eventArgs)
    {
      Console.WriteLine("Getter {0} = {1}", eventArgs.FieldInfo.Name, eventArgs.StoredFieldValue);
      
      base.OnGetValue(eventArgs);
    }

    public override void OnSetValue(FieldAccessEventArgs eventArgs)
    {
      Console.WriteLine("Setter {0} = {1} -> {2}", eventArgs.FieldInfo.Name, eventArgs.StoredFieldValue, eventArgs.ExposedFieldValue);
      var value = eventArgs.ExposedFieldValue;
      eventArgs.ExposedFieldValue = (int) value*2;
      
      base.OnSetValue(eventArgs);
    }
  }
}

postsharp2

最初から用意されてる便利クラス。

  • OnExceptionAspect
  • OnMethodBoundaryAspect
  • OnMethodInvocationAspect
  • OnFieldAccessAspect

それぞれ、だいたい名前の通り。引数や戻り値を書き換えたりも出来るよ!IOn~のインターフェースも用意されてるから、用意されてるのが気に入らないなら最初から実装してしまうのも可能。OnExceptionAspectについてはその他のクラスのOnExceptionでも取れるから単体で使う場面はそんなに無かったりするのかな~。どうなんでしょう。

属性クラスとしてAspectクラスを実装して、クラスやメソッド、フィールドに指定してビルドするのがオーソドックスな使い方だと思うけど、更に[assembly:自作Aspectクラス(~)]でBCLにゴッソリ指定出来たりするのが恐ろしい。

めっぽう気になってしかたがないのがCompositionAspect。これってDIなんですかね?

フック出来るタイミングや、シリアライズのカスタマイズやら、プラグイン(ビルド後の処理時にだと思われるけどよく分かってない)、msbuild実行されないASP.NETの対応なんかもあって、イロイロ遊べそうな感じデス!

2010年1月2日土曜日

ASP.NET MVCに似合うSubmitの振り分け

How can I change the action a form submits to based on what button is clicked in ASP.NET MVC? - Stack Overflow

前にも、この方法について考えたことがあって、その時はストラテジパターンを使ってDelegateでのコマンド振り分けでの実装をしてたんですが、少し前に違う方法を実装してるのを見て悔い改めたんです。

MVCによく似合う方法は、属性ベースで対象となるアクションを振り分ける(判定する)方法ですよね。

AcceptVerbsでHTTP Method毎に処理を振り分ける事ができるのを上手く利用して、Submitの値毎に処理を振り分けるためにActionMethodSelectorAttributeを派生したSubmitCommandAttributeというのを定義していました。

using System;
using System.Reflection;
using System.Web.Mvc;

namespace MvcApplication2.Controllers
{
  [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
  public class SubmitCommandAttribute : ActionMethodSelectorAttribute
  {
    private string _submitName;
    private string _submitValue;
    private static readonly AcceptVerbsAttribute _innerAttribute = 
new AcceptVerbsAttribute(HttpVerbs.Post); public SubmitCommandAttribute(string name) : this(name, string.Empty) { } public SubmitCommandAttribute(string name, string value) { _submitName = name; _submitValue = value; } public override bool IsValidForRequest(ControllerContext controllerContext,
MethodInfo methodInfo) { if (!_innerAttribute.IsValidForRequest(controllerContext, methodInfo)) return false; // Form Value var submitted = controllerContext.RequestContext
.HttpContext
.Request.Form[_submitName]; return string.IsNullOrEmpty(_submitValue) ? !string.IsNullOrEmpty(submitted) : string.Equals(submitted, _submitValue,
StringComparison.InvariantCultureIgnoreCase); } } }

これだけなんですけども...。HttpPostのコードを参考に内部にAcceptVerbsAttributeを保持してPOST時のみをIsValidにしてますが、DeleteとPutも必要なら要修正。

using System.Web.Mvc;

namespace MvcApplication2.Controllers
{
    public class HomeController : Controller
    {
        //
        // GET: /Home/

        public ActionResult Index()
        {
            return View();
        }

    [ActionName("Different")]
    [SubmitCommand("DoSave")]
    public ActionResult DifferentSave()
    {
      TempData["message"] = "saved! - defferent";
      return View("Index");
    }

    [ActionName("Different")]
    [SubmitCommand("DoDelete")]
    public ActionResult DifferentDelete()
    {
      TempData["message"] = "deleted! - defferent";
      return View("Index");
    }

    [ActionName("Same")]
    [SubmitCommand("DoSubmit","保存")]
    public ActionResult SameSave()
    {
      TempData["message"] = "saved! - same";
      return View("Index");
    }

    [ActionName("Same")]
    [SubmitCommand("DoSubmit","削除")]
    public ActionResult SameDelete()
    {
      TempData["message"] = "deleted! - same";
      return View("Index");
    }
  }
}

コントローラでこんな感じにアクションを定義しておいて、Viewを以下のようにしておきます。

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Index</title> </head> <body> <h1><%= TempData["message"] ?? "Click some button" %></h1> <h2>異なるnameのsubmit</h2> <% using (Html.BeginForm("Different", "Home")) { %> <input type="submit" name="DoSave" value="保存" /><br /> <input type="submit" name="DoDelete" value="削除" /> <% } %> <h2>同一nameでValueの異なるsubmit</h2> <% using (Html.BeginForm("Same","Home")) { %> <input type="submit" name="DoSubmit" value="保存" /><br /> <input type="submit" name="DoSubmit" value="削除" /> <% } %> </body> </html>

動かしてみると、最初に↓。ボタンが4種類出てきます。上2つはnameとvalueの異なるsubmitで、下2つがnameが同じでvalueが違うsubmit。

submit1

上から順にボタンを押したのが↓。

submit2 submit3 submit4 submit5

ちゃんとTempDataの値が違うのが確認できますね(ページ上部のH1)。

で、ココまで書いときながら、前にどこで見たのかを検索して探し出してみてショック。

ASP.NET MVC – Multiple buttons in the same form - David Findley's Blog

まんま、同じになるという大失態。やっぱり年始からこんなコードを書いてて先が思いやられる...。

Expression生成のスピード?

いけてるINotifyPropertyChangedの実装は、結構遅かった - かずきのBlog@Hatena

正月から興味深いエントリですね。

遅くなるのは毎回のCompileと引数に渡してるExpression生成ですかね。Compileはキャッシュしてしまえば毎回生成する必要は無いですが、そもそも以下のようにすることでCompileそのものが不要に。

    public static void Raise2<TResult>(this PropertyChangedEventHandler _this,
      Expression<Func<TResult>> propertyName)
    {
      // ハンドラに何も登録されていない場合は何もしない
      if (_this == null) return;

      // ラムダ式のBodyを取得する。MemberExpressionじゃなかったら駄目
      var memberEx = propertyName.Body as MemberExpression;
      if (memberEx == null) throw new ArgumentException();

      // () => NameのNameの部分の左側に暗黙的に存在しているオブジェクトを取得する式をゲット
      var senderExpression = memberEx.Expression as ConstantExpression;
      // ConstraintExpressionじゃないと駄目
      if (senderExpression == null) throw new ArgumentException();

      var sender = senderExpression.Value;

      // 下準備が出来たので、イベント発行!!
      _this(sender, new PropertyChangedEventArgs(memberEx.Member.Name));
    }

こうすると、実行時間は...。

kairyo

ノーマル:5ms
イケテル:3700ms
カイリョ:84ms

※平均値じゃないですが。

これを更に高速化しようとイロイロと試してみたんですが、どうやらExpression<Func<T>>の生成に時間を取られる模様。

Raiseを呼び出さずにローカル変数として

Expression<Func<string>> expr = () => Name;

と、しただけで実行時間はほぼ同じくらいかかるし。全く素敵じゃなくなるけど、↓こんな感じにしておくとちゃんと早い。

  public class KairyoEmp : INotifyPropertyChanged
  {
    private string _name;
    private static Expression<Func<string>> _expr;
    public string Name
    {
      get { return _name; }
      set
      {
        _name = value;
        if (_expr==null)
          _expr = () => Name;
        PropertyChanged.Raise2(_expr);
      }
    }

    public event PropertyChangedEventHandler PropertyChanged;
  }

kairyo2

ノーマル:5ms
カイリョ:10ms

うむ。意味のないコードになってしまいました。年始からこれじゃ先が思いやられる...。

2009年11月29日日曜日

internalのテスト

internalクラスのテストってどう書くのがセオリー?まさかReflectionってことはないよね?今まで知らなくてズルッコしてpublicにしてたんだけど、まさかこんな簡単な方法があったとは。

Easy way to use TDD with internal classes - Sean McAlinden's Blog

常識なんすか!?

namespace Samples
{
  internal class TestInternal
  {
    public string Test()
    {
      return "TestInternal method";
    }
  }

  internal static class TestInternalStatic
  {
    public static string Test()
    {
      return "TestInternalStatic method";
    }
  }
}

普通にこんなclassを作ると、Testクラスで↓こうなるもんね。

test

でも、AssemblyInfo.csに↓この行を追加。

[assembly: InternalsVisibleTo("Samples.Tests")]

※テストプロジェクトのアセンブリ名。今回はSamples.Testsっていうのを作ったのでこの名前。

test2

コンパイルエラーも出ないし、テストも普通に通る。

なんかもう今までのコードを全部書き直したくなってきた...。

Timerでいいんすか

ちょっと試しにCacheDependencyを書いてたんだけど。

using System;
using System.Web.Caching;
using System.Timers;

namespace Sample
{ public class TimerCacheDependency:CacheDependency { private readonly Timer _timer; public TimerCacheDependency(DateTime expireTime) { _timer = new Timer((expireTime - DateTime.Now).TotalMilliseconds); _timer.Elapsed += (sender, e) => NotifyDependencyChanged(this, EventArgs.Empty); _timer.Start(); } protected override void DependencyDispose() { if (_timer == null) return; _timer.Dispose(); } } }

絶対時間でExpireさせるCacheDependency。 Timer使っていいのかな。なんかキャッシュに同時に1万とかデータ入れると1万のTimerだよね...。

Comparing the Timer Classes in the .NET Framework Class Library

特になにかリソースを消費しまくるってわけではない?

Pro ASP.NET 3.5 in C# 2008 - Google ブックス

こっちはSystem.Threading.Timer使ってるな...。ぶふ~。

2009年10月17日土曜日

ASP.NET MVC 2 Preview2でのArea

Visual Web Developer Team Blog : Single Project Add View in ASP.Net MVC 2 Preview 2

まんまな感じのエントリで申し訳ない気持ちもするんですが、Preview1から全然使い方の変わってる部分でもあるんで、エントリしとこうと思った次第です(もう役にたたない情報ですがPreview1の時のエントリはこちら)。言い訳から書き出すのもすっかり定着してきた感が否めない...。

MVC 2に関してはMSDNにドキュメント整備も進んでるんで、そちらも参照すると更に理解が進んでいいですね。

Walkthrough: Creating an ASP.NET MVC Areas Application Using a Single Project

今回からRouteCollection.MapAreaRouteは廃止され、代わりにAreaRegistrationクラスが導入されてます。同じ目的でAreaRegistrationContextも追加されてます。メインはAreaRegistrationですが、このクラスを派生させたクラス(サンプルはRoutesになってるけど名前は何でもいいです)各エリアフォルダのルートに作成しておき、AreaName/RegisterAreaをそれぞれoverrideして、Route登録時にNamespacesが自動登録されるようになる仕組みです。

特に特徴的なのは、プロジェクトを分けなくてもエリア機能が使えるようになってるところです。これまでわざわざ別プロジェクトを作成しないとエリア機能を使えなかったんだけど(規模が大きくなっても開発効率が悪くならなくても済むようにプロジェクト分割は常套手段ですよね?)、あえて分割を強制しなくなりました。

と、文章だと全然意味わからないですね。

p2area

↑こんな感じです。

MvcApplication1という名前のプロジェクトの中にAreasフォルダ(これも名前は何でもいんですが、慣例としてあえてAreas)を作成。その中にエリア分割したいフォルダを更に作成。今回ならSub1とSub2。更にその中にControllersとViewsをそれぞれ作成。仕組みをわかりやすくするためにController名はHome、Action/Viewの名前はIndexとして作成しておきます。

これで、3つのHome/Indexの組が出来たことになります。各Indexアクションは以下の通り。

標準のHome/Index

    public ActionResult Index()
    {
      ViewData["Message"] = "RootのHome/Index";

      return View(new{});
    }

Sub1のHome/Index

    public ActionResult Index()
    {
      ViewData["Message"] = "Sub1エリアのHome/Index";
        return View(new ModelsLibrary.Class1());
    }

Sub2のHome/Index

    public ActionResult Index()
    {
      ViewData["Message"] = "Sub2エリアのHome/Index";
      return View();
    }

ちょいちょいViewに渡すモデルを変えてるのは実験のためです。Sub1とSub2にそれぞれAreaRegistrationクラスを作成しておいときます。

Sub1のAreaRegistration

  public class Routes : AreaRegistration
  {
    public override string AreaName
    {
      get { return "Sub1"; }
    }

    public override void RegisterArea(AreaRegistrationContext context)
    {
      context.MapRoute(
        "sub1_Default",
        "sub1/{controller}/{action}/{id}",
        new { controller = "Home", action = "Index", id = "" }
      );
    }
  }  

Sub2のAreaRegistration

  public class Routes : AreaRegistration
  {
    public override string AreaName
    {
      get { return "Sub2"; }
    }

    public override void RegisterArea(AreaRegistrationContext context)
    {
      context.MapRoute(
        "sub2_Default",
        "sub2/{controller}/{action}/{id}",
        new { controller = "Home", action = "Index", id = "" }
      );
    }
  }

最後にGlobal.asaxのルート登録部分に以下のコードを追加。

    public static void RegisterRoutes(RouteCollection routes)
    {
      routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
      
      AreaRegistration.RegisterAllAreas();
      routes.MapRoute(
        "Default",                                              // Route name
        "{controller}/{action}/{id}",                           // URL with parameters
        new { controller = "Home", action = "Index", id = "" }, // Parameter defaults
        null,
        new[] { "MvcApplication1.Controllers" }
      );

    }

前回のエントリにも書いたように、namespacesを指定しないと各エリアのHome/Indexと区別出来なくてエラーになってしまうので、標準のHome/Indexに対してきちんと指定するようにします。

p2area2

※ちゃんとnamespacesを指定しないと↑こんな感じでエラーになるっす。

えと、眠くなってきた。あと少し!

すべてのIndex.aspxは以下で統一。

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <h2><%= Html.Encode(ViewData["Message"]) %></h2>
  <h3>Area: <%= ViewContext.RouteData.DataTokens["area"] %></h3>
  <h3>Model Type:<%= Model != null ? Model.GetType().ToString() : "(null)"%></h3>
  
  <% = Html.ActionLink("Root Home", "Index", "Home", new { area = "" }, null)%><br />
  <% = Html.ActionLink("Sub1 Home", "Index", "Home", new { area = "sub1" }, null)%><br />
  <% = Html.ActionLink("Sub2 Home", "Index", "Home", new { area = "sub2" }, null)%>

</asp:Content>

で、Sub1の時だけViewPage<ModelsLibrary.Class1>を指定してModelの型を変えておきます。なんでこうするかというと、単に以下のエラーが起きるのを確認したかったから。

p2area3

Asp.NET MVC 2 Preview 2: Area's aspx namespace problem - Stack Overflow

この現象を起こしてみたかったんデス。これを回避するには~/Areas/Sub1/Viewsフォルダに~/Viewsフォルダにあるweb.configをコピーしておく事。これを忘れるとジェネリックで他のアセンブリ(じゃ無かったとしても?試してみてね!)に含まれるモデルクラスを指定すると上記エラーが発生。ビックリですね~。pageParserFilterTypeが処理してくれないって事です。

ここまでやって実行したのが↓この画面。標準/Sub1/Sub2それぞれのHome/Indexがちゃんと判別できて実行されてるのが確認出来ます。

p2area4 p2area5 p2area6

ちなみにAreaRegistrationクラスの派生クラスの名前と、~/Areasフォルダが何でもいい理由というのが、AreaRegistration.RegisterAllAreasのソースに書かれてる内容から判断出来ます。何をしてるかというとBuildManagerWrapper.GetReferencedAssembliesでアセンブリの参照出来るすべてのTypeの中からAreaRegistrationの派生クラスを抽出(IsAreaRegistrationTypeをPredicateとして)してるからですね。後はCreateContextAndRegisterでNamespacesを追加した上でoverrideされてるRegisterAreaを呼び出して、ルートを登録するだけ。なので、AreaRegistration派生(ちなみにAreaRegistration自体はabstractなので除外されます)をすべて抽出するので名前は自動でわかるようになってるというオシャレ実装。素敵だね!

DataAnnotationsのカスタム属性実装とセットになったソースは以下からどうぞ。

眠し!

2009年10月7日水曜日

V2のFuturesにViewStateが!?

Exploring the ASP.NET MVC 2 futures assemby

小野さんに振られたので↑こちらのエントリに書かれてるViewStateについての調べてみました。こういうきっかけが無いとソースを追いかけない自分に少し反省。

まさかホントにASP.NET MVCにViewStateを持ち込むのか?と、疑いたくなるようなエントリだけど何となくサンプルとして提示されてるコードが怪しい。そのまま転載させてもらうと↓こうですよ。

<% using (Html.BeginForm()) {%>
    <%Html.Serialize("person", Model); %>
    <fieldset>
        <legend>Edit person</legend>
        <p>
            <%=Html.DisplayFor(p => Model.FirstName)%>
        </p>
        <p>
            <%=Html.DisplayFor(p => Model.LastName)%>
        </p>
        <p>
            <label for="Email">Email:</label>
            <%= Html.TextBox("Email", Model.Email) %>
            <%= Html.ValidationMessage("Email", "*") %>
        </p>
        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>
<% } %>

Html.Serialize(“person”,Model)ってなんか怪しいですよね。おまえ、ホントにViewStateを吐いてくれるのか?と。1つのViewに何個も書いたらどうなるんだよ、とか、ポストバックしたControllerでコントロールツリーを構築するのか、とかなんやかんやデス。

で、考えててもラチがあかないので、サンプル書いて試してみました。

  [Serializable]
  public class Drink
  {
    [Required]
    [StringLength(10)]
    public string Name { get; set; }

    [Compare("Name", ErrorMessage = "一致しないよ!")]
    public string CheckName { get; set; }

    [Range(10, 50)]
    public int Size { get; set; }
  }

まずは、前回も使ったDrinkクラスにSerializable属性を追加。これつけないとそもそもシリアライズ出来ないです。どこでシリアライズしてるのかFuturesのソースを追いかけると、MvcSerializerクラスで実装してます。ちなみにシリアライズの方式としてPlaintext、Encrypted、Signed、EncryptedAndSignedの4種類があり、初期値はPlaintext。これはSystem.Web.UI.ObjectStateFormatterを使ってシリアライズしてて。って、おや?マジViewStateなのか?まぁ、いいや。

なのでSerialize属性をつけるわけですが、上記クラスに値を入れてサンプル通りにView書いて実行しても何も出力されない...。

<% Html.Serialize("drink",Model); %>

サンプルだからなんかおかしいのかな~。気になるので更にソースを読み進めると、Html.SerializeはそもそもMvcHtmlStringクラスを返してきます。これは、あれですよね、ASP.NET 4で導入される<%: …%>出力に向けた実装ですよ。IHtmlStringってヤツですよ。でも、ASP.NET 3.5にはそんなもの無いので、Futuresの実装はインターフェースは無しバージョン。ってことは、単純に実行したらレンダリングされるわけじゃ無くて、ToString()かToHtmlString()で取得して、それをレンダリングするようにしないとちゃんと出力されないわけですね。

そうとわかれば、以下のように変更。

<% = Html.Serialize("drink",Model).ToHtmlString() %>

これでちゃんと出力されました。以下のようなModelを渡して結果を見てみます。

    public ActionResult Drinks()
    {
      var model = new Drink {Name = "Cola", CheckName = "Pepsi", Size = 30};
      return View(model);
    }

↑これが↓こうなります。

<input name="drink" type="hidden" value="/wEy7AEAAQAAAP////8BAAAAAAAAAAw
CAAAARk12Y0FwcGxpY2F0aW9uMSwgVmVyc2lvbj0xLjAuMC4wLCBDdWx0dXJlPW5ldXRyYWw
sIFB1YmxpY0tleVRva2VuPW51bGwFAQAAABxNdmNBcHBsaWNhdGlvbjEuTW9kZWxzLkRyaW5
rAwAAABU8TmFtZT5rX19CYWNraW5nRmllbGQaPENoZWNrTmFtZT5rX19CYWNraW5nRmllbGQ
VPFNpemU+a19fQmFja2luZ0ZpZWxkAQEACAIAAAAGAwAAAARDb2xhBgQAAAAFUGVwc2keAAA
ACw==" />

バリバリBase64エンコードされてViewStateっぽいです。でも、これを復元させるコードがどうなるかと言うと、↓こうなります。

    [HttpPost]
    public ActionResult Drinks([Deserialize]Drink drink)
    {
      if(ModelState.IsValid)
      {
        // ... success code
      }
      return View(drink);
    }

このDeserialize属性クラスが何をしてるか、ってことデスよね。またしてもソースを確認すると、そこには...。

        private sealed class DeserializingModelBinder : IModelBinder {

            private readonly SerializationMode _mode;

            public DeserializingModelBinder(SerializationMode mode) {
                _mode = mode;
            }

            public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
                if (bindingContext == null) {
                    throw new ArgumentNullException("bindingContext");
                }

                ValueProviderResult vpResult;
                bindingContext.ValueProvider.TryGetValue(bindingContext.ModelName, out vpResult);
                if (vpResult == null) {
                    // nothing found
                    return null;
                }

                MvcSerializer serializer = new MvcSerializer();
                string serializedValue = (string)vpResult.ConvertTo(typeof(string));
                return serializer.Deserialize(serializedValue, _mode);
            }

        }

単なるModelBinder...。Page.ViewStateなわけじゃないですね。単にシリアライズするためにViewStateと同じものを利用してるだけです。最初の方にも書いたけど、ViewStateと同じようにシリアライズ出来るようにするためにEncryptedやSignedが指定出来るようになってるって事です。ちなみにソースを見てみると、暗号化ViewStateを生成するためにprivate sealed class TokenPersister : PageStatePersisterっていうクラスを定義してて、その中でPageクラスのインスタンスを生成し処理させてます。なんか強引。でも、AntiForgeryDataSerializerでも同じ事してたりしてちょいビックリ。

そんなことはいいとして、これが完全に独自のModelBinderになってしまってるので、これを使う場合にはDataAnnotationsが効かない。ので、hidden書き換えを抑制したいときにはEncryptedやSigned、EncryptedAndSignedを指定しておくようにしないとね!

なぜASP.NET MVCにViewStateを持ち込むんだ~、と怒り心頭な方!ご心配なく。WebFormsでいうところのViewStateでは無かったです。ホッとした。

Futuresのソースみてて気がついたんだけど、AsyncControllerがFuturesに戻されてる。Expression系のユーティリティクラスがたんまり入ってて何やら面白そうな予感がしますが、それはまた今度ってことで。

2009年10月4日日曜日

ValidationAttributeがちょっと違う

ASP.NET MVC V2も既にPreview 2が出ましたね。入力検証がDataAnnotationsを前提にした設計(IDataErrorInfoじゃ物足りないしね)になってるのと、Templateベースのモデル描画がV2での重要ポイントですよね。

入力検証にxValと同じようにModelValidatorProviderとクライアントサイドバリデーションにjQuery.validateが導入されてのが興味深い。

そもそもV1にも追加コードとして公開されてるDataAnnotationsModelBinderがあって、System.ComponentModel.DataAnnotationsのValidationAttrbiuteを使えるようになてます。が、残念ながら標準アセンブリじゃなく別途配布されてるDLLを参照設定して使うようにしないと、全く機能しないというのがあります。何でかというとValidationAttributeクラスの実装が違うのと、その検証属性の実行方法が違うから。

.NET 3.5 SP1に含まれるVaridationAttributeクラスってIsValid(object value)なのに対し、配布されてるアセンブリではIsValid(object value)に加え、IsValid(object value, ValidationContext validationContext, out ValidationResult validationResult)があります。入力値を取得して検証するだけなら対象となる、プロパティ値があればそれで事足りるかも知れないけど、たとえばCompareValidatorは実装できないですよね。同じモデルインスタンス内の他のプロパティを見れないと比較できないじゃないですか。V1の場合は、特殊なアセンブリだからいいかもしれないけど、V2は標準アセンブリだからVaridationAttribute実装にValidationContextなんて無い(実はある?)。先日1.0がリリースされたxValが使用するアセンブリも標準アセンブリ参照だから当然インスタンス参照なんてない。

と、言うわけで、V1でxValを使うときの入力検証でモデルの他プロパティを参照する際のCompareAttributeクラスを書いて遊んでみました。何に使うのかは後で考える。

まずは、モデルインスタンスを渡せるIsValidを実装するためのインターフェイス定義。ValidationAttributeとそのインターフェースを実装するCompareAttributeクラスを定義。

  public interface IInstanceValidationAttribute
  {
    bool IsValid(object instance, object value);
  }

  public class CompareAttribute : ValidationAttribute, IInstanceValidationAttribute
  {
    public string PropertyName { get; set; }
    
    public CompareAttribute(string propertyName)
    {
      PropertyName = propertyName;
    }

    public override bool IsValid(object value)
    {
      throw new NotImplementedException();
    }

    public bool IsValid(object instance, object value)
    {
      var property = instance.GetType().GetProperty(PropertyName);
      if(property==null)
        throw new ArgumentException("パラメータ間違えてるよ");

      var targetValue = property.GetValue(instance, null);
      if (targetValue != null && targetValue.Equals(value))
        return true;

      return false;
    }
  }

CompareAttributeを使うモデルクラスの定義。他にも必須チェックやら付けてみる。

  public class Drink
  {
    [Required]
    [StringLength(10)]
    public string Name { get; set; }

    [Compare("Name",ErrorMessage = "一致しないよ!")]
    public string CheckName { get; set; }
    
    [Range(10, 50)]
    public int Size { get; set; }
  }

続いて、xValのサンプルを参考に、入力検証を実行するためのDataAnnotationsValidationRunnerクラスを実装。

  internal static class DataAnnotationsValidationRunner
  {
    public static IEnumerable<ErrorInfo> GetErrors(object instance)
    {
      var attrs = from prop in TypeDescriptor.GetProperties(instance)
.Cast<PropertyDescriptor>() from attribute in prop.Attributes.OfType<ValidationAttribute>() select new { Property = prop, Validator = attribute, IsInstanceValidator = attribute is IInstanceValidationAttribute }; foreach(var attr in attrs) { bool isvalid; if (!attr.IsInstanceValidator) isvalid = attr.Validator.IsValid(attr.Property.GetValue(instance)); else isvalid = (attr.Validator as IInstanceValidationAttribute)
.IsValid(instance, attr.Property.GetValue(instance)); if (!isvalid) yield return new ErrorInfo(attr.Property.Name,
attr.Validator.FormatErrorMessage(string.Empty),
instance); } } }

なんかちょっとダサいけど、まぁ、その辺はセンスが無いってことで勘弁してください。 最後にControllerにアクションを追加してコードは完成。

    public ActionResult Drinks()
    {
      var model = new Drink();
      return View(model);
    }

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Drinks(Drink model)
    {
      var errors = DataAnnotationsValidationRunner.GetErrors(model);
      if (errors.Any())
        new RulesException(errors).AddModelStateErrors(ModelState,"");
      
      return View(model);
    }

実行したのが↓これ。

xVal xVal2 

ここまでが、V1の話で、ここからV2 P2で同じことをしてみようと思います。こんなに違うのかと思えるほどDataAnnotationsの組み込みと、ModelValidatorProviderに感動です。CompareAttributeクラスとモデルクラス(Drinkクラス)は一切いじりません。追加で作成の必要なクラスは以下の1つのみ。

  public class CompaireValidator : DataAnnotationsModelValidator<CompareAttribute>
  {
    public CompaireValidator(ModelMetadata metadata, ControllerContext context, CompareAttribute attribute) : base(metadata, context, attribute) { }
    internal static ModelValidator Create(ModelMetadata metadata, ControllerContext context, ValidationAttribute attribute)
    {
      return new CompaireValidator(metadata, context, (CompareAttribute)attribute);
    }

    public override IEnumerable<ModelValidationResult> Validate(object container)
    {
      if (!(Attribute as IInstanceValidationAttribute).IsValid(container, Metadata.Model))
        yield return (new ModelValidationResult
                        {
                          MemberName = Metadata.PropertyName, 
                  Message = Attribute.FormatErrorMessage(Metadata.GetDisplayName())
                        });
    }
  }

すごいっすね~。たったこれだけ。で、このValidatorクラスを登録するためにGlobal.asax.csのApplication_Startに1行追加。

    protected void Application_Start()
    {
      RegisterRoutes(RouteTable.Routes);
      DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(CompareAttribute), typeof(CompaireValidator));
    }

プロパティーの取出しからAttributeの取り出しまで一切合財がMVCの処理の範疇になってます。DataAnnotations関連のProvider/Validatorを使って作ってるけど、この辺も自分でベースクラスから派生させたり、AssociatedValidatorProviderからの派生で作ることも可能。もちろん簡単なのはDataAnnotations関連のクラスを派生させて作ること。

ちなみにControllerでの使用は何も考える必要も無く以下のようにしとくだけ。

    public ActionResult Drinks()
    {
      return View(new Drink());
    }

    [HttpPost]
    public ActionResult Drinks(Drink model)
    {
      if(ModelState.IsValid)
      {
        // ... success code
      }
      return View(model);
    } 

ValidationAttributeクラスは標準クラスのままなので、モデルクラスのインスタンス参照を持ってないのは今後もそうだと思うので、今回のような方法でうまいことインスタンスを渡せるオーバーロードを定義することで、対応していくのがいいんじゃないかと思う次第です。

ASP.NET MVC Reference
Brad Wilson: Enterprise Library Validation example for ASP.NET MVC 2
Shaan's Official Blog : New features in ASP.NET MVC 2 Preview

2009年9月15日火曜日

こういうのはいいのかどうか

ASP.NET MVC Transaction Attribute (using NHibernate) - Scott's Blog

NHibernateじゃなくても、以下のようなTransactionAttributeで。

using System;
using System.Transactions;
using System.Web.Mvc;

namespace MainSite.Controllers
{
  [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
  public class TransactionAttribute : ActionFilterAttribute
  {
    private TransactionScope _transactionScope;

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
      _transactionScope = new TransactionScope();

      base.OnActionExecuting(filterContext);
    }

    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
      base.OnActionExecuted(filterContext);

      if (filterContext.Exception == null)
        _transactionScope.Complete();

      _transactionScope.Dispose();
    }
  }
}

適当なDataContextを用意してこれまた適当なAction(適当すぎてすいません)で。

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

      return View();
    }

    [HttpPost,ActionName("Index")]
    [Transaction]
    public ActionResult IndexPost(string name)
    {
      var db = new DataClasses1DataContext();
      db.Table1s.InsertOnSubmit(new Table1 {name = name});
      db.SubmitChanges();

      //throw new HttpException(500,"Error!");
      return RedirectToAction("Index");
    }

Unit of Workって言うんですかね~。Exception起こせばロールバックするんだけど...。トランザクションってこのレイヤで意識するものなのかな~。う~ん。好みや規模に合わせて好きなやり方でどうぞ、ってことで。

2009年9月6日日曜日

HandleErrorの使い方リベンジ

前回の投稿の時には全然気にしてなかった問題を最近突っ込まれたので、改めて試して見ました。

そもそもASP.NET MVCで例外発生時のハンドリングをどうするのか、というのは前回の投稿に書いてるので読んでもらえれば分かると思います。と、言うと読んでもらえない気もするので簡単に説明すると、web.configでsystem.web/customErrorsをOnにしておき、ControllerやActionにHandleError属性を指定する。そうするとException毎に出力する内容を自分で制御出来るのさ!、というものです。

ここでポイントになるのはsystem.web/customErrorsでエラーハンドリングをする部分です。と、いうのも発生したエラーを捕捉し、表示したい内容を切り替える際に、この方法だとエラーページから302 Redirectで表示ページに遷移します。でも、人ではない何か(クローラやらAjaxなんかでのRESTクライアント)に正しくエラーを伝えるにはHTTP StatusCodeに正しい値をセットしたレスポンスを返さないとダメじゃないですか。ダメだとしましょう。

前回の実装では、302 Redirectの後に返されるレスポンスで正しいHTTP StatusCodeを返してる。けど、ちょっと待てと。そもそもRedirectいらないだろと。おっしゃる通りです。まったくその通りで、リクエストに対してRedirectなんかせず、スパッとステータスを返すのが正しい実装ですよね。

どうすればそういう実装が簡単にできましょうか、が今回の主題です。で、この主題を実装して正しく動くのはIIS7以降に限定なのでそうじゃない環境の人は「ふ~ん、そっすか」くらいにしか得るものがないかも。ガンバです!

まずsystem.web/customErrorsだけだとリダイレクトされてしまうので、これを何とかしたい。IHttpModuleとかで実装するのも手としてはありかもしれないですが、そんなことしなくてもweb.configにsystem.webServer/httpErrorsというのがありまして。このセクションにエラー発生時の挙動を書きます。それだけだとcutomErrorsと何が違うんだと思われても致し方なし。決定的に違うものがありまして、それがresponseMode="ExecuteURL"という属性。これを指定するとただのHTMLレスポンスでもなく、リダイレクトでもなく、指定したURLの実行結果を返してくれるんです。まばゆいくらい素敵です。

httpErrors Element [IIS 7 Settings Schema]

詳しくはリファレンスをチェケラッなんだけど、残念なことに日本語になって無くてですね...。ここは一つ「早く日本語にしてください!」とこのエントリを読んだ人はチャックに言いましょう!IISというかインフラは担当が違うと弱音を吐くようなら「担当に伝えてください!」とチャックに言いましょう!だって担当知らないし。

MSDNのドキュメントの苦情もすべてチャックに!そんなチャックはTHE TRUTH IS OUT THEREこちら(ゴメンねチャック)。

話を進める前に前回の状況のスクリーンショットを確認しておきます。

error1 error2

左が立ち上げた時の状態。右がBad Requestのリンクをクリックした状態。クリックすると拡大するのでステータスを確認してみてください。302 Redirectの後に400 Bad Requestになってますね。ちなみに開発環境のwebDev.webServerはwebServerセクションを見てくれないので、ローカルのIISにデプロイして確認する必要があります。今回はWindows 7上のIIS7.5にデプロイしてます。

まずはsystem.webServer.httpErrorsを設定します。

    <httpErrors errorMode="Custom">
      <clear />
      <error statusCode="400" path="/ErrorHandle/Errors/400" responseMode="ExecuteURL" />
      <error statusCode="403" path="/ErrorHandle/Errors/403" responseMode="ExecuteURL" />
      <error statusCode="404" path="/ErrorHandle/Errors/404" responseMode="ExecuteURL" />
    </httpErrors>

適当な感じで申し訳ないっす。 で、web.server/customErrorsを変更します。

    <customErrors mode="On" defaultRedirect="Errors" />

前回の設定内容をガッツリ消して、上記のみにしてしまいましょう。これすら消すとHandleError属性が効かなくなってエラーが捕捉出来なくなるので残しておきます。

続いて、HandleErrorAttributeクラスを派生させたHttpHandleErrorAttributeクラスを作成します。この派生クラスでOnExceptionをoverrideして処理してしまいましょう!

using System.IO;
using System.Web;
using System.Web.Mvc;

namespace ErrorHandling.Controllers
{
  public class HttpHandleErrorAttribute : HandleErrorAttribute
  {
    public override void OnException(ExceptionContext filterContext)
    {
      var exception = filterContext.Exception as HttpException;
      if (exception == null)
      {
        base.OnException(filterContext);
        return;
      }

      var statusCode = exception.GetHttpCode();
      var errorViewName = string.Format("~/Views/Errors/Error{0}.aspx", statusCode);
      if (!File.Exists( filterContext.HttpContext.Server.MapPath(errorViewName)))
        errorViewName = "~/Views/Shared/Error.aspx";

      filterContext.Result = new StatusViewResult(new ViewResult { ViewName = errorViewName }) { StatusCode = statusCode };
      filterContext.ExceptionHandled = true;
    }
  }
}

普通のExceptionは無視して、HttpExceptionだけ捕捉します。後は、ControllerにHttpHanderErrorを追加するだけ。

using System;
using System.Web;
using System.Web.Mvc;

namespace ErrorHandling.Controllers
{
  [HandleError(Order = 1)]
  [HttpHandleError]
  public class HomeController : Controller
  {
    public ActionResult Index()
    {
      ViewData["Message"] = "Welcome to ASP.NET MVC!";

      return View();
    }
// 以下省略

太字のところです。 ちなみに前回のサンプルソースに付け足してます。

実行すると↓こうなります。

error3

ちゃんと1度目のレスポンスで400がが返ってますね!

error4 error5

上の2つは左がThrow Exceptionリンクをクリックしたもので、これはcustomErrorsが効いてる結果です。右がMethod not arrowedをクリックした結果で、HttpHandleErrorでViewの定義が無かった(405を用意してないです)時のものです。

エラー処理もこれでスッキリ!

2009年8月9日日曜日

ASP.NET MVC 2 Preview1でのArea

ふぅ~。出遅れ。かなり出遅れでしょんぼり。

ASP.NET MVC V2 Preview 1 Released - ScottGu's Blog
日本語はもちろんこちら→ASP.NET MVC V2プレビュー1がリリース - @IT

少しずつ順番に見ていこうと思います。順番なのでまずはArea機能。もともとPhilさんがこの機能をV1の時から紹介(Grouping Controllers with ASP.NET MVC)してて、V1のPreviewのいつだか忘れたんですがRouting登録時にControllerのnamespaceを指定出来るようになってるんです。それを使って違うnamespaceのControllerを指定することを"Area"と呼んでるという感じです。RouteControllerへのArea登録用拡張メソッドの作成でV1は実現して他のをV2では最初から用意しといたら便利かもね~、な流れ(特に開発規模が大きくなって分業始める事を考えると)でしょうか。

とりあえず、使ってみましょう。と、いっても上記記事を見てもやり方は書いてないのでサッパリ分からないですよね。でも今回からはMSDNにすでにドキュメントが用意されてます。

Walkthrough: Organizing an ASP.NET MVC Application by Logical Areas

MSDNマガジンを機械翻訳にするなら、その分のリソースをこっちにさいて日本語化しといてくれよ、なんてぼやきは無しでいきましょう。ぶつぶつ。

まず、通常通りにMVC 2のプロジェクトを作成。”MainSite”という名前で作ってみました。

area1

そうそう、V1とV2はそれぞれ参照するアセンブリが違うので、一緒にインストールしててもなんの問題もないので、ガンガン入れて試すといいと思います。

area2

プロジェクトテンプレートももちろん最初から日本語版のVSで動きますね。こなれてきてます。

ドキュメントに書かれてる手順でサブプロジェクトを作っていきましょう。ソリューションに新しく「ソリューションフォルダ」を追加して、その中に"Sub1"と"Sub2"という名前でMVC2のプロジェクトを作成。でもって、不要なファイル達をゴッソリ削除。とにかくControllersフォルダとルートのweb.configだけが残ってればよろしデス。

area3

↑こんなようになってればOK。

で、ルーティング登録用のファイルを各サブプロジェクトに作っておけと、ドキュメントに書かれてるのでそれに従います。ん?それだと面白味に欠ける?分かりました。んじゃ、MainSiteのルーティングにまとめて書くやり方でやってみましょう!(本来はきちんと分けた方が機能分離出来て分担しやすいはずです)

MainSiteのGlobal.asaxのルート登録を以下のように書き換えます。

    public static void RegisterRoutes(RouteCollection routes)
    {
      routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

      routes.MapAreaRoute(
        "AreasSub1",
        "Sub1Default",
        "s1/{controller}/{action}/{id}",
        new {controller = "Home", action = "Index", id = ""},
        new[] {"Sub1.Controllers"}
      );

      routes.MapAreaRoute(
        "AreasSub2",
        "Sub2Default",
        "s2/{controller}/{action}/{id}",
        new { controller = "Home", action = "Index", id = "" },
        new[] { "Sub2.Controllers" }
      );

      routes.MapAreaRoute(
        "MainSite",
        "Default",
        "{controller}/{action}/{id}",
        new { controller = "Home", action = "Index", id = "" },
        new[] { "MainSite.Controllers" }
      );

      //routes.MapRoute(
      //    "Default",                                              // Route name
      //    "{controller}/{action}/{id}",                           // URL with parameters
      //    new { controller = "Home", action = "Index", id = "" }  // Parameter defaults
      //);

    }

デフォルトで登録されてるMapRouteがコメントアウトになって、MapAreaRouteで登録し直してるのには理由がありまして、Areas内のHome/Indexを表示したりしてるときに、MainSiteへのリンクをActionLinkで出力する際にエリア名を指定しておく必要があるんですね。その時、デフォルトの登録だとエリア名がないので、ちゃんとリンクを出力できなくなります。気をつけて!

MainSiteのHome/IndexにSub1とSub2へのリンクを書きます。

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ID="indexTitle" ContentPlaceHolderID="TitleContent" runat="server">
    Home Page
</asp:Content>

<asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">

  <% = Html.ActionLink("MainSite Home","Index","Home") %><br />
  <% = Html.ActionLink("Sub1 Home", "Index", "Home", new { area = "AreasSub1" }, null)%><br />
  <% = Html.ActionLink("Sub2 Home", "Index", "Home", new { area = "AreasSub2" }, null)%><br />

</asp:Content>

これで一度動かしてみます。

area4

ぬほ!ちゃんと動く。リンクをクリックしてもエラーでない。でも、まだ、Sub1/Sub2用のIndex Viewを作成してないのですべてのリンクで全く同じViewが出てきます。とりあえず、この状態でレンダリングされるHTMLは↓こんな感じで予想通り。

  <a href="/">MainSite Home</a><br />
  <a href="/s1">Sub1 Home</a><br />
  <a href="/s2">Sub2 Home</a><br />

そりゃそうっすね。特になんの説明もなくここまで進みましたが、少し捕捉。MapAreaRouteの台1引数のエリア名とActionLinkなんかで指定するRouteValueDictionaryのarea指定が一致しないとちゃんと解決出来ないので気をつけましょう。そこだけです。上記の例で言えばAreasSub1/AreasSub2がそれぞれエリア名に当たるんで、同じものをActionLinkのareaに指定しなきゃダメです。あと、まだPreviewだからだと思いますが、今のところHtmlAttributeの指定がなくてもActionLinkの引数にはnull指定で全部していするようにしないと、これまたリンクが出力されません。overload実装が部分的。

続いて、Sub1/Sub2用のViewを定義します。これがデスね、勘でやってると気がつかない(Sub1/Sub2のプロジェクトにおきたくなるよね?)んですが、MainSiteに置いておく必要があります。いや、必須じゃないけどWebFormViewEngineを書き換えれば他のプロジェクトに置いておいてもいいけど、デフォルトはメインのプロジェクトのViewsフォルダを探すようになってるので、その辺気をつけましょう。

なので、MainSite/Views/Areas/AreasSub1/Home、MainSite/Views/Areas/AreasSub2/HomeにそれぞれのIndex.aspxをおきます。ここでもAreasSub1/AreasSub2とルーティングで指定したエリア名を使う事になってます。

area5

↑こんな感じで。それぞれのIndexが区別出来るように、AreasSub1の内容は

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <h2>AreasSub1のHome/Index</h2>

</asp:Content>

AreasSub2の内容は

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <h2>AreasSub2のHome/Index</h2>

</asp:Content>

と、しておきます。ドキュメントには以下のようなサンプルが載ってるので、これで試して見るとルーティングのデータが見れるので分かりやすいかも。

<div id="main">
    <asp:ContentPlaceHolder ID="MainContent" runat="server" />
        <p>
            Controller: <%= ViewContext.RouteData.Values["controller"] %><br />
            Action: <%= ViewContext.RouteData.Values["action"] %><br />
            Area: <%= ViewContext.RouteData.DataTokens["area"] %>
        </p>
    <div id="footer">
    </div>
</div>

areaの情報(どこのnamespaceを参照するのか)が、DataTokensに入ってControllerの解決時に参照されるのが分かりますね。ソースをテケテケ追いかけてみるとですね、MapAreaRouteがMapRouteを呼び出した後に、Defaults/Constraints/DataTokensそれぞれにareaを設定してる。内部ではこれまで通り"Namespaces"で参照するようになってるので、DefaultControllerFactoryでControllerTypeを取得する部分のコードには変化無しです。

area7 area6

実際には、これだけだと、Home/Aboutそれぞれの参照先がs1/s2のArea内に入ってしまって、MainSiteを参照しなくなってしまうので、そこら辺のActionLinkにもすべてareaを指定するように変更する必要があります(デフォルトのMapRouteを消した理由はココに絡んできます)。

ちなみに、Philさんのエントリで「Single Project Areas With ASP.NET MVC 2 Preview 1」というのがありまして、同じプロジェクト内でのAreaの作り方を説明してくれてます。こっちのほうがViewsも分離(WebFormViewEngineのパスを変更)出来るし、最初からこの方がいいんじゃないかと思わずにはいられないところです。

とにかくMVCはV2になっても目が離せないですね。

最後に気になる問題がASP.NET Forumsにあがってたので紹介。

MVC2 InvalidOperationException - ASP.NET Forums

ViewへViewDataを型付きで指定して渡すとき、インターフェースを指定して渡す事も出来るんですが、その時に渡されたインスタンスを使ってEditorForで出力しようとすると型が分からないといってInvalidOperationExceptionが発生するという話。解決策としてはダサイけどEditorForに"Object"と固定でテンプレート名を渡すことで、デフォルトテンプレートを参照してくれるようになるんだそうです。リリースノートのKnown Issuesにちゃんと書かれてるけど、解決方法は書かれてないよね。

追記です。今回みたいに複数のMVCプロジェクトをソリューションに追加して実行した場合、アプリケーションのインスタンスがプロジェクト毎に起動して(今回なら3つ)無駄使い感が漂います。なので、以下のようにデバッグ時に起動しないようするとエコなんじゃないかなと思うところです。

area8

2009年7月4日土曜日

男らしさってなんですか?

前回のエントリーに引き続きT4MVCです。

前回判明したのが、ActionName属性を指定したアクションはオリジナルのアクション名でしかMVCクラスに展開されないというもの。で、改善リリースを待つか、自分でttファイルをいじるのか、どっちが男らしいかというところで、とりあえず逃げの一手を打ったんでしたね。

このままじゃべーさんにどやされる。週明けに「この腰抜けが!」なんて言われた日には、枕を涙でぬらす日々。涙じゃない、悲しみのシミだよ、なんて言ったところで「誠意ってなにかね? by 文太」とたたみかけられる。

まぁ、いいや。

早速、面白そうなものを見つけたんだから、T4追いかけてみるかと、VSでソースを開くと真っ白背景に白の文字。あ、コードハイライト効かないんだった。

そんなときには"Clarius Visual T4"。Professionalエディションを$99.99払ってまでは使い込まないだろうから、Communityエディションで。無料だし。Code Generatorエディション出たら買ってもいいかもね。いや、買わないか。で、インストールして早速T4MVC.ttファイルを開いてみると...。

t4mvc1

ぽかーんデス。なんじゃこれ。テーマ設定してるとグチャグチャっすね。

Clarius Forums • View topic - t4Editor and custom fonts and colors

フォーラム見てみたら、白バックのデフォルトテーマに戻せと書いてるけど、日本語版のVSだからか、「ツール>オプション>環境>フォントおよび色>テキストエディタ・表示項目」にそんな選択肢がそもそも出てこなかったり。ガッカリだな。無料だし、それもやむなし。そういえば、CommunityエディションだとIntelliSenceも使えないんだった。インストールする意味がまるでなかった。

で、結局↓こんな素っ気ない画面で。

t4mvc2

そろそろ、中身を確認せねば。T4の使い方は前にどこかで読んだことがあったから、細かい仕様的なのは無視。とにかく、<# #>で囲まれてる部分がテンプレート処理部。で、テンプレート内で参照出来る変数を下の方で宣言してて、その辺にいろいろコードがあるはずなので、そこら辺中心にチェックで。

一応ASP.NET MVCに関する投稿デス。

たぶんアセンブリ内をリフレクションでグルグル処理してるんだろーなと、たかをくくってたらこれが全然違うのね。ビックリした。CodeFunction2って誰ちゃん?CodeElementインターフェースってどこからはえてきたの?みたいな。どうやらVisual Studioオートメーションってことらしい。クラスの定義をpartialにしたり、関数定義をvirtualにしたり、ソースをいじるのにそうしとかないと面倒なことになるからなんだろうね。そんな物の存在を全然知らなくてたまげたけど、使う部分はちょびっとだけなんだから気にせず読み進める。

Controllers変数の中にControllerInfoのコレクションが入ってて、ControllerInfo.ActionMethodsの中にActionMethodInfoのコレクションが入ってるんだって。それらコレクション達を見ながらコード部を生成させる仕組み。今回はActionNameAttributeが宣言されてれば、アクション名をそっちに切り替えるってことをしたいので、ActionMethodInfoあたりを中心にチェック。ふと思ったけど、オリジナルのアクション名が消えるとちょっと分かりにくいかも?オリジナル+ActionName指定の両方を展開しといたほうが、優しさチラリな気がしたので、そういう事にします。

CodeFunctionのNameを書き換えるのは無理なんだろうから、ActionNameが指定されてるならそっちの名前を返すように内部のクラスを書き換える。

// Data structure to collect data about a method
class FunctionInfo: BaseFunctionInfo {
public string _actionName = null; public FunctionInfo(CodeFunction2 method) : base(method) { } public FunctionInfo(CodeFunction2 method, string actionName) : base(method) { _actionName = actionName; } public string Name { get { return _actionName ?? _method.Name; } } public string ReturnType { get { return _method.Type.CodeType.Name; } } public bool IsPublic { get { return _method.Access == vsCMAccess.vsCMAccessPublic; } } }

太字の部分が追加したコードです。さらに、このクラスを派生させてる部分も同じように変える。

// Data structure to collect data about an action method
class ActionMethodInfo: FunctionInfo {
    private bool _isOverride = true;
	
    public ActionMethodInfo(CodeFunction2 method, ControllerInfo controller): base(method) {
        Controller = controller;
    }

    public ActionMethodInfo(CodeFunction2 method, ControllerInfo controller, string actionName): base(method,actionName) {
        Controller = controller;
        _isOverride = false;
    }
    
    public bool IsOverride {get {return _isOverride;} }
    public ControllerInfo Controller { get; private set; }

    public string GeneratedName {
        get {
            // If the action name would cause a class/method conflict, append an underscore to it
            if (Controller.Name == Name)
                return Name + "_";
            return Name;
        }
    }
}

これも太字。IsOverrideって言うのが追加されてるのは、Controllerの書き換えをされる時に、アクション関数はすべてvirtualになって、そのControllerを内部で派生させて、すべてのアクションをoverrideしてるから。でも、今回追加するActionNameで指定したアクション関数はそもそもそんな名前でControllerには存在しないから、派生クラスでしか不要だし、overrideするものじゃ無いじゃないですか。なので、ここでそのフラグを保持しておいて、テンプレート処理部でこれを見て、overrideを付加するかどうか判定させるって感じです。

続いて、ActionMethodsコレクションを構築しているProcessControllerActionMethodsを書き換える。

void ProcessControllerActionMethods(ControllerInfo controllerInfo, CodeClass2 type) {
    foreach (CodeFunction2 method in GetMethods(type)) {
        ~ 長いのでココはカット ~
        // Collect misc info about the action method and add it to the collection
        controllerInfo.ActionMethods.Add(new ActionMethodInfo(method, controllerInfo));

        // ActionNameAttributeを見てそっちも追加する。
        // オリジナルが不要なら↑のAddを削除。
        foreach(CodeAttribute2 attr in method.Attributes)
        {
          if(attr.Name == "ActionName")
          {
            foreach(CodeAttributeArgument arg in attr.Arguments)
            {
              var actionName = arg.Value.Replace("\"","");
              controllerInfo.ActionMethods.Add(new ActionMethodInfo(method, controllerInfo, actionName));
            }
          }
        }
    }
}

太字部です。なんか、リフレクションじゃないから属性とか属性パラメータの取り方がちょっと特殊。MSDN最高。基本すべてソースコードを解析した結果構築されるオブジェクト達だから中身は単なる文字列。arg.Valueで取り出したActionNameに指定してるName値がソース上で定義されてるのと同じようにダブルクォーテーション付きになってるので、そこを削除。

最後にテンプレート部で処理してる部分を書き換え。

public static class MVC {
<#  foreach (var controller in Controllers) { #>
    public static <#= controller.DerivedClassName #> <#= controller.Name #> = new <#= controller.DerivedClassName #>();
<#  } #>
}

まずはMVCクラスの部分でオリジナルControllerクラスを入れ物にしてたから、テンプレートで生成した派生クラスになるように変更。こうしないとActionName名のアクション関数が無いから困るんです。

namespace <#= T4MVCNamespace #> {
<#  foreach (var controller in Controllers.Where(c => !c.SharedViewFolder)) { #>
    [CompilerGenerated]
    public class <#= controller.DerivedClassName #>: <#= controller.FullClassName #> {
        public <#= controller.DerivedClassName #>() : base(_Dummy.Instance) { }

<#      foreach (var method in controller.ActionMethods) { 
        var overrideKeyword = method.IsOverride ? "override " : "";
#>
        public <#= overrideKeyword #><#= method.ReturnType #> <#= method.Name #>(<# method.WriteFormalParameters(true); #>) {
            var callInfo = new T4MVC_<#= method.ReturnType #>("<#= controller.Name #>", "<#= method.Name #>");
<#          if (method.Parameters.Count > 0) { #>
<#              foreach (var p in method.Parameters) { #>
            callInfo.RouteValues.Add("<#= p.Name #>", <#= p.Name #>);
<#              } #>
<#          }#>
            return callInfo;
        }

<#      } #>
    }
<#  } #>

    [CompilerGenerated]
    public class _Dummy {
        private _Dummy() { }
        public static _Dummy Instance = new _Dummy();
    }
}

上記太字の部分で最後です。無駄にソースを引用してるんで、長いですけど、実質10行ほどじゃないっすかね。思ったより、変更箇所も少なくて助かりました。Visual Studioオートメーション&T4の組み合わせ恐るべし。

そうすると、前回の投稿ではIntelliSenceに出てこなかったActionName指定の部分も出てくるようになりました。

t4mvc4

やったね!

これまた興味ある方はダウンロードどうぞ(T4MVC-T.ttに名前変えてます)。

dotnetConf2015 Japan

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