ASP.NET dynamic event handler assignment problem

majorair
majorair used Ask the Experts™
on
I'm not sure if I'm even going to be able to explain my problem, but here goes:

I have a user control that is supposed to display an image, and a list of LinkButtons that cause the displayed image to change.

As each LinkButton is clicked, it fires an event handler that causes for the list to be reloaded, as well as the image to be changed to the one requested.

The onclick events are assigned to the list of LinkButtons at run-time with an AddHandler call.

I have a function called LoadInfo that is called on Page_Load. LoadInfo pulls the list of images out of the database, and writes the list of LinkButtons. It assigns the event handlers to each LinkButton in a loop, and loads the image. LoadInfo is also called from the LinkButton_onClick event handler, so that whenever a LinkButton is clicked, it causes the list of images to be reloaded, as well as the current image.

The problem is:

The first time the page loads, it calls loadInfo. Works great. I can then click on one of the LinkButtons in the list, and it reloads the control with the new image, and the link in the list is highlighted to show which image I selected. However, when I try to click on a different link AFTER the first PostBack, it just reloads the page. It doesn't call my event handler, it only reloads the page. This leads me to suspect that my event handlers were not assigned to the LinkButtons. After the loadInfo function has been called from the Page_Load again, it all works. So, I am essentially required to click on an image title TWICE to get it to load the correct image.

The other problem I had was how .NET would lose the contents of a variable between page loads. On the OnClick event of one of the image links I would assign the id of the link clicked to a global variable. However, that value would be lost, and the list would reset itself the next time I clicked a link and reloaded the control. I solved that by assigning the id to the viewstate. That maintains it regardless of what's going on on the page. However, I still have the problem of clicking twice to get a response.

It all seems to come down to the fact that after the postback, the page resets itself, variables and all. And on the postback, it doesn't properly assign event handlers if they are being assigned dynamically. It's like the page needs to start with a clean slate after the postback for it to allow another postback.

I don't know if any of this makes sense, but I've been banging my head off of it for two days and I'm ready to move on to something else.

Please help! Thanks!
Comment
Watch Question

Do more with

Expert Office
EXPERT OFFICE® is a registered trademark of EXPERTS EXCHANGE®
You are right on target about how page gets recreated on each post back. this is the nature of web application. If you need to maintain state or any variable value between postbacks, then you can one of the following methods.

1. Assign it to Viewstate on page. this you already did and is prefered way if its going to be small information.
2. Use Session variables.
3. If the variable is going to used by every user of app, then assign it to Application object.

To fix your double click problem...
Are you reassigning the event handlers after postback or not or are you waiting for page load to complete. Here is some information that you wanna keep in mind. When postback happens, your event handler gets executed after Page_Load has been executed. So if you are assigning dynamic event handlers, try to do that in button's event handler suring post backs.

Author

Commented:
Well, the loadInfo function gets called on Page_Load, as well as on imgLink_click(my button event handler). The loadInfo function handles adding the LinkButtons, as well ad assigning event handlers. My page code is as follows:

Imports es.estore
Imports System.Data.SqlClient

Public MustInherit Class productImages
    Inherits System.Web.UI.UserControl
    Protected WithEvents productImage As System.Web.UI.WebControls.Image

    Private _productId As Integer
    Private _currentImage As Integer
    Private sqlStmt As String
    Private sqlCon As sqlConnection
    Protected WithEvents linkTable As System.Web.UI.WebControls.Table
    Protected WithEvents Label1 As System.Web.UI.WebControls.Label
    Protected WithEvents Label2 As System.Web.UI.WebControls.Label
    Private sqlAdapter As SqlDataAdapter
    Private firstLoad As Boolean = False


#Region " Web Form Designer Generated Code "

    'This call is required by the Web Form Designer.
    <System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()

    End Sub

    Private Sub Page_Init(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Init
        'CODEGEN: This method call is required by the Web Form Designer
        'Do not modify it using the code editor.
        InitializeComponent()
    End Sub

#End Region

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        Dim currentImage As Integer
        sqlCon = New SqlConnection(ConfigurationSettings.AppSettings("connString"))
        sqlCon.Open()

        loadInfo(ViewState("currentImage"), "loadPage")
    End Sub

    Sub loadInfo(ByVal currentImageId, ByVal refFunc)

        Label1.Text = "Called From: " & refFunc.ToString()
        sqlAdapter = New SqlDataAdapter("selectImagesByProdId", sqlCon)
        sqlAdapter.SelectCommand.CommandType = CommandType.StoredProcedure
        sqlAdapter.SelectCommand.Parameters.Add(New SqlParameter("@productId", _productId))

        Dim imageList As New DataSet()
        sqlAdapter.Fill(imageList, "imageTable")

        If currentImageId Is Nothing Then
            currentImageId = imageList.Tables("imageTable").Rows.Item(0).Item("id")
        End If

        Dim rowObj As DataRow
        Dim tableRow As TableRow
        Dim tableCell As TableCell
        Dim imgLink As LinkButton
        Dim imgTitle As Label
        linkTable.Dispose()
        linkTable = New System.Web.UI.WebControls.Table()

        For Each rowObj In imageList.Tables("imageTable").Rows
            tableRow = New TableRow()
            tableCell = New TableCell()

            If currentImageId = rowObj.Item("id") Then
                imgTitle = New Label()
                imgTitle.Text = rowObj.Item("imageTitle") & rowObj.Item("id")
                imgTitle.CssClass = "imageList"

                tableCell.Controls.Add(imgTitle)
                tableRow.Cells.Add(tableCell)
                linkTable.Rows.Add(tableRow)
            Else
                imgLink = New LinkButton()
                imgLink.Attributes.Add("currentImageId", rowObj.Item("id"))
                imgLink.Text = rowObj.Item("imageTitle") & rowObj.Item("id")
                imgLink.CssClass = "imageList"

                AddHandler imgLink.Click, New System.EventHandler(AddressOf imageLink_click)

                tableCell.Controls.Add(imgLink)
                tableRow.Cells.Add(tableCell)
                linkTable.Rows.Add(tableRow)
            End If
        Next

        loadImage(currentImageId)
    End Sub

    Sub imageLink_click(ByVal sender As Object, ByVal e As EventArgs)
        Dim currentImage As Integer
        sender = CType(sender, LinkButton)
        ViewState("currentImage") = CInt(sender.attributes.item("currentImageId"))
        'Label2.Text = "Clicked Id: " & viewstate("currentImage")

        loadInfo(ViewState("currentImage"), "imageLink")
        'Response.Redirect("product.aspx?cimg=" & CInt(sender.attributes.item("currentImageId")) & "&productId=" & _productId)
    End Sub

    Sub loadImage(ByVal currentImageId As Integer)
        productImage.ImageUrl = productImage.ResolveUrl("../images/productImages/") & currentImageId & ".jpg"
    End Sub

    Public WriteOnly Property productId() As Integer
        Set(ByVal Value As Integer)
            _productId = Value
        End Set
    End Property
End Class


It's probably better to look at it than have me explain it.

Thanks!
In your click post backs, loadinfo is getting called twice. If clicking on links is the only control on page that will cause postbacks, then put a check in Page_Load to make sure that on postbacks, it is not adding the controls.

Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
       Dim currentImage As Integer
       sqlCon = New SqlConnection(ConfigurationSettings.AppSettings("connString"))
       sqlCon.Open()
       If Not IsPostBack Then
       loadInfo(ViewState("currentImage"), "loadPage")
       End If
   End Sub

Give this a go...and see if this helps.
OWASP Proactive Controls

Learn the most important control and control categories that every architect and developer should include in their projects.

Author

Commented:
I tried that already... When I do it, the list fails to load after I click a link. So, I click one of the LinkButtons, and when the page loads again, the list is empty.

And, oddly enough, if I get rid of the loadInfo call in the event handler, there is absolutely no difference in the performance of the page. Two clicks still works.

The only difference is that when I have the loadInfo in the event handler, the first time I click on a link it loads immediately. After that, it always requires two clicks. If I take the loadInfo OUT of the event handler, it ALWAYS requires two clicks, not just after the first time.

This leads me to believe that after the first time, the event handlers only get assigned after the page "starts over" again. It appears to be impossible to assign event handlers and have them work on a postback. Am I right?

Thanks a lot!

Author

Commented:
Also, here's an email I sent to Victor, a Microsoft MVP:

Victor,

It appears that you are correct. It is impossible to assign event handlers after Page_Load. So, here's the remaining problem:

As you can see from the code I sent, I have to be able to reload that list in response to a user click. That click is being caught by my event handler. When I reload the list, I need the event handlers to be properly assigned. So, the solution is obvious, but I don't know how to accomplish it. I need to be able to assign the new currentImageId to the ViewState, and then refresh the page so that LoadInfo can be called from Page_Load, thus correctly assigning the event handlers.

So, I need some way to force the control to reload with the new data. I can't use Response.Redirect, because it's a user control, and I don't want to mess with the containing page. Ditto for Server.Transfer. Plus, when I mess with page transfers, I lose the viewState, which is very bad. So, how can I force the control (and the page) to reload after my event handler gets called?

Thanks a bunch!

Matt
Hi,

I am mainly posting because I have the exact same problem as you. What you said in your last comment is completely correct, event handlers must be assigned before the page_load event completes, otherwise they will not work. So you cannot use the event handlers for your linkbuttons to change/add controls and event handlers on your page, since the event handler will be called after the page_load is complete. So somehow you have to handle the link button event in the page_load event, but how you do this I wish I knew.

The solution I currently have is that anytime I need to change the dynamic controls, I add a client side startup script to the page which tells it to re-post itself. When it does, the page_load event is fired again and I can assign the event handlers. Of course, this is a terrible solution but it is the best that I have come up with so far.

Zaphod.
Here is how the processing for dynamic event handlers work..

1. First time, you create the page and add event handlers for your dynamic controls.
2. when you click on these controls, it does a postback. But to handle this post back, it needs an instance of Control and the event handler to exist in the page's class. This is where it runs into trouble. If you don't add controls back on PostBack, the event never gets handled. therefore you need to add the controls back to page.

Here is what I do to take care of this problem.

1. Create a Panel control on the page that will contain the dynamically added controls.
2. Before populating this panel with control, I call Clear method to remove any existing controls.
3. Then this panel is populated with all the controls along with their event handlers.

4. I have a refresh method in my page that deals with this panel only. On postback, I make sure that the panel containing dynaic controls get repopulated. Since I am clearing the controls before populating it, there is no chance of duplicate controls appearing on the panel.

5. When this control fires a post back event, the processing goes through Page_Load event. At that time, Refresh method is called and all the dynamic controls are added back to page. This will ensure that event handler will get called. the last event that is handled in the chain is the event handler for the control that fired it. In that all processing is done to change the state of page based on this event. And finally Refresh method is called in this event to repopulte the panel based on new values and changes that were made in event handler.

Author

Commented:
Naveenkohli,

Thanks for the response...

I'm having difficulty understanding exactly how this is supposed to work, though. The problem seems to lie in the fact that I need to handle the event on or before Page_Load is called. The reason being that I cannot dynamically assign event handlers after Page_Load is called.

But, the information I need to properly handle the input is only recieved in the event handler itself. Specifically, I need the ID of the photo that was clicked. That is sent to the event handler as a part of the LinkButtons attribute collection. So, I need some way to determine, either in Page_Load or Page_Init, which button was clicked. I'm not sure that this is possible.

The only way this seems possible is to store this information in the queryString. That way, the information is immediately available when the page reloads instead of having to wait for the page execution to get to the event handler.

I think you may have the answer, but I just missed the point. If you can see where I might be mistaken in my reasoning, that would be a big help.

Thanks for all your help!
Here is kind of flow of call...

Click On Image
|
|
Page_Load
 << Reload the image controls the way they were at the time of click. also make sure that event handlers are also added >>

|
|
ImageButton Event Handler
 << Now you can get details about the event, like ID etc. Process the request. Call a routine to clean the previously added image controls in Page_Load. And add the controls that need to be added based on your processing in the event handler. >>

The point where it gets tricky is that you wantt o access the event information in  Page_Load but that does not happen. This is the way event handling happens. The control that fired the events is the last one to get a chance to look at it. So you have to recreate that control in Page_Load.

Commented:
I am having the same problem.  I'm not sure naveen is understanding, or I'm just miss-understanding.  Adding the controls in pageLoad and then trying to access them in the event handler doesn't work.  The reason in my situation is that you can't know how many controls to make in the pageLoad, because what "controls" how many controls there are is the event itself, which can only be accessed AFTER pageload.

I was doing some thinking today, and I came up with a reasonably clean solution to this problem. Basically the page has a certain amount of state information, which determines how many controls and such are to be displayed. The problem is that this state is changed by the control event handlers, which fire after the page load event. So what we need is a way to fire the page_load event after the event handlers, without doing an annoying double reload. So here is what I did.

I defined a bunch of parameters for my page, like blah.aspx?State1=34&State2=56. These parameters tell me exactly what should be displayed to the user. Then, in the event handlers of the controls, instead of adding controls and whatnot, I simply do a Response.Redirect to the same page with the new parameters which tell the page what controls to display and what to put in them. This way, you have page_load firing again after your event handlers have fired, but without having to do a double reload, since the redirect is essentially transparent to the user.

I hope this solution works for you. If you need any more clarification, let me know.

Zaphod.
Response.Redirect is not actually transparent. Server has to send response back to client and then client makes another call to server. This will actually create more network traffic and if user does not have lot of bandwidth, then there will be lot of delay. Better alternative may be to do a server.transfer (if that works). It is matter of how you percieve it. Doing a redirect is kind of doing a double Page_Load with a drawback of creating more client-server calls.
True, it is not totally transparent, but it does have the advantage over the method I mentioned earlier that the user's back button works as expected and the user can't see the double page load. I will however investigate the server.transfer method on Monday.

Zaphod.

Author

Commented:
Well, you tried:) I never did get it to work, so I did some queryString parsing tricks and got around it. It's a lot cleaner than two page loads, so I guess I'm happy:)

Thanks!
Looks like the problem was with control ID's.  Check out Edwin's answer here:

http://www.experts-exchange.com/Programming/Programming_Languages/Dot_Net/Q_20736765.html

Do more with

Expert Office
Submit tech questions to Ask the Experts™ at any time to receive solutions, advice, and new ideas from leading industry professionals.

Start 7-Day Free Trial