2886 lines
98 KiB
Plaintext
2886 lines
98 KiB
Plaintext
<#
|
|
/*
|
|
T4MVC Version 3.17.5
|
|
Find latest version and documentation at https://github.com/T4MVC/T4MVC/wiki/Documentation
|
|
Discuss on StackOverflow or on GitHub (https://github.com/T4MVC/T4MVC/issues)
|
|
|
|
T4MVC is part of the MvcContrib project, but in a different GitHub site (https://github.com/T4MVC/T4MVC)
|
|
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 license (https://github.com/T4MVC/T4MVC/blob/master/License.txt)
|
|
*/
|
|
#>
|
|
<#@ template language="C#" debug="true" hostspecific="true" #>
|
|
<#@ assembly name="System.Core" #>
|
|
<#@ 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(); #>// <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.
|
|
|
|
// Make sure the compiler doesn't complain about missing Xml comments and CLS compliance
|
|
// 0108: suppress "Foo hides inherited member Foo. Use the new keyword if hiding was intended." when a controller and its abstract parent are both processed
|
|
// 0114: suppress "Foo.BarController.Baz()' hides inherited member 'Qux.BarController.Baz()'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword." when an action (with an argument) overrides an action in a parent controller
|
|
#pragma warning disable 1591, 3008, 3009, 0108, 0114
|
|
#region T4MVC
|
|
|
|
using System;
|
|
using System.Diagnostics;
|
|
using System.CodeDom.Compiler;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Threading.Tasks;
|
|
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 partial 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 #>
|
|
{
|
|
<#
|
|
var inAreas = Areas
|
|
.Where(a => !string.IsNullOrEmpty(a.Name))
|
|
.SelectMany(a => settings.StaticFilesFolders.Select(b => settings.AreasFolder + "\\" + a.Name + "\\" + b));
|
|
|
|
foreach (string folder in settings.StaticFilesFolders
|
|
.Concat(GetStaticFilesViewFolders())
|
|
.Concat(inAreas)
|
|
) {
|
|
ProcessStaticFiles(Project, folder);
|
|
}
|
|
|
|
PushIndent(" ");
|
|
#>
|
|
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
public static partial class Bundles
|
|
{
|
|
<#
|
|
foreach (string folder in settings.StaticFilesFolders
|
|
.Concat(GetStaticFilesViewFolders())
|
|
.Concat(inAreas)
|
|
) {
|
|
ProcessBundles(Project, folder);
|
|
}
|
|
|
|
PopIndent();
|
|
#>
|
|
}
|
|
}
|
|
|
|
<#
|
|
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 RedirectToAction(Task<ActionResult> taskResult)
|
|
{
|
|
return RedirectToAction(taskResult.Result);
|
|
}
|
|
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
protected RedirectToRouteResult RedirectToActionPermanent(ActionResult result)
|
|
{
|
|
var callInfo = result.GetT4MVCResult();
|
|
return RedirectToRoutePermanent(callInfo.RouteValueDictionary);
|
|
}
|
|
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
protected RedirectToRouteResult RedirectToActionPermanent(Task<ActionResult> taskResult)
|
|
{
|
|
return RedirectToActionPermanent(taskResult.Result);
|
|
}
|
|
|
|
<#foreach (var method in controller.ActionMethodsUniqueWithoutParameterlessOverload) { #>
|
|
[NonAction]
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
public virtual <#=method.ReturnTypeFullName #> <#=method.Name #>()
|
|
{
|
|
<#if (method.IsTaskBased) { #>
|
|
var callInfo = new T4MVC_<#=method.ReturnTypeUniqueName #>(Area, Name, ActionNames.<#=method.ActionName #><# if (method.ActionUrlHttps) {#>, "https"<#}#>);
|
|
return System.Threading.Tasks.Task.FromResult(callInfo as <#=method.TaskActionTypeFullName #>);
|
|
<#} 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.IsTaskBased) { #>
|
|
return System.Threading.Tasks.Task.FromResult(callInfo as <#=method.TaskActionTypeFullName #>);
|
|
<#} 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) #>";
|
|
[<#= GeneratedCode #>]
|
|
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)) { #>
|
|
[NonAction]
|
|
partial void <#=method.Name #>Override(T4MVC_<#=method.ReturnTypeUniqueName #> callInfo<#if (method.Parameters.Count > 0) { #>, <#method.WriteFormalParameters(true); #><#}#>);
|
|
|
|
[NonAction]
|
|
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.IsTaskBased) { #>
|
|
return System.Threading.Tasks.Task.FromResult(callInfo as <#=method.TaskActionTypeFullName #>);
|
|
<#} 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);
|
|
#>
|
|
///<summary>
|
|
///Render the <b><#= partialName #></b> partial.
|
|
///</summary>
|
|
public static void <#= partialRenderMethod #>(this HtmlHelper html) {
|
|
html.RenderPartial("<#= partialPath #>");
|
|
}
|
|
|
|
///<summary>
|
|
///Render the <b><#= partialName #></b> partial.
|
|
///</summary>
|
|
public static void <#= partialRenderMethod #>(this HtmlHelper html, object model) {
|
|
html.RenderPartial("<#= partialPath #>", model);
|
|
}
|
|
<# } #>
|
|
}
|
|
}
|
|
<# manager.EndBlock(); #>
|
|
<# } #>
|
|
|
|
<#manager.StartFooter(); #>
|
|
#endregion T4MVC
|
|
#pragma warning restore 1591, 3008, 3009, 0108, 0114
|
|
<#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<AreaInfo> Areas;
|
|
static AreaInfo DefaultArea;
|
|
static Dictionary<string, ResultTypeInfo> 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<string> virtualPathesForStaticFiles = new List<string>();
|
|
|
|
static CodeTypeRef TryCreateActionResultDerivedCodeTypeRef(CodeType codeType) {
|
|
return codeType.get_IsDerivedFrom("System.Web.Mvc.ActionResult")
|
|
? Project.CodeModel.CreateCodeTypeRef(codeType.FullName)
|
|
: null;
|
|
}
|
|
|
|
enum ActionTypeMatch {
|
|
None = 0,
|
|
Direct = 1,
|
|
TaskBased = 2
|
|
}
|
|
|
|
static ActionTypeMatch TryGetActionType(CodeType codeType, out CodeTypeRef actionTypeRef) {
|
|
// check for task based
|
|
Match match = Regex.Match(codeType.FullName, "^System.Threading.Tasks.Task<(.+)>$");
|
|
if (match == null || !match.Success) {
|
|
actionTypeRef = TryCreateActionResultDerivedCodeTypeRef(codeType);
|
|
return actionTypeRef != null
|
|
? ActionTypeMatch.Direct
|
|
: ActionTypeMatch.None;
|
|
} else {
|
|
CodeTypeRef typeRef = Project.CodeModel.CreateCodeTypeRef(match.Groups[1].Value);
|
|
if (typeRef.CodeType.get_IsDerivedFrom("System.Web.Mvc.ActionResult"))
|
|
actionTypeRef = typeRef;
|
|
else
|
|
actionTypeRef = null;
|
|
return actionTypeRef != null
|
|
? ActionTypeMatch.TaskBased
|
|
: ActionTypeMatch.None;
|
|
}
|
|
}
|
|
|
|
|
|
IEnumerable<ControllerInfo> GetControllers()
|
|
{
|
|
var controllers = new List<ControllerInfo>();
|
|
|
|
foreach (var area in Areas)
|
|
{
|
|
controllers.AddRange(area.GetControllers());
|
|
}
|
|
|
|
return controllers;
|
|
}
|
|
|
|
IEnumerable<ControllerInfo> GetAbstractControllers()
|
|
{
|
|
var controllers = new List<ControllerInfo>();
|
|
|
|
foreach (var area in Areas)
|
|
{
|
|
controllers.AddRange(area.GetAbstractControllers());
|
|
}
|
|
|
|
return controllers;
|
|
}
|
|
|
|
IDictionary<string, string> GetPartials()
|
|
{
|
|
var parts = GetControllers()
|
|
.Select(m => m.ViewsFolder)
|
|
.SelectMany(m => m.Views)
|
|
.Where(m => IsPartialView(m.Value));
|
|
|
|
var partsDic = new Dictionary<string, KeyValuePair<string, string>>();
|
|
|
|
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<AreaInfo>();
|
|
ResultTypes = new Dictionary<string, ResultTypeInfo>();
|
|
|
|
// Get the DTE service from the host
|
|
var serviceProvider = Host as IServiceProvider;
|
|
if (serviceProvider != null)
|
|
{
|
|
Dte = (EnvDTE.DTE)serviceProvider.GetCOMService(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) + '\\';
|
|
|
|
// Process controllers and views from project root
|
|
ProcessProjectRoot(Project);
|
|
|
|
// Process controllers and views from areas.
|
|
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 ProcessProjectRoot(Project project)
|
|
{
|
|
var area = new AreaInfo() { Name = null };
|
|
|
|
// process controllers and views from their folders
|
|
ProcessAreaControllers(project.ProjectItems, area, null);
|
|
ProcessAreaViews(project.ProjectItems, area, null);
|
|
|
|
// does the project root contain a folder which follows the "feature folder" convention?
|
|
// If yes, controllers and views from there are added to the existing default area.
|
|
// this means, if you use both conventions, you cannot have two controllers with the same name!
|
|
if (!String.IsNullOrEmpty(settings.FeatureFolderRootArea))
|
|
{
|
|
ProjectItem featureFolderItem = GetProjectItem(project, settings.FeatureFolderRootArea);
|
|
if (featureFolderItem != null)
|
|
{
|
|
ProcessArea(featureFolderItem, area);
|
|
}
|
|
}
|
|
|
|
Areas.Add(area);
|
|
DefaultArea = area;
|
|
}
|
|
|
|
void ProcessAreas(Project project)
|
|
{
|
|
// 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, null);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process portable areas
|
|
foreach (string portableArea in settings.PortableAreas)
|
|
{
|
|
ProjectItem portableAreaProjectItem = GetProjectItem(project, portableArea);
|
|
|
|
if (portableAreaProjectItem == null)
|
|
return;
|
|
|
|
if (IsFolder(portableAreaProjectItem))
|
|
{
|
|
ProcessArea(portableAreaProjectItem, null);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ProcessArea(ProjectItem areaFolder, AreaInfo existingArea)
|
|
{
|
|
var area = existingArea ?? new AreaInfo() { Name = areaFolder.Name };
|
|
area.IsFeatureFolderArea = IsFeatureFolderArea(area.Name, areaFolder);
|
|
|
|
ProcessAreaControllers(areaFolder.ProjectItems, area, areaFolder);
|
|
ProcessAreaViews(areaFolder.ProjectItems, area, areaFolder);
|
|
|
|
if (existingArea == null)
|
|
Areas.Add(area);
|
|
}
|
|
|
|
void ProcessAreaControllers(ProjectItems areaFolderItems, AreaInfo area, ProjectItem areaFolder)
|
|
{
|
|
if (area.IsFeatureFolderArea)
|
|
{
|
|
// Every folder within the area might contain controllers when using "FeatureFolders".
|
|
ProcessControllersRecursive(areaFolder, area);
|
|
}
|
|
else
|
|
{
|
|
// Get area Controllers folder
|
|
ProjectItem controllerProjectItem = GetProjectItem(areaFolderItems, settings.ControllersFolder);
|
|
if (controllerProjectItem == null)
|
|
return;
|
|
|
|
ProcessControllersRecursive(controllerProjectItem, area);
|
|
}
|
|
}
|
|
|
|
void ProcessAreaViews(ProjectItems areaFolderItems, AreaInfo area, ProjectItem areaFolder)
|
|
{
|
|
ProjectItem viewsProjectItem;
|
|
|
|
if (area.IsFeatureFolderArea)
|
|
{
|
|
// For "FeatureFolders", we don't have to search for a "Views"-folder since the area directly contains the feature-folders holding the views.
|
|
viewsProjectItem = areaFolder;
|
|
}
|
|
else
|
|
{
|
|
// Get area Views folder
|
|
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<CodeClass2>())
|
|
{
|
|
ProcessControllerType(type, area, controllerLastWriteTime);
|
|
}
|
|
// Process all the elements that are namespaces
|
|
foreach (var ns in projectItem.FileCodeModel.CodeElements.OfType<CodeNamespace>())
|
|
{
|
|
foreach (var type in ns.Members.OfType<CodeClass2>())
|
|
{
|
|
ProcessControllerType(type, area, controllerLastWriteTime);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ProcessControllerType(CodeClass2 type, AreaInfo area, DateTime controllerLastWriteTime)
|
|
{
|
|
// Only process controllers
|
|
if (!IsController(type))
|
|
return;
|
|
|
|
// Only process "processable" controllers
|
|
if (!IsProcessableController(type))
|
|
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)
|
|
if (!HasControllerAttribute(type))
|
|
target.IsAbstract = true;
|
|
// ...unless it has the [T4MVC] attribute, then process all the action methods in the controller
|
|
else
|
|
ProcessControllerActionMethods(target, type);
|
|
}
|
|
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<ActionResult> derived types
|
|
CodeTypeRef methodType;
|
|
ActionTypeMatch match = TryGetActionType(method.Type.CodeType, out methodType);
|
|
if (match == ActionTypeMatch.None)
|
|
{
|
|
// Comment out warning, as it's more annoying than helpful
|
|
//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;
|
|
|
|
// 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, null, match == ActionTypeMatch.TaskBased ? methodType : null));
|
|
}
|
|
}
|
|
}
|
|
|
|
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 = Sanitize(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<string> GetStaticFilesViewFolders()
|
|
{
|
|
if (settings.AddAllViewsFoldersToStaticFilesFolders)
|
|
{
|
|
foreach (var area in Areas)
|
|
{
|
|
if (area.Name == null)
|
|
{
|
|
// project root
|
|
|
|
yield return settings.ViewsRootFolder;
|
|
|
|
if (area.IsFeatureFolderArea)
|
|
{
|
|
foreach (var controller in area.Controllers)
|
|
yield return settings.FeatureFolderRootArea + "\\" + controller.Name;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// areas
|
|
|
|
if (area.IsFeatureFolderArea)
|
|
{
|
|
foreach (var controller in area.Controllers)
|
|
yield return settings.AreasFolder + "\\" + area.Name + "\\" + controller.Name;
|
|
}
|
|
else
|
|
{
|
|
yield return settings.AreasFolder + "\\" + area.Name + "\\" + settings.ViewsRootFolder;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Start of Bundles
|
|
|
|
void ProcessBundles(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("/"));
|
|
}
|
|
ProcessBundleFilesRecursive(folderProjectItem, rootPath);
|
|
}
|
|
}
|
|
|
|
void ProcessBundleFilesRecursive(ProjectItem projectItem, string path)
|
|
{
|
|
int nestedLevel = BuildBundleClassStructureForProvidedPath(path);
|
|
ProcessBundleFilesRecursive(projectItem, path, new HashSet<String>());
|
|
for(int i = 0; i < nestedLevel; ++i)
|
|
{
|
|
#>
|
|
}
|
|
<#+
|
|
PopIndent();
|
|
}
|
|
}
|
|
|
|
void ProcessBundleFilesRecursive(ProjectItem projectItem, string path, HashSet<String> 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<String>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
childrenNameSet.Add(settings.AssetsNamespace);
|
|
childrenNameSet.Add(name);
|
|
|
|
var files = new List<ProjectItem>();
|
|
|
|
if (IsFolder(projectItem))
|
|
{
|
|
PushIndent(" ");
|
|
#>
|
|
public static partial class <#=EscapeID(name)#>
|
|
{
|
|
<#+
|
|
// Recurse into all the items in the folder
|
|
foreach (ProjectItem item in projectItem.ProjectItems)
|
|
{
|
|
if (IsFolder(item))
|
|
{
|
|
ProcessBundleFilesRecursive(
|
|
item,
|
|
path + "/" + projectItem.Name,
|
|
childrenNameSet);
|
|
}
|
|
|
|
if (IsFile(item))
|
|
{
|
|
files.Add(item);
|
|
}
|
|
}
|
|
|
|
BuildBundleConstants(files, path + "/" + projectItem.Name, childrenNameSet);
|
|
#>
|
|
}
|
|
<#+
|
|
PopIndent();
|
|
}
|
|
}
|
|
|
|
void BuildBundleConstants(List<ProjectItem> projectItems, string path, HashSet<String> childrenNameSet)
|
|
{
|
|
PushIndent(" ");
|
|
#>
|
|
public static class <#= settings.AssetsNamespace #>
|
|
{
|
|
<#+
|
|
PushIndent(" ");
|
|
foreach (var projectItem in projectItems)
|
|
{
|
|
// The passed in HashSet is to guarantee uniqueness with our parent and siblings
|
|
string name = SanitizeWithNoConflicts(projectItem.Name, childrenNameSet);
|
|
|
|
if (!settings.ExcludedStaticFileExtensions.Any(extension => projectItem.Name.EndsWith(extension, StringComparison.OrdinalIgnoreCase))) {
|
|
var bundleAssetPath = String.Format("{0}/{1}", path, projectItem.Name);
|
|
|
|
// if it's a non-minified javascript file
|
|
if (projectItem.Name.EndsWith(".js") && !projectItem.Name.Contains("-vsdoc"))
|
|
{
|
|
#>
|
|
public const string <#=name#> = "<#=bundleAssetPath#>";
|
|
<#+
|
|
}
|
|
else if (projectItem.Name.EndsWith(".css"))
|
|
{
|
|
#>
|
|
public const string <#=name#> = "<#=bundleAssetPath#>";
|
|
<#+
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
ProcessBundleFilesRecursive(item, path, childrenNameSet);
|
|
}
|
|
}
|
|
PopIndent();
|
|
#>
|
|
}
|
|
<#+
|
|
PopIndent();
|
|
}
|
|
|
|
int BuildBundleClassStructureForProvidedPath(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);
|
|
}
|
|
|
|
PushIndent(" ");
|
|
#>
|
|
public static partial class <#=className #>
|
|
{
|
|
<#+
|
|
parentFolder = folder;
|
|
}
|
|
return folders.Length;
|
|
}
|
|
|
|
// End of Bundles
|
|
|
|
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<String>());
|
|
for(int i = 0; i < nestedLevel; ++i) {#>
|
|
}
|
|
<#+
|
|
PopIndent();
|
|
}
|
|
}
|
|
|
|
void ProcessStaticFilesRecursive(ProjectItem projectItem, string path, HashSet<String> 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<String>();
|
|
childrenNameSet.Add(name);
|
|
|
|
if (IsFolder(projectItem))
|
|
{
|
|
#>
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
public static class <#=EscapeID(name)#> {
|
|
public 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 { #>
|
|
<#+
|
|
var mapping = new Dictionary<string, Tuple<string, string>>();
|
|
mapping.Add(".ts", Tuple.Create(".js", ".min.js"));
|
|
mapping.Add(".tsx", Tuple.Create(".js", ".min.js"));
|
|
mapping.Add(".js", Tuple.Create(".js", ".min.js"));
|
|
mapping.Add(".css", Tuple.Create(".css", ".min.css"));
|
|
|
|
if (!settings.ExcludedStaticFileExtensions.Any(extension => projectItem.Name.EndsWith(extension, StringComparison.OrdinalIgnoreCase))) {
|
|
|
|
var extension = Path.GetExtension(projectItem.Name);
|
|
|
|
if(mapping.ContainsKey(extension))
|
|
{
|
|
var map = mapping[extension];
|
|
|
|
if (!projectItem.Name.EndsWith(map.Item2)) {
|
|
string nonMinifiedName = projectItem.Name.Replace(extension, map.Item1);
|
|
string minifiedName = projectItem.Name.Replace(extension, map.Item2);
|
|
if (AddTimestampToStaticLink(projectItem)) { #>
|
|
public static readonly string <#=name#> = T4MVCHelpers.IsProduction() && T4Extensions.FileExists(UrlPath + "/<#=minifiedName#>") ? Url("<#=minifiedName#>")+"?"+T4MVCHelpers.TimestampString(UrlPath + "/<#=minifiedName#>") : Url("<#=nonMinifiedName#>")+"?"+T4MVCHelpers.TimestampString(UrlPath + "/<#=nonMinifiedName#>");
|
|
<#+} else {#>
|
|
public static readonly string <#=name#> = T4MVCHelpers.IsProduction() && T4Extensions.FileExists(UrlPath + "/<#=minifiedName#>") ? Url("<#=minifiedName#>") : Url("<#=nonMinifiedName#>");
|
|
<#+} #>
|
|
<#+}
|
|
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#>");
|
|
<#+}
|
|
}
|
|
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 #> {
|
|
public 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() sometimes throws when it doesn't exist, so catch the exception
|
|
// to return null instead.
|
|
current = items.Item(name);
|
|
|
|
if (current == null) return null;
|
|
}
|
|
catch
|
|
{
|
|
// If any chunk couldn't be found, fail
|
|
return null;
|
|
}
|
|
|
|
items = current.ProjectItems;
|
|
}
|
|
|
|
return current;
|
|
}
|
|
|
|
static bool IsController(CodeClass2 type)
|
|
{
|
|
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 bool IsProcessableController(CodeClass2 type)
|
|
{
|
|
// try get the [T4MVC] attribute
|
|
var attribute = GetAttribute(type.Attributes, "System.Web.Mvc.T4MVCAttribute");
|
|
|
|
// if [T4MVC(false)] was specified, then ignore this controller
|
|
if ((attribute != null) && (attribute.Value == "false")) return false;
|
|
|
|
// if [T4MVC] or [T4MVC(true)] was specified, then process this controller
|
|
if ((attribute != null) && (attribute.Value == "" || attribute.Value == "true")) return true;
|
|
|
|
// ...else attribute was not specified
|
|
|
|
// ignore any class whose name doesn't end with "Controller"
|
|
if (!type.FullName.EndsWith(ControllerSuffix)) return false;
|
|
|
|
// don't process a generic controller (its concrete derived classes will be processed)
|
|
if (type.IsGeneric)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool HasControllerAttribute(CodeClass2 type)
|
|
{
|
|
// try get the [T4MVC] attribute
|
|
var attribute = GetAttribute(type.Attributes, "System.Web.Mvc.T4MVCAttribute");
|
|
|
|
// see whether [T4MVC] or [T4MVC(true)] were specified
|
|
if (attribute != null)
|
|
if (attribute.Value == "" || attribute.Value == "true")
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
static string GetVirtualPath(ProjectItem item)
|
|
{
|
|
string fileFullPath = item.get_FileNames(0);
|
|
|
|
// update full path for files that are not under the app root (e.g. they could be linked files)
|
|
if (!fileFullPath.StartsWith(AppRoot, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
try
|
|
{
|
|
fileFullPath = ((ProjectItem)item.Collection.Parent).Properties.Item("FullPath").Value.ToString() + item.Name;
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// 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<CodeFunction2> GetMethods(CodeClass2 codeClass)
|
|
{
|
|
// Only look at regular method (e.g. ignore things like contructors)
|
|
return codeClass.Members.OfType<CodeFunction2>()
|
|
.Where(f => TestFunctionKind(f, vsCMFunction.vsCMFunctionFunction));
|
|
}
|
|
|
|
// Check if the class has any explicit constructor
|
|
static bool HasExplicitConstructor(CodeClass2 codeClass)
|
|
{
|
|
return codeClass.Members.OfType<CodeFunction2>().Any(
|
|
f => !f.IsShared && TestFunctionKind(f, vsCMFunction.vsCMFunctionConstructor));
|
|
}
|
|
|
|
// Check if the class has a default (i.e. no params) constructor
|
|
static bool HasExplicitDefaultConstructor(CodeClass2 codeClass)
|
|
{
|
|
return codeClass.Members.OfType<CodeFunction2>().Any(
|
|
f => !f.IsShared && TestFunctionKind(f, 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 bool TestFunctionKind(CodeFunction2 f, vsCMFunction kind)
|
|
{
|
|
try
|
|
{
|
|
return f.FunctionKind == kind;
|
|
}
|
|
catch
|
|
{
|
|
// FunctionKind blows up in some cases. Just ignore.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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 bool IsFeatureFolderArea(string areaName, ProjectItem areaFolder)
|
|
{
|
|
// default-area with regular convention?
|
|
if (areaFolder == null)
|
|
return false;
|
|
|
|
if (String.IsNullOrEmpty(areaName))
|
|
{
|
|
// Are we looking at the "Features" folder in the project root?
|
|
return areaFolder.Name == settings.FeatureFolderRootArea;
|
|
}
|
|
else
|
|
{
|
|
// is it a FeatureFolder area?
|
|
return settings.FeatureFolderAreas.Contains("*") || settings.FeatureFolderAreas.Contains(areaFolder.Name);
|
|
}
|
|
}
|
|
|
|
static bool IsFile(ProjectItem item)
|
|
{
|
|
return (item.Kind == EnvDTE.Constants.vsProjectItemKindPhysicalFile);
|
|
}
|
|
|
|
|
|
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<string> 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);
|
|
}
|
|
|
|
static string SanitizeDefaultValue(string defaultValue, string type)
|
|
{
|
|
// Normalize default values that are
|
|
// 1. default(T) for some type T
|
|
// 2. The parameterless constructor of a value type
|
|
if (Regex.IsMatch(defaultValue, @"^\s*default\s*\(.*?\)"))
|
|
{
|
|
defaultValue = string.Format("default({0})", type);
|
|
}
|
|
else if(Regex.IsMatch(defaultValue, @"^\s*new\s*.*?\s*\(\s*\)"))
|
|
{
|
|
defaultValue = string.Format("new {0}()", type);
|
|
}
|
|
|
|
return defaultValue;
|
|
}
|
|
|
|
// Data structure to collect data about an area
|
|
class AreaInfo
|
|
{
|
|
public AreaInfo()
|
|
{
|
|
Controllers = new HashSet<ControllerInfo>();
|
|
}
|
|
|
|
public string Name { get; set; }
|
|
public HashSet<ControllerInfo> Controllers { get; set; }
|
|
public bool IsFeatureFolderArea { 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<ControllerInfo> GetControllers()
|
|
{
|
|
return Controllers.Where(c => !c.IsAbstract);
|
|
}
|
|
|
|
public IEnumerable<ControllerInfo> GetAbstractControllers()
|
|
{
|
|
return Controllers.Where(c => c.IsAbstract);
|
|
}
|
|
}
|
|
|
|
// Data structure to collect data about a controller class
|
|
class ControllerInfo
|
|
{
|
|
public ControllerInfo()
|
|
{
|
|
ActionMethods = new HashSet<ActionMethodInfo>();
|
|
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
|
|
{
|
|
// remove controller suffix (if it exists)
|
|
string className = ClassName;
|
|
if (ClassName.EndsWith(ControllerSuffix)) {
|
|
className = className.Substring(0, ClassName.Length - ControllerSuffix.Length);
|
|
}
|
|
|
|
return className;
|
|
}
|
|
}
|
|
|
|
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<ActionMethodInfo> ActionMethods { get; set; }
|
|
|
|
public IEnumerable<ActionMethodInfo> CustomActionMethods
|
|
{
|
|
get
|
|
{
|
|
return ActionMethods.Where(m => m.IsCustomReturnType);
|
|
}
|
|
}
|
|
|
|
IEnumerable<ActionMethodInfo> ActionMethodsWithNoParameters
|
|
{
|
|
get
|
|
{
|
|
return ActionMethods.Where(m => m.CanBeCalledWithoutParameters);
|
|
}
|
|
}
|
|
|
|
public IEnumerable<ActionMethodInfo> ActionMethodsUniqueWithoutParameterlessOverload
|
|
{
|
|
get
|
|
{
|
|
return ActionMethodsWithUniqueNames.Except(ActionMethodsWithNoParameters, new ActionComparer());
|
|
}
|
|
}
|
|
|
|
public IEnumerable<ActionMethodInfo> CustomActionMethodsUniqueWithoutParameterlessOverload
|
|
{
|
|
get
|
|
{
|
|
return CustomActionMethodsWithUniqueNames.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());
|
|
}
|
|
}
|
|
|
|
// Return a list of actions without duplicate names (even with multiple overloads)
|
|
public IEnumerable<ActionMethodInfo> CustomActionMethodsWithUniqueNames
|
|
{
|
|
get
|
|
{
|
|
return CustomActionMethods.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 IEnumerable<IGrouping<string, MethodParamInfo>> 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<MethodParamInfo>
|
|
{
|
|
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<string, string>();
|
|
SubFolders = new List<ViewsFolderInfo>();
|
|
}
|
|
|
|
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;
|
|
string virtualPath = GetVirtualPath(item);
|
|
if (virtualPath != null)
|
|
{
|
|
Views[viewFieldName] = useNonQualifiedViewName ? Path.GetFileNameWithoutExtension(viewName) : virtualPath;
|
|
}
|
|
}
|
|
|
|
public bool HasNonQualifiedViewNames { get; private set; }
|
|
public string Name { get; set; }
|
|
public Dictionary<string, string> Views { get; private set; }
|
|
public List<ViewsFolderInfo> SubFolders { get; set; }
|
|
}
|
|
|
|
// Data structure to collect data about a method
|
|
class FunctionInfo
|
|
{
|
|
protected CodeFunction2 _method;
|
|
private string _signature;
|
|
protected CodeTypeRef _taskActionType;
|
|
|
|
public FunctionInfo(CodeFunction2 method, CodeTypeRef taskActionType = null)
|
|
{
|
|
_taskActionType = taskActionType;
|
|
|
|
Parameters = new List<MethodParamInfo>();
|
|
|
|
// 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<CodeParameter2>())
|
|
{
|
|
// 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<CodeAttributeArgument>().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 ? UniqueFullName(_taskActionType) : UniqueFullName(ReturnTypeImpl); } }
|
|
public bool IsPublic { get { return _method.Access == vsCMAccess.vsCMAccessPublic; } }
|
|
public List<MethodParamInfo> Parameters { get; private set; }
|
|
public bool CanBeCalledWithoutParameters { get; private set; }
|
|
|
|
public bool IsTaskBased { get {return _taskActionType != null; } }
|
|
|
|
// 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, CodeTypeRef taskActionType = null)
|
|
: base(method, taskActionType)
|
|
{
|
|
if (taskActionType != null) {
|
|
_taskActionType = taskActionType;
|
|
} else {
|
|
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 {
|
|
if (IsTaskBased)
|
|
return Project.CodeModel.CreateCodeTypeRef("System.Threading.Tasks.Task<" + _taskActionType.AsFullName + ">");
|
|
return _returnType ?? base.ReturnTypeImpl;
|
|
}
|
|
}
|
|
|
|
public string TaskActionTypeFullName { get { return IsTaskBased ? _taskActionType.AsFullName : null; } }
|
|
|
|
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<CodeFunction2>()
|
|
.Where(f => TestFunctionKind(f, 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<FunctionInfo> AbstractMethods
|
|
{
|
|
get
|
|
{
|
|
return _codeType.CodeType.Members.OfType<CodeFunction2>().Where(
|
|
f => f.MustImplement).Select(f => new FunctionInfo(f));
|
|
}
|
|
}
|
|
|
|
private bool IsTaskBased {
|
|
get {
|
|
CodeTypeRef methodType;
|
|
return TryGetActionType(_codeType.CodeType, out methodType) == ActionTypeMatch.TaskBased;
|
|
}
|
|
}
|
|
}
|
|
|
|
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<MvcSettings>(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.FeatureFolderAreas = 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.AssetsNamespace = "Assets";
|
|
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[] {
|
|
".cs",
|
|
".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<string> PortableAreas { get; set; }
|
|
|
|
[System.ComponentModel.Description("Name of folder in your project root that follows the FeatureFolder convention (controllers and views are placed within the same folder)")]
|
|
public string FeatureFolderRootArea { get; set; }
|
|
|
|
[System.ComponentModel.Description("Names of areas which follow the FeatureFolder convention (controllers and views are placed within the same folder).\r\nUse <Area>*</Area> if you only use Areas with FeatureFolders")]
|
|
public IEnumerable<string> FeatureFolderAreas { 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("The namespace that raw URLS used for bundles are generated")]
|
|
public string AssetsNamespace { 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, "></", name, ">"));
|
|
}
|
|
|
|
protected static void WritePropertyDesc(System.Xml.XmlWriter writer, System.ComponentModel.PropertyDescriptor property)
|
|
{
|
|
var desc = property.Attributes.OfType<System.ComponentModel.DescriptionAttribute>().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<string>
|
|
{
|
|
public XmlStringArray(IEnumerable<string> items, string name)
|
|
{
|
|
this._items = items;
|
|
this._name = name;
|
|
SetSaveAsChild(this, true);
|
|
}
|
|
|
|
string _name;
|
|
IEnumerable<string> _items;
|
|
|
|
protected override void Load(System.Xml.Linq.XElement xml)
|
|
{
|
|
var items = new List<string>();
|
|
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<string> 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 settings 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<T>(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<Block> files = new List<Block>();
|
|
private Block footer = new Block();
|
|
private Block header = new Block();
|
|
private ITextTemplatingEngineHost host;
|
|
private StringBuilder template;
|
|
protected List<String> generatedFileNames = new List<String>();
|
|
|
|
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<String> checkOutAction;
|
|
private Action<IEnumerable<String>> 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<String> keepFileNames) => ProjectSync(templateProjectItem, keepFileNames);
|
|
queryEditSave = (IVsQueryEditQuerySave2)hostServiceProvider.GetService(typeof(SVsQueryEditQuerySave));
|
|
}
|
|
|
|
private static void ProjectSync(EnvDTE.ProjectItem templateProjectItem, IEnumerable<String> keepFileNames)
|
|
{
|
|
var keepFileNameSet = new HashSet<String>(keepFileNames);
|
|
var projectFiles = new Dictionary<String, EnvDTE.ProjectItem>();
|
|
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
|
|
*/
|
|
#>
|