ASP.NET server controls are great. They allow for building reusable components, and are easy to distribute because they are entirely contained in an assembly (no .ascx files needed). One of the drawbacks of server controls is the lack of a designer/HTML editor for creating your layout. The HTML is generated in the code and generally looks messy. It can also be a headache to tweak/debug because your controls are being added in a mess of LiteralControls for the HTML.
Simplify the construction of HTML inside ASP.NET server controls using ControlInjector
Introduction:
ASP.NET
server controls are great. They allow for building reusable components,
and are easy to distribute because they are entirely contained in an
assembly (no .ascx files needed). One of the drawbacks of server
controls is the lack of a designer/HTML editor for creating your
layout. The HTML is generated in the code and generally looks messy. It
can also be a headache to tweak/debug because your controls are being
added in a mess of LiteralControls for the HTML.
Approach:
After
creating a server control with some very complex HTML, I realized that
the method of mixing HTML literal content with my private controls is
not ideal. What if there was a way to accomplish something similar to
String.Format()? We could parse a string and look for blocks like {xxx}
and inject our control there... all HTML content before and after those
blocks would be added to the Control collection as LiteralContent.
Doing
this will allow us to separate layout from content, and we can easily
use a designer to construct and view the HTML, then copy it into a
string to place in the code.
The Implementation
The HTML can
be stored as a single string, and with C#, we can use string literals
@"string" to allow for multi-line strings without any concatenation
code. It will be relatively simple to cut-n-paste from another
designer. We will need to parse the HTML string and have placeholders
for each control we want to inject. I chose to use "{controlID}" as the
placeholder text. Some might argue that a use of {0} {1} would fit the
String.Format pattern a little better, but I chose to be more verbose
here because clarity of code is what I am going for here.
Our
private controls will need to be contained inside of a hashtable. We
can use the control ID as the key, so it will be easy to work with.
Update
-- I decided to follow some advice and keep the Hashtable internal to
the class. The ControlInjector will no longer hold static methods, so
you will need to instantiate it before use. I like this design a lot
better.
We will parse the HTML string, find the indices of the {
and }, add the text to the left as a literal control, replace the {xxx}
text with the actual control, and continue. The end result will be one
single control that the client code can add to the control tree.
The Code
The ControlInjector class contains three useful methods:
public void AddControl(Control c) { //make sure that the ID is valid if(c.ID == null || c.ID == "") throw new ArgumentException("Control" + c.ToString() + " must have a valid ID", "c"); //add it to our private hashtable _hashControls.Add(c.ID, c); } public Control Inject(string html) { //create a container for our controls PlaceHolder phContent = new PlaceHolder(); //loop while we still have content to render while( html.Length > 0 ) { //find the next { character int iLBrace = html.IndexOf("{", 0); int iRBrace = html.IndexOf("}", 0); //make sure the order is correct if( iRBrace < iLBrace ) throw new FormatException("your html string " + "is not properly formatted -- a } was found before a {"); //process control (if any) if( iLBrace >= 0 ) { //add the literal content first phContent.Controls.Add( new LiteralControl( html.Substring(0, iLBrace) ) ); //get the inner content of the { } string controlID = html.Substring(iLBrace+1, iRBrace-iLBrace-1); //make sure that the control exists in the hash table if( ! _hashControls.ContainsKey(controlID) ) throw new ArgumentNullException("hashControls[" + controlID + "]", "hashControls must contain the control to add"); //add the control phContent.Controls.Add(_hashControls[controlID] as Control); //strip the part of the string that we have dealt with html = html.Substring(iRBrace + 1); } else { //not more controls, just add the rest //of the html and break out of the loop phContent.Controls.Add( new LiteralControl( html ) ); break; } } return phContent; } |
Update --
After using this utility for a while, I realized quickly that I was
still not getting any kind of designer support, and for large, complex
controls, it was still a bit unreadable. I wrote a method to allow for
getting the HTML string from an embedded resource. This way I can
create an HTML page inside my class library, set it as an embedded
resource and retain HTML designer support. Still, the designer support
is limited - you cannot design the individual controls, only the
layout. Another drawback is that I cannot edit the inner HTML of one of
the controls, I must use separate HTML files for this.
The best
part about this is that the HTML file is included in your DLL, so you
do not introduce any file dependencies at all, which is ideal.
public Control InjectFromResource(string resourceName) { //get assembly from the code that called us System.Reflection.Assembly dll = System.Reflection.Assembly.GetCallingAssembly(); //make sure resource exist in the assembly string[] resourceNames = dll.GetManifestResourceNames(); bool found=false; foreach(string rName in resourceNames) { if(rName == resourceName) { found = true; break; //no need to continue searching } } if(!found) throw new ArgumentException("The resource did not exist" + " in this assembly. Resource name:" + resourceName, "resourceName"); //get the resource from the dll System.IO.Stream streamResource = dll.GetManifestResourceStream(resourceName); //read the resource data into a byte array, then convert to string byte[] resourceData = new byte[streamResource.Length]; streamResource.Read(resourceData, 0, (int)streamResource.Length); System.Text.ASCIIEncoding enc = new System.Text.ASCIIEncoding(); string html = enc.GetString(resourceData, 0, resourceData.Length); //now pass this string to the original method to parse return Inject(html); } |
Nothing really
exciting here, the code uses reflection to get the current assembly
object, then checks to see if the resource exists. The only requirement
is that the caller assembly contains the embedded resource and they
supply the fully qualified name of the resource.
The Advantage:
What does this code give you? Well, here's a snippet from a server control with mixed HTML and controls:
protected override void CreateChildControls() { base.CreateChildControls (); label1 = new Label(); label1.ID = "label1"; label1.Text = "i am a label!"; textBox1 = new TextBox(); textBox1.ID = "textBox1"; textBox1.Text = "i am a textbox!"; dropdown1 = new DropDownList(); dropdown1.ID = "dropdown1"; dropdown1.Items.Add ( new ListItem("i am a dropdown", "") ); string html = @" <table border=2> <tr> <td align=center colspan=2>Title!</td> </tr> <tr> <td>label</td> <td>{label1}</td> </tr> <tr> <td>textBox</td> <td>{textBox1}<td> </tr> <tr> <td>dropdown1</td> <td>{dropdown1}</td> </tr> </table>"; using(ControlInjector injector = new ControlInjector()) { injector.AddControl(dropdown1); injector.AddControl(textBox1); injector.AddControl(label1); this.Controls.Add( injector.Inject(html) ); } } |
To demonstrate the
resource example, go to Visual Studio and add an HTML page to your
webcontrols project. Design the above HTML using the Visual Studio HTML
editor. Make sure and right-click the file in Solution Explorer and
choose Embedded Resource. If you don't do this, the file will not be
found at runtime. Once you have done this, the above method can be
reduced to the following:
protected override void CreateChildControls() { base.CreateChildControls (); label1 = new Label(); label1.ID = "label1"; label1.Text = "i am a label!"; textBox1 = new TextBox(); textBox1.ID = "textBox1"; textBox1.Text = "i am a textbox!"; dropdown1 = new DropDownList(); dropdown1.ID = "dropdown1"; dropdown1.Items.Add ( new ListItem("i am a dropdown", "") ); using(ControlInjector injector = new ControlInjector()) { injector.AddControl(dropdown1); injector.AddControl(textBox1); injector.AddControl(label1); this.Controls.Add( injector.InjectFromResource("MyWeb" + "Controls.MyControl.html" ) ); } } |
This will get the HTML from the resource located in the MyWebControls namespace, entitled MyControl.html.
Without the injector, we would have something like this:
Controls.Add( new LiteralControl("<table><tr><td>label1</td><td>") ); Controls.Add( label1 ); Controls.Add( new LiteralControl("</td></tr><tr><td>textbox1</td></tr>") ); Controls.Add( textbox1 ); |
... you get the idea...
multiply this mess by 100 lines of HTML mixed with controls and it's
not very easy to see what's going on.
What's next?
This
method has very minimal error checking, and that would be the first
place I would expand, but it is out of the scope of this article. (I
wanted to keep it clean and easy to understand.)
The next thing
I would like to add would be the ability to nest content inside of the
controls, something like {control1}nested html{/control1}, but that
introduces a large level of complexity to the parser, so I haven't
decided if the benefit will be worth it.
Conclusion
Hopefully
this will be of some use, and I am sure that there are other methods of
accomplishing the same task. I welcome any comments or criticisms.
Updates
*
June 30, 2005 - I removed the static methods in favor of an
instantiated use of the control. This allowed me to contain the
hashtable as an internal reference only, making the calling code a lot
simpler. I also added a method to read from an embedded resource to get
the HTML string. These two additions make the class 100% more readable
in my server controls. I hope it helps you as well!
About Subdigital:
Ben Scheirman is a professional web developer providing enterprise level ASP.NET solutions.
He
started programming in QBASIC in 1993, learning from examples
downloaded online. He migrated to Pascal in 1996, C/C++ ruled his spare
time during the late 90's, and for the past 2 years he has been
developing using C# & VB.NET.
He enjoys programming in his spare time, especially toying around with Managed DirectX.
Read his blog here