P&P

Improving The Page Flow Application Block: Decoupling Page Flow Usage From Its Implementation

In this second article of the series I'll show how you can change the Page Flow Application Block of the Web Client Software Factory to get page flows by its definition name instead of its definition type.

Improving The Page Flow Application Block

I'm starting a series of articles where I'll show how (in my opinion, obviously) to the Page Flow Application Block of the Web Client Software Factory could be improved.

Improving The Page Flow Application Block: Removing Database Dependencies

Introduction


Especially in development and demonstration scenarios, the dependency on a database can be a big hassle. In this article I'll show how to remove this dependency.


Instance Correlation Provider


A Page Flow Application Block's instance correlation provider is responsible for maintaining information about page flow instances like instance id, type and running status. These providers must implement the Microsoft.Practices.PageFlow.IPageFlowInstanceStore:


namespace Microsoft.Practices.PageFlow
{
/// <summary>
/// Defines the interface for an object that can store <see cref="IPageFlow"/>s.
/// </summary>
public interface IPageFlowInstanceStore
{
/// <summary>
/// Adds an <see cref="IPageFlow"/> to the store.
/// </summary>
/// <param name="pageFlowInstance">The <see cref="IPageFlow"/> to add.</param>
void Add(IPageFlow pageFlowInstance);

/// <summary>
/// Removes an <see cref="IPageFlow"/> from the store.
/// </summary>
/// <param name="pageFlowInstance">The <see cref="IPageFlow"/> to remove.</param>
void Remove(IPageFlow pageFlowInstance);

/// <summary>
/// Removed an <see cref="IPageFlow"/> from the store.
/// </summary>
/// <param name="id">The unique identifier of the <see cref="IPageFlow"/> to remove from the store.</param>
void Remove(Guid id);

/// <summary>
/// Returns the <see cref="Guid">Guid</see> of the last instance.
/// </summary>
/// <returns>The unique identifier of the PageFlow instance.</returns>
Guid GetLastRunningInstance();

/// <summary>
/// Retrieves the unique identifier of an <see cref="IPageFlow">IPageFlow</see> instance
/// of the desired <see cref="Type">Type</see>.
/// </summary>
/// <param name="type">The <see cref="Type"/> of <see cref="IPageFlow"/> to retrieve.</param>
/// <returns>The unique idenitifier of an instance of the desired <see cref="Type"/>.</returns>
Guid GetByType(Type type);

/// <summary>
/// Retrieves the instance <see cref="Type">Type</see> assembly qualified full name correspnding to an instance id
/// </summary>
/// <param name="id">The <see cref="Guid">Guid</see> of the instance</param>
/// <returns>The assembly qualified name of the <see cref="Type">Type</see> of the instance or null if not found</returns>
string GetInstanceType(Guid id);

/// <summary>
/// Marks an <see cref="IPageFlow">IPageFlow</see> instance as the currently running instance.
/// </summary>
/// <param name="iPageFlow">The <see cref="IPageFlow">IPageFlow</see> instance to mark as running.</param>
void SetPageFlowRunning(IPageFlow iPageFlow);

/// <summary>
/// Marks an <see cref="IPageFlow">IPageFlow</see> instance as not currently running.
/// </summary>
/// <param name="iPageFlow">The <see cref="IPageFlow">IPageFlow</see> instance to mark as not currently running.</param>
void SetPageFlowNotRunning(IPageFlow iPageFlow);
}
}


The provided implementation uses an SQL Server database to store the page flow instance data.


To remove this dependency on a database I'll have to build a new page flow instance store.


If the sole purpose of this provider was to be only used in development, some in-memory dictionary would be just fine. But I want to build something that's as much as possible alike the provided provider and of production quality.


For me, the best choice was to use the ASP.NET session state. This way I keep instances isolated from session to session (this is different from the provided provider where page flow instances can be used across ASP.NET sessions) and can, ultimately, be stored in a data base for persistence and load balancing.


Implementing the PageFlowInstanceCorrelationAspNetSessionStateProvider


To replace the database based implementation all I need is to replace the database.


Since the provided implementation only uses one table, this means that it only has one entity - a storage item.


Storage Item


This is the entity that stores the information about page flow instances and its fields have a direct correspondence to the columns of the table of the provided implementation.


[Serializable]
private class StorageItem
{
public Guid InstanceId;
public string PageFlowType;
public string CorrelationToken;
public bool Running;
}

The class is marked with the Serializable attribute to allow its instances to be serialized to and back from a persistence store like a database.


Storage


This entity represents the database table and stored procedures used to manipulate the data.


[Serializable]
private partial class Storage : System.Runtime.Serialization.ISerializable
{
private SortedList<Guid, StorageItem> items = new SortedList<Guid, StorageItem>();

/// <summary>
/// Initializes a new instance of the <see cref="T:PauloMorgado.Practices.PageFlow.Storage.AspNetSessionState.Storage"/> class.
/// </summary>
public Storage()
{
}

#region ISerializable Members

/// <summary>
/// Initializes a new instance of the <see cref="Storage"/> class.
/// </summary>
/// <param name="info">The info.</param>
/// <param name="context">The context.</param>
protected Storage(SerializationInfo info, StreamingContext context)
{
this.items = (SortedList<Guid, StorageItem>)info.GetValue("items", typeof(SortedList<Guid, StorageItem>));
}

/// <summary>
/// Populates a <see cref="T:System.Runtime.Serialization.SerializationInfo"></see> with the data needed to serialize the target object.
/// </summary>
/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo"></see> to populate with data.</param>
/// <param name="context">The destination (see <see cref="T:System.Runtime.Serialization.StreamingContext"></see>) for this serialization.</param>
/// <exception cref="T:System.Security.SecurityException">The caller does not have the required permission. </exception>
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("items", this.items);
}

#endregion

internal void InsertInstance(Guid instanceId, string pageFlowType, string correlationToken, bool running)
{
StorageItem item = new StorageItem();
item.InstanceId = instanceId;
item.PageFlowType = pageFlowType;
item.CorrelationToken = correlationToken;
item.Running = running;

this.items.Add(instanceId, item);
}

internal void DeleteInstance(Guid instanceId)
{
this.items.Remove(instanceId);
}

internal Guid GetLastRunningInstanceByCorrelationToken(string correlationToken)
{
foreach (StorageItem item in this.items.Values)
{
if (item.Running && (item.CorrelationToken == correlationToken))
{
return item.InstanceId;
}
}
return Guid.Empty;
}

internal Guid GetInstanceByTypeAndByCorrelationToken(string correlationToken, string pageFlowType)
{
foreach (StorageItem item in this.items.Values)
{
if ((item.CorrelationToken == correlationToken) && (item.PageFlowType == pageFlowType))
{
return item.InstanceId;
}
}
return Guid.Empty;
}

internal string GetTypeByInstanceId(Guid instanceId)
{
StorageItem item;
if (this.items.TryGetValue(instanceId, out item))
{
return item.PageFlowType;
}
return null;
}

internal void SetRunningInstanceForCorrelationToken(Guid instanceId, string correlationToken)
{
foreach (StorageItem item in this.items.Values)
{
// set running instances to not running
if ((item.CorrelationToken == correlationToken) && (item.Running))
{
item.Running = false;
}
// set the requested instance to running
if ((item.InstanceId == instanceId) && (!item.Running))
{
item.Running = true;
}
}
}

internal void ChangeInstanceStatus(Guid instanceId, bool running)
{
StorageItem item;
if (this.items.TryGetValue(instanceId, out item))
{
item.Running = running;
}
}
}


Since this is the entity being directly stored in the session state, the class is marked with the Serializable attribute to allow its instances to be serialized to an back from a persistence store like a database.


Page Flow Instance Correlation Provider


This class looks very much like the provided PageFlowInstanceCorrelationSqlProvider where the database is replaced with an instance of the Storage class and the calls to database stored procedures are replaced by calls to methods of the Storage class.


/// <summary>
///
An implementation of an <see cref="IPageFlowInstanceStore">IPageFlowInstanceStore</see>
///
that uses a the Enterprise Library Data Block <see cref="Database">Database</see> to manage
/// and store <see cref="IPageFlow">IPageFlow</see> instances.
/// </summary>
public partial class PageFlowInstanceCorrelationAspNetSessionStateProvider : IPageFlowInstanceStore
{
private string pageFlowStorageName;
private Storage pageFlowStorage;
private IPageFlowCorrelationTokenProvider tokenProvider;

/// <overloads>
/// Creates an instance of PageFlowInstanceCorrelationSqlProvider.
/// </overloads>
/// <summary>
/// Creates an instance of PageFlowInstanceCorrelationSqlProvider using
/// the provided database name.
/// </summary>
/// <remarks>
/// The PageFlowInstanceCorrelationSqlProvider will use a
/// <see cref="CookiePageFlowCorrelationTokenProvider">CookiePageFlowCorrelationTokenProvider</see>
/// to create and provide tokens for each instance.
/// </remarks>
/// <param name="databaseName">The name of the <see cref="P:HttpContext.Session"/> variable to use as the store.</param>
public PageFlowInstanceCorrelationAspNetSessionStateProvider(string databaseName)
: this(databaseName, new CookiePageFlowCorrelationTokenProvider())
{
}

/// <summary>
/// Creates an instance of PageFlowInstanceCorrelationSqlProvider.
/// </summary>
/// <param name="databaseName">The name of the <see cref="P:HttpContext.Session"/> variable to use as the store.</param>
/// <param name="tokenProvider">The <see cref="IPageFlowCorrelationTokenProvider">IPageFlowCorrelationTokenProvider</see>
/// implementation that will provide tokens.
/// </param>
public PageFlowInstanceCorrelationAspNetSessionStateProvider(string databaseName, IPageFlowCorrelationTokenProvider tokenProvider)
{
Guard.ArgumentNotNullOrEmptyString(databaseName, "databaseName");
Guard.ArgumentNotNull(tokenProvider, "tokenProvider");
this.pageFlowStorageName = databaseName;
this.tokenProvider = tokenProvider;
}

/// <summary>
/// Adds an <see cref="IPageFlow">IPageFlow</see> instance to the store.
/// </summary>
/// <param name="pageFlowInstance">The instance to add.</param>
public void Add(IPageFlow pageFlowInstance)
{
Guard.ArgumentNotNull(pageFlowInstance, "pageFlowInstance");
string token = this.tokenProvider.GetCorrelationToken();
this.PageFlowStorage.InsertInstance(pageFlowInstance.Id, pageFlowInstance.Definition.PageFlowType.AssemblyQualifiedName, token, false);
}

/// <overloads>
/// Removes a PageFlow instance from the store.
/// </overloads>
/// <summary>
/// Removes an <see cref="IPageFlow">IPageFlow</see> instance from the store.
/// </summary>
/// <param name="pageFlowInstance">The instance to remove.</param>
public void Remove(IPageFlow pageFlowInstance)
{
Guard.ArgumentNotNull(pageFlowInstance, "pageFlowInstance");
this.Remove(pageFlowInstance.Id);
}

/// <summary>
/// Removes the <see cref="IPageFlow">IPageFlow</see> instance with the appropriate Guid from the store.
/// </summary>
/// <param name="id">The unique identifier of the instance to remove.</param>
public void Remove(Guid id)
{
this.PageFlowStorage.DeleteInstance(id);
}

/// <summary>
/// Returns the <see cref="Guid">Guid</see> of the last instance.
/// </summary>
/// <returns>The unique identifier of the PageFlow instance.</returns>
public Guid GetLastRunningInstance()
{
string token = this.tokenProvider.GetCorrelationToken();
return this.PageFlowStorage.GetLastRunningInstanceByCorrelationToken(token);
}

/// <summary>
/// Retrieves the unique identifier of an <see cref="IPageFlow">IPageFlow</see> instance
/// of the desired <see cref="Type">Type</see>.
/// </summary>
/// <param name="type">The <see cref="Type">Type</see> of PageFlow to retrieve.</param>
/// <returns>The unique idenitifier of an instance of the desired type.</returns>
public Guid GetByType(Type type)
{
Guard.ArgumentNotNull(type, "type");

string token = this.tokenProvider.GetCorrelationToken();
return this.PageFlowStorage.GetInstanceByTypeAndByCorrelationToken(token, type.AssemblyQualifiedName);
}

/// <summary>
/// Retrieves the instance <see cref="Type">Type</see> assembly qualified full name correspnding to an instance id
/// </summary>
/// <param name="id">The <see cref="Guid">Guid</see> of the instance</param>
/// <returns>The assembly qualified name of the <see cref="Type">Type</see> of the instance or null if not found</returns>
public string GetInstanceType(Guid id)
{
PauloMorgado.Practices.PageFlow.Storage.AspNetSessionState.Utils.Guard.ArgumentNotEmptyGuid(id, "id");

return this.PageFlowStorage.GetTypeByInstanceId(id);
}

/// <summary>
/// Marks an <see cref="IPageFlow">IPageFlow</see> instance as the currently running instance.
/// </summary>
/// <param name="pageFlowInstance">The <see cref="IPageFlow">IPageFlow</see> instance to mark as running.</param>
public void SetPageFlowRunning(IPageFlow pageFlowInstance)
{
Guard.ArgumentNotNull(pageFlowInstance, "pageFlowInstance");
string token = this.tokenProvider.GetCorrelationToken();
this.PageFlowStorage.SetRunningInstanceForCorrelationToken(pageFlowInstance.Id, token);
}

/// <summary>
/// Marks an <see cref="IPageFlow">IPageFlow</see> instance as not currently running.
/// </summary>
/// <param name="pageFlowInstance">The <see cref="IPageFlow">IPageFlow</see> instance to mark as not currently running.</param>
public void SetPageFlowNotRunning(IPageFlow pageFlowInstance)
{
Guard.ArgumentNotNull(pageFlowInstance, "pageFlowInstance");
this.PageFlowStorage.ChangeInstanceStatus(pageFlowInstance.Id, false);
}

private Storage PageFlowStorage
{
get
{
Storage pageFlowStorage= HttpContext.Current.Session[this.pageFlowStorageName] as Storage;
if (pageFlowStorage == null)
{
HttpContext.Current.Session[this.pageFlowStorageName] = pageFlowStorage = new Storage();
}
return pageFlowStorage;
}
}
}


Workflow Foundation


The implementation of the page flow engine provided with the Page Flow Application Block is based on Windows Workflow Foundation and assumes that a persistence service is present and calls Unload method of the workflow instance.


So far, the only persistence service supplied with the .NET framework is the SQL persistence service, which is based on SQL Server.


I could have created my own in memory persistence service but, instead, I chose to check if a persistence service is present before calling the Unload method. This was accomplished by replacing:


_instance.Unload();

with:


System.Collections.ObjectModel.ReadOnlyCollection<WorkflowPersistenceService> persistenceServices = _instance.WorkflowRuntime.GetAllServices<WorkflowPersistenceService>();
if ((persistenceServices != null) && (persistenceServices.Count != 0))
{
_instance.Unload();
}

in the Microsoft.Practices.PageFlow.WorkflowFoundation.WorkflowFoundationPageFlow.Suspend and PageFlow.WorkflowFoundation.WorkflowFoundationPageFlowFactory.GetPageFlow methods.


Page Flow Store QuickStart


(This sample supplied with the Web Client Software Factory will be used to test and demonstrate the improvements made to the Page Flow Application Block.)


Having built a database-free page flow application block, all I need now is to configure the application to use the new implementations by editing the web.config file and replacing:


<?xml version="1.0"?>
<
configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
<
connectionStrings>
<
add name="PageFlowPersistanceStore" connectionString="Integrated Security=SSPI;Persist Security Info=False;Initial Catalog=WCSF_Quickstart;Data Source=.\SQLExpress" providerName="System.Data.SqlClient"/>
</
connectionStrings>
<
pageFlow>
<
pageFlowProvider providerType="Microsoft.Practices.PageFlow.WorkflowFoundation.WorkflowFoundationPageFlowProvider, Microsoft.Practices.PageFlow.WorkflowFoundation"/>
<
hostingWorkflowRuntime Name="Hosting">
<
Services>
<
add type="System.Workflow.Runtime.Hosting.SqlWorkflowPersistenceService, System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionString="Integrated Security=SSPI;Persist Security Info=False;Initial Catalog=WCSF_Quickstart;Data Source=.\SQLEXPRESS;" LoadIntervalSeconds="5" UnloadOnIdle="true"/>
<
add type="System.Workflow.Runtime.Hosting.ManualWorkflowSchedulerService, System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" useActiveTimers="true"/>
<
add type="System.Workflow.Activities.ExternalDataExchangeService, System.Workflow.Activities, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</
Services>
</
hostingWorkflowRuntime>
<
pageFlowInstanceStoreProvider providerType="Microsoft.Practices.PageFlow.Storage.EnterpriseLibrary.PageFlowInstanceCorrelationSqlProvider, Microsoft.Practices.PageFlow.Storage.EnterpriseLibrary" connectionString="PageFlowPersistanceStore"/>
<
pageFlowInstanceCorrelationTokenProvider providerType="Microsoft.Practices.PageFlow.Storage.EnterpriseLibrary.CookiePageFlowCorrelationTokenProvider, Microsoft.Practices.PageFlow.Storage.EnterpriseLibrary" />
</
pageFlow>
</configuration>

with:


<?xml version="1.0"?>
<
configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
<
connectionStrings>
</
connectionStrings>
<
pageFlow>
<
pageFlowProvider providerType="Microsoft.Practices.PageFlow.WorkflowFoundation.WorkflowFoundationPageFlowProvider, Microsoft.Practices.PageFlow.WorkflowFoundation"/>
<
hostingWorkflowRuntime Name="Hosting">
<
Services>
<
add type="System.Workflow.Runtime.Hosting.ManualWorkflowSchedulerService, System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" useActiveTimers="true"/>
<
add type="System.Workflow.Activities.ExternalDataExchangeService, System.Workflow.Activities, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</
Services>
</
hostingWorkflowRuntime>
<
pageFlowInstanceStoreProvider providerType="PauloMorgado.Practices.PageFlow.Storage.AspNetSessionState.PageFlowInstanceCorrelationAspNetSessionStateProvider, PauloMorgado.Practices.PageFlow.Storage.AspNetSessionState" connectionString="PageFlowPersistanceStore"/>
<
pageFlowInstanceCorrelationTokenProvider providerType="Microsoft.Practices.PageFlow.Storage.EnterpriseLibrary.CookiePageFlowCorrelationTokenProvider, PauloMorgado.Practices.PageFlow.Storage.AspNetSessionState" />
</
pageFlow>
</
configuration>

But the sample application still has a dependency to a membership provider that uses SQL Server.


To remove this dependency, I'll use the Login control with the Forms Authentication's user definitions and I'll need to edit the login page and web.config file:


<%@ Page MasterPageFile="~/Shared/QuickStarts.master" Title="Page Flow Store QuickStart - Login" Language="C#" AutoEventWireup="true" CodeFile="Login.aspx.cs" Inherits="Login" %>

<asp:Content ID="Content1" ContentPlaceHolderID="mainContent" runat="Server">
<h2>Login</h2>
<div>
<p>Enter "user" for the user name and "p@ssw0rd" for the password.</p>
<asp:Login ID="Login1" runat="server" OnAuthenticate="Login1_Authenticate">
</asp:Login>
</div>
</
asp:Content>


public partial class Login : Page
{
protected void Login1_Authenticate(object sender, System.Web.UI.WebControls.AuthenticateEventArgs e)
{
e.Authenticated = FormsAuthentication.Authenticate(Login1.UserName, Login1.Password);
}
}

<?xml version="1.0"?>
<
configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
<
connectionStrings>
<!--
removed - <add name="MembershipStore" connectionString="Integrated Security=SSPI;Persist Security Info=False;Initial Catalog=WCSF_Quickstart;Data Source=.\SQLExpress" providerName="System.Data.SqlClient"/>-->
</
connectionStrings>
<
system.web>

<authentication mode="Forms">
<!--
added start -->
<
forms>
<
credentials passwordFormat="Clear">
<
user name="user" password="p@ssw0rd"/>
</
credentials>
</
forms>
<!--
added end -->
</
authentication>

<!-- removed - <membership defaultProvider="SqlProvider">
<providers>
<clear />
<add
name="SqlProvider"
type="System.Web.Security.SqlMembershipProvider"
connectionStringName="MembershipStore"
applicationName="WCSF_Quickstart"
passwordFormat="Hashed" />
</providers>
</membership>
-->
</
configuration>


Conclusion


And I'm all set to go and develop (or even go to production) without the need for a database.


Resources


Validation Guidance Bundle

The P&P team has released another Web Client Software Factory Guidance Bundle. This time is the Validation Guidance Bundle.

What are some scenarios to consider using the guidance in this bundle for?

  • Improving UI responsiveness while reusing entity business validation logic across screens when performing validations such as:
    • Length of Employee name.
    • Employee’s email.
    • Order number is unique.
    • Order is complete.
  • Improving UI responsiveness for applications utilizing Server-Side ASP.NET validators.
  • Determining what type of validation to use for improving responsiveness and security.

Who should use this bundle?

This bundle is for Developers and Architects who are interested in improving the UI Responsiveness of validation in their Line-Of-Business ASP.NET Web applications.

What is in the bundle?

  • Validation QuickStart: source code to demonstrate how to improve UI responsiveness for validation and reuse of validation rules across pages.
  • Validation Application Block from Enterprise Library 3.1: Validation Application Block binary.
  • AJAXControlToolkit.WCSFExtensions.dll: Contains the ServerSideValidationExtender which invokes ASP.NET validators including the Enterprise Library PropertyProxyValidator via AJAX
  • Acceptance Tests: Manual tests that can be executed to walk you through the Quickstart functionality.
  • Documentation: Documentation explaining Validation Guidelines (Security, Schema, and so on) the Quickstart, and how to use the extender.

Check it out. It even has an introduction video (15' - 15MB) by Glenn Block.

Guidance Bundles From The Web Client Software Factory

The P&P team came up with a new concept for the Web Client Software Factory: Guidance Bundles.

What is a Guidance Bundle?

A Guidance Bundle is a small package of guidance whose purpose is to allow users to quickly, conveniently, and easily learn and evaluate a concept.
Although a Bundle can contain any type of guidance, it typically includes the following elements:

Source code: QuickStarts and related artifacts.
Binaries: Application block binaries required by the QuickStarts .
Written documentation: QuickStarts description and How-To topics.
Guidance Package: Visual Studio Automation for performing development activies in accordance with our guidance.
Reference Implementation: Applications that illustrate usage of our guidance in real-world scenarios.

The first one has already been released:

Contextual Autocomplete Bundle

As a user enters information in a text box on a Web page, a Web page can use AutoComplete behavior to display a list of suggested values which a user can choose from rather than typing the complete term. This is optimal in cases where the list of possible values is too large to be embedded within the page such as with a list box. For example imagine embedding a list of all of the cities in the United States.

See a demo here and get it from here.

Patterns & Practices Summit and TechEd Developers (Europe)









November  5-9 2007, Microsoft Redmond


















With:
Anders Hejlsberg Steve McConnell Scott Guthrie Scott Hanselman John Lam

http://www.pnpsummit.com/west2007.aspx










5-9 November 2007, Barcelona, Spain


















Without:
Anders Hejlsberg Steve McConnell Scott Guthrie Scott Hanselman John Lam
 

http://www.mseventseurope.com/TechEd/07/Developers/Pages/Default.aspx


No comments!

Reference Implementation With Page Flow Navigation

In a previous post, I talked about Page Flow Navigation vs. Page Navigation.

This is an adaptation of the Reference Implementation to work with Page Flow Navigation. (You’ll need the changed version of the Page Flow Application Block.)

Downloads

PageFlowWithShoppingCartQuickStart Sample With Page Flow Navigation

In a previous post, I talked about Page Flow Navigation vs. Page Navigation.

This is an adaptation of the PageFlowWithShoppingCartQuickStart sample to work with Page Flow Navigation. (You’ll need the changed version of the Page Flow Application Block.)

Downloads

Page Flow Navigation vs. Page Navigation

Introduction

I've been working with page flows for the past days and the idea of navigating to a page flow instead of navigating between pages belonging to a pageflow is making more sense each time I think about it.

The Web Client Software Factory comes with a Page Flow Application Block that governs the navigation between pages of the same page flow.

The way the Page Flow Application Block identifies a particular page flow is by the URLs of the pages that belong to that page flow. This is possible because you are still navigating to those pages. This also makes it impossible to reuse the same page between page flows.

In this post I will change the Page Flow Application Block (keeping the changes to a minimum) to provide page flow navigation instead of page navigation.

Changing the Page Flow Definition

In order to keep changes to a minimum, we'll use the Name property of the page flow as its URL. This will be the virtual address of the page flow and will also be used as the AbortPage's URL if one is not provided.

Since the individual pages will no longer be directly accessible, we can drop all methods that correlate pages with URLs:

  • IPageFlowDefinition.ContainsUrl(string url);
  • IPageFlowDefinition.GetPageFromUrl(string url);

Changing the Page Flow Definition Catalog

Since we are navigating to page flows, we don't need to keep track of the page's URLs, which makes the definition catalog simpler:

/// <summary>
/// A catalog of <see cref="IPageFlowDefinition"/>s.
/// </summary>
public class PageFlowDefinitionCatalog : IPageFlowDefinitionCatalog
{
    private Dictionary<string, IPageFlowDefinition> catalog = new Dictionary<string, IPageFlowDefinition>();

    /// <summary>
    /// Gets the number of <see cref="IPageFlowDefinition"/>s in the catalog.
    /// </summary>
    /// <value>The number of <see cref="IPageFlowDefinition"/>s in the catalog.</value>
    public int Count
    {
        get { return this.catalog.Count; }
    }

    /// <summary>
    /// Removes an <see cref="IPageFlowDefinition"/> from the catalog.
    /// </summary>
    /// <param name="definition">The definition to remove.</param>
    public void Remove(IPageFlowDefinition definition)
    {
        this.catalog.Remove(definition.Name);
    }

    /// <summary>
    /// Adds an <see cref="IPageFlowDefinition"/> to the catalog.
    /// </summary>
    /// <param name="definition">The <see cref="IPageFlowDefinition"/> to add to the catalog.</param>
    public void Add(IPageFlowDefinition definition)
    {
        this.catalog.Add(definition.Name, definition);
    }

    /// <summary>
    /// Get an <see cref="IPageFlowDefinition"/> with a specific URL from the catalog.
    /// </summary>
    /// <param name="rawUrl">The URL to search for.</param>
    /// <returns>The <see cref="IPageFlowDefinition"/> with a matching URL.</returns>
    public IPageFlowDefinition GetByUrl(string rawUrl)
    {
        IPageFlowDefinition definition;
        this.catalog.TryGetValue(rawUrl, out definition);
        return definition;
    }
}
Changing the WorkflowFoundationPageFlowDefinition

In order to comply to these changes, the provided implementation (WorkflowFoundationPageFlowDefinition) will need to be changed.

If an abort page URL is not provided it will default to the not running URL.

/// <summary>
/// Implementation of a page flow definition that relies on the <see cref="Activities.PageFlow">PageFlow activity</see>.
/// </summary>
/// <remarks>This class is used to query the page flow metadata and is accessible through the <see cref="WorkflowFoundationPageFlow.Definition"></see> propert
public class WorkflowFoundationPageFlowDefinition : IPageFlowDefinition
{
    // ...

    /// <summary>
    /// Initializes a new instance of the <see cref="WorkflowFoundationPageFlowDefinition"></see> given a <see cref="Activities.PageFlow">PageFlow activity</s
    /// </summary>
    /// <param name="definition">A <see cref="Activities.PageFlow">PageFlow</see></param>
    /// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"></see></exception>
    public WorkflowFoundationPageFlowDefinition(Activities.PageFlow definition)
    {
        if (definition == null)
            throw new ArgumentNullException("definition");

        _definition = definition;
        _abortPage = new Page("PageFlow.AbortPage", (string.IsNullOrEmpty(definition.NotRunningUrl) ? this._definition.Name : definition.AbortUrl));
    }

    // ...
}

Changing the Page Flow Provider

The implementation of IPageFlowProvider.ProcessRequest(string url) will need to change, although keeping the signature.

The ProcessResult class will need to be changed. Besides redirecting or doing nothing, there will be a new action: Rewrite.

/// <summary>
/// An object describing the results of a ProcessRequest() call on 
/// <see cref="PageFlowHttpModule"/>.
/// </summary>
public class ProcessResult
{
    /// <summary>
    /// Possible actions to take as a result of the URL processing.
    /// </summary>
    public enum ProcessAction : int
    {
        /// <summary>
        /// No action to be taken.
        /// </summary>
        None = 0,

        /// <summary>
        /// Rerwrite the URL to execute the page.
        /// </summary>
        Rewrite = 1,

        /// <summary>
        /// Redirect the request.
        /// </summary>
        Redirect = 2
    }

    private string _url;
    private ProcessAction _action;

    /// <summary>
    /// Creates an instance of a ProcessResult.
    /// </summary>
    public ProcessResult()
    {
    }

    /// <summary>
    /// Creates an instance with the specified properties.
    /// </summary>
    /// <param name="action">The action to process.</param>
    /// <param name="url">The URL.</param>
    public ProcessResult(ProcessAction action, string url)
    {
        _action = action;
        _url = url;
    }

    /// <summary>
    /// Gets the URL to redirect to.
    /// </summary>
    public string Url
    {
        get { return _url; }
    }

    /// <summary>
    /// Gets the determiniation if the request should be redirected to the Url.
    /// </summary>
    public ProcessAction Action
    {
        get { return _action; }
    }
}

Rewrite will change the HTTP request in order to execute the page registered for the current state.

Changing the WorkflowFoundationPageFlowProvider

In order to comply to these changes, the provided implementation (WorkflowFoundationPageFlowProvider) will need to be changed.

If the navigation is to a page fllow, the provider will need to instruct the HTTP module to rewrite the path. So, every method creating an instance of ProcessResult wil need to be changed.

/// <summary>
/// Implementation that uses Windows Workflow Foundation as the page flow engine.
/// </summary>
/// <remarks>
/// <para>This class is provided as a singleton by the <see cref="PageFlowDirectory"/> class</para>
/// </remarks>
public class WorkflowFoundationPageFlowProvider : IPageFlowProvider, IDisposable
{
    // ...

    /// <summary>
    /// Process the request when there is no instance in the store or the instance is NOT running.
    /// </summary>
    /// <param name="url">The request url.</param>
    /// <returns>The <see cref="ProcessResult" /> holding the redirect action to take.</returns>        
    private ProcessResult ProcessWithNoRunningInstance(string url)
    {
        ProcessResult result;
        IPageFlowDefinition definition = PageFlowDirectory.Catalog.GetByUrl(url);
        if (definition != null)
        {
            IPageFlow instance = GetPageFlow(definition.PageFlowType, false);
            if (instance == null)
            {
                result = ProcessWithNonExistingInstance(definition);
            }
            else
            {
                result = ProcessWithExistingInstance(definition, instance);
            }
        }
        else
        {
            // Do nothing.
            result = new ProcessResult(ProcessResult.ProcessAction.None, string.Empty); ;
        }

        return result;
    }

    /// <summary>
    /// Process the request with a running page flow instance.
    /// </summary>
    /// <param name="instance">The running page flow instance.</param>
    /// <param name="url">The request url.</param>
    /// <returns>The <see cref="ProcessResult" /> holding the redirect action to take.</returns>        
    private ProcessResult ProcessWithRunningInstance(IPageFlow instance, string url)
    {
        // The default action is to Rewrite to execute the current state's page.
        ProcessResult result = new ProcessResult(ProcessResult.ProcessAction.Rewrite, instance.CurrentPage.Url);

        if (!(instance.Definition.Name.Equals(url, StringComparison.OrdinalIgnoreCase)))
        {
            switch (instance.Definition.Abandonable)
            {
                case AbandonBehavior.AllowAndDiscardInstance:
                    instance.Abort(false);
                    result = ProcessRequest(url);
                    break;
                case AbandonBehavior.AllowAndSaveInstance:
                    instance.Suspend();
                    result = ProcessRequest(url);
                    break;
                case AbandonBehavior.Prevent:
                    // Redirect to the current page flow.
                    result = new ProcessResult(ProcessResult.ProcessAction.Redirect, instance.Definition.Name);
                    break;
            }
        }
        return result;
    }

    /// <summary>
    /// Process the request using the specified instance.
    /// </summary>
    /// <param name="definition">The page flow definition corresponding to the request url.</param>
    /// <param name="instance">The existing page flow instance.</param>
    /// <returns>The <see cref="ProcessResult" /> holding the redirect action to take.</returns>        
    private ProcessResult ProcessWithExistingInstance(IPageFlowDefinition definition, IPageFlow instance)
    {
        ProcessResult result;
        switch (definition.Abandonable)
        {
            case AbandonBehavior.AllowAndSaveInstance:
                result = ResumeInstance(instance, definition);
                break;
            default:
                // Redirect to the current page flow.
                result = new ProcessResult(ProcessResult.ProcessAction.Redirect, HttpContext.Current.Request.AppRelativeCurrentExecutionFilePath);
                break;
        }
        return result;
    }

    private ProcessResult ProcessWithNonExistingInstance(IPageFlowDefinition definition)
    {
        ProcessResult result;
        if (HttpContext.Current != null && HttpContext.Current.Request.HttpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase))
        {
            // NOTE: this special case happens when there is no running instance,
            //       there are no suspended instances, and the url belongs to a page flow
            //       but we are on PostBack. In this case we let the request flow, if not we
            //       send the user to NotRunning url.
            // Redirect to the current page flow.
            result = new ProcessResult(ProcessResult.ProcessAction.Redirect, definition.Name);
        }
        else
        {
            // Rewrite to execute the current page flow's not running page.
            result = new ProcessResult(ProcessResult.ProcessAction.Rewrite, definition.NotRunningRedirect);
        }
        return result;
    }

    private ProcessResult ResumeInstance(IPageFlow instance, IPageFlowDefinition definition)
    {
        ProcessResult result;
        if (instance.Status == PageFlowStatus.Suspended)
        {
            // Rewrite to execute the current page flow's not running page.
            result = new ProcessResult(ProcessResult.ProcessAction.Rewrite, definition.NotRunningRedirect);
        }
        else
        {
            if (instance.CurrentPage != null)
            {
                // Rewrite to execute the current state's page.
                result = new ProcessResult(ProcessResult.ProcessAction.Rewrite, instance.CurrentPage.Url);
            }
            else
            {
                // Rewrite to execute the current page flow's not running page.
                result = new ProcessResult(ProcessResult.ProcessAction.Rewrite, definition.NotRunningRedirect);
            }
        }
        return result;
    }
}

Changing the Page Flow HTTP Module

With the Page Flow Application Block, every thing starts in its HTTP module. So, we also need to make a few changes here.

Instead of processing the request in the PostAcquireRequestState event, the request will be processed in two steps:

  1. In the PostAuthorizeRequest event, the page flow provider will be requested to process the request. Because the path to the executing pages is being rewritten, this needs to be done even if no session state is loaded.
  2. In the PostMapRequestHandler event, if the path was rewritten in the PostAcquireRequestState event, it's rewritten back to the original URL.
/// <summary>
/// An <see cref="IHttpModule"/> for working with PageFlows
/// </summary>
public class PageFlowHttpModule : IHttpModule
{
    private ProcessResult result;
    private string originalUrl;

    // ...

    /// <summary>
    /// Initializes the PageFlowHttpModule to work with the <see cref="HttpApplication"/>.
    /// </summary>
    /// <param name="context">The <see cref="HttpApplication"/>.</param>
    public void Init(HttpApplication context)
    {
        if (context == null)
            throw new ArgumentNullException("context");

        context.PostAuthorizeRequest += new EventHandler(OnPostAuthorizeRequest);
        context.PostMapRequestHandler += new EventHandler(OnPostMapRequestHandler);

        WebConfigurationManager.OpenWebConfiguration(null);
        HttpRequestHelper.LoadExtensionsHandledByPageHandlerFactoryFromConfig((HttpHandlersSection)WebConfigurationManager.GetSection("system.web/httpHandlers"));
    }

    /// <summary>
    /// Processes an <see cref="HttpRequest"/>.
    /// </summary>
    /// <param name="request">The request to process</param>
    /// <returns>A <see cref="ProcessResult"/>.</returns>
    public ProcessResult ProcessRequest(HttpRequest request)
    {
        Guard.ArgumentNotNull(request, "request");

        ProcessResult result = new ProcessResult();

        if (HttpRequestHelper.IsHandledByPageHandlerFactory(request.AppRelativeCurrentExecutionFilePath))
        {
            result = PageFlowDirectory.Provider.ProcessRequest(request.AppRelativeCurrentExecutionFilePath);
        }
        return result;
    }

    private void OnPostAuthorizeRequest(object sender, EventArgs e)
    {
        this.result = ProcessRequest(HttpContext.Current.Request);
        switch (result.Action)
        {
            case ProcessResult.ProcessAction.Redirect:
                HttpContext.Current.Response.Redirect(this.result.Url, true);
                break;
            case ProcessResult.ProcessAction.Rewrite:
                System.Diagnostics.Debug.WriteLine(this.result.Url);
                this.originalUrl = HttpContext.Current.Request.AppRelativeCurrentExecutionFilePath;
                HttpContext.Current.RewritePath(this.result.Url, true);
                break;
        }
    }

    void OnPostMapRequestHandler(object sender, EventArgs e)
    {
        if ((this.result != null) && (this.result.Action == ProcessResult.ProcessAction.Rewrite))
        {
            System.Diagnostics.Debug.WriteLine(this.result.Url);
            HttpContext.Current.RewritePath(this.originalUrl, true);
        }
        this.result = null;
    }
}

Conclusion

This implementation has several benefits:

  • Deployment - There is no need to deploy the URL of each page. The entire set of pages of a page flow can be changed without having to notify users or reconfigure menus.
  • Security - This adds two kinds of security:
    • Security by obscurity - The user will never now how many pages make a particular page flow.
    • URL security - The "real" pages can, and should, be blocked from acess to the users.
    • Page flow security - The user can never jump into a particular state.

Disclaimer

This implementation has not been fully tested and has known problems.

Downloads

The Practices & Practices Team Is Requesting Feedback On What We Would Like Them To Build For The Web Client Software Factory

Glenn Blocks is requesting for feedback on what the p&p team should build for the next iteration of the Web Client Software Factory. If you have something in mind, let him know.