AJAX support in the ASP.NET MVC Framework
[Edit: Note that this post is based on a pre-release, and now out of date CTP of the MVC framework. I'd suggest you use it to understand concepts, but look elsewhere now if you're after up to date facts. I've recently added a post about Virtual Earth and the MVC framework that includes the use of some AJAX - read it here]
I’ve been curious about how AJAX support might be added to the ASP.NET MVC Framework. Specifically I could see huge potential in defining portions of pages (views) as User Controls inheriting from System.Web.Mvc.ViewUserControl<T>. This allows the region of the page to be rendered either inline in another view, or on its own, and therefore means that an initial page can be rendered, yet the User Control section can be updated using AJAX calls. What enables this to work is the fact that the Controller.RenderView method in the framework can handle both ViewPage and ViewUserControl derived UI elements.
So I decided to write some code to enable me to play with this. Bear in mind that this is purely flaky experimental code – I’m sure the product group will do a far better job than this sample, but the point of this post is to demonstrate the kind of things you can achieve with the MVC framework, not to produce a product! If you are interested in AJAX support in the framework, I recommend heading over to Nikhil’s blog for a quick read.
This post is actually incidental to the real reason I was writing some code, to demonstrate a different technique – which will be revealed in a future blog post in the not-too-distant future.
As usual the code included conveys no warranties etc, and has been written against the December 2007 CTP – and therefore will likely be superseded or simply won’t work on later releases J.
Anyway, to meet my needs I’ve done a number of things;
1. Created a simple Employee table, and used LINQ-to-SQL to get at the data easily.
2. Written a Controller with two actions – one to render a full initial page, and one to render just the portion of the page contained within the user control.
3. Created two views – one full page (that includes the user control), and one user control.
4. Created an extension method for the AjaxHelper type that renders a link to update a user control’s content.
Initial Setup
The code for the controller is very simple. I’ve created a simple Model entity to hold a list of Employee entities, plus a selected one;
public class EmployeeSet
{
public List<Employee> Employees { get; set; }
public Employee SelectedEmployee { get; set; }
}
There is then an action that renders a whole view, passing in the EmployeeSet entity;
[ControllerAction]
public void ViewPeople()
{
DbDataContext db = new DbDataContext();
List<Employee> all = (from e in db.Employees
select e).ToList<Employee>();
EmployeeSet set = new EmployeeSet();
set.Employees = all;
// by default, show the first employee in the user control
set.SelectedEmployee = all[0];
RenderView("People", set);
}
The key to this working is of course the “People” view, for which the first version looks very simple indeed;
<asp:Content ID="Content1"
ContentPlaceHolderID="MainContentPlaceHolder"
runat="server">
<table>
<tr>
<th>Name</th>
</tr>
<% foreach (Employee emp in ViewData.Employees)
{
%>
<tr>
<td>
<%= emp.Name %>
</td>
</tr>
<%
}
%>
</table>
<div id="Individual">
<prs:PersonInfo ID="PersonInfo1"
runat="server"
ViewDataKey="SelectedEmployee" />
</div>
</asp:Content>
Of course, this ASPX view inherits from ViewPage<EmployeeSet> to enable me to use strongly typed access to the model data. Perhaps the next most significant point is the use of the User Control – note that I have omitted the Register directive in this snippet, but it does need to be there. I specify the name of the property on the View Data that I wish to pass to the User Control in the ViewDataKey property. This ensures that the currently selected Employee is passed to the control.
To allow this to work, I have an ASCX User Control view named PersonInfo that derives from ViewUserControl<Employee>. The content of this view is incredibly simple in my example (UI design is not a focus of this post!);
<%@ Control Language="C#" AutoEventWireup="true"
CodeBehind="PersonInfo.ascx.cs"
Inherits="MvcAjax.Views.AjaxSample.PersonInfo" %>
Name: <%= ViewData.Name %><br />
Job Title: <%= ViewData.JobTitle %>
Browsing to /AjaxSample/ViewPeople will now render a list of all Employees, plus the details of a specific employee (the first one in the list) below the main list.
Adding AJAX
So my next task is to add AJAX that will allow the “Individual” DIV element content that contains the User Control to be replaced with the result of a call to a Controller Action of my choice. I love the lambda expression approach used by much of the MVC framework, so my ideal solution is to replace the “emp.Name” in the list of employees with code similar to that below that outputs a hyperlink to perform my update;
<%= Ajax.UpdateRegionLink<AjaxSampleController>(d => d.UpdatePerson(emp.Id),
"Individual", emp.Name) %>
Here I am specifying the Controller I want to use, the Action I wish to call (and its parameters), the name of the DIV element I wish to update the content of, and the link text (the Employee’s name).
Implementing this boils down to an extension method, which adds functionality to the AjaxHelper class. I came up with the following;
public static string UpdateRegionLink<T>(this AjaxHelper helper,
Expression<Action<T>> action,
string elementId,
string linkText) where T : Controller
{
string linkFormat =
"<a href=\"javascript:updateRegion('{0}', '{1}')\">{2}</a>";
string link = helper.BuildUrlFromExpression<T>(action);
return string.Format(linkFormat, elementId, link, linkText);
}
I then directly copied the BuildUrlFromExpression method from the MVC Toolkit implementation of the HtmlHelper class; in a real framework I assume this functionality would be shared, but this is a hack, not a real deliverable J
Basically all this function does is output HTML that looks like this;
<a href="javascript:updateRegion(
'Individual',
'/AjaxSample/UpdatePerson/3')">{Name}</a>
I love the automatic generation of the URL!
The code that is found at this URL is a very simple Action;
[ControllerAction]
public void UpdatePerson(int id)
{
DbDataContext db = new DbDataContext ();
Employee emp = (from e in db.Employees
where e.Id == id
select e).SingleOrDefault<Employee>();
RenderView("PersonInfo", emp);
}
It simply loads the Employee that matches the specified Identifier, and renders the “PersonInfo” User Control only – and this is the power of splitting up the UI into these parts.
Doh!
So where do I put the javascript definition of the function “updateRegion” that is called by the link generated by my extension method? I need to register it as a client script block... so my first attempt was to pass Page.ClientScript into my extension method, and register the prerequisite javascript there. If that worked, I was going to think about how to get hold of it through the ViewContext... if it is possible.
But it didn’t work. I’ve not gotten to the bottom of why yet, so I may well dedicate more time investigating in the future, but basically it doesn’t look like any client scripts are emitted when using the MVC framework. I guess this is something different about the page lifecycle execution, or perhaps an override in ViewPage.
Either way, my interim solution is to put the script in a “.js” file under the “Content” folder, and import it in the master page;
<script type="text/javascript"
src="/Content/MvcAjaxExtensions.js">
</script>
The Script
The script is quick and dirty, designed to get the job done and that’s about all, as quickly and simply as I could think of! As with all the code in this example, it is nowhere near production strength (</excuse-making>).
var regionId;
var req;
function updateRegionReturn()
{
if(req.readyState == 4)
document.getElementById(regionId).innerHTML = req.responseText;
}
function updateRegion(elementId, url)
{
regionId = elementId;
req = new XMLHttpRequest();
req.onreadystatechange = updateRegionReturn;
req.open('GET', url);
req.send();
}
It simply uses the XMLHttpRequest object to make a request to the supplied URL – which of course was built by my extension method and passed into updateRegion() by the link we generate on the page.
It Works!
So it is a little hack, but the important thing is that the Proof of Concept works – try clicking a link on the People page, and hey presto, a web request is made, the Action is invoked, the User Control is rendered, and the mark-up is updated. I think this shows that AJAX support could feel really natural in the MVC framework – and there’s no reason why many of the standard AJAX functionality couldn’t be rolled in.
This is a very simple example, but I hope it has illustrated how the framework could evolve itself, or be built on to suit your own project.
Comments
Anonymous
January 24, 2008
Misc Items Microformats: A DoD Use Case [Via: Jeff Black ] Misc Items NET Foundations - Memory model...Anonymous
January 27, 2008
One issue I have had on numerous customer engagements is that there has been a good opportunity to useAnonymous
January 27, 2008
One issue I have had on numerous customer engagements is that there has been a good opportunity to useAnonymous
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
March 11, 2008
Hi, my compiler complaines that AjaxHelper doesn't contain BuildUrlFromExpression. Only HtmlHelper does. How did you work around it?Anonymous
March 12, 2008
The comment has been removedAnonymous
March 16, 2008
Hi, null parameter is needed into req.send method, in order to make it work with Mozilla, just for feeedback :) req.send(null); David REIAnonymous
March 16, 2008
David: Good spot :-)Anonymous
June 26, 2008
It is not working for me. I bookmarked the javascripts and it did not hit. Can the update panel be used?Anonymous
June 30, 2008
Hi - this is based on a very old drop of the MVC framework, so is no doubt not applicable anymore. I'd recommend checking out the built in AJAX support in newer releases. Note that the UpdatePanel won't work; it relies on partial page postbacks which is very different to how the MVC rendering engine works (i.e. the postback bit!). Hope that helps SimonAnonymous
July 25, 2008
Listen to the Show! Jess Chadwick describes how to integrate the ASP.NET AJAX framework with ASP.NET