Home » 2015 » March

Monthly Archives: March 2015

Advertisements

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.

Advertisements

Utilize the Strategy Design Pattern for Content Editor Warnings in Sitecore

This post is a continuation of a series of posts I’m putting together around using design patterns in Sitecore implementations, and will show a “proof of concept” around using the Strategy pattern — a pattern where a family of “algorithms” (for simplicity you can think of these as classes that implement the same interface) which should be interchangeable when used by client code, and such holds true even when each do something completely different than others within the same family.

The Strategy pattern can serve as an alternative to the Template method pattern — a pattern where classes have an abstract base class that defines most of an “algorithm” for how classes that inherit from it work but provides method stubs (abstract methods) and method hooks (virtual methods) for subclasses to implement or override — and will prove this in this post by providing an alternative solution to the one I had shown in my previous post on the Template method pattern.

In this “proof of concept”, I will be adding a processor to the <getContentEditorWarnings> pipeline in order to add custom content editor warnings for Items — if you are unfamiliar with content editor warnings in Sitecore, the following screenshot illustrates an “out of the box” content editor warning around publishing and workflow state:

content-editor-warning-example

To start, I am reusing the following interface and classes from my previous post on the Template method pattern:

using System.Collections.Generic;

namespace Sitecore.Sandbox.Pipelines.GetContentEditorWarnings
{
    public interface IWarning
    {
        string Title { get; set; }

        string Message { get; set; }

        List<CommandLink> Links { get; set; }

        bool HasContent();

        IWarning Clone();
    }
}

Warnings will have a title, an error message for display, and a list of Sheer UI command links — the CommandLink class is defined further down in this post — to be displayed and invoked when clicked.

You might be asking why I am defining this when I can just use what’s available in the Sitecore API? Well, I want to inject these values via the Sitecore Configuration Factory, and hopefully this will become clear once you have a look at the Sitecore configuration file further down in this post.

Next, we have a class that implements the interface above:

using System.Collections.Generic;

namespace Sitecore.Sandbox.Pipelines.GetContentEditorWarnings
{
    public class Warning : IWarning
    {
        public string Title { get; set; }

        public string Message { get; set; }

        public List<CommandLink> Links { get; set; }

        public Warning()
        {
            Links = new List<CommandLink>();
        }

        public bool HasContent()
        {
            return !string.IsNullOrWhiteSpace(Title)
                    || !string.IsNullOrWhiteSpace(Title)
                    || !string.IsNullOrWhiteSpace(Message);
        }

        public IWarning Clone()
        {
            IWarning clone = new Warning { Title = Title, Message = Message };
            foreach (CommandLink link in Links)
            {
                clone.Links.Add(new CommandLink { Text = link.Text, Command = link.Command });
            }

            return clone;
        }
    }
}

The HasContent() method just returns “true” if the instance has any content to display though this does not include CommandLinks — what’s the point in displaying these if there is no warning content to be displayed with them?

The Clone() method makes a new instance of the Warning class, and copies values into it — this is useful when defining tokens in strings that must be expanded before being displayed. If we expand them on the instance that is injected via the Sitecore Configuration Factory, the changed strings will persistent in memory until the application pool is recycled for the Sitecore instance.

The following class represents a Sheer UI command link to be displayed in the content editor warning so content editors/authors can take action on the warning:


namespace Sitecore.Sandbox.Pipelines.GetContentEditorWarnings
{
    public class CommandLink
    {
        public string Text { get; set; }

        public string Command { get; set; }
    }
}

The Strategy pattern calls for a family of “algorithms” which can be interchangeably used. In order for us to achieve this, we need to define an interface for this family of “algorithms”:

using System.Collections.Generic;

using Sitecore.Data.Items;

namespace Sitecore.Sandbox.Pipelines.GetContentEditorWarnings.Strategy_Pattern
{
    public interface IWarningsGenerator
    {
        Item Item { get; set; }

        IEnumerable<IWarning> Generate();
    }
}

Next, I created the following class that implements the interface above to ascertain whether a supplied Item has too many child Items:

using System.Collections.Generic;

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

namespace Sitecore.Sandbox.Pipelines.GetContentEditorWarnings.Strategy_Pattern
{
    public class TooManyChildItemsWarningsGenerator : IWarningsGenerator
    {
        private int MaxNumberOfChildItems { get; set; }

        private IWarning Warning { get; set; }

        public Item Item { get; set; }

        public IEnumerable<IWarning> Generate()
        {
            AssertProperties();
            if (Item.Children.Count <= MaxNumberOfChildItems)
            {
                return new List<IWarning>();
            }

            return new[] { Warning };
        }

        private void AssertProperties()
        {
            Assert.ArgumentCondition(MaxNumberOfChildItems > 0, "MaxNumberOfChildItems", "MaxNumberOfChildItems must be set correctly in configuration!");
            Assert.IsNotNull(Warning, "Warning", "Warning must be set in configuration!");
            Assert.ArgumentCondition(Warning.HasContent(), "Warning", "Warning should have some fields populated from configuration!");
            Assert.IsNotNull(Item, "Item", "Item must be set!");
        }
    }
}

The “maximum number of child items allowed” value — this is stored in the MaxNumberOfChildItems integer property of the class — is passed to the class instance via the Sitecore Configuration Factory (you’ll see this defined in the Sitecore configuration file further down in this post).

The IWarning instance that is injected into the instance of this class will give content authors/editors the ability to convert the Item into an Item Bucket when it has too many child Items.

I then defined another class that implements the interface above — a class whose instances determine whether Items have invalid characters in their names:

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

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

namespace Sitecore.Sandbox.Pipelines.GetContentEditorWarnings.Strategy_Pattern
{
    public class HasInvalidCharacetersInNameWarningsGenerator : IWarningsGenerator
    {
        private string CharacterSeparator { get; set; }

        private string Conjunction { get; set; }

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

        private IWarning Warning { get; set; }

        public Item Item { get; set; }

        public HasInvalidCharacetersInNameWarningsGenerator()
        {
            InvalidCharacters = new List<string>();
        }

        public IEnumerable<IWarning> Generate()
        {
            AssertProperties();
            HashSet<string> charactersFound = new HashSet<string>();
            foreach (string character in InvalidCharacters)
            {
                if (Item.Name.Contains(character))
                {
                    charactersFound.Add(character.ToString());
                }
            }

            if(!charactersFound.Any())
            {
                return new List<IWarning>();
            }

            IWarning warning = Warning.Clone();
            string charactersFoundString = string.Join(CharacterSeparator, charactersFound);
            int lastSeparator = charactersFoundString.LastIndexOf(CharacterSeparator);
            if (lastSeparator < 0)
            {
                warning.Message = ReplaceInvalidCharactersToken(warning.Message, charactersFoundString);
                return new[] { warning };
            }

            warning.Message = ReplaceInvalidCharactersToken(warning.Message, Splice(charactersFoundString, lastSeparator, CharacterSeparator.Length, Conjunction));
            return new[] { warning };
        }

        private void AssertProperties()
        {
            Assert.IsNotNullOrEmpty(CharacterSeparator, "CharacterSeparator", "CharacterSeparator must be set in configuration!");
            Assert.ArgumentCondition(InvalidCharacters != null && InvalidCharacters.Any(), "InvalidCharacters", "InvalidCharacters must be set in configuration!");
            Assert.IsNotNull(Warning, "Warning", "Warning must be set in configuration!");
            Assert.ArgumentCondition(Warning.HasContent(), "Warning", "Warning should have some fields populated from configuration!");
            Assert.IsNotNull(Item, "Item", "Item must be set!");
        }

        private static string Splice(string value, int startIndex, int length, string replacement)
        {
            if(string.IsNullOrWhiteSpace(value))
            {
                return value;
            }

            return string.Concat(value.Substring(0, startIndex), replacement, value.Substring(startIndex + length));
        }

        private static string ReplaceInvalidCharactersToken(string value, string replacement)
        {
            return value.Replace("$invalidCharacters", replacement);
        }
    }
}

The above class will return an IWarning instance when an Item has invalid characters in its name — these invalid characters are defined in Sitecore configuration.

The Generate() method iterates over all invalid characters passed from Sitecore configuration and determines if they exist in the Item name. If they do, they are added to a HashSet<string> instance — I’m using a HashSet<string> to ensure the same character isn’t added more than once to the collection — which is used for constructing the warning message to be displayed to the content author/editor.

Once the Generate() method has iterated through all invalid characters, a string is built using the HashSet<string> instance, and is put in place wherever the $invalidCharacters token is defined in the Message property of the IWarning instance.

Now that we have our family of “algorithms” defined, we need a class to encapsulate and invoke these. I defined the following interface for classes that perform this role:

using System.Collections.Generic;

using Sitecore.Data.Items;

namespace Sitecore.Sandbox.Pipelines.GetContentEditorWarnings.Strategy_Pattern
{
    public interface IWarningsGeneratorContext
    {
        IWarningsGenerator Generator { get; set; }

        IEnumerable<IWarning> GetWarnings(Item item);
    }
}

I then defined the following class which implements the interface above:

using System.Collections.Generic;

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

namespace Sitecore.Sandbox.Pipelines.GetContentEditorWarnings.Strategy_Pattern
{
    public class WarningsGeneratorContext : IWarningsGeneratorContext
    {
        public IWarningsGenerator Generator { get; set; }

        public IEnumerable<IWarning> GetWarnings(Item item)
        {
            Assert.IsNotNull(Generator, "Generator", "Generator must be set!");
            Assert.ArgumentNotNull(item, "item");
            Generator.Item = item;
            return Generator.Generate();
        }
    }
}

Instances of the class above take in an instance of IWarningsGenerator via its Generator property — in a sense, we are “lock and loading” WarningsGeneratorContext instances to get them ready. Instances then pass a supplied Item instance to the IWarningsGenerator instance, and invoke its GetWarnings() method. This method returns a collection of IWarning instances.

In a way, the IWarningsGeneratorContext instances are really adapters for IWarningsGenerator instances — IWarningsGeneratorContext instances provide a bridge for client code to use IWarningsGenerator instances via its own little API.

Now that we have all of the stuff above — yes, I know, there is a lot of code in this post, and we’ll reflect on this at the end of the post — we need a class whose instance will serve as a <getContentEditorWarnings> pipeline processor:

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

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.Pipelines.GetContentEditorWarnings;

namespace Sitecore.Sandbox.Pipelines.GetContentEditorWarnings.Strategy_Pattern
{
    public class ContentEditorWarnings
    {
        private List<IWarningsGenerator> WarningsGenerators { get; set; }

        private IWarningsGeneratorContext WarningsGeneratorContext { get; set; }

        public ContentEditorWarnings()
        {
            WarningsGenerators = new List<IWarningsGenerator>();
        }

        public void Process(GetContentEditorWarningsArgs args)
        {
            AssertProperties();
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Item, "args.Item");
            
            IEnumerable<IWarning> warnings = GetWarnings(args.Item);
            if(warnings == null || !warnings.Any())
            {
                return;
            }

            foreach(IWarning warning in warnings)
            {
                AddWarning(args, warning);
            }
        }

        private IEnumerable<IWarning> GetWarnings(Item item)
        {
            List<IWarning> warnings = new List<IWarning>();
            foreach(IWarningsGenerator generator in WarningsGenerators)
            {
                IEnumerable<IWarning> generatorWarnings = GetWarnings(generator, item);
                if(generatorWarnings != null && generatorWarnings.Any())
                {
                    warnings.AddRange(generatorWarnings);
                }
            }

            return warnings;
        }

        private IEnumerable<IWarning> GetWarnings(IWarningsGenerator generator, Item item)
        {
            WarningsGeneratorContext.Generator = generator;
            return WarningsGeneratorContext.GetWarnings(item);
        }

        private void AddWarning(GetContentEditorWarningsArgs args, IWarning warning)
        {
            if(!warning.HasContent())
            {
                return;
            }

            GetContentEditorWarningsArgs.ContentEditorWarning editorWarning = args.Add();
            if(!string.IsNullOrWhiteSpace(warning.Title))
            {
                editorWarning.Title = TranslateText(warning.Title);
            }

            if(!string.IsNullOrWhiteSpace(warning.Message))
            {
                editorWarning.Text = TranslateText(warning.Message);
            }

            if (!warning.Links.Any())
            {
                return;
            }
            
            foreach(CommandLink link in warning.Links)
            {
                editorWarning.AddOption(TranslateText(link.Text), link.Command);
            }
        }

        private string TranslateText(string text)
        {
            if(string.IsNullOrWhiteSpace(text))
            {
                return text;
            }

            return Translate.Text(text);
        }

        private void AssertProperties()
        {
            Assert.IsNotNull(WarningsGeneratorContext, "WarningsGeneratorContext", "WarningsGeneratorContext must be set in configuration!");
            Assert.ArgumentCondition(WarningsGenerators != null && WarningsGenerators.Any(), "WarningsGenerators", "At least one WarningsGenerator must be set in configuration!");
        }
    }
}

The Process() method is the main entry into the pipeline processor. The method delegates to the GetWarnings() method to get a collection of IWarning instances from all IWarningGenerator instances that were injected into the class instance via the Sitecore Configuration Factory.

The GetWarnings() method iterates over all IWarningsGenerator instances, and passes each to the other GetWarnings() method overload which basically sets the IWarningGenerator on the IWarningsGeneratorContext instance, and invokes its GetWarnings() method with the supplied Item instance.

Once all IWarning instances have been collected, the Process() method iterates over the IWarning collection, and adds them to the GetContentEditorWarningsArgs instance via the AddWarning() method.

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

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <getContentEditorWarnings>
        <processor type="Sitecore.Sandbox.Pipelines.GetContentEditorWarnings.Strategy_Pattern.ContentEditorWarnings, Sitecore.Sandbox">
          <WarningsGenerators hint="list">
            <WarningsGenerator type="Sitecore.Sandbox.Pipelines.GetContentEditorWarnings.Strategy_Pattern.TooManyChildItemsWarningsGenerator, Sitecore.Sandbox">
              <MaxNumberOfChildItems>20</MaxNumberOfChildItems>
              <Warning type="Sitecore.Sandbox.Pipelines.GetContentEditorWarnings.Warning, Sitecore.Sandbox">
                <Title>This Item has too many child items!</Title>
                <Message>Please consider converting this Item into an Item Bucket.</Message>
                <Links hint="list">
                  <Link type="Sitecore.Sandbox.Pipelines.GetContentEditorWarnings.CommandLink">
                    <Text>Convert to Item Bucket</Text>
                    <Command>item:bucket</Command>
                  </Link>
                </Links>
              </Warning>
            </WarningsGenerator>
            <WarningsGenerator type="Sitecore.Sandbox.Pipelines.GetContentEditorWarnings.Strategy_Pattern.HasInvalidCharacetersInNameWarningsGenerator, Sitecore.Sandbox">
              <CharacterSeparator>,&amp;nbsp;</CharacterSeparator>
              <Conjunction>&amp;nbsp;and&amp;nbsp;</Conjunction>
              <InvalidCharacters hint="list">
                <Character>-</Character>
                <Character>$</Character>
                <Character>1</Character>
              </InvalidCharacters>
              <Warning type="Sitecore.Sandbox.Pipelines.GetContentEditorWarnings.Warning, Sitecore.Sandbox">
                <Title>The name of this Item has invalid characters!</Title>
                <Message>The name of this Item contains $invalidCharacters. Please consider renaming the Item.</Message>
                <Links hint="list">
                  <Link type="Sitecore.Sandbox.Pipelines.GetContentEditorWarnings.CommandLink">
                    <Text>Rename Item</Text>
                    <Command>item:rename</Command>
                  </Link>
                </Links>
              </Warning>
            </WarningsGenerator>
          </WarningsGenerators>
          <WarningsGeneratorContext type="Sitecore.Sandbox.Pipelines.GetContentEditorWarnings.Strategy_Pattern.WarningsGeneratorContext, Sitecore.Sandbox" />
        </processor>
      </getContentEditorWarnings>
    </pipelines>
  </sitecore>
</configuration>

Let’s test this out.

I set up an Item with more than 20 child Items, and gave it a name that includes -, $ and 1 — these are defined as invalid in the configuration file above:

strategy-1

As you can see, both warnings appear on the Item in the content editor.

Let’s convert the Item into an Item Bucket:

strategy-2

As you can see the Item is now an Item Bucket:

strategy-3

Let’s fix the Item’s name:

strategy-4

The Item’s name is now fixed, and there are no more content editor warnings:

strategy-5

You might be thinking “Mike, that is a lot of code — a significant amount over what you had shown in your previous post where you used the Template method pattern — so why bother with the Strategy pattern?”

Yes, there is more code here, and definitely more moving parts to the Strategy pattern over the Template method pattern.

So, what’s the benefit here?

Well, in the Template method pattern, subclasses are tightly coupled to their abstract base class. A change to the parent class could potentially break code in the subclasses, and this will require code in all subclasses to be changed. This could be quite a task if subclasses are defined in multiple projects that don’t reside in the same solution as the parent class.

The Strategy pattern forces loose coupling among all instances within the pattern thus reducing the likelihood that changes in one class will adversely affect others.

However, with that said, it does add complexity by introducing more code, so you should consider the pros and cons of using the Strategy pattern over the Template method pattern, or perhaps even decide if you should use a pattern to begin with.

Remember, the KISS principle should be followed wherever/whenever possible when designing and developing software.

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