Previously in MetaSharp I had a custom Templating engine for textual transformations. It was a custom grammar and was pretty simple. It worked well and all was good but it was becoming a maintenance burden for me and, I think, turned people off to MetaSharp more than attracted them. So I decided to spend a little time and try to integrate T4 into my templating library as a replacement.
It was actually reasonably simple to accomplish. The hardest part, of course, was working with AppDomains. T4 is a dynamic tool living in a static world and as such it requires an AppDomain to do its magic so it can all be unloaded without a trace. Because of this T4 isn’t very amenable to input. Most examples I found online start with firing up your template then beginning by reading a file or creating a class that connects to a database. In my case I have already have my metadata and would like to pass it into a T4 template but it’s in memory only. The first thing you have to do is make your model Serializable.
Next you need to know about the “inherits” and the “hostspecific” attributes. You add something like this to the top of your template:
<#@ template language="C#" inherits="SongMetaTemplate" hostspecific="true" debug="true" #>
This is for my Song sample application. I then create a SongMetaTemplate class that knows how to cast the model into a Song enumeration.
public abstract class SongMetaTemplate : MetaTemplate { public IEnumerable<Song> Songs { get { return ((IEnumerable<INode>)this.Model).Cast<Song>(); } } }
Adding a hostspecific attribute to the template causes the template class that inherits from the SongMetaTemplate to also have a Host property which gets loaded up with the Model provided by my pipeline. The MetaTemplate knows how to fetch that host model for me.
public abstract class MetaTemplate : TextTransformation { private object model = null; protected object Model { get { if (model == null) { dynamic metame = this; var metaHost = metame.Host as MetaSharpTextTemplatingHost; if (metaHost != null) this.model = metaHost.Model; } return model; } } public override string TransformText() { return this.GenerationEnvironment.ToString(); } }
And that’s it! My template can now get a hold of the Song AST parsed from the DSL earlier in the pipeline. Here is an example template in its entirety:
<#@ template language="C#" inherits="SongMetaTemplate" hostspecific="true" debug="true" #> <#@ assembly name="DynamicSongBuilder" #> <#@ import namespace="DynamicSongBuilder" #> <# foreach(var song in this.Songs) { #> <# foreach(var b in song.Bars) { #> <# foreach(var n in b.Notes) { #> play(<#=song.Duration #>, "<#=n.Key #>", <#=n.Octave #>); <# } } } #>
The template code is C# but the code generated is actually for my own custom language. In this case it’s parsed into Linq expressions and compiled into a delegate using a context object with a play method. It’s then executed dynamically and also collected by the GC when dereferenced. I plan to replace the custom language also, but its proving difficult without a reasonable parser to replace it.