Friday, March 4, 2011

Creating an Ajax Action Method Selector With ASP.NET MVC 3

I was perusing through the nerd dinner source code when I encroached upon a legitimate opportunity to implement my own custom Action Method Selectors. Action Method Selectors are a component of the Action Invoker, which is responsible for finding a valid action for the request's target controller. Some examples of native action invokers implemented in MVC and HttpPost and HttpGet. Check out this great post explaining some of the core extensibility points provided by the ASP.NET MVC framework.

Paste the following into a new controller. If you're using the default solution provided by MVC, just decorate your Index method of the HomeController with the HttpGet Action Method Selector.

[HttpGet]
public ActionResult Index() {
   // do something for an HTTP GET request.
}

Using intellisense, we see that the HttpGet selector instructs MVC to only execute your controller action for get requests sent over HTTP. If you try sending a post request to the Index controller, it will obviously fail.

Now that we've seen an example of a selector, let's explore my motive to create my own custom selectors for AJAX requests.

I'd wanted to implement these selectors for quite some time now, but never found a legitimate reason to do so until checking out the nerd dinner source code. There was a snippet in the AuthController that threw an exception if the request wasn't sent via AJAX over the OpenID protocol. That was all I needed.

Creating the Action Selectors












First thing's first; the action selectors. We're creating one for our 4 expected verbs: GET, POST, PUT and DELETE.

using System;
using System.Reflection;
using System.Web.Mvc;

namespace Adubb.Web.ActionSelectors {
    /// 
    /// Represents an attribute that is used to restrict an action method so that the method handles only AJAX GET requests.
    /// 
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public sealed class AjaxGetAttribute : ActionMethodSelectorAttribute {
        public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) {
            return controllerContext.HttpContext.Request.IsAjaxRequest() && controllerContext.HttpContext.Request.HttpMethod == "GET";
        }
    }

    /// 
    /// Represents an attribute that is used to restrict an action method so that the method handles only AJAX POST requests.
    /// 
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public sealed class AjaxPostAttribute : ActionMethodSelectorAttribute {
        public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) {
            return controllerContext.HttpContext.Request.IsAjaxRequest() && controllerContext.HttpContext.Request.HttpMethod == "POST";
        }
    }

    /// 
    /// Represents an attribute that is used to restrict an action method so that the method handles only AJAX PUT requests.
    /// 
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public sealed class AjaxPutAttribute : ActionMethodSelectorAttribute {
        public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) {
            return controllerContext.HttpContext.Request.IsAjaxRequest() && controllerContext.HttpContext.Request.HttpMethod == "PUT";
        }
    }

    /// 
    /// Represents an attribute that is used to restrict an action method so that the method handles only AJAX DELETE requests.
    /// 
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public sealed class AjaxDeleteAttribute : ActionMethodSelectorAttribute {
        public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) {
            return controllerContext.HttpContext.Request.IsAjaxRequest() && controllerContext.HttpContext.Request.HttpMethod == "DELETE";
        }
    }
}

Creating the Controller














If you haven't already, create a new ASP.NET MVC 3 Web Application.

Inside the HomeController, insert the following

public class HomeController : Controller {
    public ActionResult Index() {
        ViewBag.Message = "Welcome to ASP.NET MVC!";

        return View();
    }

    public ActionResult About() {
        return View();
    }

    [AjaxGet]
    public ActionResult AjaxGet() {
        return Content("ajax get succeeded. a-dubb is a genius!!");
    }

    [AjaxPost]
    public ActionResult AjaxPost() {
        return Content("ajax post succeeded. a-dubb is a genius!!");
    }

    [AjaxPut]
    public ActionResult AjaxPut() {
        return Content("ajax put succeeded. a-dubb is a genius!!");
    }

    [AjaxDelete]
    public ActionResult AjaxDelete() {
        return Content("ajax delete succeeded. a-dubb is a genius!!");
    }
}

We've simply added an action for each type of verb we expect to be sent down by the client. We have one for GET, POST, PUT, and DELETE request. Pretty straightforward.

How do we Know it Works?

We'll verify that later, but for now try hitting /Home/AjaxGet in your browser

You should see a 404.
















This is because MVC couldn't find a controller that could handle the request. That's a good start, but our goal here is to see it working with AJAX.

Creating the View















The view is simple. It's composed of a button that can initiate each type of request.

Inside About.cshtml insert the following

@{
    ViewBag.Title = "Testing A-Dubb's Extensions Client Side";
}

About

Hey there friends. We're going to write a little test page here.


Your page should look something like


















Adding AJAX

The last thing we need to do is spice up the client with a bit of AJAX. Insert the following into the bottom of About.cshtml.

var mappings = [{ 'GET' : '/Home/AjaxGet' }, { 'POST' : '/Home/AjaxPost'}, { 'PUT' : '/Home/AjaxPut' }, { 'DELETE' : '/Home/AjaxDelete' }];

$(function () {
    $('.ajax-test button')
    .each(function (index, value) {
        var verb = $(value).text();
        var url = mappings[index][verb];

        $(value).click(function () {
                execAjax(verb, url);
        });
    });
});

function execAjax(verb, url) {
    $.ajax({
        url: url,
        type: verb,
        dataType: 'text',
        success: function (data) {
            alert(data);
        }
    });
}

After clicking GET















First we created our mappings array. You can think of it as a dictionary where the verb is the key and the controller url is the value. Next we want to find all of our test buttons using the .ajax-test button selector. We iterate over each button, find the corresponding key value pair in our mappings array, and use the text of the button to retrieve the controller url. The syntax may look a little funny, but it's perfectly legite javascript. Object properties can be accessed using object.propertyname or object[propertyname]. It's just one of those things you come to know after writing javascript for a while. I thought about using JSLINQ to query my array for the right value, which would have been a little more intuitive, but thanks to the flexibility of javascript it all worked out.


And that's it!!

We've implemented a custom Action Method Selector. Feels pretty good eh? Cheers!!