January 27, 2017

Impossible to add a language in Sitecore

I had an issue very recently: I cannot add a language in Sitcore anymore. When I tried to add it, the add language wizard was freezing without any error messages.

The resolution of this issue was very simple I just had the '-' character in the InvalidItemNameChars like this:

<setting name="InvalidItemNameChars">
 <patch:attribute name="value">\/:?&quot;&lt;&gt;|[].-</patch:attribute>
</setting>
So juste remove it from this patch during the add language process and then re-add it and the problem is fix!

November 25, 2016

Tips about the fallback in Sitecore

Tip number 1

Problem: The fallback is disabled by default in the item:save events.
Tip: You can use the following code:
using(new FallbackDisabler(FallbackStates.Default))
{  
   db.GetItem()
   ...
}

Tip number 2

Problem: The fallback is not active in the scheduled tasks.
Tip: In the FieldFallback_00.config, you need to add the site 'scheduler' in the 'sites' tag.

Tip number 3

Problem: When you use the Link database to retrieve an item, you have a ItemLink (the link between the 2 objects). If you do a .GetSourceItem() on this ItemLink, the fallback is not applied.
Tip: You need to retrieve this item with the standard method: db.GetItem(itemId).

Tip number 4

Problem: The ancestor fallback didn't work in the other languages.
Tip: To enable the ancestor fallback on a field, you need to select the template's field and check the EnableAncestorFallback checkbox. But, be careful because this checkbox is not shared. So two solutions here: check it in every languages or change this checkbox to shared.

November 10, 2016

Sitecore Utility - Sanitize Query

Recently i had an issue with the standard Sitecore queries on a bucket item: when an Sitecore item begin with a 0 the queries may return no results.
This is of course an issue with the items who are in a date based bucket. You may also have some unexpected results if you have some of the reserved keywords in the item name as "and", "or", "self", "children", "parent", "sibilings", ...
So, I have create a function to "sanitize" your queries before the Sitecore queries.

And an example of the SanitizeQuery: the SanitizeQuery function:
var myQuery = "/sitecore/content/TestSites/Test/09/28/15/56/*".SanitizeQuery();
var item = Sitecore.Context.ContentDatabase.SelectSingleItem(myQuery);

I would like to create a collection of utils function on github with all those kind of reusable functions. Like this you can easilly extract the functions you are interested in or the whole project.
You will find this repository here: https://github.com/VGBenjamin/Sitecore.Utility
If you have functions that you would like to share in this directory  you can of course propose it this could become a new collaborative repository.

July 7, 2016

Glass.Mapper - Retreive items without version

Today I will give you a very small trick to retrieve an item with Glass.Mapper even if the item didn't have any version in the current language. This is be helpful when you have an item with only some shared fields. Of course, the non-shared fields retrieved will be empty if you didn't have a version in the selected language.

The trick is very simple: you just need to surround it by a VersionCountDisabler

Here is two examples:
using(new VersionCountDisabler())
{
  var model =  sitecoreService.GetItem("/sitecore/content/home");
}
using(new VersionCountDisabler())
{
  var model =  myItem.Cast(item, isLazy, inferType);
}
You can find it in the official documentation but this isn't a very well-known feature: http://www.glass.lu/Mapper/Sc/Documentation/VersionCountDisabler

In bonus, here is the way to know if you are in a VersionCountDisabler mode in your code. This is usefull if you wrap the code with a custom method as I do.

bool isInVersionCountDisabler = (Switcher.CurrentValue == VersionCountState.Disabled);

June 30, 2016

Scan and Install all the packages in a folder

Sometimes ago I have published the PackageInstaller on github and on the marketplace.

This is a useful script to install a Sitecore package in command line. One of the scenarios, I like to cover with this script is the installation of the missing packages on the different machines. This mean on each developer’s machine but also on staging and production.

To do that I have developed the following script who scan a folder and install all the packages. Then it will write the installed packages into a text file to avoid the reinstallation on this machine the next time. Of course this file shouldn't be committed :-)

<#
    .SYNOPSIS
        Install all the sitecore modules who are not installed yet. And save the list of those modules to avoid to reinstall it next time.
 
    .PARAMETER  modulesPath
        The folder who contains the modules to install
 
    .PARAMETER  allreadyInstalledModulesPath
        The folder who contains the file .txt.user with the allready installed modules

    .PARAMETER  packageInstallerExe
        The path to the file Sidewalk.SC.PackageInstaller.Client.exe

    .PARAMETER  packageInstallerSln
        The path to the file \Sidewalk.SC.PackageInstaller.sln used to build the solution only if the Sidewalk.SC.PackageInstaller.Client.exe is not found

    .PARAMETER  msBuildPath
        The path to the file msbuild.exe used to build the solution only if the Sidewalk.SC.PackageInstaller.Client.exe is not found

    .PARAMETER  sitecoreUrl
        The sitecore url where the modules needs to be installed

    .PARAMETER  sitecoreDeployFolder
        The sitecore Website folder where the modules need to be installed
 
    .EXAMPLE
        InstallMissingModules.ps1 -sitecoreUrl "http://mysitecore.sandbox.local" -sitecoreDeployFolder "C:\inetpub\wwwroot\mysitecore.sandbox\Website" -Verbose
#>
[CmdletBinding()]
param(
    [Parameter(Mandatory=$false)][string]$modulesPath = "..\..\..\Modules\",
    [Parameter(Mandatory=$false)][string]$allreadyInstalledModulesPath = "..\..\..\Modules\allreadyInstalledModules.txt.user",
    [Parameter(Mandatory=$true)][string]$sitecoreUrl,
    [Parameter(Mandatory=$true)][string]$sitecoreDeployFolder,
    [switch]$whatIf
)

$fileSystemModulePath = "$currentDir\..\..\Powershell\Libraries\FileSystem.psm1"

function Get-ScriptDirectory {
    Split-Path -parent $PSCommandPath
}

$currentDir = Get-ScriptDirectory

try {
    $modulesPathFullPath = Resolve-Path $modulesPath
 Write-Verbose -Message "Module directory: $($modulesPathFullPath.Path)"
} catch {
    throw new FileNotFoundException("The modules path is not found. $($_.Exception.Message)")
}
$packageInstallerExe = "$currentDir\PackageInstaller\SC.PackageInstaller.Client.exe"

if(-not (Test-Path $packageInstallerExe)){
    Write-Verbose -Message "The package installer exe is not found."    

    #Download the zip file from github
    $modulePath = Resolve-Path $fileSystemModulePath

    Import-Module $modulePath.Path
    $zipPath = "$currentDir\PackageInstaller.zip"    
    Get-RemoteFile -url "https://github.com/VGBenjamin/PackageInstaller/raw/master/Sidewalk.PackageInstaller.zip" -targetFile $zipPath -Verbose:$PSBoundParameters['Verbose']

    Write-Verbose -Message "Extracting the zipfile"
    try
 {
  Expand-Zip -zipPath $zipPath -destination "$currentDir\PackageInstaller\" -Verbose:$PSBoundParameters['Verbose'] -createDestinationFolderIfNeeded
 } catch [Exception]
 {
  echo $_.Exception|format-list -force
 }    
}

$allreadyInstalledModules = New-Object 'System.Collections.Generic.HashSet[String]'
$allreadyInstalledModulesPathFillPath = "$currentDir$allreadyInstalledModulesPath"

if(Test-Path $allreadyInstalledModulesPathFillPath) {  
    Write-Verbose -Message "Loading the list of allready installed modules: $allreadyInstalledModulesPathFillPath"  
    Get-Content $allreadyInstalledModulesPathFillPath | ForEach-Object { $allreadyInstalledModules.Add($_ ) | Out-Null } 

    Write-Verbose -Message "Modules allready installed: $allreadyInstalledModules"
}

foreach($module in Get-ChildItem -Path $modulesPathFullPath.Path -Include @("*.zip","*.update") -Recurse) {
    
    if(-not ($allreadyInstalledModules.Contains($module.Name))) {    
        Write-Verbose -Message "Installing the module: $($module.FullName)"
        
        if($whatIf) {
            Write-Host "WhatIf : Installing the module with the parameters: $packageInstallerExe -sitecoreUrl $sitecoreUrl -sitecoreDeployFolder $sitecoreDeployFolder -packagePath $($module.FullName) -connector tds"
        } else {
            try {
                & "$packageInstallerExe" -sitecoreUrl $sitecoreUrl -sitecoreDeployFolder $sitecoreDeployFolder -packagePath "$($module.FullName)" -connector "tds" | Out-String           
                $allreadyInstalledModules.Add($module.Name) | Out-Null
            } catch [Exception]
         {
          echo $_.Exception|format-list -force
         }
        }

        
    }
}

Set-Content -Path  $allreadyInstalledModulesPathFillPath -Value ($allreadyInstalledModules | Out-String) -WhatIf:$whatIf
This script is also available here: https://raw.githubusercontent.com/VGBenjamin/PackageInstaller/master/InstallMissingModules-github.ps1

The dependency FileSystem.psm1 is available here: https://raw.githubusercontent.com/VGBenjamin/PackageInstaller/master/FileSystem.psm1

The

November 16, 2015

Customize the ExperienceEditor Part 3 - Accessing to the registry from the javascript

Here is my other post of the same series about the customization of the client without modifying the Sitecore JS:
If you need to save boolean into the registry like the collapsed state of an element to keep the content editor's preferences you can do:

Retreive the current state:

Sitecore.ExperienceEditor.PipelinesUtil.generateRequestProcessor("ExperienceEditor.ToggleRegistryKey.Get",
    function (response) {
       mystate = response.responseValue.value;
    },{ value: "/Current_User/Page Editor/MyModule/MyValue" }).execute();
Where
  • function(response) is the function executed when you will receive the response.
  • response.responseValue.value is a boolean with the returned value. The default value of this registry is true (stored as "on" in the registry)
  • MyRegistryKey.IsCollapsed is the a unique key in the registry for the user.

Toggle the value when the user collapse the control for example

Sitecore.ExperienceEditor.PipelinesUtil.generateRequestProcessor("ExperienceEditor.ToggleRegistryKey.Toggle",
    function (response) {
        top.sidebar.isCollapsed = response.responseValue.value;
    }, { value: "/Current_User/Page Editor/MyModule/MyValue" }).execute();
You can use the same method to call the different requests commands from the Sitecore config node sitecore.experienceeditor.speak.requests who are stored by default in Sitecore.ExperienceEditor.Speak.Requests.config

November 12, 2015

Install your packages in command line

Some times ago I had publish a series of post about the automatisation of the deployment process. In this build process I install my packages in command line. I have finally rewrite and release this tool. It is now available on the marketplace and github.

The project

This project allow you to install the Sitecore packages files in command line. It support both the .zip and .update files.
If you like this project you can visit and let a comment on my blog: http://sitecoreblog.blogspot.be/

Usage

Install

You need to download and extract the PackageInstaller zip file or build the source.
Arguments
-p, --packagePath=PACKAGE PATH The PACKAGE PATH is the path to the package. The package must be located in a folder reachable by the web server.

-u, --sitecoreUrl=SITECORE URL The SITECORE URL is the url to the root of the Sitecore server.

-f, --sitecoreDeployFolder=SITECORE DEPLOY FOLDER The SITECORE DEPLOY FOLDER is the UNC path to the Sitecore web root.

-c, --connector=INSTALLATON MODE The INSTALLATON MODE could be tds or sitecore.

--pb, --publish Publish some items.

--pbc, --publishChildrenItems Publish the children items also. Need to be use with the -publish option. If you don't specify this flag you need to specify the paramter - publishRootItem

--pbsdb, --publishSourceDb=VALUE The source database to publish from (master if ommited). Need to be use with the -publish option.

--pbtdb, --publishTargetDb=VALUE The target database to publish to (web if ommited). Need to be use with the -publish option.

--pbl, --publishLanguage=VALUE The language to publish (all if ommited). Need to be use with the -publish option.

--pbi, --publishRootItem=VALUE The root item to publish (all if ommited). Need to be use with the -publish option.

--pbm, --publishMode=VALUE The publish mode must be one of those values: Full, Incremental, SingleItem, Smart (Full if ommited). Need to be use with the -publish option.

--pbt, --publishTargets=VALUE The publish target separated by a coma if multiple targets. Need to be use with the - publish option.

-h, --help Show this message and exit.

--rc, --removeconnector Remove the conenctor after the installation. it will remodify the bin folder so you should consider to let the conenctor for a better performance. --ssl Accept the self registered ssl certificate

Usage Examples
Install a sitecore package
Sidewalk.SC.PackageInstaller.Client.exe -sitecoreUrl "http://sc72rev140228" -sitecoreDeployFolder "C:\inetpub\wwwroot\sc72rev140228\Website" -packagePath "C:\temp\TestPkg.zip" -connector "sitecore"

Install a TDS package
Sidewalk.SC.PackageInstaller.Client.exe -sitecoreUrl "http://sc72rev140228" -sitecoreDeployFolder "C:\inetpub\wwwroot\sc72rev140228\Website" -packagePath "\Examples\TestPackage.TDS.update" -connector "tds"

Sources

The sources are available here: https://github.com/VGBenjamin/PackageInstaller

Issues

If you have some issues please open a ticket on https://github.com/VGBenjamin/PackageInstaller I will be notified



November 9, 2015

Small Tip : Click on elements in ExperienceEditor


You probably already had some issues in ExperienceEditor with clickable elements, who are not clickable anymore because Sitecore override the click on this zone. In the following screenshot, we had the problem on a carrousel to click on the left and right arrows.

The tip is very simple: just use ctrl+click in place of just clicking the element.

November 5, 2015

Customize the ExperienceEditor Part 2 - Override the Sitecore functions in the JS

Here is my other posts of the same series about the customization of the Sitecore client without modifying the Sitecore JS:
Today I will show you how I have inherit and override the javascript function defined by Sitecore with as few side effects as possible.
You just need to store the prototype of the original Sitecore's function, redefine this prototype and call the original function.
Here is the skeleton if the method of sitecore is:
sitecoreObject.prototype.methodName = function(param1) {
 //Do stuffs
}

This method can be override like this:
var originalProcess = sitecoreObject.prototype.methodName;
sitecoreObject.prototype.methodName = function (param1) {
    //Do my stuffs
 
    //Call teh original method and apply the correct context to this method
 originalProcess.call(this, request, command, name);    
};

Here is a real life. I will explain the client pipelines in a next post but the following method is responsible of the processing of the different processors:
scSitecore.prototype.process = function (request, command, name) {  
  name = (name == null ? command.command : name);
  this.state.pipeline = request.pipeline;
  var r;
  switch (name) {
    //The different case of the different processors names
  }
};
And I need to add my custom processor in it so basically adding another case:
var originalProcess = currentScJsWindow.scSitecore.prototype.process;
currentScJsWindow.scSitecore.prototype.process = function (request, command, name) {
    var localName = (name == null ? command.command : name);
    this.state.pipeline = request.pipeline;
    var r;

    if (localName === "ShowDialogInSidebar") {
        //Do my studd
    } else {
        //Call the normal handler
        originalProcess.call(this, request, command, name);
    }
};

November 4, 2015

Customize the ExperienceEditor Part 1 - Include your custom JS

Recently I had to customize the Sitecore ExperienceEditor. It was a very tricky experience mainly because nothing is documented officially and almost nothing on the blogs.
So I will write a series of post about this to help you in this task.
I want to customize the ExperienceEditor but I don't want to touch to the base JS files of Sitecore to have as few side effect as possible and still be able to upgrade to the newer versions.

Here is the different posts of this serie:

To include my custom JS file the first thing I have try is add my JS into the Sitecore JS bundle \sitecore\shell\Applications\Page Modes\Ouput it seem that it is possible by overriding the method Sitecore.Web.UI.WebControls.WebEditRibbon.Render() but I didn't want to decompile the code and change the existing functionality.
My workaround for it is adding a checkbox into the view section of the ribbon. When you add those kind of buttons, you are able to attach a JS to this checkbox.
To do that:
  • Open Sitecore Rocks
  • In the core DB, go to /sitecore/content/Applications/WebEdit/Ribbons/WebEdit/View/Show
  • Duplicate one of the child item of this node and change the fields you need to control the text
  • Right click on the item, task design layout (shortcut: ctrl + u) 
  • Click on Add Rendering 
  • Search and select SmallCheckButton
  • Edit the properties of you button and set the field. The one who is really interesting to include the JS is the PageCodeScriptFileName
Now you are able to do what you want in the JS. But don't forget that your JS is into the ribbon's iFrame. So if you need to access to the page elements or the Sitecore's javascript objects you shoud prefix by top. Example: top.JQuery("#container")

In the next post I will explain how to override the Sitecore functions in the JS.

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.

May 5, 2015

Activate the debugger in the third party dlls

All the advanced dotnet developers has already been frustrated because we are not able to debug step by step in the third party dlls. Of course you have tools like ILSpy or reflector to see the decompiled code but sometime it isn't enough to understand what really happens.
With a few manipulation you will be able to debug it and even (most of the time) have the watch values

  • Download and install dotpeek
  • In visual studio, click on tools, options 
    • In "debugging", unckeck the box "Enable just my code" 
    • In "Debugging" - "Symbols", note the value of the field "Cache symbols in the directory" for the next step 
  • In dotPeek:
    • Drag'n drop the assembly into the "Assembly Explorer"
    • Right click on the assembly and click on "generate pdb" 
    • In the field "Destination folder", past the value of the "Cache symbols in the directory" from the previous step and click on generate.
If you stop now you should be able to attach to your process, step into the decompiled dll. But you will be not able to see the values of the variables.
  • Two options to start visual studio in the correct mode:
    • To start it once type the following lines into a cmd prompt:
      set COMPLUS_ZapDisable=1
      cd /d C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE
      start devenv.exe
      exit
    • To set it permanently set this in the registry:
      HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment
      Add Key: COMPLUS_ZAPDISABLE
      Type: REG_SZ (String)
      Value: 1
  • You also need to disable the JIT optimization for the dll. To do that, create a .ini file with the same name as the dll on the same place (in your bin folder). Example for Sitecore.Kernel.dll, the name must be: Sitecore.Kernel.ini. Set the content of the file to:
    [.NET Framework Debugging Control] 
    GenerateTrackingInfo=1 
    AllowOptimize=0
Now you should be able to add the watch on the variables.

Enjoy!

February 26, 2015

Reading the the Windows Azure Diagnostics Table

This article is not specific to Sitecore but with Sitecore Azure you have the possibility to store your logs into what they call WAD Tables. To read it you have multiple choices:
  • The Azure Storage Explorer but when your logs are too big you will just have a OutOfMemeryException
  • The SCLA (Sitecore Log Analyser) you need to uncomment something in the web.config to activate the Azure possibility. But again when you will have big logs it will be not usable anymore.
  • Visual Studio. If you go to the server explorer, then on Azure, then Storage, Tables, and click on the WADLogsTable. As a limitation you will not be able to retrieve more than 10000 records.
  • This LinqPad with the Azure Table Storage Driver. I will explain this possibility more in details below.
When those table grow up it become just impossible to execute some custom requests on the custom fields. In fact, they are just two fields who stay queryable: PartitionKey and RowKey. But of course for the sitecore logs what you need to retrieve is all the events during a certain time range. The interesting thing about the PartitionKey is the fact that you can match a datetime to this field. You just have to do the following code to retrieve the value of those values:
string.Format("0{0}", (new DateTime(2015, 02, 26)).Ticks)
If you want more details the following article explain it in details: http://gauravmantri.com/2012/02/17/effective-way-of-fetching-diagnostics-data-from-windows-azure-diagnostics-table-hint-use-partitionkey/ By using this trick you are able to build and execute a query on the PartitionKey by using Visual Studio but, as I said, you are limited to 10000 records max.
If you need to retrieve all the records of a single day one of the solution is LinqPad. To be able to query those table you need to:
  • Download Azure Table Storage Driver
  • Install this driver by:
    • Open LinqPad
    • Click on "Add connection"
    • Click on "View more drivers..."
    • Click on browse and select your lpx file
    • Click on close
If you switch LinqPad in "c# Statement(s)" mode in the language dropdown. you are able to write query like this one:
(from l in WADLogsTable
   where l.PartitionKey.CompareTo(new DateTime(2015, 02, 13, 0,0,0).Ticks.ToString("d19")) > 0
   && l.PartitionKey.CompareTo(new DateTime(2015, 02, 14, 0, 0, 0).Ticks.ToString("d19")) < 0 
   select new
   {
    DateTime = new DateTime(l.EventTickCount.Value),
    l.Message,
    l.Level
   }).ToList().Dump();
But as you see LinqPad is also limited to 1000 records because of the azure storage api. the workaround for it is to execute the request hour by hour in a loop like this:
for(int i = 0; i < 24; i++) {
 (from l in WADLogsTable
   where l.PartitionKey.CompareTo(new DateTime(2015, 02, 13, i,0,0).Ticks.ToString("d19")) > 0
   && l.PartitionKey.CompareTo(new DateTime(2015, 02, 13, i+1, 0, 0).Ticks.ToString("d19")) < 0 
   select new
   {
    DateTime = new DateTime(l.EventTickCount.Value),
    l.Message,
    l.Level
   }).ToList().Dump(); 
}

December 3, 2014

Add a custom section in the content editor

Here the procedure to add a custom section after the Quick Info section in the content editor. Like this:

  • Create a new class like this:
    using Sitecore.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace SitecoreTestCustomizeContentEditor
    {
        public class TestSection
        {
            public void Process(RenderContentEditorArgs args)
            {
                args.EditorFormatter.RenderSectionBegin(args.Parent, "CustomInfo", "CustomInfo", "CustomInfo", "Applications/16x16/information.png", true, true);
                args.EditorFormatter.AddLiteralControl(args.Parent, "<div>test value</div>");
                args.EditorFormatter.RenderSectionEnd(args.Parent, true, true);
            }
        }
    }
  • Deploy this class in your bin folder
  • Add a pipeline into web.config in the renderContentEditor section like this
    <renderContentEditor>
     <processor type="SitecoreTestCustomizeContentEditor.TestSection, SitecoreTestCustomizeContentEditor" />
     <processor type="Sitecore.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor.RenderSkinedContentEditor, Sitecore.Client" />
     <processor type="Sitecore.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor.RenderStandardContentEditor, Sitecore.Client" />  
    </renderContentEditor>

October 22, 2014

New on the marketplace : Image Optimizer V2

I have just release a new module on the marketplace today: Image Optimizer V2


History

The first version has been developed by MIKAEL HÖGBERG is available here. Here is his blog post about it: http://mikael.com/2013/08/image-optimizer-module/ A very big thank you for this module!


New features

This version 2 is now based on jpegtran and pngquant to compress the pictures but you can use your favorite tool if you prefer. The biggest addition of this module is a wizard similar to the publishing wizard which allow you to compress a complete folder in place of compressing picture per picture.


Installation

  • Install the packages with the /sitecore/admin/UpdateInstallationWizard.aspx
  • Download pngquant here: http://pngquant.org/ under the section "Command Line" / "Binary for windows" and save this file in the root of your data folder.
  • Download jpegtran here: http://jpegclub.org/jpegtran.exe and save this file in the root of your data folder.

Advanced configurations

The configuration file is available in _App_Config\Include\ImageOptimizer.config. The following setting are available

  • imageOptimizer.pngCommand: The path to the png commande line tool for the png
  • imageOptimizer.pngOptions: The command line arguments
  • imageOptimizer.jpgCommand: The path to the png commande line tool for the jpg
  • imageOptimizer.jpgOptions: The command line arguments
  • imageOptimizer.PoolingInterval: The pool interval for the wizard you shouldn't change it

September 17, 2014

Sitecore symposium 2014 - Barcelona

For those who didn't have the chance to go the the Sitecore symposium at Barcelona this year, I will send some info, previews, ...

Day 1 - Session 1 : Keynote

In the keynote we have mainly seen the new features on the future Sitecore 8. Some screenshots of the Sitecore 8 will be uploaded when I will be back for the symposium. The good news are:
  • Sitecore 7.5 will be release in about 2 week
  • Sitecore 8 should be release before the end of the year
I can already have multiple conclusion about this first sessions:
  • The speak UI is more living than ever. Most ot the applications are or will be rewrites using this new "language"
  • After the login you are redirected to the Speak Dashboard
  • It is amazing to see how far they go with this new personalization and content testing


I will be honest, I was not very enthusiast when I saw the program of this year. But after the keynote. I think that it will be really exciting finally.

Day 1 - Session 2: Why did we do that?

This session was mainly an history of the Sitecore developments. They also have announced a new partnership with Coveo. Coveo is a new search available in Sitecore and the great thing here is that they are a free version for Sitecore 7. The non free version include some additional stuffs like the possibility for the content editor to change the filters, the boosts, ... The possibility to integrate some external datasource and the advanced configuration. After a small visit to they stand I really thing that I will investigate this product.

Day 1 - Session 3: Visit the Emerald City: Understanding the Sitecore architecture

The main things here we have learn are:
  • Sitecore 7.5+ : The new architecture for the analytics data collection. In summary, you collect all the data into the Mongo DB. Then those data are process by some pipeline to inject it into the reporting database. When a data need to be retreive the service layer decide if he need to take it from the mongo db when it is a personal information like the identity card of a visitor or for the reporting db if it is some into about the a group of user.
  • Sitecore 7.2+: The new publishing process is now multi-thread
The rest was mainly some basic architecture stuffs like the pipelines, ...
,

Day 1 - Session 4 : Compiling, personalized Sitecore StoreFronts with any commerce platform

First coding session!
A session who explain how to customize the pipelines and the SPEAK applications to add a new section into the "events" tab of the user identity card. It was an interesting session to have a quick overview of the SPEAK framework and a look at the eCommerce API.
The thing I will remember about this session is the fact that the dashboard that Sitecore provide for the User Experience are not static at all and really me to be modified by the developers. If we need to track some custom events and have a view on it.

Day 1 - Session 5 : Our princess is in another castle_ Tracking activity on non-Sitecore sites

Definitivelly the best session of the day. With this system we are able to track the statistics for a non Sitecore site. Just create a new site into the interface and after that, you will see your site with the same kind of ribbon than a normal sitecore site in webedit.
The developer is suppose to: add a line of javascript into the non sitecore site. After that he will define some place, and element who will trigger the events to track by using this ribbon. After that, the marketer, is able to associate those elements to a analytics goal or campaign
With this tool you are also able to add a sublayout .... into you old website! This is done by the same javascript and wy using the same ribbon. Just select the place to replace with your sitecore content and insert the sublayout. Select a datasource if needed of course and it is done! You are even able to do personalisation on it.

Day 1 - Session 6 :

The Skynet project : nothing to do with Terminator. This project is made to learn from the previous experiences of content testing and after that he is able to propose some segmentation of users. For example if you have 2 variants of content on a first sublayout and 3 variants of content in a second sublayout, the system will test it with the user and after that he will proposed something like: for the users form Belgium between 6AM and 4PM you should do variant 1 and it will impact 6,5% of the visits, ...


July 15, 2014

Fixing “The breakpoint will not currently be hit. No symbols have been loaded for this document.”

If you use Visual Studio you have already seen this message at least one time: The breakpoint will not currently be hit. No symbols have been loaded for this document.” and your breakpont is white and never hit.

Here is the things you should try if you have this message:


  • Clean the solution:
    • Right click on the solution
    • Click on "Clean Solution"
  • Check the environment constants
    • Right click on the project
    • Click on "Properties"
    • Click on the "Build" tab
    • Check the box "Define DEBUG constant"
    • Check the box "Define TRACE constant"
    • Rebuild all

    • Select the "Debug info" to full
      • Right click on the project
      • Click on "Properties"
      • Click on the "Build" tab
      • Click on "Advanced" 
      • Select "full" in the dropdown "Debug info"
      • Rebuild all

    June 23, 2014

    Setup a continuous integration for Sitecore with TDS - TFS 2013 and Octopus - Step 4: Align the version number

    This is a last post of a series of post about the setup of a continuous integration for Sitecore. If you have miss the 4 previous post, here is the links:
    1. Setup a continuous integration for Sitecore with TDS - TFS 2013 and Octopus - Architecture
    2. Setup a continuous integration for Sitecore with TDS - TFS 2013 and Octopus - Step 1: Customize the TFS workflow
    3. Setup a continuous integration for Sitecore with TDS - TFS 2013 and Octopus - Step 2: Configure TFS
    4. Setup a continuous integration for Sitecore with TDS - TFS 2013 and Octopus - Step 3: Configure octopus
    If you stop here you workflow should be ok but, I had another requirement: the version number. I would like to have the same reference number into every steps of my process:
    • The build number who appear in TFS
    • The assemblies files
    • The nuget packages
    • The octopus versions
    To do that:
    1. Change the "Build number format" into the section "5. Advanced" of the TFS build definition by: $(BuildDefinitionName)_$(Date:1MMdd)$(Rev:.r). Be carefull with this number format. You can see that I have use 1MMdd and not YYYYMMdd as it is by default. The reason of that is that the Assembly number must between 0 and 65535. So you have a solution for the next 6 year by changing the 1 by another number :)
    2. Add a file .ps1 anywhere in the sources with the following content and do a check-in in TFS
    3. In the section 2.5 "Pre-build script path", refer to the powershell file you have created.
    4. In the section 2.5 "Pre-build script arguments", put the following value "-assemblyVersion 1.1.J.B -fileAssemblyVersion 1.1.J.B -nugetVersion 1.1.J.B". This mean that the powersell will replace the .J.B of the different files (assembly, assemblyVersion, nuget) by the same numbers as the build number of TFS
    Here is the content of the powershell script to rename the versions:
    param
    (
     [string]$assemblyVersion,
     [string]$fileAssemblyVersion,
     [string]$nugetVersion
    )
    
    #Update the AssemblyInfo.cs and the .nuspec file to replace the X.X.J.B version number by the correct one depending of the number from TFS
    function Update-SourceVersion
    {
      param
      (
        [string]$SrcPath,
        [string]$assemblyVersion, 
        [string]$fileAssemblyVersion,
     [string]$nugetVersion
      ) 
      
        $buildNumber = $env:TF_BUILD_BUILDNUMBER
     Write-Host "env:TF_BUILD_BUILDNUMBER: $buildNumber"
     
        if ($buildNumber -eq $null)
        {
            $buildIncrementalNumber = 0
        }
        else
        {
            $splitted = $buildNumber.Split('.')
            $buildIncrementalNumber = $splitted[$splitted.Length - 1]
        }
        
        if ($fileAssemblyVersion -eq "")
        {
            $fileAssemblyVersion = $assemblyVersion
        }
         
        Write-Host "Executing Update-SourceVersion in path $SrcPath, Version is $assemblyVersion and File Version is $fileAssemblyVersion"
        
        
        $AllVersionFiles = Get-ChildItem $SrcPath AssemblyInfo.cs -recurse
        
         
        $jdate = Get-Date -format 1MMdd
     
     Write-Host "Infos: jdate: $jdate, buildIncrementalNumber: $buildIncrementalNumber"
     
        $assemblyVersion = $assemblyVersion.Replace("J", $jdate).Replace("B", $buildIncrementalNumber)
        $fileAssemblyVersion = $fileAssemblyVersion.Replace("J", $jdate).Replace("B", $buildIncrementalNumber)
         
        Write-Host "Transformed Assembly Version is $assemblyVersion and Transformed File Version is $fileAssemblyVersion"
            
        foreach ($file in $AllVersionFiles) 
        { 
            Write-Host "Modifying file " + $file.FullName
            #save the file for restore
            $backFile = $file.FullName + "._ORI"
            $tempFile = $file.FullName + ".tmp"
            Copy-Item $file.FullName $backFile
            #now load all content of the original file and rewrite modified to the same file
            Get-Content $file.FullName |
            %{$_ -replace 'AssemblyVersion\("[0-9]+(\.([0-9]+|\*)){1,3}"\)', "AssemblyVersion(""$assemblyVersion"")" } |
            %{$_ -replace 'AssemblyFileVersion\("[0-9]+(\.([0-9]+|\*)){1,3}"\)', "AssemblyFileVersion(""$fileAssemblyVersion"")" }  > $tempFile
            Move-Item $tempFile $file.FullName -force
        }
     
     $nugetVersion = $nugetVersion.Replace("J", $jdate).Replace("B", $buildIncrementalNumber)
        Write-Host "Transformed Nuspec Version is $nugetVersion"
      
        $AllNugetFiles = Get-ChildItem $SrcPath *.nuspec -recurse
     $replaceExp = '<file src="$1" target="$2.' + $nugetVersion + '.update" />'
     
     foreach ($file in $AllNugetFiles) 
        { 
            Write-Host "Modifying file " + $file.FullName
            #save the file for restore
            $backFile = $file.FullName + "._ORI"
            $tempFile = $file.FullName + ".tmp"
            Copy-Item $file.FullName $backFile
            #now load all content of the original file and rewrite modified to the same file
            Get-Content $file.FullName |
            %{$_ -replace '<version>[0-9]+(\.([0-9]+|\*)){1,3}</version>', "<version>$nugetVersion</version>" }  |
      %{$_ -replace '<file src="(.*NationalLottery\.DeHub\..*)" target="(.*)\.[0-9]+(\.([0-9]+|\*|J|B)){1,3}.update" />', $replaceExp } > $tempFile
            Move-Item $tempFile $file.FullName -force
        } 
    }
    
    Write-Host "Running Pre Build Scripts"
     
    $scriptRoot = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
    
    $scriptRootBuildFunctions = "$scriptRoot\TFSUtils.psm1"
    Write-Host "Script root: $scriptRootBuildFunctions"
    
    if ($assemblyVersion -eq "")
    {
     $assemblyVersion = "1.1.0.0"
     $fileAssemblyVersion = "1.1.J.B"
    }
    
    $srcPath = "$scriptRoot/../"
    
    Update-SourceVersion $srcPath $assemblyVersion $fileAssemblyVersion $nugetVersion

    If you use the script to create and deploy the Octopus packages as describe in the previous post, don't forget to complete the "Post-build script arguments" to "-releaseVersion 1.1.J.B" to use the same versioning system and having this number as release number in Octopus.

    Ok that is it now. I hope that this tutorial will help you in your deployment process.
    You also have a video presented to the Sitecore Virtual User Group who could be useful for it here: http://sitecoreug.org/events/January

    Here is the index of all the post who compose this tutorial:
    1. Setup a continuous integration for Sitecore with TDS - TFS 2013 and Octopus - Architecture
    2. Setup a continuous integration for Sitecore with TDS - TFS 2013 and Octopus - Step 1: Customize the TFS workflow
    3. Setup a continuous integration for Sitecore with TDS - TFS 2013 and Octopus - Step 2: Configure TFS
    4. Setup a continuous integration for Sitecore with TDS - TFS 2013 and Octopus - Step 3: Configure octopus
    5. Setup a continuous integration for Sitecore with TDS - TFS 2013 and Octopus - Step 4: Align the version number

    Setup a continuous integration for Sitecore with TDS - TFS 2013 and Octopus - Step 3: Configure octopus

    This is the 4th post of a series of post about the setup of a continuous integration for Sitecore.  If you have miss the 3 first post here are the links:
    1. Setup a continuous integration for Sitecore with TDS - TFS 2013 and Octopus - Architecture
    2. Setup a continuous integration for Sitecore with TDS - TFS 2013 and Octopus - Step 1: Customize the TFS workflow
    3. Setup a continuous integration for Sitecore with TDS - TFS 2013 and Octopus - Step 2: Configure TFS
    Now our TFS is configured correctly, the package are created and published to the nuget server. We can focus on octopus now.

    I will not explain how to setup you Octopus environments, machines, ... but only the deployment process I use. 

    Here is the steps:
    1. Start Deployment: A notification email to the different member of the project with the details of the packages who will be deployed.
    2. Clean Environment: A cleanup of the files before the deployment to be sure that if we have remove a file it will not be on the project anymore. This is a powershell script with the following content: 
      #RootFolder : the root folder of the project (we should be able to get it from the environment)  
      $rootFolder = "E:\Websites\XXX\Website"   
      
      function Remove-Item-If-Exist
      {
       param
       (
        [string]$pathToDelete,
        [switch]$force,
        [switch]$recurse
       )
       
       If (Test-Path $pathToDelete){
        Remove-Item $pathToDelete -Force:$force -Recurse:$recurse
       }
      } 
       
      #Cleanup the old files  
      Remove-Item-If-Exist "$rootFolder\Design" -Force -Recurse  
      Remove-Item-If-Exist "$rootFolder\layouts\Layouts" -Force -Recurse  
      Remove-Item-If-Exist "$rootFolder\layouts\Sublayouts" -Force -Recurse  
      Remove-Item-If-Exist "$rootFolder\layouts\UserControls" -Force -Recurse  
      Remove-Item-If-Exist "$rootFolder\bin\PROJECTNAMESPACE.*.dll" -Force   
      Remove-Item-If-Exist "$rootFolder\App_Config\Include\PROJECTNAMESPACE.*.config" -Force
    3. Deploy Files: Deploy the NuGet package. You may have multiple time this step if you have multiple nuget package with the website files to deploy.
    4. Deploy TDS: Deploy the NuGet package with the TDS files in a temporary folder. 
    5. Install TDS Packages: This is a powershell to install and publish the TDS packages. I have create a custom tool for it who scan the files in a specific folder and install the packages. If you need to do this kind of program you should take a look at the code of TDS. Basically it install a webservice into your deploy folder, use this webservice to install your packages and then remove this webservice.
      A great alternative could be this tool: https://github.com/adoprog/Sitecore-Deployment-Helpers
    To automate the creation of the release into octopus when the build is triggered into TFS we can add a post release script into TFS to create and deploy this release.

    To do that:

    1. Add a new file with the extention .ps1 somewhere in your sources and chech-in this file in TFS
    2. Edit your TFS build definition 
    3. In the tab "Process", section 2.5
    4. In the section "Post-build script path", select your .ps1 file
    5. The parameter "Post-build script arguments" I have "-releaseVersion 1.1.J.B" please refer to the post "Step 4: Align the versions numbers" for this.
    6. The content of the powershell file is the following:
      param
      (
       [string]$releaseVersion
      )
       
      #Constants
      $octopusApiUrl = "http://10.0.2.50:8088/api"
      $octopusApiKey = "API-RX6UIIWTB2ZBUWU0TRIYXXXXXXX"
      
      $projectName = "XXX"
      $scriptRoot = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
       
      #Functions
      
      # Get the formated TFS version number. Replace the tokens J and B by the current build number
      function Format-TFS-Version
      {
       param
       (
        [string]$versionToFormat
       )
       
       $buildNumber = $env:TF_BUILD_BUILDNUMBER
       Write-Host "env:TF_BUILD_BUILDNUMBER: $buildNumber"
       
          if ($buildNumber -eq $null)
          {
              $buildIncrementalNumber = 0
          }
          else
          {
              $splitted = $buildNumber.Split('.')
              $buildIncrementalNumber = $splitted[$splitted.Length - 1]
          }
          
       $jdate = Get-Date -format 1MMdd
       
       return $versionToFormat.Replace("J", $jdate).Replace("B", $buildIncrementalNumber)
      }
      
      #Create a release in Octopus with the specifiic version of the nuget packages
      function Octopus-Create-Release
      {
       param
       (
        [string]$releaseVersion
       )
       
       $releaseVersion = Format-TFS-Version $releaseVersion
       
       $corePackage = "--package=XXX.Core:$releaseVersion"
       $corporatePackage = "--package=XXX.Corporate:$releaseVersion"
       $scriptPackage = "--package=XXX.Tools.SqlScriptsUpdate:$releaseVersion"
       
       Write-Host "Create the octopus release by calling $scriptRoot\Octopus\octo.exe create-release --project=$projectName --server=$octopusApiUrl --apiKey=$octopusApiKey --version $releaseVersion $corePackage $corporatePackage $scriptPackage"
       
       &$scriptRoot\Octopus\octo.exe create-release --project=$projectName --server=$octopusApiUrl --apiKey=$octopusApiKey --version $releaseVersion $corePackage $corporatePackage $scriptPackage  
      }
      
      #Deploy a release in Octopus
      function Octopus-Deploy-Release
      {
       param
       (
        [string]$releaseVersion,
        [string]$deployToEnvironment
       )
       
       Write-Host "Deploy the octopus release by calling $scriptRoot\Octopus\octo.exe  deploy-release --project=$projectName --version=$releaseVersion --deployto=$deployToEnvironment --server=$octopusApiUrl --apiKey=$octopusApiKey"
      
       &$scriptRoot\Octopus\octo.exe deploy-release --project=$projectName --version=$releaseVersion --deployto=$deployToEnvironment --server=$octopusApiUrl --apiKey=$octopusApiKey
      }
      
      
      
      #Main program
      $releaseVersion = Format-TFS-Version $releaseVersion
      Write-Host "Release version: $releaseVersion"
      
      Octopus-Create-Release $releaseVersion
      
      Octopus-Deploy-Release $releaseVersion Integration
      


    Ok now your process should be completed. The last post is optional but allow you to have a single ID during the whole build process.

    Next steps:
    1. Setup a continuous integration for Sitecore with TDS - TFS 2013 and Octopus - Architecture
    2. Setup a continuous integration for Sitecore with TDS - TFS 2013 and Octopus - Step 1: Customize the TFS workflow
    3. Setup a continuous integration for Sitecore with TDS - TFS 2013 and Octopus - Step 2: Configure TFS
    4. Setup a continuous integration for Sitecore with TDS - TFS 2013 and Octopus - Step 3: Configure octopus
    5. Setup a continuous integration for Sitecore with TDS - TFS 2013 and Octopus - Step 4: Align the version number