Calling ASP.NET MVC Action Methods from JavaScript

asp.net, asp.net mvc, code 0 comments suggest edit

In a recent blog post, I wrote a a controller inspector to demonstrate Controller and Action Descriptors. In this blog post, I apply that knowledge to build something more useful.

One pain point when you write Ajax heavy applications using ASP.NET MVC is managing the URLs that Routing generates on the server. These URLs aren’t accessible from code in a static JavaScript file.

There are techniques to mitigate this:

  1. Generate the URLs in the view and pass them into the JavaScript API. This approach has the drawback that it isn’t unobtrusive and requires some script in the view.
  2. If you prefer the unobtrusive approach, embed the URLs in the HTML in a logical and semantic manner and the script can read them from the DOM.

Both approaches get the job done, but they start to break down when you have a list. For example, suppose I have a page that lists comic books retrieved from the server, each with a link to edit the comic book. Do I then need to generate a URL for each comic on the server and pass it into the script?

That’s not necessarily a bad idea. After all, isn’t that how Hypertext is supposed to work? When you render a set of resources, shouldn’t the response include a navigation URL for each resource?

On the other hand, you might prefer your services to return just comic books and infer the URLs by convention.

How does MvcHaack.Ajax help?

Thinking about this problem led me to build up a quick proof-of-concept prototype based on something David Fowler showed me a long time ago.

The library provides a base controller class, I tentatively named JsonController (I could extend it to support other formats, but I wanted to keep this prototype focused on one common scenario). This class sets up a custom action invoker which does a lot of the work.

With this library in place, a <script> reference pointing to the controller itself generates a jQuery based JavaScript API with methods for calling controller actions.

This API enables passing JSON objects from the client to the server, taking advantage of ASP.NET MVC’s argument model binding.

Perhaps an illustration is in order.

Lets see some codez!

The first step is to write a controller. I’ll start simple and step it up a notch later.

The controller has a single action method that returns an enumeration of anonymous objects. Since I’m inheriting from JsonController, I don’t need to specify an action result return type. I could have returned real objects too, but for the sake of simplicity, I wanted to start here.

public class ComicsController : JsonController {
  public IEnumerable List() {
    return new[] {
      new {Id = 1, Title = "Groo"},
      new {Id = 1, Title = "Batman"},
      new {Id = 1, Title = "Spiderman"}
    };
  }
}

The next step is to make sure I have a route to the controller, and not to the controller’s action. The special invoker I wrote handles action method selection. This prototype lets you use a regular route, but the JsonRoute ensures correctness.

public static void RegisterRoutes(RouteCollection routes) {
  // ... other routes
  routes.Add(new JsonRoute("json/{controller}"));
  // ... other routes ...
}

As a reminder, this second step with the JsonRoute is not required!

With this in place, I can add a script reference to the controller from an HTML page and call methods on it from JavaScript. Let’s do that and display each comic book.

First, I’ll write the HTML markup.

<script src="/json/comics?json"></script>
<script src="/scripts/comicsdemo.js"></script>
<ul id="comics"></ul>

The first script tag references an interesting URL,/json/comics?json. That URL points to the controller (not an action of the controller), but passes in a query string value. This value indicates that the controller descriptor should short circuit the request and generate a JavaScript with methods to call each action of the controller using the same technique I wrote about before.

Here’s an example of the generated script. It’s very short. In fact, most of it is pretty statick. The generated part is the array of actions passed to the $.each block and the URL.

if (typeof $mvc === 'undefined') {
    $mvc = {};
}
$mvc.Comics = [];
$.each(["List","Create","Edit","Delete"], function(action) {
    var action = this;
    $mvc.Comics[this] = function(obj) {
        return $.ajax({
            cache: false,
            dataType: 'json',
            type: 'POST',
            headers: {'x-mvc-action': action},
            data: JSON.stringify(obj),
            contentType: 'application/json; charset=utf-8',
            url: '/json/comics?invoke&action=' + action
        });
    };
});

For those of you big into REST, you’re probably groaning right now with the RPC-ness of this API. It wouldn’t be hard to extend this prototype to take a more RESTful approach. For now, I stuck with this because it more closely matches the conceptual model for ASP.NET MVC out of the box.

Reference the script, and I can now call action methods on the controller from JavaScript. For example, in the following code listing, I call the List action of the ComicsController and append the results to an unordered list. Since I didn’t need to mix client and server code to write this script, I can place it in a static script file, comicsdemo.js.

$(function() {
    $mvc.Comics.List().success(function(data) {
        $.each(data, function() {
            $('#comics').append('<li>' + this.Title + '</li>');
        });

    });
});

One more thing

It’s easy to call a parameter-less action method, but what about an action that takes in a type? Not a problem. To demonstrate, I’ll create a type on the server first.

public class ComicBook {
    public int Id { get; set; }
    public string Title { get; set; }
    public int Issue { get; set; }
}

Great! Now let’s add an action method that accepts a ComicBook as an action method parameter. For demonstration purposes, the method just returns the comic along with a message. The invoker serializes the return value to JSON for you. There is no need to wrap the return value in a JsonResult. The invoker handles that for us.

public object Save(ComicBook book) {
    return new { message = "Saved!", comic = book };
}

I can now call that action method from JavaScript and pass in a a comic book. I just need to pass in an anonymous JavaScript object with the same shape as a ComicBook. For example:

$mvc.Comics.Save({ Title: 'Adventurers', Issue: 123 })
  .success(function(data) {
      alert(data.message + ' Comic: ' + data.comic.Title);
  });

The code results in the alert pop up. This proves I posted a comic book to the server from JavaScript.

Message from webpage
(2)

Get the codez!

Ok, enough talk! If you want to try this out, I have a live demo here. One of the demos shows how this can nicely fit in with KnockoutJS.

If you want to try the code yourself, it’s available in NuGet under the ID MvcHaack.Ajax.

The source code is up at Github. Take a look and let me know what you think. Should we put something like this in ASP.NET MVC 4?

Found a typo or error? Suggest an edit! If accepted, your contribution is listed automatically here.

Comments

avatar

43 responses

  1. Avatar for Dmytrii Nagirniak
    Dmytrii Nagirniak August 18th, 2011

    Congratulations, you just reinvented Backbone, Spine, JavaScriptMVC etc that have it as the core.

  2. Avatar for Samarendra
    Samarendra August 18th, 2011

    Thanks for the post it's helpful

  3. Avatar for Bryan
    Bryan August 18th, 2011

    That looks pretty cool, gonna give it a spin ^^

  4. Avatar for Shiju Varghese
    Shiju Varghese August 18th, 2011

    Really nice POC. Please put something like this in ASP.NET MVC 4.

  5. Avatar for Adam
    Adam August 18th, 2011

    This is really interesting. I think something like this would be good in v4. I've used this (github.com/zowens/ASP.NET-MVC-JavaScript-Routing) before which I've found quite good to get the routes out for Js. Something like this maybe worth a look.
    Cheers

  6. Avatar for ashic
    ashic August 18th, 2011

    Nice idea (albeit not entirely new) but I have one issue. The $mvc is doing too much: controlling urls AND request/response AND forcing jQuery... besides, there may be other uses of the urls other than server requests. You couyld, for example, need to display "details" urls in a list / grid. I think it would be more useful if the $mvc thing just returned the url strings rather than taking over the invoking of the request. (Keeping along the lines of resource discovery and all that.)
    Might even be an idea to have a single routes script where $mvc would be initialized to all urls in the application (for all controllers and actions). A single cached larger js is better than having to load multiple small ones.

  7. Avatar for Andy
    Andy August 18th, 2011

    I agree with @ashic that client side routing would be a great addition, but that it should not be tied to jQuery Ajax. Though I assume that a production version of this code would indeed allow both routing only and full Ajax scenarios.

  8. Avatar for Stacey
    Stacey August 18th, 2011

    Re-invention or not, this is good stuff.

  9. Avatar for Elijah Manor
    Elijah Manor August 18th, 2011

    Phil,
    Nice use of the new Deferred jqXHR to add the callbacks after the initial definition of the $.ajax()
    As a side note...
    "The jqXHR.success(), jqXHR.error(), and jqXHR.complete() callbacks will be deprecated in jQuery 1.8. To prepare your code for their eventual removal, use jqXHR.done(), jqXHR.fail(), and jqXHR.always() instead." http://api.jquery.com/jQuery.ajax/#jqXHR

  10. Avatar for Justin
    Justin August 18th, 2011

    I've been getting into using backbone.js recently and having an api more integrated into MVC would be great.

  11. Avatar for Lee Smith
    Lee Smith August 18th, 2011

    Fantastic! Defiantly worth adding it to version 4.
    I would make this change to the JsonActionInvoker to fall back to default functionality if needed.

    protected override ActionResult CreateActionResult(ControllerContext controllerContext, ActionDescriptor actionDescriptor, object actionReturnValue)
    {
    if (actionDescriptor.ActionName == "Internal::Proxy" || actionDescriptor.ActionName == "Internal::ProxyDefinition")
    {
    return new JsonResult { Data = actionReturnValue, JsonRequestBehavior = JsonRequestBehavior.AllowGet };
    }
    return base.CreateActionResult(controllerContext, actionDescriptor, actionReturnValue);

    }

  12. Avatar for Joey Robichaud
    Joey Robichaud August 18th, 2011

    Instead of generating javascript when called. It should generate the javascript when the controller is saved. That way it can be referenced when writing the rest of your site's js.

  13. Avatar for A Concerned Fan
    A Concerned Fan August 18th, 2011

    "codez"
    Good post but I really find it cringeworthy when you guys start latching on to ancient memes.

  14. Avatar for haacked
    haacked August 19th, 2011

    @Dmytrii maybe it wasn't clear in the post, but part of the problem I solve can't be solved by a client framework alone. What I've done doesn't reinvent spine, nor backbone (I'm not familiar with the other library you mentioned). It complements it.
    @Elijah Thanks!

  15. Avatar for Korayem
    Korayem August 19th, 2011

    It's funny. I wrote a story on Pivotal Tracker to implement the exact functionality only to find the "HAACK" got it "hacked" before me!

  16. Avatar for Sam
    Sam August 21st, 2011

    I really like this. I don't see that it "reinvents" backbone or whatever. This is bridging the gap between client and server in nice, zero effort way.
    FYI
    Deprecation Notice: The jqXHR.success(), jqXHR.error(), and jqXHR.complete() callbacks will be deprecated in jQuery 1.8. To prepare your code for their eventual removal, use jqXHR.done(),
    jqXHR.fail(), and jqXHR.always() instead.

  17. Avatar for Sam
    Sam August 21st, 2011

    Awww... I see Elijah already pointed the depreciation notice :S

  18. Avatar for Tom McKearney
    Tom McKearney August 21st, 2011

    One thing I see as a potential problem here is that it doesn't filter out non-JsonResult Actions.
    So, if you have a Controller that serves up Views AND Json data, which, correct or not, I seem to have in abundance, it adds javascript capabilities for calling for Views, which seems like unintended consequences, no?
    Tom

  19. Avatar for an le
    an le August 23rd, 2011

    Great! thanks so much!

  20. Avatar for mike johnson
    mike johnson August 23rd, 2011

    don't get me wrong. this is a nice demo, but I'd really like MVC 4 to have a way to support html5 server side events or some sort of comet protocol. I really don't see anyone doing that in any major way. No matter what though, I always look to have MVC and ASP.NEt in general make the hard stuff easy, and the easy stuff trivial

  21. Avatar for jalchr
    jalchr August 23rd, 2011

    Those additions are welcome in asp.net mvc 4
    +1 for Comet protocol support or/and Websockets ...

  22. Avatar for Shawn Welch
    Shawn Welch August 23rd, 2011

    Yo, you look like Gilbert Gottfried.

  23. Avatar for Joseph
    Joseph August 24th, 2011

    Looks pretty cool!
    we expect much more from ASP.NET MVC 4 supplies structure to JavaScript-heavy applications by providing models with key-value binding and custom events, collections with a rich API of enumerable functions, views with declarative event handling, and connects it all to your existing application over a RESTful resources

  24. Avatar for Pjotr
    Pjotr August 24th, 2011

    Demos gives Error on page:
    JSON not defined ...

  25. Avatar for haacked
    haacked August 26th, 2011

    @Mike check out SignalR. :)

  26. Avatar for Jess Chadwick
    Jess Chadwick September 8th, 2011

    This is super awesome, and definitely something I'd like to see in MVC 4. The need for this kind of thing is too great, as shown by the fact that I keep seeing the concept of "getActionUrl(actionName)" implemented all too often... always relying on the default "controller/action/id" route and never taking server routing rules into consideration.
    One thing, though, if this were to make it into the framework: attribute or config setting (ala WCF JSON proxies), not a base class...

  27. Avatar for Marc Rubi&#241;o
    Marc Rubi&#241;o September 10th, 2011

    I love it and I have a couple of uses for this. The applications of today is essential for more accurate use of ajax calls from javascript. We must find a way to make most secure calls.

  28. Avatar for joel kurtz
    joel kurtz October 30th, 2011

    I know I'm a little late on this one. But while I like the javascript it's emitting and if we only had to support one format, json, it may look pretty "cool". But how do we handle different formats, make multiple controller that do work on the same object type? Maybe you have ideas on how to handle multiple formats? WCF Web Api has a formatter solution that seems to be on the right track. Anyways, just thinking.

  29. Avatar for Tom McKearney
    Tom McKearney November 22nd, 2011

    I think this is really nice, but I have run into a problem with it.
    It does not seem to pass parameters through normal Model Binding.
    I have yet to dig in, but if I call a method with a JSON object as its parameter, it does not seem to work unless I bypass the nice $mvc.Controller.Action methods.
    I have no idea why, but I know it is rather disappointing :(

  30. Avatar for Shawn Welch
    Shawn Welch January 3rd, 2012

    @Shawn Welch, you look like a Vanilla Ice wannabe.

  31. Avatar for Beriz
    Beriz January 23rd, 2012

    Why not render your javascript files through the razor viewengine.
    No need to render extra javascript client side and full support for @url.content inside your JS files server side...
    I've posted the howto on stackoverflow:
    stackoverflow.com/.../8969711#8969711

  32. Avatar for Nelson Lopes
    Nelson Lopes January 24th, 2012

    Muito bom, ajudou e muito aqui na empresa.
    Parabéns!

  33. Avatar for Kami
    Kami February 14th, 2012

    Did a proof of concept and it was SUPER easy to get working. Great stuff. Thanks.

  34. Avatar for Kami
    Kami February 14th, 2012

    @Tom McKearney I was able to model bind the data, but only if I pasted in a JSON object with the name of the param. So if your action had an MyModel parameter called "model", you'd have to to send in an object like { model: { prop1: 'prop1value' } }

  35. Avatar for Kami
    Kami February 14th, 2012

    @Tom McKearney Sorry, my action param was named "model" and my model also had a property called "model" which was confusing the model binder

  36. Avatar for Tom McKearney
    Tom McKearney March 1st, 2012

    Guess there's no way to make it a synchronous call in the current implementation, huh?

  37. Avatar for Mikhail Tsennykh
    Mikhail Tsennykh March 15th, 2012

    Pretty cool stuff! Thanks Phil! Please add this to MVC4!!!

  38. Avatar for Eduardo Souto
    Eduardo Souto July 31st, 2012

    You ROCK! thanks for this cool code! Please it will be part of MVC4

  39. Avatar for Vijayant Katyal
    Vijayant Katyal October 12th, 2012
  40. Avatar for Vijayant Katyal
    Vijayant Katyal October 12th, 2012

    issue closed , thanks

  41. Avatar for Jim Bancroft
    Jim Bancroft October 19th, 2012

    I'm a little late getting to this, but it looks great. However, is there a similar feature in MVC 4 I should be looking at in lieu of this? Phil mentioned in the article that this is a proof of concept as much as anything. Thanks again for writing about this subject, it's meeting a definite need.

  42. Avatar for zanzamer
    zanzamer November 7th, 2013

    MVC 5 - server side code doesn't pass post body (data) to controller action.

  43. Avatar for Suresh Sundar
    Suresh Sundar November 21st, 2013

    good example