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)
実行時に初めてコードが生成されるから、コーディング時には↓こんな感じでメソッドないぜと怒られる。
実行したところで、VSはそんなコードしらないんだってさ。
そもそも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ならすべてに適用できるのがミソですよ!