.NET (290) administrative (42) Ajax (42) AngularJS (1) ASP.NET (144) bicycle (2) books (179) browser (8) C# (128) cars (1) chess (27) CodePlex (10) Coma (8) database (46) deployment (3) Entity Framework (2) essay (109) flash/shockwave (2) flex (1) food (3) friend (2) game (20) idea (5) IIS (8) javascript (81) LInQ (2) Linux (6) management (4) manga (43) misc (664) mobile (1) movies (88) MsAccess (1) murder (2) music (65) mysql (1) news (98) permanent (1) personal (66) PHP (1) physics (2) picture (309) places (12) politics (13) programming (495) rant (118) religion (3) science (40) Sharepoint (3) software (57) T4 (2) technology (11) Test Driven Development (4) translation (2) VB (2) video (100) Visual Studio (44) web design (45) Windows API (8) Windows Forms (3) Windows Server (4) WPF/Silverlight (63) XML (11)

Thursday, August 02, 2007

Changing GridView PageCount

Update (21Th of March 2008): Very important, you need to add the CreateChildControls override in order to work. Otherwise, because of something I can only consider a GridView bug, this will happen: the last page in a mock paging grid will have, let's say, 2 items when the PageSize is 10; on a postback, the number of rows created by the gridview will be 10! even if only 2 have data. Thus, after a postback that doesn't rebind the data in the grid, the GridView.Rows.Count will be PageSize, not the actual bound number.

Update: Recreated the code completely. Now it has both PageIndex and ItemCount.
Also: Actually there is a way to get only the rows that you need in SQL Server 2005. It is a function called Row_Number and that returns the index number of a row based on a certain ordering. Then you can easily filter by it to take items from 20 to 30, for example. In this case, another interesting property of the PagedDataSource is CurrentPageIndex, to set the displayed page number in the pager.

Now, for the actual blog entry.

Why would anyone want to change the PageCount, you ask? Well, assume you have a big big table, like hundreds of thousands of rows, and you want to page it. First you must put it in a DataTable from the SQL server, so that takes time, then the table set a datasource to the GridView, then it implements the paging.

Wouldn't it be nicer to only get the data that you need from the SQL Server, then change the PageCount to show the exact page count that should have been? However, the PageCount property of the GridView is read-only. One quick solution is to get only the data you need, then fill the resulting DataTable with empty rows until you get the real row count. However, adding empty rows to DataTables is excruciatingly slow, so you don't really gain anything, and the Grid works with a big table anyway.

So this is what you do:
First of all determine how much of the data to gather.

Afterwards you need to trick the GridView into creating a Pager that shows the real row count (and possibly page index). Unfortunately you can't do this from outside the GridView. You need to inherit the GridView control and add your stuff inside. After you do this, you need to override the InitializePager method, which is just about the only protected virtual thing related to Paging that you can find in the GridView.

Code:

using System.Web.UI.WebControls;

namespace Siderite.Web.WebControls
{
public class MockPagerGrid : GridView
{
private int? _mockItemCount;
private int? _mockPageIndex;

///<summary>
/// Set it to fool the pager item Count
///</summary>
public int MockItemCount
{
get
{
if (_mockItemCount == null)
{
if (ViewState["MockItemCount"] == null)
MockItemCount = Rows.Count;
else
MockItemCount = (int) ViewState["MockItemCount"];
}
return _mockItemCount.Value;
}
set
{
_mockItemCount = value;
ViewState["MockItemCount"] = value;
}
}

///<summary>
/// Set it to fool the pager page index
///</summary>
public int MockPageIndex
{
get
{
if (_mockPageIndex == null)
{
if (ViewState["MockPageIndex"] == null)
MockPageIndex = PageIndex;
else
MockPageIndex = (int) ViewState["MockPageIndex"];
}
return _mockPageIndex.Value;
}
set
{
_mockPageIndex = value;
ViewState["MockPageIndex"] = value;
}
}

///<summary>
///Initializes the pager row displayed when the paging feature is enabled.
///</summary>
///
///<param name="columnSpan">The number of columns the pager row should span. </param>
///<param name="row">A <see cref="T:System.Web.UI.WebControls.GridViewRow"></see> that represents the pager row to initialize. </param>
///<param name="pagedDataSource">A <see cref="T:System.Web.UI.WebControls.PagedDataSource"></see> that represents the data source. </param>
protected override void InitializePager(GridViewRow row, int columnSpan, PagedDataSource pagedDataSource)
{
if (pagedDataSource.IsPagingEnabled && (MockItemCount != pagedDataSource.VirtualCount))
{
pagedDataSource.AllowCustomPaging = true;
pagedDataSource.VirtualCount = MockItemCount;
pagedDataSource.CurrentPageIndex = MockPageIndex;
}
base.InitializePager(row, columnSpan, pagedDataSource);
}

protected override int CreateChildControls
(System.Collections.IEnumerable dataSource, bool dataBinding)
{
PageIndex = MockPageIndex;
return base.CreateChildControls(dataSource, dataBinding);
}
}
}


What, what, whaaat? What is a PagedDataSource? Inside the GridView, the paging is done with a PagedDataSource, a wrapper around a normal DataSource, which has some of the GridView paging properties like PageSize, PageCount, etc. Even if the PageCount is also a read-only property, you have the AllowCustomPaging property and then the VirtualCount and CurrentPageIndex properties that you can set.

In other words: the pager is initialized at databinding. Set MockItemCount and MockPageIndex before MockPagerGrid.DataBind();

That's it.

Update: People keep asking me to provide a code sample. Let's try together. First, let's see a classic GridView use example:

gridView.DataSource=getDataSource();
gridView.PageIndex=getPageIndex();
gridView.DataBind();
As you can see, we provide a data source programatically, then set the pageindex (let's assume we took it from the URL string) and then call DataBind(). In this situation, we would load the entire data source (say, 10000 rows) then give it to the grid, which would only render something like 20 rows. Very inefficient.

Now, let's replace the original GridView control with the with MockPagerGrid. The code would look like this:

mockPagerGrid.DataSource=getDataSource(2);
mockPagerGrid.MockPageIndex=getPageIndex();
mockPagesGrid.MockItemCount=10000;
mockPagerGrid.DataBind();
This gets the rows for the second page, sets the mock ItemCount and PageIndex to the total number of rows and the page we want and then calls DataBind(). In this situation getDataSource would load only the 20 rows of page 2, would display it, then the pager would show that it is on page 2 out of 500.

This is a very simple example. It assumes you already know the total number of rows returned. A more complete example would look like this:

// starting with an arbitrary page index
var pageIndex=getPageIndex();
// do operations on the database that would return the rows for the page
// with that index, having the size of the page size of the grid
// and also get the total number of rows in the data source
CustomDataSource dataSource=getDataSource(pageIndex,mockPagerGrid.PageSize);
// set the returned rows as the data source
mockPagerGrid.DataSource=dataSource.Rows;
// set the page index
mockPagerGrid.MockPageIndex=pageIndex;
// set the total row count
mockPagesGrid.MockItemCount=dataSource.TotalRowCount;
// databind
mockPagerGrid.DataBind();

// CustomDataSource would only have two properties: Rows and TotalRowCount
// The sql for getDataSource(index,size) would be something like
// SELECT COUNT(*) FROM MyTable -- get the total count
// SELECT * FROM MyTable WHERE RowIndex>=@index*@size
// AND RowIndex<(@index+1)*@size

// for convenience, I assumed that there is a column called RowIndex in
// the table that is set to the row index


Hopefully, this will help people use this code.

39 comments:

Niall said...

This is exactly what I was looking for. I have a dynamic datasource with potentially thousands of records.

Since I can@t use an ObjectDataSource to page efficiently

I had to figure out some way of setting the page count.

Leonwoo said...

thanks for the solution.
However, after using the proposed method, I am having problem setting the correct pageIndex if i fetch the data page by page. The pageIndex always activate at the "1" even if i clicked on the "2".

Siderite said...

Well, you must also set the PageIndex, of course. I know I did it in my code somewhere, but I am at home now and I can't access it. I am sure that it can be done though :)

I hope I remember to update the post when I check the code at the office.

Anonymous said...

Sorry I can't understand why it works the first 2 times i refresh the grid. At the third it gives me problem about viewstate:

Failed to load viewstate. The control tree into which viewstate is being loaded must match the control tree that was used to save viewstate during the previous request. For example, when adding controls dynamically, the controls added during a post-back must match the type and position of the controls added during the initial request.


What is happeining?
THanks in advance if you can help me...

Siderite said...

Well, I would help you if you would leave some kind of contact information :) Why not enter the chat? I am online.

CustomPager said...

Thank you.

Thank you.

Thank you.

Again, thank you.

Seriously, I was about to ditch custom paging and implement a cap on records returned, but this saved the day.

Siderite said...

You are welcome and thank _you_! This kind of comments are the reason I keep the blog going.

Jon said...

I ran into the same GridView bug that you mentioned in your update on the 21st of March. What do I have to do in the CreateChildControls override to get my custom GridView to work?

Siderite said...

The original post did not have the CreateChildControls override at the end of the code block. If you would have used the source from then, you would have had to update the code. Basically you need to set PageIndex = MockPageIndex; in the CreateChildControls method.

Simon Hazelton said...

This is a great piece of code. Thanks, just made my life a whole lot easier.

Anonymous said...

Hi

really useful code but i still get these viewstate errors.

I'm using a custom paging using the PagerTemplate. I don't use sorting, editing and so on. Just displaying data in a single column via ItemTemplate.

When i remove all Servercontrols (button, image, hyperlink) i create in the ItemTemplate the paging works perfect.

Any ideas? Thanks!

Siderite said...

I have no idea. I can tell you that this code works, though. If you can tell me what the errors are I can trace them to what is throwing them and tell you where they come from at least.

Anonymous said...

Thank you for your reply. I really don't think that this has something to do with your code. I made some further investigations and i think the paging controls are the root of the viewstate errors. I disabled viewstate on some specific controls and now its working. I red some nice articles about viewstate and i understand why this errors could occur but i still do not understand why they rise im my case because i don't add controls in code behind dynamically. I only manipulate the controls of the pagertemplate.

nevertheless thank you for your time and your blog is really nice.

Adrian Wake said...

This is sheer genius.

Wish I'd found it 3 days ago, it would have saved me a lot of frustration.

Thanks

Adrian

Nils said...

I tried the code but still it gives me same ViewState error when I try clicking on previous pages from the last page. Please help me out. Thanks.

Siderite said...

I cannot help you without more information. Look for me on the first blog chat and we can discuss solutions.

Anonymous said...

Just needed to say... this background color is horrible!!!!

jabits said...

This is just what I needed. I was beginning to despair, after getting real paging working, and then no pages! The solution you posted is perfect! Too cool. Thanks so much for the effort!

Anonymous said...

Excellent solution!

Anonymous said...

This custom gridview worked perfectly, me being a guy who is addicted to setting datasource from codebehind, I loved every bit of it.
However for those who can afford it...Telerik RadGrid has similar properties which are very easy to set.

tom said...

I think I found an issue with this customized control.
Try this:
Place the mock gridview inside an update panel, bind it to any data source and add a button with a commandname="any".
Create the rowcommand event for the grid.
Now testing...
go to the last page of the grid, then press the button which will fire the rowcommand event.
Ghost rows are created!
Any workaround avaiable? I'll try to find a solution, will post here if I make it.

Siderite said...

Maybe you have sumbled upon a similar bug like the one described in the update on the top of the post. I am not currently working on the issue and I have little time to, but if I were to guess, then you need to work on the CreateChildControls override.

Please let me know how it goes. Maybe some time I can work on it, but it would be a waste of time if you already have solved it.

Oh, and thanks for the comment!

tom said...

Hey Side, just came up with a solution.
This will work unless you display empty rows in your gridview for some reason (kinda unusual I believe).

Okay. Here it is:

I worked on the CreateRow override.

protected override GridViewRow CreateRow(int rowIndex, int dataSourceIndex, DataControlRowType rowType, DataControlRowState rowState)
{
if (dataSourceIndex <= this.MockItemCount)
{
return base.CreateRow(rowIndex, dataSourceIndex, rowType, rowState);
}
else
{
return base.CreateRow(rowIndex, dataSourceIndex, DataControlRowType.EmptyDataRow, DataControlRowState.Normal);
}
}

Put this in your class and you are good to go!

Don't forget to put the tag
<EmptyDataRowStyle CssClass="yourCSSHere" />

and edit the css like this:

.yourCSSHERE
{
display:none
}

or else you'll get a blank line or sumethin'.

Richard said...

thank you.. thank you.. thank you.. thank you.. thank you.. thank you.. thank you.. thank you.. thank you.. thank you.. thank you.. thank you.. thank you.. thank you!!

J said...

Hi!!! thank you very much for this solution, it's really useful.

Unfortunately it has a flaw :(, in my custom control I raise a postback event when a row is clicked, that action lose the index; the virtual count is shown but everytime shows the current page as the first page.

That happens too when a CommandField is used to select the row.

I noticed when a row is clicked that neither CreateChildControls method and subsecuently nor the InitializePager is fired. So it never reset the PagerDataSource.

This is critic for me, if you or someone else resolve this issue, could please post the solution???

I'm working on it right now, if I find a solution i'll post it here.

Siderite said...

I wish I had the time to work on this... I will put it in my todo list, but don't get your hopes up.

J said...

Ok I solved the problem changing the condition of the property.

That property was returning 0 always because my CustomPageIndex atribute was never null, so it never retrieved the value in ViewState, and the default value given to it was 0, not null :S

Thanks a lot for answering man, and thanks again for this solution!!!

John said...

Thank you ;)
It helped me very much. I almost created custom pager for GridView when i found this article.
Thank you ;)

Shanthi Diana said...

Siderite,
Can you please explain me how to use your code. I am unable to use it as I don't know how to.

Siderite said...

You want to use the class inheriting from GridView instead of the GridView. You want to set MockItemCount to set the total number of items in the database. You want to set the MockPageIndex so that the pager shows at what page you are. Then you set the DataSource for the GridView only to the items that you want displayed.
The grid will show there are a zillion pages (based on the PageSize and MockItemCount) and that you are on one of them, but you only have to bind the items that are on the actual page.

Shanthi Diana said...

Ya Siderite, many thanks. I was able to do that myself yesterday. Many thanks for the user control. It is very simple to use if the logic is understood.

John said...

nice work and thanks a lot.
Although I'm having trouble with it.
My set up I created the gridview class. however I dynamically create the columns and pager using ITemplate to create the gridview because I don't know in advance the table structure.
Anyway, one question is when and wear would I set the MockIndex and MockCount? it always seems to be 0

Another Question is when reffring to the gridindex do I use the actaul mock like in RowCommand or IndexChanging?

Please help have a huge deadline and I've struggling with this for 2 weeks. no hair left

Siderite said...

Well, the basic idea of the control in the post is that you set two properties and you are all set. If you change the Pager template, then you can do whatever you want, anyway...

As I said a few comments above, you need to set the DataSource to just the rows you want displayed on the current page, set the mock item count and page index and then the default pager of the grid will show the information like you are on a certain page in a bunch of them.

If you change the template of the Pager, you can put there a 3D object in a HTML5 canvas, if you want, it can have nothing to do with the paging.

XpertProgrammer said...

Hi Siderite, plz provide me with a sample project so that I can better understand it. Thanks in Advance.

Siderite said...

I have updated the post with a code sample. I hope it works, I wrote it from memory :)

Anonymous said...

Thanks for your solution

Surya Teja Aditya Kumar G said...

Thanks alot!!!!

Prasad said...

The post successfully solved my problem in .NET page but the issue still remains while I use the custom gridview control in a SharePoint webpart. Any ideas? Please let me know at the earliest. Thanks in Advance.

Anonymous said...

Thanks for your post which help me to solve the same issue on my application.
But, if the pager is on the TOP of the grid, the problem is still present.
To correct this I had a new condition in the initializer :

protected override void InitializePager(GridViewRow row, int columnSpan, PagedDataSource pagedDataSource)
{
if (pagedDataSource.DataSource.Cast<object>().Any(i => i != null) && pagedDataSource.IsPagingEnabled && (MockItemCount != pagedDataSource.VirtualCount))
{
pagedDataSource.AllowCustomPaging = true;
pagedDataSource.VirtualCount = MockItemCount;
pagedDataSource.CurrentPageIndex = MockPageIndex;
}
base.InitializePager(row, columnSpan, pagedDataSource);
}

Now all seems to work fine.