November 21, 2018

Sitecore Contact Utilities also available for the List Management

A very common requirement for our customers is the possibility to create some rules based on the values stores in the contact’s facets.

To do that, Adam Conn have create two modules who work in combinations: the Sitecore Adaptive Rules and the Sitecore Contact Utilities. Watch out the two following videos to know how those two modules works: Sitecore Adaptive Rules, Sitecore Contact Utilities.

What I have done on my own github is:
  1. Correcting some bugs. For example, if you have twice the same values in two different facets it can mix both values in the facet. 
  2. Improving the performances: The Adam Conn implementation was based on the IDTables for caching but this implementation was very slow when you have like 30+ facets. So I have refactor the code to be able to plug your own provider and I propose a “In memory provider” by default. Trust me this makes a lot of differences in term of performances! 
  3. List management: the Adam Conn rules are only available for renderings personalization. But I also needed the same system to segment the contact lists. So I have adapted the code to be able to make this segmentation. This was really a huge work because when you personalize a component you just receive a single contact and return true a false as result of the rule. But, when you want to play with segmentation rule, you need to return a lambda who will be executed on the indexing provider (Lucene, Azure Search or Solr).
I have worked for months on this module so I will be glad if it can help your customers too and help the community!

You can find the two modules here:

I want to thank again Adam Conn for you amazing job on it too. I have done a pull request on the original repository too so if you read it do not hesitate to merge it with yours if you want to!

Enjoy



November 9, 2018

The evil code debugging tool is here

Today I have released a neew tool to be able to debug your live website by executing all the code you want easilly.
You gonna love it but you will not never tell anybody that your are using it  :-D

Who never had an incomprehensible error in a sitecore website in production and want o be able to execute a line of code IN THE CONTEXT OF YOUR SITE.



This tool is available on the sitecore marketplace: https://marketplace.sitecore.net/en/Modules/U/UniversalDebugger.aspx and on github: https://github.com/VGBenjamin/UniversalDebugger

June 18, 2018

Sitecore 8.2 : System.FormatException: Unrecognized Guid format

In Sitecore 8.2.6 and later, you could have a message like: System.FormatException: Unrecognized Guid format. Actual value: ---> System.FormatException: Unrecognized Guid format when you try to use the link database.
For example, if you rebuild the link database. This message is due to some general links not correctly formatted. In the previous versions of Sitecore it could happend that a general link contains an emtpy id attribute or no id at all like this:
<link text="Description" linktype="internal" class="" title="myTitle" target="" querystring="" id="" />
But this is not valid anymore in Sitecore 8.2.6+. To detect those link the easiest solution is running the following Powershell script:
Add-Type -AssemblyName Sitecore.Kernel


function Resolve-Error ($ErrorRecord=$Error[0])
{
   $ErrorRecord | Format-List * -Force
   $ErrorRecord.InvocationInfo |Format-List *
   $Exception = $ErrorRecord.Exception
   for ($i = 0; $Exception; $i++, ($Exception = $Exception.InnerException))
   {   "$i" * 80
       $Exception |Format-List * -Force
   }
}

function Test-Fields($item) {
    $anyState = [Sitecore.Links.ItemLinkState]::Any

    $item.Fields.ReadAll()
    for ($j = 0; $j -lt $item.Fields.Count; $j++)
    {
        $field = $item.Fields[$j];
        if ($field -ne $null)
        {
            $field2 = [Sitecore.Data.Fields.FieldTypeManager]::GetField($field)
            if ($field2 -ne $null)
            {
                Try {
                    $linksValidationResult = New-Object -TypeName "Sitecore.Links.LinksValidationResult" -ArgumentList $field, $anyState
                    $field2.ValidateLinks($linksValidationResult);
                } 
                Catch { 
                   Write-Host "Error on item: $($item.ID). Field: $($field.Name). Path: $($item.Paths.FullPath). Language: $($item.Language.Name). Value: $($field.Value)"
                }
            }
        }
    }
}

function ExecOnPath($path) {
    Write-Host "Execute on: $path"

    Get-ChildItem -Path "master:$path" -Recurse -Language * | ForEach-Object { Test-Fields  $_ }
}

ExecOnPath -Path "/sitecore/content/Home"

Write-Host "Done!"
If you also want to fix the empty links at the same time you can add those extra lines into the catch clause:
if($field.Value -eq '<link linktype="internal" />') {                
    $item.Editing.BeginEdit()
    $item[$field.Name] = ""
    $item.Editing.EndEdit()

    Write-Host "Fixed automatically!"
} else {
    Write-Host "Cannot fix it automatically..."
}

You can find the original thread who helped me to develop this script here: https://sitecore.stackexchange.com/questions/11282/rebuilding-link-database-system-formatexception-unrecognized-guid-format

March 9, 2017

Log4net - Add the stacktrace in the logs

The log4net version included in Sitecore doesn't contains the stacktrace. That mean that you cannot use %stacktrace or %stacktracedetail in the conversionsPattern.

As a workaround, you can override the appender.
First create the following class:
public class SitecoreLogFileWithStackAppender : SitecoreLogFileAppender
{
    protected override void Append(LoggingEvent loggingEvent)
    {
        var msg = loggingEvent.MessageObject as string;
        if (msg != null)
        {
            var prop = loggingEvent.GetType().GetField("m_data", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
            var mData = (LoggingEventData)prop.GetValue(loggingEvent);
            mData.Message = $"{msg}{Environment.NewLine}{Environment.StackTrace}";
            prop.SetValue(loggingEvent, mData);
        }

        base.Append(loggingEvent);
    }
}
Now you are able to configure the log4net appender like this:
<appender name="StackFileAppender" type="MyNamespace.SitecoreLogFileWithStackAppender, MyDll">
  <file value="$(dataFolder)/logs/log.Stacktraces.{date}.txt" />
  <appendToFile value="true" />
  <layout type="log4net.Layout.PatternLayout">
    <conversionPattern value="%5t %d{yyyy-MM-dd HH:mm:ss.fff} %-5p %c - %m%n%n" />
  </layout>
</appender>
For my project I have choose to log all the errors, and all the messages who begin by [SD] in a separated fil with the stacktrace. The config is the following:
<appender name="StackFileAppender" type="MyNamespace.SitecoreLogFileWithStackAppender, MyDll">
  <file value="$(dataFolder)/logs/log.Stacktraces.{date}.txt" />
  <filter type="log4net.Filter.StringMatchFilter">
    <stringToMatch value="[SD]"></stringToMatch>
  </filter>
  <filter type="log4net.Filter.DenyAllFilter" />
  <appendToFile value="true" />
  <layout type="log4net.Layout.PatternLayout">
    <conversionPattern value="%5t %d{yyyy-MM-dd HH:mm:ss.fff} %-5p %c - %m%n%n" />
  </layout>
</appender>

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