前回のエントリーに引き続きT4MVCです。
前回判明したのが、ActionName属性を指定したアクションはオリジナルのアクション名でしかMVCクラスに展開されないというもの。で、改善リリースを待つか、自分でttファイルをいじるのか、どっちが男らしいかというところで、とりあえず逃げの一手を打ったんでしたね。
このままじゃべーさんにどやされる。週明けに「この腰抜けが!」なんて言われた日には、枕を涙でぬらす日々。涙じゃない、悲しみのシミだよ、なんて言ったところで「誠意ってなにかね? by 文太」とたたみかけられる。
まぁ、いいや。
早速、面白そうなものを見つけたんだから、T4追いかけてみるかと、VSでソースを開くと真っ白背景に白の文字。あ、コードハイライト効かないんだった。
そんなときには"Clarius Visual T4"。Professionalエディションを$99.99払ってまでは使い込まないだろうから、Communityエディションで。無料だし。Code Generatorエディション出たら買ってもいいかもね。いや、買わないか。で、インストールして早速T4MVC.ttファイルを開いてみると...。
ぽかーんデス。なんじゃこれ。テーマ設定してるとグチャグチャっすね。
Clarius Forums • View topic - t4Editor and custom fonts and colors
フォーラム見てみたら、白バックのデフォルトテーマに戻せと書いてるけど、日本語版のVSだからか、「ツール>オプション>環境>フォントおよび色>テキストエディタ・表示項目」にそんな選択肢がそもそも出てこなかったり。ガッカリだな。無料だし、それもやむなし。そういえば、CommunityエディションだとIntelliSenceも使えないんだった。インストールする意味がまるでなかった。
で、結局↓こんな素っ気ない画面で。
そろそろ、中身を確認せねば。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指定の部分も出てくるようになりました。
やったね!
これまた興味ある方はダウンロードどうぞ(T4MVC-T.ttに名前変えてます)。