Click here to Skip to main content
14,173,165 members
Click here to Skip to main content
Add your own
alternative version

Stats

18.1K views
14 bookmarked
Posted 22 Sep 2013
Licenced CPOL

Couple of (not so) different approaches to paging in ASP.Net MVC Applictions (Pure MVC & Mustache)

, 22 Sep 2013
Rate this:
Please Sign up or sign in to vote.
Instead of the traditional way of paging like a numbered / text pager, this approach uses a single button to load more data within the same page

MVC Paging

Introduction

One way or the other, we are all used to providing paging abilities in our application. Conventional methods of paging may include one of the following:

  • A numbered pager
  • A text pager with links like next, previous, first and last
  • Or a combination of both

With the advent of a lot of new technologies, a lot of websites no longer follow these conventional methods. The pages where paging is needed does it for you automatically as and when you scroll (like "twitter"). Earlier they used to provide a single "More" button which does the same task of loading more data with user intervention, apart from scrolling. You might have guessed, this article is not about the conventional methods. Neither it is about the method where data is loaded when the user scrolls down the page, since I am not a fan of this method :). In this article I am going to discuss 2 methods by which paging could be accomplished using a single button. So, let's get started!!!

Background

I am working in a new project that requires paging. I wanted to get away from the conventional methods and so created this simple method of paging with just a single button. The sample project attached uses bootstrap for the basic layout of the site, which I think you would have guessed when you saw the screenshot above!

Method 1: [Mostly] Pure MVC Paging

In this method, I am going to use MVC partial views mostly and a bit of javascript. Given below is the contents of the Index action that will get things startes.

@{
    ViewBag.Title = "Index";
}

@section pageScripts
{
    @Scripts.Render("~/bundles/paging")
}

@{
    Html.RenderAction("PostsPartial", "MvcPaging");
}

To begin with, I have a BasePagingController class that contains action results and methods shared by both the paging methods, which are given below:

public class BasePagingController : Controller
{
	public ActionResult PostsPartial()
	{
		var model = GetModel(0);
		return PartialView("PostsPartial", model);
	}

	public JsonResult CheckPostsStatus(int currentPageNumber)
	{
		var posts = GetPosts();
		var totalPages = (int)Math.Ceiling((decimal)posts.Count() / 5);
		return Json((currentPageNumber + 1) < totalPages, JsonRequestBehavior.AllowGet);
	}

	protected PostViewModel GetModel(int currentPageNumber)
	{
		var posts = GetPosts();
		if (currentPageNumber == 0)
		{
			return new PostViewModel
			{
				Posts = posts.Take(NumberOfPosts).ToList()
			};
		}

		var currentPage = currentPageNumber + 1;
		return new PostViewModel
		{
			Posts = posts.Skip((currentPage - 1) * NumberOfPosts).Take(NumberOfPosts).ToList()
		};
	}

	protected List<post> GetPosts()
	{
		// -- snip --
		// - return some dummy posts -
	}

	private const int NumberOfPosts = 5;
}
</post>

PostPartial is an action method that returns a partial view with the model it needs. This method is shared by this method of paging and the next method, for creating the initial view of the page. CheckPostsStatus is another shared action method that is used by both the methods to "see" if there are more pages to follow. In the subsequent sections, I will discuss how even we could get away from this action method altogether. But for now, let's proceed! In the first line I call the GetModel method to get the required model. I call this method with a 0, since this action is "rendered" when the page is loaded for the first time with the items for page 1.

The GetModel method first checks the page number passed, if its 0, it's easy, just get the pre-defined (NumberOfPosts) number of posts and return them. If not, I use a simple method to get the relevant posts according to the page number passed and return them.

Getting back to the PostPartial action method, once the model is received, a partial view with the name PostPartial is returned with the model received in the previous step. Let's have a look at this partial view next!

@model MvcPaging.Models.PostViewModel

<div id="content">
    @Html.Partial("PostDisplayer", Model)
</div>

<div id="placeholder">
    
</div>

<div>
    <a class="btn btn-more" id="moreBtn">
        More <img id="moreBtnImg" src="@Url.Content("~/Content/images/loading.gif")" alt="loading..."/>
    </a>
</div>

<input type="hidden" id="currentPage" name="currentPage" value="1"/>

This partial view is one of the most interesting, since it sets the stage for pretty much the entire idea! The first section (div[id="content"]) contains the call to render the PostDisplayer partial view. This partial view gets a "model" with a specific number of posts. The post list is iterted an entry for each post is created. Here is the content of this partial view:

@model MvcPaging.Models.PostViewModel

@foreach (var post in Model.Posts)
{
    <div class="post-entry" id="@string.Format("post-{0}", post.PostId)">
        <div>
            <div class="pull-left" title="@post.PostTitle">
                @post.PostTitle
            </div>
            <div class="pull-right" style="color: #BDBDBD">
                <b>@post.Category</b>
            </div>
        </div>
        <br/><br/><br/>
        @Html.ActionLink("View", "ViewPost", "Details", new { questionId = post.PostId }, new { @class = "btn btn-primary" })
    </div>
    <hr/>
}

As I said before, for every entry in the posts list, I render a div with the title in the first line along with the category. Then, in the next line I display a button to "view" the entry.

Now, lets get back to the PostPartial partial view! Note div[id="placeholder"]. This div will be used for loading the content of the subsequent pages, loaded using the "more" button. To begin with, its just empty. Finally the last div represents the "more" button that will be used to load the subsequent pages. This section just displays a simple button with the text "More". Whenever the button is clicked, an image appears next to this text which is displayed until the request successfully finishes or fails (refer to the screen shot below). Whatever we saw so far is pretty trivial. So let's get to the fun part. Whenever the user clicks on the "More" button, I place an asynchronous request using ajax by passing in the current page number (available in the currentPage hidden variable, initially set to 1 on page load).

More button

$(function () {
	showHideMoreBtn('mvcpaging/checkpostsstatus');

	$(document).on('click', '#moreBtn', function (e) {
		e.preventDefault();
		var currentPgNumber = parseInt($('#currentPage').val());
		$('#moreBtnImg').show();
		$.ajax({
			type: 'GET',
			url: siteRoot + 'mvcpaging/loadmoreposts',
			data: { 'currentPageNumber': currentPgNumber },
			dataType: 'html',
			success: function (data) {
				$('#placeholder').append(data);
				$('html, body').animate({ scrollTop: $('#moreBtn').offset().top }, 1000);
				showHideMoreBtn('mvcpaging/checkpostsstatus');
				incrementPageNumber();
				$('#moreBtnImg').hide();
			},
			error: function (a, b, ctx) {
				$('#moreBtnImg').hide();
				alert('error ' + ctx);
			}
		});
	});
});

The sample project uses jQuery 2.0.3, which does not support the .live method, that is used to bind events to elements that will be added in the future. So, I have used the .on method to accomplish the same. In this case, line 4 impies that, starting from the document object, i.e. everything, bind the click event to an element with id moreBtn, by calling the callback passed next. Thus when the button is clicked and the callback raised, event argument e is used to prevent the default action, which in this case, for the button is the click event. Then the current page number is found using the hidden variable currentPage. Then I show the image within the "More" button so that the user knows that some activity is going on.

Then an ajax request is issued by using the $.ajax method provided by jQuery. LoadMorePosts is an action method that finds the posts based on the page number passed and returns it. This method is given below:

public ActionResult LoadMorePosts(int currentPageNumber)
{
	var model = GetModel(currentPageNumber);
	return PartialView("PostDisplayer", model);
}

Once the method successfully returns the partial view (remember? this is the same partial view used in the PostsPartial partial view), the success callback of the $.ajax method is called. In here, I simply append the html returned in to the div[id="placeholder"] element. Then in the next line, I just do some simple animation to move to the end of the page. In the next line, I call the showHideMoreBtn method by passing the url to be used for it to issue a request, which is defined in the paging-base.js file. The task of this function is to pass the current page number to a method called CheckPostsStatus and then use the return value to determine whether the "More" button appears or not. Given below is the code for this method:

public JsonResult CheckPostsStatus(int currentPageNumber)
{
	var posts = GetPosts();
	var totalPages = (int)Math.Ceiling((decimal)posts.Count() / 5);
	return Json((currentPageNumber + 1) < totalPages, JsonRequestBehavior.AllowGet);
}

Finally, I just increment the page number, and then finally hide the image within the "More" button. On the other hand, if the ajax request fails, the image is hidden and then an alert is displayed to indicate the user of the error. If you think there is more to this method, you are false! This is it, you now have a fully functional page that handles paging in a non-conventional way! Let's get to the next method!

Method 2: Paging using Mustache.js

In the next method, I am going to explain how I used mustache.js, a templating engine, for paging. 

A Short Intro to Mustache

Mustache is fairly simple to use! Say you need to say hello to a user. You could define the following template:

Hello {{name}}

Then, when you pass the data as shown below:

{ "name" : "karthik" }

to this template, following is the result:

Hello karthik

Esasy, isn't it? Here is a fiddle you could use to understand this more! For the sake of completeness, here is the javascript required:

$(function(){
    var data = { "name": "karthik" };
    var template = "Hello {{name}}";
    var func = Mustache.compile(template);
    var output = func(data);
    $('#content').html(output);
});

Here, in the second line the data required is defined. In line 2, the template is defined. Now in the next line, using the Mustache factory method compile, the template defined earlier is compiled, so that it could be used subsequently. This function returns a "function" that could be called by passing in the relevant data, which is done in the next line. When this function is called by passing in the data, the modified template with data is returned. I then append this to the div defined. Given below is another example where I deal with an array, here is the fiddle:

$(function(){
    var data = { "countries": [ {"name" : "India"}, {"name" : "USA"}, { "name": "Sweden"}] };
    var template = "<ul>{{#countries}}<li>{{name}}</li>{{/countries}}";
    var func = Mustache.compile(template);
    var output = func(data);
    $('#content').html(output);
});

In this case, the data passed contains an array of objects. Every object in the array contains a name property with the corresponding value. In the case of this template mustache uses {{#property_name}}...{{/property_name}} to indicate that what follows is a template for a list. In the above example, countries is a list (array). The above template creates an unordered list, with each list item being an item in the array. {{name}} as in the first example is used to print the value of the object in every item in the array.

Back to method 2!

Section before this was a very short introduction to mustache.js! Let me now explain how I put this in to use. MustachePagingController takes care of the code needed for this method. To begin with, the Index action method simply returns a view. In this view the PostsPartial partial is used to display the initial set of posts. This was already discussed in the first method. Code listing below shows the Index view:

@{
    ViewBag.Title = "Index";
}

@section pageScripts
{
    @Scripts.Render("~/bundles/paging-mustache")
}

@{
    Html.RenderAction("PostsPartial", "MustachePaging");
}

To take care of paging using the mustache templating library, I have the relevant code in the paging-mustache.js file. PostsPartial displays the "More" button and this button is wired in the javascript file to append the subsequent page contents in to the "placeholder", as shown below: 

$(function () {
    showHideMoreBtn('mvcpaging/checkpostsstatus');

    $(document).on('click', '#moreBtn', function (e) {
        e.preventDefault();
        var currentPgNumber = parseInt($('#currentPage').val());
        $('#moreBtnImg').show();
        $.ajax({
            type: 'GET',
            url: siteRoot + 'mustachepaging/loadmoreposts',
            data: { 'currentPageNumber': currentPgNumber },
            dataType: 'json',
            success: function (data) {
                var tmpl = Mustache.compile("{{#Posts}}<div class=\"post-entry\" id=\"post-{{PostId}}\"><div><div class=\"pull-left\"
title=\"{{PostTitle}}\">{{PostTitle}}</div><div class=\"pull-right\" style=\"color: 
   #BDBDBD\"><b>{{Category}}</b></div></div><br><br><br><a class=\"btn btn-primary\" 
  href=\"/Details/ViewPost?questionId={{PostId}}\">View</a></div><hr/>{{/Posts}}");
                var output = tmpl(data);
                $('#placeholder').append(output);
                $('html, body').animate({ scrollTop: $('#moreBtn').offset().top }, 1000);
                showHideMoreBtn('mvcpaging/checkpostsstatus');
                incrementPageNumber();
                $('#moreBtnImg').hide();
            },
            error: function (a, b, ctx) {
                $('#moreBtnImg').hide();
                alert('error ' + ctx);
            }
        });
    });
});

In the above code listing, lines 14 to 19 are of prime importance. The template is the html required for every entry in the listing of posts: first line with the post title and the category, and few blank lines, finally followed by the "view" button and horizontal line as shown in the following image. The call to LoadMorePosts returns a list (array) of posts, within a property called Posts. That's why the format to indicate an array {{#Posts}}...{{/Posts}} is used in the template string! This is also the reason for returning the list of posts within a property called Posts within PostViewModel instead of just returning a list!

After the call to Mustache.compile in line 18, we get a function that could be used to get the final html result, by passing in the data received as part of the ajax call, which is a list (array). Once the result is received it is appended to the placeholder and similar steps are carried out as in method 1.

In case you don't like having the template in your .js file, you could have it as a script tag in your page, as shown below:

<script type="text/template" id="tmpl">
{{#Posts}}<div class=\"post-entry\" id=\"post-{{PostId}}\"><div><div class=\"pull-left\"
title=\"{{PostTitle}}\">{{PostTitle}}</div><div class=\"pull-right\" style=\"color: 
   #BDBDBD\"><b>{{Category}}</b></div></div><br><br><br><a class=\"btn btn-primary\" 
  href=\"/Details/ViewPost?questionId={{PostId}}\">View</a></div><hr/>{{/Posts}}
</script>

And then in the .js file, you could get this template using:

var tmpl = $('#tmpl').html();

Now, instead of having the messy code in the .js file, it's available in the variable with the help of the script template! The same steps as before can be used to compile and replace with the relevant values!

Advantages of using Method 2

One of the main advantages of using the 2nd method is limiting the amount of data transferred over the wire. Consider the image given below:

Fiddler

This image is from a sample run captured in fiddler. The amount of data for the first and third entries are more or less the same, so lets ignore them for now. Regarding the 2nd and 4th entries, method 1 response size is about 405 bytes. Whereas the same call in method 2 only requires 198 bytes! That's about 50% savings in the amount of data transferred! Even though it may not sound to be a big deal, it is! Consider what happens if you page 10 / 15 times! Guess now you get the picture! Any amount of data transfer saved is a good thing :) For example, consider the case when the amount of html is much higher than the data itself. In this case, I guess you can imagine the amount of data transfer avoided because only the data was transferred, but not the html itself!

What else?

Now that we have seen the 2 methods, let me throw what else could be done! To begin with the part where the initial set of posts are loaded can be removed and replaced with another method call during the document load event, where the contents of page 0 can be loaded asynchronously instead of loading it during the button click. This would simplify a lot of things!

Another improvement is, instead of having 2 methods - 1 for getting more data and 1 for checking "if" there is more data, they could be combined in to single method by returning an object with the data and the status of the more button.

I leave this part to you, the readers! Other stuff include things like dealing with empty post list, more error handling instead of just alert messages and many more!

History

Version 1.0 released

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Karthik. A
Software Developer (Senior)
United States United States
Just another passionate software developer!

Some of the contributions to the open source world - a blog engine written in MVC 4 - sBlog.Net. Check it out here. For the codeproject article regarding sBlog.Net click here!

(Figuring out this section!)

You may also be interested in...

Comments and Discussions

 
GeneralMy vote of 5 Pin
Javier Prieto24-Sep-13 0:36
memberJavier Prieto24-Sep-13 0:36 
GeneralRe: My vote of 5 Pin
Karthik. A24-Sep-13 8:07
memberKarthik. A24-Sep-13 8:07 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web02 | 2.8.190524.3 | Last Updated 22 Sep 2013
Article Copyright 2013 by Karthik. A
Everything else Copyright © CodeProject, 1999-2019
Layout: fixed | fluid