Enhancing the 'Email the Rendered Output of an ASP.NET Web Control' CodeBy 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.|
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:
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.Controlclass. 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
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
RenderControl() method called. This method accepts a single parameter, an
RenderControl() recursively calls the
of all of the controls in the control hierarchy whose
Visible property is set to True (the default), passing
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 way the code worked in Emailing the Rendered Output of an ASP.NET Web Control was by creating a new
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
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
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."
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:
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
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:
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:
The next change is that we call the server-side form's
RenderControl() method (instead of
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.
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.