다음을 통해 공유


jQuery + MVC = Progressive Enhancement

If you write a lot of JavaScript, you really should consider adopting Progressive Enhancement as the standard way that you work.

This is basically whereby you write a web site without script, and then enhance it with script. The result is a site that does not rely on JavaScript (and hence works with accessibility tooling and down level devices that can just read plain HTML, and complements SEO principles) but has had the user experience improved with good quality Unobtrusive JavaScript. It's also a great way to enforce separation of concerns between scripts, mark-up, and styling.

My preferred way to do this in MVC is using jQuery and a very simple Controller setup, so I thought I'd share it here. Download the attached code and have a peek as we walk through this post – I've just created a really simple (incomplete!) solution that allows you to page through results.

I must also call out that the Web Application Guidance from p&p has some examples of Progressive Enhancement in action, and some excellent documentation that discusses how to do it. I'm not going to explain the basics of PE here, so give their docs a read first and then come back here.

 

Partial Views

The key to getting Progressive Enhancement working, specifically when using Partial Rendering (as per this example) is to create your Views first, and then consider where to split them up to suit your Ajax requirements. The split should usually be everything inside a region that you want to replace with content returned during the partial render. In my example, that means I moved everything inside "partialregion" into a new partial view;

The next point to make is that I prefer enhancing a page completely manually – avoiding the use of Ajax.ActionLink or Ajax.BeginForm (one could argue that these use Graceful Degradation, not PE). I find using these helpers makes me design my Views and Actions to suit the Ajax implementation – but with Progressive Enhancement I shouldn't be thinking too much about that initially. In fact, I've noticed that if I focus on the Ajax implementation too early I frequently screw up the non-Ajax version... and that's exactly why Progressive Enhancement is a good thing.

Instead, I used Html.ActionLink and Html.BeginForm, and their equivalents, and then enhance with script.

Controller Setup

The second point to make is how I structure my controller actions. Take a look in HomeController to see what I mean. The fact is I could get away with a single controller action like this;

public ActionResult Index(int? pageNumber)

{

int page = pageNumber.HasValue ? pageNumber.Value : 1;

var people = PersonRepository.GetPeople(page).ToList();

var viewModel = new PageViewModel

{ PageNumber = page, People = people };

if (Request.IsAjaxRequest())

return View("Page", viewModel);

else

return View(viewModel);

}

This action handles both my Ajax and my non-Ajax implementations, and I "enhanced" it by adding the check for Request.IsAjaxRequest.

The problem I have with this is that a complex Action just gets even more complex – and starts to become difficult to maintain. Instead I create an ActionMethodSelectorAttribute named AjaxAttribute – the code is under ~/Infrastructure in the attached. This just tells the MVC framework whether a particular method should respond to a request – according to whether Request.IsAjaxRequest returns true or false in this case. Something similar is in the MvcFutures project at the moment.

The result of this is that I can split my actions in a much more natural way – in fact, when I enhance my controller I just add another action, mark up each according to whether it should handle Ajax requests using my Ajax attribute, and optionally extract the logic they share into a helper method;

[Ajax(false)]

public ActionResult Index(int? pageNumber)

{

int page = pageNumber.HasValue ? pageNumber.Value : 1;

var viewModel = GetPageData(page);

return View(viewModel);

}

[Ajax(true)]

[ActionName("Index")]

public ActionResult Index_Ajax(int pageNumber)

{

var viewModel = GetPageData(pageNumber);

return View("Page", viewModel);

}

private PageViewModel GetPageData(int pageNumber)

{

Extracted logic

}

Note that I also use the ActionName attribute to ensure MVC treats my new Ajax action the same as the original.

The jQuery

Once we've got this in place the jQuery is so easy to put together... all we do is say "when the document is ready, add an event handler to all these links that replaces the default click functionality". In jQuery, that looks a bit like this;

$(document).ready(function() {

$.ajaxSetup({ cache: false });

enhanceLinks();

});

function enhanceLinks() {

$('#partialregion a').click(function(source) {

var target = source.target.toString();

$.ajax(

{

url: target,

success: function(data) {

$('#partialregion').html(data);

enhanceLinks();

},

method: 'get'

});

source.preventDefault();

return false;

});

}

A couple of quick points to note on this implementation;

1. I disabled caching so that the browser doesn't cache my results.

2. I used "source.preventDefault" to stop the link click from being processed normally.

3. When we've done the partial render, we also call enhanceLinks again, as the rendered content will include new unenhanced links!

Easy huh?

* Note that I could also have used jQuery's "load" function instead of $.ajax but I didn't because another post will build on this code, and load wouldn't have been appropriate for that one!

Summary

So to summarise, in this example the flow for implementing Progressive Enhancement is as follows;

1. Write the View without script, using Html.ActionLink etc to create plain HTML

2. Split out partial rendering regions into partial views

3. Add an Action overload to respond to Ajax requests

4. Extract the common logic from your two Actions into a private helper

5. Add [Ajax(true)] and [Ajax(false)] to the Actions accordingly

6. Add [ActionName] to your Ajax Action implementation

7. Write a bit of jQuery!

If you're doing something other than partial rendering (perhaps hitting a Json endpoint for example) then this might change a little, but I'll leave that to you. Whatever you do, definitely consider Progressive Enhancement as a valuable approach, and go read the p&p guidance!

I hope that's useful.

[Edit] There's the follow-up post I referred to online now here.

 

ProgressiveEnhancement.zip

Comments

  • Anonymous
    April 21, 2010
    Any reason you didn't use live('click', function()...) to bind the click on the anchors? that way you wouldn't have to call enhanceLinks when you get the results back... I think :)

  • Anonymous
    April 21, 2010
    @ Jaime; no good reason, no - so great spot! Simon

  • Anonymous
    April 22, 2010
    Good read; thanks. A parting tip: int page = pageNumber.GetValueOrDefault(1);

  • Anonymous
    April 10, 2011
    @googly I quite like the coalescing operator too... int page = pageNumber ?? 1

  • Anonymous
    July 08, 2011
    <add key="UnobtrusiveJavaScriptEnabled" value="true"/>

  • Anonymous
    July 10, 2011
    @unobtrusive I think you're over simplifying LOL :) First, this post predates MVC 3. Second, the unobtrusive behaviour in MVC 3 is a great example of the concepts I refer to here, but it doesn't cover everything... so an understanding of the principles is still essential. Simon