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 } | @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 } | @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(" "); @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(" "); @:< Prev } @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(" "); @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(" ") @: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.