Archive for category Rendering

Sitecore Image Lazy Load Module

The package and code for this module can be found at: Github – aceanindita

The bLazy.js plugin – A lazyload image script can be used to enable lazyload on your site, to reduce initial page load times drastically, especially if your site is image heavy.

While you can directly integrate your site with the bLazy plugin, you will need to ensure that your images are not loaded on page load, for which you will need to transform your html accordingly. This can also bring with it experience editor compatibility concerns.

This module will make all your images rendered from Sitecore Image and Rich Text fields, lazy loaded automatically without you having to make ANY html updates at all.

For this, we are usign the following config which does 2 things

  • Processor added to mvc.renderRendering – to include and initialize the bLazy plugin
  • Processor added to renderField – to transform html for all images rendered from the Sitecore Image field and Rich Text field, to move the src url to a different attribute recognized by the plugin, and also add a selector to tell the plugin this html needs to be transformed to lazy load the image.
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <mvc.renderRendering>
        <processor type="ImageLazyLoadModule.Pipelines.MVC.InsertbLazyInit, ImageLazyLoadModule"             patch:before="processor[@type='Sitecore.Mvc.Pipelines.Response.RenderRendering.ExecuteRenderer, Sitecore.Mvc']" />
      </mvc.renderRendering>
      <renderField>
        <processor type="ImageLazyLoadModule.Pipelines.RenderField.ImageLazyLoad, ImageLazyLoadModule" />
      </renderField>
    </pipelines>
    <settings>
      <setting name="ImageLazyLoadModule.Offset" value="200" />
      <setting name="ImageLazyLoadModule.Selector" value="b-lazy" />
    </settings>
  </sitecore>
</configuration>

You can see 2 configurable settings above, which are used when the bLazy plugin is initialized.

  • The offset controls how early you want the elements to be loaded before they’re visible.
  • The selector is the class that will be added to the img tags dynamically, which will be used by the plugin to lazy load images.

The code for mvc.renderRendering for including the script and plugin initialization

using Sitecore.Configuration;
using Sitecore.Mvc.Pipelines.Response.RenderRendering;
using Sitecore.Mvc.Presentation;

namespace ImageLazyLoadModule.Pipelines.MVC
{
    public class InsertbLazyInit : RenderRenderingProcessor
    {
        public override void Process(RenderRenderingArgs args)
        {
            Renderer renderer = args.Rendering.Renderer;
            if (renderer == null)
                return;

            bool isLayout = renderer is ViewRenderer &&
                               ((ViewRenderer)renderer).Rendering.RenderingType == "Layout";

            if (isLayout)
            {
                args.Writer.Write("<script src=\"../../sitecore modules/Lazy Load/blazy.min.js\"></script>"
                                  + "<script>"
                                  + "var bLazy = new Blazy({ selector: '."
                                  + Settings.GetSetting("ImageLazyLoadModule.Selector", "b-lazy")
                                  + "', offset: " 
                                  + Settings.GetIntSetting("ImageLazyLoadModule.Offset", 200)
                                  + ", src: 'responsive-src'});"
                                  + "</script>");
            }
        }
    }
}

The code for renderField which will transform img tag html

using HtmlAgilityPack;
using Sitecore;
using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.RenderField;
using System;

namespace ImageLazyLoadModule.Pipelines.RenderField
{
    public class ImageLazyLoad
    {
        public void Process(RenderFieldArgs args)
        {
            try
            {
                if (args == null)
                    return;

                // Trigger the code to transform the img tags only for rich text and image fields
                if (!(args.FieldTypeKey != "rich text" || args.FieldTypeKey != "image") || string.IsNullOrEmpty(args.FieldValue) ||
                     !Context.PageMode.IsNormal)
                    return;

                if (!string.IsNullOrWhiteSpace(args.Result?.FirstPart))
                {
                    HtmlDocument doc = new HtmlDocument { OptionWriteEmptyNodes = true };
                    doc.LoadHtml(args.Result.FirstPart);

                    if (doc.DocumentNode != null)
                    {
                        // Search for all img tags
                        HtmlNodeCollection imgTag = doc.DocumentNode.SelectNodes("//img");
                        if (imgTag == null || imgTag.Count == 0)
                            return;

                        foreach (HtmlNode node in imgTag)
                        {
                            if (node.Attributes["src"] != null && node.ParentNode != null)
                            {
                                string imgUrl = node.Attributes["src"].Value;
                                node.Attributes.Add("responsive-src", imgUrl);
                                node.Attributes["src"].Value = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
                                string currentClass = node.Attributes["class"] != null ? node.Attributes["class"].Value : "";
                                node.Attributes.Remove("class");
                                node.Attributes.Add("class", (string.IsNullOrWhiteSpace(currentClass) ? "" : currentClass + " ")
                                    + Settings.GetSetting("ImageLazyLoadModule.Selector", "b-lazy"));
                                node.Attributes.Remove("width");
                                node.Attributes.Remove("height");
                            }
                        }

                        args.Result.FirstPart = doc.DocumentNode.OuterHtml;
                    }
                }
            }
            catch (Exception ex)
            {
                Log.Error("Error in ImageLazyLoadModule.ImageLazyLoad:" + ex.Message, ex);
            }
        }
    }
}

You can also choose to add image transitions using css as is specified in the bLazy Documentation

.b-lazy {
-webkit-transition: opacity 500ms ease-in-out;
-moz-transition: opacity 500ms ease-in-out;
-o-transition: opacity 500ms ease-in-out;
transition: opacity 500ms ease-in-out;
max-width: 100%;
opacity: 0;
}
.b-lazy.b-loaded {
opacity: 1;
}

Here’s a very simple example of this module in use (Rich text field example)

We have a simple content item, where an image has been embedded in the rich text field from the media library

The view rendering code, simply outputs the rich text field using the Sitecore renderer (observe there are no additional classes added either)

Now for the output! When you load up the page with this ViewRendering, you see the following

Observe that the selector ‘b-lazy’ was added dynamically by our RenderField customization, and it was further picked up by the bLazy plugin and transformed successfully, evident by the ‘b-loaded’ class.

Now to see the html loaded on page load

Here you can see that the bLazy script and its initialization was added dynamically by our mvc.renderRendering pipeline update, and the img tag on page load, has a different src! This src is the base64 code of a pixel. The actual image source has been moved to a different attribute (responsive-src) which is recognized by the plugin (supplied during plugin initialization)

Hence proved.

The package and code for this module can be found at: Github – aceanindita

, , , , , , ,

Leave a comment

Sitecore RenderingsMarkerModule

Note: This module has been created as a package and is available for download along with the complete source code at Git: aceanindita

Sitecore being component based as it is, adding start and end comments to your views can be immensely helpful while debugging issues, especially if they are being worked on someone who doesn’t know the project from Adam.

Hardcoding the start / end comments isn’t a great idea for obvious reasons like being prone to developer omission in some views, refactoring / file renaming concerns etc.

In an earlier post I had shown how we had partially resolved this by making the build up of the comment dynamic using a html helper method which used the WebPageExecutingBase.VirtualPath property from the System.Web.WebPages assembly.
You can refer the post here: Using dynamic view markers in Sitecore MVC

In this method, you would need to manually wrap each of your views within a call to the said html helper method, which would dynamically include the current view path in start and end comments around your view. While this did away with the potential issues that might come out of refactoring like changing folder structures / renaming files, it still didn’t deal with the dependency on having the developer(s) remember to add the call to this html helper in every single view.

Here’s a solution for this, we added a new processor in the mvc.rendering pipeline, before and after the call of Sitecore.Mvc.Pipelines.Response.RenderRendering.ExecuteRenderer which renders the actual rendering html.

Here’s the config update made for this (patch):

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <mvc.renderRendering>
        <processor type="RenderingsMarkerModule.Pipelines.MVC.RenderingMarker.RenderMarkerStart, RenderingsMarkerModule"
            patch:before="processor[@type='Sitecore.Mvc.Pipelines.Response.RenderRendering.ExecuteRenderer, Sitecore.Mvc']" />
        <processor type="RenderingsMarkerModule.Pipelines.MVC.RenderingMarker.RenderMarkerEnd, RenderingsMarkerModule"
            patch:after="processor[@type='Sitecore.Mvc.Pipelines.Response.RenderRendering.ExecuteRenderer, Sitecore.Mvc']" />
      </mvc.renderRendering>
    </pipelines>
    <settings>
      <setting name="RenderingsMarkerModule.Enabled" value="true" />
      <setting name="RenderingsMarkerModule.ShowLayoutMarkers" value="false" />
    </settings>
  </sitecore>
</configuration>

Note we have 2 additional settings here. ‘RenderingsMarkerModule.Enabled’ lets you toggle the rendering of comments on and off – this could help you turn of this rather revealing feature on staging / production. Additionally you could toggle ‘RenderingsMarkerModule.ShowLayoutMarkers’ to show / hide comments before & after your layout. It is recommended that this remain off, since having your html response start with a html comment might not always be favorable.

Here’s the code we have in place:
RenderingsMarkerModule.Pipelines.MVC.RenderingMarker.RenderMarkerStart

using Sitecore.Configuration;
using Sitecore.Mvc.Pipelines.Response.RenderRendering;
using Sitecore.Mvc.Presentation;

namespace RenderingsMarkerModule.Pipelines.MVC.RenderingMarker
{
    public class RenderMarkerStart : RenderRenderingProcessor
    {
        public override void Process(RenderRenderingArgs args)
        {
            if (Settings.GetBoolSetting("RenderingsMarkerModule.Enabled", false))
            {
                Renderer renderer = args.Rendering.Renderer;
                if (renderer == null)
                    return;

                bool isLayout = renderer is ViewRenderer &&
                                ((ViewRenderer) renderer).Rendering.RenderingType == "Layout";
                bool showLayoutMarkers = Settings.GetBoolSetting("RenderingsMarkerModule.ShowLayoutMarkers", false);

                if (isLayout && !showLayoutMarkers) return;

                args.Writer.Write("\n<!-- START: " + renderer + " -->\n");
            }
        }
    }
}

RenderingsMarkerModule.Pipelines.MVC.RenderingMarker.RenderMarkerEnd

using Sitecore.Configuration;
using Sitecore.Mvc.Pipelines.Response.RenderRendering;
using Sitecore.Mvc.Presentation;

namespace RenderingsMarkerModule.Pipelines.MVC.RenderingMarker
{
    public class RenderMarkerEnd : RenderRenderingProcessor
    {
        public override void Process(RenderRenderingArgs args)
        {
            if (Settings.GetBoolSetting("RenderingsMarkerModule.Enabled", false))
            {
                Renderer renderer = args.Rendering.Renderer;
                if (renderer == null)
                    return;

                bool isLayout = renderer is ViewRenderer &&
                                ((ViewRenderer) renderer).Rendering.RenderingType == "Layout";
                bool showLayoutMarkers = Settings.GetBoolSetting("RenderingsMarkerModule.ShowLayoutMarkers", false);

                if (isLayout && !showLayoutMarkers) return;

                args.Writer.Write("\n<!-- END: " + renderer + " -->\n");
            }
        }
    }
}

Here’s a sample of the output

This module has been created as a package and is available for download along with the complete source code at Git: aceanindita

, , , , , , , , , , , , , , , , , ,

Leave a comment

Glass html helpers for responsive images in Sitecore with bLazy

The bLazy Plugin provides us with a great way to implement serving up resized images based on the current viewport, and additionally it also allows us to lazy load images on our site, drastically reducing the page size and download time.

We used bLazy for img tags – and also for other tags (where it simply loads up the image as a background image).

We created a few glass HTML helpers here to help us maintain consistency and increase maintainability by having this html present in only 1 place in the site.

        public HtmlString RenderResponsiveLazyImage(Expression<Func<TModel, object>> field,
            Dimensions mobileDimensions, Dimensions tabletDimensions, Dimensions desktopDimensions, 
            bool isEditable = true, NameValueCollection parameters = null)
        {
            return RenderResponsiveLazyImage(Model, field, mobileDimensions, tabletDimensions, desktopDimensions, isEditable, parameters);
        }

        public HtmlString RenderResponsiveLazyImage<T>(T model, Expression<Func<T, object>> field, 
            Dimensions mobileDimensions, Dimensions tabletDimensions, Dimensions desktopDimensions,
            bool isEditable = true, NameValueCollection parameters = null)
        {
            Image imageField = field.Compile().Invoke(model) as Image;

            if (imageField != null)
            {
                StringBuilder responsiveImageHtml = new StringBuilder();
                StringBuilder parametersHtml = new StringBuilder();
                if (parameters != null)
                {
                    var items = parameters.AllKeys.SelectMany(parameters.GetValues, (k, v) => new { key = k, value = v });
                    foreach (var item in items)
                    {
                        parametersHtml.Append(" " + item.key + "=\"" + item.value + "\"");
                    }
                }

                if (Sitecore.Context.PageMode.IsPageEditor)
                {
                    responsiveImageHtml.Append(GlassHtml.RenderImage(model, field, null, isEditable));
                }
                else if (!Sitecore.Context.PageMode.IsPageEditor && imageField.IsValid())
                {
                    responsiveImageHtml.Append("<img" + (parametersHtml.Length == 0 ? "" : parametersHtml.ToString())
                        + " src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\""
                        + " data-lazily-respond-mobile=\""
                        + ImageHelper.GetResizedSitecoreUrl(imageField.Src, 
                            mobileDimensions != null ? mobileDimensions.Width : 0, mobileDimensions != null ? mobileDimensions.Height : 0) + "\""
                        + " data-lazily-respond-tablet=\""
                        + ImageHelper.GetResizedSitecoreUrl(imageField.Src, 
                            tabletDimensions != null ? tabletDimensions.Width : 0, tabletDimensions != null ? tabletDimensions.Height : 0) + "\""
                        + " data-lazily-respond-desktop=\""
                        + ImageHelper.GetResizedSitecoreUrl(imageField.Src, 
                            desktopDimensions != null ? desktopDimensions.Width : 0, desktopDimensions != null ? desktopDimensions.Height : 0) + "\""
                        + " alt=\"" + imageField.Alt + "\" />");
                }

                return new HtmlString(responsiveImageHtml.ToString());
            }

            return new HtmlString(string.Empty);
        }

        public string RenderResponsiveLazyImageAttributes(Expression<Func<TModel, object>> field,
            Dimensions mobileDimensions, Dimensions tabletDimensions, Dimensions desktopDimensions)
        {
            return RenderResponsiveLazyImageAttributes(Model, field, mobileDimensions, tabletDimensions, desktopDimensions);
        }

        public string RenderResponsiveLazyImageAttributes<T>(T model, Expression<Func<T, object>> field,
            Dimensions mobileDimensions, Dimensions tabletDimensions, Dimensions desktopDimensions)
        {
            Image imageField = field.Compile().Invoke(model) as Image;

            if (imageField != null && imageField.IsValid())
            {
                StringBuilder responsiveLazyImageHtml = new StringBuilder();

                if (mobileDimensions != null)
                {
                    responsiveLazyImageHtml.Append("data-lazily-respond-mobile=\""
                        + ImageHelper.GetResizedSitecoreUrl(imageField.Src, mobileDimensions.Width, mobileDimensions.Height) + "\"");
                }

                if (tabletDimensions != null)
                {
                    responsiveLazyImageHtml.Append(" data-lazily-respond-tablet=\""
                        + ImageHelper.GetResizedSitecoreUrl(imageField.Src, tabletDimensions.Width, tabletDimensions.Height) + "\"");
                }

                if (desktopDimensions != null)
                {
                    responsiveLazyImageHtml.Append(" data-lazily-respond-desktop=\""
                        + ImageHelper.GetResizedSitecoreUrl(imageField.Src, desktopDimensions.Width, desktopDimensions.Height) + "\"");
                }

                return responsiveLazyImageHtml.ToString();
            }
            return string.Empty;
        }

        public static string GetResizedSitecoreUrl(string imageUrl, int width, int height, bool centerCrop = true)
        {
            if (string.IsNullOrWhiteSpace(imageUrl)) return string.Empty;

            if (height > 0)
                imageUrl = imageUrl + (imageUrl.Contains('?') ? "&h=" : "?h=") + height;
            if (width > 0)
                imageUrl = imageUrl + (imageUrl.Contains('?') ? "&w=" : "?w=") + width;
            if (centerCrop)
                imageUrl = imageUrl + (imageUrl.Contains('?') ? "&" : "?") + "usecustomfunctions=1&centercrop=1";

            return imageUrl;
        }

We used Sitecore Image Processor Module to be able to resize sitecore images here.

Where Dimensions is:

    public class Dimensions
    {
        public int ScreenSize { get; set; }
        public int Width { get; set; }
        public int Height { get; set; }
        public bool IsUrlBlank { get; set; }

        public Dimensions(int width, int height)
        {
            Width = width;
            Height = height;
        }

        public Dimensions()
        { }
    }

, , , , , , ,

Leave a comment

Resize and lazy load images in Rich Text fields in Sitecore

Often the imagery that is uploaded in Sitecore is of way higher resolution than what is required, in an attempt to produce high quality pages.
While these are uploaded onto image fields in sitecore, we can resize the images when we output them using suitable html helpers. Refer:Render Responsive Image Glass Html Helper with Sitecore Image Processor Module.

We also needed a way to achieve this for images inserted in the rich text field. Especially seeing as we had content authors adding images sized 3000×3000 in there, and page sizes soaring up to 80mb each at times!
We implemented a solution with the bLazy plugin.
The plugin documentation will tell you, that once we include the script and initialize it with the right parameters, all it takes is adding the right attributes and class into our html, to enable lazy load of images – resized if need be for each breakpoint as we determine.

Here’s what we implemented for this:

  • We added a patch to the renderField pipeline, which would transform all img tags as we needed – add the required class which is to be used as the selector for the blazy plugin, and also the breakpoint specific resized image url attributes. In addition, we also replaced the img tag source with a base64 encoded transparent gif so it wouldn’t do any extra requests.
  • We added a reference to the bLazy plugin, and initialized it with the parameters suitable to our requirements.

The pipeline code:

using HtmlAgilityPack;
using sbdshared._Classes.SBDShared.Helpers;
using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.RenderField;
using System;
using System.Web;

namespace MySite.Customizations.Customized_Sitecore
{
    public class ProcessRichTextImages
    {
        /// <summary>
        /// Processes the specified arguments.
        /// </summary>
        /// <param name="args">The arguments.</param>
        public void Process(RenderFieldArgs args)
        {
            try
            {
                if (args != null && (args.FieldTypeKey != "rich text" || string.IsNullOrEmpty(args.FieldValue) || !Context.PageMode.IsNormal))
                    return;

                if (args != null && args.Result != null && !string.IsNullOrWhiteSpace(args.Result.FirstPart))
                {
                    HtmlDocument doc = new HtmlDocument { OptionWriteEmptyNodes = true };
                    doc.LoadHtml(args.Result.FirstPart);

                    if (doc.DocumentNode != null)
                    {
                        // Search for all img tags
                        HtmlNodeCollection imgTag = doc.DocumentNode.SelectNodes("//img");
                        if (imgTag == null || imgTag.Count == 0)
                            return;

                        foreach (HtmlNode node in imgTag)
                        {
                            if (node.Attributes["src"] != null && node.ParentNode != null)
                            {
                                string imgUrl = node.Attributes["src"].Value;

                                node.Attributes.Add("data-responsive-bkg-desktop", ImageHelper.GetResizedExternalUrl(imgUrl, 580, 0));
                                node.Attributes.Add("data-responsive-bkg-tablet", ImageHelper.GetResizedExternalUrl(imgUrl, 350, 0));
                                node.Attributes.Add("data-responsive-bkg-mobile", ImageHelper.GetResizedExternalUrl(imgUrl, 200, 0));
                                node.Attributes["src"].Value = "/_Images/loader.gif";
                                string currentClass = node.Attributes["class"] != null ? node.Attributes["class"].Value : "";
                                node.Attributes.Remove("class");
                                node.Attributes.Add("class", (string.IsNullOrWhiteSpace(currentClass) ? "" : currentClass + " ") + "responsive-bkg");
                                node.Attributes.Remove("width");
                                node.Attributes.Remove("height");
                                if (node.Attributes["style"] != null && !string.IsNullOrWhiteSpace(node.Attributes["style"].Value)
                                    && (node.Attributes["style"].Value.Contains("width") || node.Attributes["style"].Value.Contains("height")))
                                {
                                    node.Attributes.Remove("style");
                                }
                            }
                        }

                        // Replace the Rich Text content with the modified content
                        args.Result.FirstPart = doc.DocumentNode.OuterHtml;
                    }
                }
            }
            catch (Exception ex)
            { Log.Error("Error in MySite.Customizations.Customized_Sitecore.ProcessRichTextImages:" + ex.Message, ex); }
        }
    }
}

We used the Sitecore Image Processor Module to get the resized sitecore images.

        public static string GetResizedSitecoreUrl(string imageUrl, int width, int height, bool centerCrop = true)
        {
            if (string.IsNullOrWhiteSpace(imageUrl)) return string.Empty;

            if (height > 0)
                imageUrl = imageUrl + (imageUrl.Contains('?') ? "&h=" : "?h=") + height;
            if (width > 0)
                imageUrl = imageUrl + (imageUrl.Contains('?') ? "&w=" : "?w=") + width;
            if (centerCrop)
                imageUrl = imageUrl + (imageUrl.Contains('?') ? "&" : "?") + "usecustomfunctions=1&centercrop=1";

            return imageUrl;
        }

Additionally – we also removed any height / width attributes added to the images in the rich text field – since the design in our case allowed for it.
We added this to the renderField processor:

<configuration>
  <sitecore>
    <pipelines>
      <renderField>
        <processor type="MySite.Customizations.Customized_Sitecore.ProcessRichTextImages, MySite" />
      </renderField>
    </pipelines>
  </sitecore>
</configuration>

Now that we had the html the way we required it for the blazy plugin, all that was needed was to include the script and initialize it:

<script src="blazy.js"></script>

And the following needed to go into the $(document).Ready() method.

var bLazy = new Blazy({
	selector: '.responsive-bkg',
    offset: 200,
    breakpoints: [{
			width: 768,
			src: 'data-responsive-bkg-mobile'
		},
		{
		      width: 980
		    , src: 'data-responsive-bkg-tablet'
		}],
	src: 'data-responsive-bkg-desktop'
});

The images would now load in a lazy-load fashion based on the current viewport breakpoint, when the cursor was within 200px of the image iteself. (Refer bLazy Documentation)

, , , , , , , , , ,

Leave a comment

Adding query support to datasources on renderings for a Sitecore multisite instance

Sitecore 8, I believe has inherent support for sitecore queries in the ‘Datasource Location’ field of renderings!
But for those of us waiting on client approvals to make the move from Sitecore 7.xx :(, here’s the way to add that support!
This will come in very handy especially while working with shared renderings to be used across multiple sites in your Sitecore instance.

2015-11-01_042131

We added the following code to the getRenderingDatasource pipeline:

using Sitecore.Diagnostics;
using Sitecore.Pipelines.GetRenderingDatasource;

namespace MySite.Customizations.Customized_Sitecore
{
    public class GetMultisiteRenderingDatasource
    {
        public void Process(GetRenderingDatasourceArgs args)
        {
            Assert.IsNotNull(args, "args");

            string text = args.RenderingItem["Datasource Location"]; 
            if (!string.IsNullOrEmpty(text))
            {
                if (text.StartsWith("query:") && !string.IsNullOrEmpty(args.ContextItemPath))
                {
                    var contextItem = args.ContentDatabase.GetItem(args.ContextItemPath);

                    if (contextItem != null)
                    {
                        text = text.Remove(0, 6); 
                        var item = contextItem.Axes.SelectSingleItem(text);

                        if (item != null)
                        {
                            args.DatasourceRoots.Add(item);
                        }
                    }
                }
            }
        }  
    }
}

The following configuration added it to the desired pipeline

<sitecore>
  <configuration>
    <pipelines>
      <getRenderingDatasource>
        <processor type="MySite.Customizations.Customized_Sitecore.GetMultisiteRenderingDatasource,MySite"
                   patch:before="processor[@type='Sitecore.Pipelines.GetRenderingDatasource.GetDatasourceLocation, Sitecore.Kernel']"/>
      </getRenderingDatasource>
    </pipelines>
  </configuration>
</sitecore>

, , , , ,

Leave a comment

Multisite configuration for shared Sitecore rendering parameters

On regular templates, we can use sitecore queries to accommodate for showing items in Sitecore link / list fields from respective site nodes in multi site instances.

For Eg:

query:./ancestor::*[@@templatename='Site Node']/Global/Components/Data/Tip Types/*

However, this will not apply to rendering parameter templates. Since while selecting the rendering parameter values in the presentation of an item, the context item is the rendering itself and not the item on which the presentation is being set.
So if we were using a common rendering parameters template for items in multiple sites, we need to rig something up, so that the options appearing in the link / list type rendering parameters show options from the respective site node.

As an example, consider a rendering parameter template, with a background color field.
2015-10-31_235421

This rendering parameter is set to a shared rendering which is used on multiple sites. However, each site has its own unique list of background color options.
To solve this issue, we needed to add support for the token like $sitenode in the example above.

The handler code we write, replaces this token with the right path, after identifying which item the presentation is being set on, and then identifying its sitenode ancestor.

using Sitecore.Data.Items;
using Sitecore.Shell.Applications.ContentEditor;
using Sitecore.Shell.Applications.ContentManager;
using Sitecore.Text;
using Sitecore.Web;
using System.Configuration;
using System.Linq;
using SharedSource.Helpers;

namespace MySite.Customizations.Customized_Sitecore
{
    public class MultisiteRenderingParamDroplink : LookupEx
    {
        public new string Source
        {
            get { return base.Source; }
            set
            {
                if (value.Contains("$sitenode"))
                {
                    Item contextItem = Sitecore.Context.ContentDatabase.Items[ItemID];

                    if (contextItem.Template.BaseTemplates
                        .Any(x => x.ID == ItemTree.Templates.System.Layout.Rendering_Parameters.Standard_Rendering_Parameters.ItemID))
                    {
                        string url = WebUtil.GetQueryString();
                        if (!string.IsNullOrWhiteSpace(url) && url.Contains("hdl"))
                        {
                            FieldEditorParameters parameters = FieldEditorOptions.Parse(new UrlString(url)).Parameters;
                            var currentItemId = parameters["contentitem"];
                            if (!string.IsNullOrEmpty(currentItemId))
                            {
                                Sitecore.Data.ItemUri contentItemUri = new Sitecore.Data.ItemUri(currentItemId);
                                contextItem = Sitecore.Data.Database.GetItem(contentItemUri);

                                Item siteNode = SitecoreHelper.GetAncestorOrSelfByTemplateId
                                    (contextItem, ConfigurationManager.AppSettings["TemplateGuid_SiteNode"]);

                                if (siteNode != null)
                                {
                                    base.Source = value.Replace("$sitenode", siteNode.Paths.FullPath);
                                    return;
                                }
                            }
                        }
                    }
                }
                base.Source = value;
            }
        }
    }
}

Helpers:

            public static Item GetAncestorOrSelfByTemplateId(Item item, string templateId)
            {
                if (item == null || string.IsNullOrEmpty(templateId))
                {
                    return null;
                }

                ID parsedTemplateId;
                return ID.TryParse(templateId, out parsedTemplateId)
                           ? GetAncestorOrSelfByTemplateId(item, parsedTemplateId)
                           : null;
            }

            public static Item GetAncestorOrSelfByTemplateId(Item item, ID templateId)
            {
                if (item == null || templateId == ID.Null || templateId == ID.Undefined)
                {
                    return null;
                }

                Item templateItem = item.Database.GetItem(templateId);
                return templateItem == null ? null : GetAncestorOrSelfByTemplateItem(item, templateItem);
            }

We then register an extension with this type in sitecore:

    <controlSources>
      <source mode="on" namespace="MySite.Customizations.Customized_Sitecore" assembly="MySite" prefix="contentExtension" />
    </controlSources>

This extension is then added to the regular Droplink field. The logic outlined above will kick in only when the current item has the template: /sitecore/templates/System/Layout/Rendering Parameters/Standard Rendering Parameters as its base template. Which would ideally apply to all rendering parameter templates.

2015-11-01_022532

Now the ‘$sitenode’ token would get translated as per the logic added in the above code to the respective site node.

, , , , , , , , , , ,

Leave a comment

Output Html / Encoded characters using glass html helpers in Sitecore

When a content author enters decoded symbols in single line / multiline sitecore text fields, the symbols don’t get encoded on their own on the site when we output them with @Editable (Glass method – supporting page editor mode).
This also applies to links rendered using @RenderLink (Glass method)
This would mean text like &reg; and &trade; getting output in place of ® and ™ respectively.

Sitecore text fields DO support these symbols being pasted on to them directly, but sometimes it is not a choice we can make, especially if we have items created and prefilled automatically through external code / a connector to a third party system etc.

To achieve this, we create a new class inheriting from the GlassView class, and create Editable / RenderLink methods which use the respective base methods, taking care of the HtmlDecode!

public abstract class MyGlassView<TModel> : GlassView<TModel>
    {
        // *********************************** Text ***********************************

        public new HtmlString Editable(Expression<Func<TModel, object>> field, object parameters = null)
        {
            return new HtmlString(HttpUtility.HtmlDecode(GlassHtml.Editable(Model, field, parameters)));
        }

        public new HtmlString Editable(Expression<Func<TModel, object>> field, Expression<Func<TModel, string>> standardOutput, object parameters = null)
        {
            return new HtmlString(HttpUtility.HtmlDecode(GlassHtml.Editable(Model, field, standardOutput, parameters)));
        }

        // *********************************** Links ***********************************

        public HtmlString RenderLink<T>(T model, Expression<Func<T, object>> field, object attributes = null,
            bool isEditable = false, string contents = null, bool isContentEmpty = false)
        {
            if (field == null)
                return new HtmlString(string.Empty);

            return new HtmlString(HttpUtility.HtmlDecode(GlassHtml.RenderLink(model, field, attributes, isEditable,
                GlobalHelper.GetLinkContents(field.Compile().Invoke(model) as Link, contents, isContentEmpty))));
        }

        public HtmlString RenderLink(Expression<Func<TModel, object>> field, object attributes = null,
            bool isEditable = false, string contents = null, bool isContentEmpty = false)
        {
            return RenderLink(Model, field, attributes, isEditable, contents, isContentEmpty);
        }
    }

All views will need to inherit from this new MyGlassView class to be able to use these new methods.

, , , , , , , , ,

Leave a comment

Using dynamic view markers in Sitecore MVC

While working with Sitecore, we add many renderings in the presentation and that makes up each page that is output for the corresponding item.

We are currently working with Sitecore MVC, and we have traditionally added start and end comments in views manually (copy pasting the path of the current view) to mark the start and end of each component on the page. This helped us debug things, made the output html more readable and maintainable, and lets face it, after a month or two, we find it hard to remember which module was called what!

Recently it was pointed out that this might be a potential security threat and we are exposing details of our folder structure in the production environment, which might not be the best idea.

So to ease the process, and remove this potential threat, we created a html helper extension method which would dynamically output the view path start and end comments subject to an appsetting value being toggled to true. This appsetting key would be toggled to false in any environment where we wouldn’t want the comments to be output!

        public static ViewMarker BeginViewMarker(this HtmlHelper htmlHelper, string filename)
        {
            htmlHelper.ViewContext.Writer.Write(RenderFilenameComment("START", filename));
            return new ViewMarker(htmlHelper.ViewContext, filename);
        }

        private static HtmlString RenderFilenameComment(string position, string filename)
        {
            if (ConfigurationManager.AppSettings["ShowViewMarkers"] == "true")
            {
                return new HtmlString(string.Format("<!-- {0}: {1} -->", position.ToUpper(CultureInfo.CurrentCulture), filename));
            }

            return new HtmlString("");
        }

        public class ViewMarker : IDisposable
        {
            private readonly TextWriter _writer;
            private readonly string _filename;
            public ViewMarker(ViewContext viewContext, string filename)
            {
                _writer = viewContext.Writer;
                _filename = filename;
            }

            public void Dispose()
            {
                _writer.Write(RenderFilenameComment("END", _filename));
            }
        }

And in the view, we would just enclose all the view html / razor within a call to this helper method:

@using (Html.BeginViewMarker(VirtualPath))
{
   ...
   ...
   ...
}

The output would look like:

2015-09-19_223640

, , , , , , , , , ,

1 Comment

Safely truncate Sitecore Rich Text / Html

We came across the need to be able to truncate text (html) entered in a rich text field to be able to display the same on compact modules / show article summaries etc.
There are many different approaches that can be taken here to be able to safely truncate html. But here’s the code that passed our testing and is currently live! We are parsing the html into XML to achieve this.

Please note:
This code truncates the text to the nearest word, and if the character limit is reached within the first word, the truncation is done mid word instead of returning no text at all. It will also allow you to optionally append ellipses to the truncated text.

        public static string TruncateHtml(string text, int charCount, bool appendEllipses = false)
        {
            text = HttpUtility.HtmlDecode(text);

            if (charCount <= 0) return text;

            try
            {
                // your data, probably comes from somewhere, or as params to a methodint 
                XmlDocument xml = new XmlDocument();
                xml.LoadXml("" + text + "");
                // create a navigator, this is our primary tool
                XPathNavigator navigator = xml.CreateNavigator();
                XPathNavigator breakPoint = null;

                // find the text node we need:
                while (navigator.MoveToFollowing(XPathNodeType.Text))
                {
                    int remainingCharacters = charCount;
                    charCount -= navigator.Value.Length;
                    if (charCount <= 0) { string lastText = TruncateText(navigator.Value, remainingCharacters) + (StripHtml(text).Length > charCount && appendEllipses ? "..." : "");
                        navigator.SetValue(lastText);
                        breakPoint = navigator.Clone();
                        break;
                    }
                }

                // first remove text nodes, because Microsoft unfortunately merges them without asking
                while (navigator.MoveToFollowing(XPathNodeType.Text))
                {
                    if (navigator.ComparePosition(breakPoint) == XmlNodeOrder.After)
                    {
                        navigator.DeleteSelf();
                    }
                }

                // moves to parent, then move the rest
                navigator.MoveTo(breakPoint);
                while (navigator.MoveToFollowing(XPathNodeType.Element))
                {
                    if (navigator.ComparePosition(breakPoint) == XmlNodeOrder.After)
                    {
                        navigator.DeleteSelf();
                    }
                }

                // moves to parent
                navigator.MoveToRoot();
                navigator.MoveToFollowing(XPathNodeType.Element);
                return navigator.InnerXml;
            }
            catch (Exception)
            {
                return text;
            }
        }

        private static string TruncateText(string str, int maxCharCount, bool appendEllipses = false)
        {
            if (string.IsNullOrWhiteSpace(str)) return string.Empty;

            str = HttpUtility.HtmlDecode(str);
            if (str.Length <= maxCharCount || maxCharCount == 0) return str; int originalStrLen = str.Length; if (str.Length > maxCharCount)
                str = str.Substring(0, maxCharCount + 1);

            int ellipsePos = str.LastIndexOfAny(new[] { ' ', '.', ',', ';', '!', '-', ']', '}', ')', '*' });
            if (ellipsePos != -1 && str.Length > ellipsePos && ellipsePos > 0)
                str = str.Substring(0, ellipsePos);

            if (ellipsePos == -1)
            {
                str = str.Substring(0, str.Length - 1);
            }

            if ((str.Length < originalStrLen) && appendEllipses)
            {
                str = str.TrimEnd('.').TrimEnd(',').TrimEnd(';').TrimEnd('-');
            }

            return str + ((str.Length < originalStrLen) && appendEllipses ? "..." : string.Empty);
        }

The TruncateText() method only truncates a given string to the nearest word, while the TruncateHtml() method is responsible for converting the passed in text into XML and safely truncating the same, taking care of maintaining valid XML and hence valid HTML.

We have also wrapped any passed in text into an external XML tag , to make sure that passed in free text also can be parsed in this way.

When it came to using this in Sitecore, here’s an example of how we have put this to use!

@Html.Raw(Editable(Model, m => m.Title, x => TextUtilityStatic.TruncateHtml(x.Title, 50, true)))

So this way, the field would still be editable in page editor mode, but output the truncated html on the page!

, , , , ,

Leave a comment

Override GlassView.RenderLink/BeginRenderLink for internal items in Sitecore

We are using sitecore 7.2 with TDS and glassmapper, and we came across a need where we needed special logic to render links to specifically internal items.

In our company we follow a certain pattern with items, something I am sure everyone follows themselves. And as a part of the standard set of base fields that every navigable item in sitecore has, we have a Navigation Title and a Redirect Link.

  • A Navigation Title serves the purpose of letting the link text to an item rendered anywhere on the site to be managed separately from the item name / headline itself. You might want a shorter title to links to a page, while the headline of the page itself might be more descriptive. So when we use GlassView.RenderLink – we would like the provision to fallback to this Navigation Title field if no description is manually managed by the content author.
  • A Redirect Link serves the purpose of allowing the content author to temporarily or permanently having an item redirect to a different page (internal or external). In case of external links, this method basically allows us to have placeholder Sitecore items for external links too! Which is especially useful when we want them to show up in Content Tree Navigation renderings. The Redirect Link is at the end of the day a link field. If the content author wanted the external link set in this field to open in a new tab (considering its an external link!), GlassView.RenderLink wouldn’t know to make this happen since this is custom logic we wanted in place based on our custom field. So we needed to make an update to GlassView.RenderLink – to be able to honor the redirect targets as well.

We wrote overrides for RenderLink and also BeginRenderLink methods to cover our bases.

GlassView.RenderLink

        public HtmlString RenderLink<T>(T model, Expression<Func<T, object>> field, object attributes = null,
            bool isEditable = false, string contents = null, bool isContentEmpty = false)
        {
            if (field == null)
                return new HtmlString(string.Empty);

            Link tempLink = field.Compile().Invoke(model) as Link;

            if (tempLink.IsRedirectNewTab())
            {
                if (attributes == null)
                {
                    attributes = new NameValueCollection { { "target", "_blank" } };
                }
                else if (attributes is NameValueCollection)
                {
                    NameValueCollection currentAttributes = attributes as NameValueCollection;

                    if (currentAttributes["target"] != null)
                    {
                        currentAttributes.Remove("target");
                    }
                    currentAttributes.Add(new NameValueCollection { { "target", "_blank" } });
                    attributes = currentAttributes;
                }
            }

            return new HtmlString(GlassHtml.RenderLink(model, field, attributes, isEditable,
                GlobalHelper.GetLinkContents(field.Compile().Invoke(model) as Link, contents, isContentEmpty)));
        }

        public HtmlString RenderLink(Expression<Func<TModel, object>> field, object attributes = null,
            bool isEditable = false, string contents = null, bool isContentEmpty = false)
        {
            return RenderLink(Model, field, attributes, isEditable, contents, isContentEmpty);
        }

GlassView.BeginRenderLink

        public override RenderingResult BeginRenderLink(Expression<Func<TModel, object>> field,
            object attributes = null, bool isEditable = false)
        {
            return BeginRenderLink(Model, field, attributes, isEditable);
        }

        public override RenderingResult BeginRenderLink<T>(T model, Expression<Func<T, object>> field,
            object attributes = null, bool isEditable = false)
        {
            Link tempLink = field.Compile().Invoke(model) as Link;

            if (tempLink.IsRedirectNewTab())
            {
                if (attributes == null)
                {
                    attributes = new NameValueCollection { { "target", "_blank" } };
                }
                else if (attributes is NameValueCollection)
                {
                    NameValueCollection currentAttributes = attributes as NameValueCollection;

                    if (currentAttributes["target"] != null)
                    {
                        currentAttributes.Remove("target");
                    }

                    currentAttributes.Add(new NameValueCollection { { "target", "_blank" } });
                    attributes = currentAttributes;
                }
            }
            return base.BeginRenderLink(model, field, attributes, isEditable);
        }

In BeginRenderLink – we usually are not too concerned about the text – so we haven’t overridden that here.
We have used 2 helper methods here:

  • IsRedirectNewTab() – an extension method to glass Link class to determine if the target of the link is an internal item AND has a redirect link set on it AND the redirect link has a target set to open in a new tab. We could ofcourse go a step further to just pull the target from the link, but the business logic here, called for specifying new window only.
  • GlobalHelper.GetLinkContents() – would return the Navigation Title field – IF no link description was managed AND the link pointed to an internal item.
        public static bool IsRedirectNewTab(this Link link)
        {
            if (link == null || !link.IsValid() || link.Type != LinkType.Internal || link.TargetId == new Guid()) return false;

            Base_Navigation baseNavigation = SharedController.SitecoreCurrentContext.GetItem<Base_Navigation>(link.TargetId);

            if (baseNavigation != null && baseNavigation.Redirect_Link.IsValid())
            {
                return baseNavigation.Redirect_Link.Target.ToLower() == "_blank";
            }

            return false;
        }

Note: We use the singleton pattern to get a single instance of the Glass.Mapper.Sc.SitecoreContext class, which is in SharedController.SitecoreCurrentContext.
Also, we have extension methods for certain classes to validate the corresponding objects (IsValid()). Please find a short note here: Glass field validations in Sitecore views

        public static string GetLinkContents(Link link, string contents, bool isContentEmpty = false)
        {
            if (string.IsNullOrEmpty(contents) && link != null && string.IsNullOrWhiteSpace(link.Text) && !isContentEmpty)
            {
                if (link.Type == LinkType.Internal)
                {
                    Base_Navigation targetItem = SharedController.SitecoreCurrentContext.GetItem<Base_Navigation>(link.TargetId);
                    if (targetItem != null)
                        return targetItem.Navigation_Title;
                }
            }
            return contents;
        }

So now RenderLink will do all the additional processing that we needed without having to make any change to the calls we make in the views to the RenderLink method.
Main thing to note here though, all our views inherit from the custom class we created (instead of GlassView) where we placed these overrides.

namespace MySite.Customizations.Customized_Glass
{
    public abstract class CustomGlassView<TModel> : GlassView<TModel>
    {
        public HtmlString RenderLink<T>(T model, Expression<Func<T, object>> field, object attributes = null,
            bool isEditable = false, string contents = null, bool isContentEmpty = false)
        {
            ...
        }

        public HtmlString RenderLink(Expression<Func<TModel, object>> field, object attributes = null,
            bool isEditable = false, string contents = null, bool isContentEmpty = false)
        {
            ...
        }

        public override RenderingResult BeginRenderLink(Expression<Func<TModel, object>> field,
            object attributes = null, bool isEditable = false)
        {
            ...
        }

        public override RenderingResult BeginRenderLink<T>(T model, Expression<Func<T, object>> field,
            object attributes = null, bool isEditable = false)
        {
            ...
        }
    }
}

So our views all inherit CustomGlassView:

@inherits MySite.Customizations.Customized_Glass.CustomGlassView<...>

, , , , , ,

1 Comment