Monday, November 10, 2008

Custom paging in ASP.NET Webforms using Datalist, PagedList and Repository

What I wanted to do today is to put together a simple web form with a list of contacts that can be paged and a drop-down menu that can be used to set the page size of the list. In this post I will show you how to do this in a neat manner without GridView or any of the DataSource objects but by using a custom paging method in your Business Logic Layer (BLL), a  DataList control and a simple paging web user control

The ASP.NET framework provides multiple ways to handle this. Here are few:

  • By using a SQLDataSource to access my table and bind a GridView to it – Very easy to implement but possibly the worst choice if you want to separate your BLL from your UI
  • By using a LinqDataSource to access my DataContext and bind a GridView to it – Just as easy to implement but not a whole lot better than SQLDataSource. You’d still be running queries from the UI and it gets complicated if you have business rules limiting the resultset based on certain criteria
  • By using a custom paging enabled ObjectDataSource to access a custom BLL method and bind GridView to it – Little tricky to implement but the only control ready to handle the resultset is the GridView and sometimes things just don’t fit in it. On top of you will have to write a separate CountListByX method for every GetListByX method because the ObjectDataSource requires it by design and that’s not something that sounds very appealing to me.
  • Sit down and write one – Now we’re talking :-)

So what did I need?

  1. A way to represent paged data – the actual data + additional information like page size, page index, total rows etc
  2. A service method to return the paged data
  3. A control to display the paged data on my web form – one that is easy to modify and that renders clean HTML
  4. A control that deals with setting the page size
  5. A control that deals with the actual paging
  6. A Web Form to put it all together

1. The PagedList

For this I decided to borrow Troy’s implementation of IPagedList (thank you Troy). I did make one addition though. I added an extra IPagedList interface because I thought that the pager should not care about strongly typed data thus ending up with the following code file in my BLL

CodeFile1.vb

Imports System.Linq
 
Public Interface IPagedList
 
    ReadOnly Property PageCount() As Integer
    ReadOnly Property TotalItemCount() As Integer
    ReadOnly Property PageIndex() As Integer
    ReadOnly Property PageNumber() As Integer
    ReadOnly Property PageSize() As Integer
    ReadOnly Property HasPreviousPage() As Boolean
    ReadOnly Property HasNextPage() As Boolean
    ReadOnly Property IsFirstPage() As Boolean
    ReadOnly Property IsLastPage() As Boolean
 
End Interface
 
Public Interface IPagedList(Of T)
    Inherits IPagedList
    Inherits IList(Of T)
End Interface
 
    Public Class PagedList(Of T)
        Inherits List(Of T)
    Implements IPagedList(Of T)
 
    Public Sub New(ByVal source As IEnumerable(Of T), ByVal index As Integer, ByVal pageSize As Integer)
        If TypeOf source Is IQueryable(Of T) Then
            Initialize(TryCast(source, IQueryable(Of T)), index, pageSize)
        Else
            Initialize(source.AsQueryable(), index, pageSize)
        End If
    End Sub
 
        Public Sub New(ByVal source As IQueryable(Of T), ByVal index As Integer, ByVal pageSize As Integer)
            Initialize(source, index, pageSize)
        End Sub
 
#Region "IPagedList Members"
 
    Private _HasNextPage As Boolean
    Private _HasPreviousPage As Boolean
    Private _IsFirstPage As Boolean
    Private _IsLastPage As Boolean
    Private _PageCount As Integer
    Private _PageIndex As Integer
    Private _PageSize As Integer
    Private _TotalItemCount As Integer
 
    Public ReadOnly Property HasPreviousPage() As Boolean Implements IPagedList(Of T).HasPreviousPage
        Get
            Return _HasPreviousPage
        End Get
    End Property
 
    Public ReadOnly Property HasNextPage() As Boolean Implements IPagedList(Of T).HasNextPage
        Get
            Return _HasNextPage
        End Get
    End Property
 
    Public ReadOnly Property IsFirstPage() As Boolean Implements IPagedList(Of T).IsFirstPage
        Get
            Return _IsFirstPage
        End Get
    End Property
 
    Public ReadOnly Property IsLastPage() As Boolean Implements IPagedList(Of T).IsLastPage
        Get
            Return _IsLastPage
        End Get
    End Property
 
    Public ReadOnly Property PageCount() As Integer Implements IPagedList(Of T).PageCount
        Get
            Return _PageCount
        End Get
    End Property
 
    Public ReadOnly Property PageIndex() As Integer Implements IPagedList(Of T).PageIndex
        Get
            Return _PageIndex
        End Get
    End Property
 
    Public ReadOnly Property TotalItemCount() As Integer Implements IPagedList(Of T).TotalItemCount
        Get
            Return _TotalItemCount
        End Get
    End Property
 
    Public ReadOnly Property PageNumber() As Integer Implements IPagedList(Of T).PageNumber
        Get
            Return PageIndex + 1
        End Get
    End Property
 
    Public ReadOnly Property PageSize() As Integer Implements IPagedList(Of T).PageSize
        Get
            Return _PageSize
        End Get
    End Property
 
 
#End Region
 
        Protected Sub Initialize(ByVal source As IQueryable(Of T), ByVal index As Integer, ByVal pageSize__1 As Integer)
        ' set source to blank list if source is null to prevent exceptions
            If source Is Nothing Then
                source = New List(Of T)().AsQueryable()
            End If
 
        ' set properties
        _TotalItemCount = source.Count()
        _PageSize = pageSize__1
        _PageIndex = index
            If TotalItemCount > 0 Then
            _PageCount = CInt(Math.Ceiling(TotalItemCount / CDbl(PageSize)))
            Else
            _PageCount = 0
            End If
        _HasPreviousPage = (PageIndex > 0)
        _HasNextPage = (PageIndex < (PageCount - 1))
        _IsFirstPage = (PageIndex <= 0)
        _IsLastPage = (PageIndex >= (PageCount - 1))
 
        ' argument checking
            If index < 0 Then
                Throw New ArgumentOutOfRangeException("PageIndex cannot be below 0.")
            End If
            If pageSize__1 < 1 Then
                Throw New ArgumentOutOfRangeException("PageSize cannot be less than 1.")
            End If
 
        ' add items to internal list
            If TotalItemCount > 0 Then
                AddRange(source.Skip((index) * pageSize__1).Take(pageSize__1).ToList())
            End If
    End Sub
 
    End Class
 
Public Module Pagination
 
    <System.Runtime.CompilerServices.Extension()> _
    Public Function ToPagedList(Of T)(ByVal source As IQueryable(Of T), ByVal index As Integer, ByVal pageSize As Integer) As PagedList(Of T)
        Return New PagedList(Of T)(source, index, pageSize)
    End Function
 
    <System.Runtime.CompilerServices.Extension()> _
    Public Function ToPagedList(Of T)(ByVal source As IEnumerable(Of T), ByVal index As Integer, ByVal pageSize As Integer) As PagedList(Of T)
        Return New PagedList(Of T)(source, index, pageSize)
    End Function
 
End Module
 

This code file will be reused later in this as well as any other app for pretty much anything that I want to show in paged form.

2. The Service Method

I already had my ContactService that gets IQueryable(Of Contact) from my Repository underneath so I just overloaded my GetAll method and formatted the data using the extension methods specified in the code file I just created above

ContactService.vb

Imports System.Runtime.CompilerServices
 
Public Class ContactService
 
    Private _ContactRepository As BLL.IContactRepository = Nothing
 
    Public Sub New()
        Me._ContactRepository = New ContactRepository()
    End Sub
 
    Public Function GetAll(ByVal pageIndex As Integer, ByVal pageSize As Integer) As PagedList(Of Contact)
        Return _ContactRepository.GetAll.ToPagedList(pageIndex, pageSize)
    End Function
 
 
End Class

3. The List Control

I decided to go with ListView because I think it renders the cleanest HTML. (People might disagree here but this implementation will work just as well with a Repeater so use whatever you want).

<asp:ListView ID="ListView1" runat="server">
    <LayoutTemplate>
        <table>
            <tr>
                <th>First name</th>
                <th>Last Name</th>
                
            </tr>
            <asp:PlaceHolder ID="itemPlaceHolder" runat="server"></asp:PlaceHolder>
        </table>
    </LayoutTemplate>
    <ItemTemplate>
        <tr>
            <td><asp:Label ID="FirstNameLabel" runat="server" Text='<%#Eval("FirstName") %>'></asp:Label></td>
            <td><asp:Label ID="LastNameLabel" runat="server" Text='<%#Eval("LastName") %>'></asp:Label></td>
        </tr>
    </ItemTemplate>
</asp:ListView>

4. The PageSize Control

Nothing fancy here. Just a DropDownList with some presets and AutoPostBack set to enabled

<asp:DropDownList ID="PageSizeDropDownList1" runat="server" AutoPostBack="True">
    <asp:ListItem Text="25" Value="25"></asp:ListItem>
    <asp:ListItem Text="50" Value="50"></asp:ListItem>
    <asp:ListItem Text="100" Value="100"></asp:ListItem>
</asp:DropDownList>

5. The Pager Control

For my Pager control I decided to make a simple Web User Control with two LinkButtons – one for back and one for forward navigation. (I know it is very simple but I just wanted to see how well it would work before extending it). This control would take the same data as the list control above thus eliminating the extra DB call for a Count method. The only purpose of it is to evaluate the IPagedList-formatted data and enable/disable the navigation buttons as well as provide feedback on the current and total pages.

Pager.ascx

<%@ Control Language="VB" AutoEventWireup="false" CodeFile="Pager.ascx.vb" Inherits="Shared_Controls_Pager" %>
<div>
    <asp:LinkButton ID="PrevPageLinkButton" runat="server" Text="<<"></asp:LinkButton>
    <asp:LinkButton ID="NextPageLinkButton" runat="server" Text=">>"></asp:LinkButton>    
</div>
<div>
   Page <asp:Label ID="PageNumberLabel" runat="server"></asp:Label> of <asp:Label ID="PageCountLabel" runat="server"></asp:Label>
</div>
    
 

and the code behind

Pager.ascx.vb

Imports System.Collections.Generic
 
Partial Class Shared_Controls_Pager
    Inherits System.Web.UI.UserControl
 
    Private _DataSource As BLL.IPagedList
    Public Property DataSource() As BLL.IPagedList
        Get
            Return _DataSource
        End Get
        Set(ByVal value As BLL.IPagedList)
            Me._DataSource = value
        End Set
    End Property
 
    Public Property PageIndex() As Integer
        Get
            Return CInt(ViewState("PageIndex"))
        End Get
        Set(ByVal value As Integer)
            ViewState("PageIndex") = value
        End Set
    End Property
 
    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
 
    End Sub
 
    Public Overrides Sub DataBind()
        'MyBase.DataBind()
        If Me.DataSource Is Nothing Then
            Throw New Exception("Pager DataSource must be specified")
        Else
            Me.PrevPageLinkButton.Enabled = DataSource.HasPreviousPage
            Me.NextPageLinkButton.Enabled = DataSource.HasNextPage
        End If
        Me.PageNumberLabel.Text = DataSource.PageNumber
        Me.PageCountLabel.Text = DataSource.PageCount
 
    End Sub
 
    Protected Sub PrevPageLinkButton_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles PrevPageLinkButton.Click
        Me.PageIndex -= 1
        RaiseEvent PageIndexChanged(Me, New EventArgs)
    End Sub
 
    Protected Sub NextPageLinkButton_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles NextPageLinkButton.Click
        Me.PageIndex += 1
        RaiseEvent PageIndexChanged(Me, New EventArgs)
    End Sub
 
    Public Event PageIndexChanged(ByVal sender As Object, ByVal e As EventArgs)
End Class

6. The Web Form

My web form was very simple too. It only had the 3 controls on all it did was do initial binding and listen for either the PageIndexChanged event of the Pager control or SelectedIndexChanged of the DropDownList control and rebind the data using the new values in case either of those occur

ListAll.aspx

<asp:DropDownList ID="PageSizeDropDownList1" runat="server" AutoPostBack="True">
    <asp:ListItem Text="2" Value="2"></asp:ListItem>
    <asp:ListItem Text="25" Value="25"></asp:ListItem>
    <asp:ListItem Text="50" Value="50"></asp:ListItem>
    <asp:ListItem Text="100" Value="100"></asp:ListItem>
</asp:DropDownList>
<asp:ListView ID="ListView1" runat="server">
    <LayoutTemplate>
        <table>
            <tr>
                <th>First name</th>
                <th>Last Name</th>
                
            </tr>
            <asp:PlaceHolder ID="itemPlaceHolder" runat="server"></asp:PlaceHolder>
        </table>
    </LayoutTemplate>
    <ItemTemplate>
        <tr>
            <td><asp:Label ID="FirstNameLabel" runat="server" Text='<%#Eval("FirstName") %>'></asp:Label></td>
            <td><asp:Label ID="LastNameLabel" runat="server" Text='<%#Eval("LastName") %>'></asp:Label></td>
        </tr>
    </ItemTemplate>
</asp:ListView>    
<uc1:Pager ID="Pager1" runat="server" />

ListAll.aspx.vb

 
Partial Class Contacts_ListAll
    Inherits System.Web.UI.Page
 
    Dim svc As BLL.ContactService = New BLL.ContactService
    Dim contacts As BLL.IPagedList(Of BLL.Contact)
 
    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        If Page.IsPostBack = True Then
        Else                        
            contacts = svc.GetAll(Pager1.PageIndex, Me.PageSizeDropDownList1.SelectedValue)
            DataBind()
        End If
    End Sub
 
    Public Overrides Sub DataBind()
        'MyBase.DataBind()        
 
        Me.ListView1.DataSource = contacts
        Me.ListView1.DataBind()
        Me.Pager1.DataSource = contacts
        Me.Pager1.DataBind()
    End Sub
 
    Protected Sub CreateContactLinkButton_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles CreateContactLinkButton.Click
        Response.Redirect("Create.aspx")
    End Sub
 
    Protected Sub PageSizeDropDownList1_SelectedIndexChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles PageSizeDropDownList1.SelectedIndexChanged        
        contacts = svc.GetAll(0, Me.PageSizeDropDownList1.SelectedValue)
        DataBind()
    End Sub
 
    Protected Sub Pager1_PageLinkClicked(ByVal sender As Object, ByVal e As System.EventArgs) Handles Pager1.PageIndexChanged
        contacts = svc.GetAll(Pager1.PageIndex, Me.PageSizeDropDownList1.SelectedValue)
        DataBind()
    End Sub
 
End Class

This is it. Very simple and yet very effective. Both – IPagedList and Pager could be reused later for anything else and nothing is leaking from my BLL into my UI. All I have to do now is spice up the pager control a bit but I will leave it for the next post.

1 comment:

Blogger said...

Are you looking to earn cash from your traffic with popup ads?
If so, did you try using PopCash?