Monday 26 June 2017

Sitecore Glass Custom Field Mapping

Creating a custom Sitecore Glass field mapping

Recently I was building out some models using Sitecore Glass for a dynamic menu'ing module; when I realized that Glass didn't offer an out of the box solution to map an item's parent Id to a field.

There is however the SitecoreParent attribute, but this loads the entire object, which could compromise performance.  So I decided to see how easy it would be develop a custom mapping for Sitecore Glass.

I took a look at how the SitecoreIdAttribute and SitecoreParentAttribute worked and set about creating my own SitecoreParentIdAttribute.

There are three pieces that you need when creating a Glass mapper:
  1. The attribute
  2. The configuration
  3. The data mapper

The Attribute

    public class SitecoreParentIdAttribute : AbstractPropertyAttribute
    {

        protected IEnumerable<type> AcceptedTypes { get; private set; }
        public SitecoreParentIdAttribute()

        {
            AcceptedTypes = new Type[2]
            {
                typeof (ID),
                typeof (Guid)
            };
        }

        public override AbstractPropertyConfiguration Configure(PropertyInfo propertyInfo)
        {
            var config = new ParentIdConfiguration();
            Configure(propertyInfo, config);
            return config;
        }

        public void Configure(PropertyInfo propertyInfo, ParentIdConfiguration config)
        {
            if (AcceptedTypes.All(x => propertyInfo.PropertyType != x))
                throw new ConfigurationException(string.Format("Property type {0} not supported as an ID on {1}", propertyInfo.PropertyType.FullName, propertyInfo.DeclaringType.FullName));
            config.Type = propertyInfo.PropertyType;
            base.Configure(propertyInfo, config);
        }
    }

In the above code we define the attribute, the accepted types (Guid and ID), and the configuration class that will hold any additional information.


The Configuration

    public class ParentIdConfiguration : AbstractPropertyConfiguration
    {
        public Type Type { get; set; }

        protected override AbstractPropertyConfiguration CreateCopy()
        {
            return new ParentIdConfiguration();
        }

        protected override void Copy(AbstractPropertyConfiguration copy)
        {
            (copy as ParentIdConfiguration).Type = Type;
            base.Copy(copy);
        }
    }

In the above code we define the configuration class that will also be used by the mapper to tie it back to the attribute.


The Data Mapper

   public class SitecoreParentIdMapper : AbstractDataMapper
    {
        private Func<Item, object> _getValue;

        public SitecoreParentIdMapper()
        {
            ReadOnly = true;
        }

        public override void MapToCms(AbstractDataMappingContext mappingContext)
        {
            throw new NotImplementedException();
        }

        public override object MapToProperty(AbstractDataMappingContext mappingContext)
        {
            return _getValue(((SitecoreDataMappingContext) mappingContext).Item);
        }

        public override void Setup(DataMapperResolverArgs args)
        {
            if (args.PropertyConfiguration.PropertyInfo.PropertyType == typeof (Guid))
                _getValue = item => item.ParentID.Guid;
            else if (args.PropertyConfiguration.PropertyInfo.PropertyType == typeof (ID))
                _getValue = item => item.ParentID;
            else
                throw new NotSupportedException(
                    "The type {0} on {0}.{1} is not supported by SitecoreIdMapper".Formatted(
                        args.PropertyConfiguration.PropertyInfo.ReflectedType.FullName,
                        args.PropertyConfiguration.PropertyInfo.Name));
            base.Setup(args);
        }

        public override bool CanHandle(AbstractPropertyConfiguration configuration, Context context)
        {
            return configuration is ParentIdConfiguration;
        }
    }


In the above code we define how to map to the field (current item being the context).  The most important pieces here are the:
  • Setup method - where we define the mapping
  • CanHandle method - where we define which attributes (via configuration) are tied to the mapping.  
We don't need to implement the MapToCMS method as we will never want to update Sitecore with this value.  So to ensure that the mapper knows  not to use the method; we set the ReadOnly boolean flag to true.

N.B. If you break point the CanHandle method you will see it hit multiple times as each field with a Glass attribute on it is parsed.

Here it is in use:
[SitecoreParentId]
public virtual Guid ParentId { get; set; }
You'll need to register your mapping with whatever IoC framework, you are using. In a future post I will go into this in more details, but suffice to say for Sitecore's own IoC framework I did:
[MethodImpl(MethodImplOptions.NoInlining)]
public static void AddGlassMaps(this IServiceCollection serviceCollection, string assemblyDefinition)
{
    Directory.SetCurrentDirectory(AssemblyDirectory);
    var config = new Config();

    var resolver = new DependencyResolver(config);

    var context = Context.Create(resolver);
    resolver.DataMapperFactory.Add(() => new SitecoreParentIdMapper());

    context.Load(GlassLoaders(assemblyDefinition));
}
And thats all there is to it :)