Design Details

Workflow modeling components

The Workflow Model layer defines how a UI workflow is created to capture business logic, thus providing navigation for host applications. The goals for this layer are:

Navigator

A Navigator is a simplified state machine activity. If a child of the Navigator is an InteractionActivity, it represents a user interaction. The Navigator coordinates the execution of interaction activities by interpreting transitions defined between two interaction activities. An InteractionActivity can block when it waits for a user input. The navigator becomes idle when it reaches a blocking state and control is returned to the host at that point.

Different types of non-blocking activities can be children of a Navigator, however the only blocking activities allowed as direct children of a Navigator are interaction activities. Each child activity when added to the navigator is automatically attached with a Transitions property. This property is a collection of Transition objects. Each transition models a link between the source activity and the target activity. Each transition has an associated Condition property that is of type ActivityCondition. Each transition is evaluated at runtime in the order of their position in the collection to determine which target to move to from the source. The first transition that evaluates to true becomes the active link and the workflow moves to the state it points to.

The Navigator executes every child in a separate execution context. This allows a child to be executed more than once, thus enable backward navigation.

The Navigator keeps a History list which contains a list of activities that are executed in the past. The list behaves similarly to a backward stack inside a browser. Every time the navigator moves from one state (Activity) to another state (Activity), an entry is added to the history list. When the host explicitly asks the Navigator to move to an earlier executed state, the history list shrinks to where the target activity had been in the list and all entries after that point are cleared from the list. We do not maintain a forward history stack.

The following is the definition of the Navigator class.

[Designer(typeof(NavigatorDesigner), typeof(IDesigner))]
[DesignerSerializer(typeof(NavigatorCodeDomSerializer), typeof(CodeDomSerializer))]
[ActivityValidator(typeof(NavigatorValidator))]
public class Navigator : CompositeActivity, IActivityEventListener<ActivityExecutionStatusChangedEventArgs>, IActivityEventListener<QueueEventArgs>
{
    // Public events
    public event EventHandler<NavigatorEventArgs> Navigating{ add; remove; }
 
    // Public properties
    public string StartWith { get; set; }
    public SynchronizationMode SynchronizationMode { get; set; }
    public Permission OwnerPermission { get; set; }
    public bool EnforceOwnerPermission { get; set; }
 
    // Property accessor for attached properties
    public static object GetTransitions(object dependencyObject);
    public static void SetTransitions(object dependencyObject, object value);
 
    // Proptected members
    protected Activity Current { get; }
 
    // ProcessCommand implementation
    protected virtual void ProcessCommand(Command cmd, ActivityExecutionContext executionContext);
}
The Navigating Event

The Navigating event is fired just before the navigator moves to the next activity from the current activity. Because it fires before any transitions are evaluated, it gives the workflow developer a chance to overwrite the target of the navigation. If the event handler returns a non-empty NavigatorEventArgs.NavigateTo, the navigator will move to this activity without evaluating the transitions specified for the current activity. The NavigatorEventArgs class is defined as followes:

public sealed class NavigatorEventArgs : EventArgs
{
    public string NavigateTo { get; set; }
}
Public Properties
Protected Members
ProcessCommand Implementation

It is necessary for the host application to be able to query for and change workflow state. The Navigator class implements the ProcessCommand virtual method that can perform actions that would support APIs defined on the NavigationManager.

protected virtual void ProcessCommand(object cmd, ActivityExecutionContext executionContext);

The Navigator receives command messages through a message queue created during its initialization process. The name of the queue is the same as the name of the Navigator activity. When a command message arrives in the queue, the Navigator is notified about its arrival. The Navigator takes the message from the queue and calls the virtual function ProcessCommand to execute the command.

The type of the command message enqueued by the host determines what command the host wants to execute. For example, the host can issue a GetCurrentContextCommand if it wants to query for the current workflow state and a SynchronizeCommand if it want to move to a particular state. There is an implicit contract between the host and the navigator as to what types of command message means what.

Transition

The Transition class represents a potential transition between the source and the target. Each transition has an associated Condition, which is evaluated at runtime. The first transition whose condition evaluates to true becomes the actual transition. The target of the actual transition becomes the next activity to be executed.

The following is the definition for the Transition class:

public sealed class Transition : DependencyObject
{
    // Public properties
    public string Target { get; set; }
    public ActivityCondition Condition { get; set; }
    public RuleSetReference ValidationRules { get; set; }
    public string Name { get; set; }
}

The TransitionCollection class is a collection class that contains transitions. The definition is as follows

public sealed class TransitionCollection : KeyedCollection<string, Transition>
{
    protected override string GetKeyForItem(Transition item)
    {
        return item.Target;
    }
}
Other Supporting Types

The design experience is an important part of building the navigator. The following types are used to provide a rich design experience.

NavigatorWorkflow

The NavigatorWorkflow is the root activity for a UI workflow. It inherits from Navigator. It has an additional property TimeoutDuration that can be used to automatically clean up abandoned workflow instances.

The following shows the definition for NavigatorWorkflow:

[Designer(typeof(NavigatorWorkflowDesigner), typeof(IDesigner))]
[ActivityValidator(typeof(NavigatorWorkflowValidator))]
public class NavigatorWorkflow : Navigator, IActivityEventListener<QueueEventArgs>
{
    public TimeSpan TimeoutDuration { get; set; }
}

Interaction Activities

InteractionBaseActivity

The InteractionBaseActivity class is an abstract class that captures the logic of modeling a user interaction inside the Navigator.

An InteractionBaseActivity is an event activity that unblocks the execution of the workflow when it receives a message from the host. It receives input messages through a message queue created during its initialization process. The name of the queue is the same as the name of the activity. When an InteractionBaseActivity starts executing, it first produces an InteractionContext object that captures the current state of the workflow. The InteractionContext generated is passed to the host application. It contains all the information necessary to display the UI associated with the current interaction activity, including the name of the current interaction and the appropriate data structure that contains the data to be displayed by the UI. The InteractionBaseActivity then checks to see if a message is available in the queue. If a message is available, it picks up the message and continues to execute its children if there are any. If a message is not available in the queue, it subscribe to the message arrive event and blocks the execution. The execution continues when a message arrives in the queue.

The following is the definition of the InteractionBaseActivity:

[ActivityValidator(typeof(InteractionActivityValidator))]
public abstract class InteractionBaseActivity: CompositeActivity, IDynamicPropertyTypeProvider, IActivityEventListener<QueueEventArgs>
{
    // Public properties
    public bool ReceiveInput { get; set; }
    public Type ActivityInputType { get; set; }
    public object ActivityInput { get; set; }
    public Type ActivityOutputType { get; set; }
    public object ActivityOutput { get; set; }
    public Permission RequiredPermission { get; set; }
    public Exception Error { get; set; }
 
    // Public Events
    public event EventHandler<EventArgs> Initialized { add; remove; }
    public event EventHandler<InputArrivedEventArgs> InputArrived { add; remove; }
 
    // Protected virtual members
    protected internal virtual InteractionContext GetInteractionContext();
    protected virtual void OnInitialize(IServiceProvider provider);
    protected virtual void OnUninitialize(IServiceProvider provider);
    protected virtual ActivityExecutionStatus OnExecute(ActivityExecutionContext executionContext);
    protected virtual ActivityExecutionStatus OnCancel(ActivityExecutionContext executionContext);
    protected virtual ActivityExecutionStatus OnHandleFault(ActivityExecutionContext executionContext, Exception exception);
    protected virtual void OnClose(IServiceProvider provider);
}
Public Properties
Public Events
public class UserInputArrivedEventArgs : EventArgs
{
    public string UserInput { get; }
}

Out-of-box we provide two concrete implementations of the InteractionBaseActivity: InteractionActivity and InteractionGroupActivity.

InteractionActivity

An InteractionActivity can receive one single input message. When a message arrives in the queue, the InteractionActivity takes the message, unblocks the workflow and executes its child in sequential order. It closes when the last child is closed.

The following is the definition of the InteractionActivity:

[ToolboxBitmap(typeof(InteractionActivity), "Resources.InteractionActivity.png")]
[ToolboxItem(typeof(InteractionActivityToolboxItem))]
[Designer(typeof(InteractionActivityDesigner))]
public class InteractionActivity: InteractionBaseActivity, IActivityEventListener<ActivityExecutionStatusChangedEventArgs>
{
    // Protected member overrides
    protected override ActivityExecutionStatus OnExecute(ActivityExecutionContext executionContext);
 
    // Event handler for the Close event on child closing.
    void IActivityEventListener<ActivityExecutionStatusChangedEventArgs>.OnEvent(Object sender, ActivityExecutionStatusChangedEventArgs e);
}

InteractionGroupActivity

An InteractionGroupActivity can receive multiple input messages. Every time a message comes to the queue, the InteractionGroupActivity takes the message, and based on the input data in the message activates one of its children. More than one child activities can be active (status = Executing) at the same time. The InteractionActivity then goes back to wait for another message to come in. It does not close until either the FinishCondition is met or all of its children have closed.

The following is the definition of the InteractionGroupActivity:

[ToolboxBitmap(typeof(InteractionGroupActivity), "Resources.InteractionGroup.png")]
[ToolboxItem(typeof(InteractionGroupActivityToolboxItem))]
[Designer(typeof(InteractionGroupDesigner))]
public class InteractionGroupActivity: InteractionBaseActivity, IActivityEventListener<ActivityExecutionStatusChangedEventArgs>
{
    // Public Members
    public ActivityCondition UntilCondition { get; set; }
 
    // Protected member overrides
    protected override ActivityExecutionStatus OnExecute(ActivityExecutionContext executionContext);
 
    // Event handler for the Close event on child closing.
    void IActivityEventListener<ActivityExecutionStatusChangedEventArgs>.OnEvent(Object sender, ActivityExecutionStatusChangedEventArgs e)
}
Other Supporting Types

The following types are implemented to support the design time experience for interaction activities.

Workflow hosting components

NavigationManager

Continuation based web development has been the central scheme for many new web development platforms. It’s based on the premise that web applications can be written using a stateful programming model while at the same time maintain is scalability inherent in the stateless web server.

WF is the perfect platform to implement a continuation server with its ability to pause, resume, load and unload programming states. All we’re missing is an API that exposes the WF capabilities in a more UI-developer-friendly fashion. The NavigationManager class provides exactly that. This class creates an abstraction on top the WF WorkflowRuntime and WorkflowInstance classes. It creates and manages workflows for the running UI application. It also serves as a service container for runtime services.

The NavigationManager allows the UI application to access the workflow but hides the complexities that are associated with the WorkflowRuntime and the WorkflowInstance class.

Its methods reflect the basic operations provided by a navigation service, such as Start, GoFoward, GoBack, NavigateTo, etc. The NavigationManager also provides access to workflow state through the GetCurrentContext and GetHistory methods.

There is a one-to-one relationship between the NavigationManager and the WorkflowRuntime. The NavigationManager object creates and maintains a reference to a WorkflowRuntime object. The NavigationManager expose WorkflowRuntime as a public property. This allows more sophisticated solutions to fully customize the runtime environment. Certain restrictions are enforced by the NavigationManager as to what services can be added to the runtime. For example, only the SynchronizationContextSchedulerService can be used. No other scheduling services are allowed. We’ll take about why we impose this restriction later when we talk about the SynchronizationContextSchedulerService.

The following APIs are exposed on the NavigationManager class.

public sealed class NavigationManager : IManageContinuation
{
    // Constructors
    public NavigationManager();
    public NavigationManager(string configSection);
 
    // Continuation Implementation
    public Guid CreateWorkflow(XmlReader xaml, XmlReader rules);
    public Guid CreateWorkflow(XmlReader xaml, XmlReader rules, Dictionary<string, object> parameters);
    public Guid CreateWorkflow(Type workflowType);
    public Guid CreateWorkflow(Type workflowType, Dictionary<string, object> parameters);
    public InteractionContext Start(Guid durableHandle);
    public InteractionContext GoForward(Guid durableHandle, UserInput input);
    public InteractionContext GoForward(Guid durableHandle, Bookmark bookmark, UserInput input);
    public InteractionContext GoBack(Guid durableHandle);
    public InteractionContext Navigate(Guid durableHandle, Bookmark bookmark);
    public void Terminate(Guid durableHandle);
    public InteractionContext GetCurrentContext(Guid durableHandle);
    public InteractionContext GetContext(Guid durableHandle, Bookmark bookmark);
    public ReadOnlyCollection<Bookmark> GetHistory(Guid durableHandle);
 
    // Public Properties
    public WorkflowRuntime WorkflowRuntime { get; set; }
}
CreateWorkflow

The Create overloads create a new instance of the workflow. These overloads directly correspond to the CreateWorkflow methods on WorkflowRuntime. We support creating workflow instances from both a precompiled workflow type as well as a XOML document in the forms of XmlReader.

Start

Start calls WorkflowInstance.Start to start running the workflow after it’s created. Start is a synchronous call. The call will not return until the first Idle state is reached by the workflow. In most cases, the initialization and the execution will happen on the same thread as the caller thread.

GoForward

A UI workflow typically consists of a navigator, a set of interaction activities and rules that govern how to move between these interaction activities. The GoForward method allows the host application to submit data to the workflow and signal the workflow to move from the current interaction to the next. The method works by sending a message to the interaction activity that is currently waiting for input. Once the message is received by the interaction activity, the workflow continues its execution until it reaches the next interaction activity, at which point a new InteractionContext will be created and control returns back to the host.

GoBack

GoBack method sends a message to the workflow to cancel the current interaction activity and re-execute the previous activity. GoBack will only succeed if the workflow allows synchronization either to all activities or activities that are in the history list.

Nagivate

NavigateTo method allows the host application to cancel the current interaction activity and to start executing a different activity specified by the host. Navigate will fail if the workflow does not allow synchronization or if the target activity is not in the history list but the workflow only allows synchronization to activities that are in the history list. We use this method to implement client cached backward navigation. When the host submits data from a previously cached page, we first synchronize the workflow with the client UI by navigating to the interaction corresponds to the previous UI, and then submit from there.

GetContext & GetCurrentContext

GetCurrentContext returns the current InteractionContext. An InteractionContext object contains information representing the state of the workflow.

GetContext returns the InteractionContext for a specific Bookmark (i.e. interaction).

GetHistory

The GetHistory method returns the list of past Bookmarks since the beginning of the workflow in the reverse order as they’re performed. A history is only valid when the workflow instance is not completed. We’ll discuss more on how the history stack is created later when we talk about the workflow model.

More about Command Processing

Since host applications can not directly access the workflow object, the only way to query or change workflow state is through messaging. GoBack, Navigate, GetContext, GetCurrentContext and GetHistory are all implemented through message exchanges between the NavigationManager and the workflow. We call this Command Processing.

To implement command processing, the host must know how to send a command message and the workflow must know how to receive this message and process the command. To establish a standard communication of command messages between the host and the workflow, we automatically create a command processing message queue for the root navigator activity. The queue name, which is the name of the root activity, is well-known by both the NavigationManager and the Navigator activity. The queue is created when the Navigator activity is initialized and a subscription to the queue event is added to notify the navigator of message arrivals.

Once a command message arrives, the navigator will need to know how to process it. Our Navigator class implements the ProcessCommand virtual method. This method can differentiate different types of command by inspecting the type of the message. It then process each command that it recognized. If it does not recognize the command, the message is ignored.

InteractionContext

The InteractionContext class captures information needed to represent the current interaction. It is defined as the following:

public class InteractionContext
{
    public Bookmark Bookmark { get; }
    public object ActivityOutput { get; }
    private PrincipalPermission requiredPermission { get; }
    private Exception validationError { get; }
}

For InteractionGroupActivity, we introduce a derived class called InteractionGroupContext to include information about child interactions.

public class InteractionGroupContext : InteractionContext
{
    public ReadonlyCollection<ageContext> ActiveChildContexts { get; }
    public ReadonlyCollection<ageContext> AvailableChildContexts { get; }
}

UserInput

The UserInput class defines data submitted to the workflow from the host application.

public class UserInput
{
    public string Action { get; set; }
    public object ActivityInput { get; set; }
    public IPrincipal Principal { get; set; }
}

Bookmark

The Bookmark class defines data needed to identify each interaction activity inside a workflow.

public class Bookmark
{
    public string ActivityName { get; set; }
    public Guid InteractionId { get; set; }
}

Configuration Support

The NavigationManager uses a config handler similar to WorkflowRuntime config handler.

An optional StartOnDemand attribute can be used to specify whether or not to automatically start a new workflow instance when the application starts. The default is “true”. If set to “false”, the application will start a new workflow instance by passing a parameter “StartOnDemand=true” in the query string.

The Workflow element is used to specify which workflow to be associated with the application. The attribute mode can be either “Compiled” or “XOML”. When mode is “Compiled”, the value attribute should specify the type of the compiled workflow; when mode is “XOML”, the value attribute should specify the location of the XOML file, another attribute “RulesFile” is also used to optionally specify the location of a rules file.

The Services element contains types and parameters for workflow runtime services.

Below is an example of the NavigationSettings config section:

<NavigationManagerSettings StartOnDemand="false" >
  <Workflow mode="Compiled" value="ASPUIWorkflow.Workflow1, ASPUIWorkflow"/>
  <!--<Workflow mode="XOML" value="WebSite/XAMLWorkflow.xoml" rulesFile="WebSite/XAMLWorkflow.rules" />-->
  <Services>
    <add type="System.Workflow.Runtime.Hosting.SqlWorkflowPersistenceService, System.Workflow.Runtime, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" ConnectionString="Initial Catalog=ASPPersistence;Data Source=localhost;Integrated Security=SSPI;" UnloadOnIdle="true"/>
  </Services>
</NavigationManagerSettings>

UI specific hosting components

Hosting inside ASP.Net Application

We provide an out-of-box WFWebHostingModule class that makes hosting PageFlow directly inside an Asp.Net application very easy. The WFWebHostingModule class provides a central point of control where all client requests are initially processed. It acts as a translator between client inputs and the workflow. It translates page actions to workflow commands, causing the workflow to change state, thus producing the next page.

The HttpApplication Pipeline

The following is a list of operations that happens when an http request is processed by an HttpApplication class (for a complete list of events fired during a request/response cycle, see the MSDN article “ASP.NET Application Life Cycle Overview”):

  1. When ASP.NET receives the first request for any resource in an application, a class named ApplicationManager creates an application domain and a HostingEnvironment.
  2. The ASP.NET core objects such as HttpContext, HttpRequest, and HttpResponse are created and initialized.
  3. The application is started by creating an instance of the HttpApplication class.
  4. Any configured modules are created.
  5. Request processing starts after the HttpApplication has validated the incoming request and identified the location of the requested resources.
  6. The BeginRequest event is fired, followed authentication and cache resolution event.
  7. Based on the file name extension of the requested resource (mapped in the application's configuration file), a class that implements IHttpHandler is selected to process the request. If the request is for an object (page) derived from the Page class and the page needs to be compiled, ASP.NET compiles the page before creating an instance of it.
  8. IHttpHandler.ProcessRequest is called to generate response to be returned to the client.
  9. Other post-ProcessRequest events are fired.
  10. The EndRequest event is fired.

The WFWebHostingModule implements the IHttpModule interface and subscribes to the BeginRequest and the EndRequest events.

BeginRequest

When BeginRequest is called, the module checks to see if there is a workflow already associated with the current request by looking for the “FlowId” parameter in the query string. If it does not find one, it starts a new workflow based on the workflow type specified in the web.config file. The workflow starts running and the first interaction activity will be executed to produce the first InteractionContext. The module then looks up the page url from the config file using the activity name inside the InteractionContext and redirects to the page. The workflow then goes on idle, waiting for user to click the “Next” button, and returns the control to the module. The module exits BeginRequest and the normal HttpApplication pipeline resumes, sending the response down to the client.

If an existing workflow is found to be associated with the request, the module calls NavigationManager.GetCurrentContext to get the current InteractionContext. This causes the workflow being reloaded into the process. This procedure ensures that the user can come back to the page they left off from in a previous session and all the data will be restored automatically.

EndRequest

When EndRequest is called, the module processes the resume event initiated by the user clicking the SubmitButton on the web page. The module first identifies the type of resume action as being either “Submit” or “Back” or “Cancel” and then acts accordingly.

AspUserInput Class

The AspUserInput inherits from UserInput. In addition to store input data submitted by the UI components, it provides static methods to be called by the UI components to submit the data.

public class AspNetUserInput : UserInput
{
    public IPrincipal Principal { get; set; }
 
    public static void GoForward(object data, IPrincipal principal);
    public static void GoForward(string action, object data, IPrincipal principal);
    public static void GoBack(IPrincipal principal);
    public static void Cancel(IPrincipal principal);
}

These static methods does nothing but creating an AspUserInput class instance and put it in the HttpContext.Items collection. When the request is processed by the WFWebHostingModule, the module looks up the current UserInput and calls the correct method on NavigationManager to pass the user input data to the workflow and moves on to the next destination page.

Configuration Support

The WFWebHostingModule uses a config handler to define associations between interaction activities and web pages. Following is an example:

<AspNavigationSettings>
  <PageMappings>
    <add bookmark="EnterAlias" location="~/Default.aspx"/>
    <add bookmark="EnterProfile" location="~/Page2.aspx"/>
    <add bookmark="EnterBilling" location="~/Page3.aspx"/>
    <add bookmark="Problem" location="~/Page4.aspx"/>
    <add bookmark="Summary" location="~/Page5.aspx"/>
    <add bookmark="Confirmation" location="~/LastPage.aspx"/>
  </PageMappings>
  <ExceptionMappings>
    <add type="Microsoft.Samples.Workflow.UI.WorkflowNotFoundException" location="~/ErrorPage.aspx"/>
    <add type="Microsoft.Samples.Workflow.UI.WorkflowCanceledException" location="~/ErrorPage.aspx"/>
    <add type="System.ArgumentException" location="~/ErrorPage.aspx"/>
    <add type="System.Security.SecurityException" location="~/ErrorPage.aspx"/>
  </ExceptionMappings>
</AspNavigationSettings>

The PageMappings element contains a list of associations of activity names (“bookmark”) to its corresponding web page names (“location”).
The ExceptionMappings element specifies a list of association of an exception types (“type”) with a web page that is used to display this exception (“localtion”).

SynchronousSchedulerService

The SynchronousSchedulerService uses the .Net SynchronizationContext to implement workflow thread scheduling. Several UI technologies support the .Net SynchronizationContext in their threading model. By building a scheduler service on top of SynchronizationContext, we can delegate complex threading logic to individual UI technology and work seamlessly with all of them. The SynchronousSchedulerService delegate the Schedule() call to SynchronizationContext.Send. We do not care how each UI technology implements Send.

The NavigationManager APIs are entirely synchronous as we call SynchronizationContext.Send for all workflow operations. If the client application needs to optimize user response, it should call the NavigationManager APIs using the standard .Net BeginInvoke async programming model.

The following shows the design and implementation of the SynchronousSchedulerService class:

public sealed class SynchronousSchedulerService : WorkflowSchedulerService
{
    protected override void Schedule(WaitCallback callback, Guid workflowInstanceId)
    {
        if (SynchronizationContext.Current != null)
        {
            SynchronizationContext.Current.Send(delegate(object o2) { callback(workflowInstanceId); }, null);
        }
        else
            callback(workflowInstanceId);
    }
}

Normally, to change the scheduling behavior of WF, the host would write a custom WorkflowSchedulerService. However, a UI developer is more familiar with the already prevalent .Net concept of SynchronizationContext. So instead of letting the host applications to plug in their own WorkflowSchedulerService, we let them change the scheduling behavior through implementing their own SynchronizationContext. The host would implement the Send method to change workflow scheduling.

© 2007 Microsoft Corporation. All rights reserved.