SoftwareFactories

WCSF geekSpeak: What do you want to see?

Just let me know to see if I can accommodate it on my geekSpeak.

I’m on geekSpeak

Or I'll be, on January 23rd.

Here is the complete list of web casts for December 2007 and January 2008:

A new phase for the Acropolis project

The Acropolis Team has announced that Acropolis is going to enter a new phase.

Bug Found On The Page Flow Without Database Improvement

Joern found a nasty bug in my code. I've uploaded the updated source code in all articles.

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.

Web Client Software Factory Knowledge Base

Check out the knowledge base page at the Web Client Guidance Community site.