Multiple forms; content outside the ASP.NET form

Bryan Drenner asked on March 19, 2015 15:52

I want to have multiple forms on a single page, for example, a login form in the site header, a search form in the site footer, and a contact form as the page-level content. I'll have an AngularJS controller assigned to each. Since Kentico's web parts rely on Web Forms' plumbing, it appears that pages must be wrapped by a single form entirely. This is semantically inaccurate, and it makes it impossible to utilize multiple forms on a single page, as allowed by the HTML5 standard. One solution is to put ContentPlaceHolders before, inside, and after the ASP.NET form. However, Kentico web parts fail when placed in the placeholders outside the ASP.NET form, which complicates managing that content. Finally, Angular doesn't play well with multiple conceptual forms being wrapped by a single form element.

I discovered a minor hack to allow multiple forms while managing content outside the ASP.NET form using standard Kentico web parts. On the master page, in the form, I wrap the main ContentPlaceHolder in a PlaceHolder. I add an empty PlaceHolder just after the form. My master page looks like this:

...
<form id="Form1" novalidate runat="server">
    <asp:PlaceHolder runat="server" ID="plcManagers">
        <ajaxToolkit:ToolkitScriptManager ID="manScript" runat="server" LoadScriptsBeforeUI="false" EnableViewState="false" ScriptMode="Release" />
        <cms:CMSPortalManager ID="manPortal" runat="server" EnableViewState="false" />
    </asp:PlaceHolder>
    <asp:PlaceHolder ID="LeaperSource" runat="server">
        <asp:ContentPlaceHolder ID="InAspForm" runat="server" />
    </asp:PlaceHolder>
</form>
<asp:PlaceHolder ID="LeaperDestination" runat="server" />
...

Most of that is boilerplate provided by the Kentico documentation. However, notice the LeaperSource and LeaperDestination PlaceHolders. In the code-behind, I move all controls from LeaperSource to LeaperDestination, as long as the page is not being viewed in the backoffice.

protected void Page_PreRender(object sender, EventArgs e)
{
    // ... return early if PortalContext indicates that the
    // page is being viewed in the backoffice ...

    foreach (Control control in this.LeaperSource.Controls)
    {
        this.LeaperDestination.Controls.Add(control);
    }
    this.LeaperSource.Controls.Clear();
}

I checked, and Editable Regions still function properly, no matter how the page is being viewed. The only downside I can think of is that the published page's controls do not support View State. Considering the ASP.NET community's consensus on the obsolescence of View State, that's probably a good thing.

If you must use View State, then you could still use my technique, but provide three ContentPlaceHolders inside the ASP.NET form:

<asp:PlaceHolder ID="PreFormLeaperDestination" runat="server" />
<form id="Form1" novalidate runat="server">
    <asp:PlaceHolder runat="server" ID="plcManagers">
        <ajaxToolkit:ToolkitScriptManager ID="manScript" runat="server" LoadScriptsBeforeUI="false" EnableViewState="false" ScriptMode="Release" />
        <cms:CMSPortalManager ID="manPortal" runat="server" EnableViewState="false" />
    </asp:PlaceHolder>
    <asp:PlaceHolder ID="PreFormLeaperSource" runat="server">
        <asp:ContentPlaceHolder ID="BeforeAspForm" runat="server" />
    </asp:PlaceHolder>
    <asp:ContentPlaceHolder ID="InAspForm" runat="server" />
    <asp:PlaceHolder ID="PostFormLeaperSource" runat="server">
        <asp:ContentPlaceHolder ID="AfterAspForm" runat="server" />
    </asp:PlaceHolder>
</form>
<asp:PlaceHolder ID="PostFormLeaperDestination" runat="server" />

In this case, the code-behind would look like this:

protected void Page_PreRender(object sender, EventArgs e)
{
    // ... return early if PortalContext indicates that the
    // page is being viewed in the backoffice ...

    foreach (Control control in this.PreFormLeaperSource.Controls)
    {
        this.PreFormLeaperDestination.Controls.Add(control);
    }
    foreach (Control control in this.PostFormLeaperSource.Controls)
    {
        this.PostFormLeaperDestination.Controls.Add(control);
    }
    this.PreFormLeaperSource.Controls.Clear();
    this.PostFormLeaperSource.Controls.Clear();
}

Can anyone think of any other caveats to this technique?

   Please, sign in to be able to submit a new answer.