Proper RenderingModel Error

December 29, 2016 in #Sitecore | | | Share on Google+

I think we've all seen it (at least when you frequently use Glass and View Renderings), the dreaded The model item passed into the dictionary is of type 'Sitecore.Mvc.Presentation.RenderingModel', but this dictionary requires a model item of type 'MyNamespace.Models.MyGlassModel'.
This error can appear for various reasons but what seems to be the most common for me is when a datasource item "disappears" (deleted but left with a broken link, bad content package, bad publish, ...) and Sitecore passes the default RenderingModel instead of the requested item which in turn generates this error.

The problem is, it can be really hard to figure out the rendering causing it. In the log you'll find somewhat of a trace, detailing the view that triggered it and all the views above it (ancestors) but what if you have multiple renderings of the same type on that page? And why make it crash in the first place?

That's why I've created a (very simple) WarnableExecuteRenderer. I know, such a flash name. It actually just wraps around the standard Sitecore.Mvc.Pipelines.Response.RenderRendering and catches any exceptions to see if it's this specific one. If it is we'll just gather some data, log a proper response and return a nice error to the frontend.

Screenshot

Like you can see in the screenshot above, the error message shows us the placeholder, the view (or in case of a controller rendering the Controller/Action) and the datasource as requested (in this case a broken link).

The code itself is quite easy, first we just override the default pipeline and load some settings for the template

public class WarnableExecuteRenderer : Sitecore.Mvc.Pipelines.Response.RenderRendering.ExecuteRenderer  
{
    private readonly string _renderingModelErrorMessage;
    private readonly string _errorHtmlTemplate;
    private readonly bool _renderErrorOnlyWhileEditing;

    public WarnableExecuteRenderer() : base()
    {
        _renderingModelErrorMessage = Sitecore.Configuration.Settings.GetSetting("WarnableExecuteRenderer.ErrorMessage",
            "The model item passed into the dictionary is of type 'Sitecore.Mvc.Presentation.RenderingModel'");
        _errorHtmlTemplate = Sitecore.Configuration.Settings.GetSetting("WarnableExecuteRenderer.HtmlTemplate",
            "<div style=\"padding: 10px; background-color: red;\"><h1>An error occured while trying to render this component.</h1><ul><li>Placeholder: '{0}'</li><li>Rendering: '{1}'</li><li>Datasource: '{2}'</li></ul></div>");
        _renderErrorOnlyWhileEditing =
            Sitecore.Configuration.Settings.GetBoolSetting("WarnableExecuteRenderer.ErrorOnlyWhileEditing", false);
    }

}

and then we execute the base pipeline and catch any errors

protected override bool Render(Renderer renderer, TextWriter writer, RenderRenderingArgs args)  
{
    try
    {
        return base.Render(renderer, writer, args);
    }
    catch (InvalidOperationException invalidOperationException)
    {
        if (!IsRenderingModelError(invalidOperationException)) throw;
        if (args.Rendering == null || args.Rendering.Renderer == null) throw;

        var renderingString = "";
        if (args.Rendering.Renderer is ViewRenderer)
        {
            var rendering = ((ViewRenderer)args.Rendering.Renderer);
            renderingString = rendering.ViewPath;
        }
        else if (args.Rendering.Renderer is ControllerRenderer)
        {
            var rendering = ((ControllerRenderer)args.Rendering.Renderer);
            renderingString = rendering.ControllerName + "." + rendering.ActionName;
        }

        var errorMessage =
            string.Format(
                "Unable to render rendering due to missing datasource. Placeholder: '{0}', Rendering: '{1}', Datasource: '{2}'",
                args.Rendering.Placeholder, renderingString, args.Rendering.DataSource);

        Log.Error(errorMessage, invalidOperationException, GetType());

        if ((_renderErrorOnlyWhileEditing && Sitecore.Context.PageMode.IsExperienceEditorEditing) ||
            !_renderErrorOnlyWhileEditing)
        {
            writer.WriteLine(_errorHtmlTemplate, args.Rendering.Placeholder, renderingString,
                args.Rendering.DataSource);
        }
        else
        {
            throw new Exception(errorMessage, invalidOperationException);
        }

        return true;

    }
}

In order to make sure we only execute our code if it's this specific exception we have this little bit of code

private bool IsRenderingModelError(InvalidOperationException exception)  
{
    if (exception == null) return false;
    if (exception.InnerException == null) return false;
    if (string.IsNullOrWhiteSpace(exception.InnerException.Message)) return false;

    return exception.InnerException.Message.StartsWith(_renderingModelErrorMessage);
}

And then it's just a matter of patching it into our config

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" >  
  <sitecore>

    <pipelines>

      <mvc.renderRendering>
        <processor type="Boondoggle.Sc.Pipelines.Mvc.RenderRendering.WarnableExecuteRenderer, Boondoggle.Sc"
                   patch:instead="processor[@type='Sitecore.Mvc.Pipelines.Response.RenderRendering.ExecuteRenderer, Sitecore.Mvc']" />
      </mvc.renderRendering>

    </pipelines>

  </sitecore>
</configuration>  

That's it, just a small post. Like always, if you have any questions you can find me on the Sitecore Slack!

Github source: https://github.com/fverswijver/Boondoggle.Sc.WarnableExecuteRenderer

December 29, 2016 in #Sitecore | | | Share on Google+