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.LinqPublic 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) ElseInitialize(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 _HasPreviousPageEnd Get
End Property
Public ReadOnly Property HasNextPage() As Boolean Implements IPagedList(Of T).HasNextPage
Get Return _HasNextPageEnd Get
End Property
Public ReadOnly Property IsFirstPage() As Boolean Implements IPagedList(Of T).IsFirstPage
Get Return _IsFirstPageEnd Get
End Property
Public ReadOnly Property IsLastPage() As Boolean Implements IPagedList(Of T).IsLastPage
Get Return _IsLastPageEnd Get
End Property
Public ReadOnly Property PageCount() As Integer Implements IPagedList(Of T).PageCount
Get Return _PageCountEnd Get
End Property
Public ReadOnly Property PageIndex() As Integer Implements IPagedList(Of T).PageIndex
Get Return _PageIndexEnd Get
End Property
Public ReadOnly Property TotalItemCount() As Integer Implements IPagedList(Of T).TotalItemCount
Get Return _TotalItemCountEnd Get
End Property
Public ReadOnly Property PageNumber() As Integer Implements IPagedList(Of T).PageNumber
Get Return PageIndex + 1End Get
End Property
Public ReadOnly Property PageSize() As Integer Implements IPagedList(Of T).PageSize
Get Return _PageSizeEnd Get
End Property
#End RegionProtected 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 exceptionsIf 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 checkingIf 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 listIf 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.CompilerServicesPublic 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.GenericPartial Class Shared_Controls_Pager Inherits System.Web.UI.UserControlPrivate _DataSource As BLL.IPagedList
Public Property DataSource() As BLL.IPagedList
Get Return _DataSourceEnd Get
Set(ByVal value As BLL.IPagedList)
Me._DataSource = valueEnd Set
End Property
Public Property PageIndex() As Integer
GetReturn CInt(ViewState("PageIndex"))
End Get
Set(ByVal value As Integer)
ViewState("PageIndex") = valueEnd 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.HasNextPageEnd If
Me.PageNumberLabel.Text = DataSource.PageNumber Me.PageCountLabel.Text = DataSource.PageCountEnd Sub
Protected Sub PrevPageLinkButton_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles PrevPageLinkButton.Click
Me.PageIndex -= 1RaiseEvent PageIndexChanged(Me, New EventArgs)
End Sub
Protected Sub NextPageLinkButton_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles NextPageLinkButton.Click
Me.PageIndex += 1RaiseEvent 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.PageDim 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