Sometimes having a single page at the end of an endpoint isn't enough. Many times with working with a web form, you aren't actually working with a single logical unit. Sometimes there are different steps to the process which your web forms represents. Perhaps it's an account wizard, a checkout workflow, or just a contact page with an input and "thank you" page. In each of these situations you aren't working with a simple web form, but a more complex superposition of pages which act as one endpoint to the user.
The common solution for this type of situation is a design pattern which splits each part of the sequence into its own page with a centralized controller moving the user around. Themelia Pro implement this abstract design pattern with a concept called a Sequence. This concept, though extremely simple, can seriously improve the manageability of your forms. In fact, sequences aren't just for wizards, they are also for saving multiple designs of pages.
Once you get used to sequences you may actually find yourself using them more than classic web forms. Because of this, web site structure sample shown in the introduction to Themelia Pro web site design shows how pages are the minority in Themelia Pro web sites. Where as sequences become the overall helper for various related pages, individual pages are left for simple single-view pages like a FAQ or an address list.
Creating a Sequence
To begin the discussion on sequences, look at the following sample:
Here we see three sequences: Checkout, PasswordReset, and Security. For now, we are going to zoom out attention into the darkened region, namely, the PasswordReset sequence.
A sequence is created by creating sequence page, containing a sequence controller, which contains a series of sequence views, each on a sequence user control, with the option of adding sequence designs.
In terms of actual implementation, you create the sequence structure by creating the physical page, controller, and user controls. However, you setup a sequence either in declarative code or in configuration, where declarative code takes precidence. Setup via configuration is discussed in Setup via Configuration.
Let's see the actual setup of our sample PasswordReset sequence:
<%@ Page CodeFile="PasswordReset.aspx.cs" Inherits="Sequence.PasswordReset" %>
<t:SequenceController SequenceName="PasswordReset" ID="Controller" runat="server">
<Views>
<t:SequenceView ID="Input" runat="server" />
<t:SequenceView ID="Success" runat="server" />
</Views>
</t:SequenceController>
This page represents what just about every sequence will look like: you have views and a sequence controller with an ID of "Controller". In this controller there is a required attribute called SequenceName. This is the name of the sequence and, therefore, also the name of the folder which contains the views.
This sequence has two SequenceView controls. Each view ID matches to a user control of the same name under the folder declared by the SequenceName property. For example, in this case, the view controls are "PasswordReset\Input.ascx" and "PasswordReset\Success.ascx".
That's a complete sequence. Now let's move to the code behind of the sequence page itself: sequences inherit from Themelia.Web.Controls.SequencePage and are required to override the SelectInitView method. This method allows you to set the initial view based on whatever login you decide.
namespace Sequence
{
public partial class ProblemReset : Themelia.Web.Controls.SequencePage
{
//- @SelectInitView -//
public override String SelectInitView()
{
return "Input";
}
}
}
The following example is a contact sequence that should give you a better idea of what kind of logic we're talking about:
<t:SequenceController SequenceName="Contact" ID="Controller" runat="server">
<Views>
<t:SequenceView ID="ProblemInput" runat="server" />
<t:SequenceView ID="ProblemSaved" runat="server" />
<t:SequenceView ID="MessageInput" runat="server" />
<t:SequenceView ID="MessageSent" runat="server" />
</Views>
</t:SequenceController>
Here's the code behind:
namespace Sequence
{
public partial class Contact : Themelia.Web.Controls.SequencePage
{
//- @SelectInitView -//
public override String SelectInitView()
{
String firstUrlPart = Themelia.Web.Http.GetHttpPart(Themelia.Web.Http.Position.First);
if (firstUrlPart == "suggestion" || firstUrlPart == "contact")
{
return "MessageInput";
}
else if (firstUrlPart == "problem")
{
return "ProblemInput";
}
//+
return "MessageInput";
}
}
}
As you can see, when the sequence loads it checks to see if the first part of the URL (http://www.tempuri.org/thispart/) is "suggestion" or "contact". If so, then the user wants to send a suggestion or contact someone.
However, this same sequence handles bug submission as well. Perhaps when an uncaught exception is thrown somewhere in the web site, the user is redirected here to describe what he or she was doing. If the user is accessing the /problem/ path, then the sequence is told to load that.
It's important to note that if an incorrect view name is returned, then no view will show. So, if you want a view to show, then be sure to set a default ("MessageInput" in the above example).
View Setup
What's exactly inside a sequence view? The view declared by the controller maps to a physical user control. This user control must inherit from Themelia.Web.Controls.SequenceUserControl. That's the only requirement. Returning to the password reset sequence example, here is the Input.ascx file:
<%@ Control AutoEventWireup="false" Language="C#" Inherits="Sequence.ResetPassword.Input" CodeFile="Input.ascx.cs" %>
<h1>Password Reset</h1>
<div class="contact-input">
<strong><label for="<%=txtEmail.ClientID %>">E-mail Address</label></strong><br />
<asp:TextBox ID="txtEmail" runat="server"></asp:TextBox><br />
<br />
<asp:Button ID="btnSubmit" runat="server" Text="Go to step 2"></asp:Button>
<span class="error-message"><asp:Literal ID="litError" runat="server"></asp:Literal></span>
</div>
Views are discussed in more detail in Working with Views.
Endpoint Registration
At this point it would be very valuable to reference
Reusing User Controls
There's no reason to create a user control for a bunch of different things if they are actually the same. Image that you had two purposes for an input and success view combo, but the differences in the two purposes requires only that there be a very minor difference between them.
For example, you are working with a Process sequence that has two views: input and saved. Furthermore, you "process" two types of things, but the input content for each as well as the saved content for each are the same except for some textual differences (or perhaps a bit of logic setting a control as hidden, or whatever).
In this situation you can could a single input view and a single success view and simply register them multiple times using the ControlName attribute. Here's an example:
<t:SequenceController SequenceName="Process" ID="Controller" runat="server">
<Views>
<t:SequenceView ID="Input" runat="server">
<t:SequenceView ID="Saved" runat="server">
<t:SequenceView ID="ComplexInput" ControlName="Input" runat="server">
<t:SequenceView ID="ComplexSaved" ControlName="Saved" runat="server">
</Views>
</t:SequenceController>
In this case, the "Process\Input.ascx" and "Process\Saved.ascx" user controls are each used twice. The logic to figure out what's what would be inside of each control. Say you had the following configuration:
<themelia.web>
<webDomains>
<add defaultPage="/Page_/Security/Home.aspx">
<handlers>
<add matchType="contains" text="/process/complex/" type="SequenceAlias" parameter="{Process}" />
<add matchType="contains" text="/process/single/" type="SequenceAlias" parameter="{Process}" />
</handlers>
</add>
</webDomains>
</themelia.web>
With this, you can easily do something like the following in Input.ascx or Saved.ascx to centralize your views:
//- #OnInit -//
protected override void OnInit(EventArgs e)
{
if (Themelia.Web.Http.GetHttpPart(Themelia.Web.Http.Position.Second) == "single")
{
ddlMyComplexDropDownList.Visible = false;
litMyComplexText = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.";
}
//+
base.OnInit(e);
}
Note that you could actually use the ControlName attribute for all the views. This allows you to completely decouple your view names from your control names.
Setting the Page Title
Since a sequence consists of multiple views, each view could potentially require it's own page title. Setting the page title from a user control is as easy as setting the PageTitle property. It's important to not that in the scope of the ASP.NET page life cycle, this title can be set as late as the OnPreRender method, but no later. Here's an example:
namespace Sequence.Contact
{
public partial class MessageSent : Themelia.Web.Controls.SequenceUserControl
{
//- #OnPreRender -//
protected override void OnPreRender(EventArgs e)
{
this.PageTitle = "Thank you for your message";
//+
base.OnPreRender(e);
}
}
}
You can base the title on whatever logic you choose. For example, look at the following sample web site structure representing four fictional products:
- http://www.mydomain.com/product/astra/feedback/
- http://www.mydomain.com/product/supra/feedback/
- http://www.mydomain.com/product/ipsum/feedback/
- http://www.mydomain.com/product/lorem/feedback/
In this scenario we can easily automate our page title by creating a common base class for each user control (localization of the word "Feedback" would obviously be added in production):
public class FeedbackSequenceUserControl : Themelia.Web.Controls.SequenceUserControl
{
//- #OnPreRender -//
protected override void OnPreRender(System.EventArgs e)
{
this.PageTitle = Themelia.Case.GetPascalCase(Http.GetHttpPart(Http.Position.First)) + " Feedback";
//+
base.OnPreRender(e);
}
}
This custom user control will set the title to the PascalCased product name plus the word "Feedback". So http://www.mydomain.com/product/astra/feedback/ will automatically have a page title of "Astra Feedback".
As you can see in this scenario that sequences aren't just for wizards. They can provide easy management and organization to related or patterened pages.
ASP.NET Control Registration
If you are going to be using sequences with any regularity, then you should probably register them into your web.config file. Here's a snippet of that registration for quick reference:
<system.web>
<pages>
<controls>
<add tagPrefix="t" namespace="Themelia.Web.Controls" assembly="Themelia.Web"/>
</controls>
</pages>
</system.web>