Home » Search results for 'Sitecore Configuration Factory' (Page 3)

Search Results for: Sitecore Configuration Factory

Download Images and Save to the Media Library Via a Custom Content Editor Image Field in Sitecore

Yesterday evening — a Friday evening by the way (what, you don’t code on Friday evenings? πŸ˜‰ ) — I wanted to have a bit of fun by building some sort of customization in Sitecore but was struggling on what to build.

After about an hour of pondering, it dawned on me: I was determined to build a custom Content Editor Image field that gives content authors the ability to download images from a supplied URL; save the image to disk; upload the image into the Media Libary; and then set it on a custom Image field of an Item.

I’m sure someone has built something like this in the past and may have even uploaded a module that does this to the Sitecore Marketplace — I didn’t really look into whether this had already been done before since I wanted to have some fun by taking on the challenge. What follows is the fruit of that endeavor.

Before I move forward, I would like to caution you on using the code that follows — I have not rigorously tested this code at all so use at your own risk.

Before I began coding, I thought about how I wanted to approach this challenge. I decided I would build a custom Sitecore pipeline to handle this code. Why? Well, quite frankly, it gives you flexibility on customization, and also native Sitecore code is hugely composed of pipelines — why deviate from the framework?

First, I needed a class whose instances would serve as the custom pipeline’s arguments object. The following class was built for that:

using Sitecore.Data;
using Sitecore.Pipelines;

namespace Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary
{
    public class DownloadImageToMediaLibraryArgs : PipelineArgs
    {
        public Database Database { get; set; }

        public string ImageFileName { get; set; }

        public string ImageFilePath { get; set; }

        public string ImageItemName { get; set; }

        public string ImageUrl { get; set; }

        public string MediaId { get; set; }

        public string MediaLibaryFolderPath { get; set; }

        public string MediaPath { get; set; }

        public bool FileBased { get; set; }

        public bool IncludeExtensionInItemName { get; set; }

        public bool OverwriteExisting { get; set; }

        public bool Versioned { get; set; }
    }
}

I didn’t just start off with all of the properties you see on this class — it was an iterative process where I had to go back, add more and even remove some that were no longer needed. You will see why I have these on it from the code below.

I decided to employ the Template method pattern in this code, and defined the following abstract base class which all processors of my custom pipeline will sub-class:

using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary
{
    public abstract class DownloadImageToMediaLibraryProcessor
    {
        public void Process(DownloadImageToMediaLibraryArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if(!CanProcess(args))
            {
                AbortPipeline(args);
                return;
            }

            Execute(args);
        }

        protected abstract bool CanProcess(DownloadImageToMediaLibraryArgs args);

        protected virtual void AbortPipeline(DownloadImageToMediaLibraryArgs args)
        {
            args.AbortPipeline();
        }

        protected abstract void Execute(DownloadImageToMediaLibraryArgs args);
    }
}

All processors of the custom pipeline will have to implement the CanProcess and Execute methods above, and also have the ability to redefine the AbortPipeline method if needed.

The main magic for all processors happen in the Process method above — if the processor can process the data supplied via the arguments object, then it will do so using the Execute method. Otherwise, the pipeline will be aborted via the AbortPipeline method.

The following class serves as the first processor of the custom pipeline.

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.IO;

namespace Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary
{
    public class SetProperties : DownloadImageToMediaLibraryProcessor
    {
        private string UploadDirectory { get; set; }

        protected override bool CanProcess(DownloadImageToMediaLibraryArgs args)
        {
            Assert.IsNotNullOrEmpty(UploadDirectory, "UploadDirectory must be set in configuration!");
            Assert.IsNotNull(args.Database, "args.Database must be supplied!");
            return !string.IsNullOrWhiteSpace(args.ImageUrl)
                && !string.IsNullOrWhiteSpace(args.MediaLibaryFolderPath);
        }

        protected override void Execute(DownloadImageToMediaLibraryArgs args)
        {
            args.ImageFileName = GetFileName(args.ImageUrl);
            args.ImageItemName = GetImageItemName(args.ImageUrl);
            args.ImageFilePath = GetFilePath(args.ImageFileName);
        }

        protected virtual string GetFileName(string url)
        {
            Assert.ArgumentNotNullOrEmpty(url, "url");
            return FileUtil.GetFileName(url);
        }

        protected virtual string GetImageItemName(string url)
        {
            Assert.ArgumentNotNullOrEmpty(url, "url");
            string fileNameNoExtension = GetFileNameNoExtension(url);
            if(string.IsNullOrWhiteSpace(fileNameNoExtension))
            {
                return string.Empty;
            }

            return ItemUtil.ProposeValidItemName(fileNameNoExtension);
        }

        protected virtual string GetFileNameNoExtension(string url)
        {
            Assert.ArgumentNotNullOrEmpty(url, "url");
            return FileUtil.GetFileNameWithoutExtension(url);
        }

        protected virtual string GetFilePath(string fileName)
        {
            Assert.ArgumentNotNullOrEmpty(fileName, "fileName");
            return string.Format("{0}/{1}", FileUtil.MapPath(UploadDirectory), fileName);
        }
    }
}

Instances of the above class will only run if an upload directory is supplied via configuration (see the patch include configuration file down below); a Sitecore Database is supplied (we have to upload this image somewhere); an image URL is supplied (can’t download an image without this); and a Media Library folder is supplied (where are we storing this image?).

The Execute method then sets additional properties on the arguments object that the next processors will need in order to complete their tasks.

The following class serves as the second processor of the custom pipeline. This processor will download the image from the supplied URL:

using System.Net;

namespace Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary
{
    public class DownloadImage : DownloadImageToMediaLibraryProcessor
    {
        protected override bool CanProcess(DownloadImageToMediaLibraryArgs args)
        {
            return !string.IsNullOrWhiteSpace(args.ImageUrl)
                && !string.IsNullOrWhiteSpace(args.ImageFilePath);
        }

        protected override void Execute(DownloadImageToMediaLibraryArgs args)
        {
            using (WebClient client = new WebClient())
            {
                client.DownloadFile(args.ImageUrl, args.ImageFilePath);
            }
        }
    }
}

The processor instance of the above class will only execute when an image URL is supplied and a location on the file system is given — this is the location on the file system where the image will live before being uploaded into the Media Library.

If all checks out, the image is downloaded from the given URL into the specified location on the file system.

The next class serves as the third processor of the custom pipeline. This processor will upload the image on disk to the Media Library:

using System.IO;

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Resources.Media;
using Sitecore.Sites;

namespace Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary
{
    public class UploadImageToMediaLibrary : DownloadImageToMediaLibraryProcessor
    {
        private string Site { get; set; }

        protected override bool CanProcess(DownloadImageToMediaLibraryArgs args)
        {
            Assert.IsNotNullOrEmpty(Site, "Site must be set in configuration!");
            return !string.IsNullOrWhiteSpace(args.MediaLibaryFolderPath)
                && !string.IsNullOrWhiteSpace(args.ImageItemName)
                && !string.IsNullOrWhiteSpace(args.ImageFilePath)
                && args.Database != null;
        }

        protected override void Execute(DownloadImageToMediaLibraryArgs args)
        {
            MediaCreatorOptions options = new MediaCreatorOptions
            {
                Destination = GetMediaLibraryDestinationPath(args),
                FileBased = args.FileBased,
                IncludeExtensionInItemName = args.IncludeExtensionInItemName,
                OverwriteExisting = args.OverwriteExisting,
                Versioned = args.Versioned,
                Database = args.Database
            };
            
            MediaCreator creator = new MediaCreator();
            MediaItem mediaItem;
            using (SiteContextSwitcher switcher = new SiteContextSwitcher(GetSiteContext()))
            {
                using (FileStream fileStream = File.OpenRead(args.ImageFilePath))
                {
                    mediaItem = creator.CreateFromStream(fileStream, args.ImageFilePath, options);
                }
            }
            
            if (mediaItem == null)
            {
                AbortPipeline(args);
                return;
            }
            
            args.MediaId = mediaItem.ID.ToString();
            args.MediaPath = mediaItem.MediaPath;
        }

        protected virtual SiteContext GetSiteContext()
        {
            SiteContext siteContext = SiteContextFactory.GetSiteContext(Site);
            Assert.IsNotNull(siteContext, string.Format("The site: {0} does not exist!", Site));
            return siteContext;
        }

        protected virtual string GetMediaLibraryDestinationPath(DownloadImageToMediaLibraryArgs args)
        {
            return string.Format("{0}/{1}", args.MediaLibaryFolderPath, args.ImageItemName);
        }
    }
}

The processor instance of the class above will only run when we have a Media Library folder location; an Item name for the image; a file system path for the image; and a Database to upload the image to. I also ensure a “site” is supplied via configuration so that I can switch the site context — when using the default of “shell”, I was being brought to the image Item in the Media Library after it was uploaded which was causing the image not to be set on the custom Image field on the Item.

If everything checks out, we upload the image to the Media Library in the specified location.

The next class serves as the last processor of the custom pipeline. This processor just deletes the image from the file system (why keep it around since we are done with it?):

using Sitecore.IO;

namespace Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary
{
    public class DeleteImageFromFileSystem : DownloadImageToMediaLibraryProcessor
    {
        protected override bool CanProcess(DownloadImageToMediaLibraryArgs args)
        {
            return !string.IsNullOrWhiteSpace(args.ImageFilePath)
                && FileUtil.FileExists(args.ImageFilePath);
        }

        protected override void Execute(DownloadImageToMediaLibraryArgs args)
        {
            FileUtil.Delete(args.ImageFilePath);
        }
    }
}

The processor instance of the class above can only delete the image if its path is supplied and the file exists.

If all checks out, the image is deleted.

The next class is the class that serves as the custom Image field:

using System;
using System.Net;

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Pipelines;
using Sitecore.Shell.Framework;
using Sitecore.Web.UI.Sheer;

using Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary;

namespace Sitecore.Sandbox.Shell.Applications.ContentEditor
{
    public class Image : Sitecore.Shell.Applications.ContentEditor.Image
    {
        public Image()
            : base()
        {
        }

        public override void HandleMessage(Message message)
        {
            Assert.ArgumentNotNull(message, "message");
            if (string.Equals(message.Name, "contentimage:download", StringComparison.CurrentCultureIgnoreCase))
            {
                GetInputFromUser();
                return;
            }

            base.HandleMessage(message);
        }

        protected void GetInputFromUser()
        {
            RunProcessor("GetImageUrl", new ClientPipelineArgs());
        }

        protected virtual void GetImageUrl(ClientPipelineArgs args)
        {
            if (!args.IsPostBack)
            {
                SheerResponse.Input("Enter the url of the image to download:", string.Empty);
                args.WaitForPostBack();
            }
            else if (args.HasResult && IsValidUrl(args.Result))
            {
                args.Parameters["imageUrl"] = args.Result;
                args.IsPostBack = false;
                RunProcessor("ChooseMediaLibraryFolder", args);
            }
            else
            {
                CancelOperation(args);
            }
        }

        protected virtual bool IsValidUrl(string url)
        {
            if (string.IsNullOrWhiteSpace(url))
            {
                return false;
            }

            try
            {
                HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url);
                request.Method = "HEAD";
                request.GetResponse();
            }
            catch (Exception ex)
            {
                SheerResponse.Alert("The specified url is not valid. Please try again.");
                return false;
            }

            return true;
        }

        protected virtual void RunProcessor(string processor, ClientPipelineArgs args)
        {
            Assert.ArgumentNotNullOrEmpty(processor, "processor");
            Sitecore.Context.ClientPage.Start(this, processor, args);
        }

        public void ChooseMediaLibraryFolder(ClientPipelineArgs args)
        {
            if (!args.IsPostBack)
            {
                Dialogs.BrowseItem
                (
                    "Select A Media Library Folder",
                    "Please select a media library folder to store this image.",
                    "Applications/32x32/folder_into.png",
                    "OK",
                    "/sitecore/media library", 
                    string.Empty
                );

                args.WaitForPostBack();
            }
            else if (args.HasResult)
            {
                Item folder = Client.ContentDatabase.Items[args.Result];
                args.Parameters["mediaLibaryFolderPath"] = folder.Paths.FullPath;
                RunProcessor("DownloadImage", args);
            }
            else
            {
                CancelOperation(args);
            }
        }

        protected virtual void DownloadImage(ClientPipelineArgs args)
        {
            DownloadImageToMediaLibraryArgs downloadArgs = new DownloadImageToMediaLibraryArgs
            {
                Database = Client.ContentDatabase,
                ImageUrl = args.Parameters["imageUrl"],
                MediaLibaryFolderPath = args.Parameters["mediaLibaryFolderPath"]
            };

            CorePipeline.Run("downloadImageToMediaLibrary", downloadArgs);
            SetMediaItemInField(downloadArgs);
        }

        protected virtual void SetMediaItemInField(DownloadImageToMediaLibraryArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if(string.IsNullOrWhiteSpace(args.MediaId) || string.IsNullOrWhiteSpace(args.MediaPath))
            {
                return;
            }

            XmlValue.SetAttribute("mediaid", args.MediaId);
            Value = args.MediaPath;
            Update();
            SetModified();
        }

        protected virtual void CancelOperation(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            args.AbortPipeline();
        }
    }
}

The class above subclasses the Sitecore.Shell.Applications.ContentEditor.Image class — this lives in Sitecore.Kernel.dll — which is the “out of the box” Content Editor Image field. The Sitecore.Shell.Applications.ContentEditor.Image class provides hooks that we can override in order to augment functionality which I am doing above.

The magic of this class starts in the HandleMessage method — I intercept the message for a Menu item option that I define below for downloading an image from a URL.

If we are to download an image from a URL, we first prompt the user for a URL via the GetImageUrl method using a Sheer UI api call (note: I am running these methods as one-off client pipeline processors as this is the only way you can get Sheer UI to run properly).

If we have a valid URL, we then prompt the user for a Media Library location via another Sheer UI dialog (this is seen in the ChooseMediaLibraryFolder method).

If the user chooses a location in the Media Library, we then call the DownloadImage method as a client pipeline processor — I had to do this since I was seeing some weird behavior on when the image was being saved into the Media Library — which invokes the custom pipeline for downloading the image to the file system; uploading it into the Media Library; and then removing it from disk.

I then duct-taped everything together using the following patch include configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <controlSources>
      <source mode="on" namespace="Sitecore.Sandbox.Shell.Applications.ContentEditor" assembly="Sitecore.Sandbox" prefix="sandbox-content"/>
    </controlSources>
    <overrideDialogs>
      <override dialogUrl="/sitecore/shell/Applications/Item%20browser.aspx" with="/sitecore/client/applications/dialogs/InsertSitecoreItemViaTreeDialog">
        <patch:delete/>
      </override>
    </overrideDialogs>
    <pipelines>
      <downloadImageToMediaLibrary>
        <processor type="Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary.SetProperties, Sitecore.Sandbox">
          <UploadDirectory>/upload</UploadDirectory>
        </processor>  
        <processor type="Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary.DownloadImage, Sitecore.Sandbox" />
        <processor type="Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary.UploadImageToMediaLibrary, Sitecore.Sandbox">
          <Site>website</Site>
        </processor>
        <processor type="Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary.DeleteImageFromFileSystem, Sitecore.Sandbox" />
      </downloadImageToMediaLibrary>
    </pipelines>
  </sitecore>
</configuration>

One thing to note in the above file: I’ve disabled the SPEAK dialog for the Item Browser — you can see this in the <overrideDialogs> xml element — as I wasn’t able to set messaging text on it but could do so using the older Sheer UI dialog.

Now that all code is in place, we need to tell Sitecore that we have a new Image field. I do so by defining it in the Core database:

external-image-core-1

We also need a new Menu item for the “Download Image” link:

external-image-core-2

Let’s take this for a spin!

I added a new field using the custom Image field type to my Sample item template:

template-new-field-external-image

As you can see, we have this new Image field on my “out of the box” home item. Let’s click the “Download Image” link:

home-external-image-1

I was then prompted with a dialog to supply an image URL. I pasted one I found on the internet:

home-external-image-2

After clicking “OK”, I was prompted with another dialog to choose a Media Library location for storing the image. I chose some random folder:

home-external-image-3

After clicking “OK” on that dialog, the image was magically downloaded from the internet; uploaded into the Media Library; and set in the custom Image field on my home item:

home-external-image-4

If you have any comments, thoughts or suggestions on this, please drop a comment.

Addendum:
It’s not a good idea to use the /upload directory for temporarily storing download images — see this post for more details.

If you decide to use this solution — by the way, use this solution at your own risk πŸ˜‰ — you will have to change the /upload directory in the patch include configuration file above.

Inject Dependencies Into Sitecore MVC Razor Views Using a SitecoreHelper Extension Method

Last week before the Christmas holiday break, I came up with a solution similar to what I am going to show here except that I had used the Simple Injector Dependency Injection framework in that solution (if you would like me to add a blog post on that approach, please let me know in a comment).

I wanted a “quick and dirty” solution — well, I guess this is really in the eye of the beholder on whether the following is a good idea or not, but I’m throwing it out there just in case it helps out someone — where I could inject a dependency into an MVC Razor view via an HtmlHelper extension method, and in that solution, I used this approach for injecting an object instance which grabs values from a Sitecore Dictionary Domain. I am going to recreate this solution here though using the Sitecore Configuration Factory (we’re all not using Simple Injector in our solutions but the Sitecore Configuration Factory is available to all of us via the Sitecore API).

I first defined the following interface for class instances which return a value for a given dictionary entry key:

using Sitecore.Globalization;

namespace Sitecore.Sandbox.Dictionaries
{
    public interface ITranslator
    {
        string Text(string key, params object[] parameters);

        string Text(TranslateOptions options, string key, params object[] parameters);
    }
}

The following class implements the interface above:

using Sitecore.Diagnostics;
using Sitecore.Globalization;

namespace Sitecore.Sandbox.Dictionaries
{
    public class DomainDictionaryTranslator : ITranslator
    {
        public string Domain { get; set; }

        public virtual string Text(string key, params object[] parameters)
        {
            AssertDomainDictionary();
            Assert.ArgumentNotNullOrEmpty(key, "key");
            return Translate.TextByDomain(Domain, key, parameters);
        }

        public virtual string Text(TranslateOptions options, string key, params object[] parameters)
        {
            AssertDomainDictionary();
            Assert.ArgumentNotNullOrEmpty(key, "key");
            return Translate.TextByDomain(Domain, options, key, parameters);
        }

        protected virtual void AssertDomainDictionary()
        {
            Assert.IsNotNullOrEmpty(Domain, "The Domain must be set!");
        }
    }
}

Client code of the class above must supply the Domain name for the Sitecore Dictionary via the Domain property on the class.

Client code can then use either Text method by supplying a key for the lookup — both methods just delegate to the corresponding static methods defined on the Sitecore.Globalization.Translate class.

We now need classes that serve as Factories. I created the following interface for such classes:

namespace Sitecore.Sandbox.Helpers.Sitecore
{
    public interface IFactory
    {
        T CreateObject<T>(string configPath, bool assert) where T : class;
    }
}

The following class implements the above interface, and basically has one method that delegates to the static CreateObject() method on the Sitecore.Configuration.Factory class:

using Sitecore.Configuration;

namespace Sitecore.Sandbox.Helpers.Sitecore
{
    public class ConfigurationFactory : IFactory
    {
        public T CreateObject<T>(string configPath, bool assert) where T : class
        {
            return Factory.CreateObject(configPath, assert) as T;
        }
    }
}

Now, we need an extension method on the Sitecore.Mvc.Helpers.SitecoreHelper class — this lives in Sitecore.Mvc.dll — so that we can leverage the code defined above (check out Sitecore MVP Kevin BrechbΓΌhl’s blog post where he talks about this approach as well as another):

using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.Mvc.Helpers;

namespace Sitecore.Sandbox.Helpers.Sitecore
{
    public static class ConfigurationFactoryHelper
    {
        private static IFactory ConfigurationFactory { get; set; }

        static ConfigurationFactoryHelper()
        {
            ConfigurationFactory = CreateFactory();
        }

        public static T CreateObject<T>(this SitecoreHelper sitecoreHelper, string configPath, bool assert) where T : class
        {
            return ConfigurationFactory.CreateObject<T>(configPath, assert);
        }

        private static IFactory CreateFactory()
        {
            IFactory factory = Factory.CreateObject("configurationFactory", true) as IFactory;
            Assert.IsNotNull(factory, "The configurationFactory must be defined in configuration!");
            return factory;
        }
    }
}

We are instantiating an instance of the ConfigurationFactory class defined above via the Sitecore Configuration Factory, and then setting it on a static property in this class — all members and methods on this class must be static since it contains extension methods.

The CreateObject() method just delegates to the ConfigurationFactory class instance’s CreateObject() method and returns its value.

I then glued everything together using the following Sitecore patch configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <configurationFactory type="Sitecore.Sandbox.Helpers.Sitecore.ConfigurationFactory, Sitecore.Sandbox" singleInstance="true" />
    <domainDictionaryTranslator type="Sitecore.Sandbox.Dictionaries.DomainDictionaryTranslator, Sitecore.Sandbox" singleInstance="true">
      <Domain>MyCoolDictionary</Domain>
    </domainDictionaryTranslator>
  </sitecore>
</configuration>

Let’s see this in action!

First, we need a Dictionary Domain. I created one with the following folder with entries:

label-one-dictionary

label-two-dictionary

Now, we new a Razor view to make this work. I created the following, and mapped it to my home page’s presentation details:

@using Sitecore.Mvc;
@using Sitecore.Sandbox.Helpers.Sitecore
@using Sitecore.Sandbox.Dictionaries

@{
    var translator = Html.Sitecore().CreateObject<ITranslator>("domainDictionaryTranslator", true);
    if (translator == null)
    {
        return;
    }

    var labelOne = translator.Text("mycooldictionary.somelabels.labelone");
    var labelTwo = translator.Text("mycooldictionary.somelabels.labeltwo");
    if (string.IsNullOrWhiteSpace(labelOne) && string.IsNullOrWhiteSpace(labelTwo))
    {
        return;
    }
}

<div>
    @if (!string.IsNullOrWhiteSpace(labelOne))
    {
        <h2>@labelOne</h2>
    }

    @if (!string.IsNullOrWhiteSpace(labelTwo))
    {
        <h2>@labelTwo</h2>
    }
</div>

After building and deploying, I navigated to my home page. As you can see, the dictionary entry values display:

dictionary-values-displayed

If you have any thoughts on this, please drop a comment.

Render a Custom General Link Field Attribute using a Custom Glass.Mapper Controller in Sitecore

In my previous posts — please be sure to read this post followed by this post and then this post before moving forward since you’ll need some context, and some of the code below is dependent on code in these previous posts — I showed two approaches for rendering a custom attribute — I called this attribute Tag in these posts and will continue to do so here — on a link set in a General Link field which is rendered using the Sitecore ORM Glass.Mapper.

In this post, I am going to share an approach on achieving the same but using a custom GlassController coupled with a class that implements the IGlassHtml interface — the code for this class can be found in this post.

First, we need a model to experiment with. I built the following class to serve as an example for this post:

using Sitecore.Data;

using Glass.Mapper.Sc.Configuration.Attributes;

using Sitecore.Sandbox.Glass.Mapper.Sc.Fields;

namespace Sitecore.Sandbox.Models
{
    public class SampleItemModel
    {
        [SitecoreId]
        ID ItemID { get; set; }

        public string Title { get; set; }

        public string Text { get; set; }

        [SitecoreField("Link One")]
        public TagLink LinkOne { get; set; }

        [SitecoreField("Link Two")]
        public TagLink LinkTwo { get; set; }
    }
}

Next, we need a ViewModel. I built the following as well to serve as an example for this post:

using System.Web.Mvc;

using Sitecore.Data;

using Glass.Mapper.Sc.Configuration.Attributes;

namespace Sitecore.Sandbox.Models.ViewModels
{
    public class SampleItemViewModel
    {
        public MvcHtmlString Title { get; set; }

        public MvcHtmlString Text { get; set; }

        public MvcHtmlString LinkOne { get; set; }

        public MvcHtmlString LinkTwo { get; set; }
    }
}

I then built the following subclass of Glass.Mapper.Sc.Web.Mvc.GlassController:

using System;
using System.Linq.Expressions;
using System.Web;
using System.Web.Mvc;

using Sitecore.Configuration;
using Sitecore.Diagnostics;

using Glass.Mapper.Sc;
using Glass.Mapper.Sc.Web.Mvc;
using Glass.Mapper.Sc.Web;
using GlassMapperSc = Glass.Mapper.Sc;

namespace Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc.Controllers
{
    public abstract class SandboxGlassController : GlassController
    {
        private static IGlassHtmlFactory GlassHtmlFactory { get; set; }

        static SandboxGlassController()
        {
            GlassHtmlFactory = CreateGlassHtmlFactory();
        }

        public SandboxGlassController() 
            : this(GetContextFromHttp())
        {
        }

        protected SandboxGlassController(ISitecoreContext sitecoreContext) 
            : this(sitecoreContext, GlassHtmlFactory.CreateGlassHtml(sitecoreContext), new RenderingContextMvcWrapper(), null)
        {
        }

        public SandboxGlassController(ISitecoreContext sitecoreContext, IGlassHtml glassHtml, IRenderingContext renderingContextWrapper, HttpContextBase httpContext)
            : base(sitecoreContext, glassHtml, renderingContextWrapper, httpContext)
        {
        }

        protected static IGlassHtmlFactory CreateGlassHtmlFactory()
        {
            IGlassHtmlFactory factory = Factory.CreateObject("sandbox.Glass.Mvc/glassHtmlFactory", true) as IGlassHtmlFactory;
            Assert.IsNotNull(factory, "Be sure the configuration is correct in utilities/customAttributesAdder of your Sitecore configuration!");
            return factory;
        }

        protected static ISitecoreContext GetContextFromHttp()
        {
            try
            {
                return GlassMapperSc.SitecoreContext.GetFromHttpContext(null);
            }
            catch (Exception exception)
            {
                Log.Error("Failed to create SitecoreContext", exception, typeof(SandboxGlassController));
            }

            return null;
        }

        protected virtual MvcHtmlString Editable<T>(T model, Expression<Func<T, object>> field, object parameters = null)
        {
            return ConvertToMvcHtmlString(GlassHtml.Editable(model, field, parameters));
        }

        protected virtual MvcHtmlString RenderLink<T>(T model, Expression<Func<T, object>> link, object attributes = null, bool IsEditable = false)
        {
            return ConvertToMvcHtmlString(GlassHtml.RenderLink(model, link, attributes, IsEditable));
        }

        protected virtual MvcHtmlString ConvertToMvcHtmlString(string value)
        {
            return new MvcHtmlString(value);
        }
    }
}

I declared the above class as abstract since I don’t see how it could live on its own — none of its methods return ViewResult or ActionResult instances.

When an instance of the above class is created, an instance of a IGlassHtmlFactory — this is defined in my last post — is used to create an instance of a IGlassHtml which adds a Tag attribute and value to the link attributes when applicable. The Editable and RenderLink methods delegate to methods with the same signature on the IGlassHtml instance, and then transform the returned strings into MvcHtmlString instances via the ConvertToMvcHtmlString method so that the fields work in the Sitecore Experience Editor

I then built the following subclass of the above class for testing:

using System.Web.Mvc;

using Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc.Controllers;
using Sitecore.Sandbox.Models;
using Sitecore.Sandbox.Models.ViewModels;

namespace Sitecore.Sandbox.Web.Mvc.Controllers
{
    public class SampleItemController : SandboxGlassController
    {
        public ViewResult MainContent()
        {
            SampleItemModel model = SitecoreContext.GetCurrentItem<SampleItemModel>();
            if(model == null)
            {
                return View();
            }

            SampleItemViewModel viewModel = new SampleItemViewModel
            {
                Title = Editable(model, x => x.Title),
                Text = Editable(model, x => x.Text),
                LinkOne = RenderLink(model, x => x.LinkOne, null, true),
                LinkTwo = RenderLink(model, x => x.LinkTwo, null, true)
            };

            return View(viewModel);
        }
    }
}

The MainContent method gets an instance of a SampleItemModel from the current Item — in this example these are fields on the home Item — and transforms this into a SampleItemViewModel instance using the protected methods defined on its base class. The SampleItemViewModel instance is then sent to the View.

I then whipped up the following Razor View so we can see some data on the front-end:

@model Sitecore.Sandbox.Models.ViewModels.SampleItemViewModel

@if(Model == null)
{
    return;
}

<div id="Content">
    <div id="LeftContent">
    </div>
    <div id="CenterColumn">
        <div id="Header">
            <img src="/~/media/Default Website/sc_logo.png" id="scLogo" />
        </div>
        <h1 class="contentTitle">
            @Model.Title
        </h1>
        <div class="contentDescription">
            @Model.Text
            <div>
                @Model.LinkOne
            </div>
            <div>
                @Model.LinkTwo
            </div>
        </div>
    </div>
</div>

There isn’t much going on in the above Razor View — it’s just displaying data from the ViewModel.

I am going to omit how I wired-up the Controller above with a Controller rendering in Sitecore. If you don’t know how to wire-up Controllers with Controller renderings in Sitecore, please watch this video by Martina Welander.

Let’s see if this works (works on my machine πŸ˜‰ ).

First, I ensured I had Tag atrributes set on my two General Link fields on my home Item in Sitecore:

tag-attributes-are-set-controller

Once I built and deployed everything, I navigated to my homepage Item, and saw the following rendered HTML:

tag-attribute-rendered-controller

As you can see, it worked. πŸ˜€

If you have any questions/comments/thoughts, please leave a comment.

A 2nd Approach to Render a Custom General Link Field Attribute in a Sitecore MVC View Rendering via Glass.Mapper

In my previous post, I shared an approach for customizing the Glass.Mapper Sitecore ORM to render a custom attribute on a link defined in a General Link field (I called this attribute Tag and will continue to do so in this post).

In this post, I will share a second approach — an approach that extends the “out of the box” Html Helper in Glass.

Note: be sure to read this post first followed by my last post before reading the current post — I am omitting code from both of these which is used here.

I first created a class that implements the Glass.Mapper.Sc.IGlassHtml interface:

using System;
using System.Collections.Specialized;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq.Expressions;

using Sitecore.Collections;
using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Diagnostics;

using Glass.Mapper.Sc;
using Glass.Mapper.Sc.Fields;
using Glass.Mapper.Sc.Web.Ui;
using Utilities = Glass.Mapper.Utilities;

using Sitecore.Sandbox.Glass.Mapper.Sc.Attributes;
using Sitecore.Sandbox.Glass.Mapper.Sc.Fields;

namespace Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc
{
    public class SandboxGlassHtml : IGlassHtml
    {
        private ICustomAttributesAdder attributesAdder;
        private ICustomAttributesAdder AttributesAdder
        {
            get
            {
                if (attributesAdder == null)
                {
                    attributesAdder = GetCustomAttributesAdder();
                }

                return attributesAdder;
            }
        }

        private IGlassHtml InnerGlassHtml { get; set; }

        public ISitecoreContext SitecoreContext
        {
            get
            {
                return InnerGlassHtml.SitecoreContext;
            }
        }

        public SandboxGlassHtml(ISitecoreContext sitecoreContext)
            : this(new GlassHtml(sitecoreContext))
        {
        }

        protected SandboxGlassHtml(IGlassHtml innerGlassHtml)
        {
            SetInnerGlassHtml(innerGlassHtml);
        }

        private void SetInnerGlassHtml(IGlassHtml innerGlassHtml)
        {
            Assert.ArgumentNotNull(innerGlassHtml, "innerGlassHtml");
            InnerGlassHtml = innerGlassHtml;
        }

        public virtual RenderingResult BeginRenderLink<T>(T model, Expression<Func<T, object>> field, TextWriter writer, object attributes = null, bool isEditable = false)
        {
            object attributesModified = AttributesAdder.AddTagAttribute(model, field, attributes);
            return InnerGlassHtml.BeginRenderLink(model, field, writer, attributesModified, isEditable);
        }

        public virtual string Editable<T>(T target, Expression<Func<T, object>> field, object parameters = null)
        {
            return InnerGlassHtml.Editable(target, field, parameters);
        }

        public virtual string Editable<T>(T target, Expression<Func<T, object>> field, Expression<Func<T, string>> standardOutput, object parameters = null)
        {
            return InnerGlassHtml.Editable(target, field, standardOutput, parameters);
        }

        public virtual GlassEditFrame EditFrame(string buttons, string path = null, TextWriter output = null)
        {
            return InnerGlassHtml.EditFrame(buttons, path, output);
        }

        public virtual GlassEditFrame EditFrame<T>(T model, string title = null, TextWriter output = null, params Expression<Func<T, object>>[] fields) where T : class
        {
            return InnerGlassHtml.EditFrame(model, title, output, fields);
        }

        public virtual T GetRenderingParameters<T>(NameValueCollection parameters) where T : class
        {
            return InnerGlassHtml.GetRenderingParameters<T>(parameters);
        }

        public virtual T GetRenderingParameters<T>(string parameters) where T : class
        {
            return InnerGlassHtml.GetRenderingParameters<T>(parameters);
        }

        public virtual T GetRenderingParameters<T>(NameValueCollection parameters, ID renderParametersTemplateId) where T : class
        {
            return InnerGlassHtml.GetRenderingParameters<T>(parameters, renderParametersTemplateId);
        }

        public virtual T GetRenderingParameters<T>(string parameters, ID renderParametersTemplateId) where T : class
        {
            return InnerGlassHtml.GetRenderingParameters<T>(parameters, renderParametersTemplateId);
        }

        public virtual string RenderImage<T>(T model, Expression<Func<T, object>> field, object parameters = null, bool isEditable = false, bool outputHeightWidth = false)
        {
            return InnerGlassHtml.RenderImage(model, field, parameters, isEditable, outputHeightWidth);
        }

        public virtual string RenderLink<T>(T model, Expression<Func<T, object>> field, object attributes = null, bool isEditable = false, string contents = null)
        {
            object attributesModified = AttributesAdder.AddTagAttribute(model, field, attributes);
            return InnerGlassHtml.RenderLink(model, field, attributesModified, isEditable, contents);
        }

        public virtual string ProtectMediaUrl(string url)
        {
            return InnerGlassHtml.ProtectMediaUrl(url);
        }

        protected virtual ICustomAttributesAdder GetCustomAttributesAdder()
        {
            return CustomAttributesAdder.Current;
        }
    }
}

In the above class, I’m using the Decorator Pattern — another Glass.Mapper.Sc.IGlassHtml instance (this is set to an instance of Glass.Mapper.Sc.GlassHtml by default — have a look at the public constructor above) is passed to the class instance and stored in a private property. Every interface-defined method implemented in this class delegates to the inner-IGlassHtml instance.

Since I’m only targeting links in this solution, I utilize a CustomAttributesAdder instance — this is a Singleton which I shared in my last post which is defined in the Sitecore configuration file further down in this post — in both RenderLink methods. The CustomAttributesAdder instance adds the Tag attribute name and value to the attributes collection when applicable. The modified/unmodified attributes collection is then passed to the RenderLink method with the same signature on the inner Glass.Mapper.Sc.IGlassHtml instance.

Now, we need a way to instantiate the above class. I decided to create the following interface for classes that create instances of classes that implement the Glass.Mapper.Sc.IGlassHtml interface:

using Glass.Mapper.Sc;

namespace Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc
{
    public interface IGlassHtmlFactory
    {
        IGlassHtml CreateGlassHtml(ISitecoreContext sitecoreContext);
    }
}

I then built the following class which creates an instance of the SandboxGlassHtml class defined above:

using Sitecore.Diagnostics;

using Glass.Mapper.Sc;

namespace Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc
{
    public class SandboxGlassHtmlFactory : IGlassHtmlFactory
    {
        public IGlassHtml CreateGlassHtml(ISitecoreContext sitecoreContext)
        {
            Assert.ArgumentNotNull(sitecoreContext, "sitecoreContext");
            return new SandboxGlassHtml(sitecoreContext);
        }
    }
}

There isn’t much going on in the the class above exception object instantiation — the above is an example of the Factory method pattern for those who are curious.

Now, we need an extension method on the ASP.NET MVC HtmlHelper instance used in our Razor views in order to leverage the custom Glass.Mapper.Sc.IGlassHtml class defined above:

using System.Web.Mvc;

using Sitecore.Configuration;
using Sitecore.Diagnostics;

using Glass.Mapper.Sc;
using Glass.Mapper.Sc.Web.Mvc;

using Sitecore.Sandbox.Glass.Mapper.Sc.Attributes;

namespace Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc
{
    public static class SandboxHtmlHelperExtensions
    {
        private static IGlassHtmlFactory GlassHtmlFactory { get; set; }

        static SandboxHtmlHelperExtensions()
        {
            GlassHtmlFactory = CreateGlassHtmlFactory();
        }

        public static GlassHtmlMvc<T> SandboxGlass<T>(this HtmlHelper<T> htmlHelper)
        {
            IGlassHtml glassHtml = GlassHtmlFactory.CreateGlassHtml(SitecoreContext.GetFromHttpContext(null));
            Assert.IsNotNull(glassHtml, "glassHtml cannot be null!");
            return new GlassHtmlMvc<T>(glassHtml, htmlHelper.ViewContext.Writer, htmlHelper.ViewData.Model);
        }

        private static IGlassHtmlFactory CreateGlassHtmlFactory()
        {
            IGlassHtmlFactory factory = Factory.CreateObject("sandbox.Glass.Mvc/glassHtmlFactory", true) as IGlassHtmlFactory;
            Assert.IsNotNull(factory, "Be sure the configuration is correct in utilities/customAttributesAdder of your Sitecore configuration!");
            return factory;
        }
    }
}

In the SandboxGlass method above, we instantiate an instance of the IGlassHtmlFactory which is defined in Sitecore configuration (see the patch configuration file below) and use it to create an instance of whatever Glass.Mapper.Sc.IGlassHtml it is tasked to create (in our case here it’s an instance of the SandboxGlassHtml class defined above). This is then passed to a newly created instance of Glass.Mapper.Sc.Web.Mvc.GlassHtmlMvc.

I then glued all the pieces together using the following Sitecore patch configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <controlSources>
      <source mode="on" namespace="Sitecore.Sandbox.Shell.Applications.ContentEditor" assembly="Sitecore.Sandbox" prefix="sandbox-content"/>
    </controlSources>
    <fieldTypes>
      <fieldType name="General Link">
        <patch:attribute name="type">Sitecore.Sandbox.Data.Fields.TagLinkField, Sitecore.Sandbox</patch:attribute>
      </fieldType>
      <fieldType name="General Link with Search">
        <patch:attribute name="type">Sitecore.Sandbox.Data.Fields.TagLinkField, Sitecore.Sandbox</patch:attribute>
      </fieldType>
      <fieldType name="link">
        <patch:attribute name="type">Sitecore.Sandbox.Data.Fields.TagLinkField, Sitecore.Sandbox</patch:attribute>
        </fieldType>
    </fieldTypes>
    <pipelines>
      <dialogInfo>
        <processor type="Sitecore.Sandbox.Pipelines.DialogInfo.SetDialogInfo, Sitecore.Sandbox">
          <ParameterNameAttributeName>name</ParameterNameAttributeName>
          <ParameterValueAttributeName>value</ParameterValueAttributeName>
          <Message>contentlink:externallink</Message>
          <Url>/sitecore/shell/Applications/Dialogs/External link.aspx</Url>
          <parameters hint="raw:AddParameter">
            <parameter name="height" value="300" />
          </parameters>
        </processor>
      </dialogInfo>
      <renderField>
        <processor patch:after="processor[@type='Sitecore.Pipelines.RenderField.GetInternalLinkFieldValue, Sitecore.Kernel']" 
                   type="Sitecore.Sandbox.Pipelines.RenderField.SetTagAttributeOnLink, Sitecore.Sandbox">
          <TagXmlAttributeName>tag</TagXmlAttributeName>
          <TagAttributeName>tag</TagAttributeName>
          <BeginningHtml>&lt;a </BeginningHtml>
        </processor>  
      </renderField>
    </pipelines>
    <sandbox.Glass.Mvc>
      <customAttributesAdder type="Sitecore.Sandbox.Glass.Mapper.Sc.Attributes.CustomAttributesAdder, Sitecore.Sandbox" singleInstance="true" />
      <glassHtmlFactory type="Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc.SandboxGlassHtmlFactory, Sitecore.Sandbox" singleInstance="true" />
    </sandbox.Glass.Mvc>
  </sitecore>
</configuration>

Let’s see if this works.

For testing, I created the following Razor view — notice how I’m using the Html Helper instead of using the methods on the class the Razor view inherits from:

@inherits Glass.Mapper.Sc.Web.Mvc.GlassView<Sitecore.Sandbox.Models.ViewModels.ISampleItem>
@using Glass.Mapper.Sc.Web.Mvc
@using Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc

<div id="Content">
    <div id="LeftContent">
    </div>
    <div id="CenterColumn">
        <div id="Header">
            <img src="/~/media/Default Website/sc_logo.png" id="scLogo" />
        </div>
        <h1 class="contentTitle">
            @Html.SandboxGlass().Editable(x => x.Title)
        </h1>
        <div class="contentDescription">
            @Html.SandboxGlass().Editable(x => x.Text)
            <div>
                @Html.SandboxGlass().RenderLink(x => x.LinkOne)
            </div>
            <div>
                @Html.SandboxGlass().RenderLink(x => x.LinkTwo)
            </div>
        </div>
    </div>
</div>

After building and deploying everything above, I made sure I had some tags defined on some General Link fields on my home Item in Sitecore:

tag-attributes-raw-values

I then navigated to my homepage; looked at the rendered HTML; and saw the following:

tag-attributes-rendered-SandboxGlassHtml

As you can see it worked. πŸ™‚

If you have any questions/comments/thoughts on this, please share in a comment.

Until next time, be sure to:

sitecore-all-the-things

πŸ˜€

One Approach to Render a Custom General Link Field Attribute in a Sitecore MVC View Rendering via Glass.Mapper

In my previous post, I shared a way to add a custom attribute to the General Link field in Sitecore — in that post I called this attribute “Tag” and will continue to do so here — and also showed how to render it on the front-end using the Sitecore Link field control.

You might have been asking yourself when reading that last post “Mike, how would I go about getting this to work in the Sitecore ORM Glass.Mapper?” (well, actually, I planted a seed in that post that I was going to write another post on how to get this to work in Glass.Mapper so you might not have been asking yourself that at all but instead were thinking “Mike, just get on with it!”).

In this post, I am going to show you one approach on how to tweak Glass to render a Tag attribute in the rendered markup of a General Link field (I’m not going reiterate the bits on how to customize the General Link field as I had done in my previous post, so you might want to have a read of that first before reading this post).

I first created a custom Sitecore.Data.Fields.LinkField class:

using Sitecore.Data.Fields;

namespace Sitecore.Sandbox.Data.Fields
{
    public class TagLinkField : LinkField
    {
        public TagLinkField(Field innerField)
            : base(innerField)
        {
        }

        public TagLinkField(Field innerField, string runtimeValue)
            : base(innerField, runtimeValue)
        {
        }

        public string Tag
        {
            get
            {
                return GetAttribute("tag");
            }
            set
            {
                this.SetAttribute("tag", value);
            }
        }   
    }
}

An instance of this class will magically create a XML representation of itself when saving to the General Link field, and will also parse the attributes that are defined in the XML.

Next, we need a Glass.Mapper field like Glass.Mapper.Sc.Fields.Link but with an additional property for the Tag value. This sound like an opportune time to subclass Glass.Mapper.Sc.Fields.Link and add a new property to hold the Tag value πŸ˜‰ :

using Glass.Mapper.Sc.Fields;

namespace Sitecore.Sandbox.Glass.Mapper.Sc.Fields
{
    public class TagLink : Link
    {
        public string Tag { get; set; }
    }
}

There’s nothing much in the above class except for an additional property for the Tag attribute value.

I then built the following Glass.Mapper.Sc.DataMappers.AbstractSitecoreFieldMapper for the TagLink:

using System;

using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Links;
using Sitecore.Resources.Media;

using Glass.Mapper;
using Glass.Mapper.Sc;
using Glass.Mapper.Sc.Configuration;
using Glass.Mapper.Sc.DataMappers;
using Glass.Mapper.Sc.Fields;
using Utilities = Glass.Mapper.Sc.Utilities;

using Sitecore.Sandbox.Data.Fields;
using Sitecore.Sandbox.Glass.Mapper.Sc.Fields;

namespace Sitecore.Sandbox.Glass.Mapper.Sc.DataMappers
{
    public class SitecoreFieldTagLinkMapper : AbstractSitecoreFieldMapper
    {
        private AbstractSitecoreFieldMapper InnerLinkMapper { get; set;}

        public SitecoreFieldTagLinkMapper()
            : this(new SitecoreFieldLinkMapper(), typeof(TagLink))
        {
        }

        public SitecoreFieldTagLinkMapper(AbstractSitecoreFieldMapper innerLinkMapper, Type linkType)
            : base(linkType)
        {
            SetInnerLinkMapper(innerLinkMapper);
        }

        private void SetInnerLinkMapper(AbstractSitecoreFieldMapper innerLinkMapper)
        {
            Assert.ArgumentNotNull(innerLinkMapper, "innerLinkMapper");
            InnerLinkMapper = innerLinkMapper;
        }
        
        public override string SetFieldValue(object value, SitecoreFieldConfiguration config, SitecoreDataMappingContext context)
        {
            return InnerLinkMapper.SetFieldValue(value, config, context);
        }
        
        public override object GetFieldValue(string fieldValue, SitecoreFieldConfiguration config, SitecoreDataMappingContext context)
        {
            return InnerLinkMapper.GetFieldValue(fieldValue, config, context);
        }

        public override object GetField(Field field, SitecoreFieldConfiguration config, SitecoreDataMappingContext context)
        {
            if (field == null || field.Value.Trim().IsNullOrEmpty())
            {
                return null;
            }

            TagLink link = new TagLink();
            TagLinkField linkField = new TagLinkField(field);
            link.Anchor = linkField.Anchor;
            link.Class = linkField.Class;
            link.Text = linkField.Text;
            link.Title = linkField.Title;
            link.Target = linkField.Target;
            link.Query = linkField.QueryString;
            link.Tag = linkField.Tag;

            switch (linkField.LinkType)
            {
                case "anchor":
                    link.Url = linkField.Anchor;
                    link.Type = LinkType.Anchor;
                    break;
                case "external":
                    link.Url = linkField.Url;
                    link.Type = LinkType.External;
                    break;
                case "mailto":
                    link.Url = linkField.Url;
                    link.Type = LinkType.MailTo;
                    break;
                case "javascript":
                    link.Url = linkField.Url;
                    link.Type = LinkType.JavaScript;
                    break;
                case "media":
                    if (linkField.TargetItem == null)
                        link.Url = string.Empty;
                    else
                    {
                        global::Sitecore.Data.Items.MediaItem media =
                            new global::Sitecore.Data.Items.MediaItem(linkField.TargetItem);
                        link.Url = global::Sitecore.Resources.Media.MediaManager.GetMediaUrl(media);
                    }
                    link.Type = LinkType.Media;
                    link.TargetId = linkField.TargetID.Guid;
                    break;
                case "internal":
                    var urlOptions = Utilities.CreateUrlOptions(config.UrlOptions);
                    link.Url = linkField.TargetItem == null ? string.Empty : LinkManager.GetItemUrl(linkField.TargetItem, urlOptions);
                    link.Type = LinkType.Internal;
                    link.TargetId = linkField.TargetID.Guid;
                    link.Text = linkField.Text.IsNullOrEmpty() ? (linkField.TargetItem == null ? string.Empty : linkField.TargetItem.DisplayName) : linkField.Text;
                    break;
                default:
                    return null;
            }

            return link;
        }

        public override void SetField(Field field, object value, SitecoreFieldConfiguration config, SitecoreDataMappingContext context)
        {
            if (field == null)
            {
                return;
            }

            TagLink link = value as TagLink;
            if(link == null)
            {
                return;
            }

            Item item = field.Item;
            TagLinkField linkField = new TagLinkField(field);
            if (link == null || link.Type == LinkType.NotSet)
            {
                linkField.Clear();
                return;
            }
            
            switch (link.Type)
            {
                case LinkType.Internal:
                    linkField.LinkType = "internal";
                    if (linkField.TargetID.Guid != link.TargetId)
                    {
                        if (link.TargetId == Guid.Empty)
                        {
                            ItemLink iLink = new ItemLink(item.Database.Name, item.ID, linkField.InnerField.ID, linkField.TargetItem.Database.Name, linkField.TargetID, linkField.TargetItem.Paths.FullPath);
                            linkField.RemoveLink(iLink);
                        }
                        else
                        {
                            ID newId = new ID(link.TargetId);
                            Item target = item.Database.GetItem(newId);
                            if (target != null)
                            {
                                linkField.TargetID = newId;
                                ItemLink nLink = new ItemLink(item.Database.Name, item.ID, linkField.InnerField.ID, target.Database.Name, target.ID, target.Paths.FullPath);
                                linkField.UpdateLink(nLink);
                                linkField.Url = LinkManager.GetItemUrl(target);
                            }
                            else throw new Exception(String.Format("No item with ID {0}. Can not update Link linkField", newId));
                        }

                    }
                    break;
                case LinkType.Media:
                    linkField.LinkType = "media";
                    if (linkField.TargetID.Guid != link.TargetId)
                    {
                        if (link.TargetId == Guid.Empty)
                        {
                            ItemLink iLink = new ItemLink(item.Database.Name, item.ID, linkField.InnerField.ID, linkField.TargetItem.Database.Name, linkField.TargetID, linkField.TargetItem.Paths.FullPath);
                            linkField.RemoveLink(iLink);
                        }
                        else
                        {
                            ID newId = new ID(link.TargetId);
                            Item target = item.Database.GetItem(newId);

                            if (target != null)
                            {
                                MediaItem media = new MediaItem(target);

                                linkField.TargetID = newId;
                                ItemLink nLink = new ItemLink(item.Database.Name, item.ID, linkField.InnerField.ID, target.Database.Name, target.ID, target.Paths.FullPath);
                                linkField.UpdateLink(nLink);
                                linkField.Url = MediaManager.GetMediaUrl(media);
                            }
                            else throw new Exception(String.Format("No item with ID {0}. Can not update Link linkField", newId));
                        }
                    }
                    break;
                case LinkType.External:
                    linkField.LinkType = "external";
                    linkField.Url = link.Url;
                    break;
                case LinkType.Anchor:
                    linkField.LinkType = "anchor";
                    linkField.Url = link.Anchor;
                    break;
                case LinkType.MailTo:
                    linkField.LinkType = "mailto";
                    linkField.Url = link.Url;
                    break;
                case LinkType.JavaScript:
                    linkField.LinkType = "javascript";
                    linkField.Url = link.Url;
                    break;
            }

            if (!link.Anchor.IsNullOrEmpty())
            {
                linkField.Anchor = link.Anchor;
            }
                
            if (!link.Class.IsNullOrEmpty())
            {
                linkField.Class = link.Class;
            }
                
            if (!link.Text.IsNullOrEmpty())
            {
                linkField.Text = link.Text;
            }
                
            if (!link.Title.IsNullOrEmpty())
            {
                linkField.Title = link.Title;
            }
                
            if (!link.Query.IsNullOrEmpty())
            {
                linkField.QueryString = link.Query;
            }
                
            if (!link.Target.IsNullOrEmpty())
            {
                linkField.Target = link.Target;
            }

            if (!link.Tag.IsNullOrEmpty())
            {
                linkField.Tag = link.Tag;
            }
        }
    }
}

Most of the code in the GetField and SetField methods above are taken from the same methods in Glass.Mapper.Sc.DataMappers.SitecoreFieldLinkMapper except for the additional lines for the TagLink.

In both methods a TagLinkField instance is created so that we can get the Tag value from the field.

The follow class is used by an <initialize> pipeline processor that configures Glass on Sitecore application start:

using Glass.Mapper.Configuration;
using Glass.Mapper.IoC;
using Glass.Mapper.Maps;
using Glass.Mapper.Sc;
using Glass.Mapper.Sc.IoC;
using Sitecore.Sandbox.DI;
using Sitecore.Sandbox.Glass.Mapper.Sc.DataMappers;
using IDependencyResolver = Glass.Mapper.Sc.IoC.IDependencyResolver;

namespace Sitecore.Sandbox.Web.Mvc.App_Start
{
    public static class GlassMapperScCustom
    {
		public static IDependencyResolver CreateResolver(){
			var config = new Config();
            DependencyResolver dependencyResolver = new DependencyResolver(config);
            AddDataMappers(dependencyResolver);
            return dependencyResolver;
		}

        private static void AddDataMappers(DependencyResolver dependencyResolver)
        {
            if(dependencyResolver == null)
            {
                return;
            }
            
            dependencyResolver.DataMapperFactory.Replace(15, () => new SitecoreFieldTagLinkMapper());
        }

		public static IConfigurationLoader[] GlassLoaders(){			
			
			/* USE THIS AREA TO ADD FLUENT CONFIGURATION LOADERS
             * 
             * If you are using Attribute Configuration or automapping/on-demand mapping you don't need to do anything!
             * 
             */

			return new IConfigurationLoader[]{};
		}
		public static void PostLoad(){
			//Remove the comments to activate CodeFist
			/* CODE FIRST START
            var dbs = Sitecore.Configuration.Factory.GetDatabases();
            foreach (var db in dbs)
            {
                var provider = db.GetDataProviders().FirstOrDefault(x => x is GlassDataProvider) as GlassDataProvider;
                if (provider != null)
                {
                    using (new SecurityDisabler())
                    {
                        provider.Initialise(db);
                    }
                }
            }
             * CODE FIRST END
             */
		}
		public static void AddMaps(IConfigFactory<IGlassMap> mapsConfigFactory)
        {
			// Add maps here
            ContainerManager containerManager = new ContainerManager();
            foreach (var map in containerManager.Container.GetAllInstances<IGlassMap>())
            {
                mapsConfigFactory.Add(() => map);
            }
        }
    }
}

I added the AddDataMappers method to it. This method replaces the “out of the box” SitecoreFieldLinkMapper with a SitecoreFieldTagLinkMapper instance — the “out of the box” SitecoreFieldLinkMapper lives in the 15th place in the index (I determined this using .NET Reflector on one of the Glass.Mapper assemblies).

Now that the above things are squared away, we need a way to add the Tag attribute with its value to the attributes collection that is passed to Glass so that it can transform this into rendered HTML. I decided to define an interface for classes that do that:

using System;
using System.Linq.Expressions;

namespace Sitecore.Sandbox.Glass.Mapper.Sc.Attributes
{
    public interface ICustomAttributesAdder
    {
        object AddTagAttribute<T>(T model, Expression<Func<T, object>> field, object attributes);
    }
}

Classes that implement the above interface will take in a Glass Model instance, the field we are rendering, and the existing collection of attributes that are to be rendered by Glass.

The following class implements the above interface:

using System;
using System.Collections.Specialized;
using System.Linq.Expressions;

using Sitecore.Configuration;
using Sitecore.Diagnostics;

using Glass.Mapper.Sc;

using Sitecore.Sandbox.Glass.Mapper.Sc.Fields;

namespace Sitecore.Sandbox.Glass.Mapper.Sc.Attributes
{
    public class CustomAttributesAdder : ICustomAttributesAdder
    {
        private static readonly Lazy<ICustomAttributesAdder> current = new Lazy<ICustomAttributesAdder>(() => { return GetCustomAttributesAdder(); });

        public static ICustomAttributesAdder Current
        {
            get
            {
                return current.Value;
            }
        }

        public CustomAttributesAdder()
        {
        }

        public virtual object AddTagAttribute<T>(T model, Expression<Func<T, object>> field, object attributes)
        {
            TagLink tagLink = field.Compile()(model) as TagLink;
            if (tagLink == null || string.IsNullOrWhiteSpace(tagLink.Tag))
            {
                return attributes;
            }

            NameValueCollection attributesCollection;
            if (attributes is NameValueCollection)
            {
                attributesCollection = attributes as NameValueCollection;
            }
            else
            {
                attributesCollection = Utilities.GetPropertiesCollection(attributes, true, true);
            }

            attributesCollection.Add("tag", tagLink.Tag);
            return attributesCollection;
        }

        private static ICustomAttributesAdder GetCustomAttributesAdder()
        {
            ICustomAttributesAdder adder = Factory.CreateObject("sandbox.Glass.Mvc/customAttributesAdder", true) as ICustomAttributesAdder;
            Assert.IsNotNull(adder, "Be sure the configuration for CustomAttributesAdder is correct in utilities/customAttributesAdder of your Sitecore configuration!");
            return adder;
        }
    }
}

The AddTagAttribute method above first determines if the field passed to it is a TagLink. If it’s not, it just returns the attribute collection unaltered.

The method also determines if there is a Tag value. If there is no Tag value, it just returns the attribute collection “as is” since we don’t want to render an attribute with an empty value.

If the field is a TagLink and there is a Tag value, the method adds the Tag attribute name and value into the attributes collection that was passed to it, and then returns the modified NameValueCollection instance.

I decided to use the Singleton Pattern for the above class — the type of the class is defined in Sitecore configuration (see the patch configuration file further down in this post — since I am going to reuse it in my next post where I’ll show another approach on rendering a Tag attribute using Glass.Mapper, and I had built both approaches simultaneously (I decided to break these into separate blog posts since this one by itself will already be quite long).

Next, I built a new subclass of Glass.Mapper.Sc.Web.Mvc.GlassView so that I can intercept attributes collection passed to the RenderLink methods on the “out of the box” Glass.Mapper.Sc.Web.Mvc.GlassView (our Razor views will have to inherit from the class below in order for everything to work):

using System;
using System.Collections.Specialized;
using System.Linq.Expressions;
using System.Web;

using Sitecore.Configuration;
using Sitecore.Diagnostics;

using Sitecore.Sandbox.Glass.Mapper.Sc.Attributes;
using Sitecore.Sandbox.Glass.Mapper.Sc.Fields;

using Glass.Mapper.Sc;
using Glass.Mapper.Sc.Fields;
using Glass.Mapper.Sc.Web.Mvc;

namespace Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc
{
    public abstract class SandboxGlassView<TModel> : GlassView<TModel> where TModel : class
    {
        private ICustomAttributesAdder attributesAdder;
        private ICustomAttributesAdder AttributesAdder 
        { 
            get
            {
                if (attributesAdder == null)
                {
                    attributesAdder = GetCustomAttributesAdder();
                }

                return attributesAdder;
            }
        }

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

        public override HtmlString RenderLink(Expression<Func<TModel, object>> field, object attributes = null, bool isEditable = false, string contents = null)
        {
            object attributesModified = AttributesAdder.AddTagAttribute(Model, field, attributes);
            return base.RenderLink(field, attributesModified, isEditable, contents);
        }

        public override HtmlString RenderLink<T>(T model, Expression<Func<T, object>> field, object attributes = null, bool isEditable = false, string contents = null)
        {
            object attributesModified = AttributesAdder.AddTagAttribute(model, field, attributes);
            return base.RenderLink<T>(model, field, attributesModified, isEditable, contents);
        }

        protected virtual ICustomAttributesAdder GetCustomAttributesAdder()
        {
            return CustomAttributesAdder.Current;
        }
    }
}

I used the CustomAttributesAdder Singleton instance to add the Tag attribute name and value into the passed attributes collection, and then pass it on to the base class to do its magic.

I then strung everything together using the following Sitecore patch configuration file (Note: lots of stuff in this configuration file come from my previous post so I advise having a look at it):

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <controlSources>
      <source mode="on" namespace="Sitecore.Sandbox.Shell.Applications.ContentEditor" assembly="Sitecore.Sandbox" prefix="sandbox-content"/>
    </controlSources>
    <fieldTypes>
      <fieldType name="General Link">
        <patch:attribute name="type">Sitecore.Sandbox.Data.Fields.TagLinkField, Sitecore.Sandbox</patch:attribute>
      </fieldType>
      <fieldType name="General Link with Search">
        <patch:attribute name="type">Sitecore.Sandbox.Data.Fields.TagLinkField, Sitecore.Sandbox</patch:attribute>
      </fieldType>
      <fieldType name="link">
        <patch:attribute name="type">Sitecore.Sandbox.Data.Fields.TagLinkField, Sitecore.Sandbox</patch:attribute>
        </fieldType>
    </fieldTypes>
    <pipelines>
      <dialogInfo>
        <processor type="Sitecore.Sandbox.Pipelines.DialogInfo.SetDialogInfo, Sitecore.Sandbox">
          <ParameterNameAttributeName>name</ParameterNameAttributeName>
          <ParameterValueAttributeName>value</ParameterValueAttributeName>
          <Message>contentlink:externallink</Message>
          <Url>/sitecore/shell/Applications/Dialogs/External link.aspx</Url>
          <parameters hint="raw:AddParameter">
            <parameter name="height" value="300" />
          </parameters>
        </processor>
      </dialogInfo>
      <renderField>
        <processor patch:after="processor[@type='Sitecore.Pipelines.RenderField.GetInternalLinkFieldValue, Sitecore.Kernel']" 
                   type="Sitecore.Sandbox.Pipelines.RenderField.SetTagAttributeOnLink, Sitecore.Sandbox">
          <TagXmlAttributeName>tag</TagXmlAttributeName>
          <TagAttributeName>tag</TagAttributeName>
          <BeginningHtml>&lt;a </BeginningHtml>
        </processor>  
      </renderField>
    </pipelines>
    <sandbox.Glass.Mvc>
      <customAttributesAdder type="Sitecore.Sandbox.Glass.Mapper.Sc.Attributes.CustomAttributesAdder, Sitecore.Sandbox" singleInstance="true" />
    </sandbox.Glass.Mvc>
  </sitecore>
</configuration>

Let’s see this in action!

For testing, I created the following interface for a model for my Sitecore instance’s Home Item (we are using fields defined on the Sample Item template):

using Glass.Mapper.Sc.Configuration.Attributes;

using Sitecore.Sandbox.Glass.Mapper.Sc.Fields;

namespace Sitecore.Sandbox.Models.ViewModels
{
    public interface ISampleItem
    {
        string Title { get; set; }

        string Text { get; set; }

        [SitecoreField("Link One")]
        TagLink LinkOne { get; set; }

        [SitecoreField("Link Two")]
        TagLink LinkTwo { get; set; }
    }
}

Model instances of the above interface will have two TagLink instances on them.

Next, I built the following Glass.Mapper.Sc.Maps.SitecoreGlassMap for my model interface defined above:

using Glass.Mapper.Sc.Maps;

using Sitecore.Sandbox.Models.ViewModels;

namespace Sitecore.Sandbox.Mappings.ViewModels.SampleItem
{
    public class SampleItemMap : SitecoreGlassMap<ISampleItem>
    {
        public override void Configure()
        {
            Map(x =>
            {
                x.AutoMap();
            });
        }
    }
}

Glass.Mapper will create an instance of the above class which will magically create a concrete instance of a class that implements the ISampleItem interface.

We need to plug the above into the front-end. I did this using the following Razor view:

@inherits Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc.SandboxGlassView<Sitecore.Sandbox.Models.ViewModels.ISampleItem>
@using Glass.Mapper.Sc.Web.Mvc
@using Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc

<div id="Content">
    <div id="LeftContent">
    </div>
    <div id="CenterColumn">
        <div id="Header">
            <img src="/~/media/Default Website/sc_logo.png" id="scLogo" />
        </div>
        <h1 class="contentTitle">
            @Editable(x => x.Title)
        </h1>
        <div class="contentDescription">
            @Editable(x => x.Text)
            <div>
                @RenderLink(x => x.LinkOne)
            </div>
            <div>
                @RenderLink(x => x.LinkTwo)
            </div>
        </div>
    </div>
</div>

The above Razor file inherits from SandboxGlassView so that it can access the RenderLink methods that were defined in the SandboxGlassView class.

I then ensured I had some tag attributes set on some General Link fields on my home Item (I kept these the same as my last blog post):

tag-attributes-raw-values

After doing a build and navigating to my homepage Item, I saw the following in the rendered HTML:

tag-attributes-rendered

As you can see, it worked magically. πŸ™‚

magic

If you have any questions/comments/thoughts on the above, please share in a comment.

Also, I would like to thank Sitecore MVP Nat Mann for helping me on some of the bits above. Without your help Nat, there would be no solution and no blog post.

Until next time, keep on Sitecore-ing. πŸ˜€

Add a Custom Attribute to the General Link Field in Sitecore

In my current project, I needed to find a way to give content authors the ability to add a custom attribute — let’s call this custom attribute Tag for simplicity– to the “Insert Link” and “Insert External Link” dialogs of the General Link field (NOTE: the following solution does not use the “out of the box” SPEAK dialogs that ship with Sitecore 7.2 and up. This solution uses the older Sheer UI dialogs. Perhaps I will share a solution in the future on how to do the following using the newer SPEAK dialogs).

You might be asking why? Well, let’s imagine that there is some magical JavaScript code that puts a click event on links, and grabs the value of the tag attribute for reporting purposes — perhaps the JavaScript calls a service that captures this information.

In this post, I am going to share how I went about doing this minus the code I needed to add to get this to work in the Glass.Mapper ORM (I’m going to show you this code in my next blog post).

I first built the following custom LinkField class (this class is not used in this solution but will be used in my next blog post where I should how to integrate the functionality below in Glass.Mapper. I’m just setting the stage πŸ˜‰ ):

using Sitecore.Data.Fields;

namespace Sitecore.Sandbox.Data.Fields
{
    public class TagLinkField : LinkField
    {
        public TagLinkField(Field innerField)
            : base(innerField)
        {
        }

        public TagLinkField(Field innerField, string runtimeValue)
            : base(innerField, runtimeValue)
        {
        }

        public string Tag
        {
            get
            {
                return GetAttribute("tag");
            }
            set
            {
                this.SetAttribute("tag", value);
            }
        }   
    }
}

The class above subclasses Sitecore.Data.Fields.Link (this lives in Sitecore.Kernel.dll) — this class represents a link in Sitecore — and added a new Tag property (this class will magically parse or save this value into the field’s underlying XML).

Next, I created the following Sheer UI form for a custom “Insert Link” dialog:

using System;
using System.Xml;
using System.Collections.Specialized;

using Sitecore;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Shell.Applications.Dialogs;
using Sitecore.Shell.Applications.Dialogs.InternalLink;
using Sitecore.Web.UI.HtmlControls;
using Sitecore.Web.UI.Sheer;
using Sitecore.Xml;

namespace Sitecore.Sandbox.Shell.Applications.Dialogs.InternalLink
{
    public class TagInternalLinkForm : InternalLinkForm
    {
        private const string TagAttributeName = "tag";

        protected Edit Tag;

        private NameValueCollection customLinkAttributes;
        protected NameValueCollection CustomLinkAttributes 
        { 
            get
            {
                if(customLinkAttributes == null)
                {
                    customLinkAttributes = new NameValueCollection();
                    ParseLinkAttributes(GetLink());
                }

                return customLinkAttributes;
            }
        }

        protected override void OnLoad(EventArgs e)
        {
            Assert.ArgumentNotNull(e, "e");
            base.OnLoad(e);
            if (Context.ClientPage.IsEvent)
            {
                return;
            }
            
            LoadControls();
        }

        protected override void ParseLink(string link)
        {
            base.ParseLink(link);
            ParseLinkAttributes(link);
        }

        protected virtual void ParseLinkAttributes(string link)
        {
            Assert.ArgumentNotNull(link, "link");
            XmlDocument xmlDocument = XmlUtil.LoadXml(link);
            if (xmlDocument == null)
            {
                return;
            }

            XmlNode node = xmlDocument.SelectSingleNode("/link");
            if (node == null)
            {
                return;
            }

            CustomLinkAttributes[TagAttributeName] = XmlUtil.GetAttribute(TagAttributeName, node);
        }

        protected virtual void LoadControls()
        {
            string tagValue = CustomLinkAttributes[TagAttributeName];
            if (!string.IsNullOrWhiteSpace(tagValue))
            {
                Tag.Value = tagValue;
            }
        }

        protected override void OnOK(object sender, EventArgs args)
        {
            Assert.ArgumentNotNull(sender, "sender");
            Assert.ArgumentNotNull(args, "args");
            Item selectionItem = Treeview.GetSelectionItem();
            if (selectionItem == null)
            {
                Context.ClientPage.ClientResponse.Alert("Select an item.");
            }
            else
            {
                string attributeFromValue = LinkForm.GetLinkTargetAttributeFromValue(this.Target.Value, this.CustomTarget.Value);
                string queryString = this.Querystring.Value;
                if (queryString.StartsWith("?", StringComparison.InvariantCulture))
                    queryString = queryString.Substring(1);

                Packet packet = new Packet("link", new string[0]);
                LinkForm.SetAttribute(packet, "text", (Control)Text);
                LinkForm.SetAttribute(packet, "linktype", "internal");
                LinkForm.SetAttribute(packet, "anchor", (Control)Anchor);
                LinkForm.SetAttribute(packet, "title", (Control)Title);
                LinkForm.SetAttribute(packet, "class", (Control)Class);
                LinkForm.SetAttribute(packet, "querystring", queryString);
                LinkForm.SetAttribute(packet, "target", attributeFromValue);
                LinkForm.SetAttribute(packet, "id", selectionItem.ID.ToString());

                TrimEditControl(Tag);
                LinkForm.SetAttribute(packet, TagAttributeName, (Control)Tag);

                Assert.IsTrue(!string.IsNullOrEmpty(selectionItem.ID.ToString()) && ID.IsID(selectionItem.ID.ToString()), "ID doesn't exist.");
                SheerResponse.SetDialogValue(packet.OuterXml);
                SheerResponse.CloseWindow();
            }
        }

        protected virtual void TrimEditControl(Edit control)
        {
            if(control == null || string.IsNullOrEmpty(control.Value))
            {
                return;
            }

            control.Value = control.Value.Trim();
        }
    }
}

The OnLoad method invokes its base class’ OnLoad method — the base class’ OnLoad method loads values from the field’s XML into the Edit controls on the form — and also parses the value from the tag XML attribute and places it into the Tag Edit control.

The ParseLink method above is where values from the field’s XML are extracted — these are extracted from the XML attributes of the field. The ParseLink method delegates to the ParseLinkAttributes method which extracts the value from the tag attribute.

The OnOK method is where values from the Edit controls are extract and passed to a class instance that generates XML for the field. I could not call the base class’ OnOK method since it would prevent me from saving the custom tag attribute and value, so I “borrowed/stole” code from it, and then added my modifications.

I then added new Tag Literal and Edit controls to the “Internal Link” dialog, and also updated the CodeBeside xml element to point to my new class (I copy and pasted this from /sitecore/shell/Applications/Dialogs/InsertLink.InsertLink.xml and put my new file into /sitecore/shell/Override/InternalLink/InsertLink.xml in my website root — always put custom Sheer UI dialogs XML files in /sitecore/shell/Override/ so that you don’t run into issues when upgrading Sitecore):

<?xml version="1.0" encoding="utf-8" ?>
<control xmlns:def="Definition" xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense">
  <InternalLink>
    <FormDialog Icon="Network/32x32/link.png" Header="Internal Link" Text="Select the item that you want to create a link to and specify the appropriate properties." OKButton="OK">
      <Stylesheet Key="Style">
        .ff input { 
          width: 160px;
        }        
      </Stylesheet>
      <CodeBeside Type="Sitecore.Sandbox.Shell.Applications.Dialogs.InternalLink.TagInternalLinkForm, Sitecore.Sandbox" />

      <DataContext ID="InternalLinkDataContext"/>
     
      <GridPanel Columns="2" Width="100%" Height="100%" CellPadding="4" Style="table-layout:fixed">
        <Scrollbox Width="100%" Height="100%" Class="scScrollbox scFixSize" Background="window" Padding="0" Border="1px solid #CFCFCF" GridPanel.VAlign="top" GridPanel.Width="100%" GridPanel.Height="100%">
          <TreeviewEx ID="Treeview" DataContext="InternalLinkDataContext" MultiSelect="False" Width="100%"/>
        </Scrollbox>
      
        <Scrollbox Width="256" Height="100%" Background="transparent" Border="none" GridPanel.VAlign="top" GridPanel.Width="256">
          <GridPanel CellPadding="2" Columns="2">
            <Literal Text="Link Description:" GridPanel.NoWrap="true"/>
            <Edit ID="Text"/>
            
            <Literal Text="Anchor:" GridPanel.NoWrap="true"/>
            <Edit ID="Anchor"/>

            <Label for="Target" GridPanel.NoWrap="true"><Literal Text="Target Window:"/></Label>
            <Combobox ID="Target" Width="100%" Change="OnListboxChanged">
              <ListItem Value="Self" Header="Active browser"/>
              <ListItem Value="New" Header="New browser"/>
              <ListItem Value="Custom" Header="Custom"/>
            </Combobox>
            
            <Panel ID="CustomLabel" Background="transparent" Border="none" GridPanel.NoWrap="true" GridPanel.Align="right"><Label For="CustomTarget"><Literal Text="Custom:" /></Label></Panel>
            <Edit ID="CustomTarget" />

            <Literal Text="Style Class:" GridPanel.NoWrap="true"/>
            <Edit ID="Class"/>
            
            <Literal Text="Alternate Text:" GridPanel.NoWrap="true"/>
            <Edit ID="Title"/>

            <Literal Text="Query String:" GridPanel.NoWrap="true"/>
            <Edit ID="Querystring"/>
          
            <Literal Text="Tag:" GridPanel.NoWrap="true"/>
            <Edit ID="Tag"/>
          </GridPanel>
        </Scrollbox>
      
      </GridPanel>
      
    </FormDialog>            
  </InternalLink>
</control>

Likewise, I repeated the steps for the “External Link” dialog’s code-beside class (I’m not going to go into details here since they are the same as the “Insert Link” dialog class above):

using System;

using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.Shell.Applications.Dialogs;
using Sitecore.Shell.Applications.Dialogs.ExternalLink;
using Sitecore.Web.UI.HtmlControls;
using Sitecore.Web.UI.Sheer;
using Sitecore.Xml;
using System.Collections.Specialized;
using System.Xml;

namespace Sitecore.Sandbox.Shell.Applications.Dialogs.ExternalLink
{
    public class TagExternalLinkForm : ExternalLinkForm
    {
        private const string TagAttributeName = "tag";

        protected Edit Tag;

        private NameValueCollection customLinkAttributes;
        protected NameValueCollection CustomLinkAttributes
        {
            get
            {
                if (customLinkAttributes == null)
                {
                    customLinkAttributes = new NameValueCollection();
                    ParseLinkAttributes(GetLink());
                }

                return customLinkAttributes;
            }
        }

        protected override void ParseLink(string link)
        {
            base.ParseLink(link);
            ParseLinkAttributes(link);
        }

        protected virtual void ParseLinkAttributes(string link)
        {
            Assert.ArgumentNotNull(link, "link");
            XmlDocument xmlDocument = XmlUtil.LoadXml(link);
            if (xmlDocument == null)
            {
                return;
            }

            XmlNode node = xmlDocument.SelectSingleNode("/link");
            if (node == null)
            {
                return;
            }

            CustomLinkAttributes[TagAttributeName] = XmlUtil.GetAttribute(TagAttributeName, node);
        }

        protected override void OnLoad(EventArgs e)
        {
            Assert.ArgumentNotNull(e, "e");
            base.OnLoad(e);
            if (Context.ClientPage.IsEvent)
            {
                return;
            }

            LoadControls();
        }

        protected virtual void LoadControls()
        {
            string tagValue = CustomLinkAttributes[TagAttributeName];
            if (!string.IsNullOrWhiteSpace(tagValue))
            {
                Tag.Value = tagValue;
            }
        }
        
        protected override void OnOK(object sender, EventArgs args)
        {
            Assert.ArgumentNotNull(sender, "sender");
            Assert.ArgumentNotNull(args, "args");
            string path = GetPath();
            string attributeFromValue = LinkForm.GetLinkTargetAttributeFromValue(Target.Value, CustomTarget.Value);
            Packet packet = new Packet("link", new string[0]);
            LinkForm.SetAttribute(packet, "text", (Control)Text);
            LinkForm.SetAttribute(packet, "linktype", "external");
            LinkForm.SetAttribute(packet, "url", path);
            LinkForm.SetAttribute(packet, "anchor", string.Empty);
            LinkForm.SetAttribute(packet, "title", (Control)Title);
            LinkForm.SetAttribute(packet, "class", (Control)Class);
            LinkForm.SetAttribute(packet, "target", attributeFromValue);

            TrimEditControl(Tag);
            LinkForm.SetAttribute(packet, TagAttributeName, (Control)Tag);

            SheerResponse.SetDialogValue(packet.OuterXml);
            SheerResponse.CloseWindow();
        }

        private string GetPath()
        {
            string url = this.Url.Value;
            if (url.Length > 0 && url.IndexOf("://", StringComparison.InvariantCulture) < 0 && !url.StartsWith("/", StringComparison.InvariantCulture))
            {
                url = string.Concat("http://", url);
            }
                
            return url;
        }

        protected virtual void TrimEditControl(Edit control)
        {
            if (control == null || string.IsNullOrEmpty(control.Value))
            {
                return;
            }

            control.Value = control.Value.Trim();
        }
    }
}

I also added a Label and Edit control for the Tag as I did for the “Insert Link” dialog above (the “out of the box” External Link dialog xml file lives in /sitecore/shell/Applications/Dialogs/ExternalLink/ExternalLink.xml of the Sitecore website root. When creating custom one be sure to put it in /sitecore/shell/override of your website root):

<?xml version="1.0" encoding="utf-8" ?>
<control xmlns:def="Definition" xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense">
  <ExternalLink>
    <FormDialog Header="Insert External Link" Text="Enter the URL for the external website that you want to insert a link to and specify any additional properties for the link." OKButton="Insert">

      <CodeBeside Type="Sitecore.Sandbox.Shell.Applications.Dialogs.ExternalLink.TagExternalLinkForm, Sitecore.Sandbox"/>

      <GridPanel Class="scFormTable" CellPadding="2" Columns="2" Width="100%">
        <Label For="Text" GridPanel.NoWrap="true">
          <Literal Text="Link description:"/>
        </Label>
        <Edit ID="Text" Width="100%" GridPanel.Width="100%"/>

        <Label For="Url" GridPanel.NoWrap="true">
          <Literal Text="URL:"/>
        </Label>
        <Border>
          <GridPanel Columns="2" Width="100%">
            <Edit ID="Url" Width="100%" GridPanel.Width="100%" />
            <Button id="Test" Header="Test" Style="margin-left: 10px;" Click="OnTest"/>
          </GridPanel>
        </Border>

        <Label for="Target" GridPanel.NoWrap="true">
          <Literal Text="Target window:"/>
        </Label>
        <Combobox ID="Target" GridPanel.Width="100%" Width="100%" Change="OnListboxChanged">
          <ListItem Value="Self" Header="Active browser"/>
          <ListItem Value="New" Header="New browser"/>
          <ListItem Value="Custom" Header="Custom"/>
        </Combobox>

        <Panel ID="CustomLabel" Disabled="true" Background="transparent" Border="none" GridPanel.NoWrap="true">
          <Label For="CustomTarget">
            <Literal Text="Custom:" />
          </Label>
        </Panel>
        <Edit ID="CustomTarget" Width="100%" Disabled="true"/>

        <Label For="Class" GridPanel.NoWrap="true">
          <Literal Text="Style class:" />
        </Label>
        <Edit ID="Class" Width="100%" />

        <Label for="Title" GridPanel.NoWrap="true">
          <Literal Text="Alternate text:"/>
        </Label>
        <Edit ID="Title" Width="100%" />
        
        <Label for="Tag" GridPanel.NoWrap="true">
          <Literal Text="Tag:"/>
        </Label>
        <Edit ID="Tag" Width="100%" />
      </GridPanel>

    </FormDialog>
  </ExternalLink>
</control>

Since the “out of the box” “External Link” dialog isn’t tall enough for the new Tag Label and Edit controls — I had no quick way of changing this since the height of the dialog is hard-coded in Sitecore.Shell.Applications.ContentEditor.Link in Sitecore.Client — I decided to create a new Content Editor field for the General Link field — this is further down in this post — which grabs the Url of the dialog and dimensions from a custom pipeline I built (the dimensions live in the patch configuration file that is found later on in this post). This custom pipeline uses the following PipelineArgs class:

using Sitecore.Collections;
using Sitecore.Pipelines;

namespace Sitecore.Sandbox.Pipelines.DialogInfo
{
    public class DialogInfoArgs : PipelineArgs
    {
        public string Message { get; set; }

        public string Url { get; set; }

        public SafeDictionary<string, string> Parameters { get; set; }

        public DialogInfoArgs()
        {
            Parameters = new SafeDictionary<string, string>();
        }

        public bool HasInformation()
        {
            return !string.IsNullOrWhiteSpace(Url);
        }
    }
}

Each dialog defined in a pipeline processor of this custom pipeline will specify the dialog’s Url; it’s message — this is how the code ascertains which dialog to load; and any properties of the dialog (e.g. height).

I then built the following class that serves as a processor for this custom pipeline:

using System;
using System.Xml;

using Sitecore.Collections;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Pipelines.DialogInfo
{
    public class SetDialogInfo
    {
        protected virtual string ParameterNameAttributeName { get; private set; }

        protected virtual string ParameterValueAttributeName { get; private set; }

        protected virtual string Message { get; private set; }

        protected virtual string Url { get; private set; }

        protected virtual SafeDictionary<string, string> Parameters { get; private set; }

        public SetDialogInfo()
        {
            Parameters = new SafeDictionary<string, string>();
        }

        public void Process(DialogInfoArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if(!CanProcess(args))
            {
                return;
            }

            SetDialogInformation(args);
        }

        protected virtual bool CanProcess(DialogInfoArgs args)
        {
            return !string.IsNullOrWhiteSpace(Message)
                && !string.IsNullOrWhiteSpace(Url)
                && args != null
                && !string.IsNullOrWhiteSpace(args.Message)
                && string.Equals(args.Message, Message, StringComparison.CurrentCultureIgnoreCase);
        }
        
        protected virtual void SetDialogInformation(DialogInfoArgs args)
        {
            args.Url = Url;
            args.Parameters = Parameters;
        }

        protected virtual void AddParameter(XmlNode node)
        {
            Assert.ArgumentNotNullOrEmpty(ParameterNameAttributeName, "ParameterNameAttributeName");
            Assert.ArgumentNotNullOrEmpty(ParameterValueAttributeName, "ParameterValueAttributeName");
            if (node == null || !IsAttributeSet(node.Attributes[ParameterNameAttributeName]) || !IsAttributeSet(node.Attributes[ParameterValueAttributeName]))
            {
                return;
            }

            Parameters[node.Attributes[ParameterNameAttributeName].Value] = node.Attributes[ParameterValueAttributeName].Value;
        }

        protected bool IsAttributeSet(XmlAttribute attribute)
        {
            return attribute != null && !string.IsNullOrEmpty(attribute.Value);
        }
    }
}

The Sitecore Configuration Factory injects the dialog’s url, message and parameters into the class instance.

The CanProcess method determines if there is match with the message that is sent via the DialogInfoArgs instance passed to the processor’s Process method. If there is a match, the Url and dialog parameters are set on the DialogInfoArgs instance.

If there isn’t a match, the processor just exits and does nothing.

I then built the following class to serve as a custom Sitecore.Shell.Applications.ContentEditor.Link:

using System;
using System.Collections.Specialized;

using Sitecore.Collections;
using Sitecore.Diagnostics;
using Sitecore.Pipelines;
using Sitecore.Shell.Applications.ContentEditor;
using Sitecore.Web.UI.Sheer;

using Sitecore.Sandbox.Pipelines.DialogInfo;

namespace Sitecore.Sandbox.Shell.Applications.ContentEditor
{
    public class TagLink : Link
    {
        public override void HandleMessage(Message message)
        {
            Assert.ArgumentNotNull(message, "message");
            if (message["id"] != ID)
            {
                return;
            }

            DialogInfoArgs info = GetDialogInformation(message.Name);
            if (info.HasInformation())
            {
                Insert(info.Url, ToNameValueCollection(info.Parameters));
                return;
            }

            base.HandleMessage(message);
        }

        protected virtual DialogInfoArgs GetDialogInformation(string message)
        {
            Assert.ArgumentNotNullOrEmpty(message, "message");
            DialogInfoArgs args = new DialogInfoArgs { Message = message };
            CorePipeline.Run("dialogInfo", args);
            return args;
        }

        protected virtual NameValueCollection ToNameValueCollection(SafeDictionary<string, string> dictionary)
        {
            if(dictionary == null)
            {
                return new NameValueCollection();
            }

            NameValueCollection collection = new NameValueCollection();
            foreach(string key in dictionary.Keys)
            {
                collection.Add(key, dictionary[key]);
            }

            return collection;
        }
    }
}

The HandleMessage method above passes the message name to the custom <dialogInfo> pipeline and gets back a DialogInfoArgs instance with the dialog’s Url and parameters if there is a match. If there is no match, then the HandleMessage method delegates to its base class’ HandleMessage method (there are dialog Urls and Parameters baked in it).

Now we need to let Sitecore know about the above Content Editor class. We do so like this:

point-to-custom-general-link-in-core

Now that the Content Editor bits are in place, we need some code to render the tag on the front-end of the website. I do this in the following class which serves as a custom <renderField> pipeline processor:

using System.Xml;

using Sitecore.Pipelines.RenderField;
using Sitecore.Xml;

namespace Sitecore.Sandbox.Pipelines.RenderField
{
    public class SetTagAttributeOnLink
    {
        private string TagXmlAttributeName { get; set; }

        private string TagAttributeName { get; set; }
        
        private string BeginningHtml { get; set; }

        public void Process(RenderFieldArgs args)
        {
            if (!CanProcess(args))
            {
                return;
            }

            args.Result.FirstPart = AddTagAttributeValue(args.Result.FirstPart, TagAttributeName, GetXmlAttributeValue(args.FieldValue, TagXmlAttributeName));
        }

        protected virtual bool CanProcess(RenderFieldArgs args)
        {
            return !string.IsNullOrWhiteSpace(TagAttributeName)
                    && !string.IsNullOrWhiteSpace(BeginningHtml)
                    && !string.IsNullOrWhiteSpace(TagXmlAttributeName)
                    && args != null
                    && args.Result != null
                    && HasXmlAttributeValue(args.FieldValue, TagAttributeName)
                    && !string.IsNullOrWhiteSpace(args.Result.FirstPart)
                    && args.Result.FirstPart.ToLower().StartsWith(BeginningHtml.ToLower());
        }

        protected virtual bool HasXmlAttributeValue(string linkXml, string attributeName)
        {
            return !string.IsNullOrWhiteSpace(GetXmlAttributeValue(linkXml, attributeName));
        }

        protected virtual string GetXmlAttributeValue(string linkXml, string attributeName)
        {
            XmlDocument xmlDocument = XmlUtil.LoadXml(linkXml);
            if(xmlDocument == null)
            {
                return string.Empty;
            }

            XmlNode node = xmlDocument.SelectSingleNode("/link");
            if (node == null)
            {
                return string.Empty;
            }

            return XmlUtil.GetAttribute(TagAttributeName, node);
        }

        protected virtual string AddTagAttributeValue(string html, string attributeName, string attributeValue)
        {
            if(string.IsNullOrWhiteSpace(html) || string.IsNullOrWhiteSpace(attributeName) || string.IsNullOrWhiteSpace(attributeValue))
            {
                return html;
            }

            int index = html.LastIndexOf(">");
            if (index < 0)
            {
                return html;
            }

            string firstPart = html.Substring(0, index);
            string attribute = string.Format(" {0}=\"{1}\"", attributeName, attributeValue);
            string lastPart = html.Substring(index);
            return string.Concat(firstPart, attribute, lastPart);
        }
    }
}

The Process method above delegates to the CanProcess method which determines if the generated HTML by the previous <renderField> pipeline processors should be manipulated — the code should only run it the generated HTML is a link and only when there is a tag attribute set on the field.

If the HTML should be manipulated, we basically add the tag attribute with its value it to the generated link HTML — this is done in the AddTagAttributeValue method.

I then wired everything together via the following patch configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <controlSources>
      <source mode="on" namespace="Sitecore.Sandbox.Shell.Applications.ContentEditor" assembly="Sitecore.Sandbox" prefix="sandbox-content"/>
    </controlSources>
    <fieldTypes>
      <fieldType name="General Link">
        <patch:attribute name="type">Sitecore.Sandbox.Data.Fields.TagLinkField, Sitecore.Sandbox</patch:attribute>
      </fieldType>
      <fieldType name="General Link with Search">
        <patch:attribute name="type">Sitecore.Sandbox.Data.Fields.TagLinkField, Sitecore.Sandbox</patch:attribute>
      </fieldType>
      <fieldType name="link">
        <patch:attribute name="type">Sitecore.Sandbox.Data.Fields.TagLinkField, Sitecore.Sandbox</patch:attribute>
        </fieldType>
    </fieldTypes>
    <pipelines>
      <dialogInfo>
        <processor type="Sitecore.Sandbox.Pipelines.DialogInfo.SetDialogInfo, Sitecore.Sandbox">
          <ParameterNameAttributeName>name</ParameterNameAttributeName>
          <ParameterValueAttributeName>value</ParameterValueAttributeName>
          <Message>contentlink:externallink</Message>
          <Url>/sitecore/shell/Applications/Dialogs/External link.aspx</Url>
          <parameters hint="raw:AddParameter">
            <parameter name="height" value="300" />
          </parameters>
        </processor>
      </dialogInfo>
      <renderField>
        <processor patch:after="processor[@type='Sitecore.Pipelines.RenderField.GetInternalLinkFieldValue, Sitecore.Kernel']" 
                   type="Sitecore.Sandbox.Pipelines.RenderField.SetTagAttributeOnLink, Sitecore.Sandbox">
          <TagXmlAttributeName>tag</TagXmlAttributeName>
          <TagAttributeName>tag</TagAttributeName>
          <BeginningHtml>&lt;a </BeginningHtml>
        </processor>  
      </renderField>
    </pipelines>
  </sitecore>
</configuration>

Let’s try this out!

For testing I added two General Link fields to the Sample Item template (/sitecore/templates/Sample/Sample Item in the master database):

added-two-general-link-fields

I also had to add two Link field controls to the sample rendering.xslt that ships with Sitecore:

added-two-link-field-controls

Let’s test the “Insert Link” dialog:

set-tag-value-insert-link

After clicking the “OK” button and saving the Item, I looked at the “Raw values” on the field and saw that the tag was added to the field’s xml:

tag-value-set-insert-link

Let’s see if this works on the “Insert External Link” dialog:

set-tag-value-insert-external-link

After clicking the “OK” button and saving the Item, I looked at the “Raw values” on the field and saw that the tag was added to the field’s xml:

tag-value-set-insert-external-link

After publishing everything, I navigated to my home page and looked at its rendered HTML. As you can see, the tag attributes were added to the links:

links-front-end-rendered

If you have any comments or thoughts on this, please drop a comment.

Until next time, keep on Sitecoring!

Expand New Tokens Added to Standard Values on All Items Using Its Template in Sitecore

If you have read some of my older posts, you probably know by now how much I love writing code that expands tokens on Items in Sitecore, and decided to build another solution that expands new tokens added to Standard Values Items of Templates — out of the box, these aren’t expanded on preexisting Items that use the Template of the Standard Values Item, and end up making their way in fields on those preexisting Items (for an alternative solution, check out this older post I wrote some time ago).

In the following solution — this solution is primarily composed of a custom pipeline — tokens that are added to fields on the Standard Values Item will be expanded on all Items that use the Template of the Standard Values Item after the Standard Values Item is saved in the Sitecore client (I hook into the <saveUI> pipeline for this action on save).

We first need a class whose instance serves as the custom pipeline’s argument object:

using Sitecore.Data.Items;
using Sitecore.Pipelines;
using System.Collections.Generic;

namespace Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems
{
    public class ExpandNewTokensOnAllItemsArgs : PipelineArgs
    {
        public Item StandardValuesItem { get; set; }

        private List<Item> items;
        public List<Item> Items 
        {
            get
            {
                if(items == null)
                {
                    items = new List<Item>();
                }

                return items;
            }
            set
            {
                items = value;
            }
        }
    }
}

The caller of the custom pipeline is required to pass the Standard Values Item that contains the new tokens. One of the processors of the custom pipeline will collect all Items that use its Template — these are stored in the Items collection property.

The instance of the following class serves as the first processor of the custom pipeline:

using System;

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Data.Managers;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems
{
    public class EnsureStandardValues
    {
        public void Process(ExpandNewTokensOnAllItemsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.StandardValuesItem, "args.StandardValuesItem");
            if(IsStandardValues(args.StandardValuesItem))
            {
                return;
            }

            args.AbortPipeline();
        }

        protected virtual bool IsStandardValues(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return StandardValuesManager.IsStandardValuesHolder(item);
        }
    }
}

This processor basically just ascertains whether the Item passed as the Standard Values Item is indeed a Standard Values Item — the code just delegates to the static IsStandardValuesHolder() method on Sitecore.Data.StandardValuesManager (this lives in Sitecore.Kernel.dll).

The instance of the next class serves as the second step of the custom pipeline:

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

using Sitecore.Data.Fields;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems
{
    public class EnsureUnexpandedTokens
    {
        private List<string> Tokens { get; set; }

        public EnsureUnexpandedTokens()
        {
            Tokens = new List<string>();
        }

        public void Process(ExpandNewTokensOnAllItemsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.StandardValuesItem, "args.StandardValuesItem");
            if (!Tokens.Any())
            {
                args.AbortPipeline();
                return;
            }

            args.StandardValuesItem.Fields.ReadAll();
            foreach(Field field in args.StandardValuesItem.Fields)
            {
                if(HasUnexpandedTokens(field))
                {
                    return;
                }
            }
            
            args.AbortPipeline();
        }

        protected virtual bool HasUnexpandedTokens(Field field)
        {
            Assert.ArgumentNotNull(field, "field");
            foreach(string token in Tokens)
            {
                if(field.Value.Contains(token))
                {
                    return true;
                }
            }

            return false;
        }
    }
}

A collection of tokens are injected into the class’ instance via the Sitecore Configuration Factory — see the patch configuration file further down in this post — and determines if tokens exist in any of its fields. If no tokens are found, then the pipeline is aborted. Otherwise, we exit the Process() method immediately.

The instance of the following class serves as the third processor of the custom pipeline:

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

using Sitecore.ContentSearch;
using Sitecore.ContentSearch.SearchTypes;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Data.Managers;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems
{
    public class CollectAllItems
    {
        public void Process(ExpandNewTokensOnAllItemsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.StandardValuesItem, "args.StandardValuesItem");
            args.Items = GetAllItemsByTemplateID(args.StandardValuesItem.TemplateID);
            if(args.Items.Any())
            {
                return;
            }

            args.AbortPipeline();
        }

        protected virtual List<Item> GetAllItemsByTemplateID(ID templateID)
        {
            Assert.ArgumentCondition(!ID.IsNullOrEmpty(templateID), "templateID", "templateID cannot be null or empty!");
            using (var context = ContentSearchManager.GetIndex("sitecore_master_index").CreateSearchContext())
            {
                var query = context.GetQueryable<SearchResultItem>().Where(i => i.TemplateId == templateID);
                return query.ToList().Select(result => result.GetItem()).ToList();
            }  
        }
    }
}

This class uses the Sitecore.ContentSearch API to find all Items that use the Template of the Standard Values Item. If at least one Item is found, we exit the Process() method immediately. Otherwise, we abort the pipeline.

The instance of the class below serves as the fourth processor of the custom pipeline:

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

using Sitecore.ContentSearch;
using Sitecore.ContentSearch.SearchTypes;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Data.Managers;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems
{
    public class FilterStandardValuesItem
    {
        public void Process(ExpandNewTokensOnAllItemsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Items, "args.Items");
            if(!args.Items.Any())
            {
                return;
            }

            args.Items = args.Items.Where(item => !IsStandardValues(item)).ToList();
        }

        protected virtual bool IsStandardValues(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return StandardValuesManager.IsStandardValuesHolder(item);
        }
    }
}

The code in this class ensures the Stardard Values Item is not in the collection of Items. It’s probably not a good idea to expand tokens on the Standard Values Item. πŸ™‚

The instance of the next class serves as the final processor of the custom pipeline:

using System.Linq;

using Sitecore.Configuration;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Data;

namespace Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems
{
    public class ExpandTokens
    {
        private MasterVariablesReplacer TokenReplacer { get; set; }

        public ExpandTokens()
        {
            TokenReplacer = GetTokenReplacer();
        }

        public void Process(ExpandNewTokensOnAllItemsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Items, "args.Items");
            if (!args.Items.Any())
            {
                args.AbortPipeline();
                return;
            }

            foreach(Item item in args.Items)
            {
                ExpandTokensOnItem(item);
            }
        }

        protected virtual void ExpandTokensOnItem(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            item.Fields.ReadAll();
            item.Editing.BeginEdit();
            TokenReplacer.ReplaceItem(item);
            item.Editing.EndEdit();
        }

        protected virtual MasterVariablesReplacer GetTokenReplacer()
        {
            return Factory.GetMasterVariablesReplacer();
        }
    }
}

The code above uses the instance of Sitecore.Data.MasterVariablesReplacer (subclass or otherwise) — this is defined in your Sitecore configuration at settings/setting[@name=”MasterVariablesReplacer”] — and passes all Items housed in the pipeline argument instance to its ReplaceItem() method — each Item is placed in an editing state before having their tokens expanded.

I then built the following class to serve as a <saveUI> pipeline processor (this pipeline is triggered when someone saves an Item in the Sitecore client):

using Sitecore;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Pipelines;
using Sitecore.Pipelines.Save;

using Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems;

namespace Sitecore.Sandbox.Pipelines.SaveUI
{
    public class ExpandNewStandardValuesTokens
    {
        private string ExpandNewTokensOnAllItemsPipeline { get; set; }

        public void Process(SaveArgs args)
        {
            Assert.IsNotNullOrEmpty(ExpandNewTokensOnAllItemsPipeline, "ExpandNewTokensOnAllItemsPipeline must be set in configuration!");
            foreach (SaveArgs.SaveItem saveItem in args.Items)
            {
                Item item = GetItem(saveItem);
                if(IsStandardValues(item))
                {
                    ExpandNewTokensOnAllItems(item);
                }
            }
        }

        protected virtual Item GetItem(SaveArgs.SaveItem saveItem)
        {
            Assert.ArgumentNotNull(saveItem, "saveItem");
            return Client.ContentDatabase.Items[saveItem.ID, saveItem.Language, saveItem.Version];
        }

        protected virtual bool IsStandardValues(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return StandardValuesManager.IsStandardValuesHolder(item);
        }

        protected virtual void ExpandNewTokensOnAllItems(Item standardValues)
        {
            CorePipeline.Run(ExpandNewTokensOnAllItemsPipeline, new ExpandNewTokensOnAllItemsArgs { StandardValuesItem = standardValues });
        }
    }
}

The code above invokes the custom pipeline when the Item being saved is a Standard Values Item — the Standard Values Item is passed to the pipeline via a new ExpandNewTokensOnAllItemsArgs instance.

I then glued all of the pieces above together in the following patch configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <expandNewTokensOnAllItems>
        <processor type="Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems.EnsureStandardValues, Sitecore.Sandbox" />
        <processor type="Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems.EnsureUnexpandedTokens, Sitecore.Sandbox">
          <Tokens hint="list">
            <Token>$name</Token>
            <Token>$id</Token>
            <Token>$parentid</Token>
            <Token>$parentname</Token>
            <Token>$date</Token>
            <Token>$time</Token>
            <Token>$now</Token>
          </Tokens>
        </processor>
        <processor type="Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems.CollectAllItems, Sitecore.Sandbox" />
        <processor type="Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems.FilterStandardValuesItem, Sitecore.Sandbox" />
        <processor type="Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems.ExpandTokens, Sitecore.Sandbox" />
      </expandNewTokensOnAllItems>
    </pipelines>
    <processors>
      <saveUI>
        <processor patch:before="saveUI/processor[@type='Sitecore.Pipelines.Save.Save, Sitecore.Kernel']" 
                   mode="on" type="Sitecore.Sandbox.Pipelines.SaveUI.ExpandNewStandardValuesTokens">
          <ExpandNewTokensOnAllItemsPipeline>expandNewTokensOnAllItems</ExpandNewTokensOnAllItemsPipeline>
        </processor>  
      </saveUI>
    </processors>
  </sitecore>
</configuration>

Let’s see this in action!

I added three new fields to a template, and added some tokens in them:

added-new-tokens

After clicking save, I navigated to one of the content Items that use this Template:

tokens-expanded

As you can see, the tokens were expanded. πŸ™‚

If you have any thoughts on this, please drop a comment.

Yet Another Way to Store Data Outside of the Sitecore Experience Platform

Last February, Sitecore MVP Nick Wesselman shared an awesome blog post on storing data outside of the Sitecore® Experience Platform™ using the NHibernate ORM framework — if you haven’t had a chance to read this, I strongly recommend that you do — which is complete magic, and a simple solution where you don’t have to worry about spinning up your own database tables for storing information.

But, let’s suppose you aren’t allowed to use an ORM like NHibernate in your solution for some reason — I won’t go into potential reasons but let’s make pretend there is one — and you have to find a way to store Sitecore specific information but don’t want to go through the trouble of spinning up a new Sitecore database due to the overhead involved. What can you do?

Well, you can still store information in a non-Sitecore database using the Sitecore API. The following “proof of concept” does this, and is basically modeled after how Sitecore manages data stored in the IDTable and Links Database.

The code in the following “proof of concept” adds/retrieves/deletes alternative URLs for Sitecore Items in the following custom database table:

itemurls-sql-table

I’m not going to talk much about the SQL table or SQL statements used in this “proof of concept” since it’s beyond the scope of this post.

Of course we all love things that are performant — and our clients love when we make things performant — so I decided to start off my solution using the following adapter — this includes the interface and concrete class — for an instance of Sitecore.Caching.Cache (this lives in Sitecore.Kernel.dll):

using System;
using System.Collections;

using Sitecore.Caching;
using Sitecore.Data;
using Sitecore.Diagnostics.PerformanceCounters;

namespace Sitecore.Sandbox.Caching
{
    public interface ICacheProvider
    {
        bool CacheWriteEnabled { get; set; }

        int Count { get; }

        CachePriority DefaultPriority { get; set; }

        bool Enabled { get; set; }

        AmountPerSecondCounter ExternalCacheClearingsCounter { get; set; }

        ID Id { get; }

        long MaxSize { get; set; }

        string Name { get; }

        long RemainingSpace { get; }

        bool Scavengable { get; set; }

        long Size { get; }

        object SyncRoot { get; }

        object this[object key] { get; }

        void Add(ID key, ICacheable data);

        void Add(ID key, string value);

        void Add(string key, ICacheable data);

        void Add(string key, ID value);

        Cache.CacheEntry Add(string key, string data);

        void Add(ID key, object data, long dataLength);

        void Add(object key, object data, long dataLength);

        void Add(string key, object data, long dataLength);

        void Add(object key, object data, long dataLength, DateTime absoluteExpiration);

        void Add(object key, object data, long dataLength, TimeSpan slidingExpiration);

        Cache.CacheEntry Add(string key, object data, long dataLength, DateTime absoluteExpiration);

        void Add(string key, object data, long dataLength, EventHandler<EntryRemovedEventArgs> removedHandler);

        Cache.CacheEntry Add(string key, object data, long dataLength, TimeSpan slidingExpiration);

        void Add(object key, object data, long dataLength, TimeSpan slidingExpiration, DateTime absoluteExpiration);

        void Clear();

        bool ContainsKey(ID key);

        bool ContainsKey(object key);

        ArrayList GetCacheKeys();

        ArrayList GetCacheKeys(string keyPrefix);

        Cache.CacheEntry GetEntry(object key, bool updateAccessed);

        object GetValue(object key);

        void Remove(object key);

        void Remove<TKey>(Predicate<TKey> predicate);

        void RemoveKeysContaining(string value);

        void RemovePrefix(string keyPrefix);

        void Scavenge();
    }
}
using System;
using System.Collections;

using Sitecore.Caching;
using Sitecore.Data;
using Sitecore.Diagnostics;
using Sitecore.Diagnostics.PerformanceCounters;

namespace Sitecore.Sandbox.Caching
{
    public class CacheProvider : ICacheProvider
    {
        private Cache Cache { get; set; }

        public CacheProvider(string cacheName, string cacheSize)
        {
            Assert.ArgumentNotNullOrEmpty(cacheName, "cacheName");
            Assert.ArgumentNotNullOrEmpty(cacheSize, "cacheSize");
            Cache = new Cache(cacheName, StringUtil.ParseSizeString(cacheSize));
        }

        public bool CacheWriteEnabled 
        {
            get
            {
                return Cache.CacheWriteEnabled;
            }
            set
            {
                Cache.CacheWriteEnabled = value;
            }
        }

        public int Count 
        {
            get
            {
                return Cache.Count;
            }
        }

        public CachePriority DefaultPriority 
        {
            get
            {
                return Cache.DefaultPriority;
            }
            set
            {
                Cache.DefaultPriority = value;
            }
        }

        public bool Enabled 
        {
            get
            {
                return Cache.Enabled;
            }
            set
            {
                Cache.Enabled = value;
            }
        }

        public AmountPerSecondCounter ExternalCacheClearingsCounter 
        {
            get
            {
                return Cache.ExternalCacheClearingsCounter;
            }
            set
            {
                Cache.ExternalCacheClearingsCounter = value;
            }
        }

        public ID Id 
        {
            get
            {
                return Cache.Id;
            }
        }

        public long MaxSize 
        {
            get
            {
                return Cache.MaxSize;
            }
            set
            {
                Cache.MaxSize = value;
            }
        }

        public string Name 
        {
            get
            {
                return Cache.Name;
            }
        }

        public long RemainingSpace
        {
            get
            {
                return Cache.RemainingSpace;
            }
        }

        public bool Scavengable 
        {
            get
            {
                return Cache.Scavengable;
            }
            set
            {
                Cache.Scavengable = value;
            }
        }

        public long Size
        {
            get
            {
                return Cache.Size;
            }
        }

        public object SyncRoot
        {
            get
            {
                return Cache.SyncRoot;
            }
        }

        public object this[object key] 
        { 
            get
            {
                return Cache[key];
            } 
        }

        public void Add(ID key, ICacheable data)
        {
            Cache.Add(key, data);
        }

        public void Add(ID key, string value)
        {
            Cache.Add(key, value);
        }

        public void Add(string key, ICacheable data)
        {
            Cache.Add(key, data);
        }

        public void Add(string key, ID value)
        {
            Cache.Add(key, value);
        }

        public Cache.CacheEntry Add(string key, string data)
        {
            return Cache.Add(key, data);
        }

        public void Add(ID key, object data, long dataLength)
        {
            Cache.Add(key, data, dataLength);
        }

        public void Add(object key, object data, long dataLength)
        {
            Cache.Add(key, data, dataLength);
        }

        public void Add(string key, object data, long dataLength)
        {
            Cache.Add(key, data, dataLength);
        }

        public void Add(object key, object data, long dataLength, DateTime absoluteExpiration)
        {
            Cache.Add(key, data, dataLength, absoluteExpiration);
        }

        public void Add(object key, object data, long dataLength, TimeSpan slidingExpiration)
        {
            Cache.Add(key, data, dataLength, slidingExpiration);
        }

        public Cache.CacheEntry Add(string key, object data, long dataLength, DateTime absoluteExpiration)
        {
            return Cache.Add(key, data, dataLength, absoluteExpiration);
        }

        public void Add(string key, object data, long dataLength, EventHandler<EntryRemovedEventArgs> removedHandler)
        {
            Cache.Add(key, data, dataLength, removedHandler);
        }

        public Cache.CacheEntry Add(string key, object data, long dataLength, TimeSpan slidingExpiration)
        {
            return Cache.Add(key, data, dataLength, slidingExpiration);
        }

        public void Add(object key, object data, long dataLength, TimeSpan slidingExpiration, DateTime absoluteExpiration)
        {
            Cache.Add(key, data, dataLength, slidingExpiration, absoluteExpiration);
        }

        public void Clear()
        {
            Cache.Clear();
        }

        public bool ContainsKey(ID key)
        {
            return Cache.ContainsKey(key);
        }

        public bool ContainsKey(object key)
        {
            return Cache.ContainsKey(key);
        }

        public ArrayList GetCacheKeys()
        {
            return Cache.GetCacheKeys();
        }

        public ArrayList GetCacheKeys(string keyPrefix)
        {
            return Cache.GetCacheKeys(keyPrefix);
        }

        public Cache.CacheEntry GetEntry(object key, bool updateAccessed)
        {
            return Cache.GetEntry(key, updateAccessed);
        }
        
        public object GetValue(object key)
        {
            return Cache.GetValue(key);
        }

        public void Remove(object key)
        {
            Cache.Remove(key);
        }

        public void Remove<TKey>(Predicate<TKey> predicate)
        {
            Cache.Remove<TKey>(predicate);
        }

        public void RemoveKeysContaining(string value)
        {
            Cache.RemoveKeysContaining(value);
        }

        public void RemovePrefix(string keyPrefix)
        {
            Cache.RemovePrefix(keyPrefix);
        }

        public void Scavenge()
        {
            Cache.Scavenge();
        }
    }
}

I’m not going to talk about the above interface or class since it just wraps Sitecore.Caching.Cache, and there isn’t much to talk about here.

Next, I spun up the following class that represents an entry in our custom SQL table:

using System;

using Sitecore.Caching;
using Sitecore.Data;
using Sitecore.Reflection;

using Newtonsoft.Json;

namespace Sitecore.Sandbox.Data.Providers.ItemUrls
{
    public class ItemUrlEntry : ICacheable, ICloneable
    {
        public ID ItemID { get; set; }

        public string Site { get; set; }
        
        public string Database { get; set; }
        
        public string Url { get; set; }

        bool cacheable;
        bool ICacheable.Cacheable
        {
            get
            {
                return cacheable;
            }
            set
            {
                cacheable = value;
            }
        }

        bool ICacheable.Immutable
        {
            get
            {
                return true;
            }
        }

        event DataLengthChangedDelegate ICacheable.DataLengthChanged
        {
            add
            {
            }
            remove
            {
            }
        }

        long ICacheable.GetDataLength()
        {
            return TypeUtil.SizeOfID()
                    + TypeUtil.SizeOfString(Site) 
                    + TypeUtil.SizeOfString(Database) 
                    + TypeUtil.SizeOfString(Url);
        }

        public object Clone()
        {
            return new ItemUrlEntry { ItemID = ItemID, Site = Site, Database = Database, Url = Url };
        }

        public override string ToString()
        {
            return JsonConvert.SerializeObject(this);
        }
    }
}

Entries can contain the ID of the Sitecore Item; the specific site we are storing this url for; and the target Database.

You’ll notice I’ve implemented the Sitecore.Caching.ICacheable interface. I’ve done this so I can store entries in cache for performance. I’m not going to go much into the details of how this works since there isn’t much to point out.

I also override the ToString() method for testing purposes. You’ll see this in action later on when we test this together.

Next, we need some sort of provider to manage these entries. I’ve defined the following interface for such a provider:

using System.Collections.Generic;

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Sites;

namespace Sitecore.Sandbox.Data.Providers.ItemUrls
{
    public interface IItemUrlsProvider
    {
        void AddEntry(ItemUrlEntry entry);
        
        void RemoveEntry(ItemUrlEntry entry);

        Item GetItem(ItemUrlEntry entry);

        ItemUrlEntry GetEntry(ItemUrlEntry entry);

        IEnumerable<ItemUrlEntry> GetAllEntries();
    }
}

IItemUrlsProviders should have the ability to add/remove/retrieve entries. They should also offer the ability to get all entries — I need this for testing later on in this post.

Plus, as a “nice to have”, these providers should return a Sitecore Item for a given entry. Such would be useful when retrieving and setting the context Sitecore Item via a custom Item Resolver (you would typically have an <httpRequestBegin> pipeline processor that does this).

I then created the following class that implements the IItemUrlsProvider interface defined above. This class is specific to adding/removing/retrieving entries from a custom SQL database:

using System;
using System.Collections.Generic;

using Sitecore.Caching;
using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.DataProviders.Sql;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

using Sitecore.Sandbox.Caching;

namespace Sitecore.Sandbox.Data.Providers.ItemUrls.SqlServer
{
    public class SqlServerItemUrlsProvider : IItemUrlsProvider
    {
        private SqlDataApi SqlDataApi { get; set; }

        protected ICacheProvider CacheProvider { get; private set; }

        protected string CachePrefix { get; private set; }

        public SqlServerItemUrlsProvider(SqlDataApi sqlDataApi, ICacheProvider cacheProvider, string cachePrefix)
        {
            Assert.ArgumentNotNull(sqlDataApi, "sqlDataApi");
            Assert.ArgumentNotNull(cacheProvider, "cacheProvider");
            Assert.ArgumentNotNullOrEmpty(cachePrefix, "cachePrefix");
            SqlDataApi = sqlDataApi;
            CacheProvider = cacheProvider;
            CachePrefix = cachePrefix;
        }

        public void AddEntry(ItemUrlEntry entry)
        {
            Assert.ArgumentNotNull(entry, "entry");
            Assert.ArgumentCondition(!ID.IsNullOrEmpty(entry.ItemID), "entry.ItemID", "entry.ItemID cannot be null or empty");
            Assert.ArgumentNotNullOrEmpty(entry.Site, "entry.Site");
            Assert.ArgumentNotNullOrEmpty(entry.Database, "entry.Database");
            Assert.ArgumentNotNullOrEmpty(entry.Url, "entry.Url");
            const string addEntrySql = "INSERT INTO {0}ItemUrls{1} ( {0}ItemID{1}, {0}Site{1}, {0}Database{1}, {0}Url{1} ) VALUES ( {2}itemID{3}, {2}site{3}, {2}database{3}, {2}url{3} )";
            var success = Factory.GetRetryer().Execute(() =>
            {
                object[] parameters = new object[] { "itemID", entry.ItemID, "site", entry.Site, "database", entry.Database, "url", entry.Url };
                return SqlDataApi.Execute(addEntrySql, parameters) > 0;
            });

            if (success)
            {
                AddToCache(entry);
            }
        }

        public void RemoveEntry(ItemUrlEntry entry)
        {
            const string deleteEntrySql = "DELETE FROM {0}ItemUrls{1} WHERE {0}Site{1} = {2}site{3} AND {0}Database{1} = {2}database{3} AND {0}Url{1} = {2}url{3}";
            var success = Factory.GetRetryer().Execute(() =>
            {
                object[] parameters = new object[] { "site", entry.Site, "database", entry.Database, "url", entry.Url };
                return SqlDataApi.Execute(deleteEntrySql, parameters) > 0;
            });

            if (success)
            {
                RemoveFromCache(entry);
            }
        }

        public Item GetItem(ItemUrlEntry entry)
        {
            ItemUrlEntry foundEntry = GetEntry(entry);
            if(foundEntry == null)
            {
                return null;
            }

            Database database = Factory.GetDatabase(foundEntry.Database);
            if(database == null)
            {
                return null;
            }

            try
            {
                return database.Items[foundEntry.ItemID];
            }
            catch(Exception ex)
            {
                Log.Error(ToString(), ex, this);
            }

            return null;
        }

        public ItemUrlEntry GetEntry(ItemUrlEntry entry)
        {
            ItemUrlEntry foundEntry = GetFromCache(entry);
            if (foundEntry != null)
            {
                return foundEntry;
            }

            const string getEntrySql = "SELECT {0}ItemID{1} FROM {0}ItemUrls{1} WHERE {2}Site = {2}site{3} AND {2}Database{3} = {2}database{3} AND {0}Url{1} = {2}url{3}";
            object[] parameters = new object[] { "site", entry.Site, "database", entry.Database, "url", entry.Url };
            using (DataProviderReader reader = SqlDataApi.CreateReader(getEntrySql, parameters))
            {
                if (!reader.Read())
                {
                    return null;
                }

                ID itemID = ID.Parse(SqlDataApi.GetGuid(0, reader));
                if (ID.IsNullOrEmpty(itemID))
                {
                    return null;
                }

                foundEntry = entry.Clone() as ItemUrlEntry;
                foundEntry.ItemID = itemID;
                AddToCache(entry);
                return foundEntry;
            }
        }

        public IEnumerable<ItemUrlEntry> GetAllEntries()
        {
            const string getAllEntriesSql = "SELECT {0}ItemID{1}, {0}Site{1}, {0}Database{1}, {0}Url{1} FROM {0}ItemUrls{1}";
            IList<ItemUrlEntry> entries = new List<ItemUrlEntry>();
            using (DataProviderReader reader = SqlDataApi.CreateReader(getAllEntriesSql, new object[0]))
            {
                while(reader.Read())
                {
                    ID itemID = ID.Parse(SqlDataApi.GetGuid(0, reader));
                    if (!ID.IsNullOrEmpty(itemID))
                    {
                        entries.Add
                        (
                            new ItemUrlEntry 
                            {
                                ItemID = itemID, 
                                Site = SqlDataApi.GetString(1, reader), 
                                Database = SqlDataApi.GetString(2, reader), 
                                Url =  SqlDataApi.GetString(3, reader)
                            }
                        );
                    } 
                }
            }

            return entries;
        }

        protected virtual void AddToCache(ItemUrlEntry entry)
        {
            CacheProvider.Add(GetCacheKey(entry), entry);
        }

        protected virtual void RemoveFromCache(ItemUrlEntry entry)
        {
            CacheProvider.Remove(GetCacheKey(entry));
        }

        protected virtual ItemUrlEntry GetFromCache(ItemUrlEntry entry)
        {
            return CacheProvider[GetCacheKey(entry)] as ItemUrlEntry;
        }

        protected virtual string GetCacheKey(ItemUrlEntry entry)
        {
            Assert.ArgumentNotNull(entry, "entry");
            Assert.ArgumentNotNull(entry.Site, "entry.Site");
            Assert.ArgumentNotNull(entry.Database, "entry.Database");
            Assert.ArgumentNotNull(entry.Url, "entry.Url");
            return string.Join("#", CachePrefix, entry.Site, entry.Database, entry.Url);
        }
    }
}

Sitecore.Data.DataProviders.Sql.SqlDataApi and ICacheProvider instances along with a cache prefix are injected into the class instance’s constructor using the Sitecore Configuration Factory (you’ll get a better idea of how this happens when you have a look at the patch configuration file towards the bottom of this post). These are saved to properties on the class instance so they can be leveraged by the methods on the class.

One thing I would like to point out is the Sitecore.Data.DataProviders.Sql.SqlDataApi class is an abstraction — it’s an abstract class that is subclassed by Sitecore.Data.SqlServer.SqlServerDataApi in Sitecore.Kernel.dll. This concrete class does most of the leg work on talking to the SQL Server database, and we just utilize methods on it for adding/deleting/removing entries.

The AddEntry() method delegates the database saving operation to the Sitecore.Data.DataProviders.Sql.SqlDataApi instance, and then uses the ICacheProvider instance for storing the entry in cache.

The RemoveEntry() method also leverages the Sitecore.Data.DataProviders.Sql.SqlDataApi instance for deleting the entry from the database, and then removes the entry from cache via the ICacheProvider instance.

The GetEntry() method does exactly what you think it does. It tries to get the entry first from cache via the ICacheProvider instance and then the database via the Sitecore.Data.DataProviders.Sql.SqlDataApi instance if the entry was not found in cache. If the Item was not in cache but was in the database, the GetEntry() method then saves the entry to cache.

I then created the following Singleton for testing:

using System;
using System.Collections.Generic;

using Sitecore.Configuration;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Data.Providers.ItemUrls
{
    public class ItemUrlsProvider : IItemUrlsProvider
    {
        private static readonly Lazy<IItemUrlsProvider> lazyInstance = new Lazy<IItemUrlsProvider>(() => new ItemUrlsProvider());

        public static IItemUrlsProvider Current { get { return lazyInstance.Value; } }

        private IItemUrlsProvider InnerProvider { get; set; }

        private ItemUrlsProvider()
        {
            InnerProvider = GetInnerProvider();
        }

        public void AddEntry(ItemUrlEntry entry)
        {
            InnerProvider.AddEntry(entry);
        }

        public void RemoveEntry(ItemUrlEntry entry)
        {
            InnerProvider.RemoveEntry(entry);
        }

        public Item GetItem(ItemUrlEntry entry)
        {
            return InnerProvider.GetItem(entry);
        }

        public ItemUrlEntry GetEntry(ItemUrlEntry entry)
        {
            return InnerProvider.GetEntry(entry);
        }

        public IEnumerable<ItemUrlEntry> GetAllEntries()
        {
            return InnerProvider.GetAllEntries();
        }

        protected virtual IItemUrlsProvider GetInnerProvider()
        {
            IItemUrlsProvider provider = Factory.CreateObject("itemUrlsProvider", true) as IItemUrlsProvider;
            Assert.IsNotNull(provider, "itemUrlsProvider must be set in configuration!");
            return provider;
        }
    }
}

The Singleton above basically decorates the IItemUrlsProvider instance defined in Sitecore configuration — see the configuration file below — and delegates method calls to it.

I then wired everything together using the following patch configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <itemUrlsProvider id="custom" type="Sitecore.Sandbox.Data.Providers.ItemUrls.$(database).$(database)ItemUrlsProvider, Sitecore.Sandbox" singleInstance="true">
      <param type="Sitecore.Data.$(database).$(database)DataApi, Sitecore.Kernel" desc="sqlDataApi">
        <param connectionStringName="$(id)"/>
      </param>
      <param type="Sitecore.Sandbox.Caching.CacheProvider, Sitecore.Sandbox" desc="cacheProvider">
        <param desc="cacheName">[ItemUrls]</param>
        <param desc="cacheSize">500KB</param>
      </param>
      <param desc="cachePrefix">ItemUrlsEntry</param>
    </itemUrlsProvider>
  </sitecore>
</configuration>

For testing, I whipped up a standalone ASP.NET Web Form (yes, there are more elegant ways to do this but it’s Sunday so cut me some slack πŸ˜‰ ):

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

using Sitecore.Data.Items;

using Sitecore.Sandbox.Data.Providers.ItemUrls;

namespace Sitecore.Sandbox.Web.tests
{
    public partial class ItemUrlsProviderTest : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            IItemUrlsProvider provider = ItemUrlsProvider.Current;
            Item home = Sitecore.Context.Database.GetItem("/sitecore/content/home");
            StringBuilder output = new StringBuilder();
            
            ItemUrlEntry firstEntry = new ItemUrlEntry { ItemID = home.ID, Site = Sitecore.Context.Site.Name, Database = Sitecore.Context.Database.Name, Url = "/this/does/not/exist" };
            output.AppendFormat("Adding {0} as an entry.<br />", firstEntry);
            provider.AddEntry(firstEntry);

            ItemUrlEntry secondEntry = new ItemUrlEntry { ItemID = home.ID, Site = Sitecore.Context.Site.Name, Database = Sitecore.Context.Database.Name, Url = "/fake/url" };
            output.AppendFormat("Adding {0} as an entry.<br />", secondEntry);
            provider.AddEntry(secondEntry);

            ItemUrlEntry thirdEntry = new ItemUrlEntry { ItemID = home.ID, Site = Sitecore.Context.Site.Name, Database = Sitecore.Context.Database.Name, Url = "/another/fake/url" };
            output.AppendFormat("Adding {0} as an entry.<hr />", thirdEntry);
            provider.AddEntry(thirdEntry);

            ItemUrlEntry fourthEntry = new ItemUrlEntry { ItemID = home.ID, Site = Sitecore.Context.Site.Name, Database = Sitecore.Context.Database.Name, Url = "/blah/blah/blah" };
            output.AppendFormat("Adding {0} as an entry.<hr />", fourthEntry);
            provider.AddEntry(fourthEntry);

            ItemUrlEntry fifthEntry = new ItemUrlEntry { ItemID = home.ID, Site = Sitecore.Context.Site.Name, Database = Sitecore.Context.Database.Name, Url = "/i/am/a/url" };
            output.AppendFormat("Adding {0} as an entry.<hr />", fifthEntry);
            provider.AddEntry(fifthEntry);

            output.AppendFormat("Current saved entries:<br /><br />{0}<hr />", string.Join("<br />", provider.GetAllEntries().Select(entry => entry.ToString())));

            output.AppendFormat("Removing entry {0}.<br /><br />", firstEntry.ToString());
            provider.RemoveEntry(firstEntry);

            output.AppendFormat("Current saved entries:<br /><br />{0}", string.Join("<br />", provider.GetAllEntries().Select(entry => entry.ToString())));
            litResults.Text = output.ToString();
        }
    }
}

The test above adds five entries, and then deletes one. It also outputs what’s in the database after specific operations.

After doing a build, I pulled up the above Web Form in my browser and saw this once the page was done rendering:

ItemUrlsProviderTest

If you have any thoughts on this, please share in a comment.

Until next time, have a Sitecoredatalicious day!

Make Incompatible Class Interfaces Work Together using the Adapter Pattern in Sitecore

This post is a continuation of a series of blog posts I’m putting together around using design patterns in Sitecore, and will share a “proof of concept” on employing the Adapter pattern — a structural pattern used when you need classes of different interfaces to work together. In other words, you need one class’ interface to “adapt” to another.

Believe it or not, most developers — and hopefully most reading this post — are already quite familiar with the Adapter pattern even if it’s not recognizable by name.

How so?

Well, I don’t know about you but I spend a lot of time making code from different APIs work together. I typically have to do this when making use of a third-party library that I cannot change, and usually do this by having one class “wrap” another and its methods. Commonly, the Adapter pattern is known as a “wrapper”.

I showcased the following “proof of concept” during my presentation at SUGCON Europe 2015. This code flips images upside down after they are uploaded to the Media Library — yeah, I know, pretty useful, right? πŸ˜‰ Keep in mind this code is for educational purposes only, and serves no utility in any practical sense in your Sitecore solutions — if you do have a business need for flipping images upside down after uploading them to the Media Library, please share in a comment.

I first started off with the following interface:

using Sitecore.Data.Items;

namespace Sitecore.Sandbox.Resources.Media
{
    public interface IMediaImageFlipper
    {
        MediaItem MediaItem { get; set; }

        void Flip();
    }
}

Classes that implement the interface above basically flip images within Sitecore.Data.Items.MediaItem instances — this is defined in Sitecore.Kernel.dll — upside down via their Flip() method.

The following class implements the above interface:

using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;

using Sitecore.Data.Items;
using Sitecore.Diagnostics;

using ImageProcessor;

namespace Sitecore.Sandbox.Resources.Media
{
    public class ImageFactoryFlipper : IMediaImageFlipper
    {
        public MediaItem MediaItem { get; set; }

        private List<string> TargetMimeTypes { get; set; }

        private ImageFactory ImageFactory { get; set; }

        public ImageFactoryFlipper()
            : this(new ImageFactory())
        {
        }

        public ImageFactoryFlipper(ImageFactory imageFactory)
        {
            TargetMimeTypes = new List<string>();
            Assert.ArgumentNotNull(imageFactory, "imageFactory");
            ImageFactory = imageFactory;
        }

        public void Flip()
        {
            if (!ShouldFlip(MediaItem))
            {
                return;
            }

            using (MemoryStream outputStream = new MemoryStream())
            {
                ImageFactory.Load(MediaItem.GetMediaStream()).Rotate(180.0f).Save(outputStream);

                using (new EditContext(MediaItem))
                {
                    MediaItem.InnerItem.Fields["Blob"].SetBlobStream(outputStream);
                }
            }
        }

        protected virtual bool ShouldFlip(MediaItem mediaItem)
        {
            if (mediaItem == null || string.IsNullOrWhiteSpace(mediaItem.MimeType) || !TargetMimeTypes.Any() || ImageFactory == null)
            {
                return false;
            }

            return TargetMimeTypes.Any(targetMimeType => string.Equals(targetMimeType, mediaItem.MimeType, StringComparison.CurrentCultureIgnoreCase));
        }
    }
}

In the above class, I am “wrapping” an ImageFactory class instance — this class comes with the ImageProcessor .NET library which does some image manipulation (I found this .NET library via a Google search and have no idea how good it is, but it’s good enough for this “proof of concept”) — and inject it using Poor man’s dependency injection via the default constructor.

The Flip() method is where the magic happens. It calls the ShouldFlip() method which ascertains whether the MediaItem property is set on the class instance and whether the image found within it should be flipped — an image should be flipped if it has a MIME type that is within the list of MIME types that are injected into the class instance via the Sitecore Configuration Factory (see the patch configuration file below).

If the image should be flipped, the Flip() method uses the ImageFactory instance to flip the image upside down — it does this by rotating it 180 degrees — and then saves the flipped image contained within the MemoryStream instance into the MediaItem’s Blob field (this is where images are saved on Media Library Items).

Now that we have a class that flips images, we need a MediaCreator — a subclass of Sitecore.Resources.Media.MediaCreator (this lives in Sitecore.Kernel.dll) — to leverage an instance of the IMediaImageFlipper to do the image manipulation. The follow class does this:

using System.IO;

using Sitecore.Data.Items;
using Sitecore.Resources.Media;

namespace Sitecore.Sandbox.Resources.Media
{
    public class ImageFlipperMediaCreator : MediaCreator
    {
        private IMediaImageFlipper Flipper { get; set; }

        public override Item CreateFromStream(Stream stream, string filePath, bool setStreamIfEmpty, MediaCreatorOptions options)
        {
            MediaItem mediaItem = base.CreateFromStream(stream, filePath, setStreamIfEmpty, options);
            if (Flipper == null)
            {
                return mediaItem;
            }

            Flipper.MediaItem = mediaItem;
            Flipper.Flip();
            return mediaItem;
        }
    }
}

After an image is uploaded to the Media Library, we pass the new MediaItem to the IMediaImageFlipper instance — this instance is injected using the Sitecore Configuration Factory (see the configuration file below) — and invoke its Flip() method to flip the image, and return the new MediaItem when complete.

I then utilize an instance of the MediaCreator above in a subclass of Resources.Media.MediaProvider.MediaProvider (I am going to replace the “out of the box” MediaProvider with the following class using the configuration file below):

using Sitecore.Diagnostics;
using Sitecore.Resources.Media;

namespace Sitecore.Sandbox.Resources.Media
{
    public class ImageFlipperMediaProvider : MediaProvider
    {
        private MediaCreator FlipperCreator { get; set; }

        public override MediaCreator Creator
        {
            get
            {
                return FlipperCreator ?? base.Creator;
            }
            set
            {
                Assert.ArgumentNotNull(value, "value");
                FlipperCreator = value;
            }
        }
    }
}

The MediaCreator that lives in the FlipperCreator property is injected into the class instance through the Sitecore Configuration Factory (see the patch configuration file below), and is returned by the Creator property’s accessor if it’s not null.

I then registered all of the above in Sitecore using the following patch configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <mediaLibrary>
      <mediaProvider>
        <patch:attribute name="type">Sitecore.Sandbox.Resources.Media.ImageFlipperMediaProvider, Sitecore.Sandbox</patch:attribute>
        <FlipperCreator type="Sitecore.Sandbox.Resources.Media.ImageFlipperMediaCreator, Sitecore.Sandbox">
          <Flipper type="Sitecore.Sandbox.Resources.Media.ImageFactoryFlipper, Sitecore.Sandbox">
            <TargetMimeTypes hint="list">
              <TargetMimeType>image/jpeg</TargetMimeType>
              <TargetMimeType>image/png</TargetMimeType>
            </TargetMimeTypes>
          </Flipper>
		    </FlipperCreator>
      </mediaProvider>
    </mediaLibrary>
  </sitecore>
</configuration>

Let’s test this.

I selected the following images for uploading to the Media Library:

selected-some-files-to-upload

As you can see, all uploaded images were flipped upside down:

images-upside-down

If you have any thoughts on this, or examples where you’ve employed the Adapter pattern in your Sitecore solutions, please share in a comment.

Until next time, have a Sitecorelicious day!

Use the Factory Method Pattern for Object Creation in Sitecore

This post is a continuation of a series of posts I’m putting together around using design patterns in Sitecore, and will show a “proof of concept” around using the Factory Method pattern — a creational pattern whereby client code obtain instances of objects without knowing the concrete class types of these objects. This pattern promotes loose coupling between objects being created and the client code that use them.

In this “proof of concept”, I am using an Item Validator to call a factory method to obtain a “fields comparer” object to ascertain whether one field contains a value greater than a value in another field, and will show this for two different field types in Sitecore.

I first defined an interface for objects that will compare values in two Sitecore fields:

using Sitecore.Data.Fields;

namespace Sitecore.Sandbox.Data.Validators.ItemValidators.FieldComparers
{
    public interface IFieldsComparer
    {
        bool IsFieldOneLessThanOrEqualToFieldTwo(Field fieldOne, Field fieldTwo);
    }
}

Instances of classes that implement the IFieldsComparer interface above will ascertain whether the value in fieldOne is less than or equal to the value in fieldTwo.

I then defined a class that implements the IFieldsComparer interface to compare integer values in two fields:

using System;

using Sitecore.Data.Fields;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Data.Validators.ItemValidators.FieldComparers
{
    public class IntegerFieldsComparer : IFieldsComparer
    {
        public bool IsFieldOneLessThanOrEqualToFieldTwo(Field fieldOne, Field fieldTwo)
        {
            Assert.ArgumentNotNull(fieldOne, "fieldOne");
            Assert.ArgumentNotNull(fieldTwo, "fieldTwo");
            return ParseInteger(fieldOne) <= ParseInteger(fieldTwo);
        }

        protected virtual int ParseInteger(Field field)
        {
            int fieldValue;
            int.TryParse(field.Value, out fieldValue);
            return fieldValue;
        }
    }
}

There isn’t much to see in the class above. The class parses the integer values in each field, and checks to see if the value in fieldOne is less than or equal to the value in fieldTwo.

Now, let’s create a another class — one that compares DateTime values in two different fields:

using System;

using Sitecore.Data.Fields;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Data.Validators.ItemValidators.FieldComparers
{
    public class DateFieldsComparer : IFieldsComparer
    {
        public bool IsFieldOneLessThanOrEqualToFieldTwo(Field fieldOne, Field fieldTwo)
        {
            Assert.ArgumentNotNull(fieldOne, "fieldOne");
            Assert.ArgumentNotNull(fieldTwo, "fieldTwo");
            return ParseDateTime(fieldOne) <= ParseDateTime(fieldTwo);
        }

        protected virtual DateTime ParseDateTime(Field field)
        {
            return DateUtil.IsoDateToDateTime(field.Value);
        }
    }
}

Similarly to the IFieldsComparer class for integers, the class above parses the field values into DateTime instances, and ascertains whether the DateTime value in fieldOne occurs before or at the same time as the DateTime value in fieldTwo.

You might now be asking “Mike, what about other field types?” Well, I could have defined more IFieldsComparer classes for other fields but this post would go on and on, and we both don’t want that πŸ˜‰ So, to account for other field types, I’ve defined the following Null Object for fields that are not accounted for:

using Sitecore.Data.Fields;

namespace Sitecore.Sandbox.Data.Validators.ItemValidators.FieldComparers
{
    public class NullFieldsComparer : IFieldsComparer
    {
        public bool IsFieldOneLessThanOrEqualToFieldTwo(Field fieldOne, Field fieldTwo)
        {
            return true;
        }
    }
}

The Null Object class above just returns true without performing any comparison.

Now that we have “fields comparers”, we need a Factory method. I’ve defined the following interface for objects that will create instances of our IFieldsComparer:

using Sitecore.Data.Fields;

namespace Sitecore.Sandbox.Data.Validators.ItemValidators.FieldComparers
{
    public interface IFieldsComparerFactory
    {
        IFieldsComparer GetFieldsComparer(Field fieldOne, Field fieldTwo);
    }
}

Instances of classes that implement the interface above will return the appropriate IFieldsComparer for comparing the two passed fields.

The following class implements the IFieldsComparerFactory interface above:

using System;
using System.Collections.Generic;
using System.Xml;

using Sitecore.Configuration;
using Sitecore.Data.Fields;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Data.Validators.ItemValidators.FieldComparers
{
    public class FieldsComparerFactory : IFieldsComparerFactory
    {
        private static volatile IFieldsComparerFactory current;
        private static object locker = new Object();

        public static IFieldsComparerFactory Current
        {
            get
            {
                if (current == null)
                {
                    lock (locker)
                    {
                        if (current == null)
                        {
                            current = CreateNewFieldsComparerFactory();
                        }
                    }
                }

                return current;
            }
        }

        private static IDictionary<string, XmlNode> FieldsComparersTypes { get; set; }

        private IFieldsComparer NullFieldsComparer { get; set; }

        static FieldsComparerFactory()
        {
            FieldsComparersTypes = new Dictionary<string, XmlNode>();
        }

        public IFieldsComparer GetFieldsComparer(Field fieldOne, Field fieldTwo)
        {
            Assert.IsNotNull(NullFieldsComparer, "NullFieldsComparer must be set in configuration!");
            if (!AreEqualIgnoreCase(fieldOne.Type, fieldTwo.Type) || !FieldsComparersTypes.ContainsKey(fieldOne.Type))
            {
                return NullFieldsComparer;
            }

            XmlNode configNode = FieldsComparersTypes[fieldOne.Type];
            if(configNode == null)
            {
                return NullFieldsComparer;
            }

            IFieldsComparer comparer = Factory.CreateObject(configNode, false) as IFieldsComparer;
            if (comparer == null)
            {
                return NullFieldsComparer;
            }

            return comparer;
        }

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

        protected virtual void AddFieldsComparerConfigNode(XmlNode configNode)
        {
            if(configNode.Attributes["fieldType"] == null || string.IsNullOrWhiteSpace(configNode.Attributes["fieldType"].Value))
            {
                return;
            }

            if (configNode.Attributes["type"] == null || string.IsNullOrWhiteSpace(configNode.Attributes["type"].Value))
            {
                return;
            }

            FieldsComparersTypes[configNode.Attributes["fieldType"].Value] = configNode;
        }

        private static IFieldsComparerFactory CreateNewFieldsComparerFactory()
        {
            return Factory.CreateObject("factories/fieldsComparerFactory", true) as IFieldsComparerFactory;
        }
    }
}

The AddFieldsComparerConfigNode() method above is used by the Sitecore Configuration Factory to add configuration-defined Xml nodes that define field types and their IFieldsComparer — these are placed into the FieldsComparersTypes dictionary for later look-up and instantiation.

The GetFieldsComparer() factory method tries to figure out which IFieldsComparer to return from the FieldsComparersTypes dictionary. If an appropriate IFieldsComparer is found for the two fields, the method uses Sitecore.Configuration.Factory.CreateObject() — this is defined in Sitecore.Kernel.dll — to create the instance that is defined in the type attribute of the XmlNode that is stored in the FieldsComparersTypes dictionary.

If an appropriate IFieldsComparer cannot be determined for the passed fields, then the Null Object IFieldsComparer — this is injected into the NullFieldsComparer property via the Sitecore Configuration Factory — is returned.

As a quick and dirty solution for retrieving an instance of the class above, I’ve used the Singleton pattern. An instance of the class above is created by the Sitecore Configuration Factory via the CreateNewFieldsComparerFactory() method, and is placed into the Current property.

I then defined all of the above in the following Sitecore patch configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <factories>
      <fieldsComparerFactory type="Sitecore.Sandbox.Data.Validators.ItemValidators.FieldComparers.FieldsComparerFactory, Sitecore.Sandbox">
        <NullFieldsComparer type="Sitecore.Sandbox.Data.Validators.ItemValidators.FieldComparers.NullFieldsComparer, Sitecore.Sandbox" />
        <fieldComparers hint="raw:AddFieldsComparerConfigNode">
          <fieldComparer fieldType="Datetime" type="Sitecore.Sandbox.Data.Validators.ItemValidators.FieldComparers.DateFieldsComparer, Sitecore.Sandbox" />
          <fieldComparer fieldType="Integer" type="Sitecore.Sandbox.Data.Validators.ItemValidators.FieldComparers.IntegerFieldsComparer, Sitecore.Sandbox" />
        </fieldComparers>
      </fieldsComparerFactory>
    </factories>
  </sitecore>
</configuration>

Now that we have our factory in place, we need an Item Validator to use it:

using System.Runtime.Serialization;

using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Data.Validators;

using Sitecore.Sandbox.Data.Validators.ItemValidators.FieldComparers;

namespace Sitecore.Sandbox.Data.Validators.ItemValidators
{
    public class FieldOneValueLessThanOrEqualToFieldTwoValueValidator : StandardValidator
    {
        public override string Name
        {
            get
            {
                return Parameters["Name"];
            }
        }

        private string fieldOneName;
        private string FieldOneName
        {
            get
            {
                if (string.IsNullOrWhiteSpace(fieldOneName))
                {
                    fieldOneName = Parameters["FieldOneName"];
                }

                return fieldOneName;
            }
        }

        private string fieldTwoName;
        private string FieldTwoName
        {
            get
            {
                if (string.IsNullOrWhiteSpace(fieldTwoName))
                {
                    fieldTwoName = Parameters["FieldTwoName"];
                }

                return fieldTwoName;
            }
        }

        public FieldOneValueLessThanOrEqualToFieldTwoValueValidator()
        {
        }

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

        protected override ValidatorResult Evaluate()
        {
            Item item = GetItem();
            if (IsValid(item))
            {
                return ValidatorResult.Valid;
            }

            Text = GetErrorMessage(item);
            return GetFailedResult(ValidatorResult.Warning);
        }

        private bool IsValid(Item item)
        {
            if (item == null || string.IsNullOrWhiteSpace(FieldOneName) || string.IsNullOrWhiteSpace(FieldTwoName))
            {
                return true;
            }

            Field fieldOne = item.Fields[FieldOneName];
            Field fieldTwo = item.Fields[FieldTwoName];
            if(fieldOne == null || fieldTwo == null)
            {
                return true;
            }

            return IsFieldOneLessThanOrEqualToFieldTwo(fieldOne, fieldTwo);
        }

        private bool IsFieldOneLessThanOrEqualToFieldTwo(Field fieldOne, Field fieldTwo)
        {
            IFieldsComparer fieldComparer = GetFieldsComparer(fieldOne, fieldTwo);
            return fieldComparer.IsFieldOneLessThanOrEqualToFieldTwo(fieldOne, fieldTwo);
        }

        protected virtual IFieldsComparer GetFieldsComparer(Field fieldOne, Field fieldTwo)
        {
            return FieldsComparerFactory.Current.GetFieldsComparer(fieldOne, fieldTwo);
        }

        protected virtual string GetErrorMessage(Item item)
        {
            string message = Parameters["ErrorMessage"];
            if (string.IsNullOrWhiteSpace(message))
            {
                return string.Empty;
            }

            message = message.Replace("$fieldOneName", FieldOneName);
            message = message.Replace("$fieldTwoName", FieldTwoName);

            return GetText(message, new[] { item.DisplayName });
        }

        protected override ValidatorResult GetMaxValidatorResult()
        {
            return base.GetFailedResult(ValidatorResult.Suggestion);
        }
    }
}

The real magic of the class above occurs in the IsValid(), IsFieldOneLessThanOrEqualToFieldTwo() and GetFieldsComparer() methods.

The IsValid() method gets the two fields being compared, and passes these along to the IsFieldOneLessThanOrEqualToFieldTwo() method.

The IsFieldOneLessThanOrEqualToFieldTwo() method passes the two fields to the GetFieldsComparer() — this returns the appropriate IFieldsComparer from the GetFieldsComparer() factory method on the FieldsComparerFactory Singleton — and uses the IFieldsComparer to ascertain whether the value in fieldOne is less than or equal to the value in fieldTwo.

If the value in fieldOne is less than or equal to the value in fieldTwo then the Item has passed validation. Otherwise, it has not, and an error message is passed back to the Sitecore client — we are replacing some tokens for fieldOne and fieldTwo in a format string to give the end user some information on the fields that are in question.

I then set up the Item Validator for Integer fields:

integer-comparer-item-validator

I also set up another Item Validator for Datetime fields:

datetime-item-validator

Let’s take this for a spin!

I entered some integer values in the two integer fields being compared:

integer-fields-item

As you can see, we get a warning.

I then set some Datetime field values on the two Datetime fields being compared:

datetime-fields-item

Since ‘Datetime One’ occurs in time after ‘Datetime Two’, we get a warning as expected.

If you have any thoughts on this, please share in a comment.