Fully Accessible And SEO Friendly Ajax Paging Using DataPager
Hey All,
Working on my current project I
implemented paging using a listview and a datapager. I then
decided it would be much nicer to use AJAX for my paging so
wrapped all this up in an updatepanel. Next step was the
issue when you would goto a page, select a product and hit
the browser back button you would get the first page not the
page you were last on. To fix this I simply implemented the
ASP.NET AJAX Futures History control which allowed me to
save my current page and restore this at a later time.
Perfect
I thought until I started thinking about SEO, now my product
catalogue was dead to a search engine as it would only see
the first page and not be able to do any further paging. To
fix this I went about creating a SEO friendly linkbutton
control (I have blogged about this a while back but this is
the first time I used it in real life). Basically what the
SEO Friendly linkbutton does is render a normal navigateURL
and the postback as an onclick. This way with Javascript
turned on you get a postback but without you have a normal
URL, in my case I am passing the page # in my url like so:
http://site.com/catalogue/page-XX/Whatever.aspx, I am using
URL Rewriter.NET
for my URL rewriting so making a nice URL for this was as
simple as adding a new rule into my web.config.
Firstly
here is my custom SEOLinkButton control (Its in VB.NET as is
my current project, I have a C# version too but will just
post the VB.NET version unless requested):
Public
Class SEOLinkButton
Inherits LinkButton
#Region
"Properties"
Public Property NavigateURL() As
String
Get
Return
If(ViewState("NavigateURL") Is Nothing, "",
ViewState("NavigateURL").ToString())
End Get
Set(ByVal value As String)
ViewState("NavigateURL") = value
End Set
End Property
#End Region
Protected
Overrides Sub AddAttributesToRender(ByVal writer As
System.Web.UI.HtmlTextWriter)
If (Me.Page
IsNot Nothing) Then
Me.Page.VerifyRenderingInServerForm(Me)
End
If
Me.EnsureID()
writer.AddAttribute(HtmlTextWriterAttribute.Id,
Me.ClientID)
If (Not
String.IsNullOrEmpty(Me.CssClass)) Then
writer.AddAttribute(HtmlTextWriterAttribute.Class,
Me.CssClass)
End If
If (Not
Me.Enabled) Then
writer.AddAttribute(HtmlTextWriterAttribute.Disabled,
"disabled")
End If
If (Not
String.IsNullOrEmpty(Me.NavigateURL) AndAlso Me.Enabled)
Then
' Set the href to be our
navigateUrl.
writer.AddAttribute(HtmlTextWriterAttribute.Href,
Me.ResolveUrl(Me.NavigateURL))
End If
If (Me.Enabled) Then
Dim customScript
As String = Me.OnClientClick
If
(customScript.Length > 0 AndAlso Not
customScript.EndsWith(";")) Then
customScript = customScript + ";"
End If
Dim opts As PostBackOptions = Me.GetPostBackOptions()
Dim evt As String = Nothing
If (opts
IsNot Nothing) Then
evt =
Me.Page.ClientScript.GetPostBackEventReference(opts)
End If
' The onclick now becomes our
postback, and the appended custom script.
writer.AddAttribute(HtmlTextWriterAttribute.Onclick,
String.Format("{0}; {1} return false;", evt,
customScript))
End If
End
Sub
End Class
------------------------------------------------------------------------------------------------------------------
BTW - the control may not be 100% but it seems ok in my
initial tests.
The Code:
Handling all the possible scenarios is a little bit of a
pain, you need to worry about if the user comes in and has
JS on and visits the page, they will get ajax paging. If the
user comes in following a google link which passes in the
page as part of the url, it will show that page and then
allow further paging to use AJAX but it will save future
page state using AJAX history. If the user comes in and has
Javascript turned off then they will use the non AJAX
paging. I think this covers most bases for now.
Pager Template:
Here is how I use the SEOLinkButton in my custom pager
template for my DataPager:
<cc2:SEOLinkButton
ID="PreviousButton" runat="server" CommandName="Previous"
Text='Previous'
Visible='<%# Container.StartRowIndex > 0 %>'
NavigateURL='<%# Util.GetMenuLink(CurrentMenu,
Math.Ceiling(CType(((Container.StartRowIndex +
Container.MaximumRows) / (Container.MaximumRows)), Double))
- 1) %>' />
<cc2:SEOLinkButton ID="NextButton" runat="server"
CommandName="Next"
Text='Next'
Visible='<%# (Container.StartRowIndex +
Container.PageSize) < Container.TotalRowCount %>'
NavigateURL='<%# Util.GetMenuLink(CurrentMenu,
Math.Ceiling(CType(((Container.StartRowIndex +
Container.MaximumRows) / (Container.MaximumRows)), Double))
+ 1) %>'
/>
Excuse my Util.GetMenuLink method, this
basically is just an internal helper to generate the links
in my system. This will just render
/content/page-x/whatever.aspx. Basically this allows us to
have the a next and previous page link. The only difference
now instead of only being a javascript based postback. We
now can have a non javascript based URL which will allow a
search engine to still page through our catalogue but allow
a user with a JS enabled browser to get full AJAX paging.
Below explains howto handle the few
different scenarios:
Handling Non Ajax Paging:
To handle the paging I need to do a few things
in code. First what I am doing is in my ListViews
LayoutCreated event (done in here as it is late and seemed
like a good idea at the time, and also my datapager is
within my listviews template). I basically find my datapager
control and check if we were passed in a Page# via our
querystring, if so I run set the pagers properties like so,
this basically sets the current page of the pager. I only do
this if it is not a Postback, so that it will only select a
page based on query string the first time. This allows
further AJAX postbacks to page as normal.
Also in
this same area I add a new history point to my History
control which stores our current page number, this is needed
to basically set the initial state of the History control so
that it knows what page is started on.
Private
Sub LayoutCreated(ByVal sender As Object, ByVal e As
System.EventArgs) Handles lvProducts.LayoutCreated
If (Not Page.IsPostBack) Then
Dim page as
integer = 'get current page here
Pager.SetPageProperties(((page * pgSize) - pgSize), pgSize,
True)
' init the history controls
state
ucHistory.AddHistoryPoint("CurrentPage",
page)
End If
End Sub
Handling Ajax Paging:
I hook
into the history controls Navigate event in which I retrieve
the current page index from the history control and proceed
to load that page up. This handles the case where a user
hits the page, pages a few pages. Goes off to another page
and then hits the back button. The navigate event is fired
off which allows us to load the last page they were on.
Basically I do something like so in my Navigate event:
Protected
Sub ucHistory_Navigate(ByVal sender As Object, ByVal args As
Microsoft.Web.Preview.UI.Controls.HistoryEventArgs) Handles
ucHistory.Navigate
Dim page As Integer =
1
If (args.State.ContainsKey("CurrentPage"))
Then
' If the current page is stored in
state.
page =
args.State("CurrentPage").ToString().ToInteger()
End If
' Set the pager up.
Pager.SetPageProperties(((page * pgSize) - pgSize), pgSize,
True)
End Sub
Custom Paging:
Because I am using a custom
DataPager template I need to handle the DataPagerTemplate
Page_Changed event, in here I check the CommandName whether
it be Next or Previous, and then I select the specified
page. In this method. I also set a history point for my AJAX
history control which sets the index of the page we have
selected.
Protected Sub Page_Changed(ByVal sender
As Object, ByVal e As DataPagerCommandEventArgs)
Select Case e.CommandName
Case "Next"
' get next page
Case "Previous"
' get previous page
End Select
ucHistory.AddHistoryPoint("CurrentPage",
Math.Ceiling(CType(((e.NewStartRowIndex + e.NewMaximumRows)
/ (e.NewMaximumRows)), Double)))
End Sub
Summary:
I am sofar happy with this
solution. It is only a test at this stage but I think I can
pretty much wrap it up from this point on. It gives me the
best of both worlds. Accessiblity and Full AJAX support when
enabled. It is good that you can visit a link to a page
which has the page# specified in the URL and from that point
on you are able to have AJAX based paging. I also like the
fact you can RightClick "Open in new window" for your next
page, maybe not everyones cup of tea but I find this useful.
Plus when you get to that page you once again have full AJAX
support if required.
I guess the only anoyance to
some might be that your URL now has a rather nasty long
value after the #, this is where the state is stored for you
history control. And when you come in via a link to a page
and you have something like this:
http://site.com/catalogue/page-4/Foo.aspx#%7B%22__s%22%3A%22%2FwEXAQULQ3VycmVudFBhZ2UHAAAAAAAACECagm2NuwtiGmq8f%2FX7RRwGq4BY1g%3D%3D%22%7D
It
is a bit messy but for the accessiblity and features I have
gotten by using this method I am going to ignore that
totally.
I have assumed you have knowledge of
the ASP.NET AJAX Futures control when I wrote this,
click here
to learn about this excellent control. I also did not go
into how I am getting my data either. I use a LINQDataSoure
on my page but you might do different.
If you have any further suggestions or questions feel free
to post a comment.
Thanks
Stefan