code blog foo - tag line bar

Ajax.BeginForm vs $.Post

click here to download source

About 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

jQuery $.post Pros/Cons

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