3 December 2014

Sitecore 7.x MVC and placeholder key with Capital Letter

Another post today about Sitecore MVC and Placeholder Key. We have this project using Sitecore MVC where the placeholders are defined with capital letters, Eventhough the Page Editor recommendation guide (page 14 to 15) does not say that using Capital Letters will create issues in MVC.... well.... it does. So even if not said directly on the guide: use lower case for your placeholder name... But just in case it is too late and you need to page editor, you can read the following...

In MVC, adding a placeholder in the renderings with capital letters will not create any problem when binding any rendering to this placeholder. However, Sitecore will not resolve your Placeholder settings correctly and you will not see the "Add Here" buttons neither the "allowed controls"

To summarise, if you do something like:

        @Html.Sitecore().Placeholder("SideColumn")

You will end up withthe SideColumn placeholder not even displayed, even if you add capital letters in the Placeholder Settings:



That would not really bother me as I would need to rename all placeholder into lower case and voila!!! But I wanted to find out if there would be an easier way and something that would allow me to have capital letter in the Name... When trying to see where this is falling, you have to check how the Chrome Data are rendered. If you look at the pipeline "GetChromeData", you can see that there is a processor for Placeholder:

      < getChromeData>
        < processor type="Sitecore.Pipelines.GetChromeData.Setup, Sitecore.Kernel" />
        < processor type="Sitecore.Pipelines.GetChromeData.GetFieldChromeData, Sitecore.Kernel" />
        < processor type="Sitecore.Pipelines.GetChromeData.GetWordFieldChromeData, Sitecore.Kernel" />
        < processor type="Sitecore.Pipelines.GetChromeData.GetRenderingChromeData, Sitecore.Kernel" />
        < processor type="Sitecore.Pipelines.GetChromeData.GetEditFrameChromeData, Sitecore.Kernel" />
        < processor type="Sitecore.Pipelines.GetChromeData.GetPlaceholderChromeData, Sitecore.Kernel" />
      < /getChromeData>


When following the trail from the GetPlaceholderChromeData you will find that the code is calling another pipeline "GetPlaceHolderRenderings"
                        GetPlaceholderRenderingsArgs getPlaceholderRenderingsArgs = new GetPlaceholderRenderingsArgs(text, layout, args.Item.Database)
                        {
                            OmitNonEditableRenderings = true
                        };
                        CorePipeline.Run("getPlaceholderRenderings", getPlaceholderRenderingsArgs);

with the pipeline looking like:
      < getPlaceholderRenderings>
        < processor type="Sitecore.Pipelines.GetPlaceholderRenderings.GetAllowedRenderings, Sitecore.Kernel" />
        < processor type="Sitecore.Pipelines.GetPlaceholderRenderings.GetPredefinedRenderings, Sitecore.Kernel" />
        < processor type="Sitecore.Pipelines.GetPlaceholderRenderings.RemoveNonEditableRenderings, Sitecore.Kernel" />
        < processor type="Sitecore.Pipelines.GetPlaceholderRenderings.GetPlaceholderRenderingsDialogUrl, Sitecore.Kernel" />
      < /getPlaceholderRenderings>



Continuing the trail... go to the first processor: GetAllowedRenderings. In there you will find out how the PlaceholderItem is getting retrieved
    using (new DeviceSwitcher(args.DeviceId, args.ContentDatabase))
    {
     item = Client.Page.GetPlaceholderItem(args.PlaceholderKey, args.ContentDatabase, args.LayoutDefinition);
    }


If you look inside this method you will see that those placeholder Item are retrieved from the cache:
 PlaceholderCache placeholderCache = PlaceholderCacheManager.GetPlaceholderCache(database.Name);
 Item item = placeholderCache[placeholderKey];
 if (item != null)
 {
  return item;
 }
 int num = placeholderKey.LastIndexOf('/');
 if (num >= 0)
 {
  string key = StringUtil.Mid(placeholderKey, num + 1);
  item = placeholderCache[key];
 }
 return item;

You will note that the placeholderCache.IsKeyCaseSensitive is false, and this return null if you are passing the key as capital letters... So if we look at how the cache is build:
public virtual void Reload()
{
 lock (FieldRelatedItemCache.Lock)
 {
  this.itemIDs = new SafeDictionary();
  string query = string.Format("{0}//*[@@templateid = '{1}']", this.ItemRoot.Paths.FullPath, this.ItemTemplate);
  Item[] array = this.Database.SelectItems(query);
  if (array != null)
  {
   Item[] array2 = array;
   for (int i = 0; i < array2.Length; i++)
   {
    Item item = array2[i];
    string text = item[this.FieldKey];
    if (!string.IsNullOrEmpty(text))
    {
     string cacheKey = this.GetCacheKey(text);
     if (!this.itemIDs.ContainsKey(cacheKey))
     {
      this.Add(text, item);
     }
    }
   }
  }
 }
}

with the method GetCacheKey where you can see that if the IsKeyCaseSensitive is false the cache key will be lower case...
protected virtual string GetCacheKey(string key)
{
 if (this.IsKeyCaseSensitive || string.IsNullOrEmpty(key))
 {
  return key;
 }
 return key.ToLowerInvariant();
}


Also inside the Add method there is also a call to the GetCacheKey... So
 string cacheKey = this.GetCacheKey(key);
 lock (FieldRelatedItemCache.Lock)
 {
  this.itemIDs[cacheKey] = item.ID;
 }

That means the key is stored as capital Letter if you have define your field in the placeholder settings as capital letter. So what happens where we try to retrieve the item from the cache is that we are passing the Key as Capital letter but it is stored as lower case...
public virtual Item this[string key]
{
 get
 {
  Item result;
  lock (FieldRelatedItemCache.Lock)
  {
   ID itemId;
   if (!this.itemIDs.TryGetValue(key, out itemId))
   {
    result = null;
   }
   else
   {
    Item item = this.Database.GetItem(itemId, Context.Language, Version.Latest);
    if (item == null)
    {
     this.itemIDs.Remove(key);
    }
    result = item;
   }
  }
  return result;
 }
}


So I went around this issue by adding a custom pipeline action on the GetChromeData pipeline to convert the Placeholder Key to Lower Invariant if the Key for the cache manager is not sensitive...
namespace MyProject.Business.Sitecore.Pipelines
{
    public class GetPlaceholderKeyForChromeData : GetPlaceholderChromeData
    {
        public override void Process(GetChromeDataArgs args)
        {
            if ("placeholder".Equals(args.ChromeType, StringComparison.OrdinalIgnoreCase))
            {
                PlaceholderCache placeholderCache = PlaceholderCacheManager.GetPlaceholderCache(args.Item.Database.Name);
                if (!placeholderCache.IsKeyCaseSensitive)
                {
                    string keyString = args.CustomData["placeHolderKey"] as string;
                    args.CustomData["placeHolderKey"] = keyString.ToLowerInvariant();
                }
            }
        }
    }
}

With the following config patch
      
< getchromedata>
        < processor patch:before="processor[@type='Sitecore.Pipelines.GetChromeData.GetPlaceholderChromeData, Sitecore.Kernel']" type="MyProject.Business.Sitecore.Pipelines.GetPlaceholderKeyForChromeData, MyProject.Business">
      < /processor>
< /getchromedata>


No comments:

Post a Comment