Home » Fields
Category Archives: Fields
Service Locate or Create Objects Defined in a Fully Qualified Type Name Field in Sitecore
<TL;DR>
This is — without a doubt — the longest blog post I have ever written — and hopefully to ever write as it nearly destroyed me 😉 — so will distill the main points in this TL;DR synopsis.
Most bits in Sitecore Experience Forms use objects/service class instances sourced from the Sitecore IoC container but not all. Things not sourced from the Sitecore IoC container are defined on Items in the following folders:
Why?
¯\_(ツ)_/¯
This is most likely due to their fully qualified type names being defined in a type field on Items contained in these folders, and sourcing these from the Sitecore IoC is not a thing OOTB in Sitecore as far as I am aware (reflection is used to create them):
Moreover, this is the same paradigm found in Web Forms for Marketers (WFFM) for some of its parts (Save Actions are an example).
Well, this paradigm bothers me a lot — I strongly feel that virtually everything should be sourced from the Sitecore IoC container as it promotes SOLID principles, a discussion I will leave for another time — so went ahead and built a system of Sitecore pipelines and service classes to:
- Get the fully qualified type name string out of a field of an Item.
- Resolve the Type from the string from #1.
- Try to find the Type in the Sitecore IoC container using Service Locator (before whinging about using Service Locator for this, keep in mind that it would be impossible to inject everything from the IoC container into a class instance’s constructor in order to find it). If found, return to the caller. Otherwise, proceed to #4.
- Create an instance of the Type using Reflection. Return the result to the caller.
Most of the code in the solution that follows are classes which serve as custom pipeline processors for 5 custom pipelines. Pipelines in Sitecore — each being an embodiment of the chain-of-responsibility pattern — are extremely flexible and extendable, hence the reason for going with this approach.
I plan on putting this solution up on GitHub in coming days (or weeks depending on timing) so it is more easily digestible than in a blost post. For now, Just have a scan of the code below.
Note: This solution is just a Proof of concept (PoC). I have not rigorously tested this solution; have no idea what its performance is nor the performance impact it may have; and definitely will not be held responsible if something goes wrong if you decided to use this code in any of your solutions. Use at your own risk!
</TL;DR>
Now that we have that out of the way, let’s jump right into it.
I first created the following abstract class to serve as the base for all pipeline processors in this solution:
using Sitecore.Pipelines; namespace Sandbox.Foundation.ObjectResolution.Pipelines { public abstract class ResolveProcessor<TPipelineArgs> where TPipelineArgs : PipelineArgs { public void Process(TPipelineArgs args) { if (!CanProcess(args)) { return; } Execute(args); } protected virtual bool CanProcess(TPipelineArgs args) => args != null; protected virtual void AbortPipeline(TPipelineArgs args) => args?.AbortPipeline(); protected virtual void Execute(TPipelineArgs args) { } } }
The Execute() method on all pipeline processors will only run when the processor’s CanProcess() method returns true. Also, pipeline processors have the ability to abort the pipeline where they are called.
I then created the following abstract class for all service classes which call a pipeline to “resolve” a particular thing:
using Sitecore.Abstractions; using Sitecore.Pipelines; namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers { public abstract class PipelineObjectResolver<TArguments, TPipelineArguemnts, TResult> where TPipelineArguemnts : PipelineArgs { private readonly BaseCorePipelineManager _corePipelineManager; protected PipelineObjectResolver(BaseCorePipelineManager corePipelineManager) { _corePipelineManager = corePipelineManager; } public TResult Resolve(TArguments arguments) { TPipelineArguemnts args = CreatePipelineArgs(arguments); RunPipeline(GetPipelineName(), args); return GetObject(args); } protected abstract TResult GetObject(TPipelineArguemnts args); protected abstract TPipelineArguemnts CreatePipelineArgs(TArguments arguments); protected abstract string GetPipelineName(); protected virtual void RunPipeline(string pipelineName, PipelineArgs args) => _corePipelineManager.Run(pipelineName, args); } }
Each service class will “resolve” a particular thing with arguments passed to their Resolve() method — these service class’ Resolve() method will take in a TArguments type which serves as the input arguments for it. They will then delegate to a pipeline via the RunPipeline() method to do the resolving. Each will also parse the results returned by the pipeline via the GetObject() method.
Moving forward in this post, I will group each resolving pipeline with their service classes under a <pipeline name /> section.
<resolveItem />
I then moved on to creating a custom pipeline to “resolve” a Sitecore Item. The following class serves as its arguments data transfer object (DTO):
using System.Collections.Generic; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Globalization; using Sitecore.Pipelines; using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ItemResolvers; namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveItem { public class ResolveItemArgs : PipelineArgs { public Database Database { get; set; } public string ItemPath { get; set; } public Language Language { get; set; } public IList<IItemResolver> ItemResolvers { get; set; } = new List<IItemResolver>(); public Item Item { get; set; } } }
The resolution of an Item will be done by a collection of IItemResolver instances — these are defined further down in this post — which ultimately do the resolution of the Item.
Next, I created the following arguments class for IItemResolver instances:
using Sitecore.Data; using Sitecore.Globalization; namespace Sandbox.Foundation.ObjectResolution.Models.Resolvers.ItemResolvers { public class ItemResolverArguments { public Database Database { get; set; } public Language Language { get; set; } public string ItemPath { get; set; } } }
Since I hate calling the “new” keyword directly on classes, I created the following factory interface which will construct the argument objects for both the pipeline and service classes for resolving an Item:
using Sitecore.Data; using Sitecore.Globalization; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ItemResolvers; using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveItem; using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType; namespace Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ItemResolvers { public interface IItemResolverArgumentsFactory { ItemResolverArguments CreateItemResolverArguments(ResolveTypeArgs args); ItemResolverArguments CreateItemResolverArguments(ResolveItemArgs args); ItemResolverArguments CreateItemResolverArguments(Database database = null, Language language = null, string itemPath = null); ResolveItemArgs CreateResolveItemArgs(ItemResolverArguments arguments); ResolveItemArgs CreateResolveItemArgs(Database database = null, Language language = null, string itemPath = null); } }
Here is the class that implements the interface above:
using Sitecore.Data; using Sitecore.Globalization; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ItemResolvers; using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveItem; using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType; namespace Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ItemResolvers { public class ItemResolverArgumentsFactory : IItemResolverArgumentsFactory { public ItemResolverArguments CreateItemResolverArguments(ResolveTypeArgs args) { if (args == null) { return null; } return CreateItemResolverArguments(args.Database, args.Language, args.ItemPath); } public ItemResolverArguments CreateItemResolverArguments(ResolveItemArgs args) { if (args == null) { return null; } return CreateItemResolverArguments(args.Database, args.Language, args.ItemPath); } public ItemResolverArguments CreateItemResolverArguments(Database database = null, Language language = null, string itemPath = null) { return new ItemResolverArguments { Database = database, Language = language, ItemPath = itemPath }; } public ResolveItemArgs CreateResolveItemArgs(ItemResolverArguments arguments) { if (arguments == null) { return null; } return CreateResolveItemArgs(arguments.Database, arguments.Language, arguments.ItemPath); } public ResolveItemArgs CreateResolveItemArgs(Database database = null, Language language = null, string itemPath = null) { return new ResolveItemArgs { Database = database, Language = language, ItemPath = itemPath }; } } }
It just creates argument types for the pipeline and service classes.
The following interface is for classes that “resolve” Items based on arguments set on an ItemResolverArguments instance:
using Sitecore.Data.Items; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ItemResolvers; namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ItemResolvers { public interface IItemResolver { Item Resolve(ItemResolverArguments arguments); } }
I created a another interface for an IItemResolver which resolves an Item from a Sitecore Database:
namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ItemResolvers { public interface IDatabaseItemResolver : IItemResolver { } }
The purpose of this interface is so I can register it and the following class which implements it in the Sitecore IoC container:
using Sitecore.Data.Items; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ItemResolvers; namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ItemResolvers { public class DatabaseItemResolver : IDatabaseItemResolver { public Item Resolve(ItemResolverArguments arguments) { if (!CanResolveItem(arguments)) { return null; } if(arguments.Language == null) { return arguments.Database.GetItem(arguments.ItemPath); } return arguments.Database.GetItem(arguments.ItemPath, arguments.Language); } protected virtual bool CanResolveItem(ItemResolverArguments arguments) => arguments != null && arguments.Database != null && !string.IsNullOrWhiteSpace(arguments.ItemPath); } }
The instance of the class above will return a Sitecore Item if a Database and Item path (this can be an Item ID) are supplied via the ItemResolverArguments instance passed to its Reolve() method.
Now, let’s start constructing the processors for the pipeline:
First, I created an interface and class for adding a “default” IItemResolver to a collection of IItemResolver defined on the pipeline’s arguments object:
namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveItem.AddDefaultItemResolverProcessor { public interface IAddDefaultItemResolver { void Process(ResolveItemArgs args); } }
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ItemResolvers; namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveItem.AddDefaultItemResolverProcessor { public class AddDefaultItemResolver : ResolveProcessor<ResolveItemArgs>, IAddDefaultItemResolver { private readonly IDatabaseItemResolver _databaseItemResolver; public AddDefaultItemResolver(IDatabaseItemResolver databaseItemResolver) { _databaseItemResolver = databaseItemResolver; } protected override bool CanProcess(ResolveItemArgs args) => base.CanProcess(args) && args.ItemResolvers != null; protected override void Execute(ResolveItemArgs args) => args.ItemResolvers.Add(GetTypeResolver()); protected virtual IItemResolver GetTypeResolver() => _databaseItemResolver; } }
In the above class, I’m injecting the IDatabaseItemResolver instance — this was shown further up in this post — into the constructor of this class, and then adding it to the collection of resolvers.
I then created the following interface and implementation class to doing the “resolving” of the Item:
namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveItem.ResolveItemProcessor { public interface IResolveItem { void Process(ResolveItemArgs args); } }
using System.Linq; using Sitecore.Data.Items; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ItemResolvers; using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ItemResolvers; using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ItemResolvers; namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveItem.ResolveItemProcessor { public class ResolveItem : ResolveProcessor<ResolveItemArgs>, IResolveItem { private readonly IItemResolverArgumentsFactory _itemResolverArgumentsFactory; public ResolveItem(IItemResolverArgumentsFactory itemResolverArgumentsFactory) { _itemResolverArgumentsFactory = itemResolverArgumentsFactory; } protected override bool CanProcess(ResolveItemArgs args) => base.CanProcess(args) && args.Database != null && !string.IsNullOrWhiteSpace(args.ItemPath) && args.ItemResolvers.Any(); protected override void Execute(ResolveItemArgs args) => args.Item = GetItem(args); protected virtual Item GetItem(ResolveItemArgs args) { ItemResolverArguments arguments = CreateItemResolverArguments(args); if (arguments == null) { return null; } foreach (IItemResolver resolver in args.ItemResolvers) { Item item = resolver.Resolve(arguments); if (item != null) { return item; } } return null; } protected virtual ItemResolverArguments CreateItemResolverArguments(ResolveItemArgs args) => _itemResolverArgumentsFactory.CreateItemResolverArguments(args); } }
The class above just iterates over all IItemResolver instances on the PipelineArgs instance; passes an ItemResolverArguments instance the Resolve() method on each — the ItemResolverArguments instance is created from a factory — and returns the first Item found by one of the IItemResolver instances. If none were found, null is returned.
Now, we need to create a service class that calls the custom pipeline. I created the following class to act as a settings class for the service.
namespace Sandbox.Foundation.ObjectResolution.Models.Resolvers.ItemResolvers { public class ItemResolverServiceSettings { public string ResolveItemPipelineName { get; set; } } }
An instance of this class will be injected into the service — the instance is created by the Sitecore Configuration Factory — and its ResolveItemPipelineName property will contain a value from Sitecore Configuration (see the Sitecore patch configuration file towards the bottom of this blog post).
I then created the following interface for the service — it’s just another IItemResolver — so I can register it in the Sitecore IoC container:
namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ItemResolvers { public interface IItemResolverService : IItemResolver { } }
The following class implements the interface above:
using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ItemResolvers; using Sitecore.Abstractions; using Sitecore.Data.Items; using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ItemResolvers; using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveItem; namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ItemResolvers { public class ItemResolverService : PipelineObjectResolver<ItemResolverArguments, ResolveItemArgs, Item>, IItemResolverService { private readonly ItemResolverServiceSettings _settings; private readonly IItemResolverArgumentsFactory _itemResolverArgumentsFactory; public ItemResolverService(ItemResolverServiceSettings settings, IItemResolverArgumentsFactory itemResolverArgumentsFactory, BaseCorePipelineManager corePipelineManager) : base(corePipelineManager) { _settings = settings; _itemResolverArgumentsFactory = itemResolverArgumentsFactory; } protected override Item GetObject(ResolveItemArgs args) { return args.Item; } protected override ResolveItemArgs CreatePipelineArgs(ItemResolverArguments arguments) => _itemResolverArgumentsFactory.CreateResolveItemArgs(arguments); protected override string GetPipelineName() => _settings.ResolveItemPipelineName; } }
The above class subclasses the abstract PipelineObjectResolver class I had shown further above in this post. Most of the magic happens in that base class — for those interested in design patterns, this is an example of the Template Method pattern if you did not know — and all subsequent custom pipeline wrapping service classes will follow this same pattern.
I’m not going to go much into detail on the above class as it should be self-evident on what’s happening after looking at the PipelineObjectResolver further up in this post.
<resolveType />
I then started code for the next pipeline — a pipeline to resolve Types.
I created the following PipelineArgs subclass class whose instances will serve as arguments to this new pipeline:
using System; using System.Collections.Generic; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Globalization; using Sitecore.Pipelines; using Sandbox.Foundation.ObjectResolution.Services.Cachers; using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ItemResolvers; using Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers; namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType { public class ResolveTypeArgs : PipelineArgs { public Database Database { get; set; } public string ItemPath { get; set; } public Language Language { get; set; } public IItemResolver ItemResolver { get; set; } public Item Item { get; set; } public string TypeFieldName { get; set; } public string TypeName { get; set; } public IList<ITypeResolver> TypeResolvers { get; set; } = new List<ITypeResolver>(); public ITypeCacher TypeCacher { get; set; } public Type Type { get; set; } public bool UseTypeCache { get; set; } } }
I then created the following class to serve as an arguments object for services that will resolve types:
using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Globalization; namespace Sandbox.Foundation.ObjectResolution.Models.Resolvers.TypeResolvers { public class TypeResolverArguments { public Database Database { get; set; } public Language Language { get; set; } public string ItemPath { get; set; } public Item Item { get; set; } public string TypeFieldName { get; set; } public string TypeName { get; set; } public bool UseTypeCache { get; set; } } }
As I had done for the previous resolver, I created a factory to create arguments for both the PipelineArgs and arguments used by the service classes. Here is the interface for that factory class:
using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Globalization; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.TypeResolvers; using Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject; using Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject; using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject; using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType; namespace Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.TypeResolvers { public interface ITypeResolverArgumentsFactory { TypeResolverArguments CreateTypeResolverArguments(ResolveObjectArgs args); TypeResolverArguments CreateTypeResolverArguments(LocateObjectArgs args); TypeResolverArguments CreateTypeResolverArguments(CreateObjectArgs args); TypeResolverArguments CreateTypeResolverArguments(ResolveTypeArgs args); TypeResolverArguments CreateTypeResolverArguments(Database database = null, Language language = null, string itemPath = null, Item item = null, string typeFieldName = null, string typeName = null, bool useTypeCache = false); ResolveTypeArgs CreateResolveTypeArgs(TypeResolverArguments arguments); ResolveTypeArgs CreateResolveTypeArgs(Database database = null, Language language = null, string itemPath = null, Item item = null, string typeFieldName = null, string typeName = null, bool useTypeCache = false); } }
The following class implements the interface above:
using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Globalization; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.TypeResolvers; using Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject; using Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject; using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject; using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType; namespace Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.TypeResolvers { public class TypeResolverArgumentsFactory : ITypeResolverArgumentsFactory { public TypeResolverArguments CreateTypeResolverArguments(ResolveObjectArgs args) { if (args == null) { return null; } return CreateTypeResolverArguments(args.Database, args.Language, args.ItemPath, args.Item, args.TypeFieldName, args.TypeName, args.UseTypeCache); } public TypeResolverArguments CreateTypeResolverArguments(LocateObjectArgs args) { if (args == null) { return null; } return CreateTypeResolverArguments(args.Database, args.Language, args.ItemPath, args.Item, args.TypeFieldName, args.TypeName, args.UseTypeCache); } public TypeResolverArguments CreateTypeResolverArguments(CreateObjectArgs args) { if (args == null) { return null; } return CreateTypeResolverArguments(args.Database, args.Language, args.ItemPath, args.Item, args.TypeFieldName, args.TypeName, args.UseTypeCache); } public TypeResolverArguments CreateTypeResolverArguments(ResolveTypeArgs args) { return CreateTypeResolverArguments(args.Database, args.Language, args.ItemPath, args.Item, args.TypeFieldName, args.TypeName, args.UseTypeCache); } public TypeResolverArguments CreateTypeResolverArguments(Database database = null, Language language = null, string itemPath = null, Item item = null, string typeFieldName = null, string typeName = null, bool useTypeCache = false) { return new TypeResolverArguments { Database = database, Language = language, ItemPath = itemPath, Item = item, TypeFieldName = typeFieldName, TypeName = typeName, UseTypeCache = useTypeCache }; } public ResolveTypeArgs CreateResolveTypeArgs(TypeResolverArguments arguments) { if (arguments == null) { return null; } return CreateResolveTypeArgs(arguments.Database, arguments.Language, arguments.ItemPath, arguments.Item, arguments.TypeFieldName, arguments.TypeName, arguments.UseTypeCache); } public ResolveTypeArgs CreateResolveTypeArgs(Database database = null, Language language = null, string itemPath = null, Item item = null, string typeFieldName = null, string typeName = null, bool useTypeCache = false) { return new ResolveTypeArgs { Database = database, Language = language, ItemPath = itemPath, Item = item, TypeFieldName = typeFieldName, TypeName = typeName, UseTypeCache = useTypeCache }; } } }
I’m not going to discuss much on the class above — it just creates instances of TypeResolverArguments and ResolveTypeArgs based on a variety of things provided to each method.
I then created the following interface for a pipeline processor to resolve an Item and set it on the passed PipelineArgs instance if one wasn’t provided by the caller or set by another processor:
namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.SetItemResolverProcessor { public interface ISetItemResolver { void Process(ResolveTypeArgs args); } }
The following class implements the interface above:
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ItemResolvers; namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.SetItemResolverProcessor { public class SetItemResolver : ResolveProcessor<ResolveTypeArgs>, ISetItemResolver { private readonly IItemResolverService _itemResolverService; public SetItemResolver(IItemResolverService itemResolverService) { _itemResolverService = itemResolverService; } protected override bool CanProcess(ResolveTypeArgs args) => base.CanProcess(args) && args.Database != null && !string.IsNullOrWhiteSpace(args.ItemPath); protected override void Execute(ResolveTypeArgs args) => args.ItemResolver = GetItemResolver(); protected virtual IItemResolver GetItemResolver() => _itemResolverService; } }
In the class above, I’m injecting an instance of a IItemResolverService into its constructor, and setting it on the ItemResolver property of the ResolveTypeArgs instance.
Does this IItemResolverService interface look familiar? It should as it’s the IItemResolverService defined further up in this post which calls the <resolveItem /> pipeline.
Now we need a processor to resolve the Item. The following interface and class do this:
namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.ResolveTypeProcessor { public interface IResolveItem { void Process(ResolveTypeArgs args); } }
using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ItemResolvers; using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ItemResolvers; namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.ResolveTypeProcessor { public class ResolveItem : ResolveProcessor<ResolveTypeArgs>, IResolveItem { private readonly IItemResolverArgumentsFactory _itemResolverArgumentsFactory; public ResolveItem(IItemResolverArgumentsFactory itemResolverArgumentsFactory) { _itemResolverArgumentsFactory = itemResolverArgumentsFactory; } protected override bool CanProcess(ResolveTypeArgs args) => base.CanProcess(args) && args.Database != null && args.ItemResolver != null; protected override void Execute(ResolveTypeArgs args) => args.Item = args.ItemResolver.Resolve(CreateTypeResolverArguments(args)); protected virtual ItemResolverArguments CreateTypeResolverArguments(ResolveTypeArgs args) => _itemResolverArgumentsFactory.CreateItemResolverArguments(args); } }
The class above just delegates to the IItemResolver instance on the ResolveTypeArgs instance to resolve the Item.
Next, we need a processor to get the fully qualified type name from the Item. The following interface and class are for a processor that does just that:
namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.SetTypeNameProcessor { public interface ISetTypeName { void Process(ResolveTypeArgs args); } }
namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.SetTypeNameProcessor { public class SetTypeName : ResolveProcessor<ResolveTypeArgs>, ISetTypeName { protected override bool CanProcess(ResolveTypeArgs args) => base.CanProcess(args) && args.Item != null && !string.IsNullOrWhiteSpace(args.TypeFieldName); protected override void Execute(ResolveTypeArgs args) => args.TypeName = args.Item[args.TypeFieldName]; } }
The class above just gets the value from the field where the fully qualified type is defined — the name of the field where the fully qualified type name is defined should be set by the caller of this pipeline.
I then defined the following interface and class which will sort out what the Type object is based on a fully qualified type name passed to it:
using System; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.TypeResolvers; namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers { public interface ITypeResolver { Type Resolve(TypeResolverArguments arguments); } }
I then created the following interface for a service class that will delegate to Sitecore.Reflection.ReflectionUtil to get a Type with a provided fully qualified type name:
using System; namespace Sandbox.Foundation.ObjectResolution.Services.Reflection { public interface IReflectionUtilService { Type GetTypeInfo(string type); object CreateObject(Type type); object CreateObject(Type type, object[] parameters); } }
Here’s the class that implements the interface above:
using System; using Sitecore.Reflection; namespace Sandbox.Foundation.ObjectResolution.Services.Reflection { public class ReflectionUtilService : IReflectionUtilService { public Type GetTypeInfo(string type) { return ReflectionUtil.GetTypeInfo(type); } public object CreateObject(Type type) { return ReflectionUtil.CreateObject(type); } public object CreateObject(Type type, object[] parameters) { return ReflectionUtil.CreateObject(type, parameters); } } }
The class above also creates objects via the ReflectionUtil static class with a passed type and constructor arguments — this will be used in the <createObject /> pipeline further down in this post.
I then defined the following interface for a class that will leverage the IReflectionUtilService service above:
namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers { public interface IReflectionTypeResolver : ITypeResolver { } }
This is the class that implements the interface above:
using System; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.TypeResolvers; using Sandbox.Foundation.ObjectResolution.Services.Reflection; namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers { public class ReflectionTypeResolver : IReflectionTypeResolver { private readonly IReflectionUtilService _reflectionUtilService; public ReflectionTypeResolver(IReflectionUtilService reflectionUtilService) { _reflectionUtilService = reflectionUtilService; } public Type Resolve(TypeResolverArguments arguments) { if (string.IsNullOrWhiteSpace(arguments?.TypeName)) { return null; } return GetTypeInfo(arguments.TypeName); } protected virtual Type GetTypeInfo(string typeName) => _reflectionUtilService.GetTypeInfo(typeName); } }
The class above just delegates to the IReflectionUtilService to get the Type with the supplied fully qualified type name.
I then created the following interface and class to represent a pipeline processor to add the ITypeResolver above to the collection of ITypeResolver on the ResolveTypeArgs instance passed to it:
namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.AddDefaultTypeResolverProcessor { public interface IAddDefaultTypeResolver { void Process(ResolveTypeArgs args); } }
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers; namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.AddDefaultTypeResolverProcessor { public class AddDefaultTypeResolver : ResolveProcessor<ResolveTypeArgs>, IAddDefaultTypeResolver { private readonly IReflectionTypeResolver _reflectionTypeResolver; public AddDefaultTypeResolver(IReflectionTypeResolver reflectionTypeResolver) { _reflectionTypeResolver = reflectionTypeResolver; } protected override bool CanProcess(ResolveTypeArgs args) => base.CanProcess(args) && args.TypeResolvers != null; protected override void Execute(ResolveTypeArgs args) => args.TypeResolvers.Add(GetTypeResolver()); protected virtual ITypeResolver GetTypeResolver() => _reflectionTypeResolver; } }
There isn’t much going on in the class above. The Execute() method just adds the IReflectionTypeResolver to the TypeResolvers collection.
When fishing through the Sitecore Experience Forms assemblies, I noticed the OOTB code was “caching” Types it had resolved from Type fields.. I decided to employ the same approach, and defined the following interface for an object that caches Types:
using System; namespace Sandbox.Foundation.ObjectResolution.Services.Cachers { public interface ITypeCacher { void AddTypeToCache(string typeName, Type type); Type GetTypeFromCache(string typeName); } }
Here is the class that implements the interface above:
using System; using System.Collections.Concurrent; namespace Sandbox.Foundation.ObjectResolution.Services.Cachers { public class TypeCacher : ITypeCacher { private static readonly ConcurrentDictionary<string, Type> TypeCache = new ConcurrentDictionary<string, Type>(); public void AddTypeToCache(string typeName, Type type) { if (string.IsNullOrWhiteSpace(typeName) || type == null) { return; } TypeCache.TryAdd(typeName, type); } public Type GetTypeFromCache(string typeName) { if (string.IsNullOrWhiteSpace(typeName)) { return null; } Type type; if (!TypeCache.TryGetValue(typeName, out type)) { return null; } return type; } } }
The AddTypeToCache() method does exactly what the method name says — it will add the supplied Type to cache with the provided type name as the key into the ConcurrentDictionary dictionary on this class.
The GetTypeFromCache() method above tries to get a Type from the ConcurrentDictionary instance on this class, and returns to the caller if it was found. If it wasn’t found, null is returned.
The following interface and class serve as a pipeline processor to set a ITypeCacher instance on the ResolveTypeArgs instance passed to it:
namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.SetTypeCacherProcessor { public interface ISetTypeCacher { void Process(ResolveTypeArgs args); } }
using Sandbox.Foundation.ObjectResolution.Services.Cachers; namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.SetTypeCacherProcessor { public class SetTypeCacher : ResolveProcessor<ResolveTypeArgs>, ISetTypeCacher { private readonly ITypeCacher _typeCacher; public SetTypeCacher(ITypeCacher typeCacher) { _typeCacher = typeCacher; } protected override bool CanProcess(ResolveTypeArgs args) => base.CanProcess(args) && args.UseTypeCache && args.TypeCacher == null; protected override void Execute(ResolveTypeArgs args) => args.TypeCacher = GetTypeCacher(); protected virtual ITypeCacher GetTypeCacher() => _typeCacher; } }
There isn’t much going on in the class above except the injection of the ITypeCacher instance defined further up, and setting that instance on the ResolveTypeArgs instance if it hasn’t already been set.
Now, we need to resolve the Type. The following interface and its implementation class do just that:
namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.ResolveTypeProcessor { public interface IResolveType { void Process(ResolveTypeArgs args); } }
using System; using System.Linq; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.TypeResolvers; using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.TypeResolvers; using Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers; namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.ResolveTypeProcessor { public class ResolveType : ResolveProcessor<ResolveTypeArgs>, IResolveType { private readonly ITypeResolverArgumentsFactory _typeResolverArgumentsFactory; public ResolveType(ITypeResolverArgumentsFactory typeResolverArgumentsFactory) { _typeResolverArgumentsFactory = typeResolverArgumentsFactory; } protected override bool CanProcess(ResolveTypeArgs args) => base.CanProcess(args) && args.TypeResolvers != null && args.TypeResolvers.Any() && !string.IsNullOrWhiteSpace(args.TypeName); protected override void Execute(ResolveTypeArgs args) => args.Type = Resolve(args); protected virtual Type Resolve(ResolveTypeArgs args) { Type type = null; if (args.UseTypeCache) { type = GetTypeFromCache(args); } if (type == null) { type = GetTypeInfo(args); } return type; } protected virtual Type GetTypeInfo(ResolveTypeArgs args) { TypeResolverArguments arguments = CreateTypeResolverArguments(args); if (arguments == null) { return null; } foreach (ITypeResolver typeResolver in args.TypeResolvers) { Type type = typeResolver.Resolve(arguments); if (type != null) { return type; } } return null; } protected virtual Type GetTypeFromCache(ResolveTypeArgs args) => args.TypeCacher.GetTypeFromCache(args.TypeName); protected virtual TypeResolverArguments CreateTypeResolverArguments(ResolveTypeArgs args) => _typeResolverArgumentsFactory.CreateTypeResolverArguments(args); } }
Just as I had done in the <resolveItem /> pipeline further up in this post, the above processor class will iterate over a collection of “resolvers” on the PipelineArgs instance — in this case it’s the TypeResolvers — and pass an arguments instance to each’s Resolve() method. This arguments instance is created from a factory defined further up in this post.
I then created the following settings class for the service class that will wrap the <resolveType /> pipeline:
namespace Sandbox.Foundation.ObjectResolution.Models.Resolvers.TypeResolvers { public class TypeResolverServiceSettings { public string ResolveTypePipelineName { get; set; } } }
The value on the ResolveTypePipelineName property will come from the Sitecore patch file towards the bottom of this post.
I then created the following interface for the service class that will wrap the pipeline — if you are a design patterns buff, this is an example of the adapter pattern:
namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers { public interface ITypeResolverService : ITypeResolver { } }
The following class implements the interface above:
using System; using Sitecore.Abstractions; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.TypeResolvers; using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType; using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.TypeResolvers; namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers { public class TypeResolverService : PipelineObjectResolver<TypeResolverArguments, ResolveTypeArgs, Type>, ITypeResolverService { private readonly TypeResolverServiceSettings _settings; private readonly ITypeResolverArgumentsFactory _typeResolverArgumentsFactory; public TypeResolverService(TypeResolverServiceSettings settings, ITypeResolverArgumentsFactory typeResolverArgumentsFactory, BaseCorePipelineManager corePipelineManager) : base(corePipelineManager) { _settings = settings; _typeResolverArgumentsFactory = typeResolverArgumentsFactory; } protected override Type GetObject(ResolveTypeArgs args) { return args.Type; } protected override ResolveTypeArgs CreatePipelineArgs(TypeResolverArguments arguments) => _typeResolverArgumentsFactory.CreateResolveTypeArgs(arguments); protected override string GetPipelineName() => _settings.ResolveTypePipelineName; } }
I’m not going to go into details about the class above as it’s just like the other service class which wraps the <resolveItem /> defined further above in this post.
Still following? We’re almost there. 😉
<locateObject />
So we now have a way to resolve Items and Types, we now need to find a Type from an Item in the IoC container. I created a PipelineArgs class for a pipeline that does just that:
using System; using System.Collections.Generic; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Globalization; using Sitecore.Pipelines; using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectLocators; using Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers; namespace Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject { public class LocateObjectArgs : PipelineArgs { public Database Database { get; set; } public string ItemPath { get; set; } public Language Language { get; set; } public Item Item { get; set; } public string TypeFieldName { get; set; } public string TypeName { get; set; } public bool UseTypeCache { get; set; } public ITypeResolver TypeResolver { get; set; } public Type Type { get; set; } public IList<IObjectLocator> Locators { get; set; } = new List<IObjectLocator>(); public object Object { get; set; } } }
In reality, this next pipeline can supply an object from anywhere — it doesn’t have to be from an IoC container but that’s what I’m doing here. I did, however, make it extendable so you can source an object from wherever you want, even from the Post Office. 😉
I then created the following arguments object for service classes that will “locate” objects:
using System; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Globalization; namespace Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectLocators { public class ObjectLocatorArguments { public Database Database { get; set; } public Language Language { get; set; } public string ItemPath { get; set; } public Item Item { get; set; } public string TypeFieldName { get; set; } public string TypeName { get; set; } public Type Type { get; set; } public bool UseTypeCache { get; set; } } }
As I had done for the previous two “resolvers”, I created a factory to create arguments objects — both for the pipeline and service classes:
using System; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Globalization; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectLocators; using Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject; using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject; namespace Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectLocators { public interface IObjectLocatorArgumentsFactory { ObjectLocatorArguments CreateObjectLocatorArguments(ResolveObjectArgs args); ObjectLocatorArguments CreateObjectLocatorArguments(LocateObjectArgs args); ObjectLocatorArguments CreateObjectLocatorArguments(Database database = null, Language language = null, string itemPath = null, Item item = null, string typeFieldName = null, string typeName = null, Type type = null, bool useTypeCache = false); LocateObjectArgs CreateLocateObjectArgs(ObjectLocatorArguments arguments); LocateObjectArgs CreateLocateObjectArgs(Database database = null, Language language = null, string itemPath = null, Item item = null, string typeFieldName = null, string typeName = null, Type type = null, bool useTypeCache = false); } }
using System; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Globalization; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectLocators; using Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject; using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject; namespace Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectLocators { public class ObjectLocatorArgumentsFactory : IObjectLocatorArgumentsFactory { public ObjectLocatorArguments CreateObjectLocatorArguments(ResolveObjectArgs args) { if (args == null) { return null; } return CreateObjectLocatorArguments(args.Database, args.Language, args.ItemPath, args.Item, args.TypeFieldName, args.TypeName, args.Type, args.UseTypeCache); } public ObjectLocatorArguments CreateObjectLocatorArguments(LocateObjectArgs args) { if (args == null) { return null; } return CreateObjectLocatorArguments(args.Database, args.Language, args.ItemPath, args.Item, args.TypeFieldName, args.TypeName, args.Type, args.UseTypeCache); } public ObjectLocatorArguments CreateObjectLocatorArguments(Database database = null, Language language = null, string itemPath = null, Item item = null, string typeFieldName = null, string typeName = null, Type type = null, bool useTypeCache = false) { return new ObjectLocatorArguments { Database = database, Language = language, ItemPath = itemPath, Item = item, TypeFieldName = typeFieldName, TypeName = typeName, Type = type }; } public LocateObjectArgs CreateLocateObjectArgs(ObjectLocatorArguments arguments) { if (arguments == null) { return null; } return CreateLocateObjectArgs(arguments.Database, arguments.Language, arguments.ItemPath, arguments.Item, arguments.TypeFieldName, arguments.TypeName, arguments.Type, arguments.UseTypeCache); } public LocateObjectArgs CreateLocateObjectArgs(Database database = null, Language language = null, string itemPath = null, Item item = null, string typeFieldName = null, string typeName = null, Type type = null, bool useTypeCache = false) { return new LocateObjectArgs { Database = database, Language = language, ItemPath = itemPath, Item = item, TypeFieldName = typeFieldName, TypeName = typeName, Type = type }; } } }
The above class implements the interface above. It just creates arguments for both the pipeline and service classes.
I then defined the following interface for a pipeline processor to set the ITypeResolver (defined way up above in this post):
namespace Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.SetTypeResolverProcessor { public interface ISetTypeResolver { void Process(LocateObjectArgs args); } }
This class implements the interface above:
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers; namespace Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.SetTypeResolverProcessor { public class SetTypeResolver : ResolveProcessor<LocateObjectArgs>, ISetTypeResolver { private readonly ITypeResolverService _typeResolverService; public SetTypeResolver(ITypeResolverService typeResolverService) { _typeResolverService = typeResolverService; } protected override bool CanProcess(LocateObjectArgs args) => base.CanProcess(args) && args.TypeResolver == null; protected override void Execute(LocateObjectArgs args) { args.TypeResolver = GetTypeResolver(); } protected virtual ITypeResolver GetTypeResolver() => _typeResolverService; } }
In the class above, I’m injecting the ITypeResolverService into its constructor — this is the service class that wraps the <resolveType /> pipeline defined further up — and set it on the LocateObjectArgs instance if it’s not already set.
Next, I created the following interface for a processor that will “resolve” the type from the TypeResolver set on the LocateObjectArgs instance:
namespace Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.ResolveTypeProcessor { public interface IResolveType { void Process(LocateObjectArgs args); } }
The following class implements the interface above:
using System; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.TypeResolvers; using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.TypeResolvers; namespace Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.ResolveTypeProcessor { public class ResolveType : ResolveProcessor<LocateObjectArgs>, IResolveType { private readonly ITypeResolverArgumentsFactory _typeResolverArgumentsFactory; public ResolveType(ITypeResolverArgumentsFactory typeResolverArgumentsFactory) { _typeResolverArgumentsFactory = typeResolverArgumentsFactory; } protected override bool CanProcess(LocateObjectArgs args) => base.CanProcess(args) && args.Type == null && args.TypeResolver != null; protected override void Execute(LocateObjectArgs args) { args.Type = Resolve(args); } protected virtual Type Resolve(LocateObjectArgs args) { TypeResolverArguments arguments = CreateTypeResolverArguments(args); if (arguments == null) { return null; } return args.TypeResolver.Resolve(arguments); } protected virtual TypeResolverArguments CreateTypeResolverArguments(LocateObjectArgs args) => _typeResolverArgumentsFactory.CreateTypeResolverArguments(args); } }
The class above just “resolves” the type from the TypeResolver set on the LocateObjectArgs instance. Nothing more to see. 😉
I then defined the following interface for a family of classes that “locate” objects from somewhere (perhaps a magical place. 😉 ):
using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectLocators; namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectLocators { public interface IObjectLocator { object Resolve(ObjectLocatorArguments arguments); } }
Well, we can’t use much magic in this solution, so I’m going to “locate” things in the Sitecore IoC container, so defined the following interface for a class that will employ Service Locator to find it in the Sitecore IoC container:
namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectLocators { public interface IServiceProviderLocator : IObjectLocator { } }
This class implements the interface above:
using System; using Sitecore.DependencyInjection; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectLocators; namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectLocators { public class ServiceProviderLocator : IServiceProviderLocator { private readonly IServiceProvider _serviceProvider; public ServiceProviderLocator() { _serviceProvider = GetServiceProvider(); } protected virtual IServiceProvider GetServiceProvider() { return ServiceLocator.ServiceProvider; } public object Resolve(ObjectLocatorArguments arguments) { if (arguments == null || arguments.Type == null) { return null; } return GetService(arguments.Type); } protected virtual object GetService(Type type) => _serviceProvider.GetService(type); } }
In the class above, I’m just passing a type to the System.IServiceProvider’s GetService() method — the IServiceProvider instance is grabbed from the ServiceProvider static member on Sitecore.DependencyInjection.ServiceLocator static class.
Next, I need a processor class to add an instance of the Service Locator IObjectLocator class above to the collection of IObjectLocator instances on the LocateObjectArgs instance, so I defined the following interface for a processor class that does just that:
namespace Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.AddDefaultObjectLocatorProcessor { public interface IAddDefaultObjectLocator { void Process(LocateObjectArgs args); } }
Here’s the implementation class for the interface above:
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectLocators; namespace Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.AddDefaultObjectLocatorProcessor { public class AddDefaultObjectLocator : ResolveProcessor<LocateObjectArgs>, IAddDefaultObjectLocator { private readonly IServiceProviderLocator _serviceProviderLocator; public AddDefaultObjectLocator(IServiceProviderLocator serviceProviderLocator) { _serviceProviderLocator = serviceProviderLocator; } protected override bool CanProcess(LocateObjectArgs args) => base.CanProcess(args) && args.Locators != null; protected override void Execute(LocateObjectArgs args) => args.Locators.Add(GetObjectLocator()); protected virtual IObjectLocator GetObjectLocator() => _serviceProviderLocator; } }
It’s just adding the IServiceProviderLocator instance to the collection of Locators set on the LocateObjectArgs instance.
Great, so we have things that can “locate” objects but need to have a processor that does the execution of that step to actually find those objects. The following interface is for a processor class that does just that:
namespace Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.LocateObjectProcessor { public interface ILocateObject { void Process(LocateObjectArgs args); } }
And here’s its implementation class:
using System.Linq; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectLocators; using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectLocators; using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectLocators; namespace Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.LocateObjectProcessor { public class LocateObject : ResolveProcessor<LocateObjectArgs>, ILocateObject { private readonly IObjectLocatorArgumentsFactory _objectLocatorArgumentsFactory; public LocateObject(IObjectLocatorArgumentsFactory objectLocatorArgumentsFactory) { _objectLocatorArgumentsFactory = objectLocatorArgumentsFactory; } protected override bool CanProcess(LocateObjectArgs args) => base.CanProcess(args) && args.Locators != null && args.Locators.Any() && args.Type != null; protected override void Execute(LocateObjectArgs args) => args.Object = Resolve(args); protected virtual object Resolve(LocateObjectArgs args) { ObjectLocatorArguments arguments = CreateObjectLocatorArguments(args); if (arguments == null) { return null; } foreach (IObjectLocator objectLocator in args.Locators) { object obj = objectLocator.Resolve(arguments); if (obj != null) { return obj; } } return null; } protected virtual ObjectLocatorArguments CreateObjectLocatorArguments(LocateObjectArgs args) => _objectLocatorArgumentsFactory.CreateObjectLocatorArguments(args); } }
As I had done in the previous pipelines, I’m just iterating over a collection of classes that “resolve” for a particular thing — here I’m iterating over all IObjectLocator instances set on the LocateObjectArgs instance. If one of them find the object we are looking for, we just set it on the LocateObjectArgs instance.
As I had done for the other pipelines, I created a service class that wraps the new pipeline I am creating. The following class serves as a settings class for that service class:
namespace Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectLocators { public class ObjectLocatorServiceSettings { public string LocateObjectPipelineName { get; set; } } }
An instance of the class above will be created by the Sitecore Configuration Factory, and its LocateObjectPipelineName property will contain a value defined in the Sitecore patch file further down in this post.
I then created the following interface for the service class that will wrap this new pipeline:
namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectLocators { public interface IObjectLocatorService : IObjectLocator { } }
Here’s the class that implements the interface above:
using Sitecore.Abstractions; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectLocators; using Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject; using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectLocators; namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectLocators { public class ObjectLocatorService : PipelineObjectResolver<ObjectLocatorArguments, LocateObjectArgs, object>, IObjectLocatorService { private readonly ObjectLocatorServiceSettings _settings; private readonly IObjectLocatorArgumentsFactory _objectLocatorArgumentsFactory; public ObjectLocatorService(ObjectLocatorServiceSettings settings, IObjectLocatorArgumentsFactory objectLocatorArgumentsFactory, BaseCorePipelineManager corePipelineManager) : base(corePipelineManager) { _settings = settings; _objectLocatorArgumentsFactory = objectLocatorArgumentsFactory; } protected override object GetObject(LocateObjectArgs args) { return args.Object; } protected override LocateObjectArgs CreatePipelineArgs(ObjectLocatorArguments arguments) => _objectLocatorArgumentsFactory.CreateLocateObjectArgs(arguments); protected override string GetPipelineName() => _settings.LocateObjectPipelineName; } }
I’m not going talk much about the class above — it’s following the same pattern as the other classes that wrap their respective pipelines.
<createObject />
So what happens when we cannot find an object via the <locateObject /> pipeline? Well, let’s create it.
I defined the following PipelineArgs class for a new pipeline that creates objects:
using System; using System.Collections.Generic; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Globalization; using Sitecore.Pipelines; using Sandbox.Foundation.ObjectResolution.Services.Cachers; using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectCreators; using Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers; namespace Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject { public class CreateObjectArgs : PipelineArgs { public Database Database { get; set; } public string ItemPath { get; set; } public Language Language { get; set; } public Item Item { get; set; } public string TypeFieldName { get; set; } public string TypeName { get; set; } public ITypeResolver TypeResolver { get; set; } public Type Type { get; set; } public object[] Parameters { get; set; } public IList<IObjectCreator> Creators { get; set; } = new List<IObjectCreator>(); public object Object { get; set; } public bool UseTypeCache { get; set; } public ITypeCacher TypeCacher { get; set; } } }
I then defined the following class for service classes that create objects:
using System; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Globalization; namespace Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectCreators { public class ObjectCreatorArguments { public Database Database { get; set; } public Language Language { get; set; } public string ItemPath { get; set; } public Item Item { get; set; } public string TypeFieldName { get; set; } public string TypeName { get; set; } public Type Type { get; set; } public bool UseTypeCache { get; set; } public object[] Parameters { get; set; } } }
Since the “new” keyword promotes tight coupling between classes, I created the following factory interface for classes that create the two arguments types shown above:
using System; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Globalization; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectCreators; using Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject; using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject; namespace Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectCreators { public interface IObjectCreatorArgumentsFactory { ObjectCreatorArguments CreateObjectCreatorArguments(ResolveObjectArgs args); ObjectCreatorArguments CreateObjectCreatorArguments(CreateObjectArgs args); ObjectCreatorArguments CreateObjectCreatorArguments(Database database = null, Language language = null, string itemPath = null, Item item = null, string typeFieldName = null, string typeName = null, Type type = null, bool useTypeCache = false, object[] parameters = null); CreateObjectArgs CreateCreateObjectArgs(ObjectCreatorArguments arguments); CreateObjectArgs CreateCreateObjectArgs(Database database = null, Language language = null, string itemPath = null, Item item = null, string typeFieldName = null, string typeName = null, Type type = null, bool useTypeCache = false, object[] parameters = null); } }
The following class implements the interface above:
using System; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Globalization; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectCreators; using Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject; using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject; namespace Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectCreators { public class ObjectCreatorArgumentsFactory : IObjectCreatorArgumentsFactory { public ObjectCreatorArguments CreateObjectCreatorArguments(ResolveObjectArgs args) { if (args == null) { return null; } return CreateObjectCreatorArguments(args.Database, args.Language, args.ItemPath, args.Item, args.TypeFieldName, args.TypeName, args.Type, args.UseTypeCache, args.ObjectCreationParameters); } public ObjectCreatorArguments CreateObjectCreatorArguments(CreateObjectArgs args) { if (args == null) { return null; } return CreateObjectCreatorArguments(args.Database, args.Language, args.ItemPath, args.Item, args.TypeFieldName, args.TypeName, args.Type, args.UseTypeCache, args.Parameters); } public ObjectCreatorArguments CreateObjectCreatorArguments(Database database = null, Language language = null, string itemPath = null, Item item = null, string typeFieldName = null, string typeName = null, Type type = null, bool useTypeCache = false, object[] parameters = null) { return new ObjectCreatorArguments { Database = database, Language = language, ItemPath = itemPath, Item = item, TypeFieldName = typeFieldName, TypeName = typeName, Type = type, Parameters = parameters }; } public CreateObjectArgs CreateCreateObjectArgs(ObjectCreatorArguments arguments) { if (arguments == null) { return null; } return CreateCreateObjectArgs(arguments.Database, arguments.Language, arguments.ItemPath, arguments.Item, arguments.TypeFieldName, arguments.TypeName, arguments.Type, arguments.UseTypeCache, arguments.Parameters); } public CreateObjectArgs CreateCreateObjectArgs(Database database = null, Language language = null, string itemPath = null, Item item = null, string typeFieldName = null, string typeName = null, Type type = null, bool useTypeCache = false, object[] parameters = null) { return new CreateObjectArgs { Database = database, Language = language, ItemPath = itemPath, Item = item, TypeFieldName = typeFieldName, TypeName = typeName, Type = type, UseTypeCache = useTypeCache, Parameters = parameters }; } } }
The class above just creates CreateObjectArgs and ObjectCreatorArguments instances.
Let’s jump into the bits that comprise the new pipeline.
The following interface is for a processor class that sets the ITypeResolver on the CreateObjectArgs instance:
namespace Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.SetTypeResolverProcessor { public interface ISetTypeResolver { void Process(CreateObjectArgs args); } }
Here’s the processor class that implements the interface above:
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers; namespace Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.SetTypeResolverProcessor { public class SetTypeResolver : ResolveProcessor<CreateObjectArgs>, ISetTypeResolver { private readonly ITypeResolverService _typeResolverService; public SetTypeResolver(ITypeResolverService typeResolverService) { _typeResolverService = typeResolverService; } protected override bool CanProcess(CreateObjectArgs args) => base.CanProcess(args) && args.Type == null && args.TypeResolver == null; protected override void Execute(CreateObjectArgs args) { args.TypeResolver = GetTypeResolver(); } protected virtual ITypeResolver GetTypeResolver() => _typeResolverService; } }
Nothing special going on — we’ve seen something like this before further up in this post.
Now, we need a processor to “resolve” types. The following interface is for a processor class which does just that:
namespace Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.ResolveTypeProcessor { public interface IResolveType { void Process(CreateObjectArgs args); } }
And here is the processor class that implements the interface above:
using System; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.TypeResolvers; using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.TypeResolvers; namespace Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.ResolveTypeProcessor { public class ResolveType : ResolveProcessor<CreateObjectArgs>, IResolveType { private readonly ITypeResolverArgumentsFactory _typeResolverArgumentsFactory; public ResolveType(ITypeResolverArgumentsFactory typeResolverArgumentsFactory) { _typeResolverArgumentsFactory = typeResolverArgumentsFactory; } protected override bool CanProcess(CreateObjectArgs args) => base.CanProcess(args) && args.Type == null && args.TypeResolver != null; protected override void Execute(CreateObjectArgs args) { args.Type = Resolve(args); } protected virtual Type Resolve(CreateObjectArgs args) { TypeResolverArguments arguments = CreateTypeResolverArguments(args); if (arguments == null) { return null; } return args.TypeResolver.Resolve(arguments); } protected virtual TypeResolverArguments CreateTypeResolverArguments(CreateObjectArgs args) => _typeResolverArgumentsFactory.CreateTypeResolverArguments(args); } }
I’m not going to discuss much on this as we’ve already seen something like this further up in this post.
I then defined the following interface for a family of classes that create objects:
using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectCreators; namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectCreators { public interface IObjectCreator { object Resolve(ObjectCreatorArguments arguments); } }
Since I’m not good at arts and crafts, we’ll have to use reflection to create objects. The following interface is for a class that uses reflection to create objects:
namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectCreators { public interface IReflectionObjectCreator : IObjectCreator { } }
The following class implements the interface above:
using System.Linq; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectCreators; using Sandbox.Foundation.ObjectResolution.Services.Reflection; namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectCreators { public class ReflectionObjectCreator : IReflectionObjectCreator { private readonly IReflectionUtilService _reflectionUtilService; public ReflectionObjectCreator(IReflectionUtilService reflectionUtilService) { _reflectionUtilService = reflectionUtilService; } public object Resolve(ObjectCreatorArguments arguments) { if (arguments == null || arguments.Type == null) { return null; } if (arguments.Parameters == null || !arguments.Parameters.Any()) { return _reflectionUtilService.CreateObject(arguments.Type); } return _reflectionUtilService.CreateObject(arguments.Type, arguments.Parameters); } } }
This class above just delegates to the IReflectionUtilService instance — this is defined way up above in this post — injected into it for creating objects.
Now we need to put this IReflectionObjectCreator somewhere so it can be used to create objects. The following interface is for a processor class that adds this to a collection of other IObjectCreator defined on the CreateObjectArgs instance:
namespace Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.AddDefaultObjectCreatorProcessor { public interface IAddDefaultObjectCreator { void Process(CreateObjectArgs args); } }
And here is the magic behind the interface above:
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectCreators; namespace Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.AddDefaultObjectCreatorProcessor { public class AddDefaultObjectCreator : ResolveProcessor<CreateObjectArgs>, IAddDefaultObjectCreator { private readonly IReflectionObjectCreator _reflectionObjectCreator; public AddDefaultObjectCreator(IReflectionObjectCreator reflectionObjectCreator) { _reflectionObjectCreator = reflectionObjectCreator; } protected override bool CanProcess(CreateObjectArgs args) => base.CanProcess(args) && args.Creators != null; protected override void Execute(CreateObjectArgs args) => args.Creators.Add(GetObjectLocator()); protected virtual IObjectCreator GetObjectLocator() => _reflectionObjectCreator; } }
We are just adding the IReflectionObjectCreator instance to the Creators collection on the CreateObjectArgs instance.
Now, we need a processor that delegates to each IObjectCreator instance in the collection on the CreateObjectArgs instance. The following interface is for a processor that does that:
namespace Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.CreateObjectProcessor { public interface ICreateObject { void Process(CreateObjectArgs args); } }
Here’s the above interface’s implementation class:
using System.Linq; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectCreators; using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject; using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectCreators; using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectCreators; namespace Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.CreateObjectProcessor { public class CreateObject : ResolveProcessor<CreateObjectArgs>, ICreateObject { private readonly IObjectCreatorArgumentsFactory _objectCreatorArgumentsFactory; public CreateObject(IObjectCreatorArgumentsFactory objectCreatorArgumentsFactory) { _objectCreatorArgumentsFactory = objectCreatorArgumentsFactory; } protected override bool CanProcess(CreateObjectArgs args) => base.CanProcess(args) && args.Creators.Any(); protected override void Execute(CreateObjectArgs args) { args.Object = CreateObjectFromArguments(args); } protected virtual object CreateObjectFromArguments(CreateObjectArgs args) { ObjectCreatorArguments arguments = CreateObjectCreatorArguments(args); if (arguments == null) { return null; } foreach (IObjectCreator objectCreator in args.Creators) { object result = CreateObjectFromArguments(objectCreator, arguments); if (result != null) { return result; } } return null; } protected virtual ObjectCreatorArguments CreateObjectCreatorArguments(CreateObjectArgs args) => _objectCreatorArgumentsFactory.CreateObjectCreatorArguments(args); protected virtual object CreateObjectFromArguments(IObjectCreator objectCreator, ObjectCreatorArguments arguments) => objectCreator.Resolve(arguments); } }
The above class just iterates over the IObjectCreator collection on the CreateObjectArgs instance, and tries to create an object using each. The IObjectCreatorArgumentsFactory instance assists in creating the ObjectCreatorArguments instance from the CreateObjectArgs instance so it can make such calls on each IObjectCreator instance.
If an object is created from one them, it just uses that and stops the iteration.
It’s probably a good idea to only cache Types when an object was actually created from the Type. The following interface is for a processor that sets a ITypeCacher on the CreateObjectArgs instance — this class will add the Type to a cache (perhaps in a bank somewhere on the Cayman Islands? 😉 ):
namespace Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.SetTypeCacherProcessor { public interface ISetTypeCacher { void Process(CreateObjectArgs args); } }
Here’s the implementation class for the interface above:
using Sandbox.Foundation.ObjectResolution.Services.Cachers; namespace Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.SetTypeCacherProcessor { public class SetTypeCacher : ResolveProcessor<CreateObjectArgs>, ISetTypeCacher { private readonly ITypeCacher _typeCacher; public SetTypeCacher(ITypeCacher typeCacher) { _typeCacher = typeCacher; } protected override bool CanProcess(CreateObjectArgs args) => base.CanProcess(args) && args.UseTypeCache && args.TypeCacher == null; protected override void Execute(CreateObjectArgs args) => args.TypeCacher = _typeCacher; } }
It’s just setting the injected ITypeCacher — the implementation class is defined further up in this post — on the CreateObjectArgs instance.
Now, we need to use the ITypeCacher to cache the type. The following interface is for a processor class delegates to the ITypeCacher instance to cache the Type:
namespace Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.CacheTypeProcessor { public interface ICacheType { void Process(CreateObjectArgs args); } }
Here is the process class which implements the interface above:
namespace Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.CacheTypeProcessor { public class CacheType : ResolveProcessor<CreateObjectArgs>, ICacheType { protected override bool CanProcess(CreateObjectArgs args) => base.CanProcess(args) && !string.IsNullOrWhiteSpace(args.TypeName) && args.Type != null && args.UseTypeCache && args.TypeCacher != null; protected override void Execute(CreateObjectArgs args) => AddTypeToCache(args); protected virtual void AddTypeToCache(CreateObjectArgs args) => args.TypeCacher.AddTypeToCache(args.TypeName, args.Type); } }
It should be self-explanatory what’s happening here. If not, please drop a comment below.
Now, we need a service class that wraps this new pipeline. I created the following settings class for that service:
namespace Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectCreators { public class ObjectCreatorServiceSettings { public string CreateObjectPipelineName { get; set; } } }
An instance of this class is created by the Sitecore Configuration Factory just as the other ones in this post are.
I then defined the following interface for the service class that will wrap this new pipeline — it’s just another IObjectCreator:
namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectCreators { public interface IObjectCreatorService : IObjectCreator { } }
This class implements the interface above:
using Sitecore.Abstractions; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectCreators; using Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject; using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectCreators; namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectCreators { public class ObjectCreatorService : PipelineObjectResolver<ObjectCreatorArguments, CreateObjectArgs, object>, IObjectCreatorService { private readonly ObjectCreatorServiceSettings _settings; private readonly IObjectCreatorArgumentsFactory _objectCreatorArgumentsFactory; public ObjectCreatorService(ObjectCreatorServiceSettings settings, IObjectCreatorArgumentsFactory objectCreatorArgumentsFactory, BaseCorePipelineManager corePipelineManager) : base(corePipelineManager) { _settings = settings; _objectCreatorArgumentsFactory = objectCreatorArgumentsFactory; } protected override object GetObject(CreateObjectArgs args) { return args.Object; } protected override CreateObjectArgs CreatePipelineArgs(ObjectCreatorArguments arguments) => _objectCreatorArgumentsFactory.CreateCreateObjectArgs(arguments); protected override string GetPipelineName() => _settings.CreateObjectPipelineName; } }
I’m not going to go into details on the above as you have seen this pattern further above in this post.
<resolveObject />
Now we need a way to glue together all pipelines created above. The following PipelineArgs class is for — yet another pipeline 😉 — that glues everything together:
using System; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Globalization; using Sitecore.Pipelines; using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectCreators; using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectLocators; using Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers; namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject { public class ResolveObjectArgs : PipelineArgs { public Database Database { get; set; } public string ItemPath { get; set; } public Language Language { get; set; } public Item Item { get; set; } public string TypeFieldName { get; set; } public string TypeName { get; set; } public ITypeResolver TypeResolver { get; set; } public Type Type { get; set; } public IObjectLocator ObjectLocator; public bool FoundInContainer { get; set; } public IObjectCreator ObjectCreator; public bool UseTypeCache { get; set; } public object[] ObjectCreationParameters { get; set; } public object Object { get; set; } } }
I also created the following class for the service class that will wrap this new pipeline:
using System; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Globalization; namespace Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectResolvers { public class ObjectResolverArguments { public Database Database { get; set; } public Language Language { get; set; } public string ItemPath { get; set; } public Item Item { get; set; } public string TypeFieldName { get; set; } public string TypeName { get; set; } public Type Type { get; set; } public bool UseTypeCache { get; set; } public object[] ObjectCreationParameters { get; set; } } }
I bet you are guessing that I’m going to create another factory for these two classes above. Yep, you are correct. Here is the interface for that factory:
using System; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Globalization; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectResolvers; using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject; namespace Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectResolvers { public interface IObjectResolverArgumentsFactory { ObjectResolverArguments CreateObjectResolverArguments(Database database, string itemPath, string typeFieldName, Language language, object[] objectCreationParameters); ObjectResolverArguments CreateObjectResolverArguments(Item item, string typeFieldName, bool useTypeCache, object[] objectCreationParameters); ResolveObjectArgs CreateResolveObjectArgs(ObjectResolverArguments arguments); ResolveObjectArgs CreateResolveObjectArgs(Database database = null, string itemPath = null, Language language = null, Item item = null, string typeFieldName = null, string typeName = null, Type type = null, bool useTypeCache = false, object[] objectCreationParameters = null); } }
The following class implements the factory interface above:
using System; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Globalization; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectResolvers; using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject; namespace Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectResolvers { public class ObjectResolverArgumentsFactory : IObjectResolverArgumentsFactory { public ObjectResolverArguments CreateObjectResolverArguments(Database database, string itemPath, string typeFieldName, Language language, object[] objectCreationParameters) { return new ObjectResolverArguments { Database = database, ItemPath = itemPath, TypeFieldName = typeFieldName, Language = language, ObjectCreationParameters = objectCreationParameters }; } public ObjectResolverArguments CreateObjectResolverArguments(Item item, string typeFieldName, bool useTypeCache, object[] objectCreationParameters) { return new ObjectResolverArguments { Item = item, TypeFieldName = typeFieldName, UseTypeCache = useTypeCache, ObjectCreationParameters = objectCreationParameters }; } public ResolveObjectArgs CreateResolveObjectArgs(ObjectResolverArguments arguments) { if (arguments == null) { return null; } return CreateResolveObjectArgs(arguments.Database, arguments.ItemPath, arguments.Language, arguments.Item, arguments.TypeFieldName, arguments.TypeName, arguments.Type, arguments.UseTypeCache, arguments.ObjectCreationParameters); } public ResolveObjectArgs CreateResolveObjectArgs(Item item, string typeFieldName, object[] objectCreationParameters) { return new ResolveObjectArgs { Item = item, TypeFieldName = typeFieldName, ObjectCreationParameters = objectCreationParameters }; } public ResolveObjectArgs CreateResolveObjectArgs(Database database = null, string itemPath = null, Language language = null, Item item = null, string typeFieldName = null, string typeName = null, Type type = null, bool useTypeCache = false, object[] objectCreationParameters = null) { return new ResolveObjectArgs { Database = database, ItemPath = itemPath, Language = language, Item = item, TypeFieldName = typeFieldName, TypeName = typeName, Type= type, UseTypeCache = useTypeCache, ObjectCreationParameters = objectCreationParameters }; } } }
The following interface is for a processor class that sets the ITypeResolver on the ResolveObjectArgs instance:
namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject { public interface ISetTypeResolver { void Process(ResolveObjectArgs args); } }
Here’s its implementation class:
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers; namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject { public class SetTypeResolver : ResolveProcessor<ResolveObjectArgs>, ISetTypeResolver { private readonly ITypeResolverService _typeResolverService; public SetTypeResolver(ITypeResolverService typeResolverService) { _typeResolverService = typeResolverService; } protected override bool CanProcess(ResolveObjectArgs args) => base.CanProcess(args) && args.TypeResolver == null; protected override void Execute(ResolveObjectArgs args) { args.TypeResolver = GetTypeResolver(); } protected virtual ITypeResolver GetTypeResolver() => _typeResolverService; } }
We have already seen this twice, so I won’t discuss it again. 😉
Next, we need to resolve the type. The following interface is for a processor class that does that type resolution:
namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.ResolveTypeProcessor { public interface IResolveType { void Process(ResolveObjectArgs args); } }
Here’s the class that implements the interface above:
using System; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.TypeResolvers; using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.TypeResolvers; namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.ResolveTypeProcessor { public class ResolveType : ResolveProcessor<ResolveObjectArgs>, IResolveType { private readonly ITypeResolverArgumentsFactory _typeResolverArgumentsFactory; public ResolveType(ITypeResolverArgumentsFactory typeResolverArgumentsFactory) { _typeResolverArgumentsFactory = typeResolverArgumentsFactory; } protected override bool CanProcess(ResolveObjectArgs args) => base.CanProcess(args) && args.Type == null && args.TypeResolver != null; protected override void Execute(ResolveObjectArgs args) { args.Type = Resolve(args); } protected virtual Type Resolve(ResolveObjectArgs args) { TypeResolverArguments arguments = CreateTypeResolverArguments(args); if (arguments == null) { return null; } return args.TypeResolver.Resolve(arguments); } protected virtual TypeResolverArguments CreateTypeResolverArguments(ResolveObjectArgs args) => _typeResolverArgumentsFactory.CreateTypeResolverArguments(args); } }
I’m also not going to discuss this as I’ve done this somewhere up above. 😉
Now we need a processor to “locate” objects in the Sitecore IoC container. The following interface is for a processor class that does just that:
namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.SetObjectLocatorProcessor { public interface ISetObjectLocator { void Process(ResolveObjectArgs args); } }
The following class implements the interface above:
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectLocators; namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.SetObjectLocatorProcessor { public class SetObjectLocator : ResolveProcessor<ResolveObjectArgs>, ISetObjectLocator { private readonly IObjectLocatorService _objectLocatorService; public SetObjectLocator(IObjectLocatorService objectLocatorService) { _objectLocatorService = objectLocatorService; } protected override bool CanProcess(ResolveObjectArgs args) => base.CanProcess(args) && args.ObjectLocator == null; protected override void Execute(ResolveObjectArgs args) => args.ObjectLocator = GetObjectLocator(); protected virtual IObjectLocator GetObjectLocator() => _objectLocatorService; } }
The class above just sets the IObjectLocatorService — this is the service class which wraps the <locateObject /> pipeline defined further up in this post — on the ResolveObjectArgs instance.
I then created the following interface to delegate to the ObjectLocator property on the ResolveObjectArgs to “locate” the object:
namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.LocateObjectProcessor { public interface ILocateObject { void Process(ResolveObjectArgs args); } }
And here’s the class that implements this interface above:
using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectLocators; using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectLocators; namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.LocateObjectProcessor { public class LocateObject : ResolveProcessor<ResolveObjectArgs>, ILocateObject { private readonly IObjectLocatorArgumentsFactory _objectLocatorArgumentsFactory; public LocateObject(IObjectLocatorArgumentsFactory objectLocatorArgumentsFactory) { _objectLocatorArgumentsFactory = objectLocatorArgumentsFactory; } protected override bool CanProcess(ResolveObjectArgs args) => base.CanProcess(args) && args.Object == null && args.ObjectLocator != null; protected override void Execute(ResolveObjectArgs args) { args.Object = Locate(args); args.FoundInContainer = args.Object != null; if (!args.FoundInContainer) { return; } AbortPipeline(args); } protected virtual object Locate(ResolveObjectArgs args) => args.ObjectLocator.Resolve(CreateObjectLocatorArguments(args)); protected virtual ObjectLocatorArguments CreateObjectLocatorArguments(ResolveObjectArgs args) => _objectLocatorArgumentsFactory.CreateObjectLocatorArguments(args); } }
The above class just tries to “locate” the object using the ObjectLocator set on the ResolveObjectArgs instance.
In the event we can’t find the object via the IObjectLocator, we should create the object instead. I created the following interface to set an IObjectCreator instance on the ResolveObjectArgs instance:
namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.SetObjectCreatorProcessor { public interface ISetObjectCreator { void Process(ResolveObjectArgs args); } }
And here’s its implementation class:
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectCreators; namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.SetObjectCreatorProcessor { public class SetObjectCreator : ResolveProcessor<ResolveObjectArgs>, ISetObjectCreator { private readonly IObjectCreatorService _objectCreatorService; public SetObjectCreator(IObjectCreatorService objectCreatorService) { _objectCreatorService = objectCreatorService; } protected override bool CanProcess(ResolveObjectArgs args) => base.CanProcess(args) && args.ObjectCreator == null; protected override void Execute(ResolveObjectArgs args) => args.ObjectCreator = GetObjectCreator(); protected virtual IObjectCreator GetObjectCreator() => _objectCreatorService; } }
The class above just sets the IObjectCreatorService — this is the service class which wraps the <createObject /> pipeline defined further up in this post — on the ResolveObjectArgs instance.
Next, we need to delegate to this IObjectCreator to create the object. The following interface is for a class that creates objects:
namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.CreateObjectProcessor { public interface ICreateObject { void Process(ResolveObjectArgs args); } }
And here’s the implementation of the interface above:
using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectCreators; using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectCreators; namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.CreateObjectProcessor { public class CreateObject : ResolveProcessor<ResolveObjectArgs>, ICreateObject { private readonly IObjectCreatorArgumentsFactory _objectCreatorArgumentsFactory; public CreateObject(IObjectCreatorArgumentsFactory objectCreatorArgumentsFactory) { _objectCreatorArgumentsFactory = objectCreatorArgumentsFactory; } protected override bool CanProcess(ResolveObjectArgs args) => base.CanProcess(args) && args.Object == null && args.ObjectCreator != null; protected override void Execute(ResolveObjectArgs args) => args.Object = Resolve(args); protected virtual object Resolve(ResolveObjectArgs args) => args.ObjectCreator.Resolve(CreateObjectCreatorArguments(args)); protected virtual ObjectCreatorArguments CreateObjectCreatorArguments(ResolveObjectArgs args) => _objectCreatorArgumentsFactory.CreateObjectCreatorArguments(args); } }
The class above just delegates to the IObjectCreator instance of the ResolveObjectArgs instance to create the object
Like the other 4 pipelines — holy cannoli, Batman, there are 5 pipelines in total in this solution! — I created a service class that wraps this new pipeline. The following class serves as a settings class for this service:
namespace Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectResolvers { public class ObjectResolverServiceSettings { public string ResolveObjectPipelineName { get; set; } public bool UseTypeCache { get; set; } } }
An instance of the above is created by the Sitecore Configuration Factory.
The following interface defines a family of classes that “resolve” objects. Unlike the other pipelines, we will only have one class that implements this interface:
using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectResolvers; namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectResolvers { public interface IObjectResolver { TObject Resolve<TObject>(ObjectResolverArguments arguments) where TObject : class; object Resolve(ObjectResolverArguments arguments); } }
The following class implements the interface above:
using Sitecore.Abstractions; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectResolvers; using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject; using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectResolvers; namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectResolvers { public class ObjectResolverService : PipelineObjectResolver<ObjectResolverArguments, ResolveObjectArgs, object>, IObjectResolver { private readonly ObjectResolverServiceSettings _settings; private readonly IObjectResolverArgumentsFactory _objectResolverArgumentsFactory; public ObjectResolverService(ObjectResolverServiceSettings settings, IObjectResolverArgumentsFactory objectResolverArgumentsFactory, BaseCorePipelineManager corePipelineManager) : base(corePipelineManager) { _settings = settings; _objectResolverArgumentsFactory = objectResolverArgumentsFactory; } public TObject Resolve<TObject>(ObjectResolverArguments arguments) where TObject : class { return Resolve(arguments) as TObject; } protected override object GetObject(ResolveObjectArgs args) { return args.Object; } protected override ResolveObjectArgs CreatePipelineArgs(ObjectResolverArguments arguments) => _objectResolverArgumentsFactory.CreateResolveObjectArgs(arguments); protected override string GetPipelineName() => _settings.ResolveObjectPipelineName; } }
I then register every single thing above — and I mean EVERYTHING — in the Sitecore IoC via the following IServicesConfigurator class:
using System; using Microsoft.Extensions.DependencyInjection; using Sitecore.Abstractions; using Sitecore.DependencyInjection; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ItemResolvers; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectCreators; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectLocators; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectResolvers; using Sandbox.Foundation.ObjectResolution.Models.Resolvers.TypeResolvers; using Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.AddDefaultObjectCreatorProcessor; using Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.CacheTypeProcessor; using Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.CreateObjectProcessor; using Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.ResolveTypeProcessor; using Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.SetTypeCacherProcessor; using Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.SetTypeResolverProcessor; using Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.AddDefaultObjectLocatorProcessor; using Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.LocateObjectProcessor; using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveItem.AddDefaultItemResolverProcessor; using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveItem.ResolveItemProcessor; using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.SetObjectCreatorProcessor; using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.SetObjectLocatorProcessor; using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.AddDefaultTypeResolverProcessor; using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.SetItemResolverProcessor; using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.SetTypeNameProcessor; using Sandbox.Foundation.ObjectResolution.Services.Cachers; using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ItemResolvers; using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectCreators; using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectLocators; using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectResolvers; using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.TypeResolvers; using Sandbox.Foundation.ObjectResolution.Services.Reflection; using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ItemResolvers; using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectCreators; using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectLocators; using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectResolvers; using Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers; namespace Sandbox.Foundation.ObjectResolution { public class ObjectResolutionConfigurator : IServicesConfigurator { public void Configure(IServiceCollection serviceCollection) { ConfigureCachers(serviceCollection); ConfigureFactories(serviceCollection); ConfigureItemResolvers(serviceCollection); ConfigureTypeResolvers(serviceCollection); ConfigureObjectCreators(serviceCollection); ConfigureObjectLocators(serviceCollection); ConfigureObjectResolvers(serviceCollection); ConfigureResolveItemPipelineProcessors(serviceCollection); ConfigureResolveTypePipelineProcessors(serviceCollection); ConfigureLocateObjectPipelineProcessors(serviceCollection); ConfigureCreateObjectPipelineProcessors(serviceCollection); ConfigureResolveObjectPipelineProcessors(serviceCollection); ConfigureOtherServices(serviceCollection); } private void ConfigureCachers(IServiceCollection serviceCollection) { serviceCollection.AddSingleton<ITypeCacher, TypeCacher>(); } private void ConfigureFactories(IServiceCollection serviceCollection) { serviceCollection.AddSingleton<IItemResolverArgumentsFactory, ItemResolverArgumentsFactory>(); serviceCollection.AddSingleton<ITypeResolverArgumentsFactory, TypeResolverArgumentsFactory>(); serviceCollection.AddSingleton<IObjectLocatorArgumentsFactory, ObjectLocatorArgumentsFactory>(); serviceCollection.AddSingleton<IObjectCreatorArgumentsFactory, ObjectCreatorArgumentsFactory>(); serviceCollection.AddSingleton<IObjectResolverArgumentsFactory, ObjectResolverArgumentsFactory>(); } private void ConfigureItemResolvers(IServiceCollection serviceCollection) { serviceCollection.AddSingleton<IDatabaseItemResolver, DatabaseItemResolver>(); serviceCollection.AddSingleton(GetItemResolverServiceSetting); serviceCollection.AddSingleton<IItemResolverService, ItemResolverService>(); } private ItemResolverServiceSettings GetItemResolverServiceSetting(IServiceProvider provider) { return CreateConfigObject<ItemResolverServiceSettings>(provider, "moduleSettings/foundation/objectResolution/itemResolverServiceSettings"); } private void ConfigureTypeResolvers(IServiceCollection serviceCollection) { serviceCollection.AddSingleton<IReflectionTypeResolver, ReflectionTypeResolver>(); serviceCollection.AddSingleton(GetTypeResolverServiceSettings); serviceCollection.AddSingleton<ITypeResolverService, TypeResolverService>(); } private TypeResolverServiceSettings GetTypeResolverServiceSettings(IServiceProvider provider) { return CreateConfigObject<TypeResolverServiceSettings>(provider, "moduleSettings/foundation/objectResolution/typeResolverServiceSettings"); } private void ConfigureObjectCreators(IServiceCollection serviceCollection) { serviceCollection.AddSingleton<IReflectionObjectCreator, ReflectionObjectCreator>(); serviceCollection.AddSingleton(GetObjectCreatorServiceSettings); serviceCollection.AddSingleton<IObjectCreatorService, ObjectCreatorService>(); } private ObjectCreatorServiceSettings GetObjectCreatorServiceSettings(IServiceProvider provider) { return CreateConfigObject<ObjectCreatorServiceSettings>(provider, "moduleSettings/foundation/objectResolution/objectCreatorServiceSettings"); } private void ConfigureObjectLocators(IServiceCollection serviceCollection) { serviceCollection.AddSingleton<IServiceProviderLocator, ServiceProviderLocator>(); serviceCollection.AddSingleton(GetObjectLocatorServiceSettings); serviceCollection.AddSingleton<IObjectLocatorService, ObjectLocatorService>(); } private ObjectLocatorServiceSettings GetObjectLocatorServiceSettings(IServiceProvider provider) { return CreateConfigObject<ObjectLocatorServiceSettings>(provider, "moduleSettings/foundation/objectResolution/objectLocatorServiceSettings"); } private void ConfigureObjectResolvers(IServiceCollection serviceCollection) { serviceCollection.AddSingleton<IReflectionObjectCreator, ReflectionObjectCreator>(); serviceCollection.AddSingleton(GetObjectResolverServiceSettings); serviceCollection.AddSingleton<IObjectResolver, ObjectResolverService>(); } private ObjectResolverServiceSettings GetObjectResolverServiceSettings(IServiceProvider provider) { return CreateConfigObject<ObjectResolverServiceSettings>(provider, "moduleSettings/foundation/objectResolution/objectResolverServiceSettings"); } private void ConfigureResolveItemPipelineProcessors(IServiceCollection serviceCollection) { serviceCollection.AddSingleton<IAddDefaultItemResolver, AddDefaultItemResolver>(); serviceCollection.AddSingleton<IResolveItem, ResolveItem>(); } private void ConfigureResolveTypePipelineProcessors(IServiceCollection serviceCollection) { serviceCollection.AddSingleton<ISetItemResolver, SetItemResolver>(); serviceCollection.AddSingleton<Pipelines.ResolveType.ResolveTypeProcessor.IResolveItem, Pipelines.ResolveType.ResolveTypeProcessor.ResolveItem>(); serviceCollection.AddSingleton<ISetTypeName, SetTypeName>(); serviceCollection.AddSingleton<IAddDefaultTypeResolver, AddDefaultTypeResolver>(); serviceCollection.AddSingleton<Pipelines.ResolveType.SetTypeCacherProcessor.ISetTypeCacher, Pipelines.ResolveType.SetTypeCacherProcessor.SetTypeCacher>(); serviceCollection.AddSingleton<Pipelines.ResolveType.ResolveTypeProcessor.IResolveType, Pipelines.ResolveType.ResolveTypeProcessor.ResolveType>(); } private void ConfigureLocateObjectPipelineProcessors(IServiceCollection serviceCollection) { serviceCollection.AddSingleton<Pipelines.LocateObject.SetTypeResolverProcessor.ISetTypeResolver, Pipelines.LocateObject.SetTypeResolverProcessor.SetTypeResolver>(); serviceCollection.AddSingleton<Pipelines.LocateObject.ResolveTypeProcessor.IResolveType, Pipelines.LocateObject.ResolveTypeProcessor.ResolveType>(); serviceCollection.AddSingleton<IAddDefaultObjectLocator, AddDefaultObjectLocator>(); serviceCollection.AddSingleton<ILocateObject, LocateObject>(); } private void ConfigureCreateObjectPipelineProcessors(IServiceCollection serviceCollection) { serviceCollection.AddSingleton<ISetTypeResolver, SetTypeResolver>(); serviceCollection.AddSingleton<IResolveType, ResolveType>(); serviceCollection.AddSingleton<IAddDefaultObjectCreator, AddDefaultObjectCreator>(); serviceCollection.AddSingleton<ICreateObject, CreateObject>(); serviceCollection.AddSingleton<ISetTypeCacher, SetTypeCacher>(); serviceCollection.AddSingleton<ICacheType, CacheType>(); } private void ConfigureResolveObjectPipelineProcessors(IServiceCollection serviceCollection) { serviceCollection.AddSingleton<Pipelines.ResolveObject.ISetTypeResolver, Pipelines.ResolveObject.SetTypeResolver>(); serviceCollection.AddSingleton<Pipelines.ResolveObject.ResolveTypeProcessor.IResolveType, Pipelines.ResolveObject.ResolveTypeProcessor.ResolveType>(); serviceCollection.AddSingleton<ISetObjectLocator, SetObjectLocator>(); serviceCollection.AddSingleton<Pipelines.ResolveObject.LocateObjectProcessor.ILocateObject, Pipelines.ResolveObject.LocateObjectProcessor.LocateObject>(); serviceCollection.AddSingleton<ISetObjectCreator, SetObjectCreator>(); serviceCollection.AddSingleton<Pipelines.ResolveObject.CreateObjectProcessor.ICreateObject, Pipelines.ResolveObject.CreateObjectProcessor.CreateObject>(); } private void ConfigureOtherServices(IServiceCollection serviceCollection) { serviceCollection.AddSingleton<IReflectionUtilService, ReflectionUtilService>(); } private TConfigObject CreateConfigObject<TConfigObject>(IServiceProvider provider, string path) where TConfigObject : class { BaseFactory factory = GetService<BaseFactory>(provider); return factory.CreateObject(path, true) as TConfigObject; } private TService GetService<TService>(IServiceProvider provider) { return provider.GetService<TService>(); } } }
Finally, I strung all the pieces together using the following Sitecore patch config file:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <services> <configurator type="Sandbox.Foundation.ObjectResolution.ObjectResolutionConfigurator, Sandbox.Foundation.ObjectResolution" /> </services> <pipelines> <resolveItem> <processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveItem.AddDefaultItemResolverProcessor.IAddDefaultItemResolver, Sandbox.Foundation.ObjectResolution" resolve="true" /> <processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveItem.ResolveItemProcessor.IResolveItem, Sandbox.Foundation.ObjectResolution" resolve="true" /> </resolveItem> <resolveType> <processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.SetItemResolverProcessor.ISetItemResolver, Sandbox.Foundation.ObjectResolution" resolve="true" /> <processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.ResolveTypeProcessor.IResolveItem, Sandbox.Foundation.ObjectResolution" resolve="true" /> <processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.SetTypeNameProcessor.ISetTypeName, Sandbox.Foundation.ObjectResolution" resolve="true" /> <processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.AddDefaultTypeResolverProcessor.IAddDefaultTypeResolver, Sandbox.Foundation.ObjectResolution" resolve="true" /> <processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.SetTypeCacherProcessor.ISetTypeCacher, Sandbox.Foundation.ObjectResolution" resolve="true" /> <processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.ResolveTypeProcessor.IResolveType, Sandbox.Foundation.ObjectResolution" resolve="true" /> </resolveType> <locateObject> <processor type="Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.SetTypeResolverProcessor.ISetTypeResolver, Sandbox.Foundation.ObjectResolution" resolve="true" /> <processor type="Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.ResolveTypeProcessor.IResolveType, Sandbox.Foundation.ObjectResolution" resolve="true" /> <processor type="Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.AddDefaultObjectLocatorProcessor.IAddDefaultObjectLocator, Sandbox.Foundation.ObjectResolution" resolve="true" /> <processor type="Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.LocateObjectProcessor.ILocateObject, Sandbox.Foundation.ObjectResolution" resolve="true" /> </locateObject> <createObject> <processor type="Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.SetTypeResolverProcessor.ISetTypeResolver, Sandbox.Foundation.ObjectResolution" resolve="true" /> <processor type="Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.ResolveTypeProcessor.IResolveType, Sandbox.Foundation.ObjectResolution" resolve="true" /> <processor type="Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.AddDefaultObjectCreatorProcessor.IAddDefaultObjectCreator, Sandbox.Foundation.ObjectResolution" resolve="true" /> <processor type="Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.CreateObjectProcessor.ICreateObject, Sandbox.Foundation.ObjectResolution" resolve="true" /> <processor type="Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.SetTypeCacherProcessor.ISetTypeCacher, Sandbox.Foundation.ObjectResolution" resolve="true" /> <processor type="Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.CacheTypeProcessor.ICacheType, Sandbox.Foundation.ObjectResolution" resolve="true" /> </createObject> <resolveObject> <processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.ISetTypeResolver, Sandbox.Foundation.ObjectResolution" resolve="true" /> <processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.ResolveTypeProcessor.IResolveType, Sandbox.Foundation.ObjectResolution" resolve="true" /> <processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.SetObjectLocatorProcessor.ISetObjectLocator, Sandbox.Foundation.ObjectResolution" resolve="true" /> <processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.LocateObjectProcessor.ILocateObject, Sandbox.Foundation.ObjectResolution" resolve="true" /> <processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.SetObjectCreatorProcessor.ISetObjectCreator, Sandbox.Foundation.ObjectResolution" resolve="true" /> <processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.CreateObjectProcessor.ICreateObject, Sandbox.Foundation.ObjectResolution" resolve="true" /> </resolveObject> </pipelines> <moduleSettings> <foundation> <objectResolution> <itemResolverServiceSettings type="Sandbox.Foundation.ObjectResolution.Models.Resolvers.ItemResolvers.ItemResolverServiceSettings, Sandbox.Foundation.ObjectResolution" singleInstance="true"> <ResolveItemPipelineName>resolveItem</ResolveItemPipelineName> </itemResolverServiceSettings> <typeResolverServiceSettings type="Sandbox.Foundation.ObjectResolution.Models.Resolvers.TypeResolvers.TypeResolverServiceSettings, Sandbox.Foundation.ObjectResolution" singleInstance="true"> <ResolveTypePipelineName>resolveType</ResolveTypePipelineName> </typeResolverServiceSettings> <objectLocatorServiceSettings type="Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectLocators.ObjectLocatorServiceSettings, Sandbox.Foundation.ObjectResolution" singleInstance="true"> <LocateObjectPipelineName>locateObject</LocateObjectPipelineName> </objectLocatorServiceSettings> <objectCreatorServiceSettings type="Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectCreators.ObjectCreatorServiceSettings, Sandbox.Foundation.ObjectResolution" singleInstance="true"> <CreateObjectPipelineName>createObject</CreateObjectPipelineName> </objectCreatorServiceSettings> <objectResolverServiceSettings type="Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectResolvers.ObjectResolverServiceSettings, Sandbox.Foundation.ObjectResolution" singleInstance="true"> <ResolveObjectPipelineName>resolveObject</ResolveObjectPipelineName> <UseTypeCache>true</UseTypeCache> </objectResolverServiceSettings> </objectResolution> </foundation> </moduleSettings> </sitecore> </configuration>
In my next post, I will be using the entire system above for resolving custom Forms Submit Actions from the Sitecore IoC container. Stay tuned for that post.
If you have made it this far, hats off to you. 😉
Sorry for throwing so much at you, but that’s what I do. 😉
On closing, I would like to mention that the system above could be used for resolving types from the Sitecore IoC container for WFFM but this is something that I will not investigate. If you happen to get this to work on WFFM, please share in a comment below.
Until next time, keep on Sitecoring.
Download Random Giphy Images and Save to the Media Library Via a Custom Content Editor Image Field in Sitecore
In my previous post I created a custom Content Editor image field in the Sitecore Experience Platform. This custom image field gives content authors the ability to download an image from outside of their Sitecore instance; save the image to the Media Library; and then map that resulting Media Library Item to the custom Image field on an Item in the content tree.
Building that solution was a great way to spend a Friday night (and even the following Saturday morning) — though I bet some people would argue watching cat videos on YouTube might be better way to spend a Friday night — and even gave me the opportunity to share that solution with you guys.
After sharing this post on Twitter, Sitecore MVP Kam Figy replied to that tweet with the following:
This gave me an idea: why not modify the solution from my previous post to give the ability to download a random image from Giphy via their the API?
You might be asking yourself “what is this Giphy thing?” Giphy is basically a site that allows users to upload images — more specifically animated GIFs — and then associate those uploaded images with tags. These tags are used for finding images on their site and also through their API.
You might be now asking “what’s the point of Giphy?” The point is to have fun and share a laugh; animated GIFs can be a great way of achieving these.
Some smart folks out there have built integrations into other software platforms which give users the ability pull images from the Giphy API. An example of this can be seen in Slack messaging application.
As a side note, if you aren’t on the Sitecore Community Slack, you probably should be. This is the fastest way to get help, share ideas and even have some good laughs from close to 1000 Sitecore developers, architects and marketers from around the world in real-time. If you would like to join the Sitecore Community Slack, please let me know and I will send you an invite though please don’t ask for an invite in comments section below on this post. Instead reach out to me on Twitter: @mike_i_reynolds. You can also reach out to Sitecore MVP Akshay Sura: @akshaysura13.
Here’s an example of me calling up an image using some tags in one of the channels on the Sitecore Community Slack using the Giphy integration for Slack:
There really isn’t anything magical about the Giphy API — all you have to do is send an HTTP request with some query string parameters. Giphy’s API will then give you a response in JSON:
Before I dig into the solution below, I do want to let you know I will not be talking about all of the code in the solution. Most of the code was repurposed from my previous post. If you have not read my previous post, please read it before moving forward so you have a full understanding of how this works.
Moreover, do note there is probably no business value in using the following solution as is — it was built for fun on another Friday night and Saturday morning. 😉
To get data out of this JSON response, I decided to use Newtonsoft.Json. Why did I choose this? It was an easy decision: Newtonsoft.Json comes with Sitecore “out of the box” so it was convenient for me to choose this as a way to parse the JSON coming from the Giphy API.
I created the following model classes with JSON to C# property mappings:
using Newtonsoft.Json; namespace Sitecore.Sandbox.Providers { public class GiphyData { [JsonProperty("type")] public string Type { get; set; } [JsonProperty("id")] public string Id { get; set; } [JsonProperty("url")] public string Url { get; set; } [JsonProperty("image_original_url")] public string ImageOriginalUrl { get; set; } [JsonProperty("image_url")] public string ImageUrl { get; set; } [JsonProperty("image_mp4_url")] public string ImageMp4Url { get; set; } [JsonProperty("image_frames")] public string ImageFrames { get; set; } [JsonProperty("image_width")] public string ImageWidth { get; set; } [JsonProperty("image_height")] public string ImageHeight { get; set; } [JsonProperty("fixed_height_downsampled_url")] public string FixedHeightDownsampledUrl { get; set; } [JsonProperty("fixed_height_downsampled_width")] public string FixedHeightDownsampledWidth { get; set; } [JsonProperty("fixed_height_downsampled_height")] public string FixedHeightDownsampledHeight { get; set; } [JsonProperty("fixed_width_downsampled_url")] public string FixedWidthDownsampledUrl { get; set; } [JsonProperty("fixed_width_downsampled_width")] public string FixedWidthDownsampledWidth { get; set; } [JsonProperty("fixed_width_downsampled_height")] public string FixedWidthDownsampledHeight { get; set; } [JsonProperty("fixed_height_small_url")] public string FixedHeightSmallUrl { get; set; } [JsonProperty("fixed_height_small_still_url")] public string FixedHeightSmallStillUrl { get; set; } [JsonProperty("fixed_height_small_width")] public string FixedHeightSmallWidth { get; set; } [JsonProperty("fixed_height_small_height")] public string FixedHeightSmallHeight { get; set; } [JsonProperty("fixed_width_small_url")] public string FixedWidthSmallUrl { get; set; } [JsonProperty("fixed_width_small_still_url")] public string FixedWidthSmallStillUrl { get; set; } [JsonProperty("fixed_width_small_width")] public string FixedWidthSmallWidth { get; set; } [JsonProperty("fixed_width_small_height")] public string FixedWidthSmallHeight { get; set; } [JsonProperty("username")] public string Username { get; set; } [JsonProperty("caption")] public string Caption { get; set; } } }
using Newtonsoft.Json; namespace Sitecore.Sandbox.Providers { public class GiphyMeta { [JsonProperty("status")] public int Status { get; set; } [JsonProperty("msg")] public string Message { get; set; } } }
using Newtonsoft.Json; namespace Sitecore.Sandbox.Providers { public class GiphyResponse { [JsonProperty("data")] public GiphyData Data { get; set; } [JsonProperty("meta")] public GiphyMeta Meta { get; set; } } }
Every property above in every class represents a JSON property/object in the response coming back from the Giphy API.
Now, we need a way to make a request to the Giphy API. I built the following interface whose instances will do just that:
namespace Sitecore.Sandbox.Providers { public interface IGiphyImageProvider { GiphyData GetRandomGigphyImageData(string tags); } }
The following class implements the interface above:
using System; using System.Net; using Sitecore.Diagnostics; using Newtonsoft.Json; using System.IO; namespace Sitecore.Sandbox.Providers { public class GiphyImageProvider : IGiphyImageProvider { private string RequestUrlFormat { get; set; } private string ApiKey { get; set; } public GiphyData GetRandomGigphyImageData(string tags) { Assert.IsNotNullOrEmpty(RequestUrlFormat, "RequestUrlFormat"); Assert.IsNotNullOrEmpty(ApiKey, "ApiKey"); Assert.ArgumentNotNullOrEmpty(tags, "tags"); string response = GetJsonResponse(GetRequestUrl(tags)); if(string.IsNullOrWhiteSpace(response)) { return new GiphyData(); } try { GiphyResponse giphyResponse = JsonConvert.DeserializeObject<GiphyResponse>(response); if(giphyResponse != null && giphyResponse.Meta != null && giphyResponse.Meta.Status == 200 && giphyResponse.Data != null) { return giphyResponse.Data; } } catch(Exception ex) { Log.Error(ToString(), ex, this); } return new GiphyData(); } protected virtual string GetRequestUrl(string tags) { Assert.ArgumentNotNullOrEmpty(tags, "tags"); return string.Format(RequestUrlFormat, ApiKey, Uri.EscapeDataString(tags)); } protected virtual string GetJsonResponse(string requestUrl) { Assert.ArgumentNotNullOrEmpty(requestUrl, "requestUrl"); try { WebRequest request = HttpWebRequest.Create(requestUrl); request.Method = "GET"; string json; using (WebResponse response = request.GetResponse()) { using (Stream responseStream = response.GetResponseStream()) { using (StreamReader sr = new StreamReader(responseStream)) { return sr.ReadToEnd(); } } } } catch (Exception ex) { Log.Error(ToString(), ex, this); } return string.Empty; } } }
Code in the methods above basically take in tags for the type of random image we want from Giphy; build up the request URL — the template of the request URL and API key (I’m using the public key which is open for developers to experiment with) are populated via the Sitecore Configuration Factory (have a look at the patch include configuration file further down in this post to get an idea of how the properties of this class are populated); make the request to the Giphy API; get back the response; hand the response over to some Newtonsoft.Json API code to parse JSON into model instances of the classes shown further above in this post; and then return the nested model instances.
I then created the following Sitecore.Shell.Applications.ContentEditor.Image subclass which represents the custom Content Editor Image field:
using System; using Sitecore.Configuration; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Pipelines; using Sitecore.Shell.Framework; using Sitecore.Web.UI.Sheer; using Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary; using Sitecore.Sandbox.Providers; namespace Sitecore.Sandbox.Shell.Applications.ContentEditor { public class GiphyImage : Sitecore.Shell.Applications.ContentEditor.Image { private IGiphyImageProvider GiphyImageProvider { get; set; } public GiphyImage() : base() { GiphyImageProvider = GetGiphyImageProvider(); } protected virtual IGiphyImageProvider GetGiphyImageProvider() { IGiphyImageProvider giphyImageProvider = Factory.CreateObject("imageProviders/giphyImageProvider", false) as IGiphyImageProvider; Assert.IsNotNull(giphyImageProvider, "The giphyImageProvider was not properly defined in configuration"); return giphyImageProvider; } public override void HandleMessage(Message message) { Assert.ArgumentNotNull(message, "message"); if (string.Equals(message.Name, "contentimage:downloadGiphy", StringComparison.CurrentCultureIgnoreCase)) { GetInputFromUser(); return; } base.HandleMessage(message); } protected void GetInputFromUser() { RunProcessor("GetGiphyTags", new ClientPipelineArgs()); } protected virtual void GetGiphyTags(ClientPipelineArgs args) { if (!args.IsPostBack) { SheerResponse.Input("Enter giphy tags:", string.Empty); args.WaitForPostBack(); } else if (args.HasResult) { args.Parameters["tags"] = args.Result; args.IsPostBack = false; RunProcessor("GetGiphyImageUrl", args); } else { CancelOperation(args); } } protected virtual void GetGiphyImageUrl(ClientPipelineArgs args) { GiphyData giphyData = GiphyImageProvider.GetRandomGigphyImageData(args.Parameters["tags"]); if (giphyData == null || string.IsNullOrWhiteSpace(giphyData.ImageUrl)) { SheerResponse.Alert("Unfortunately, no image matched the tags you specified. Please try again."); CancelOperation(args); return; } args.Parameters["imageUrl"] = giphyData.ImageUrl; args.IsPostBack = false; RunProcessor("ChooseMediaLibraryFolder", args); } 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 the Giphy 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; args.IsPostBack = false; 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 does not differ much from the Image class I shared in my previous post. The only differences are in the instantiation of an IGiphyImageProvider object using the Sitecore Configuration Factory — this object is used for getting the Giphy image URL from the Giphy API; the GetGiphyTags() method prompts the user for tags used in calling up a random image from Giphy; and in the GetGiphyImageUrl() method which uses the IGiphyImageProvider instance to get the image URL. The rest of the code in this class is unmodified from the Image class shared in my previous post.
I then defined the IGiphyImageProvider code in the following patch include configuration file:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <imageProviders> <giphyImageProvider type="Sitecore.Sandbox.Providers.GiphyImageProvider, Sitecore.Sandbox" singleInstance="true"> <RequestUrlFormat>http://api.giphy.com/v1/gifs/random?api_key={0}&tag={1}</RequestUrlFormat> <ApiKey>dc6zaTOxFJmzC</ApiKey> </giphyImageProvider> </imageProviders> </sitecore> </configuration>
Be sure to check out the patch include configuration file from my previous post as it contains the custom pipeline that downloads images from a URL.
You should also refer my previous post which shows you how to register a custom Content Editor field in the core database of Sitecore.
Let’s test this out.
We need to add this new field to a template. I’ve added it to the “out of the box” Sample Item template:
My Home item uses the above template. Let’s download a random Giphy image on it:
I then supplied some tags for getting a random image:
Let’s choose a place to save the image in the Media Library:
As you can see, the image was downloaded and saved into the Media Library in the selected folder, and then saved in the custom field on the Home item:
If you are curious, this is the image that was returned by the Giphy API:
If you have any thoughts on this, please share in a comment.
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:
We also need a new Menu item for the “Download Image” link:
Let’s take this for a spin!
I added a new field using the custom Image field type to my Sample item template:
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:
I was then prompted with a dialog to supply an image URL. I pasted one I found on the internet:
After clicking “OK”, I was prompted with another dialog to choose a Media Library location for storing the image. I chose some random folder:
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:
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.
Add a New Sitecore Link Field Type Without Writing Any Custom Code
I’m sure others have blogged about doing something similar to the following — I probably have also blogged about this more than once but cannot remember everything I’ve blogged about given that I have a huge number of blog posts — but I figure this will be helpful to folks new to Sitecore or those who have seen this before but need some reminding.
The other day, I had to create a new Link field type that only gives content authors/editors the ability to insert links to Items within the Media Library. The following steps are what I used to make this happen. I didn’t need to write any custom code, and this would also work for other solutions similar to this though keep in mind that this solution will only work within the Content Editor — to make this work in the Experience Editor, you will have to write some custom code which I might show in a future blog post.
Step 1: Duplicate an existing field type.
Here I am duplicating ‘/sitecore/system/Field types/Link Types/General Link’ in the Core database:
Step 2: Delete button items that you don’t need.
I’ve deleted all buttons that are not related to Media library items, and also preserved the Follow and Clear button items:
Step 3: Add a new field using the new type you have created in step 1.
Just add the new field on a template:
Step 4: Go to an item using the template from step 3.
As you can see, only the buttons that you preserved from step 2 are there:
Step 5: Click one of the buttons that you preserved from step 2.
Here, I clicked the ‘Insert media link’ button:
After clicking the ‘Insert’ button from the dialog in step 5, I see that a Media Library Item link was set within this new field:
Raw values:
If you have any comments/thoughts/suggestions on this, please share in 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:
Once I built and deployed everything, I navigated to my homepage Item, and saw the following rendered HTML:
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><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:
I then navigated to my homepage; looked at the rendered HTML; and saw the following:
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:
😀
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><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):
After doing a build and navigating to my homepage Item, I saw the following in the rendered HTML:
As you can see, it worked magically. 🙂
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:
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><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):
I also had to add two Link field controls to the sample rendering.xslt that ships with Sitecore:
Let’s test the “Insert Link” dialog:
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:
Let’s see if this works on the “Insert External Link” dialog:
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:
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:
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:
After clicking save, I navigated to one of the content Items that use this Template:
As you can see, the tokens were expanded. 🙂
If you have any thoughts on this, please drop a comment.
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:
I also set up another Item Validator for Datetime fields:
Let’s take this for a spin!
I entered some integer values in the two integer fields being compared:
As you can see, we get a warning.
I then set some Datetime field values on the two Datetime fields being compared:
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.