asp:Cover Story
LANGUAGES: VB .NET
TECHNOLOGIES: Forms | Server Form Controls |
Custom Controls | ASP.NET Event Model
Many From One
Build a custom server form control to add missing
support for multiple server forms and forms that post to other pages.
By Paul Wilson
ASP.NET simplifies many common development tasks with its
post-back, event-driven control architecture. This architecture limits each Web
page, however, to a single server form that must always post back to itself. In
this article, I will show you how to create a custom server form control that
overcomes these server form limitations. This control finally allows multiple
server forms and/or forms that post to other pages. (You can download the code
for this control).
The most common scenario for using multiple forms is to
have a small login or search form on every page of a Web site. This common
functionality is not related to the processing on each page, so a separate form
often is used to post to the relevant page. An additional benefit of using
separate forms is each form gets its own default button in most Web browsers
automatically. Also, you might encounter many scenarios where user input must
be posted to a completely different server, such as for payment processing.
The best way to incorporate this functionality is to use
the built-in server form, possibly extending it through inheritance. The
ASP.NET page framework, however, uses private fields and methods - which you
can't override - to prohibit multiple server forms. Any attempted solution,
therefore, must avoid the built-in server form and instead re-create the
necessary functionality. For this solution to work successfully, you must
include support for as many server controls as possible, including their events
and state.
Create the Form Control
Every custom control must inherit from some base control,
preferably one that provides the greatest built-in functionality. The built-in
server form (HtmlForm) is derived from the HtmlContainerControl class, found in
the System.Web.UI.HtmlControls namespace. HtmlContainerControl provides the
basic support needed for a server control to contain other children controls.
Thus, the custom server form control you will build in this article also should
inherit from HtmlContainerControl. You can use HtmlContainerControl to build
any HTML control, so you must pass the actual HTML tag - in this case, "form" -
to its constructor:
Public Class Form : Inherits
HtmlContainerControl
Public Sub New()
MyBase.New("form")
End Sub
End Class
Next, you should create any HTML attributes the form tag
requires as properties and output them in the RenderAttributes override (see
Figure 1). I created properties here for action and method; for method, an
enumeration is created that contains get and post.
Public Enum MethodEnum
[get]
post
End Enum
<ToolboxData("<{0}:Form
runat=server></{0}:Form>"), _
ToolboxItem(GetType(WebControlToolboxItem)), _
Designer(GetType(ReadWriteControlDesigner))> _
Public Class Form
Inherits
HtmlContainerControl
Private _method As MethodEnum = MethodEnum.post
Private _action As
String = ""
Public Sub New()
MyBase.New("form")
End Sub
Public Property Method() As MethodEnum
Get
Return Me._method
End Get
Set(ByVal Value As
MethodEnum)
Me._method = Value
End Set
End Property
Public Property Action() As String
Get
Return Me._action
End Get
Set(ByVal Value As
String)
Me._action = Value
End Set
End Property
Protected Overrides Sub RenderAttributes _
(ByVal writer As
System.Web.UI.HtmlTextWriter)
If Me.ID = Nothing Then
Me.ID = Me.ClientID
End If
MyBase.RenderAttributes(writer)
writer.WriteAttribute("name", Me.ID)
writer.WriteAttribute("method", Me.Method.ToString())
If Me.Action =
"" Then
writer.WriteAttribute("action", _
Me.Context.Request.RawUrl, True)
Else
writer.WriteAttribute("action", Me.Action, True)
End If
End Sub
End Class
Figure 1. This code is the basic skeleton of the class
that will become the custom server form control. Here you see the definition of
the Form class and its main properties, Method and Action, along with its
designer attributes.
You also could extend this example control easily using
properties for target and enctype if these are important. The call to the
MyBase.RenderAttributes method is needed to write out the id attribute, which
first is checked for being undefined. Finally, the action attribute is set to
be the current page by default, similar to the normal postback, if no specific
action is provided:
If Me.Action = "" Then
writer.WriteAttribute("action", _
Me.Context.Request.RawUrl, True)
Else
writer.WriteAttribute("action", Me.Action, True)
End If
The resulting custom server form control (see Figure 2) is
used similarly to the built-in server form except it requires a Register
directive:
<%@ Register TagPrefix="Wilson"
Assembly="WilsonDemoForm"
Namespace="Wilson.DemoForm" %>
<Wilson:Form id="frmCstm" method="post"
runat="server">
<!-Server Form Content and Child Controls Go Here -->
</Wilson:Form>
Figure 2. This is what the Web form designer sees when using this custom
server form control. Note that all its contained controls appear as expected.
The custom form might have a thin border in the designer, and you can set its
properties in the designer, as well.
Support Web Controls
The first problem this custom server form control
encounters is that the main Web controls cause an exception when they are not
included in the built-in server form. Luckily, the exception that occurs when
using these Web controls provides the name of the method causing the real
problem - VerifyRenderingInServerForm. Because this is a public virtual method,
you simply need to add an appropriate override of this method in the page class
code:
Public Overrides Sub
VerifyRenderingInServerForm _
(ByVal control As
System.Web.UI.Control)
' Do NOT call MyBase.VerifyRenderingInServerForm
End Sub
When you examine this method with the ILDASM utility,
which is included with the .NET SDK, you see that no functionality is lost -
the method does nothing except throw an exception when the control passed in is
not in a server form. This method's override therefore should not contain any
implementation to overcome the limitation of using Web controls without a
server form.
Another problem is that the custom server form control
does not support server control events, and it doesn't restore posted values of
form fields. Again, using ILDASM, you see that all events and posted values
should occur automatically whenever the IsPostBack property of the Page class
is true. Moreover, examining the DeterminePostBackMode method shows that the
mere presence of the __ViewState field causes IsPostBack to be set to true.
Thus, the following override of RenderChildren, with the hidden form field
__ViewState, is enough to trigger server control events and restore posted
values:
Protected Overrides Sub
RenderChildren _
(ByVal writer As
System.Web.UI.HtmlTextWriter)
writer.WriteLine("<input
type='hidden' _
name='PostBackForm'
value='" + Me.ID + "' />")
writer.WriteLine("<input type='hidden' _
name='__VIEWSTATE'
value='' />")
MyBase.RenderChildren(writer)
End Sub
Also note the addition of another hidden form field, PostBackForm,
defined to be the form's control ID. When multiple forms are used, this field
enables the developer to check which form caused the postback to occur. This
should look similar to the IsPostBack property of the Page class, except this
time it exists on the form itself. This read-only property, named IsPostBack,
is then added to the custom server form's control class (Figure 3 contains a
list of properties):
Public ReadOnly Property IsPostBack()
As Boolean
Get
If Me.Context Is
Nothing Then ' Designer Support
Return False
Else
Return
(Me.Context.Request.Form _
("PostBackForm") = Me.ID)
End If
End Get
End Property
|
Property
|
Type
|
Comments
|
|
ID
|
String
|
|
|
Action
|
String
|
Current page is default
|
|
Method
|
MethodEnum
|
Post (default) or Get
|
|
IsPostBack
|
Boolean
|
ReadOnly at run time
|
Figure 3. Here is a summary of the properties of the
custom server form control. Unlike the built-in server form, you can set the
Action property to post the page to any valid URL. A new property, IsPostBack,
lets you determine which of the multiple forms caused a postback.
Track the ViewState
The next issue is that the contents of ViewState are not
maintained in this custom server form control. The easiest way to save and
restore ViewState's contents is to store it in a session variable for the page.
You do this by overriding both LoadPageStateFromPersistenceMedium and
SavePageStateToPersistenceMedium (see Figure 4).
Public Class Page
Inherits
System.Web.UI.Page
Public Overrides Sub VerifyRenderingInServerForm _
(ByVal control As
System.Web.UI.Control)
' Do NOT call
MyBase.VerifyRenderingInServerForm
End Sub
Protected Overrides Sub OnInit _
(ByVal e As
System.EventArgs)
MyBase.OnInit(e)
Me.RegisterViewStateHandler()
End Sub
Protected Overrides Function _
LoadPageStateFromPersistenceMedium() As Object
Return
Session("ViewState")
End Function
Protected Overrides Sub _
SavePageStateToPersistenceMedium _
(ByVal viewState As
Object)
Session("ViewState")
= viewState
Me.RegisterHiddenField("__VIEWSTATE", "")
End Sub
End Class
Figure 4. This code is the base Page class you must
use with this custom form control to enable Web controls and ViewState. Unlike
most controls, these changes in the Page class are critical because the page
framework itself is responsible for these features.
This override actually is quite common. It minimizes the
client load as ViewState can be huge, and it's even the default in the mobile
controls. It also requires that the __ViewState hidden field be registered with
the page so it continues to be output for the built-in server form. This is
enough to save and restore the contents of ViewState if a built-in server form
is being used. Fortunately, the page can be notified easily to store its
ViewState in other cases through a simple call to RegisterViewStateHandler:
Protected Overrides Sub OnInit _
(ByVal e As
System.EventArgs)
MyBase.OnInit(e)
Me.RegisterViewStateHandler()
End Sub
Note that this control is unusual because it requires all
these overrides in the Page class itself (see Figure 4). The easiest way to do
this is to make each Web page inherit from this base Page class instead of
re-implementing these overrides.
Submit JavaScript Forms
This solution still lacks one component. Most advanced Web
controls depend on client-side JavaScript to post back the form dynamically. If
you examine the HTML source of a typical ASP.NET page, you'll see that the
required JavaScript function, named __doPostBack, is included in the built-in
server form. It's actually quite easy to add this function to the existing
RenderChildren method (see Figure 5).
Private Const _formName As String =
"PostBackForm"
Public ReadOnly Property IsPostBack() As Boolean
Get
If Me.Context Is Nothing Then
Return False
Else
Return
(Me.Context.Request.Form _
(Form._formName) = Me.ID)
End If
End Get
End Property
Protected Overrides Sub RenderChildren _
(ByVal writer As System.Web.UI.HtmlTextWriter)
writer.WriteLine()
writer.WriteLine("<input type='hidden' name='" + _
Form._formName +
"' value='" + Me.ID + "' />")
writer.WriteLine("<input type='hidden' _
name='__VIEWSTATE'
value='' />")
Dim Html As
StringBuilder = New StringBuilder()
Dim HtmlWriter As
StringWriter = New StringWriter(Html)
Dim TempWriter As
HtmlTextWriter = _
New
HtmlTextWriter(HtmlWriter)
MyBase.RenderChildren(TempWriter)
Dim DoPostBack As Boolean = _
(Html.ToString().IndexOf("__doPostBack") >= 0)
If DoPostBack Then
Html.Replace("__doPostBack", "doPostBack_" &
Me.ID)
writer.WriteLine("<input type='hidden' _
name='__EVENTTARGET' value='' />")
writer.WriteLine("<input type='hidden' _
name='__EVENTARGUMENT' value='' />")
writer.WriteLine("<script type='text/javascript'>")
writer.WriteLine("<!--")
writer.WriteLine(vbTab & "function doPostBack_" + _
Me.ID +
"(eventTarget, eventArgument) {")
writer.WriteLine(vbTab & vbTab & _
"var
theForm = document." + Me.ID + ";")
writer.WriteLine(vbTab & vbTab & _
"theForm.__EVENTTARGET.value = eventTarget;")
writer.WriteLine(vbTab & vbTab & _
"theForm.__EVENTARGUMENT.value
= eventArgument;")
writer.WriteLine(vbTab & vbTab & "theForm.submit();")
writer.WriteLine(vbTab & "}")
writer.WriteLine("//-->")
writer.WriteLine("</script>")
End If
writer.Write(Html.ToString())
End Sub
End Class
Figure 5. This code shows the changes required in the
Form class from the earlier basic skeleton in Figure 1 that defined the class
and its properties. The changes in the RenderChildren method are necessary to
support the JavaScript form submission used by advanced Web controls.
But this creates another problem. Each JavaScript function
must have a unique name, yet this same function is rendered in each of the
multiple forms. Also, each Web control might render a reference to this __doPostBack
function, so all the controls in the form must be altered. Here's the solution,
if you can capture all the HTML associated with the form before it is rendered:
Dim DoPostBack As Boolean = _
(Html.ToString().IndexOf("__doPostBack") >= 0)
If DoPostBack Then
Html.Replace("__doPostBack", "doPostBack_" &
Me.ID)
writer.WriteLine("<!-- JavaScript (see Figure 5) -->")
End If
This code replaces each reference to __doPostBack with a
new function, doPostBack, appended with the unique form ID. String replacements
are not the most efficient operation, so the previous code first checks if
__doPostBack exists in the HTML. This also mimics the built-in server form
because it renders only the required JavaScript when needed. The remaining
issue is that this operation requires capturing all the HTML associated with
the form - before it is rendered. The solution is to use a temporary
HtmlTextWriter class, built up from StringBuilder and StringWriter classes, to
capture the HTML from the original HtmlTextWriter. Then the captured HTML can
be modified before it is written back to the original HtmlTextWriter:
Dim Html As StringBuilder = New StringBuilder()
Dim HtmlWriter As StringWriter = New StringWriter(Html)
Dim TempWriter As HtmlTextWriter = _
New HtmlTextWriter(HtmlWriter)
MyBase.RenderChildren(TempWriter)
' Modify the Content of the Html StringBuilder Here
writer.Write(Html.ToString())
Now your custom server form control is complete, letting
you have multiple server forms and forms that post to other pages. Note that
this custom server form control also can coexist with the built-in server form,
which remains the only way to use validators.
It is essential to
recall that unlike most controls, this custom control also requires Web pages
that use it to implement certain overrides or inherit from the included custom
page class. You might also consider further changes to add other properties as
needed, store ViewState using your own custom technique, or handle validation
controls with custom JavaScript. Finally, please write the ASP.NET team if this
should be included in version 2.0, because the current plan does not include
native support for multiple forms or forms that post to other pages.
The sample code in this
article is available for download.
Paul Wilson is a software architect in Atlanta,
currently working with a medical device company. He specializes in Microsoft
technologies including .NET, C#, ASP, SQL, COM+, and VB, and his WilsonWebForm
control allows multiple forms and non-postback forms in ASP.NET. He is a
Microsoft MVP in ASP.NET as well as an MCAD, MCSD, MCDBA, and MCSE, and he is a
moderator on Microsoft's ASP.NET Forums. Visit his Web site, http://www.WilsonDotNet.com,
or e-mail him at mailto:Paul@WilsonDotNet.com.
Alternative Solution
The typical ASP.NET solution for posting to other pages is
either to use Server.Transfer or Response.Redirect. Server.Transfer makes the
various form fields available to the next page automatically, but the client's
browser is not notified of the new page. Response.Redirect requires saving the
relevant user input somewhere else, such as a query string or possibly a
session. Moreover, Server.Transfer does not work when the page is on another
server, and Response.Redirect does not cause a post to occur. Basically,
neither of these solutions will work in situations that require an actual post
of data to occur on another Web server, which actually is quite common.
Luckily, you can set a form's action attribute to work
with server forms using client-side JavaScript. Each button can set the action
attribute of the form individually, leaving other buttons and controls free to
continue normal postback processing:
Private Sub Page_Load(ByVal sender As
System.Object, _
ByVal e As
System.EventArgs) Handles MyBase.Load
btnNotPostBack.Attributes.Add("onclick", _
"frmServer.action = 'OtherPage.aspx';")
End Sub
You also can set this same JavaScript for the entire
server form all at once using the RegisterStartupScript method. Note that
posting to another page requires the use of the Request.Form collection in the
receiving page because the original control references will not be available.
Tell us what you think! Please send any comments about
this article to mailto:feedback@aspnetPRO.com.
Please include the article title and author.