Reputation: 13581
I build a lot of MVC apps from scratch, but that use existing databases.
Using the Entity Framework -> Reverse Engineer Code First context menu item, I get the Code First classes, DbContext class and the mapping classes from the database.
However, I would also like to generate MetaData classes, so that I can add my customised DisplayName attributes, etc.
The MetaData Classes would be in a different directory (MetaData), so that they don't clutter up the Models directory.
Anyone know of a T4 Template that does this? It would be odd if I am the first person with this requirement...
I am new to T4, but any template that gets files from a given directory, reads each one in a loop, amends it a bit (ideally, adding an attribute to a property!), then writes to a new file in a different directory would be fine, as from there on in, I can figure out how to do it for my specific purpose.
I don't want the files generated at the same time as the Reverse Engineered Code First files as I don't want to overwrite my MetaData classes. To avoid doing this when I DO run the template, I would amend/write the template so that if the file already exists in the MetaData
directory, the template skips that entity and a new MetaData file is not created to overwrite the existing one.
I have seen stuff for Model First and Database First, but not code first. I suppose I could adapt one of those to code first, just replacing the EDMX bits with reading in the previously generated files, getting the properties and adding the DisplayName attribute to them.
Hope this makes sense?
I deleted the first edit as I have made progress. See EDIT 2 below.
Have also deleted EDIT 2 as I have solved all my problems. See my answer below.
Upvotes: 2
Views: 1066
Reputation: 13581
I have been able to solve my problem using blood, sweat, tears and Tangible T4's TemplateFileManagerV2.1.ttinclude and their VisualStudioAutomationHelper.ttinclude, albeit with the modification suggested by Tangible T4 support in the following post:
As I don't have the Pro edition of Tangible T4 it was a bit painful. Hey ho, I'm not looking gift horses in the mouth.
The only outstanding problem is that I can't detect whether a property in the source file is virtual, so I get the navigation properties in my buddy metadata classes as well, which I didn't want. I'll live to fight that one another day.
Also, I can create the files, but they are not included in the project. The code to include them is simple, but I couldn't get it work in the same file, so had to split it out into separate files as follows:
T4_1_GenerateCodeFirstBuddies.tt T4_2_GenerateCodeFirstBuddies.tt
This separation has a collateral benefit in that T4_1_GenerateCodeFirstBuddies.tt uses the two Tangible T4 helper .ttincludes, one of which leaves a residual error. Running my second file removes the error and the red wavy lines in the solution explorer, which I find really distracting.
So, the code for my files is as follows:
<#@ template debug="true" hostSpecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#@ Assembly Name="System.Core" #>
<#@ import namespace="System" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Diagnostics" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="EnvDTE" #>
<#@ include file="VisualStudioAutomationHelper.ttinclude" #>
<#@ include file="TemplateFileManagerV2.1.ttinclude" #><#
var modelFileDirectory = this.Host.ResolvePath("Models");
var metaDataFilesDirectory = this.Host.ResolvePath("MetaData");
var nspace = "";
var manager = TemplateFileManager.Create(this);
foreach(var file in System.IO.Directory.GetFiles(modelFileDirectory, "*.cs"))
{
var projectItem = this.VisualStudioHelper.FindProjectItem(file);
foreach(EnvDTE.CodeClass classInFile in this.VisualStudioHelper.CodeModel.GetAllCodeElementsOfType(projectItem.FileCodeModel.CodeElements, EnvDTE.vsCMElement.vsCMElementClass, false))
{
var name = classInFile.Name;
if(nspace == "") nspace = classInFile.Namespace.Name;
// Danger: Beware if a table name includes the string "Context" or "AspNet"!!
// These files are removed because they are either the DbContext, or the sysdiagram file, or else the AspNet.Identity tables
if(name != "sysdiagram" && name.IndexOf("Context") == -1 && name.IndexOf("AspNet") == -1)
{
if(!FileExists(metaDataFilesDirectory, classInFile.Name + "MetaData.cs"))
{
manager.StartNewFile(name +"MetaData.cs", "", "MetaData"); #>
using System;
using System.Collections.Generic;
using System.ComponentModel;
//using System.ComponentModel.DataAnnotations;
//using Wingspan.Web.Mvc.Extensions;
using Wingspan.Web.Mvc.Crud;
namespace <#= nspace #>
{
public class <#= name + "MetaData" #>
{
<# foreach (CodeElement mem in classInFile.Members)
{
if (mem.Kind == vsCMElement.vsCMElementProperty) // && "[condition to show that mem is not marked as virtual]")
{
PushIndent(" ");
WriteLineDisplayName(mem);
WriteLineProperty(mem);
WriteLine("");
PopIndent();
}
} #>
}
public partial class <#= name #> : IInjectItemSL
{
public ItemSL ItemSL
{
get
{
return new ItemSL
{
ItemId = <#= name #>Id, ItemText = Name
};
}
}
}
}<#
}
}
}
}
manager.Process();
#>
<#+
// Check for file existence
bool FileExists(string directory, string filename)
{
return File.Exists(Path.Combine(directory, filename));
}
// Get current folder directory
string GetCurrentDirectory()
{
return System.IO.Path.GetDirectoryName(Host.TemplateFile);
}
string GetRootDirectory()
{
return this.Host.ResolvePath("");
}
// Get content of file name
string xOutputFile(string filename)
{
using(StreamReader sr =
new StreamReader(Path.Combine(GetCurrentDirectory(),filename)))
{
return sr.ReadToEnd();
}
}
// Get friendly name for property names
string GetFriendlyName(string value)
{
return Regex.Replace(value,
"([A-Z]+)", " $1",
RegexOptions.Compiled).Trim();
}
void WriteLineProperty(CodeElement ce)
{
var access = ((CodeProperty) ce).Access == vsCMAccess.vsCMAccessPublic ? "public" : "";
WriteLine(access + " " + (((CodeProperty) ce).Type).AsFullName + " " + ce.Name + " { get; set; }");
}
void WriteLineDisplayName(CodeElement ce)
{
var name = ce.Name;
if (!string.IsNullOrEmpty(name))
{
name = GetFriendlyName(name);
WriteLine(string.Format("[DisplayName(\"{0}\")]", name));
}
}
#>
<#@ template debug="true" hostSpecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#@ Assembly Name="System.Core" #>
<#@ import namespace="System" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Diagnostics" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="EnvDTE" #>
<#@ include file="VisualStudioAutomationHelper.ttinclude" #>
<#@ include file="TemplateFileManagerV2.1.ttinclude" #><#
var metaDataFilesDirectory = this.Host.ResolvePath("MetaData");
var metaDataFiles = System.IO.Directory.GetFiles(metaDataFilesDirectory, "*.cs");
var project = VisualStudioHelper.CurrentProject;
var projectItems = project.ProjectItems;
foreach( var f in metaDataFiles)
{
projectItems.AddFromFile(f);
}
#>
The output files generated are good enough for me, and look along the lines of:
using System;
using System.Collections.Generic;
using System.ComponentModel;
//using System.ComponentModel.DataAnnotations;
//using Wingspan.Web.Mvc.Extensions;
using Wingspan.Web.Mvc.Crud;
namespace BuddyClassGenerator.Models
{
public class ChemicalMetaData
{
[DisplayName("Chemical Id")]
public System.Guid ChemicalId { get; set; }
[DisplayName("Active Ingredient")]
public System.String ActiveIngredient { get; set; }
[DisplayName("Type")]
public System.String Type { get; set; }
[DisplayName("LERAP")]
public System.String LERAP { get; set; }
[DisplayName("Hazard Classification")]
public System.String HazardClassification { get; set; }
[DisplayName("MAPP")]
public System.Int32 MAPP { get; set; }
[DisplayName("Hygiene Practice")]
public System.String HygienePractice { get; set; }
[DisplayName("Medical Advice")]
public System.String MedicalAdvice { get; set; }
[DisplayName("Label")]
public System.String Label { get; set; }
[DisplayName("PPE")]
public System.String PPE { get; set; }
[DisplayName("Warnings")]
public System.String Warnings { get; set; }
[DisplayName("Products")]
public System.Collections.Generic.ICollection<BuddyClassGenerator.Models.Product> Products { get; set; }
}
public partial class Chemical : IInjectItemSL
{
public ItemSL ItemSL
{
get
{
return new ItemSL
{
ItemId = ChemicalId, ItemText = Name
};
}
}
}
You will no doubt note that I have put two classes in the same file. Might not be best practice but it saves me time and visual clutter in the folders, so it is my privilege.
To do list: 1, not include the navigation properties in the buddy class; 2, remove the namespace names from the property types.
I hope this helps someone, but do remember that to get it to work you will need the Tangible T4 ttincludes detailed above.
Upvotes: 2