advertisement

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.
Rating: (0)
DevProConnections
InstantDoc ID #123831

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.

 

 

 

ARTICLE TOOLS

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
Free Tech Advisor
Get the 5-Chapter Guide to Developing Mobile Apps today!



      
GOOGLE LINKS
SPONSORED LINKS
FEATURED LINKS