Home » Customization » Dude, Where’s My Processor? Filling the Void in the SaveRichTextContent and LoadRichTextContent Sitecore Pipelines

Dude, Where’s My Processor? Filling the Void in the SaveRichTextContent and LoadRichTextContent Sitecore Pipelines

Sitecore Technology MVP 2016
Sitecore MVP 2015
Sitecore MVP 2014

Enter your email address to follow this blog and receive notifications of new posts by email.

Some of you might be aware that I frequently go through the Web.config of my local instance of Sitecore looking for opportunities to extend or customize class files referenced within it — I may have mentioned this in a previous post, and no doubt have told some Sitecore developers/enthusiasts in person I do this at least once per day. I must confess: I usually do this multiple times a day.

Last night, I was driven to explore something I have noticed in the Web.config of my v6.5 instance — my attention has been usurped many times by the saveRichTextContent and loadRichTextContent pipeline nodes being empty.

These two pipelines allow you to make changes to content within Rich Text fields before any save actions on your item in the Sitecore client.

I remembered that one of them did have a pipeline processor defined within it at one point. It was time to do some research.

After conducting some research — truth be told, I only googled a couple of times — I stumbled upon some release notes on SDN discussing the saveRichTextContent Web.config pipeline, and that this pipeline did contain a processor in it at one point — the Sitecore.Shell.Controls.RichTextEditor.Pipelines.SaveRichTextContent.EmbedInParagraph processor — although I don’t remember what this processor did, and don’t have an older version of Sitecore.Client.dll to investigate. I could download an older version of Sitecore from SDN, but decided to leave that exercise for another snowy weekend.

I decided to explore whether the option to add custom processors to these pipelines still existed. I came up with an idea straight out of the 1990’s — having marquee tags animate content across my pages.

As an aside, back in the 1990’s, almost every webpage — all webpages were called homepages then — had at least one marquee. Most had multiple — it was the cool thing to do back then, asymptotic only to having an ‘Under Construction’ image on your homepage. Employing this practice today would be considered anathema.

I decided to reuse my concept of manipulator from my Manipulate Field Values in a Custom Sitecore Web Forms for Marketers DataProvider article, and created a new manipulator to wrap specified tags in marquee tags:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Sitecore.Sandbox.Utilities.Manipulators.Base
{
    public interface IWrapHtmlTagsInTagManipulator : IManipulator<string>
    {
    }
}

I thought it would be a good idea to define a DTO for my manipulator to pass objects to it in a clean manner:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Sitecore.Sandbox.Utilities.Manipulators.DTO
{
    public class WrapHtmlTagsInTagManipulatorSettings
    {
        public string WrapperTag { get; set; }
        public IEnumerable<string> TagsToWrap { get; set; }
    }
}

Next, I built my manipulator:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Diagnostics;

using Sitecore.Sandbox.Utilities.Manipulators.Base;
using Sitecore.Sandbox.Utilities.Manipulators.DTO;

using HtmlAgilityPack;

namespace Sitecore.Sandbox.Utilities.Manipulators
{
    public class WrapHtmlTagsInTagManipulator : IWrapHtmlTagsInTagManipulator
    {
        private WrapHtmlTagsInTagManipulatorSettings Settings { get; set; }

        private WrapHtmlTagsInTagManipulator(WrapHtmlTagsInTagManipulatorSettings settings)
        {
            SetSettings(settings);
        }

        private void SetSettings(WrapHtmlTagsInTagManipulatorSettings settings)
        {
            AssertSettings(settings);
            Settings = settings;
        }

        private static void AssertSettings(WrapHtmlTagsInTagManipulatorSettings settings)
        {
            Assert.ArgumentNotNull(settings, "settings");
            Assert.ArgumentNotNullOrEmpty(settings.WrapperTag, "settings.WrapperTag");
            Assert.ArgumentNotNull(settings.TagsToWrap, "settings.TagsToWrap");
        }

        public string Manipulate(string html)
        {
            Assert.ArgumentNotNullOrEmpty(html, "html");
            HtmlNode documentNode = GetHtmlDocumentNode(html);

            foreach (string tagToWrap in Settings.TagsToWrap)
            {
                WrapTags(documentNode, tagToWrap);
            }

            return documentNode.InnerHtml;
        }

        private void WrapTags(HtmlNode documentNode, string tagToWrap)
        {
            HtmlNodeCollection htmlNodes = documentNode.SelectNodes(CreateNewDescendantsSelector(tagToWrap));

            foreach(HtmlNode htmlNode in htmlNodes)
            {
                WrapHtmlNodeIfApplicable(documentNode, htmlNode);
            }
        }

        private void WrapHtmlNodeIfApplicable(HtmlNode documentNode, HtmlNode htmlNode)
        {
            if (!AreEqualIgnoreCase(htmlNode.ParentNode.Name, Settings.WrapperTag))
            {
                WrapHtmlNode(documentNode, htmlNode, Settings.WrapperTag);
            }
        }

        private static void WrapHtmlNode(HtmlNode documentNode, HtmlNode htmlNode, string wrapperTag)
        {
            HtmlNode wrapperHtmlNode = documentNode.OwnerDocument.CreateElement(wrapperTag);
            AddNewParent(wrapperHtmlNode, htmlNode);
        }

        private static void AddNewParent(HtmlNode newParentHtmlNode, HtmlNode htmlNode)
        {
            Assert.ArgumentNotNull(newParentHtmlNode, "newParentHtmlNode");
            Assert.ArgumentNotNull(htmlNode, "htmlNode");
            htmlNode.ParentNode.ReplaceChild(newParentHtmlNode, htmlNode);
            newParentHtmlNode.AppendChild(htmlNode);
        }

        private static bool AreEqualIgnoreCase(string stringOne, string stringTwo)
        {
            return string.Equals(stringOne, stringTwo, StringComparison.InvariantCultureIgnoreCase);
        }

        private static string CreateNewDescendantsSelector(string tag)
        {
            Assert.ArgumentNotNullOrEmpty(tag, "tag");
            return string.Format("//{0}", tag);
        }

        private HtmlNode GetHtmlDocumentNode(string html)
        {
            HtmlDocument htmlDocument = CreateNewHtmlDocument(html);
            return htmlDocument.DocumentNode;
        }

        private HtmlDocument CreateNewHtmlDocument(string html)
        {
            HtmlDocument htmlDocument = new HtmlDocument();
            htmlDocument.LoadHtml(html);
            return htmlDocument;
        }

        public static IWrapHtmlTagsInTagManipulator CreateNewWrapHtmlTagsInTagManipulator(WrapHtmlTagsInTagManipulatorSettings settings)
        {
            return new WrapHtmlTagsInTagManipulator(settings);
        }
    }
}

My manipulator class above uses Html Agility Pack to find targeted html elements, and wrap them in newly created marquee tags — which are also created via Html Agility Pack.

I decided to create a base class to contain core logic that will be used across both of my pipeline processors:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Sandbox.Utilities.Manipulators;
using Sitecore.Sandbox.Utilities.Manipulators.Base;
using Sitecore.Sandbox.Utilities.Manipulators.DTO;

namespace Sitecore.Sandbox.Shell.Controls.RichTextEditor.Pipelines.Base
{
    public abstract class AddSomeMarqueesBase
    {
        private IWrapHtmlTagsInTagManipulator _HtmlManipulator;
        private IWrapHtmlTagsInTagManipulator HtmlManipulator
        {
            get
            {
                if(_HtmlManipulator == null)
                {
                    _HtmlManipulator = CreateNewWrapHtmlTagsInTagManipulator();
                }

                return _HtmlManipulator;
            }
        }

        private IWrapHtmlTagsInTagManipulator CreateNewWrapHtmlTagsInTagManipulator()
        {
            return WrapHtmlTagsInTagManipulator.CreateNewWrapHtmlTagsInTagManipulator(CreateNewWrapHtmlTagsInTagManipulatorSettings());
        }

        protected virtual WrapHtmlTagsInTagManipulatorSettings CreateNewWrapHtmlTagsInTagManipulatorSettings()
        {
            return new WrapHtmlTagsInTagManipulatorSettings
            {
                WrapperTag = "marquee",
                TagsToWrap = new string[] { "em", "img" }
            };
        }

        protected virtual string ManipulateHtml(string html)
        {
            if (!string.IsNullOrEmpty(html))
            {
                return HtmlManipulator.Manipulate(html);
            }

            return html;
        }
    }
}

This base class creates an instance of our manipulator class above, passing in the required DTO housing the wrapper tag and tags to wrap settings.

Honestly, while writing this article and looking at this code, I am not completely happy about how I implemented this base class. I should have added a constructor which takes in the manipulator instance — thus allowing subclasses to provide their own manipulators, especially if these subclasses need to use a different manipulator than the one used by default in the base class.

Further, it probably would have been prudent to put the html tags I defined in my DTO instance into a patch config file.

Next, I defined my loadRichTextContent pipeline processor:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Shell.Controls.RichTextEditor.Pipelines.LoadRichTextContent;

using Sitecore.Sandbox.Shell.Controls.RichTextEditor.Pipelines.Base;

namespace Sitecore.Sandbox.Shell.Controls.RichTextEditor.Pipelines.LoadRichTextContent
{
    public class AddSomeMarquees : AddSomeMarqueesBase
    {
        public void Process(LoadRichTextContentArgs args)
        {
            args.Content = ManipulateHtml(args.Content);
        }
    }
}

Followed by my saveRichTextContentpipeline processor:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Shell.Controls.RichTextEditor.Pipelines.SaveRichTextContent;

using Sitecore.Sandbox.Shell.Controls.RichTextEditor.Pipelines.Base;

namespace Sitecore.Sandbox.Shell.Controls.RichTextEditor.Pipelines.SaveRichTextContent
{
    public class AddSomeMarquees : AddSomeMarqueesBase
    {
        public void Process(SaveRichTextContentArgs args)
        {
            args.Content = ManipulateHtml(args.Content);
        }
    }
}

Thereafter, I glued everything together via a patch config file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <loadRichTextContent>
        <processor type="Sitecore.Sandbox.Shell.Controls.RichTextEditor.Pipelines.LoadRichTextContent.AddSomeMarquees, Sitecore.Sandbox"/>
      </loadRichTextContent>
      <saveRichTextContent>
        <processor type="Sitecore.Sandbox.Shell.Controls.RichTextEditor.Pipelines.SaveRichTextContent.AddSomeMarquees, Sitecore.Sandbox"/>
      </saveRichTextContent>
    </pipelines>
  </sitecore>
</configuration>

Time to see the fruits of my labor above.

I’ve added some content in a Rich Text field:

RTF-Design-Before-Marquees

Here’s the html in the Rich Text field:

RTF-Html-Before-Marquees

I clicked the ‘Accept’ button in the Rich Text dialog window, and then saw the targeted content come to life:

RTF-With-Marquees

I launched the dialog window again to investigate what the html now looks like:

RTF-Html-With-Marquees

Mission accomplished — we now have marquees! 🙂

I do want to point out I could not get my loadRichTextContent pipeline processor to run. I thought it would run when opening the Rich Text dialog, although I was wrong — it did not. I also tried to get it to run via the ‘Edit Html’ button, but to no avail.

If I am looking in the wrong place, or this is a known issue in Sitecore, please drop a comment and let me know.

Advertisement

1 Comment

  1. Thanks a bunch, very useful

Comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: