Home » Configuration » Use the Factory Method Pattern for Object Creation in Sitecore

Use the Factory Method Pattern for Object Creation in Sitecore

Sitecore Technology MVP 2016
Sitecore MVP 2015
Sitecore MVP 2014

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

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.


Comment

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