Sitecore MVC Autofac Dependancy Resolution

Recently we decided to integrate Autofac in our Sitecore MVC Solution. After a bit of research I found some pretty nice and useful articles on the topic of using Dependency Injection with Sitecore MVC, but there wasn’t anything focused on Autofac, so I decided to put a blog post around it. The approach is based on a blog article about Ninject, by Mark Cassidy Thomas Stern – Sitecore MVC new Ninject Controller Factory and the Sitecore Mvc Contrib amazing sample implementation by Kevin Obee.

So to build our Autofac Injection we basically need 4 things:

  1. An Autofac Container Factory, which is going to build the Autofac Container
  2. An Autofac Controller Factory, which is going to replace the Sitecore Controller Factory
  3. Replacement Pipeline that is going to initialize the Autofac Container and replace the default Sitecore controller Factory with the custom Autofac Controller Factory
  4. Configuring the pipeline to replace the default Sitecore Pipeline.

Building an Autofac Container Factory

The first step is to create an Autofac Container Factory. It needs to build the container, register the controllers in our scope and to leave us a place to register all other Modules,Types, etc. we need to register. For ease of use I am also going to use the Autofac MVC 5 Integration Package which provides a method called RegisterControllers which is going to be used in the Controller Registration. The sample code can be found bellow:


using System;
using Autofac;
using Autofac.Integration.Mvc;

namespace AutofacDependancyResolver.DependancyResolution.AutoFac.Factory
{
    public class AutofacContainerFactory
    {
        public IContainer Create()
        {
            var builder = new ContainerBuilder();

            // Register All Controllers In The Current Scope
            builder.RegisterControllers(AppDomain.CurrentDomain.GetAssemblies()).InstancePerRequest();

            // Register Modules
            builder.RegisterModule<ServicesModule>();

            // Register Additional Things
            // ...

            // Build The Container and return it as a result
            return builder.Build();
        }
    }
}

So what the code actually does is creating a new ContainerBuilder which is used to build the Container, registers all controllers in the current domain (only controllers), registration of a custom module (not required) and returns the built container as a result. And that is it – simple as that 🙂

Building an Autofac Controller Factory

In order to build a custom Controller Factory, the class needs to implement from the DefaultControllerFactory. It comes with 2 Important Methods that must be overridden:

  1. GetControllerInstance – which is going to resolve the controller out of our IoC Container
  2. ReleaseController – which is irrelevant for Autofac Managed Application, as the release is done by the IoC container.

using System;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using Autofac;

namespace AutofacDependancyResolver.DependancyResolution.AutoFac.Factory
{
    public class AutofacControllerFactory : DefaultControllerFactory
    {
        private readonly IContainer _container;

        public AutofacControllerFactory(IContainer container)
        {
            if (container == null)
            {
                throw new ArgumentNullException(nameof(container));
            }

            _container = container;
        }

        protected override IController GetControllerInstance(RequestContext context, Type controllerType)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (controllerType == null)
            {
                throw new HttpException(404,
                    string.Format("No Controller Type Found For Path {0}", context.HttpContext.Request.Path));
            }

            object controller;

            if (_container.TryResolve(controllerType, out controller))
            {
                return (IController) controller;
            }

            throw new HttpException(404,
                string.Format("No Controller Type: {0} Found For Path {1}", controllerType.FullName,
                    context.HttpContext.Request.Path));
        }

        public override void ReleaseController(IController controller)
        {
        }
    }
}

The code basically tries to resolve the controller from our IoC container. If no controller is found – a 404 exception is thrown.

The Pipeline !

So the last step is to register a pipeline that is going to replace the default controller factory with the Autofac Controller Factory. The code for the pipeline looks like this:


using System.Web.Mvc;
using Autofac.Integration.Mvc;
using AutofacDependancyResolver.DependancyResolution.AutoFac.Factory;
using Sitecore.Pipelines;

namespace AutofacDependancyResolver.DependancyResolution.AutoFac.Pipelines
{
    public class InitializeAutofacControllerFactory
    {
        public virtual void Process(PipelineArgs args)
        {
            SetControllerFactory(args);
        }

        private void SetControllerFactory(PipelineArgs args)
        {
            var containerFactory = new AutofacContainerFactory();
            var container = containerFactory.Create();
            DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
            var controllerFactory = new AutofacControllerFactory(container);
            ControllerBuilder.Current.SetControllerFactory(controllerFactory);
        }
    }
}

So the default controller factory is set to our controller factory ! Now the only thing remaining is to replace the pipeline with our own ! The configuration should be similar to:



<?xml version="1.0"?>

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <initialize>
        <processor type="AutofacDependancyResolver.DependancyResolution.AutoFac.Pipelines.InitializeAutofacControllerFactory, AutofacDependancyResolver.DependancyResolution" patch:before="*[@type='Sitecore.Mvc.Pipelines.Loader.InitializeControllerFactory, Sitecore.Mvc']" />
        <processor type="Sitecore.Mvc.Pipelines.Loader.InitializeControllerFactory, Sitecore.Mvc">
          <patch:delete />
        </processor>
      </initialize>
    </pipelines>
  </sitecore>
</configuration>


And that is it ! The Autofac resolving is ready to go ! 🙂

TL;DR;

You can find the example solution on Bitbucket !

Happy Injecting !

Sitecore SVG Support

SVG support is a topic that has already been discussed multiple times in blog posts, stackoverflow answers etc.

The main and the most common solution is to register the SVG MIME Type under the /sitecore/mediaLibrary/mediaTypes node like the following line.




<mediaLibrary>
      <mediaTypes>
        <mediaType name="SVG" extensions="svg">
          <mimeType>image/svg+xml</mimeType>
          <forceDownload>false</forceDownload>
          <sharedTemplate>system/media/unversioned/image</sharedTemplate>
          <versionedTemplate>system/media/versioned/image</versionedTemplate>
          <mediaValidator type="Sitecore.Resources.Media.ImageValidator"/>
          <thumbnails>
            <generator type="Sitecore.Resources.Media.ImageThumbnailGenerator, Sitecore.Kernel">
              <extension>png</extension>
            </generator>
            <width>150</width>
            <height>150</height>
            <backgroundColor>#FFFFFF</backgroundColor>
          </thumbnails>
        </mediaType>
      </mediaTypes>
    </mediaLibrary>


And that is it ! Now your SVG images can be safely uploaded to the Media Library !

However now you will encounter some problems when the content editors start uploading and using SVG Images, so I decided to put a blog post about them.

Note: All source code shown here is using Sitecore 8.1 Initial Release. Also because the code for the corresponding pages is pretty big I will just post the relevant fragments here. The full code of the solution can be found on BitBucket.

Now we’ve got the SVG images into the media library. The next step is to use them around the site right? Which leads me to:

Problem 1: My Rich Text Editor is not accepting SVGs !

Imagine one of our content editors decided to place the SVG image in a Rich Text Field somewhere in the site, sounds simple right ? Well, it does sound simple, but as it seems – it is not, because after trying to add the image via the Insert Sitecore Media button the following message appears:

Media Library

As it seems Sitecore still doesn’t like SVGs and it doesn’t treat them as images that can be placed in the Rich Text fields. So after spending some time with the decompiled Sitecore.Client assembly, the exception can be traced to the following line located in the InsertImageForm class in the OnOK function:


else if (!(MediaManager.GetMedia(MediaUri.Parse((Item) mediaItem)) is ImageMedia))
{
    SheerResponse.Alert("The selected item is not an image. Select an image to continue.");
}

So Sitecore is not considering our SVG as ImageMedia.

Note: Here is the time to say that I haven`t found an approach to register the SVG as ImageMedia. If someone has an idea I will be very grateful if he can post it in the comments section.

Well the fix is pretty straightforward – nothing fancy here. We need to hijack the dialog and add some custom logic to recognize our SVG as a real image.

The first thing we need to do is to create our custom class that will be an exact replica of the decompiled code of the InsertImageForm class, except the modified else/if statement which should be modified to as follows:

else if (!(MediaManager.GetMedia(MediaUri.Parse(mediaItem)) is ImageMedia || mediaItem.MimeType == "image/svg+xml"]))
{
    SheerResponse.Alert("The selected item is not an image. Select an image to continue.");
}

The next step is to make the dialog use our own class. The XML Layout file for this dialog is sitecore\shell\Controls\Rich Text Editor\InsertImage\InsertImage.xml. There we need to replace the following line:


<CodeBeside Type="Sitecore.Shell.Controls.RichTextEditor.InsertImage.InsertImageForm,Sitecore.Client">

with



<CodeBeside Type="[YOUR_CLASS_NAME],[YOUR_ASSEMBLY]"/>


Note: I usually prefer to copy and paste the XML Layout into /sitecore/shell/override folder instead of modifying the existing one.

And now we can safely add our SVG Image !

Media Library Uploaded

But there is still a problem, which leads me to:

Problem 2: My Rich Text Editor is not handling the SVG correctly !

So now we need to make the image appear ! The problem shows itself when we check the html of the Rich Text Editor.

SVG Image

The SVG image is handled as a normal Sitecore Media File, meaning that it goes through the image handler and is displayed as a normal image in <img> tag. The problem is that the SVG is actually a XML File which needs to be rendered as it is for consistency. If your FrontEnd developers don’t want to manipulate the actual SVG when it gets rendered – you can stop reading here :). If you are one of the few unlucky ones – no worries, there is hope ! The salvation is again in the OnOK method of the InsertImageForm class, just a line bellow our modifications. If the image is considered a valid image file the following code gets executed:


MediaUrlOptions shellOptions = MediaUrlOptions.GetShellOptions();
shellOptions.Language = this.ContentLanguage;
string text = !string.IsNullOrEmpty(HttpContext.Current.Request.Form["AlternateText"]) ? HttpContext.Current.Request.Form["AlternateText"] : mediaItem.Alt;
Tag image = new Tag("img");
this.SetDimensions(mediaItem, shellOptions, image);
image.Add("Src", MediaManager.GetMediaUrl(mediaItem, shellOptions));
image.Add("Alt", StringUtil.EscapeQuote(text));
image.Add("_languageInserted", "true");
if (this.Mode == "webedit")
{
    SheerResponse.SetDialogValue(StringUtil.EscapeJavascriptString(image.ToString()));
    base.OnOK(sender, args);
}
else
    SheerResponse.Eval("scClose(" + StringUtil.EscapeJavascriptString(image.ToString()) + ")");

So no matter the image type – it always gets appended to an <img> tag with the corresponding URL. In order to make our SVG fully functional instead of rendering the image itself we need to render the XML contents of the actual media file. It can be done easily by reading the stream of the actual media image. Also we need to make sure that we set the width and height in which we want to display our SVG. The modified code looks like this:


if (mediaItem.MimeType == "image/svg+xml")
{
   string result;

   using (StreamReader reader = new StreamReader(mediaItem.GetMediaStream(), Encoding.UTF8))
   {
      result = reader.ReadToEnd();
   }

   XDocument svg = XDocument.Parse(result);
   NameValueCollection form = HttpContext.Current.Request.Form;

   if (svg.Document?.Root != null)
   {
      int width;

      if (int.TryParse(form["Width"], out width))
      {
         svg.Document.Root.SetAttributeValue("width", width);
      }

      int height;

      if (int.TryParse(form["Height"], out height))
      {
          svg.Document.Root.SetAttributeValue("height", height);
      }

      result = svg.ToString();
   }

   if (Mode == "webedit")
   {
      SheerResponse.SetDialogValue(StringUtil.EscapeJavascriptString(result));
      base.OnOK(sender, args);
   }
   else
   {
      SheerResponse.Eval("scClose(" + StringUtil.EscapeJavascriptString(result) + ")");
   }

}
else
{
   Tag image = new Tag("img");
   SetDimensions(mediaItem, shellOptions, image);
   image.Add("Src", MediaManager.GetMediaUrl(mediaItem, shellOptions));
   image.Add("Alt", StringUtil.EscapeQuote(text));
   image.Add("_languageInserted", "true");

   if (Mode == "webedit")
   {
      SheerResponse.SetDialogValue(StringUtil.EscapeJavascriptString(image.ToString()));
      base.OnOK(sender, args);
   }
   else
   {
        SheerResponse.Eval("scClose(" + StringUtil.EscapeJavascriptString(image.ToString()) + ")");
   }
}

By doing this modification – now the SVG is rendered as the xml it should be rendered as ! 🙂

SVG Rendered

So up to now we can properly render SVG Images in our Rich Text Editor ! But

Problem 3: But what about rendering it on a Sublayout/Rendering ?

Well, the next struggle is to render the SVG correctly in our components. And here is one of the things I love most about Sitecore – all field renders (if used correctly :)) goes through the RenderField pipeline ! Meaning – we just need to modify the GetImageFieldValue processor with our own and we should be set !

The GetImageFieldValue processor renders the image thanks to ImageRenderer helper class. So we need to create our own ImageRenderer which is going to support SVGs as well.

We are going to create a new class named ImageRendererEx which will contain all the code from the original ImageRenderer, but have modifications for rendering our SVG images. The only change here should be in the Render() method. The modified Render function looks like this:


public virtual RenderFieldResult Render()
{
   var obj = Item;
   if (obj == null)
   {
      return RenderFieldResult.Empty;
   }

   var keyValuePairs = Parameters;

   if (keyValuePairs == null)
   {
      return RenderFieldResult.Empty;
   }

   ParseNode(keyValuePairs);

   var innerField = obj.Fields[FieldName];

   if (innerField != null)
   {
      imageField = new ImageField(innerField, FieldValue);

      ParseField(imageField);
      AdjustImageSize(imageField, scale, maxWidth, maxHeight, ref width,
      ref height);

      if (imageField.MediaItem != null)
      {
         MediaItem imageMediaItem = new MediaItem(imageField.MediaItem);

         if (imageMediaItem.MimeType == "image/svg+xml")
         {
            string result;

            using (StreamReader reader = new StreamReader(imageMediaItem.GetMediaStream(), Encoding.UTF8))
            {
               result = reader.ReadToEnd();
            }

            XDocument svg = XDocument.Parse(result);

            if (svg.Document?.Root != null)
            {
               if (width > 0)
               {
                  svg.Document.Root.SetAttributeValue("width", width);
               }

               if (height > 0)
               {
                  svg.Document.Root.SetAttributeValue("height", height);
               }

               result = svg.ToString();
            }

            return new RenderFieldResult(result);
         }
      }
   }

   var site = Context.Site;

   if ((string.IsNullOrEmpty(source) || IsBroken(imageField)) && site != null &&
site.DisplayMode == DisplayMode.Edit)
   {
      source = GetDefaultImage();
      className += " scEmptyImage";
      className = className.TrimStart(' ');
   }

   if (string.IsNullOrEmpty(source))
   {
      return RenderFieldResult.Empty;
   }

   var imageSource = GetSource();
   var stringBuilder = new StringBuilder("<img "); AddAttribute(stringBuilder, "src", imageSource); AddAttribute(stringBuilder, "border", border); AddAttribute(stringBuilder, "hspace", hspace); AddAttribute(stringBuilder, "vspace", vspace); AddAttribute(stringBuilder, "class", className); AddAttribute(stringBuilder, "alt", HttpUtility.HtmlAttributeEncode(alt), xhtml); if (width > 0)
   {
   AddAttribute(stringBuilder, "width", width.ToString());
   }

   if (height > 0)
   {
   AddAttribute(stringBuilder, "height", height.ToString());
   }

   CopyAttributes(stringBuilder, keyValuePairs);
   stringBuilder.Append(" />");
   return new RenderFieldResult(stringBuilder.ToString());
}

And we are done ! Now no matter if we are rendering the image in a MVC Rendering or WebForms Sublayout, if we are using the Sitecore Helper or the Web Control (Worth to mention that it will also work if you are using ORM as long as it is using the RenderField pipeline to fill the values – like Glass Mapper is :)) we will get a nicely formatted SVG image!

Field Rendered Image

 

Full code can be found on BitBucket.

Happy SVGing ! 🙂

Sitecore Custom Workflow Email Action

Workflows are one of the most powerful tools in the Sitecore toolkit. An interesting request we recently had was to build a custom workflow action that will send email to a different mailing group/groups based on the location and the language of the item moved to next step of the workflow. At first it seemed like a complex task but as in many cases – Sitecore is pretty extensible. In this blog post I will cover the simplest possible implementation where we will just send the email to a different mailing group or email. The build should be pretty extensible, so feel free to extend based on it. There are three main steps required for the component implementation:

  1. Create set of templates that will allow the users to control the rules.
  2. Create a new save action template that will build on top of the existing email action.
  3. Code it 🙂

Rule Engine (Kind of :))

For the rule engine we will need 2 templates – one folder template will just store our rules and one rule template. The rule item should give the administrators a chance to pick a start path(from where the items will count as a members of the section) and a possibility to pick languages in which the approvers should be able to move the the item to the next step of the workflow, and finally a list of emails. To be consistent, we are going to add the templates to /sitecore/templates/System/Workflow

So here is an example of how the rule template should look like:

Rule Template

The next decision we have to make is where to place the rules in the content tree. I usually don`t like adding items to the /sitecore/system node if it is not absolutely necessary, but it seems like a good location to place them. Another good option is to place them under the settings node of the website (if there is one). In this example I will place them under the /sitecore/system node like this:

Workflow Rules

Save Action Template

The EmailActionEx should be similar to the existing sitecore/templates/System/Workflow/Email action. The only addition is adding our custom parameter, so the user will be able to pick rules for the action. The save action should look like this:

Email Action Ex

Keep in mind that the Rules DataSource might vary based on where you decided to place your rules folder.

Code Time ! 🙂

When coding the custom action we need to make sure that we keep the existing token functionality of the default functionality of the existing save action (like tokens etc). Snippet of the source code can be found below:


using System;
using System.Collections.Generic;
using System.Linq;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using System.Net.Mail;
using Sitecore.Globalization;
using Sitecore.Workflows.Simple;
 
namespace Sitecore.Workflows.Web.Workflows
{
    public class SendEmailExAction
    {
        public void Process(WorkflowPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
 
            ProcessorItem processorItem = args.ProcessorItem;
 
            if (processorItem == null)
            {
                return;
            }
 
            Item innerItem = processorItem.InnerItem;
 
            string fullPath = innerItem.Paths.FullPath;
 
            List<string> recipents =
                GetItems(innerItem, "rules")
                    .Where(x => ShouldSendEmail(args.DataItem, x))
                    .SelectMany(x => x.Fields["Emails"].Value.Split(';'))
                    .ToList();
 
            if (recipents.Any())
            {
 
                string from = GetText(innerItem, "from", args);
                string mailServer = GetText(innerItem, "mail server", args);
                string subject = GetText(innerItem, "subject", args);
                string message = GetText(innerItem, "message", args);
 
                Error.Assert(from.Length > 0, "The 'From' field is not specified in the mail action item: " + fullPath);
                Error.Assert(subject.Length > 0,
                    "The 'Subject' field is not specified in the mail action item: " + fullPath);
                Error.Assert(mailServer.Length > 0,
                    "The 'Mail server' field is not specified in the mail action item: " + fullPath);
 
                MailMessage mailMessage = new MailMessage();
 
                mailMessage.From = new MailAddress(from);
                mailMessage.Subject = subject;
                mailMessage.Body = message;
 
                foreach (string recipent in recipents)
                {
                    mailMessage.To.Add(new MailAddress(recipent));
                }
 
                SmtpClient client = new SmtpClient(mailServer);
 
                try
                {
                    client.Send(mailMessage);
                }
                catch (Exception ex)
                {
                    Log.Error("EmailExAction Threw An Exception", ex, this);
                }
            }
        }
 
        ///

<summary>
        /// Gets the text.
        /// 
        /// </summary>


        /// <param name="commandItem">The command item.</param><param name="field">The field.</param><param name="args">The arguments.</param>
        /// <returns/>
        private string GetText(Item commandItem, string field, WorkflowPipelineArgs args)
        {
            string text = commandItem[field];
            return text.Length > 0 ? ReplaceVariables(text, args) : string.Empty;
        }
 
        ///

<summary>
        /// Replaces the variables.
        /// 
        /// </summary>


        /// <param name="text">The text.</param><param name="args">The arguments.</param>
        /// <returns/>
        private string ReplaceVariables(string text, WorkflowPipelineArgs args)
        {
            text = text.Replace("$itemPath$", args.DataItem.Paths.FullPath);
            text = text.Replace("$itemLanguage$", args.DataItem.Language.ToString());
            text = text.Replace("$itemVersion$", args.DataItem.Version.ToString());
            return text;
        }
 
        private bool ShouldSendEmail(Item dataItem, Item ruleItem)
        {
            return IsItemMatch(dataItem, ruleItem) && IsLanguageMatch(dataItem, ruleItem);
        }
 
        private bool IsItemMatch(Item dataItem, Item ruleItem)
        {
            Item ruleStartItem = GetStartItem(ruleItem);
 
            return ruleStartItem == null || IsAncestor(dataItem, ruleStartItem);
        }
 
        private bool IsLanguageMatch(Item dataItem, Item ruleItem)
        {
            List<Language> languages = GetLanguages(ruleItem).ToList();
 
            return !languages.Any() || languages.Contains(dataItem.Language);
        }
 
        private IEnumerable<Language> GetLanguages(Item ruleItem)
        {
            MultilistField selectedLanguages = ruleItem.Fields["Languages"];
 
            if (selectedLanguages != null && selectedLanguages.TargetIDs.Any())
            {
                return selectedLanguages.GetItems().Select(x => Language.Parse(x.Name));
            }
 
            return Enumerable.Empty<Language>();
        }
 
        ///

<summary>
        /// Gets the items
        /// </summary>


        /// <param name="commandItem"></param>
        /// <param name="field"></param>
        /// <returns></returns>
        private IEnumerable<Item> GetItems(Item commandItem, string field)
        {
            MultilistField rules = commandItem.Fields[field];
 
            if (rules != null && rules.TargetIDs.Any())
            {
                return rules.GetItems();
            }
 
            return Enumerable.Empty<Item>();
        }
 
        private Item GetStartItem(Item ruleItem)
        {
            ReferenceField startPathField = ruleItem.Fields["StartPath"];
 
            if (startPathField != null && startPathField.TargetItem != null)
            {
                return startPathField.TargetItem;
            }
 
            return null;
 
        }
 
        private bool IsAncestor(Item currentItem, Item ancestor)
        {
            if (currentItem.ID == ancestor.ID)
            {
                return true;
            }
 
            if (currentItem.Parent != null)
            {
                return IsAncestor(currentItem.Parent, ancestor);
            }
 
            return false;
        }
    }
}

In the code we need to check if the item matches both rules for language and location. If one or more of the rules are matched – we send the email to the corresponding emails. Otherwise we skip them. If there is no match – no email is sent.

And that is it ! Now we have our custom email action that can send items based on the item location and the item language !

The Sample Project can be found on BitBucket.

For people without TDS the package can be downloaded separately from here: SitecoreWorkflows-1.0.zip

Happy Workflowling !

The Amazing World of RAZL Part 2 – Script Like A Boss

It has been almost a year from my first post about The Amazing World of Razl and the second part finally found its place in the posts pipeline. In this post I am going to dig deeper into one of the most amazing and useful Razl features – Scripting. Also for the brave ones who reach the end of the article there will be a sneak peak of the new Razl version! 🙂

First, let’s go through a real life scenario that every single Sitecore developer faced at some point. Many times we find ourselves slammed with bugs, which are caused by differences in content in our QA and Production Instances. So every single time we need to explain that our QA instance is not up to date with the latest Production content and that it is normal to have these issues. After that the frequent scenario is – “Can you please update the content, so we can regression test feature X”. Following that, we need to go to our Production CM, export a content package, and install it on our QA instances. The huge problem, with this, is that it happens pretty frequently. Especially on big projects with many content dependencies (i.e. I want to test the search boost I requested but I get only 2 search results in total). This is the case in which the Razl Scripting mode shines.

Razl scripts are actually a fairly simple xml files which can be executed from the command line. They have 2 main components – connections and operations.

Connections

Connections are actually a way to describe a Sitecore Instance that Razl needs to connect to. The basic XML for Razl looks like this:




<connection name="" readOnly ="" install="" preset="">



<url></url>



<accessGuid></accessGuid>



<database></database>



<path></path>



</connection>



So let’s explore what the different parameters and attributes do:

  • name – This is the main identifier that is going to get used by the operations to determine the Instance they are going to execute on.
  • readOnly – The possible values are true and false. Specifies if the connection is read only, i.e. if we try to execute a write command on a read only connection – it will not get executed. It is a good practice in your scripts to set your connection to read only.
  • install– The possible values are again true or false. Specifies if the script should install the Razl Service if it is not already installed. If set to true – the path parameter must be specified.
  • url– Used to define the url of the Sitecore Instance we want to connect to.
  • accessGuid– Defines the access Guid which is used by Razl to access the Sitecore Instances. If Razl is already installed on the Sitecore Instance – the Guid can be obtained by reading it`s value from the web.config defined in [Webroot]\_CMP.
  • database– The database which is going to be used by the connection. Nothing fancy here.
  • path– The UNC path to the Sitecore instances. It is used only when you want to install the Razl Service on the server. My recommendation is to preinstall the Razl Service on all servers you want to script on, so you won`t have to expose your servers file systems.
  • preset– If you already have the connection in the RAZL desktop application and you are too lazy to write some xml – you can use the preset property to pass the name as it is in your RAZL application. I usually advice against that, because you cannot be sure which presets are actually installed on the server and in most cases you won`t be the only person using the script.

There is no upper limit on how many connections you can define and naturally, you should have at least 2 connections defined in the script – because you are going to use it on at least 2 Instances (or 2 Databases of a single instance).

Operations

Operations are the way of script telling RAZL what needs to be executed. The basic operation XML looks like this:




<operation name="" source="" target="">

<parameter name="param">

<value></value>

...

</parameter>

</operation>



So let’s take a look at what the different attributes do:

  • source– The name of the connection you want to use for sourcing Sitecore Instance/Database Combo (Copy From :))
  • target– The name of the connection you want to use for targeted Sitecore Instance/Database Combo (Copy To :))
  • name– Name is actually used for the operation types – CopyItem, CopyAll and CopyHistory. I will cover what each of the 3 operation types do later in this post as we need to get familiar with the parameters first 🙂

Every operation can have one or more parameters. All parameters have a name attribute, every operation type has a list of supported parameter names. Parameters themselves can be single valued and multi-valued.

Here is an example of a

  1. Single valued parameter xml:

<parameter name="">[value]</parameter>

  1. Multi-valued parameter



<parameter name="">

<value>[value]</value>

<value>[value]</value>

.....

</parameter>



As different operation types accept different types of parameters, I will describe them in a separate section.

Operation Types

Copy Item Operation

Base Syntax:




<operation name="CopyItem" source="" target="">



<parameter name="itemId"></parameter>



</operation>



Functionality:

The Copy Item operation copies a single item from the source to the target database. It requires one parameter – itemId which is the ID of the item in the source database. Nothing fancy here.

Copy All Operation

Base Syntax:




<operation name="CopyAll" source="" target="">



<parameter name="itemId"></parameter>



<parameter name="overwrite"></parameter>



</operation>





Functionality:

The Copy All operation copies an item and its descendants from the source to the target database. It requires 2 parameters – itemId which is the ID of the starting item in the source database from which the copying starts and overwrite is a parameter which determines what to do with items which are present in the target database, but are missing in the source database, i.e. if the setting is set to true and there is an item in the target database that is not present in the source – it will get deleted, if the setting is set to false – the item will be skipped.

Copy History Operation

Base Syntax:


<operation name="CopyHistory" source="" target="">



<parameter name="from"></parameter>



<parameter name="to"></parameter>



<parameter name="recycle"></parameter>



<parameter name="include">



<value></value>



</parameter>



<parameter name="exclude">



<value></value>



</parameter>



</operation>



Functionality:

I am sure many of you are familiar that Sitecore comes with a very powerful History Engine. The history engine uses a SQL Table to store all events that happened to an item. The history engine is enabled by default in the recent releases of Sitecore, before that you needed to enable that manually from the web.config.

The Razl copy history operation will read the actions from this table in the source database which occurred in a certain time frame and execute them over the target database. As this operation is more risky:) it accepts several parameters to give you better control over what is going on.

  • The “from” parameter specifies the start time from which Razl should start reading events from the history table. The date needs to be in the following format: yyyy-MM-ddThh:mm:ss. This is the only required parameter of the operation.
  • The “to” parameter specifies the end time when Razl should stop reading events from the history table. The date format is the same like the one for the “from” parameter: yyyy-MM-ddThh:mm:ss. Parameter is optional and if it is not set every event up to now will be executed.
  • The “recycle” is a boolean parameter and it specifies what Razl should do with deleted items. If set to true it will move them to recycle bin and if set to false it will remove them (they will just be deleted not recycled). I strongly advise of always setting this parameter to true as you cannot be sure what the content editors might have done. The parameter is optional with a default value of true.
  • The other two parameters (includeand exclude) are multi-valued and control which items (and their descendants) should be included/excluded when performing events. Both parameters work with item paths and exclude has bigger power over include (in case a path is included in both parameters). If the parameters are left blank – all actions will get executed. If there is a single path in the include – only actions over this item and its descendants will be executed.

I also created a small set of scripts which can be used for a good starting point when you are writing your own. The scripts can be found on BitBucket.

RAZL 2.6

And now – some spoilers! 🙂

There is a new release of Razl coming soon! 🙂

With the new release there are several new features, along with huge improvements to the scripting mode! Here are some of the highlights of the new release!

Lightning Mode

Razl 2.6 will introduce a new Lightning Mode for comparison. It increases comparison speed dramatically by using only the revision number when comparing!

Deep Compare

One of the most requested Razl features, deep compare will ship with the new release! Now you will be able to deep compare items without the need to go down the tree !

Scripting Mode Improvements

Razl 2.6 will introduce 5 new operation types:

  1. CopyVersion – For copying a specific version of an item.
  2. DeleteItem – For deleting items.
  3. MoveItem – For moving items in the content tree.
  4. SetFieldValue – For setting field values from a Razl Script.
  5. SetPropertyValue – For setting property values !

Also – a new parameter lightningMode will be introduced for CopyAll and CopyHistory operations to use the new lightning mode for quick comparison of huge trees.

Make sure you stay up to date with the new Razl Features by

Razl Website: http://razl.net/

Hedgehog Development Website: http://www.hhogdev.com/

Our Twitter: @hhogdev

Happy Razling !

Permissions Required To Edit Rendering Parameters

Recently we faced a problem in which we needed a role that had to be able to edit the content and the presentation details of an item in the content tree. For authoring usually the sitecore\Author is the way to go (if you don`t have any specific multi-site or language specific rules) and the obvious choice for designing is to use the built-in sitecore\Designer role. But as it seems the designer role has Read, Write, Rename, Create and Delete permissions on almost everything which is located under the /sitecore/layout and /sitecore/templates folder as it can be seen on the screenshot below.

Sitecore Designer Permissions

This was a bit too much for our needs as we were not going to use this role for creating and designing renderings or setting presentation details on template level, but we needed it just to be able to edit the presentation details of an item.

The next possible resolution was to use the sitecore\Sitecore Client Designing (or give our custom role the necessary permissions to access the designer tab and edit the sublayouts) as it already had permissions to edit the presentation details of an item. Everything seemed to work fine until we tried to set the datasource of a sublayout in the Content Manager and saw that the role lacks the necessary permissions to edit the parameter template fields.

Parameter Template No Permissions

The first confusing part was that the role had permissions on the Caching section – which might actually be a bug in Sitecore.

The second confusing part was that the role had Field Read and Field Write access on the affected fields as seen on the screenshot below:

Field Write Enabled

So after some deep diving in the Sitecore.Client and the Sitecore.Kernel Assemblies I found that the actual rendering of the field happens in the RenderField Method of the EditorFormatter class.

 

    public virtual void RenderField(System.Web.UI.Control parent, Editor.Field field, bool readOnly)
    {
      Assert.ArgumentNotNull((object) parent, &amp;amp;amp;amp;amp;amp;quot;parent&amp;amp;amp;amp;amp;amp;quot;);
      Assert.ArgumentNotNull((object) field, &amp;amp;amp;amp;amp;amp;quot;field&amp;amp;amp;amp;amp;amp;quot;);
      Sitecore.Data.Fields.Field itemField = field.ItemField;
      Item fieldType = this.GetFieldType(itemField);
      if (fieldType == null)
        return;
      if (!itemField.CanWrite)
        readOnly = true;
      this.RenderMarkerBegin(parent, field.ControlID);
      this.RenderMenuButtons(parent, field, fieldType, readOnly);
      this.RenderLabel(parent, field, fieldType, readOnly);
      this.RenderField(parent, field, fieldType, readOnly);
      this.RenderMarkerEnd(parent);
    }

The moment when it is decided if the field can be modified is the check for itemField.CanWrite. Which leads to:

    public bool CanWrite
    {
      get
      {
        if (AuthorizationManager.GetAccess((ISecurable) this, (Account) Context.User, AccessRight.FieldWrite).Permission == AccessPermission.Deny)
          return false;
        if (this.ID == FieldIDs.Security || this.ID == FieldIDs.InheritSecurity)
          return this.Item.Access.CanAdmin();
        return this.Item.Access.CanWrite();
      }
    }

As can be seen from the code above the access for fieldwrite is taken into account is only when the permission is set to Deny. So the actual check that is happening is if there is write access on the current item (which is actually the _standard values of the /sitecore/templates/System/Layout/Rendering Parameters/Standard Rendering Parameters template).

All that said, after we gave write permissions on the _standard values of the standard rendering paramaters template – everything started working !

TL; DR  🙂

Give Write Access to /sitecore/templates/System/Layout/Rendering Parameters/Standard Rendering Parameters/_Standard Values

WFFM Session Aware Single-Line Text Field

Recently we faced an issue where a value of certain Single-Line Text Field in Web Forms For Marketers had to be populated by a variable in the session. The problem is that WFFM only supports query string parameters for populating fields. The good thing is that WFFM is extremely easy to extend :). The tricky part here is that with the recent versions of Sitecore the field needs to be created for MVC as well (WFFM 2.4+ and Sitecore 7+). In addition to the field there will be two boolean values which will control if the field is going to be hidden (display:none;) and if the session variable should be removed after the field is populated (Which is easily doable with decorators).

So all in all building a session aware Single-Line Text Field requires 2 classes – 1 Main where our properties will be for WebForms and 1 for MVC. It is also required to register the field with WFFM which means creating a custom Field Item.

First goes the main class. It should have 3 Properties:

  1. SessionKey for storing the key which should be captured.
  2. DeleteKeyAfterCapture – flag which indicates if the key needs to be removed from the session after the value is already used.
  3. IsHidden – flag which indicates if the field needs to be hidden or displayed.

In WFFM the type of the field which will be displayed in the form designer is controlled by the VisualFieldType decorator. The problem here is that the Boolean Field type (not counting the tag field for Engagement Analytics :)) is represented by <select> element with two options – Yes and No. In the code there is a workaround for this problem taken from the DropList field. Here is the code for the class:

using System.ComponentModel;
using System.ComponentModel.Design;
using System.Web;
using Sitecore.Form.Core.Attributes;
using Sitecore.Form.Web.UI.Controls;

namespace WFFM.Custom.Fields
{
    [Designer("System.Windows.Forms.Design.ParentControlDesigner, System.Design", typeof(IDesigner))]
    public class SingleLineSessionTextField : SingleLineText
    {
        [VisualCategory("Session")]
        [VisualProperty("Session Key", 100), DefaultValue("")]
        public string SessionKey { get; set; }

        private bool _deleteKeyAfterCapture = true;

        [DefaultValue("Yes")]
        [VisualCategory("Session")]
        [VisualFieldType(typeof (Sitecore.Form.Core.Visual.BooleanField))]
        [VisualProperty("Delete Key After Capture?", 200)]
        public string DeleteKeyAfterCapture
        {
            get
            {
                return _deleteKeyAfterCapture ? "Yes" : "No";
            }
            set
            {
                _deleteKeyAfterCapture = value == "Yes";
            }
        }

        private bool _isHidden = true;

        [VisualCategory("Session")]
        [VisualFieldType(typeof (Sitecore.Form.Core.Visual.BooleanField))]
        [VisualProperty("Is Hidden?", 300), DefaultValue("Yes")]
        public string IsHidden
        {
            get
            {
                return _isHidden ? "Yes" : "No";
            }
            set
            {
                _isHidden = value == "Yes";
            }
        }

        protected override void OnLoad(System.EventArgs e)
        {
            if (!string.IsNullOrWhiteSpace(SessionKey))
            {
                if (HttpContext.Current != null && HttpContext.Current.Session[SessionKey] != null)
                {
                    Text = HttpContext.Current.Session[SessionKey].ToString();

                    if (_deleteKeyAfterCapture)
                    {
                        HttpContext.Current.Session.Remove(SessionKey);
                    }

                }
            }

            if (_isHidden)
            {
                Attributes["style"] = "display:none";
            }
        }
    }
}

The logic is located in the OnLoad method. There is basically a check for the Session Key and the field Text is prepopulated from the Session if the key exists – nothing complex 🙂

Second goes the class for the MVC field which is only required if the field is going to be used in MVCForm Rendering.

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web;
using Sitecore.Data.Items;

namespace WFFM.Custom.MVC.Fields
{
    public class SingleLineSessionTextField : Sitecore.Forms.Mvc.Models.Fields.SingleLineTextField
    {
        public string SessionKey { get; set; }

        [DataType(DataType.Text)]
        public override object Value { get; set; }

        public SingleLineSessionTextField(Item item)
            : base(item)
        {
            Initialize();
        }

        private void Initialize()
        {
            KeyValuePair<string, string> deleteKeyAfterCapture =
                ParametersDictionary.FirstOrDefault(x => x.Key.ToUpper() == "DELETEKEYAFTERCAPTURE");

            KeyValuePair<string, string> isHidden =
                ParametersDictionary.FirstOrDefault(x => x.Key.ToUpper() == "ISHIDDEN");

            if (!string.IsNullOrWhiteSpace(SessionKey))
            {
                if (HttpContext.Current != null && HttpContext.Current.Session[SessionKey] != null)
                {
                    Value = HttpContext.Current.Session[SessionKey].ToString();

                    if (deleteKeyAfterCapture.Value.ToUpper() != "YES")
                    {
                        HttpContext.Current.Session.Remove(SessionKey);
                    }

                    if (isHidden.Value.ToUpper() != "YES")
                    {
                        if (!string.IsNullOrWhiteSpace(CssClass))
                        {
                            CssClass += " hidden";
                        }
                        else
                        {
                            CssClass = "hidden";
                        }

                    }
                }
            }
        }
    }
}

As the properties are already present we can just reuse them. The problem is that the properties for the complex structures like the BooleanField cannot be handled directly and they need to be taken from the ParametersDictionary. In WFFM 2.5 there is an extension method for getting values from the ParametersDictionary(and any other dictionary basically :)). So it is possible to replace the KeyValue Pair selections with the following if the target version is 2.5:


string deleteKeyAfterCapture = Sitecore.Forms.Mvc.Extensions.DictionaryExtensions.GetValue(this.ParametersDictionary, "deletekeyaftercapture");
string isHidden = Sitecore.Forms.Mvc.Extensions.DictionaryExtensions.GetValue(this.ParametersDictionary, "ishidden");

The only thing remaining is registering the field with WFFM. This is achieved by creating an item of from the Field Type Template under /sitecore/system/Modules/Web Forms for Marketers/Settings/Field Types. (My personal preference is to place them under /sitecore/system/Modules/Web Forms for Marketers/Settings/Field Types/Custom, but any other folder will do it).

The main values that need to be populated are:

  1. Assembly – The assembly in which the Main (WebForms) class is located.
  2. Class – Fully Qualified Class Name of the Main (WebForms) class.
  3. MVC Type – Required only if the field is going to be used in MVCForm Rendering. Must be in the following format – [FullyQualifiedClassName], [AssemblyName]

Here is an example with the values set:

WFFM Field

And it is done ! The field type is now selectable in the form designer and the properties are edited from the left hand menu, as it can be seen on the screenshot bellow!

Form Designer

The full code for the field can be found on BitBucket .

 

Sitecore Adaptive Images – Huge Crawling Logs Problem

Recently we noticed that on our production environment the Crawling Log files were abnormally big – between 500 MB and 1 GB. After some digging we found out that they were just filling up with exceptions while crawling the media library. The exceptions look like this:


4576 10:48:07 WARN Could not compute value for ComputedIndexField: urllink for indexable: sitecore://master/{8EA15044-EE2C-41DB-81D6-0A9C42062814}?lang=en&ver=1
Exception: System.NullReferenceException
Message: Object reference not set to an instance of an object.
Source: Sitecore.Kernel
at Sitecore.Context.PageMode.get_IsNormal()
at adaptiveImages.AdaptiveImagesMediaProvider.GetMediaUrl(MediaItem item)
at Sitecore.ContentSearch.ComputedFields.UrlLink.ComputeFieldValue(IIndexable indexable)
at Sitecore.ContentSearch.LuceneProvider.LuceneDocumentBuilder.AddComputedIndexFields()

Considering the case that we have tons of media (over 2000 items) and languages (there is a separate exception for each language) we had some pretty serious index time performance issues and hard disk space problems.

After some debugging we found out that the Sitecore.Context.PageMode.IsNormal was throwing null reference. The huge problem was that there is no way to protect against null pointers here because you cannot check the Sitecore.Context and the Sitecore.Context.PageMode for nulls. The good part is that the PageMode.IsNormal method code is using the Sitecore.Context.Site (which is the actual null in this case) to check the PageMode.


public static bool IsNormal
{
    get
    {
        return Context.Site.DisplayMode == DisplayMode.Normal;
    }
}

With this information the fix became pretty trivial. As try-catch is pretty expensive operation – we just needed to check if the Context.Site is not null and the Context.Site.DisplayMode is normal. The fixes took place in the two GetMediaUrl overrides. Here is the code for the modified methods which were causing the exception.

  /// <summary>
        /// Gets a media URL.
        /// </summary>
        /// <param name="item">The media item.</param>
        /// <returns>
        /// The media URL.
        /// </returns>
        public override string GetMediaUrl(MediaItem item)
        {
            Assert.ArgumentNotNull(item, "item");

            // FIX FOR CHECKING THE PAGE MODE
            bool isNormal = Context.Site != null && Context.Site.DisplayMode == DisplayMode.Normal;

            //If media item is not an image or the page context is not normal, then return
            if (!IsImage(item) || !isNormal)
                return base.GetMediaUrl(item);

            MediaUrlOptions mediaUrlOptions = new MediaUrlOptions();

            return GetMediaUrl(item, mediaUrlOptions);
        }

        /// <summary>
        /// Gets the media URL.
        /// </summary>
        /// <param name="item">The item.</param>
        /// <param name="mediaUrlOptions">The media URL options.</param>
        /// <returns></returns>
        public override string GetMediaUrl(MediaItem item, MediaUrlOptions mediaUrlOptions)
        {
            Assert.ArgumentNotNull(item, "item");
            Assert.ArgumentNotNull(mediaUrlOptions, "mediaUrlOptions");

            // FIX FOR CHECKING THE PAGE MODE
            bool isNormal = Context.Site != null && Context.Site.DisplayMode == DisplayMode.Normal;

            //If media item is not an image or the page context is not normal, then return
            if (!IsImage(item) || !isNormal || Context.Database == null || Context.Database.Name != _database)
                return base.GetMediaUrl(item, mediaUrlOptions);

            //If resolution cookie is not set
            if (!IsResolutionCookieSet())
            {
                //If mobileFirst is set to FALSE or user agent is identifying as a desktop, return with largest break-point resolution
                if (!_mobileFirst || IsDesktopBrowser())
                {
                    mediaUrlOptions.MaxWidth = GetLargestBreakpoint();
                    return base.GetMediaUrl(item, mediaUrlOptions);
                }
                //Return with mobile-first breakpoint (Smallest)
                mediaUrlOptions.MaxWidth = GetMobileFirstBreakpoint();
                return base.GetMediaUrl(item, mediaUrlOptions);
            }

            // If Max-width is not set or Max-width is greater than the selected break-point, then set the Max-width to the break-point
            if (mediaUrlOptions.MaxWidth == 0 || mediaUrlOptions.MaxWidth > GetScreenResolution())
                mediaUrlOptions.MaxWidth = GetScreenResolution();

            // If Max-width is not set and the 'maxWidth' setting is not empty, then set the Max-width property to the maxWidth
            if (mediaUrlOptions.MaxWidth == 0 && !string.IsNullOrEmpty(_maxWidth))
            {
                int maxWidth = 0;
                if (int.TryParse(_maxWidth, out maxWidth))
                {
                    // If pixel ratio is normal
                    if (GetCookiePixelDensity() == 1)
                        mediaUrlOptions.MaxWidth = maxWidth;
                    else
                        mediaUrlOptions.MaxWidth = maxWidth * GetCookiePixelDensity();
                }

            }

            return base.GetMediaUrl(item, mediaUrlOptions);
        }

Basically we are using an isNormal variable to check the Context.Site display mode instead of using the Context.PageMode.IsNormal property:


            bool isNormal = Context.Site != null && Context.Site.DisplayMode == DisplayMode.Normal;

By doing this we kept the module’s consistency and logic as we just added a null check for the Context.Site. After the fix our logs became around 2 kb again and our indexing speed came back to normal !

Happy Log Fixing ! 🙂

 

5 Reasons To Pick Active Commerce For Your E-Commerce Needs

Recently the guys at Active Commerce (Follow @activecommerce) provided a free development training for Sitecore MVPs led by Nick Wesselman (Follow @techphoria414). The guys there did an amazing job in implementing a really simple yet powerful E-Commerce solution for Sitecore, so I decided to write a blog post about why you should pick Active Commerce as your E-Commerce platform.

Reason 1 –  It is built on top of Sitecore with Sitecore ! 

One of the best things about Active Commerce is that it uses all the built-in Sitecore Capabilities for creating an E-Commerce solution. Active Commerce uses and extends the Sitecore Layout Engine by adding skinning capabilities (which will be covered in Reason 2). It uses and extends Sitecore Ecommerce Services (SES) for E-Commerce related things like product catalogs, shopping carts, etc. Also it ships with already build templates for common things like product, product variants, filters and so on.

Reason 2 – Shapeshifting (more known as Active Commerce Skinning System)

Active Commerce ships with already built frontend (which is in fact pretty good and responsive). The bad part about this is that our clients don`t want to be mainstream and they want their own brand “look and feel”. The good part about this is that the guys at Active Commerce know that ! The Skinning System is an extension of the existing Sitecore Layout Engine, which allows the developers to utilize the existing skin and just replace the parts which need modification (including styles, html templates, scripts, images etc.). So here is a brief overview of the frontend goodies Active Commerce utilizes to make our lives easier:

  1. Cassete for asset management, bundling, minification and bundling.
  2. LESS Preprocessor for styling – which means less development work to modify the existing styles thanks to the variables and mixins less provides.
  3. jQuery-tmpl for HTML Templating – which allows quick markup changes thanks to the dynamic generation of the HTML Templates.
  4. jQuery for javascript library – nothing to say here – even a frontend newbie like me is familiar with jQuery :).
  5. Special mobile-optimized templates and behaviours (Using the Sitecore Device Layer).

Reason 3 – Easy to Configure

As already stated in Reason 1 – Active Commerce is built to work seamlessly with Sitecore, which means that it utilizes the already existing Sitecore config patching strategy we are used to. If you want to extend or just modify something Active Commerce related there are patch files which are self-descriptive (and well commented :)). Of course Active Commerce is not configurable solely by configuration files, there is a pretty robust system for site configuration inside the Content Tree. From inside the content tree your editors will be able to configure everything starting from basic contact data of the company through tax calculators, default currencies, etc.

Reason 4 – Inversion of Commerce (or Control :)) and Major Extension Points

Active Commerce is easily extendable thanks to the fact that it uses Microsoft Unity as dependency injection container. Unity makes it easier for the developers to replace existing functionality, extend the existing components and add their own custom logic on top. From data modeling standpoint – Active Commerce uses Glass Mapper as ORM which makes it even easier to extend the models and create custom types for our own data models – the only thing necessary is to inherit from the ActiveCommerce.Products.Product class.

Reason 5 – Support

Having a good support team is one of the most important things when you want your product to be successful and in my experience I can honestly say the guys in the Active Commerce Support team are amazing. I saw nothing else but fast and proper responses when I had any questions.

Happy E-Commercing ! 🙂

Language Fallback Module: Endless Fallback Problem Fix

Alex Shyba`s Language Fallback is one of the most popular modules for multi-language websites. Thanks to this module it is possible to automatically fallback to another language version of the item if there is no version found in the requested language. The module is proven to work in many Sitecore solutions.

However, there is one serious issue which may cause a lot of problems even in production environments – there is no protection against circular fallbacks. For example there is no protection for language to fallback to itself (A->A->…), or languages to have circular fallback to each other (A->B->A->…), or even more complex cases with more than 3 languages involved (A->B->C->A->…). This scenarios might bring your servers to the ground with nothing else except application pool crash and some strange error in the server error logs.

On the bright side there is a quick fix that can resolve the problem.

First we need to download the Module Source from the Sitecore Marketplace.

After that we need to open the Sitecore.LanguageFallbackItemProvider solution. The fix will take place in LanguageFallbackItemProvider class. Currently the method recursively calls itself until the item is found in the target language or until there is no fallback language defined. You can check the code below.

namespace Sitecore.Data.Managers
{
  using Globalization;

  using Items;

  /// <summary>
  ///
  /// </summary>
  public class LanguageFallbackItemProvider : ItemProvider
  {
    /// <summary>
    ///
    /// </summary>
    /// <param name="itemId"></param>
    /// <param name="language"></param>
    /// <param name="version"></param>
    /// <param name="database"></param>
    /// <returns></returns>
    protected override Item GetItem(ID itemId, Language language, Version version, Database database)
    {
      var item = base.GetItem(itemId, language, version, database);

      if (item == null)
      {
        return item;
      }

      if (item.Versions.GetVersionNumbers().Length > 0)
      {
        return item;
      }

      // there is no version in this language.
      Language fallbackLanguage = LanguageFallbackManager.GetFallbackLanguage(language, database, itemId);
      if (fallbackLanguage == null)
      {
        return item;
      }

      Item fallback = GetItem(itemId, fallbackLanguage, Version.Latest, database);

      var stubData = new ItemData(fallback.InnerData.Definition, item.Language, item.Version, fallback.InnerData.Fields);
      var stub = new StubItem(itemId, stubData, database) { OriginalLanguage = item.Language };
      stub.RuntimeSettings.SaveAll = true;

      return stub;
    }
  }
}

The idea behind the fix is to create a new method which will check if the fallback language equals the current language or if not – to keep a collection of languages which were already tested for fallback value and if the languages start to repeat – to return the item (or null). The fixed code below.



namespace Sitecore.Data.Managers
{
    using Globalization;

    using Items;

    using System.Collections.Generic;

    /// <summary>
    ///
    /// </summary>
    public class LanguageFallbackItemProvider : ItemProvider
    {
        /// <summary>
        ///
        /// </summary>
        /// <param name="itemId"></param>
        /// <param name="language"></param>
        /// <param name="version"></param>
        /// <param name="database"></param>
        /// <returns></returns>
        protected override Item GetItem(ID itemId, Language language, Version version, Database database)
        {
            return GetItem(itemId, language, version, database, new List<Language> { language });

        }

        private Item GetItem(ID itemId, Language language, Version version, Database database,
                             List<Language> fallbackedLanguages)
        {

            var item = base.GetItem(itemId, language, version, database);

            if (item == null)
            {
                return item;
            }

            if (item.Versions.GetVersionNumbers().Length > 0)
            {
                return item;
            }

            Language fallbackLanguage = LanguageFallbackManager.GetFallbackLanguage(language, database, itemId);

            // If the fallback language is null, or the fallback language matches the current language (A-A Reference), or if the language was already tested for fallback value (A-B-... Reference) - Return the item (null).
            if (fallbackLanguage == null || fallbackLanguage == language || fallbackedLanguages.Contains(fallbackLanguage))
            {
                return item;
            }

            //Add the fallback language to the collection
            fallbackedLanguages.Add(fallbackLanguage);

            Item fallback = GetItem(itemId, fallbackLanguage, Version.Latest, database, fallbackedLanguages);

            var stubData = new ItemData(fallback.InnerData.Definition, item.Language, item.Version,
                                        fallback.InnerData.Fields);
            var stub = new StubItem(itemId, stubData, database) { OriginalLanguage = item.Language };
            stub.RuntimeSettings.SaveAll = true;

            return stub;
        }

    }
}

And that is it ! Now you can sleep well knowing that even if someone messes up with the language fallbacks the server won`t go down !

Happy Fallbacking ! 🙂

SPEAK UI Filterable TreeView

While testing some Sitecore 7.5 features and reviewing the Sitecore 8 Videos it is obvious that SPEAK UI is quickly becoming the new trend for developing Sitecore Modules. Recently I stumbled upon the following SDN Thread – SPEAK UI – Filter TreeView. It turns out that the existing TreeView doesn’t support features like filtering by Template or Item ID. On the bright side – every SPEAK Control can be extended pretty easy.

NOTE: You must have Sitecore Rocks Installed To Develop SPEAK Modules (and you need to have it in general, because it is awesome !). If you don`t have Sitecore Rocks Installed, please visit this place first to find some tutorials and the extension itself.

NOTE: For the purposes of this blog post I am going to use the following technologies – Visual Studio 2013, Sitecore 7.5, Sitecore Rocks 1.2.6.0

Here are the 3 easy steps to create a Filterable TreeView.

Step 1 – Setup the Visual Studio Project

Create a new ASP.NET Web Application Project (I am going to name it TreeViewWithFilters). It should use the empty project template with folders and core references for MVC. If you are not sure on how to do this please see the screenshot bellow.

Project Setup

After the project is created delete everything from it and create the following folder structure – /sitecore/shell/client/Business Component Library/Layouts/Renderings/ListsAndGrids/TreeViews. Your project should look like the screenshot bellow.

Tree View With Filters

Set the System.Web.Mvc Reference`s Copy Local Property to False (for some strange reason it is conflicting with the Sitecore Version despite the fact it is using the same version that Sitecore uses).

From your local Sitecore Instance add references to Sitecore.Kernel, Sitecore.Mvc, Sitecore.Speak.Client, Sitecore.Speak.Mvc (I usually keep the files in a folder named “Components” inside the solution root) with Copy Local set to False.

With this the project is all set and we can start hacking !

Step 2 – Create the Custom Component

Under the TreeViews folder create a new SPEAK Component with JavaScript. Place it under /core/sitecore/client/Business Component Library/Layouts/Renderings/Lists and Grids in the Content Treer. Your Solution and your Sitecore Content Tree should now look like this:

Initial Creation

After this is set – we need to create the Rendering Parameters. As we are going to extend the existing Tree View – copy and paste the TreeView Parameters (with Rocks you can use Clipboard -> Copy (CTRL + C), Clipboard -> Paste (CTRL + V)) and rename them to TreeViewWithFilters Parameters. After this is set we are going to add a new template section called Filters. The section will have one Single-Line Text Field – ExcludeTemplatesForDisplay (Make sure the field is marked as Shared). The template should look like the screenshot bellow.

Rendering Parameters

After this is set let’s move to the code !

Step 3 – The Codezzz !

Again as we are going to extend the existing TreeView we will reuse all the code there and just do a few small modifications. The original TreeView code files are:

  • [SITECORE_WEB_ROOT\sitecore\shell\client\Business Component Library\Layouts\Renderings\ListsAndGrids\TreeViews\TreeView.cshtml
  • [SITECORE_WEB_ROOT\sitecore\shell\client\Business Component Library\Layouts\Renderings\ListsAndGrids\TreeViews\TreeView.js

TreeViewWithFilter.cshtml Changes

Our TreeViewWithFilter.cshtml should look like this:


@using Sitecore
@using Sitecore.Data.Items
@using Sitecore.Globalization
@using Sitecore.Mvc
@using Sitecore.Mvc.Presentation
@using Sitecore.Resources
@using Sitecore.Web.UI
@using Sitecore.Web.UI.Controls.Common.JqueryUiUserControls
@model RenderingModel
@{
    var userControl = this.Html.Sitecore().Controls().GetJqueryUiUserControl(this.Model.Rendering);
    userControl.Class = "sc-treeview";
    // TreeViewWithFilters Addition
    userControl.Requires.Script("business", "treeviewwithfilters.js");
    var rootItemIcon = string.Empty;
    string rootItemId = userControl.GetString("RootItem");
    string contentLanguage = userControl.GetString("ContentLanguage");
    var selectedItems = userControl.GetString("SelectedItemId");
    var preLoadPath = userControl.GetString("PreLoadPath");

    var checkedItems = userControl.GetString("CheckedItems");
    var clickFolderMode = userControl.GetInt("ClickFolderMode");
    var checkboxEnabled = userControl.GetBool("IsCheckboxEnabled");
    var showScIcons = userControl.GetBool("ShowSitecoreIcons");
    var selectMode = userControl.GetInt("SelectMode");

    Item rootItem = null;
    if (!string.IsNullOrEmpty(rootItemId))
    {
        rootItem = ClientHost.Databases.ContentDatabase.GetItem(rootItemId, Language.Parse(contentLanguage));
    }

    if (rootItem == null)
    {
        rootItem = ClientHost.Databases.ContentDatabase.GetRootItem();
    }

    rootItemIcon = Images.GetThemedImageSource(!string.IsNullOrEmpty(rootItem.Appearance.Icon) ? rootItem.Appearance.Icon : "Applications/16x16/documents.png", ImageDimension.id16x16);
    userControl.SetAttribute("data-sc-rootitem", rootItem.DisplayName + "," + rootItem.Database.Name + "," + rootItem.ID + "," + rootItemIcon);
    userControl.SetAttribute("data-sc-rootitempath", rootItem.Paths.Path);
    userControl.SetAttribute("data-sc-loadpath", preLoadPath);
    userControl.SetAttribute("data-sc-contentlanguage", contentLanguage);

    // TreeViewWithFilters Addition
    userControl.SetAttribute("data-sc-excludetemplatesfordisplay", userControl.GetString("ExcludeTemplatesForDisplay"));

    userControl.AddOptionAttribute("selectedItems");
    //userControl.AddOptionAttribute("preLoadPath");
    userControl.AddOptionAttribute("checkedItems");
    userControl.AddOptionAttribute("clickFolderMode");
    userControl.AddOptionAttribute("selectMode");
    userControl.AddBoolOptionAttribute("IsCheckboxEnabled", "checkbox");
    userControl.AddBoolOptionAttribute("ShowSitecoreIcons");
    userControl.AddBoolOptionAttribute("ShowHiddenItems", "showhiddenitems");

    var htmlAttributes = userControl.HtmlAttributes;
}
<div @htmlAttributes>
    <ul></ul>
</div>

There are 2 minor changes to the original code:

1. Changing the required script to our Javascript file:


    userControl.Requires.Script("business", "treeviewwithfilters.js");

2. Passing the ExcludeTemplatesForDisplay parameter:


 // TreeViewWithFilters Addition
 userControl.SetAttribute("data-sc-excludetemplatesfordisplay", userControl.GetString("ExcludeTemplatesForDisplay"));

TreeViewWithFilter.js Changes

Our TreeViewWithFilter.js should look like this:


/// <reference path="../../../../../../assets/lib/dist/sitecore.js" />
/// <reference path="../../../../../../assets/vendors/underscore/underscore.js" />

require.config({
    paths: {
        dynatree: "/sitecore/shell/client/Speak/Assets/lib/ui/1.1/deps/DynaTree/jquery.dynatree-1.2.4",
        dynatreecss: "/sitecore/shell/client/Speak/Assets/lib/ui/1.1/deps/DynaTree/skin-vista/ui.dynatree",
    },
    shim: {
        "dynatree": { deps: ["jqueryui"/*, 'css!dynatreecss'*/] }
    }
});

define(['sitecore', 'dynatree'], function (_sc) {
    var control = {
        componentName: "TreeView",
        selector: ".sc-treeview",
        control: "dynatree",
        namespace: "ui-",

        attributes:
        [
          { name: "selectedItemId", added: true },
          { name: "selectedItemPath", added: true },
          { name: "checkedItemIds", added: true },
          { name: "pathToLoad", added: true },
          { name: "isBusy", added: true },
          { name: "selectedNode", added: true },
          { name: "showSitecoreIcons", added: true, defaultValue: true },
          { name: "isCheckboxEnabled", pluginProperty: "checkbox", defaultValue: true },     // Show checkboxes.
          { name: "isKeyboardSupported", pluginProperty: "keyboard", defaultValue: true },     // Support keyboard navigation.
          { name: "isPersist", pluginProperty: "persist", defaultValue: false },      // Persist expand-status to a cookie
          { name: "isAutoFocus", pluginProperty: "autoFocus", defaultValue: true },    // Set focus to first child, when expanding or lazy-loading.
          { name: "isAutoCollapse", pluginProperty: "autoCollapse", defaultValue: false }, // Automatically collapse all siblings, when a node is expanded.
          { name: "clickFolderMode", defaultValue: 1 }, // 1:activate, 2:expand, 3:activate and expand
          { name: "selectMode", defaultValue: 3 },      // 1:single, 2:multi, 3:multi-hier
          { name: "isNoLink", pluginProperty: "noLink", defaultValue: false },       // Use <span> instead of <a> tags for all nodes
          { name: "debugLevel", defaultValue: 0 },       // 0:quiet, 1:normal, 2:debug
          { name: "showHiddenItems", defaultValue: false },
          { name: "contentLanguage", defaultValue: "en" },

          // TreeViewWithFilters Addition
          { name: "excludeTemplatesForDisplay", defaultValue: "" }
        ],

        events:
        [
          { name: "onPostInit" },        // Callback(isReloading, isError) when tree was (re)loaded.
          { name: "onActivate", on: "onActivate" },        // Callback(dtnode) when a node is activated.
          { name: "onDeactivate" },      // Callback(dtnode) when a node is deactivated.
          { name: "onSelect", on: "onSelect" },          // Callback(flag, dtnode) when a node is (de)selected.
          { name: "onExpand" },          // Callback(flag, dtnode) when a node is expanded/collapsed.
          { name: "onLazyRead", on: "nodeExpanding" },   // Callback(dtnode) when a lazy node is expanded for the first time.
          { name: "onCustomRender" },    // Callback(dtnode) before a node is rendered. Return a HTML string to override.
          { name: "onCreate" },          // Callback(dtnode, nodeSpan) after a node was rendered for the first time.
          { name: "onRender" },          // Callback(dtnode, nodeSpan) after a node was rendered.
          { name: "postProcess" }        // Callback(data, dataType) before an Ajax result is passed to dynatree.
        ],

        functions:
        [
          { name: "disable" },
          { name: "enable" },
          { name: "getTree" },
          { name: "getRoot" },
          { name: "getActiveNode" },
          { name: "getSelectedNodes" }
        ],

        view:
        {
            initialized: function () {
                var clickFolderModeAttribute = this.$el.attr("data-sc-option-clickfoldermode"),
                  selectModeAttribute = this.$el.attr("data-sc-option-selectmode");

                this.model.set("clickFolderMode", clickFolderModeAttribute && !isNaN(clickFolderModeAttribute) ? parseInt(clickFolderModeAttribute, 10) : 1);
                this.model.set("selectMode", selectModeAttribute && !isNaN(selectModeAttribute) ? parseInt(selectModeAttribute, 10) : 3);
                this.model.set("contentLanguage", this.$el.attr("data-sc-contentlanguage"));
                this.model.set("showHiddenItems", this.$el.attr("data-sc-option-showhiddenitems") !== undefined && this.$el.attr("data-sc-option-showhiddenitems").toLowerCase() === "true");

                // TreeViewWithFilters Addition
                this.model.set("excludeTemplatesForDisplay", this.$el.attr("data-sc-excludetemplatesfordisplay"));

                var items = this.$el.attr("data-sc-rootitem");
                if (!items) {
                    return;
                }

                var pathToLoad = this.$el.attr("data-sc-loadpath");

                if (pathToLoad) {
                    this.model.set("pathToLoad", pathToLoad);
                }

                var parts = items.split("|");
                var selectedItemUri = null;

                for (var n = 0; n < parts.length; n++) {
                    var uri = parts[n].split(',');

                    var databaseUri = new _sc.Definitions.Data.DatabaseUri(uri[1]);
                    var itemUri = new _sc.Definitions.Data.ItemUri(databaseUri, uri[2]);
                    var itemIcon = uri[3];

                    var rootNode = {
                        title: uri[0],
                        key: itemUri.getItemId(),
                        isLazy: true,
                        isFolder: true,
                        icon: this.model.get("showSitecoreIcons") ? itemIcon : "",
                        url: "#",
                        path: "/" + uri[0],
                        itemUri: itemUri,
                        selected: selectedItemUri == null
                    };

                    if (this.widget) {
                        this.widget.apply(this.$el, ["getRoot"]).addChild(rootNode);
                    }

                    if (this.model.get("pathToLoad") && this.model.get("pathToLoad") !== "") {
                        this.loadKeyPath();

                    }
                }

                this.pendingRequests = 0;
            },

            onActivate: function (node) {
                if (node && node.data && node.data.itemUri && node.data.path) {

                    this.model.set("selectedNode", node.data);
                    this.model.set("selectedItemId", node.data.itemUri.itemId);
                    this.model.set("selectedItemPath", node.data.path);
                }
            },

            onSelect: function (flag, node) {
                var list = [];

                _.each(node.tree.getSelectedNodes(), function (dnode, index) {
                    list.push(dnode.data.itemUri.itemId);
                });
                this.model.set("checkedItemIds", list.join("|"));
            },

            nodeExpanding: function (node) {

                var itemUri = node.data.itemUri, children;

                this.pendingRequests++;
                this.model.set("isBusy", true);

                var self = this;

                // TreeViewWithFilters Addition
                var excludedTemplatesList = new Array();
                if (this.model.get("excludeTemplatesForDisplay") !== undefined && this.model.get("excludeTemplatesForDisplay") != "") {
                    excludedTemplatesList = this.model.get("excludeTemplatesForDisplay").split("|");
                }

                var database = new _sc.Definitions.Data.Database(itemUri.getDatabaseUri());
                database.getChildren(itemUri.getItemId(), function (items) {
                    var res = [],
                      filteredItems = _.filter(items, function (item) {

                          // TreeViewWithFilters Modification

                          if (self.model.get("showHiddenItems")) {
                              return true && $.inArray(item.$templateId, excludedTemplatesList) == -1;
                          }

                          return item.__Hidden !== "1" && $.inArray(item.$templateId, excludedTemplatesList) == -1;
                      });

                    self.appendLoadedChildren(node, filteredItems, res);

                    self.pendingRequests--;
                    if (self.pendingRequests <= 0) {
                        self.pendingRequests = 0;
                        self.model.set("isBusy", false);
                    }

                    if (self.model.get("pathToLoad") && self.model.get("pathToLoad").length > 0) {
                        self.loadKeyPath();
                    }
                }, {
                    fields: ["__Hidden"],
                    language: self.model.get("contentLanguage")
                });

            },

            appendLanguageParameter: function (item) {
                if (item.$icon.indexOf(".ashx") > 0) {
                    item.$icon += "&la=" + this.model.get("contentLanguage");
                    item.$mediaurl += "&la=" + this.model.get("contentLanguage");
                }
            },

            appendLoadedChildren: function (parentNode, childrenNodes, destArray) {
                //add children

                var self = this;
                _.each(childrenNodes, function (item) {
                    var newNode = {};
                    newNode.rawItem = item;
                    newNode.title = item.$displayName;
                    newNode.key = item.itemId;
                    if (self.model.get("showSitecoreIcons")) {
                        self.appendLanguageParameter(item);
                        newNode.icon = item.$icon;
                    }
                    newNode.itemUri = item.itemUri;
                    newNode.path = item.$path;
                    newNode.select = self.model.get("selectMode") === 3 ? parentNode.isSelected() : false;
                    newNode.isFolder = item.$hasChildren;
                    newNode.isLazy = item.$hasChildren;
                    destArray.push(newNode);

                }, this);

                parentNode.setLazyNodeStatus(DTNodeStatus_Ok);
                parentNode.addChild(destArray);
                //expand needed node
            },
            loadKeyPath: function () {
                var separator = "/",
                    pathParts,
                    currentNodeId,
                    path = this.model.get("pathToLoad"),
                    tree = this.widget.apply(this.$el, ["getTree"]),
                    node;

                pathParts = path.split(separator);
                if (pathParts.length === 0) {
                    return false;
                }
                else {
                    currentNodeId = pathParts.shift();
                    if (!currentNodeId) return false;
                    node = tree.getNodeByKey(currentNodeId);
                    if (!node) {
                        this.model.set("pathToLoad", "");

                        return false;
                    }

                    this.model.set("pathToLoad", pathParts.join(separator));
                    node.expand();

                    if (pathParts.length === 0) {
                        this.model.set("selectedItemId", currentNodeId);
                        node.activate(true);
                        node.select(true);
                    }

                }
            }
        }
    };

    _sc.Factories.createJQueryUIComponent(_sc.Definitions.Models, _sc.Definitions.Views, control);
});

The method is modified to do the following.

1. Register our new attribute with the model.

   // TreeViewWithFilters Addition
          { name: "excludeTemplatesForDisplay", defaultValue: "" }

2. Load the value we passed in the rendering

         // TreeViewWithFilters Addition
                this.model.set("excludeTemplatesForDisplay", this.$el.attr("data-sc-excludetemplatesfordisplay"));

3. Override the nodeExpanding method in order for it to filter based on our custom filter.

        // TreeViewWithFilters Addition
                var excludedTemplatesList = new Array();
                if (this.model.get("excludeTemplatesForDisplay") !== undefined && this.model.get("excludeTemplatesForDisplay") != "") {
                    excludedTemplatesList = this.model.get("excludeTemplatesForDisplay").split("|");
                }

                var database = new _sc.Definitions.Data.Database(itemUri.getDatabaseUri());
                database.getChildren(itemUri.getItemId(), function (items) {
                    var res = [],
                      filteredItems = _.filter(items, function (item) {

                          // TreeViewWithFilters Modification

                          if (self.model.get("showHiddenItems")) {
                              return true && $.inArray(item.$templateId, excludedTemplatesList) == -1;
                          }

                          return item.__Hidden !== "1" && $.inArray(item.$templateId, excludedTemplatesList) == -1;
                      });

And this is it !

The component can be found in the standard Speak Rendering selector as seen on the screenshot bellowTree View With Filters

After the component is included in the layout the custom field can be edited from the component properties.

Tree View With Filters Properties

Currently the code supports only TemplateIDs, but it can be modified to support template keys etc.

The code is also pushed to Bitbucket along with some extra filters – ExcludeItemsForDisplay and IncludeTemplatesForDisplay. Adding new filters is just a matter of modifying the Parameter templates, handling them in the rendering and modifying the nodeExpanding method in order for it to support your custom type of filter.

Happy Speaking !

%d bloggers like this: