06.29
Have you ever had to create a large web form for users to fill out and then receive an email copy after its submitted? That can be tedious work. The first few times I did it, I used a StringBuilder to build the email HTML one control at a time. Later, I viewed the HTML output of the page and replaced all input controls with spans, and then put that HTML in a StringBuilder. Either of these methods work, but it gets real annoying when I later have to add a field or two to the form and therefore to the email HTML.
I knew there had to be a way to do this programmatically without copying and pasting into a StringBuilder. Well, there is. Here’s a rather common code snippet that does just this:
public static string GetRenderedHtml(this Control control) { StringBuilder sbHtml = new StringBuilder(); using (StringWriter stringWriter = new StringWriter(sbHtml)) using (HtmlTextWriter textWriter = new HtmlTextWriter(stringWriter)) { control.RenderControl(textWriter); } return sbHtml.ToString(); }
This is great! Let’s try it out on this simple example:
<div id="divForm" runat="server"> <fieldset class="inputArea"> <legend>Contact</legend> <asp:Label runat="server" AssociatedControlID="txtName"> Name</asp:Label> <asp:TextBox runat="server" ID="txtName" /> <asp:Label runat="server" AssociatedControlID="txtEmail"> Email</asp:Label> <asp:TextBox runat="server" ID="txtEmail" /> <asp:Label runat="server" AssociatedControlID="txtWebsite"> Website</asp:Label> <asp:TextBox runat="server" ID="txtWebsite" /> <asp:Label runat="server" AssociatedControlID="txtComment"> Comment</asp:Label> <asp:TextBox runat="server" ID="txtComment" TextMode="MultiLine" Rows="4" cols="30" /> <asp:Button ID="btnSubmit" runat="server" Text="Submit" OnClick="btnSubmit_Click" /> </fieldset> </div>
protected void btnSubmit_Click(object sender, EventArgs e) { txtRenderedHtml.Text = divForm.GetRenderedHtml(); }
Here is what we get:
Control 'txtName' of type 'TextBox' must be placed inside a form tag with runat=server.
So how do you get around that? Well, lets think about this. I’m trying to capture a form and render it as HTML to be included in an email, so I don’t want any TextBoxes. Lets replace the TextBoxes (and any other editable controls) with Labels and try again.
public static void ReplaceEditableControls(this Control control) { // don't bother with controls that aren't visible if (!control.Visible) { return; } ListControl listControl = control as ListControl; IButtonControl buttonControl = control as IButtonControl; IValidator validator = control as IValidator; IEditableTextControl textControl = control as IEditableTextControl; UpdatePanel updatePanel = control as UpdatePanel; if (validator != null || buttonControl != null) { control.Visible = false; } else if (listControl != null && listControl.SelectedItem != null) { Label label = new Label {Text = listControl.SelectedItem.Text, CssClass = "text"}; Replace(listControl, label); } else if (textControl != null) { Label label = new Label {Text = textControl.Text, CssClass = "text"}; Replace((Control) textControl, label); } else if (updatePanel != null) { // replace the update panel with a place holder PlaceHolder holder = new PlaceHolder(); Control[] panelControls = new Control[updatePanel.ContentTemplateContainer.Controls.Count]; updatePanel.ContentTemplateContainer.Controls.CopyTo(panelControls, 0); foreach (Control panelControl in panelControls) { holder.Controls.Add(panelControl); } ReplaceEditableControls(holder); Replace(updatePanel, holder); } else if (control.HasControls()) { Control[] controlsCopy = new Control[control.Controls.Count]; control.Controls.CopyTo(controlsCopy, 0); foreach (Control controlCopy in controlsCopy) { ReplaceEditableControls(controlCopy); } } }
There are a few things to note here.
-
The check for ListControl is before IEditableTextControl because of the way it implements IEditableTextControl. ListControl.Text returns ListControl.SelectedValue, but ListControl.SelectedItem.Text makes more sense.
-
UpdatePanels are a special case because of ContentTemplate. They are replaced with a PlaceHolder and then the method is recursively called on each child control.
-
Finally, if the control has a control collection of its own, a recursive call is made on each child control.
-
Notice that the control collection is copied to an array before making the recursive call. This is because the control collection is modified and you can’t modify a collection while iterating it. Well, you can, but you will have problems.
Now we can change the button handler to:
protected void btnSubmit_Click(object sender, EventArgs e) { divForm.ReplaceEditableControls(); }
Which will render the following HTML:
<div id="divForm"> <fieldset class="inputArea"> <legend>Contact</legend> <label for="txtName"> Name</label> <span id="txtName" class="text">John Rummell</span> <label for="txtEmail"> Email</label> <span id="txtEmail" class="text">jrummell@example.com</span> <label for="txtWebsite"> Website</label> <span id="txtWebsite" class="text">john.rummell.info</span> <label for="txtComment"> Comment</label> <span id="txtComment" class="text">Check out this new post!</span> </fieldset> </div>
To capture this as a string, just add a call to GetRenderedHtml:
protected void btnSubmit_Click(object sender, EventArgs e) { divForm.ReplaceEditableControls(); string html = divForm.GetRenderedHtml(); //TODO: send email }
(The form style is a slight variation of Janko’s tutorial)
[...] you’re using WebForms, I would recommend rendering your log sheet in a Control as a string, and then attaching that to a [...]