Open VisualStudio2022 and create a new net6.0 class library.
Create a class to use in the T4 template and create a T4 template and use the class.
Class:
namespace ClassLibraryT4
{
public class Class1
{
public static string DoTheThing() { return "TheThing"; }
}
}
Now build the project so that its dll file exists on disc.
T4:
<## template debug="false" hostspecific="false" language="C#" #>
<## assembly name="$(SolutionDir)ClassLibraryT4\bin\Debug\net6.0\ClassLibraryT4.dll" #>
<## import namespace="ClassLibraryT4" #>
<## output extension=".cs" #>
namespace ClassLibraryT4
{
public class TheGeneratedClass
{
private const string _TheThing = "<# Class1.DoTheThing(); #>";
}
}
The T4 now fails to run because
nThe type 'Object' is defined in an assembly that is not referenced. You must add a reference to assembly 'System.Runtime, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'.
If I add to the T4:
<## assembly name="System.Runtime"#>
Then I now get
Error Running transformation: System.IO.FileNotFoundException: Could not load file or assembly 'System.Runtime, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies. The system cannot find the file specified.
File name: 'System.Runtime, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
at Microsoft.VisualStudio.TextTemplating6765B00A4659E4D1054752E9A2C829A21EECD20197C4EDDD8F5675E0DB91730A0DFF4528F1622E70821097EC90F6A2D0DE05F4739B3E0CD1BCAF45AAA20D419D.GeneratedTextTransformation.TransformText()
at System.Dynamic.UpdateDelegates.UpdateAndExecute1[T0,TRet](CallSite site, T0 arg0)
at Microsoft.VisualStudio.TextTemplating.TransformationRunner.PerformTransformation()
Can T4s work?
It seems to be impossible to use any outisde code; this does work in the T4:
private const string _TheThing = "<#= 5+2 #>";
and so does this:
private const string _TheThing = "<#= Thing() #>";
...
<#+
private static string Thing() {
return "thing";
}
#>
but this also has the _Could not load file or assembly System.Runtime...` problem:
<#+
private static string Thing() {
return Class1o.DoTheThing();
}
#>
Can T4s work?
Yes.
You just need to ensure that all used dll's are for x86 architecture.
This is limitation by Visual Studio as it is a 32-bit app.
The safest way to get this working is to use netstandard2.0 for any assembly to be loaded in a T4 template. If you include plain C# code (via include directive) only, then you may get away with it even if it uses net6.0 APIs after tweaking the assembly directives for a while. However, you will need to tweak it up to the point where it works for both, the Visual Studio and the MSBuild hosts.
Related
I am trying to put a config file in the PCL portion of my IOS/Android app.
The documentation at: https://github.com/mrbrl/PCLAppConfig
suggests:
Assembly assembly = typeof(App).GetTypeInfo().Assembly;
ConfigurationManager.AppSettings = new ConfigurationManager(assembly.GetManifestResourceStream("DemoApp.App.config")).GetAppSettings;
I expect DemoApp is the assembly name of their sample app so I have:
using PCLAppConfig;
namespace LYG
{
public class App : Application
{
public App ()
{
Assembly assembly = typeof(App).GetTypeInfo().Assembly;
ConfigurationManager.AppSettings = new ConfigurationManager(assembly.GetManifestResourceStream("LYG.App.config")).AppSettings;
}
...
}
}
I get the following compile error:
/Users/greg/Projects/LYG/LYG/LYG.cs(122,122): Error CS1061: Type PCLAppConfig.ConfigurationManager' does not contain a definition forGetAppSettings' and no extension method GetAppSettings' of typePCLAppConfig.ConfigurationManager' could be found. Are you missing an assembly reference? (CS1061) (LYG)
This is my packages.config file:
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="PCLAppConfig" version="0.3.1" targetFramework="portable45-net45+win8+wp8" />
<package id="PCLStorage" version="1.0.2" targetFramework="portable45-net45+win8+wp8" />
<package id="Xamarin.Forms" version="2.3.2.127" targetFramework="portable45-net45+win8+wp8" />
</packages>
I also tried with parentheses:
ConfigurationManager.AppSettings = new ConfigurationManager(assembly.GetManifestResourceStream("LYG.App.config")).AppSettings();
Why can't the GetAppSettings be found?
You need to add the PCLAppConfig nuget package to your PCL and platforms projects.
Then I take you are trying to use the resource based app.config;
I just updated the documentation to reflect the last version update.
then use:
Assembly assembly = typeof(App).GetTypeInfo().Assembly;
ConfigurationManager.Initialise(assembly.GetManifestResourceStream("DemoApp.App.config"));
in doubt, check the demo project on github : https://github.com/mrbrl/PCLAppConfig/tree/master/src/DemoApp
I work with a bunch of something.js.tt JavaScript files using Knockout and a bunch of something-else.tt HTML files.
The infrastructure is mostly a C backend with Perl serving API and we use these .tt files to show the HTML and .js.tt to serve the Knockout.js code. What is .tt?
A TT file is a Visual Studio Text Template, developed by Microsoft.
Text Template Transformation Toolkit, shortly written as T4, uses the .tt file extension for its source files. It is Microsoft's template-based text generation framework included with Visual Studio.
For more info, see the docs.
If you take a look inside the file, you'll probably notice a lot of logic injecting things. This is because this kind of files are used to generate other files.
As explained in the MS page shared by #Recev Yildiz:
In Visual Studio, a T4 text template is a mixture of text blocks and control logic that can generate a text file.
The control logic is written as fragments of program code in Visual C# or Visual Basic. In Visual Studio 2015 Update 2 and later, you can use C# version 6.0 features in T4 templates directives.
The generated file can be text of any kind, such as a web page, or a resource file, or program source code in any language.
There are two kinds of T4 text templates: run time and design time.
Here's an example of a code I've got from a Entity Framework file, from a ASP.NET Web Application (.NET Framework) project (MVC design):
<## template language="C#" debug="false" hostspecific="true"#>
<## include file="EF6.Utility.CS.ttinclude"#><##
output extension=".cs"#><#
const string inputFile = #"DBModel.edmx";
var textTransform = DynamicTextTransformation.Create(this);
var code = new CodeGenerationTools(this);
var ef = new MetadataTools(this);
var typeMapper = new TypeMapper(code, ef, textTransform.Errors);
var loader = new EdmMetadataLoader(textTransform.Host, textTransform.Errors);
var itemCollection = loader.CreateEdmItemCollection(inputFile);
var modelNamespace = loader.GetModelNamespace(inputFile);
var codeStringGenerator = new CodeStringGenerator(code, typeMapper, ef);
var container = itemCollection.OfType<EntityContainer>().FirstOrDefault();
if (container == null)
{
return string.Empty;
}
#>
//------------------------------------------------------------------------------
// <auto-generated>
// <#=CodeGenerationTools.GetResourceString("Template_GeneratedCodeCommentLine1")#>
//
// <#=CodeGenerationTools.GetResourceString("Template_GeneratedCodeCommentLine2")#>
// <#=CodeGenerationTools.GetResourceString("Template_GeneratedCodeCommentLine3")#>
// </auto-generated>
//------------------------------------------------------------------------------
<#
var codeNamespace = code.VsNamespaceSuggestion();
if (!String.IsNullOrEmpty(codeNamespace))
{
#>
namespace <#=code.EscapeNamespace(codeNamespace)#>
{
<#
PushIndent(" ");
}
#>
using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
<#
if (container.FunctionImports.Any())
{
#>
using System.Data.Entity.Core.Objects;
using System.Linq;
<#
}
#>
<#=Accessibility.ForType(container)#> partial class <#=code.Escape(container)#> : DbContext
{
public <#=code.Escape(container)#>()
: base("name=<#=container.Name#>")
{
<#
if (!loader.IsLazyLoadingEnabled(container))
{
#>
this.Configuration.LazyLoadingEnabled = false;
<#
}
foreach (var entitySet in container.BaseEntitySets.OfType<EntitySet>())
{
// Note: the DbSet members are defined below such that the getter and
// setter always have the same accessibility as the DbSet definition
if (Accessibility.ForReadOnlyProperty(entitySet) != "public")
{
#>
<#=codeStringGenerator.DbSetInitializer(entitySet)#>
<#
}
}
#>
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
throw new UnintentionalCodeFirstException();
}
<#
foreach (var entitySet in container.BaseEntitySets.OfType<EntitySet>())
{
#>
<#=codeStringGenerator.DbSet(entitySet)#>
<#
}
foreach (var edmFunction in container.FunctionImports)
{
WriteFunctionImport(typeMapper, codeStringGenerator, edmFunction, modelNamespace, includeMergeOption: false);
}
#>
}
The file was way larger than what you see here. And as you can see, it seems to be a really busy code.
This is the context where the file is placed:
TT stands for - Visual Studio Text Template is a software development tool created by the Microsoft.
Further explanation - TT file contains text block and control logic used for generating new files. To write the Text Template file we can use either - Visual C# or Visual Basic Code
It's mainly used for handling Run Time text generation and source code generation both at once. They're like normal text files and can be viewed in any text editor.
I'm trying to use some reflection in a .tt file, more specifically to determine the KnownTypes on a class. To do this I just use simple reflection, or rather want to use simple reflection, but when I try to:
List<String> GetKnownTypes(EntityType entity)
{
List<String> knownTypes = new List<String>();
System.Reflection.MemberInfo info = typeof(EntityType);
object[] attributes = info.GetCustomAttributes(typeof(KnownTypeAttribute), false);
for (int i = 0; i < attributes.Length; i++)
{
KnownTypeAttribute attr = (KnownTypeAttribute)attributes[i];
knownTypes.Add(attr.Type.Name);
}
return knownTypes;
}
I get slapped around the ears with an error:
Error 1 Compiling transformation: The type or namespace name
'KnownTypeAttribute' could not be found (are you missing a using
directive or an assembly reference?)
But, I have a reference to System.Runtime.Serialization. I also import
<## import namespace="System.Runtime.Serialization" #> at the beginning of the tt file.
The target framework is .NET framework 4 (no client profile).
Any thought?
Do you have an <## assembly #> directive to bring in System.Runtime.Serialization? In VS2010, project references don't play any part in assembly resolution in T4.
I have a T4 that is generating multiple .html files.
After creating them all it then deletes them. I have seen the files create in both explorer and VS2010 ultimate (the solution explorer bar grows and then shrinks right back).
I have modified Oleg Synch's updated multiple output code as follows:
ProjectItem GetTemplateItem(DTE dte)
{
return // Find the .tt file's ProjectItem
dte.Solution.FindProjectItem(Host.TemplateFile);
}
void SaveOutput(string outputFileName,List<string> savedOutputs)
{
string templateDirectory = Path.GetDirectoryName(Host.TemplateFile);
string outputFilePath = Path.Combine(templateDirectory, outputFileName);
var text= this.GenerationEnvironment.ToString();
WriteDiagnosticLine("Writing:"+text.Length+" characters");
File.WriteAllText(outputFilePath,text);
this.GenerationEnvironment = new StringBuilder();
ProjectItem templateProjectItem = GetTemplateItem(Dte);
templateProjectItem.ProjectItems.AddFromFile(outputFilePath);
savedOutputs.Add(outputFileName);
WriteDiagnosticLine("Added:"+outputFileName);
}
void WriteDiagnosticLine(string line)
{
System.Diagnostics.Debug.WriteLine(line);
}
the initial code to set Dte is
bool AlwaysKeepTemplateDirty = true;
DTE Dte;
var serviceProvider = Host as IServiceProvider;
if (serviceProvider != null) {
Dte = serviceProvider.GetService(typeof(SDTE)) as 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("T4Generator can only execute through the Visual Studio host");
}
it happens whether I make a change and hit save, or right click the .tt file and say run custom tool.
In case it's helpful here's my tt declaration:
<## template language="C#" hostspecific="True" debug="True" #>
<## output extension=".txt" #>
<## assembly name="System.Core" #>
<## assembly name="System.Xml" #>
<## assembly name="Microsoft.VisualStudio.Shell.Interop.8.0" #>
<## assembly name="EnvDTE" #>
<## assembly name="EnvDTE80" #>
<## import namespace="System.IO" #>
<## import namespace="System.Linq" #>
<## import namespace="System.Collections.Generic" #>
<## import namespace="System.Text" #>
<## import namespace="System.Text.RegularExpressions" #>
<## import namespace="Microsoft.VisualStudio.Shell.Interop" #>
<## import namespace="EnvDTE" #>
<## import namespace="EnvDTE80" #>
removing
<## include file="T4Toolbox.tt" #>
seems to have solved it. pretty bizarre.
I am trying to get the folder names inside the Views folder using T4 Template and it keeps giving me the following errors:
Error 3 Compiling transformation: The name 'Server' does not exist in the current context c:\Projects\LearningASPMVC\LearningASPMVCSolution\LearningMVC\StronglyTypedViews.tt 20 47
Error 4 A namespace does not directly contain members such as fields or methods C:\Projects\LearningASPMVC\LearningASPMVCSolution\LearningMVC\StronglyTypedViews.cs 1 1 LearningMVC
Here is the T4 Template:
<## template language="C#" debug="True" hostspecific="True" #>
<## output extension=".cs" #>
<## assembly name="System.Web" #>
<## import namespace="System.IO" #>
<## import namespace="System.Web" #>
using System;
namespace StronglyTypedViews
{
<#
string[] folders = Directory.GetDirectories(Server.MapPath("Views"));
foreach(string folderName in folders)
{
#>
public static class <#= folderName #> { }
<# } #>
}
UPDATE: Got it working using the physical path:
<## template language="C#" debug="True" hostspecific="True" #>
<## output extension=".cs" #>
<## assembly name="System.Web" #>
<## assembly name="System.Web.Mvc" #>
<## import namespace="System.Web.Mvc" #>
<## import namespace="System.IO" #>
<## import namespace="System.Web" #>
using System;
namespace StronglyTypedViews
{
<#
string viewsFolderPath = #"C:\Projects\LearningASPMVC\LearningASPMVCSolution\LearningMVC\";
string[] folders = Directory.GetDirectories(viewsFolderPath + "Views");
foreach(string folderName in folders)
{
#>
public static class <#= System.IO.Path.GetFileName(folderName) #> {
<#
foreach(string file in Directory.GetFiles(folderName)) {
#>
public const string <#= System.IO.Path.GetFileNameWithoutExtension(file) #> = "<#= System.IO.Path.GetFileNameWithoutExtension(file).ToString() #>";
<# } #>
<# } #>
}
}
T4 templates are executed in a temporary context that visual studio creates, and is well outside your web application. That temporary context is intended for generating the output text file. It is not a web application in any way, and is unrelated to the web application your are authoring. As a result, System.Web.HttpContext doesn't have any value assigned, and MapPath() cannot be invoked.
Environment.CurrentDirectory isn't much help either, as the template is executed in some temporary folder.
What can you do? If you can use absolute paths, go ahead and do that. Otherwise, adding the hostspecific attribute in the <## template#> directive will allow you to the Host variable, and its ResolvePath() method. ResolvePath lets you resolve paths relative to the TT file itself.
For example (example.tt):
<## template language="C#" hostspecific="True" #>
<## output extension=".cs" #>
// <#=Host.ResolvePath(".")#>
Output (example.cs):
// C:\Users\myusername\Documents\Visual Studio 2008\Projects\MvcApplication1\MvcApplication1\.
Oleg Sych's post about the template directive has a section about the hostspecific attribute.