Changes to ModelingTextTransformation in September CTP of DSL Tools for Visual Studio
(or how I learned to love loading multiple models in the same T4 template and stop worrying…)
A couple of posts ago, I described some changes to the DirectiveProcessor infrastructure in T4, our Text Templating technology, for the September CTP of the DSL Tools. This has consequences for the ModelingTextTransformation class which I'll go into here.
Firstly, let's review what a TextTransformation class is. When T4 processes a template, it generates a temporary class containing code to emit the output of the transform. That code lives in a method called TransformText:
public string TransformText()
The class is compiled, the TransformText method is invoked and the string returned is used as the content of the generated file. To make life easier for the template author, this generated class uses a standard base class, Microsoft.VisualStudio.TextTemplating.TextTransformation. This base class provides methods to generate an Error or a Warning, and to Write text explicitly to the output string. As well as providing an implementation of TransformText, the generated, derived class will also carry properties and methods emitted by the directive processors specified in the template to allow access to the data that they import, for example, a Model property.
Relatively speaking, it's quite hard work for directive processors to emit a lot of code, as typically they do it using the CodeDom, which provides language independence, but is verbose to program.
As an alternative, T4 allows a custom base class to be specified per template. This class must eventually derive from Microsoft.VisualStudio.TextTemplating.TextTransformation. You can then add custom methods and properties to this class that your directive processors can rely on at transform time. The virtual Initialize method will get called on this class just before the TransformText method.
This is how the DSL Tools use T4 - we supply, and rely on, a custom base class called ModelingTextTransformation which contains the basic infrastructure needed to set up and initialize a Store for reading model files into.
In previous versions of the DSL Tools, this class provided two abstract methods that directive processors had to emit code to implement:
protected abstract Type[] GetObjectModels();
protected abstract void LoadModels();
These methods were called during the implementation of the ModelingTextTransformation.Initialize method, which created a Store object. The first method returned the types of DomainModels to set up the Store object with and the second method actually loaded the model files specified by the directive processors into the Store.
Unfortunately, this system broke down when multiple directive processors (or multiple instances of the same directive processor) were used in the same template. Each directive processor emitted an override of the two methods and the generated class consequently would not compile as it had two LoadModels methods and two GetObjectModels methods.
To solve this, the September CTP removes both of the above abstract methods from ModelingTextTransformation and replaces them with a single concrete helper method:
protected void AddDomainModel(Type modelType)
This method allows directive processors to add their DomainModels to a list maintained by ModelingTextTransformation, which is used within the Initialize method to set up the Store object. It is now the responsibility of directive processors themselves to emit code to actually load models into the store.
But how does this help? Previously, directive processors could only emit code to add or override members in the generated TextTransformation-derived class. With multiple directive processors, only one of them could override the Initialize method, which was the only method that was guaranteed to be called by the framework. Hence some mechanism, such as a specialist directive processor, was needed to coordinate work from multiple processors. This made arbitrary composition of text templates arduous to say the least.
So now we return to the new methods added to the RequiresProvidesDirectiveProcessor class in the September CTP:
string GeneratePreInitializationCode(…)
string GeneratePostInitializationCode(…)
The framework now always generates the Initialize override in the TextTransformation-derived class, using the "pre" code, then a call to base.Initialize() then the "post" code as the method body.
This allows our directive processors to emit a call to AddDomainModel() before the base.Initialize call where the consolidated list of domain models is used to set up the Store object. The directive processors then emit "post" code to Load the specified model files safe in the knowledge that they are loading into a correctly initialized Store. Because directive processors are now adding method body code, any number of processor instances can contribute to the Initialize method.
We also removed the Validate override point in ModelingTextTransformation for the same reason - multiple directive processor instances can't all override Validate. If you want to Validate, you should also emit that as "post" code.
Phew, long post, so here's the takeaway…
- You no longer need a custom base class to load multiple files in one template, as the standard ModelingTextTransformation class now supports multiple directive processors.
- If you're creating a custom base class, don't provide abstract or virtual methods to override - you'll lock out templates with multiple directive processors; instead, provide helper methods and use the Pre and Post methods in your directive processors to add code that calls those helpers.