Tuesday, July 31, 2007

Hidden Controls and ViewState

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.

Saturday, July 28, 2007

Implementing ITemplate as Anonymous Method

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. 

Tuesday, July 10, 2007

What's Wrong with Accordion Control?

Briefly

  1. Controls in a AccordionPane are not instantiated until PreRender
  2. Values of TextBoxes, CheckBoxes and other controls are not preserved between postbacks.

With More Details and Solution of the Problems

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:


Object reference not set to an instance of an object.

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.NullReferenceException: Object reference not set to an instance of an object.

Source Error:





Line 13:     protected void Page_Load(object sender, EventArgs e)
Line 14: {
Line 15: if (CheckBox1.Checked) {
Line 16: //
Line 17: }

 


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.)


So, what's wrong with Accordion and AccordionPane?


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.


Workaround


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
 

Wednesday, July 4, 2007

Dynamically Created Controls: Dynamic Rows Sample

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:

Web Page with Dynamic Rows

One Row

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);
}
}

Web Page Structure


The sample web page consists of 3 areas:



  • A placeholder for dynamically created rows
  • A label to display sample result
  • A button to do a postback (just to play with)

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>

View State and Control Ids


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;
}
}

Post Back


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();
}

Creating Controls


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.


Initialization


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.


Handling User Actions (Postback Events)


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. 


Using Dynamic Controls


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();
}

Once Again



  1. Save enough information in view state to be able to reconstruct dynamically created controls.
  2. Override LoadViewState and reconstruct dynamically created controls using the the information retrieved from the view state.
  3. Initialize your page in the OnLoad method by setting initial values in the ViewState and then call your method to reconstruct controls.
  4. Create, remove or update your controls in response to user actions in event handlers.  Do not forget to update view state data used to reconstruct controls on the next postback.