Let me set the stage for this post first. Peergroups.be, the sharing economy website I develop for the non-profit WijDelen, is based on ASP.NET MVC. So I have a bunch of Controller
classes with methods that end with return View(model);
.
Our first goal was to have a web application running as quickly as possible. Now that we have a stable system with active users, we’re looking into writing a mobile app. The mobile app can have the same pages/screens as the web application. Looking at the future might point us towards Progressive Web Apps, but that currently doesn’t seem a viable option.
First because PWA’s are still quite new. Not all browsers out there have 100% support for service workers yet, even if that will soon change.
Second, because it would mean a significant rewrite of our current, OrchardCMS-bases web application.
Finally, because, while I feel more than confident in javascript, it’s not my best language. In such a situation, I don’t think it’s OK to let a non-profit pay me for my fun and learning.
So, the stage is set: we have an existing ASP.NET MVC web application, and want a mobile app. I’ve chosen Xamarin as the platform, because C# is what I know best. But how do I get the data from the MVC Controllers to my mobile app? Here’s where we can use a little trick.
In essence, what return View(model)
does, is it puts certain properties of a (view)model into certain parts of a HTML template. What if we could just return the (view)model as JSON, and then use it in our Xamarin app?
Turns out, it is pretty simple. In my controllers, I changed the return statement to:
return this.ViewOrJson(model);
And the ViewOrJson
method is an extension method on an MVC Controller
.
The first thing we want to do, is check if we’re requesting JSON or not. If so, return JSON. If not, do what the return View(model)
would do:
public static ActionResult ViewOrJson(this Controller controller, object model) { if (controller.Request?.AcceptTypes != null && controller.Request.AcceptTypes.Contains("application/json")) { // return JSON } if (model != null) { controller.ViewData.Model = model; } return new ViewResult { ViewData = controller.ViewData, TempData = controller.TempData, ViewEngineCollection = controller.ViewEngineCollection }; }
Now all we need to do is return the JSON. I added an extra step to return a different model if there is an error:
if (!controller.ModelState.IsValid) { controller.Response.StatusCode = (int) HttpStatusCode.BadRequest; var errorModel = new ErrorsModel { Errors = controller.ModelState .Where(x => x.Value.Errors.Any()) .Select(x => new ErrorModel { Field = x.Key, Messages = x.Value.Errors.Select(m => m.ErrorMessage).ToList() }) .ToList() }; return new ContentResult { ContentType = "application/json", Content = JsonConvert.SerializeObject(errorModel, new JsonSerializerSettings {ContractResolver = new CamelCasePropertyNamesContractResolver()}), ContentEncoding = Encoding.UTF8 }; } return new ContentResult { ContentType = "application/json", Content = JsonConvert.SerializeObject(model, new JsonSerializerSettings {ContractResolver = new CamelCasePropertyNamesContractResolver()}), ContentEncoding = Encoding.UTF8 };
This will give us the serialized viewmodel if all is well. But if there are model errors, we will receive a HTTP 400
error with a body like this:
{ "errors": [ { "field": "Description", "messages": [ "Please provide a description of the item you need." ] }, { "field": "Name", "messages": [ "Please provide a name." ] } ] }
Putting it all together:
public static ActionResult ViewOrJson(this Controller controller, object model) { if (controller.Request?.AcceptTypes != null && controller.Request.AcceptTypes.Contains("application/json")) { if (!controller.ModelState.IsValid) { controller.Response.StatusCode = (int) HttpStatusCode.BadRequest; var errorModel = new ErrorsModel { Errors = controller.ModelState.Where(x => x.Value.Errors.Any()).Select(x => new ErrorModel { Field = x.Key, Messages = x.Value.Errors.Select(m => m.ErrorMessage).ToList() }).ToList() }; return new ContentResult { ContentType = "application/json", Content = JsonConvert.SerializeObject(errorModel, new JsonSerializerSettings {ContractResolver = new CamelCasePropertyNamesContractResolver()}), ContentEncoding = Encoding.UTF8 }; } return new ContentResult { ContentType = "application/json", Content = JsonConvert.SerializeObject(model, new JsonSerializerSettings {ContractResolver = new CamelCasePropertyNamesContractResolver()}), ContentEncoding = Encoding.UTF8 }; } if (model != null) controller.ViewData.Model = model; return new ViewResult { ViewData = controller.ViewData, TempData = controller.TempData, ViewEngineCollection = controller.ViewEngineCollection }; }
I realize a separate WebAPI project would be better, and REST is better, but we’re still in a startup-phase, trying to move fast.
This now allows me to keep my Controllers as they were, except for the return statement. All requests from a browser keep working and keep returning HTML, but a request for JSON now returns the (view)model as JSON.