Hi Kevin:
Read Building ASP.NET Server Controls -- Apress. Damn good.
As for how to get this far (and further since I was able to solve it a
couple of hours after I posted
) These are the points I've distilled so
far:
a) There are several levels of controls:
i) basic control that inherits from Control. That means you generally
have to implement IPostBackEventHandler and/or IPostBackDataHandler (i think
those are the names -- atleast they are something like that). Plus you have
to have a pretty good idea of Styles/ViewState...therefore, to solve that
kind of headache, I suggest moving up the food chain to:
ii) basic control that inherits from WebControl. That's pretty easy...
An obvious example would be inherit and extend something like Label.
iii) Composite control -- now things get a little interesting here.
Again, I have experimented with enheriting the wrapper control from
Control -- but frankly, inheriting from WebControl makes it much easier.
iv) Control that has sub-elements. Eg: a SELECT like control that has
OPTIONS (or Asp:ListItem). I'll come back to this in a second.
v) Controls that have sub templates
vi) Controls that have nested Sub-Elements (which is what the original
post was about).
This all took me a little to 'see'/figure out, hence why I am emphasizing
that not all controls are the same -- the last 3 are the ones we are talking
about now.
Lets go back to the iv) first -- I was making a Combo (editable pulldown),
and I was having a lot of trouble on figuring out how to add OPTIONS to
it... Turns out that the secret is the attribute
[ParseChildren] attribute...that you put above the Control : WebControl def.
There are 3 ways it can be written - and they do different things:
a) ParseChildren(false) -- means "Any subelements are NOT Properties - - so
has to fit other criteria".
b) ParseChildren(true) -- means "Parse child tags and try to match them to
Properties. Something like
<cc1:MyControl>
<ForeColor >
</cc1:MyControl>
(or something like that -- frankly I havn't used it this way yet so I'm not
sure how that would look-- but it's something like this...)
and finally
c) ParseChildren(true,"ITEMS") -- which is the beginning of where things get
interesting. This states Parse sub tags as properties -- and if you see
<items> then add then to a collection given by the control.
in other words this would be valid if
<cc1:MyControl>
<asp:listItem Text="John">
<asp:listItem Text="Sam">
<asp:listItem Text="Mary">
</cc1:MyControl>
What this will do is take all sub elements found, and try to add it to a
public property you have provided in your control that must be called Items
since you declared in the attribute that it would be so:
public ArrayList Items {get {return _List;}}
Did you get that part? -- it takes all tags and shoves them in List... in
other words this is valid to -- although probably not at all what you want:
<cc1:MyControl>
<asp:listItem Text="John">
<asp:listItem Text="Sam">
<asp:Label Text="Damn">
</cc1:MyControl>
This will still stick the item in the Items arrayList -- but probably cause
you headaches at Render() time.
Therefore -- although that looked sooo easy -- it's usually a red herring --
so everything I just said ignore, and let's point out a more flexible, but
managaed, way:
This time make the attribute like this
[ParseChildren(ChildrenAsProperties = false)]
AND override the Control's AddParsedSubObject method to only add items of a
specific type:
protected override void AddParsedSubObject(Object obj) {
if (obj is ListItem){_Items.Add((ListItem)obj);}else {/*ignore
me*/}
}
See where this is going? AddParsedSubObject always happened - -but now you
are controlling which items to add... You can tell it to ignore the Label --
or any Literals that got in there by accident/whatever.
We are starting to get where we want -- although it ticks me off that I have
to use "asp:ListItem" rather than OPTION -- it's much more verbose, and
frankly I'm sure that I will type it wrong...so...I have to figure out a way
to get it to accept tags that start with <OPTION....But...
There is still one hitch though....ASP is not very smart: It will recognize
any item that is ASP: tagged (eg: ListItems) but is stupid as a plank if you
want it to recognize something like plain HTML. And anything it doesn't
recognize it will treat as HtmlGenericControl.
In other words, if you typed something like
<cc1:MyControl>
<option Text="John"/>
<option Text="Sam"/>
<option Text="Mary"/>
</cc1:MyControl>
AddParsedSubObject WILL be called -- but not 3 times -- only once!!!, being
passed one big HtmlGenericControl whose contents are '<option
Text="John"/><option Text="Sam"/><option Text="Mary"/>"...
Ie bluddy useless.
The problem is because ASP has a default controlBuilder -- a fancy name for
customparser? -- that only looks for Asp tags. You have to make one that is
a bit smarter, looking out for your own tags.
Good news is that it is really simple -- in essense you are asking it to
look out for startTags that match what you are looking for:
a) Stick this on your control btw:
ControlBuilderAttribute(typeof(MyBuilder))
b) An example of a Builder
With this in place you can then type:
<cc1:MyControl>
<option Text="John"/>
<ITEM Text="Sam"/>
<option Text="Mary"/>
<asp:listitem Text="Mary"/>
<asp:Label Text="Discard me">
</cc1:MyControl>
You will end up with AppendLiteralControl being called 3 times -- and
ignoreing the Label. Much nicer.
You just have to finish up a rendering that does something like
PreRender(){
foreach (ListItem X in _Items){
MySelect.Items.Add(X);
}
}
I think that just about covers Type 3.
As for the more complicated version -- a Tree that parses its children
deeper than 1 -- something like a Menu control as I was writting this
week -- It's so similar -- I just couldn't see it at first.
The error was two part:
a) I should have made the attribute:
[ParseChildren(true,"OUTLOOKBARITEM"),PersistChildren(true),ControlBuilderAt
tribute(typeof(MyBuilder))]
on the OutlookBarItem... It's not at all what is happening...
It should have been
[ParseChildren(false),PersistChildren(true),
ControlBuilderAttribute(typeof(MyBuilder))]
AND
I should have realized that the OutlookItem -- which is my custom ListItem
on steroids -- needs to be (duh) a WebControl for it to parse/render.
Anyway -- the corrected (and working -- although I have not added the JS and
CSS to make it work) code is attached below -- and it parses correctly as I
intended...
I hope that these notes helped rather than confuse you even more...
Let
me know -- and I'll try to make another stab at explaining it if I was as
clear as mud
But buy the book. Was money well spent in my opiou nion. It doesn't answer
everything (for example I had to figure out how to read nested sets by my
self) but I don't think I could have without a leg up from the book.
Plus, As is amply clear by now -- I am neither good with code in the first
place, nor a good writer -- they are
Very best and good luck,
Sky
Ps: one last point -- there is still one last part to work out that i havn't
figure out: it renders in runtime -- but it's giving me the dreaded gray box
in the IDE. Something not's instantiated in Design mode? Grrr.
The corrected code is as follows:
public class MyBuilder : ControlBuilder {
public override Type GetChildControlType(string tagName, IDictionary
attribs) {
if ((tagName.ToUpper().IndexOf("FOLDER")>-1)||
(tagName.ToUpper().IndexOf("ITEM")>-1)||
(tagName.ToUpper().IndexOf("NODE")>-1)){
return typeof(XOutlookBarItem);
}
return null;
}
public override void AppendLiteralString(string s) {}
public override bool AllowWhitespaceLiterals(){return false;}
}
[ParseChildren(false),PersistChildren(true)]
[ ControlBuilderAttribute(typeof(MyBuilder))]
public class XOutlookBarItem : Control {
//==========================================================
//PRIVATE FIELDS
//==========================================================
XOutlookBar _ParentMenu =null;
XOutlookBarItem _ParentNode = null;
bool _IsFolder=false;
string _Label = string.Empty;
string _Url = string.Empty;
string _ImgUrl = string.Empty;
string _Target = "_self";
string _ToolTip = string.Empty;
//==========================================================
//PUBLIC PROPERTIES
//==========================================================
//----------------------------------------------------------
[Category(" XAct Appearance")]
public string ImgUrl {get {return _ImgUrl;}set{_ImgUrl = value;}}
[Category(" XAct Appearance")]
public string ImageUrl {get {return _ImgUrl;}set{_ImgUrl = value;}}
[Category(" XAct Appearance")]
public bool IsFolder {get {return _IsFolder;}set{_IsFolder = value;}}
[Category(" XAct Data")]
public string Label {get {return _Label;}set{_Label = value;}}
[Category(" XAct Data")]
public string Url {get {return _Url;}set{_Url = value;}}
[Category(" XAct Data")]
public string Target {get {return _Target;}set{_Target = value;}}
[Category(" XAct Data")]
public string Title {get {return _ToolTip;}set {_ToolTip =value;}}
[Category(" XAct Data")]
public string ToolTip {get {return _ToolTip;}set {_ToolTip =value;}}
[Browsable(false)]
public XOutlookBar ParentMenu {get {if (_ParentMenu!=null){return
_ParentMenu;}else{if (_ParentNode!=null){return
_ParentNode.ParentMenu;}}return null;}set{_ParentMenu = value;}}
[Browsable(false)]
public XOutlookBarItem ParentNode {get {return _ParentNode;}set{_ParentNode
= value;}}
//==========================================================
//CONSTRUCTOR
//==========================================================
public XOutlookBarItem():base(){
this.PreRender += new EventHandler(Page_PreRender);
}
//==========================================================
//LIFECYCLE
//==========================================================
protected override void AddParsedSubObject(Object obj) {
//Only allow Nodes as children:
if (obj is XOutlookBarItem){
XOutlookBarItem oNode = (XOutlookBarItem)obj;
string tCheck = oNode.Label;
oNode._ParentNode = this;
oNode._ParentMenu = this.ParentMenu;
this.Controls.Add(oNode);
}
}
protected void Page_PreRender(object sender, EventArgs e){
this.Controls.Add(_Render());
}
protected override void Render(HtmlTextWriter output) {
if ((this.Site != null) && (this.Site.DesignMode)){
this.ChildControlsCreated=false;
this.EnsureChildControls();
try{this.OnPreRender(EventArgs.Empty);}catch{}
}
base.Render(output);
}
//==========================================================
//PRIVATE METHODS
//==========================================================
private WebControl _Render(){
if (this.IsFolder){return _RenderAsFolder();}else{return _RenderAsItem();}
}
private WebControl _RenderAsFolder(){
Panel oDO = new Panel();
Panel oDIT = new Panel();oDO.Controls.Add(oDIT);
Label oLabel = new Label();oDIT.Controls.Add(oLabel);
oLabel.Text = this.Label;
Panel oDIB = new Panel();oDO.Controls.Add(oDIB);
oDO.CssClass = this.ParentMenu.CssClassFolder;
oDIT.CssClass = this.ParentMenu.CssClassFolderButton;
oLabel.CssClass = this.ParentMenu.CssClassFolderButtonLabel;
oDIB.CssClass = this.ParentMenu.CssClassFolderItemArea;
while (this.Controls.Count>0){
XOutlookBarItem oChild = (XOutlookBarItem)this.Controls[0];
//this.Controls.Remove(oChild); //Doesn't look like this is needed...
oChild.ParentMenu = this.ParentMenu;
oDIB.Controls.Add(oChild);
}
if (this._ToolTip != string.Empty){
oDIT.ToolTip = _ToolTip;
oDIT.Style["CURSOR"]="hand";
}
if (this.Url!=string.Empty){oDIT.Attributes["Url"] = this.Url;}
if (this.Target!=string.Empty){oDIT.Attributes["Target"] = this.Target;}
return oDO;
}
private WebControl _RenderAsItem(){
Panel oDO = new Panel();
oDO.CssClass = this.ParentMenu.CssClassItem;
Label oLabel = new Label();oLabel.Text = this.Label;
oLabel.CssClass = this.ParentMenu.CssClassItemLabel;
if (this.ParentMenu.ShowItemImages){
Image oImg = new Image();
if (this.ParentMenu.ItemImageAlignment == eAlign.Top){
oDO.Controls.Add(oImg);
oDO.Controls.Add(new LiteralControl("<br/>"));
oDO.Controls.Add(oLabel);
}else if (this.ParentMenu.ItemImageAlignment == eAlign.Right){
oDO.Controls.Add(oLabel);
oDO.Controls.Add(oImg);
}
else if (this.ParentMenu.ItemImageAlignment == eAlign.Left){
oDO.Controls.Add(oImg);
oDO.Controls.Add(oLabel);
}
else if (this.ParentMenu.ItemImageAlignment == eAlign.Bottom){
oDO.Controls.Add(oLabel);
oDO.Controls.Add(new LiteralControl("<br/>"));
oDO.Controls.Add(oImg);
}
if (this.ImgUrl!=string.Empty){oImg.ImageUrl=this.ImgUrl;}
oImg.CssClass = this.ParentMenu.CssClassItemImage + " " + "HOVER";
}else{
oDO.Controls.Add(oLabel);
}
if (this._ToolTip != string.Empty){
oDO.ToolTip = _ToolTip;
oDO.Style["CURSOR"]="hand";
}
if (this.Url!=string.Empty){oDO.Attributes["Url"] = this.Url;}
if (this.Target!=string.Empty){oDO.Attributes["Target"] = this.Target;}
return oDO;
}
}
public enum eAlign{
Top,
Right,
Bottom,
Left
}
/// <summary>
/// Description résumée de XOutlookBar.
/// </summary>
[ParseChildren(ChildrenAsProperties = false)]
[PersistChildren(true)]
[ControlBuilderAttribute(typeof(MyBuilder))]
public class XOutlookBar : WebControl {
//==========================================================
//EVENT HANDLING
//==========================================================
//==========================================================
//SUB ELEMENTS
//==========================================================
//==========================================================
//FIELDS
//==========================================================
//Javascript:
private const string _JSClassDefName = "XOutlookBar";
private const string _JSClassDefFileName = _JSClassDefName + ".js";
private string _JSClassDefPath = "XAct.Resources.aspx?Assembly="+
System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Assembly.GetNa
me().Name + "&" + "Res=";
static int _JSClassInstanceCounter = 0;
private string _JSClassInstanceName = string.Empty;
//----------------------------------------------------------
//Css:
private string _CssPath = "";
private string _CssClassFolder = "XOB_FOLDER";
private string _CssClassFolderButton = "XOB_FOLDER_BUTTON";
private string _CssClassFolderButtonLabel = "XOB_FOLDER_BUTTON_LABEL";
private string _CssClassFolderButtonImage = "XOB_FOLDER_BUTTON_IMAGE";
private string _CssClassFolderItemArea = "XOB_FOLDER_ITEMAREA";
private string _CssClassItem = "XOB_ITEM";
private string _CssClassItemImage = "XOB_ITEM_IMAGE";
private string _CssClassItemLabel = "XOB_ITEM_LABEL";
//----------------------------------------------------------
//Layout:
private bool _ShowFolderImages = true;
private bool _ShowItemImages = true;
private eAlign _FolderImageAlignment = eAlign.Right;
private eAlign _ItemImageAlignment = eAlign.Top;
//----------------------------------------------------------
//Folder Tracking:
private int _CurrentFolderID = 0;
//==========================================================
//PROPERTIES
//==========================================================
//----------------------------------------------------------
[Category(" XAct Behavior")]
public int CurrentFolderID {get {return _CurrentFolderID;}set
{_CurrentFolderID = value;}}
//----------------------------------------------------------
[Category(" XAct Appearance")]
public bool ShowFolderImages {get {return _ShowFolderImages;}set
{_ShowFolderImages = value;}}
[Category(" XAct Appearance")]
public bool ShowItemImages {get {return _ShowItemImages;}set
{_ShowItemImages = value;}}
[Category(" XAct Appearance")]
public eAlign FolderImageAlignment {get {return
_FolderImageAlignment;}set{_FolderImageAlignment = value;}}
[Category(" XAct Appearance")]
public eAlign ItemImageAlignment {get {return
_ItemImageAlignment;}set{_ItemImageAlignment = value;}}
//----------------------------------------------------------
[Category(" XAct Appearance - CSS")]
public string CssPath {get {return _CssPath;}set{_CssPath = value;}}
[Category(" XAct Appearance - CSS")]
public string CssClassFolder {get {return
_CssClassFolder;}set{_CssClassFolder = value;}}
[Category(" XAct Appearance - CSS")]
public string CssClassFolderButton {get {return
_CssClassFolderButton;}set{_CssClassFolderButton = value;}}
[Category(" XAct Appearance - CSS")]
public string CssClassFolderButtonLabel {get {return
_CssClassFolderButtonLabel;}set{_CssClassFolderButtonLabel = value;}}
[Category(" XAct Appearance - CSS")]
public string CssClassFolderButtonImage {get {return
_CssClassFolderButtonImage;}set{_CssClassFolderButtonImage = value;}}
[Category(" XAct Appearance - CSS")]
public string CssClassFolderItemArea {get {return
_CssClassFolderItemArea;}set{_CssClassFolderItemArea = value;}}
[Category(" XAct Appearance - CSS")]
public string CssClassItem {get {return _CssClassItem;}set{_CssClassItem =
value;}}
[Category(" XAct Appearance - CSS")]
public string CssClassItemImage {get {return
_CssClassItemImage;}set{_CssClassItemImage = value;}}
[Category(" XAct Appearance - CSS")]
public string CssClassItemLabel {get {return
_CssClassItemLabel;}set{_CssClassItemLabel = value;}}
//----------------------------------------------------------
//[TypeConverter(GetType(System.Drawing.ColorConverter))]
//==========================================================
//CONSTRUCTOR
//==========================================================
public XOutlookBar():base(HtmlTextWriterTag.Div){
ResSrvHandler.Install();
//Init Js:
_JSClassInstanceName = _JSClassDefName +
_JSClassInstanceCounter;_JSClassInstanceCounter +=1;
//Define path to Css Resource:
if (_CssPath == string.Empty){_CssPath = _JSClassDefPath +
_JSClassDefName+".css";}
//Wire up Handlers for Control Events:
this.PreRender += new System.EventHandler(this.Page_PreRender);
//Create Controls:
this.EnsureChildControls();
}
//==========================================================
//LIFECYCLE
//==========================================================
protected override void AddParsedSubObject(Object obj) {
if (obj is XOutlookBarItem){
//Add to Controls only if of the right type:
XOutlookBarItem oNode = (XOutlookBarItem)obj;
//This time I added them directly...no private _List . Seems to work fine.
this.Controls.Add(oNode);
}else{
//Ignore and discard...
}
}
protected override void CreateChildControls(){
//No Controls to add --- it's all done by AddParsedSubObject
ChildControlsCreated=true;
}
private void Page_PreRender(object sender, EventArgs e){
foreach (XOutlookBarItem oChild in this.Controls){
//Wire it up -- the sub items will need a pointer to me to get CSS layout
info
oChild.ParentMenu = this;
}
_Embed_JS();
_Embed_CSS();
}
protected override void Render(HtmlTextWriter output) {
if ((this.Site != null) &&
(this.Site.DesignMode)){try{this.OnPreRender(EventArgs.Empty);}catch{}}
base.Render(output);
}
//==========================================================
//PRIVATE METHODS
//==========================================================
private void _Embed_JS(){
Tools.EmbedScriptClassDefResource(false,this.Page,_JSClassDefName,
_JSClassDefFileName, _JSClassDefPath);
string tJS_Specific =
string.Format(
"<script>\n"+
"//CLASSINIT:BEGIN----------------------------------------------------------
-\n"+
_JSClassInstanceName + " = new
"+_JSClassDefName+"('{0}','{1}','{2}','{3}','{4}','{5}','{6}','{7}','{8}','{
9}','{10}');\n"+
"//CLASSINIT:END------------------------------------------------------------
-\n"+
"</script>\n",
this.ClientID,
_CurrentFolderID,
string.Empty,
this._CssClassFolder,
this._CssClassFolderButton,
this._CssClassFolderButtonLabel,
this._CssClassFolderButtonImage,
this._CssClassFolderItemArea,
this._CssClassItem,
this._CssClassItemLabel,
this._CssClassItemImage
);
Page.RegisterStartupScript(_JSClassInstanceName, tJS_Specific);
}
private void _Embed_CSS(){
if (!Page.IsStartupScriptRegistered(_JSClassDefName + "_CSS")){
string tCSS = string.Format("<link type=\"text/css\" rel=\"StyleSheet\"
href=\"{0}\"/>\n", _CssPath);
Page.RegisterStartupScript(_JSClassDefName + "_CSS", tCSS);
}
}
}