Using a filter to execute a different action?

954 views Asked by At

I want to avoid having lots of if Request.IsAjaxRequest() in my controllers. I was thinking that if I could condense this logic to an ActionFilter it would be easy to adopt a convention in my application to provide a second action for any request that may use Ajax, while providing a fall back if JavaScript is disabled.

public ActionResult Details(int id)
{
  // called normally, show full page
}

public ActionResult Details_Ajax(int id)
{
  // called through ajax, return a partial view
}

I initially thought I could do something like this:

public class AjaxRenameAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        filterContext.RouteData.Values["action"] = filterContext.RouteData.Values["action"] + "_Ajax";

    }

But that won't work because the action to invoke is decided and then the Filters on it are processed.

I don't really want to return a RedirectResult each time someone calls an action, it seems a bit pointless to double the amount of Http requests.

Is there a different way to route through the request to a different action? Or is what I'm doing unadvisable and I should look for a better way of doing things?

Cheers

2

There are 2 answers

0
Aaronaught On BEST ANSWER

AcceptAjaxAttribute is probably is what's needed here; however, I'd like to propose a different way of thinking about this problem.

Not all Ajax requests are equal. An Ajax request could be trying to accomplish any of the following things:

  • Binding JSON data to a rich grid (like jqGrid);
  • Parsing/transforming XML data, such as an RSS feed;
  • Loading partial HTML into an area of the page;
  • Asynchronously loading a script (google.load can do this);
  • Handling a one-way message from the client;
  • And probably a few more that I'm forgetting.

When you "select" a specific "alternate action" based solely on the IsAjaxRequest method, you are tying something very generic - an asynchronous request - to specific functionality on the server. It's ultimately going to make your design more brittle and also make your controller harder to unit test (although there are ways to do it, you can mock the context).

A well-designed action should be consistent, it should only care what the request was for and not how the request was made. One might point to other attributes like AuthorizeAttribute as exceptions, but I would make a distinction for filters, which most of the time describe behaviour that has to happen either a "before" or "after" the action takes place, not "instead of."

Getting to the point here, the goal stated in the question is a good one; you should definitely have different methods for what are correctly described as different actions:

public ActionResult Details(int id)
{
    return View("Details", GetDetails(id));
}

public ActionResult JsonDetails(int id)
{
    return Json(GetDetails(id));
}

public ActionResult PartialDetails(int id)
{
    return PartialView("DetailTable", GetDetails(id));
}

And so on. However, using an Ajax action selector to choose between these methods is following the practice of "graceful degradation", which has essentially been superseded (at least IMO) by progressive enhancement.

This is why, although I love ASP.NET MVC, I mostly eschew the AjaxHelper, because I don't find that it expresses this concept that well; it tries to hide too much from you. Instead of having the concept of an "Ajax Form" or an "Ajax Action", let's do away with the distinction and stick to straight HTML, then inject the Ajax functionality separately once we're certain that the client can handle it.

Here's an example in jQuery - although you can do this in MS AJAX too:

$(function() {
    $("#showdetails").click(function() {
        $("#details").load("PartialDetails", { id: <%= Record.ID %> });
        return false;
    }
});

This is all it takes to inject Ajax into an MVC page. Start with a plain old HTML link and override it with an Ajax call that goes to a different controller action.

Now, if on somewhere else on your site you decide you want to use a grid instead, but don't want to break the pages using partial rendering, you can write something like this (let's say you have a single master-detail page with a list of "orders" on the left side and a detail table on the right):

$(".detaillink").click(function() {
    $('#detailGrid').setGridParam({
        url: $(this).attr("href").replace(/\/order\/details/i,
            "/order/jsondetails")
    }); 
    $("#detailGrid").trigger("reloadGrid");  
});

This approach completely decouples client behaviour from server behaviour. The server is effectively saying to the client: If you want the JSON version, ask for the JSON version, and oh, by the way, here's a script to convert your links if you know how to run it. No action selectors and finagling with method overloads, no special mocking you have to do in order to run a simple test, no confusion over which action does what and when. Just a couple of lines of JavaScript. The controller actions are short and sweet, exactly the way they should be.

This is not the only approach. Obviously, classes such as AcceptAjaxAttribute exist because they expected some developers to use the request-detection method. But after having experimented quite a bit with both, I find this way much easier to reason about and therefore easier to design/code correctly.

5
takepara On

How about AcceptAjaxAttribute in MvcFutures?