Multiple Forms in Master Page Site

March 30, 2009 at 11:14 PMBen

When integrating a website with 3rd party services such as search providers or payment processors, it's not an uncommon need to have a submit button that posts to that 3rd party's site.

ASP.NET 2.0 introduced the PostBackUrl property for controls that can initiate a post -- Buttons, ImageButtons, LinkButtons.  With this property, you can have your page post to any URL.  This is a somewhat decent solution to this problem.  The major downside to PostBackUrl is it requires the visitor to have JavaScript enabled in their browser.  If JavaScript is disabled, you end up with a normal postback to your own page.  To me, this makes PostBackUrl not a very good choice to turn to.

The other day, I needed to send a visitor to a payment processor's site.  My site was using master pages.  I was collecting some preliminary information from the visitor on a Content page.  Once collected, I was going to show them a confirmation of what they would be paying for and give them a button to move onto the payment processor.  I decided to use the PostBackUrl property on this button.

The payment processor needed a few hidden fields included in the form submission.  I put those hidden input fields including the values into non-server hidden input fields since I didn't want the Name or Ids of the input fields mangled by ASP.NET.  The form looked good, and clicking the button took me to the payment processor's website -- but as soon as I got there, the only thing on the page was some error message complaining about incoming data (a very non-specific error message).

I knew their site and this particular landing page worked when I tested posting data in a non-ASP.NET environment.  I confirmed the correct hidden input fields were being sent in the POST through Fiddler, but still no dice.  The only explanation I could come up with is when using PostBackUrl, not only are my custom hidden fields being passed to this other site, but so are all the other standard ASP.NET hidden fields -- ViewState, EventValidation, etc.  It's possible the payment processor's site was not expecting other form values to be passed to it.

So I decided to ditch PostBackUrl ... which was fine since I wasn't a big fan of PostBackUrl to begin with.  I came up with a pretty simple solution to having multiple forms in this master page environment.  The typical master page setup is where a <form runat="server"> tag in the master page surrounds the ContentPlaceHolder.

<body>
    <form id="form1" runat="server">
        <asp:ContentPlaceHolder id="ContentPlaceHolder1" runat="server">
        </asp:ContentPlaceHolder>
    </form>
</body>

Now if you put your own <form> tag in the Content page, you'll end up with a form nested in another form.  Nested forms are not valid.  Here's what I did to avoid nested forms, but still end up with multiple forms.

<body>    
    <form id="form1" runat="server">
        <asp:ContentPlaceHolder id="cphForm" runat="server">
        </asp:ContentPlaceHolder>
    </form>
    
    <asp:ContentPlaceHolder id="cphNoForm" runat="server">
    </asp:ContentPlaceHolder>
</body>

The master page now has 2 content place holders.  One is wrapped in a <form runat="server"> tag, and the other isn't.  The type of controls allowed when not wrapped in a <form runat="server"> tag is limited.  For instance, you cannot have ASP.NET Buttons, TextBoxes and a number of other controls outside a <form runat="server"> tag.  You can however use Literals, PlaceHolders and basically any HTML controls with a runat="server" tag.  Here's the content page markup.

<asp:Content ID="cntForm" ContentPlaceHolderID="cphForm" Runat="Server">
    
    <asp:PlaceHolder ID="phForm" runat="server">
    
        <h2>Select an Item to Purchase</h2>
        <asp:RadioButtonList ID="rblMenu" runat="server">
            <asp:ListItem Text="Item 1" Value="item1" Selected="True"></asp:ListItem>
            <asp:ListItem Text="Item 2" Value="item2"></asp:ListItem>
            <asp:ListItem Text="Item 3" Value="item3"></asp:ListItem>
        </asp:RadioButtonList>
        <asp:Button ID="btnContinue" runat="server"
                    Text="Continue" OnClick="continuePurchase" />

    </asp:PlaceHolder>
    
</asp:Content>

<asp:Content ID="cntNoForm" ContentPlaceHolderID="cphNoForm" Runat="Server">

    <asp:PlaceHolder ID="phConfirmation" runat="server" Visible="false">
    
        <h2>
           Your Selected Item:
           <asp:Literal ID="litSelectedItem" runat="server"></asp:Literal></h2>
    
        <form method="post" action="http://www.example.com/">
            <input type="hidden" name="myId" value="someID" />
            <input type="hidden" name="itemCode" value="<%= itemCode %>" />
            <input type="hidden" name="itemAmount" value="<%= itemAmt %>" />
            <input type="submit" name="payNow" value="Pay Now" />
        </form>
    
    </asp:PlaceHolder>

</asp:Content>

The content page is making use of both content place holders.  The top Content control contains a place holder, which contains a simple order form.  The second Content control contains a place holder with its visibility set to false.  So when the page is first pulled up, nothing in the second Content control is yet visible.

Once an item is selected, and the Continue button is clicked, a postback is done.  During the postback, the placeholder in the first Content control is made invisible, and the placeholder in the second Content control is made visible.  Because the second Content control (and all of its contents) are outside of the <form runat="server"> tag, we've achieved having two different, non-nested <form> tags on the same page.  Here's what the rendered HTML looks like after the person has selected their item and is ready to be sent to the payment processor:

<body>    
   <div>
      <form name="aspnetForm" method="post" action="Content1.aspx" id="aspnetForm">
         <div>
            <input type="hidden" name="__VIEWSTATE"
                   id="__VIEWSTATE" value="some long value" />
         </div>
      </form>
     
      <h2>Your Selected Item: Item 2</h2>
    
      <form method="post" action="http://www.example.com/">
         <input type="hidden" name="myId" value="someID" />
         <input type="hidden" name="itemCode" value="item2" />
         <input type="hidden" name="itemAmount" value="30" />
         <input type="submit" name="payNow" value="Pay Now" />
      </form>
   </div>    
</body>

With this approach, I was able to post the form to the payment processor, and the form contained hidden input fields only relevant to the processor.  This approach will work in most master page situations.  It may be difficult to implement if you have server side controls requiring a <form runat="server"> tag in your master page -- outside of the main content place holder.  In this case, it may still be possible to implement this two ContentPlaceHolder approach, if you do some juggling around of your controls and/or layout.  However, in a lot of situations, this is a practical way to achieve multiple form tags in an ASP.NET site.

Comments (3) -

Thanks, nice and useful idea. Spacial when working with PayPal and such service.

I looked around and other posts did not work. This was a great solution to this problem. Thank you

This idea works great.  I have a master page site with two HTML forms (wrapped with literal controls) used solely for posting hidden fields to 3rd party payment processor.  Since literal controls are accessible from the code-behind I can manipulate the hidden field values at run-time depending upon user choices.  I had it mostly working except only one of the forms would work when running nested inside the main ASP.Net form tag.  When I applied the separate content place holder outside the ASP.Net form tag both forms worked.  Thanks Ben!

Comments are closed