2.14.2006

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:

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

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!!!

12 Comments:

Anonymous Anonymous said...

I am just getting started in making custom composite controls.

Would it be correct to have the _textBox = new TextBox(); inside the constructor for the custom control?

February 15, 2006 10:47 AM  
Blogger Infinity88 said...

It's ok to create controls in your constructor, but they are really meant to be added to the control collection from within CreateChildControls().

ASP.NET itself cheats this rule though, because declared controls (ie, markup with runat=server in your form) classify as child controls, but they are 'added' during construction.

If you really want to create the controls in your constructor, then be sure and .Add() them too. Generally I'd recommend doing it all in CreateChildControls though, and then just call EnsureChildControls() if you really need them that early on.

February 15, 2006 11:08 AM  
Anonymous Anonymous said...

The alternative would be to expose some public methods to access the properties of the controls. Something like:

public void setText(string sometext) {
this.txtFoo.Text = sometext;
}

February 22, 2006 7:01 AM  
Blogger Infinity88 said...

Methods are fine, but you can't call a method declaratively. Using a property allows users of the control to assign values on the controls tag:

<abc:MyControl runat="server" MyProperty="123" />

Can't do that with a method.

February 22, 2006 8:05 AM  
Anonymous Anonymous said...

Hello Dave,

This would have been a nice solution was it not for the problem that when the property is delegated to a childcontrol, its value is not displayed anymore in properties window of visual studio. Probably that is the reason why it is not recommended by microsoft. Do you have an idea how to resolve this problem ?

March 29, 2006 4:32 AM  
Blogger Infinity88 said...

I assume you are referring to the comment I made on Scott Gu's blog about making the TextBox control itself a property so that users can access all of the TextBox's properties, like:

<abc:MyControl TextBox-MaxLength="5" ... />

Luckily Microsoft has given us a great way to deal with that. Slap this attribute on your TextBox property, and it tells the designer that properties _of the property_ are to be serialized instead of the object itself:

[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]

You should see the TextBox's designer-exposed properties automatically.

Here's a quick article that goes into more detail about that attribute:

DesignerSerializationVisibilityAttribute

Is this what you were referring to?

Thanks for the awesome comment!

March 29, 2006 8:04 AM  
Anonymous Anonymous said...

Hi Dave,

yes, I have read your comment on Scott Guthrie's blog. That is how I came to get to know your blog. And while I will certainly keep this DesignerSerializationVisibility attribute in mind for future use, I was indeed refering to something else but my explation was not too good. Here a second try :

In a test project I have the following custom control

public class LabelTextBox : WebControl, INamingContainer
{
private TextBox t;
private Label l;

public LabelTextBox() {
this.EnsureChildControls();
}

public string Text {
get {
return t.Text;
}
set {
t.Text = value;
}
}
public string Title {
get {
return l.Text;
}
set {
l.Text = value;
}
}
protected override void CreateChildControls() {
t = new TextBox();
l = new Label();
Controls.Add(l);
Controls.Add(t);
}
}

and on a webpage it is used as :

cc1:LabelTextBox ID="LabelTextBox1" Text="MyText" Title="MyTitle" runat="server"

When I am in source view of the webpage and the cursor is on this tag, in the properties window it is displayed that Text="MyText" and Title="MyTitle". Everything OK, no problem.

But now switch to design view and keep the control selected. In the properties window it is displayed that Text="MyText" (ok) but Title="" (?!!).

When the control inherits from CompositeControl the problem is even worser. Both the Text as the Title attribute are empty in design view.

Probably this has something to do with how the designer interacts with the custom control. I don't have this problem when the properties are stored in the viewstate of the custom control itself.

thanks for any help with this, Stefaan

April 07, 2006 4:45 AM  
Blogger Infinity88 said...

I haven't expierenced this problem because I rarely if ever use the designer view. I will look into it and see if I can duplicate the issue, thanks!

April 07, 2006 8:08 AM  
Anonymous Anonymous said...

That's really funny, what you described in this article is exactly what I pointed out two weeks ago at some code reviews in our team :)

I think the disconnection isn't the biggest problem here (although it's annoying as well). The really stupid thing is that the property goes twice to viewstate! You know that's BLOATING ViewState :)))

Anyway, thanks for a really awesome blog!

July 04, 2006 10:44 AM  
Anonymous Anonymous said...

Hi Dave, unfortunately what you describe in the post doesn't work in Design View. I experienced it when I downloaded your SmartTextBox. When setting properties in Design Mode they aren't persisted. You should eventually change the code of your (great) SmartTextBox to make it work. Strange that no one reported the issue.

July 27, 2006 6:52 PM  
Anonymous Anonymous said...

Hi Dave, I found out what was missing to make it work in design view. You need to override the method RecreateChildControls() this way:

protected override void RecreateChildControls()
{
EnsureChildControls();
}

and everything works fine now.

July 29, 2006 6:12 AM  
Blogger Infinity88 said...

Simone --

Thanks for the research and fix. I will update the article as soon as I can. Since I really don't use the designer I tend to 'forget' about that world. It does take some finesse to make the designer happy.

August 02, 2006 9:37 AM  

Post a Comment

<< Home