Skip to main content

Microsoft

Sitecore MVC – Use Value Providers to Bind from Sitecore

A few months ago a question popped up on Sitecore Stack Exchange about why parameters passed into the Html.Sitecore().ControllerRendering(string controllerName, string actionName, object parameters) method introduced in Sitecore 8.2 do not bind to the action parameters in the called action method.
I answered why the parameters don’t bind and suggested that if you want this functionality, you would have to create a custom model binder. In fact, it’s a bit simpler than that. In this post I’ll cover what MVC Value Providers are and demonstrate how you can use them in your Sitecore projects by adding in this functionality.

Role of MVC Model Binders

If you’re not familiar with Model Binding in MVC I’ll give a quick overview. Before MVC, if you wanted to access values in the query string or form data, you might write something like this:

public partial class DemoSublayout : UserControl
{
  protected void Page_Load(object sender, EventArgs e)
  {
    int a;
    int.TryParse(Request.QueryString["a"], out a);
    // do something with a
  }
}

You got the QueryString dictionary either by accessing the Request property on the UserControl, or through HttpContext.Current.Request. I still see this in Sitecore MVC projects today:

public class DemoController : Controller
{
  public ActionResult DemoAction()
  {
    int a;
    int.TryParse(Request.QueryString["a"], out a);
    // do something with a
    return View();
  }
}

MVC introduced Model Binders to get rid of this ceremony. Instead, in MVC you can refactor that controller action like this, and it’ll work just the same:

public class DemoController : Controller
{
  public ActionResult DemoAction(int a)
  {
    // do something with a
    return View();
  }
}

When your action executes, the MVC Model Binders look through four parts of the request to populate the action parameters: the query string, form data, request body, and route data. If the name of the action parameter, e.g., a, is found in one of those sources, the value will be parsed into the correct type and the action parameter will be automatically populated. It works for complex types too.

Adding Model Binding Sources

Out of the box, MVC’s Model Binders bind action parameters from four places in the request: the query string, form data, request body, and route data. What if you want MVC to bind from other places, such as cookies, headers, or, as asked on Sitecore Stack Exchange, the current Rendering Context? It’s simple.
You could write your own Model Binder, but you don’t even have to do that. The Model Binders built in to MVC rely on Value Providers to do their work. The current request’s query string, form data, and route data, all exist as individual Value Providers in MVC.
As an example, let’s create a Value Provider for the current rendering’s properties. First create a ValueProviderFactory:

public class RenderingPropertiesValueProviderFactory : ValueProviderFactory
{
  public override IValueProvider GetValueProvider(ControllerContext controllerContext)
  {
    var renderingContext = RenderingContext.CurrentOrNull;
    if (renderingContext == null) return null;
    var renderingProperties = renderingContext.Rendering.Properties.ToDictionary();
    return new DictionaryValueProvider<string>(renderingProperties, CultureInfo.CurrentCulture);
  }
}

It’s not a lot of work. Check that a Rendering Context is actually present; if not, return null so that the MVC Model Binders don’t try to leverage a non-existing source. Then convert the Rendering.Properties Enumerable into a Dictionary and plug it into a new DictionaryValueProvider<string>.
To save you the work of reinventing the wheel, Microsoft has provided a couple of IValueProvider implementations that will serve most needs: DictionaryValueProvider<T> and NameValueCollectionValueProvider. Before you implement your own IValueProvider, see if you can adapt your data source to fit into one of these two types.
Be careful that you add using statements for System.Web.Mvc and not System.Web.Http.ValueProviders. Web API uses Value Provider classes with the same names, but they are not compatible with MVC and vice versa.
Next add your ValueProviderFactory into MVC through a pipeline processor:

public class RegisterValueProviders
{
  public void Process(PipelineArgs args)
  {
    RegisterValueProviderFactories(ValueProviderFactories.Factories);
  }
  protected virtual void RegisterValueProviderFactories(ValueProviderFactoryCollection valueProviderFactories)
  {
    valueProviderFactories.Add(new RenderingPropertiesValueProviderFactory());
  }
}

And register your pipeline processor with Sitecore:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <initialize>
        <processor type="SitecoreDemo.RegisterValueProviders, SitecoreDemo"/>
      </initialize>
    </pipelines>
  </sitecore>
</configuration>

And with that, MVC will use values in the RenderingContext.CurrentOrNull.Rendering.Properties collection when binding action parameters! Now you can pass parameters to your controllers from your views through @Html.Sitecore().ControllerRendering():

@model SampleViewModel
<div>
  @Html.Sitecore().ControllerRendering("Sample", "SampleAction", new { a = 5 })
</div>

And it will be populated:

public class DemoController : Controller
{
  public ActionResult DemoAction(int a)
  {
    // do something with a
    return View(a);
  }
}

This makes for a nice demonstration of MVC Value Providers. However, whether or not you should use Html.Sitecore().ControllerRendering(), is another question entirely, as pointed out by this guitar guy.

Caveat Emptor

You must take care when adding new Value Providers that always have certain values present. For example, if you’re using Controller Renderings, the Rendering.Properties dictionary will always have values present for RenderingType, Controller, and ControllerAction. So if you’re expecting the value RenderingType to be present in the query string, for example, and want your component to behave differently when it’s not, you may be surprised when your renderingType action parameter always has a value.
Additionally, you may have noticed above that the RenderingPropertiesValueProviderFactory was added to the global ValueProviderFactories collection. This means that Sitecore’s MVC controllers will also use your custom Value Provider(s). You must be very careful here, especially if your Value Provider has values that are always present, like this RenderingPropertiesValueProviderFactory. Although you may never use renderingType, controller, or controllerAction as action parameter names, there’s no guarantee that Sitecore isn’t currently or won’t in the future. This could cause unintended consequences in the Content Editor, Experience Editor, or elsewhere.
To be super safe, instead of adding your custom Value Provider(s) to the global ValueProviderFactories collection, you can add them directly to your controller, or better yet, a base controller that all of your controllers inherit from:

public class BaseDemoController : Controller
{
  protected override void Initialize(RequestContext requestContext)
  {
    base.Initialize(requestContext);
    var valueProvider = ValueProvider as ValueProviderCollection;
    valueProvider?.Add(new RenderingPropertiesValueProviderFactory().GetValueProvider(ControllerContext));
  }
}

If you take this approach, you can rest easy knowing that your Value Provider(s) will have no effect on the built-in Sitecore controllers.

Conclusion

It’s easy to extend MVC’s Model Binding facilities with Value Providers. Have you used them on your Sitecore projects? Let me know in the comments.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Corey Smith, Sitecore MVP & Lead Technical Consultant

I'm a Sitecore MVP and Lead Technical Consultant at Perficient focusing on Sitecore and custom .NET development.

More from this Author

Categories
Follow Us
TwitterLinkedinFacebookYoutubeInstagram