I have been working with the Technical Preview of Sitecore 6.6 on a project and have been for the most part happy with the way that Sitecore (which truly is an MVC implementation unto itself) has been expanded to support ASP.NET MVC. That said, getting up to speed with the combined platform has not been entirely without stumbles and today I want to share one area where Sitecore could have really made things shine from the "it just works" perspective.
A couple days ago I was asked by a colleague about the usage of the "Parameters" field that is defined on Sitecore's Controller Rendering data template. Based on the standard way that Sitecore handles a field named Parameters, I was able to deduce that the field expected key/value pairs separated by the "&" character, but beyond that I wasn't sure and didn't see anything from a documentation perspective to guide me, so it was time to dig and find out where the data in the field was made available. My first thought was that it would be really nice if Sitecore handled the parameters in this field consistently with the way that ASP.NET MVC handles the various parameter collections on the HttpRequest object and automatically maps them to parameters of the action method executing. Being the hopeful sort, I configured a name/value pair on one of my renderings, added a parameter with matching name to the controller action and fired up the bugger to see... that the parameter was not populated.
Having established that the field's value was not going to be presented to me the way that I had hoped it would, the next assumption that I would work on was that Sitecore would handle this field similar to how they handle other similar data and would plug it into some ambient object that I could reference from within the controller method. After a considerable amount of guessing, testing, and cracking code open with Redgate's Reflector (a must-have companion to Sitecore documentation), I found that the most direct way to access the parameter was through the ambient RenderingContext object using code similar to:
string myArgument = string.Empty;
var rc = Sitecore.Mvc.Presentation.RenderingContext.CurrentOrNull;
if (rc != null)
{
var parms = rc.Rendering.Parameters;
myArgument = parms["myArgument"];
}
At this point, we know how this field is used out of the box from Sitecore and can provide information from Sitecore's Content Editor that will be available when the controller action is executing, but it feels a little dirty. In order to properly test the action method I would have to do a lot of setup work and possible use an isolation framework such as Pex and Moles to get at a value that my action method is dependent upon. Notice I said that my method is dependent upon the value but in order to meet that dependency I've accepted another dependency upon Sitecore's RenderingContext. I'm a big believer in, when possible, ensuring that any piece of code explicitly advertises dependencies using the method signature, so I found myself still wanting this to work the same as if the parameters were in the request route, querystring, or form by being able to add a myArgument parameter to the action method and have this parameter populated by the framework. Lucky for us, the ASP.NET MVC framework is extremely flexible and provides some easy to grok and use extensibility points.
ASP.NET MVC is able to provide information from the request as input parameters to controller actions because it uses objects which implement an interface called IValueProvider and have been registered to service the application. The most basic statement of responsibility for an IValueProvider implementation is "I know about some data which is indexed by key. If you hand me the key for a piece of data that I know about I give you that data". When preparing to invoke a controller action, the framework queries registered IValueProvider implementations with the name of each method argument to see if the ValueProvider can supply a value for the parameter.
(the rest of this post will assume you're working along and make a lot more sense if you do)
Let's pull Sitecore out of the equation for a second to simplify things and create an extremely simple IValueProvider implementation. For this example, I first create a new ASP.NET MVC3 project in Visual Studio, selecting "Internet Application" and otherwise taking defaults (I'm assuming that anyone reading this far in the post either already knows how to do this or will need to take a quick run through one of the many available basic MVC tutorials such as the MVC Music Store). Once the new project is created, go to the Index action of HomeController. This action sets a Message property on the ViewBag to "Welcome to ASP.NET MVC!" and invokes the View, which has been coded to display the Message. For our example, we will remove the hard coded message from this controller (although we'll leave it just as hard coded somewhere else - this is sample code).
For the first step in our exercise, add a string parameter to the Index action method called welcomeMessage and use the value of this argument to set the ViewBag.Message property. The updated Index action should look like:
public ActionResult Index(string welcomeMessage)
{
ViewBag.Message = welcomeMessage;
return View();
}
This represents the entirety of the change that you will make to either the controller or view. If you run the application now, the home page will display and no message will be presented to the user because no value was supplied to the Action method. Let's now write a ValueProvider to ensure this parameter gets populated. We'll start by creating a new class called StaticValueProvider. When the class is created, we'll update the using statements to ensure that they include the following:
using System.Collections.Specialized;
using System.Globalization;
using System.Web.Mvc;
With the appropriate using statements in place, we'll update the StaticValueProvider class to implement the IValueProvider interface. The System.Web.Mvc library already contains a pretty flexible dictionary-like implementation called NameValueCollectionValueProvider, so we'll just wrap that and let it do most of the real work for us. The completed class looks like:
public class StaticValueProvider : IValueProvider
{
private NameValueCollectionValueProvider _wrappedProvider;
public StaticValueProvider(ControllerContext controllerContext)
{
var parameters = new NameValueCollection();
parameters.Add("welcomeMessage", "Hello from the value provider!");
_wrappedProvider = new NameValueCollectionValueProvider(parameters, CultureInfo.InvariantCulture);
}
public bool ContainsPrefix(string prefix)
{
return _wrappedProvider.ContainsPrefix(prefix);
}
public ValueProviderResult GetValue(string key)
{
return _wrappedProvider.GetValue(key);
}
}
Notice that the only entry in the collection matches the name of the argument to our HomeController's Index action. This is the important "secret sauce" that will make things work.
We've got our new value provider now, but that's not quite enough to be finished. Mvc obtains IValueProvider instances using factories that are registered when the application starts up. These factories extend the abstract ValueProviderFactory class by initializing and returning the appropriate implementation of IValueProvider from the GetValueProvider method. While I wouldn't do so in production code, for the sake of this example, I'm going to add the following class definition within the StaticValueProvider.cs source file:
public class StaticValueProviderFactory : ValueProviderFactory
{
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
return new StaticValueProvider(controllerContext);
}
}
Now that we have a factory, we can register it by adding the following line to the end of the Application_Start method in Global.asax.cs:
ValueProviderFactories.Factories.Add(new StaticValueProviderFactory());
If you've done everything right to this point, you should be able to run the application and be presented with the home page reading "Hello from the value provider!".
Now that you have the basics of the IValueProvider down, you have everything you need to enhance your Sitecore MVC implementation by adding an IValueProvider that exposes values from the ambient RenderingContext's Parameters property. I'll provide the code for the IValueProvider implementation (which should look VERY familiar) and you can use the work we've already done as a reference to create and register the factory:
public class RenderingContextValueProvider : IValueProvider
{
private NameValueCollectionValueProvider _wrappedProvider = null;
public RenderingContextValueProvider(ControllerContext controllerContext)
{
var collection = new NameValueCollection();
var rc = RenderingContext.CurrentOrNull;
if (rc != null && rc.Rendering != null)
{
foreach(var parameter in rc.Rendering.Parameters)
{
collection.Add(parameter.Key,
parameter.Value);
}
}
_wrappedProvider = new NameValueCollectionValueProvider(collection, CultureInfo.InvariantCulture);
}
public bool ContainsPrefix(string prefix)
{
return _wrappedProvider.ContainsPrefix(prefix);
}
public ValueProviderResult GetValue(string key)
{
return _wrappedProvider.GetValue(key);
}
}
In this post I've discussed the MVC IValueProvider used to map data to controller action method arguments and how this can be integrated into your Sitecore 6.6 MVC solution.