Ajax.BeginForm vs $.Post
click here to download sourceAbout the Source
This was written using the MVC 1.0 project template (I will update this to MVC 2.0 shortly). If you have difficulty opening the project in Visual Studio, chances are you have ASP.NET MVC 2.0 installed. Refer to this link for instructions on how to change over the project: Convert an MVC 1.0 Project to MVC 2.0.
Also in MVC 2, returning JSON via an HTTP/GET Controller action will throw an exception if you do:
public ActionResult Search(string searchString)
{
//populate search results
List<KeyValuePair<string,string>> searchResults = ...;
//this will throw an exception in ASP.NET MVC 2
return Json(searchResults);
}
Again, the code above will throw an exception and must be changed to the following if you plan on upgrading the source code for this post to ASP.NET MVC 2:
public ActionResult Search(string searchString)
{
//populate search results
List<KeyValuePair<string,string>> searchResults = ...;
//this will fix the exception
return Json(searchResults, JsonRequestBehavior.AllowGet);
}
ASP.NET MVC 1.0 and 2.0 have this really interesting HtmlHelper (or AjaxHelper) called Ajax.BeginForm. At first glance, this looks like a lot of voodoo...and it kinda is. But let's see if we can bring light to what actually occurs when you use Ajax.BeginForm and how it compares to using good ol' jQuery and DHTML.
Cutting to the Chase
I really like jQuery and it's a light weight, right against the metal, yet so simple approach to async communication...and I use it heavily in my ASP.NET MVC development. But, Ajax.BeginForm is definitely something that you shouldn't pass up. You can call Ajax.BeginForm the UpdatePanel for ASP.NET MVC (except that it doesn't suck). It's not as light weight with they async communication like jQuery is, but being able to use strongly typed partial views is extremely valuable (to reiterate: extremely valuable). For full blown explanation, read on. If you want to just get right into the code, just use the link at the top of this post.
Scenario
In this scenario, we are going to save a customer asynchronously and replace the editable form with a read-only representation. The source code is available for download at the top of this post by the way...
This editable form:
Will be converted into a readonly Customer when the save button is clicked:
Ajax.BeginForm Version
Step 1: You need to reference MicrosoftAjax.js and MicrosoftMvcAjax.js.
<!-- consider using Url.Content("") as opposed to ../ relative paths -->
<script src="../../Scripts/MicrosoftAjax.js" type="text/javascript"></script>
<script src="../../Scripts/MicrosoftMvcAjax.js" type="text/javascript"></script>
Step 2: Create a partial view that represents the readonly version of the customer.
<!-- this partial view is named CustomerReadOnly.ascx --> <%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Customer>" %> <%@ Import Namespace="AjaxBeginForm.Models" %> Id: <%= ViewData.Model.Id.ToString() %><br /> First Name: <%= ViewData.Model.FirstName %><br /> Last Name: <%= ViewData.Model.LastName %><br /> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis erat.<br />
Step 3: Create a controller action that saves the customer and returns the partial view.
[ActionName("Customer")]
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Customer(Customer customer)
{
//save customer to database
//then return partial view
return PartialView("CustomerReadOnly", customer);
}
Step 4: Create the form page for saving the customer (leveraging Ajax.BeginForm).
<%
string saveCustomerUrl = Url.RouteUrl(new { controller = "Home", action = "Customer" });
%>
<div id="divCustomer">
<% using (Ajax.BeginForm("Customer", //controller action name
"Home", //controller name
new AjaxOptions //ajax options that tell mvc how to perform the replacement
{
UpdateTargetId = "divCustomer", //id of div to update
InsertionMode = InsertionMode.Replace, //how the div will be updated
HttpMethod = "Post" //how to call the controller action
}))
{ %>
<!-- Id for customer -->
<input type="hidden" value="<%= Guid.NewGuid().ToString() %>" name="Id" />
<!-- First Name for customer -->
First Name:
<input type="text" name="FirstName" /><br />
<!-- Last Name for customer -->
Last Name
<input type="text" name="LastName" /><br />
<!-- submit button -->
<input type="submit" value="save" />
<% } %>
</div>
Fiddler Output for Ajax.BeginForm
Here is the request and response when clicking the save button.
Request: Id=1a0358e8-8931-4b0e-a7b9-3df910729fc2 &FirstName=John&LastName=Doe &X-Requested-With=XMLHttpRequest Response: Id: 1a0358e8-8931-4b0e-a7b9-3df910729fc2<br/> First Name: John<br /> Last Name: Doe<br /> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis erat.
Html Output for Ajax.BeginForm
Here is the html that get's generated with Ajax.BeginForm.
<div id="divCustomer">
<form action="/Home/Customer"
method="post"
onsubmit="Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, httpMethod: 'Post', updateTargetId: 'divCustomer' });">
<input type="hidden" value="1a0358e8-8931-4b0e-a7b9-3df910729fc2" name="Id" />
First Name: <input type="text" name="FirstName" /><br />
Last Name <input type="text" name="LastName" /><br />
<input type="submit" value="go" />
</form>
</div>
Keep in mind that more html is returned in the asynchronous response.
jQuery and DHTML Version
Now let's try the same thing using good ol' jQuery HTML.
Step 1: You need to reference jQuery.
<script src="../../Scripts/jquery-1.3.2.js" type="text/javascript"></script>
Step 2: Create a controller action that saves the customer and returns a JSONResult.
public ActionResult Customer(Customer customer)
{
//save to the database
//if you are using MVC 1.0 you would do this:
return Json(customer);
//if you are using MVC 2.0 you would do this:
return Json(customer, JsonRequestBehavior.AllowGet);
}
Step 3: Create the view page to call the SaveCustomer javascript method.
<!-- the form -->
<div id="divCustomer">
<input type="hidden" value="<%= Guid.NewGuid().ToString() %>" id="id" />
First Name: <input type="text" id="firstName" /><br />
Last Name <input type="text" id="lastName" /><br />
<!-- the submit button will call the SaveCustomer() javascript method -->
<input type="submit" value="save" onclick="javascript:SaveCustomer();" />
</div>
Step 4: Create the javascript code to call the controller action.
<%
string saveCustomerUrl = Url.RouteUrl(new { controller = "Home", action = "Customer" });
%>
<script type="text/javascript">
function SaveCustomer() {
$.post(
//controller action to call
'<%= saveCustomerUrl %>',
//post parameters
{ id: $('#id').val(),
firstName: $('#firstName').val(),
lastName: $('#lastName').val() },
//function call when the save method returns successfully
function(data) {
//replace the div with the following html
$('#divCustomer').html('Id: ' +
$('#id').val() +
'<br/>' +
'First Name: ' +
$('#firstName').val() +
'<br/>' +
'Last Name: ' +
$('#lastName').val() +
'<br/>' +
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis erat.<br />');
},
//data parameter will be a JSON serialization
'json');
return false;
}
</script>
Fiddler Output for $.post
Here is the request and response when clicking the save button.
Request:
id=c7b2e44a-8042-4812-9a1f-5de288846046&firstName=John&lastName=Doe
Response:
{"Id":"c7b2e44a-8042-4812-9a1f-5de288846046","FirstName":"John","LastName":"Doe"}
Html Output for $.post
The html is verbatim from Step 3 and Step 4.
Analysis....
So you've seen both approaches to saving a customer...now for the pros and cons...
Ajax.BeginForm Pros/Cons
- Pro: We have a strongly typed partial view at our disposal...as opposed to having to do string concatenation in javascript for the readonly version of the customer (this is a really good thing btw).
- Pro: All the javascript for performing the asynchronous call is generated automagically for us.
- Con: We are required to have a submit button in our form that must be clicked (by the user or through javascript), otherwise this approach will not perform the async action.
- Con: The html payload from the partial view will be returned every single time the asynchronous postback is executed.
- Con: It may smell bad to some that a controller is returning a partial view...
jQuery $.post Pros/Cons
- Pro: It's super light (take a look at the payload from fiddler).
- Pro: Fine grained control over how things get updated and what goes over the wire and when.
- Con: We have to do our own javascript. Nothing is automagically generated for us.
- Con: We have to do string concatenations for the html as opposed to relying on a strongly typed view (this is a really bad thing btw).
Getting Around String Concatenation in Javascript Using Microtemplating
One of the major cons with using $.post is that we'll have to perform string concatenations for any html we want to generate (either that or we manually have to append items to the DOM..which isn't fun either). The solution is to create a microtemplate that can be applied to the html:
Here is the save customer form with a microtemplate:
<div id="divCustomer">
<input type="hidden" value="<%= Guid.NewGuid().ToString() %>" id="id" />
First Name: <input type="text" id="firstName" /><br />
Last Name <input type="text" id="lastName" /><br />
<input type="submit" value="go" onclick="javascript:SaveCustomer();" />
</div>
<!--
The $Id, $FirstName, and $LastName are the microtemplate place holders for the Id, FirstName, LastName of the customer
-->
<div id="customerMicroTemplate" class="microtemplate">
Id: $Id<br />
First Name: $FirstName<br />
Last Name: $LastName<br />
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis erat.<br />
</div>
And here is the new and improved javascript code that takes advantage of the microtemplate.
<script type="text/javascript">
$(InitializeComponents);
function InitializeComponents() {
//register all of the micro templates on the page
$('.microtemplate').microtemplate('register');
}
function SaveCustomer() {
$.post(
//uri to post to
'<%= saveCustomerUrl %>',
//parameters to send
{ id: $('#id').val(),
firstName: $('#firstName').val(),
lastName: $('#lastName').val() },
//method to call when JSON object representing the customer is returned
function(data) {
//apply the micro template given a key and the object
$('#divCustomer').microtemplate('apply', 'customerMicroTemplate', data);
},
'json');
return false;
}
</script>
Given microtemplating, we don't have to do any kind of string concatenations and at the same time we can take advantage of the lighter payload that goes over the wire. woot.
Microtemplate jQuery Plug-In Source Code
Here is the source code (the really really crude source code) for the microtemplate jquery plugin I conjured up. All this is available as a download at the top of this post.
(function($) {
$.fn.microtemplate = function(args, templateKey, data) {
if (jQuery.templates == null) {
jQuery.templates = new Array();
}
if (args == 'register') {
this.each(function() {
jQuery.templates[$(this).attr('id')] = $(this).html();
$(this).hide();
});
}
else if (args == 'apply') {
this.each(function() {
var templateFormat = jQuery.templates[templateKey];
var properties = templateFormat.match(/[$]\w*/g);
var finalHtml = "";
var rowToWrite = templateFormat; //take the template
var dataItem = data;
for (j = 0; j < properties.length; j++) {
var propertyName = properties[j];
if (propertyName.length != 0) {
var propertyWithoutPrefix = propertyName.replace('$', '');
var propertyValue = dataItem[propertyWithoutPrefix];
if (dataItem[propertyWithoutPrefix] != null) {
rowToWrite = rowToWrite.replace(propertyName, propertyValue);
}
}
}
finalHtml += rowToWrite;
$(this).html(finalHtml);
});
}
return this;
};
})(jQuery);
Written: 4/8/2010