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月23日土曜日

ハーフゾーン+ハーフマンツー

ホッケーを愛する日本全国の人々へ:COVERAGE(カバリッジ)~最終ステップ「5 on 5」 - livedoor Blog(ブログ)

必読。

一度で理解できないとあきらめず、理解できるまで何度でも読もう。アイス・インラインの区別、人数の差異は関係ない内容。

難しい・わからない・理解できない。それなら、理解できるまで考えればいいだけだから。

あぁ~、プレーオフもないし暇すぎる。来期な~。いいんだけど。いい年こいて精神論でホッケーするのはバカらしいと思う今日この頃。

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

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