Error resolving SpeakUI scripts in Sitecore

While working with Speak UI in Sitecore, I added a new Speak component rendering to my solution.

2017-03-31_152916

Once I added this rendering to my speak page, it started throwing a javascript error:

Script error for: /-/speak/v1/client/

2017-03-31_153237

After some poking around, I realized that this issue was occurring because of the space in my rendering name!

So while this url was giving a 404: http://redirectmanager.local/-/speak/v1/client/Redirects_Listing.js, the url with the space was working just fine:

2017-03-31_153530.png

So short of customizing the ResolveScript processor (see below screenshot) which is responsible for resolving the rendering script from your folder to the expected url with the set prefix (see below RequireJsCustomHandler setting), the work around would be to avoid spaces in your rendering names for this version and use underscores if you have to!

<!--   SPEAK HTML REQUIRE JS CUSTOM HANDLER             Specifies the prefix that should trigger the HTTP request customer handler (see the customHandlers section in the web.config file).             Default: /-/speak/v1/        -->
<setting name="Speak.Html.RequireJsCustomHandler" value="/-/speak/v1/" patch:source="Sitecore.Speak.config"/>

2017-03-31_153809.png

Hope this helps!

Advertisements

Sitecore rocks add component error

A quick post here, to troubleshoot an error I faced while trying to add a new speak component to my project using Sitecore Rocks.

While trying to add the component, I kept seeing this issue.
“Count not load file or assembly ‘Sitecore.Rocks, Version=2.0.0.0, Culture=neytral, PublicKeyToken=a3d60f56f71b90’ or one of its dependencies. The system cannot find the file specified.”

Even though my Sitecore Rocks extension was updated only today, the error persisted.
For some reason however, uninstalling the extension and reinstalling the same and then restarting Visual Studio resolved the issue for me.
Hope this helps!

Any additional information on the root cause of this issue is welcome.

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

Reading custom Sitecore configuration

While writing out some Sitecore functionality, we often need to add new Sitecore settings. Basic setting fields can be easily added in the sitecore section.

These settings can be retrieved in code using the methods provided in Sitecore.Configuration.Settings, eg.

Settings.GetIntSetting("SitecoreModules.ImageLazyLoadResizeModule.Offset", 200)

If you have a more complex structure of settings in mind, you might be better off creating your own custom xml structure within the Sitecore configuration settings mode.

Here’s an example

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <SitecoreModules.ImageLazyLoadResizeModule>
      <DesktopBreakpointInfo CssClass="data-responsive-desktop" Breakpoint="0" ImageMaxWidth="580"  />
      <TabletBreakpointInfo CssClass="data-responsive-tablet" Breakpoint="980" ImageMaxWidth="350"  />
      <MobileBreakpointInfo CssClass="data-responsive-mobile" Breakpoint="768" ImageMaxWidth="200"  />
    </SitecoreModules.ImageLazyLoadResizeModule>
  </sitecore>
</configuration>

To read these settings i created a couple of POCO classes

    public class Breakpoint
    {
        public string Name { get; set; }
        public string CssClass { get; set; }
        public string BreakpointPx { get; set; }
        public int ImageMaxWidth { get; set; }
    }

    public class BreakpointsConfig
    {
        public Breakpoint MobileBreakpoint { get; set; }
        public Breakpoint TabletBreakpoint { get; set; }
        public Breakpoint DesktopBreakpoint { get; set; }
    }

Following code allowed us to populate the settings

using Sitecore.Configuration;
using Sitecore.Xml;
using SitecoreModules.ImageLazyLoadResizeModule.Models;
using System.Web;
using System.Xml;

namespace SitecoreModules.ImageLazyLoadResizeModule.Helpers
{
    public class BreakpointConfigHelper
    {
        public static BreakpointsConfig GetBreakpointInfoConfig()
        {
                BreakpointsConfig breakpointsConfig = new BreakpointsConfig();
                breakpointsConfig.DesktopBreakpoint = GetConfigFromNode(Factory.GetConfigNode("SitecoreModules.ImageLazyLoadResizeModule/DesktopBreakpointInfo"));
                breakpointsConfig.TabletBreakpoint = GetConfigFromNode(Factory.GetConfigNode("SitecoreModules.ImageLazyLoadResizeModule/TabletBreakpointInfo"));
                breakpointsConfig.MobileBreakpoint = GetConfigFromNode(Factory.GetConfigNode("SitecoreModules.ImageLazyLoadResizeModule/MobileBreakpointInfo"));
        }
    }
}

GetConfigFromNode

       private static Breakpoint GetConfigFromNode(XmlNode node)
        {
            if (node == null) return null;

            int imageMaxWidth;
            if (!int.TryParse(XmlUtil.GetAttribute("ImageMaxWidth", node), out imageMaxWidth))
                return null;

            return new Breakpoint
            {
                Name = XmlUtil.GetAttribute("Name", node),
                CssClass = XmlUtil.GetAttribute("CssClass", node),
                BreakpointPx = XmlUtil.GetAttribute("Breakpoint", node),
                ImageMaxWidth = imageMaxWidth
            };
        }

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

EditFrame vs Experience Editor Buttons in Sitecore

EditFrame and Experience editor buttons are both great ways of adding experience editor support to our Sitecore pages.

  • EditFrame buttons
    • Create item in core database (I usually just duplicate the Default folder at /sitecore/content/Applications/WebEdit/Edit Frame Buttons/Default). You can add the field(s) you want to make editable in the ‘Fields’ field
    • In your view, you need to wrap the html that you want to make editable, with the EditFrame code, where you need to supply it with the path of your edit frame button in core, and the item ID of the item you want to edit. The fields made editable would depend on the fields you include in the EditFrame button item as shown in the above image.
      @using (BeginEditFrame("/sitecore/content/Applications/WebEdit/Edit Frame Buttons/Headline", Model.Id.ToString()))
      {
      <div>@Model.Headline</div>
      }
      

    You will now be able to see additional buttons around the html you enclosed in the EditFrame code

  • Experience Editor Buttons
    • Create button item in Core database – use the template – /sitecore/templates/System/WebEdit/Field Editor Button and create the /sitecore/content/Applications/WebEdit/Custom Experience Buttons
    • In your rendering, select the button you just created in the ‘Experience Editor Buttons’ field

    The additional button you created will now be available when you select the entire rendering. If the datasource is available, that item is considered for editing and the field name mentioned in the experience editor button item in core is searched for in that item, else the context item is considered for the same.

Points to note about these 2 implementations:

EditFrame Buttons Experience Editor Buttons
Providing fields to edit Pipe separated – In the core item for the button Pipe separated – In the core item for the button
Item to be edited Item id passed in, in the view code The datasource item is used if available, else the context item is used. This is irrespective of whether the fields mentioned in the button item exist in the datasource item.
Code change required Yes, code needs to be added in the view as shown above. No
Html to be selected  Only the html around which the EditFrame code is wrapped needs to be selected to see the button. The available buttons show up when the entire rendering is selected.
Flexibility  You can choose to edit any number of items selecting any single html tag as is the requirement. Only 1 item can be edited per rendering (datasource or context item), and individual html elements cannot be selected to see the button(s).

Error creating Areas / Controllers in Sitecore SIM generated solution

Always If you are using SIM to create your Sitecore visual studio solution, you will get the following structure:

I am using SIM 1.4.0.383 with Sitecore 8.2 – Update 2

When you try to add a controller or an area in your project, you might see the following error:

On looking this up, many results seem to suggest clearing temporary asp.net files at C:\WINDOWS\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files\
This didnt work for me.

On looking further, this issue seemed to be caused because web.config is not included in the project by default.

Include the web.config in your project all is good! Always ensure your web.config is available in your solution and is valid when you encounter a similar error.

Add workflow notifications for all editable items on a Sitecore page

While editing a page in experience editor mode, when we edit the fields which are a part of the context item, if the item goes into the edit stage in the workflow, we see a notification on saving the item.
This notification tells us that the item needs to go through workflow to be published.

2016-11-09_212657

Now, if a module on the page gets data from a rendering datasource item, and we edit that content, there is no notification on the page on item save, even though the datasource item would infact need to go through the workflow to be able to be published.
We used this excellent module form Sitecore marketplace to resolve this issue: Datasource Workflow Module

Now we get additional notifications when we edit rendering datasource items.

2016-11-09_214522

However this doesn’t address reference items that the datasource item might refer to.

So what we did to resolve this issue, is marry the code of this module with code to collect all items which are editable on the current context page (Thanks to this post!). So while we render the page, we collect all editable items on the page and then run a check on this list, to find all items which are not in the final stage of workflow, and show notifications in the notification area for all these items.

Following are the config updates you will need:

<configuration>
  <sitecore>
      <pipelines>
        <renderField>
          <processor type="MySite.Sitecore.Pipelines.RenderField.CollectItemReferences, MySite.Sitecore"></processor>
        </renderField>
        <getPageEditorNotifications>
          <processor type="MySite.Sitecore.Pipelines.GetPageEditorNotifications.GetReferenceWorkflowNotification, MySite.Sitecore" />
        </getPageEditorNotifications>
        <getContentEditorWarnings>
          <processor type="MySite.Sitecore.Pipelines.GetContentEditorWarnings.GetReferenceWorkflowNotification, MySite.Sitecore" />
        </getContentEditorWarnings>
        <getChromeData>
          <processor type="MySite.Sitecore.Pipelines.GetChromeData.EditFrameCollectItemReferences, MySite.Sitecore"></processor>
        </getChromeData>
      </pipelines>
      <setting name="ReferenceWorkflowNotification.ShowWorkflowNoAccessMessage" value="true" />
      <setting name="ReferenceWorkflowNotification.UseHtmlInMessaging" value="true" />
    </settings>
    <referenceWorkflowNotificationItems>
      <webEdit>
        <itemReferences type="MySite.Sitecore.Models.ItemReferenceCollection, MySite.Sitecore" singleInstance="true"></itemReferences>
      </webEdit>
    </referenceWorkflowNotificationItems>
  </sitecore>
</configuration>

Following is the code in use:

using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Layouts;
using Sitecore.Rules.ConditionalRenderings;
using Sitecore.SecurityModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web;
using MySite.Sitecore.Models;
using Sc = Sitecore;

namespace MySite.Sitecore.Extensions
{
    public static class ItemExtensions
    {	
        private const string ContentEditorUrlFormat = "{0}://{1}/sitecore/shell/Applications/Content Editor?id={2}&amp;sc_content=master&amp;fo={2}&amp;vs={3}&amp;la={4}";

        public static RenderingReference[] GetRenderingReferences(this Item i)
        {
            return i == null ? new RenderingReference[0] : i.Visualization.GetRenderings(Sc.Context.Device, false);
        }

        public static List<Item> GetAllUniqueDataSourceItems(this Item i, bool includePersonalizationDataSourceItems = true, bool includeMultiVariateTestDataSourceItems = true)
        {
            var list = new List<Item>();
            foreach (RenderingReference reference in i.GetRenderingReferences())
            {
                list.AddUnqiueDataSourceItem(reference.GetDataSourceItem());

                if (includePersonalizationDataSourceItems)
                {
                    foreach (var dataSourceItem in reference.GetPersonalizationDataSourceItem())
                    {
                        list.AddUnqiueDataSourceItem(dataSourceItem);
                    }
                }

                if (includeMultiVariateTestDataSourceItems)
                {
                    foreach (Item dataSourceItem in reference.GetMultiVariateTestDataSourceItems())
                    {
                        list.AddUnqiueDataSourceItem(dataSourceItem);
                    }
                }
            }

            list.AddRange(AddDatasourceReferenceItems(i));
            list = list.DistinctBy(l => l.ID).ToList();

            return list;
        }

        private static List<Item> AddDatasourceReferenceItems(Item context)
        {
            return ItemReferenceCollection.FromConfiguration().GetItemReferences(context.ID.Guid)
                .Select(x => context.Database.GetItem(new ID(x))).ToList();
        }

        public static List<Item> GetDataSourceItems(this Item i)
        {
            List<Item> list = new List<Item>();
            foreach (RenderingReference reference in i.GetRenderingReferences())
            {
                Item dataSourceItem = reference.GetDataSourceItem();
                if (dataSourceItem != null)
                {
                    list.Add(dataSourceItem);
                }
            }
            return list;
        }

        public static Item GetDataSourceItem(this RenderingReference reference)
        {
            if (reference != null)
            {
                return GetDataSourceItem(reference.Settings.DataSource, reference.Database);
            }
            return null;
        }

        public static List<Item> GetPersonalizationDataSourceItems(this Item i)
        {
            var list = new List<Item>();
            foreach (var reference in i.GetRenderingReferences())
            {
                list.AddRange(reference.GetPersonalizationDataSourceItem());
            }
            return list;
        }

        private static IEnumerable<Item> GetPersonalizationDataSourceItem(this RenderingReference reference)
        {
            var list = new List<Item>();
            if (reference != null && reference.Settings.Rules != null && reference.Settings.Rules.Count > 0)
            {
                list.AddRange(reference.Settings.Rules.Rules.SelectMany(r => r.Actions).OfType<SetDataSourceAction<ConditionalRenderingsRuleContext>>().Select(setDataSourceAction => GetDataSourceItem(setDataSourceAction.DataSource, reference.Database)).Where(dataSourceItem => dataSourceItem != null));
            }
            return list;
        }

        public static List<Item> GetMultiVariateTestDataSourceItems(this Item i)
        {
            var list = new List<Item>();
            foreach (var reference in i.GetRenderingReferences())
            {
                list.AddRange(reference.GetMultiVariateTestDataSourceItems());
            }
            return list;
        }

        private static IEnumerable<Item> GetMultiVariateTestDataSourceItems(this RenderingReference reference)
        {
            var list = new List<Item>();
            if (reference != null && !string.IsNullOrEmpty(reference.Settings.MultiVariateTest))
            {
                using (new SecurityDisabler())
                {
                    // Sitecore 7 to Sitecore 8:
                    var mvVariateTestForLang = Sc.Analytics.Testing.TestingUtils.TestingUtil.MultiVariateTesting.GetVariableItem(reference);
                    
                    // Sitecore 8.1 and above:
                    //var contentTestStore = new Sitecore.ContentTesting.Data.SitecoreContentTestStore();
                    //var mvVariateTestForLang =  contentTestStore.GetMultivariateTestVariable(reference, reference.Language);
                    
                    var variableItem = (mvVariateTestForLang != null) ? mvVariateTestForLang.InnerItem : null;
                    if (variableItem == null) return list;
                    foreach (Item mvChild in variableItem.Children)
                    {
                        var mvDataSourceItem = mvChild.GetInternalLinkFieldItem("Datasource");
                        if (mvDataSourceItem != null)
                        {
                            list.Add(mvDataSourceItem);
                        }
                    }
                }
            }
            return list;
        }

        private static bool ListContainsItem(IEnumerable<Item> list, Item dataSourceItem)
        {
            var uniqueId = dataSourceItem.GetUniqueId();
            return list.Any(e => e.GetUniqueId() == uniqueId);
        }

        private static void AddUnqiueDataSourceItem(this List<Item> list, Item dataSourceItem)
        {
            if (dataSourceItem != null && !ListContainsItem(list, dataSourceItem))
            {
                list.Add(dataSourceItem);
            }
        }

        private static Item GetDataSourceItem(string id, Database db)
        {
            Guid itemId;
            return Guid.TryParse(id, out itemId)
                                    ? db.GetItem(new ID(itemId))
                                    : db.GetItem(id);
        }

        public static string GetContentEditorUrl(this Item i)
        {
            var requestUri = HttpContext.Current.Request.Url;
            return string.Format(ContentEditorUrlFormat, requestUri.Scheme, requestUri.Host, WebUtility.HtmlEncode(i.ID.ToString()), i.Version.Number, i.Language.CultureInfo.Name);
        }

        public static Item GetInternalLinkFieldItem(this Item i, string internalLinkFieldName)
        {
            if (i == null) return null;
            InternalLinkField ilf = i.Fields[internalLinkFieldName];
            if (ilf != null && ilf.TargetItem != null)
            {
                return ilf.TargetItem;
            }
            return null;
        }

        public static IEnumerable<T> DistinctBy<T, TKey>(this IEnumerable<T> items, Func<T, TKey> property)
        {
            return items.GroupBy(property).Select(x => x.First());
        }
    }
}
using Sitecore.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using Sc = Sitecore;

namespace MySite.Sitecore.Models
{
    public class ItemReferenceCollection : IItemReferenceCollection
    {
        protected static object SyncRoot = new object();
        protected IDictionary<Guid, IDictionary<Guid, Guid>> ItemReferences = new Dictionary<Guid, IDictionary<Guid, Guid>>();

        ///
<summary>
        /// Adds the item reference.
        /// </summary>

        /// <param name="itemId">The item identifier.</param>
        public void AddItemReference(Guid itemId)
        {
            lock (SyncRoot)
            {
                var contextItem = Sc.Context.Item;

                if (contextItem == null)
                {
                    return;
                }

                if (ItemReferences.ContainsKey(contextItem.ID.Guid))
                {
                    if (!ItemReferences[contextItem.ID.Guid].ContainsKey(itemId))
                    {
                        ItemReferences[contextItem.ID.Guid].Add(itemId, itemId);
                    }
                }
                else
                {
                    ItemReferences.Add(contextItem.ID.Guid, new Dictionary<Guid, Guid> { { itemId, itemId } });
                }
            }
        }

        ///
<summary>
        /// Adds the item references.
        /// </summary>

        /// <param name="itemIds">The item ids.</param>
        public void AddItemReferences(IEnumerable<Guid> itemIds)
        {
            foreach (var itemId in itemIds)
            {
                AddItemReference(itemId);
            }
        }

        ///
<summary>
        /// Gets the item references.
        /// </summary>

        /// <param name="currentItemId">The current item identifier.</param>
        /// <returns></returns>
        public IEnumerable<Guid> GetItemReferences(Guid currentItemId)
        {
            lock (SyncRoot)
            {
                if (ItemReferences.ContainsKey(currentItemId))
                {
                    return ItemReferences[currentItemId].Values.ToArray();
                }
            }

            return new List<Guid>();
        }

        ///
<summary>
        /// Clears the specified item references.
        /// </summary>

        /// <param name="currentItemId">The current item identifier.</param>
        public void Clear(Guid currentItemId)
        {
            lock (SyncRoot)
            {
                if (ItemReferences.ContainsKey(currentItemId))
                {
                    ItemReferences.Remove(currentItemId);
                }
            }
        }

        ///
<summary>
        /// Gets the instance configured in Sitecore config using Sitecore.Configuration.Factory.
        /// </summary>

        /// <returns></returns>
        public static IItemReferenceCollection FromConfiguration()
        {
            return Factory.CreateObject("referenceWorkflowNotificationItems/webEdit/itemReferences", false) as IItemReferenceCollection;
        }
    }

    public interface IItemReferenceCollection
    {
        void AddItemReference(Guid itemId);

        void AddItemReferences(IEnumerable<Guid> itemIds);

        IEnumerable<Guid> GetItemReferences(Guid currentItemId);

        void Clear(Guid currentItemId);
    }
}
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Globalization;
using Sitecore.Security.AccessControl;
using Sitecore.Workflows;
using Sc = Sitecore;

namespace MySite.Sitecore.Models
{
    public class ItemWorkflowModel
    { 
        #region Fields

        private readonly Item _contextItem;
        private readonly Database _database;
        private readonly IWorkflowProvider _workflowProvider;
        private readonly IWorkflow _workflow;
        private readonly WorkflowState _workflowState;
        WorkflowCommand[] _commands;
        private bool? _showWorkflowNoAccessMessage;
        private const string ShowWorkflowNoAccessMessageKey = "ReferenceWorkflowNotification.ShowWorkflowNoAccessMessage";

        #endregion

        #region Properties

        public Item ContextItem
        {
            get
            {
                return _contextItem;
            }
        }

        public Database Database
        {
            get
            {
                return _database;
            }
        }

        public IWorkflowProvider WorkflowProvider
        {
            get
            {
                return _workflowProvider;
            }
        }

        public IWorkflow Workflow
        {
            get
            {
                return _workflow;
            }
        }
        public WorkflowState WorkflowState
        {
            get
            {
                return _workflowState;
            }
        }

        public WorkflowCommand[] Commands
        {
            get
            {
                if (_commands == null)
                {
                    _commands = WorkflowFilterer.FilterVisibleCommands(Workflow.GetCommands(ContextItem));
                    if (_commands == null)
                    {
                        _commands = new WorkflowCommand[0];
                    }
                }
                return _commands;
            }
        }

        public bool ShowWorkflowNoAccessMessage
        {
            get
            {
                if (!_showWorkflowNoAccessMessage.HasValue)
                {
                    _showWorkflowNoAccessMessage = Sc.Configuration.Settings.GetBoolSetting(ShowWorkflowNoAccessMessageKey, false);
                }
                return _showWorkflowNoAccessMessage.Value;
            }
        }

        public bool ShowNotification
        {
            get
            {
                return NoNulls && !WorkflowState.FinalState && (Commands.Length > 0 && HasWriteAccess() || ShowWorkflowNoAccessMessage);
            }
        }

        public bool NoNulls
        {
            get
            {
                return (Database != null && WorkflowProvider != null && Workflow != null && WorkflowState != null);
            }
        }

        private bool UseHtmlMessaging
        {
            get
            {
                return Sc.Configuration.Settings.GetBoolSetting("ReferenceWorkflowNotification.UseHtmlInMessaging", false);
            }
        }

        #endregion

        #region Constructor

        public ItemWorkflowModel(Item i)
        {
            if (i != null)
            {
                _contextItem = i;
                _database = i.Database;
                if (_database != null)
                {
                    _workflowProvider = _database.WorkflowProvider;
                    if (_workflowProvider != null)
                    {
                        _workflow = _workflowProvider.GetWorkflow(ContextItem);
                        if (_workflow != null)
                        {
                            _workflowState = _workflow.GetState(ContextItem);
                        }
                    }
                }
            }
        }

        #endregion

        #region Methods

        public bool HasWriteAccess()
        {
            return AuthorizationManager.IsAllowed(ContextItem, AccessRight.ItemWrite, Sc.Context.User);
        }

        public string GetEditorDescription(bool isPageEditor = true)
        {
            string noAccessMessage = string.Empty;
            string workflowDisplayName = Workflow.Appearance.DisplayName;
            string workflowStateDisplayName = WorkflowState.DisplayName;
            string itemDisplayName = ContextItem.Paths.ContentPath;

            if (UseHtmlMessaging && !isPageEditor)
            {
                workflowDisplayName = BoldValue(workflowDisplayName);
                workflowStateDisplayName = BoldValue(workflowStateDisplayName);
                itemDisplayName = BoldValue(itemDisplayName);
            }
            else
            {
                workflowDisplayName = SingleQuoteValue(workflowDisplayName);
                workflowStateDisplayName = SingleQuoteValue(workflowStateDisplayName);
                itemDisplayName = SingleQuoteValue(itemDisplayName);
            }

            if (!HasWriteAccess())
            {
                noAccessMessage = " You cannot change the workflow because do not have write access to this item.";
            }

            return Translate.Text("The data source item {0} is in the {1} state of the {2} workflow.{3}", itemDisplayName, workflowStateDisplayName, workflowDisplayName, noAccessMessage);
        }

        private static string SingleQuoteValue(string value)
        {
            return string.Format("'{0}'", value);
        }

        private static string BoldValue(string value)
        {
            return string.Format("<b>{0}</b>", value);
        }

        #endregion
    }
}
using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.GetChromeData;
using System;

namespace MySite.Sitecore.Pipelines.GetChromeData
{
    public class EditFrameCollectItemReferences : GetChromeDataProcessor
    {
        public override void Process(GetChromeDataArgs args)
        {
            try
            {
                if (CanCollectReferences(args))
                {
                    Models.ItemReferenceCollection.FromConfiguration().AddItemReference(args.Item.ID.Guid);
                }
            }
            catch (Exception ex)
            {
                Log.Error("Error in EditFrameCollectItemReferences pipeline processor.", ex, this);
            }
        }

        protected virtual bool CanCollectReferences(GetChromeDataArgs args)
        {
            return args != null && args.Item != null && !args.Aborted
                && args.ChromeType != null && args.ChromeData != null
                && "editFrame".Equals(args.ChromeType, StringComparison.OrdinalIgnoreCase)
                && (Context.PageMode.IsPageEditor || Context.PageMode.IsPageEditorEditing);
        }
    }
}
using MySite.Sitecore.Extensions;
using MySite.Sitecore.Models;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.GetContentEditorWarnings;
using Sitecore.Shell.Framework.CommandBuilders;


namespace MySite.Sitecore.Pipelines.GetContentEditorWarnings
{
    public class GeReferenceWorkflowNotification
    {
        #region Methods

        public void Process(GetContentEditorWarningsArgs arguments)
        {
            Assert.ArgumentNotNull(arguments, "arguments");
            if (arguments.Item != null)
            {
                foreach (var ds in arguments.Item.GetAllUniqueDataSourceItems())
                {
                    GetNotifications(arguments, ds);
                }
            }
        }

        public void GetNotifications(GetContentEditorWarningsArgs arguments, Item contextItem)
        {
            if (arguments == null) return;
            var wfModel = new ItemWorkflowModel(contextItem);
            if (wfModel.ShowNotification)
            {
                SetNotification(arguments, wfModel);
            }
        }

        private void SetNotification(GetContentEditorWarningsArgs arguments, ItemWorkflowModel wfModel)
        {
            var editorNotification = arguments.Add();
            editorNotification.Title = "Datasource Item in Workflow";
            editorNotification.Text = wfModel.GetEditorDescription(false);
            editorNotification.Icon = wfModel.WorkflowState.Icon;
            if (wfModel.HasWriteAccess())
            {
                foreach (var command in wfModel.Commands)
                {
                    editorNotification.AddOption(command.DisplayName, new WorkflowCommandBuilder(wfModel.ContextItem, wfModel.Workflow, command).ToString());
                }
            }
        }

        #endregion
    }
}
using MySite.Sitecore.Extensions;
using MySite.Sitecore.Models;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.GetPageEditorNotifications;
using Sitecore.Shell.Framework.CommandBuilders;
using System.Linq;


namespace MySite.Sitecore.Pipelines.GetPageEditorNotifications
{
    public class GetReferenceWorkflowNotification : GetPageEditorNotificationsProcessor
    {
        #region Methods

        public override void Process(GetPageEditorNotificationsArgs arguments)
        {
            Assert.ArgumentNotNull(arguments, "arguments");
            if (arguments.ContextItem == null) return;
            foreach (var dataSourceArguments in arguments.ContextItem.GetAllUniqueDataSourceItems().Select(ds => new GetPageEditorNotificationsArgs(ds)))
            {
                GetNotifications(dataSourceArguments);
                arguments.Notifications.AddRange(dataSourceArguments.Notifications);
            }
        }

        public void GetNotifications(GetPageEditorNotificationsArgs arguments)
        {
            if (arguments == null) return;
            var wfModel = new ItemWorkflowModel(arguments.ContextItem);
            if (wfModel.ShowNotification)
            {
                SetNotification(arguments, wfModel);
            }
        }

        private void SetNotification(GetPageEditorNotificationsArgs arguments, ItemWorkflowModel wfModel)
        {
            var editorNotification = new PageEditorNotification(wfModel.GetEditorDescription(), PageEditorNotificationType.Warning)
            {
                Icon = wfModel.WorkflowState.Icon
            };
            if (wfModel.HasWriteAccess())
            {
                foreach (var notificationOption in wfModel.Commands
                    .Select(command => new PageEditorNotificationOption(command.DisplayName, new WorkflowCommandBuilder(wfModel.ContextItem, wfModel.Workflow, command).ToString())))
                {
                    editorNotification.Options.Add(notificationOption);
                }
            }
            arguments.Notifications.Add(editorNotification);
        }

        #endregion
    }
}
using MySite.Sitecore.Models;
using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.RenderField;
using System;

namespace MySite.Sitecore.Pipelines.RenderField
{
    public class CollectItemReferences
    {
        public void Process(RenderFieldArgs args)
        {
            try
            {
                if (CanCollectReferences(args))
                {
                    ItemReferenceCollection.FromConfiguration().AddItemReference(args.Item.ID.Guid);
                }
            }
            catch(Exception ex)
            {
                Log.Error("Error in CollectItemReferences pipeline processor.", ex, this);
            }
        }

        protected virtual bool CanCollectReferences(RenderFieldArgs args)
        {
            return args != null && args.Item != null && !args.Aborted 
                && !args.DisableWebEdit && !args.DisableWebEditContentEditing
                && (Context.PageMode.IsPageEditor || Context.PageMode.IsPageEditorEditing);
        }
    }
}
using MySite.Sitecore.Extensions;
using MySite.Sitecore.Models;
using Sitecore.Data.Validators;
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;

namespace MySite.Sitecore.Validators
{
    [Serializable]
    public class ComponentsinFinalWorkflow : StandardValidator
    {
        #region Properties

        public override string Name
        {
            get { return "Component Workflow State"; }
        }

        #endregion

        #region Constructor

        public ComponentsinFinalWorkflow(SerializationInfo info, StreamingContext context) : base(info, context)
        {
        }

        public ComponentsinFinalWorkflow()
        {
        }

        #endregion

        #region Methods

        protected override ValidatorResult Evaluate()
        {
            var item = GetItem();
            var result = ValidatorResult.Valid;
            var paths = new List<string>();
            foreach (var ds in item.GetAllUniqueDataSourceItems())
            {
                var wfModel = new ItemWorkflowModel(ds);
                if (wfModel.NoNulls && !wfModel.WorkflowState.FinalState)
                {
                    paths.Add(ds.Paths.ContentPath);
                    result = GetMaxValidatorResult();
                }
            }

            Text = string.Empty;
            if (paths.Count > 0)
            {
                Text += GetText("The item{0} {1} should be in a final workflow state.", paths.Count > 1 ? "s" : string.Empty, GetPathsString(paths));
            }

            return result;
        }

        private string GetPathsString(List<string> paths)
        {
            string formatString = "\"{0}\"{1}";
            if (paths.Count == 1)
            {
                return string.Format(formatString, paths[0], string.Empty);
            }
            else if (paths.Count == 2)
            {
                return string.Format(formatString, paths[0], " and ") + string.Format(formatString, paths[1], string.Empty);
            }

            string pathsString = string.Empty;
            string separator = ", ";
            for (int i = 0; i < paths.Count; i++)
            {
                if (i == paths.Count - 1)
                {
                    separator = string.Empty;
                }
                if (i == paths.Count - 2)
                {
                    separator = ", and ";
                }
                pathsString += string.Format(formatString, paths[i], separator);
            }
            return pathsString;
        }

        protected override ValidatorResult GetMaxValidatorResult()
        {
            return ValidatorResult.Warning;
        }

        #endregion
    }
}

Once you have these updates in place, you’ll see that there will be a notification for each editable item on the page which is not in the final stage of workflow in content editor and experience editor mode!

2016-11-09_215548

Editor to create image hotspots in Sitecore

We had the need to create a module which showed an image with hotspots on it – basically positional tooltips.

We created subitems of the module for each hotspot, and expected the content author to provide the position of the hotspot on the module image – in percentage – relative to the top / left of the image.
Now this posed an issue for the content authors since they had to either guess the position of each hotspot or upload the image onto a different online tool and then get the position from there to add into out hotspot sub items.

To remedy this we created a tool and integrated it with the Sitecore Content Editor – which would allow the content author to just click on the points on the image and create the corresponding hotspots.

To make this tool more versatile, we have made it configurable, so it can be used with any instance of Sitecore and any module, as long as it is configured in the hotspot specific configuration xml.

Here’s an example of the end result:

2016-03-22_191037

2016-03-22_190113

To achieve this, we needed to implement the following steps:

  1. Create an aspx page in your solution which Sitecore will load as a tab in the content editor.2016-03-22_210644
  2. Create an Editor item in Core DB, which will point to the aspx page you created.
  3. Set the Editor item on the standard values item of the template which would have the image on which the hotspots need to be set.
  4. Add the relevant code in the aspx page.

Create an aspx page in your solution

In our solution, we added the aspx page in the following location:

2016-03-22_220705

Create an Editor item in Core DB

2016-03-22_220900

Set the Editor item on the standard values item of the template

In this example, we have a POI module datasource template – which will have the image field for which we want to create the hotspots / points of interest. So we will set this editor in the standard values of this datasource template.

2016-03-22_221350.png

ASPX page code

We have the back end and front end code here. From a usability perspective, the current code only enables the content author to create new hotpot / point of interest items. Editing / deleting hotpot / point of interest items can be done directly at the individual hotspot level. Additionally, to make it convenient for a content author to identify which hotspot they want to edit / delete, on load of the tab, we have front end code, which shows the current set of hotspots that already exist as sub items.

While the top / left coordinate % is auto populated by this tool, we have an optional field for the tooltip description which a content author can choose to populate before creating the hotspot.

Futher, in our project, we have tried to make this tool as configurable as possible! So now, we can use this tool on any module, by making sitecore and config updates!

So here’s the code we used:

Aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Hotspots.aspx.cs" Inherits="shared.sitecore_modules.Shell.Editors.ProductResourceHotspot.Hotspots" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Hotspots</title>
    <script src="//code.jquery.com/jquery-1.10.2.min.js"></script>
    <script src="./hotspots.js"></script>
    <link type="text/css" rel="stylesheet" href="./hotspot.css" />
</head>
<body>
    <form id="formHotSpot" runat="server">
        <div data-hotspot='<%=HotSpotsJson %>' class="hotspot-image-wrapper">
            <asp:Image ID="imgResource" runat="server" CssClass="resource-image" />
            <br />
            <asp:Label ID="lblNoImageFound" runat="server" CssClass="not-available">
                Image not found.
            </asp:Label>
            <div id="existingHotspots"></div>
        </div>
        <table id="tableHotSpots">
        </table>
        <input type="hidden" id="hotspotCounter" name="hotspotCounter" />
        <asp:HiddenField runat="server" ID="hiddenHotspotTemplateId" />
        <asp:Button ID="btnSubmit" runat="server" Text="Create Hotspots" OnClick="btnSubmit_Click" CssClass="submit" />
    </form>
</body>
</html>

You’ll see some additional labels etc above, to show alerts to the content author (clearly, I am way too used to MVC now and have completely forgotten how to make nice in asp.net! Please excuse!)

Code Behind

Sitecore actually loads up our aspx page in an iframe, passing in the item id in a querystring parameter, which we use below.

namespace shared.sitecore_modules.Shell.Editors.ProductResourceHotspot
{
    public partial class Hotspots : System.Web.UI.Page
    {
        public string HotSpotsJson { get; set; }

        public hotspot HotspotTemplateInfo { get; set; }

        protected void Page_Load(object sender, EventArgs e)
        {
            LoadHotspotTemplateInfo();

            string itemId = Request.QueryString["id"];
            if (itemId != null)
            {
                Item currentItem = SitecoreHelper.ItemMethods.GetItemFromGUIDInMaster(itemId);

                hotspotHotspottemplate currentTemplateHotspotSetting = HotspotTemplateInfo.Items
                    .FirstOrDefault(x => x.sourcetemplate[0].Value == currentItem.TemplateID.ToString());

                if (currentTemplateHotspotSetting != null)
                {
                    Field rawField = currentItem.Fields[currentTemplateHotspotSetting.sourceimagefieldname[0].Value];
                    bool fieldIsValidAndHasContent = rawField != null && !string.IsNullOrEmpty(rawField.Value) &&
                                                    FieldTypeManager.GetField(rawField) is ImageField;
                    if (fieldIsValidAndHasContent)
                    {
                        ImageField imageField = rawField;
                        if (imageField.MediaItem != null)
                        {
                            var mediaUrlOptions = new MediaUrlOptions { UseItemPath = false, AbsolutePath = true, Database = Factory.GetDatabase("master") };
                            string imageUrl = MediaManager.GetMediaUrl(imageField.MediaItem, mediaUrlOptions);

                            if (!string.IsNullOrWhiteSpace(imageUrl))
                            {
                                imgResource.Visible = true;
                                lblNoImageFound.Visible = false;
                                imgResource.ImageUrl = Request.Url.Scheme + "://" + Request.Url.Host + imageUrl;
                            }
                            else
                            {
                                imgResource.Visible = false;
                                lblNoImageFound.Visible = true;
                            }
                        }
                        else
                        {
                            imgResource.Visible = false;
                            lblNoImageFound.Visible = true;
                        }
                    }
                    else
                    {
                        imgResource.Visible = false;
                        lblNoImageFound.Visible = true;
                    }

                    hiddenHotspotTemplateId.Value = currentTemplateHotspotSetting.hotspotitemtemplate[0].Value;

                    if (imgResource.Visible)
                    {
                        List<Item> currentHotspots = currentItem.HasChildren
                            ? currentItem.GetChildren().InnerChildren
                                .Where(x => x.TemplateID.ToString() == currentTemplateHotspotSetting.hotspotitemtemplate[0].Value).ToList()
                            : null;

                        if (currentHotspots != null && currentHotspots.Any())
                        {
                            HotSpotsJson = new JavaScriptSerializer().Serialize(new
                            {
                                HotSpots = currentHotspots.Select(x => new
                                {
                                    Description = x.Name + ": "
                                                  + SitecoreHelper.ItemRenderMethods.GetRawValueByFieldName("Description", x, false),
                                    Left = SitecoreHelper.ItemRenderMethods.GetRawValueByFieldName("Left Coordinate", x, false),
                                    Top = SitecoreHelper.ItemRenderMethods.GetRawValueByFieldName("Right Coordinate", x, false)
                                })
                            });
                        }
                    }
                }
            }
        }

        protected void btnSubmit_Click(object sender, EventArgs e)
        {
            int? hotspotCounter = Request.Form["hotspotCounter"].SafeToInt();
            string itemId = Request.QueryString["id"];
            Item currentItem = SitecoreHelper.ItemMethods.GetItemFromGUIDInMaster(itemId);

            if (hotspotCounter != null)
            {
                using (new SecurityDisabler())
                {
                    for (int i = 1; i <= hotspotCounter; i++)
                    {
                        Item hotspotItem = AddUpdateHelper.CreateSitecoreItemUsingTemplate(currentItem.Paths.FullPath,
                            hiddenHotspotTemplateId.Value, "Hotspot" + DateTime.Now.ToString("ddhhmmssfff"));

                        hotspotItem.Editing.BeginEdit();
                        hotspotItem.Fields[IInteractive_Point_Of_InterestConstants.POI_Top_CoordinateFieldName].Value = Request.Form["txtTop_" + i];
                        hotspotItem.Fields[IInteractive_Point_Of_InterestConstants.POI_Left_CoordinateFieldName].Value = Request.Form["txtLeft_" + i];
                        hotspotItem.Fields[IInteractive_Point_Of_InterestConstants.POI_DescriptionFieldName].Value = Request.Form["txtDesc_" + i];
                        hotspotItem.Fields[IInteractive_Point_Of_InterestConstants.POI_TitleFieldName].Value = "Hotspot" + i;
                        hotspotItem.Editing.EndEdit();
                    }
                }
            }
        }

        private void LoadHotspotTemplateInfo()
        {
            if (Application["HotspotTemplateInfo"] != null)
            {
                HotspotTemplateInfo = (hotspot)Application["HotspotTemplateInfo"];
                return;
            }

            XmlSerializer ser = new XmlSerializer(typeof(hotspot));
            using (XmlReader reader = XmlReader.Create(AppDomain.CurrentDomain.BaseDirectory
                + "\\sitecore modules\\Shell\\Editors\\Hotspots\\HotspotsTemplateData.xml"))
            {
                HotspotTemplateInfo = (hotspot)ser.Deserialize(reader);
                Application["HotspotTemplateInfo"] = HotspotTemplateInfo;
            }
        }
    }
}

XML Configuration

In the above code, you’ll see that we are reading some configuration from an xml file. What we are essentially doing in this xml file, is specifying by template, which field will contain the image that we need to load in the hotspot tab, and the hotspot item template to use. We are using the same base template for hotspots, which is why you see the field names hardcoded in the code above. You could however include these field names also in the xml configuration if your field names between templates are likely to change!

Here’s a sample of the xml in use:

<?xml version="1.0" encoding="utf-8" ?>

<hotspot>

  <hotspottemplate>
    <sourcetemplate hint="/sitecore/templates/User Defined/MySite/Components/Modules/Enhanced POI Module">{85FB47EE-CFD4-405F-84A7-B53BF46CDA25}</sourcetemplate>
    <sourceimagefieldname hint="">Background Image</sourceimagefieldname>
    <hotspotitemtemplate hint="/sitecore/templates/User Defined/MySite/Components/Data/Point Of Interest">{43FB275D-9157-48B3-A68E-F143F044B7EE}</hotspotitemtemplate>
  </hotspottemplate>

</hotspot>

Auto generated cs file for the XML config

I used xsd.exe from Visual Studio developer tools command prompt to generate the class (hotspot) for this xml. You could alternatively do this manually!

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:4.0.30319.18444
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

using System.Xml.Serialization;

// 
// This source code was auto-generated by xsd, Version=4.0.30319.33440.
// 


/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.0.30319.33440")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true)]
[System.Xml.Serialization.XmlRootAttribute(Namespace="", IsNullable=false)]
public partial class hotspot {
    
    private hotspotHotspottemplate[] itemsField;
    
    /// <remarks/>
    [System.Xml.Serialization.XmlElementAttribute("hotspottemplate", Form=System.Xml.Schema.XmlSchemaForm.Unqualified)]
    public hotspotHotspottemplate[] Items {
        get {
            return this.itemsField;
        }
        set {
            this.itemsField = value;
        }
    }
}

/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.0.30319.33440")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true)]
public partial class hotspotHotspottemplate {
    
    private hotspotHotspottemplateSourcetemplate[] sourcetemplateField;
    
    private hotspotHotspottemplateSourceimagefieldname[] sourceimagefieldnameField;
    
    private hotspotHotspottemplateHotspotitemtemplate[] hotspotitemtemplateField;
    
    /// <remarks/>
    [System.Xml.Serialization.XmlElementAttribute("sourcetemplate", Form=System.Xml.Schema.XmlSchemaForm.Unqualified, IsNullable=true)]
    public hotspotHotspottemplateSourcetemplate[] sourcetemplate {
        get {
            return this.sourcetemplateField;
        }
        set {
            this.sourcetemplateField = value;
        }
    }
    
    /// <remarks/>
    [System.Xml.Serialization.XmlElementAttribute("sourceimagefieldname", Form=System.Xml.Schema.XmlSchemaForm.Unqualified, IsNullable=true)]
    public hotspotHotspottemplateSourceimagefieldname[] sourceimagefieldname {
        get {
            return this.sourceimagefieldnameField;
        }
        set {
            this.sourceimagefieldnameField = value;
        }
    }
    
    /// <remarks/>
    [System.Xml.Serialization.XmlElementAttribute("hotspotitemtemplate", Form=System.Xml.Schema.XmlSchemaForm.Unqualified, IsNullable=true)]
    public hotspotHotspottemplateHotspotitemtemplate[] hotspotitemtemplate {
        get {
            return this.hotspotitemtemplateField;
        }
        set {
            this.hotspotitemtemplateField = value;
        }
    }
}

/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.0.30319.33440")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true)]
public partial class hotspotHotspottemplateSourcetemplate {
    
    private string hintField;
    
    private string valueField;
    
    /// <remarks/>
    [System.Xml.Serialization.XmlAttributeAttribute()]
    public string hint {
        get {
            return this.hintField;
        }
        set {
            this.hintField = value;
        }
    }
    
    /// <remarks/>
    [System.Xml.Serialization.XmlTextAttribute()]
    public string Value {
        get {
            return this.valueField;
        }
        set {
            this.valueField = value;
        }
    }
}

/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.0.30319.33440")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true)]
public partial class hotspotHotspottemplateSourceimagefieldname {
    
    private string hintField;
    
    private string valueField;
    
    /// <remarks/>
    [System.Xml.Serialization.XmlAttributeAttribute()]
    public string hint {
        get {
            return this.hintField;
        }
        set {
            this.hintField = value;
        }
    }
    
    /// <remarks/>
    [System.Xml.Serialization.XmlTextAttribute()]
    public string Value {
        get {
            return this.valueField;
        }
        set {
            this.valueField = value;
        }
    }
}

/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.0.30319.33440")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true)]
public partial class hotspotHotspottemplateHotspotitemtemplate {
    
    private string hintField;
    
    private string valueField;
    
    /// <remarks/>
    [System.Xml.Serialization.XmlAttributeAttribute()]
    public string hint {
        get {
            return this.hintField;
        }
        set {
            this.hintField = value;
        }
    }
    
    /// <remarks/>
    [System.Xml.Serialization.XmlTextAttribute()]
    public string Value {
        get {
            return this.valueField;
        }
        set {
            this.valueField = value;
        }
    }
}

Stylesheet

Following is the hotspot.css that was used (crude alert!)

.resource-image {
    max-height: 700px;
    max-width: 700px;
    border: 1px solid black;
    margin-left: 20px;
    margin-top: 20px;
}

.desc {
    width: 400px;
}

.dimension {
    width: 30px;
}

.submit {
    display: none;
}

.delete {
    width: 20px;
    height: 20px;
    background-repeat: no-repeat;
    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAABGdBTUEAALGeYUxB9wAAACBjSFJNAAB6JQAAgIMAAPn/AACA6AAAUggAARVYAAA6lwAAF2/XWh+QAAAACXBIWXMAAA7EAAAOxAGVKw4bAAACoklEQVQ4T62V3UtTcRjHv8uXdNYUp2y+YJgQGGFYkgUqFXVldTWILvwLAm+CLIRICLwML7oJRLzzouiFUkMpnZSiOY/T1LmZ5Zpz5pxm0/m2p+ec83M69RCIH3g4v/M8z/me58fvyzk6YnCIHBFXhTDHpro8MJEJA55phCddWJ/3Y/NEHsxnCxCjtGizwRHo/oIVSYJzPoAr1dWqYMDtRoJjCIkba6BpNzZ4PbVGSHlSB2OSXn16B7LQbHs7FuueIpiZiT8lZSi2WJCUmKhu+ajRCNfMHILDA9AFZqBb/YuTji4sF2bj56hDbomwuLSE8Zvl8N+5je+FhUh7VIOrFRWKmII84RaOwSEau3+XFi9lUvDMMZrLiaFRbhl59UapuxobqZfvO5OP04emJiW3myhBmRWOsdZmsueZaTIG5NSDvrJIt8VCfXx9HxdLNptNbd6HPYJbLHD0P66hPh1ogIVsHG9zc2l8RX6lNlG22UkyR8aNcoRi45RDkO1k8HphWF+Xy5poCrrb2uAsKoKeBX4ZU+E3m5ERCmGw9LLo0EBMGsVwbS194lI/R4vBQA7e5irnrWWlFOSc9cE9tXEf9ghOvHxBHfyQfBCfOTpaWkSF/RpcpqEL+TSbCpqUJJGNJkrQ53TSRyE2zPG6slJUtvGFQuQygHrP5VNY5HYSJdhpMimnyVam1pws8on8bn5IA+TiHqn5nchsExGcslpJimcjJ3BjPPuuo0tU9sdeVUWDp/JoWdxvERGcqH9OnlvXyFuQRY5cPXl9WvOpbHJYkxLI3dOjJgQR21C6CSnF55FechHhLDP7TtNRCnI1u74B/pFvakKw/fniz8/vhmdI8zjQZzqN61UP/yOpIts8Tl2qKHMKFgILZJfsynYOyiH/AoB/WsAsVQ1Uw2EAAAAASUVORK5CYII=);
}

.hotspot-image-wrapper {
    position: relative;
    display: inline-block;
}

#existingHotspots span {
    position: absolute;
}

    #existingHotspots span:before {
        display: inline-block;
        background-color: yellow;
        width: 20px;
        height: 20px;
        content: "";
        border-radius: 50%;
    }

#tableHotSpots {
    margin-left: 20px;
    margin-top: 20px;
    font-family: sans-serif;
    font-size: 13px;
}

#btnSubmit {
    margin-top: 10px;
    margin-left: 210px;
    margin-bottom: 20px;
}

Javascript

And here’s the javascript – hotspot.js (another crude alert – using a template engine would be well advised here!)

$(document).ready(function () {
    var hotspotsobj = $("div[data-hotspot]").data("hotspot");

    if (hotspotsobj != "") {
        $.each(hotspotsobj.HotSpots, function(key, value) {
            $("#existingHotspots").append("<span style=\"left:" + value.Left + "%;top:" + value.Top + "%;\" title=\"" + value.Description + "\"></span>");
        });
    }

    $(document).on("click", ".delete", function() {
        if (confirm("Delete Hotspot?")) {
            $(this).closest("tr").remove();
            if ($("#tableHotSpots tr").length == 1) {
                $("#tableHotSpots tr").remove();
                $(".submit").hide();
            }
        }
    });

    $("#imgResource").click(function (e) {
        if ($(".not-available").length > 0) {
            return;
        }

        if ($(this).attr("src").value != "") {
            var offset_t = $(this).offset().top - $(window).scrollTop();
            var offset_l = $(this).offset().left - $(window).scrollLeft();

            var left = Math.round((e.clientX - offset_l));
            var top = Math.round((e.clientY - offset_t));

            var percentageLeft = Math.round((left / $(this).width()) * 100);
            var percentageTop = Math.round((top / $(this).height()) * 100);

            var hotspotCount = $("#tableHotSpots tr").length;

            if (hotspotCount == 0) {
                $("#tableHotSpots").append("<tr><td>Description</td><td>Left</td><td>Top</td><td></td></tr>");
                hotspotCount++;
                $(".submit").show();
            }

            $("#tableHotSpots").append("<tr><td><input class=\"desc\" name=\"txtDesc_" + hotspotCount + "\" type=\"text\"></input></td>"
                + "<td><input class=\"dimension\" type=\"text\" name=\"txtLeft_" + hotspotCount + "\" value=\"" + percentageLeft + "\"></input></td>"
                + "<td><input class=\"dimension\" type=\"text\" name=\"txtTop_" + hotspotCount + "\" value=\"" + percentageTop + "\"></input></td>"
                + "<td><div class=\"delete\"></div></td></tr>");

            $("#tableHotSpots tr:last-child td:first-child input").focus();

            $("#hotspotCounter").val(hotspotCount);
        }
    });
});

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()
        { }
    }