February 21, 2003 12:02 AM

Many From One

Build a custom server form control to add missing support for multiple server forms and forms that post to other pages.
DevConnections
Rating: (0)

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.

 

 

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.

 

 

 

Add a Comment

There are no comments to display. Be the first one!
You must log on before posting a comment.

Are you a new visitor? Register Here

advertisement




Comments from the DevConnections Community

Join our community of development pros.

Windows problem

I all, I have a problem on my Windows Vista that began afetr the purchase of an external Hard Disk Freecom. A few days afetr the purchase I discon...

Most Recent Posts

GOOGLE LINKS
SPONSORED LINKS
FEATURED LINKS