August 31, 2015

Setup the Fortis code generation with TDS

Setup the Fortis code generation with TDS

I use now Fortis to generate my C# classes Models who correspond to the Sitecore Templates. If you don't know Fortis you should read it first: http://fortis.ws/about/. It is really valuable to have this kind of strongly typed model in our projects because it improve the productivity and reduce the errors in the code. We had internal solution to generate those models before but the guys of Fortis have really done a great job on it.

The easiest way to generate those class for me is by using TDS. Here is the steps to setup the code generation:

  1. Enable the code generation in your TDS project. To do that:
    1. Right click on the project, then properties
    2. Check Enable Code Generation
    3. In "Target Project", specify the project where the class will be generated.
    4. In "code generation target file", specify the relative path in the target project where the class will be generated including the class name.
  2. Now you should have a folder named "Code Generation Templates" below your TDS project. Open this folder in windows explorer.
  3. Create a first file named Header.tt and add the following content in it:
    <#@ template language="C#" #>
    <#
    // The Header Template is used to generate code that goes at the top of the generated code file. This template is executed only once.
    // it will typically generate #using statements or add other one time only elements to the generated file.
    
    //Parameters passed to Template for code generation
    
    //   Model: The ProjectHeder object contains information about the TDS project generating the code and the project where the
    //          generated code will reside.
    #>
    <#@ parameter name="Model" type="HedgehogDevelopment.SitecoreProject.VSIP.CodeGeneration.Models.ProjectHeader" #>
    <#
    //  DefaultNamespace: The DefaultNamespace parameter contains the default namespace of the project where the generated
    //       code file resides.
    #>
    <#@ parameter name="DefaultNamespace" type="System.String" #>
    <#
    /* The project header class:
    public class ProjectHeader
    {
        /// <summary>
        /// The name of the TDS project
        /// </summary>
        public string TDSProjectName { get; set; }
    
        /// <summary>
        /// The full path to the TDS project file
        /// </summary>
        public string TDSProjectPath { get; set; }
        
        /// <summary>
        /// The name of the target project
        /// </summary>
        public string TargetProjectName { get; set; }
    
        /// <summary>
        /// The full path to the target project
        /// </summary>
        public string TargetProjectPath { get; set; }
    
        /// <summary>
        /// The full path to the generated project file
        /// </summary>
        public string GeneratedFilePath { get; set; }
    
        /// <summary>
        /// Gets or sets the base namespace as set in the TDS project.
        /// </summary>
        public string BaseNamespace { get; set; }
    }
    */
    #>
    
    using System;
    using System.Collections.Generic;
    using Sitecore.Data.Items;
    using Sitecore.ContentSearch;
    using Sitecore.ContentSearch.Linq.Common;
    using Fortis.Model;
    using Fortis.Model.Fields;
    using Fortis.Providers;
  4. Create a second file in the same directory named, Item.tt with the following content:
    <#@ template language="C#" debug="true" #>
    <#@ assembly name="System.Core.dll" #>
    <#@ import namespace="System.Collections" #>
    <#@ import namespace="System.Collections.Generic" #>
    <#@ import namespace="System.Globalization" #>
    <#@ import namespace="System.Linq" #>
    <#@ import namespace="System.Text" #>
    <#@ import namespace="System.Text.RegularExpressions" #>
    <#@ import namespace="HedgehogDevelopment.SitecoreProject.VSIP.CodeGeneration.Models" #>
    <#
    // The Item template is called for every Sitecore item elegible for code generation. TDS will execute this T4 template every time a
    // template or field on a template changes in TDS. The T4 template is responsible for generating code for only the Sitecore item TDS
    // passes to the template. TDS will join all created templates together to create a single file. 
    //
    // Version 4 of TDS only supports generating code for Sitecore Template items.
    
    // Parameters passed to the T4 Template for code generation
    
    //   Model: This parameter contains information about the Sitecore Item to be generated. The Model will always be a type that inherits from SitecoreItem.
    #>
    <#@ parameter name="Model" type="HedgehogDevelopment.SitecoreProject.VSIP.CodeGeneration.Models.SitecoreItem" #>
    <#
    //  DefaultNamespace: The DefaultNamespace parameter contains the default namespace of the project where the generated
    //       code file resides.
    #>
    <#@ parameter name="DefaultNamespace" type="System.String" #>
    <#
    /*   The following types are used during code generation:
    
    /// <summary>
    /// Represents the SitecoreItem to be passed to the T4 template. Any object that is a SitecoreItem will inherit from this object.
    /// </summary>
    public class SitecoreItem
    {
        /// <summary>
        /// The Sitecore item ID.
        /// </summary>
        public Guid ID { get; set; }
    
        /// <summary>
        /// The name of the Sitecore item. This may be different than the Display Name.
        /// </summary>
        public string Name { get; set; }
    
        /// <summary>
        /// The path to the item from the Sitecore root.
        /// </summary>
        public string Path { get; set; }
    
        /// <summary>
        /// Any custom data associated with the item. This data can be set on the property page associated with the Sitecore item in the solution explorer.
        /// </summary>
        public string Data { get; set; }
    
        /// <summary>
        /// The Parent SitecoreItem in the Sitecore hierarchy.
        /// </summary>
        public SitecoreItem Parent { get; set; }
    
        /// <summary>
        /// The name of the template the item is based on
        /// </summary>
        public string TemplateName { get; set; }
    
        /// <summary>
        /// The ID of the template the item is based on
        /// </summary>
        public Guid TemplateId { get; set; }
    
        /// <summary>
        /// Additional sitecore fields. These fields are set on the Code Generation Property page. 
        /// The key in the dictionary is the Field name, the value is the value of the field.
        /// </summary>
        public Dictionary<string, string> SitecoreFields;
    
     /// <summary>
        /// The calculated Namespace for the item. Each Sitecore item above the template is represented as part of the namespace. 
        /// A new Namespace can be set at any item in the items property page. This allows the code generation namespace to be arranged differently 
        /// than the Sitecore template hierarchy.
        /// </summary>
        public string Namespace { get; set; }
    }
    
    /// <summary>
    /// Represents Template specific information for code generation.
    /// </summary>
    public class SitecoreTemplate : SitecoreItem
    {
        /// <summary>
        /// The namespace broken out into individual segments.
        /// </summary>
        public IEnumerable<string> NamespaceSegments { get; }
    
        /// <summary>
        /// A list of all templates this template inherits from.
        /// </summary>
        public List<SitecoreTemplate> BaseTemplates { get; set; }
    
        /// <summary>
        /// A list of Sitecore Fields that make up this sitecore template.
        /// </summary>
        public List<SitecoreField> Fields { get; set; }
    }
    
    /// <summary>
    /// Represents Field specific information for code generation.
    /// </summary>
    public class SitecoreField : SitecoreItem
    {
        /// <summary>
        /// The type of the field from the template editor.
        /// </summary>
        public string Type { get; set; }
    }
    */   
    #>
    <#
    SitecoreTemplate template = Model as SitecoreTemplate;
    
    if (template == null)
    {
     return string.Empty;
    }
    
    
    var baseTemplates = template.BaseTemplates;
    var baseTemplatesRecursive = RecursiveBaseTemplateList(template);
    var combinedTemplateList = new List<SitecoreTemplate>(baseTemplatesRecursive);
    
    combinedTemplateList.Add(template);
    
    #>
    #region <#= template.Name #> (<#= RelativeNamespace(template) #>)
    namespace <#= FullNamespace(template) #>
    {
        
    <#
     var isRenderingParametersTemplate = HasRenderingOptionsBase(combinedTemplateList);
     if (isRenderingParametersTemplate)
     {
    #>
        using Fortis.Model.RenderingParameters;
    <#
     }
    #>
    
     /// <summary>
     /// <para>Template interface</para>
     /// <para>Template: <#= template.Name #></para>
     /// <para>ID: <#= template.ID.ToString("b").ToUpper() #></para>
     /// <para><#= template.Path #></para>
     /// </summary>
     [TemplateMapping("<#= template.ID.ToString("b").ToUpper() #>", "<#= GetTemplateMappingType(isRenderingParametersTemplate, true) #>")]
     public partial interface <#= InterfaceName(template.Name) #> : <#= GetBaseTemplateInterface(isRenderingParametersTemplate) #> <#
     foreach (var baseTemplate in baseTemplates)
     {
      #>, <#= FullNamespace(baseTemplate) + "." + InterfaceName(baseTemplate.Name)  #><#
     }
    #>
    
     {  
    <# 
      foreach(var field in template.Fields)
      {
    #>
         /// <summary>
      /// <para>Template: <#= template.Name #></para><para>Field: <#= TitleCase(field.Name) #></para><para>Data type: <#= field.Type #></para>
            /// </summary>
    <# if (!isRenderingParametersTemplate && IsSupportedSearchFieldType(field.Type)) { #>
      [IndexField("<#= field.Name.Replace(" ", "_").ToLowerInvariant() + GetFieldTypeSearchAffix(field.Type) #>")]
    <# } #>
      <#= GetFieldWrapperTypeInterface(field.Type) #> <#= TitleCase(field.Name) #> { get; }
    <# if (!isRenderingParametersTemplate && IsSupportedSearchFieldType(field.Type)) { #>
    
         /// <summary>
      /// <para>Template: <#= template.Name #></para><para>Field: <#= TitleCase(field.Name) #></para><para>Data type: <#= field.Type #></para>
            /// </summary>
      [IndexField("<#= field.Name.Replace(" ", "_").ToLowerInvariant() #>")]
    <# } #>
       <#= GetReturnType(GetFieldWrapperType(field.Type)) #> <#= TitleCase(field.Name) #>Value { get; }
    <#
      }
    #>
     }
    
     /// <summary>
     /// <para>Template class</para><para><#= template.Path #></para>
     /// </summary>
     [PredefinedQuery("TemplateId", ComparisonType.Equal, "<#= template.ID.ToString("b").ToUpper() #>", typeof(Guid))]
     [TemplateMapping("<#= template.ID.ToString("b").ToUpper() #>", "<#= GetTemplateMappingType(isRenderingParametersTemplate, false) #>")]
     public partial class <#= ClassName(template.Name) #> : <#= GetBaseTemplateClass(isRenderingParametersTemplate) #>, <#= InterfaceName(template.Name) #>
     {
    <# if (!isRenderingParametersTemplate) { #>
      private Item _item = null;
    
      public <#= ClassName(template.Name) #>(ISpawnProvider spawnProvider) : base(null, spawnProvider) { }
    
      public <#= ClassName(template.Name) #>(Guid id, ISpawnProvider spawnProvider) : base(id, spawnProvider) { }
    
      public <#= ClassName(template.Name) #>(Guid id, Dictionary<string, object> lazyFields, ISpawnProvider spawnProvider) : base(id, lazyFields, spawnProvider) { }
    
    <# } #>
      public <#= ClassName(template.Name) #>(<#= GetConstructorParameters(isRenderingParametersTemplate) #>, ISpawnProvider spawnProvider) : base(<#= GetBaseConstructorParameters(isRenderingParametersTemplate) #>, spawnProvider)
      {
    <# if (!isRenderingParametersTemplate) { #>
       _item = item;
    <# } #>
      }
    
    <#
     foreach(var fieldTemplate in combinedTemplateList)
     {
            foreach(var field in fieldTemplate.Fields)
            {
    #>
      public const string <#= TitleCase(field.Name) #>FieldName = "<#= field.Name #>";
    
      /// <summary><para>Template: <#= template.Name #></para><para>Field: <#= TitleCase(field.Name) #></para><para>Data type: <#= field.Type #></para></summary>
    <# if (!isRenderingParametersTemplate && IsSupportedSearchFieldType(field.Type)) { #>
      [IndexField("<#= field.Name.Replace(" ", "_").ToLowerInvariant() + GetFieldTypeSearchAffix(field.Type) #>")]
    <# } #>
      public virtual <#= GetFieldWrapperTypeInterface(field.Type) #> <#= TitleCase(field.Name) #>
      {
    <# if (isRenderingParametersTemplate) { #>
       get { return (Fortis.Model.RenderingParameters.Fields.<#= GetFieldWrapperType(field.Type) #>)GetField("<#= field.Name #>", "<#= field.Type.ToLower() #>"); }
    <# } else { #>
       get { return GetField<<#= GetFieldWrapperType(field.Type) #>>("<#= field.Name #>"<# if (IsSupportedSearchFieldType(field.Type)) { #>, "<#= field.Name.Replace(" ", "_").ToLowerInvariant() + GetFieldTypeSearchAffix(field.Type) #>"<# } #>); }
    <# } #>
      }
    
      /// <summary><para>Template: <#= template.Name #></para><para>Field: <#= TitleCase(field.Name) #></para><para>Data type: <#= field.Type #></para></summary>
    <# if (!isRenderingParametersTemplate && IsSupportedSearchFieldType(field.Type)) { #>
      [IndexField("<#= field.Name.Replace(" ", "_").ToLowerInvariant() #>")]
    <# } #>
       public <#= GetReturnType(GetFieldWrapperType(field.Type)) #> <#= TitleCase(field.Name) #>Value
      {
       get { return <#= TitleCase(field.Name) #>.Value; }
      }
    <#
      }
     }
    #> 
     }
    }
    #endregion
    
    <#+
    private const string SitecoreSystemTemplatePath = "/sitecore/templates/System/";
    private const string ClientTemplatePath = "/sitecore/templates/User Defined/";
    
    public string GetBaseTemplateInterface(bool isRenderingParametersTemplate)
    {
     return InterfaceName(GetBaseTemplateClass(isRenderingParametersTemplate));
    }
    
    public string GetBaseTemplateClass(bool isRenderingParametersTemplate)
    {
     return isRenderingParametersTemplate ? "RenderingParameterWrapper" : "CustomItemWrapper";
    }
    
    public string GetConstructorParameters(bool isRenderingParametersTemplate)
    {
     return (isRenderingParametersTemplate ? "Dictionary<string, string> " : "Item ") + GetBaseConstructorParameters(isRenderingParametersTemplate);
    }
    
    public string GetBaseConstructorParameters(bool isRenderingParametersTemplate)
    {
     return isRenderingParametersTemplate ? "parameters" : "item";
    }
    
    public string GetTemplateMappingType(bool isRenderingParametersTemplate, bool isInterface)
    {
     return (isInterface ? "Interface" : string.Empty) + (isRenderingParametersTemplate ? "RenderingParameter" : isInterface ? "Map" : string.Empty);
    }
    
    public string GetReturnType(string fieldType)
    {
     switch (fieldType)
     {
      case "BooleanFieldWrapper":
      case "IBooleanFieldWrapper":
       return "bool";
      case "DateTimeFieldWrapper":
      case "IDateTimeFieldWrapper":
       return "DateTime";
      case "ListFieldWrapper":
      case "IListFieldWrapper":
       return "IEnumerable<Guid>";
      case "IntegerFieldWrapper":
      case "IIntegerFieldWrapper":
       return "long";
      case "NumberFieldWrapper":
      case "INumberFieldWrapper":
       return "float";
      default:
       return "string";
     }
    }
    
    public bool IsSupportedSearchFieldType(string typeName)
    {
     switch (typeName.ToLower())
     {
      case "checkbox":
      case "date":
      case "datetime":
      case "checklist":
      case "treelist":
      case "treelist with search":
      case "treelistex":
      case "multilist":
      case "multilist with search":
      case "droplink":
      case "droptree":
      case "general link":
      case "general link with search":
      case "text":
      case "single-line text":
      case "multi-line text":
      case "rich text":
      case "number":
      case "integer":
      case "tags":
       return true;
      default:
       return false;
     }
    }
    
    public string GetFieldTypeSearchAffix(string typeName)
    {
     var affix = "FieldWrapper";
     
     switch (typeName.ToLower())
     {
      case "checkbox":
       affix = "_b";
       break;
      case "date":
      case "datetime":
       affix = "_tdt";
       break;
      case "checklist":
      case "treelist":
      case "treelist with search":
      case "treelistex":
      case "multilist":
      case "multilist with search":
      case "tags":
       affix = "_sm";
       break;
      case "droplink":
      case "droptree":
       affix = "_s";
       break;
      case "general link":
      case "general link with search":
      case "text":
      case "single-line text":
      case "multi-line text":
      case "rich text":
       affix = "_t";
       break;
      case "number":
       affix = "_tf";
       break;
      case "integer":
       affix = "_tl";
       break;
      default:
       throw new Exception("No mapping for " + typeName);
     }
    
     return affix;
    }
    
    public string GetFieldWrapperTypeInterface(string typeName)
    {
     return "I" + GetFieldWrapperType(typeName);
    }
    
    public string GetFieldWrapperType(string typeName)
    {
     var wrapperType = "FieldWrapper";
     
     switch (typeName.ToLower())
     {
      case "checkbox":
       wrapperType = "BooleanFieldWrapper";
       break;
      case "image":
       wrapperType = "ImageFieldWrapper";
       break;
      case "file":
       wrapperType = "FileFieldWrapper";
       break;
      case "date":
      case "datetime":
       wrapperType = "DateTimeFieldWrapper";
       break;
      case "checklist":
      case "treelist":
      case "treelist with search":
      case "treelistex":
      case "multilist":
      case "multilist with search":
      case "tags":
       wrapperType = "ListFieldWrapper";
       break;
      case "droplink":
      case "droptree":
       wrapperType = "LinkFieldWrapper";
       break;
      case "general link":
      case "general link with search":
       wrapperType = "GeneralLinkFieldWrapper";
       break;
      case "text":
      case "single-line text":
      case "multi-line text":
       wrapperType = "TextFieldWrapper";
       break;
      case "rich text":
       wrapperType = "RichTextFieldWrapper";
       break;
      case "number":
       wrapperType = "NumberFieldWrapper";
       break;
      case "integer":
       wrapperType = "IntegerFieldWrapper";
       break;
      default:
       wrapperType = "TextFieldWrapper";
       break;
     }
    
     return wrapperType;
    }
    
    public string ClassName(string name)
    {
     return TitleCase(name);
    }
    
    public string InterfaceName(string name)
    {
     return "I" + TitleCase(name);
    }
    
    public string TitleCase(string name)
    {
     name = Regex.Replace(name, "([a-z](?=[A-Z])|[A-Z](?=[A-Z][a-z]))", "$1 ");
     name = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(name);
     name = Regex.Replace(name, @"[^a-zA-Z0-9]", String.Empty);
    
     var firstChar = 0;
    
     if (int.TryParse(name.Substring(0, 1), out firstChar))
     {
      var numberToWord = string.Empty;
    
      switch(firstChar)
      {
       case 0:
        numberToWord = "Zero";
        break;
       case 1:
        numberToWord = "One";
        break;
       case 2:
        numberToWord = "Two";
        break;
       case 3:
        numberToWord = "Three";
        break;
       case 4:
        numberToWord = "Four";
        break;
       case 5:
        numberToWord = "Five";
        break;
       case 6:
        numberToWord = "Six";
        break;
       case 7:
        numberToWord = "Seven";
        break;
       case 8:
        numberToWord = "Eight";
        break;
       case 9:
        numberToWord = "Nine";
        break;
      }
    
      name = numberToWord + name.Remove(0, 1);
     }
     
     return name;
    }
    
    public string RelativeNamespace(SitecoreTemplate template)
    {
     var relativeNamespace = string.Empty;
    
     if (template.Path.StartsWith(SitecoreSystemTemplatePath))
     {
      relativeNamespace = "ScSystem";
     }
     else if (template.Path.StartsWith(ClientTemplatePath))
     {
      var paths = template.Path.Replace(ClientTemplatePath, string.Empty).Split('/');
    
      //relativeNamespace = TitleCase(paths[0]);
      relativeNamespace = "UserDefined";
     }
     else
     {
      relativeNamespace = "Custom";
     }
     
     return relativeNamespace;
    }
    
    public string FullNamespace(SitecoreTemplate template)
    {
     return DefaultNamespace + ".Templates." + RelativeNamespace(template);
    }
    
    public IEnumerable<SitecoreTemplate> RecursiveBaseTemplateList(SitecoreTemplate template)
    {
     var list = new List<SitecoreTemplate>();
    
     if (template == null || template.BaseTemplates == null)
     {
      return list;
     }
     
     foreach (var baseTemplate in template.BaseTemplates)
     {
      if (!list.Any(t => t.ID == baseTemplate.ID))
      {
       list.Add(baseTemplate);
      }
    
      foreach (var innerBaseTemplate in RecursiveBaseTemplateList(baseTemplate))
      {
       if (!list.Any(t => t.ID == innerBaseTemplate.ID))
       {
        list.Add(innerBaseTemplate);
       }
      }
     }
     
     return list;
    }
    
    public bool HasRenderingOptionsBase(IEnumerable<SitecoreTemplate> templateItems)
    {
     var renderingParameterTemplateId = "8CA06D6A-B353-44E8-BC31-B528C7306971".ToLower();
     return templateItems.Any(t => t.ID.ToString() == renderingParameterTemplateId);
    }
    #>
  5. Remark: Those two files come from https://github.com/Fortis-Collection/fortis/tree/master/TDS/Code%20Generation%20Templates but I have improve 2-3 stuffs. For example, in the github file the rendering parameters method doesn't work because the standard rendering parameter Guid is not the correct one, I have moved the namespaces to have it only once, ...
  6. In Visual Studio, below the TDS project, right click on Code Generation Templates | Add | Existing Item... and select the two tt files.
  7. Now, return into the TDS project properties and set the Header Transorm file and the Base Transform File to the two tt files.

If you need some rendering parameters you will see that they will not be detected as rendering parameters. To fix it you just have to include the template: \sitecore\templates\System\Layout\Rendering Parameters\Standard Rendering Parameters in TDS. Of course you can set the property "Code Generation Template" to none if you don't want to generate the class for this one but at least it will fix your issue.

No comments:

Post a Comment