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 missingsupport for multiple server forms and forms that post to other pages.
By Paul Wilson
ASP.NET simplifies many common development tasks with itspost-back, event-driven control architecture. This architecture limits each Webpage, however, to a single server form that must always post back to itself. Inthis article, I will show you how to create a custom server form control thatovercomes these server form limitations. This control finally allows multipleserver forms and/or forms that post to other pages. (You can download the codefor this control).
The most common scenario for using multiple forms is tohave a small login or search form on every page of a Web site. This commonfunctionality is not related to the processing on each page, so a separate formoften is used to post to the relevant page. An additional benefit of usingseparate forms is each form gets its own default button in most Web browsersautomatically. Also, you might encounter many scenarios where user input mustbe posted to a completely different server, such as for payment processing.
The best way to incorporate this functionality is to usethe built-in server form, possibly extending it through inheritance. TheASP.NET page framework, however, uses private fields and methods - which youcan't override - to prohibit multiple server forms. Any attempted solution,therefore, must avoid the built-in server form and instead re-create thenecessary functionality. For this solution to work successfully, you mustinclude support for as many server controls as possible, including their eventsand 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-inserver form (HtmlForm) is derived from the HtmlContainerControl class, found inthe System.Web.UI.HtmlControls namespace. HtmlContainerControl provides thebasic support needed for a server control to contain other children controls.Thus, the custom server form control you will build in this article also shouldinherit from HtmlContainerControl. You can use HtmlContainerControl to buildany HTML control, so you must pass the actual HTML tag - in this case, "form" -to its constructor:
Public Class Form : InheritsHtmlContainerControl
Public Sub New()
MyBase.New("form")
End Sub
End Class
Next, you should create any HTML attributes the form tagrequires as properties and output them in the RenderAttributes override (seeFigure 1). I created properties here for action and method; for method, anenumeration is created that contains get and post.
Public Enum MethodEnum
[get]
post
End Enum
<ToolboxData("<{0}:Formrunat=server></{0}:Form>"), _
ToolboxItem(GetType(WebControlToolboxItem)), _
Designer(GetType(ReadWriteControlDesigner))> _
Public Class Form
InheritsHtmlContainerControl
Private _method As MethodEnum = MethodEnum.post
Private _action AsString = ""
Public Sub New()
MyBase.New("form")
End Sub
Public Property Method() As MethodEnum
Get
Return Me._method
End Get
Set(ByVal Value AsMethodEnum)
Me._method = Value
End Set
End Property
Public Property Action() As String
Get
Return Me._action
End Get
Set(ByVal Value AsString)
Me._action = Value
End Set
End Property
Protected Overrides Sub RenderAttributes _
(ByVal writer AsSystem.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 classthat will become the custom server form control. Here you see the definition ofthe Form class and its main properties, Method and Action, along with itsdesigner attributes.
You also could extend this example control easily usingproperties for target and enctype if these are important. The call to theMyBase.RenderAttributes method is needed to write out the id attribute, whichfirst is checked for being undefined. Finally, the action attribute is set tobe the current page by default, similar to the normal postback, if no specificaction 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) isused similarly to the built-in server form except it requires a Registerdirective:
<%@ 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 customserver 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 itsproperties in the designer, as well.
Support Web Controls
The first problem this custom server form controlencounters is that the main Web controls cause an exception when they are notincluded in the built-in server form. Luckily, the exception that occurs whenusing these Web controls provides the name of the method causing the realproblem - VerifyRenderingInServerForm. Because this is a public virtual method,you simply need to add an appropriate override of this method in the page classcode:
Public Overrides SubVerifyRenderingInServerForm _
(ByVal control AsSystem.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 isnot in a server form. This method's override therefore should not contain anyimplementation to overcome the limitation of using Web controls without aserver form.
Another problem is that the custom server form controldoes not support server control events, and it doesn't restore posted values ofform fields. Again, using ILDASM, you see that all events and posted valuesshould occur automatically whenever the IsPostBack property of the Page classis true. Moreover, examining the DeterminePostBackMode method shows that themere 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 postedvalues:
Protected Overrides SubRenderChildren _
(ByVal writer AsSystem.Web.UI.HtmlTextWriter)
writer.WriteLine("<inputtype='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 fieldenables the developer to check which form caused the postback to occur. Thisshould look similar to the IsPostBack property of the Page class, except thistime 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 alist of properties):
Public ReadOnly Property IsPostBack()As Boolean
Get
If Me.Context IsNothing 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 thecustom server form control. Unlike the built-in server form, you can set theAction 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 notmaintained in this custom server form control. The easiest way to save andrestore ViewState's contents is to store it in a session variable for the page.You do this by overriding both LoadPageStateFromPersistenceMedium andSavePageStateToPersistenceMedium (see Figure 4).
Public Class Page
InheritsSystem.Web.UI.Page
Public Overrides Sub VerifyRenderingInServerForm _
(ByVal control AsSystem.Web.UI.Control)
' Do NOT callMyBase.VerifyRenderingInServerForm
End Sub
Protected Overrides Sub OnInit _
(ByVal e AsSystem.EventArgs)
MyBase.OnInit(e)
Me.RegisterViewStateHandler()
End Sub
Protected Overrides Function _
LoadPageStateFromPersistenceMedium() As Object
ReturnSession("ViewState")
End Function
Protected Overrides Sub _
SavePageStateToPersistenceMedium _
(ByVal viewState AsObject)
Session("ViewState")= viewState
Me.RegisterHiddenField("__VIEWSTATE", "")
End Sub
End Class
Figure 4. This code is the base Page class you mustuse with this custom form control to enable Web controls and ViewState. Unlikemost controls, these changes in the Page class are critical because the pageframework itself is responsible for these features.
This override actually is quite common. It minimizes theclient load as ViewState can be huge, and it's even the default in the mobilecontrols. It also requires that the __ViewState hidden field be registered withthe page so it continues to be output for the built-in server form. This isenough to save and restore the contents of ViewState if a built-in server formis being used. Fortunately, the page can be notified easily to store itsViewState in other cases through a simple call to RegisterViewStateHandler:
Protected Overrides Sub OnInit _
(ByVal e AsSystem.EventArgs)
MyBase.OnInit(e)
Me.RegisterViewStateHandler()
End Sub
Note that this control is unusual because it requires allthese overrides in the Page class itself (see Figure 4). The easiest way to dothis is to make each Web page inherit from this base Page class instead ofre-implementing these overrides.
Submit JavaScript Forms
This solution still lacks one component. Most advanced Webcontrols depend on client-side JavaScript to post back the form dynamically. Ifyou examine the HTML source of a typical ASP.NET page, you'll see that therequired JavaScript function, named __doPostBack, is included in the built-inserver form. It's actually quite easy to add this function to the existingRenderChildren 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 AsStringBuilder = New StringBuilder()
Dim HtmlWriter AsStringWriter = New StringWriter(Html)
Dim TempWriter AsHtmlTextWriter = _
NewHtmlTextWriter(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 & _
"vartheForm = 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 theForm class from the earlier basic skeleton in Figure 1 that defined the classand its properties. The changes in the RenderChildren method are necessary tosupport the JavaScript form submission used by advanced Web controls.
But this creates another problem. Each JavaScript functionmust have a unique name, yet this same function is rendered in each of themultiple forms. Also, each Web control might render a reference to this __doPostBackfunction, 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 anew function, doPostBack, appended with the unique form ID. String replacementsare not the most efficient operation, so the previous code first checks if__doPostBack exists in the HTML. This also mimics the built-in server formbecause it renders only the required JavaScript when needed. The remainingissue is that this operation requires capturing all the HTML associated withthe form - before it is rendered. The solution is to use a temporaryHtmlTextWriter class, built up from StringBuilder and StringWriter classes, tocapture the HTML from the original HtmlTextWriter. Then the captured HTML canbe 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, lettingyou have multiple server forms and forms that post to other pages. Note thatthis custom server form control also can coexist with the built-in server form,which remains the only way to use validators.
It is essential torecall that unlike most controls, this custom control also requires Web pagesthat use it to implement certain overrides or inherit from the included custompage class. You might also consider further changes to add other properties asneeded, store ViewState using your own custom technique, or handle validationcontrols with custom JavaScript. Finally, please write the ASP.NET team if thisshould be included in version 2.0, because the current plan does not includenative support for multiple forms or forms that post to other pages.
The sample code in thisarticle is available for download.
Paul Wilson is a software architect in Atlanta,currently working with a medical device company. He specializes in Microsofttechnologies including .NET, C#, ASP, SQL, COM+, and VB, and his WilsonWebFormcontrol allows multiple forms and non-postback forms in ASP.NET. He is aMicrosoft MVP in ASP.NET as well as an MCAD, MCSD, MCDBA, and MCSE, and he is amoderator 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 iseither to use Server.Transfer or Response.Redirect. Server.Transfer makes thevarious form fields available to the next page automatically, but the client'sbrowser is not notified of the new page. Response.Redirect requires saving therelevant user input somewhere else, such as a query string or possibly asession. Moreover, Server.Transfer does not work when the page is on anotherserver, and Response.Redirect does not cause a post to occur. Basically,neither of these solutions will work in situations that require an actual postof data to occur on another Web server, which actually is quite common.
Luckily, you can set a form's action attribute to workwith server forms using client-side JavaScript. Each button can set the actionattribute of the form individually, leaving other buttons and controls free tocontinue normal postback processing:
Private Sub Page_Load(ByVal sender AsSystem.Object, _
ByVal e AsSystem.EventArgs) Handles MyBase.Load
btnNotPostBack.Attributes.Add("onclick", _
"frmServer.action = 'OtherPage.aspx';")
End Sub
You also can set this same JavaScript for the entireserver form all at once using the RegisterStartupScript method. Note thatposting to another page requires the use of the Request.Form collection in thereceiving page because the original control references will not be available.
Tell us what you think! Please send any comments aboutthis article to mailto:feedback@aspnetPRO.com.Please include the article title and author.