Composite Controls made easy
Scott Gu has an interesting article in his blog, pointing to another article in MSDN, about creating Composite Controls.
Its a great read, a must read, even.
I just have one problem with it. Near the beginning of the article there is some sample code as follows:
This is supposed to be an example of how you can use composite controls to make your life easier. This one allows you to set a label to a textbox, which redners before the textbox. Great use of compositing.public class LabelTextBox : WebControl, INamingContainer
{
public string Text {
get {
object o = ViewState["Text"];
if (o == null)
return String.Empty;
return (string) o;
}
set { ViewState["Text"] = value; }
}
public string Title {
get {
object o = ViewState["Title"];
if (o == null)
return String.Empty;
return (string) o;
}
set { ViewState["Title"] = value; }
}
protected override void CreateChildControls()
{
Controls.Clear();
CreateControlHierarchy();
ClearChildViewState();
}
protected virtual void CreateControlHierarchy()
{
TextBox t = new TextBox();
Label l = new Label();
t.Text = Text;
l.Text = Title;
Controls.Add(l);
Controls.Add(t);
}
}
Except for one little detail...
Before I explain the problem let me just say... details like this are really frustrating. ASP.NET is very powerful, but with that power comes some responsibility. It is very easy to do things the "wrong" way. It's also very, very easy to do things the right way, but you have to know the difference. And knowing the difference means having a fairly deep understanding of how the framework, well, works. This and my previous post on ViewState are just two examples of these mundane yet crucial details.
And now on to the juicy stuff...
My problem with the example code is really a state management thing. When it creates the textbox and label, it "copies" the Title and Text properties, which are ViewState-based properties of the composite control, into them. That raises some red flags to me, because as soon as you are finished copying the values into the child controls, your public properties are now completely disconnected from them. If someone, somewhere, for some reason, changes your Title or Text property value after this code occurs, it's too late. They will be dumb-founded as to why your control refuses to listen to their instructions (it will still render to 'old' value).
THE SOLUTION
The solution is so elegant in my opinion, I'm not sure why this isn't official recommended practice for compositing. I call it "delegating the properties". It means, don't store the state yourself, use the child control itself to store the state. The real problem was we had the value of our properties stored in two locations -- our ViewState, and our child controls' ViewState. Using the child control itself to store the state means it will always be in just one place, a place where we the parent and the child control can agree on. Here's how:
private TextBox txtFoo;
public MyControl()
{
this.EnsureChildControls();
}
public string Text
{
get { return this.txtFoo.Text; }
set { this.txtFoo.Text = value; }
}
protected override CreateChildControls()
{
this.txtFoo = new TextBox()
this.Controls.Add(txtFoo);
}
The Text property does nothing more than access the textbox's Text property. No longer do we need to 'copy' the value into the textbox -- its already there!
The important thing about this trick is to simply call EnsureChildControls() in your constructor. That's so the textbox will exist should someone try to set the Text property (which is very early on if they set it declaratively). Alternatively, you could call EnsureChildControls() within the get and set like so:
public string Text
{
get
{
this.EnsureChildControls();
return this.txtFoo.Text;
}
set
{
this.EnsureChildControls();
this.txtFoo.Text = value;
}
}
However, if you have several properties that do this, its much easier to just do it in the constructor. Less lines of code result.
Happy control building!!!