Watch the video here
This article demonstrates how to implement a WebDataGrid custom pager with the following features:
This article will teach you how to extend the WebDataGrid’s default paging behavior and integrate a WebSlider control drive changes to the grid’s page size.
To begin, create a page and name it Pager.aspx and add a ScriptManager and drag a WebDataGrid to the page. Next switch to design view and open the Smart Tag on the grid and click on Edit Behaviors. Once the Smart Tag is open, check the checkbox next to Paging.
Once the paging behavior is turned on, switch back to the markup and locate the <ig:Paging> nodes and add a <PagerTemplate> node. Your code should now look like this:
<form id="form1" runat="server"> <asp:ScriptManager ID="sm" runat="server" /> <ig:WebDataGrid ID="wdg" Width="650" runat="server"> <Behaviors> <ig:Paging PageSize="5"> <PagerTemplate> </PagerTemplate> </ig:Paging> </Behaviors> </ig:WebDataGrid> </form>
Notice that the width of the grid is constrained to 650 pixels. This is necessary to make sure there is enough room on the page to the right of the grid for the slider that will allow the user to adjust the page size.
Before continuing let’s take a moment to review some of the nuances of the event methods found in ASP.NET pages and user controls. While Page_Load is certainly the most famous page event method, there are a few others that are worthwhile being familiar. For an in-depth discussion on the different methods available on an ASP.NET page, check out this podcast: Using the ASP.NET Page Lifecycle.
For the solution presented in this article, you will use each of the below methods for different reasons:
The event methods created from events from controls you add to the page fire in between page load and pre-render. This gives you a reliable path to prepare data on load, manipulate with UI controls and finally make last-minute formatting decisions during pre-render.
Open the code behind to bind your grid to a data source. In this case the grid is binding to a collection from the BookRepository.
Note: For more information on the BookRepository, please check out: The Illusion of Persistence: Saving Test Data
The Page_Load event method will handle filling up a member variable with the data from the repository. The Pre_Render event method is responsible for binding the data to the grid.
Enter the following code in Pager.aspx.cs:
protected BookCollection _books; ... protected void Page_Load(object sender, EventArgs e) { this._books = BookRepository.Instance.GetBooks(100); } protected void Page_PreRender(object sender, EventArgs e) { this.wdg.DataSource = this._books; this.wdg.DataBind(); }
Run Pager.aspx and you should see a grid filled with data.
Create a new user control in the same folder as Pager.aspx and name it CustomPager.ascx.
The following image gives you an idea of what the finished product looks like.
In order to achieve the features found in this pager you must begin by adding the appropriate controls to the user control.
Enter the following markup into CustomPager.aspx:
<div style="width:100%;text-align:center;"> <div> <asp:Literal ID="litFirstPage" Visible="false" runat="server" /> <asp:linkbutton CommandArgument="first" OnClick="changePage" ID="btnFirstPage" Text="&lt;&lt;" runat="server" /> <asp:Literal ID="litPrevPage" Visible="false" runat="server" /> <asp:linkbutton CommandArgument="prev" OnClick="changePage" ID="btnPrevPage" Text="&lt;" runat="server" /> Page <asp:DropDownList ID="ddlPageNumbers" AutoPostBack="true" runat="server" onselectedindexchanged="changePage" /> of <%= TotalNumberOfPages.ToString() %> (<%= TotalRecordCount.ToString() %> items) <asp:Literal ID="litNextPage" runat="server" /> <asp:LinkButton CommandArgument="next" OnClick="changePage" ID="btnNextPage" Text="&gt;" runat="server" /> <asp:Literal ID="litLastPage" runat="server" /> <asp:LinkButton CommandArgument="last" OnClick="changePage" ID="btnLastPage" Text="&gt;&gt;" runat="server" /> </div> </div>
There is a lot of markup here, so we’ll step through each part with care. The first part of this markup is a simple DIV responsible for centering the controls in the paging template. After looking carefully at the code you will notice a Literal/LinkButton pair pattern of controls. The arrows on the pager will at certain times be enabled and other times disabled. For instance, when you are on the last page the link to go to the last page should not be enabled. The control’s visibility are alternately toggled depending on the context of the control.
The next section of pager is the set of controls that exposes a DropDownList and is accompanied by some expressions to expose the number of pages and total number of records. You will implement the properties needed to support these expressions in the code-behind later in this article.
Be aware that often in ASP.NET development each server control has it’s own event method in the code-behind. In the interest of simplifying the code for this solution, notice that each LinkButton and the DropDownList are pointing to the the changePage method in their respective event handlers. The code-behind will implement some simple logic to determine which control is initiating the command and what the appropriate action is to take.
Also notice that on each LinkButton the command argument is either: first, prev, next or last. These arguments will drive the logic used to help change the page later in this article.
The DropDownList must have it’s AutoPostBack property set to true to allow the paging template to immediately respond to the user’s request to change the page.
Switch to the code-behind in CustomPager.ascx.cs to begin implementing the logic for this page.
For now you must add a stub for the changePage method.
protected void changePage(object sender, EventArgs e) { }
The body of this method will be added later in the article, but for now this stub will keep your page from experiencing compilation errors.
Initially the user control must know about the context of the grid that it is hosted. To get the information from it’s context and to provide the properties that are exposed in the above markup to report the total number of pages and total number of records, enter the following code:
public int TotalRecordCount { get; set; } public int PageSize { get; set; } public void SetContext(int totalRecordCount, int initialPageSize) { this.TotalRecordCount = totalRecordCount; this.PageSize = initialPageSize; }
This code simply injects the context-data needed for the control into the appropriate properties.
Next you must add two properties that simplify the work of dealing with page numbers. At different times in your code you will want to use the page number and other times the page index. While these values essentially point to the same information, having to continually use one property and increment or decrement the value depending on the situation creates sloppy code. Therefore implementing these two properties helps keep the code readable and succinct.
Enter the following code into the code-behind:
protected int PageNumber { get { if (this.ViewState["PageNumber"] != null) { return Convert.ToInt32(this.ViewState["PageNumber"]); } else { this.ViewState.Add("PageNumber", 1); return 1; } } set { if (this.ViewState["PageNumber"] != null) { this.ViewState["PageNumber"] = value; } else { this.ViewState.Add("PageNumber", value); } } } protected int PageIndex { get { if (this.PageNumber > 1) { return this.PageNumber - 1; } else { return 0; } } }
The PageNumber properties uses ViewState to persist it’s value between requests to the page. If the ViewState entry does not exist the code presumes that the grid should begin on page one. The PageIndex property imply looks at the PageNumber and tries to figure out what the appropriate index is for the current page.
Next create another helper property that will return the total number of pages:
protected int TotalNumberOfPages { get { return (this.TotalRecordCount / this.PageSize); } }
This property simply divides the total records by the page size to come up with the total number of pages.
Next you will create some helper methods that are called during Page_PreRender to format the UI controls just before the page is released back to the browser. If you recall, the pager template featured a drop-down list that allows the user to choose a page to navigate to directly. The drop-down must be cleared and then re-filled upon each request to accommodate any changes in page size and thus the number of pages in the list.
Create the following method in your code-behind:
private void FillPageNumbersList() { this.ddlPageNumbers.Items.Clear(); for (int i = 1; i <= this.TotalNumberOfPages; i++) { this.ddlPageNumbers.Items.Add(new ListItem(i.ToString(), i.ToString())); } }
Notice that the text and value attributes have the same value: the number of the page. This makes the UI clear as the drop-down is set in context of a sentence telling the user which page is selected and the value is easily retrieved from the control for later use.
Now that the drop-down is filled with the appropriate items, you need to make sure the selected page is marked within the drop-down. To do this, enter the following method:
private void FormatPageNumbersList() { if (this.ddlPageNumbers.Items.Count >= this.PageNumber) { this.ddlPageNumbers.SelectedIndex = this.PageIndex; } else { this.ddlPageNumbers.SelectedIndex = 0; } }
This method sets the drop-down’s selected index equal to the PageIndex as long as there are more items than the selected page number.
Next, for the arrow controls, there is some logic required to make sure the arrows show up either as text or links, depending on the content.
Enter the following method into your code-behind for CustomPager.ascx:
private void FormatArrowControls() { int pgNo = this.PageNumber; this.btnFirstPage.Visible = (pgNo != 1); this.btnPrevPage.Visible = (pgNo != 1); this.btnNextPage.Visible = (pgNo != this.TotalNumberOfPages); this.btnLastPage.Visible = (pgNo != this.TotalNumberOfPages); this.litFirstPage.Text = this.btnFirstPage.Text; this.litPrevPage.Text = this.btnPrevPage.Text; this.litNextPage.Text = this.btnNextPage.Text; this.litLastPage.Text = this.btnLastPage.Text; this.litFirstPage.Visible = !this.btnFirstPage.Visible; this.litPrevPage.Visible = !this.btnPrevPage.Visible; this.litNextPage.Visible = !this.btnNextPage.Visible; this.litLastPage.Visible = !this.btnLastPage.Visible; }
The first order of business is to set aside the value for PageNumber. If you remember that the PageNumber property is implemented as a wrapper around a ViewState entry. Instead of forcing the page to convert the un-typed data in ViewState back to an integer each time the value is required, this method sets aside the value into a local variable.
Once the page number is located each button’s visibility is set depending on how it must appear based on the page number. Recall from the template that you implemented pairs of links and literals to show the arrows as links or as simple text. The second section of this method will set the value of the corresponding literals to what is entered in the LinkButtons. This way whatever text is set for the LinkButtons, the Literal controls will automatically get the same Text value. Finally, the last section reverses the visibility on the literals from how the links were previously set. This ensures only one control out of each pair will appear on the page.
The next step is to call the three methods you just implemented from the Page_PreRender method in CustomPager.ascx:
protected void Page_PreRender(object sender, EventArgs e) { this.FillPageNumbersList(); this.FormatPageNumbersList(); this.FormatArrowControls(); }
Now that the pager control is starting to take shape return to Pager.aspx and switch to Design Mode. Drag the user control CustomPager.ascx on to the design surface for Pager.aspx. Switch to the markup view and move the user control markup in between the <PagerTemplate> elements.
Your markup should now look like this:
<ig:WebDataGrid ID="wdg" Width="650" runat="server"> <Behaviors> <ig:Paging PageSize="5"> <PagerTemplate> <uc1:CustomPager ID="customPager" runat="server" /> </PagerTemplate> </ig:Paging> </Behaviors> </ig:WebDataGrid>
Note: To keep the naming clean the ID is now customPager not customPager1
Now that the user control is nested inside the WebDataGrid, Visual Studio will no longer generate a protected property that grants automatic access to the control in the code-behind. Therefore what you need to do is open the code-behind and provide programmatic access to the instance of the user control. Todo this, you will use the Page_Init method to setup a local member variable that points to the custom pager control.
Drop to the code-behind of Pager.aspx and enter the following code:
private WebDataGrid_CustomPaging_test_CustomPager _customPager; protected void Page_Init(object sender, EventArgs e) { this._customPager = this.wdg.Behaviors.Paging.PagerTemplateContainer.FindControl("customPager") as WebDataGrid_CustomPaging_test_CustomPager; }
First declare a private member typed as the pager control. The type name for this control is easiest found by opening the user control’s code-behind file and copying the type name and pasting it here. Be sure you have the proper type name for the _customPager variable. The directory structure in your solution is likely different from what you see above and will need what matches your class.
Next, in the Page_Init you must drill into the WebDataGrid’s Paging behavior and call FindControl to locate the instance of the pager control inside the grid. Lastly you must cast the returning object into the same type as the pager template.
Now that you have an instance of the custom pager, you can call the SetContext method to provide the control with its initial values. Update Page_Load to set the control context. Your Page_Load should now look like this:
protected void Page_Load(object sender, EventArgs e) { this._books = BookRepository.Instance.GetBooks(100); this._customPager.SetContext(this._books.Count, this.wdg.Behaviors.Paging.PageSize); }
The SetContext method requires the result set count and the initial page size. These values are passed in from the collection class count and the page size of the grid.
The approach used in this solution that notifies the host page, where the grid is instantiated, it to raise an event telling the page to take some sort of action. The following code implements an EventArgs class that will carry the information required to allow the grid to respond to changes in page numbers and page sizes.
Create a new class in the App_Code folder (or in the root of your project if you don’t have an App_Code folder) and name it PageSettingsChangedEventArgs.cs.
Enter the code for this class as:
public class PageSettingsChangedEventArgs : System.EventArgs { public int PageNumber { get; set; } public int PageSize { get; set; } public int TotalNumberOfPages { get; set; } public int PageIndex { get { return this.PageNumber - 1; } } public PageSettingsChangedEventArgs() { } }
This class inherits from System.EventArgs so it may be used to raise events. The class explicitly tracks the PageNumber, PageSize and TotalNumberOfPages. The PageIndex is calculated from the PageNumber. At different times each of these properties are used to send messages to the grid.
Previously in the article you created a stub for the changePage method. Now that some of the supporting properties are now implemented in your user control you can now proceed to filling out the body of the changePage method.
Begin by opening the code-behind for CustomPager.ascx. The following code will replace the stub you have for changePage:
protected void changePage(object sender, EventArgs e) { if (sender is DropDownList) { this.PageNumber = Convert.ToInt32(((DropDownList)sender).SelectedValue); } else if (sender is LinkButton) { switch (((LinkButton)sender).CommandArgument) { case "first": this.PageNumber = 1; break; case "prev": this.PageNumber--; break; case "next": this.PageNumber++; break; case "last": this.PageNumber = this.TotalNumberOfPages; break; default: break; } } else { throw new NotImplementedException("changePage is not implemented for " + sender.GetType().Name); } this.RaiseChangePageEvent(this.PageNumber); } public event EventHandler<PageSettingsChangedEventArgs> PageChanged; private void RaiseChangePageEvent(int pageNumber) { if (this.PageChanged != null) { this.PageChanged(this, new PageSettingsChangedEventArgs() { PageNumber = this.PageNumber, TotalNumberOfPages = this.TotalNumberOfPages }); } }
Starting from the top, the changePage method’s body is now responsible for sending messages out that the page number is changed, no matter what type of control the change was initiated.
Line 3 attempts to look at the type of the sender argument to see if the DropDownList initiated the request. If the change came from the drop-down, then the code reaches into the SelectedValue property and sets it to the PageNumber property.
If the request is not originating from the drop-down then it tries to see if perhaps a LinkButton was used to call the method. Recall from the markup for the pager template’s UI controls, each of the LinkButtons had a CommandArgument that matches each item in the switch statement beginning on line 9. From there the switch statement takes the appropriate action based on how the user is expecting the page number to change.
Line 29 attempts to warn the the user if a new control that is not accounted for in this procedure tries to call the changePage method. (This exception will likely only seen by developer who may be tasked with updating the control.)
Line 32 calls a method that is responsible for raising the event signaling that the page number is changed.
Line 35 declares the PageChanged event using a generic EventHandler typed as the new EventArgs class you previously created in this section.
Finally, line 37 contains the method for doing the work of raising the event. In standard C# notation, the object must first check to see if any other objects have subscribed to the event the method is preparing to raise. If there are objects observing this event, then the event is raised and the custom EventArgs class is filled with the new PageNumber and the updated TotalNumber of pages.
Once this method is fired code in Pager.aspx will handle the event. Return to the code-behind for Pager.aspx and update the Page_Init method to match the following code:
protected void Page_Init(object sender, EventArgs e) { this._customPager = this.wdg.Behaviors.Paging.PagerTemplateContainer.FindControl("customPager") as WebDataGrid_CustomPaging_test_CustomPager; this._customPager.PageChanged += new EventHandler<PageSettingsChangedEventArgs>(_customPager_PageChanged); } void _customPager_PageChanged(object sender, PageSettingsChangedEventArgs e) { if ((e.TotalNumberOfPages -1) < e.PageIndex) { this.wdg.Behaviors.Paging.PageIndex = 0; } else { this.wdg.Behaviors.Paging.PageIndex = e.PageIndex; } }
What’s added here is the PageChanged event is being handled by a new event handler named _customPage_PageChanged. The code for this method is simple, the grid’s PageIndex is set to what is passed in from the event arguments, found on like 15 in the above code listing.
There is some logic before the page index is set. This logic exists to protect from a bug that could arise from a feature you haven’t implemented yet in this article. Consider the situation where the grids page size is set to a small value like 5 and then the user navigated to page 20. If the user then switched the page size to a larger value, like 25, then the 20th page may no longer exists. The data is being split up among the pages differently depending on the number of records and the page size. Therefore the code between lines 9 and 12 guard the page from trying to set the current page that does not exist by returning to page 1 if the PageIndex is outside the bounds of the number of pages available.
Now run Pager.aspx and try changing pages via the LinkButtons and DropDownList.
The page size is manipulated by changing the values of a WebSlider control. Begin by dragging a WebSlider on to the CustomPager.ascx. To fill out the properties of the WebSilder, update your markup to match the following code:
<div id="pageSlider"> <span class="slider-label">Page<br />Size</span> <cc1:WebSlider ID="slPageSize" runat="server" Height="100px" Orientation="Vertical" LargeChangeAsString="5" SmallChangeAsString="5" ToolTip="Page Size" MaxValueAsString="25" MinValueAsString="5" ValueType="Int" ThumbsInteractionMode="Lock" onvaluechanged="slPageSize_ValueChanged" ShowPlusMinusButtons="False" SnapToSmallChange="True"> <AutoPostBackFlags ValueChanged="On" /> </cc1:WebSlider> </div>
The opening DIV found on line 1 of the above listing should come directly after the closing DIV tag that surrounds the set of Literal and LinkButton controls implemented in the previous section. The DIV will also get some styling from a style sheet entry you are soon to create.
The next set of elements are a simple label to give a hint to the user as to the purpose of the slider.
The many of slider’s properties are self-explanatory, so only this discussion will only focus on items significant to it’s behavior. The orientation is set to vertical so the slider may be comfortably placed next to the side of the grid. The LargeChangeAsString and SmallChangeAsString are constrained to 5 unit increments to force the slider to change with 5 possible points. The idea here is to only allow the slider to be set to a pre-defined number of values making it easy to work with the data and make the change easily understood by the user. The small set of values is enforced by the MaxValueAsString and MinValueAsString. So in the end the page size may be 5 items at a minimum and 25 items at most. Then page sizes in between may only be 10, 15 or 20 items per page.
The properties ThumbsInteractionMode and SnapToSmallChange are set appropriately to allow the slider to naturally move to the next increment. This approach works well in this situation because the user doesn’t have to be precise about clicking on such a small control – the control will move it to the closest acceptable value based on where the user clicked on the control.
The WebSlider control by default features buttons at the edges of the slider to provide fine-grained control over changing the values. In this case these buttons are not necessary and are hidden from the user.
The AutoPostBackFlags have the ValueChanged event turned on so when a change to the slider is made the page will respond to the event.
Finally the slider’s onvaluechanged event handler is pointing to the slPageSize_ValueChanged method. Open the code-behind of CustomPager.ascx and enter the following code:
public event EventHandler<PageSettingsChangedEventArgs> PageSizeChanged; protected void slPageSize_ValueChanged(object sender, Infragistics.Web.UI.EditorControls.SliderValueChangedEventArgs e) { if (this.PageSizeChanged != null) { this.PageSize = Convert.ToInt32(this.slPageSize.Value); if (this.TotalNumberOfPages < this.PageNumber) { this.RaiseChangePageEvent(1); } this.PageSizeChanged(this, new PageSettingsChangedEventArgs() { PageSize = this.PageSize, TotalNumberOfPages = this.TotalNumberOfPages }); } }
First, the PageSizeChanged event is declared in the control so the slider’s event hander can raise this event which will bubble up to the page. Inside slPageSize_ValueChanged the code checks to see if any objects are subscribed to the event before raising the event. The value for PageSize is found by interrogating the Value property of the slider.
Lines 9 through 12 of this code listing exist to protect against a user trying to navigate to a page doesn’t exist because of a page size/page count mismatch. For more information see the explanation found under Dealing with Page Size/Page Count Mismatches in the previous section.
Lastly the PageSizeChanged event is raised passing in the new PageSize and updated NumberOfPages.
To complete the logic required, the host page must handle the PageSizeChanged event. Return to web page’s code-behind found at Pager.aspx.cs. First you must update the Page_Init method to match the following code:
protected void Page_Init(object sender, EventArgs e) { this._customPager = this.wdg.Behaviors.Paging.PagerTemplateContainer.FindControl("customPager") as WebDataGrid_CustomPaging_test_CustomPager; this._customPager.PageChanged += new EventHandler<PageSettingsChangedEventArgs>(_customPager_PageChanged); this._customPager.PageSizeChanged += new EventHandler<PageSettingsChangedEventArgs>(_customPager_PageSizeChanged); } void _customPager_PageSizeChanged(object sender, PageSettingsChangedEventArgs e) { this.wdg.Behaviors.Paging.PageSize = e.PageSize; if ((e.TotalNumberOfPages - 1) < e.PageIndex) { this.wdg.Behaviors.Paging.PageIndex = 0; } }
Line 5 in the above listing is the only line in the Page_Init method that is different from before. This line subscribes to the pager control’s PageSizeChanged event.
The event handler is found in lines 8 through 16. When the page size is changed the new page size is extracted from the event arguments and set to the grids PageSize property found off the Paging behavior.
Lines 12 through 15 implement the required checking for out-of-bound page indices, which should now be familiar to you.
The final step in the process is to update the style sheet to give the slider its position and formatting. Open the markup for Pager.aspx and add the following style block directly after the close of the <title> tag:
<style type="text/css"> #pageSlider { position:absolute; top:20px; left:650px; width:80px; } .slider-label { font-size:.8em;text-align:center; } </style>
With this styling in place you may now run your page and the custom pager template is fully functional!
In this article you learned to implement a custom pager template which allows the user to change pages and page sizes, reports the current page number, total number of page and total number of records. You learned to extend the built-in paging features of the WebDataGrid and use the WebSlider to power the page size.