Walkthrough: Creating and Running Text Templates
You can use the toolkit in Domain-Specific Language Tools to transform text templates. By transforming (also known as running) text templates, you can generate text artifacts that combine text blocks with information supplied from input sources, such as models. For more information, see Generating Artifacts Using Text Templates.
This walkthrough shows you how to use Domain-Specific Language Tools to create and transform a text template. You will use the Class Diagrams solution template as a starting point for a domain-specific language that you create.
Tasks illustrated in this walkthrough include:
Creating a domain-specific language
Creating a custom text template
Adding code to a text template
Transforming a text template
Generating classes based on a model
Adding class features to a text template
Generating code from relationships
Generating code from relationship properties
Splitting a text template into multiple files
Generating text from a class feature
Validating the model before code generation
Accessing multiple models from a text template
注意
If errors occur in this walkthrough, see Common Validation Errors and Warnings in Text Templates.
Prerequisites
To complete this walkthrough, you will need:
Visual Studio 2008.
注意
For a list of the editions that Domain-Specific Language Tools supports, see Supported Visual Studio Editions for Domain-Specific Language Tools.
Visual Studio 2008 SDK.
Creating a Domain-Specific Language
First, you use the Domain-Specific Language Designer Wizard to create a domain-specific language called ClassDiagramExample. Then you open the experimental build.How to: Create Domain-Specific Language Solutions
To create a domain-specific language
On the File menu, point to New, and click Project.
The New Project dialog box appears.
Under Project types, expand Other Project Types, and click Extensibility.
Under Visual Studio installed templates, click Domain-Specific Language Designer.
In Name, type ClassDiagramExample, and click OK.
On the Select Domain-Specific Language Options page, click Class Diagrams in the list of solution templates, and then click Next.
On the Define New Model File Type page, under What extension should model files use?, type testcd, and then click Next.
On the Specify Product Details page, replace the name of the company with Fabrikam, and click Finish.
The ClassDiagramExample project opens.
注意
When you create a project, the system automatically generates the default templates. When you generate a template, you will see a message that warns you not to run text templates from untrusted sources. If you do not want this message to appear again, select the Do not show this message again check box, and click OK. Otherwise, click OK.
On the Build menu, click Rebuild Solution.
On the Debug menu, click Start Debugging.
The experimental build opens.
注意
You complete the rest of this walkthrough in the experimental build. For more information about the experimental build, see Experimental Build.
In Solution Explorer, open Sample.testcd.
This model contains the following classes:
Member
Library
Reservation
Loan
Title
Item
This is not a complete list. You might use these classes to implement a system for reserving books at a library.
Creating a Custom Text Template
Now that the experimental build is running, you can create a custom text template. Later, you will use this text template to return information about the Sample.testcd model.
To create a text template
On the Project menu, click Add New Item.
The Add New Item dialog box appears.
Under Visual Studio installed templates, click Text File.
In Name, type LibraryCode.tt, and click Add.
The system adds the file to the project.
注意
When you name a text template, you can either specify any extension that you want, such as .tt, or you can leave the default extension of .txt.
In Solution Explorer, click LibraryCode.tt.
警告
When you add LibraryCode.tt to the project, the file will be highlighted in Solution Explorer, but it is not selected. If you do not select LibraryCode.tt, the following step will not work.
In the Properties window, set the Custom Tool property to TextTemplatingFileGenerator.
注意
If you use .tt as the extension for a text template, the system automatically sets the Custom Tool property to TextTemplatingFileGenerator. You can use other extensions if you want. If you use another extension, you will have to set the Custom Tool property manually.
In Solution Explorer, right-click LibraryCode.tt, and then click Run Custom Tool.
The system generates the file, LibraryCode.cs. To display this file, expand the LibraryCode.tt node.
注意
The extension of the generated file is .cs by default. However, you can change the extension of the file that you just generated and the default extension for files that you generate later. For more information, see How to: Specify File Output Types in Text Templates.
Adding Code to a Text Template
With the blank text template open, you can add some directives, statements, and expressions. In text templates, you indicate directives with <#@ tags, statements with <# tags, and expressions with <#= tags. For more information, see Adding Code to Text Templates.
In this procedure, you add directives and statements that generate a list of the classes in the Sample.testcd model.
To add code to a text template
Copy the following two directives to LibraryCode.tt:
注意
By adding the two directives at the top, template and ClassDiagramExample, you can use the elements of the Sample.testcd model as classes in the text template code. For more information, see Accessing Models from Text Templates.
<#@ template inherits="Microsoft.VisualStudio.TextTemplating.VSHost.ModelingTextTransformation"#> <#@ ClassDiagramExample processor="ClassDiagramExampleDirectiveProcessor" requires="fileName='Sample.testcd'" #>
Copy the following statement and expression lines to LibraryCode.tt. This code generates a list of the classes that are in the Sample.testcd model:
<# foreach(ModelType type in this.ModelRoot.Types) { #> <#= type.Name#> <# } #>
Transforming a Text Template
With the text template complete, the next step is to transform LibraryCode.tt. This transformation generates an output file that contains a simple list of the classes in the Sample.testcd model.
To transform the text template
In Solution Explorer, right-click LibraryCode.tt, and then click Run Custom Tool.
注意
You can transform all of the text templates in your solution by clicking Transform All Templates on the Solution Explorer toolbar. Also, you transform a single text template every time that you save the file.
In Solution Explorer, double-click LibraryCode.cs.
The file opens with the following output:
Item
Title
Book
Member
Library
Loan
MultipleAssociation1
Reservation
MultipleAssociation2
注意
At this point, you will see an error in the Error List window. You did not cause this error by running the custom tool. Because the generated text file has an extension of .cs, Visual Studio is trying to compile it. At this intermediate stage, the generated text will not compile. You can ignore this error.
Delete the contents of LibraryCode.cs, and then repeat step 1.
The system regenerates the file.
Note You can also delete the output file itself, not just the contents of the file. You regenerate the output file every time that you transform the text template.
Generating Classes Based on a Model
This next procedure modifies the text template to generate Visual C# classes in LibraryCode.cs, which is based on the Sample.testcd model.
To generate Visual C# classes in the output file
Replace the code in LibraryCode.tt with the following code.
The changes are highlighted.
<#@ template inherits="Microsoft.VisualStudio.TextTemplating.VSHost.ModelingTextTransformation"#> <#@ ClassDiagramExample processor="ClassDiagramExampleDirectiveProcessor" requires="fileName='Sample.testcd'" #> <# foreach(ModelType type in this.ModelRoot.Types) { #> <# if(type is ModelClass) {#>public class <#= type.Name#>{}<# }#> <# } #>
Text without text template tags (<# and #>) is a text block and will be generated in the output file exactly as you type it. In this case, the text block is public class and its corresponding opening and closing brackets. The expression statement <#= type.Name#> adds the class name to the output file LibraryCode.cs. For more information, see Adding Code to Text Templates.
The output file reflects the format of the text template. The text public class <#= type.Name> and the opening and closing parentheses for the class appear on the left side of LibraryCode.tt. Because these items appear on the left side, the system formats the output file LibraryCode.cs with consistent indentations. This technique is used in various areas in the rest of this walkthrough.
In Solution Explorer, right-click LibraryCode.tt, and then click Run Custom Tool.
注意
The compilation error from previous steps no longer appears.
Open LibraryCode.cs.
The system now generates the classes as Visual C# classes instead of as a simple list:
public class Item { } public class Title { } public class Book { } public class Member { } public class Library { } public class Loan { } public class Reservation { }
Adding Class Features to a Text Template
Your C# code takes its class names, such as library, from the names in your model. Your text template will generate faulty code if you type a name into a model class that is not a valid C# identifier.
In the following procedure, you create a helper function that translates the name from the model into a valid C# identifier.
You create helper functions in text templates by adding them to class feature blocks. In text templates, you indicate class feature blocks with <#+ tags. For more information, see Class Feature Syntax.
You also call the Warning method, which you can use to add custom messages to the Error List window. The custom warning tells the user that the name of a model class contains no valid characters.
To add helper functions to the text template
Replace the code in LibraryCode.tt with the following code.
The changes are highlighted.
<#@ template inherits="Microsoft.VisualStudio.TextTemplating.VSHost.ModelingTextTransformation"#> <#@ ClassDiagramExample processor="ClassDiagramExampleDirectiveProcessor" requires="fileName='Sample.testcd'" #> <#@ import namespace = "System.Text.RegularExpressions" #> <# foreach(ModelType type in this.ModelRoot.Types) { if(type is ModelClass) { ModelClass classType = (ModelClass)type; string className = MakeValidName(type.Name); if(string.IsNullOrEmpty(className)) { // Generate a validation constraint Warning (String.Format("ClassName '{0}' is not a valid class name", type.Name)); } else {#>public class <#= className#> <# if(classType.Superclass != null) { #>: <#= classType.Superclass.Name#> <# } #>{}<# } } }#><#+private static string MakeValidName(string typeName){ //Use the System.Text.RegularExpressions namespace specified in the import directive //Remove non-alpha characters string fixedName = Regex.Replace(typeName,"[^a-zA-Z]",""); return fixedName;} #>
In Solution Explorer, right-click LibraryCode.tt, and then click Run Custom Tool.
Open LibraryCode.cs.
The output looks like this:
public class Item { } public class Title { } public class Book : Item { } public class Member { } public class Library { } public class Loan { } public class Reservation { }
Generating Code from Relationships
In the next procedure, you extend LibraryCode.tt to generate code based on the domain relationships in the model file Sample.testcd. In each ModelClass, you generate code for the ModelAttributes, which are linked through the ClassHasAttributes relationship. From the domain-specific language definition, you can see whether this relationship generates the Attributes property for the domain class, ModelClass.
To add private fields to the classes in the generated text output file
Replace the code in LibraryCode.tt with the following code.
The changes are highlighted.
<#@ template inherits="Microsoft.VisualStudio.TextTemplating.VSHost.ModelingTextTransformation"#> <#@ ClassDiagramExample processor="ClassDiagramExampleDirectiveProcessor" requires="fileName='Sample.testcd'" #> <#@ import namespace = "System.Text.RegularExpressions" #> <# foreach(ModelType type in this.ModelRoot.Types) { if(type is ModelClass) { ModelClass classType = (ModelClass)type; string className = MakeValidName(type.Name); if (string.IsNullOrEmpty(className)) { // Generate a validation constraint Warning(String.Format("ClassName '{0}' is not a valid class name", type.Name)); } else { #>public class <#= className#> <# if(classType.Superclass != null) { #>: <#= classType.Superclass.Name#> <# } #> { <# foreach(ModelAttribute attribute in classType.Attributes) {#> <#= MakeValidName(attribute.Type)#> <#= MakeValidName(attribute.Name)#>;<# }#> } <# } } } #> <#+private static string MakeValidName(string typeName){ //Use the System.Text.RegularExpressions namespace specified in the import directive //Remove non-alpha characters string fixedName = Regex.Replace(typeName,"[^a-zA-Z]",""); return fixedName;} #>
In Solution Explorer, right-click LibraryCode.tt, and click Run Custom Tool.
Open LibraryCode.cs.
The classes are now generated as Visual C# classes that have data types included in them. The output looks like this:
public class Item { } public class Title { string name; } public class Book : Item { } public class Member { } public class Library { } public class Loan { Date commenced; } public class Reservation { Date made; }
Generating Code from Relationship Properties
In this procedure, you add code to implement bidirectional associations by using a pair of properties. There is one property in the class at each end of the relationship.
To perform this task, you must obtain the names that the user has given to each end of the relationship. These properties are for the relationship itself, so you must navigate to individual instance links of the relationship by using its GetLinksTo… static methods.
To generate the bidirectional associations in the output file
Replace the code in LibraryCode.tt with the following code.
The changes are highlighted.
<#@ template inherits="Microsoft.VisualStudio.TextTemplating.VSHost.ModelingTextTransformation"#> <#@ ClassDiagramExample processor="ClassDiagramExampleDirectiveProcessor" requires="fileName='Sample.testcd'" #> <#@ import namespace = "System.Text.RegularExpressions" #> <# foreach(ModelType type in this.ModelRoot.Types) { if(type is ModelClass) { ModelClass classType = (ModelClass)type; string className = MakeValidName(type.Name); if (string.IsNullOrEmpty(className)) { // Generate a validation constraint Warning(String.Format("ClassName '{0}' is not a valid class name", type.Name)); } else { #> public class <#= className#> <# if(classType.Superclass != null) { #>: <#= classType.Superclass.Name#> <# } #> { <# foreach(ModelAttribute attribute in classType.Attributes) {#> <#= attribute.Type#> <#= attribute.Name#>;<# } foreach(BidirectionalAssociation association in BidirectionalAssociation.GetLinksToBidirectionalTargets(classType)) { ModelClass associatedClass = association.BidirectionalTarget; // Note that we use the Role name here. if(!string.IsNullOrEmpty(association.SourceRoleName) && !string.IsNullOrEmpty(association.TargetRoleName)) {#> private <#= associatedClass.Name#> <#= association.TargetRoleName#>Value; public <#= associatedClass.Name#> <#= association.TargetRoleName#> { get{ return <#= association.TargetRoleName#>Value; } set { if (<#= association.TargetRoleName#> != value) {if (<#= association.TargetRoleName#> != null) <#= association.TargetRoleName#>.<#=association.SourceRoleName#> = null;<#= association.TargetRoleName#>Value = value;if (value != null) { <#= association.TargetRoleName#>Value.<#=association.SourceRoleName#>=this;} } } }<# } else { Warning(String.Format("Ignoring BidirectionalAssociation from {0} to {1} because its SourceRoleName or TargetRoleName is not defined", classType.Name, associatedClass.Name)); } }#><# foreach(BidirectionalAssociation association in BidirectionalAssociation.GetLinksToBidirectionalSources(classType)) { ModelClass associatedClass = association.BidirectionalSource; if(!string.IsNullOrEmpty(association.SourceRoleName) && !string.IsNullOrEmpty(association.TargetRoleName)) {#> private <#= associatedClass.Name#> <#= association.SourceRoleName#>Value; public <#= associatedClass.Name#> <#= association.SourceRoleName#> { get {return <#= association.SourceRoleName#>Value; } set { if (<#= association.SourceRoleName#> != value){if (<#= association.SourceRoleName#> != null) <#= association.SourceRoleName#>.<#= association.TargetRoleName#> = null;<#= association.SourceRoleName#>Value = value;if (value != null){<#= association.SourceRoleName#>Value.<#= association.TargetRoleName#>=this;} } } }<# } else { Warning(String.Format("Ignoring BidirectionalAssociation from {0} to {1} because its SourceRoleName or TargetRoleName is not defined", classType.Name, associatedClass.Name)); } }#> } <# } } } #> <#+private static string MakeValidName(string typeName){ //Use the System.Text.RegularExpressions namespace specified in the import directive //Remove non-alpha characters string fixedName = Regex.Replace(typeName,"[^a-zA-Z]",""); return fixedName;} #>
In Solution Explorer, right-click LibraryCode.tt, and then click Run Custom Tool.
Open LibraryCode.cs.
Bidirectional associations appear in the Reservation and Item classes as properties, as shown in the following example:
public class Item { private Title titleValue; public Title title { get {return titleValue; } set { if (title != value) { if (title != null) title.stock = null; titleValue = value; if (value != null) { titleValue.stock=this; } } } } } public class Title { string name; private Item stockValue; public Item stock { get{ return stockValue; } set { if (stock != value) { if (stock != null) stock.title = null; stockValue = value; if (value != null) { stockValue.title=this; } } } } } public class Book : Item { } public class Member { } public class Library { } public class Loan { Date commenced; } public class Reservation { Date made; }
Splitting a Text Template into Multiple Files
If a text template becomes too large to manage effectively, you can split it into multiple files. This makes it easier to manage and it allows you to reuse common code. For more information, see How to: Split Text Templates Into Multiple Files.
Because your text template contains a helper function, you can separate it from the rest of the code by moving the helper function into a separate file. You can use the include directive in the original text template to access the helper function in the new file.
To split a text template into multiple files
On the Project menu, click Add New Item.
The Add New Item - Debugging dialog box appears.
In the Visual Studio installed templates pane, click Text File.
In Name, type HelperFunctions.ttinclude, and then click Add.
The system adds file to your project.
Copy the import directive and the helper functions from LibraryCode.tt into HelperFunctions.ttinclude.
HelperFunctions.ttinclude now looks like this:
<#@ import namespace = "System.Text.RegularExpressions" #> <#+ private static string MakeValidName(string typeName) { //Use the System.Text.RegularExpressions namespace specified in the import directive //Remove non-alpha characters string fixedName = Regex.Replace(typeName,"[^a-zA-Z]",""); return fixedName;} #>
On the File menu, click Save HelperFunctions.ttinclude.
Delete the copied import directive and helper functions from LibraryCode.tt.
Add the following directive to LibraryCode.tt:
<#@ include file = "HelperFunctions.ttinclude"#>
On the File menu, click Save LibraryCode.tt.
In Solution Explorer, right-click LibraryCode.tt, and then click Run Custom Tool.
Open LibraryCode.cs.
The output looks like this:
public class Item { private Title titleValue; public Title title { get {return titleValue; } set { if (title != value) { if (title != null) title.stock = null; titleValue = value; if (value != null) { titleValue.stock=this; } } } } } public class Title { string name; private Item stockValue; public Item stock { get{ return stockValue; } set { if (stock != value) { if (stock != null) stock.title = null; stockValue = value; if (value != null) { stockValue.title=this; } } } } } public class Book : Item { } public class Member { } public class Library { } public class Loan { Date commenced; } public class Reservation { Date made; }
Generating Text from a Class Feature
You can generate text from a helper function in a class feature, whether it is in the main file or a separate included file. In this example, you generate code from the source and target ends of the BidirectionalAssociation. Then you move that code into a helper function and call it from the main template.
To generate text from a class feature
Open HelperFunctions.ttinclude.
Append this function after the existing function.
注意
The function contains several blocks, but they are all class feature blocks (“<#+”). Also, MakeValidName has been applied to the role names.
<#+ private void CreateAccessor(ModelClass classType, ModelClass associatedClass, string fromRoleName, string toRoleName) { if(!string.IsNullOrEmpty(toRoleName) && !string.IsNullOrEmpty(fromRoleName)) { string validClassName = MakeValidName(associatedClass.Name); string validFromName = MakeValidName(fromRoleName); string validToName = MakeValidName(toRoleName); #> private <#= validClassName#> <#= validFromName#>Value; public <#= validClassName#> <#= validFromName#> { get{ return <#= validFromName #>Value; } set { if (<#= validFromName#> != value) { if (<#= validFromName#> != null) <#= validFromName#>.<#=validToName#> = null; <#= validFromName#>Value = value; if (value != null) { <#= validFromName#>Value.<#=validToName#>=this; } } } } <#+ } else { Warning(String.Format("Ignoring BidirectionalAssociation from {0} to {1} because its SourceRoleName or TargetRoleName is not defined", classType.Name, associatedClass.Name)); } } #>
On the File menu, click Save HelperFunctions.ttinclude.
Replace the code in LibraryCode.tt with the following code.
The changes are highlighted.
<#@ template inherits="Microsoft.VisualStudio.TextTemplating.VSHost.ModelingTextTransformation"#> <#@ ClassDiagramExample processor="ClassDiagramExampleDirectiveProcessor" requires="fileName='Sample.testcd'" #> <#@ include file = "HelperFunctions.ttinclude"#> <# foreach(ModelType type in this.ModelRoot.Types) { if(type is ModelClass) { ModelClass classType = (ModelClass)type; string className = MakeValidName(type.Name); if (string.IsNullOrEmpty(className)) { // We should also generate a proper validation constraint for this Warning(String.Format("ClassName '{0}' is not a valid class name", type.Name)); } else { #> public class <#= className#> <# if(classType.Superclass != null) { #>: <#= classType.Superclass.Name#> <# } #> { <# foreach(ModelAttribute attribute in classType.Attributes) { #> <#= attribute.Type#> <#= attribute.Name#>; <# } foreach(ClassOperation operation in classType.Operations) { #> public void <#= operation.Name#>() { } <# } #> <# foreach(BidirectionalAssociation association in BidirectionalAssociation.GetLinksToBidirectionalTargets(classType)) { CreateAccessor(classType, association.BidirectionalTarget, association.TargetRoleName, association.SourceRoleName); } #> <# foreach(BidirectionalAssociation association in BidirectionalAssociation.GetLinksToBidirectionalSources(classType)) {CreateAccessor(classType, association.BidirectionalSource, association.SourceRoleName, association.TargetRoleName); } #> } <# } } } #>
In Solution Explorer, right-click LibraryCode.tt, and then click Run Custom Tool.
The system saves your changes.
Validating the Model Before Code Generation
Certain characteristics of the model in Sample.testcd will cause your generator to generate incorrect code. These cases include:
Multiple ModelClasses that have the same name (or that have the same name after illegal characters and digits are removed).
Multiple ModelAttributes and Association roles with the same name in the same ModelClass.
Empty type name for a ModelAttribute.
Loops in the graph of inheritance relationships
As you develop your code generator, you will find other examples of incorrect code. To discourage the language user from creating models that have these errors, you should create appropriate validation constraints that will be applied when the user saves the model file.
In general, you should write validation constraints whenever you create templates or any other tools that process models. Using validation constraints ensures that the models that are passed to your tools and templates are clean enough for them to work from. You can display error messages when the language user validates the model or when a tool or template generates code from the model. However, you should display error messages as early as possible so that the language user can easily identify the elements to which the errors refer. For more information, see Walkthrough: Adding Validation to a Domain Model.
Accessing Multiple Models from a Text Template
You can access more than one model from the same text template. In this section, you create a second model, and then you use one text template for both of them. After you transform the text template, TestMultiple.tt, the code for both models will appear in the output file, TestMultiple.txt.
注意
For more information about how to use multiple models with text templates, see Accessing Models from Text Templates.
To add a second model to your project
On the Project menu, click Add New Item.
The Add New Item dialog box appears.
In the Add New Item dialog box, scroll down to the My Templates section, and click ClassDiagramExample.
警告
Do not click Class Diagram in the Visual Studio installed templates section.
In Name, type Second.testcd, and click Add.
The system adds the file to the project.
In Solution Explorer, open Second.testcd.
From the Toolbox, drag two class objects onto the model designer.
On the File menu, click Save Second.testcd to save the new model.
To access multiple models from a text template
On the Project menu, click Add New Item.
The Add New Item dialog box appears.
In the Visual Studio installed templates pane, click Text File.
In Name, type TestMultiple.tt, and click Add.
The system adds the file to the project.
In Solution Explorer, click TestMultiple.tt.
In the Properties window, be sure you set the Custom Tool property to TextTemplatingFileGenerator.
Add the following directives to the top of the file.
<#@ template inherits="Microsoft.VisualStudio.TextTemplating.VSHost.ModelingTextTransformation" #> <#@ output extension=".txt" #>
Add the following directives.
These directives allow you to access the models.
注意
The provides parameter for the Second.testcd model changes the name of the ModelRoot property to SecondModelRoot. This change prevents a conflict with the name of the ModelRoot property for the Sample.testcd model.
<#@ ClassDiagramExample processor="ClassDiagramExampleDirectiveProcessor" requires="fileName='Sample.testcd'" #> <#@ ClassDiagramExample processor="ClassDiagramExampleDirectiveProcessor" requires="fileName='Second.testcd'" provides="ModelRoot=SecondModelRoot"#>
Add the following code, which generates a list of the classes that are in each of the two models.
Model 1: <# foreach(ModelType type in this.ModelRoot.Types) { #> <#= type.Name#> <# } #> Model 2: <# foreach(ModelType type in this.SecondModelRoot.Types) { #> <#= type.Name#> <# } #>
In Solution Explorer, right-click TestMultiple.tt, and then click Run Custom Tool.
In Solution Explorer, open TestMultiple.txt, which looks like the following:
Model 1: Library Member Reservation Book Item CD Title MultipleAssociation 1 Loan Relation Model 2: ModelClass 1 ModelClass 2
Security
For more information, see Security of Text Templates.
Next Steps
This walkthrough does not provide a complete description of all functionality for text templates. For example, you can debug text templates, create custom directive processors, specify culture, and include assemblies in text templates. For more information, see Generating Artifacts Using Text Templates.
See Also
Concepts
Generating Artifacts Using Text Templates
Architecture of the Text Template Transformation Process