Archive for November, 2015

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

Asynchronous resize of images (async http handler) with default sitecore image

Images can usually take up most of your page load time. In our project we were loading product images from the servers, and resizing them before serving them up on the site. To improve performance, we decided to try and do the resizing and image loading there after asynchoronously.
So we created an asynchronous http handler to perform this action.

We were resizing external images (not stored in sitecore), but we did want to load up a default image stored in sitecore if the image didn’t exist on the filesystem.
To achieve this, we passed in the url of the default image (language specific) as a querystring parameter to the http handler itself. In this case, it would check if the intended image existed on the filesystem, if not, it would return the resized default image in the response stream.

using Sitecore.Diagnostics;
using System;
using System.Drawing;
using System.IO;
using System.Net;
using System.Threading;
using System.Web;

namespace MySite.Customizations
{
    class ImageResizer : IHttpAsyncHandler
    {
        public bool IsReusable { get { return false; } }

        public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, Object extraData)
        {
            ImageProcessor asyncImgProcessor = new ImageProcessor(cb, context, extraData);
            asyncImgProcessor.StartAsyncWork();
            return asyncImgProcessor;
        }

        public void EndProcessRequest(IAsyncResult result)
        {
        }

        public void ProcessRequest(HttpContext context)
        {
        }
    }

    class ImageProcessor : IAsyncResult
    {
        private bool _completed;
        private Object _state;
        private AsyncCallback _callback;
        private HttpContext _context;

        bool IAsyncResult.IsCompleted { get { return _completed; } }
        WaitHandle IAsyncResult.AsyncWaitHandle { get { return null; } }
        Object IAsyncResult.AsyncState { get { return _state; } }
        bool IAsyncResult.CompletedSynchronously { get { return false; } }

        public ImageProcessor(AsyncCallback callback, HttpContext context, Object state)
        {
            _callback = callback;
            _context = context;
            _state = state;
            _completed = false;
        }

        public void StartAsyncWork()
        {
            ThreadPool.QueueUserWorkItem(StartAsyncTask, null);
        }

        private void StartAsyncTask(Object workItemState)
        {
            if (_context.Request.QueryString["path"] != null)
            {
                try
                {
                    string imagePath = _context.Request.QueryString["path"];

                    if (string.IsNullOrWhiteSpace(imagePath))
                    {
                        EndAsyncTask();
                        return;
                    }

                    imagePath = HttpUtility.Decode(imagePath);
                    Image image = null;
                    bool isFileSystem = false;

                    string fileSystemImagePath;

                    if (!ImageHelper.IsValidProductFileSystemImage(imagePath, out fileSystemImagePath))
                    {
                        imagePath = _context.Request.Url.Scheme + "://" + _context.Request.Url.Host
                            + HttpUtility.Decode(_context.Request.QueryString["defaultimage"]);
                    }
                    else
                    {
                        image = Image.FromFile(fileSystemImagePath);
                        isFileSystem = true;
                    }

                    int h = 0, w = 0;
                    if (_context.Request.QueryString["h"] != null)
                    {
                        h = Convert.ToInt32(string.IsNullOrWhiteSpace(_context.Request.QueryString["h"]) ? "0" : _context.Request.QueryString["h"]);
                    }
                    if (_context.Request.QueryString["w"] != null)
                    {
                        w = Convert.ToInt32(string.IsNullOrWhiteSpace(_context.Request.QueryString["w"]) ? "0" : _context.Request.QueryString["w"]);
                    }

                    try
                    {
                        if (!isFileSystem)
                        {
                            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(imagePath);
                            using (WebResponse response = request.GetResponse())
                            using (HttpWebResponse httpResponse = (HttpWebResponse)response)
                            {
                                if (httpResponse.StatusCode != HttpStatusCode.OK)
                                {
                                    EndAsyncTask();
                                    return;
                                }
                                using (Stream imageStream = response.GetResponseStream())
                                {
                                    if (imageStream == null)
                                    {
                                        EndAsyncTask();
                                        return;
                                    }

                                    image = Image.FromStream(imageStream);
                                }
                            }
                        }

                        byte[] imageBytes = ImageHelper.ResizeAndCropImage(image, w, h,
                            (_context.Request.QueryString["crop"] == null || _context.Request.QueryString["crop"] == "true"));

                        image.Dispose();

                        _context.Response.ContentType = "image/jpeg";
                        if (imageBytes != null)
                        {
                            _context.Response.BinaryWrite(imageBytes);
                        }
                        _context.Response.End();
                    }
                    catch (Exception ex)
                    {
                        Log.Error(ex.Message, ex);
                        EndAsyncTask();
                        return;
                    }
                }
                catch (Exception ex)
                {
                    Log.Error("Exception in fetching product image for resize: " + ex.StackTrace, ex, this);
                }
            }
            EndAsyncTask();
        }

        public void EndAsyncTask()
        {
            _completed = true;
            _callback(this);
        }
    }
}

In the above code, IsValidProductFileSystemImage – basically executes a System.IO.File.Exists call to check whether the image in the ‘path’ parameter exists.
Additionally, the code for ResizeAndCropImage can be found at: Resize Images with Crop and Quality Control.

, , , , , ,

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

Page editor support in Sitecore for css background images

While displaying images on your site, sometimes it is preferable to use css style background images instead of the img tag.
Some of the reasons I have located for this:

  • It lets you have actual text in the element (offset with CSS so people can’t see it) instead of an alt attribute which is theoretically better food for search engines.
  • It also lets you use sprites (background images containing multiple pictures which are cropped with background-position, height and width) to reduce the numbers of HTTP requests being made for images.
  • Use background-image if you intend to have people print your page and you do not want the image to be included by default.
  • Use background-image if you need for only a portion of the image to be visible, as with CSS sprites.
  • Use background-image with background-size:cover in order to stretch a background image to fill its entire window.
  • Use CSS background images when doing image-replacement of text eg. paragraphs/headers.
  • Use CSS background images if the image is not part of the content.

When we use background images, how do we get page editor support for such images?

One great solution would ofcourse be using Sitecore EditFrames. (EditFrame with Sitecore MVC and Glass.Mapper)

But – this does involve creating additional items in the Core DB, especially if you have different field names for the image fields.
If you wanted to bypass this step, here’s a solution!

For every background image you render – make sure you add a couple of lines for the scenario when this page is loaded in page editor mode:


<figure class="page-banner" style="background-image: url('/en/sitecore/shell/~/media/images/banners/startup.jpg')">
    @if (Sitecore.Context.PageMode.IsPageEditor)
    {
        @RenderImage(m => m.Background_Image, new { @class = "page-editor-editable-image" }, isEditable: true)
    }
</figure>

You just need to make sure that this page editor friendly image (with the special class we define) is on the top of all your content – layer wise (Settings z-index:-1;position:relative to the remaining content will ensure this).

2015-11-01_070436

Once this is done, here’s how your image will appear in page editor mode:

2015-11-01_064908

, , , ,

2 Comments

Using Sitecore Image field / Treelist with images in a multisite instance

To come up with a flexible solution to using shared templates across sites with image fields as well – while the sites have separate media repositories, we came up with a solution where the common Site Node (a common parent to all sites) has a field “Media Repository” where we select the corresponding media repository for each site node.

2015-11-01_053241

Using this setup, we are able to link a site node to its corresponding media repository and hence, point the image source of an item to its respective repository folder.

The only restriction here, is that the folder structure would need to be the same in every site media repository. But note – this will also apply to any solution we use for datasources with Sitecore queries in other link / list Sitecore fields.

The code to do this:

using Sitecore.Data.Items;
using System;
using SharedSource.Helpers;

namespace SharedSource.SitecoreFields
{
    public class SiteImage : Sitecore.Shell.Applications.ContentEditor.Image
    {
        public string ItemID
        {
            get;
            set;
        }

        protected override void OnPreRender(EventArgs e)
        {
            base.OnPreRender(e);

            string source = ServerProperties["Source"].ToString();
            if (String.IsNullOrEmpty(ItemID) || String.IsNullOrEmpty(source) || !source.StartsWith("$sitemediapath")) return;

            SetSource(String.Empty);

            Item current = Sitecore.Context.ContentDatabase.GetItem(ItemID);
            if (current == null) return;

            string siteMediaPath = GetSiteMediaPath(current);
            if (siteMediaPath == null) return;

            source = source.Replace("$sitemediapath", siteMediaPath);
            SetSource(source);
        }

        protected void SetSource(string source)
        {
            ServerProperties["Source"] = source;
            Source = source;
        }

        public static string GetSiteMediaPath(Item item)
        {
            Item site = SitecoreHelper.GetAncestorOrSelfByTemplateId(item, new ID("{Site Node Template Id}"));
            Item path = SitecoreHelper.GetReferenceField(site, "Media Repository");
            return path.Paths.FullPath;
        }
    }
}

2 special custom sitecore helper methods are used here:

  • GetAncestorOrSelfByTemplateId: Basically goes recursively up the content tree to find an ancestor item match by template id.
  • GetReferenceField: This method gets the item referenced in say a droplink field, by fetching the item based on the guid stored in the field.

This content extension will need to be registered in sitecore configuration:

<configuration>
  <sitecore>
    <controlSources>
      <source mode="on" namespace="SharedSource.SitecoreFields" assembly="SharedSource" prefix="contentExtension" />
    </controlSources>
  </sitecore>
</configuration>

And we can add the extension to the simple Image field in the Core DB
2015-11-01_055554

Also, if you have a scenario where you’d want a content author to be able to select multiple images on an item from a treelist, then you’d want to add this support on the treelist field as well. We chose to go with a new custom field though 🙂

using Sitecore.Data.Items;
using Sitecore.Shell.Applications.ContentEditor;

namespace SharedSource.SitecoreFields
{
    public class CustomTreeList : TreeList
    {
        public new string Source
        {
            get { return base.Source; }
            set
            {
                if (!value.StartsWith("$sitemediapath"))
                {
                    base.Source = value;
                }
                else
                {
                    Item item = Sitecore.Context.ContentDatabase.Items[this.ItemID];
                    if (item != null)
                    {
                        string source = value;
                        string mediapath = SiteImage.GetSiteMediaPath(item);
                        source=source.Replace("$sitemediapath", mediapath);
                        base.Source = source;
                    }
                }
            }
        }
    }
}

2015-11-01_060147

2015-11-01_060354

, , , , ,

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

Custom CSS for RTF field for a Multisite Sitecore solution

Sitecore uses the telerik rich text field for allowing rich text on the items. While this allows the content author to enter any styled html they please, we might also want to provide them with a finite list of pre-existing styles which adhere to the current site design.
These can be managed in a CSS file and the CSS file can be referenced by overriding the WebStyleSheet setting in sitecore configuration:

      <setting name="WebStylesheet">
        <patch:attribute name="value">/_CSS/WYSIWYGStyle.css</patch:attribute>
      </setting>

The styles will then appear in the rich text editor for any rich text field:

2015-10-31_233732

2015-10-31_233714

In this case, the css class names in the css file, are TEXT–STYLE-1, link–style-1 etc. If we want more user friendly names, say with spaces etc, we need to override them in the sitecore/shell/Controls/Rich Text Editor/ToolsFile.xml file that telerik uses:

Eg:

<root>
   <classes>
      <class name="Fancy Style Name" value=".TEXT--STYLE-1" />
   </classes>
</root>

Now, all this works perfectly well in a single site environment. If you want to have a different set of styles available to the content authors for each site in a multisite instance of sitecore, you’ll have to get a bit more creative 🙂

Here’s how we went about it.
We created a separate CSS file for each site.
Each site in our sitecore instance had a parent site node item. We added a field to enter the site specific css file path on the node.

2015-10-31_235421

On the site nodes, we would set paths relative to the website folder, eg: /_CSS/Site1/rtfstyles.css

Now for the fun part, in the code:

using Sitecore.Data.Items;
using Sitecore.Web;
using SharedSource.Helpers;

namespace MySite.Customizations.Customized_Sitecore
{
    public class RichTextEditorConfiguration : Sitecore.Shell.Controls.RichTextEditor.EditorConfiguration
    {
        public RichTextEditorConfiguration(Item profile)
            : base(profile)
        { }

        protected override void SetupStylesheets()
        {
            string id = WebUtil.GetQueryString("id");
            Item currentItem = SitecoreHelper.GetItemFromGUIDInMaster(id);
            Item siteNode = SitecoreHelper.GetAncestorOrSelfByTemplateId(currentItem, ISite_NodeConstants.TemplateIdString);

            if (siteNode != null && siteNode.Fields[ISite_NodeConstants.Rich_Text_Field_StylesheetFieldName] != null)
            {
                Editor.CssFiles.Add(siteNode.Fields[ISite_NodeConstants.Rich_Text_Field_StylesheetFieldName].Value);
            }
            base.SetupStylesheets();
        }
    }
}

Helpers:

            public static Item GetItemFromGUIDInMaster(string guid)
            {
                Item item = null;
                if (!string.IsNullOrEmpty(guid))
                {
                    ID ItemID = ID.Parse(guid);
                    Database masterDB = Factory.GetDatabase("master");
                    item = masterDB.GetItem(ItemID);
                }
                return item;
            }

            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;
            }

We use glassmapper here, so ISite_NodeConstants.Rich_Text_Field_StylesheetFieldName contains “Rich Text Field Stylesheet”.

We then patch into the HtmlEditor.DefaultConfigurationType setting:

      <setting name="HtmlEditor.DefaultConfigurationType">
        <patch:attribute name="value">MySite.Customizations.Customized_Sitecore.RichTextEditorConfiguration,MySite</patch:attribute>
      </setting>

Note: This code is triggered when you click on the ‘Show Editor’ button above.

, , , , , , , , ,

1 Comment