Monday, April 14, 2008

Problems with the MVC Framework RenderComponent helper method

I've just been having a frustrating time attempting to use the new HtmlHelper extension method in the MVC Framework CTP2, RenderComponent. There are so many problems with it that it's really not at all ready for any serious use. Here's a thread on the MVC forum dedicated to some of it's shortcomings:

http://forums.asp.net/t/1240638.aspx

When the MVC Framework was announced, several design goals were given including:

  1. It should allow for unit testing
  2. It should enable the use of IoC containers
  3. It should allow for multiple view engines

All three of these have been broken by RenderComponent. The base component type, ComponentController has one real method, RenderView. Here's what it looks like in Reflector:

public void RenderView(string viewName, object viewData)
{
   if (!viewName.EndsWith(".aspx", StringComparison.OrdinalIgnoreCase))
   {
       viewName = viewName + ".aspx";
   }
   string str = base.GetType().Name.Replace("Controller", "");
   ViewPage handler = (ViewPage) Activator.CreateInstance(BuildManager.GetCompiledType(("~/Components/" + str) + "/Views/" + viewName));
   handler.Url = new UrlHelper(this.Context);
   handler.Html = new HtmlHelper(this.Context);
   handler.SetViewData(viewData);
   StringBuilder sb = new StringBuilder();
   StringWriter writer = new StringWriter(sb, CultureInfo.CurrentCulture);
   HttpContext.Current.Server.Execute(handler, writer, true);
   string str4 = sb.ToString();
   this.RenderedHtml = str4;
}

The first thing to notice is that the RenderView method isn't virtual which means you can't mock it. I got around it with some horrible hackery. I defined an intermediate base class that my controller components use and I've hidden the RenderView method with a new virtual method.

public class ComponentControllerBase : ComponentController
{
   public ComponentControllerBase()
       : base()
   {
   }

   public ComponentControllerBase(ViewContext viewContext)
       : base(viewContext)
   {
   }

   public virtual new void RenderView(string viewName)
   {
       RenderView(viewName, null);
   }

   public virtual new void RenderView(string viewName, object viewData)
   {
       base.RenderView(viewName, viewData);
   }
}

The next thing is that the RenderView code is hard coded to use aspx view pages rather than using the configured view engine. I'm using the aspx view engine anyway, so it's not a killer for me, but even so it's very disappointing to see this.

Lastly, something which I do care about is the lack of a ComponentControllerFactory to allow us to substitute an alternative way of creating components. I am using the Windsor IoC container in my project and I would expect the RenderComponent method to call out to an abstract factory which would allow me to provide the components from Windsor. Luckily for me Jeremy Skinner has provided an alternative RenderComponent method in MVC contrib called RenderComponent2 which does just that. All I needed to do was register my Windsor container with his ComponentControllerBuilder:

// automatically register component controllers
container.Register(AllTypes
   .Of<ComponentController>()
   .FromAssembly(Assembly.GetExecutingAssembly())
   .Configure(c => c.LifeStyle.Transient.Named(c.Implementation.Name.ToLower())));

// set the component controller factory to the windsor container
ComponentControllerBuilder.Current.SetComponentControllerFactory(
   new IoCComponentControllerFactory(new WindsorDependencyResolver(container)));

Note the excellent new windsor mass registration API.

So in conclusion, it's a pretty poor show from the MVC team. I sincerely hope that a better implementation is given in CTP3.

Note: all this hackery is displayed in it's awfulness in my current project, sutekishop, which you can get the source of here:

http://code.google.com/p/sutekishop/

1 comment:

Jeremy said...

Hi Mike,

Credit for MvcContrib's RenderComponent2 should go Tuna Toksöz - he wrote the code, I just applied the patch.

Jeremy