Glorious Hack #97: Wrapping regular ViewResults as JSON for rich client-side processing
I recently had the need on a ASP.NET MVC/js project to wrap the results of a controller action in JSON so the middle-class UI could consume and then do one of:
- Redisplay the current active popup because validation failed (and I wanted to use the built-in MVC validation)
- Close the current popup and refresh a grid on the parent page with the new item
- Display a generic error message page (in the case of something unexpected like an EndOfWorldException being thrown (by who?))
- Display an even nicer error message page with contextual information returned from the controller about the data entity
Logic like this seems to want to occur on the server side – a combination of ActionResult and some logical branching would normally work but in the context of a rich javascript UI and the current context being an active jQuery dialog box that won’t work – the user won’t like a full page refresh, and you can’t tell the javascript which ViewResult you’ve returned of the above cases.
So I came up with the following which is either a glorious hack or just a hack:
- Have the controller return JsonResult in all cases with a instance of a simple wrapper type ValidationResultViewModel
- Have the javascript then decide what to do with the payload based on this information
- Generate payload by running the ViewResult via your controller and making the payload a regular string
So the ValidationResultViewModel class looks like this:
public class ValidationResultViewModel
{
public bool IsValid { get; private set; }
public bool IsError { get; private set; }
public string Payload { get; private set; }
public long EntityId { get; private set; }
public static ValidationResultViewModel Make(bool isValid, bool isError, string payload, long entityId)
{
return new ValidationResultViewModel
{
IsValid = isValid,
IsError = isError,
Payload = payload,
EntityId = entityId
};
}
}
This class is populated by the controller’s action like so:
protected JsonResult PartialViewToValidationJson(string viewName, object model, bool isValid, bool isError, long entityId = 0)
{
string payload = RenderPartialViewToString(viewName, model);
var viewModel = ValidationResultViewModel.Make(isValid, isError, payload, entityId);
return this.Json(viewModel, JsonRequestBehavior.AllowGet);
}
protected string RenderPartialViewToString(string viewName, object model)
{
if (string.IsNullOrEmpty(viewName))
viewName = ControllerContext.RouteData.GetRequiredString("action");
ViewData.Model = model;
using (StringWriter sw = new StringWriter())
{
ViewEngineResult viewResult = ViewEngines.Engines.FindPartialView(ControllerContext, viewName);
ViewContext viewContext = new ViewContext(ControllerContext, viewResult.View, ViewData, TempData, sw);
viewResult.View.Render(viewContext, sw);
return sw.GetStringBuilder().ToString();
}
}
There is a bit of magic here – we are rendering the view as the engine would run it and then transforming its payload back into a string to return to the client. These methods live on a BaseController class and reuse ViewData et al. from the existing context.
This is not a normal approach – ViewResult and the way it works are abstracted to allow unit testing and separation of concerns, etc. We are reusing this to say that we want to force it to render as we want to tradeoff some performance on the server for some magic on the UI.
So now your controller is free to populate it as it sees fit:
[HttpPost]
public virtual ActionResult Edit(SomethingViewModel viewModel)
{
if (!ModelState.IsValid)
return this.PartialViewToValidationJson(MVC.Something.Views.Basics, viewModel, false, false);
try
{
Something.Save(viewModel.ToSomething(), this.User.Username);
var something = Something.GetById(viewModel.SomethingId);
viewModel.Populate(something);
return this.PartialViewToValidationJson(MVC.Something.Views.Basics, viewModel, true, false);
}
catch (Exception e)
{
return this.PartialViewToValidationJson(MVC.Shared.Views.Error, ErrorViewModel.Make(e), false, true);
}
}
Think russian dolls – in your controller action you are essentially calling another controller action, rendering it, then making it a string and returning it as part of your returned result:
-> popup -> js does a $.post to Something/Edit -> using existing Model renders Something/Basic as a string -> makes ValidationViewModel -> returns as JsonResult
The client can then do things like this:
$.ajax({
async: false,
cache: false,
context: form,
type: form.attr('method'),
url: form.attr('action'),
data: form.serialize(),
success: function(data) {
if (data.IsValid) {
successTarget.replaceWith(data.Payload);
ret = true;
}
else if (data.IsError) {
if (data.Payload) {
errorTarget.find("form").replaceWith($(data.Payload));
}
ret = false;
}
else {
// replace only form contents to prevent overriding
// any .dialog or other post load changes to target
if (data.Payload) {
errorTarget.find("form").replaceWith($(data.Payload).find("form"));
}
ret = false;
}
}
});
I actually ended up replacing this with generic callbacks so that you could pass in what the UI should do in the event or an error condition, success, validation failure:
success: function(data) {
if (data.IsValid) {
validationSuccessCallback(data);
}
else if (data.IsError) {
errorCallback(data);
}
else {
validationFailureCallback(data);
}
}
And we have extended it a bit to include other client-side view model type data.
A hack, but one that fits the tradeoffs on the table – sacrifice some performance on the server side and add some complexity for the payoff of a nice wicked fast client UI experience.