Extending Media Item Templates with Custom Glass.Mapper Data Handlers

On a recent project I needed to add a couple of fields to Media Items templates in order store some additional data. We are using the Glass Mapper ORM in our project and out of the box there are two fields which we can map to: Image and File. I also needed to retrieve the file size, which neither of these fields returned either.

It turned out to be relatively simple to extend Glass Mapper and add our own custom data handler to map the additional fields.

First, we need to extend an existing Glass field:

namespace Glass.Custom.Fields
{
    public class ExtendedFile : Glass.Mapper.Sc.Fields.File
    {
        public new string Src { get; internal set; }
        public string Md5Hash { get; set; }
        public long Size { get; set; }
    }
}

Unfortunately the Src property is internal set only so we have to use the new modifier to hide the base property instead. We also add our own properties as well as a Size property which will map to the default field in the media template.

We then need to add a custom data handler to maps the fields in Sitecore to our model.

using System;
using Glass.Mapper;
using Glass.Mapper.Sc;
using Glass.Mapper.Sc.Configuration;
using Glass.Mapper.Sc.DataMappers;
using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Links;
using Sitecore.Resources.Media;

namespace Glass.Custom.DataMappers
{
    public class SitecoreFieldExtendedFileMapper : AbstractSitecoreFieldMapper
    {
        public SitecoreFieldExtendedFileMapper()
            : base(typeof(ExtendedFile))
        {
        }

        public override object GetField(Field field, SitecoreFieldConfiguration config, SitecoreDataMappingContext context)
        {
            FileField fileField = new FileField(field);
            ExtendedFile file = new ExtendedFile();
            file.Id = fileField.MediaID.Guid;

            if (fileField.MediaItem != null)
            {
                file.Src = MediaManager.GetMediaUrl(fileField.MediaItem);

                // Extended File Attributes
                file.Md5Hash = fileField.MediaItem[IFileExtensionConstants.MD5HashFieldName];
                file.Size = ((MediaItem) fileField.MediaItem).Size;
            }

            return file;
        }

        public override void SetField(Field field, object value, SitecoreFieldConfiguration config, SitecoreDataMappingContext context)
        {
            ExtendedFile file = value as ExtendedFile;
            var item = field.Item;
            FileField fileField = new FileField(field);

            if (file == null)
            {
                fileField.Clear();
                return;
            }

            if (fileField.MediaID.Guid != file.Id)
            {
                if (file.Id == Guid.Empty)
                {
                    ItemLink link = new ItemLink(item.Database.Name, item.ID, fileField.InnerField.ID, fileField.MediaItem.Database.Name, fileField.MediaID, fileField.MediaItem.Paths.FullPath);
                    fileField.RemoveLink(link);
                }
                else
                {
                    ID newId = new ID(file.Id);
                    Item target = item.Database.GetItem(newId);
                    if (target != null)
                    {
                        fileField.MediaID = newId;
                        ItemLink link = new ItemLink(item.Database.Name, item.ID, fileField.InnerField.ID, target.Database.Name, target.ID, target.Paths.FullPath);
                        fileField.UpdateLink(link);
                    }
                    else throw new MapperException("No item with ID {0}. Can not update File Item field".Formatted(newId));
                }
            }
        }

        public override string SetFieldValue(object value, SitecoreFieldConfiguration config, SitecoreDataMappingContext context)
        {
            throw new NotImplementedException();
        }

        public override object GetFieldValue(string fieldValue, SitecoreFieldConfiguration config, SitecoreDataMappingContext context)
        {
            throw new NotImplementedException();
        }

    }
}

EDIT: Be sure to read part two of the post where the above has been cleaned up somewhat.

There’s nothing to clever going on here, most of this code is copied directly from SitecoreFieldFileMapper and the implementation of the GetField() method extended to map our additional fields. Everything else stays the same, including the SetField() method, I’ll follow up with another blog post on exactly why I needed to extend the template.

So why the copy/paste? Notice that we have inherited our class from AbstractSitecoreFieldMapper and not SitecoreFieldFileMapper, which unfortunately only has a parameterless constructor. The abstract class accepts an array of objects in its constructor to specify which types it can handle the mapping for (but TypesHandled is marked as private set as well :(), in this instance we are calling base(typeof(ExtendedFile)).

The only thing left is to hook it all up, we need to register the data handler in CreateResolver() located in App_Start\GlassMapperScCustom.cs:

public static IDependencyResolver CreateResolver(){
	var config = new Glass.Mapper.Sc.Config();
            
    var dependencyResolver = new DependencyResolver(config);

    dependencyResolver.DataMapperFactory.Insert(8, () => new SitecoreFieldExtendedFileMapper());

    return dependencyResolver;
}

Our custom handler has been inserted into the 8th position, we could have just inserted it at position 0, we’ll just make sure it is before the inherited field mapper as specified in DataMapperConfigFactory.cs.

I didn’t want to replace the existing File mapper, I had very specific areas of the site where the extended information would be needed and since most of our models are code generated, it was simpler to update for limited usage.

With everything hooked up, the only thing left to do is to make use of our new extended Glass Field.

Our model. Nothing clever going on here.

public partial interface IFileDownload : IGlassBase 
{
    [SitecoreField(IFileDownloadConstants.DownloadFileFieldName)]
    Fields.ExtendedFile DownloadFile  { get; set; }

    [SitecoreField(IFileDownloadConstants.OtherField)]
    string OtherField  { get; set; }
	
    [SitecoreField(IFileDownloadConstants.YetAnotherField)]
    string YetAnotherField { get; set; }
}

And our view. Nothing clever going on here either, but now we have access to our additional properties.

@model IFileDownload 
                                                        
@if (Model.DownloadFile != null)
{
    <a href="@Model.DownloadFile.Src" class="button">@Translate.Text(Dictionary.Download)</a>
    <br />@Translate.Text(Dictionary.MD5) : @Model.DownloadFile.Md5Hash
    <br />@Translate.Text(Dictionary.FileSize) : @Model.DownloadFile.Size.ToFileSize()
}

Yay!

Additonal reading:

Advertisements

2 comments

  1. Mike Reynolds · October 31, 2015

    Brilliant post Kam!

    I haven’t looked into this myself but instead of overriding all of these methods, couldn’t you have employed the decorator pattern where you would wrap an instance of a SitecoreFieldFileMapper, and delegate to some of the methods on it ?

    This most likely wouldn’t prevent the copy/paste need on the GetField and SetField methods but could be used for the other methods.

    Mike

    • jammykam · October 31, 2015

      Thanks Mike. That’s not a bad idea thinking about it. Ideally, if the TypesHandler was not marked as `private set` then I could have inherited from File and 90% of the code would have disappeared since I could have reset that property in my constructor and it’s only the GetField() method I had to make changes to. In this instance, the code is fairly straight forward but for Image it is a lot more involved so copy/paste seems like a worse idea. I’ll suggest that change is made in the Glass project.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s