none
How to: Generate many files from one template? RRS feed

  • Question

  • I have a DSL now taking shape, including generating code that executes properly, and I must say I am pleased with how quickly it has come together.

    Now I have a secondary goal to fill: producing static documentation from the diagram. I'd like to produce a set of HTML files (one for each instance of the primary classes in the DSL). I don't see how I can do that with the current structure, which allows me to generate exactly one file from each template.

    Do I need to create a new DirectiveProcessor or TextTransformation class?

    Can you offer any pointers on how to create many files from one template?

    Thanks,
    Brian.

    Tuesday, June 14, 2005 2:20 PM

Answers

  • Hi Brian,

    Here's a bit of code that may be of help to you. The use is as follows:



    ...
    <# using(new File(<filename>, this)) { #>
    <template-content>
    <# } #>
    ...

     

    Basically you encapsulate the bit of template you want to use to produce a given file by a using statement that specifies the file to be created as first parameter and the text transformation instance as second.

    Here's the File class' definition:


     
       public class File : System.IDisposable
       {
          private string m_fileName;
          private string m_bufferContent;
          private StringBuilder m_textBuffer;

          public File(string fileName, TextTransformation textTransformation)
          {
             string outputDir = null;
             PropertyInfo outputDirProp = textTransformation.GetType().GetProperty("OutputDirectory");
             if (outputDirProp != null)
                outputDir = (string)outputDirProp.GetValue(textTransformation, null);

             m_fileName = (outputDir ?? "") + fileName;

             PropertyInfo genEnvProp = textTransformation.GetType().GetProperty("GenerationEnvironment", BindingFlags.FlattenHierarchy|BindingFlags.NonPublic|BindingFlags.Instance);
             if (genEnvProp != null)
             {
                m_textBuffer = (StringBuilder)genEnvProp.GetValue(textTransformation, null);
                m_bufferContent = m_textBuffer.ToString();
                m_textBuffer.Remove(0, m_textBuffer.Length);
             }
             return;

          }

          #region IDisposable Members
          public void Dispose()
          {
             if (m_textBuffer != null)
             {
                using (StreamWriter streamWriter = new StreamWriter(m_fileName))
                {
                   streamWriter.Write(m_textBuffer.ToString());
                }
                m_textBuffer.Remove(0, m_textBuffer.Length);
                m_textBuffer.Append(m_bufferContent);
             }
             return;
          }
          #endregion
       }

     

    The main idea here is to use the 'GenerationEnvironment' property on the text transformation object as this property holds the intermediate results of the evaluation. (the 'OutputDirectory' is a property that my own directive processor introduces in the text transformation class)

    HTH,
    Gabriel

    Wednesday, June 15, 2005 4:34 PM
  • Hi Brian,

    As has been mentioned, the DSL tools are currently planned to support this in the future, so this is only a *current* workaround.

    The snippets below assume that you have a added a context menu to your designer, by adding the menu in the CTC file (see ms-help://MS.VSCC.v80/MS.VSIPCC.v80/MS.VSIP.v80/dv_vsenvsdk/html/8d571db0-94d1-45ea-b2bd-121aa9026b80.htm for more info). OnMenuGenerateHtml below is the handler for the menu.

    The following snippets also assume that you are working with the Blank Language ConceptA and ConceptB entities, and that your language is called Language1.

    In the handler for the generate HTML files context menu, you can add code to access the model. If you add the handler to the CommandSet class, then this.CurrentData.Store gives you access to the model. For example, to access the current list of list of ConceptBs, you would use:

    this.CurrentData.Store.ElementDirectory.GetElements(ConceptB.MetaClassGuid);

    /// <summary>
    /// Generate An Html File for each ConceptB defined
    /// </summary>
    /// <param name="sender">The source of the event
    </param>
    /// <param name="e">The event parameters
    </param>
    internal void OnMenuGenerateHtml (object sender, EventArgs e)
    {
        IList conceptBElements = this.CurrentData.Store.ElementDirectory.GetElements(ConceptB.MetaClassGuid);

        foreach(ConceptB b in conceptBElements)
        {
            // Write the Html for this item
            writeHtmlFile(b);
        }
    }

    In the writeHtmlFile function you could either directly generate the HTML yourself, or write a template to do it for you as Alan suggests. The latter approach would be closer to the way the DSL tools will probably support this type of functionality, but currently still needs a couple of “tweaks”. The file could be generated using standard file IO, or using VSIP commands to add them to the project (perhaps from a .vstemplate file as the Add new item dialog does it).

    The basic process is to read the template text in, replace a placeholder in the template with the name of the current object being processed, run the template through the T4 template engine, and write the results to file.  I’m using GUIDs for the string Replace placeholders, to make them unlikely to clash with anything else in the file.

    I included the template used as an embedded resource within the designer DLL, so you don’t need to worry about where to locate it on disk. However, this means that you will also need to tell the template what the name of the model file is, as you may use it for more than one model file, and won’t know the name at designer creation time.

    /// <summary>
    /// Writes an Html File for the selected Concept B
    /// </summary>
    /// <param name="b">The Concept B to write the html for</param>
    private void writeHtmlFile(ConceptB b)
    {
          string htmFileName = @"c:\temp\" + b.Name + ".htm";  

          // Read the template file from the embedded resources, 
          // and Replace the Placeholders for the FileName and BElement Elements 
          // in the template

          string templateContent = readEmbeddedTextResource("Resources.ConceptBToHtm.templatesnippet");

          templateContent = templateContent.Replace(fileNamePlaceholder, this.CurrentData.FileName);
          templateContent = templateContent.Replace(currentBElementPlaceholder, b.Name);               

          // Run the T4 Templating engine over the template
          ITextTemplating templateGen = (ITextTemplating)Language1Package.GetGlobalService(typeof(STextTemplating));
          string htmlContent = templateGen.ProcessTemplate(null, templateContent, null);

          // Write the resulting content to an html file
          writeToFile(htmFileName, htmlContent);
    }

    // Placeholder for the filename of the model in the template
    const string fileNamePlaceholder = "21AB0162-D7EF-41d6-B614-3FBC8DF0B82D";   
    // Placeholder for the B element being rendered in the template
    const string currentBElementPlaceholder = "8CE6BDE0-E48F-4dc0-99D9-183FF700FB1F";

    The template snippet file would look something like:

    <#@ template inherits="Microsoft.VisualStudio.TextTemplating.VSHost.ModelingTextTransformation"#>
    <#@ concepta processor="Language1DirectiveProcessor" requires="fileName='21AB0162-D7EF-41d6-B614-3FBC8DF0B82D'" #>

    <#
    ConceptB b = getBElement("8CE6BDE0-E48F-4dc0-99D9-183FF700FB1F");
    if(b != null)
    {
    #>
    <html>
       <body>
              <h1>Help Page for <#=b.Name #></h1>
          </body>
    </html>
    <#
    }
    #>

    <#+
    private ConceptB getBElement(string bName)
    {
          ConceptB ret = null;
          foreach(ConceptB b in this.ConceptA.Bs)
          {     
                if(bName == b.Name)
                {
                      ret = b;
                      break;
                }
          }
          return ret;
    }
    #>

    Hope this helps.

    Thursday, June 16, 2005 9:20 AM

All replies

  • Hi Brian,

    Unfortunately, one file per template is a limitation of the current templating system.  We're planning on making this experience better, though, in future drops, so you'll be able to invoke a single template multiple times to generate multiple files. 

    Thanks,
    Grayson [MSFT]
    Tuesday, June 14, 2005 9:34 PM
  • Thanks Grayson.

    What about other routes then.

    Eg: If I were to create a menu item using the VS Extensibility Kit how would I get a reference to the root of the domain model?
    From there I could write code to build all the documentation I like.

    Brian.
    Tuesday, June 14, 2005 9:55 PM
  • You're right Brian, we don't directy support that at present.

    However, you can 
       (a) Provide a context menu on your diagram that invokes your own HTML generator;
       (b) Invoke the Templating engine per class, to create a file from a template you supply.

    Yes, it's a bit of work, but not too difficult.
    We'll shortly post an example in which this technique is illustrated.
    Tuesday, June 14, 2005 10:58 PM
    Moderator
  • Alan,

    That is great news, I'll look forward to seeing your example.
    If I get anything working in the meantime I'll post it here.

    Thanks,
    Brian.
    Wednesday, June 15, 2005 10:24 AM
  • Hi Brian,

    Here's a bit of code that may be of help to you. The use is as follows:



    ...
    <# using(new File(<filename>, this)) { #>
    <template-content>
    <# } #>
    ...

     

    Basically you encapsulate the bit of template you want to use to produce a given file by a using statement that specifies the file to be created as first parameter and the text transformation instance as second.

    Here's the File class' definition:


     
       public class File : System.IDisposable
       {
          private string m_fileName;
          private string m_bufferContent;
          private StringBuilder m_textBuffer;

          public File(string fileName, TextTransformation textTransformation)
          {
             string outputDir = null;
             PropertyInfo outputDirProp = textTransformation.GetType().GetProperty("OutputDirectory");
             if (outputDirProp != null)
                outputDir = (string)outputDirProp.GetValue(textTransformation, null);

             m_fileName = (outputDir ?? "") + fileName;

             PropertyInfo genEnvProp = textTransformation.GetType().GetProperty("GenerationEnvironment", BindingFlags.FlattenHierarchy|BindingFlags.NonPublic|BindingFlags.Instance);
             if (genEnvProp != null)
             {
                m_textBuffer = (StringBuilder)genEnvProp.GetValue(textTransformation, null);
                m_bufferContent = m_textBuffer.ToString();
                m_textBuffer.Remove(0, m_textBuffer.Length);
             }
             return;

          }

          #region IDisposable Members
          public void Dispose()
          {
             if (m_textBuffer != null)
             {
                using (StreamWriter streamWriter = new StreamWriter(m_fileName))
                {
                   streamWriter.Write(m_textBuffer.ToString());
                }
                m_textBuffer.Remove(0, m_textBuffer.Length);
                m_textBuffer.Append(m_bufferContent);
             }
             return;
          }
          #endregion
       }

     

    The main idea here is to use the 'GenerationEnvironment' property on the text transformation object as this property holds the intermediate results of the evaluation. (the 'OutputDirectory' is a property that my own directive processor introduces in the text transformation class)

    HTH,
    Gabriel

    Wednesday, June 15, 2005 4:34 PM
  • Hi Brian,

    As has been mentioned, the DSL tools are currently planned to support this in the future, so this is only a *current* workaround.

    The snippets below assume that you have a added a context menu to your designer, by adding the menu in the CTC file (see ms-help://MS.VSCC.v80/MS.VSIPCC.v80/MS.VSIP.v80/dv_vsenvsdk/html/8d571db0-94d1-45ea-b2bd-121aa9026b80.htm for more info). OnMenuGenerateHtml below is the handler for the menu.

    The following snippets also assume that you are working with the Blank Language ConceptA and ConceptB entities, and that your language is called Language1.

    In the handler for the generate HTML files context menu, you can add code to access the model. If you add the handler to the CommandSet class, then this.CurrentData.Store gives you access to the model. For example, to access the current list of list of ConceptBs, you would use:

    this.CurrentData.Store.ElementDirectory.GetElements(ConceptB.MetaClassGuid);

    /// <summary>
    /// Generate An Html File for each ConceptB defined
    /// </summary>
    /// <param name="sender">The source of the event
    </param>
    /// <param name="e">The event parameters
    </param>
    internal void OnMenuGenerateHtml (object sender, EventArgs e)
    {
        IList conceptBElements = this.CurrentData.Store.ElementDirectory.GetElements(ConceptB.MetaClassGuid);

        foreach(ConceptB b in conceptBElements)
        {
            // Write the Html for this item
            writeHtmlFile(b);
        }
    }

    In the writeHtmlFile function you could either directly generate the HTML yourself, or write a template to do it for you as Alan suggests. The latter approach would be closer to the way the DSL tools will probably support this type of functionality, but currently still needs a couple of “tweaks”. The file could be generated using standard file IO, or using VSIP commands to add them to the project (perhaps from a .vstemplate file as the Add new item dialog does it).

    The basic process is to read the template text in, replace a placeholder in the template with the name of the current object being processed, run the template through the T4 template engine, and write the results to file.  I’m using GUIDs for the string Replace placeholders, to make them unlikely to clash with anything else in the file.

    I included the template used as an embedded resource within the designer DLL, so you don’t need to worry about where to locate it on disk. However, this means that you will also need to tell the template what the name of the model file is, as you may use it for more than one model file, and won’t know the name at designer creation time.

    /// <summary>
    /// Writes an Html File for the selected Concept B
    /// </summary>
    /// <param name="b">The Concept B to write the html for</param>
    private void writeHtmlFile(ConceptB b)
    {
          string htmFileName = @"c:\temp\" + b.Name + ".htm";  

          // Read the template file from the embedded resources, 
          // and Replace the Placeholders for the FileName and BElement Elements 
          // in the template

          string templateContent = readEmbeddedTextResource("Resources.ConceptBToHtm.templatesnippet");

          templateContent = templateContent.Replace(fileNamePlaceholder, this.CurrentData.FileName);
          templateContent = templateContent.Replace(currentBElementPlaceholder, b.Name);               

          // Run the T4 Templating engine over the template
          ITextTemplating templateGen = (ITextTemplating)Language1Package.GetGlobalService(typeof(STextTemplating));
          string htmlContent = templateGen.ProcessTemplate(null, templateContent, null);

          // Write the resulting content to an html file
          writeToFile(htmFileName, htmlContent);
    }

    // Placeholder for the filename of the model in the template
    const string fileNamePlaceholder = "21AB0162-D7EF-41d6-B614-3FBC8DF0B82D";   
    // Placeholder for the B element being rendered in the template
    const string currentBElementPlaceholder = "8CE6BDE0-E48F-4dc0-99D9-183FF700FB1F";

    The template snippet file would look something like:

    <#@ template inherits="Microsoft.VisualStudio.TextTemplating.VSHost.ModelingTextTransformation"#>
    <#@ concepta processor="Language1DirectiveProcessor" requires="fileName='21AB0162-D7EF-41d6-B614-3FBC8DF0B82D'" #>

    <#
    ConceptB b = getBElement("8CE6BDE0-E48F-4dc0-99D9-183FF700FB1F");
    if(b != null)
    {
    #>
    <html>
       <body>
              <h1>Help Page for <#=b.Name #></h1>
          </body>
    </html>
    <#
    }
    #>

    <#+
    private ConceptB getBElement(string bName)
    {
          ConceptB ret = null;
          foreach(ConceptB b in this.ConceptA.Bs)
          {     
                if(bName == b.Name)
                {
                      ret = b;
                      break;
                }
          }
          return ret;
    }
    #>

    Hope this helps.

    Thursday, June 16, 2005 9:20 AM
  • Excellent, thank you both.

    It looks like either of those solutions will do all I need.

    Brian.
    Thursday, June 16, 2005 11:29 AM
  • Hi,

    I mentioned this useful also for me, but now, I am solving problem of "OutputDirectory"

    I want to get same directory of template file, but i don't know the right way.

    I was trying it using DTE, but

    DTE dte = (DTE)Package.GetGlobalService(typeof(SDTE))

    returns null.

    Can you please tell, how to get reference to DTE ? I'am preferring this, because via this interface i can access SourceControl property too.

    Thanks, Jan

    Thursday, January 26, 2006 2:52 PM
  • Oh, now i see, that templates are run in another AppDomain that designer, so DTE service is null.

    Custom DirectiveProcessor will solve this...

     

    Thursday, January 26, 2006 5:39 PM
  • Bear in mind that if you write templates which manipulate Visual Studio via the DTE or other means, then you won't be able to run them with command-line tools to integrate them into any other processes such as build. That may or may not be important for your scenarios.

    ------------

    Oh, now i see, that templates are run in another AppDomain that designer, so DTE service is null.

    Custom DirectiveProcessor will solve this...



    Link
    Friday, January 27, 2006 10:40 PM
    Moderator
  • The name of the element is used in Annie's approach to get the reference to the right element. Somebody any better ideas to use something to identify an element instead of the name?

    I tried to use the Id of the element, but the Id seems to be different in the code then it is in the template.

    Greets,

    Gerben [Avanade]

    Wednesday, February 15, 2006 6:07 PM
  • The sample code from Annie changed a little bit in the June CTP:

    http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=466746&SiteID=1

    Saturday, June 10, 2006 8:07 PM
  • Hi,

    I tried your proposed solution and it indeed works as said. The next problem when having generated is being able to include them in the project containing the model. Since the splitting of the files is done in the template, by handing over to a 'handling' object it is this handling object that creates and knows about the file. So how can this handling object (the File object in your example) interact with the current DTE?  I read in a post that the textgenerator runs in a seperate AppDomain so does this mean that this is not possible.

    Do you have experience with this?

    Thanks in advance.

    Regards,

    W. Jansoone

    Thursday, December 28, 2006 10:31 PM