Wednesday, March 28, 2012

LinkButtons programmically added to updatepanel click event not firing

I have a conditional updatepanel with a multiview control inside. The first view displays a list of items in a table with a linkbutton next to each one created programmically with an eventhandler for the click event. The click event changes the multiview's active view to the next view to display controls for editing that item. The item table's content is first initialized in the OnLoad() function inside a if(!isPostback). I plan to eventually have a save button in the second view that will update the table's content after that. (ScriptManager EnablePartialRender = "true").

Ex:

    for(int i = 0; i < itemcount; i++) { ... LinkButton itemedit = new LinkButton(); itemedit.Text = "Edit"; itemedit.Click += new EventHandler(itemedit_Click); table.Controls.Rows[i].Cells[1].Add(itemedit); ...}

When I view the page and click the linkbutton next to one of the items in the updatepanel it starts to update. However, the view never changes. Further tests have shown that the click event is never being handled. If I change the code so that the table is updated during initial load and postbacks, I can get the click event to fire once. After the multiview is goes back to the original view, the click event can no longer be raised until the whole page is reloaded.

So, what I am trying to figure out is how to create eventhandlers at runtime inside an updatepanel and getting them to fire on an update.

Well, I have found a solution. I desided to scale the problem down to the most basic functions by creating a test application containing an updatepanel, multiview control with two views, and some code in the background to create a linkbutton to switch from one view to the other. The page looks like this as generated by VS:

<%@. Page Language="C#" AutoEventWireup="true" CodeFile="dynamicevent.aspx.cs" Inherits="dynamicevent" %
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<atlas:ScriptManager ID="ScriptManager1" runat="server" EnablePartialRendering="True">
</atlas:ScriptManager>

</div>
<atlas:UpdatePanel ID="UpdatePanel1" runat="server">
<ContentTemplate>
<asp:MultiView ID="MultiView1" runat="server" ActiveViewIndex="0">
<asp:View ID="View1" runat="server">
view1</asp:View>
<asp:View ID="View2" runat="server">
view2</asp:View>
</asp:MultiView>
</ContentTemplate>
</atlas:UpdatePanel>
</form>
</body>
</html>

The page is pretty basic and so is the code behind:

public partialclass dynamicevent : System.Web.UI.Page{protected void Page_Load(object sender, EventArgs e) { LinkButton button =new LinkButton(); button.Text ="switch"; button.Click +=new EventHandler(button_Click);this.View1.Controls.Add(button); }void button_Click(object sender, EventArgs e) {this.MultiView1.ActiveViewIndex = 1; }}

This code creates one LinkButton to switch from ActiveViewIndex=0 to 1 and it works just fine. However, on a larger scale, creating a hundred or more LinkButtons next to records that must be requested from a database each time the page handles a postback doesn't sound like a good idea to me. So, I moved by button creation code into an if(!IsPostBack) block:

protected void Page_Load(object sender, EventArgs e) {if (!this.IsPostBack) { LinkButton button =new LinkButton(); button.Text ="switch"; button.Click +=new EventHandler(button_Click);this.View1.Controls.Add(button); } }
The changes break the page. When I click the LinkButton, the updatepanel reloads the contents of the first view minus the linkbutton. The linkbutton is lost and its eventhandler with it. So, perhaps atlas requires that everything remain wired up perfectly up until after Page_Load() is called at least. I did some thinking and came up with some code that fixes the problem by using the cache to store the button to be rewired on the next postback. It appears that that is all the work I need to do to get my click handler to work. I think the benefits are significant enough although it is hard to see in this example: protected void Page_Load(object sender, EventArgs e)
{
if (!this.IsPostBack)
{
LinkButton button =new LinkButton();
button.Text ="switch";
button.Click +=new EventHandler(button_Click);

this.View1.Controls.Add(button);
Cache.Insert("test", button);
}
else
{
((LinkButton)Cache["test"]).Click +=new EventHandler(button_Click);
this.FindControl(((LinkButton)Cache["test"]).Parent.UniqueID).Controls.Add(((LinkButton)Cache["test"]));
}
}

The code rewires the event handler during a postback. The Control.Parent .Controls.Add() does not work directly for the LinkButton in the cache but the FindControl() method works just fine. I believe that although they have the same name, the first parent of the LinkButton is not the same as the second one. I am new to ASP.NET v2 and Atlas but I do not remember coming across this problem in the past with .NET 1.1.

If anyone has a better solution, please let me know. At this moment I am facing writing some manager class to wire up all my eventhandlers on on each postback and I really would like a better way.


changed

this.FindControl(((LinkButton)Cache["test"]).Parent.UniqueID).Controls.Add(((LinkButton)Cache["test"]));
to
this.Controls.Add(((LinkButton)Cache["test"]));
and it works just fine. This means that it does not matter where the LinkButton is located, just that it is added back to the page.

Thanks a lot for the solution you posted. I have been facing the same problem and your solution worked for me. But, I wonder if there is some other solution to this issue. If I find any I will post it here.

Thanks again.


Yes, that's not specific to Atlas. Any control that's added dynamically to the control tree must be added back on every subsequent postback.

I came across another issue when I was working with this yesterday. What I saw was that after a few postbacks my dynamically added link buttons won't work in fact it threw an object required error. Reason, they were removed from the cache and hencethis.Controls.Add(((LinkButton)Cache["test"])); would not work.

So, I tried to check if the cache had the button in it and only then I would execute this statement else I would create new linkbuttons. But guess what, I got back to the same problem I started off with. The button would not fire the event.

In the solution we completely depend on the cache to make sure that the linkbuttons are wired to the respective events. What would happen when the buttons are removed from the cache for whatever reason, especially in a production environment?


Ok, I think I may have a solution for this...as bleroy indicated this is the default behavior with dynamically added controls (that you have to add them to the page on every postback). So, if you add the linkbuttons to the page on Page_Init instead of Page_Load, you will not have to worry about caching them in order to re-wire the controls with the events.

Let me know if this was helpful.


I'd like to respectfully point out that putting a control in the cache is a bad, bad, bad idea and you should never ever do it. One reason is that this maintains an in-memory reference to the instance of the control, hence to the corresponding instance of the page and thus to a huge object graph that should have been thrown away at the end of the request. There is no way this is not going to blow up whenever you get more than a few simultaneous requests.

You should only put data (that is, disconnected data) or output (i.e. strings) in cache...

No comments:

Post a Comment