So you're hacking away with your
GridView and you're amazing. You're like a programming god. You've got your
GridView displaying hundreds of pages of easily sorted data. Your product owner asks you for a specific kind of paging. No problem! You've got every pager known to man! You've got numeric, first last next previous, numeric first last, next previous. You tell your product owner, whatever they want, it's no problem. They want next previous numeric. Now, what is that?
Using the code
Please someone tell me how it is that Microsoft has *still* not implemented this trivial looking pager!? Seriously, these are the built-in choices:
You get a cookie if you notice the one option they didn't include here. Ummm... hmmm. Yeah! NumericNextPrevious!!! Why not? I don't know. In this article, I'm going to provide you with the template I've created (as well as the unit tests that were used to build it), and explain how it works.
In case you didn't notice, this is actually a
GridView and a custom pager. I made some tweaks to my Enhanced GridView to support very simple integration of this new pager. You just have a setting on the
EnableNextPrevNumericPager="true" and voila! For those of you who care about the how's and unit tests, please read on.
public class NextPrevNumericPagerTemplate : ITemplate
public void InstantiateIn(Control container)
int pagerStartIndex = startPageIndex(_pageIndex, _pageCount);
int pagerEndIndex = endPageIndex(_pageIndex, _pageCount);
createCorrectPageButtons(container, pagerStartIndex, pagerEndIndex);
So, that's really the core of the code. Basically, it will create a pager that looks like this:
Page 12 of 31
<a href="#">< Prev</a> <a href="#">...</a> <a href="#">11</a> 12 <a href="#">13</a> <a href="#">14</a> <a href="#">15</a> <a href="#">16</a> <a href="#">17</a> <a href="#">18</a> <a href="#">19</a> <a href="#">20</a> <a href="#">...</a> <a href="#">Next ></a>
The class itself inherits from the
ITemplate interface, and the method that we use is the
InstantiateIn method. All of the controls we want in our
GridView's pager, we add to the control variable that the
InstantiateIn method takes as a parameter. Where the code says
createResultsSummary, that's where we create the part that tells us what page we are on. It takes in the control and updates it. Since it is updating the control directly, we don't need it to return any value.
spacer method adds a space to the control. Please ignore the inconsistency of this being the only time at this level that we need a spacer. I will remedy that at a future time. The
createPrevEllipsisIfNeeded only adds the ellipsis if there are results previous to the set of pages you are looking at etc. etc.
The most important methods being called here are actually the
endPageIndex. They determine what will be the start and end range of the pages that will show up in our pager. This was really where I leveraged TDD. It helps to program in a couple of test cases and see if the code behaves how you'd like, make changes to your code if it doesn't, and repeat until your tests go green. I'll show one of those methods now as well as one of its unit tests.
public void PagerTemplate_startPageIndex_CeilingFloorTest()
int totalPageCount = 6;
int currentPageIndex = 5;
int expected = 0;
int actual = accessor.startPageIndex(currentPageIndex, totalPageCount);
Assert.AreEqual(expected, actual, "The expected value was not returned.");
This unit test basically describes what my start and end range should be when the currently viewed page is near the high end of a small range. This particular test was coded to reproduce a bug I found while doing some manual testing. In English, it says, if you're on page 6 (
currentPageIndex+1) of six total pages, I expect the starting page number to be one. In my tests before I fixed this, the lower end was -3... hehe. Yeah and by the way, I know what you're thinking... expecting to see some NUnit? Not anymore. Unfortunately, I'm working to embrace Microsoft's Team System unit testing framework, so I'm going to be showing less if not no more of the NUnit tests.
private int startPageIndex(int currentPageIndex, int totalPageCount)
int startingPageToDisplay = 0;
startingPageToDisplay = currentPageIndex - 4;
if ((pageIndex(totalPageCount) - currentPageIndex) < 5)
startingPageToDisplay = pageIndex(totalPageCount) - 9;
if (startingPageToDisplay < 0)
startingPageToDisplay = 0;
Hopefully, this helps you see why unit tests can be a life saver. This code basically gives the following rules:
- (Assumed throughout the code) There are only to be, at most, ten pages displayed on the pager at one time.
- The current page should be as close to the center of the range as possible. This is the reason we subtracted 4 from the starting page. We can either have 4 or 5 pages on the left to keep this as centered as possible.
- I'd rather see more pages to the right of the current page. This is the reason I chose to have only 4 on the left side, which will leave 5 pages for the right side.
- If the last page index and the current page index are less than 5 pages apart, then show all the pages to the left (up to the maximum). This is where the bug was happening. To account for that, I added the following, which passed my unit tests.
- If our starting index is less than zero, then it should be set to zero. We'll just have less than ten pages to show.
Now, as far as using this with my
GridView goes... all you should have to do is set the custom pager template of your
GridView to an instance of this class. It's the same thing to get this to work with your custom
GridView as well. I just needed to override the correct event. This is all the code I needed to add to my Enhanced GridView to hook the two up together:
protected override void InitializePager(GridViewRow row, int columnSpan,
base.PagerTemplate = new NextPrevNumericPagerTemplate(this.PageIndex,
base.InitializePager(row, columnSpan, pagedDataSource);
Points of interest
If you'll notice, I'm utilizing unit tests on private methods. The reason for this is I am trying my best to use Test Driven Development more and more. If I am writing code that requires design, I'd rather do that via tests. There may be an argument made for methods being made to be simpler or perhaps moved to a different class. That's possible. I just know that there are plenty of instances where testing private methods made sense to me and making them into new classes did not.
I've also heard that private code changes a lot more than the public code, and it can become a hassle to keep the tests up to date. In my opinion, those tests should be updated first. You should be programming your expectations into those tests and then programming the implementation into your code. That's my two cents on this controversy, for the mean time. Take it for what it's worth.
I like to keep my articles fairly concise, focusing on giving you the gist of how I programmed things. I'm assuming an intermediate to advanced level developer is reading this (creating custom server controls isn't a beginner's task), and I try my best to explain what I do explain at the level of an intermediate beginner. I'd like to explain more than I think is necessary to ensure I don't glance over complexities. If you have any questions, feel free to give me a holler!