Wednesday, April 20, 2011

Implementing clickable page numbers in search result

Classic scenario: Take user input, get a search-result and display it in pages to the user. I then need to display buttons for First, Next, Previous etc, and I maintain the users current page in viewstate. All is good, works fine.

Then I need to implement clickable page numbers, ie. 1-2-3-4-5-6 etc.

Rendering them is simple. I generate a linkbutton control at runtime, add commandargument with the page number and add a handler to it, so click are to be handled. Then I add it to a placeholder, and it is displayed as expected.

But then... If I did not already have a shaved head, I would be pulling out my hair getting the events to fire as expected every time.

How should I do this, so my events are always wired up and able to fire when the paging-linkbuttons are called?

Below is the important parts of the code, some pseudo to make it (hopefully) easier to understand, what I am doing.

Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
  If Not Page.IsPostBack Then
     Search()
  End If
End Sub

Sub Search
    'Misc databinding stuff, searches and displays results for the page specified in Me.CurrentPage
    RenderPagingControls()
End Sub

Sub RenderPagingControls
   'loop throug pagenumbers, Build a linkbutton control, add it to a placeholder
    AddHandler lbn.Click, AddressOf lbnNumber_Click
    lblPageNumbers.Controls.Add(lbn)
    ...

End Sub

Protected Sub lbnNumber_Click(ByVal sender As Object, ByVal e As EventArgs)
    Dim b As LinkButton = CType(sender, LinkButton)
    Me.CurrentPage = CInt(b.CommandArgument)
    Search()
End Sub

Public Property CurrentPage() As Integer
    Get
        Dim o As Object = Me.ViewState("CurrentPage")
        If o Is Nothing Then
            Return 1
        Else
            Return CType(o, Integer)
        End If
    End Get
    Set(ByVal value As Integer)
        Me.ViewState("CurrentPage") = value
    End Set
End Property

Protected Sub lbnNumber_Click(ByVal sender As Object, ByVal e As EventArgs)
    Dim b As LinkButton = CType(sender, LinkButton)
    Me.CurrentPage = CInt(b.CommandArgument)
    Search()
End Sub
From stackoverflow
  • iirc... adding controls dynamically at runtime is a bit tricky. The control tree must be rebuilt during post back... but before viewstate is loaded (not sure when in the page life cycle... but way before page load). So... your problem is that by the time asp.net is trying to figure out your event the actual originating control has not yet been created.

  • I'm going to recommend against a LinkButton and recommend Hyperlinks / QueryString parameters instead. For several reasons:

    1. Your page will be much more efficient without the viewstate overhead of a link button.
    2. If these are public facing pages, you'll get better indexing of all the pages if they can be accessed via hyperlinks (and indexed via search engines).
    3. You'll find them much easier to implement. No event management, etc.

    You would redefine your CurrentPage method as (hopefully this is correct, I'm better at C# than vb.net):

    Public Property CurrentPage() As Integer
        Get
            Dim o As Object = Me.Request.QueryString("page")
            If o Is Nothing Then
                Return 1
            Else
                Return CType(o, Integer)
            End If
        End Get
    End Property
    

    Then just add hyperlinks for each page.

    <a href='mypage.aspx?page=1'>1</a> - <a href='mypage.aspx?page=2'>2</a>
    etc...
    

    Alternative: If you want to use the LinkButton, you might want to consider putting a single LinkButton in a repeater. Then the only event you have to worry about is the OnItemCommand event. Then no dynamic controls or events. Something like this:

    <asp:Repeater ID="rptPages" runat="server" OnItemCommand='doPaging'>
      <ItemTemplate>
        <asp:LinkButton ID="LinkButton1" runat="server" Text='<%# (Container.DataItem).ToString()  %>'
        CommandArgument='<%# (Container.DataItem).ToString() %>' />
      </ItemTemplate>
      <SeparatorTemplate>-</SeparatorTemplate>
    </asp:Repeater>
    

    Bind this control to an array (or list) of consecutive Integers (as many are there are pages). Then in your doPaging function (as I call it), check RepeaterCommandEventArgs.CommandArgument to get the page number.

    Kjensen : I thought about it, but there are quite a few other controls, where I do utilize the viewstate, so I would have to add 5-6 other controls to the querystring... On the other hand, I spent half a day trying to get this crap to work... I will think about it. :)
    Keltex : @Kjensen See my alternative answer above. I would just use a repeater. Then you just the CommandArgument to supply the page #.
  • This code works (sorry it's in C#):

    protected void SearchButton_Click(object sender, EventArgs e)
    {
     //clear the collection!
     pnlPageNumber.Controls.Clear();
    
     //simulate search
     System.Random rnd = new Random();
    
     //create page buttons
     for (int i = 0; i < rnd.Next(3, 15); i++)
     {
      LinkButton lb = new LinkButton();
      pnlPageNumber.Controls.Add(lb);
      lb.ID = "btn" + i;
      lb.Text = i.ToString();
      lb.CommandArgument = i.ToString();
      lb.Command += new CommandEventHandler(linkbutton_Command);
    
      //optional literal
      pnlPageNumber.Controls.Add(new LiteralControl(" "));
     }
    
     ViewState["control#"] = Panel1.Controls.Count;
    }
    
    protected void Page_Load(object sender, EventArgs e)
    {
     if (IsPostBack)
     {
      //Recreate link buttons
      //This is necessary to ensure proper event binding
    
      int count = 0;
    
      if (ViewState["control#"] != null)
       count = (int)ViewState["control#"];
    
      for (int i = 0; i < count; i++)
      {
       LinkButton lb = new LinkButton();
       pnlPageNumber.Controls.Add(lb);
       lb.ID = "btn" + i; //make sure IDs are the same here and on Search
       lb.Command += new CommandEventHandler(linkbutton_Command);
    
       //this is not necessary, but if you do, make sure its in both places
       pnlPageNumber.Controls.Add(new LiteralControl(" "));
      }
     }
    }
    
    void linkbutton_Command(object sender, CommandEventArgs e)
    {
     Response.Write(e.CommandArgument.ToString() + " CLICK<br />");
    }
    
  • You could use the DataPager control -- the only limitation is you have to use it with the ListView control, but you should be able to represent your data using the ListView control fairly easily because it is very flexible. You can set the DataSource of the ListView control to the result of your data result, whether that be a DataSet, Collection, Array, etc.

    To create the paging controls with "first", "last", and page numbers, set up the DataPager like this (where ListView1 is the ID of your ListView control):

    <asp:DataPager ID="DataPager1" runat="server" 
       PagedControlID="ListView1" PageSize="25">
       <Fields>
          <asp:NextPreviousPagerField FirstPageText="first" ShowFirstPageButton="True" 
                 ShowNextPageButton="False" ShowPreviousPageButton="False" />
          <asp:NumericPagerField />
          <asp:NextPreviousPagerField LastPageText="last" ShowLastPageButton="True" 
                 ShowNextPageButton="False" ShowPreviousPageButton="False" />
       </Fields>
    </asp:DataPager>
    

    By design, the DataPager uses the whole result set from the database, but you can improve the performance by caching the result and using that on the subsequent requests.

    Hope this helps.

  • Thanks for the answers, guys. I tried out Austins first, but I must be missing something, because I keep getting the same behaviour of linkbuttons only working every second time... So I gave up on that, and saw the alternative solution with the repeater by Keltex! It is as brilliant as it is simple, and we don't have to worry about any page lifecycle bullshit.

    It just fucking works! ;)

    If somebody should need something similar in the future, here is the relevant code behind the scenes:

    Sub Search()
        ...
        RenderPagingControls()
    End Sub
    
    Sub RenderPagingControls()
        Dim pages As New ArrayList
        For i As Integer = 1 To Me.PageCount
            pages.Add(i)
        Next
    
        repPageNumbersTop.DataSource = pages
        repPageNumbersTop.DataBind()
    
        repPageNumbersBottom.DataSource = pages
        repPageNumbersBottom.DataBind()
    
    End Sub
    
    Public Property CurrentPage() As Integer
        Get
            Dim o As Object = Me.ViewState("CurrentPage")
            If o Is Nothing Then
                Return 1
            Else
                Return CType(o, Integer)
            End If
        End Get
        Set(ByVal value As Integer)
            Me.ViewState("CurrentPage") = value
        End Set
    End Property
    
    Public Property PageCount() As Integer
        Get
            Dim o As Object = Me.ViewState("PageCount")
            If o Is Nothing Then
                Return 0
            Else
                Return CType(o, Integer)
            End If
        End Get
        Set(ByVal value As Integer)
            Me.ViewState("PageCount") = value
        End Set
    End Property
    
    
    Protected Sub repPageNumbersTop_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.RepeaterCommandEventArgs) Handles repPageNumbersTop.ItemCommand, repPageNumbersBottom.ItemCommand
        Me.CurrentPage = CType(e.CommandArgument, Integer)
        Search()
    End Sub
    
    Private Sub repPageNumbersTop_ItemDataBound(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.RepeaterItemEventArgs) Handles repPageNumbersTop.ItemDataBound, repPageNumbersBottom.ItemDataBound
        If e.Item.ItemType = ListItemType.Item Or e.Item.ItemType = ListItemType.AlternatingItem Then
            Dim lbn As LinkButton = CType(e.Item.FindControl("lbnPageNumber"), LinkButton)
            If lbn.CommandArgument = Me.CurrentPage.ToString Then
                lbn.Enabled = False
            End If
        End If
    End Sub
    
    Keltex : @Kjensen: Thanks for the accepting my answer. You also might be able to replace repPageNumbersTop_ItemDataBound with a property on the LinkButton: Enabled='<%# (Container.DataItem).ToString <> Me.CurrentPage.ToString %>' (my vb.net isn't too good...)

0 comments:

Post a Comment