Thursday 9 June 2011

Custom SEO Friendly Paging with ASP.NET Repeater and SqlDataSource

One of my favourite things about ASP.NET is the number of ways that you can rub the lamp to summon a new genie. Take paging for example, the ability to move through pages of data using Next, Previous, First, Last and numeric page number buttons is present in the GridView and ListView controls but not so in the Repeater control. One approach to adding paging to the Repeater control is the PagedDataSource method that adds the default paging mechanism to the control.

While the PagedDataSource is useful in many situations, its default paging mechanism relies on retrieving all of the data on each and every request. Furthermore, it works by posting the request back to the page which means that search engines will not be able to index beyond the first page of data.

It is possible to make retrieval much more efficient by leveraging the ROW_NUMBER() feature of SQL Server and configuring an ObjectDataSource for custom paging (see Scott Mitchell’s article Custom Paging in ASP.NET 2.0 with SQL Server 2005) however there is no such direct means if you are using the SqlDataSource (a two-tier application architecture that's faster to implement than the three-tier ObjectDataSource, and one that I use extensively).

It is also now also possible (using ASP.NET 3.5) to add search engine friendly paging by combining a ListView control with a DataPager (instructing it to generate an SEO-friendly paging interface) and although this is a landmark development, if you're not using the ListView control or if you need more control over your paging behaviour (or you're simply using ASP.NET 2.0) the good news is that it's quite easy to roll your own reusable SEO-friendly paging control.

This article shows you how to implement super-efficient, google-style SEO-friendly paging using the Repeater control, the SqlDataSource control and custom paging.

First up, we have a stored procedure that will return our filtered, sorted and paged result set along with a count of the total number of matches:
USE [SqlPagedDataSource]
GO
/****** Object:  StoredProcedure [dbo].[usp_SampleData_GetData]    Script Date: 06/09/2011 14:51:22 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

CREATE PROCEDURE [dbo].[usp_SampleData_GetData]

 @StartRow int,
 @PageSize int,
 @Sort varchar(50) = 'Name',
 @SortDirection varchar(50) = 'Asc',
 @Search varchar(255) = NULL,

 @RowCount int = NULL OUTPUT

AS
BEGIN

 SET @RowCount = (
 SELECT
  COUNT(*)
 FROM
  [dbo].[SampleData]
 WHERE
  (@Search IS NULL OR
   (@Search IS NOT NULL AND
    ([Name] LIKE '%' + @Search + '%' OR
     [Phone] LIKE '%' + @Search + '%' OR
     [Email] LIKE '%' + @Search + '%'
    )
   )
  )
 )

 SELECT * FROM
  (SELECT ROW_NUMBER() OVER(ORDER BY 
   ( CASE WHEN @Sort = 'Name' And @SortDirection = 'Asc' THEN [Name] END ) ASC,
   ( CASE WHEN @Sort = 'Name' And @SortDirection = 'Desc' THEN [Name] END ) DESC,
   ( CASE WHEN @Sort = 'Phone' And @SortDirection = 'Asc' THEN [Phone] END ) ASC,
   ( CASE WHEN @Sort = 'Phone' And @SortDirection = 'Desc' THEN [Phone] END ) DESC,
   ( CASE WHEN @Sort = 'Email' And @SortDirection = 'Asc' THEN [Email] END ) ASC,
   ( CASE WHEN @Sort = 'Email' And @SortDirection = 'Desc' THEN [Email] END ) DESC) As RowNum
   ,[Name]
   ,[Phone]
   ,[Email]
  FROM
   [dbo].[SampleData]
  WHERE
   (@Search IS NULL OR
    (@Search IS NOT NULL AND
     ([Name] LIKE '%' + @Search + '%' OR
      [Phone] LIKE '%' + @Search + '%' OR
      [Email] LIKE '%' + @Search + '%'
     )
    )
   )
  ) T
  WHERE
   RowNum BETWEEN @StartRow AND (@StartRow + (@PageSize - 1))

END

Next, to access the data from the database we're going to build on the SqlDataSource control by adding some properties and extending its behaviour to support our SEO-friendly custom paging. By creating a new ‘SqlPagedDataSource’ web server control (that inherits from SqlDataSource) and directly integrating its ‘Selecting’ and ‘Selected’ events we can insert, capture and then expose the information that makes paging possible – such as inserting the current page size, handling the page number being requested and capturing the total number of known matches.

The SqlPagedDataSource control then exposes the literal pager content to any control that wants to consume it (such as our SqlPagedDataSourcePager which we'll come on to later).

Use the SqlPagedDataSource control by declaring it as you would the ordinary SqlDataSource and then specify the PageSize, PageSetLength, SortName and SortDirection properties as required:
<zyky:SqlPagedDataSource ID="pds1" runat="server"
    ProviderName="System.Data.SqlClient"
    ConnectionString="<%$ ConnectionStrings:ConnStr %>"
    CancelSelectOnNullParameter="false"
    SelectCommand="usp_SampleData_GetData"
    SelectCommandType="StoredProcedure"
    PageSize="5"
    PageSetLength="10"
    SortName="Name"
    SortDirection="Asc">
    <SelectParameters>
    </SelectParameters>
</zyky:SqlPagedDataSource>

Then use the DataSourceID property to bind your databound control to the SqlPagedDataSource:
<asp:Repeater ID="Repeater1" runat="server" DataSourceID="pds1">
    <HeaderTemplate>
        <ul>
    </HeaderTemplate>
    <ItemTemplate>
        <li>
            <asp:Label ID="NameDataLabel" runat="server" Text='<%#Eval("Name")%>'></asp:Label>,
            <asp:Label ID="PhoneDataLabel" runat="server" Text='<%#Eval("Phone")%>'></asp:Label>,
            <asp:Label ID="EmailDataLabel" runat="server" Text='<%#Eval("Email")%>'></asp:Label>
        </li>
    </ItemTemplate>
    <FooterTemplate>
        </ul>
    </FooterTemplate>
</asp:Repeater>

Then we have the pager itself ‘SqlPagedDataSourcePager’, it's a simple web control that you can hook up to the SqlPagedDataSource via the ‘DataSourceID’ property to render the contents of the pager on the page:
<zyky:SqlPagedDataSourcePager ID="SqlPagedDataSourcePager1" runat="server" DataSourceID="pds1"></zyky:SqlPagedDataSourcePager>

The control itself has a really tiny footprint and simply inherits the Literal control to render its output:
Imports Microsoft.VisualBasic

Namespace Zyky

    Public Class SqlPagedDataSourcePager

        Inherits System.Web.UI.WebControls.Literal

        Public Property DataSourceID() As String
            Get
                Return CType(ViewState("DataSourceID"), String)
            End Get
            Set(ByVal value As String)
                ViewState("DataSourceID") = value
            End Set
        End Property

        Protected Overrides Sub Render(ByVal writer As System.Web.UI.HtmlTextWriter)
            Dim ds As Zyky.SqlPagedDataSource = Page.FindControl(DataSourceID)
            If Not ds Is Nothing Then
                writer.Write(ds.Contents)
            End If
            MyBase.Render(writer)
        End Sub

    End Class

End Namespace

Finally, we can output some information about the results such as the current page, the number of pages or the number of results by reading off the properties that we exposed in our SqlPagedDataSource control.

I chose to write a simple templated control to display this information:
<zyky:SqlPagedDataSourceInfoPanel ID="InfoPanel1" runat="server" DataSourceID="pds1">
    <ResultTemplate>
        Page
        <zyky:SqlPagedDataSourceInfo ID="Info1" runat="server" DataSourceID="pds1" PropertyName="CurrentPageQuery" />
        of
        <zyky:SqlPagedDataSourceInfo ID="Info2" runat="server" DataSourceID="pds1" PropertyName="PageCount" />
        for
        <zyky:SqlPagedDataSourceInfo ID="Info3" runat="server" DataSourceID="pds1" PropertyName="Results" />
        Results
    </ResultTemplate>
    <NoResultTemplate>
        << No Results >>
    </NoResultTemplate>
</zyky:SqlPagedDataSourceInfoPanel>

There you have it, a completely reusable seo-friendly custom paging interface for just about any type of databound control that you can think of. Now that’s magic!

Demo

View a live demo at http://code.zyky.com/sqlpageddatasource/

Code

Download the Source Code

Trails