Use Multiple Forms in an ASP.NET page
ASP.NET has a great setup where a page posts back to itself and all controls are contained in one form. However, there are times when it's useful to have multiple forms setup on a page each with their own inputs and url to post to. Unfortunately, aspx pages make it difficult to accomplish which you are certainly aware given the fact that you found this article. Follow me here as I show you a solution I've come to use quite a lot with my applications.
One solution that I've used before but have found to be limited in its application is using multiple form elements but without making them server controls. In other words not applying the runat=server attribute to them. Yes, this does work however it makes it difficult to place the buttons for the user to submit the form with. I mean who really wants to place all their form buttons at the very top or bottom of a page. I did have a little success using absolute positioning of the buttons. I was able to get them positioned at just the right place on my form. But that required the text on your page to always be the same. No, I found that use absolute positioning just wasn't reliable enough in this situation.
So, what's the answer you ask? Something I've found to be very useful and have yet to find a downside to are iframes. If you've never used these before let me explain what they are. It's like a div tag only allows you to specify the url of another page and then displays the output of that second page inside the element. Because we're not limited to the number of iframes we have on an aspx page we can use as many forms as we want. Now I know what you might be thinking - that's not really placing multiple forms on one page. Yes, you are correct BUT it does allow you to accomplish what you're trying to do. Let me show you how.
Let's say you're in charge of creating an e-commerce site and you plan out using paypal to process the transactions. One of the features they offer is the "Buy Now" button. It's a simple way to allow the user to buy one product from your site. The way it works is you setup a form with the correct inputs for the paypal system and a button to submit the form. Naturally you would surround the button with information about the product but in this case we're going to put the button inside the iframe. My first example I'm an going to setup as an html file since it will be static. We'll get to the dynamic form in a bit. Below is a copy of the example on paypal's web site. I added the html, head and body elements.
<html>
<head>
<title>Buy Now</title>
</head>
<body>
<form action="https://www.paypal.com/cgi-bin/webscr" method="post">
<input type="hidden" name="cmd" value="_xclick">
<input type="hidden" name="business" value="sample@sample.com">
<input type="hidden" name="item_name" value="Item Name Goes Here">
<input type="hidden" name="item_number" value="Item Number Goes Here">
<input type="hidden" name="amount" value="100.00">
<input type="hidden" name="no_shipping" value="2">
<input type="hidden" name="no_note" value="1">
<input type="hidden" name="currency_code" value="USD">
<input type="hidden" name="bn" value="IC_Sample">
<input type="image" src="https://www.paypal.com/en_US/i/btn/x-click-but23.gif" name="submit" alt="Make payments with payPal - it's fast, free and secure!">
<img alt="" src="https://www.paypal.com/en_US/i/scr/pixel.gif" width="1" height="1">
</form>
</body>
</html>
Try copying and pasting this into an html page yourself. Test it out by viewing it in a browser and pressing the button. You'll notice it posts to paypal just as expected. Now let's put this into an iframe. Create a new aspx page and add some text to it plus an iframe to point to our first page. Here is an example of what I created.
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<p>
This is a great product! You really should buy it.
</p>
<iframe src="StaticForm.htm"></iframe>
</form>
</body>
</html>
The previous markup assumed that this aspx page and the first html page are in the same directory. Now view this in a browser. Below is a screenshot of what this looks like in firefox. Notice a couple things. First there's a border around the iframe and it has too much height.
At least we've got the button showing. Now those two issues are resolved quite easily by applying a few styling rules. Take a look at our aspx markup this time.
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<p>
This is a great product! You really should buy it.
</p>
<iframe src="StaticForm.htm" style="border: none; height: 35px; overflow: hidden;"></iframe>
</form>
</body>
</html>
Again here is a screen shot of the output.
Alright... now we're getting somewhere. Notice the button looks as if it's contained in the aspx page. No border and less height. We're looking good. However, there is something I need to point out. Try creating both the html and aspx page and view the aspx page in a browser. Then press the button. Doesn't work exactly as expected does it? Yes, that's because the form inside the iframe is posting to paypal inside the iframe. So whenever we use this technique we also need to apply another attribute to the form which is the target attribute. So going back to our html page and adding the attribute we get this markup.
<html>
<head>
<title>Buy Now</title>
</head>
<body>
<form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top">
<input type="hidden" name="cmd" value="_xclick">
<input type="hidden" name="business" value="sample@sample.com">
<input type="hidden" name="item_name" value="Item Name Goes Here">
<input type="hidden" name="item_number" value="Item Number Goes Here">
<input type="hidden" name="amount" value="100.00">
<input type="hidden" name="no_shipping" value="2">
<input type="hidden" name="no_note" value="1">
<input type="hidden" name="currency_code" value="USD">
<input type="hidden" name="bn" value="IC_Sample">
<input type="image" src="https://www.paypal.com/en_US/i/btn/x-click-but23.gif" name="submit" alt="Make payments with payPal - it's fast, free and secure!">
<img alt="" src="https://www.paypal.com/en_US/i/scr/pixel.gif" width="1" height="1">
</form>
</body>
</html>
I set the target to "_top". This will make the form post in the top level frame which in this case will be our aspx page. Try reloading the aspx page in your browser and press the button again. This time it works great.
So you've come a long way with this solution. You can create multiple html pages with their own unique form and action and display them all on one aspx page. But what if you want to create a dynamic page for your forms and pass them parameter values? How would you set that up. One way is to just create another aspx page. However, I think there's a more efficient method - Httphandlers. What are those you ask? First of all they're any class that implements the IHttpHandler interface. Their purpose is to determine the best way to handle an incoming request. There is one setup to handle all requests with an aspx extension. Naturally that's used for web page requests. But you can configure your own for any extension or path really. The reason I think they're more efficient is that they don't have to have the heavyweight Page class created and run through the whole page cycle. We can simply write the text for the whole form to the output stream ourselves.
First let's start out by creating a base class that can be used for any form we'll need. I'm going to name this class "ExternalFormBase" and it will implement IHttpHandler. Take a look at what we have at this point.
using System;
using System.Web;
using System.Web.UI;
public abstract class ExternalFormBase : IHttpHandler
{
#region IHttpHandler Members
public bool IsReusable
{
get { throw new Exception("The method or operation is not implemented."); }
}
public void ProcessRequest(HttpContext context)
{
throw new Exception("The method or operation is not implemented.");
}
#endregion
}
Notice a couple things. First I made this class abstract as it is serving as a base. Second, in order to implement IHttpHandler we have to declare a property called IsReusable of type Boolean and a method called ProcessRequest which takes an instance of the HttpContext class as an argument. I am going to set the IsReusable property to return false because I'm going to make the class stateful. So that gives us this code.
using System;
using System.Web;
using System.Web.UI;
public abstract class ExternalFormBase : IHttpHandler
{
#region IHttpHandler Members
public bool IsReusable
{
get { return false; }
}
public void ProcessRequest(HttpContext context)
{
throw new Exception("The method or operation is not implemented.");
}
#endregion
}
Now for the ProcessRequest method. This is where the meat of your code will be. I'm going to copy a reference of the context argument to a module level variable just so it doesn't need to be passed around. Then I'm going to step through each part that makes up the html of a page and write them to the output.
using System;
using System.Web;
using System.Web.UI;
public abstract class ExternalFormBase : IHttpHandler
{
#region Fields
HttpContext m_Context;
#endregion
#region Properties
abstract protected string Action { get; }
protected virtual HttpContext Context
{
get { return m_Context; }
}
public bool IsReusable
{
get { return false; }
}
#endregion
#region Methods
public virtual void ProcessRequest(HttpContext context)
{
m_Context = context;
context.Response.Write("<html>\r\n");
context.Response.Write("<head>\r\n");
WriteHeadContent();
context.Response.Write("</head>\r\n");
context.Response.Write("<body>\r\n");
context.Response.Write(string.Format("<form action=\"{0}\" method=\"post\" target=\"_top\">\r\n", Action));
WriteBodyContent();
context.Response.Write("</form>\r\n");
context.Response.Write("</body>\r\n");
context.Response.Write("</html>");
}
protected abstract void WriteBodyContent();
protected abstract void WriteHeadContent();
#endregion
}
Notice I stored the HttpContext as a field and exposed it through a protected property for all inheriting classes. I added an abstract property called "Action". This will be used to indicate the url that the form should post to. I also added two abstract methods called "WriteBodyContent" and "WriteHeadContent". The WriteHeadContent method is called from the ProcessRequest method in between where it writes the open and closing head tags. The WriteBodyContent is called in between writing the open and closing form tags. This allows the inheriting class to specify any links to create in the head that it needs and all inputs in the body. If you follow the logic through ProcessRequest you'll be able to see how the html is generated and sent to the output.
Before I move on to creating an inheriting class let me add a few helper methods. These will make it much simpler to write inputs into the html.
virtual protected void WriteInput(string name, string value)
{
WriteInput(name, name, value);
}
virtual protected void WriteInput(string name, string id, string value)
{
Context.Response.Write(string.Format("<input type=\"hidden\" id=\"{0}\" name=\"{1}\" value=\"{2}\" />\r\n", id, name, value));
}
virtual protected void WriteInput(Control control)
{
HtmlTextWriter writer = new HtmlTextWriter(new System.IO.StringWriter());
control.RenderControl(writer);
Context.Response.Write(writer.InnerWriter.ToString() + "\r\n");
}
These methods allow you to add one input to the output stream. Take a good look at each overload and you should be able to figure out how each one is used.
Now to create an extension. Let's go back to our paypal buy now form. This time we're going to pass a few variables in the querystring. Here is an example of how I would write the handler for paypal.
using System;
using System.Web;
using System.Web.UI.WebControls;
public class PayPalForm : ExternalFormBase
{
#region Properties
protected override string Action
{
get { return "https://www.paypal.com/cgi-bin/webscr"; }
}
#endregion
#region Methods
protected override void WriteBodyContent()
{
WriteInput("cmd", "_xclick");
WriteInput("business", "sample@sample.com");
WriteInput("item_name", Context.Request.QueryString["itemname"]);
WriteInput("item_number", Context.Request.QueryString["itemnumber"]);
WriteInput("amount", Context.Request.QueryString["amount"]);
ImageButton button = new ImageButton();
button.ImageUrl = "https://www.paypal.com/en_US/i/btn/x-click-but23.gif";
button.ID = "submit";
button.AlternateText = "Make payments with payPal - it's fast, free and secure!";
WriteInput(button);
}
protected override void WriteHeadContent()
{
}
#endregion
}
First, I overrode the Action property to return the correct url for paypal. I didn't add anything to the WriteHeadContent because I didn't need to add anything for this example. But you may want to add some style links for styling your buttons. The WriteBodyContent is where I added most code. You'll see that two inputs are hard-coded because they're static values. Best practice suggests I place the value in the appSettings section of web.config but for this example I will just hard-code them. Then I add three variable inputs which get their values from the querystring. Now in a real application you would never want to pass the amount in a querystring. That's quite a security breach. What you would do is pass the item number and then retrieve the other information from the database. I'll leave the code for that up to you.
Ok so with these two classes setup how do we consume them in our application. Make sure they're saved in the app_code directory then open your web.config file. Find your way to the system.web section. Inside that if you don't already have an httpHandler section add it. Then add an entry for your handler like this.
<httpHandlers>
<add path="buynow.ashx" type="PayPalForm" verb="*"/>
</httpHandlers>
The path is the actual path you will set as the source of the iframe. This can be anything you want but to make things easiest on yourself use an ashx extension. This extension doesn't require any extra configuration. Then specify the name of the new class we created. The verb attribute can be one of three values. GET, POST or *. The * means all types of requests.
Now go back to your aspx page. We'll change the source to our handler and add a few querystring values.
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<p>
This is a great product! You really should buy it.
</p>
<iframe src="buynow.ashx?itemname=Product+Name&itemnumber=0000&amount=35.00" style="border: none; height: 35px; overflow: hidden;"></iframe>
</form>
</body>
</html>
Run the page and press the button. You'll notice it passed the amount we specified in the querystring. Play around with this.
That's about all there is to it. You can take the customization from here. Let's recap one last time.
- Use an iframe element to display a button inside another form.
- Use an html page for static forms or an httphandler for dynamic ones.
- Be sure to set the target of the form to "_top"
- Configure your handler in web.config