My Blog is Moving to http://weblogs.asp.net/ysolodkyy
My blog is moving to http://weblogs.asp.net/ysolodkyy
RSS feed at www.feedburner.com remains unchanged: http://feeds.feedburner.com/ItCouldBeDone.
My blog is moving to http://weblogs.asp.net/ysolodkyy
RSS feed at www.feedburner.com remains unchanged: http://feeds.feedburner.com/ItCouldBeDone.
Posted by Yuriy Solodkyy at 10:53 PM 0 comments
This is from MSDN - http://msdn2.microsoft.com/en-us/library/system.globalization.numberstyles(vs.80).aspx and it works like described. But I expected it to allow "0x".
AllowHexSpecifier
Indicates that the numeric string represents a hexadecimal value. Valid hexadecimal values include the numeric digits 0-9 and the hexadecimal digits A-F and a-f. Hexadecimal values can be left-padded with zeros. Strings parsed using this style are not permitted to be prefixed with "0x".
Posted by Yuriy Solodkyy at 10:49 PM 0 comments
Labels: .Net
Many people find it mysterious when they get a message box with the following error message:
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.
Unfortunately ASP.NET does not provide enough information to troubleshoot this exception easily. I am going to explain reasons that can cause this exception and give a tip on how to troubleshoot it.
The event validation is designed to prevent malicious requests from breaking your software. When a server page is rendered and sent to the browser web controls tell ASP.NET about all allowed values that the web page can submit back to the server. The collected values are hashed and stored in the hidden input in the web page. When the postback values are received from the client postback values are validated against the hashes stored in this hidden input.
<input type="hidden" name="__EVENTVALIDATION" id="__EVENTVALIDATION" value="/wEWDALLu5z7BQKdkKesBQL0pYTCDAL1pYTCDAL2pYTCDAL3pYTCDALwpYTCDALxpYTCDALypYTCDALjpYTCDALspYTCDAL0pcTBDAn2Nrc2z9979msu0SdahUlFp5dq" />
If server encounters any value with a hash which is among hashes in the __EVENTVALIDATION field it throws an exception with the message above. If you develop with pure ASP.NET and receive such exception it usually means that someone attempts to tamper a request and does it incorrectly.
With the ASP.NET AJAX extensions however it becomes possible to get this exception as a result of developer mistake.
I've constructed a sample which demonstrates what you have to do (really you should not do) to get this weird message box.
<%@ Page Language="C#" AutoEventWireup="true" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Conditional Update Panel and Event Validation</title>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server" />
<div>
<asp:UpdatePanel runat="server" ID="FirstUpdatePanel">
<ContentTemplate>
<asp:Button runat="server" ID="ClickMeButton" Text="Click Me!" />
</ContentTemplate>
</asp:UpdatePanel>
<asp:UpdatePanel runat="server" ID="UpdatePanel" UpdateMode="Conditional">
<ContentTemplate>
<asp:DropDownList runat="server" ID="DropDownList">
</asp:DropDownList>
</ContentTemplate>
</asp:UpdatePanel>
</div>
</form>
</body>
</html>
<script runat="server">
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
int startCounter = (int?)ViewState["Counter"] ?? 0;
startCounter++;
ViewState["Counter"] = startCounter;
DropDownList.Items.Clear();
for (int i = startCounter; i < startCounter + 10; i++)
{
ListItem listItem = new ListItem();
listItem.Value = i.ToString();
listItem.Text = i.ToString();
DropDownList.Items.Add(listItem);
if (i == startCounter)
{
listItem.Selected = true;
}
}
//UpdatePanel.Update(); // Uncomment this line to fix the sample!
}
</script>
If you run this sample and click the button twice you will see exactly the same message box. So, what is causing ASP.NET to throw an exception in the sample above?
The sample changes items in the DropDownList on each postback and changes them in a way that previously selected item disappears from the list on the next postback. (Similar things happen if you implement cascading dropdowns with autopostbacks). If you disable partial rendering by setting ScriptManager property
EnablePartialRendering="false"
you will see that the page works perfectly. DropDownList tells ASP.NET about all values in the items collection while it is been rendered and ASP.NET successfully validates submitted value against hashes stored in the "event validation" hidden input on the next postback. If you re-enable partial rendering in this sample, one small but still important thing changes. Render method of the DropDownList is still invoked on every postback, but output of the render method is not sent to the browser, because it is in the UpdatePanel which is not updated in response to button click. (I intentionally made button to cause the asynchronous postback without updating second UpdatePanel). Now, __EVENTVALIDATION hidden input is successfully updated with new hash values for DropDownList, but the DropDownList itself remains unchanged on the client and still submits old selected value on the next postback. When the server receives this value it cannot validate it against the new set of hashes in the "event validation" hidden input and throws he exception.
So, the conclusion is very simple: "Don't change controls during the async postback that are outside an update panel or are in the update panel which is not updated during this asynchronous postback." Or vice versa: "If you have conditional update panels force them to update their client state if you change controls inside the update panel." (See commented code in the sample and try uncommenting it).
Downloadable version of the sample: EventValidation.zip
It is not always easy to find what causes the "event validation failed" problem in the complex ASP.NET page. I recommend following steps to find the control causing the problem:
Now when you know which controls causes the problem you should check:
Posted by Yuriy Solodkyy at 1:04 AM 10 comments
WebClient class is very useful when you need to download or upload date from or to the Web. However, when you need to make a sequence of calls you find that WebClient does not preserve cookies set by the server between requests. Fortunately, WebClient gives you an opportunity to handle cookies by yourself.
The very simple solution is to override GetWebRequest method of the WebClient class and set CookieContainer property.
Here is my implementation:
public class CookieAwareWebClient : WebClient
{
private CookieContainer m_container = new CookieContainer();
protected override WebRequest GetWebRequest(Uri address)
{
WebRequest request = base.GetWebRequest(address);
if (request is HttpWebRequest)
{
(request as HttpWebRequest).CookieContainer = m_container;
}
return request;
}
}
Posted by Yuriy Solodkyy at 12:56 AM 7 comments
Labels: .Net
You will probably never really need to do this, but if it happens, here is how it can be accomplished.
So, assume you have two classes Base and Derived. The Base class introduces a virtual method named Test. The Derived class overrides this method.
Now, as I said in very rare circumstances, you may need to derive another class from the Derived class and in the overridden method Test call original version of Test method. By original I mean implementation provided by the Base class.
C# and VB.Net, the most popular .Net languages do not provide any way to write something similar to:
class DerivedFromDerived : Derived
{
public override void Test()
{
base.base.Test();
}
}
When you call a base class version of Test() C# compiler generates:
.method public hidebysig virtual instance void Test() cil managed
{
.maxstack 8
L_0000: nop
L_0001: ldarg.0
L_0002: call instance void Derived::Test()
L_0007: nop
L_0008: ret
}
The goal is to make compiler generate:
L_0002: call instance void Base::Test()
The syntax of method name used in IL reminded me that C++ always allowed invoking an implementation of the very base class. And indeed, the straightforward solution is to implement it in C++/CLI:
public ref class DerivedFromDerived: public Derived
{
public:
virtual void Test() override
{
Base::Test();
}
};
This C++ code generates verifiable safe IL code.
Posted by Yuriy Solodkyy at 7:44 PM 7 comments
Labels: .Net
UpdatePanel control is used for different purposes like reducing flickering of the page and reducing network traffic generated by a web site. Developers often wrap input boxes into an UpdatePanel to implement cascading drop-downs and update other related controls. Cascading drop-downs can be implemented with a help of Ajax Control Toolkit control extenders, but in general case you will either need to write script code or place controls in an update panel. The latest approach is very easy to implement, but it also has a lot of drawbacks. If you wrap input boxes (and other input controls) in the update panel, you must be aware about the following consequences:
You can place this UpdatePanel to an ASPX page to see the second problem by yourself:
<asp:UpdatePanel runat="server" ID="up">
<ContentTemplate>
<div>
<asp:DropDownList ID="ddl1" runat="server" AutoPostBack="True">
<asp:ListItem>1</asp:ListItem>
<asp:ListItem>2</asp:ListItem>
<asp:ListItem>3</asp:ListItem>
</asp:DropDownList></div>
<div>
<asp:DropDownList ID="ddl2" runat="server" AutoPostBack="True">
<asp:ListItem>1</asp:ListItem>
<asp:ListItem>2</asp:ListItem>
<asp:ListItem>3</asp:ListItem>
</asp:DropDownList></div>
<div>
<asp:TextBox runat="server" ID="tb"></asp:TextBox><br />
</div>
</ContentTemplate>
</asp:UpdatePanel>
I need to remind some details of how UpdatePanel works to answer this question.
If ScriptManager EnablePartialRendering property is set to true, controls on the web page can initiate asynchronous post-back. When it happens web page send asynchronous request to the server. The web page instance at the server goes through all the phases in its normal lifecycle, but instead of full rendering only partial rendering happens. The html content for updated panels is sent back to the client in the format that client script manager control can recognize. The client part of the ScriptManager control parses the response and set innerHTML properties of all DIV elements generated by UpdatePanels.
So, what happens to the UpdatePanel with input controls? When client script receives a response from the server with new Html content for the UpdatePanel it assigns this content to the DIV element generated by the UpdatePanel. Input focus at this time is in one of the controls inside the DIV element (UpdatePanel). The browser destroys the old content of the UpdatePanel including input controls and creates new elements by parsing new content assigned to the innerHTML property. Input focus moves out of the update panel when focused input controls is being destroyed and it is not restored later when the new controls are created from the assigned html.
The basic idea behind the solution is to save the ID of the control with input focus before the update panel is updated and set input focus back to that control after the update panel is updated.
I come with the following JavaScript which restores the lost focus in the update panel.
var lastFocusedControlId = "";
function focusHandler(e) {
document.activeElement = e.originalTarget;
}
function appInit() {
if (typeof(window.addEventListener) !== "undefined") {
window.addEventListener("focus", focusHandler, true);
}
Sys.WebForms.PageRequestManager.getInstance().add_pageLoading(pageLoadingHandler);
Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded(pageLoadedHandler);
}
function pageLoadingHandler(sender, args) {
lastFocusedControlId = typeof(document.activeElement) === "undefined"
? "" : document.activeElement.id;
}
function focusControl(targetControl) {
if (Sys.Browser.agent === Sys.Browser.InternetExplorer) {
var focusTarget = targetControl;
if (focusTarget && (typeof(focusTarget.contentEditable) !== "undefined")) {
oldContentEditableSetting = focusTarget.contentEditable;
focusTarget.contentEditable = false;
}
else {
focusTarget = null;
}
targetControl.focus();
if (focusTarget) {
focusTarget.contentEditable = oldContentEditableSetting;
}
}
else {
targetControl.focus();
}
}
function pageLoadedHandler(sender, args) {
if (typeof(lastFocusedControlId) !== "undefined" && lastFocusedControlId != "") {
var newFocused = $get(lastFocusedControlId);
if (newFocused) {
focusControl(newFocused);
}
}
}
Sys.Application.add_init(appInit);
If you save this code to FixFocus.js file, it can be used as:
<asp:ScriptManager ID="c_scriptManager" runat="server">
<Scripts>
<asp:ScriptReference Path="~/FixFocus.js" />
</Scripts>
</asp:ScriptManager>
Unfortunately, different browsers handle input focus a little differently. Mozilla FireFox browser does not provide an easy way to get currently focus element at all. The script, therefore, handles these differences between browsers.
You may find the focusControl function a little strange. I cannot explain the magic it does, but this is really required to set focus to the control in the Internet Explorer. ASP.NET AJAX extensions use this code itself when setting focus set by ScriptManager.SetFocus method.
I tested this code with Internet Explorer 7 and the most recent build of Mozilla FireFox.
You can download the sample web site here.
You can often solve the problem by placing each input control into its own update panel. However, this approach does not solve the problem if user press TAB to go to the next control, which is in its own UpdatePanel.
The code above is a workaround. If you need to implement cascading drop-downs it is better to go with Ajax Control Toolkit Cascading Drop-Down Control Extenders. If you need to use UpdatePanel or several UpdatePanels use this code, but be aware of problem #1 which is not solved by this script.
Posted by Yuriy Solodkyy at 9:35 PM 27 comments
I don't know if it is obvious how important ViewState is for hidden controls. I would like to stress where ViewState plays important role for hidden controls (not visible) as it often leads to not obvious problems.
If you like to explore how different ASP.NET controls use ViewState to store their data, Nikhil Kothari's Web Development Helper is a very helpful tool. I definitely recommend to download and install it, if you like to know what is in your ViewState.
TextBox when placed on a web form usually does not require ViewState to be enabled to provide its core functionality "being input box". You can check that __VIEWSTATE hidden field is the same across post-backs if you change the value in your TextBox on the following form:
<form id="form1" runat="server">
<div>
<asp:TextBox runat="server" ID="TextBox1"></asp:TextBox>
<asp:TextBox runat="server" ID="TextBox2"></asp:TextBox>
<asp:Button runat="server" ID="Submit" Text="Submit" />
</div>
</form>
ASP.NET does not store value of the Text property of TextBox in the ViewState, because its value is submitted by the <input type=text> to the server on each post-back. (ASP.NET does save Text property into ViewState if TextChanged event has at least one event handler. This is required to compare currently submitted value with a previous value and raise event only when the value has changed).
However, when TextBox control is not visible or it is placed in any not visible control, TextBox controls does not render <input type=text> to the Html writer and ViewState plays its important role. Invisible TextBox controls save their Text property as all other properties in its ViewState collection.
It is easy to see how disabled ViewState impacts behavior of:
<asp:MultiView runat="server" ID="MultiView">
<asp:View runat="server" ID="View1">
<asp:Button runat="server" ID="SwitchToNextButton" Text="Next" OnClick="SwitchToNextButton_Click" />
<asp:TextBox runat="server" ID="TextBox" />
</asp:View>
<asp:View runat="server" ID="View2">
<asp:Button runat="server" ID="SwitchToPreviousButton" Text="Previous" OnClick="SwitchToPreviousButton_Click" />
</asp:View>
</asp:MultiView>
<asp:Button runat="server" ID="Button" Text="Postback" />
protected void SwitchToNextButton_Click(object sender, EventArgs e)
{
MultiView.SetActiveView(View2);
}
protected void SwitchToPreviousButton_Click(object sender, EventArgs e)
{
MultiView.SetActiveView(View1);
}
You can type some text in the TextBox and switch to another View in the MultiView. Then, if you click "Previous" button, the first View appears and TextBox control shows text you have typed.
If you disable ViewState for the MultiView (and thus for all its children), TextBox keeps showing your text only while it is visible. Once you hide it by switching to another view, TextBox becomes empty.
Be accurate when disabling ViewState for a whole page or for a large part of the page, if you don't know what controls exactly will be placed there.
Posted by Yuriy Solodkyy at 10:40 PM 2 comments
Labels: ASP.NET
ITemplate interface is used by multiple ASP.NET controls to setup repeatable or dynamically instantiated parts of a control.
The ITemplate interface consists of a single method InstantiateIn which takes single parameter container. Implementation is expected to instantiate template controls in the given container when InstantiateIn method is invoked.
public interface ITemplate {
void InstantiateIn(Control container);
}
ASP.NET runtime supports ITemplate by providing built-in facilities for creating instances of ITemplate from markup. ASP.NET runtime assigns UpdatePanel.ContentTemplate property a reference to ITemplate implementation which instantiates a Button.
<asp:UpdatePanel ID="UpdatePanel1" runat="server">
<ContentTemplate>
<asp:Button ID="Button1" runat="server" Text="Button" />
</ContentTemplate>
</asp:UpdatePanel>
Most often ITemplate interface implemented by providing ASP.NET markup for server control properties as in the previous example or as UserControls.
However, if you often need to implement ITemplate interface in code it becomes quite boring to create a new class for each case. Such classes typically look like:
using System.Web.UI;
using System.Web.UI.WebControls;
public class TemplateImplementation : ITemplate {
private string m_someUsefulData;
public TemplateImplementation(string someUsefulParameter) {
m_someUsefulData = someUsefulParameter;
}
public void InstantiateIn(Control container) {
Button b = new Button();
b.ID = "B";
b.Text = m_someUsefulData;
container.Controls.Add(b);
}
}
and then used like:
emailField.ItemTemplate = new TemplateImplementation("Some Text");
This class constructor takes parameters and store them in private fields for use them in the InstantiateIn method. You need to pass everything you need to instantiate controls into the constructor.
Fortunately, compiler can do all this routine for us allowing to write straightforward code to instantiate required controls in context of the place where ITemplate property is assigned. This can be accomplished by creating generic ITemplate implementation taking delegate to a method creating controls as constructor parameter.
public delegate void InstantiateTemplateDelegate(Control container);
public class GenericTemplateImplementation : ITemplate {
private InstantiateTemplateDelegate m_instantiateTemplate;
public void InstantiateIn(Control container) {
m_instantiateTemplate(container);
}
public GenericTemplateImplementation(InstantiateTemplateDelegate instantiateTemplate) {
m_instantiateTemplate = instantiateTemplate;
}
}
This simple ITemplate implementation can be used to assign ItemTemplate property of a TemplateField column in the GridView. Your code will be similar to:
string buttonText = "Get e-mail";
TemplateField emailField = new TemplateField();
emailField.HeaderText = "Dynamically Added E-Mail Field";
emailField.ItemTemplate = new GenericTemplateImplementation(
delegate(Control container)
{
Button b = new Button();
b.ID = "b";
b.Text = buttonText;
container.Controls.Add(b);
});
GridView1.Columns.Add(emailField);
The code creating button in the previous sample can access buttonText variable which is defined in the context where ItemTemplate property is assigned. You, therefore, avoid creating new class and passing the entire required context to its constructor.
Posted by Yuriy Solodkyy at 1:43 AM 4 comments
Labels: ASP.NET, Dynamic Controls
Accordion control is a nice visual control in the Ajax Control Toolkit which can be used similar to TabContainer control. It allows user to switch between different pages of content.
The very simple sample of how to use the Accordion control:
<act:Accordion ID="Accordion1" runat="server">
<Panes>
<act:AccordionPane runat="server" ID="pane1">
<Header>
Header 1</Header>
<Content>
Content 1</Content>
</act:AccordionPane>
<act:AccordionPane runat="server" ID="AccordionPane1">
<Header>
Header 2</Header>
<Content>
Content 2</Content>
</act:AccordionPane>
</Panes>
</act:Accordion>
Content and Header are ITemplate properties of AccordionPane control. Both these properties are decorated with
[TemplateInstance(TemplateInstance.Single)]
attribute, which makes controls declared inside this template accessible as page properties like any other controls on the page. Thus if you have the following accordion on the web page
<act:Accordion ID="Accordion1" runat="server">
<Panes>
<act:AccordionPane runat="server" ID="pane1">
<Header>
Header 1</Header>
<Content>
<h2>
Test</h2>
<asp:CheckBox runat="server" ID="CheckBox1"
Text="CheckBox" />
</Content>
</act:AccordionPane>
</Panes>
</act:Accordion>
you should be able to write the following code:
protected void Page_Load(object sender, EventArgs e)
{
if (CheckBox1.Checked) {
//
}
}
And indeed TemplateInstance.Single works and you can compile this code. However, when you attempt browsing your page you get:
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.Object reference not set to an instance of an object.
|
Well, this is not what you expect from TemplateInstance.Single. CheckBox1 is not instantiated on Page_Load. Moreover, if you try handling CheckedChanged event, you will find that it does not fire.
<asp:CheckBox runat="server" ID="CheckBox1" Text="CheckBox"
AutoPostBack=true OnCheckedChanged="CheckBox1_CheckedChanged" />
and even more, checkbox checked state is not preserved between postbacks. (TextBox value always remains the same if you replace CheckBox with TextBox.)
I could explain not instantiating controls before the OnLoad event by the fact that when page is loading for the first time CreateChildControls is first called in PreRender page lifecycle phase. However, post data processing usually triggers CreateChildControls and any controls receiving post data are created. So, accordion is different.
When response Html is rendered, client id of my checkbox control becomes ctl03_CheckBox1 and its name is ctl03$CheckBox1. So, when ASP.NET processes post data it first looks for ctl03 control and then if it is found asks ctrl03 control to find CheckBox1 control. Ctl03 control must be marked with INamingContainer. If you enable page trace and look for Ctl03 control, you find that it is AccordionContentPanel control. AccordionContentPanel is an internal control in the AccordionPane and it is naming container.
However, when I try to find ctl03 control in my page, I cannot find it until PreRender phase when AccordionPanes instantiate their templates an so does ASP.NET.
I looked into the source code. CreateChildControls is overridden in both Accordion and AccordionPane classes, but both these controls are not naming containers; while ASP.NET asks only naming containers to create their children. ASP.NET would ask ctl03 to create its children if it had exist.
I verified that adding a INamingContainer marker interface to Accordion and AccordionPane class fixes all problems described above. (Well, I am too lazy to check if it does not break data binding features).
However, as Accordion and AccordionPane controls are not naming containers in current build of Ajax Control Toolkit, workaround for these problems is required.
If you have declaratively created accordion controls on your web page, add to your page_init handler the following:
Accordion1.FindControl("nothing");
for each accordion on the page. It helps!
I posted a bug request to www.codeplex.com hoping this can be fixed: http://www.codeplex.com/AtlasControlToolkit/WorkItem/View.aspx?WorkItemId=11615
Posted by Yuriy Solodkyy at 1:26 AM 34 comments
Sample code: DynamicRowsSample.zip
I described most important problems you may face when dealing with dynamically created controls with in my post (Dynamically Created Controls in ASP.NET). The sample code in that post demonstrated most of the described problems and showed how to created pages with dynamic layout.
However, the previous sample does not make it obvious how to implement another common scenario with dynamic controls. So, I am going to explain ho to build a web page with dynamically added and removed rows like:
To separate implementation of one row and a web page handling dynamic rows, I placed implementation of a row into a user control in WebUserControl.ascx. The WebUserControl class exposes events and properties required to communicate with the web page.
public event EventHandler InsertAbove;
public event EventHandler InsertBellow;
public event EventHandler Remove;
public string Text {
get {
return c_textBox.Text;
}
}
User control fires its events in response to buttons clicks. For example, "Insert Above" button click is handled by:
protected void InserAboveClick(object sender, EventArgs e) {
if (InsertAbove != null) {
InsertAbove(this, e);
}
}
The sample web page consists of 3 areas:
These areas are described in ASP.NET markup as:
<div>
<asp:PlaceHolder runat="server" ID="c_placeHolder">
</asp:PlaceHolder>
<br />
<asp:Label ID="c_resultLabel" runat="server"
Text="Label">
</asp:Label>
<br />
<asp:Button ID="c_postBackButton" runat="server" Text="Do PostBack!" />
</div>
Dynamically created controls must be re-created on each postback and these re-created controls must have the same IDs as they had when were rendered previous time. You need to know all these details when creating controls on postback. The best place to keep this information between postbacks is a page view state.
Each dynamically created control must get its unique ID and this ID shall not be changed between postbacks. Otherwise ASP.NET will not be able to restore a view state of the control and its children. The sample application allows inserting and removing rows, so it is not good idea to assign IDs each line incrementally. If you do so, and user clicks "remove" button, the next line will get ID of the removed line and view state will be restored incorrectly.
I, therefore, allocate each row ID only once and do not re-use the same ID for newly created rows. The LastControlId is a ViewState backed property which remembers ID of the last row created by a user.
public int LastControlId {
get {
object v = ViewState["LastControlId"];
return v != null ? (int)v : 0;
}
set {
ViewState["LastControlId"] = value;
}
}
IDs of the currently existing rows are stored in another ViewState backed property: ControlIdList. This ArrayList holds an ordered list of IDs. It is used on each postback to create rows with correct IDs.
public ArrayList ControlIdList {
get {
object v = ViewState["ControlIdList"];
return v != null ? (ArrayList)v : null;
}
set {
ViewState["ControlIdList"] = value;
}
}
ASP.NET does not remember dynamically created controls neither in ViewState nor anywhere else. It is a developer responsibility to reconstruct any dynamically created controls on each postback. So, you need to create your rows on each postback. Information stored in ViewState backed properties is enough to create controls with correct IDs. ASP.NET will do the remaining job on restoring controls state.
In my earlier post I explained that the best place to re-create dynamic controls is LoadViewState method. Following this recommendation, the sample application creates rows in LoadViewState method by calling CreateControls method.
protected override void LoadViewState(object savedState) {
base.LoadViewState(savedState);
CreateControls();
}
CreateControls method loops over IDs stored in the ControlIdList property, creates rows and adds them to placeholder exactly in the same order as they are stored in the ControlIdList. (IDs in the list may be unordered because users are allowed to insert rows).
private Hashtable m_controls = new Hashtable();
private void CreateControls() {
ArrayList ids = ControlIdList;
if (ids != null) {
foreach (int id in ids) {
WebUserControl control = CreateControl(id);
c_placeHolder.Controls.Add(control);
}
}
}
private WebUserControl CreateControl(int id) {
WebUserControl result =
(WebUserControl)LoadControl("WebUserControl.ascx");
result.ID = "c_" + id;
result.InsertAbove += InsertRowAbove;
result.InsertBellow += InsertRowBellow;
result.Remove += RemoveRow;
m_controls[id] = result;
return result;
}
References to created controls are saved for future use in the m_controls hash-table. Saving references to dynamically created controls allows to avoid using FindControl method later.
When a user first loads the page it has no ViewState and LoadViewState is not invoked. However, the page should show the first row to user. The first row can be created by the same CreateControls method, but you need to initialize ViewState backed properties before calling this method. I did in the OnLoad method. (You can do it in the OnInit method as well, but you cannot move exactly the same implementation to OnInit).
protected override void OnLoad(EventArgs e) {
base.OnLoad(e);
if (!IsPostBack) {
LastControlId = 1;
ArrayList idList = new ArrayList();
idList.Add(0);
ControlIdList = idList;
CreateControls();
}
DisplayResult();
}
Note that initialization is only required when a user first opens the page.
Server event fires when user clicks on any of the "Insert Above", "Insert Bellow" or "Remove". It fires after the LoadViewState method is invoked, so dynamic controls are already reconstructed. These controls have already loaded their previous ViewState. TextBoxes and similar controls have already processed post-data and their Text properties reflect last changes made by the user.
A task of event handlers is to prepare the page for rendering reflecting changes requested by the user and to save these changes allowing correct reconstruction of the page after a postback.
private void InsertRowBellow(object sender, EventArgs e) {
WebUserControl control = (WebUserControl)sender;
int index = control.Parent.Controls.IndexOf(control) + 1;
CreateControlAt(index);
DisplayResult();
}
private void CreateControlAt(int index) {
int id = GetNewId();
WebUserControl newControl = CreateControl(id);
ControlIdList.Insert(index, id);
c_placeHolder.Controls.AddAt(index, newControl);
}
private int GetNewId() {
int id = LastControlId++;
return id;
}
The CreateControlAt method does the most important job. It generates new ID for the row which is unique across postbacks, creates new row controls and inserts it at the right position. The CreateControlAt method also updates the ControlIdList property to allow correct reconstruction of the page on the next post-back.
The sample application displays concatenated text from all the text boxes below the rows. The label text is updated in the OnLoad method. If the page layout is changed later in the event handler the label text is updated once again to reflect recent changes.
private void DisplayResult() {
StringBuilder sb = new StringBuilder();
bool first = true;
foreach (int id in ControlIdList) {
if (!first) {
sb.Append(", ");
}
else {
first = false;
}
WebUserControl control = (WebUserControl)m_controls[id];
sb.Append(control.Text);
}
c_resultLabel.Text = sb.ToString();
}
Posted by Yuriy Solodkyy at 2:51 AM 7 comments
Labels: ASP.NET, Dynamic Controls
UpdatePanel is a rare ASP.NET (actually ASP.NET AJAX Extensions) control which benefits from the TemplateInstanceAttribute and especially TemplateInstance.Single case. Its ContentTemplate property is decorated with [TemplateInstance(TemplateInstance.Single)] attribute. This setting allows accessing declaratively created controls in the ContentTemplate via page fields generated by ASP.NET.
The PreInit event is not intended for setting up any controls, but it is nice place to see a difference between Panel and UpdatePanel.
If you have the following controls on your page:
<asp:ScriptManager runat="server" ID="c_scriptManager">
</asp:ScriptManager>
<div>
<asp:Label runat="server" ID="c_label"></asp:Label>
<asp:UpdatePanel runat="server" ID="c_updatePanel">
<ContentTemplate>
<asp:Button runat="server" ID="c_buttonForTest"
Text="Just a Button" />
</ContentTemplate>
</asp:UpdatePanel>
<asp:Panel runat="server" ID="c_panel">
<asp:Button runat="server" ID="c_buttonForTest2"
Text="Just a Test" />
</asp:Panel>
</div>
and the following code behind:
protected override void OnInit(EventArgs e) {
base.OnInit(e);
if (c_buttonForTest != null) {
c_label.Text += "c_buttonForTest ";
}
if (c_buttonForTest2 != null) {
c_label.Text += "c_buttonForTest2 ";
}
}
you will notice that label output is just c_buttonForTest2. What does it mean? It means that UpdatePanel does not instantiate its ContentTemplate until after the OnInit phase.
So, be careful. Public page fields holding references to controls might be yet un-initialized before the OnInit phase and it is not a good idea to access controls in PreInit phase. You may get "Object reference not set to an instance of an object" exception.
As I said earlier it is recommended to set up dynamically created controls before they are added to the parent control Controls collection. This is necessary to avoid storing these settings in the ViewState. ASP.NET starts tracking ViewState changes immediately after the OnInit event has fired.
This code snippet is used to add a button to the placeholder control:
TextBox textBox = new TextBox();
textBox.ID = "c_textBox";
textBox.ForeColor = Color.Green;
c_placeholder.Controls.Add(textBox);
It follows recommendation and sets control properties before it is added to the placeholder. The same is possible for user controls loaded from the .ascx file:
WebUserControl3 userControl =
(WebUserControl3)LoadControl(@"~/Scenario 3/WebUserControl3.ascx");
userControl.ID = "c_rightUserControl";
userControl.ImportantColor = Color.BlueViolet;
c_bodyPlaceHolder.Controls.Add(userControl);
This sample also works for WebSuserControl3.ImportantColor implemented as:
public Color ImportantColor {
get {
return c_importantLabel.ForeColor;
}
set {
c_importantLabel.ForeColor = value;
}
}
and c_importantLabel being simple static control inside the user control like:
<%@ Control Language="C#" ClassName="WebUserControl3" %>
<h2>
WebUserControl3</h2>
<p><asp:Label ID="c_importantLabel" runat="server" Text="Important Label"></asp:Label></p>
However, if you anytime decide to move c_importantLabel inside the UpdatePanel, you will get "Object reference not set to an instance of an object" exception.
User control OnInit fires inside the call to c_bodyPlaceHolder.Controls.Add(userControl) and c_importantLabel gets its value their.
I posted a long story about the problems you can face while creating controls dynamically and how to avoid these problems. One of the important conclusions was to set control properties before adding them to the parent control Controls collection. As it is shown here, it is not good for user controls containing UpatePanel.
MSDN says that Init event is raised after all controls have been initialized.
The same applies to user controls.
So, approach demonstrated in my previous post is a little incorrect. It works fine almost everywhere, but there are cases when it does not. It is necessary to move initialization of user control properties to its Init event handler. C# 2,0 anonymous methods are very helpful in this case. The problem is fixed by wrapping user control initialization in:
WebUserControl3 userControl =
(WebUserControl3)LoadControl(@"~/Scenario 3/WebUserControl3.ascx");
userControl.ID = "c_rightUserControl";
userControl.Init += delegate {
userControl.ImportantColor = Color.BlueViolet;
};
c_bodyPlaceHolder.Controls.Add(userControl);
This code works regardless of whether c_importantLabel is inside the UpdatePanel or not. The c_importantLabel field is already initialized when Init event fires.
Since the old approach works almost in all cases, I do not think it is necessary to wrap initialization of any control in an anonymous method. However, I would do it for initialization of any user control from the outside.
I have updated the sample application from the post about Dynamically Created Controls in ASP.NET to use UpdatePanels everywhere. The only other change is using anonymous method to initialize WebUserControl1.
Attachment: DynamicallyCreatedControlsAjax.zip
An alternative point of view on the same problem can be: "User control shall be ready to handle assignment to its properties before its children are completely initialized".
The converted sample application demonstrates that WebUserControl2 which follows this principle works fine without any changes and allows initialization of its properties before the Init event.
Anyway, you may have methods in your user control returning internal controls and they will return null before Init event.
TabPanel and AccordionPane are similar to UpdatePanel in their behavior.
Posted by Yuriy Solodkyy at 2:40 AM 4 comments
Labels: AJAX, ASP.NET, Dynamic Controls