In a couple of my past posts — Synchronize IDTable Entries Across Multiple Sitecore Databases Using a Composite IDTableProvider and
Chain Together Sitecore Client Commands using a Composite Command — I used the Composite design pattern to chain together functionality in two or more classes with the same interface — by interface here, I don’t strictly mean a C# interface but any class that servers as a base class for others where all instances share the same methods — and had a thought: how could I go about making a generic solution to chain together any class type defined in Sitecore configuration?
As a “proof of concept” I came up with the following solution while taking a break from my judgely duties reviewing 2015 Sitecore Hackathon modules.
I first defined an interface for classes that will construct objects using the Sitecore Configuration Factory:
using System.Collections.Generic; using System.Xml; namespace Sitecore.Sandbox.Shared { public interface IConfigurationFactoryInstances<TInstance> { void AddInstance(XmlNode source); IEnumerable<TInstance> GetInstances(); } }
The following class implements the methods defined in the interface above:
using System.Collections.Generic; using System.Xml; using Sitecore.Configuration; namespace Sitecore.Sandbox.Shared { public class ConfigurationFactoryInstances<TInstance> : IConfigurationFactoryInstances<TInstance> where TInstance : class { private IList<TInstance> Instances { get; set; } public ConfigurationFactoryInstances() { Instances = new List<TInstance>(); } public virtual IEnumerable<TInstance> GetInstances() { return Instances; } public void AddInstance(XmlNode configNode) { TInstance instance = CreateInstance(configNode); if (instance == null) { return; } Instances.Add(instance); } protected virtual TInstance CreateInstance(XmlNode configNode) { if (configNode == null) { return null; } TInstance instance = Factory.CreateObject(configNode, true) as TInstance; if (instance == null) { return null; } return instance; } } }
The AddInstance() method in the class above — along with the help of the CreateInstance() method — takes in a System.Xml.XmlNode instance and attempts to create an instance of the type denoted by TInstance using the Sitecore Configuration Factory. If the instance was successfully created (i.e. it’s not null), it is added to a list.
The GetInstances() method in the class above just returns the list of the instances that were added by the AddInstance() method.
Since I’ve been posting a lot of meme images on Twitter lately — you can see the evidence here — I’ve decided to have a little fun tonight with this “proof of concept”, and created the following composite MediaProvider:
using System.Xml; using Sitecore.Data.Items; using Sitecore.Resources.Media; using Sitecore.Sandbox.Shared; namespace Sitecore.Sandbox.Resources.Media { public class CompositeMediaProvider : MediaProvider { private static IConfigurationFactoryInstances<MediaProvider> Instances { get; set; } static CompositeMediaProvider() { Instances = new ConfigurationFactoryInstances<MediaProvider>(); } public override string GetMediaUrl(MediaItem item, MediaUrlOptions options) { foreach (MediaProvider mediaProvider in Instances.GetInstances()) { string url = mediaProvider.GetMediaUrl(item, options); if(!string.IsNullOrWhiteSpace(url)) { return url; } } return base.GetMediaUrl(item, options); } protected virtual void AddMediaProvider(XmlNode configNode) { Instances.AddInstance(configNode); } } }
The AddMediaProvider() method in the class above adds new instances of Sitecore.Resources.Media.MediaProvider through delegation to an instance of the ConfigurationFactoryInstances class. The Sitecore Configuration Factory will call the AddMediaProvider() method since it’s defined in the patch include configuration file shown later in this post
The GetMediaUrl() method iterates over all instances of Sitecore.Resources.Media.MediaProvider that were created and stored by the ConfigurationFactoryInstances instance, and calls each of their GetMediaUrl() methods. The first non-null or empty URL from one of these “inner” instances is returned to the caller. If none of the instances return a URL, then the class above returns the value given by its base class’ GetMediaUrl() method.
I then spun up three MediaProvider classes to serve up specific image URLs of John West — I found these somewhere on the internet 😉 — when they encounter media Items with specific names (I am not advocating that anyone hard-codes anything like this — these classes are only here to serve as examples):
using System; using Sitecore.Data.Items; using Sitecore.Resources.Media; namespace Sitecore.Sandbox.Resources.Media { public class JohnWestOneMediaProvider : MediaProvider { public override string GetMediaUrl(MediaItem item, MediaUrlOptions options) { if(item.Name.Equals("john-west-1", StringComparison.CurrentCultureIgnoreCase)) { return "http://cdn.meme.am/instances/500x/43030540.jpg"; } return string.Empty; } } }
using System; using Sitecore.Data.Items; using Sitecore.Resources.Media; namespace Sitecore.Sandbox.Resources.Media { public class JohnWestTwoMediaProvider : MediaProvider { public override string GetMediaUrl(MediaItem item, MediaUrlOptions options) { if (item.Name.Equals("john-west-2", StringComparison.CurrentCultureIgnoreCase)) { return "http://cdn.meme.am/instances/500x/43044627.jpg"; } return string.Empty; } } }
using System; using Sitecore.Data.Items; using Sitecore.Resources.Media; namespace Sitecore.Sandbox.Resources.Media { public class JohnWestThreeMediaProvider : MediaProvider { public override string GetMediaUrl(MediaItem item, MediaUrlOptions options) { if (item.Name.Equals("john-west-3", StringComparison.CurrentCultureIgnoreCase)) { return "http://cdn.meme.am/instances/500x/43030625.jpg"; } return string.Empty; } } }
I then registered all of the above in Sitecore using a 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.CompositeMediaProvider, Sitecore.Sandbox</patch:attribute> <mediaProviders hint="raw:AddMediaProvider"> <mediaProvider type="Sitecore.Sandbox.Resources.Media.JohnWestOneMediaProvider, Sitecore.Sandbox" /> <mediaProvider type="Sitecore.Sandbox.Resources.Media.JohnWestTwoMediaProvider, Sitecore.Sandbox" /> <mediaProvider type="Sitecore.Sandbox.Resources.Media.JohnWestThreeMediaProvider, Sitecore.Sandbox" /> <mediaProvider type="Sitecore.Resources.Media.MediaProvider, Sitecore.Kernel" /> </mediaProviders> </mediaProvider> </mediaLibrary> </sitecore> </configuration>
Let’s see this in action!
To test, I uploaded four identical photos of John West to the Media Library:
I then inserted these into a Rich Text field on my home Item:
I saved my home Item and published everything. Once the publish was finished, I navigated to my home page and saw the following:
As you can see, the three custom John West MediaProvider class instances served up their URLs, and the “out of the box” Sitecore.Resources.Media.MediaProvider instance served up its URL on the last image.
If you have any thoughts on this, please share in a comment.
Until next time, have a Sitecoretastic day!