View Article        

Current Articles | Categories | Search | Syndication

Page: 1 of 3
Previous Page | Next Page
posted @ Monday, May 12, 2008 10:07 PM by SkySigal

Introduction

The issue of creating CompositeControls that have child controls is not especially difficult -- what is much more complicated is how to wire up the inner controls' properties in order to expose them on the outside of the composite control.

There are many times when one wants to do this: it can be the InnerButton.FontName, could be TopCombo.DataSource, etc.
The question boils down to whether to forward the properties directly to the child controls, or to hold their values in the container...and it's not an easy question to answer.
The answer is to know which method to choose for each property.

 

 

Public Property Wrapping  a Private/Protected Field

We all know the standard way of exposing a class Property:

public string LabelText {
    get {return _LabelText;}
    set {_LabelText = value;}
}
private string _LabelText;

public Layout Layout {
    get {return _Layout;}
    set {_Layout = value;}
} 
private Layout _Layout = Layout.LeftToRight;

public ListControlType {
    get {return _ListControlType;}
    set {_ListControlType = value;}
}

  • Pros
    • Simplicity
  • Cons
    • The property value does not survive round-tripping to the client and back.

 

ViewStated Simple Properties (ie Primitives)

A far better solution is to use the Control's built in StateBag to hold the backend of the  properties, in order for the property values  to survive round-tripping to the client and back.

public string LabelText {
    get {
        object o = ViewState["LabelText"];
        return (o!=null)?(string)o:string.Empty;
    }    
    set {
        ViewState["LabelText"]=value;
    }
}
public Layout Layout {
    get {
        object o = ViewState["Layout"]; 
        return (o!=null)?(Layout)o:Layout.LeftToRight;}
    set {
        ViewState["Layout"] = value;
}
public ListControlType ListControlType {
    get {
        object o = ViewState["ListControlType"];
        return (o!=null)?(ListControlType)o:ListControlType.ListBox;
    }    
    set {
        ViewState["ListControlType"]=value;
        ChildControlsCreated=false;
    }
}

 

  • Pros
    • The property value, and its changes, will survive round-tripping to the client, and will be available for use after the PostBack completes OnInit.
    • Because the value is un-connected to any child control, it can be used to set many child controls at once.

      For example, you could offer an ButtonBackground property (a string url to an image) -- and use it to set 26 child controls: the control will only Persist 1 copy of that string if it needs to, and in most cases, won't need to persist it at all (see below that the ViewState bag is smart enough to not persist values if it doesn't need to).
    • Discussion of how the StateBag works is beyond the scope of this article, but before moving I would like to quickly point out the following:

      Contrary to hysterical assumptions by new comers to ASP.NET, values that are put the StateBag via the ControlBuilder parsing Attributes, will *not* round-trip to the client in the base64 serialization of the ViewState: the StateBag is smart enough to realize that attributes don't need to be sent both in the html tags + ViewState serialization. Nor will properties put in there before or during OnInit.  But new properties will (which is what we want.
  • Cons
    • More typing than just a default public Property/Private Field that you might be used to if coming from WinForms programming.
    • Round-tripping persistence is lost if the control's ViewStating feature is turned off (but then you're no worse off than if you used a simple public property/private field solution).

 

IMPORTANT: When to Transfer ViewStated Properties to Inner Control Properties

The trick when using ViewStated properties, whether we are talking about ViewStating Simple properties or persisting complex properties, is to not pass the properties to the child control in such a way that the child control *also* ViewStates the value.

The usual time to transfer these values are either in CreateChildControls() (or PrepareControlHierarchy() if in a DataBindable control that has been NK'ed*), just after creating the control, but before the child control has been added to its parent, or later, in PrepareChildControls() (if the control has been NK'ed*), which is invoked by Render, after SaveViewState() has been invoked so its safe to set the child controls' properties without doubling the contents of ViewState.

ViewStated Complex Properties (ie Style Objects, etc.)

The problem with the Control's StateBag is that its very useful -- but it doesn't support the tracking of much beyond simple primitives (strings, int32, enums, etc.) If you want to persist a larger object -- say a Style object with several sub-properties -- you will have to do a bit more work. Actually, you will have to do quite a bit more work:

  • The object we wish to save must implement IStateManager (Style objects, ListItemCollections, etc...but most other objects you will have to implement it yourself).
  • Next, you have to ensure the Property is wired up to account for IStateManager methods:
    public Style Items {
        get {
            if (_ComplexObject == null) {
                _ComplexObject = new Style();
                if (this.IsTrackingViewState==true){((IStateManager)_ComplexObject).TrackViewState();}
            }
            return _ComplexObject;
        }
    }
    private Style _ComplexObject;
    
  • Finally, because the Complex object needs to be persisted separately from the Control's built in ViewState StateBag, one has to override the control's  IStateManager methods (LoadViewState/SaveViewState/TrackViewState) to ensure that this complex object gets saved/restored along with the ViewState:
protected override void LoadViewState(object savedState) {
    object[] blobs = (object[])(savedState);
    base.LoadViewState(blobs[0]);//Load the ViewState statebag...
    ((IStateManager)_MyComplexObject).LoadViewState(blobs[1]);//Load our ComplexObject...
}

protected override object SaveViewState() {
    object[] blobs = new object[2];
    blobs[0] = base.SaveViewState();//Save the ViewState statebag...
    blobs[1] = ((IStateManager)_MyComplexObject).SaveViewState();//Save our ComplexObject:
    return (object)blobs;
}

protected override void TrackViewState() {
    base.TrackViewState();
    //While turning on the ViewState statebag's flag, turn on the ComplexObject's
    //flag if it exists:
    if (_Items != null) {((IStateManager)_MyComplexObject).TrackViewState();}
}

 

  • Pros
    • The property value, and its changes, will survive round-tripping to the client, and will be available for use after the PostBack completes OnInit.
    • You can use
  • Cons
    • Personally, I think that having to overload the Control's methods is a right pita, and I think MS could/should have been made it a lot easier to implement the Persistance of larger objects. Its a lot of work to debug each control...
ViewStated Complex Properties (ie ListItemCollection)

Don't. Or at least think carefully about it. Although a ListItemCollection is compatible, storing it is generally a red-herring...

 

IMPORTANT: When to Transfer ViewStated Properties to Inner Control Properties

The trick when using ViewStated properties, whether we are talking about ViewStating Simple properties or persisting complex properties, is to not pass the properties to the child control in such a way that the child control *also* ViewStates the value.

The usual time to transfer these values are either in CreateChildControls() (or PrepareControlHierarchy() if in a DataBindable control that has been NK'ed*), just after creating the control, but before the child control has been added to its parent, or later, in PrepareChildControls() (if the control has been NK'ed*), which is invoked by Render, after SaveViewState() has been invoked so its safe to set the child controls' properties without doubling the contents of ViewState.

 

Direct Property Setting

In the above 3 cases, it was always the outer parent or container control's job to store the backend of the property values, whether in a private non-persisted field, or a persisted using the built in persistence system.

Direct Property Setting takes a different route:  it relies on the inner control persisting the value. This is done by directly wrapping the child control's properties with a get/sets:

protected Label _InnerLabel;
...
public string LabelText {
    get {return;_InnerLabel.Text;}
    set {_InnerLabel.Text = value;}
}

But the above is not enough -- and to understand why requires a quick discussion on EnsureChildControls().

TODO: Fill in here quick explanation.

In most cases these properties are being set for the first time when ControlBuilder has parsed the page's aspx tag for the control, has instantiated it, and is now parsing the control tag's attributes and nested tags, in order to set its public properties.

At such an early part of the control's lifecycle, its CreateChildControls() has not yet been invoked by anyone, so no child controls (including the above mentioned _InnerButton) exist yet.

To ensure that that innerButton is not a null and cause an Exception, you must preface the get and set with EnsureChildControls() to force the creation of the control before the inner child's property is accessed:

public string InnerLabel {
    get {EnsureChildControls();return;_InnerLabel.Text;}
    set {EnsureChildControls();_InnerLabel.Text = value;}
}

Note:
In case your wondering, EnsureChildControls() builds the child controls, and when done, sets ChildControlsCreated=true -- which is how it then knows to not build the control tree every time a property is accessed...Unless some one actively goes and resets ChildControlsCreated=false ...which we will discuss later on.

  • Pros
    • Clarity: unlike the ViewStated properties above, you can see at a glance where the property is being applied.
  • Cons
    • Requires using EnsureChildControls() everywhere.
      Because this is a quite unnatural recipe, it tends to introduce bugs due to someone forgetting to put EnsureChildControls() on every single property get and set that needs it.

      Note:
      This desire to avoid using EnsureChildControls() in every get;set; has led to many different quests for a solution:
      the most common being that users artificially call EnsureChildControls() early, in OnInit(), and even in the Constructor()
      The long and short of it is that these early calls don't solve the problem:
      • the Constructor is too early for it to have have parsed the attributes.
      • the OnInit is too late (after ControlBuilder parses the delegates the attribute values to the properties) and
    • Because it is directly tied to a child control, you can't very well use that property to set several child controls properties at once like you could do with a ViewStated property value (I guess you could, technically, pull it off in some cases -- but I wouldn't recommend it).
    • But most damning is the loss of changes caused by Events and PostBack data if there is any other property that resets ChildControlsCreated to false.

      To see what I mean by this, imagine the following:
      • First Request:
        • ControlBuilder reads the tag, sees a tag called LabelText, with a value of 'OrigValue' and sets the Control's public property by the same name.
          • This set invokes EnsureChildControls(), which in turn invokes CreateChildControls(), which creates a child Label ('_InnerLabel'.
          • Once _InnerLabel exists, its Text property is set to 'OrigValue'
          • It is added to the control, and therefore page,
            • Its persistence system is started...
        • PreRender:
          • ChildControlsCreated is already true, so nothing needs to happen -- off it goes.
            (The labels portion of ViewState is empty by the way).
      • PostBack (due to a button):
        • We start again, exactly as before...namely:
          ControlBuilder reads the tag, sees a tag called LabelText, with a value of 'OrigValue' and sets the Control's public property by the same name.
          • This set invokes EnsureChildControls(), which in turn invokes CreateChildControls(), which creates a child Label ('_InnerLabel'.
          • Once innerLabel exists, its Text property is set to 'OrigValue'
          • It is added to the control, and therefore page,
            • Its persistence system is started...
        • Events are processed.
          • CreateChildControls is invoked recursively to find the Button that the event data is for...
          • The button is found, and it updates the _innerLabel.Text += "!!!";
          • But the button also resets CreateChildControls to false...
        • PreRender:
          • Invokes EnsureChildControls(), which in turn invokes CreateChildControls because ChildControlsCreated is back to false:
            • the old label is discarded
            • and a new label is made in its place:
              • ...the attributes are parsed and the Text is set to ... "OrigValue"
              • ...it is added to the page...
                • ...it is not rehydrated with its portion of the ViewState (because it is empty)...
          • Off it goes again.
            (The labels portion of ViewState is empty by the way).
      • What just happened?
        What just happened is that the system did work...but we never saw it: we discarded the Label that had changed, and replaced it with a new one as blank as the day it was born!

 

 
Recycling Inner Controls

There is a way around this, Recycling, the inner control.

In the above example, we created the new child Controls in CreateChildControls as follows:

protected Label _InnerLabel;
protected Button _InnerButton;
...
protected void CreateChildControls(){
    
    this.Controls.Clear();

    //Make new child controls:
    table = new Table();this.Controls.Add(table);
    row = new TableRow();table.Rows.Add(row);
    cellA = new TableCell();row.Cells.Add(cellA);
    cellB = new TableCell();row.Cells.Add(cellB);

    _InnerLabel = new Label();
    cellA.Controls.Add(_InnerLabel);

    _InnerButton = new Button();
    cellA.Controls.Add(_InnerButton);    
    
    //Mark that we've been here...
    ChildControlsCreated = true;
}

but when we came back and did it again, we didn't look first before we leaped, and in the process -- as we saw above -- trod all over the Label that had changed (actually, we discarded the Label, and replaced without even noticing what we had replaced).

A work around is to check first -- and not recreate the inner control if we don't have to:

protected Label _InnerLabel;
protected Button _InnerButton;
...
protected void CreateChildControls(){
    
    this.Controls.Clear();

    //Make new child controls:
    table = new Table();this.Controls.Add(table);
    row = new TableRow();table.Rows.Add(row);
    cellA = new TableCell();row.Cells.Add(cellA);
    cellB = new TableCell();row.Cells.Add(cellB);

    if (_InnerLabel==null){_InnerLabel = new Label();}
    cellA.Controls.Add(_InnerLabel);

    if (_InnerButton==null){_InnerButton = new Button();}
    cellA.Controls.Add(_InnerButton);    
    
    //Mark that we've been here...
    ChildControlsCreated = true;
}

What we gain from this is that we get to now keep the control that got changed.

  • Pros
    • We keep the simplicity of Direct Wiring
    • We keep the clarity of Direct Wiring
    • We get to keep and persist the values we are after.
  • Cons
    • Because it is directly tied to a child control, you can't very well use that property to set several child controls properties at once like you could do with a ViewStated property value (I guess you could, technically, pull it off in some cases -- but I wouldn't recommend it).
    • The system works for Non-Templated controls.
    • The system works for Templateable controls that are using a Default Template that is built into the control.
    • The system does not work for Templateable controls that have been set with a User-Defined (ie, declarative tags...) template.
      • Why? Because the tags that define the Templated controls are converted to code by ASP.NET - we have no access to this process in order to tell it to Recycle rather than Replace the child controls.
      • But its OK!
        Templated Controls, by definition are when users take over the layout and choice of controls by using their own declarative code.
        Therefore -- by definition -- there is no need to set custom Layout and ControlType properties that would reset ChildControlsCreated to false.
      • TODO:CHECK And if a ChildControlsCreated is called anyway even though we were supplied a user-defined templates?
 

 

Resetting ChildControlsCreated to false versus EnsureChildControls

From the above it will have become clear that problems can happen when ChildControlsCreated is reset to false -- but problematic as it is, resetting is sometimes needed (it also happens to be part of the DataBind process).

TODO: Check again what happens for a Templated control, with inner button, and DataBindable...

So the first question is: what kind of properties need to reset ChildControlsCreated

The answer of course is anything that CreateChildControls() will need to determine a course of action, and that can't be put off to PreRender(), or Render(), or PrepareControlHierarchy(). Candidates are:
  • Layout Properties:
    Properties that change the layout of the control appear to fit the bill:  it would appear to make sense to layout the controls in CreateChildControls differently if the layout is LeftToRight versus RightToLeft.
    • I know that this type of property is usually used in CreateChildControls()...But I am going to suggest an alternate approach.
      If you can -- try to NOT do this flipping around business here, but actually built it always the same way (eg: a default LeftToRight), and only later, after ViewState has been saved (namely in PrepareControlHierarchy), flip things around.
    • I know that what I am saying is...interesting...but look what we get for it:
      • The control hierarchy is the same for each post: there is no chance that PostBack Data gets lost, and we would no longer ever bump into the dreaded scenarios where ViewState can't resolve itself.
    • It technically is easy enough to move around things:
      • It can be done in non-templateable controls.
      • It can be done in templateable controls that are using a default Template, as long as you check first:
        void PrepareControlHierarchy(){
            ...
            if (_usingDefaultTemplate){
               if (Layout == Layout.RightToLeft){  
                 ...flip things around...
               }
            }else{
              ...no idea how he/she has laid out child controls
              ...so can't flip/move them. Leave well alone,
              ...and therefore, yes, we are ignoring 
              ...the Layout Property.
            }
        }
      • It obviously cannot be done with Templateable controls that are using user-supplied templates...But that's OK!

        Templated Controls, by definition are when users take over the layout and choice of controls by using their own declarative code.
        Therefore -- by definition -- there is no need to set custom Layout and ControlType properties that would reset ChildControlsCreated to false.
      • What happens if the layout can't be massaged around? In other words, its too big, too complex to simply move around?
        TODO: In that case, we do reset...
  • Control Type Properties:
    Another type of property that would reset ChildControlsCreated to false would be a property that specifies the type of control to render, such as a property called ListControlType, which would define whether the control is to render a ListBox, DropDownList, or CheckBoxList.

    This is a little more complex. Options are:
    • One could create a default ListBox, using a common Type, and rebuild in only if required within PrepareControlHierarchy:
      ListControl newControl;
      if (ListControlType == ListControlType.DropDownList){
        newControl = new DropDownList();
      }else if (ListControlType == ListControlType.ListBox){
        newControl = new ListBox();
        ((ListBox)newControl).Rows = 4;
      }else{
        newControl = null;
      }
      
      if (_wcListControl == null){
          //This is first time EnsureChildControls
          //has been called...
          _wcListControl = newControl;
      } else {
          if( _wcListControl.GetType() != newControl.GetType() ){
              //ListControlType has changed...
              //Need to transfer from type1 to type2:
              foreach(ListItem item in _wcListControl.Items){
                  newControl.Items.Add(item);
              }
              //Once transferred...
              _wcListControl = newControl;
          }
      }

 

Conclusion

The second question is what do we do about it? What's the plan?

The interesting thing to note is that you have several ways to skin the cat and get the job done.

  • CompositeControls that have no properties that reset ChildControlsCreated:
    If the control is a simple one, that does not have any cause to reset CreateChildControls, then using EnsureChildControls() is a perfectly fine solution.
  • CompositeControls that have one or more properties that reset ChildControlsCreated:
    If the control does have properties that need to reset CreateChildControls, try to stick to the following recipe:
    • Make sure the Properties are ChildControlsCreated are Attributes of the control,
    • Instead of opting for Direct Wiring+Recycling, try to use Delayed Setting for every single property that is exposed as an Attribute.
    • But for complex properties of child controls, such as Items, do it via Direct Setting + Recycling.
      • What will happen:
        Because Attributes are parsed first (see this article), the properties that could possibly reset ChildControlsCreated (eg: Layout/ControlType) will be parsed and set by ControlBuilder by the time it gets around to parsing nested tags, only then triggering EnsureChildControls().
      • As for the real possibility of a coder instantiating your control via code, and setting Items (triggering EnsureChildControls) before setting the Layout, resetting ChildControlsCreated, that is still a possible waste of CPU due to multiple passes through CreateChildControls() -- but one that is unavoidable.
        As far as I can tell, there is nothing else that you  can do other than put a tip in the documentation to set that property first.
powered by metaPost
Page: 1 of 3
Previous Page | Next Page

Comments

By Michel @ Tuesday, October 06, 2009 11:56 PM
Thanks! Good article about persisting properties. This helped me a lot solving some issues with my server control!

Click here to post a comment

             
Copyright 2007 by Sky Sigal