Files
Disco/Disco.Web/T4MVC.tt
T
2013-05-16 19:14:29 +10:00

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
*/
#>