Why would we want to change the output of ASP.NET server controls?
When writing either a custom web control derived from an existing control or a control adapter, it is not uncommon that we need to modify the html output of that is normally generated only slightly, without having to reinvent the entire wheel. In fact, saying that this “is not uncommon” is a bit of an understatement. If you’ve seen many of our recent development posts on this blog you will notice that we spend a fair amount of time trying to manipulate the output of asp.net. As user interfaces gain complexity and other client side frameworks enter the picture, we consistently find ourselves less than satisfied with the standard output offered by ASP.NET server controls. Recoding all of the output can be a risky proposition because the original microsoft rendering code often contains conditionals that account for various scenarios, browsers and property settings. In the case of control adapters, the ability to use virtual methods with inheritance to override the rendering of particular attributes or elements is somewhat limited. This might leave the developer somewhat stuck with needing to re-invent the wheel with their control adapter. I’ll go over a simple, general technique for those, “I want to produce the exact same html except…” situations. Keep reading to see a neat little short cut to this tak much easier, and no…we’re not talking about doing string replaces on the output stream!
How to Make Small Modifications to the XHtml Generated from Controls Using a StringWriter and XmlDocument
Say you’re writing a control or a control adapter and you want to just simply include one extra attribute, or maybe an extra nested element (e.g. a div or a span with a css class on it), ideally there would be something analogous to virtual methods and inheritance where you can say, “do the base method, except here, where I’ll override what is normally done, etc.”
The scenario of inheriting from a Control gives you some flexibility because .net exposes some of the modular methods for rendering out the control to be overridden, such as RenderBeginTag, RenderChildren. ControlAdapters are a lot more sparse, you mostly just have a base call to render out how the control would be rendered out if the base control adapter was being used to render out the control (with the base action frequently being to render out the control as if no control adapter was being applied).
On possible solution to all of this is to just create a temporary HtmlTextWriter wrapping a StringWriter and do a base call to Render passing that writer in. You can now get what the original xhtml would have been by calling .ToString() on the inner StringWriter.
To work with the generated html, while there isn’t an HtmlTextReader, keep in mind that since the controls should be generating xhtml, that should mean the text will be well-formed xml, so you can XmlDocument (or an XmlTextReader) to navigate through the xhtml generated and mindlessly rewrite it to the original HtmlTextWriter with the ‘except’ cases written in.
One possible exception to the generated text being xml that I could imagine is if there is a control that have an enclosing element. In that case you would want your adapter to just prepend and append custom opening and closing tags around the base xhtml and then, when doing the re-writing of the original xhtml, just don’t write the outermost element.
Below is an illustrative example where I write a control adapter for drop down lists that for each option in the list, encloses the text with a span element that has a class attribute of “my-span”.
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5: using System.Web.UI.Adapters;
6: using System.IO;
7: using System.Web.UI;
8: using System.Xml;
10: namespace DelphicSage.TestAdapters
12: public class DropDownListAdapter : ControlAdapter
14: protected override void Render(HtmlTextWriter writer)
16: StringWriter baseStringWriter = new StringWriter();
17: HtmlTextWriter baseWriter = new HtmlTextWriter(baseStringWriter);
18: baseWriter.NewLine = writer.NewLine;
19: baseWriter.Indent = writer.Indent;
20: // baseWriter.FormatProvider = writer.FormatProvider;
21: // baseWriter.Encoding = writer.Encoding;
26: string baseHtml = baseStringWriter.ToString();
27: if (!string.IsNullOrEmpty(baseHtml))
29: XmlDocument originalXhtml = new XmlDocument();
30: originalXhtml.PreserveWhitespace = true;
32: this.WriteNode(writer, originalXhtml);
36: protected virtual void WriteNode(HtmlTextWriter writer, XmlNode xhtmlNode)
38: switch (xhtmlNode.NodeType)
40: case XmlNodeType.Text:
41: WriteTextNode(writer, xhtmlNode);
43: case XmlNodeType.Whitespace:
44: WriteWhitespaceNode(writer, xhtmlNode);
46: case XmlNodeType.Element:
47: WriteElementNode(writer, xhtmlNode);
50: WriteUnknownNode(writer, xhtmlNode);
56: private void WriteUnknownNode(HtmlTextWriter writer, XmlNode xhtmlNode)
58: WriteChildNodes(writer, xhtmlNode);
61: protected virtual void WriteTextNode(HtmlTextWriter writer, XmlNode xhtmlNode)
66: protected virtual void WriteWhitespaceNode(HtmlTextWriter writer, XmlNode xhtmlNode)
69: WriteChildNodes(writer, xhtmlNode);
72: protected virtual void WriteChildNodes(HtmlTextWriter writer, XmlNode xhtmlNode)
74: if (xhtmlNode.HasChildNodes)
76: foreach (XmlNode childNode in xhtmlNode.ChildNodes)
78: WriteNode(writer, childNode);
83: protected virtual void WriteElementNode(HtmlTextWriter writer, XmlNode xhtmlNode)
87: WriteAttributes(writer, xhtmlNode);
89: if (xhtmlNode.Name.ToLower() == "option")
93: writer.WriteAttribute("class", "my-span");
101: if (xhtmlNode.HasChildNodes)
104: WriteChildNodes(writer, xhtmlNode);
110: writer.Write(" />");
115: protected virtual void WriteAttributes(HtmlTextWriter writer, XmlNode xhtmlNode)
117: if (xhtmlNode.Attributes != null)
119: foreach (XmlAttribute attribute in xhtmlNode.Attributes)
121: // the base xHtml seems to generate Attributes as
122: // attribute_name='foo("bar");'
123: // but the HtmlTextWriter.WriterAttribute spits out attribute_name="foo"
124: // so we'll switch the double-quotes (") in the attribute's original value to
125: // to a single quote ('), to get something more like
126: // attribute_name="foo('bar');"
127: writer.WriteAttribute(attribute.Name, attribute.Value.Replace(""", "'"));
As you can see, it is much easier to parse and manipulate an XMLDocument object than to mess around with a string.