Utilising the render field pipeline when using Glass for Sitecore

How I invoke Sitecores RenderField pipeline when using Glass.Mapper.Sc without having to use magic string (gimme type safety!)
June 19 2013

Glass.lu produces Glass.Mapper.Sc, a cool ORM for Sitecore items.  It allows us .NET devs to create POCO objects to model items in our Sitecore CMS content tree.  This provides the option to bind Glass model directly to the our razor views or to use it as an intermediary to populate DTO view models in our controller actions. 

The most frequently used use scenario is to create views inheriting from Glass.Mapper.Sc.Web.Mvc.GlassView<>.  In this manner we can create razor views as we would with any standard ASP.NET MVC razor application.

@inherits Glass.Mapper.Sc.Web.Mvc.GlassView<MammothSite.Models.ContactUs>
@{
    ViewBag.Title = Model.Title;    
}

<h2>@Model.Title</h2>

<div class="grid_14">
    <h3>Call us at @Model.GeneralPhone), or email @Model.GeneralEmail</h3>        
    @Editable(m => m.BodyText)
</div>

The above nearly works. But not quite.  Here’s my glass model (simplified for this demo).

[SitecoreType(TemplateName = "Contact Us")]
public class ContactUs : ContentItem
{
    [SitecoreField(FieldType = SitecoreFieldType.MultiLineText)]
    public virtual string Address { get; set; }

    [SitecoreField(FieldType = SitecoreFieldType.GeneralLink)]
    public virtual Link GeneralEmail { get; set; }
    
    [SitecoreField]
    public virtual string GeneralPhone { get; set; }

    [SitecoreField]
    public virtual IEnumerable<StaffMember> Contacts { get; set; }
}

 

You’ll notice that the GeneralEmail property is of type Glass.Mappers.Sc.Fields.Link.  Using the view defined above this will render the text “glass.mapper.sc.fields.link” (the type of the property) and not the actual property value.  If this property is of type string, Sitecore will render a link element.  Normally the link element would get processed by the Render Field pipeline, but using the above view synytax does not call the Render Field pipeline.  This is a problem.  It seems, as of version 3.0.3.11, Glass does not provide a way to do this.  You need to use the Sitecore method

@inherits Glass.Mapper.Sc.Web.Mvc.GlassView<ContactUs>
@{
    ViewBag.Title = Model.Title;    
}

<h2>@Model.Title</h2>

<div class="grid_14">
    <h3>Call us at @Model.GeneralPhone), or email @Html.Sitecore().Field("GeneralEmail")</h3>        
    @Editable(m => m.BodyText)
</div>

This will invoke the pipeline and render the link correctly. However I have to use a string.  Not optimal. Also it’s disconnected from the model.  Internally the method uses the current item in context.  (You can pass an item into the Field method but there’s no elegant way to get the item from the Glass model).

To complicate matters, the site I am migrating to MVC has a custom email renderer to prevent bots from easily getting email addresses to spam.  I needed that renderer to be invoked everywhere I’m using a link. 

The initial option I considered was to create an extension method off the Link type and replicate the code inside my custom rendered (or call that custom rendered).  That’s bad for so many reasons, including:

  • I would need to do this for every link field ever.  Any developer that came after me would need to know this.
  • It bypasses the existing standard method to render fields.
  • I cannot chain renders for a field
  • I would have to maintain duplicate code
  • It’s just dumb

So really, I needed a way to call Sitecores Render Field pipeline.  Being lazy, I went and searched through the Glass source code for something similar.  The existing GlassView provides the ability to perform Page Editting, via the Editable method, so I looked there first.  I followed the call chain through to GlassHtml and blatantly copied the code out of MakeEditable. The difference being I didn’t need the IsInEditingMode check.

I created an extension method off IGlassHtml

public static class GlassHtmlHelper
{
    // Invokes the render field pipeline
    public static string Field<T>(this IGlassHtml glassHtml, Expression<Func<T, object>> field, T target)
    {
        if (field.Parameters.Count > 1)            
            throw new MapperException("Too many parameters in linq expression {0}".Formatted(field.Body));

        MemberExpression memberExpression;

        var body = field.Body as UnaryExpression;
        if (body != null)
        {
            memberExpression = body.Operand as MemberExpression;
        }
        else if (!(field.Body is MemberExpression))
        {
            throw new MapperException("Expression doesn't evaluate to a member {0}".Formatted(field.Body));
        }
        else
        {
            memberExpression = (MemberExpression) field.Body;
        }

        //this contains the expression that will evaluate to the object containing the property
        var objectExpression = memberExpression.Expression;

        var finalTarget = Expression.Lambda(objectExpression, field.Parameters).Compile().DynamicInvoke(target);

        var site = global::Sitecore.Context.Site;
        
        if (glassHtml.SitecoreContext.GlassContext == null) 
            throw new NullReferenceException("Context cannot be null");

        var config = glassHtml.SitecoreContext.GlassContext.GetTypeConfiguration(finalTarget) as SitecoreTypeConfiguration; 
        var scClass = config.ResolveItem(finalTarget, glassHtml.SitecoreContext.Database);

        
        //lambda expression does not always return expected memberinfo when inheriting
        //c.f. http://stackoverflow.com/questions/6658669/lambda-expression-not-returning-expected-memberinfo
        var prop = config.Type.GetProperty(memberExpression.Member.Name);

        //interfaces don't deal with inherited properties well
        if (prop == null && config.Type.IsInterface)
        {
            Func<Type, PropertyInfo> interfaceCheck = null;
            interfaceCheck = (inter) =>
            {
                var interfaces = inter.GetInterfaces();
                var properties =
                    interfaces.Select(x => x.GetProperty(memberExpression.Member.Name)).Where(
                        x => x != null);
                if (properties.Any()) return properties.First();
                else
                    return interfaces.Select(x => interfaceCheck(x)).FirstOrDefault(x => x != null);
            };
            prop = interfaceCheck(config.Type);
        }

        if (prop != null && prop.DeclaringType != prop.ReflectedType)
        {
            //properties mapped in data handlers are based on declaring type when field is inherited, make sure we match
            prop = prop.DeclaringType.GetProperty(prop.Name);
        }

        if (prop == null)
            throw new MapperException("Field Renderer error. Could not find property {0} on type {1}".Formatted(memberExpression.Member.Name, config.Type.FullName));

        //ME - changed this to work by name because properties on interfaces do not show up as declared types.
        var dataHandler = config.Properties.FirstOrDefault(x => x.PropertyInfo.Name == prop.Name);
        if (dataHandler == null)
        {
            throw new MapperException(
                "Page editting error. Could not find data handler for property {2} {0}.{1}".Formatted(
                prop.DeclaringType, prop.Name, prop.MemberType));
        }

        var renderer = new FieldRenderer 
        {
            Item = scClass,
            FieldName = ((SitecoreFieldConfiguration)dataHandler).FieldName
        };
        return renderer.Render();
    }        
}

 

And I subclasses GlassView

public abstract class GlassView<TModel> : Glass.Mapper.Sc.Web.Mvc.GlassView<TModel>        
{
    public HtmlString RenderField(Expression<Func<TModel, object>> field)
    {
        return new HtmlString(GlassHtml.Field(field, Model));
    }
}

Now if I want to invoke the RenderField pipeline I just need to call that method on my view.

inherits MammothSite.GlassView<MammothSite.Models.ContactUs>
@{
    ViewBag.Title = Model.Title;    
}

<h2>@Model.Title</h2>

<div class="grid_14 alpha">
    <h3>Call us at @RenderField(m => m.GeneralPhone), or email @RenderField(m => m.GeneralEmail) </h3>        
    @Editable(m => m.BodyText)
</div>

Now I've got type safety and it's making use of my Model.

Post a comment

comments powered by Disqus