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
 

12 Comments

  • This exactly what i was looking for thanks

  • No worries,

    I think it is good that with only a little extra effort we can make our AJAX sites accessible. Full functionality with JS and basic yet full functionality without.

    Best thing is the ASP.NET AJAX library and the Futures make this soo easy to do. I know I did this in PHP once and it was a real nightmare to do.


    Thanks
    Stefan

  • I wrote a C# Version. Check it out here:

    public class SEOLinkBUtton : LinkButton {
    #region Properties

    public string NavigateURL {
    get {
    return ViewState["NavigateURL"].ToString() ?? String.Empty;
    }
    set {
    ViewState["NavigateURL"] = value;
    }
    }

    #endregion

    public SEOLinkBUtton() {
    //
    // TODO: Add constructor logic here
    //
    }

    protected override void AddAttributesToRender(HtmlTextWriter writer) {
    base.AddAttributesToRender(writer);

    if (this.Page != null)
    this.Page.VerifyRenderingInServerForm(this);

    this.EnsureID();
    writer.AddAttribute(HtmlTextWriterAttribute.Id, this.ClientID);

    if (!String.IsNullOrEmpty(this.CssClass))
    writer.AddAttribute(HtmlTextWriterAttribute.Class, this.CssClass);

    if (!this.Enabled)
    writer.AddAttribute(HtmlTextWriterAttribute.Disabled, "disabled");

    // Set the href to be our navigateUrl
    if(!String.IsNullOrEmpty(this.NavigateURL) && this.Enabled)
    writer.AddAttribute(HtmlTextWriterAttribute.Href, this.ResolveUrl(this.NavigateURL));

    if (this.Enabled) {
    String customScript = this.OnClientClick;

    if ((customScript.Length > 0) && !customScript.EndsWith(";"))
    customScript += ";";

    PostBackOptions opts = this.GetPostBackOptions();

    String evt = null;

    if (opts != null)
    evt = this.Page.ClientScript.GetPostBackEventReference(opts);
    // 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(this.enabled)
    } // end void

    }

  • Thanks for that mate :) saved me converting the new version I have to C# :):), which I need to do now anyway hehe to add to my new control library.

    Good to see you guys find it helpful

    Thanks
    Stefan

  • Hello,

    I am currnetly on holidays and will have to look at this one for you when I get back, can you possibly email me your sample project @ stefan.sedich@gmail.com and I will look into it in around 2 weeks.

    Thanks
    Stefan

  • Hello,

    Had another email in regards to this and got around to fixing it. With the C# version there seemed to be an error in the AddAttributesToRender override it was calling the base AddAttributesToRender method, this was what was breaking it. The updated version is below, sorry for the delay in looking into this.

    Thanks
    Stefan

    public class SEOLinkBUtton : LinkButton {

    #region Properties

    public string NavigateURL {

    get {

    return ViewState["NavigateURL"].ToString() ?? String.Empty;

    }

    set {

    ViewState["NavigateURL"] = value;

    }

    }

    #endregion

    public SEOLinkBUtton() {

    //

    // TODO: Add constructor logic here

    //

    }

    protected override void AddAttributesToRender(HtmlTextWriter writer) {

    if (this.Page != null)

    this.Page.VerifyRenderingInServerForm(this);

    this.EnsureID();

    writer.AddAttribute(HtmlTextWriterAttribute.Id, this.ClientID);

    if (!String.IsNullOrEmpty(this.CssClass))

    writer.AddAttribute(HtmlTextWriterAttribute.Class, this.CssClass);

    if (!this.Enabled)

    writer.AddAttribute(HtmlTextWriterAttribute.Disabled, "disabled");

    // Set the href to be our navigateUrl

    if(!String.IsNullOrEmpty(this.NavigateURL) && this.Enabled)

    writer.AddAttribute(HtmlTextWriterAttribute.Href, this.ResolveUrl(this.NavigateURL));

    if (this.Enabled) {

    String customScript = this.OnClientClick;

    if ((customScript.Length > 0) && !customScript.EndsWith(";"))

    customScript += ";";

    PostBackOptions opts = this.GetPostBackOptions();

    String evt = null;

    if (opts != null)

    evt = this.Page.ClientScript.GetPostBackEventReference(opts);

    // 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(this.enabled)

    } // end void

    }

  • Very cool... but I'm running into a problem... all my skin formatting seems to get lost. Any ideas?

  • Dave,

    I do not use skins my self, but if you send me a sample project which replicates this I can take a peek.

    Email me at stefan.sedich[at]gmail.com


    Thanks
    Stefan

  • I forgot to say thanks for this! Such a small error.

  • thx good examples

  • It's easy and usefull.

  • Can I use this control in an ASP.Net 2.0 (C#) website?

Comments have been disabled for this content.