Infinite Scrolling Using ASP.NET Web API Data Service and KnockoutJS

Posted by: Suprotim Agarwal , on 7/4/2013, in Category ASP.NET
Views: 20038
Abstract: The infinite scroll pattern is simply a different kind of pagination. In this article, we will see how to implement an infinite scroll using ASP.NET WebAPI, KnockoutJS and jQuery

The most common requirement for a line of business application is to represent data in pages. Usually this involves showing data in a grid and showing a navigate pane with either page numbers or Next/Previous button (or both) to navigate to multiple pages. This works fine in most cases.

However imagine the use case where you are representing a list or stream of data (instead of grid) like say a list of blog post summaries, where the end user is reading from top to bottom, as they go down. In such a case having pagination, kind of breaks the flow. Instead of hitting the next button, if you can load the next page of data as the user scroll down, it would be a nice user experience. Today we will see how to implement this behavior. This UI pattern is often referred to as infinite scrolling.

Note: Infinite Scrolling is not a silver bullet for solving all problems related to paged data. Pick and choose your use case.

 

The Infinite Scrolling Scenario

In our sample scenario, we have a Web API service that returns paged list of Posts where a Post is a simple entity with Title, Text and Author. The Web Service fetches data from the repository and serves up 5 pages at a time and stops when there is no more data to fetch.

The Implementation

Step 1: We start off with a ASP.NET  MVC 4 project and pick the Basic Template for our Solution.

Step 2: Next we add the Post entity in the Models folder.

public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Text { get; set; }
public string Author { get; set; }
}

Step 3: Given our oversimplified use case, we install the MvcScaffolding package and scaffold the Data Access code (using EF) as well as the CRUD pages.

PM> install-package MvcScaffolding

add-mvc-controller

Step 4: We add an empty WebAPI controller called PostServiceController to retrieve the data.

add-webapi-controller

Step 5: Then add an empty HomeController to serve up the Home Page.

add-home-controller

Step 6: Add a Home folder under Views and add an Index.cshtml that will have our Infinite Scroll view.

add-home-page

Step 7: At this point we run the Application and use the Posts controller and CRUD screens to add some data to the Posts.

Note: If you get a jQuery not found error, remove the two script references on top of the Create.cshtml and Edit.cshtml and add the following at the bottom of the page.

@section scripts{
    @Scripts.Render(@"~/Scripts/jqueryval");   
}

create-post

After we’ve added about 15 posts full of Lorem Ipsum, we end up with an Index page that looks similar to the following

default-index-page

Well, we have our test data now, so let’s see what it takes to implement the infinite scroll pattern

Step 8: We add an infiniteScroll.js in the Scripts folder. In this script, we’ll do the following:

1. Declare a view model with two properties to store all the posts that were loaded and the current page that was last loaded.

var viewModel = {
    posts: ko.observableArray(),
    currentPage: ko.observable(-1)
};

2. In document ready event, we do the following

a. Invoke KO data binding to bind the view model,

b. Call the Web API service and retrieve data for first page.

c. Attach an event handler for the Window Scroll event and if the scroll position has reached the bottom, call the Web API for the next page of data.

$(document).ready(function ()
{
    ko.applyBindings(viewModel);
    getData(viewModel.currentPage() + 1);
    $(window).scroll(function (evt)
    {
        evt.preventDefault();
        if ($(window).scrollTop() >= $(document).height() - $(window).height())
        {
            console.log("Scroll Postion" + $(window).scrollTop());
            getData(viewModel.currentPage() + 1);
        }
    });
});

3. In the getData method that retrieves the data from the Web API, we do the following:

a. Check if the requested page number is not the current page number (so as to not load the same page of data twice). I had to add this condition because some browsers fire the scroll event again after Knockout does data binding with new data. This results in duplicate calls and completely messed up data list.

b. Next we do an Ajax post to the PostService Web API that we added earlier. We’ll see the details of the API in a bit.

c. Once the call is done, we increment the current page count. This is important because we don’t want to increment page numbers unless data is returned. No data returned implies we’ve reached the bottom

d. Finally we add the data item returned into the view model which updates the UI thanks to KO binding.

function getData(pageNumber)
{
    if (viewModel.currentPage() != pageNumber)
    {
        console.log("Scroll Postion begin getData" + $(window).scrollTop());
        $.ajax({
            url: "/api/PostService",
            type: "get",
            contentType: "application/json",
            data: { id: pageNumber }
        }).done(function (data)
        {
            if (data.length > 0)
            {
                viewModel.currentPage(viewModel.currentPage() + 1);
                for (i = 0; i < data.length; i++)
                {
                    viewModel.posts.push(data[i]);
                    console.log("Scroll Postion looping ViewModel" + $(window).scrollTop());
                }
            }
        });
    }
}

Step 9: Now that we are done with fetching data on the client, let’s see the WebAPI implementation.

[HttpGet]
public IEnumerable<Post> GetPosts(int id)
{
    IEnumerable<Post> posts = _repository.Page(id, 5);
    return posts;
}

The GetPosts method simply calls the PostPage method on the repository. But if you have been following along, you’ll see no PostPage method was created in the Repository. Correct, I could have done the paging in the service itself, but decided to update the Repository interface to provide us a mechanism to get Paged data for the Posts DBSet.

The interface is as follows

IQueryable<Post> PostPage(int pageNumber, int pageSize);

The implementation is as follows

public IQueryable<Post> PostPage(int pageNumber, int pageSize)
{
    return context.Posts.OrderBy(f => f.Id).Skip(pageSize * pageNumber).Take(pageSize);
}

We simply use the Skip and Take functions to page through data. As seen in Service call, the page size is set to 5 and pageNumber is determined as we scroll through data.

Step 10: With the data fetching complete, all we have to do now is complete the Home\Index.cshtml so that our Home page has a neatly formatted, infinite scrolling View. The markup is rather simple as you can see below:

@{
    ViewBag.Title = "Posts";
}

<h2 style="text-align:center">Posts</h2>
<section data-bind="foreach: posts">
<div style="width: 25%; height: auto; float: left; text-align: right;
    padding: 55px 20px 0px 0px">
  <h3>
   <label data-bind="text: Author"></label>
  </h3>
</div>
<div style="width: 60%; height: auto; float: left">
  <h1>
   <label data-bind="text: Title"></label>
  </h1>
  <label data-bind="text: Text"></label>
</div>
</section>

@section Scripts{
<script src="~/Scripts/knockout-2.2.0.debug.js"></script>
<script src="~/Scripts/infiniteScroll.js"></script>
}

We have used the foreach binding to bind the views property of the ViewModel. Inside the section, we have multiple divs to layout the Author, Title and Text.

At the bottom we have the reference to KO and infiniteScroll.js. That’s it we are all done and it’s demo time.

Demo

Run the application and the home page will come up with 5 posts

infinite-scrolling-home-page

Now scroll down and you’ll see more posts get added as you reach the bottom. You’ll notice the scrollbar goes up indicating more data is available and when you scroll down further, it shows the newly loaded data.

infinite-scrolling-updated-page

Awesome!

Caveat

One caveat worth mentioning is, make sure your first page of data introduces a scroll bar. If your first page of data doesn’t introduce a scrollbar then the scroll event doesn’t fire as there is nothing to scroll and the subsequent pages will not be loaded.

Conclusion

The infinite scroll pattern is simply a different kind of pagination. Use it judiciously and where appropriate. I believe it’s appropriate for streams of data rather than a fixed set of say 100 records broken up into 10 pages. For smaller fixed sets of data it’s still okay to have pagination. Whatever your use case, now you have a new pagination technique in your arsenal! Enjoy!

Download the entire source code of this article (Github)

Give a +1 to this article if you think it was well written. Thanks!
Recommended Articles
Suprotim Agarwal, ASP.NET Architecture MVP, MCSD, MCAD, MCDBA, MCSE, is the CEO of A2Z Knowledge Visuals Pvt. He primarily works as an Architect Consultant and provides consultancy on how to design and develop .NET centric database solutions.

Suprotim is the founder and primary contributor to DotNetCurry, DNC .NET Magazine, SQLServerCurry and DevCurry. He has also written an EBook 51 Recipes using jQuery with ASP.NET Controls. and is authoring another one at The Absolutely Awesome jQuery CookBook.

Follow him on twitter @suprotimagarwal


Page copy protected against web site content infringement by Copyscape


User Feedback
Comment posted by Nom on Tuesday, January 7, 2014 8:52 AM
This was great content and, although I took a slightly different approach, being that I'm developing a social page that includes both the original comment and comments posted for the comment on the same page.  My issue, any ideas would be great, is that when the additional records are returned, I get the original comments but not the additional comments.  Not all original comments have a comment yet but should have a comment textbox with a button to comment and then post back to display the message.  Any thoughts?
Comment posted by Suprotim Agarwal on Tuesday, January 7, 2014 7:04 PM
"both the original comment and comments posted for the comment on the same page" - not sure I understand that. Can you rephrase?
Comment posted by Francisco on Saturday, February 22, 2014 12:19 AM
Is there any easy way to adapt this example to instead of use Skip and Take to use something that takes the last post Id on the page and get 10 more after it? I ask that because there are cases that you could have constant updates, if by the time you try go get the second page there are 3 new posts, it will repeat the last 3 posts.

Post your comment
Name:  
E-mail: (Will not be displayed)
Comment:
Insert Cancel