1.17. Paging Through Content Without the Pages

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 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.

Note

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.

Asynchronous controllers