To read the article online, visit http://www.4GuysFromRolla.com/articles/102203-1.aspx

Enhancing the 'Email the Rendered Output of an ASP.NET Web Control' Code

By Scott Mitchell


A Follow-Up Article is Available!
If you try out the code discussed in this article in an ASP.NET 2.0 application, you may wind up with an exception that reads: "RegisterForEventValidation can only be called during Render();" If so, check out Emailing the Rendered Output of an ASP.NET Web Control in ASP.NET 2.0 for a discussion on why this error arises along with a couple of workarounds.

Introduction


In a previous article by yours truly, Emailing the Rendered Output of an ASP.NET Web Control, I demonstrated how, with a few lines of code, one could provide functionality in an ASP.NET Web page to email the contents of a Web control, such as a DataGrid. This functionality would provide a Web site the capability to offer its end users useful features such as "Email Yourself Your Invoice," or "Email Me the Results of my Search." The image below shows an email containing a DataGrid Web control sent from the code examined in the previous article:

Screenshot of the output of an emailed DataGrid.

While this technique and code works well for DataGrids that just contain BoundColumns or TemplateColumns without Buttons or LinkButtons, it breaks when the DataGrid is configured for sorting or paging or contains ButtonColumn, EditCommandColumn, or TemplateColumns with Buttons or LinkButtons. We'll discuss the reasons why in detail in this article, as well as look at a couple of workarounds. If you have yet to read Emailing the Rendered Output of an ASP.NET Web Control please take a moment to do so before continuing on with this article.

First Things First - A Bit About ASP.NET Web Controls


Realize that all ASP.NET controls are derived, either directly or indirectly, from the System.Web.UI.Control class. This class provides all of the base methods and properties needed to instantiate, initialize, and render a control. Methods exist to maintain state across postbacks, to detect changes in state across postbacks in order to raise events, and to determine if a client-side action should trigger an event upon postback.

Realize too that each control can contain a collection of children controls. This relationship is recursive, since a given control's children controls can contain children controls themselves. This complete family of controls can be visually displayed as a hierarchy, as shown in the image to the right. (The image to the right shows what a control hierarchy would look like for a page with a Label Web control following by a server-side form (<form runat="server">). Inside the server-side form there's a Label, a TextBox, a DropDownList, and a Button Web control. The DropDownList has three ListItems.) This hierarchy is often referred to as the control hierarchy. For an ASP.NET Web page, the System.Web.UI.Page class is the control at the top of the hierarchy. All Web controls defined in the HTML portion of a page are recursively added to the top-level Page class instance to form a suitable control hierarchy. Each time an ASP.NET Web page is requested, this control hierarchy is recreated.

All Web controls have a life-cycle, a series of steps they go through when an ASP.NET Web page is requested. This includes loading property values that are declared in the Web control's declarative syntax, tracking programmatic changes to the property values, loading the property values from the previous postback (if the page has been posted back), firing any needed server-side events, and so on. One of the final stages in the life-cycle is rendering. Rendering is the process of producing the HTML markup for the Web control.

Rendering begins by having the Page class's RenderControl() method called. This method accepts a single parameter, an HtmlTextWriter instance. RenderControl() recursively calls the RenderControl() method of all of the controls in the control hierarchy whose Visible property is set to True (the default), passing around the HtmlTextWriter instance. Each control in the hierarchy adds its markup to the end of the HtmlTextWriter instance. After all controls' RenderControl() method has been called, the Page class returns the HtmlTextWriter instance, which contains the full HTML markup for the page.

The way the code worked in Emailing the Rendered Output of an ASP.NET Web Control was by creating a new HtmlTextWriter instance and calling the RenderControl() method of the control whose contents we wanted to email. This caused the HTML markup for the control to be squirted into the HtmlTextWriter, which we could then snake out and append to the body of an HTML-formatted email. The actual code that accomplished this was the following five lines:

Dim SB as New StringBuilder()    
Dim SW as New StringWriter(SB)
Dim htmlTW as New HtmlTextWriter(SW)    

dgPopularFAQs.RenderControl(htmlTW)
 
Dim dataGridHTML as String = SB.ToString()

Why Programmatically Rendering a DataGrid with a ButtonColumn Raises an Exception


The code in Emailing the Rendered Output of an ASP.NET Web Control works flawlessly with Web controls that do not need to be placed within a server-side form. However, there are a set of Web controls that do need to be placed in a server-side form, specifically those that cause postbacks. For example, the Button Web control renders as a submit button that, when clicked, posts back the server-side form. Hence, a Button control must be placed within a server-side form. Similarly, controls like LinkButtons, ImageButtons, and so on, must also be in server-side forms.

If you take a moment to try to create an ASP.NET Web page with a Button Web control not in a server-side form you'll see that you get an error stating, "The Button 'ButtonID' must be placed in a form tag with runat=server." This error is raised because in the Render() method of the Button Web control, a call is made to Page.VerifyRenderingInServerForm(). This method checks to ensure that when this control is rendered after a server-side form has begun rendering, but before it has finished rendering. (The server-side form is actually an instance of the System.Web.UI.HtmlControls.HtmlForm class.)

Now, consider the case of a DataGrid with a ButtonColumn (or an EditCommandColumn, or a TemplateColumn with a LinkButton or Button in it...). When we call the DataGrid's RenderControls() method, it recursively iterates through the DataGrid's control hierarchy, calling the RenderControls() method for each of the descendant controls. ButtonColumns are actually implemented as either LinkButton or Button instances, depending on value of the ButtonColumn's ButtonType property. So, when the RenderControls() method gets called for this column's LinkButton, the LinkButton calls Page.VerifyRenderingInServerForm() to ensure that the DataGrid is within a server-side form. This method notes that a server-side form was not rendered prior to this LinkButton being rendered, and therefore throws an exception.

Not surprisingly, if you try to email a DataGrid that contains a control in its hierarchy that needs to be within a server-side form, you'll get the error message "Control 'some ID' of type 'DataGridLinkButton' must be placed inside a form tag with runat=server."

Examining Workarounds


The reason we cannot render the DataGrid without error is because it contains a control in its hierarchy that needs to be rendered in a server-side form, but the server-side form has yet to be rendered. One option is to call the RenderControl() method of the server-side form, rather than of the DataGrid. That is, instead of:

dgPopularFAQs.RenderControl(htmlTW)

We'd do:

myForm.RenderControl(htmlTW)

This approach has a couple of disadvantages. First, the server-side form might contain a number of Web controls in it, but we might only be interested in emailing the rendered contents of one particular control. Second, the Page class only allows a server-side form to be rendered once per page invocation. That is, since we are rendering the server-side form to email the DataGrid, we must make sure to set the form's Visible property to False so that during the Page's rendering stage the form's RenderControls() method is not called again. (The ASP.NET page model only allows for one server-side form per page. Therefore, whenever a server-side form is rendered, a flag is set. If, on the same page, a server-side form is attempted to be rendered again, an exception is thrown with an explanation that there can be only one server-side form on the page.)

To fix these disadvantages, we must do two things in the code that emails the rendered DataGrid. First, we must make sure to set the form's Visible property to False so that it is not re-rendered. (Recall that only those controls in the control hierarchy whose Visible property is set to True are rendered.) Second, we need to set the Visible property to False for all of the controls in the form's controls collection - save the one control we want to email - before we render the form. This will have the effect of only the control we want to email being rendered.

The following code shows how to do this:

<% @Import Namespace="System.Web.Mail" %>
<% @Import Namespace="System.IO" %>
<% @Import Namespace="System.Data" %>
<% @Import Namespace="System.Data.SqlClient" %>
<script language="vb" runat="server">
  ... Page_Load & BindData() methods omitted for brevity ...
  ... Refer to Emailing the Rendered Output of an ASP.NET Web Control for their code ...

  Sub HideAllButXInY(controlToEmail as Control, ssForm as HtmlForm)
    'Set the Visible property to False for all controls in ssForm EXCEPT controlToEmail
    Dim c as Control
    For Each c in ssForm.Controls
      If Not c.Equals(controlToEmail) Then
        c.Visible = False
      End If
    Next
  End Sub
  
  Sub EmailDataGrid(sender as Object, e as EventArgs)
    'Hide those controls you don't want emailed.
    HideAllButXInY(dgPopularFAQs, myForm)
  
    Dim SB as New StringBuilder()    
    Dim SW as New StringWriter(SB)
    Dim htmlTW as New HtmlTextWriter(SW)
    myForm.RenderControl(htmlTW)
    
    beforeEmail.Visible = False
    afterEmail.Visible = True
    lblFinalMessage.Text = "The DataGrid has been emailed to " & txtEmailAddy.Text
       
    Dim dataGridHTML as String = SB.ToString()
    
    'Now, send the email
	'Create an instance of the MailMessage class
	Dim objMM as New MailMessage()

	'Set the properties
    objMM.To = txtEmailAddy.Text
    objMM.From = "mitchell@4guysfromrolla.com"

    'Send the email in text format
    objMM.BodyFormat = MailFormat.Html

    'Set the subject
    objMM.Subject = "DataGrid Emailing"

    'Set the body - use VbCrLf to insert a carriage return
    objMM.Body = "<font face=""Verdana"">This is an example of emailing a " & _
             "DataGrid Web control via an ASP.NET Web page." & dataGridHTML
	
    SmtpMail.Send(objMM)
  End Sub
</script>

<asp:Panel runat="server" id="beforeEmail">
	<form runat="server" id="myForm">
	  <asp:datagrid id="dgPopularFAQs" runat="server"
	        AutoGenerateColumns="False"
	        Font-Name="Verdana"  Width="85%"
	        HorizontalAlign="Center"
	        ItemStyle-Font-Size="9pt"
	        AlternatingItemStyle-BackColor="#dddddd">
	     
	     <HeaderStyle BackColor="Navy" ForeColor="White" Font-Bold="True"
	          Font-Size="13pt" HorizontalAlign="Center" /> 
	       
	     <Columns>
           <asp:EditCommandColumn EditText="Edit" ... />
	       <asp:BoundColumn HeaderText="FAQ ID" DataField="FAQID"
	               ItemStyle-HorizontalAlign="Center" />
	       <asp:BoundColumn HeaderText="Question" DataField="Description" />
	       <asp:BoundColumn HeaderText="Total Views" DataField="ViewCount"
	              DataFormatString="{0:#,###}" ItemStyle-HorizontalAlign="right" />
	     </Columns>
	  </asp:DataGrid>

	  <p>
	  <asp:TextBox id="txtEmailAddy" runat="server" />
	  <asp:Button Text="Email DataGrid" OnClick="EmailDataGrid" runat="server" />
	</form>
</asp:Panel>
<asp:Panel runat="server" id="afterEmail" Visible="False">
	<asp:Label runat="server" id="lblFinalMessage" />
</asp:Panel>

The bold text shows the changes from the code in Emailing the Rendered Output of an ASP.NET Web Control. In the EmailDataGrid() sub we first call HideAllButXInY(), which takes in two controls and sets the Visible property of ever control in the second parameter's Controls collection to False except for the control specified in the first parameter. Therefore, to hide every control in the server-side form but dgPopularFAQs (the DataGrid we want to email), we call: HideAllButXInY(dgPopularFAQs, myForm).

The next change is that we call the server-side form's RenderControl() method (instead of dgPopularFAQs's RenderControl(). Next, we need to make sure that the server-side form is not rendered when the Page instance begins the rendering stage. If it is, an exception will be thrown and the user will see an unsightly error message. This is accomplished by wrapping the form in a Panel control, and setting the Panel's Visible property to False. At the same time, we show a previously hidden Panel (afterEmail), which shows the user a short confirmation message.

The screenshot below shows the email received from the script above. Note that it includes the EditCommandColumn. Of course, clicking the hyperlink in that column won't allow the user to directly edit the page, since the LinkButtons are wired up to client-side JavaScript that is rendered by the server-side form which causes the page to be posted back.

One disadvantage of this approach is that it requires that upon postback the form containing the DataGrid cannot be rendered again. But what if you want to still show the DataGrid to the user? If so, we have to implement a different approach, which we'll discuss in Part 2.

  • Read Part 2!

  • Article Information
    Article Title: ASP.NET.Enhancing the 'Email the Rendered Output of an ASP.NET Web Control' Code
    Article Author: Scott Mitchell
    Published Date: October 22, 2003
    Article URL: http://www.4GuysFromRolla.com/articles/102203-1.aspx


    Copyright 2014 QuinStreet Inc. All Rights Reserved.
    Legal Notices, Licensing, Permissions, Privacy Policy.
    Advertise | Newsletters | E-mail Offers