|
|
|
|
posted @ Friday, May 16, 2008 7:36 PM by SkySigal
When writing CompositeControls there are 3 or 4 rules to follow to make them successful every time: General Background Knowledge - Know that ControlBuilder parses declarative code in the following order:
You probably will never need this, but sometimes it can be handy to know that the ControlBuilder builds your control and fills its properties in a certain order: - ITemplate properties first,
- then Attributes, alphabetically,
- then Nested Tags, alphabetically.
- Always make your CompositeControl as an INamingContainer.
This is one of those rules that doesn't look too important -- but will so easily lead you to wrong conclusions about how WebControls work. CompositeControls and instantiating their Children - Always create Child Controls within the method that is provided for this -- namely CreateChildControls(), or better yet CreateControlHierarchy(booL).
Any Child Control created must be instantiated and added to the Page Hierarchy within the context (ie, in it, or a method invoked by it) of the CreateChildControls() method. Important: Instantiating Child Controls at any other location (OnInit, OnLoad, PreRender, etc.) you lose PostBack wiring capabilities because the PostBack mechanism invokes CreateChildControls() -- an not any other method -- to find the method the post back data or event is for. Important: In CompositeControls that offer DataBinding capabilities, the method to use will be a custom CreateControlHierarchy(bool) rather than the default CreateChildControls().
- Never invoke CreateChildControls() directly.
Never invoke CreateChildControls() directly: -- it is meant to be invoked only via the EnsureChildControls() method, since EnsureChildControls() always checks to make sure that CreateChildControls() has not been called already: void EnsureChildControls(){
if (!ChildControlsCreated){
CreateChildControls();
}
ChildControlsCreated=true;
}
- And Avoid invoking EnsureChildControls() yourself if you can -- let the Framework invoke it as it sees best.
Avoid calling EnsureChildControls() yourself: you're almost invariably going in the wrong direction if you are.
Note:
Yes, I know that many people invoke EnsureChildControls() within each set;get; following the Direct Property Setting methodology, and I'm not that against it...but read my article on properties first.
- On First Request, EnsureChildControls() is called in PreRender if not invoked before (eg, in a get;set;}.
On PostBack it can happen before OnLoad if:
- the control is a composite control that has child controls,
- one or more of its child Controls implements IPostBackDataEventHandler,
- a PostBack Event is coming back from a child control.
- Always call Controls.Clear() at the top of CreateChildControls() or CreateControlHierarchy(bool) if the control has been NK'ed*
Always call Controls.Clear() before creating children because it might be that you are re-creating the Controls on PostBack (ie that CreateChildControls() was first called by the Init phase of PostBack to process PostBack data or events(see above),
but because a set; property has now reset the ChildControlsCreated to false, PreRender() has asked EnsureChildControls() to recreate the child controls (ie, this can be the second (or 3rd) time around -- delete what's there first:
public void CreateChildControls(){
//IMPORTANT: Call 'base', or you will be recalling EnsureChildControls!
base.Controls.Clear();
...etc...
ChildControlsCreated=true;
}
- What is this CreateControlHierarchy(bool) you are talking about?!?
You won't find it in the controls or MSDN documentation -- its a mod that you have to implement yourself.
First proposed in Nikhil's Kothari book, the pattern was to unify the child control development process and share it between CreateChildControls() and the DataBind() method:
/// <summary>
/// Creates Child Controls of this CompositeControl.
/// </summary>
protected override void CreateChildControls() {
//Clear first (the reason why makes sense when called later by DataBind())
//Use base...or it will trigger CompositeControls.EnsureChildControls first...
base.Controls.Clear();
this.CreateControlHierarchy(false);
//Set the flag so that CreateChildControls doesn't get called
//again, unless by DataBind() -- which is the only exception.
this.ChildControlsCreated = true;
}
//Exists in CompositeControl (see Lutz)
public override void DataBind() {
base.OnDataBinding(EventArgs.Empty);
//If this is not forced to True
//(ie 'false' if DataBind is called before PreRender/EnsureChildControls)...
//Controls.Clear() on next line will actually
//trigger Controls{get...}, triggering EnsureChildControls/CreateChildControls
this.ChildControlsCreated = true;
//Clear Controls, State, and start tracking.
this.TrackViewState();
base.Controls.Clear();
this.ClearChildState();
//Let CreateChildControls rebuild everything:
this.CreateControlHierarchy(true);
}
/// <summary>
/// Method to create required ChildControls within.
/// Called by both CreateChildControls and DataBind
/// See Remarks.
/// </summary>
/// <remarks>
/// <para>
/// Called by <see cref="EnsureChildControls"/> at <see cref="PreRender"/> on
/// first page request, or during
/// Initialization of page on PostBack
/// if the Control implements <see cref="IPostBackDataHandler"/>, or
/// a child Control <see cref="IPostBackEventHandler"/> has triggered an event.
/// </para>
/// </internal>
abstract protected void CreateControlHierarchy(bool dataBind);
Modifying your control in fit this pattern (and implementing the PrepareControlHierarchy() pattern shown below), is to "NK" the control....
- Always remember to set ChildControlsCreated to true at the bottom of your CreateChildControls (or PrepareControlHierarchy(bool) if NK'ed) and DataBind()
Beginners often forget that this is crucial or it will base.Controls.Clear() + recreate all the controls several times through the lifespan of the control + break a whole lot of things along the way (PostBack, events, etc. will fail).
- When creating child Event raising Controls Always ID/Name them.
PostBack data and PostBack events finds the control they are intended for by their name (eg: ctrl1:YourButton).
Changing locations of unnamed child items causes new names to be used (eg: ctrl0:YourButton), which causes the PostBack mechanism not be able to find the control it is intended for.
- Try to use CreateChildControls (and/or CreateControlHierarchy) for just that...Creating, not making pretty just yet...
Try to not set properties of child controls here...do it later, in PrepareControlHierarchy().
If you absolutely must, set the properties after you've created the child control, but before you've added the child control to its parent, triggering its ViewState mechanism to start tracking changes.
Properties of CompositeControls
- You can use Direct Property Setting for simple controls.
You can use Direct Property Setting + Recycling if CreateChildControls will be triggered.
You should opt for Delayed Property Setting if you can.
But fall back to Direct + Recycling if you can't.
Don't worry about SelectedItem and SelectedIndex -- they are marked Browseable(false).
Read this article first.
- When using the Delayed Property Setting methodology, apply the property in PrepareControlHierarchy(bool).
Again: PrepareControlHierarchy(bool) should be used just to create child controls -- not set its properties.
To not bloat the ViewState of the child control, apply it in PrepareControlHierarchy(bool), after the child control has packed up its ViewState,
- If you absolutely have to apply property values in PrepareControlHierarchy(bool), do it before the child control's tracking is enabled.
Again: If you have to apply properties in PrepareControlHierarchy(bool), make sure you apply them before the child control is added to its parent, so that the value is transferred prior to the inner control starting to track ViewState changes.
- What PrepareControlHierarchy(bool) are you talking about?!?
You won't find it in the controls -- this is a mod you have to implement.
First proposed as a recipe in Nikhil's book I think, its done by taking over the Render() stage of the Control in order to inject it:
I don't like retyping it in every control, so its implemented in both XAct.Web.Controls.Common.BaseCompositeControl and BaseDataBoundControl
sealed protected override void Render(HtmlTextWriter writer) {
//If using default designer, make sure control is rebuilt
//and passes through PreRender every time:
if ((this.Site != null) && (this.Site.DesignMode)) {
this.ChildControlsCreated = false; this.EnsureChildControls();
try { this.OnPreRender(EventArgs.Empty); } catch { }
}
//Apply Styling to Child Controls:
PrepareControlHierarchy();
//Call final Render:
base.Render(writer);
}
/// <summary>
/// Called by Render to apply non-ViewStated Styling,etc.,
/// and possibly rearranges Elements.
/// </summary>
abstract protected void PrepareControlHierarchy();
- Transfer the values in PrepareControlHierarchy(bool) if DataBinding is involved (*).
TODO:Check this.
- Use Immediate Delegation rather than Delayed Delegation on properties such as:
- public ListItemCollection InnerDropDownItems {get;}
- public int InnerDropDownSelectedIndex {get;set}
- public ListItem InnerDropDownSelectedItem {get;set;}
Note:
This is not as difficult as it appears because SelectedIndex and SelectedItem would be Browse Only attributes.
When you want to make your Controls Templateable
- Take the time to make your Controls Templateable
TODO:
- Create a TemplateContainer for each Template you will be instantiating:
- Use the TemplateContainerAttribute on your ITemplate properties
Make sure you mark the ITemplate property with the TemplateContainer that it will be instantiated within:
[
Browsable(false),
PersistenceMode(PersistenceMode.InnerProperty),
TemplateContainer(typeof(MainTemplateContainer)),
]
public ITemplate MainTemplate {
get { return _MainTemplate; }
set { _MainTemplate = value; }
}
private ITemplate _MainTemplate = null;
Why? It's how the page at runtime can later define the Container/context when DataBinding:
<MyControl>
<Templates>
<Main>
<%# Container.DataItem.Price.ToString() %>
</Main>
</Template>
</MyControl>
- Reset the ChildControlsCreated flag in an ITemplate {set;}
When you set an ITemplate make sure that ChildControlsCreated is reset to false:
TODO: Explain Why.
[
Browsable(false),
PersistenceMode(PersistenceMode.InnerProperty),
TemplateContainer(typeof(MainTemplateContainer)),
]
public ITemplate MainTemplate {
get {return _MainTemplate;}
set {
_MainTemplate = value;
//Reset to false if
//EnsureChildControls has already been called:
ChildControlsCreated=false;
}
}
private ITemplate _MainTemplate = null;
-
Instantiate the template within CreateChildControls:
To do it, use code such as the following:
protected override void CreateControlHierarchy(bool dataBind) {
//Old Habit:
Control container = this;
//Make Main Template Container:
_ContainerMain = new MainTemplateContainer(this);
_ContainerMain.ID = "TC";
//Instantiate Main Template:
if (this.MainTemplate == null) {
this.MainTemplate = new MainTemplateDefault();
}
this.MainTemplate.InstantiateIn(_ContainerMain);
...
//apply Indirect Property Settings if need be...
...
//Now add containerMain to page,
//triggering TrackViewState in it and all children:
container.Controls.Add(_ContainerMain);
}
When you need to go beyond using the ViewState StateBag
- Managing Inner objects that implement IStateManager:
- If you offer a control that has sub classes that implement IStateManager, you of course will have to override the 3 IStateManager methods (SaveViewState, LoadViewState and TrackViewState):
protected override void LoadViewState(object savedState) {
object[] blobs = (object[])(savedState);
base.LoadViewState(blobs[0]);
((IStateManager)_Items).LoadViewState(blobs[1]);
}
protected override object SaveViewState() {
object[] blobs = new object[2];
blobs[0] = base.SaveViewState();
blobs[1] = ((IStateManager)_Items).SaveViewState();
return (object)blobs;
}
protected override void TrackViewState() {
base.TrackViewState();
if (_Items != null) {((IStateManager)_Items).TrackViewState();}
}
Important:
Notice how it checks to see if the private field exists first. If it exists, the flag is turned on. It it does not yet exist, do nothing.
BUT!:
Because the inner property might not have existed at the time the Control.TrackViewState() was invoked, it is possible that the Control is already tracking ViewState by the time the sub IStateManager class needs to be created... Therefore you have to check again and optionally turn it on as you are creating it:
public Style InnerStyleA {
get {
if (_InnerStyleA == null){
//Need to create it:
_InnerStyle = new Style();
//Turn it on?
if (this.IsTrackingViewState){
((IStateManager)_InnerStyle).TrackViewState();
}
}
return _InnerStyleA;
}
}
private Style _InnerStyleA;
Between the first and second check, one of these two checks will ensure that TrackViewState is turned on by the inner IStateManager class if the outer one is turned on.
Just in case, know that there are other ways of writing the above checks: you can get rid of the checks/turn on clauses in the outer parent's Control.TrackViewState, if you rewrite your code as follows:
public Style InnerStyleA {
get {
if (_InnerStyleA == null){
//Need to create it:
_InnerStyle = new Style();
}
//CHECK EVERY TIME:
if ( (!((IStateManager)_InnerStyle).IsTrackingViewState)
&& (this.IsTrackingViewState)) {
((IStateManager)_InnerStyle).TrackViewState();
}
return _InnerStyleA;
}
}
private Style _InnerStyleA;
- If you want to parse nested tags as members of a collection, you need to add the following attributes:
[
ParseChildren(true, "Items"),
PersistChildren(true),
]
public class MyListControl : WebControl, INamingContainer {
...
//So that it feeds nested tags into
public ListItemCollection Items {get;}
...
}
Attributes that you need to know about
Comments
By
Rob @
Thursday, September 25, 2008 3:55 AM
|
|
Great article, some real useful bits of info here, thanks for sharing :)
|
|
|
By
Tahir @
Friday, November 28, 2008 6:25 PM
|
Thank you for the great tips. I've been looking for a solution to my problem for 2 days until I came across your post (my problem being view state not loading on post back for items inside a template).
2 cents from me: if some other part of your page makes use of a control inside a template during OnLoad and that control will only be available after CreateChildControls, you will obviously encounter an error. I moved that code from OnLoad to OnPreRender. Is there any other solution to this problem?
|
|
|
By
Tahir @
Monday, December 01, 2008 12:08 PM
|
|
In addition to my previous post: by adding a call to EnsureChildControls() to OnInit in the template container; that works both on GET and POST to solve the problem with accessing child controls of the ITemplate. Everything else has to be done according to the "golder rules" as outlined above.
|
|
|
Click here to post a comment
|
|