asp tutorials, asp.net tutorials, sample code, and Microsoft news from 15Seconds
Data Access  |   Troubleshooting  |   Security  |   Performance  |   ADSI  |   Upload  |   Email  |   Control Building  |   Component Building  |   Forms  |   XML  |   Web Services  |   ASP.NET  |   .NET Features  |   .NET 2.0  |   App Development  |   App Architecture  |   IIS  |   Wireless
 
Pioneering Active Server
 Power Search





Active News
15 Seconds Weekly Newsletter
• Complete Coverage
• Site Updates
• Upcoming Features

More Free Newsletters
Reference
News
Articles
Archive
Writers
Code Samples
Components
Tools
FAQ
Feedback
Books
Links
DL Archives
Community
Messageboard
List Servers
Mailing List
WebHosts
Consultants
Tech Jobs
15 Seconds
Home
Site Map
Press
Legal
Privacy Policy
internet.commerce














internet.com
IT
Developer
Internet News
Small Business
Personal Technology
International

Search internet.com
Advertise
Corporate Info
Newsletters
Tech Jobs
E-mail Offers

HardwareCentral
Compare products, prices, and stores at Hardware Central!

Creating a Generic Pager Control
By Tomasz Kaszuba
Rating: 4.1 out of 5
Rate this article


  • email this article to a colleague
  • suggest an article


    Effectively showing data so it doesn't confuse the end user is a main objective in developing almost all Web data presentation applications. Showing 20 records on one page is bearable but showing 10,000 certainly can be confusing. Splitting the data across several pages, or paging the data, is a commonly employed solution to this problem.

    ASP.NET provides only one control that supports paging, the DataGrid. The DataGrid pager control is fine for intranet applications but for public applications, the DataGrid pager doesn't provide much of the functionality needed to make flexible Web applications. For one thing, the DataGrid control limits the Web designer in where he can place the pager or what it should look like. For example, the DataGrid certainly doesn't allow the designer the option of placing the pager vertically. Another control that can benefit from paging is the repeater control. The repeater control allows the Web developer to quickly configure how data is shown, but the paging behavior must be implemented by the Web developer. Implementing a custom pager for different controls that change depending on the data source or the presentation can be time consuming. A generic pager control that's not bound to a specific presentation control is a great time saver. A good generic pager control is more than just a data pager; it should also provide the following functionality:

    1. Provide First, Previous, Next, Last and paging buttons
    2. Be sensitive to the data. If the pager is set to show 10 records per page and only nine are shown, then the pager shouldn't be visible. On the first page the Previous and First buttons shouldn't be shown. On the last page the Next and the Last methods shouldn't be shown.
    3. Independent of the control that's in charge of the presentation
    4. The ability to handle a variety of current and future data sources
    5. Easily configurable presentation to integrate with custom applications
    6. Notify other controls when paging is taking place
    7. Easy to use even by inexperienced Web designers
    8. Provide properties to relevant paging data

    There are a few commercial pagers available that provide such functionality, but they don't come cheap. For cash-strapped Web companies creating a custom pager control becomes a necessity.

    ASP.NET provides three ways to create custom Web controls: user controls, composite controls, and custom controls. The third type of control, custom control, is a bit misleading. All of the mentioned controls are actually custom controls but what makes the composite control different from the custom control is the dependence on the CreateChildControls() method, which allows the control to re-render itself based on raised events. For the generic pager, the composite control model is chosen.

    The following sequence diagram outlines the general mechanism of the pager control.

    Even thought the pager control aims to be independent of the presentation control, it must have some way to access the data. Every class that derives from the Control class provides a DataBinding event. By registering itself as a listener to the DataBinding event, the pager can listen-in and make changes to the data. Since all controls that derive from the Control class posses this event, the pager control achieves independence from the presentation control. In other words, any control that derives form the Control class, almost all Web controls, can be bound to. Once the presentation control raises the DataBinding event, the pager control can intercept the DataSource property. Unfortunately MS does not provide an interface that all data bound classes implement, such as an IdataSourceProvider, and not all controls derived from the Control or WebControl class provide a DataSource property, so upcasting to the Control class isn't an option. The only alternative is to use reflection to manipulate the DataSource property directly. Before discussing the event handler method, it should be pointed out that in order to register as an event listener, a reference must be established to the presentation control. The pager control exposes a simple string property, BindToControl, which the Web developer can set through code or through the aspx page to bind the DataSource to the presentation control.

    publicstring BindToControl

    {

    get

          {

    if (_bindcontrol == null)

                thrownew NullReferenceException("You must bind to a control through the BindToControl property before you use the pager");

    return _bindcontrol;

    }

          set{_bindcontrol=value;}

    }

    This method is important enough that it's a good idea to throw a more meaningful message than a standard NullReferenceException. In the pager's OnInit event handler the call to resolve the reference to the presentation control is made. The OnInit event handler must be used (instead of the constructor) to make sure that the JIT compiled aspx page has set the BindToControl method.

    protectedoverridevoid OnInit(EventArgs e)

    {

          _boundcontrol = Parent.FindControl(BindToControl);

    BoundControl.DataBinding += new EventHandler(BoundControl_DataBound);

          base.OnInit(e);

    }

    The search for the presentation control is made by searching the pager's Parent control, which in the case of this article is the main page template. There is much danger in using the Parent property in this manner. If, for example, the pager were to be imbedded into another control, such as a Table control, the call to the Parent property would return a reference to the Table control. Since the FindControl method only searches the current control collection, the presentation control won't be found unless it's in that collection. A safer method is to recursively search through each control's control collection until the control is found.

    Once the BoundControl is found, the pager is registered as listener to the DataBinding event. Since the pager control manipulates the data source, it's important that this event handler be the last in the calling chain. As long as the presentation control registers its event handlers for the DataBinding event in the OnInit event handler (the default), there won't be a problem when the pager manipulates the data source.

    The DataBound event handler takes care of the acquisition of the DataSource property of the presentation control.

    privatevoid BoundControl_DataBound(object sender,System.EventArgs e)

    {

          Type type = sender.GetType();

          _datasource = type.GetProperty("DataSource");

          if (_datasource == null)

                thrownew NotSupportedException("The Pager control doesn't support controls that don't contain a datasource");

    object data = _datasource.GetGetMethod().Invoke(sender,null);

          BindParent();

    }

    Through reflection, the call is made to invoke the Get part of the DataSource property and return a reference to the actual data source. The data source is now known but the pager still needs to know how to manipulate it. A lot of effort went into making the pager presentation independent. Making it data source dependent would defeat the purpose of constructing a flexible control. A pluggable architecture ensures that the pager control will be able to handle all sorts of different data sources, .NET provided or custom.

    The perfect solution to provide a robust, scalable and pluggable architecture can be achieved by using the [GoF] builder pattern.

    The IDataSourceAdapter interface defines the most basic element, or plug, the pager needs to manipulate the data.

    publicinterface IDataSourceAdapter

    {

          int TotalCount{get;}

          object GetPagedData(int start,int end);

    }

    The TotalCount property returns the total number of elements in the data source before manipulating the data while the GetPagedData method manipulates the data source by returning a subset of the original data. For example: if the data source is a simple array containing 20 elements and the pager shows 10 elements per page, then a subset of this data would be elements 0-9 for page one and 10-19 for page two. A plug for a DataView type is provided by the DataViewAdapter.

    internalclass DataViewAdapter:IDataSourceAdapter

    {

          private DataView _view;

          internal DataViewAdapter(DataView view)

          {

                _view = view;

          }

          publicint TotalCount

          {

                get{return (_view == null) ? 0 : _view.Table.Rows.Count;}         }

          publicobject GetPagedData(int start, int end)

          {

                DataTable table = _view.Table.Clone();

                for (int i = start;i<=end && i<= TotalCount;i++)

                {

                      table.ImportRow(_view[i-1].Row);

                }

                return table;

          }

    }

    The DataViewAdapter implements the IDataSourceAdapter's GetPagedData method by cloning the original DataTable and then importing rows from the original DataTable to the cloned table. The class's visibility is intentionally set to internal in order to hide the implementation from the Web developer and provide an easier interface through the Builder classes.

    publicabstractclass AdapterBuilder

    {

          privateobject _source;

          privatevoid CheckForNull()

          {

    if (_source == null) thrownew NullReferenceException("You must provide a valid source");

          }

          publicvirtualobject Source

          {

                get

                {

                      CheckForNull();

                      return _source;}

                set

                {

                      _source = value;

                      CheckForNull();

                }

          }

          publicabstract IDataSourceAdapter Adapter{get;}

    }

    The abstract AdapterBuilder class provides a more manageable interface to the IdataSourceAdapter type. By employing an extra level of abstraction, instead of using the IDataSourceAdapter directly, it provides an extra layer where pre processing instructions, before paging the data, can take place. The builder also allows the actual implementation, such as the DataViewAdapter to be hidden away from the pager user.

    publicclass DataTableAdapterBuilder:AdapterBuilder

    {

          private DataViewAdapter _adapter;

          private DataViewAdapter ViewAdapter

          {

                get

                {

                      if (_adapter == null)

                      {

                            DataTable table = (DataTable)Source;

    _adapter = new DataViewAdapter(table.DefaultView);

                      }

                      return _adapter;

                }

          }

          publicoverride IDataSourceAdapter Adapter

          {

                get{return ViewAdapter;}

          }

    }

     

    publicclass DataViewAdapterBuilder:AdapterBuilder

    {

          private DataViewAdapter _adapter;

          private DataViewAdapter ViewAdapter

          {

                get

                {   //lazy instantiate

                      if (_adapter == null)

                      {

    _adapter = new DataViewAdapter((DataView)Source);

                      }

                      return _adapter;

                }

          }

          publicoverride IDataSourceAdapter Adapter

          {

                get{return ViewAdapter;}

          }

    }

    The DataView type and the DataTable type are so closely related that it might make sense to make a general DataAdapter. It would be enough to add another constructor that handles a DataTable. Alas when the user needs different functionality for a DataTable, the entire class would have to be replaced or inherited. By constructing a new builder that uses the same IdataSourceAdapter, the user has more freedom on how they choose to implement the adapter.

    In the pager control, the look-up of the proper builder is handled by a type safe collection.

    publicclass AdapterCollection:DictionaryBase

    {

          privatestring GetKey(Type key)

          {

                return key.FullName;

          }

          public AdapterCollection()

          {}

          publicvoid Add(Type key,AdapterBuilder value)

          {

                Dictionary.Add(GetKey(key),value);

          }

          publicbool Contains(Type key)

          {

                return Dictionary.Contains(GetKey(key));

          }

          publicvoid Remove(Type key)

          {

                Dictionary.Remove(GetKey(key));

          }

    public AdapterBuilder this[Type key]

          {

                get{return (AdapterBuilder)Dictionary[GetKey(key)];}

                set{Dictionary[GetKey(key)]=value;}

          }

    }

    The AdapterCollection relies on the type of the DataSource, which fits in perfectly with the BoundControl_DataBound method. The index key used is the Type.FullName method, ensuring the index key is unique for each type. This puts the responsibility on the AdapterCollection to contain only one builder for a given type. Adding the builder look-up to the BoundControl_DataBound method results in the following:

    public AdapterCollection Adapters

    {

          get{return _adapters;}

    }

    privatebool HasParentControlCalledDataBinding

    {

          get{return _builder != null;}

    }

     

    privatevoid BoundControl_DataBound(object sender,System.EventArgs e)

    {

          if (HasParentControlCalledDataBinding) return;

          Type type = sender.GetType();

          _datasource = type.GetProperty("DataSource");

          if (_datasource == null)

                thrownew NotSupportedException("The Pager control doesn't support controls that don't contain a datasource");

          object data = _datasource.GetGetMethod().Invoke(sender,null);

     

          _builder = Adapters[data.GetType()];

          if (_builder == null)

                thrownew NullReferenceException("There is no adapter installed to handle a datasource of type "+data.GetType());

          _builder.Source = data;

     

          BindParent();

    }

    The BoundControl_DataBound method also checks to see if the builder is already created with the HasParentControlCalledDataBinding. If it has, then it doesn't go through the exercise of finding the proper builder again. This of course assumes that the user doesn't call DataBinding with different DataSources, which of course they shouldn't. The Adapters table is initialized in the constructor.

    public Pager()

    {

          _adapters = new AdapterCollection();

          _adapters.Add(typeof(DataTable),new DataTableAdapterBuilder());

          _adapters.Add(typeof(DataView),new DataViewAdapterBuilder());

    }

         

    The last method to implement is to call the BindParent to manipulate and return the data.

     

    privatevoid BindParent()

    {

          _datasource.GetSetMethod().Invoke(BoundControl,

    new object[] { _builder.Adapter.GetPagedData( StartRow,ResultsToShow*CurrentPage)});

    }

    The method is pretty simple since the actual manipulation of the data is done by the Adapter. Once finished, reflection is again used but this time to set the DataSource property of the presentation control. The behavior of the pager control is now nearly finished, but without proper presentation, it is not very useful.

    As stated earlier the best way to achieve a solid separation of presentation from logic is to use templates, or more specifically the Itemplate interface. Indeed, MS realizes the power of templates and employs them almost everywhere, even in the page parser itself. Templates unfortunately aren't the easiest mechanism, and they take a while to learn, but there are many tutorials available that should ease the learning curve. Getting back to the pager control, the pager control contains the following buttons, First, Previous, Next, Last plus the individual pagers. The four navigation buttons are chosen from the ImageButton class instead of the LinkButton class. From a professional Web design point of view an image is more often useful than just a link.

    public ImageButton FirstButton{get {return First;}}

    public ImageButton LastButton{get {return Last;}}

    public ImageButton PreviousButton{get {return Previous;}}

    public ImageButton NextButton{get {return Next;}}

    The individual pagers are created dynamically since they depend on the datasource and how many records and pagers should be shown per page. The pagers will be added to a Panel which allows the Web designer to specify where he would like to show the pagers. More on the creation of the pagers later, for now the pager control needs to provide a template to allow the user to customize the look and feel of the pager.

    [TemplateContainer(typeof(LayoutContainer))]

    public ITemplate Layout

    {

          get{return (_layout;}

          set{_layout =value;}

    }

     

    publicclass LayoutContainer:Control,INamingContainer

    {

          public LayoutContainer()

          {this.ID = "Page";}

    }

    The LayoutContainer class provides a holder for the template. The addition of the custom ID in a template container is always a good idea since it prevents problems that arise with events and how they're called by the page. The following UML diagram defines the presentation of the pager control.

    The first step in creating a template is to define a simple layout in the aspx page:

    <Layout>

    <asp:ImageButtonid="First"Runat="server" AlternateText="first"/>

    <asp:ImageButtonid="Previous"Runat="server"AlternateText="previous"/>

       <asp:ImageButtonid="Next"Runat="server"AlternateText="next"/>

       <asp:ImageButtonid="Last"Runat="server"AlternateText="last"/>

       <asp:PanelID="Pager"Runat="server"/>

    </Layout>

    For the purpose of this example, the layout doesn't contain any formatting, such as tables etc ... But they can and should be included as shown later.

    The Itemplate interface provides only one method, InstantiateIn, which parses the template and binds it with the holder.

    privatevoid InstantiateTemplate()

    {

          _container = new LayoutContainer();

          Layout.InstantiateIn(_container);

          First = (ImageButton)_container.FindControl("First");

          Previous = (ImageButton)_container.FindControl("Previous");

          Next = (ImageButton)_container.FindControl("Next");

          Last = (ImageButton)_container.FindControl("Last");

          Holder = (Panel)_container.FindControl("Pager");

    this.First.Click += new System.Web.UI.ImageClickEventHandler(this.First_Click);

    this.Last.Click += new System.Web.UI.ImageClickEventHandler(this.Last_Click);

    this.Next.Click += new System.Web.UI.ImageClickEventHandler(this.Next_Click);

    this.Previous.Click += new System.Web.UI.ImageClickEventHandler(this.Previous_Click);

    }

    The first thing the page control's InstatiateTemplate method does is instantiate the template, Layout.InstantiateIn(_container). The container is just another control that is used like any other control. Making use of this fact, the InstantiateTemplate method finds the four navigational buttons and the panel needed to hold the individual pagers. The buttons are found by their IDs. This is a small limitation placed on the pager control. The navigational buttons MUST have the pre-defined IDs, "First","Previous","Next","Last", "Pager", or they won't be found. Unfortunately this is the only alternative with the presentation structure chosen. The other alternative is for each of the buttons to inherit from the ImageButton class thereby defining a new type. Since each button will be of a different type then a recursive search through the container could be implemented to find a particular type forgoing the need to name the buttons properly. But with proper documentation such a small requirement shouldn't pose problems.

    Once the four buttons are found, then the proper event handlers are bound to them. A very important decision must be made as to when the InstantiateTemplate should be called. Normally such a method would be called in the CreateChildControls method, since it basically does just that, creates child controls. Since the pager control doesn't ever change its child controls, then it doesn't need the functionality provided by the CreateChildControls method to change its rendering state based on some event. The quicker the child controls get rendered the better. A good place then to call the instantiate method is in the OnInit event.

    protectedoverridevoid OnInit(EventArgs e)

    {

          _boundcontrol = Parent.FindControl(BindToControl);

    BoundControl.DataBinding += new EventHandler(BoundControl_DataBound);

          InstantiateTemplate();

          Controls.Add(_container);

          base.OnInit(e);

    }

    The OnInit method also performs a very important act, it adds the container to the pager control. Without adding the container to the pager's control collection then the template wouldn't be shown since the Render method would never be called. A template can also be defined programmatically by implementing the Itemplate interface. In step with providing a good flexible control, this feature can be used to provide a default template in case the user doesn't provide one in the aspx page.

    publicclass DefaultPagerLayout:ITemplate

    {

          private ImageButton Next;

          private ImageButton First;

          private ImageButton Last;

          private ImageButton Previous;

          private Panel Pager;

         

          public DefaultPagerLayout()

          {

                Next = new ImageButton();

                First = new ImageButton();

                Last = new ImageButton();

                Previous = new ImageButton();

                Pager = new Panel();

     

                Next.ID="Next"; Next.AlternateText="Next";

                First.ID="First"; First.AlternateText="First";

                Last.ID = "Last"; Last.AlternateText ="Last";

                Previous.ID="Previous"; Previous.AlternateText="Previous";

                Pager.ID="Pager";

          }

          publicvoid InstantiateIn(Control control)

          {

                control.Controls.Clear();

                Table table = new Table();

                table.BorderWidth = Unit.Pixel(0);

                table.CellSpacing= 1;

                table.CellPadding =0;

                TableRow row = new TableRow();

                row.VerticalAlign = VerticalAlign.Top;

                table.Rows.Add(row);

                TableCell cell = new TableCell();

                cell.HorizontalAlign = HorizontalAlign.Right;

                cell.VerticalAlign = VerticalAlign.Middle;

                cell.Controls.Add(First);

                cell.Controls.Add(Previous);

                row.Cells.Add(cell);

                cell = new TableCell();

                cell.HorizontalAlign= HorizontalAlign.Center;

                cell.Controls.Add(Pager);

                row.Cells.Add(cell);

                cell = new TableCell();

                cell.VerticalAlign = VerticalAlign.Middle;

                cell.Controls.Add(Next);

                cell.Controls.Add(Last);

                row.Cells.Add(cell);

                control.Controls.Add(table);

          }

    }

    The DefaultPagerLayout implements all the navigation elements programmatically that were added in the aspx page but this time it formats the elements with a normal HTML table. Now if the user forgets to implement their presentation template, then a default template will be provided for them.

    [TemplateContainer(typeof(LayoutContainer))]

    public ITemplate Layout

    {

          get{return (_layout == null)? new DefaultPagerLayout():_layout;}

          set{_layout =value;}

    }

    Getting back to generating the individual pagers, the pager control needs to first establish some useful properties that will tell the control how many individual pagers to generate.

    publicint CurrentPage

    {

          get

          {

                string cur = (string)ViewState["CurrentPage"];

    return (cur == string.Empty || cur ==null)? 1 : int.Parse(cur);

          }

          set

          {

                ViewState["CurrentPage"] =