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.

View Source improvements in IE8

March 27, 2009 at 10:38 PMBen

Compared to Firefox and Chrome, IE has always had a very plain rendering when viewing the source of an HTML page.  The source just shows up as plain text in Notepad.  HTML is of course just plain text, but Firefox and Chrome add coloring for matching HTML tags which makes looking at the source a little more pleasant.

IE8 now does what these others browsers have been doing.  The HTML source no longer is displayed in Notepad, but in an IE source viewing pop-up window.  This IE source viewer now does tag coloring and even includes line numbers.  This is a nice little improvement.

IE8 Source Viewer

I did notice one other interesting feature when doing a View Source in IE8.  On the File menu of the source viewer, if you select 'Save', you have a choice to save the HTML Source (nothing special here) or save the 'Formatted HTML View' (screenshot below).  This "save formatted HTML view" will create an HTML file of how IE8's source viewer is displaying the source -- including the tag coloring.  You can then open up that saved HTML file in any browser to have the source display exactly as it does in IE8's source viewer.

IE8 Source Viewer - Save as Formatted HTML View

The file size of the "formatted html view" is considerably larger than the size of the plain HTML source without the formatting.  For instance, for a particular 45 KB HTML page I tried this in, the formatted html view file is 452 KB.

I'm guessing the new color tags and syntax highlighting in the new source viewer was done via HTML markup.  So it probably wasn't a big deal for the IE team to just include this new save as 'Formatted HTML View' option -- since the formatted HTML source was already there.  In any event, it's nice it's there for whenever the need of the formatted html source could be used.

Posted in: General

Tags: , ,

BlogEngine.NET Widget: About Me

March 10, 2009 at 12:09 PMBen

Bloggers often have a few words they like to say about themselves.  With BlogEngine.NET, there are a few ways to achieve this.  In the default BE.NET installation, a TextBox widget titled "About the author" is already in the blog's sidebar.  The author can just click 'Edit' and quickly describe themselves using the WYSIWYG editor.  BE.NET users will find the TextBox widget is a very powerful widget because it's generic enough to display virtually anything.

Another option for About Me is to create a Page in BE.NET, and add a link on your blog to the About Me page.  The main difference with this 'Page' approach is the About Me content is only seen on the About Me page, rather than the widget approach where the About Me content is shown on every blog page in the sidebar.  With the Page approach, you also get a lot more room horizontally which is nice if you have some pictures or other space consuming content.  I'm currently using the Page approach for the About Me page on this blog.

A third approach which is very similar to using the TextBox widget described above is to use an "About Me" widget.  BE.NET allows each editor/administrator to create their own profile on the Profiles tab in the control panel.  On the Profile page, each editor/administrator can enter details such as their Name, Phone Number, Address, a Photo image, and last but not least, About Me!  The About Me box is a WYSIWYG editor.  Currently, profile information entered is hardly used anywhere within BE.NET.  The information is available, however, through the AuthorProfile class in BlogEngine.

The AboutMe widget displays the About Me text entered for a user on the Profiles tab in the control panel.

If multiple editors/administrators have been created on the Users tab in the control panel, a separate profile can be created for each one of these users on the Profiles tab.  Because multiple profiles may exist, the AboutMe widget needs to know which profile's About Me content it should display.  The edit control in the AboutMe widget allows the user to choose the profile to display the About Me content for.  Because the settings for each widget are stored independently of one another, multiple AboutMe widgets may be added to the blog -- one AboutMe widget for each profile.

The AboutMe widget consists of the 2 required files you find for widgets -- widget.ascx, widget.ascx.cs.  The edit control is made up of edit.ascx and edit.ascx.cs.  A download link for the entire widget is available at the bottom of this post if you are interesting in trying it out in your own BlogEngine.NET installation.

<%@ Control Language="C#" AutoEventWireup="true" CodeFile="widget.ascx.cs"
 Inherits="widgets_AboutMe_widget" %>

<asp:PlaceHolder runat="Server" ID="phAboutMe" />
#region Using

using System;
using System.Web;
using System.Web.UI;
using System.Collections.Specialized;
using BlogEngine.Core;

#endregion

public partial class widgets_AboutMe_widget : WidgetBase
{
    public override void LoadWidget()
    {
        string CacheKey = "widget_aboutme_" + this.WidgetID.ToString();
        string ProfileUserName = (string)HttpRuntime.Cache[CacheKey];

        if (ProfileUserName == null)
        {
            StringDictionary settings = GetSettings();

            if (settings.ContainsKey("UserName"))
                ProfileUserName = settings["UserName"];

            if (ProfileUserName == null)
                ProfileUserName = string.Empty;

            HttpRuntime.Cache[CacheKey] = ProfileUserName;
        }

        if (string.IsNullOrEmpty(ProfileUserName))
        {
            // Find the first profile with About Me content.
            foreach (AuthorProfile profile in AuthorProfile.Profiles)
            {
                if (!string.IsNullOrEmpty(profile.AboutMe))
                { 
                    ProfileUserName = profile.UserName;
                    HttpRuntime.Cache[CacheKey] = ProfileUserName;
                    break;
                }
            }
        }

        if (!string.IsNullOrEmpty(ProfileUserName))
        {
            AuthorProfile profile = AuthorProfile.GetProfile(ProfileUserName);
            if (profile != null && !string.IsNullOrEmpty(profile.AboutMe))
            {
                phAboutMe.Controls.Add(new LiteralControl(profile.AboutMe));
            }
        }        
    }

    public override string Name
    {
        get { return "AboutMe"; }
    }

    public override bool IsEditable
    {
        get { return true; }
    }
}
<%@ Control Language="C#" AutoEventWireup="true" CodeFile="edit.ascx.cs"
Inherits="widgets_AboutMe_edit" %>

<div>

    <h3>Select the Profile to show About Me for</h3>
    
    <div id="noProfilesAvailable" runat="server">
        No profiles have yet been created.
    </div>
    
    <asp:RadioButtonList ID="rblProfileToDisplay"
     runat="server"
     DataTextField="UserName"
     DataValueField="UserName">
    </asp:RadioButtonList>
    
    <br />
    
    <div>
        Note: Make sure you have entered About Me content for
        the selected user above.  This content should be
        entered on the Profiles tab in the Control Panel.
    </div>

</div>
#region Using

using System;
using System.Collections.Specialized;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using BlogEngine.Core;

#endregion

public partial class widgets_AboutMe_edit : WidgetEditBase
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (!Page.IsPostBack)
        {
            string SelectedUserName = null;

            StringDictionary settings = GetSettings();
            if (settings.ContainsKey("UserName"))
                SelectedUserName = settings["UserName"];

            if (!string.IsNullOrEmpty(SelectedUserName))
            {
                if (AuthorProfile.GetProfile(SelectedUserName) != null)
                    rblProfileToDisplay.SelectedValue = SelectedUserName;
            }

            rblProfileToDisplay.DataSource = AuthorProfile.Profiles;
            rblProfileToDisplay.DataBind();

            noProfilesAvailable.Visible = rblProfileToDisplay.Items.Count == 0;
        }
    }

    public override void Save()
    {
        StringDictionary settings = GetSettings();
        settings["UserName"] = rblProfileToDisplay.SelectedValue;
        SaveSettings(settings);
        HttpRuntime.Cache.Remove("widget_aboutme_" + this.WidgetID.ToString());
    }
}


AboutMe Widget Download (2.4 kb)

Posted in: Development

Tags: ,