ASP.NET LoginStatus Control – Overriding the ReturnURL in generated QueryString

asp.netforms-authentication

I have a website that uses the ASP.NET Login Controls and Forms Authentication. I've placed the asp:LoginStatus control inside another Web User Control that manages the display of the Header portion of my site.

The issue I have is that the ReturnURL presented by the LoginStatus control references the path to the Header Control, not the page the user is currently on. This is probably due to the class hierarchy and that the Header Control (ascx) actually uses Server.Execute on an .aspx file to generate the HTML. This is a work around to avoid the issue of not being allowed to have more than one server-side form on a page.

So the actual class Hierarchy of the Page is as follows:

Default.aspx - Uses Page.Master
Page.Master includes <foo:Header> 
    (with a reference to "~/Controls/Components/Header.ascx")
Header.ascx simply includes an <asp:Literal> 
    on Page_Load performs a Server.Execute ("~/Controls/Pages/Header.aspx") 
    and writes the content out to the Literal
Header.aspx includes <asp:LoginStatus>

When the user clicks on the Login link they are correctly redirected to Login.aspx, however the ReturnURL displayed is (incorrectly – although I can understand why) "ReturnUrl=%2fControls%2fPages%2fHeader.aspx".

Once on the Login page I can quite happily handle the LoggedIn event to correctly redirect the user to the right place. What I would like to do is either:
1) Remove the ReturnURL from the query string altogether
2) Be able to control the ReturnURL when the LoginStatus Control is rendered.

I've done some Reflector-ing of the System.Web.UI.WebControls.LoginStatus and it appears that it is hard-coded to always use a ReturnURL, based on the following code:

private string NavigateUrl
{
    get
    {
        if (!base.DesignMode)
        {
            return FormsAuthentication.GetLoginPage(null, true);
        }
        return "url";
    }
}

It is always setting reuseReturnURL to true.

Possibly, my only choice is to roll my own LoginStatus control?

[EDIT: Originally for the sake of brevity, I omitted the following details]

Here is a really simple example of what I am trying to acheive:

Web Application Project has the following structure:
WebSite
– Controls
– Components
– Footer.ascx
– Header.ascx
– MasterPages
– Site.Master
– Default.aspx
– Login.aspx

The page markups are below if you're interested.

I've created the Web User Control for separation of concerns, however on the Login page to use the asp:Login controls they need to be nested in a (server-side) form. The asp:LoginStatus control also needs to be nested in a (server-side) form. As you can't have more than one server-side form on a page, this breaks.

Also, the answer is not just to suppress the LoginStatus control on the Login page. Imagine if I just wanted to add a little Search Control on the main page, which would also rely on a (server-side) form. Hence the reason for using a Server.Execute and generating a page from an ASPX. This "tricks" .NET into allowing multiple server-side forms on a page. (Don't ask me how… I don't know!)

Maybe my entire architecture design is wrong, but how to others have multiple Web User Controls on a page that require server-side forms? Or don't they?

Site.Master markup:

<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Site.master.cs" Inherits="WebSite.MasterPages.Site" %>

<%@ Register TagPrefix="bs" TagName="Footer" Src="~/Controls/Components/Footer.ascx" %>
<%@ Register TagPrefix="bs" TagName="Header" Src="~/Controls/Components/Header.ascx" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title></title>
    <asp:ContentPlaceHolder ID="head" runat="server">
    </asp:ContentPlaceHolder>
</head>
<body id="Body" runat="server">
    <div id="container">
        <!-- start header -->
        <bs:Header ID="Header" runat="server" />
        <!-- end header -->
        <div id="central">
            <div id="main">
                <asp:PlaceHolder ID="MainContentPlaceHolder" runat="server">
                    <!-- start main content -->
                    <div>
                        <asp:ContentPlaceHolder ID="MainContent" runat="server" />
                    </div>
                    <!-- end main content -->
                </asp:PlaceHolder>
            </div>
        </div>
        <!-- start footer -->
        <bs:Footer ID="Footer" runat="server" />
        <!-- end footer -->
    </div>

</body>
</html>

Default.aspx markup:

<%@ Page MasterPageFile="~/MasterPages/Site.Master" Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebSite._Default" %>

<asp:Content ID="Content1" runat="server" ContentPlaceHolderID="MainContent">
    Main Body Content <br />
    <br />

</asp:Content>

Header.ascx markup:

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="Header.ascx.cs" Inherits="WebSite.Controls.Components.Header" %>
<div id="header">
    Header Content <br />
    <div id="loginstatus">
        <form id="Form1" runat="server">
        <asp:LoginView ID="displayloginname" runat="server">
            <AnonymousTemplate>
                <a href="../../Registration.aspx">Register</a>
            </AnonymousTemplate>
            <LoggedInTemplate>
                Welcome
                <asp:LoginName runat="server" ID="ctlLoginName" />
            </LoggedInTemplate>
        </asp:LoginView>
        <asp:LoginStatus ID="displayloginstatus" runat="server" LoginText="Login" LogoutPageUrl="~/Default.aspx"
            LogoutAction="Redirect" />
        </form>

        <br />
    </div>
</div>

Footer.ascx markup:

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="Footer.ascx.cs" Inherits="Website.Controls.Components.Footer" %>

        <div id="footer">
            Footer Content
            <ul class="links">
            <asp:PlaceHolder ID="ListItems" Runat="server">
                <li><a runat="server" id="HomeLink" href="~/">Home</a></li>
                <li><a runat="server" href="~/" ID="A1">About Us</a></li>
                <li><a id="A2" runat="server" href="~/">Contact Us</a></li>
                <li><a id="A3" runat="server" href="~/">Privacy Policy</a></li>
                <li><a id="A4" runat="server" href="~/">Accessibility Policy</a></li>
                <li><a id="A5" runat="server" href="~/">Legal Notices</a></li>
                <li><a id="A6" runat="server" href="~/">Sitemap</a></li>
                <li><a id="A7" runat="server" href="~/">RSS Feeds</a></li>
            </asp:PlaceHolder>
            </ul>

        </div>

Login.aspx markup:

<%@ Page MasterPageFile="~/MasterPages/Site.Master" Language="C#" AutoEventWireup="true" CodeBehind="Login.aspx.cs" Inherits="Website.Login" %>

<asp:Content ID="Content1" runat="server" ContentPlaceHolderID="MainContent">
    Main Body Content <br />
    <br />
    <form id="form1" runat="server">
    <div>

        <asp:Login ID="Login1" runat="server">
        </asp:Login>

    </div>
    </form>
</asp:Content>

Best Answer

I see two options:

  1. Put your form tag in your master page. Just below the <div id="container"> and wrapping all content in that div.
  2. Change your header to not require an ASP.NET form. Not sure if the LoginView or LoginStatus both require an ASP.NET form. And then you can leave the form tag for the aspx files that use the master page (like you have in Login.aspx). This is what I prefer is to only put form tags in aspx pages (not in master files) and make sure headers and footer do not need an ASP.NET form (they could use a regular form tag just not with runat=server).
Related Topic