<# /* T4MVC Version 3.7.4 Find latest version and documentation at http://mvccontrib.codeplex.com/wikipage?title=T4MVC Discuss on StackOverflow or on Codeplex (https://t4mvc.codeplex.com/discussions) T4MVC is part of the MvcContrib project, but in a different Codeplex location (http://t4mvc.codeplex.com) Maintained by David Ebbo, with much feedback from the MVC community (thanks all!) david.ebbo@microsoft.com http://twitter.com/davidebbo http://blog.davidebbo.com/ (previously: http://blogs.msdn.com/davidebb) Related blog posts: http://blogs.msdn.com/davidebb/archive/tags/T4MVC/default.aspx Please use in accordance to the MvcContrib license (http://mvccontrib.codeplex.com/license) */ #> <#@ template language="C#" debug="true" hostspecific="true" #> <#@ assembly name="System.Core" #> <#@ assembly name="Microsoft.VisualStudio.Shell.Interop" #> <#@ assembly name="EnvDTE" #> <#@ assembly name="EnvDTE80" #> <#@ assembly name="VSLangProj" #> <#@ assembly name="System.Xml" #> <#@ assembly name="System.Xml.Linq" #> <#@ import namespace="System.Collections.Generic" #> <#@ import namespace="System.IO" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Text" #> <#@ import namespace="System.Text.RegularExpressions" #> <#@ import namespace="Microsoft.VisualStudio.Shell.Interop" #> <#@ import namespace="EnvDTE" #> <#@ import namespace="EnvDTE80" #> <#@ import namespace="Microsoft.VisualStudio.TextTemplating" #> <# // To debug, uncomment the next two lines !! // System.Diagnostics.Debugger.Launch(); // System.Diagnostics.Debugger.Break(); #> <#settings=MvcSettings.Load(Host);#> <#PrepareDataToRender(this); #> <#var manager = Manager.Create(Host, GenerationEnvironment); #> <#manager.StartHeader(); #>// // This file was generated by a T4 template. // Don't change it directly as your change would get overwritten. Instead, make changes // to the .tt file (i.e. the T4 template) and save it to regenerate this file. // Make sure the compiler doesn't complain about missing Xml comments #pragma warning disable 1591 #region T4MVC using System; using System.Diagnostics; using System.CodeDom.Compiler; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using System.Web; using System.Web.Hosting; using System.Web.Mvc; using System.Web.Mvc.Ajax; using System.Web.Mvc.Html; using System.Web.Routing; using <#=settings.T4MVCNamespace #>; <#foreach (var referencedNamespace in settings.ReferencedNamespaces) { #> using <#=referencedNamespace #>; <#} #> <#manager.EndBlock(); #> [<#= GeneratedCode #>, DebuggerNonUserCode] public static class <#=settings.HelpersPrefix #> { <#if (settings.IncludeAreasToken) { #> public static class Areas { <#} #> <#foreach (var area in Areas.Where(a => !string.IsNullOrEmpty(a.Name))) { #> static readonly <#=area.Name #>Class s_<#=area.Name #> = new <#=area.Name #>Class(); public static <#=area.Name #>Class <#=EscapeID(area.Namespace) #> { get { return s_<#=area.Name #>; } } <#} #> <#if (settings.IncludeAreasToken) { #> } <#} #> <#foreach (var controller in DefaultArea.GetControllers()) { #> public static <#=controller.FullClassName #> <#=controller.Name #> = new <#=controller.FullDerivedClassName #>(); <#} #> } namespace <#=settings.T4MVCNamespace #> { <#foreach (var area in Areas.Where(a => !string.IsNullOrEmpty(a.Name))) { #> [<#= GeneratedCode #>, DebuggerNonUserCode] public class <#=area.Name #>Class { public readonly string Name = "<#=ProcessAreaOrControllerName(area.Name) #>"; <#foreach (var controller in area.GetControllers()) { #> public <#=controller.FullClassName #> <#=controller.Name #> = new <#=controller.FullDerivedClassName #>(); <#} #> } <#} #> } namespace <#=settings.T4MVCNamespace #> { [<#= GeneratedCode #>, DebuggerNonUserCode] public class Dummy { private Dummy() { } public static Dummy Instance = new Dummy(); } } <#foreach (var resultType in ResultTypes.Values) { #> [<#= GeneratedCode #>, DebuggerNonUserCode] internal partial class T4MVC_<#=resultType.UniqueName #> : <#=resultType.FullName #>, IT4MVCActionResult { public T4MVC_<#=resultType.UniqueName #>(string area, string controller, string action, string protocol = null): base(<#resultType.Constructor.WriteNonEmptyParameterValues(true); #>) { this.InitMVCT4Result(area, controller, action, protocol); } <#foreach (var method in resultType.AbstractMethods) { #> <#=method.IsPublic ? "public" : "protected" #> override <#=method.ReturnType#> <#=method.Name #>(<#method.WriteFormalParameters(true); #>) {<# if(method.ReturnType != "void") {#> return default(<#=method.ReturnType#>); <#} #> } <#} #> public string Controller { get; set; } public string Action { get; set; } public string Protocol { get; set; } public RouteValueDictionary RouteValueDictionary { get; set; } } <#} #> namespace <#=settings.LinksNamespace #> { <# foreach (string folder in settings.StaticFilesFolders.Concat(GetStaticFilesViewFolders())) { ProcessStaticFiles(Project, folder); } #> [<#= GeneratedCode #>, DebuggerNonUserCode] public static partial class Bundles { [<#= GeneratedCode #>, DebuggerNonUserCode] public static partial class Scripts {} [<#= GeneratedCode #>, DebuggerNonUserCode] public static partial class Styles {} } } <# RenderAdditionalCode(); #> <#foreach (var controller in GetAbstractControllers().Where(c => !c.HasDefaultConstructor)) { #> <#manager.StartNewFile(controller.GeneratedFileName); #> namespace <#=controller.Namespace #> { [<#= GeneratedCode #>, DebuggerNonUserCode] public partial class <#=controller.ClassName #> { protected <#=controller.ClassName #>() { } } } <#manager.EndBlock(); #> <#} #> <#foreach (var controller in GetControllers()) { #> <# // Don't generate the file at all if the existing one is up to date // NOTE: disable this optimization since it doesn't catch view changes! It can be re-enabled later if smarter change detection is added //if (controller.GeneratedCodeIsUpToDate) { // manager.KeepGeneratedFile(controller.GeneratedFileName); // continue; //} #> <#manager.StartNewFile(controller.GeneratedFileName); #> <#if (!String.IsNullOrEmpty(controller.Namespace)) { #> namespace <#=controller.Namespace #> { <#} #> public <#if (!controller.NotRealController) { #>partial <#} #>class <#=controller.ClassName #> { <#if (!controller.NotRealController) { #> <#if (!controller.HasExplicitConstructor) { #> [<#= GeneratedCode #>, DebuggerNonUserCode] public <#=controller.ClassName #>() { } <#} #> [<#= GeneratedCode #>, DebuggerNonUserCode] protected <#=controller.ClassName #>(Dummy d) { } [<#= GeneratedCode #>, DebuggerNonUserCode] protected RedirectToRouteResult RedirectToAction(ActionResult result) { var callInfo = result.GetT4MVCResult(); return RedirectToRoute(callInfo.RouteValueDictionary); } [<#= GeneratedCode #>, DebuggerNonUserCode] protected RedirectToRouteResult RedirectToActionPermanent(ActionResult result) { var callInfo = result.GetT4MVCResult(); return RedirectToRoutePermanent(callInfo.RouteValueDictionary); } <#foreach (var method in controller.ActionMethodsUniqueWithoutParameterlessOverload) { #> [NonAction] [<#= GeneratedCode #>, DebuggerNonUserCode] public virtual <#=method.ReturnTypeFullName #> <#=method.Name #>() { <#if (method.ReturnTypeFullName == "System.Threading.Tasks.Task") { #> var callInfo = new T4MVC_<#=method.ReturnTypeUniqueName #>(Area, Name, ActionNames.<#=method.ActionName #><# if (method.ActionUrlHttps) {#>, "https"<#}#>); return System.Threading.Tasks.Task.FromResult(callInfo as ActionResult); <#} else { #> return new T4MVC_<#=method.ReturnTypeUniqueName #>(Area, Name, ActionNames.<#=method.ActionName #><# if (method.ActionUrlHttps) {#>, "https"<#}#>); <#} #> } <#} #> <#foreach (var method in controller.CustomActionMethodsUniqueWithoutParameterlessOverload) { #> [NonAction] [<#= GeneratedCode #>, DebuggerNonUserCode] public virtual <#=method.ReturnTypeFullName #> <#=method.ActionName #>() { return new T4MVC_<#=method.ReturnTypeUniqueName #>(Area, Name, ActionNames.<#=method.ActionName #><# if (method.ActionUrlHttps) {#>, "https"<#}#>); } <#} #> <#foreach (var method in controller.CustomActionMethods) { #> [NonAction] [<#= GeneratedCode #>, DebuggerNonUserCode] public virtual <#=method.ReturnTypeFullName #> <#=method.ActionName #>(<#method.WriteFormalParameters(true, true); #>) { var callInfo = new T4MVC_<#=method.ReturnTypeUniqueName #>(Area, Name, ActionNames.<#=method.ActionName #><# if (method.ActionUrlHttps) {#>, "https"<#}#>); <#if (method.Parameters.Count > 0) { #> <#foreach (var p in method.Parameters) { #> ModelUnbinderHelpers.AddRouteValues(callInfo.RouteValueDictionary, <#=p.RouteNameExpression #>, <#=p.Name #>); <#} #> <#}#> <#if (method.ReturnTypeFullName == "System.Threading.Tasks.Task") { #> return System.Threading.Tasks.Task.FromResult(callInfo as ActionResult); <#} else { #> return callInfo; <#} #> } <#} #> [<#= GeneratedCode #>, DebuggerNonUserCode] public <#=controller.ClassName #> Actions { get { return <#=controller.T4MVCControllerFullName #>; } } [<#= GeneratedCode #>] public readonly string Area = "<#=ProcessAreaOrControllerName(controller.AreaName) #>"; [<#= GeneratedCode #>] public readonly string Name = "<#=ProcessAreaOrControllerName(controller.Name) #>"; [<#= GeneratedCode #>] public const string NameConst = "<#=ProcessAreaOrControllerName(controller.Name) #>"; static readonly ActionNamesClass s_actions = new ActionNamesClass(); [<#= GeneratedCode #>, DebuggerNonUserCode] public ActionNamesClass ActionNames { get { return s_actions; } } [<#= GeneratedCode #>, DebuggerNonUserCode] public class ActionNamesClass { <#foreach (var method in controller.ActionMethodsWithUniqueNames) { #> <# if (settings.UseLowercaseRoutes) { #> public readonly string <#=method.ActionName #> = (<#=method.ActionNameValueExpression #>).ToLowerInvariant(); <# } else { #> public readonly string <#=method.ActionName #> = <#=method.ActionNameValueExpression #>; <# } } #> } <# // Issue: we can't honor UseLowercaseRoutes here because ToLowerInvariant() is not valid in constants! if (!settings.UseLowercaseRoutes) { #> [<#= GeneratedCode #>, DebuggerNonUserCode] public class ActionNameConstants { <#foreach (var method in controller.ActionMethodsWithUniqueNames) { #> public const string <#=method.ActionName #> = <#=method.ActionNameValueExpression #>; <# } #> } <#} } #> <#if (settings.GenerateParamsForActionMethods && !settings.GenerateParamsAsConstantsForActionMethods){ foreach (var group in controller.UniqueParameterNamesGroupedByActionName) if (group.Any()) { #> static readonly ActionParamsClass_<#=group.Key #> s_params_<#=group.Key #> = new ActionParamsClass_<#=group.Key #>(); [<#= GeneratedCode #>, DebuggerNonUserCode] public ActionParamsClass_<#=group.Key #> <#=group.Key + settings.ParamsPropertySuffix #> { get { return s_params_<#=group.Key #>; } } [<#= GeneratedCode #>, DebuggerNonUserCode] public class ActionParamsClass_<#=group.Key #> { <#foreach (var param in group) { #> <# if (settings.UseLowercaseRoutes) { #> public readonly string <#=param.Name #> = (<#=param.RouteNameExpression #>).ToLowerInvariant(); <# } else { #> public readonly string <#=param.Name #> = <#=param.RouteNameExpression #>; <# } } #> } <# } #> <#} #> <#if (settings.GenerateParamsAsConstantsForActionMethods){ foreach (var group in controller.UniqueParameterNamesGroupedByActionName) if (group.Any()) { #> [<#= GeneratedCode #>, DebuggerNonUserCode] public class <#=group.Key + settings.ParamsPropertySuffix#> { <#foreach (var param in group) { #> <# if (settings.UseLowercaseRoutes) { #> public const string <#=param.Name #> = (<#=param.RouteNameExpression #>).ToLowerInvariant(); <# } else { #> public const string <#=param.Name #> = <#=param.RouteNameExpression #>; <# } } #> } <# } #> <#} #> static readonly ViewsClass s_views = new ViewsClass(); [<#= GeneratedCode #>, DebuggerNonUserCode] public ViewsClass Views { get { return s_views; } } [<#= GeneratedCode #>, DebuggerNonUserCode] public class ViewsClass { <#RenderControllerViews(controller);#> } } <#if (!controller.NotRealController) { #> [<#= GeneratedCode #>, DebuggerNonUserCode] public partial class <#=controller.DerivedClassName #> : <#=controller.FullClassName #> { public <#=controller.DerivedClassName #>() : base(Dummy.Instance) { } <#foreach (var method in controller.ActionMethods.Where(m => !m.IsCustomReturnType)) { #> partial void <#=method.Name #>Override(T4MVC_<#=method.ReturnTypeUniqueName #> callInfo<#if (method.Parameters.Count > 0) { #>, <#method.WriteFormalParameters(true); #><#}#>); public override <#=method.ReturnTypeFullName #> <#=method.Name #>(<#method.WriteFormalParameters(true); #>) { var callInfo = new T4MVC_<#=method.ReturnTypeUniqueName #>(Area, Name, ActionNames.<#=method.ActionName #><# if (method.ActionUrlHttps) {#>, "https"<#}#>); <#if (method.Parameters.Count > 0) { #> <#foreach (var p in method.Parameters) { #> ModelUnbinderHelpers.AddRouteValues(callInfo.RouteValueDictionary, <#=p.RouteNameExpression #>, <#=p.Name #>); <#} #> <#}#> <#=method.Name #>Override(callInfo<#if (method.Parameters.Count > 0) { #><#foreach (var p in method.Parameters) { #>, <#=p.Name #><#}}#>); <#if (method.ReturnTypeFullName == "System.Threading.Tasks.Task") { #> return System.Threading.Tasks.Task.FromResult(callInfo as ActionResult); <#} else { #> return callInfo; <#} #> } <#} #> } <#} #> <#if (!String.IsNullOrEmpty(controller.Namespace)) { #> } <#} #> <#manager.EndBlock(); #> <#} #> <# if (settings.ExplicitHtmlHelpersForPartials) { manager.StartNewFile("T4MVC.ExplicitExtensions.cs"); #> namespace System.Web.Mvc { [<#= GeneratedCode #>] public static class HtmlHelpersForExplicitPartials { <# foreach(var partialView in GetPartials()) { string partialName = partialView.Key; string partialPath = partialView.Value; string partialRenderMethod = string.Format(settings.ExplicitHtmlHelpersForPartialsFormat, partialName); #> /// ///Render the <#= partialName #> partial. /// public static void <#= partialRenderMethod #>(this HtmlHelper html) { html.RenderPartial("<#= partialPath #>"); } /// ///Render the <#= partialName #> partial. /// public static void <#= partialRenderMethod #>(this HtmlHelper html, object model) { html.RenderPartial("<#= partialPath #>", model); } <# } #> } } <# manager.EndBlock(); #> <# } #> <#manager.StartFooter(); #> #endregion T4MVC #pragma warning restore 1591 <#manager.EndBlock(); #> <#settings.SaveChanges(manager); #> <#manager.Process(settings.SplitIntoMultipleFiles); #> <#@ Include File="T4MVC.tt.hooks.t4" #> <#+ static MvcSettings settings; const string ControllerSuffix = "Controller"; static DTE Dte; static Project Project; static string AppRoot; static HashSet Areas; static AreaInfo DefaultArea; static Dictionary ResultTypes; static TextTransformation TT; static string T4FileName; static string T4Folder; static string GeneratedCode = @"GeneratedCode(""T4MVC"", ""2.0"")"; static Microsoft.CSharp.CSharpCodeProvider codeProvider = new Microsoft.CSharp.CSharpCodeProvider(); List virtualPathesForStaticFiles = new List(); IEnumerable GetControllers() { var controllers = new List(); foreach (var area in Areas) { controllers.AddRange(area.GetControllers()); } return controllers; } IEnumerable GetAbstractControllers() { var controllers = new List(); foreach (var area in Areas) { controllers.AddRange(area.GetAbstractControllers()); } return controllers; } IDictionary GetPartials() { var parts = GetControllers() .Select(m => m.ViewsFolder) .SelectMany(m => m.Views) .Where(m => IsPartialView(m.Value)); var partsDic = new Dictionary>(); foreach(var part in parts) { string viewName = Sanitize(part.Key); // Check if we already have a partial view by that name (e.g. if two Views folders have the same ascx) int keyCollisionCount = partsDic.Where(m => m.Key == viewName || m.Value.Key == viewName).Count(); if (keyCollisionCount > 0) { // Append a numbered suffix to avoid the conflict partsDic.Add(viewName + keyCollisionCount.ToString(), part); } else { partsDic.Add(viewName, part); } } return partsDic.ToDictionary(k => k.Key, v => v.Value.Value); } bool IsPartialView(string viewFilePath) { string viewFileName = Path.GetFileName(viewFilePath); if (viewFileName.EndsWith(".ascx")) return true; if ((viewFileName.EndsWith(".cshtml") || viewFileName.EndsWith(".vbhtml")) && viewFileName.StartsWith("_")) { return true; } return false; } void PrepareDataToRender(TextTransformation tt) { TT = tt; T4FileName = Path.GetFileName(Host.TemplateFile); T4Folder = Path.GetDirectoryName(Host.TemplateFile); Areas = new HashSet(); ResultTypes = new Dictionary(); // Get the DTE service from the host var serviceProvider = Host as IServiceProvider; if (serviceProvider != null) { Dte = (EnvDTE.DTE)serviceProvider.GetService(typeof(EnvDTE.DTE)); } // Fail if we couldn't get the DTE. This can happen when trying to run in TextTransform.exe if (Dte == null) { throw new Exception("T4MVC can only execute through the Visual Studio host"); } Project = GetProjectContainingT4File(Dte); if (Project == null) { Error("Could not find the VS Project containing the T4 file."); return; } // Get the path of the root folder of the app AppRoot = Path.GetDirectoryName(Project.FullName) + '\\'; ProcessAreas(Project); } Project GetProjectContainingT4File(DTE dte) { // Find the .tt file's ProjectItem ProjectItem projectItem = dte.Solution.FindProjectItem(Host.TemplateFile); // If the .tt file is not opened, open it if (projectItem.Document == null) projectItem.Open(EnvDTE.Constants.vsViewKindCode); return projectItem.ContainingProject; } void ProcessAreas(Project project) { // Process the default area ProcessArea(project.ProjectItems, null); // Get the Areas folder ProjectItem areaProjectItem = GetProjectItem(project, settings.AreasFolder); // Process areas folder if (areaProjectItem != null) { foreach (ProjectItem item in areaProjectItem.ProjectItems) { if (IsFolder(item)) { ProcessArea(item.ProjectItems, item.Name); } } } // Process portable areas foreach (string portableArea in settings.PortableAreas) { ProjectItem portableAreaProjectItem = GetProjectItem(project, portableArea); if (portableAreaProjectItem == null) return; if (IsFolder(portableAreaProjectItem)) { ProcessArea(portableAreaProjectItem.ProjectItems, portableAreaProjectItem.Name); } } } void ProcessArea(ProjectItems areaFolderItems, string name) { var area = new AreaInfo() { Name = name }; ProcessAreaControllers(areaFolderItems, area); ProcessAreaViews(areaFolderItems, area); Areas.Add(area); if (String.IsNullOrEmpty(name)) DefaultArea = area; } void ProcessAreaControllers(ProjectItems areaFolderItems, AreaInfo area) { // Get area Controllers folder ProjectItem controllerProjectItem = GetProjectItem(areaFolderItems, settings.ControllersFolder); if (controllerProjectItem == null) return; ProcessControllersRecursive(controllerProjectItem, area); } void ProcessAreaViews(ProjectItems areaFolderItems, AreaInfo area) { // Get area Views folder ProjectItem viewsProjectItem = GetProjectItem(areaFolderItems, settings.ViewsRootFolder); if (viewsProjectItem == null) return; ProcessAllViews(viewsProjectItem, area); } void ProcessControllersRecursive(ProjectItem projectItem, AreaInfo area) { // Recurse into all the sub-items (both files and folder can have some - e.g. .tt files) foreach (ProjectItem item in projectItem.ProjectItems) { ProcessControllersRecursive(item, area); } if (projectItem.FileCodeModel != null) { DateTime controllerLastWriteTime = File.GetLastWriteTime(projectItem.get_FileNames(0)); foreach (var type in projectItem.FileCodeModel.CodeElements.OfType()) { ProcessControllerType(type, area, controllerLastWriteTime); } // Process all the elements that are namespaces foreach (var ns in projectItem.FileCodeModel.CodeElements.OfType()) { foreach (var type in ns.Members.OfType()) { ProcessControllerType(type, area, controllerLastWriteTime); } } } } void ProcessControllerType(CodeClass2 type, AreaInfo area, DateTime controllerLastWriteTime) { // Only process controllers if (!IsController(type)) return; // Don't process generic classes (their concrete derived classes will be processed) if (type.IsGeneric) return; //Ignore references to controllers we create if(area.Controllers.Any(c => c.DerivedClassName == type.Name)) return; // Make sure the class is partial if (type.ClassKind != vsCMClassKind.vsCMClassKindPartialClass) { try { type.ClassKind = vsCMClassKind.vsCMClassKindPartialClass; } catch { // If we couldn't make it partial, give a warning and skip it Warning(String.Format("{0} was not able to make the class {1} partial. Please change it manually if possible", T4FileName, type.Name)); return; } Warning(String.Format("{0} changed the class {1} to be partial", T4FileName, type.Name)); } // Collect misc info about the controller class and add it to the collection var controllerInfo = new ControllerInfo { Area = area, Namespace = type.Namespace != null ? type.Namespace.Name : String.Empty, ClassName = type.Name }; //Filter references to controllers we create foreach(var derived in area.Controllers.Where(c => c.ClassName == controllerInfo.DerivedClassName).ToArray()) area.Controllers.Remove(derived); // Check if the controller has changed since the generated file was last created DateTime lastGenerationTime = File.GetLastWriteTime(controllerInfo.GeneratedFileFullPath); if (lastGenerationTime > controllerLastWriteTime) { controllerInfo.GeneratedCodeIsUpToDate = true; } // Either process new ControllerInfo or integrate results into existing object for partially defined controllers var target = area.Controllers.Add(controllerInfo) ? controllerInfo : area.Controllers.First(c => c.Equals(controllerInfo)); target.HasExplicitConstructor |= HasExplicitConstructor(type); target.HasExplicitDefaultConstructor |= HasExplicitDefaultConstructor(type); if (type.IsAbstract) { // If it's abstract, set a flag and don't process action methods (derived classes will) target.IsAbstract = true; } else { // Process all the action methods in the controller ProcessControllerActionMethods(target, type); } } void ProcessControllerActionMethods(ControllerInfo controllerInfo, CodeClass2 current) { bool isAsyncController = IsAsyncController(current); // We want to process not just the controller class itself, but also its parents, as they // may themselves define actions for (CodeClass2 type = current; type != null && type.FullName != "System.Web.Mvc.Controller"; type = (CodeClass2)type.Bases.Item(1)) { // If the type doesn't come from this project, some actions on it will fail. Try to get a real project type if possible. if (type.InfoLocation != vsCMInfoLocation.vsCMInfoLocationProject) { // Go through all the projects in the solution for (int i = 1; i <= Dte.Solution.Projects.Count; i++) { Project prj = null; try { prj = Dte.Solution.Projects.Item(i); } catch (System.Runtime.Serialization.SerializationException) { // Some project types (that we don't care about) cause a strange exception, so ingore it continue; } // Skip it if it's the current project or doesn't have a code model try { if (prj == Project || prj.CodeModel == null) continue; } catch (System.NotImplementedException) { // Installer project does not implement CodeModel property continue; } try { // If we can get a local project type, use it instead of the original var codeType = prj.CodeModel.CodeTypeFromFullName(type.FullName); if (codeType != null && codeType.InfoLocation == vsCMInfoLocation.vsCMInfoLocationProject) { type = (CodeClass2)codeType; break; } } catch (System.ArgumentException) { // CodeTypeFromFullName throws when called on VB projects with a type it doesn't know // (instead of returning null), so ignore those exceptions (See http://t4mvc.codeplex.com/workitem/7) } } } foreach (CodeFunction2 method in GetMethods(type)) { // Ignore non-public methods if (method.Access != vsCMAccess.vsCMAccessPublic) continue; // Ignore methods that are marked as not being actions if (GetAttribute(method.Attributes, "System.Web.Mvc.NonActionAttribute") != null) continue; // Ignore methods that are marked as Obsolete if (GetAttribute(method.Attributes, "System.ObsoleteAttribute") != null) continue; // Ignore generic methods if (method.IsGeneric) continue; if(isAsyncController && settings.SupportAsyncActions && (method.Type.TypeKind == vsCMTypeRef.vsCMTypeRefVoid) && method.Name.EndsWith("Async")) { //Async methods return void and there could be multiple matching Completed methods, so we will use //the generic ActionResult as the return type for the method. var resultType = Project.CodeModel.CreateCodeTypeRef("System.Web.Mvc.ActionResult"); // If we haven't yet seen this return type, keep track of it if (!ResultTypes.ContainsKey(resultType.AsFullName)) { var resTypeInfo = new ResultTypeInfo(resultType); ResultTypes[resultType.AsFullName] = resTypeInfo; } // Collect misc info about the action method and add it to the collection controllerInfo.ActionMethods.Add(new ActionMethodInfo(method, current, resultType)); continue; } // This takes care of avoiding generic types which cause method.Type.CodeType to blow up if (method.Type.TypeKind != vsCMTypeRef.vsCMTypeRefCodeType || !(method.Type.CodeType is CodeClass2)) continue; // We only support action methods that return an ActionResult and Task derived types if (!method.Type.CodeType.get_IsDerivedFrom("System.Web.Mvc.ActionResult") && method.Type.CodeType.FullName !="System.Threading.Tasks.Task") { Warning(String.Format("{0} doesn't support {1}.{2} because it doesn't return a supported {3} type", T4FileName, type.Name, method.Name, method.Type.CodeType.FullName)); continue; } // Ignore async completion methods as they can't really be used in T4MVC, and can cause issues. // See http://stackoverflow.com/questions/5419173/t4mvc-asynccontroller if (isAsyncController && method.Name.EndsWith("Completed", StringComparison.OrdinalIgnoreCase)) continue; var methodType = method.Type; if(method.Type.CodeType.FullName == "System.Threading.Tasks.Task") methodType = Project.CodeModel.CreateCodeTypeRef("System.Web.Mvc.ActionResult"); // If we haven't yet seen this return type, keep track of it var resTypeInfo2 = new ResultTypeInfo(methodType); if (!ResultTypes.ContainsKey(resTypeInfo2.FullName)) { ResultTypes[resTypeInfo2.FullName] = resTypeInfo2; } // Make sure the method is virtual if (!method.CanOverride && method.OverrideKind != vsCMOverrideKind.vsCMOverrideKindOverride) { try { method.CanOverride = true; } catch { // If we couldn't make it virtual, give a warning and skip it Warning(String.Format("{0} was not able to make the action method {1}.{2} virtual. Please change it manually if possible", T4FileName, type.Name, method.Name)); continue; } Warning(String.Format("{0} changed the action method {1}.{2} to be virtual", T4FileName, type.Name, method.Name)); } // Collect misc info about the action method and add it to the collection controllerInfo.ActionMethods.Add(new ActionMethodInfo(method, current)); } } } void ProcessAllViews(ProjectItem viewsProjectItem, AreaInfo area) { // Go through all the sub-folders in the Views folder foreach (ProjectItem item in viewsProjectItem.ProjectItems) { // We only care about sub-folders, not files if (!IsFolder(item)) continue; // Find the controller for this view folder ControllerInfo controller = area.Controllers.SingleOrDefault(c => c.Name.Equals(item.Name, StringComparison.OrdinalIgnoreCase)); if (controller == null) { // If it doesn't match a controller, treat as a pseudo-controller for consistency controller = new ControllerInfo { Area = area, NotRealController = true, Namespace = MakeClassName(settings.T4MVCNamespace, area.Name), ClassName = item.Name + ControllerSuffix }; area.Controllers.Add(controller); } AddViewsRecursive(item.ProjectItems, controller.ViewsFolder); } } void AddViewsRecursive(ProjectItems items, ViewsFolderInfo viewsFolder) { AddViewsRecursive(items, viewsFolder, false); } void AddViewsRecursive(ProjectItems items, ViewsFolderInfo viewsFolder, bool useNonQualifiedViewNames) { // Go through all the files in the subfolder to get the view names foreach (ProjectItem item in items) { if (item.Kind == EnvDTE.Constants.vsProjectItemKindPhysicalFile) { // Ignore some extensions that are normally not views if (settings.ExcludedViewExtensions.Any(extension => Path.GetExtension(item.Name).Equals(extension, StringComparison.OrdinalIgnoreCase))) continue; viewsFolder.AddView(item, useNonQualifiedViewNames); } else if (item.Kind == EnvDTE.Constants.vsProjectItemKindPhysicalFolder) { string folderName = Path.GetFileName(item.Name); if (folderName.Equals("App_LocalResources", StringComparison.OrdinalIgnoreCase)) continue; // Use simple view names if we're already in that mode, or if the folder name is in the collection bool folderShouldUseNonQualifiedViewNames = useNonQualifiedViewNames || settings.NonQualifiedViewFolders.Contains(folderName, StringComparer.OrdinalIgnoreCase); var subViewFolder = new ViewsFolderInfo() { Name = folderName }; viewsFolder.SubFolders.Add(subViewFolder); AddViewsRecursive(item.ProjectItems, subViewFolder, folderShouldUseNonQualifiedViewNames); } } } void RenderControllerViews(ControllerInfo controller) { PushIndent(" "); RenderViewsRecursive(controller.ViewsFolder, controller); PopIndent(); } void RenderViewsRecursive(ViewsFolderInfo viewsFolder, ControllerInfo controller) { if(!viewsFolder.HasNonQualifiedViewNames) { #> static readonly _ViewNamesClass s_ViewNames = new _ViewNamesClass(); public _ViewNamesClass ViewNames { get { return s_ViewNames; } } public class _ViewNamesClass { <#+ PushIndent(" "); foreach (var viewPair in viewsFolder.Views) { WriteLine("public readonly string " + EscapeID(Sanitize(viewPair.Key)) + " = \"" + viewPair.Key + "\";"); } PopIndent(); #> } <#+} // For each view, generate a readonly string foreach (var viewPair in viewsFolder.Views) { WriteLine("public readonly string " + EscapeID(Sanitize(viewPair.Key)) + " = \"" + viewPair.Value + "\";"); } // For each sub folder, generate a class and recurse foreach (var subFolder in viewsFolder.SubFolders) { string name = Sanitize(subFolder.Name); string className = "_" + name; // If the folder name is the same as the parent, add a modifier to avoid class name conflicts // http://mvccontrib.codeplex.com/workitem/7153 if (name == Sanitize(viewsFolder.Name)) { className += "_"; }#> static readonly <#=className#>Class s_<#=name#> = new <#=className#>Class(); public <#=className#>Class <#=EscapeID(name)#> { get { return s_<#=name#>; } } [<#= GeneratedCode #>, DebuggerNonUserCode] public partial class <#=className#>Class { <#+ PushIndent(" "); RenderViewsRecursive(subFolder, controller); PopIndent(); WriteLine("}"); } } IEnumerable GetStaticFilesViewFolders() { if (settings.AddAllViewsFoldersToStaticFilesFolders) { foreach (var area in Areas) { yield return area.Name == null ? settings.ViewsRootFolder : settings.AreasFolder + "\\" + area.Name + "\\" + settings.ViewsRootFolder; } } } void ProcessStaticFiles(Project project, string folder) { ProjectItem folderProjectItem = GetProjectItem(project, folder); if (folderProjectItem != null) { var rootPath = "~"; if (folder.Contains("\\")) { rootPath += "/" + folder.Replace("\\", "/"); rootPath = rootPath.Substring(0, rootPath.LastIndexOf("/")); } ProcessStaticFilesRecursive(folderProjectItem, rootPath); } } void ProcessStaticFilesRecursive(ProjectItem projectItem, string path) { int nestedLevel = BuildClassStructureForProvidedPath(path); ProcessStaticFilesRecursive(projectItem, path, new HashSet()); for(int i = 0; i < nestedLevel; ++i) {#> } <#+ PopIndent(); } } void ProcessStaticFilesRecursive(ProjectItem projectItem, string path, HashSet nameSet) { // The passed in HashSet is to guarantee uniqueness with our parent and siblings string name = SanitizeWithNoConflicts(projectItem.Name, nameSet); // This HashSet is to guarantee uniqueness of our direct children // We add our own name to it to avoid class name conflicts (http://mvccontrib.codeplex.com/workitem/7153) var childrenNameSet = new HashSet(); childrenNameSet.Add(name); if (IsFolder(projectItem)) { #> [<#= GeneratedCode #>, DebuggerNonUserCode] public static class <#=EscapeID(name)#> { private const string URLPATH = "<#=path#>/<#=projectItem.Name#>"; public static string Url() { return T4MVCHelpers.ProcessVirtualPath(URLPATH); } public static string Url(string fileName) { return T4MVCHelpers.ProcessVirtualPath(URLPATH + "/" + fileName); } <#+ PushIndent(" "); // Recurse into all the items in the folder foreach (ProjectItem item in projectItem.ProjectItems) { ProcessStaticFilesRecursive( item, path + "/" + projectItem.Name, childrenNameSet); } PopIndent(); #> } <#+ } else { #> <#+ if (!settings.ExcludedStaticFileExtensions.Any(extension => projectItem.Name.EndsWith(extension, StringComparison.OrdinalIgnoreCase))) { // if it's a non-minified javascript file if (projectItem.Name.EndsWith(".js") && !projectItem.Name.EndsWith(".min.js")) { string minifiedName = projectItem.Name.Replace(".js", ".min.js"); if (AddTimestampToStaticLink(projectItem)) { #> public static readonly string <#=name#> = T4MVCHelpers.IsProduction() && T4Extensions.FileExists(URLPATH + "/<#=minifiedName#>") ? Url("<#=minifiedName#>")+"?"+T4MVCHelpers.TimestampString(URLPATH + "/<#=minifiedName#>") : Url("<#=projectItem.Name#>")+"?"+T4MVCHelpers.TimestampString(URLPATH + "/<#=projectItem.Name#>"); <#+} else {#> public static readonly string <#=name#> = T4MVCHelpers.IsProduction() && T4Extensions.FileExists(URLPATH + "/<#=minifiedName#>") ? Url("<#=minifiedName#>") : Url("<#=projectItem.Name#>"); <#+} #> <#+} else if (projectItem.Name.EndsWith(".css") && !projectItem.Name.EndsWith(".min.css")) { string minifiedName = projectItem.Name.Replace(".css", ".min.css"); if (AddTimestampToStaticLink(projectItem)) { #> public static readonly string <#=name#> = T4MVCHelpers.IsProduction() && T4Extensions.FileExists(URLPATH + "/<#=minifiedName#>") ? Url("<#=minifiedName#>")+"?"+T4MVCHelpers.TimestampString(URLPATH + "/<#=minifiedName#>") : Url("<#=projectItem.Name#>")+"?"+T4MVCHelpers.TimestampString(URLPATH + "/<#=projectItem.Name#>"); <#+} else {#> public static readonly string <#=name#> = T4MVCHelpers.IsProduction() && T4Extensions.FileExists(URLPATH + "/<#=minifiedName#>") ? Url("<#=minifiedName#>") : Url("<#=projectItem.Name#>"); <#+} #> <#+} else if (AddTimestampToStaticLink(projectItem)) { #> public static readonly string <#=name#> = Url("<#=projectItem.Name#>")+"?"+T4MVCHelpers.TimestampString(URLPATH + "/<#=projectItem.Name#>"); <#+} else { #> public static readonly string <#=name#> = Url("<#=projectItem.Name#>"); <#+} } #> <#+ // Non folder items may also have children (virtual folders, Class.cs -> Class.Designer.cs, template output) // Just register them on the same path as their parent item foreach (ProjectItem item in projectItem.ProjectItems) { ProcessStaticFilesRecursive(item, path, childrenNameSet); } } } int BuildClassStructureForProvidedPath(string path) { var folders = path.Split(new char[] {'/', '~'}, StringSplitOptions.RemoveEmptyEntries); var parentFolder = String.Empty; var currentPath = "~"; foreach(var folder in folders) { currentPath += "/" + folder; string className = EscapeID(Sanitize(folder)); // If the folder name is the same as the parent, add a modifier to avoid class name conflicts // http://mvccontrib.codeplex.com/workitem/7153 if (parentFolder == folder) { className += "_"; } if(!virtualPathesForStaticFiles.Contains(currentPath)) { virtualPathesForStaticFiles.Add(currentPath);#> [<#= GeneratedCode #>, DebuggerNonUserCode] public static partial class <#=className #> { private const string URLPATH = "<#=currentPath#>"; public static string Url() { return T4MVCHelpers.ProcessVirtualPath(URLPATH); } public static string Url(string fileName) { return T4MVCHelpers.ProcessVirtualPath(URLPATH + "/" + fileName); } <#+ } else { #> public static partial class <#=className #> { <#+ } PushIndent(" "); parentFolder = folder; } return folders.Length; } ProjectItem GetProjectItem(Project project, string name) { return GetProjectItem(project.ProjectItems, name); } ProjectItem GetProjectItem(ProjectItems items, string subPath) { ProjectItem current = null; foreach (string name in subPath.Split('\\')) { try { // ProjectItems.Item() throws when it doesn't exist, so catch the exception // to return null instead. current = items.Item(name); } catch { // If any chunk couldn't be found, fail return null; } items = current.ProjectItems; } return current; } static bool IsController(CodeClass2 type) { // Ignore any class which name doesn't end with "Controller" if (!type.FullName.EndsWith(ControllerSuffix)) return false; for (; type.FullName != "System.Web.Mvc.Controller"; type = (CodeClass2)type.Bases.Item(1)) { if (type.Bases.Count == 0) return false; } return true; } static bool IsAsyncController(CodeClass2 type) { for (; type.FullName != "System.Web.Mvc.AsyncController"; type = (CodeClass2)type.Bases.Item(1)) { if (type.Bases.Count == 0) return false; } return true; } static string GetVirtualPath(ProjectItem item) { string fileFullPath = item.get_FileNames(0); if (!fileFullPath.StartsWith(AppRoot, StringComparison.OrdinalIgnoreCase)) throw new Exception(string.Format("File {0} is not under app root {1}. Please report issue.", fileFullPath, AppRoot)); // Make a virtual path from the physical path return "~/" + fileFullPath.Substring(AppRoot.Length).Replace('\\', '/'); } static string ProcessAreaOrControllerName(string name) { return settings.UseLowercaseRoutes ? name.ToLowerInvariant() : name; } // Return all the CodeFunction2 in the CodeElements collection static IEnumerable GetMethods(CodeClass2 codeClass) { // Only look at regular method (e.g. ignore things like contructors) return codeClass.Members.OfType() .Where(f => f.FunctionKind == vsCMFunction.vsCMFunctionFunction); } // Check if the class has any explicit constructor static bool HasExplicitConstructor(CodeClass2 codeClass) { return codeClass.Members.OfType().Any( f => !f.IsShared && f.FunctionKind == vsCMFunction.vsCMFunctionConstructor); } // Check if the class has a default (i.e. no params) constructor static bool HasExplicitDefaultConstructor(CodeClass2 codeClass) { return codeClass.Members.OfType().Any( f => !f.IsShared && f.FunctionKind == vsCMFunction.vsCMFunctionConstructor && f.Parameters.Count == 0); } // Find a method with a given name static CodeFunction2 GetMethod(CodeClass2 codeClass, string name) { return GetMethods(codeClass).FirstOrDefault(f => f.Name == name); } // Find an attribute of a given type on an attribute collection static CodeAttribute2 GetAttribute(CodeElements attributes, string attributeType) { for (int i = 1; i <= attributes.Count; i++) { try { var attrib = (CodeAttribute2)attributes.Item(i); if (attributeType.Split(',').Contains(attrib.FullName, StringComparer.OrdinalIgnoreCase)) { return attrib; } } catch { // FullName can throw in some cases, so just ignore those attributes continue; } } return null; } static CodeAttribute2 GetAttribute(CodeClass2 type, string attributeType) { while(type != null) { var attribute = GetAttribute(type.Attributes, attributeType); if(attribute != null) return attribute; if (type.Bases.Count == 0) return null; type = (CodeClass2)type.Bases.Item(1); } return null; } static string UniqueFullName(CodeTypeRef codeType) { return UniqueFullName(codeType.CodeType); } static string UniqueFullName(CodeType codeType) { var uniqueName = codeType.FullName; // Match characters not allowed in class names. uniqueName = Regex.Replace(uniqueName, @"[^\p{Ll}\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{Nl}\d]", "_"); // Remove duplicate '_' characters uniqueName = Regex.Replace(uniqueName, @"__+", "_"); // Remove trailing '_' characters uniqueName = uniqueName.TrimEnd('_'); return uniqueName; } // Return whether a ProjectItem is a folder and not a file static bool IsFolder(ProjectItem item) { return (item.Kind == EnvDTE.Constants.vsProjectItemKindPhysicalFolder); } static string MakeClassName(string ns, string classname) { return String.IsNullOrEmpty(ns) ? classname : String.IsNullOrEmpty(classname) ? ns : ns + "." + codeProvider.CreateEscapedIdentifier(classname); } static string SanitizeWithNoConflicts(string token, HashSet names) { string name = Sanitize(token); while (names.Contains(name)) { name += "_"; } names.Add(name); return name; } static string Sanitize(string token) { if (token == null) return null; // Replace all invalid chars by underscores token = Regex.Replace(token, @"[\W\b]", "_", RegexOptions.IgnoreCase); // If it starts with a digit, prefix it with an underscore token = Regex.Replace(token, @"^\d", @"_$0"); // Check for reserved words // TODO: Clean this up and add other reserved words (keywords, etc) if (token == "Url") token = "_Url"; return token; } static string EscapeID(string id) { return codeProvider.CreateEscapedIdentifier(id); } // Data structure to collect data about an area class AreaInfo { public AreaInfo() { Controllers = new HashSet(); } public string Name { get; set; } public HashSet Controllers { get; set; } public string Namespace { get { // When *not* using an 'Areas' token, we need to disambiguate conflicts // between Area names and controller names (from the default Area) if (!settings.IncludeAreasToken && DefaultArea.Controllers.Any(c => c.Name == Name)) return Name + "Area"; return Name; } } public IEnumerable GetControllers() { return Controllers.Where(c => !c.IsAbstract); } public IEnumerable GetAbstractControllers() { return Controllers.Where(c => c.IsAbstract); } } // Data structure to collect data about a controller class class ControllerInfo { public ControllerInfo() { ActionMethods = new HashSet(); ViewsFolder = new ViewsFolderInfo(); } public AreaInfo Area { get; set; } public string AreaName { get { return Area.Name ?? ""; } } public string T4MVCControllerFullName { get { string name = settings.HelpersPrefix; if (!String.IsNullOrEmpty(AreaName)) { if (settings.IncludeAreasToken) name += ".Areas." + EscapeID(Area.Namespace); else name += "." + EscapeID(Area.Namespace); } return name + "." + Name; } } public string ViewPath { get { if (string.IsNullOrEmpty(Area.Name)) return String.Format("~/{0}/{1}/", settings.ViewsRootFolder, Name); else return String.Format("~/{0}/{1}/{2}/", settings.AreasFolder, settings.ViewsRootFolder, Name); } } // True when this is not a real controller, but a placeholder for views folders that don't match a controller public bool NotRealController { get; set; } public bool HasExplicitConstructor { get; set; } public bool HasExplicitDefaultConstructor { get; set; } public bool HasDefaultConstructor { get { return !HasExplicitConstructor || HasExplicitDefaultConstructor; } } public bool IsAbstract { get; set; } public bool GeneratedCodeIsUpToDate { get; set; } public string ClassName { get; set; } public string Name { get { // Trim the Controller suffix return ClassName.Substring(0, ClassName.Length - ControllerSuffix.Length); } } public string Namespace { get; set; } public string FullClassName { get { return MakeClassName(Namespace, ClassName); } } public string DerivedClassName { get { return "T4MVC_" + ClassName; } } public string FullDerivedClassName { get { if (NotRealController) return FullClassName; return MakeClassName(Namespace, DerivedClassName); } } public string GeneratedFileName { get { return MakeClassName(AreaName, ClassName + ".generated.cs"); } } public string GeneratedFileFullPath { get { return Path.Combine(T4Folder, GeneratedFileName); } } public HashSet ActionMethods { get; set; } public IEnumerable CustomActionMethods { get { return ActionMethods.Where(m => m.IsCustomReturnType); } } IEnumerable ActionMethodsWithNoParameters { get { return ActionMethods.Where(m => m.CanBeCalledWithoutParameters); } } public IEnumerable ActionMethodsUniqueWithoutParameterlessOverload { get { return ActionMethodsWithUniqueNames.Except(ActionMethodsWithNoParameters, new ActionComparer()); } } public IEnumerable CustomActionMethodsUniqueWithoutParameterlessOverload { get { return CustomActionMethodsWithUniqueNames.Except(ActionMethodsWithNoParameters, new ActionComparer()); } } // Return a list of actions without duplicate names (even with multiple overloads) public IEnumerable ActionMethodsWithUniqueNames { get { return ActionMethods.Distinct(new ActionComparer()); } } // Return a list of actions without duplicate names (even with multiple overloads) public IEnumerable CustomActionMethodsWithUniqueNames { get { return CustomActionMethods.Distinct(new ActionComparer()); } } class ActionComparer : IEqualityComparer { public bool Equals(ActionMethodInfo x, ActionMethodInfo y) { return x.ActionName == y.ActionName; } public int GetHashCode(ActionMethodInfo obj) { return obj.ActionName.GetHashCode(); } } public IEnumerable> UniqueParameterNamesGroupedByActionName { get { var comp = new ActionParameterComparer(); return ActionMethods .SelectMany(m => m.Parameters, (m, p) => new { Method = m, Parameter = p }) .GroupBy(m => m.Method.ActionName, m => m.Parameter) .Select(g => new { g.Key, Items = g.Distinct(comp) }) .SelectMany(g => g.Items, (g, i) => new { g.Key, Item = i }) .GroupBy(i => i.Key, i => i.Item); } } class ActionParameterComparer : IEqualityComparer { public bool Equals(MethodParamInfo x, MethodParamInfo y) { return x.Name == y.Name; } public int GetHashCode(MethodParamInfo obj){ return obj.Name.GetHashCode(); } } public ViewsFolderInfo ViewsFolder { get; private set; } public override string ToString() { return Name; } public override bool Equals(object obj) { return obj != null && FullClassName == ((ControllerInfo)obj).FullClassName; } public override int GetHashCode() { return FullClassName.GetHashCode(); } } // Info about a view folder, its views and its sub view folders class ViewsFolderInfo { public ViewsFolderInfo() { Views = new Dictionary(); SubFolders = new List(); } public void AddView(ProjectItem item, bool useNonQualifiedViewName) { string viewName = Path.GetFileName(item.Name); string viewFieldName = Path.GetFileNameWithoutExtension(viewName); // If the simple view name is already in use, include the extension (e.g. foo_ascx instead of just foo) if (Views.ContainsKey(viewFieldName)) viewFieldName = Sanitize(viewName); HasNonQualifiedViewNames = HasNonQualifiedViewNames | useNonQualifiedViewName; Views[viewFieldName] = useNonQualifiedViewName ? Path.GetFileNameWithoutExtension(viewName) : GetVirtualPath(item); } public bool HasNonQualifiedViewNames { get; private set; } public string Name { get; set; } public Dictionary Views { get; private set; } public List SubFolders { get; set; } } // Data structure to collect data about a method class FunctionInfo { protected CodeFunction2 _method; private string _signature; public FunctionInfo(CodeFunction2 method) { Parameters = new List(); // Can be null when an custom ActionResult has no ctor if (method == null) return; _method = method; // Build a unique signature for the method, used to avoid duplication _signature = method.Name; CanBeCalledWithoutParameters = true; // Process all the parameters foreach (var p in method.Parameters.OfType()) { // If any param is not optional, then the method can't be called without parameters if (p.ParameterKind != vsCMParameterKind.vsCMParameterKindOptional) { CanBeCalledWithoutParameters = false; } // Note: if the param name starts with @ (e.g. to escape a keyword), we need to trim that string routeNameExpression = "\"" + p.Name.TrimStart('@') + "\""; // If there is a [Bind(Prefix = "someName")] attribute, use it if (p.InfoLocation != vsCMInfoLocation.vsCMInfoLocationExternal) { var attrib = GetAttribute(p.Attributes, "System.Web.Mvc.BindAttribute"); if (attrib != null) { var arg = attrib.Arguments.OfType().FirstOrDefault(a => a.Name == "Prefix"); if (arg != null) routeNameExpression = arg.Value; } } Parameters.Add( new MethodParamInfo() { Name = p.Name, RouteNameExpression = routeNameExpression, Type = p.Type.AsString, DefaultValue = p.DefaultValue }); _signature += "," + p.Type.AsString; } } protected virtual CodeTypeRef ReturnTypeImpl { get { return _method.Type; } } public string Name { get { return _method.Name; } } public string ReturnType { get { return ReturnTypeImpl.AsString; } } public string ReturnTypeFullName { get { return ReturnTypeImpl.AsFullName; } } public string ReturnTypeUniqueName { get { return IsTaskBased ? "System_Web_Mvc_ActionResult" : UniqueFullName(ReturnTypeImpl); } } public bool IsPublic { get { return _method.Access == vsCMAccess.vsCMAccessPublic; } } public List Parameters { get; private set; } public bool CanBeCalledWithoutParameters { get; private set; } private bool IsTaskBased { get {return ReturnTypeImpl.AsFullName == "System.Threading.Tasks.Task"; } } // Write out all the parameters as part of a method declaration public void WriteFormalParameters(bool first, bool includeDefaults = false) { foreach (var p in Parameters) { if (first) first = false; else TT.Write(", "); TT.Write(p.Type + " " + p.Name); if(includeDefaults && !string.IsNullOrEmpty(p.DefaultValue)) TT.Write(" = " + p.DefaultValue); } } // Pass non-empty param values to make sure the ActionResult ctors don't complain // REVIEW: this is a bit dirty public void WriteNonEmptyParameterValues(bool first) { foreach (var p in Parameters) { if (first) first = false; else TT.Write(", "); if(!string.IsNullOrEmpty(p.DefaultValue)) TT.Write(p.DefaultValue); else { switch (p.Type) { case "string": TT.Write("\" \""); break; case "byte[]": TT.Write("new byte[0]"); break; default: TT.Write("default(" + p.Type + ")"); break; } } } } public override bool Equals(object obj) { return obj != null && _signature == ((FunctionInfo)obj)._signature; } public override int GetHashCode() { return _signature.GetHashCode(); } } // Data structure to collect data about an action method class ActionMethodInfo : FunctionInfo { public ActionMethodInfo(CodeFunction2 method, CodeClass2 controller, CodeTypeRef asyncType = null) : base(method) { if(asyncType != null) { // Remove the Async from the end of the name to match the actual Action routing would use. // This also separates the Action Calls from the implementation _actionName = method.Name.Remove(method.Name.Length - 5); _returnType = asyncType; } // Normally, the action name is the method name. But if there is an [ActionName] on // the method, get the expression from that instead ActionNameValueExpression = '"' + ActionName + '"'; var attrib = GetAttribute(method.Attributes, "System.Web.Mvc.ActionNameAttribute"); if (attrib != null) { var arg = (CodeAttributeArgument)attrib.Arguments.Item(1); ActionNameValueExpression = arg.Value; } if (GetAttribute(method.Attributes, settings.AttributeIndicatingHttps) != null || GetAttribute(controller, settings.AttributeIndicatingHttps) != null) { ActionUrlHttps = true; } } string _actionName; CodeTypeRef _returnType; protected override CodeTypeRef ReturnTypeImpl { get { return _returnType ?? base.ReturnTypeImpl; } } public string ActionName { get { return _actionName ?? base.Name; } } public string ActionNameValueExpression { get; set; } public bool ActionUrlHttps {get; set; } public bool IsCustomReturnType { get { return _returnType != null; } } } // Data about an ActionResult derived type class ResultTypeInfo { CodeTypeRef _codeType; public ResultTypeInfo(CodeTypeRef codeType) { _codeType = codeType; // Use the constructor with the least number of parameters var ctor = _codeType.CodeType.Members.OfType() .Where(f => f.FunctionKind == vsCMFunction.vsCMFunctionConstructor) .OrderBy(f => f.Parameters.Count) .FirstOrDefault(); Constructor = new FunctionInfo(ctor); } public string Name { get { return _codeType.AsString; } } public string FullName { get { return this.IsTaskBased ? "System.Web.Mvc.ActionResult" : _codeType.AsFullName; } } public string UniqueName { get { return this.IsTaskBased ? "System_Web_Mvc_ActionResult" : UniqueFullName(_codeType); } } public FunctionInfo Constructor { get; set; } public IEnumerable AbstractMethods { get { return _codeType.CodeType.Members.OfType().Where( f => f.MustImplement).Select(f => new FunctionInfo(f)); } } private bool IsTaskBased { get {return _codeType.AsFullName == "System.Threading.Tasks.Task"; } } } class MethodParamInfo { public string Name { get; set; } public string RouteNameExpression { get; set; } public string Type { get; set; } public string DefaultValue { get; set; } } class MvcSettings : XmlSettings { public static MvcSettings Load(ITextTemplatingEngineHost host) { return Load(host); } public MvcSettings() { this.T4MVCNamespace = "T4MVC"; this.HelpersPrefix = "MVC"; this.ReferencedNamespaces = new XmlStringArray(new string[] { }, "Namespace"); this.AreasFolder = "Areas"; this.PortableAreas = new XmlStringArray(new string[] { }, "Area"); this.IncludeAreasToken = false; this.ControllersFolder = "Controllers"; this.ViewsRootFolder = "Views"; this.NonQualifiedViewFolders = new XmlStringArray(new string[] { "DisplayTemplates", "EditorTemplates" }, "ViewFolder"); this.GenerateActionResultInterface = true; this.GenerateParamsAsConstantsForActionMethods = false; this.GenerateParamsForActionMethods = true; this.SupportAsyncActions = false; this.UseLowercaseRoutes = false; this.LinksNamespace = "Links"; this.AddTimestampToStaticLinks = false; this.StaticFilesFolders = new XmlStringArray(new string[] { "Scripts", "Content", }, "FileFolder"); this.ExcludedStaticFileExtensions = new XmlStringArray(new string[] { ".cs", ".cshtml", ".aspx", ".ascx" }, "Extension"); this.ExcludedViewExtensions = new XmlStringArray(new string[] { ".master", ".js", ".css" }, "Extension"); this.AttributeIndicatingHttps = "System.Web.Mvc.RequireHttpsAttribute"; this.GenerateSecureLinksInDebugMode = false; this.ParamsPropertySuffix = "Params"; this.ExplicitHtmlHelpersForPartialsFormat = "Render{0}"; this.SplitIntoMultipleFiles = true; } [System.ComponentModel.Description("The namespace used by some of T4MVC's generated code")] public string T4MVCNamespace { get; set; } [System.ComponentModel.Description("The prefix used for things like MVC.Dinners.Name and MVC.Dinners.Delete(Model.DinnerID)")] public string HelpersPrefix { get; set; } [System.ComponentModel.Description("Namespaces to be referenced by the generated code")] public XmlStringArray ReferencedNamespaces { get; set; } [System.ComponentModel.Description("The folder under the project that contains the areas")] public string AreasFolder { get; set; } [System.ComponentModel.Description("You can list folders containing portable areas here")] public IEnumerable PortableAreas { get; set; } [System.ComponentModel.Description("Choose whether you want to include an 'Areas' token when referring to areas.\r\ne.g. Assume the Area is called Blog and the Controller is Post:\r\n- When false use MVC.Blog.Post.etc...\r\n- When true use MVC.Areas.Blog.Post.etc...")] public bool IncludeAreasToken { get; set; } [System.ComponentModel.Description("The folder under the project that contains the controllers")] public string ControllersFolder { get; set; } [System.ComponentModel.Description("The folder under the project that contains the views")] public string ViewsRootFolder { get; set; } [System.ComponentModel.Description("Views in DisplayTemplates and EditorTemplates folders shouldn't be fully qualifed\r\nas it breaks the templated helper code")] public XmlStringArray NonQualifiedViewFolders { get; set; } [System.ComponentModel.Description("If true, the T4MVC action result interface will be generated\r\nIf false, the namespace of the interface must be referenced in the 'ReferencedNamespaces' setting")] public bool GenerateActionResultInterface { get; set; } [System.ComponentModel.Description("If true, [new] overrides will be created for async actions on AsyncControllers\r\nThis breaks the Go To Definition function for async actions.")] public bool SupportAsyncActions { get; set; } [System.ComponentModel.Description("If true, use lower case tokens in routes for the area, controller and action names")] public bool UseLowercaseRoutes { get; set; } [System.ComponentModel.Description("The namespace that the links are generated in (e.g. \"Links\", as in Links.Content.nerd_jpg)")] public string LinksNamespace { get; set; } [System.ComponentModel.Description("If true, links to static files include a query string containing the file's last change time.\r\nThis way, when the static file changes, the link changes and guarantees that the client will re-request the resource.\r\ne.g. when true, the link looks like: \"/Content/nerd.jpg?2009-09-04T12:25:48\"\r\nSee http://mvccontrib.codeplex.com/workitem/7163 for potential issues with this feature")] public bool AddTimestampToStaticLinks { get; set; } [System.ComponentModel.Description("Folders containing static files for which links are generated (e.g. Links.Scripts.Map_js)")] public XmlStringArray StaticFilesFolders { get; set; } [System.ComponentModel.Description("If true, static file helpers are generated for all view folders. See https://t4mvc.codeplex.com/discussions/445358")] public bool AddAllViewsFoldersToStaticFilesFolders { get; set; } [System.ComponentModel.Description("Static files to exclude from the generated links")] public XmlStringArray ExcludedStaticFileExtensions { get; set; } [System.ComponentModel.Description("Files to exclude from the generated views")] public XmlStringArray ExcludedViewExtensions { get; set; } [System.ComponentModel.Description("When creating links with T4MVC, it can force them to HTTPS if the action method you are linking to requires Http.")] public string AttributeIndicatingHttps { get; set; } public bool GenerateSecureLinksInDebugMode { get; set; } [System.ComponentModel.Description("Set this to false to omit the generation of parameters for action methods.")] public bool GenerateParamsForActionMethods { get; set; } [System.ComponentModel.Description("Set this to true to omit the generation of parameters for action methods as constants. Choose this or GenerateParamsForActionMethods.")] public bool GenerateParamsAsConstantsForActionMethods { get; set; } [System.ComponentModel.Description("The suffix added to action method names for the property containing the parameters, for example ImportParams\r\nfor the Import action method.")] public string ParamsPropertySuffix { get; set; } [System.ComponentModel.Description("create explicit HtmlHelpers for rendering partials")] public bool ExplicitHtmlHelpersForPartials { get; set; } public string ExplicitHtmlHelpersForPartialsFormat { get; set; } [System.ComponentModel.Description("If true,the template output will be split into multiple files.")] public bool SplitIntoMultipleFiles { get; set; } } /* XmlSettings base classes, if you need to modify the T4MVC properties edit the MvcSettings Class Above */ /// Base XmlSettings class, responsible for reading/writing the settigns file contents, all settings other /// than string convertable types should decend from this class abstract class XmlSettingsBase { protected XmlSettingsBase() { this.NeedsSave = true; } protected virtual void Init() { } protected bool SaveAsChild { get; private set; } protected bool NeedsSave { get; private set; } protected static void SetSaveAsChild(XmlSettingsBase settings, bool value) { settings.SaveAsChild = value; } protected static void SetNeedsSave(XmlSettingsBase settings, bool value) { settings.NeedsSave = value; } protected static void WriteCommentedProperty(System.Xml.XmlWriter writer, string name) { writer.WriteComment(string.Concat("<", name, ">")); } protected static void WritePropertyDesc(System.Xml.XmlWriter writer, System.ComponentModel.PropertyDescriptor property) { var desc = property.Attributes.OfType().FirstOrDefault(); if(desc != null) { writer.WriteComment(desc.Description); } } protected virtual void Load(System.Xml.Linq.XElement xml) { this.NeedsSave = false; int matched = 0; int read = 0; foreach(System.ComponentModel.PropertyDescriptor property in System.ComponentModel.TypeDescriptor.GetProperties(this)) { object pvalue; if(typeof(XmlSettingsBase).IsAssignableFrom(property.PropertyType) || (((pvalue = property.GetValue(this)) != null) && typeof(XmlSettingsBase).IsAssignableFrom(pvalue.GetType()))) { read++; var value = xml.Element(property.Name); if(value != null) { var settings = (XmlSettingsBase)property.GetValue(this); settings.Load(value); if(!settings.NeedsSave) matched++; settings.SaveAsChild = true; } } else if(!property.IsReadOnly) { read++; var value = xml.Element(property.Name); if(value != null) { if(property.Converter.CanConvertFrom(typeof(string))) { matched++; property.SetValue(this, property.Converter.ConvertFromString(value.Value)); } else { System.Reflection.MethodBase parser = property.PropertyType.GetMethod("Parse", new Type[] { typeof(string) }); if(parser == null) parser = property.PropertyType.GetConstructor(new Type[] { typeof(string) }); if(parser != null) { matched++; property.SetValue(this, parser.Invoke(null, new Object[] { value.Value })); } } } } } this.NeedsSave = this.NeedsSave || (matched < read); } protected virtual void Save(System.Xml.XmlWriter writer) { foreach(System.ComponentModel.PropertyDescriptor property in System.ComponentModel.TypeDescriptor.GetProperties(this)) { var value = property.GetValue(this); WritePropertyDesc(writer, property); if(value != null) { if(typeof(XmlSettingsBase).IsAssignableFrom(value.GetType())) { var settings = (XmlSettingsBase)property.GetValue(this); if((settings != null) && settings.SaveAsChild) { writer.WriteStartElement(property.Name); settings.Save(writer); writer.WriteEndElement(); } } else if(!property.IsReadOnly) { writer.WriteElementString(property.Name, property.Converter.ConvertToString(value)); } } else { WriteCommentedProperty(writer, property.Name); } } } } /// Custom class to allow string arrays to be read and written to/from settings class XmlStringArray : XmlSettingsBase, IEnumerable { public XmlStringArray(IEnumerable items, string name) { this._items = items; this._name = name; SetSaveAsChild(this, true); } string _name; IEnumerable _items; protected override void Load(System.Xml.Linq.XElement xml) { var items = new List(); foreach(var item in xml.Elements(this._name)) { items.Add(item.Value); } this._items = items; SetNeedsSave(this, false); } protected override void Save(System.Xml.XmlWriter writer) { if(this._items == null || !this._items.Any()) { WriteCommentedProperty(writer, this._name); return; } foreach(var item in this._items) { writer.WriteElementString(this._name, item); } } public IEnumerator GetEnumerator() { return this._items.GetEnumerator(); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return this.GetEnumerator(); } } /// This is the base class for the standard settings, the main settigns class should inherit from this /// one since it provides the methods to interact with the T4 system and EnvDTE. Sub-properties can /// just inherit from XmlSettingsBase. abstract class XmlSettings : XmlSettingsBase { protected static T Load(ITextTemplatingEngineHost host) where T : XmlSettings, new() { T settings = new T(); settings.Init(host); return settings; } void Init(ITextTemplatingEngineHost host) { this.TemplateFile = Path.GetFileName(host.TemplateFile); this.TemplateFolder = Path.GetDirectoryName(host.TemplateFile); // Get the DTE service from the host var serviceProvider = host as IServiceProvider; if (serviceProvider != null) { this.DTE = (EnvDTE.DTE)serviceProvider.GetService(typeof(EnvDTE.DTE)); } // Fail if we couldn't get the DTE. This can happen when trying to run in TextTransform.exe if (this.DTE == null) { throw new Exception("T4Build can only execute through the Visual Studio host"); } this.ProjectItem = this.DTE.Solution.FindProjectItem(host.TemplateFile); // If the .tt file is not opened, open it if (this.ProjectItem.Document == null) this.ProjectItem.Open(EnvDTE.Constants.vsViewKindCode); this.Project = this.ProjectItem.ContainingProject; if (Project == null) { throw new Exception("Could not find the VS Project containing the T4 file."); } this.Load(); this.Init(); } public string TemplateFile { get; private set; } public string TemplateFolder { get; private set; } public DTE DTE { get; private set; } public ProjectItem ProjectItem { get; private set; } public Project Project { get; private set; } ProjectItem FindProjectItemRecursive(ProjectItems items, string name) { if(items == null) return null; foreach(ProjectItem item in items) { if(item.Name.Equals(name) || item.Name.StartsWith(name + ".")) return item; var found = FindProjectItemRecursive(item.ProjectItems, name); if(found != null) return found; } return null; } protected ProjectItem FindProjectItem(string name) { return this.FindProjectItemRecursive(this.Project.ProjectItems, name); } protected string SettingsFile { get { return Path.Combine(this.TemplateFolder, string.Concat(this.TemplateFile, ".settings.xml")); } } void Load() { if(System.IO.File.Exists(this.SettingsFile)) try { this.Load(System.Xml.Linq.XElement.Load(this.SettingsFile)); } catch { throw; } } public void SaveChanges(Manager manager) { // Avoid saving if we dont need to; if(!this.NeedsSave) return; if(manager.FileOkToWrite(this.SettingsFile)) { var settings = new System.Xml.XmlWriterSettings { Indent = true }; using(var writer = System.Xml.XmlWriter.Create(this.SettingsFile, settings)) { writer.WriteStartDocument(); writer.WriteStartElement(this.GetType().Name); this.Save(writer); writer.WriteEndElement(); writer.WriteEndDocument(); } var item = this.ProjectItem.Collection.AddFromFile(this.SettingsFile); item.Properties.Item("ItemType").Value = "None"; } else TT.Error("Cannot save settings file! " + this.SettingsFile); } } /* Manager.tt from Damien Guard: http://damieng.com/blog/2009/11/06/multiple-outputs-from-t4-made-easy-revisited */ // Manager class records the various blocks so it can split them up class Manager { private class Block { public String Name; public int Start, Length; } private Block currentBlock; private List files = new List(); private Block footer = new Block(); private Block header = new Block(); private ITextTemplatingEngineHost host; private StringBuilder template; protected List generatedFileNames = new List(); public static Manager Create(ITextTemplatingEngineHost host, StringBuilder template) { return (host is IServiceProvider) ? new VSManager(host, template) : new Manager(host, template); } public virtual bool FileOkToWrite(String fileName) { return true; } public void KeepGeneratedFile(String name) { name = Path.Combine(Path.GetDirectoryName(host.TemplateFile), name); generatedFileNames.Add(name); } public void StartNewFile(String name) { if (name == null) throw new ArgumentNullException("name"); CurrentBlock = new Block { Name = name }; } public void StartFooter() { CurrentBlock = footer; } public void StartHeader() { CurrentBlock = header; } public void EndBlock() { if (CurrentBlock == null) return; CurrentBlock.Length = template.Length - CurrentBlock.Start; if (CurrentBlock != header && CurrentBlock != footer) files.Add(CurrentBlock); currentBlock = null; } public virtual void Process(bool split) { if (split) { EndBlock(); String headerText = template.ToString(header.Start, header.Length); String footerText = template.ToString(footer.Start, footer.Length); String outputPath = Path.GetDirectoryName(host.TemplateFile); files.Reverse(); foreach (Block block in files) { String fileName = Path.Combine(outputPath, block.Name); String content = headerText + template.ToString(block.Start, block.Length) + footerText; generatedFileNames.Add(fileName); CreateFile(fileName, content); template.Remove(block.Start, block.Length); } } } protected virtual void CreateFile(String fileName, String content) { if (IsFileContentDifferent(fileName, content)) File.WriteAllText(fileName, content); } public virtual String GetCustomToolNamespace(String fileName) { return null; } public virtual String DefaultProjectNamespace { get { return null; } } protected bool IsFileContentDifferent(String fileName, String newContent) { return !(File.Exists(fileName) && File.ReadAllText(fileName) == newContent); } private Manager(ITextTemplatingEngineHost host, StringBuilder template) { this.host = host; this.template = template; } private Block CurrentBlock { get { return currentBlock; } set { if (CurrentBlock != null) EndBlock(); if (value != null) value.Start = template.Length; currentBlock = value; } } private class VSManager : Manager { private EnvDTE.ProjectItem templateProjectItem; private EnvDTE.DTE dte; private Action checkOutAction; private Action> projectSyncAction; private IVsQueryEditQuerySave2 queryEditSave; public override String DefaultProjectNamespace { get { return templateProjectItem.ContainingProject.Properties.Item("DefaultNamespace").Value.ToString(); } } public override String GetCustomToolNamespace(string fileName) { return dte.Solution.FindProjectItem(fileName).Properties.Item("CustomToolNamespace").Value.ToString(); } public override void Process(bool split) { if (templateProjectItem.ProjectItems == null) return; base.Process(split); projectSyncAction.EndInvoke(projectSyncAction.BeginInvoke(generatedFileNames, null, null)); } public override bool FileOkToWrite(String fileName) { CheckoutFileIfRequired(fileName); return base.FileOkToWrite(fileName); } protected override void CreateFile(String fileName, String content) { if (IsFileContentDifferent(fileName, content)) { CheckoutFileIfRequired(fileName); File.WriteAllText(fileName, content); } } internal VSManager(ITextTemplatingEngineHost host, StringBuilder template) : base(host, template) { var hostServiceProvider = (IServiceProvider)host; if (hostServiceProvider == null) throw new ArgumentNullException("Could not obtain IServiceProvider"); dte = (EnvDTE.DTE)hostServiceProvider.GetService(typeof(EnvDTE.DTE)); if (dte == null) throw new ArgumentNullException("Could not obtain DTE from host"); templateProjectItem = dte.Solution.FindProjectItem(host.TemplateFile); checkOutAction = (String fileName) => dte.SourceControl.CheckOutItem(fileName); projectSyncAction = (IEnumerable keepFileNames) => ProjectSync(templateProjectItem, keepFileNames); queryEditSave = (IVsQueryEditQuerySave2)hostServiceProvider.GetService(typeof(SVsQueryEditQuerySave)); } private static void ProjectSync(EnvDTE.ProjectItem templateProjectItem, IEnumerable keepFileNames) { var keepFileNameSet = new HashSet(keepFileNames); var projectFiles = new Dictionary(); var originalFilePrefix = Path.GetFileNameWithoutExtension(templateProjectItem.get_FileNames(0)) + "."; foreach (EnvDTE.ProjectItem projectItem in templateProjectItem.ProjectItems) projectFiles.Add(projectItem.get_FileNames(0), projectItem); // Remove unused items from the project foreach (var pair in projectFiles) if (!keepFileNames.Contains(pair.Key) && !(Path.GetFileNameWithoutExtension(pair.Key) + ".").StartsWith(originalFilePrefix)) pair.Value.Delete(); // Add missing files to the project foreach (String fileName in keepFileNameSet) if (!projectFiles.ContainsKey(fileName)) templateProjectItem.ProjectItems.AddFromFile(fileName); } private void CheckoutFileIfRequired(String fileName) { if (queryEditSave != null) { uint pfEditVerdict; queryEditSave.QuerySaveFile(fileName, 0, null, out pfEditVerdict); } else { var sc = dte.SourceControl; if (sc != null && sc.IsItemUnderSCC(fileName) && !sc.IsItemCheckedOut(fileName)) checkOutAction.EndInvoke(checkOutAction.BeginInvoke(fileName, null, null)); } } } } /* End of Manager.tt */ #>