views:

1036

answers:

2

A ASP.NET page's ViewState seems to have troubles keeping up with dynamicly removed controls and the values in them.

Let's take the following code as an example:

ASPX:

<form id="form1" runat="server">
<div>
    <asp:Panel runat="server" ID="controls" />
</div>
</form>

CS:

protected void Page_Init(object sender, EventArgs e) {
    Button b = new Button();
    b.Text = "Add";
    b.Click +=new EventHandler(buttonOnClick);
    form1.Controls.Add(b);
    Button postback = new Button();
    postback.Text = "Postback";
    form1.Controls.Add(postback);
}

protected void Page_Load(object sender, EventArgs e) {
    if (ViewState["controls"] != null) {
        for (int i = 0; i < int.Parse(ViewState["controls"].ToString()); i++) {
            controls.Controls.Add(new TextBox());
            Button remove = new Button();
            remove.Text = "Remove";
            remove.Click +=new EventHandler(removeOnClick);
            controls.Controls.Add(remove);
            controls.Controls.Add(new LiteralControl("<br />"));
        }
    }
}

protected void removeOnClick(object sender, EventArgs e) {
    Control s = sender as Control;
    //A hacky way to remove the components around the button and the button itself
    s.Parent.Controls.Remove(s.Parent.Controls[s.Parent.Controls.IndexOf(s) + 1]);
    s.Parent.Controls.Remove(s.Parent.Controls[s.Parent.Controls.IndexOf(s) - 1]);
    s.Parent.Controls.Remove(s.Parent.Controls[s.Parent.Controls.IndexOf(s)]);
    ViewState["controls"] = (int.Parse(ViewState["controls"].ToString()) - 1).ToString();
}

protected void buttonOnClick(object sender, EventArgs e) {
    if (ViewState["controls"] == null)
        ViewState["controls"] = "1";
    else
        ViewState["controls"] = (int.Parse(ViewState["controls"].ToString()) + 1).ToString();
    controls.Controls.Add(new TextBox());
}

Then, let's say you create 4 controls and insert the following values:

[ 1 ] [ 2 ] [ 3 ] [ 4 ]

We want to delete the second control; after removing the second control the output is:

[ 1 ] [ 3 ] [ 4 ]

which is what we want. Unfortunately, on a subsequent PostBack, the list becomes:

[ 1 ] [ ] [ 3 ]

So, my question is, why is this happening? As far as I've read, ViewState should save the properties of the controls in relation to their indexes, not the actual controls.

A: 

You are doing half your code in the Page_Load and half your code in your postback events. Somewhere in this mess, you are running into conflicts. The fact that they don't show up until the second postback tells me there is some run in your logic.

I am not sure exactly where the problem is occurring, but ViewState handling is not the most fun thing in the world, when you are doing custom ViewState crud. I would have to rebuild the app and set watch conditions to see what is happening, but I am fairly sure it is an impedence mismatch between your Page_Load code and the event handlers.

Gregory A Beamer
+5  A: 

Couple things. Whether controls are loaded by their ID or Index depends on the ViewStateModeById attribute. By default it is false (meaning load by index).

However, text boxes are handled differently. Their view state does not contain input value unless they are disabled or invisible. The text property gets overwritten by posted values using their IDs. Since you are not managing text box IDs, this is what happens.

After you've added four controls, you have four text boxes: ctrl0, ctrl1, ctrl2, and ctrl3 with values 1, 2, 3, and 4 respectively.

Next, you remove the ctrl1 box and client gets three boxes: ctrl0, ctrl2, and ctrl3 with according values. Now, when you do any postback, these three values get submitted in the form ctrl0=1&ctrl2=3&ctrl3=4.

Then, on Page_Load, you create three controls, this time: ctrl0, ctrl1, ctrl2 with no values.

The Framework calls LoadRecursive to load view states and then ProcessPostData to assign input values. It sees submitted ctrl0 and ctrl2, finds controls with the same id and assigns them values 1 and 3. It does not find ctrl3, so it skips it. The remaining ctrl1 simply carries on w/o any value.

As an example, consider this solution (not the best):

protected void Page_Init(object sender, EventArgs e)
{
    Button b = new Button();
    b.Text = "Add";
    b.Click += new EventHandler(buttonOnClick);
    form1.Controls.Add(b);

    Button postback = new Button();
    postback.Text = "Postback";
    form1.Controls.Add(postback);
}

protected void Page_Load(object sender, EventArgs e)
{
    if (ViewState["controls"] != null)
    {
     List<string> ids = (List<string>)ViewState["controls"];

     for (int i = 0; i < ids.Count; i++)
     {
      TextBox textbox = new TextBox();
      textbox.ID = string.Format("txt_{0}", ids[i]);
      textbox.Text = textbox.ID;
      controls.Controls.Add(textbox);

      Button remove = new Button();
      remove.Text = "Remove";
      remove.Click += new EventHandler(removeOnClick);
      remove.ID = ids[i];
      controls.Controls.Add(remove);

      controls.Controls.Add(new LiteralControl("<br />"));
     }
    }
}

protected void removeOnClick(object sender, EventArgs e)
{
    Control btn = sender as Control;

    List<string> ids = (List<string>)ViewState["controls"];
    ids.Remove(btn.ID);

    //A hacky way to remove the components around the button and the button itself
    btn.Parent.Controls.Remove(btn.Parent.Controls[btn.Parent.Controls.IndexOf(btn) + 1]);
    btn.Parent.Controls.Remove(btn.Parent.Controls[btn.Parent.Controls.IndexOf(btn) - 1]);
    btn.Parent.Controls.Remove(btn);

    ViewState["controls"] = ids;
}

protected void buttonOnClick(object sender, EventArgs e)
{
    List<string> ids;

    if (ViewState["controls"] == null)
     ids = new List<string>();
    else
     ids = (List<string>)ViewState["controls"];

    string id = Guid.NewGuid().ToString();
    TextBox textbox = new TextBox();
    textbox.ID = string.Format("txt_{0}", id);
    textbox.Text = textbox.ID;
    controls.Controls.Add(textbox);

    Button remove = new Button();
    remove.Text = "Remove";
    remove.Click += new EventHandler(removeOnClick);
    remove.ID = id;
    controls.Controls.Add(remove);

    controls.Controls.Add(new LiteralControl("<br />"));

    ids.Add(id);

    ViewState["controls"] = ids;
}
Ruslan