Composite Control: Changes not being stored on postback

M

Mark Olbert

This involves a family of related, databound ASPNET2 composite controls.

I've managed to arrange things so that the composite controls restore themselves from ViewState on postback after they're initially
configured during DataBind(). Thanks to Steven Cheng for pointing out that you have to set the constituent control properties after
you add them to the composite control collection for the restore to work!

However, I now have a different problem. At least, I think it's a different problem.

One of the composite controls is a collection of HtmlInputRadioButton controls. All are initially unselected. When I select a
particular HtmlInputRadioButton and do a simple postback (i.e., just a roundtrip to the server; no other processing takes place) the
page that reappears doesn't show the selection. Instead, all the HtmlInputRadioButton controls in that composite control are
unselected again.

But here's the weird part: if I make the selection again -- or indeed any selection, from any of the other, related composite
controls on the same page -- and do a postback, the selection(s) show up! For some reason, the very first selections, whatever they
are, don't get stored. But the ones on subsequent roundtrips do.

I find this very confusing. I think this may mean there's something wrong with the way in which the controls are initialized when
first created (i.e., through DataBind()), but I'm not sure. Or does it mean that I have to do some postback handling? But if so, why
does the Framework appear to take care of things on the subsequent roundtrips?

Can anyone suggest some ideas on what may be causing the behavior? And how I fix it?

- Mark
 
S

Steven Cheng[MSFT]

Hi Mark,

Welcome and glad to see that you're got progress on your custom controls.

Regarding on the new problem you mentioned, I think at least the readio
buttions have correct restore their status from viewstate and updates are
stored since in the sequential postback, the change has been displayed. The
problem is possibly that some other code(at creating time or intializing
time) override the control setting. Are you still use databinding or just
a collection to dynamically create the Html radio button list? If
convenient, would you provide some furhter code logic so that I can try
performing some local test.

Regards,

Steven Cheng
Microsoft Online Support

Get Secure! www.microsoft.com/security
(This posting is provided "AS IS", with no warranties, and confers no
rights.)
 
M

Mark Olbert

Steven,

Here's some test code for the controls which demonstrates the problems. First, the master control:

using System;
using System.ComponentModel;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace OlbertMcHughLLC.Web.Controls
{
[ToolboxData("<{0}:OddPostBack runat=server></{0}:OddPostBack>")]
public class OddPostBack : CompositeControl
{
private int numChildren = -1;

[
Bindable(true),
Category("Data"),
]
public int NumChildren
{
get { return numChildren; }
set { numChildren = value; }
}

[
Bindable(true),
Category("Appearance"),
DefaultValue("Introduction"),
]
public string Introduction
{
get
{
if( ViewState["intro"] == null ) return String.Empty;

return (string) ViewState["intro"];
}

set { ViewState["intro"] = value; }
}

public override void DataBind()
{
Introduction = "This is a test";

if( numChildren <= 0 ) return;

for( int idx = 0; idx < numChildren; idx++ )
{
OddPostBackChild curSQ = this[idx];

if( curSQ is OddPostBackChild )
{
OddPostBackChild choiceSQ = curSQ as OddPostBackChild;
choiceSQ.DataSource = 5;
}

curSQ.Text = String.Format("Control {0}", idx + 1);
curSQ.QuestionID = idx;
}

DataBindChildren();
}

protected OddPostBackChild this[int idx]
{
get
{
EnsureChildControls();

return FindControl(String.Format("odd{0}", idx + 1)) as OddPostBackChild;
}
}

protected string ChildControlList
{
get
{
if( ViewState["questionSet"] == null ) return String.Empty;

return (string) ViewState["questionSet"];
}

set { ViewState["questionSet"] = value; }
}

protected override void CreateChildControls()
{
Controls.Clear();

if( numChildren > 0 )
{
// child control list stores, in the view state, the list of
// child controls to be recreated on postback. In this case it's a
// silly approach. In the real code, it's important because there are
// different child controls created for different entries in the data
// that we're binding to
StringBuilder temp = new StringBuilder();
for( int idx = 0; idx < numChildren; idx++ )
{
temp.Append('x');
}

ChildControlList = temp.ToString();
}

if( ChildControlList.Length == 0 ) return;

for( int idx = 0; idx < ChildControlList.Length; idx++ )
{
OddPostBackChild newCtl = new OddPostBackChild();
newCtl.ID = String.Format("odd{0}", idx + 1);

Controls.Add(newCtl);
}
}

protected override void RenderContents( HtmlTextWriter writer )
{
writer.Write(Introduction);
writer.WriteLine();

writer.WriteFullBeginTag("ol");
writer.WriteLine();

foreach( Control curQC in Controls )
{
if( curQC is OddPostBackChild )
{
writer.WriteFullBeginTag("li");

curQC.RenderControl(writer);

writer.WriteEndTag("li");
writer.WriteLine();
}
}

writer.WriteEndTag("ol");
writer.WriteLine();
}
}
}

Next, the detail control:

using System;
using System.ComponentModel;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;

namespace OlbertMcHughLLC.Web.Controls
{
[DefaultProperty("Text")]
[ToolboxData("<{0}:OddPostBackChild runat=server></{0}:OddPostBackChild>")]
public class OddPostBackChild : CompositeControl
{
private Label lblQuestion;
private int numOptions = -1;

public OddPostBackChild()
{
}

[
Bindable(true),
Category("Data"),
DefaultValue(-1),
]
public int QuestionID
{
get
{
if( ViewState["questionID"] == null ) return -1;

return (int) ViewState["questionID"];
}

set { ViewState["questionID"] = value; }
}

[
Bindable(true),
Category("Appearance"),
DefaultValue("abcd"),
]
public string Text
{
get
{
EnsureChildControls();
return lblQuestion.Text;
}

set
{
EnsureChildControls();
lblQuestion.Text = value;
}
}

[
Bindable(true),
Category("Data"),
DefaultValue(-1),
]
public long QuestionNumber
{
get
{
if( ViewState["questionNum"] == null ) return -1;

return (long) ViewState["questionNum"];
}

set { ViewState["questionNum"] = value; }
}

[
Bindable(true),
Category("Data"),
]
public int DataSource
{
get { return numOptions; }

set
{
numOptions = value;

// this is a silly approach, but in the
// real code we store the number of entries in the list
// of complex objects that is being assigned to the datasource
NumChoices = numOptions;
}
}

[
Browsable(false),
]
public Label this[int idx]
{
get
{
EnsureChildControls();

return FindControl(String.Format("oddChildLabel{0}", idx + 1)) as Label;
}
}

protected int NumChoices
{
get
{
if( ViewState["numChoices"] == null ) return 0;

return (int) ViewState["numChoices"];
}

set { ViewState["numChoices"] = value; }
}

public override void DataBind()
{
if( numOptions <= 0 ) return;

for( int idx = 0; idx < numOptions; idx++ )
{
this[idx].Text = String.Format("&nbsp;Option {0}", idx + 1);
}
}

protected override void CreateChildControls()
{
Controls.Clear();

lblQuestion = new Label();
lblQuestion.ID = "lblQuestion";

Controls.Add(lblQuestion);

string quesID = "question" + QuestionID.ToString();

for( int idx = 0; idx < NumChoices; idx++ )
{
Controls.Add(new LiteralControl("<br />"));

HtmlInputRadioButton newCtl = new HtmlInputRadioButton();
newCtl.ID = String.Format("oddChild{0}", idx + 1);
newCtl.Name = quesID;
Controls.Add(newCtl);

Label newLbl = new Label();
newLbl.ID = String.Format("oddChildLabel{0}", idx + 1);
Controls.Add(newLbl);
}
}

public override void RenderBeginTag( HtmlTextWriter writer )
{
base.RenderBeginTag(writer);

writer.WriteFullBeginTag("p");
writer.WriteLine();
}

public override void RenderEndTag( HtmlTextWriter writer )
{
base.RenderEndTag(writer);

writer.WriteEndTag("p");
writer.WriteLine();
}
}
}

If you include an instance of OddPostBack on a plain aspx page with a submit button, and add the following code to the code-behind
file, you'll be able to demonstrate the problem:

protected void Page_Load( object sender, EventArgs e )
{
if( !Page.IsPostBack )
{
oddCtl.NumChildren = 3;
oddCtl.DataBind();
}
}

As a reminder, what you'll see is that the first choice made in any detail control is not saved on postback, while all subsequent
choices are saved on postback.

- Mark
 
M

Mark Olbert

Steven,

Some interesting follow-up testing results: the problem only manifests itself with HtmlInputRadioButton controls. If you replace the
line in the detail control which creates the radio button with, say, a checkbox:

for( int idx = 0; idx < NumChoices; idx++ )
{
Controls.Add(new LiteralControl("<br />"));

HtmlInputCheckBox newCtl = new HtmlInputCheckBox();
....

the problem goes away (i.e., even changes made on the first post are retained on postback). Same thing if you make it a textbox
instead.

So the problem seems to be something unique to HtmlInputRadioButton controls. I suspect it may have something to do with the fact
that they appear to have an additional "value" when they work as a group, namely, the name of the currently selected radio button.

Comments?

- Mark
 
M

Mark Olbert

Okay, problem solved, I think. It's the result of not initializing the detail control's properties in the proper sequence.
Specifically, in the DataBind() code for the master control:

public override void DataBind()
{
Introduction = "This is a test";

if( numChildren <= 0 ) return;

for( int idx = 0; idx < numChildren; idx++ )
{
OddPostBackChild curSQ = this[idx];

if( curSQ is OddPostBackChild )
{
OddPostBackChild choiceSQ = curSQ as OddPostBackChild;
choiceSQ.DataSource = 5;
}

// this next pair of lines causes the error. Assigning a value to the detail
// control's Text property causes an implicit EnsureChildControls() to be
// be called, which assigns the wrong "group name" to the radio buttons,
// because QuestionID hasn't been assigned yet.
// reversing the order of the assignments solves the problem.
curSQ.Text = String.Format("Control {0}", idx + 1);
curSQ.QuestionID = idx;
}

DataBindChildren();
}

I've seen numerous reports of these kinds of hair-pulling subtle bugs in ASPNET code. I think someone needs to sit down and rethink
the architecture, because sequence-specific dependencies like this are really, really annoying.

If you have any other suggestions on design patterns that can avoid these kinds of problems, I'm all ears.

- Mark
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,755
Messages
2,569,536
Members
45,009
Latest member
GidgetGamb

Latest Threads

Top