Thursday, September 2, 2010

"Endless Scrolling" With An ASP.Net DataPager

On the front page of MotorShout we have quite a complex news feed which allows you to filter the feed by news type (e.g. news, new photos/videos, forum posts etc.) and even further filter that by selecting one or more manufacturers. I've managed a lot of the messy stuff with views in SQL Server and a stored procedure that brings it all together. Until this morning the feed would display 20 items sorted by date depending on your filter settings but now, now you can get much more.

The Challenge

Make this tricky mess of data sources be able to display more than the 20 top records if the user wants to see more.

How I Did It

Now, the existing news feed was based around a ListView inside an UpdatePanel and I didn't want to go down the path of totally rewriting in AJAX as everything was already in place and working nicely. That lead me down this path.

My first thoughts were to use some kind of paging in SQL Server to get it to throw the web server 20 records at a time or to grab a large set of records and cache them on the web server and serve them up to the user 20 records at a time. But in the end I went with something much easier to implement and actually quite speedy as it turns out.

The idea I ran with was quite simple and was just a matter of increasing the PageSize on a DataPager in the ListView with each PostBack until I ran out of records. This may cause problems if you are displaying thousands of results but I've limited my results to the top 100 but that's still 5 times the original page size. I may increase in future if it's not too big a performance hit.

The first step was to add a DataPager to the ListView with a PreRender event handler (we'll get back to this later). Pretty straightforward. This is my LayoutTemplate in the ListView, note the lack of a PageSize set on the DataPager. Also, the content in the news feed is static so I've disabled the viewstate.

<asp:ListView ID="NewsFeedListView" EnableViewState="false" OnDataBinding="NewsFeedListView_DataBinding" runat="server">
<LayoutTemplate>
<asp:DataPager ID="DataPager" OnPreRender="DataPager_PreRender" runat="server" />
<asp:PlaceHolder ID="ItemPlaceholder" runat="server" />
<LayoutTemplate>

The next step was to add a button somewhere outside the ListView but still inside the UpdatePanel to trigger a PostBack to get more records. I also added a Literal for when we've run out of records (see the PreRender method on the DataPager below for what I do with this). For the sake of brevity I've left out some fiddly stuff to display a spinner next to the button so the user knows something is happening.

<asp:LinkButton ID="GetMoreResults" OnClick="GetMoreResults_Click" runat="server">Get more results...<asp:LinkButton>
<asp:Literal ID="NoMoreResults" EnableViewState="false" Visible="false" runat="server">No More Results </asp:Literal>

I also added a HiddenField (you can use whatever you like but this keeps it simple) control for keeping track of how many records we are currently displaying. This needs to be inside the UpdatePanel!

<asp:HiddenField ID="CurrentPageSize" runat="server" />

OK, now we're ready to do some code. In my Page_Init() I put the initial setting for our HiddenField making sure it doesn't run if we are in a PostBack. Instead of hardcoding the number (like below) I set a global constant but you get the gist of it.

protected void Page_Init(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
this.CurrentPageSize.Value = 20;
}
}

Next I added the Click event handler for the "get more" button. Here I just needed to increment the CurrentPageSize value to be used by the DataPager, store that in our HiddenField then rebind the data.

protected void GetMoreResults_Click(object sender, EventArgs e)
{
int currentPageSize = int.Parse(this.CurrentPageSize.Value);
if (currentPageSize > 0)
{
currentPageSize = currentPageSize + 20;
this.CurrentPageSize.Value = currentPageSize.ToString();
}
this.FilterTheNews(); // My method to rebind the listview
}

The penultimate step was to add in a DataBinding (not DataBound!) event handler on the ListView. This is where I set the PageSize of the DataPager to the new larger number.

protected void NewsFeedListView_DataBinding(object sender, EventArgs e)
{
var pager = NewsFeedListView.FindControl("DataPager") as DataPager;
int currentPageSize = int.Parse(this.CurrentPageSize.Value);
if (pager != null && currentPageSize > 0)
{
pager.PageSize = currentPageSize;
}
}

And now the cherry on top, the PreRender method for the DataPager I mentioned above, which simply hides the "get more" button if we are out of records and shows the "no more records" literal. It checks if the current page size is more or equal to the total number of records and if so, we've maxed out our records.

protected void DataPager_PreRender(object sender, EventArgs e)
{
var pager = sender as DataPager;
int currentPageSize = int.Parse(this.CurrentPageSize.Value);

if (currentPageSize >= pager.TotalRowCount)
{
this.GetMoreResults.Visible = false;
this.NoMoreResults.Visible = true;
}
}

So there you have it! Pretty simple stuff and with the help of the UpdatePanel you give the user the impression that they're adding more records to a growing list.

4 comments:

Anonymous said...

Awesome! Thank you for sharing :)

Anonymous said...

I love you.

Unknown said...

Great code man! can you post the sql code you used? im using visual studio 2010 and sql server 2008

Unknown said...
This comment has been removed by the author.