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