To AJAX, or not to AJAX - rendering in ASP.NET MVC
One issue I have had on numerous customer engagements is that there has been a good opportunity to use AJAX functionality, but I need to support a significant user base that cannot use Javascript. Typically this is to meet accessibility requirements (and laws), and the users in question tend to be using screen readers or similar software. Ensuring a good experience for both of these groups of users is essential.
There are ways to detect browser settings, but a) sometimes these do not give an accurate indication as to the user’s preference, depending on the organisation’s desktop configuration, and b) I want to use this to demonstrate something! So how could we use the ASP.NET MVC framework to make our life easier?
First of all, read my thoughts on “Ajax support in ASP.NET MVC”, as I’ll be using the example to demonstrate my approach. Now let’s define what we need to be able to do.
1. Detect whether the user wishes to enable AJAX functionality
2. If the user will permit AJAX, use javascript to update the employee details user control contents with a partial page rendering call to the server.
3. If the user will not permit AJAX, re-render the entire page, but selecting the correct Employee (rather than defaulting to the first in the list as my original demo does).
Detecting preference from the URL
I decided to use Routing to determine which version of a page a user wants to view. I chose the following two URL schemes;
https://mysite/ajax/Demo/ViewPeople
https://mysite/html/Demo/ViewPeople
When “ajax” is in the URL, we will enable javascript functionality, and when “html” is in its place, we will disable javascript functionality. I think this is a really clean way of directing requests, and letting users bookmark different versions of the site – even though, as we will see, they could be running the same code base.
The key to achieving this is to add new route definitions to the top of the Application_Start event in Global.asax. My first thought was to do something as follows;
RouteTable.Routes.Add(new Route
{
Url = "ajax/[controller]/[action]/[id]",
Defaults = new { action="Index", id=(int?)null, ajax="Yes" },
RouteHandler = typeof(MvcRouteHandler)
});
RouteTable.Routes.Add(new Route
{
Url = "html/[controller]/[action]/[id]",
Defaults = new { action="Index", id=(int?)null, ajax="No" },
RouteHandler = typeof(MvcRouteHandler)
});
The key here is that nowhere in the URL is the “ajax” member found, but I can declare it with a default and if that route is matched, “ajax” will receive a value of Yes or No and be passed into the destination Action (either as a named method parameter, or as an entry in the RouteData.Values collection).
[One important point to note is that I would have liked to have defined the “ajax” as a Boolean, but I believe the CTP handles Booleans incorrectly, so I’ve resorted to a string for now.]
This is a useful technique, but in this case we can do something even simpler;
RouteTable.Routes.Add(new Route
{
Url = "[mode]/[controller]/[action]/[id]",
Defaults = new { action = "Index", id = (int?)null },
Validation = new { mode = "^(ajax|html)$" },
RouteHandler = typeof(MvcRouteHandler)
});
This route will match requests that start “/ajax/” or “/html/”, and then do the usual Controller and Action matching. The value of this “mode” parameter will be passed to the Action in the same way as the “ajax” parameter in the option above. Key to this approach is the validation line – which prevents “/madeupvalue/Controller/Action/1” from matching this route – the mode must be “ajax” or “html”.
Note that I have also applied the “mode” parameter to the “Default.aspx” rule. This means that on entering the site the user will always have to specify which version they require, and that will be passed on throughout all the links as the default. There are other ways to work this – such as a menu page prompting the user, but I chose the simplest example.
Creating a dual purpose entry point
For my approach, we must now create an Action that can render the whole page, including the user control, either on a user’s first visit (and so defaulting to the first Employee in the list) or when a non-AJAX user requests details on a specific Employee. I’ve come up with the following (refer to my previous AJAX post for the original code);
[ControllerAction]
public void ViewPeople(int? id, string mode)
{
DbDataContext db = new DbDataContext();
List<Employee> all =
(from e in db.Employees select e).ToList<Employee>();
EmployeeSet set = new EmployeeSet();
set.Employees = all;
if (id.HasValue)
{
set.SelectedEmployee = (from e in db.Employees
where e.Id == id.Value
select e).SingleOrDefault<Employee>();
}
else
{
// default to first employee
set.SelectedEmployee = all[0];
}
set.EnableAjax = (String.Compare(mode, "ajax", true) == 0);
RenderView("People", set);
}
The key here is that the “id” parameter is nullable. Therefore, if an “id” is specified, the first Employee is used as the selected one, otherwise the Employee who’s Identifier is specified is used. There we go – one dual purpose Action!
Note also that the “mode” parameter is used to set a new property on the EmployeeSet Model class indicating whether AJAX is enabled or not – we will see this in action in the view later.
No change is needed to the UpdatePerson action, as this will always be used by the AJAX-enabled view.
Tweaking the View
The last step is to update our view to behave differently according to the user’s preference. We’ve already passed this preference into the view using the model data, so it should be pretty straightforward. All we need to do is conditionally replace our AJAX link builder with a fixed HTML link to our new dual-purpose view.
Trimming out the irrelevant bits and bobs, my new view looks like this;
<table>
<tr>
<th>Name</th>
</tr>
<% foreach (Employee emp in ViewData.Employees)
{
%>
<tr>
<td>
<% if (ViewData.EnableAjax)
{ %>
<%= Ajax.UpdateRegionLink<AjaxSampleController>(d => d.UpdatePerson(emp.Id), "Individual", emp.Name) %>
<%}
else
{ %>
<%= Html.ActionLink(emp.Name, new { action = "ViewPeople", id = emp.Id })%>
<% } %>
</td>
</tr>
<%
} %>
<tr>
</tr>
</table>
<div id="Individual">
<prs:PersonInfo runat="server" ViewDataKey="SelectedEmployee" />
</div>
You’ll notice that I’m using the Html.ActionLink method – and also that I have not specified the mode of “html” in the parameters; this is because if we’re viewing the HTML version, it will default to the current request’s value for us.
And that’s it – we now have a view that will work for both javascript and non-javascript users, governed by their choice of URL, with only one code base to maintain. I’m sure this could be greatly improved with some tidying and tweaking, but I hope this has illustrated how powerful Routing can be when used in conjunction with the rest of the framework.
Other Approaches
It is worth pointing out that we could do this in many ways – we could have two separate Views (“PeopleHtml” and “PeopleAjax” for example), and write our own IViewFactory, which gets plugged in by a custom IControllerFactory... and I’m sure there are many other ways. I love how flexible this framework is – there always seems to be a couple of options.
Comments
Anonymous
February 03, 2008
PingBack from http://www.andreas-schlapsi.at/2008/02/04/links-zu-aspnet-mvc/Anonymous
February 04, 2008
In my previous two posts ( one and two ) discussing the use of AJAX within an ASP.NET MVC Framework application,Anonymous
February 04, 2008
In my previous two posts ( one and two ) discussing the use of AJAX within an ASP.NET MVC Framework applicationAnonymous
February 06, 2008
The correct way to do it is to have one URI space for your site and add every layer above plain, static, old, boring HTML such as JavaScript, CSS and Ajax in an unobtrusive, gracefully degrading way. If you need to create your web application twice to make it work with and without JavaScript simultaneously, then I suggest you keep your hands off JavaScript entirely. There are several Ajax libraries that make this "progressive enhancement" pretty easy. jQuery is one of them, and probably even the best. With it you can easilly select which nodes in the Document Object Model you want to enhance with JavaScript functionality and add it with ease. This makes the application not only much easier for everyone to use (both with and without JavaScript), but also to maintain because there's only one code path through your entire application.Anonymous
February 06, 2008
The comment has been removed