1.13. Implementing Ajax to Enhance the User Experience

When you click a link and the full web page is reloaded with the updated content, this can feel like a slow process, especially when only a small amount of the content is being updated.

Update previously created Html.ActionLink calls to use the Ajax helper and the Ajax.ActionLink to only reload the content being changed.

MVC provides several great helper classes. So far throughout this book, the HTML helper class has been used extensively. In all of the views created, it was used at least once in each of them. In this recipe, the HTML helper class will be swapped out in the Books/Index view and replaced with the Ajax helper class.

Implementing Ajax requires a bit of additional setup before it can be used. Oftentimes I have found that this additional work can deter developers from using it. Let it be known that the additional setup time required is well worth it, because the benefits gained in the user experience are well worth the effort.

The setup starts with the Web.config file. Two keys must be set to true, ClientValidationEnabled and UnobtrusiveJavaScriptEnabled:

<?xml version="1.0"?>
<configuration>
  <connectionStrings>
    <add name="ApplicationServices" connectionString=
         "data source=.\SQLEXPRESS;Integrated Security=SSPI;
         AttachDBFilename=|DataDirectory|aspnetdb.mdf;
         User Instance=true" providerName="System.Data.SqlClient"/> 
  </connectionStrings>

  <appSettings>
    <add key="webpages:Version" value="1.0.0.0" />
    <add key="ClientValidationEnabled" value="true" />
    <add key="UnobtrusiveJavaScriptEnabled" value="true" />
    <add key="smtpServer" value="localhost" />
    <add key="smtpPort" value="25" />
    <add key="smtpUser" value="" />
    <add key="smtpPass" value="" />
  </appSettings>

  <system.web>
    <compilation debug="true" targetFramework="4.0">
      <assemblies>
        <add assembly="System.Web.Abstractions, 
               Version=4.0.0.0, Culture=neutral, 
               PublicKeyToken=31BF3856AD364E35" />
        <add assembly="System.Web.Helpers, 
               Version=1.0.0.0, Culture=neutral, 
               PublicKeyToken=31BF3856AD364E35" />
        <add assembly="System.Web.Routing, 
               Version=4.0.0.0, Culture=neutral, 
               PublicKeyToken=31BF3856AD364E35" />
        <add assembly="System.Web.Mvc, 
               Version=3.0.0.0, Culture=neutral, 
               PublicKeyToken=31BF3856AD364E35" />
        <add assembly="System.Web.WebPages, 
               Version=1.0.0.0, Culture=neutral, 
               PublicKeyToken=31BF3856AD364E35" />        <add assembly="System.Data.Entity, 
               Version=4.0.0.0, Culture=neutral, 
               PublicKeyToken=b77a5c561934e089" />
      </assemblies>
    </compilation>

    ...
  </system.web>

  ...
</configuration>

The final setup step that needs to be completed is to include several JavaScript files. This will be done in the shared layout that is used by all of the views created to date. In Views/Shared/_Layout.cshtml, two JavaScript files have been included in the <head> tag:

<!DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Title</title>
    <link href="@Url.Content("~/Content/Site.css")" 
        rel="stylesheet" type="text/css" />
    <script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" 
        type="text/javascript"></script>
    <script src="
        @Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" 
           type="text/javascript"></script>
</head>
<body>
    <div class="page">
        <div id="header">
            <div id="title">
                <h1>My MVC Application</h1>
            </div>
            <div id="logindisplay">
                @Html.Partial("_LogOnPartial")
                [ @Html.ActionLink("English", "ChangeLanguage", 
                   "Home", new { language = "en" }, null) ]
                [ @Html.ActionLink("Français", "ChangeLanguage", 
                   "Home", new { language = "fr" }, null) ]
            </div>
            <div id="menucontainer">
                <ul id="menu">
                    <li>
                       @Html.ActionLink("Home", "Index", "Home")
                    </li>
                    <li>
                       @Html.ActionLink("About", "About", "Home")
                    </li>
                </ul>
            </div>
        </div>        <div id="main">
            @RenderBody()
        </div>
        <div id="footer">
        </div>
    </div>
</body>
</html>

These files are automatically included in the base MVC 3 application. That completes the core of the Ajax setup. Next, the Books/Index view will be updated. In the following example, the three filter links and sortable header links have been updated to use the Ajax.ActionLink instead of the Html.ActionLink:

@model PagedList.IPagedList<MvcApplication4.Models.Book>

@if (IsAjax)
{
    Layout = null;
}

<h2>@MvcApplication4.Resources.Resource1.BookIndexTitle</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<p>
    Show:
    @if (ViewBag.CurrentFilter != "")
    {
        @Ajax.ActionLink("All", "Index", new { 
               sortOrder = ViewBag.CurrentSortOrder, 
               Keyword = ViewBag.CurrentKeyword }, 
               new AjaxOptions { UpdateTargetId = "main" })
    }
    else
    {
        @:All
    }
    &nbsp; | &nbsp;
    @if (ViewBag.CurrentFilter != "NewReleases")
    {
        @Ajax.ActionLink("New Releases", "Index", new { 
               filter = "NewReleases", 
               sortOrder = ViewBag.CurrentSortOrder, 
               Keyword = ViewBag.CurrentKeyword }, 
               new AjaxOptions { UpdateTargetId = "main" })
    }
    else
    {
        @:New Releases
    }
    &nbsp; | &nbsp;
    @if (ViewBag.CurrentFilter != "ComingSoon")    {
        @Ajax.ActionLink("Coming Soon", "Index", new { 
               filter = "ComingSoon", 
               sortOrder = ViewBag.CurrentSortOrder, 
               Keyword = ViewBag.CurrentKeyword }, 
               new AjaxOptions { UpdateTargetId = "main" })
    }
    else
    {
        @:Coming Soon
    }
</p>
@using (Html.BeginForm())
{
    @:Search: @Html.TextBox("Keyword") 
    <input type="submit" value="Search" />
}
@Html.Partial("_Paging")
<table>
    <tr>
        <th>
            @Ajax.ActionLink("Title", "Index", new { 
               sortOrder = ViewBag.TitleSortParam, 
               filter = ViewBag.CurrentFilter, 
               Keyword = ViewBag.CurrentKeyword }, 
               new AjaxOptions { UpdateTargetId = "main" })
        </th>
        <th>
            @Ajax.ActionLink("Isbn", "Index", new { 
               sortOrder = ViewBag.IsbnSortParam, 
               filter = ViewBag.CurrentFilter, 
               Keyword = ViewBag.CurrentKeyword }, 
               new AjaxOptions { UpdateTargetId = "main" })
        </th>
        <th>
            Summary
        </th>
        <th>
            @Ajax.ActionLink("Author", "Index", new { 
               sortOrder = ViewBag.AuthorSortParam, 
               filter = ViewBag.CurrentFilter, 
               Keyword = ViewBag.CurrentKeyword }, 
               new AjaxOptions { UpdateTargetId = "main" })
        </th>
        <th>
            Thumbnail
        </th>
        <th>
            @Ajax.ActionLink("Price", "Index", new { 
               sortOrder = ViewBag.PriceSortParam, 
               filter = ViewBag.CurrentFilter, 
               Keyword = ViewBag.CurrentKeyword }, 
               new AjaxOptions { UpdateTargetId = "main" })
        </th>        <th>
            @Ajax.ActionLink("Published", "Index", new { 
               sortOrder = ViewBag.PublishedSortParam, 
               filter = ViewBag.CurrentFilter, 
               Keyword = ViewBag.CurrentKeyword }, 
               new AjaxOptions { UpdateTargetId = "main" })
        </th>
        <th></th>
    </tr>

@foreach (var item in Model)
{
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.Title)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Isbn)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Summary)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Author)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Thumbnail)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Price)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Published)
        </td>
        <td>
            @Html.ActionLink("Edit", 
               "Edit", new { id = item.ID }) |
            @Html.ActionLink("Details", 
               "Details", new { id = item.ID }) |
            @Html.ActionLink("Delete", 
               "Delete", new { id = item.ID })
        </td>
    </tr>
}

</table>

@Html.Partial("_Paging")

The key thing that was done is that new AjaxOptions were added as the last parameter of the ActionLink function. This means that when the Ajax link is clicked by the user, the results of the Ajax request should update the HTML element with the id of main. If you look in the shared layout altered earlier, you will notice that it contains a <div> with the id of main. In fact, this <div> is the container for the @RenderBody() function which is where the output of a view goes.

The other important thing that was done is a check for Ajax done at the top of the view. If the request was completed via Ajax, the layout is set to null. This is an extremely important factor because if this isn’t done, the results of the Ajax request will contain not only the results of the view, but the full layout as well, which would be placed inside of the layout again.

To finish off this example, the Shared/_Paging view will also be updated to use the Ajax helper as well:

<p>
    @if (Model.HasPreviousPage)
    {
        @Ajax.ActionLink("<< First", "Index", new { 
               page = 1, 
               sortOrder = ViewBag.CurrentSortOrder, 
               filter = ViewBag.CurrentFilter, 
               Keyword = ViewBag.CurrentKeyword }, 
               new AjaxOptions { UpdateTargetId = "main" })
        @Html.Raw("&nbsp;");
        @Ajax.ActionLink("< Prev", "Index", new { 
               page = Model.PageNumber - 1, 
               sortOrder = ViewBag.CurrentSortOrder, 
               filter = ViewBag.CurrentFilter, 
               Keyword = ViewBag.CurrentKeyword }, 
               new AjaxOptions { UpdateTargetId = "main" })
    }
    else
    {
        @:<< First
        @Html.Raw("&nbsp;");
        @:< Prev
    }

    &nbsp;&nbsp;
    
    @if (Model.HasNextPage)
    {
        @Ajax.ActionLink("Next >", "Index", new { 
               page = Model.PageNumber + 1, 
               sortOrder = ViewBag.CurrentSortOrder, 
               filter = ViewBag.CurrentFilter, 
               Keyword = ViewBag.CurrentKeyword }, 
               new AjaxOptions { UpdateTargetId = "main" })
        @Html.Raw("&nbsp;");
        @Ajax.ActionLink("Last >>", "Index", new { 
               page = Model.PageCount, 
               sortOrder = ViewBag.CurrentSortOrder, 
               filter = ViewBag.CurrentFilter, 
               Keyword = ViewBag.CurrentKeyword }, 
               new AjaxOptions { UpdateTargetId = "main" })
    }
    else
    {
        @:Next >
        @Html.Raw("&nbsp;")
        @:Last >>
    }
</p>

Now when the user clicks on a link that changes the list of books, the full page is not reloaded and only the list of books is updated, providing a much better and faster user experience.

Also, if the client does not support JavaScript (e.g., when a search engine visits), the link will still function normally, allowing both a user with JavaScript disabled and the search engine to still access the content through a normal full page reload.

AjaxHelper