May 2012
Volume 27 Number 05
ASP.NET - Introducing the Navigation for ASP.NET Web Forms Framework
By Graham Mendick | May 2012
The Navigation for ASP.NET Web Forms framework, an open source project hosted at navigation.codeplex.com, lets you write Web Forms code with unit test coverage and adherence to don’t repeat yourself (DRY) principles that would make an ASP.NET MVC application green with envy.
Although there has been some abandonment of Web Forms in favor of MVC, with some developers growing tired of large codebehinds unreachable by unit tests, this new framework—in conjunction with data binding—makes a compelling argument for taking a fresh look at Web Forms.
Data binding with ObjectDataSource controls has been around since Visual Studio 2005, allowing for cleaner codebehinds and data-retrieval code to be unit tested, but there have been some problems inhibiting its uptake—for example, raising an exception was the only way of reporting a business validation failure back to the UI.
The vast majority of the Web Forms development effort for the upcoming release of Visual Studio has been invested in data binding, bringing across model binding concepts from MVC to resolve these issues—for example, the introduction of model state addresses the business validation failure communication issue. However, two thorns remain in the side of data binding related to navigation and data passing—but they can be painlessly extracted using the Navigation for ASP.NET Web Forms framework (which I’ll call the “Navigation framework” hereafter for brevity).
The first thorn is that there’s no abstraction for navigation logic, unlike in MVC where it’s encapsulated in the controller method return types. This results in redirect calls inside data-bound methods, preventing them from being unit tested. The second thorn is that the type of a parameter of an ObjectDataSource determines where its value comes from—for example, a QueryStringParameter always gets its data from the query string. This prevents the same data source from being used in different navigational contexts—such as postback and non-postback—without substantial logic in the dreaded codebehind.
The Navigation framework removes these thorns by taking a holistic approach to navigation and data passing. Regardless of the type of navigation being performed—be it hyperlink, postback, AJAX history or unit test—the data being passed is always held in the same way. In future articles, I’ll show how this leads to empty codebehinds with fully unit-tested data retrieval and navigation logic and also to Search Engine Optimization (SEO)-friendly, progressively enhanced single-page applications with no code duplication for JavaScript-enabled and -disabled scenarios. This article introduces the Navigation framework and demonstrates some of its basic—but key—concepts by building a sample Web application.
Sample Application
The sample Web application is an online survey. This survey has only two questions and displays a “thank you” message upon completion. Each question is represented by a separate ASPX page called Question1.aspx and Question2.aspx respectively, and the “thank you” message has its own page called Thanks.aspx.
The first question asked is, “Which ASP.NET technology are you currently using?” to which the possible answers are either “Web Forms” or “MVC.” So to Question1.aspx I’ll add the question and the hardcoded radio button answers:
<h1>Question 1</h1>
<h2>Which ASP.NET technology are you currently using?</h2>
<asp:RadioButtonList ID="Answer" runat="server">
<asp:ListItem Text="Web Forms" Selected="True" />
<asp:ListItem Text="MVC" />
</asp:RadioButtonList>
The second question, “Are you using the Navigation for ASP.NET Web Forms framework?” has answers of “Yes” or “No” and is marked up in a similar fashion.
Getting Started
The most straightforward way to set up the survey Web project to use the Navigation framework is to install it using the NuGet Package Manager. Running the command “Install-Package Navigation” from within the Package Manager Console will add the required reference and configuration. If you’re not using Visual Studio 2010, manual setup instructions can be found at navigation.codeplex.com/documentation.
Navigation Configuration
The Navigation framework can be thought of as a state machine, where each different state represents a page and moving from one state to another—or navigating between pages—is termed a transition. This predefined set of states and transitions is configured in the StateInfo.config file created by the NuGet installation. Without this underpinning configuration, running the survey application will throw an exception.
Because states are essentially just pages, the survey application requires three states, one for each of its three pages:
<state key="Question1" page="~/Question1.aspx">
</state>
<state key="Question2" page="~/Question2.aspx">
</state>
<state key="Thanks" page="~/Thanks.aspx">
</state>
From now on I’ll refer to the different states by their key names, Question1, Question2 and Thanks, rather than by the pages they represent.
Because transitions describe the possible navigations between states, the survey application requires two transitions. One is for the navigation from Question1 to Question2 and another is for the navigation from Question2 to Thanks. A transition appears as a child of the state being exited and points, via its “to” attribute, at the state being entered:
<state key="Question1" page="~/Question1.aspx">
<transition key="Next" to="Question2"/>
</state>
<state key="Question2" page="~/Question2.aspx">
<transition key="Next" to="Thanks"/>
</state>
<state key="Thanks" page="~/Thanks.aspx">
</state>
Dialogs are the final element of the configuration and represent a logical grouping of states. The survey application only requires one dialog because Question1, Question2 and Thanks are effectively a single navigation path. The dialog’s “initial” attribute must point to the starting state—that is, Question1:
<dialog key="Survey" initial="Question1" path="~/Question1.aspx">
<state key="Question1" page="~/Question1.aspx">
<transition key="Next" to="Question2"/>
</state>
<state key="Question2" page="~/Question2.aspx">
<transition key="Next" to="Thanks"/>
</state>
<state key="Thanks" page="~/Thanks.aspx">
</state>
</dialog>
You’ll notice that each dialog, state and transition has a key attribute. I chose to name the state keys after the page names, but this isn’t necessary. Note, though, that all keys must be unique within their parent; for example, you can’t have sibling states with the same key.
With Question1.aspx as the start page, the survey application now starts successfully in the Question1 state. However, the survey remains stuck in this state because there’s no way to progress to Question2.
Navigation
It’s useful to split the different sorts of Web Forms navigation into two camps. The non-postback camp is where control is passed from one ASPX page to another and takes the form of hyperlinks, redirects or transfers. The postback camp is where control remains on the same page and takes the form of postbacks, partial page requests or AJAX history. This second kind will be examined in a future article discussing the single-page interface pattern. In this article, I’ll focus on the first type of navigation.
To move between pages, a URL must be built. Prior to Visual Studio 2008, the only option was to manually construct URLs from hardcoded ASPX page names, causing tight coupling between pages that made applications brittle and hard to maintain. The introduction of routing alleviated this problem, with configurable route names used in place of page names. However, the fact that routing throws an exception if used outside a Web environment—combined with routing’s resistance to mocking—makes it an enemy of unit testing.
The Navigation framework retains the loose coupling provided by routing and is a friend of unit testing. Similar to the use of route names, instead of hardcoding ASPX page names, it’s the dialog and transition keys configured in the preceding section that are referenced in code; the state navigated to is determined by the respective “initial” and “to” attributes.
Returning to the survey, the Next transition key can be used to move from Question1 to Question2. I’ll add a Next button to Question1.aspx and the following code to its associated click handler:
protected void Next_Click(object sender, EventArgs e)
{
StateController.Navigate("Next");
}
The key passed in to the Navigate method is matched against the configured child transitions of the Question1 state and then the state identified by the “to” attribute is displayed—that is, Question2. I’ll add the same button and handler to Question2.aspx. If you run the survey, you’ll find you can navigate through the three states by clicking the Next buttons.
You might have noticed the second question is Web Forms-specific and, as such, is irrelevant when “MVC” is selected as the first answer. The code needs changing to handle this scenario, navigating directly from Question1 to Thanks and bypassing Question2 entirely.
The current configuration doesn’t allow navigation from Question1 to Thanks because the only transition listed is to Question2. So I’ll change the configuration by adding a second transition below the Question1 state:
<state key="Question1" page="~/Question1.aspx">
<transition key="Next" to="Question2"/>
<transition key="Next_MVC" to="Thanks"/>
</state>
With this new transition in place it’s simple to adjust the Next button click handler to pass a different transition key depending on the answer chosen:
if (Answer.SelectedValue != "MVC")
{
StateController.Navigate("Next");
}
else
{
StateController.Navigate("Next_MVC");
}
A survey wouldn’t be much good if it didn’t allow the user to change answers. Currently there’s no way to return to a previous question (aside from the browser back button). To navigate back you might think you need to add two transitions below Thanks, pointing to Question1 and Question2, and another below Question2, pointing to Question1. Although this would work, it’s unnecessary because back navigation comes free with the Navigation framework.
Breadcrumb navigation is a set of links providing access to each previous page the user visited in reaching the current one. Web Forms has breadcrumb navigation built into its site map functionality. However, because site maps are represented by a fixed navigational structure, for a given page these breadcrumbs are always the same regardless of the route taken. They can’t handle situations like that found in the survey where the route to Thanks sometimes excludes Question2. The Navigation framework, by keeping track of states visited as navigations occur, builds up a breadcrumb trail of the actual route taken.
To demonstrate, I’ll add a hyperlink to Question2.aspx and in the codebehind programmatically set its NavigateUrl property using back navigation. A distance parameter must be passed indicating how many states to go back to, a value of 1 meaning the immediate predecessor:
protected void Page_Load(object sender, EventArgs e)
{
Question1.NavigateUrl = StateController.GetNavigationBackLink(1);
}
If you run the app and answer “Web Forms” to question one you’ll see the hyperlink on Question2.aspx takes you back to the first question.
I’ll do the same for Thanks.aspx, although it’s a bit trickier because two hyperlinks are needed (one for each question), and the user may not have seen both questions—that is, if he answered “MVC” to the first. The number of precedent states can be checked before deciding how to set up the hyperlinks (see Figure 1).
Figure 1 Dynamic Back Navigation
protected void Page_Load(object sender, EventArgs e)
{
if (StateController.CanNavigateBack(2))
{
Question1.NavigateUrl = StateController.GetNavigationBackLink(2);
Question2.NavigateUrl = StateController.GetNavigationBackLink(1);
}
else
{
Question1.NavigateUrl = StateController.GetNavigationBackLink(1);
Question2.Visible = false;
}
}
The survey is now functional, allowing you to fill in questions and amend previous answers. But there’s little point to a survey if these answers aren’t put to use. I’ll show how they can be passed from Question1 and Question2 to Thanks, where they’ll be displayed in summary form.
Data Passing
There are as many different ways of passing data in Web Forms as there are ways to navigate. With non-postback navigation, where control is passed from one page to another (via hyperlink, redirect or transfer), query string or route data can be used. With postback navigation, where control remains on the same page (via postback, partial page request or AJAX history), control values, view state or event arguments are the likely candidates.
Prior to Visual Studio 2005, codebehinds were burdened with handling this passed-in data, so they swelled with value-extraction and type-conversion logic. Their load was considerably lightened with the introduction of data source controls and select parameters (“value providers” in the next version of Visual Studio). However, these select parameters are tied to a specific data source and they can’t dynamically switch sources depending on the navigational context. For example, they can’t retrieve their values alternatively from a control or from the query string depending on whether it’s a postback or not. Working around these limitations causes code to leak back into the codebehind, reverting back to square one of bloated and untestable codebehinds.
The Navigation framework avoids such problems by providing a single data source regardless of the navigation involved, called state data. The first time a page is loaded, the state data is populated with any data passed during the navigation, in a fashion similar to query string or route data. A marked difference, however, is that state data isn’t read-only, so as subsequent postback navigations occur, it can be updated to reflect the page’s current incarnation. This will prove beneficial when I revisit back navigation toward the end of this section.
I’ll change the survey so the answer to the first question is passed to the Thanks state where it will be redisplayed to the user. Data is passed while navigating via a collection of key-value pairs, called NavigationData. I’ll change the Next click handler of Question1.aspx so the answer to the first question is passed to the next state:
NavigationData data = new NavigationData();
data["technology"] = Answer.SelectedValue;
if (Answer.SelectedValue != "MVC")
{
StateController.Navigate("Next", data);
}
else
{
StateController.Navigate("Next_MVC", data);
}
This NavigationData passed during navigation is used to initialize the state data that’s made available to the next state via the Data property on the StateContext object. I’ll add a Label to Thanks.aspx and set its Text property to display the answer passed in:
Summary.Text = (string) StateContext.Data["technology"];
If you run the survey, you’ll notice this summary information is only displayed when the answer to the first question is “MVC”; an answer of “Web Forms” is never shown. This is because NavigationData is only available to the next state, but not to any states reached as a result of subsequent navigation. So an answer of “Web Forms” is present in the Question2 state data, but isn’t available by the time Thanks is reached. One way to solve this is to change Question2.aspx so that it relays the answer to the first question—that is, it takes the answer out of its state data and passes it to Thanks when it navigates:
NavigationData data = new NavigationData();
data["technology"] = StateContext.Data["technology"];
StateController.Navigate("Next", data);
This approach isn’t ideal because it couples Question1 and Question2 together, forcing the latter state to be aware of the data being passed in by the former. For example, a new question can’t be inserted between the first and second without a corresponding change to Question2.aspx. A future-proof implementation involves creating a new NavigationData containing all of the Question2 state data; this is achieved by passing true to the NavigationData constructor:
NavigationData data = new NavigationData(true);
StateController.Navigate("Next", data);
Another key difference between state data and query string or route data is that with state data you’re not restricted to passing strings. Rather than pass the answer as a string, as was done for Question1, for Question2 I’ll pass a bool to Thanks, with a value of true corresponding to “Yes”:
NavigationData data = new NavigationData(true);
data["navigation"] = Answer.SelectedValue == "Yes" ? true : false;
StateController.Navigate("Next", data);
You can see its data type is preserved when it’s retrieved from the Thanks state data:
Summary.Text = (string) StateContext.Data["technology"];
if (StateContext.Data["navigation"] != null)
{
Summary.Text += ", " +
(bool) StateContext.Data["navigation"];
}
The survey is complete, except for one problem: Answers to questions aren’t retained when using the back navigation hyperlinks. For example, when returning to Question1 from Thanks the context is lost, so the default “Web Forms” radio button is always selected regardless of the answer given.
In the previous section you saw the benefit of back navigation over static site map breadcrumbs. Another limitation of the breadcrumbs generated by the site map is that they don’t carry any data. This means following them can lose contextual information. For example, they can’t pass the “MVC” answer previously selected when they return to Question1 from Thanks. The Navigation framework, by keeping track of the state data associated with the states visited as navigations occur, builds up a context-sensitive breadcrumb trail. During a back navigation this state data is restored, allowing the page to be recreated exactly as before.
Armed with context-sensitive back navigation, I can change the survey so that answers are retained when revisiting states. The first stage is to set the answers into state data in the Next click handlers, prior to navigating away:
StateContext.Data["answer"] = Answer.SelectedValue;
Now, when Question1 or Question2 are revisited, the state data will contain the answer previously selected. It’s then a simple matter to retrieve this answer in the Page_Load method and preselect the relevant radio button:
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
if (StateContext.Data["answer"] != null)
{
Answer.SelectedValue = (string)StateContext.Data["answer"];
}
}
}
The survey, now complete, isn’t susceptible to the errors commonly encountered in Web applications when users press the browser back button (or have multiple browser windows open). Such problems typically arise when page-specific data is persisted in a server-side session. Although there’s only one session object, there can be multiple “current” versions of a single page. For example, using the back button to retrieve a “stale” version of a page from the browser cache might cause the client and server to be out of sync. The Navigation framework faces no such issues because it doesn’t have any server-side cache. Instead, the state, state data and breadcrumb trail are all held in the URL. This does mean, however, that a user can change these values by editing the URL.
Making MVC Jealous
Earlier I stated that the Navigation framework lets you create Web Forms code to make MVC jealous. After such a bold claim you might be feeling a bit short-changed by the survey sample application, because it probably has MVC holding its nose to avoid the unsavory whiff of codebehind about it. But please don’t despair; this was merely an introduction to the core concepts. Future articles will focus on architectural integrity, with particular attention paid to unit testing and DRY principles.
In the second installment I’ll build a data-bound sample with empty codebehinds and complete unit test code coverage. This coverage will even include the navigational code, notoriously difficult to test in an MVC application.
In the third installment I’ll build an SEO-friendly single-page application. It’ll use progressive enhancement, employing ASP.NET AJAX when JavaScript is enabled and degrading gracefully when it’s disabled, with the same data-bound methods used in both scenarios. Again, this is tricky to achieve in an MVC application.
If this whets your appetite and you can’t wait to try out some of the more advanced functionality, be sure to download the comprehensive feature documentation and sample code from navigation.codeplex.com.
Graham Mendick is Web Forms’ biggest fan and wants to show it can be just as architecturally sound as ASP.NET MVC. He authored the Navigation for ASP.NET Web Forms framework, which he believes—when used with data binding—can breathe new life into Web Forms.
Thanks to the following technical expert for reviewing this article: Damian Edwards