DD4T: Failing it softly

DD4T, Tridion

Once you develop a new template for DD4T, you can also put a Controller and an Action into the Template Metadata. This allows for a nice form of control over your templates. Clean seperation of code and template and really use the power of MVC.

But it is annoying with deployments and your development team. Once you create the template and publish it, you create the Controller, Action and View (and maybe a model) on your local development machine. If the page is published, the rest of your team and your development environment will generate 500 error pages. For deployments it is also annoying if you need to rollback. I've made a custom ComponentPresentationRenderer and adjusted the ComponentController.

ComponentPresentationRenderer

So I've adjusted the DefaultComponentPresentationRenderer to fail softly instead of showing a server error. I copied the original from the DD4T source code here and adjusted the RenderComponentPresentation function like so:

private static MvcHtmlString RenderComponentPresentation(IComponentPresentation cp, HtmlHelper htmlHelper)
{
    string controller = null;
    string action = null;

    string defaultController = ConfigurationHelper.ComponentPresentationController;
    string defaultAction = ConfigurationHelper.ComponentPresentationAction;

    string customController = null;
    string customAction = null;

    Type[] types = System.Reflection.Assembly.GetExecutingAssembly().GetTypes();

    if (cp.ComponentTemplate.MetadataFields != null && cp.ComponentTemplate.MetadataFields.ContainsKey("controller"))
    {
        customController = cp.ComponentTemplate.MetadataFields["controller"].Value;
        Type type = types.Where(t => t.Name == (customController + "Controller")).SingleOrDefault();
        if (type != null)
        {
            controller = customController;
        }
        else
        {
            controller = null;
        }
    }
    else
    {
        controller = defaultController;
    }

    if (cp.ComponentTemplate.MetadataFields != null && cp.ComponentTemplate.MetadataFields.ContainsKey("action"))
    {
        customAction = cp.ComponentTemplate.MetadataFields["action"].Value;
        Type type = types.Where(t => t.Name == (controller + "Controller")).SingleOrDefault();

        if (type != null && type.GetMethod(customAction) != null)
        {
            action = customAction;
        }
        else
        {
            action = null;
        }
    }
    else
    {
        action = defaultAction;
    }

    MvcHtmlString result = MvcHtmlString.Empty;
    if (!String.IsNullOrEmpty(controller) && !String.IsNullOrEmpty(action))
    {
        LoggerService.Debug("about to render component presentation with controller {0} and action {1}", LoggingCategory.Performance, controller, action);
        //return ChildActionExtensions.Action(htmlHelper, action, controller, new { componentPresentation = ((ComponentPresentation)cp) });
        result = htmlHelper.Action(action, controller, new { componentPresentation = ((ComponentPresentation)cp) });
        LoggerService.Debug("finished rendering component presentation with controller {0} and action {1}", LoggingCategory.Performance, controller, action);
        return result;
    }
    else
    {
        if (HttpContext.Current.IsDebuggingEnabled)
        {
            LoggerService.Warning("No controller and action found for {0}.{1} ", LoggingCategory.Controller, customController, customAction);
            result = MvcHtmlString.Create(String.Format("<!-- No controller and action found for {0}.{1} -->", customController, customAction));
        }
    }
    return result;
}

If debugging is enabled it will show a message in a html comment showing the controller with the associated function does not exist.
Don't forget to update your web.config to point to your new renderer.

ComponentController

The adjustment to the ComponentController will require a custom view as well. As the TridionControllerBase forces us to use a ViewResult instead of an ActionResult, we need to return a view.

The controller

protected override ViewResult GetView(IComponentPresentation componentPresentation)
{
    // TODO: define field names in Web.config
    if (!componentPresentation.ComponentTemplate.MetadataFields.ContainsKey("view"))
    {
        throw new ConfigurationException("no view configured for component template " + componentPresentation.ComponentTemplate.Id);
    }

    string viewName = componentPresentation.ComponentTemplate.MetadataFields["view"].Value;

    ViewEngineResult result = ViewEngines.Engines.FindView(ControllerContext, viewName, null);
    if (result.View != null)
    {
        return View(viewName, componentPresentation);
    }
    else
    {
        ViewData["viewName"] = viewName;
        return View("Empty");
    }

}

This code will first check for the existence of the view. Once it does, it will render the view normally. If it doesn't it will render an Empty View with debug information.

The view

@{
    Layout = null;
}
@if (HttpContext.Current.IsDebuggingEnabled)
{
    <text>
    <!-- View @ViewData["viewName"] does not exist. -->
    </text>
}

In a HTML comment it will show a message that the view doesn't exists, but only when debugging is enabled.

This will enable you to rollback safely. Only parts that are new in Tridion will not render. This is no solution to changes made to schemas though, that will take some more programming in your views.

EDIT: Currently DD4T does not allow to inject the ComponentPresentationRenderer, so you have to call it from your views like this:

@Html.RenderComponentPresentations(new ComponentPresentationRenderer())

I've already made an improvement request on the DD4T issue tracker.