2402 lines
83 KiB
Plaintext
2402 lines
83 KiB
Plaintext
<#
|
|
/*
|
|
T4MVC Version 3.6.4
|
|
Find latest version and documentation at http://mvccontrib.codeplex.com/wikipage?title=T4MVC
|
|
Discuss on StackOverflow or on Codeplex (https://t4mvc.codeplex.com/discussions)
|
|
|
|
T4MVC is part of the MvcContrib project, but in a different Codeplex location (http://t4mvc.codeplex.com)
|
|
Maintained by David Ebbo, with much feedback from the MVC community (thanks all!)
|
|
david.ebbo@microsoft.com
|
|
http://twitter.com/davidebbo
|
|
http://blog.davidebbo.com/ (previously: http://blogs.msdn.com/davidebb)
|
|
|
|
Related blog posts: http://blogs.msdn.com/davidebb/archive/tags/T4MVC/default.aspx
|
|
|
|
Please use in accordance to the MvcContrib license (http://mvccontrib.codeplex.com/license)
|
|
*/
|
|
#>
|
|
<#@ template language="C#" debug="true" hostspecific="true" #>
|
|
<#@ assembly name="System.Core" #>
|
|
<#@ assembly name="Microsoft.VisualStudio.Shell.Interop" #>
|
|
<#@ assembly name="EnvDTE" #>
|
|
<#@ assembly name="EnvDTE80" #>
|
|
<#@ assembly name="VSLangProj" #>
|
|
<#@ assembly name="System.Xml" #>
|
|
<#@ assembly name="System.Xml.Linq" #>
|
|
<#@ import namespace="System.Collections.Generic" #>
|
|
<#@ import namespace="System.IO" #>
|
|
<#@ import namespace="System.Linq" #>
|
|
<#@ import namespace="System.Text" #>
|
|
<#@ import namespace="System.Text.RegularExpressions" #>
|
|
<#@ import namespace="Microsoft.VisualStudio.Shell.Interop" #>
|
|
<#@ import namespace="EnvDTE" #>
|
|
<#@ import namespace="EnvDTE80" #>
|
|
<#@ import namespace="Microsoft.VisualStudio.TextTemplating" #>
|
|
<# // To debug, uncomment the next two lines !!
|
|
// System.Diagnostics.Debugger.Launch();
|
|
// System.Diagnostics.Debugger.Break();
|
|
#>
|
|
<#settings=MvcSettings.Load(Host);#>
|
|
<#PrepareDataToRender(this); #>
|
|
<#var manager = Manager.Create(Host, GenerationEnvironment); #>
|
|
<#manager.StartHeader(); #>// <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
|
|
#pragma warning disable 1591
|
|
#region T4MVC
|
|
|
|
using System;
|
|
using System.Diagnostics;
|
|
using System.CodeDom.Compiler;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Web;
|
|
using System.Web.Hosting;
|
|
using System.Web.Mvc;
|
|
using System.Web.Mvc.Ajax;
|
|
using System.Web.Mvc.Html;
|
|
using System.Web.Routing;
|
|
using <#=settings.T4MVCNamespace #>;
|
|
<#foreach (var referencedNamespace in settings.ReferencedNamespaces) { #>
|
|
using <#=referencedNamespace #>;
|
|
<#} #>
|
|
<#manager.EndBlock(); #>
|
|
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
public static class <#=settings.HelpersPrefix #>
|
|
{
|
|
<#if (settings.IncludeAreasToken) { #>
|
|
public static class Areas
|
|
{
|
|
<#} #>
|
|
<#foreach (var area in Areas.Where(a => !string.IsNullOrEmpty(a.Name))) { #>
|
|
static readonly <#=area.Name #>Class s_<#=area.Name #> = new <#=area.Name #>Class();
|
|
public static <#=area.Name #>Class <#=EscapeID(area.Namespace) #> { get { return s_<#=area.Name #>; } }
|
|
<#} #>
|
|
<#if (settings.IncludeAreasToken) { #>
|
|
}
|
|
<#} #>
|
|
<#foreach (var controller in DefaultArea.GetControllers()) { #>
|
|
public static <#=controller.FullClassName #> <#=controller.Name #> = new <#=controller.FullDerivedClassName #>();
|
|
<#} #>
|
|
}
|
|
|
|
namespace <#=settings.T4MVCNamespace #>
|
|
{
|
|
<#foreach (var area in Areas.Where(a => !string.IsNullOrEmpty(a.Name))) { #>
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
public class <#=area.Name #>Class
|
|
{
|
|
public readonly string Name = "<#=ProcessAreaOrControllerName(area.Name) #>";
|
|
<#foreach (var controller in area.GetControllers()) { #>
|
|
public <#=controller.FullClassName #> <#=controller.Name #> = new <#=controller.FullDerivedClassName #>();
|
|
<#} #>
|
|
}
|
|
<#} #>
|
|
}
|
|
|
|
namespace <#=settings.T4MVCNamespace #>
|
|
{
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
public class Dummy
|
|
{
|
|
private Dummy() { }
|
|
public static Dummy Instance = new Dummy();
|
|
}
|
|
}
|
|
|
|
<#foreach (var resultType in ResultTypes.Values) { #>
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
internal partial class T4MVC_<#=resultType.UniqueName #> : <#=resultType.FullName #>, IT4MVCActionResult
|
|
{
|
|
public T4MVC_<#=resultType.UniqueName #>(string area, string controller, string action, string protocol = null): base(<#resultType.Constructor.WriteNonEmptyParameterValues(true); #>)
|
|
{
|
|
this.InitMVCT4Result(area, controller, action, protocol);
|
|
}
|
|
<#foreach (var method in resultType.AbstractMethods) { #>
|
|
<#=method.IsPublic ? "public" : "protected" #> override void <#=method.Name #>(<#method.WriteFormalParameters(true); #>) { }
|
|
<#} #>
|
|
|
|
public string Controller { get; set; }
|
|
public string Action { get; set; }
|
|
public string Protocol { get; set; }
|
|
public RouteValueDictionary RouteValueDictionary { get; set; }
|
|
}
|
|
<#} #>
|
|
|
|
|
|
|
|
namespace <#=settings.LinksNamespace #>
|
|
{
|
|
<#
|
|
foreach (string folder in settings.StaticFilesFolders) {
|
|
ProcessStaticFiles(Project, folder);
|
|
}
|
|
#>
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
public static partial class Bundles
|
|
{
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
public static partial class Scripts {}
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
public static partial class Styles {}
|
|
}
|
|
}
|
|
|
|
<#
|
|
RenderAdditionalCode();
|
|
#>
|
|
<#foreach (var controller in GetAbstractControllers().Where(c => !c.HasDefaultConstructor)) { #>
|
|
<#manager.StartNewFile(controller.GeneratedFileName); #>
|
|
namespace <#=controller.Namespace #>
|
|
{
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
public partial class <#=controller.ClassName #>
|
|
{
|
|
protected <#=controller.ClassName #>() { }
|
|
}
|
|
}
|
|
<#manager.EndBlock(); #>
|
|
<#} #>
|
|
|
|
<#foreach (var controller in GetControllers()) { #>
|
|
<#
|
|
// Don't generate the file at all if the existing one is up to date
|
|
// NOTE: disable this optimization since it doesn't catch view changes! It can be re-enabled later if smarter change detection is added
|
|
//if (controller.GeneratedCodeIsUpToDate) {
|
|
// manager.KeepGeneratedFile(controller.GeneratedFileName);
|
|
// continue;
|
|
//}
|
|
#>
|
|
<#manager.StartNewFile(controller.GeneratedFileName); #>
|
|
<#if (!String.IsNullOrEmpty(controller.Namespace)) { #>
|
|
namespace <#=controller.Namespace #>
|
|
{
|
|
<#} #>
|
|
public <#if (!controller.NotRealController) { #>partial <#} #>class <#=controller.ClassName #>
|
|
{
|
|
<#if (!controller.NotRealController) { #>
|
|
<#if (!controller.HasExplicitConstructor) { #>
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
public <#=controller.ClassName #>() { }
|
|
|
|
<#} #>
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
protected <#=controller.ClassName #>(Dummy d) { }
|
|
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
protected RedirectToRouteResult RedirectToAction(ActionResult result)
|
|
{
|
|
var callInfo = result.GetT4MVCResult();
|
|
return RedirectToRoute(callInfo.RouteValueDictionary);
|
|
}
|
|
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
protected RedirectToRouteResult RedirectToActionPermanent(ActionResult result)
|
|
{
|
|
var callInfo = result.GetT4MVCResult();
|
|
return RedirectToRoutePermanent(callInfo.RouteValueDictionary);
|
|
}
|
|
|
|
<#foreach (var method in controller.ActionMethodsUniqueWithoutParameterlessOverload) { #>
|
|
[NonAction]
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
public virtual <#=method.ReturnTypeFullName #> <#=method.Name #>()
|
|
{
|
|
<#if (method.ReturnTypeFullName == "System.Threading.Tasks.Task<System.Web.Mvc.ActionResult>") { #>
|
|
var callInfo = new T4MVC_<#=method.ReturnTypeUniqueName #>(Area, Name, ActionNames.<#=method.ActionName #><# if (method.ActionUrlHttps) {#>, "https"<#}#>);
|
|
return System.Threading.Tasks.Task.FromResult(callInfo as ActionResult);
|
|
<#} else { #>
|
|
return new T4MVC_<#=method.ReturnTypeUniqueName #>(Area, Name, ActionNames.<#=method.ActionName #><# if (method.ActionUrlHttps) {#>, "https"<#}#>);
|
|
<#} #>
|
|
}
|
|
<#} #>
|
|
<#foreach (var method in controller.CustomActionMethodsUniqueWithoutParameterlessOverload) { #>
|
|
[NonAction]
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
public virtual <#=method.ReturnTypeFullName #> <#=method.ActionName #>()
|
|
{
|
|
return new T4MVC_<#=method.ReturnTypeUniqueName #>(Area, Name, ActionNames.<#=method.ActionName #><# if (method.ActionUrlHttps) {#>, "https"<#}#>);
|
|
}
|
|
<#} #>
|
|
<#foreach (var method in controller.CustomActionMethods) { #>
|
|
[NonAction]
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
public virtual <#=method.ReturnTypeFullName #> <#=method.ActionName #>(<#method.WriteFormalParameters(true, true); #>)
|
|
{
|
|
var callInfo = new T4MVC_<#=method.ReturnTypeUniqueName #>(Area, Name, ActionNames.<#=method.ActionName #><# if (method.ActionUrlHttps) {#>, "https"<#}#>);
|
|
<#if (method.Parameters.Count > 0) { #>
|
|
<#foreach (var p in method.Parameters) { #>
|
|
ModelUnbinderHelpers.AddRouteValues(callInfo.RouteValueDictionary, <#=p.RouteNameExpression #>, <#=p.Name #>);
|
|
<#} #>
|
|
<#}#>
|
|
<#if (method.ReturnTypeFullName == "System.Threading.Tasks.Task<System.Web.Mvc.ActionResult>") { #>
|
|
return System.Threading.Tasks.Task.FromResult(callInfo as ActionResult);
|
|
<#} else { #>
|
|
return callInfo;
|
|
<#} #>
|
|
}
|
|
<#} #>
|
|
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
public <#=controller.ClassName #> Actions { get { return <#=controller.T4MVCControllerFullName #>; } }
|
|
[<#= GeneratedCode #>]
|
|
public readonly string Area = "<#=ProcessAreaOrControllerName(controller.AreaName) #>";
|
|
[<#= GeneratedCode #>]
|
|
public readonly string Name = "<#=ProcessAreaOrControllerName(controller.Name) #>";
|
|
[<#= GeneratedCode #>]
|
|
public const string NameConst = "<#=ProcessAreaOrControllerName(controller.Name) #>";
|
|
|
|
static readonly ActionNamesClass s_actions = new ActionNamesClass();
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
public ActionNamesClass ActionNames { get { return s_actions; } }
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
public class ActionNamesClass
|
|
{
|
|
<#foreach (var method in controller.ActionMethodsWithUniqueNames) { #>
|
|
<# if (settings.UseLowercaseRoutes) { #>
|
|
public readonly string <#=method.ActionName #> = (<#=method.ActionNameValueExpression #>).ToLowerInvariant();
|
|
<# } else { #>
|
|
public readonly string <#=method.ActionName #> = <#=method.ActionNameValueExpression #>;
|
|
<# }
|
|
} #>
|
|
}
|
|
|
|
<#
|
|
// Issue: we can't honor UseLowercaseRoutes here because ToLowerInvariant() is not valid in constants!
|
|
if (!settings.UseLowercaseRoutes) { #>
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
public class ActionNameConstants
|
|
{
|
|
<#foreach (var method in controller.ActionMethodsWithUniqueNames) { #>
|
|
public const string <#=method.ActionName #> = <#=method.ActionNameValueExpression #>;
|
|
<#
|
|
} #>
|
|
}
|
|
|
|
<#}
|
|
} #>
|
|
|
|
<#if (settings.GenerateParamsForActionMethods && !settings.GenerateParamsAsConstantsForActionMethods){
|
|
foreach (var group in controller.UniqueParameterNamesGroupedByActionName) if (group.Any()) { #>
|
|
static readonly ActionParamsClass_<#=group.Key #> s_params_<#=group.Key #> = new ActionParamsClass_<#=group.Key #>();
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
public ActionParamsClass_<#=group.Key #> <#=group.Key + settings.ParamsPropertySuffix #> { get { return s_params_<#=group.Key #>; } }
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
public class ActionParamsClass_<#=group.Key #>
|
|
{
|
|
<#foreach (var param in group) { #>
|
|
<# if (settings.UseLowercaseRoutes) { #>
|
|
public readonly string <#=param.Name #> = (<#=param.RouteNameExpression #>).ToLowerInvariant();
|
|
<# } else { #>
|
|
public readonly string <#=param.Name #> = <#=param.RouteNameExpression #>;
|
|
<# }
|
|
} #>
|
|
}
|
|
<# } #>
|
|
<#} #>
|
|
<#if (settings.GenerateParamsAsConstantsForActionMethods){
|
|
|
|
foreach (var group in controller.UniqueParameterNamesGroupedByActionName) if (group.Any()) { #>
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
public class <#=group.Key + settings.ParamsPropertySuffix#>
|
|
{
|
|
<#foreach (var param in group) { #>
|
|
<# if (settings.UseLowercaseRoutes) { #>
|
|
public const string <#=param.Name #> = (<#=param.RouteNameExpression #>).ToLowerInvariant();
|
|
<# } else { #>
|
|
public const string <#=param.Name #> = <#=param.RouteNameExpression #>;
|
|
<# }
|
|
} #>
|
|
}
|
|
<# } #>
|
|
<#} #>
|
|
static readonly ViewsClass s_views = new ViewsClass();
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
public ViewsClass Views { get { return s_views; } }
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
public class ViewsClass
|
|
{
|
|
<#RenderControllerViews(controller);#>
|
|
}
|
|
}
|
|
|
|
<#if (!controller.NotRealController) { #>
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
public partial class <#=controller.DerivedClassName #> : <#=controller.FullClassName #>
|
|
{
|
|
public <#=controller.DerivedClassName #>() : base(Dummy.Instance) { }
|
|
|
|
<#foreach (var method in controller.ActionMethods.Where(m => !m.IsCustomReturnType)) { #>
|
|
partial void <#=method.Name #>Override(T4MVC_<#=method.ReturnTypeUniqueName #> callInfo<#if (method.Parameters.Count > 0) { #>, <#method.WriteFormalParameters(true); #><#}#>);
|
|
|
|
public override <#=method.ReturnTypeFullName #> <#=method.Name #>(<#method.WriteFormalParameters(true); #>)
|
|
{
|
|
var callInfo = new T4MVC_<#=method.ReturnTypeUniqueName #>(Area, Name, ActionNames.<#=method.ActionName #><# if (method.ActionUrlHttps) {#>, "https"<#}#>);
|
|
<#if (method.Parameters.Count > 0) { #>
|
|
<#foreach (var p in method.Parameters) { #>
|
|
ModelUnbinderHelpers.AddRouteValues(callInfo.RouteValueDictionary, <#=p.RouteNameExpression #>, <#=p.Name #>);
|
|
<#} #>
|
|
<#}#>
|
|
<#=method.Name #>Override(callInfo<#if (method.Parameters.Count > 0) { #><#foreach (var p in method.Parameters) { #>, <#=p.Name #><#}}#>);
|
|
<#if (method.ReturnTypeFullName == "System.Threading.Tasks.Task<System.Web.Mvc.ActionResult>") { #>
|
|
return System.Threading.Tasks.Task.FromResult(callInfo as ActionResult);
|
|
<#} else { #>
|
|
return callInfo;
|
|
<#} #>
|
|
}
|
|
|
|
<#} #>
|
|
}
|
|
<#} #>
|
|
<#if (!String.IsNullOrEmpty(controller.Namespace)) { #>
|
|
}
|
|
<#} #>
|
|
|
|
<#manager.EndBlock(); #>
|
|
<#} #>
|
|
|
|
|
|
<# if (settings.ExplicitHtmlHelpersForPartials) {
|
|
manager.StartNewFile("T4MVC.ExplicitExtensions.cs"); #>
|
|
|
|
namespace System.Web.Mvc {
|
|
[<#= GeneratedCode #>]
|
|
public static class HtmlHelpersForExplicitPartials
|
|
{
|
|
<#
|
|
foreach(var partialView in GetPartials()) {
|
|
string partialName = partialView.Key;
|
|
string partialPath = partialView.Value;
|
|
string partialRenderMethod = string.Format(settings.ExplicitHtmlHelpersForPartialsFormat, partialName);
|
|
#>
|
|
///<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
|
|
<#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>();
|
|
|
|
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.GetService(typeof(EnvDTE.DTE));
|
|
}
|
|
|
|
// Fail if we couldn't get the DTE. This can happen when trying to run in TextTransform.exe
|
|
if (Dte == null)
|
|
{
|
|
throw new Exception("T4MVC can only execute through the Visual Studio host");
|
|
}
|
|
|
|
Project = GetProjectContainingT4File(Dte);
|
|
|
|
if (Project == null)
|
|
{
|
|
Error("Could not find the VS Project containing the T4 file.");
|
|
return;
|
|
}
|
|
|
|
// Get the path of the root folder of the app
|
|
AppRoot = Path.GetDirectoryName(Project.FullName) + '\\';
|
|
|
|
ProcessAreas(Project);
|
|
}
|
|
|
|
Project GetProjectContainingT4File(DTE dte)
|
|
{
|
|
|
|
// Find the .tt file's ProjectItem
|
|
ProjectItem projectItem = dte.Solution.FindProjectItem(Host.TemplateFile);
|
|
|
|
// If the .tt file is not opened, open it
|
|
if (projectItem.Document == null)
|
|
projectItem.Open(EnvDTE.Constants.vsViewKindCode);
|
|
|
|
return projectItem.ContainingProject;
|
|
}
|
|
|
|
void ProcessAreas(Project project)
|
|
{
|
|
// Process the default area
|
|
ProcessArea(project.ProjectItems, null);
|
|
|
|
// Get the Areas folder
|
|
ProjectItem areaProjectItem = GetProjectItem(project, settings.AreasFolder);
|
|
|
|
// Process areas folder
|
|
if (areaProjectItem != null)
|
|
{
|
|
foreach (ProjectItem item in areaProjectItem.ProjectItems)
|
|
{
|
|
if (IsFolder(item))
|
|
{
|
|
ProcessArea(item.ProjectItems, item.Name);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process portable areas
|
|
foreach (string portableArea in settings.PortableAreas)
|
|
{
|
|
ProjectItem portableAreaProjectItem = GetProjectItem(project, portableArea);
|
|
|
|
if (portableAreaProjectItem == null)
|
|
return;
|
|
|
|
if (IsFolder(portableAreaProjectItem))
|
|
{
|
|
ProcessArea(portableAreaProjectItem.ProjectItems, portableAreaProjectItem.Name);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ProcessArea(ProjectItems areaFolderItems, string name)
|
|
{
|
|
var area = new AreaInfo() { Name = name };
|
|
ProcessAreaControllers(areaFolderItems, area);
|
|
ProcessAreaViews(areaFolderItems, area);
|
|
Areas.Add(area);
|
|
|
|
if (String.IsNullOrEmpty(name))
|
|
DefaultArea = area;
|
|
}
|
|
|
|
void ProcessAreaControllers(ProjectItems areaFolderItems, AreaInfo area)
|
|
{
|
|
// Get area Controllers folder
|
|
ProjectItem controllerProjectItem = GetProjectItem(areaFolderItems, settings.ControllersFolder);
|
|
if (controllerProjectItem == null)
|
|
return;
|
|
|
|
ProcessControllersRecursive(controllerProjectItem, area);
|
|
}
|
|
|
|
void ProcessAreaViews(ProjectItems areaFolderItems, AreaInfo area)
|
|
{
|
|
// Get area Views folder
|
|
ProjectItem viewsProjectItem = GetProjectItem(areaFolderItems, settings.ViewsRootFolder);
|
|
if (viewsProjectItem == null)
|
|
return;
|
|
|
|
ProcessAllViews(viewsProjectItem, area);
|
|
}
|
|
|
|
void ProcessControllersRecursive(ProjectItem projectItem, AreaInfo area)
|
|
{
|
|
|
|
// Recurse into all the sub-items (both files and folder can have some - e.g. .tt files)
|
|
foreach (ProjectItem item in projectItem.ProjectItems)
|
|
{
|
|
ProcessControllersRecursive(item, area);
|
|
}
|
|
|
|
if (projectItem.FileCodeModel != null)
|
|
{
|
|
DateTime controllerLastWriteTime = File.GetLastWriteTime(projectItem.get_FileNames(0));
|
|
foreach (var type in projectItem.FileCodeModel.CodeElements.OfType<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;
|
|
|
|
// Don't process generic classes (their concrete derived classes will be processed)
|
|
if (type.IsGeneric)
|
|
return;
|
|
|
|
//Ignore references to controllers we create
|
|
if(area.Controllers.Any(c => c.DerivedClassName == type.Name))
|
|
return;
|
|
|
|
// Make sure the class is partial
|
|
if (type.ClassKind != vsCMClassKind.vsCMClassKindPartialClass)
|
|
{
|
|
try
|
|
{
|
|
type.ClassKind = vsCMClassKind.vsCMClassKindPartialClass;
|
|
}
|
|
catch
|
|
{
|
|
// If we couldn't make it partial, give a warning and skip it
|
|
Warning(String.Format("{0} was not able to make the class {1} partial. Please change it manually if possible", T4FileName, type.Name));
|
|
return;
|
|
}
|
|
Warning(String.Format("{0} changed the class {1} to be partial", T4FileName, type.Name));
|
|
}
|
|
|
|
// Collect misc info about the controller class and add it to the collection
|
|
var controllerInfo = new ControllerInfo
|
|
{
|
|
Area = area,
|
|
Namespace = type.Namespace != null ? type.Namespace.Name : String.Empty,
|
|
ClassName = type.Name
|
|
};
|
|
|
|
//Filter references to controllers we create
|
|
foreach(var derived in area.Controllers.Where(c => c.ClassName == controllerInfo.DerivedClassName).ToArray())
|
|
area.Controllers.Remove(derived);
|
|
|
|
// Check if the controller has changed since the generated file was last created
|
|
DateTime lastGenerationTime = File.GetLastWriteTime(controllerInfo.GeneratedFileFullPath);
|
|
if (lastGenerationTime > controllerLastWriteTime)
|
|
{
|
|
controllerInfo.GeneratedCodeIsUpToDate = true;
|
|
}
|
|
|
|
// Either process new ControllerInfo or integrate results into existing object for partially defined controllers
|
|
var target = area.Controllers.Add(controllerInfo) ? controllerInfo : area.Controllers.First(c => c.Equals(controllerInfo));
|
|
target.HasExplicitConstructor |= HasExplicitConstructor(type);
|
|
target.HasExplicitDefaultConstructor |= HasExplicitDefaultConstructor(type);
|
|
|
|
if (type.IsAbstract)
|
|
{
|
|
// If it's abstract, set a flag and don't process action methods (derived classes will)
|
|
target.IsAbstract = true;
|
|
}
|
|
else
|
|
{
|
|
// Process all the action methods in the controller
|
|
ProcessControllerActionMethods(target, type);
|
|
}
|
|
}
|
|
|
|
void ProcessControllerActionMethods(ControllerInfo controllerInfo, CodeClass2 current)
|
|
{
|
|
|
|
bool isAsyncController = IsAsyncController(current);
|
|
|
|
// We want to process not just the controller class itself, but also its parents, as they
|
|
// may themselves define actions
|
|
for (CodeClass2 type = current; type != null && type.FullName != "System.Web.Mvc.Controller"; type = (CodeClass2)type.Bases.Item(1))
|
|
{
|
|
|
|
// If the type doesn't come from this project, some actions on it will fail. Try to get a real project type if possible.
|
|
if (type.InfoLocation != vsCMInfoLocation.vsCMInfoLocationProject)
|
|
{
|
|
// Go through all the projects in the solution
|
|
for (int i = 1; i <= Dte.Solution.Projects.Count; i++)
|
|
{
|
|
Project prj = null;
|
|
try
|
|
{
|
|
prj = Dte.Solution.Projects.Item(i);
|
|
}
|
|
catch (System.Runtime.Serialization.SerializationException)
|
|
{
|
|
// Some project types (that we don't care about) cause a strange exception, so ingore it
|
|
continue;
|
|
}
|
|
|
|
// Skip it if it's the current project or doesn't have a code model
|
|
try
|
|
{
|
|
if (prj == Project || prj.CodeModel == null)
|
|
continue;
|
|
}
|
|
catch (System.NotImplementedException)
|
|
{
|
|
// Installer project does not implement CodeModel property
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
// If we can get a local project type, use it instead of the original
|
|
var codeType = prj.CodeModel.CodeTypeFromFullName(type.FullName);
|
|
if (codeType != null && codeType.InfoLocation == vsCMInfoLocation.vsCMInfoLocationProject)
|
|
{
|
|
type = (CodeClass2)codeType;
|
|
break;
|
|
}
|
|
}
|
|
catch (System.ArgumentException)
|
|
{
|
|
// CodeTypeFromFullName throws when called on VB projects with a type it doesn't know
|
|
// (instead of returning null), so ignore those exceptions (See http://t4mvc.codeplex.com/workitem/7)
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (CodeFunction2 method in GetMethods(type))
|
|
{
|
|
// Ignore non-public methods
|
|
if (method.Access != vsCMAccess.vsCMAccessPublic)
|
|
continue;
|
|
|
|
// Ignore methods that are marked as not being actions
|
|
if (GetAttribute(method.Attributes, "System.Web.Mvc.NonActionAttribute") != null)
|
|
continue;
|
|
|
|
// Ignore methods that are marked as Obsolete
|
|
if (GetAttribute(method.Attributes, "System.ObsoleteAttribute") != null)
|
|
continue;
|
|
|
|
// Ignore generic methods
|
|
if (method.IsGeneric)
|
|
continue;
|
|
|
|
if(isAsyncController && settings.SupportAsyncActions && (method.Type.TypeKind == vsCMTypeRef.vsCMTypeRefVoid) && method.Name.EndsWith("Async"))
|
|
{
|
|
//Async methods return void and there could be multiple matching Completed methods, so we will use
|
|
//the generic ActionResult as the return type for the method.
|
|
var resultType = Project.CodeModel.CodeTypeFromFullName("System.Web.Mvc.ActionResult");
|
|
// If we haven't yet seen this return type, keep track of it
|
|
if (!ResultTypes.ContainsKey(resultType.Name))
|
|
{
|
|
var resTypeInfo = new ResultTypeInfo(resultType);
|
|
ResultTypes[resultType.FullName] = 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
|
|
if (!method.Type.CodeType.get_IsDerivedFrom("System.Web.Mvc.ActionResult") && method.Type.CodeType.FullName !="System.Threading.Tasks.Task<System.Web.Mvc.ActionResult>")
|
|
{
|
|
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(method.Type.CodeType);
|
|
if (!ResultTypes.ContainsKey(resTypeInfo2.FullName))
|
|
{
|
|
ResultTypes[resTypeInfo2.FullName] = resTypeInfo2;
|
|
}
|
|
|
|
// Make sure the method is virtual
|
|
if (!method.CanOverride && method.OverrideKind != vsCMOverrideKind.vsCMOverrideKindOverride)
|
|
{
|
|
try
|
|
{
|
|
method.CanOverride = true;
|
|
}
|
|
catch
|
|
{
|
|
// If we couldn't make it virtual, give a warning and skip it
|
|
Warning(String.Format("{0} was not able to make the action method {1}.{2} virtual. Please change it manually if possible", T4FileName, type.Name, method.Name));
|
|
continue;
|
|
}
|
|
Warning(String.Format("{0} changed the action method {1}.{2} to be virtual", T4FileName, type.Name, method.Name));
|
|
}
|
|
|
|
// Collect misc info about the action method and add it to the collection
|
|
controllerInfo.ActionMethods.Add(new ActionMethodInfo(method, current));
|
|
}
|
|
}
|
|
}
|
|
|
|
void ProcessAllViews(ProjectItem viewsProjectItem, AreaInfo area)
|
|
{
|
|
// Go through all the sub-folders in the Views folder
|
|
foreach (ProjectItem item in viewsProjectItem.ProjectItems)
|
|
{
|
|
|
|
// We only care about sub-folders, not files
|
|
if (!IsFolder(item))
|
|
continue;
|
|
|
|
// Find the controller for this view folder
|
|
ControllerInfo controller = area.Controllers.SingleOrDefault(c => c.Name.Equals(item.Name, StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (controller == null)
|
|
{
|
|
// If it doesn't match a controller, treat as a pseudo-controller for consistency
|
|
controller = new ControllerInfo
|
|
{
|
|
Area = area,
|
|
NotRealController = true,
|
|
Namespace = MakeClassName(settings.T4MVCNamespace, area.Name),
|
|
ClassName = item.Name + ControllerSuffix
|
|
};
|
|
area.Controllers.Add(controller);
|
|
}
|
|
|
|
AddViewsRecursive(item.ProjectItems, controller.ViewsFolder);
|
|
}
|
|
}
|
|
|
|
void AddViewsRecursive(ProjectItems items, ViewsFolderInfo viewsFolder)
|
|
{
|
|
AddViewsRecursive(items, viewsFolder, false);
|
|
}
|
|
|
|
void AddViewsRecursive(ProjectItems items, ViewsFolderInfo viewsFolder, bool useNonQualifiedViewNames)
|
|
{
|
|
// Go through all the files in the subfolder to get the view names
|
|
foreach (ProjectItem item in items)
|
|
{
|
|
if (item.Kind == EnvDTE.Constants.vsProjectItemKindPhysicalFile)
|
|
{
|
|
if (Path.GetExtension(item.Name).Equals(".master", StringComparison.OrdinalIgnoreCase))
|
|
continue; // ignore master files
|
|
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("}");
|
|
}
|
|
}
|
|
|
|
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)#> {
|
|
private const string URLPATH = "<#=path#>/<#=projectItem.Name#>";
|
|
public static string Url() { return T4MVCHelpers.ProcessVirtualPath(URLPATH); }
|
|
public static string Url(string fileName) { return T4MVCHelpers.ProcessVirtualPath(URLPATH + "/" + fileName); }
|
|
<#+
|
|
PushIndent(" ");
|
|
|
|
// Recurse into all the items in the folder
|
|
foreach (ProjectItem item in projectItem.ProjectItems)
|
|
{
|
|
ProcessStaticFilesRecursive(
|
|
item,
|
|
path + "/" + projectItem.Name,
|
|
childrenNameSet);
|
|
}
|
|
|
|
PopIndent();
|
|
#>
|
|
}
|
|
|
|
<#+
|
|
}
|
|
else { #>
|
|
<#+
|
|
if (!settings.ExcludedStaticFileExtensions.Any(extension => projectItem.Name.EndsWith(extension, StringComparison.OrdinalIgnoreCase))) {
|
|
// if it's a non-minified javascript file
|
|
if (projectItem.Name.EndsWith(".js") && !projectItem.Name.EndsWith(".min.js")) {
|
|
string minifiedName = projectItem.Name.Replace(".js", ".min.js");
|
|
if (AddTimestampToStaticLink(projectItem)) { #>
|
|
public static readonly string <#=name#> = T4MVCHelpers.IsProduction() && T4Extensions.FileExists(URLPATH + "/<#=minifiedName#>") ? Url("<#=minifiedName#>")+"?"+T4MVCHelpers.TimestampString(URLPATH + "/<#=minifiedName#>") : Url("<#=projectItem.Name#>")+"?"+T4MVCHelpers.TimestampString(URLPATH + "/<#=projectItem.Name#>");
|
|
<#+} else {#>
|
|
public static readonly string <#=name#> = T4MVCHelpers.IsProduction() && T4Extensions.FileExists(URLPATH + "/<#=minifiedName#>") ? Url("<#=minifiedName#>") : Url("<#=projectItem.Name#>");
|
|
<#+} #>
|
|
<#+}
|
|
else if (projectItem.Name.EndsWith(".css") && !projectItem.Name.EndsWith(".min.css")) {
|
|
string minifiedName = projectItem.Name.Replace(".css", ".min.css");
|
|
if (AddTimestampToStaticLink(projectItem)) { #>
|
|
public static readonly string <#=name#> = T4MVCHelpers.IsProduction() && T4Extensions.FileExists(URLPATH + "/<#=minifiedName#>") ? Url("<#=minifiedName#>")+"?"+T4MVCHelpers.TimestampString(URLPATH + "/<#=minifiedName#>") : Url("<#=projectItem.Name#>")+"?"+T4MVCHelpers.TimestampString(URLPATH + "/<#=projectItem.Name#>");
|
|
<#+} else {#>
|
|
public static readonly string <#=name#> = T4MVCHelpers.IsProduction() && T4Extensions.FileExists(URLPATH + "/<#=minifiedName#>") ? Url("<#=minifiedName#>") : Url("<#=projectItem.Name#>");
|
|
<#+} #>
|
|
<#+}
|
|
else if (AddTimestampToStaticLink(projectItem)) { #>
|
|
public static readonly string <#=name#> = Url("<#=projectItem.Name#>")+"?"+T4MVCHelpers.TimestampString(URLPATH + "/<#=projectItem.Name#>");
|
|
<#+}
|
|
else { #>
|
|
public static readonly string <#=name#> = Url("<#=projectItem.Name#>");
|
|
<#+}
|
|
} #>
|
|
<#+
|
|
// Non folder items may also have children (virtual folders, Class.cs -> Class.Designer.cs, template output)
|
|
// Just register them on the same path as their parent item
|
|
foreach (ProjectItem item in projectItem.ProjectItems)
|
|
{
|
|
ProcessStaticFilesRecursive(item, path, childrenNameSet);
|
|
}
|
|
}
|
|
}
|
|
|
|
int BuildClassStructureForProvidedPath(string path)
|
|
{
|
|
var folders = path.Split(new char[] {'/', '~'}, StringSplitOptions.RemoveEmptyEntries);
|
|
var parentFolder = String.Empty;
|
|
var currentPath = "~";
|
|
foreach(var folder in folders)
|
|
{
|
|
currentPath += "/" + folder;
|
|
string className = EscapeID(Sanitize(folder));
|
|
// If the folder name is the same as the parent, add a modifier to avoid class name conflicts
|
|
// http://mvccontrib.codeplex.com/workitem/7153
|
|
if (parentFolder == folder)
|
|
{
|
|
className += "_";
|
|
}
|
|
|
|
if(!virtualPathesForStaticFiles.Contains(currentPath))
|
|
{
|
|
virtualPathesForStaticFiles.Add(currentPath);#>
|
|
|
|
[<#= GeneratedCode #>, DebuggerNonUserCode]
|
|
public static partial class <#=className #> {
|
|
private const string URLPATH = "<#=currentPath#>";
|
|
public static string Url() { return T4MVCHelpers.ProcessVirtualPath(URLPATH); }
|
|
public static string Url(string fileName) { return T4MVCHelpers.ProcessVirtualPath(URLPATH + "/" + fileName); }
|
|
<#+ } else {
|
|
#>
|
|
|
|
public static partial class <#=className #> {
|
|
<#+ }
|
|
PushIndent(" ");
|
|
parentFolder = folder;
|
|
}
|
|
return folders.Length;
|
|
}
|
|
|
|
ProjectItem GetProjectItem(Project project, string name)
|
|
{
|
|
return GetProjectItem(project.ProjectItems, name);
|
|
}
|
|
|
|
ProjectItem GetProjectItem(ProjectItems items, string subPath)
|
|
{
|
|
|
|
ProjectItem current = null;
|
|
foreach (string name in subPath.Split('\\'))
|
|
{
|
|
try
|
|
{
|
|
// ProjectItems.Item() throws when it doesn't exist, so catch the exception
|
|
// to return null instead.
|
|
current = items.Item(name);
|
|
}
|
|
catch
|
|
{
|
|
// If any chunk couldn't be found, fail
|
|
return null;
|
|
}
|
|
items = current.ProjectItems;
|
|
}
|
|
|
|
return current;
|
|
}
|
|
|
|
static bool IsController(CodeClass2 type)
|
|
{
|
|
// Ignore any class which name doesn't end with "Controller"
|
|
if (!type.FullName.EndsWith(ControllerSuffix)) return false;
|
|
|
|
for (; type.FullName != "System.Web.Mvc.Controller"; type = (CodeClass2)type.Bases.Item(1))
|
|
{
|
|
if (type.Bases.Count == 0)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool IsAsyncController(CodeClass2 type)
|
|
{
|
|
for (; type.FullName != "System.Web.Mvc.AsyncController"; type = (CodeClass2)type.Bases.Item(1))
|
|
{
|
|
if (type.Bases.Count == 0)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static string GetVirtualPath(ProjectItem item)
|
|
{
|
|
string fileFullPath = item.get_FileNames(0);
|
|
if (!fileFullPath.StartsWith(AppRoot, StringComparison.OrdinalIgnoreCase))
|
|
throw new Exception(string.Format("File {0} is not under app root {1}. Please report issue.", fileFullPath, AppRoot));
|
|
|
|
// Make a virtual path from the physical path
|
|
return "~/" + fileFullPath.Substring(AppRoot.Length).Replace('\\', '/');
|
|
}
|
|
|
|
static string ProcessAreaOrControllerName(string name)
|
|
{
|
|
return settings.UseLowercaseRoutes ? name.ToLowerInvariant() : name;
|
|
}
|
|
|
|
// Return all the CodeFunction2 in the CodeElements collection
|
|
static IEnumerable<CodeFunction2> GetMethods(CodeClass2 codeClass)
|
|
{
|
|
// Only look at regular method (e.g. ignore things like contructors)
|
|
return codeClass.Members.OfType<CodeFunction2>()
|
|
.Where(f => f.FunctionKind == vsCMFunction.vsCMFunctionFunction);
|
|
}
|
|
|
|
// Check if the class has any explicit constructor
|
|
static bool HasExplicitConstructor(CodeClass2 codeClass)
|
|
{
|
|
return codeClass.Members.OfType<CodeFunction2>().Any(
|
|
f => !f.IsShared && f.FunctionKind == vsCMFunction.vsCMFunctionConstructor);
|
|
}
|
|
|
|
// Check if the class has a default (i.e. no params) constructor
|
|
static bool HasExplicitDefaultConstructor(CodeClass2 codeClass)
|
|
{
|
|
return codeClass.Members.OfType<CodeFunction2>().Any(
|
|
f => !f.IsShared && f.FunctionKind == vsCMFunction.vsCMFunctionConstructor && f.Parameters.Count == 0);
|
|
}
|
|
|
|
// Find a method with a given name
|
|
static CodeFunction2 GetMethod(CodeClass2 codeClass, string name)
|
|
{
|
|
return GetMethods(codeClass).FirstOrDefault(f => f.Name == name);
|
|
}
|
|
|
|
// Find an attribute of a given type on an attribute collection
|
|
static CodeAttribute2 GetAttribute(CodeElements attributes, string attributeType)
|
|
{
|
|
for (int i = 1; i <= attributes.Count; i++)
|
|
{
|
|
try
|
|
{
|
|
var attrib = (CodeAttribute2)attributes.Item(i);
|
|
if (attributeType.Split(',').Contains(attrib.FullName, StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
return attrib;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// FullName can throw in some cases, so just ignore those attributes
|
|
continue;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static CodeAttribute2 GetAttribute(CodeClass2 type, string attributeType)
|
|
{
|
|
while(type != null) {
|
|
var attribute = GetAttribute(type.Attributes, attributeType);
|
|
if(attribute != null)
|
|
return attribute;
|
|
if (type.Bases.Count == 0)
|
|
return null;
|
|
type = (CodeClass2)type.Bases.Item(1);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static string UniqueFullName(CodeType codeType)
|
|
{
|
|
var uniqueName = codeType.FullName;
|
|
|
|
// Match characters not allowed in class names.
|
|
uniqueName = Regex.Replace(uniqueName, @"[^\p{Ll}\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{Nl}\d]", "_");
|
|
|
|
// Remove duplicate '_' characters
|
|
uniqueName = Regex.Replace(uniqueName, @"__+", "_");
|
|
|
|
// Remove trailing '_' characters
|
|
uniqueName = uniqueName.TrimEnd('_');
|
|
|
|
return uniqueName;
|
|
}
|
|
|
|
// Return whether a ProjectItem is a folder and not a file
|
|
static bool IsFolder(ProjectItem item)
|
|
{
|
|
return (item.Kind == EnvDTE.Constants.vsProjectItemKindPhysicalFolder);
|
|
}
|
|
|
|
static string MakeClassName(string ns, string classname)
|
|
{
|
|
return String.IsNullOrEmpty(ns) ? classname :
|
|
String.IsNullOrEmpty(classname) ? ns : ns + "." + codeProvider.CreateEscapedIdentifier(classname);
|
|
}
|
|
|
|
static string SanitizeWithNoConflicts(string token, HashSet<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);
|
|
}
|
|
|
|
// 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 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
|
|
{
|
|
// Trim the Controller suffix
|
|
return ClassName.Substring(0, ClassName.Length - ControllerSuffix.Length);
|
|
}
|
|
}
|
|
|
|
public string Namespace { get; set; }
|
|
|
|
public string FullClassName
|
|
{
|
|
get
|
|
{
|
|
return MakeClassName(Namespace, ClassName);
|
|
}
|
|
}
|
|
|
|
public string DerivedClassName
|
|
{
|
|
get
|
|
{
|
|
return "T4MVC_" + ClassName;
|
|
}
|
|
}
|
|
|
|
public string FullDerivedClassName
|
|
{
|
|
get
|
|
{
|
|
if (NotRealController)
|
|
return FullClassName;
|
|
return MakeClassName(Namespace, DerivedClassName);
|
|
}
|
|
}
|
|
|
|
public string GeneratedFileName
|
|
{
|
|
get
|
|
{
|
|
return MakeClassName(AreaName, ClassName + ".generated.cs");
|
|
}
|
|
}
|
|
|
|
public string GeneratedFileFullPath
|
|
{
|
|
get
|
|
{
|
|
return Path.Combine(T4Folder, GeneratedFileName);
|
|
}
|
|
}
|
|
|
|
public HashSet<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;
|
|
Views[viewFieldName] = useNonQualifiedViewName ? Path.GetFileNameWithoutExtension(viewName) : GetVirtualPath(item);
|
|
}
|
|
|
|
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;
|
|
|
|
public FunctionInfo(CodeFunction2 method)
|
|
{
|
|
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 CodeType ReturnTypeImpl { get { return _method.Type.CodeType; } }
|
|
|
|
public string Name { get { return _method.Name; } }
|
|
public string ReturnType { get { return ReturnTypeImpl.Name; } }
|
|
public string ReturnTypeFullName { get { return ReturnTypeImpl.FullName; } }
|
|
public string ReturnTypeUniqueName { get { return IsTaskBased ? "System_Web_Mvc_ActionResult" : UniqueFullName(ReturnTypeImpl); } }
|
|
public bool IsPublic { get { return _method.Access == vsCMAccess.vsCMAccessPublic; } }
|
|
public List<MethodParamInfo> Parameters { get; private set; }
|
|
public bool CanBeCalledWithoutParameters { get; private set; }
|
|
|
|
private bool IsTaskBased { get {return ReturnTypeImpl.FullName == "System.Threading.Tasks.Task<System.Web.Mvc.ActionResult>"; } }
|
|
|
|
// 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, CodeType asyncType = null)
|
|
: base(method)
|
|
{
|
|
if(asyncType != null)
|
|
{
|
|
// Remove the Async from the end of the name to match the actual Action routing would use.
|
|
// This also separates the Action Calls from the implementation
|
|
_actionName = method.Name.Remove(method.Name.Length - 5);
|
|
_returnType = asyncType;
|
|
}
|
|
// Normally, the action name is the method name. But if there is an [ActionName] on
|
|
// the method, get the expression from that instead
|
|
ActionNameValueExpression = '"' + ActionName + '"';
|
|
var attrib = GetAttribute(method.Attributes, "System.Web.Mvc.ActionNameAttribute");
|
|
if (attrib != null)
|
|
{
|
|
var arg = (CodeAttributeArgument)attrib.Arguments.Item(1);
|
|
ActionNameValueExpression = arg.Value;
|
|
}
|
|
|
|
if (GetAttribute(method.Attributes, settings.AttributeIndicatingHttps) != null || GetAttribute(controller, settings.AttributeIndicatingHttps) != null)
|
|
{
|
|
ActionUrlHttps = true;
|
|
}
|
|
}
|
|
|
|
string _actionName;
|
|
CodeType _returnType;
|
|
protected override CodeType ReturnTypeImpl { get { return _returnType ?? base.ReturnTypeImpl; } }
|
|
|
|
public string ActionName { get { return _actionName ?? base.Name; } }
|
|
public string ActionNameValueExpression { get; set; }
|
|
public bool ActionUrlHttps {get; set; }
|
|
|
|
public bool IsCustomReturnType { get { return _returnType != null; } }
|
|
|
|
}
|
|
|
|
// Data about an ActionResult derived type
|
|
class ResultTypeInfo
|
|
{
|
|
CodeType _codeType;
|
|
public ResultTypeInfo(CodeType codeType)
|
|
{
|
|
_codeType = codeType;
|
|
|
|
var ctor = _codeType.Members.OfType<CodeFunction2>().FirstOrDefault(
|
|
f => f.FunctionKind == vsCMFunction.vsCMFunctionConstructor);
|
|
Constructor = new FunctionInfo(ctor);
|
|
}
|
|
|
|
public string Name { get { return _codeType.Name; } }
|
|
public string FullName { get { return this.IsTaskBased ? "System.Web.Mvc.ActionResult" : _codeType.FullName; } }
|
|
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.Members.OfType<CodeFunction2>().Where(
|
|
f => f.MustImplement).Select(f => new FunctionInfo(f));
|
|
}
|
|
}
|
|
|
|
private bool IsTaskBased { get {return _codeType.FullName == "System.Threading.Tasks.Task<System.Web.Mvc.ActionResult>"; } }
|
|
}
|
|
|
|
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.IncludeAreasToken = false;
|
|
this.ControllersFolder = "Controllers";
|
|
this.ViewsRootFolder = "Views";
|
|
this.NonQualifiedViewFolders = new XmlStringArray(new string[] {
|
|
"DisplayTemplates",
|
|
"EditorTemplates"
|
|
}, "ViewFolder");
|
|
this.GenerateActionResultInterface = true;
|
|
this.GenerateParamsAsConstantsForActionMethods = false;
|
|
this.GenerateParamsForActionMethods = true;
|
|
this.SupportAsyncActions = false;
|
|
this.UseLowercaseRoutes = false;
|
|
this.LinksNamespace = "Links";
|
|
this.AddTimestampToStaticLinks = false;
|
|
this.StaticFilesFolders = new XmlStringArray(new string[] {
|
|
"Scripts",
|
|
"Content",
|
|
}, "FileFolder");
|
|
this.ExcludedStaticFileExtensions = new XmlStringArray(new string[] {
|
|
".cs"
|
|
}, "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("Choose whether you want to include an 'Areas' token when referring to areas.\r\ne.g. Assume the Area is called Blog and the Controller is Post:\r\n- When false use MVC.Blog.Post.etc...\r\n- When true use MVC.Areas.Blog.Post.etc...")]
|
|
public bool IncludeAreasToken { get; set; }
|
|
|
|
[System.ComponentModel.Description("The folder under the project that contains the controllers")]
|
|
public string ControllersFolder { get; set; }
|
|
|
|
[System.ComponentModel.Description("The folder under the project that contains the views")]
|
|
public string ViewsRootFolder { get; set; }
|
|
|
|
[System.ComponentModel.Description("Views in DisplayTemplates and EditorTemplates folders shouldn't be fully qualifed\r\nas it breaks the templated helper code")]
|
|
public XmlStringArray NonQualifiedViewFolders { get; set; }
|
|
|
|
[System.ComponentModel.Description("If true, the T4MVC action result interface will be generated\r\nIf false, the namespace of the interface must be referenced in the 'ReferencedNamespaces' setting")]
|
|
public bool GenerateActionResultInterface { get; set; }
|
|
|
|
[System.ComponentModel.Description("If true, [new] overrides will be created for async actions on AsyncControllers\r\nThis breaks the Go To Definition function for async actions.")]
|
|
public bool SupportAsyncActions { get; set; }
|
|
|
|
[System.ComponentModel.Description("If true, use lower case tokens in routes for the area, controller and action names")]
|
|
public bool UseLowercaseRoutes { get; set; }
|
|
|
|
[System.ComponentModel.Description("The namespace that the links are generated in (e.g. \"Links\", as in Links.Content.nerd_jpg)")]
|
|
public string LinksNamespace { get; set; }
|
|
|
|
[System.ComponentModel.Description("If true, links to static files include a query string containing the file's last change time.\r\nThis way, when the static file changes, the link changes and guarantees that the client will re-request the resource.\r\ne.g. when true, the link looks like: \"/Content/nerd.jpg?2009-09-04T12:25:48\"\r\nSee http://mvccontrib.codeplex.com/workitem/7163 for potential issues with this feature")]
|
|
public bool AddTimestampToStaticLinks { get; set; }
|
|
|
|
[System.ComponentModel.Description("Folders containing static files for which links are generated (e.g. Links.Scripts.Map_js)")]
|
|
public XmlStringArray StaticFilesFolders { get; set; }
|
|
|
|
[System.ComponentModel.Description("Static files to exclude from the generated links")]
|
|
public XmlStringArray ExcludedStaticFileExtensions { 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 settigns class should inherit from this
|
|
/// one since it provides the methods to interact with the T4 system and EnvDTE. Sub-properties can
|
|
/// just inherit from XmlSettingsBase.
|
|
abstract class XmlSettings : XmlSettingsBase
|
|
{
|
|
protected static T Load<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
|
|
*/
|
|
#>
|