从文本模板访问模型

使用文本模板,可以创建基于域特定语言模型的报表文件、源代码文件和其他文本文件。 有关文本模板的基本信息,请参阅代码生成和 T4 文本模板。 调试 DSL 时,文本模板将在实验模式下工作,并且还可在已部署 DSL 的计算机上工作。

注意

创建 DSL 解决方案时,调试项目中会生成示例文本模板 *.tt 文件。 更改域类的名称时,这些模板将不再工作。 尽管如此,它们包括你需要的基本指令,并提供可更新以匹配 DSL 的示例。

从文本模板访问模型:

  • 将模板指令的 inherit 属性设置为 Microsoft.VisualStudio.TextTemplating.VSHost.ModelingTextTransformation。 这将提供对 Store 的访问。

  • 为要访问的 DSL 指定指令处理器。 这会加载 DSL 程序集,以便在文本模板的代码中使用其域类、属性和关系。 它还会加载你指定的模型文件。

    从 DSL 最小语言模板创建新 Visual Studio 解决方案时,将在调试项目中创建类似于以下示例的 .tt 文件。

<#@ template inherits="Microsoft.VisualStudio.TextTemplating.VSHost.ModelingTextTransformation" #>
<#@ output extension=".txt" #>
<#@ MyLanguage processor="MyLanguageDirectiveProcessor" requires="fileName='Sample.myDsl1'" #>

This text will be output directly.

This is the name of the model: <#= this.ModelRoot.Name #>

Here is a list of elements in the model:
<#
  // When you change the DSL Definition, some of the code below may not work.
  foreach (ExampleElement element in this.ExampleModel.Elements)
  {#>
<#= element.Name #>
<#
  }
#>

请注意有关此模板的以下几点:

  • 模板可以使用在 DSL 定义中定义的域类、属性和关系。

  • 模板加载你在 requires 属性中指定的模型文件。

  • this 中的属性包含根元素。 代码可以在其中导航到模型的其他元素。 属性名称通常与 DSL 的根域类相同。 在此示例中,它是 this.ExampleModel

  • 虽然编写代码片段的语言是 C#,但你可以生成任何类型的文本。 或者,可以通过将 language="VB" 属性添加到 template 指令,在 Visual Basic 中编写代码。

  • 若要调试模板,请将 debug="true" 添加到 template 指令。 如果发生异常,则模板将在 Visual Studio 的另一个实例中打开。 如果希望在代码中的特定点中断调试器,请插入语句 System.Diagnostics.Debugger.Break();

    有关详细信息,请参阅调试 T4 文本模板

关于 DSL 指令处理器

模板可以使用在 DSL 定义中定义的域类。 这是通过通常出现在模板开头附近的指令来进行的。 在上面的示例中,它如下所示。

<#@ MyLanguage processor="MyLanguageDirectiveProcessor" requires="fileName='Sample.myDsl1'" #>

指令名称(在本例中为 MyLanguage)派生自 DSL 名称。 它调用作为 DSL 的一部分生成的指令处理器。 可以在 Dsl\GeneratedCode\DirectiveProcessor.cs 中找到它的源代码。

DSL 指令处理器执行两个主要任务:

  • 它有效地将程序集和导入指令插入到引用 DSL 的模板中。 这允许你在模板代码中使用域类。

  • 它加载你在 requires 参数中指定的文件,并在 this 中设置一个引用所加载模型的根元素的属性。

在运行模板之前验证模型

可以在执行模板之前对模型进行验证。

<#@ MyLanguage processor="MyLanguageDirectiveProcessor" requires="fileName='Sample.myDsl1';validation='open|load|save|menu'" #>

请注意:

  1. filenamevalidation 参数用“;”分隔,并且不能有其他分隔符或空格。

  2. 验证类别的列表确定将执行哪些验证方法。 应该用“|”分隔多个类别,并且不得有其他分隔符或空格。

    如果发现错误,将在错误窗口中报告该错误,结果文件将包含错误消息。

从文本模板访问多个模型

注意

此方法使你可以读取同一模板中的多个模型,但不支持 ModelBus 引用。 若要读取由 ModelBus 引用链接的模型,请参阅在文本模板中使用 Visual Studio ModelBus

如果要从同一文本模板访问多个模型,则必须为每个模型调用一次生成的指令处理器。 必须在 requires 参数中指定每个模型的文件名。 必须在 provides 参数中指定要用于根域类的名称。 必须为每个指令调用中的 provides 参数指定不同值。 例如,假设有三个名为 Library.xyz、School.xyz 和 Work.xyz 的模型文件。 若要从同一文本模板访问它们,必须编写三个类似于下面的指令调用。

<#@ ExampleModel processor="<YourLanguageName>DirectiveProcessor" requires="fileName='Library.xyz'" provides="ExampleModel=LibraryModel" #>
<#@ ExampleModel processor="<YourLanguageName>DirectiveProcessor" requires="fileName='School.xyz'" provides="ExampleModel=SchoolModel" #>
<#@ ExampleModel processor="<YourLanguageName>DirectiveProcessor" requires="fileName='Work.xyz'" provides="ExampleModel=WorkModel" #>

注意

此代码示例适用于基于最小语言解决方案模板的语言。

若要访问文本模板中的模型,你现在可以编写类似于以下示例代码的代码。

<#
foreach (ExampleElement element in this.LibraryModel.Elements)
...
foreach (ExampleElement element in this.SchoolModel.Elements)
...
foreach (ExampleElement element in this.WorkModel.Elements)
...
#>

动态加载模型

如果要在运行时确定要加载的模型,可以在程序代码中动态加载模型文件,而不是使用特定于 DSL 的指令。

但是,特定于 DSL 的指令的功能之一是导入 DSL 命名空间,以便模板代码可以使用该 DSL 中定义的域类。 由于未使用指令,因此必须为可能加载的所有模型添加 <assembly> 和 <import> 指令。 如果可以加载的不同模型都是同一 DSL 的所有实例,那么这很简单。

若要加载该文件,最有效的方法是使用 Visual Studio ModelBus。 在典型场景中,文本模板将使用特定于 DSL 的指令以常规方式加载第一个模型。 该模型将包含对另一个模型的 ModelBus 引用。 可以使用 ModelBus 打开引用的模型并访问特定元素。 有关详细信息,请参阅在文本模板中使用 Visual Studio ModelBus

在不太常见的情况下,你可能想要打开一个只具有文件名且不在当前 Visual Studio 项目中的模型文件。 在这种情况下,可以使用操作说明:在程序代码中从文件打开模型中所述的方法来打开文件。

从模板生成多个文件

如果要生成几个文件(例如,为模型中的每个元素生成一个单独的文件),则有几种可能的方法。 默认情况下,每个模板文件仅生成一个文件。

拆分长文件

在此方法中,你将使用一个模板生成单个文件,并用分隔符分隔。 然后,将文件拆分为其各个部分。 有两个模板,一个用于生成单个文件,另一个用于拆分。

LoopTemplate.t4 生成单个长文件。 请注意,其文件扩展名为“.t4”,因为当你单击“转换所有模板”时不应直接处理。 此模板采用参数,该参数指定分隔段的分隔符字符串:

<#@ template inherits="Microsoft.VisualStudio.TextTemplating.VSHost.ModelingTextTransformation" #>
<#@ parameter name="delimiter" type="System.String" #>
<#@ output extension=".txt" #>
<#@ MyDSL processor="MyDSLDirectiveProcessor" requires="fileName='SampleModel.mydsl1';validation='open|load|save|menu'" #>
<#
  // Create a file segment for each element:
  foreach (ExampleElement element in this.ExampleModel.Elements)
  {
    // First item is the delimiter:
#>
<#= string.Format(delimiter, element.Id) #>

   Element: <#= element.Name #>
<#
   // Here you generate more content derived from the element.
  }
#>

LoopSplitter.tt 调用 LoopTemplate.t4,然后将生成的文件拆分为其各个段。 请注意,此模板不必是建模模板,因为它不会读取模型。

<#@ template hostspecific="true" language="C#" #>
<#@ output extension=".txt" #>
<#@ import namespace="Microsoft.VisualStudio.TextTemplating" #>
<#@ import namespace="System.Runtime.Remoting.Messaging" #>
<#@ import namespace="System.IO" #>

<#
  // Get the local path:
  string itemTemplatePath = this.Host.ResolvePath("LoopTemplate.t4");
  string dir = Path.GetDirectoryName(itemTemplatePath);

  // Get the template for generating each file:
  string loopTemplate = File.ReadAllText(itemTemplatePath);

  Engine engine = new Engine();

  // Pass parameter to new template:
  string delimiterGuid = Guid.NewGuid().ToString();
  string delimiter = "::::" + delimiterGuid + ":::";
  CallContext.LogicalSetData("delimiter", delimiter + "{0}:::");
  string joinedFiles = engine.ProcessTemplate(loopTemplate, this.Host);

  string [] separateFiles = joinedFiles.Split(new string [] {delimiter}, StringSplitOptions.None);

  foreach (string nameAndFile in separateFiles)
  {
     if (string.IsNullOrWhiteSpace(nameAndFile)) continue;
     string[] parts = nameAndFile.Split(new string[]{":::"}, 2, StringSplitOptions.None);
     if (parts.Length < 2) continue;
#>
 Generate: [<#= dir #>] [<#= parts[0] #>]
<#
     // Generate a file from this item:
     File.WriteAllText(Path.Combine(dir, parts[0] + ".txt"), parts[1]);
  }
#>