A lot of websites today interact with a database. If your website receives a lot of traffic, the SQL queries to retrieve the data can be quite intense. More importantly because the average user clicks a link within 15 seconds of arriving at your website, the work to retrieve and generate the content might be unnecessary, especially when the content is “below the fold” (not visible without scrolling first). To help solve this issue, content will be loaded “on-demand”. Enough content will be loaded to make the page feel populated and as the user scrolls down to read it, more content will be populated behind the scenes without affecting the user experience.
Using Asynchronous
controllers
along with JQuery to load a specific amount of upfront content and then
load further content on-demand when the user begins scrolling through
the website content.
Asynchronous
controllers are
probably underused in many MVC applications to date—most likely because
people don’t know about them, or more importantly, don’t know when to
use them. The following is an excerpt from the MSDN site listed in the
See Also section:
“In applications where thread starvation might occur, you can configure actions to be processed asynchronously. An asynchronous request takes the same amount of time to process as a synchronous request. For example, if a request makes a network call that requires two seconds to complete, the request takes two seconds whether it is performed synchronously or asynchronously. However, during an asynchronous call, the server is not blocked from responding to other requests while it waits for the first request to complete. Therefore, asynchronous requests prevent request queuing when there are many requests that invoke long-running operations.”
In this example, using Asynchronous requests is the perfect solution because it will free up IIS to serve more important requests, such as a new user arriving at the site for the first time. Where as, loading on-demand content for a user is less important because most people won’t even notice the additional content being loaded.
In a typical social website, a user’s comments are most likely to contain the most activity. In a previous recipe, the ability to comment on a book was created. In this example, the homepage of the site will be updated to list the most recent comments. Enough comments will be displayed so that scroll bars will appear. Once the user begins scrolling, an Ajax request to an asynchronous controller will be made to retrieve additional comments.
To begin, the Home/Index
view
must be updated to display the most recent comments. To provide some
context around the comment, basic details about the book will also be
displayed with links to view the book. A new controller will be created
to display the comments, so this view will simply call the render
function of the view to be created further down.
@model IEnumerable<MvcApplication4.Models.BookComment> @{ ViewBag.Title = "Home Page"; } <h2>@ViewBag.Message</h2> <p> To learn more about ASP.NET MVC visit <a href="http://asp.net/mvc" title="ASP.NET MVC Website"> http://asp.net/mvc</a>. </p> <script type="text/javascript"> var lastY = 0; var currentY = 0; var page = 1; var maxPages = @ViewBag.maxPages; $(window).scroll(function () { if (page < maxPages) { currentY = $(window).scrollTop(); if (currentY - lastY > 200 * (page - 1)) { lastY = currentY; page++; $.get('CommentFeed/Comments?page=' + page, function(data) { $('#comments').append(data); }); } } }); </script> <div id="comments"> <h2>Recent Comments</h2> @Html.Partial("../CommentFeed/Comments", Model) </div>
In the above example, there is also some relatively complex
JavaScript code that is executed when the window is scrolled. Several
global JavaScript variables are defined to keep track of the current “y”
scroll location, the last “y” scroll location, and the current page
being retrieved. When the window’s scrollTop
position minus the last scroll
location is greater than a specific number, new book comments are
retrieved through Ajax and appended to the list of comments. For your
own website, you will need to adjust the number of pixels that works
best, based on the height of the content, to ensure that new content is
always retrieved in advance.
Next, the HomeController
needs
updating to retrieve the list of book comments. The comments are ordered
by the created date in descending order to ensure the newest comments
are displayed first. To prevent intense database load, the list of
comments will be reduced to a small number. This should be adjusted on
your website to ensure there is just enough content to cause scrollbars.
In the example below, the comments are limited to 3. The maximum number
of pages is also determined by dividing the total count of comments by
3. The max pages are used to prevent further Ajax calls once the maximum
comments have been returned.
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using System.Globalization; using System.Data.Entity; using MvcApplication4.Models; namespace MvcApplication4.Controllers { public class HomeController : Controller { private BookDBContext db = new BookDBContext(); public ActionResult Index() { ViewBag.Message = "Welcome to ASP.NET MVC!"; // Get our recent comments var bookcomments = db.BookComments.Include( b => b.Book).OrderByDescending(b => b.Created). Take(3); var count = db.BookComments.Count(); ViewBag.maxPages = count / 3 + 1; return View(bookcomments); } ... } }
This same functionality needs to be duplicated into a new
asynchronous controller. With the Controllers
folder selected, right-click and
select Add→Controller. The new controller will be
called CommentFeedController
. This
controller doesn’t need the scaffolded functions, so under the
Template drop-down, change the selection to
Empty controller and press
Add.
This controller will look slightly different than a typical controller. With asynchronous controllers, one view is split into two functions. The first function performs the asynchronous request (e.g., retrieve the comments). The second function receives the results of the asynchronous call and returns or displays the results.
In the following example, a partial view is rendered. In some applications, it might be beneficial to reduce the network traffic, return a JSON result, and let the JavaScript code deal with the display. However, to simplify this example and focus on asynchronous controllers, the former will be used and a partial view is returned.
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using MvcApplication4.Models; using System.Data.Entity; namespace MvcApplication4.Controllers { public class CommentFeedController : AsyncController { private BookDBContext db = new BookDBContext(); public void CommentsAsync(int page) { AsyncManager.OutstandingOperations.Increment(); AsyncManager.Sync(() => { var bookcomments = db.BookComments.Include( b => b.Book).OrderByDescending(b => b.Created).Skip(page * 3).Take(3); AsyncManager.Parameters["bookcomments"] = bookcomments; AsyncManager.OutstandingOperations.Decrement(); }); } public ActionResult CommentsCompleted( IEnumerable<BookComment> bookcomments) { return PartialView(bookcomments); } } }
The first function, CommentsAsync
, receives the current page
passed in from JavaScript and uses this value to retrieve the next three
comments. The first thing that happens is that the outstanding
operations are incremented. Then through the Sync
method, the comments are retrieved and
passed as a variable to the second function. The final thing that
happens is that the outstanding operations is decremented. It’s
important that the increment and decrement counter match; otherwise, the
sync manager will cancel the request after a certain period of time when
they do not match, to prevent never-ending requests.
The second function receives the book comments and returns a
partial view. This is the same partial view that is called from the
Home/Index
view. The final step in
this process is to create the partial view. Begin by right-clicking on
the Views
folder and select
Add→New Folder. This folder should be called CommentFeed
to match the controller name. Then
with this folder selected, right-click and select
Add→View. The view will be called Comments
—be sure to check the
Partial View before adding it.
@model IEnumerable<MvcApplication4.Models.BookComment> @foreach (var item in Model) { <h3><a href="@Url.Action("Details", "Books", new { ID=item.Book.ID } )"> @Html.DisplayFor(modelItem => item.Book.Title) </a></h3> <h4>Comment Posted: @Html.DisplayFor( modelItem => item.Created)</h4> <p>@MvcHtmlString.Create(Html.Encode(item.Comment).Replace( Environment.NewLine, "<br />"))</p> }
The following view loops through the comments and first displays
the title of the book and links to the details page of it, then the date
the comment was created, and finally the actual comment itself. Because
comments might contain linebreaks, each new line is replaced with a
<br/>
tag to match the spacing
entered by the comment.