How to get the Rendering Datasource after an AJAX postback in Sitecore MVC
When a controller or view rendering is executed in Sitecore MVC, details about the current rendering are accessed from the RenderingContext
class (Sitecore.Mvc.Presentation.RenderingContext
). Typically in a controller you would use RenderingContext.Current.Rendering.Item
to retrieve the datasource set for that rendering. Not so obvious is that when you do an AJAX post to your controller, you are often bypassing the Sitecore MVC rendering pipeline and therefore you will find that the rendering context is uninitialised in your post action. If you need to access that datasource item again during the post action (e.g. to determine a page to redirect to, or a message to display) then your code will throw a null reference exception.
The quick and dirty solution
One solution to this problem is to simply add a field to your model which can be used to store the ID of the datasource. You would then add a hidden field to your rendering to persist this datasource ID for the postback. In the controller you would then need to parse the ID and retrieve the item from the context database - remember that although the RenderingContext will not be initialised, Sitecore.Context.Database
will be (as usual during a http request).
A clean solution using MVC Model Binding
A nicer solution which I have used involves the following:
- A simple interface type “IFormModel” with a Sitecore.Data.Item property - this will be implemented by any models for which you need the persisted datasource.
- A HtmlHelper extension method which embeds the ID of the datasource item as a hidden field within the form.
- Custom ModelBinderProvider and ModelBinder which detect the type of the model being bound and if it implements our “IFormModel” interface, read the embedded ID and set the ‘Item’ property of the model.
1. The Interface definition
This is simply an interface with a single ‘Item’ property set by your controller code during the initial GET request.
public interface IFormModel
{
Sitecore.Data.Items.Item Item { get; set; }
}
A sample class might look like this:
public class LoginViewModel : IFormModel
{
public string UserName { get; set; }
public string Password { get; set; }
public Item Item { get; set; }
}
2. HtmlHelper extension method
This extension method accepts an object implementing IFormModel
as an argument and returns a hidden field whose value is set to the ID of the Item.
public static MvcHtmlString FormData(this HtmlHelper htmlHelper, IFormModel model)
{
Assert.ArgumentNotNull(model, "model");
Assert.IsNotNull(model.Item, "IFormModel.Item cannot be null");
return htmlHelper.Hidden("__AjaxFormData",model.Item.ID.ToShortID().ToString()));
}
Note:
- If you don’t want to expose Sitecore IDs to the outside world, consider using System.Web.Security.MachineKey.Protect/Unprotect to encrypt and decrypt your ID values.
- You may wish to avoid the magic string antipattern here and move the value “__AjaxFormData” to a custom Sitecore setting in a config include file.
3. Custom ModelBinderProvider / ModelBinder
Model binding in ASP.NET MVC is responsible for the magic which transforms a set of posted form values into the complex type passed to a controller action. The idea here is to piggy-back off the existing model binding, detecting the presence of a model implementing our IFormModel
interface, then to set the Model’s Item
property based on the value of the hidden field created with our custom FormData() extension method. The ModelBinder is as follows:
public class FormModelModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
object model = base.BindModel(controllerContext, bindingContext);
if (typeof (IFormModel).IsAssignableFrom(bindingContext.ModelType))
{
var formModel = model as IFormModel;
Assert.IsNotNull(viewModel, "viewModel must not be null");
var formDataValue = controllerContext.HttpContext.Request["__AjaxFormData"];
ShortID dataSourceId;
if (ShortID.TryParse(formDataValue, out dataSourceId))
{
viewModel.Item = Sitecore.Context.Database.GetItem(dataSourceId.ToID());
}
}
return model;
}
}
When I first started investigating the use of a custom ModelBinder, I found that registering a ModelBinder against an interface definition does not work at runtime (it compiles, but does nothing!). So attempting the following does not work:
ModelBinders.Binders.Add(typeof(IFormModel), new FormModelModelBinder());
This is because the model binding requires an exact type match to work. Then I found this article by Matt Hidinger which explains how to create (and register) a custom ModelBinderProvider. The ModelBinderProvider code is below:
public class FormModelBinderProvider : Dictionary, IModelBinderProvider
{
public IModelBinder GetBinder(Type modelType)
{
var binders = from binder in this
where binder.Key.IsAssignableFrom(modelType)
select binder.Value;
return binders.FirstOrDefault();
}
}
This is registered at app startup, (either in Global.asax.cs or in a related App_Start class) as follows:
ModelBinderProviders.BinderProviders.Add(new FormModelBinderProvider
{
{typeof (IViewModel), new ViewModelModelBinder()}
});
4. Controller usage
Below is an example of how our controller might look with its corresponding Get and Post actions:
[HttpGet]
public PartialViewResult Login()
{
//assign the Item property during the GET request
//where we have a valid RenderingContext
var viewModel = new LoginViewModel
{
Item = RenderingContext.Current.Rendering.Item
};
return PartialView("Login", viewModel);
}
[HttpPost]
public ActionResult Login(LoginViewModel viewModel)
{
//RenderingContext is currently un-initialized
//but viewModel.Item has now been automatically set by our custom ModelBinder
var item = viewModel.Item;
}
Conclusion
This solution will be of use in any solution where AJAX post-backs are used within a Sitecore MVC solution.
If you have any questions or feedback feel free to leave a comment below!