Composite Controls: Dude, where’s my data? June 25, 2009
Posted by codinglifestyle in ASP.NET, C#, CodeProject.Tags: composite, control, createchildcontrol, custom, event, oninit, postback, usercontrol, viewstate, webcontrol
trackback
I started my .NET career writing WebParts for SharePoint 2003. You would think this would make me a bit of a server control expert, as WebParts are essentially gussied up server controls. Yet, I’ve officially wasted 2 days on an age old composite control problem which typically involves these kinds of google searches:
- server control postback missing data
- postback server control child control missing
- composite control event not firing
- webcontrol createchildcontrol postback control
- viewstate webcontrol data missing empty blank
Sound familiar? I understand your pain and hope I can help. Composite controls can be tricky, if they aren’t set up just right even the most basic example won’t work properly.
First, keep in mind there are two types of custom controls developers typically write. There are user controls (.ascx) and server controls (.cs). We will focus on a server control. One gotcha for a composite control is using the right base class*. We want to use the CompositeControl base class, not WebControl or Control. This will tell ASP.NET to ensure the IDs of our child controls are unique (via INamingContainer). This is simple but very important, in order for ASP.NET to wire up events and serialize ViewState the ID of a control needs to be identical at all times. So only assign a literal string to your child control and let .NET worry about making it unique.
Now there are two cases to consider, the first is where our composite server control creates a static set of controls. This is a straightforward case, because we can create the controls at any time. The most important function in a server control is CreateChildControls(). This is where you can create and assign your controls to member variables. We can have properties and get and set values straight to our member controls. In every property just call EnsureChildControls() in each get and set.
private TextBox _TextBox;
[Bindable(true), Category(“TextBoxEx”), DefaultValue(“”), Localizable(true)]
public string Text
{
get
{
EnsureChildControls();
return _TextBox.Text;
}
set
{
EnsureChildControls();
_TextBox.Text = value;
}
}
protected override void CreateChildControls()
{
_TextBox = new TextBox();
_TextBox.ID = “_Text”;
Controls.Add(_TextBox);
}
In reality we may have properties or logic which determine which controls are created or how our control behaves. This is a complicated scenario, because we cannot create the controls before the logic has been initialized (otherwise our logic will not know which controls to dynamically create)! In this case, we want our control’s own properties independent of the child controls and we’ll store this information in ViewState, not a control. We want to avoid calling EnsureChildControls() and delay calling CreateChildControls() prematurely. This allows the control to be initialized first so that when CreateChildControls() is called our logic will know which controls to create. First let’s see how to store a property in ViewState.
private const string STR_CreateTextBox = “CreateTextBox”;
public bool CreateTextBox
{
get
{
if (ViewState[STR_CreateTextBox] == null)
return false;
else return (bool)ViewState[STR_CreateTextBox];
}
set
{
ViewState[STR_CreateTextBox] = value;
}
}
If you were wondering if ViewState was the right place to store properties the answer is maybe. For a bound property it is overkill as it will be set back to its original value every postback. In that case we simply need a property with a backing store (or in C# 3.0+ a simple get; set; will do). But when our logic needs to be stored ViewState is the place to persist it. Just remember that the dynamically created controls don’t need ViewState of their own so we’ll be sure to turn that off when we create them. The right place to create and add our dynamic controls is in CreateChildControls(). Let’s create a TextBox based on some logic stored in the composite control’s ViewState.
protected override void CreateChildControls()
{
if (CreateTextBox)
{
_TextBox = new TextBox();
_TextBox.ID = “_Text”;
_TextBox.EnableViewState = false;
_TextBox.TextChanged += new EventHandler(Text_TextChanged);
Controls.Add(_TextBox);
}
}
void Text_TextChanged(object sender, EventArgs e)
{
string sText = ((TextBox)sender).Text;
}
Lets take a look at what we’re doing here. We are dynamically creating our TextBox control in CreateChildControls(). We are setting the ID to a literal string, ASP.NET will make sure our name is unique because we inherit from CompositeControl. We are setting EnableViewState to false because, as discussed, our composite control is already taking care of ViewState. We are adding an event as an example as this is the right place to setup any events you might need.
Now here is the interesting bit: How do we get the user’s value back from a dynamic TextBox? Take another look at the property Text above and note that the getter calls CreateChildControls(). This will ensure our textbox is recreated and the form will syncronize the user’s form data back in to the textbox. We could also capture the value using the TextBox’s text changed event to do some processing or whatever. With this event, when we postback, our Text_TextChanged event will fire before a OK button’s click event on the page due its place in the control hierarchy. This allows our event to manipulate the TextBox’s text value before our OK button’s click event occurs.
I’ll just note that you may be reading some advice on forum’s to override OnInit. Contrary to that advice, CreateChildControls() is the right place to dynamically create your controls and events. OnPreRender() is a great place for initialization as it is called after all properties are set. And, of course, Render() gives you complete control on how your control will be drawn.
* Now I have to mention Repeater. Repeater may cause a lot of pain with your server controls. You may see your control working properly outside a repeater and suddenly all go pearshaped when used with one. After a lot of trial and error I discovered this had to do with the ID assigned to the dynamic controls. We know ASP.NET depends on the ID being identical at all points for events and serialization with the form and viewstate to take place. Sad to say when our composite control is inside a Repeater we can not trust our IDs to .NET. So we do not want to inherit from CompositeControl or INamingContainer. Instead, assign a unique id yourself. Being that it is a repeater, this can not be a literal string because no 2 controls can have the same ID. Instead try _TextBox.ID = this.ID + “_Text”;
So although there are several points and gotchas to consider, think about using a custom control to lend some OO design to your UI. Be sure to unit test on a simple website to make development of the control easier. Good luck!
Dear Friends,
I hope you are doing well. I have launched a web site http://www.codegain.com and it is basically aimed C#,JAVA,VB.NET,ASP.NET,AJAX,Sql Server,Oracle,WPF,WCF and etc resources, programming help, articles, code snippet, video demonstrations and problems solving support. I would like to invite you as an author and a supporter. Looking forward to hearing from you and hope you will join with us soon.
Please forward this email to all of your friends who are related IT. Send to us your feed about site also.
Thank you
RRaveen
Founder CodeGain.com
Dude, You just saved my bacon. I’ve spent several days trying to figure out why my GridView wasn’t able to page correctly, and this blog post was the missing piece to me figuring it out. Thanks a bunch man!
Thank you very much! I was banging my head against the wall for almost a week. The key thing that helped me was, Overriding the OnPreRender() method by initializing the control properties with the structure variables saved in the SaveControlState() function here, instead of in the CreateChildControls() method. Please see below:
Protected Overrides Sub OnPreRender(e As System.EventArgs)
MyBase.OnPreRender(e)
Me.Calendar1.DayNameFormat = Values._DayNameFormat
Me.Calendar1.VisibleDate = Values._CalendarRefDate
Me.Calendar2.DayNameFormat = Values._DayNameFormat
Me.Calendar2.VisibleDate = DateAdd(DateInterval.Month, 1, Values._CalendarRefDate)
End Sub
Protected Overrides Sub OnInit(e As System.EventArgs)
MyBase.OnInit(e)
Page.RegisterRequiresControlState(Me)
End Sub
Protected Overrides Function SaveControlState() As Object
Dim Obj As Object = MyBase.SaveControlState
Dim OldState As New ArrayList
OldState.Add(Obj)
OldState.Add(Values)
Return OldState
End Function
Protected Overrides Sub LoadControlState(savedState As Object)
Dim ObA As ArrayList = CType(savedState, ArrayList)
Dim Obj As Object = CType(ObA(0), Object)
MyBase.LoadControlState(Obj)
Me.Values = CType(ObA(1), CtlValues)
End Sub