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.

Leave a Reply

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

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

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