|
<# |
|
/* |
|
T4MVC Version 2.4.01 + AIS Modifications |
|
|
|
AIS Modifications are to fix StyleCop and CLSCompliant-related issues: |
|
* Marking the file with an // <auto-generated /> comment |
|
* Changing the region to include the term 'Generated Code' |
|
* Marking all public members that start with _ as CLSCompliant(false) |
|
* Marking all members that reference said members as CLSCompliant(false) |
|
Full details at http://mo.notono.us/2009/08/making-t4mvc-comply-with-stylecop.html |
|
and http://mo.notono.us/2009/08/making-t4mvc-comply-with-cls.html |
|
|
|
Find latest version on http://aspnet.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=24471&ProjectName=aspnet |
|
|
|
Written by David Ebbo, with much feedback from the MVC community (thanks all!) |
|
david.ebbo@microsoft.com |
|
http://twitter.com/davidebbo |
|
http://blogs.msdn.com/davidebb |
|
|
|
Related blog posts: |
|
http://blogs.msdn.com/davidebb/archive/2009/07/28/t4mvc-2-4-updates-settings-file-sub-view-folders-actionname-support-and-more.aspx |
|
http://blogs.msdn.com/davidebb/archive/2009/06/30/t4mvc-2-2-update-routing-forms-di-container-fixes.aspx |
|
http://blogs.msdn.com/davidebb/archive/2009/06/26/the-mvc-t4-template-is-now-up-on-codeplex-and-it-does-change-your-code-a-bit.aspx |
|
http://blogs.msdn.com/davidebb/archive/2009/06/17/a-new-and-improved-asp-net-mvc-t4-template.aspx |
|
|
|
Feel free to use and modify to fit your needs. |
|
|
|
This T4 template for ASP.NET MVC apps creates strongly typed helpers that eliminate the use |
|
of literal strings when referring the controllers, actions and views. |
|
|
|
To use it, simply copy it and T4MVC.settings.t4 to the root of your MVC application. |
|
|
|
This will enable the following scenarios: |
|
|
|
Refer to controller, action and view names as shown in these examples: |
|
- MVC.Dinners.Name: "Dinners" (controller name). |
|
- MVC.Dinners.Views.DinnerForm: "DinnerForm" (view name) |
|
- MVC.Dinners.Actions.Delete: "Delete" (action name) |
|
|
|
Strong type certain scenarios that refer to controller actions. e.g. |
|
- Html.ActionLink("Delete Dinner", MVC.Dinners.Delete(Model.DinnerID)) |
|
- Url.Action(MVC.Dinners.Delete(Model.DinnerID)) |
|
- RedirectToAction(MVC.Dinners.Delete(dinner.DinnerID)) |
|
- Route defaults e.g. |
|
routes.MapRoute( |
|
"UpcomingDinners", |
|
"Dinners/Page/{page}", |
|
MVC.Dinners.Index(null) |
|
); |
|
|
|
Refer to your static images and script files with strong typing, e.g. |
|
Instead of <img src="/Content/nerd.jpg" ...>, you can write <img src="<%= Links.Content.nerd_jpg %>" ...> |
|
Instead of <script src="/Scripts/Map.js" ...>, you can write <script src="<%= Links.Scripts.Map_js %>" ...> |
|
Or if the file name is dynamic, you can write: Links.Content.Url("foo.jpg") |
|
|
|
KNOWN ISSUES: |
|
- Users running VisualSVN have reported some errors when T4MVC tries to change actions to virtual and controllers to partial. |
|
The suggestion when that happens is to manually make those changes. This is just a one time thing you need to do. |
|
- It will not locate controllers that live in a different project or assembly |
|
|
|
HISTORY: |
|
|
|
2.4.01 (07-29-2009): |
|
- Put all the generated code in a T4MVC #region. This is useful to tell tools like ReSharper to ignore it. |
|
- Fixed issue where controller methods returning generic types cause template to blow up |
|
- Added a setting in T4MVC.settings.t4 to turn off the behavior that always keeps the template dirty |
|
|
|
2.4.00 (07-28-2009): |
|
- Added support for configurable settings in a separate T4MVC.settings.t4 file |
|
- Added a parameter-less pseudo-action for every action that doesn't already have a parameter-less overload |
|
- Added support for having T4MVC.tt in a sub folder instead of always at the root of the project |
|
- Fixed issue when a base controller doesn't have a default ctor |
|
- Added T4Extensions into System.Web.Mvc namespace to fix ambiguous resolution issue |
|
- Misc cleanup |
|
|
|
2.3.01 (07-10-2009): |
|
- Fixed issue with [ActionName] attribute set to non literal string values (e.g. [ActionName(SomeConst + "Abc")]) |
|
- Fixed duplication issue when partial controller classes have a base type which contains action methods |
|
- Skip App_LocalResources when processing views |
|
- Cleaned up rendering logic |
|
|
|
2.3.00 (07-07-2009): |
|
- Added support for sub view folders |
|
- Added support for [ActionName] attribute |
|
- Improved handling when the controller comes from a different project |
|
- Don't try to process generic controller classes |
|
|
|
2.2.03 (07-06-2009): |
|
- Added support for action methods defined on controller base classes |
|
- Improved error handling when not able to change actions to virtual and controllers to partial |
|
|
|
2.2.02 (07-01-2009): |
|
- Fixed break caused by incorrect support for derived ActionResult types in 2.2.01 |
|
- Fixed issue with duplicate view tokens getting generated when you have both foo.aspx and foo.ascx |
|
|
|
2.2.01 (07-01-2009): |
|
- Added support for action methods that return a type derived from ActionResult (as opposed to exactly an ActionResult) |
|
- Fixed issue when controller is using partial classes |
|
- Fixed folder handling logic to deal with generated files |
|
- Fixed issue with folder names that are C# keyword |
|
- Throw NotSupportedException instead of NotImplementedException to avoid being viewed as a TODO |
|
|
|
2.2.00 (06-30-2009): |
|
- Added strongly typed support to MapRoute |
|
- Changed constructor generation to avoid confusing IoC containers |
|
- Fixed issue with empty Content folder |
|
- Fixed issue with abstract controller base classes |
|
|
|
2.1.00 (06-29-2009): |
|
- Added Html.BeginForm overloads that use the strongly typed pattern |
|
- Added Url() helpers on static resources to increase flexibility |
|
- Changed generated constants (view and action names, static files) to be readonly strings |
|
- Fixed null ref exception in Solution Folder logic |
|
|
|
2.0.04 (06-28-2009): |
|
- Fixed issue with files and folders with names starting with a digit |
|
|
|
2.0.03 (06-27-2009): |
|
- Rework code element enumeration logic to work around a VS2010 issue. The template should now work with VS2010 beta 1! |
|
- Reduced some redundancy in the generated code |
|
|
|
2.0.02 (06-27-2009): |
|
- Added ActionLink overloads that take object instead of dictionary (from both Html and Ajax) |
|
|
|
2.0.01 (06-26-2009): |
|
- Fixed issue with files and folders with invalid identifier characters (e.g. spaces, '-', '.') |
|
|
|
2.0.00 (06-26-2009): as described in http://blogs.msdn.com/davidebb/archive/2009/06/26/the-mvc-t4-template-is-now-up-on-codeplex-and-it-does-change-your-code-a-bit.aspx |
|
- Added support for refactoring in Action methods |
|
- The T4 file automatically runs whenever you build, instead of being done manually |
|
- Support for strongly typed links to static resources |
|
- Fix: supports controllers that are in sub-folders of the Controllers folder and not directly in there |
|
- Fix: works better with nested solution folder |
|
- Random other small fixes |
|
|
|
1.0.xx (06-17-2009): the original based on this post |
|
http://blogs.msdn.com/davidebb/archive/2009/06/17/a-new-and-improved-asp-net-mvc-t4-template.aspx |
|
|
|
*/ |
|
#> |
|
<#@ template language="C#v3.5" debug="true" hostspecific="true" #> |
|
<#@ assembly name="System.Core" #> |
|
<#@ assembly name="Microsoft.VisualStudio.Shell.Interop.8.0" #> |
|
<#@ assembly name="EnvDTE" #> |
|
<#@ assembly name="EnvDTE80" #> |
|
<#@ import namespace="System.Collections.Generic" #> |
|
<#@ import namespace="System.IO" #> |
|
<#@ import namespace="System.Linq" #> |
|
<#@ import namespace="System.Text.RegularExpressions" #> |
|
<#@ import namespace="Microsoft.VisualStudio.Shell.Interop" #> |
|
<#@ import namespace="EnvDTE" #> |
|
<#@ import namespace="EnvDTE80" #> |
|
<#@ import namespace="Microsoft.VisualStudio.TextTemplating" #> |
|
<# PrepareDataToRender(this); #> |
|
// <auto-generated /> |
|
// 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. |
|
|
|
#region T4MVC Generated Code |
|
|
|
using System; |
|
using System.Collections.Generic; |
|
using System.Runtime.CompilerServices; |
|
using System.Web; |
|
using System.Web.Mvc; |
|
using System.Web.Mvc.Ajax; |
|
using System.Web.Mvc.Html; |
|
using System.Web.Routing; |
|
using <#= T4MVCNamespace #>; |
|
|
|
[CompilerGenerated] |
|
public static class <#= HelpersPrefix #> { |
|
<# foreach (var controller in GetControllers()) { #> |
|
public static <#= controller.FullClassName #> <#= controller.Name #> = new <#= controller.DerivedClassName #>(); |
|
<# } #> |
|
} |
|
|
|
<# foreach (var controller in GetAbstractControllers().Where(c => !c.HasDefaultConstructor)) { #> |
|
namespace <#= controller.Namespace #> { |
|
public partial class <#= controller.ClassName #> { |
|
protected <#= controller.ClassName #>() { } |
|
} |
|
} |
|
<# } #> |
|
|
|
<# foreach (var controller in GetControllers()) { #> |
|
namespace <#= controller.Namespace #> { |
|
public <# if (!controller.SharedViewFolder) { #>partial <# } #>class <#= controller.ClassName #> { |
|
|
|
<# if (!controller.SharedViewFolder) { #> |
|
<# if (!controller.HasExplicitConstructor) { #> |
|
public <#= controller.ClassName #>() { } |
|
|
|
<# } #> |
|
[CLSCompliant(false)] |
|
[CompilerGenerated] |
|
protected <#= controller.ClassName #>(_Dummy d) { } |
|
|
|
protected RedirectToRouteResult RedirectToAction(ActionResult result) { |
|
var callInfo = (IT4MVCActionResult)result; |
|
return RedirectToRoute(callInfo.RouteValues); |
|
} |
|
|
|
<# foreach (var method in controller.ActionMethodsUniqueWithoutParameterlessOverload) { #> |
|
[NonAction] |
|
public ActionResult <#= method.Name #>() { |
|
return new T4MVC_ActionResult(Name, Actions.<#= method.ActionName #>); |
|
} |
|
|
|
<# } #> |
|
|
|
[CompilerGenerated] |
|
public readonly string Name = "<#= controller.Name #>"; |
|
|
|
static readonly _Actions s_actions = new _Actions(); |
|
|
|
[CLSCompliant(false)] |
|
[CompilerGenerated] |
|
public _Actions Actions { get { return s_actions; } } |
|
|
|
[CLSCompliant(false)] |
|
[CompilerGenerated] |
|
public class _Actions { |
|
<# foreach (var method in controller.ActionMethodsWithUniqueNames) { #> |
|
public readonly string <#= method.ActionName #> = <#= method.ActionNameValueExpression #>; |
|
<# } #> |
|
} |
|
<# } #> |
|
|
|
static readonly _Views s_views = new _Views(); |
|
|
|
[CLSCompliant(false)] |
|
[CompilerGenerated] |
|
public _Views Views { get { return s_views; } } |
|
|
|
[CLSCompliant(false)] |
|
[CompilerGenerated] |
|
public class _Views { |
|
<# RenderControllerViews(controller);#> |
|
} |
|
} |
|
} |
|
<# } #> |
|
|
|
namespace <#= T4MVCNamespace #> { |
|
<# foreach (var controller in GetControllers().Where(c => !c.SharedViewFolder)) { #> |
|
[CompilerGenerated] |
|
public class <#= controller.DerivedClassName #>: <#= controller.FullClassName #> { |
|
public <#= controller.DerivedClassName #>() : base(_Dummy.Instance) { } |
|
|
|
<# foreach (var method in controller.ActionMethods) { #> |
|
public override <#= method.ReturnType #> <#= method.Name #>(<# method.WriteFormalParameters(true); #>) { |
|
var callInfo = new T4MVC_<#= method.ReturnType #>("<#= controller.Name #>", Actions.<#= method.ActionName #>); |
|
<# if (method.Parameters.Count > 0) { #> |
|
<# foreach (var p in method.Parameters) { #> |
|
callInfo.RouteValues.Add("<#= p.Name #>", <#= p.Name #>); |
|
<# } #> |
|
<# }#> |
|
return callInfo; |
|
} |
|
|
|
<# } #> |
|
} |
|
<# } #> |
|
|
|
[CLSCompliant(false)] |
|
[CompilerGenerated] |
|
public class _Dummy { |
|
private _Dummy() { } |
|
public static _Dummy Instance = new _Dummy(); |
|
} |
|
} |
|
|
|
namespace System.Web.Mvc { |
|
[CompilerGenerated] |
|
public static class T4Extensions { |
|
public static string ActionLink(this HtmlHelper htmlHelper, string linkText, ActionResult result) { |
|
return htmlHelper.RouteLink(linkText, result.GetRouteValueDictionary()); |
|
} |
|
|
|
public static string ActionLink(this HtmlHelper htmlHelper, string linkText, ActionResult result, object htmlAttributes) { |
|
return ActionLink(htmlHelper, linkText, result, new RouteValueDictionary(htmlAttributes)); |
|
} |
|
|
|
public static string ActionLink(this HtmlHelper htmlHelper, string linkText, ActionResult result, IDictionary<string, object> htmlAttributes) { |
|
return htmlHelper.RouteLink(linkText, result.GetRouteValueDictionary(), htmlAttributes); |
|
} |
|
|
|
public static MvcForm BeginForm(this HtmlHelper htmlHelper, ActionResult result, FormMethod formMethod) { |
|
return htmlHelper.BeginForm(result, formMethod, null); |
|
} |
|
|
|
public static MvcForm BeginForm(this HtmlHelper htmlHelper, ActionResult result, FormMethod formMethod, object htmlAttributes) { |
|
return BeginForm(htmlHelper, result, formMethod, new RouteValueDictionary(htmlAttributes)); |
|
} |
|
|
|
public static MvcForm BeginForm(this HtmlHelper htmlHelper, ActionResult result, FormMethod formMethod, IDictionary<string, object> htmlAttributes) { |
|
var callInfo = (IT4MVCActionResult)result; |
|
return htmlHelper.BeginForm(callInfo.Action, callInfo.Controller, callInfo.RouteValues, formMethod, htmlAttributes); |
|
} |
|
|
|
public static string Action(this UrlHelper urlHelper, ActionResult result) { |
|
return urlHelper.RouteUrl(result.GetRouteValueDictionary()); |
|
} |
|
|
|
public static string ActionLink(this AjaxHelper ajaxHelper, string linkText, ActionResult result, AjaxOptions ajaxOptions) { |
|
return ajaxHelper.RouteLink(linkText, result.GetRouteValueDictionary(), ajaxOptions); |
|
} |
|
|
|
public static string ActionLink(this AjaxHelper ajaxHelper, string linkText, ActionResult result, AjaxOptions ajaxOptions, object htmlAttributes) { |
|
return ajaxHelper.RouteLink(linkText, result.GetRouteValueDictionary(), ajaxOptions, new RouteValueDictionary(htmlAttributes)); |
|
} |
|
|
|
public static string ActionLink(this AjaxHelper ajaxHelper, string linkText, ActionResult result, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes) { |
|
return ajaxHelper.RouteLink(linkText, result.GetRouteValueDictionary(), ajaxOptions, htmlAttributes); |
|
} |
|
|
|
public static Route MapRoute(this RouteCollection routes, string name, string url, ActionResult result) { |
|
return routes.MapRoute(name, url, result, (ActionResult)null); |
|
} |
|
|
|
public static Route MapRoute(this RouteCollection routes, string name, string url, ActionResult result, object defaults) { |
|
// Start by adding the default values from the anonymous object (if any) |
|
var routeValues = new RouteValueDictionary(defaults); |
|
|
|
// Then add the Controller/Action names and the parameters from the call |
|
foreach (var pair in result.GetRouteValueDictionary()) { |
|
routeValues.Add(pair.Key, pair.Value); |
|
} |
|
|
|
// Create and add the route |
|
var route = new Route(url, routeValues, new MvcRouteHandler()); |
|
routes.Add(name, route); |
|
return route; |
|
} |
|
|
|
public static RouteValueDictionary GetRouteValueDictionary(this ActionResult result) { |
|
return ((IT4MVCActionResult)result).RouteValues; |
|
} |
|
|
|
public static void InitMVCT4Result(this IT4MVCActionResult result, string controller, string action) { |
|
result.Controller = controller; |
|
result.Action = action; ; |
|
result.RouteValues = new RouteValueDictionary(); |
|
result.RouteValues.Add("Controller", controller); |
|
result.RouteValues.Add("Action", action); |
|
} |
|
} |
|
} |
|
|
|
[CompilerGenerated] |
|
public interface IT4MVCActionResult { |
|
string Action { get; set; } |
|
string Controller { get; set; } |
|
RouteValueDictionary RouteValues { get; set; } |
|
} |
|
|
|
<# foreach (var resultType in ResultTypes.Values) { #> |
|
[CompilerGenerated] |
|
public class T4MVC_<#= resultType.Name #> : <#= resultType.Name #>, IT4MVCActionResult { |
|
public T4MVC_<#= resultType.Name #>(string controller, string action): base(<# resultType.Constructor.WriteNonEmptyParameterValues(true); #>) { |
|
this.InitMVCT4Result(controller, action); |
|
} |
|
<# foreach (var method in resultType.AbstractMethods) { #> |
|
<#= method.IsPublic ? "public" : "protected" #> override void <#= method.Name #>(<# method.WriteFormalParameters(true); #>) { } |
|
<# } #> |
|
|
|
public string Controller { get; set; } |
|
public string Action { get; set; } |
|
public RouteValueDictionary RouteValues { get; set; } |
|
} |
|
|
|
<# } #> |
|
|
|
namespace Links { |
|
<# |
|
foreach (string folder in StaticFilesFolders) { |
|
ProcessStaticFiles(Project, folder); |
|
} |
|
#> |
|
} |
|
|
|
#endregion T4MVC |
|
|
|
<#@ Include File="T4MVC.settings.t4" #> |
|
<#+ |
|
const string T4MVCNamespace = "T4MVC"; |
|
const string ControllerSuffix = "Controller"; |
|
|
|
static DTE Dte; |
|
static Project Project; |
|
static HashSet<ControllerInfo> Controllers; |
|
static Dictionary<string, ResultTypeInfo> ResultTypes; |
|
static TextTransformation TT; |
|
static string T4FileName; |
|
|
|
static IEnumerable<ControllerInfo> GetControllers() { |
|
return Controllers.Where(c => !c.IsAbstract); |
|
} |
|
|
|
static IEnumerable<ControllerInfo> GetAbstractControllers() { |
|
return Controllers.Where(c => c.IsAbstract); |
|
} |
|
|
|
void PrepareDataToRender(TextTransformation tt) { |
|
|
|
TT = tt; |
|
T4FileName = Path.GetFileName(Host.TemplateFile); |
|
Controllers = new HashSet<ControllerInfo>(); |
|
ResultTypes = new Dictionary<string, ResultTypeInfo>(); |
|
|
|
// Get the DTE service from the host |
|
Dte = (DTE)((IServiceProvider)Host).GetService(typeof(SDTE)); |
|
|
|
Project = GetProjectContainingT4File(Dte); |
|
if (Project == null) { |
|
Error("Could not find the VS Project containing the T4 file."); |
|
return; |
|
} |
|
|
|
ProcessControllersFolder(Project); |
|
ProcessAllViews(Project); |
|
} |
|
|
|
Project GetProjectContainingT4File(DTE dte) { |
|
// Just locating the Project that contains this T4 file is immensely difficult. |
|
// If there is an easier way to do it, I'd love to know! |
|
foreach (Project project in dte.Solution.Projects) { |
|
|
|
Project foundProject = GetProjectContainingT4File(project); |
|
if (foundProject != null) |
|
return foundProject; |
|
} |
|
|
|
return null; |
|
} |
|
|
|
Project GetProjectContainingT4File(Project project) { |
|
if (project.ConfigurationManager != null) { |
|
// It's a Project |
|
|
|
// Get the folder the project is in, making sure it ends with '\' |
|
string projectFolder = Path.GetDirectoryName(project.FileName); |
|
if (!projectFolder.EndsWith("\\")) |
|
projectFolder += '\\'; |
|
|
|
// If the .tt file is not under this folder, skip the project |
|
if (!Host.TemplateFile.StartsWith(projectFolder, StringComparison.OrdinalIgnoreCase)) |
|
return null; |
|
|
|
// Get the relative path to the .tt file inside the project |
|
string t4SubPath = Host.TemplateFile.Substring(projectFolder.Length); |
|
|
|
// If it has the T4 file we're looking for, it's the one we want |
|
ProjectItem projectItem = GetProjectItem(project, t4SubPath); |
|
if (projectItem != null) { |
|
// If the .tt file is not opened, open it |
|
if (projectItem.Document == null) |
|
projectItem.Open(Constants.vsViewKindCode); |
|
|
|
if (AlwaysKeepTemplateDirty) { |
|
// Mark the .tt file as unsaved. This way it will be saved and update itself next time the |
|
// project is built. Basically, it keeps marking itself as unsaved to make the next build work. |
|
// Note: this is certainly hacky, but is the best I could come up with so far. |
|
projectItem.Document.Saved = false; |
|
} |
|
|
|
return project; |
|
} |
|
} |
|
else if (project.ProjectItems != null) { |
|
// It may be a solution folder. Need to recurse. |
|
|
|
foreach (ProjectItem item in project.ProjectItems) { |
|
if (item.SubProject == null) |
|
continue; |
|
|
|
Project foundProject = GetProjectContainingT4File(item.SubProject); |
|
if (foundProject != null) |
|
return foundProject; |
|
} |
|
} |
|
|
|
return null; |
|
} |
|
|
|
void ProcessControllersFolder(Project project) { |
|
// Get the Controllers folder |
|
ProjectItem controllerProjectItem = GetProjectItem(project, ControllersFolder); |
|
if (controllerProjectItem == null) |
|
return; |
|
|
|
ProcessControllersRecursive(controllerProjectItem); |
|
} |
|
|
|
void ProcessControllersRecursive(ProjectItem projectItem) { |
|
|
|
// 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); |
|
} |
|
|
|
if (projectItem.FileCodeModel != null) { |
|
// Process all the elements that are namespaces |
|
foreach (CodeNamespace ns in GetNamespaces(projectItem.FileCodeModel.CodeElements)) { |
|
ProcessControllerTypesInNamespace(ns); |
|
} |
|
} |
|
} |
|
|
|
void ProcessControllerTypesInNamespace(CodeNamespace ns) { |
|
foreach (CodeClass2 type in GetClasses(ns.Members)) { |
|
|
|
// Only process types that end with Controller |
|
// REVIEW: this check is not super reliable. Should look at base class. |
|
if (!type.Name.EndsWith(ControllerSuffix, StringComparison.OrdinalIgnoreCase)) |
|
continue; |
|
|
|
// Don't process generic classes (their concrete derived classes will be processed) |
|
if (type.IsGeneric) |
|
continue; |
|
|
|
// 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)); |
|
continue; |
|
} |
|
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() { |
|
Namespace = ns.Name, |
|
ClassName = type.Name |
|
}; |
|
|
|
// Either process new ControllerInfo or integrate results into existing object for partially defined controllers |
|
var target = Controllers.Add(controllerInfo) ? controllerInfo : 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) { |
|
|
|
// 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 |
|
foreach (Project prj in Dte.Solution.Projects) { |
|
// Skip it if it's the current project or doesn't have a code model |
|
if (prj == Project || prj.CodeModel == null) |
|
continue; |
|
|
|
// 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; |
|
} |
|
} |
|
} |
|
|
|
foreach (CodeFunction2 method in GetMethods(type)) { |
|
// Ignore non-public methods |
|
if (method.Access != vsCMAccess.vsCMAccessPublic) |
|
continue; |
|
|
|
// This takes care of avoiding generic types which cause method.Type.CodeType to blow up |
|
if (method.Type.TypeKind != vsCMTypeRef.vsCMTypeRefCodeType) |
|
continue; |
|
|
|
// We only support action methods that return an ActionResult derived type |
|
if (!method.Type.CodeType.get_IsDerivedFrom("System.Web.Mvc.ActionResult")) { |
|
Warning(String.Format("{0} doesn't support {1}.{2} because it doesn't return a supported ActionResult type", T4FileName, type.Name, method.Name)); |
|
continue; |
|
} |
|
|
|
// If we haven't yet seen this return type, keep track of it |
|
if (!ResultTypes.ContainsKey(method.Type.CodeType.Name)) { |
|
var resTypeInfo = new ResultTypeInfo(method.Type.CodeType); |
|
|
|
ResultTypes[method.Type.CodeType.Name] = resTypeInfo; |
|
} |
|
|
|
// Make sure the method is virtual |
|
if (!method.CanOverride) { |
|
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, controllerInfo)); |
|
} |
|
} |
|
} |
|
|
|
void ProcessAllViews(Project project) { |
|
// Get the Views folder |
|
ProjectItem viewsProjectItem = GetProjectItem(project, ViewsRootFolder); |
|
if (viewsProjectItem == null) |
|
return; |
|
|
|
// 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; |
|
|
|
ControllerInfo controller; |
|
// Treat Shared as a pseudo-controller for consistency |
|
if (item.Name.Equals("Shared", StringComparison.OrdinalIgnoreCase)) { |
|
controller = new ControllerInfo() { SharedViewFolder = true, Namespace = T4MVCNamespace, ClassName = "Shared" + ControllerSuffix }; |
|
Controllers.Add(controller); |
|
} |
|
else { |
|
// Find the controller for this view folder |
|
controller = Controllers.SingleOrDefault(c => c.Name.Equals(item.Name, StringComparison.OrdinalIgnoreCase)); |
|
|
|
if (controller == null) { |
|
Error(String.Format("The Views folder has a sub-folder named '{0}', but there is no matching controller", item.Name)); |
|
continue; |
|
} |
|
} |
|
|
|
AddViewsRecursive(String.Empty, item.ProjectItems, controller.ViewsFolder); |
|
} |
|
} |
|
|
|
void AddViewsRecursive(string prefix, ProjectItems items, ViewsFolderInfo viewsFolder) { |
|
// Go through all the files in the subfolder to get the view names |
|
foreach (ProjectItem item in items) { |
|
string viewName = Path.GetFileNameWithoutExtension(item.Name); |
|
if (item.Kind == Constants.vsProjectItemKindPhysicalFile) { |
|
if (Path.GetExtension(item.Name).Equals(".master", StringComparison.OrdinalIgnoreCase)) |
|
continue; // ignore master files |
|
viewsFolder.Views.Add(viewName); |
|
} |
|
else if (item.Kind == Constants.vsProjectItemKindPhysicalFolder) { |
|
var subViewFolder = new ViewsFolderInfo() { FullName = prefix + viewName }; |
|
if (subViewFolder.Name.Equals("App_LocalResources", StringComparison.OrdinalIgnoreCase)) |
|
continue; |
|
viewsFolder.SubFolders.Add(subViewFolder); |
|
AddViewsRecursive(prefix + viewName + "/", item.ProjectItems, subViewFolder); |
|
} |
|
} |
|
} |
|
|
|
void RenderControllerViews(ControllerInfo controller) { |
|
PushIndent(" "); |
|
RenderViewsRecursive(controller.ViewsFolder); |
|
PopIndent(); |
|
} |
|
|
|
void RenderViewsRecursive(ViewsFolderInfo viewsFolder) { |
|
|
|
// For each view, generate a readonly string |
|
foreach (string view in viewsFolder.Views) { |
|
WriteLine("public readonly string " + Sanitize(view) + " = \"" + viewsFolder.GetFullViewName(view) + "\";"); |
|
} |
|
|
|
// For each sub folder, generate a class and recurse |
|
foreach (var subFolder in viewsFolder.SubFolders) { |
|
string newClassName = Sanitize(subFolder.Name);#> |
|
static readonly _<#=newClassName#> s_<#=newClassName#> = new _<#=newClassName#>(); |
|
public _<#=newClassName#> <#=newClassName#> { get { return s_<#=newClassName#>; } } |
|
public partial class _<#=newClassName#>{ |
|
<#+ |
|
PushIndent(" "); |
|
RenderViewsRecursive(subFolder); |
|
PopIndent(); |
|
|
|
WriteLine("}"); |
|
} |
|
} |
|
|
|
void ProcessStaticFiles(Project project, string folder) { |
|
|
|
ProjectItem folderProjectItem = GetProjectItem(project, folder); |
|
if (folderProjectItem != null) { |
|
ProcessStaticFilesRecursive(folderProjectItem, "~"); |
|
} |
|
} |
|
|
|
void ProcessStaticFilesRecursive(ProjectItem projectItem, string path) { |
|
|
|
if (IsFolder(projectItem)) { #> |
|
[CompilerGenerated] |
|
public static class @<#=Sanitize(projectItem.Name) #> { |
|
public static string Url() { return VirtualPathUtility.ToAbsolute("<#=path#>/<#=projectItem.Name#>"); } |
|
public static string Url(string fileName) { return VirtualPathUtility.ToAbsolute("<#=path#>/<#=projectItem.Name#>/" + fileName); } |
|
<#+ |
|
PushIndent(" "); |
|
|
|
// Recurse into all the items in the folder |
|
foreach (ProjectItem item in projectItem.ProjectItems) { |
|
ProcessStaticFilesRecursive(item, path + "/" + projectItem.Name); |
|
} |
|
|
|
PopIndent(); |
|
#> |
|
} |
|
|
|
<#+ |
|
} |
|
else { #> |
|
public static readonly string <#=Sanitize(projectItem.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); |
|
} |
|
} |
|
} |
|
|
|
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; |
|
} |
|
|
|
// Return all the CodeNamespaces in the CodeElements collection |
|
static IEnumerable<CodeNamespace> GetNamespaces(CodeElements codeElements) { |
|
return GetElements<CodeNamespace>(codeElements); |
|
} |
|
|
|
// Return all the CodeClass2 in the CodeElements collection |
|
static IEnumerable<CodeClass2> GetClasses(CodeElements codeElements) { |
|
return GetElements<CodeClass2>(codeElements); |
|
} |
|
|
|
// Return all the CodeFunction2 in the CodeElements collection |
|
static IEnumerable<CodeFunction2> GetMethods(CodeClass2 codeClass) { |
|
// Only look at regular method (e.g. ignore things like contructors) |
|
return GetElements<CodeFunction2>(codeClass.Members) |
|
.Where(f => f.FunctionKind == vsCMFunction.vsCMFunctionFunction); |
|
} |
|
|
|
// Check if the class has any explicit constructor |
|
static bool HasExplicitConstructor(CodeClass2 codeClass) { |
|
return GetElements<CodeFunction2>(codeClass.Members).Any( |
|
f => f.FunctionKind == vsCMFunction.vsCMFunctionConstructor); |
|
} |
|
|
|
// Check if the class has a default (i.e. no params) constructor |
|
static bool HasExplicitDefaultConstructor(CodeClass2 codeClass) { |
|
return GetElements<CodeFunction2>(codeClass.Members).Any( |
|
f => 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); |
|
} |
|
|
|
static IEnumerable<T> GetElements<T>(CodeElements codeElements) where T : class { |
|
|
|
// Note: this code can be simplified to just: |
|
// return codeElements.OfType<T>(); |
|
// But this breaks on VS2010 beta due to a VS bug (that should be fixed in VS2010 beta 2). |
|
// For now, work around using this alternate code which avoids getting the enumerator |
|
|
|
for (int i = 1; i <= codeElements.Count; i++) { |
|
var codeNamespace = codeElements.Item(i) as T; |
|
|
|
if (codeNamespace != null) |
|
yield return codeNamespace; |
|
} |
|
} |
|
|
|
// Return whether a ProjectItem is a folder and not a file |
|
static bool IsFolder(ProjectItem item) { |
|
return (item.Kind == Constants.vsProjectItemKindPhysicalFolder); |
|
} |
|
|
|
static string Sanitize(string token) { |
|
// 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; |
|
} |
|
|
|
// Data structure to collect data about a controller class |
|
class ControllerInfo { |
|
public ControllerInfo() { |
|
ActionMethods = new HashSet<ActionMethodInfo>(); |
|
ViewsFolder = new ViewsFolderInfo(); |
|
} |
|
|
|
// True when this is not a real controller, but a placeholder for the Shared views folder |
|
public bool SharedViewFolder { 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 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 Namespace + "." + ClassName; |
|
} |
|
} |
|
|
|
public string DerivedClassName { |
|
get { |
|
if (SharedViewFolder) |
|
return FullClassName; |
|
return "T4MVC_" + ClassName; |
|
} |
|
} |
|
|
|
public HashSet<ActionMethodInfo> ActionMethods { get; set; } |
|
|
|
IEnumerable<ActionMethodInfo> ActionMethodsWithNoParameters { |
|
get { |
|
return ActionMethods.Where(m => m.Parameters.Count == 0); |
|
} |
|
} |
|
|
|
public IEnumerable<ActionMethodInfo> ActionMethodsUniqueWithoutParameterlessOverload { |
|
get { |
|
return ActionMethodsWithUniqueNames.Except(ActionMethodsWithNoParameters, new ActionComparer()); |
|
} |
|
} |
|
|
|
// Return a list of actions without duplicate names (even with multiple overloads) |
|
public IEnumerable<ActionMethodInfo> ActionMethodsWithUniqueNames { |
|
get { |
|
return ActionMethods.Distinct(new ActionComparer()); |
|
} |
|
} |
|
|
|
class ActionComparer : IEqualityComparer<ActionMethodInfo> { |
|
public bool Equals(ActionMethodInfo x, ActionMethodInfo y) { |
|
return x.ActionName == y.ActionName; |
|
} |
|
|
|
public int GetHashCode(ActionMethodInfo obj) { |
|
return obj.ActionName.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 HashSet<string>(); |
|
SubFolders = new List<ViewsFolderInfo>(); |
|
} |
|
|
|
public string FullName { get; set; } |
|
public string Name { |
|
get { |
|
return FullName.Substring(FullName.LastIndexOf("/") + 1); |
|
} |
|
} |
|
public HashSet<string> Views { get; private set; } |
|
public List<ViewsFolderInfo> SubFolders { get; set; } |
|
|
|
public string GetFullViewName(string viewName) { |
|
if (String.IsNullOrEmpty(FullName)) |
|
return viewName; |
|
|
|
return FullName + "/" + viewName; |
|
} |
|
} |
|
|
|
// Data structure to collect data about a method |
|
class FunctionInfo { |
|
protected CodeFunction2 _method; |
|
private string _signature; |
|
|
|
public FunctionInfo(CodeFunction2 method) { |
|
_method = method; |
|
|
|
// Build a unique signature for the method, used to avoid duplication |
|
_signature = method.Name; |
|
|
|
// Process all the parameters |
|
Parameters = new List<MethodParamInfo>(); |
|
foreach (var p in GetElements<CodeParameter2>(method.Parameters)) { |
|
Parameters.Add( |
|
new MethodParamInfo() { |
|
Name = p.Name, |
|
Type = p.Type.AsString |
|
}); |
|
_signature += "," + p.Type.AsString; |
|
} |
|
} |
|
|
|
public string Name { get { return _method.Name; } } |
|
public string ReturnType { get { return _method.Type.CodeType.Name; } } |
|
public bool IsPublic { get { return _method.Access == vsCMAccess.vsCMAccessPublic; } } |
|
public List<MethodParamInfo> Parameters { get; private set; } |
|
|
|
// Write out all the parameters as part of a method declaration |
|
public void WriteFormalParameters(bool first) { |
|
foreach (var p in Parameters) { |
|
if (first) |
|
first = false; |
|
else |
|
TT.Write(", "); |
|
|
|
TT.Write(p.Type + " " + p.Name); |
|
} |
|
} |
|
|
|
// 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(", "); |
|
|
|
switch (p.Type) { |
|
case "string": |
|
TT.Write("\" \""); |
|
break; |
|
case "byte[]": |
|
TT.Write("new byte[0]"); |
|
break; |
|
default: |
|
TT.Write("null"); |
|
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, ControllerInfo controller): base(method) { |
|
// Normally, the action name is the method name. But if there is an [ActionName] on |
|
// the method, get the expression from that instead |
|
ActionNameValueExpression = '"' + Name + '"'; |
|
for (int i = 1; i <= method.Attributes.Count; i++) { |
|
var attrib = (CodeAttribute2)method.Attributes.Item(i); |
|
if (attrib.FullName == "System.Web.Mvc.ActionNameAttribute") { |
|
var arg = (CodeAttributeArgument)attrib.Arguments.Item(1); |
|
ActionNameValueExpression = arg.Value; |
|
} |
|
} |
|
} |
|
|
|
public string ActionName { get { return Name; } } |
|
public string ActionNameValueExpression { get; set; } |
|
} |
|
|
|
// Data about an ActionResult derived type |
|
class ResultTypeInfo { |
|
CodeType _codeType; |
|
public ResultTypeInfo(CodeType codeType) { |
|
_codeType = codeType; |
|
|
|
var ctor = GetElements<CodeFunction2>(_codeType.Members).FirstOrDefault( |
|
f => f.FunctionKind == vsCMFunction.vsCMFunctionConstructor); |
|
Constructor = new FunctionInfo(ctor); |
|
} |
|
|
|
public string Name { get { return _codeType.Name; } } |
|
public FunctionInfo Constructor { get; set; } |
|
public IEnumerable<FunctionInfo> AbstractMethods { |
|
get { |
|
return GetElements<CodeFunction2>(_codeType.Members).Where( |
|
f => f.MustImplement).Select(f => new FunctionInfo(f)); |
|
} |
|
} |
|
} |
|
|
|
class MethodParamInfo { |
|
public string Name { get; set; } |
|
public string Type { get; set; } |
|
} |
|
|
|
#> |