ASP.NET: How to destroy dynamically added User Control - asp.net

I added user control to a PlaceHolder with an ID phCustomFields. When I want to remove the user control in phCustomFields, i call phCustomFields.Controls.Clear(). However, after doing this, my usercontrol's Page_Load method still gets hit (i verified this by putting a breakpoint in the user control.) Why is my user control's page_load method still being called? i thought the instance of my user control was removed and destroyed once i called the Clear() method.
Update
Here is some sample code that demonstrates my problem. By setting the breakpoint at the Page_Load method of HelloWorld.ascx and debugging Page.aspx, the debug process would stop at the Page_Load method in HelloWorld.ascx which is expected and fine by me. But when I click on "Remove HelloWorld.ascx" button to remove the user control and then click on "Do Nothing" button to cause a postback, the debug process STILL stops at HelloWorld.ascx's Page_Load method. However, this shouldn't happen because the Clear() method in phTest was called.
Page.aspx
<%# Page Language="C#" AutoEventWireup="true" CodeBehind="Page.aspx.cs" Inherits="WebApplication1.Page" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
Control C = LoadControl("HelloWorld.ascx");
phTest.Controls.Add(C);
}
protected void Remove_OnClick(object sender, EventArgs e)
{
phTest.Controls.Clear();
}
</script>
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:Button Text="Does nothing" runat="server" />
<asp:Button Text="Remove HelloWorld.ascx" OnClick="Remove_OnClick" runat="server" />
<asp:PlaceHolder ID="phTest" Visible="false" runat="server">
</asp:PlaceHolder>
</div>
</form>
</body>
</html>
HelloWorld.ascx
<%# Control Language="C#" AutoEventWireup="true" CodeBehind="HelloWorld.ascx.cs" Inherits="WebApplication1.HelloWorld" %>
<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
int i = 0;
}
</script>
<b>Hello World</b>

The OnClick event happens after the Page_Load event in the Page Life Cycle. On a postback the user controls Page_Load event will still be called. If you put the Clear() command later in the parent page life cycle, that should help.
EDIT As per your update. The reason that the Page_Load of the user control is happening every time is because you have the code to add that control to the page in the Page_Load event of your .aspx page. Basically, every time the page is requested, that user control is going to be added to the page(and hit the user control Page_Load event).
If you want a certain action to happen/not happen on a postback use the IsPostBack property of the page. Not sure exactly what you want to do with the page, but hopefully that helps a little bit.

Clear() definitely removes the controls from the control tree and hence the page lifecycle. Internally it sets the reference to the control to null.
The only way this could still be happening is if another instance of that control lives somewhere else in the control tree, or you're not calling Clear as early as you think, or not calling it on the right collection. At what point in the page lifecycle are you calling Clear()?
Edit: you are calling Clear in the Remove_OnClick evnet handler, which is long after Page_Load has fired for all controls. OnLoad will fire for the page and for all controls before it moves on to the next event.

Here's a few relevant highlights from the webforms page life cycle:
Page load event handled
Postback events, including click events handled
Page pre-render event handled
So you have the option of performing whatever processing you need to do in the control in the Page_PreRender event handler instead of Page_Load. As this occurs after the postback events are handled, it will not happen if the control has been removed.

Related

AjaxControlToolkit issue in Async WebForms pages

There is an issue with AjaxControlToolkit extenders (or any other extenders) in WebForms pages that run Async tasks. If your extender is not visible initially, and you run an async task where you make it visible (e.g. if the visibility should be determined based on the data that you read asynchronously), then you will get the following System.ArgumentException:
Extender control '[ControlID]' is not a registered extender control.
Extender controls must be registered using RegisterExtenderControl() before calling RegisterScriptDescriptors().
After some analysis, here is what causes this.
ExtenderControl base class, which all AjaxControlToolkit extenders
subclass, calls ScriptManager.RegisterExtenderControl() in its
OnPreRender method. Moreover, ScriptManager will throw an exception
if RegisterExtenderControl() is called during any stage other than
PreRender.
For controls that are not visible during the PreRender stage, the
OnPreRender method will not be called, and hence the extender
control will not be registered.
WebForms pages run all async tasks after the PreRender stage, and
before the Render stage. So, if you make your extender visible in
the async task, then it will not be visible yet in the PreRender
stage, and only in the Render stage.
Finally, extender controls call
ScriptManager.RegisterScriptDescriptors() during the Render stage,
which throws the above exception due to the control not having been
registered in the PreRender stage.
Has anyone found a fix or workaround for this?
This seems to be a huge limitation in WebForms, where you cannot effectively use both async tasks and extender controls in the same page.
Below is a sample web page that illustrates this issue.
<%# Page Async="true" MasterPageFile="~/Site.Master" Language="C#" AutoEventWireup="true" Inherits="System.Web.UI.Page" %>
<%# Import Namespace="System.Threading.Tasks" %>
<%# Register Assembly="AjaxControlToolkit" Namespace="AjaxControlToolkit" TagPrefix="ajaxToolkit" %>
<asp:Content ID="BodyContent" ContentPlaceHolderID="MainContent" runat="server">
<asp:Panel ID="Panel1" runat="server">
<asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
<ajaxToolkit:CalendarExtender ID="CalendarExtender1" runat="server" TargetControlID="TextBox1" />
</asp:Panel>
</asp:Content>
<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
Panel1.Visible = false;
RegisterAsyncTask(new PageAsyncTask(ReadAsync));
}
// Making the panel visible below will result in the following exception:
// Extender control 'CalendarExtender1' is not a registered extender control.
// Extender controls must be registered using RegisterExtenderControl() before calling RegisterScriptDescriptors().
private async Task ReadAsync()
{
Panel1.Visible = true;
await Task.CompletedTask;
}
</script>
Based on suggestions posted to this question here
https://forums.asp.net/p/2163988/6293999.aspx?p=True&t=637169211245961059
and here https://github.com/DevExpress/AjaxControlToolkit/issues/523, I have come up with a generic way to work around this problem.
Basically, your page would track controls that have extenders and can change their visibility while running in an async task. Then it will make those controls temporarily visible at the proper time on PreRender to avoid the error, and then hide them as needed after the PreRender is complete. The key here is to use the page's new SetControlVisible method to set control's visibility.
The full source code for this workaround is posted here: https://github.com/Xomega-Net/XomegaFramework/blob/master/src/Xomega.Framework.Web/Views/WebPage.cs

Keep a value accessible in Page.Init between postbacks in asp.net

Ok, I've already learned that in order to keep the dynamic (created) controls and their viewstate in asp.net we must (re)create them in the Page init event, so they already exist within the page's control hierarchy before the view state is loaded.
As it says in this article.
There is no problem to achieve this behaviour if the criteria to create these controls is outside the web environment, for example a database. But what should I do if what I use to decide how many dynamic controls I have to create is actually a value that is in the controls?
I try to explain it with an example, maybe it's clearer:
Let's say that we have a textbox and two buttons. In the textbox I write the number of how many dynamic controls I want to create, for example 4 checkbox. When I hit the button1 the controls should be created. No problem. Then I check some checkboxes and hit the button2 just to fire a postback. Now I should recreate the controls in the page init event, like we said before, in order to maintain the controls and their state.
And here comes the problem. Because of I'm in the init stage I have no viewstate so I'm no able to access the value in the textbox that tells me how many dynamic checkbox should I create.
I thought that storing the value in the session object would do the trick, but it doesn't. The session object is no accessible as well.
Where can I save the value that it'll be accessible from the init event too?
Thanks and sorry for the long post!
First thing - textbox value is not stored/retrieved from view state, you cannot get textbox value from viewstate.
Coming to actual problem, here is the sequence of (imp) events init -> load view state -> bind postback data -> page load. You can retrieve textbox value only after bind postback data event (which actually takes posted data and binds to the textbox control).
In init only option is to use Request.Form{"textboxid"] to get the textbox value.
You are on the right track.
If you use TextBox, you do not need another ViewState to keep track of how many controls has been created, because TextBox control has its own ViewState already.
You can use either Page_Init or Page_Load to load control back.
<%# Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm1.aspx.cs"
Inherits="WebApplication2010.WebForm1" %>
<!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></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:TextBox runat="server" ID="NumberTextBox" />
<asp:Button runat="server" ID="CreateControlButton"
OnClick="CreateControlButton_Click"
Text="Create Control" />
<br />
<asp:PlaceHolder runat="server" ID="PlaceHolder1"></asp:PlaceHolder>
</div>
</form>
</body>
</html>
using System;
using System.Web.UI;
namespace WebApplication2010
{
public partial class WebForm1 : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (IsPostBack)
{
int ids;
if (Int32.TryParse(NumberTextBox.Text, out ids))
{
for (int i = 0; i < ids; i++)
{
Control ctrl = Page.LoadControl("WebUserControl.ascx");
ctrl.ID = i.ToString();
PlaceHolder1.Controls.Add(ctrl);
}
}
}
}
protected void CreateControlButton_Click(object sender, EventArgs e)
{
}
}
}
<%# Control Language="C#" AutoEventWireup="true"
CodeBehind="WebUserControl.ascx.cs"
Inherits="WebApplication2010.WebUserControl" %>
<asp:CheckBox runat="server" ID="CheckBox1" />
<asp:Button runat="server" ID="Button1" OnClick="Button_Click"
Text="Post Back" />
<asp:Label runat="server" ID="Label1" />
<br />
using System;
namespace WebApplication2010
{
public partial class WebUserControl : System.Web.UI.UserControl
{
protected void Button_Click(object sender, EventArgs e)
{
Label1.Text = " This control was posted back.";
}
}
}
This is the common paradox with page lifecycle when you work with code behind. For example, you save the editing customerid in viewstate but controls need to bind on init to be able to read their posted values.
Best real life solution is to do what bound controls do on postback, call explicilately the LoadPostBackData on IPostBackDataHandler interface that all controls implement, after you have created them in Page Load event.
You should create an extension method for Controls and call it when needed:
control.DataBind();
control.LoadPostedData(); // extension method

Dynamically creating ASP.NET Web User Control contain AJAX Extender is throwing error "Extender controls may not be registered after PreRender"

I am trying to dynamically add a web user control that contains an AJAX collapsible panel with a Gridview inside the panel when a user clicks on a button. I am able to add a single instance of the user control but when I add additional user controls it throws the following error:
Description: An unhandled exception occurred during the execution of
the current web request. Please review the stack trace for more
information about the error and where it originated in the code.
Exception Details: System.InvalidOperationException: Extender controls
may not be registered after PreRender.
Source Error:
An unhandled exception was generated during the execution of the
current web request. Information regarding the origin and location of
the exception can be identified using the exception stack trace below.
I am new to ASP.NET Development and the method I am using to handle postbacks is to store the user controls in a list and add them again on prerender method call. I am not sure what to do or if I am handling the creation of the user controls correctly. Any advice is appreciated.
Here is the back end code:
public partial class Test : System.Web.UI.Page
{
private IList<Control> _persistedControls;
private const string PersistedControlsSessionKey = "thispage_persistedcontrols";
static int count = 1;
private IList<Control> PersistedControls()
{
if (_persistedControls == null)
{
if (Session[PersistedControlsSessionKey] == null)
Session[PersistedControlsSessionKey] = new List<Control>();
_persistedControls = Session[PersistedControlsSessionKey] as IList<Control>;
}
return _persistedControls;
}
protected void Page_Load(object sender, EventArgs e)
{
PersistedControls();
}
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
IList<Control> controlsToGenerate = PersistedControls();
// regenerate dynamically created controls
foreach (var control in controlsToGenerate)
{
MasterPanel.Controls.Add(control);
}
}
protected void Button1_Click(object sender, EventArgs e)
{
Control control = LoadControl("~/WebUserControl/UseCaseSetupUserControl.ascx");
control.ID = "uc" + count++.ToString();
MasterPanel.Controls.Add(control);
_persistedControls.Add(control);
MasterPanel.Controls.Add(new LiteralControl("<br />"));
_persistedControls.Add(new LiteralControl("<br />"));
}
}
Here is the ASPX Code:
<%# Page Language="C#" AutoEventWireup="true" CodeFile="Test.aspx.cs" Inherits="Test" %>
<!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></title>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server">
</asp:ScriptManager>
<div>
<asp:Button ID="Button1" runat="server" Text="Button" onclick="Button1_Click" />
<br />
<asp:Panel ID="MasterPanel" runat="server">
</asp:Panel>
</div>
</form>
</body>
</html>
Create your controls as early as possible in the Page life cycle.
Since you have the PersistedControls on load, that is where you should add them to the controls collection. The best place to add controls in Init.
If you really can't do that then use Load.
If this must be done on an event, then you can add the control there for the first time, but persist it and remember to add it back on Init or Load from the next postback onwards.
If you add controls after PreRender they cannot add any of their data to the ViewState. SaveViewState occurs just before PreRender. A lot of controls rely heavily on the ViewState to function properly. The exception is simply telling you that you should add your control earlier on in the Page lifecycle.

ASP.NET Web Forms multiple views and the lifecycle of controls that are not Visible

Given a scenario where a page/control should display different views (like tabs) in different circumstances (query string argument, postback from a control, user setting retrieved from the database) I would normally put a control like MultiView or Placeholder and switch the active view index or the Visible property like this:
<%# Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm2.aspx.cs" Inherits="TestWebApplication.WebForm2" %>
<%# Register Src="SomeControl.ascx" TagName="SomeControl" TagPrefix="uc1" %>
<!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></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:PlaceHolder runat="server" ID="phControl" Visible="false">
<uc1:SomeControl ID="SomeControl1" runat="server" />
</asp:PlaceHolder>
<asp:PlaceHolder runat="server" ID="phNotControl" Visible="false">
Some other content
</asp:PlaceHolder>
</div>
</form>
</body>
</html>
And I would switch the views in a code behind depending on the logic like this:
using System;
namespace TestWebApplication
{
public partial class WebForm2 : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
phControl.Visible = false;
phNotControl.Visible = true;
}
}
}
This is the control:
<%# Control Language="C#" AutoEventWireup="true" CodeBehind="SomeControl.ascx.cs" Inherits="TestWebApplication.SomeControl" EnableViewState="false" %>
I am the control
And the code behind of the control:
using System;
namespace TestWebApplication
{
public partial class SomeControl : System.Web.UI.UserControl
{
protected void Page_Init(object sender, EventArgs e)
{
Response.Write("I am the control's Init and I am executing heavy operations <br />");
}
protected void Page_Load(object sender, EventArgs e)
{
Response.Write("I am the control's Load <br />");
}
protected void Page_PreRender(object sender, EventArgs e)
{
Response.Write("I am the control's PreRender and I am only called if the control is Visible <br />");
}
}
}
However what if the control in question has some relatively heavy work to do (calling the database) in its init and load events? They get executed even if the control is not Visible and will slow down the page. One possible solution I see is moving all the work in the PreRender method of the child control because it is not executed for controls which are not visible. However this will not work if the control accepts user input from some dynamically populated control (think of dropdownlist) because the logic that populates the dropdown with options will not be executed.
I've come up with the following solutions:
Move the heavy work in the PreRender method and wrap it in an if(!IsPostBack). Then let the ViewState hold the possible values for the dropdown and ASP.NET will restore it. The downside is that it is using ViewState and bumping up the size of the page.
Create the control dynamically with LoadControl and add it in the placeholder from code behind. This will trigger catch up events and the code that populates the dropdown can be put in the Init method where it belongs. The downside is that the layout is defined in the code behind. Looking at the markup one cannot see what controls are on the page and looking at the code behind it is not clear where the control will appear. It also makes styling and other "design" operations more difficult.
Leave it like it is. The downside is that there will be additional database queries and if there are a lot of views these would add up.
My actual question is how I solve this problem without putting stuff in the ViewState, without multiple queries and without dragging my layout into the code behind.
Could it be a solution to make an AJAX call for your heavy database work?
Then you could display the View and in the background load the data you need and display it. The page would load really fast and you could display some indication that data is currently being loaded.
How about creating a public method on in your user controls, such as InitControl(). Do all the heavy work within InitControl() instead on page load. Then, on parent page, when page is being shown, call InitControl.
using System;
namespace TestWebApplication
{
public partial class WebForm2 : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
phControl.Visible = false;
phNotControl.Visible = true;
SomeControl1.InitControl();
}
}
}
Don't do any work within OnLoad event of your user controls.

TemplateField button causing GridView Invalid Postback

Ok, so I've got a template field in a gridview that contains just a simple button...
<%# Page Language="C#" AutoEventWireup="true" CodeFile="Administration.aspx.cs"
Inherits="Administration" %>
<%# Register TagPrefix="ajaxToolkit" Namespace="AjaxControlToolkit" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Keywords Administration</title>
</head>
<body class="popupbody">
<form id="form1" runat="server">
<ajaxToolkit:ToolkitScriptManager ID="ToolkitScriptManager1" runat="server" EnablePartialRendering="true" CombineScripts="false"></ajaxToolkit:ToolkitScriptManager>
<asp:Label ID="AddLabel" runat="server">Add a Keyword</asp:Label>
<br />
<asp:TextBox ID="AddTextBox" runat="server" />
<asp:Button ID="AddButton" Text="Add" runat="server" OnClick="AddKeyword_Click" />
<asp:GridView ID="KeywordsGridView" AllowPaging="false" AutoGenerateColumns="false" BackColor="white"
GridLines="None" HeaderStyle-CssClass="Table_Header" RowStyle-CssClass="Table_Style"
OnRowDataBound="RowBound" runat="server">
<Columns>
<asp:TemplateField>
<ItemTemplate>
<asp:Button runat="server" />
</ItemTemplate>
</asp:TemplateField>
<asp:BoundField DataField="References" SortExpression="References" HeaderText="Total References" />
<asp:BoundField DataField="Keyword" SortExpression="Keyword" HeaderText="Keyword" />
</Columns>
</asp:GridView>
</form>
</body>
</html>
Whenever I click the button I get the error...
Invalid postback or callback argument. Event validation is enabled using
<pages enableEventValidation="true"/> in configuration or <%# Page
EnableEventValidation="true" %> in a page. For security purposes, this feature
verifies that arguments to postback or callback events originate from the server
control that originally rendered them. If the data is valid and expected, use the
ClientScriptManager.RegisterForEventValidation method in order to register the
postback or callback data for validation.
I've found a decent amount of articles referencing this issue, including a couple on SO, for example...
Invalid postback or callback argument. Event validation is enabled using '<pages enableEventValidation="true"/>'
and...
Invalid postback or callback argument
I might just be misunderstanding, but as far as I can tell they don't really help me. How do I get this to go away without setting enableEventValidation="false"?
EDIT Posted all the code for my page.
I know this post is old, but I also had this problem recently.
After checking the how the code was generated for the asp:ButtonField and also for the asp:Button that I have added in an ItemTemplate I discovered quite a big difference:
asp:ButtonField:
input type="button" onclick="javascript:__doPostBack('ctl00$ctl00$cphMain$cphContent$gvEmails','SendFromBatch$0')" value="Continue">
asp:Button:
input id="ctl00_ctl00_cphMain_cphContent_gvEmails_ctl02_btnCont" type="submit" onclick="FreezeScreen('Emails are being sent...');" value="Continue" name="ctl00$ctl00$cphMain$cphContent$gvEmails$ctl02$btnCont">
The Type was the problem! So, after i changed the UseSubmitBehavior from "Submit" to "False" I did not get the error anymore.
EDIT:
I recently wanted to change from the normal btns to imgBtns so that I can save some space and also make the gridview nicer :D. Of course I had the same problem popping up and the imagebtn does not have UseSubmitBehavior property, so I started looking after a solution.
Found the following (The code below is in a UserControl).
1) Bind in !IsPostBack
2) Register the usercontrol itself in the Render
and everything works just as planned - no validation errors.
protected void Page_Load(object sender, EventArgs e)
{
OnLoadingEvent(new EventArgs());
if (!Page.IsPostBack)
{
gvEmails.DataSource = odsEmails;
try
{
gvEmails.DataBind();
}
catch (Exception)
{
}
}
if (!writeText) divWriteEmail.Visible = false;
}
protected override void Render(HtmlTextWriter writer)
{
Page.ClientScript.RegisterForEventValidation(this.UniqueID);
base.Render(writer);
}
EDIT:
I was playing around with the code above and asked myself, what if you actually need to rebing your objectdatasource - what happens then? Well the code above will not work if you rebind in the page load, because the gridview will be rebound again when you click a btn in the gridview's row and it will not be recognized as being generated on the server. Fair and square, but how to avoid this behaviour? Well... i figured out a way, maybe not the best but it does the trick.
Actually, you do not want to rebind the gridview when you click a btn on the gv's row... but how do we know that since the page load method is first called? Well, actually i do not want to rebind the gridview if the select param of the object data source do not change. So, I keep the select param of the objectDataSource in the session view and I rebind my gridview only when one of them changes - this behaviour is only during the PageLoad event. In order to see latest rows i click refresh and get the latest rows with no problem and at the moment i click a gvRow btn, the error dissapears.
To make all of this happen you have to
Directly call the .Select() of the object data source
Catch the OnSelecting event from the datasource, save the select param set the e.Cancel = true
Check if the select param are different and then bind the gridview and this time when catching the OnSelecting you have to set e.Cancel = false in order to retrieve the data from the DB - only one time.
Hope this helps!
This fellow found a solution to a similar problem (scroll down to about the 4th comment), which was to set unique id's for the GridView buttons.
I had the similar error today but with a different solution. I've worked with gridviews for years now and never had the issue so I figured it must be something stupid. Turns out I had forgotten to put my code that loads the gridview into a if(!Page.IsPostBack) block which caused the button to be recreated after I clicked it, thus causing the error. Placing the loading code in said block eliminated the issue.
You need to give your button an ID. Just a runat="server" does not meet the minimum information that needs to be provided for a server control to be created.

Resources