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?
- A way to represent paged data – the actual data + additional information like page size, page index, total rows etc
- A service method to return the paged data
- A control to display the paged data on my web form – one that is easy to modify and that renders clean HTML
- A control that deals with setting the page size
- A control that deals with the actual paging
- 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:
Are you looking to earn cash from your traffic with popup ads?
If so, did you try using PopCash?
Post a Comment