0/0 | |
|
The questions I have are around forwarding/wiring properties intelligently, and flexibly from the outside of a CompositeControl to the child Controls.
The following is pretty long -- but I think required to point out the various holes I've already tried and climbed back out of...
In essense, I've come across two methods to do this (Nikhil's versus ASP.NET's team) -- but each with their own costs.
Can someone help?
To demonstrate the issue that I am trying to solve, let's consider the following CompositeControl:
- CompositeControl
- Properties
- string ButtonText {get;set;}
- eButtonType ButtonType {get;set;} //Image or Link
- eListControlType ListControlType {get;set;}
- ListItemCollection Items {get;}
- ChildControls
- Control InnerButton; //Image or Link
- ListControl InnerListControl;
A disection of the two approaches:
#1: The Nikhil Approach ("Direct Wiring"): EnsureChildControls() everywhere If you've bought Nikhil's great book on making control's you're going to see the following solution to forwarding these properties:
//simple property public string ButtonText { get {EnsureChildControls();return _InnerButton.Text;} set {EnsureChildControls();_InnerButton.Text = value;} }
//complex property: public ListItemsCollection Items { get {EnsureChildControls();return _InnerListControl.Items;} }
//ViewStated parameter of this control. public eListControlType ListControlType { get {object o=ViewState["ListControlType"];return (o!=null)?(eListControlType)o:eListControlType.DropDownList;} set { ViewState["ListControlType"] =value; ChildControlsCreated=false; //if childs already created, have to rebuild everything. } }
public eButtonType ButtonType{ get {object o=ViewState["ListControlTypeA"];return (o!=null)?(eListControlType)o:eListControlType.DropDownList;} set { ViewState["ListControlTypeA"] =value; ChildControlsCreated=false; //if childs already created, have to rebuild everything. } }
ControlBuilder, and the Order of Processing Attributes Now, the ControlBuilder, after investigation, turns out that has absolutely no atomic behavior and transfers the parameters from the declarative tags to the control's properties immediately, one by one, in ALPHABETIC order, then doing its nested tags, also in alphabetic order...
In other words, it will parse:
<CC1:MyControl ButtonType="Button" ListControlType="DropDown" ButtonText="Submit"> <Items> <option>Uno</option> <option>Dos</option> <option>Tres</option> </Items> </CC1>
into C# that does about the same thing as :
MyControl ctrl = new MyControl(); //Set properties before adding control to page and triggering its TrackViewState ctrl.ButtonText = "Submit"; //'b' comes before 'l'...and CreateChildControls() is called first time... ctrl.ButtonType = eButtonType.Button;//...which resets the ChildControlsCreated flag! ctrl.ListControlTypeA = eListControlType.DropDown;//...and CreateChildControls() is called again, then resets the ChildControlsCreated flag! ctrl.Items.Add(new ListItem("Uno"));//...and CreateChildControls() is called again... ctrl.Items.Add(new ListItem("Due")); ctrl.Items.Add(new ListItem("Tres")); //add to page, and turn ctrl's tracking on: this.Controls.Add(ctrl);
What this means is that, by setting the ListControlType, it's going to reset CreateChildControls() (has to!), and then the Items.Add() statements are going to force EnsureChildControls()->CreateChildControls() to be called again...and in the process, the value of ButtonText is going to get lost.
Solution: Move application of ButtonText to PrepareControlHierarchy() Yes...so tis true that we could change our code around so that the Attributes (not the Nested Items) are applied later:
//ViewStated parameter of this control: public eListControlType ButtonText { get {object o=ViewState["ButtonText"];return (o!=null)?(string)o:string.Empty;} set {ViewState["ButtonText"] =value; } }
in order to apply it much later: void PrepareControlHierarchy(){ //Apply cosmetic stuff after ViewState saved: ... _InnerButton.Text = this.Text; ... }
Yes. That's true. But frankly, this is also a simple composite control, where such a solution is possible. But in a more real-world scenario, a little more complex, it doesn't.
For example, the new Login control is built as a TemplateContainer+Container scenario, where it has a nested property of Items, then Templates: <MyControl> <Template> <Select id="Categories"/> </Template> <Items> <option>Uno</option> </Items> </MyControl>
Here, as we saw above, the Control builder will ALWAYS read Items before Template -- so it will force EnsureChildControls() in the Items {get;} before it has a user-defined template and use a pre-supplied Defaulttemplate if the control is correctly built.. This means that later , when Template property gets set, it will have to reset ChildControlsCreated... In other words, I will lose the Items put into the DefaultTemplate.Items!
Solution: Can ControlBuilder be taught to process attributes in a different order? I've peered in at the code that's in ControlBuilder...and other than a Filtering of some kind going on that I have no idea what/where its coming from/what it does, most of the properties are marked as Internal, so there's not much leeway to surgically teach ControlBuilder how to process first properties that are marked with a custom attribute saying ("MeFirstPlease")...Or is there a way?
Storing local copies of variables set I've mucked around trying to not lose these properties by doing things like:
public string ButtonText { get {return (base.ChildControlsCreated)?_InnerButton.Text:_CopyOfButtonText;} set {_CopyOfButtonText=value;EnsureChildControls(); }
The NET Framework approach ("InDirect Wiring"): 2Stepped approach This seemed to me to be a serious issue, so I Reflected on how the ASP.NET team built their controls in System.Web.UI.WebControls, and I see a different pattern...which I've tentatively named InDirect Wiring.
Basically, the design pattern is based on the fact that the container control becomes the ViewState handler for the child controls, and the property get/set statements look like this:
public string ButtonText { get {object o=ViewState["ButtonText"];return (o!=null)?(string)o:string.Empty;} set {ViewState["ButtonText"] =value;} } public ListItemsCollection Items { get { if (_Items == null){ _Items = new ListItemCollection(); if (this.IsTrackingViewState){ ((IStateManager)_Items).TrackViewState();} } return _Items; } } private ListItemCollection _Items;
What's happening here is that even if the inner controls have viewsated Text and Items properties, we're going to remake them here in the outer container control, viewstating them in the outer control, and later transfer them to the nested child controls in such a way that the child controls don't persist the stuff as well...
protected overrided void CreateChildControls(){ Button btn = new Button(); //transfer property from outer to inner before control's TrackViewState is turned on: _btn.Text = this.ButtonText; //Add, which turns on ctrl's trackviewstate(): btn.Controls.Add(_InnerButton);
//Depending on type chosen, make a different ListControl derivative: ListControl listControl = (ListControlType==DropDown)?new DropDownList():new ListBox(); //transfer items from outer to inner before control is added to its container: foreach (ListItem item in this.Items) { listControl.Items.Add(item); } //Add, which turns on ctrl's trackviewstate(): this.Controls.Add(listControl); }
At this point, when I had found these sources, I thought that I had found the solution to my problems-- the solution that the ASP.NET team is using would not lose the ButtonText property if eListType was modified...
... but it causes two other nightmares.
The Items Collection is out of Date First of all, the Items property is only looking at current data up till CreateChildControls(). After that, if I ask for its contents it will show me old stuff (ie what was added declaratively). If I add an Item to the InnerControl's.iTems collection due to an eventhandler, it won't be reflected on the outside.
Solutions Attempted have been allong the following lines:
The 'Moving target' solution: public ListItemCollection Items { get { if (!ChildControlsCreated){ if (_Items == null){ _Items = new ListItemCollection(); if (this.IsTrackingViewState()){(IStateManager)_Items).TrackViewState();} } }else{ //Children were built, so list contents has been copied to inner control. //_Items is no longer relevant. _InnerListControl.Items; } } }
but that's doesn't pan out as a solution as far as I can tell. It not only confuses me no end, but I'm probably confusing the computer too.
Deferring to PrepareControlHierarchy: Instead of transfering the declaratively added items to the inner control in CreateControls(), wait till PrepareControlHierarchy() -- if any databinding must be done in between those two moments, do it to the container's Items, and not the inner ListControl...
The Pros: everything is unified, and appears to actually works for once... the Items is the right list, and the selectedIndex could actually work. The Cons: upon postback the SelectedIndexChanged will always yield -1 as a result, because the inner control's list has only 0 elements in it at the time of processing the event (it won't have more elements till PrepareControlHierarchy which happens much later, during Render).
ViewState the ChildControl, not the outer control's list. This seems better logic to me -- much like Nikhil's Direct Wiring approach in some regards, but I havn't seen how exactly. For one, it brings back the need for the 'moving target' if/else clauses demonstrated above, secondly... this approach is going to yield better results when we wrap around a GridView, or any other Templated databound control, simply because there is no IStateManaged object to work with in those cases -- we need the child controls of the GridView to have enough info to build themselves properly in CreateChildControls, rather than late in PrepareChildControls or other solution. Atleast I think that will be an issue.
HELP. HELP.HELP. In essense I've scanned the whole net framwork for a single control that wraps around a ListControl or GridView or DataGrid, and exposes its SelectedIndex property, and/or Items collection and there isn't one, so I have no examples of how to do it properly.
Very many thanks for reading this far -- I know it was long -- but hopefully I will get a complete answer from someone who has figured it out. I'm just hoping that I am missing something really obvious, and that it can be done...
Thank you. Sky Sigal
|