Introduction
When creating long forms for websites, I often find it is useful to have sections of the form that can be expanded/contracted or just plain closed when done. With this in mind, I created a baseControl
that can be inherited by my Web Custom Controls that will allow each to have the functionality to be expanded and contracted, as well as some other functions. This all worked perfectly, but did mean you had to create all your controls in code. I don't really see this as a problem, but some people did point out that it might be nice to be able to drag the baseControl
onto a web form and then dump other controls into it. So, this is what I did.
The control is basically my own version of a panel, I'll call it basePanel
. This panel has a title bar which holds the title, an Expand or Contract button, and a Close button. There is then a main DIV
area that any control can be placed into. Then there is a footer which holds a Cancel button and a Save button. All of these things are controllable via properties, so you can make the resulting control fit a good range of functional uses.
Here are some examples:
Example 1
This first example shows three basePanel
controls. As you can see, the header and footer sections are in grey. I've chosen not to have a Close button on display, but the Cancel and Save buttons are visible in the open controls footer.
Example 2
This example shows the basePanel
control without the header or footer sections. This is actually an example of the inherited version that allows us to perform several actions against one business object. In this example, the control acts as a Search, Create, and Display/Select control.
Example 3
This example shows the basic control with everything left on. You can see the addition of the Close button. In this example, I had dropped a button control onto my basePanel
to check that event handling was working for child controls.
Example 4
I've highlighted the control here in red as it's pretty inconspicuous. This is actually the same control as in Example 2, but this time it is in Display/Select mode. This example really shows the versatility of the multimode control; every time I need to do any function on that specific business object, I can use the same custom Web Control.
Now, I will go through how the basePanel
works. Next time I will explain the multimode control.
basePanel
Before I begin, let me plead my case for doing this. I'm 100% sure that there is a much easier way of achieving the results, but after three days of looking, I could not find out how to do this. Unfortunately, the new help functionality on VS2005 had just been installed, and it now appears that finding answers is the sole domain of Google. Shame really, help in 2003 was kind of helpful. If anyone knows a simple way to create a custom control that you can drop other controls into at design time, please let me know.
The first problem I had with creating a control that I could drop other controls into was how to tell the IDE that this was allowed for that control. Eventually, I found what I needed.
[ToolboxData("<{0}:basePanel runat="server">")]
[Designer("System.Web.UI.Design.ReadWriteControlDesigner, System.Design")]
[PersistChildren(true)]
[ParseChildren(false)]
The [ToolboxData]
attribute just lets it be shown in the toolbox, and sets the default naming when dragged onto a page. The real work happens with the other three attributes. Personally, I find it really difficult to get good information about what attribute tags are out there and what they do.
So the attributes solved the problem of not being able to nest controls inside my existing control. The next problem was that if you rendered the control, all the nested controls appeared outside of the basePanel
. This was fixed by some fairly major overriding of the Render
method.
protected override void Render(System.Web.UI.HtmlTextWriter writer)
{
if (HasControls())
{
System.IO.MemoryStream memstr = new System.IO.MemoryStream();
System.IO.StreamWriter stream = new System.IO.StreamWriter(memstr);
HtmlTextWriter wt = new HtmlTextWriter(stream);
for (int i = 0; i < 2; i++)
{
this.Controls[i].RenderControl(wt);
wt.Flush();
}
memstr.Seek(0, System.IO.SeekOrigin.Begin);
System.IO.StreamReader rd = new System.IO.StreamReader(memstr);
char[] bytes = new char[memstr.Length];
rd.ReadBlock(bytes, 0, (int)memstr.Length);
System.Text.StringBuilder sb = new System.Text.StringBuilder();
sb.EnsureCapacity((int)memstr.Length);
sb.Append(bytes);
memstr.Close();
stream.Close();
rd.Close();
wt.Close();
memstr = new System.IO.MemoryStream();
stream = new System.IO.StreamWriter(memstr);
wt = new HtmlTextWriter(stream);
for (int i = 2; i < Controls.Count; i++)
{
this.Controls[i].RenderControl(wt);
wt.Flush();
}
memstr.Seek(0, System.IO.SeekOrigin.Begin);
rd = new System.IO.StreamReader(memstr);
bytes = new char[memstr.Length];
rd.ReadBlock(bytes, 0, (int)memstr.Length);
System.Text.StringBuilder sbChild = new System.Text.StringBuilder();
sbChild.EnsureCapacity((int)memstr.Length);
sbChild.Append(bytes);
memstr.Close();
stream.Close();
rd.Close();
wt.Close();
sb.Replace("< div class="mainDiv" >",
"< div class="mainDiv" >" + sbChild.ToString());
writer.WriteLine(sb.ToString());
}
}
As you can see, the code above basically renders the first two controls (root control and my custom panel) and puts them in a StringBuilder
(sb
). It then renders everything after these controls into another StringBuilder
(sbChild
). Then, at the very end, there is a replace done on the mainDiv
class to append all the nested controls into the centre of the panel. It's not elegant, but it works.
I'm hoping the rest of the custom control will make sense to anyone who has created a custom control before. If you haven't and you get stuck, please let me know.