Click here to Skip to main content
Click here to Skip to main content
Go to top

Navigational Workflows Unleashed in WWF/ASP.NET 3.5

, 21 Oct 2008
Rate this:
Please Sign up or sign in to vote.
Case-study on the internals of a Navigational Workflow engine for a fictional dating website called “World Wide Dating”.

DateSite Screenshot

Contents

Introduction

What is a Navigational Workflow? In a nutshell, a Navigational Workflow is a method of describing navigational logic for an application as well as the rules that ensure that logic is adhered to. Any application that must follow some sort of process flow requires a Navigational Workflow.

This article is a case-study on the internals of a Navigational Workflow engine for a fictional dating website called "World Wide Dating". The host application is written in ASP.NET, and the Navigational Workflow is written using Windows Workflow Foundation.

Navigational Logic and Rules

World Wide Dating has six main pages:

  1. Default.aspx (the start page)
  2. Basics.aspx
  3. Appearance.aspx
  4. Lifestyle.aspx
  5. Interests.aspx
  6. Complete.aspx (the last page)

The page that first appears when the application starts is default.aspx. From here, the user can create, rehydrate, and delete Navigational Workflows. The "natural flow" of the workflow is:

natural workflow logic

In addition to the above, the workflow allows jumping from one State to another if, and only if, the user has visited that State already using the "natural flow" defined above. To give a concrete example, imagine having navigated the following scenario: default => basics => appearance => lifestyle. The user is now in the lifestyle State. From here, they can follow the workflow "naturally" and go to the interests State or back to the appearance State. In addition, the user can choose to skip to the basics State. The user cannot, however, skip to the complete State, because that State has not yet been visited. If the user attempts to prematurely skip to a State, then they are redirected to an error page (more on error pages later).

Setting Up the Workflow Runtime

Because this is an ASP.NET web application, we must load the ManualWorkflowSchedulerService into the workflow runtime. If we do not, then workflows will execute asynchronously and the host application might return before the workflow is done executing. This is set up inside the web.config:

<add type=
    "System.Workflow.Runtime.Hosting.ManualWorkflowSchedulerService, 
     System.Workflow.Runtime, 
     Version=3.0.0.0, 
     Culture=neutral, 
     PublicKeyToken=31bf3856ad364e35" />

There is considerable overhead that gets introduced in creating and starting the workflow runtime with each request. To avoid this, we can load the workflow runtime just once during application startup, and shut it down when the application ends. This is done in Global.asax.cs:

protected void Application_Start(object sender, EventArgs e)
{
    // create an instance of workflow runtime, 
    // loading settings from web.config
    WorkflowRuntime workflowRuntime = 
        new WorkflowRuntime(
            WorkflowManager.WorkflowRuntimeKey);

    // add the external data exchange service to the 
    // runtime to allow for local services
    ExternalDataExchangeService exchangeService = 
        new ExternalDataExchangeService(
            WorkflowManager.LocalServicesKey);
    workflowRuntime.AddService(exchangeService);

    // start the workflow runtime
    workflowRuntime.StartRuntime();

    // save the runtime for use by the entire application
    Application[WorkflowManager.WorkflowRuntimeKey] = workflowRuntime;
}

protected void Application_End(object sender, EventArgs e)
{
    // obtain reference to workflow runtime
    WorkflowRuntime workflowRuntime = 
        Application[WorkflowManager.WorkflowRuntimeKey] 
            as WorkflowRuntime;

    // stop the runtime
    if (workflowRuntime != null)
    {
        workflowRuntime.StopRuntime();
        workflowRuntime.Dispose();
    }
}

Navigational Workflow Architecture

Our Navigational Workflow is built using the State-Machine workflow template. Each StateActivity in the Navigational Workflow maps to a corresponding ASPX page. This mapping is inside the web.config:

<elements>
    <element stateName="InitialState" pageName="default.aspx" weight="0" />
    <element stateName="BasicsState" pageName="basics.aspx" weight="10" />
    <element stateName="AppearanceState" pageName="appearance.aspx" weight="20" />
    <element stateName="LifestyleState" pageName="lifestyle.aspx" weight="30" />
    <element stateName="InterestsState" pageName="interests.aspx" weight="40" />
    <element stateName="CompleteState" pageName="complete.aspx" weight="50" />
</elements>

The structure of the application is represented in the following diagram:

DateSite Architecture

The ASP.NET host application uses the Workflow Manager as a wrapper to communicate with the Navigational Workflow. All requests into the Navigational Workflow are delegated through the Workflow Manager.

Creating and Starting a New Navigational Workflow

The host application makes the following call to start a new workflow:

_workflowManager.StartNewWorkflow();

The StartNewWorkflow method is located in WorkflowManager.cs, and is responsible for creating and starting a new Navigational Workflow instance:

public string StartNewWorkflow()
{
    // create and start the navigational workflow instance
    WorkflowInstance instance = 
        _workflowRuntime.CreateWorkflow(typeof(NavigationWorkflow));
    instance.Start();

    // execute the workflow synchronously on the current thread
    RunWorkflow(instance.InstanceId);

    // put the workflow instance id in session for this user
    this.CurrentWorkflowInstanceId = instance.InstanceId;

    // execute the workflow on the current thread and return the page to go to
    return GetPageToGoTo(instance.InstanceId);
}

From here, control transfers to the Navigational Workflow instance defined in NavigationalWorkflow.cs. The InitialState is registered as the starting state of the workflow, and is the point of entry into the workflow:

InitialState Screenshot

The first Activity inside the InitialState Activity is of type StateInitializationActivity. The Activities within a StateInitializationActivity execute immediately upon entering a StateActivity. Double-clicking on the initializeInitialState Activity opens up the dialog below:

initialStateInitialActivity Screenshot

The first Activity that executes is a CodeActivity called codeInitializeInitialState, and it executes the code below:

private void codeInitializeInitialState_ExecuteCode(object sender, EventArgs e)
{
    _heaviestWeight = -1; // default setting for heaviest weight
}

The _heaviestWeight variable is a marker that keeps track of how far along in the Navigational Workflow a particular user got. This is used in determining whether or not the user can skip to a given State (and in essence, whether or not the user can skip to a given ASPX page). This marker will be discussed in more detail later in the section Skipping Between Pages. We will defer discussion of the remaining code in the InitialState and move on to explore the role Events play in navigation.

The Previous and Next Events: How Navigation Works

Introduction

The heart of the Navigational Workflow is the Previous and Next events. The host application raises these events via the Workflow Manager to obtain the previous and next pages to go to. Every StateActivity in the Navigational Workflow listens for the Previous and Next events. It is these events that drive transitions between different StateActivitys. Once the workflow transitions into a StateActivity, the corresponding ASPX page of the current StateActivity is queried from the web.config and passed to the host application.

Explanation

Let us imagine that the current State of the Navigational Workflow is the LifeStyle State:

lifestyle screenshot

According to the web.config, the ASPX page that maps to this State is lifestyle.aspx (that is the page the user should be on when in this State), and the weight of the Lifestyle State is 30:

<element stateName="LifestyleState" pageName="lifestyle.aspx" weight="30" />

The LifeStyle State listens for two events. These events are Previous and Next, and are defined in INavigationService.cs:

event EventHandler<ExternalDataEventArgs> Previous;
event EventHandler<ExternalDataEventArgs> Next;

Let us examine how to transition to the previous page starting from the host application and work our way back to the Navigational Workflow. To navigate to the previous page, the host application makes the following call into the WorkflowManager:

// navigate to the previous page
string pageToGoTo = _workflowManager.Previous();
_workflowManager.ManagedRedirect(pageToGoTo);

The above code first calls the Previous method on the WorkflowManager to obtain the page to go to. The next line is a wrapper around Response.Redirect to transfer the user to the previous page. We will now explore the Previous method of the WorkflowManager:

public string Previous()
{
    /* 180 */ // prepare external data exchange arguments
    /* 181 */ ExternalDataEventArgs args = 
                new ExternalDataEventArgs(this.CurrentWorkflowInstanceId);

    /* 183 */ // make local service call into Navigation Workflow
    /* 184 */ _navigationService.OnPrevious(args);

    /* 186 */ // execute the workflow on the current thread and return the page to go to
    /* 187 */return GetPageToGoTo(args.InstanceId);
}

Line 181 prepares the ExternalDataEventArgs to send to the Navigational Workflow. Line 184 raises the Previous event inside the LifestyleState in the Navigational Workflow; however, since we are using the System.Workflow.Runtime.Hosting.ManualWorkflowSchedulerService to execute our workflows, the Navigational Workflow will not receive the event until ManualWorkflowSchedulerService explicitly runs the workflow instance. Let's take a look at line 187:

private string GetPageToGoTo(Guid instanceId)
{
    /* 399 */ // execute the workflow on the current asp.net thread
    /* 400 */ RunWorkflow(instanceId);

    /* 402 */ // return where the user should go
    /* 403 */ return _pageToGoTo;
}

Line 400 makes a call to RunWorkflow, which is where the ManualWorkflowSchedulerService gets called to execute the current Navigational Workflow instance:

private bool RunWorkflow(Guid instanceId)
{
    ManualWorkflowSchedulerService scheduler = GetWorkflowSchedulerService();
    return scheduler.RunWorkflow(instanceId);
}

Now that control has transferred to the Navigational Workflow, let us examine how the Lifestyle State is set up to listen for the Previous event, and what happens when it receives it:

lifestyle screenshot

The eventLifestyleStatePrevious EventDrivenActivity contains the code that listens for the Previous event. Double-clicking eventLifestyleStatePrevious opens the following:

lifestyleStateEventLifestyleStatePrevious screenshot

The HandleExternalEventActivity named handleLifestylePrevious is listening for the Previous event. Examining the handleLifestylePrevious properties illustrate how to set up the event listening:

handleLifestylePreviousProperties Screenshot

After the Previous event is received, the SetStateActivity executes and moves the Navigational Workflow into the Appearance State:

appearanceState Screenshot

Upon entering the Appearance State, the StateInitializationActivity named initializeAppearanceState executes. Double-clicking it opens up the designer for the Activity:

initializeAppearanceState Screenshot

There is a single CallExternalMethodActivity. To figure out what exactly is happening, we need to investigate the properties of the CallExternalMethodActivity:

callAppearanceStateOnPageToGoTo Screenshot

The CallExternalMethodActivity calls the OnPageToGoTo Local Service method, passing the PageToGoToEventArgs into it. The OnPageToGoTo method raises the PageToGoToReceived event which the WorkflowManager is listening for. Before the Local Service method executes, the CallExternalMethodActivity executes the InitializeOutgoingMessage method:

private void InitializeOutgoingMessage(object sender, EventArgs e)
{
    /* 90 */ // initialize the outgoing args to send to the host application
    /* 91 */ _pageToGoToEventArgs = 
                new PageToGoToEventArgs(this.WorkflowInstanceId, 
                    GetPageToGoTo());
    /* 92 */
    /* 93 */ // each state has a weight associated with it. keep 
             // track of the heaviest weight (logically the
    /* 94 */ // furthest state in the navigation process) 
             // so we can skip there later
    /* 95 */ if (_heaviestWeight < NavigationSection.Current.
                                        Elements[this.CurrentStateName].Weight)
    /* 96 */ {
    /* 97 */    _heaviestWeight = NavigationSection.Current.
                                        Elements[this.CurrentStateName].Weight;
    /* 98 */ }
}

The GetPageToGoTo method on line 91 retrieves the corresponding page associated with this StateActivity from the web.config:

private string GetPageToGoTo()
{
    // query the page to go to from the navigation section
    string pageToGoTo = NavigationSection.Current.
                            Elements[this.CurrentStateName].PageName;
    return pageToGoTo;
}

When the Local Service method OnPageToGoTo executes, the Workflow Manager is listening for it and executes the following code upon receiving the event:

private void _navigationService_PageToGoToReceived(object sender, 
                                PageToGoToEventArgs e)
{
    // get the page to go to
    _pageToGoTo = e.PageToGoTo;
}

The above code inside the Navigational Workflow executes when the RunWorkflow method is called. If we go back and take a look at the GetPageToGoTo method:

private string GetPageToGoTo(Guid instanceId)
{
    /* 399 */ // execute the workflow on the current asp.net thread
    /* 400 */ RunWorkflow(instanceId);

    /* 402 */ // return where the user should go
    /* 403 */ return _pageToGoTo;
}

By the time line 403 executes, the _pageToGoTo variable will already have been populated with the page to go to. The host application receives this page, and then redirects accordingly.

Synchronization, Self-Healing, and the Browser Back/Forward Buttons

Browser Back/Forward Buttons

In the past, the browser's back/forward buttons have caused many issues with synchronization. The issue is that most browsers cache web pages so that when you click the browser's back/forward buttons, a cached copy of the web page is retrieved. Because the page is cached, there is no postback to the server to update the Navigational Workflow and to perform server side validation. In the past, the most simple solution was to disable the browser's back/forward buttons. This section proposes a solution to the problems presented using the browser back/forward buttons.

The first thing that must be done is caching must be disabled. With caching disabled, the browser has no choice but to postback to the server to retrieve the web page whenever the browser's back/forward buttons are clicked. There is a performance hit associated with this in that every request must be sent to the server; however, this is the only way to force a postback to the server to occur when the user clicks the browser's back/forward buttons. The code below is in the MasterPage of the host application, and disables caching across the website:

protected void Page_Load(object sender, EventArgs e)
{
    // disable page caching
    Response.Expires = 0;
    Response.Cache.SetNoStore();
    Response.AppendHeader("Pragma", "no-cache");
}

The code above will make sure that the page postbacks to the server whenever the user clicks the browser's back/forward buttons. It has been tested and verified to work in both Internet Explorer 7 and Firefox. Safari 3.1, however, requires one extra step. To get the page to postback to the server when the browser's back/forward button is clicked using the Safari browser, the following JavaScript method is attached to the onunload event of the HTML body in the Master page:

<body onunload="javascript:safariBackForwardBrowserButtonFix();">

There is nothing fancy about the JavaScript method that is called. Its method body is empty, but nevertheless, this is what is required to force the Safari browser to postback to the server when its back/forward buttons are clicked.

But, how do we know that the page is really posting back to the server when clicking the browser's back/forward buttons? One way to test for this is to use breakpoints. However, there is also a control on every page that is called: DebuggingPane.ascx. One piece of information this control displays is the current datetime timestamp. If the timestamp changes to the current time when you click the browser's back/forward buttons, then you can be sure a postback to the server occurred.

Synchronization and Self-Healing

Synchronization ensures that the web page the user is on is the web page that they should be on. The following pages require synchronization (note that the pages in the list below are represented by StateActivitys in the Navigational Workflow, the distinction is that Error pages are not represented by StateActivitys):

  1. Basics.aspx
  2. Appearance.aspx
  3. Lifestyle.aspx
  4. Interests.aspx
  5. Complete.aspx

The pages above all call the SynchronizeWorkflow method inside of their Page Load event. The SynchronizeWorkflow method is located inside the Workflow Manager class:

public void SynchronizeWorkflow()
{
    // check if there is a workflow in session
    if (this.CurrentWorkflowInstanceId == Guid.Empty)
    {
        // redirect to the start page
        ManagedRedirect(GetStartPage());
    }

    // the page to go to if the workflow is not synchronized
    string pageToGoTo = string.Empty;

    try
    {
        // raise event to ask the workflow if it is synchronized
        IncomingSynchronizeEventArgs args = 
            new IncomingSynchronizeEventArgs(
                this.CurrentWorkflowInstanceId, GetCurrentPageName());
        _navigationService.OnIncomingSynchronize(args);

        // execute the workflow synchronously on the current thread
        RunWorkflow(this.CurrentWorkflowInstanceId);

        // return if the workflow is synchronized
        if (_isSynchronized)
        {
            return;
        }

        // get the name of the state that the user should be in
        StateMachineWorkflowInstance instance = 
            new StateMachineWorkflowInstance(
                _workflowRuntime, this.CurrentWorkflowInstanceId);
        instance.SetState(_stateToGoTo);

        // get the name of the page that the user should be on
        pageToGoTo = Rehydrate(this.CurrentWorkflowInstanceId);            
    }
    catch (Exception ex)
    {
        // could not synchronize the workflow
        ManagedRedirect(GetStartPage());
    }

    // if the page the user should be on is not 
    // the same as the page that they currently
    // are on then redirect the user to the page they should be on
    if (!pageToGoTo.Equals(GetCurrentPageName()))
    {
        ManagedRedirect(pageToGoTo);
    }
}

The SynchronizeWorkflow method asks the Navigational Workflow if it is in sync by raising the IncomingSynchronize event. The Navigational Workflow listens for this event, and returns its response by raising the OutgoingSynchronizeReceived event. The synchronization code checks to see if any type of URL hacking has occurred. By URL hacking, I am referring to modifications made to the World Wide Dating website URL by the user and not through the Navigational Workflow code. If the URL has been modified by the user, then the synchronization code checks to see if the URL the user is trying to navigate to is an allowable page for them to be on. If it is allowed, then the Workflow Manager will self-heal the Navigational Workflow and transition it to the proper StateActivity. If it is not, then the Workflow Manager will redirect the user back to the page they were on before they attempted to hack the URL. It is worth noting that clicking the browser's back/forward button is a way of hacking the URL; the reason being is that by clicking these buttons, events are not raised inside the Navigational Workflow that indicates it needs to transition to another State. Therefore, self-healing also occurs when using the browser back/forward buttons.

If we double-click the eventIncomingSynchronize EventDrivenActivity inside the Navigational Workflow, the below appears in the designer:

eventIncomingSynchronize Screenshot

The HandleExternalEventActivity called handleIncomingSynchronize listens for the IncomingSynchronize event. The CallExternalEventActivity called callOnOutgoingSynchronize has the following properties:

callOutgoingSynchronize Screenshot

The first thing that happens here is the InitializeOutgoingSynchronizeMessageEventArgs method executes:

private void InitializeOutgoingSynchronizeMessageEventArgs(object sender, EventArgs e)
{
    NavigationElement expectedElement = 
        NavigationSection.Current.Elements.
            GetElementByPageName(GetPageToGoTo());
            
    NavigationElement currentElement = 
        NavigationSection.Current.Elements.
            GetElementByPageName(_incomingSynchronizeEventArgs.CurrentPage);

    // initialize the outgoing synchronize event args
    _outgoingSynchronizeEventArgs = 
        new OutgoingSynchronizeEventArgs(this.WorkflowInstanceId);

    // check if the workflow is synchronized by comparing the 
    // expected page to the current page the user is on
    _outgoingSynchronizeEventArgs.IsSynchronized = 
        expectedElement.PageName.Equals(currentElement.PageName);

    // workflow is synchronized
    if (_outgoingSynchronizeEventArgs.IsSynchronized)
    {
        return;
    }


    /* the below checks are in reference to if the 
       user tried to manipulate the URL by hand */


    // if the weight of the page the user is on is less than or equal
    // to the heaviest weight, then the workflow is able to "self-heal" and
    // transition itself to the state it should be in.the host application 
    // will make the transition, but the StateToGoTo below tells it which is
    // the right state to transition to.
    if (currentElement.Weight <= _heaviestWeight)
    {
        _outgoingSynchronizeEventArgs.StateToGoTo = currentElement.StateName;
    }
    // otherwise the user is trying to skip to a page further
    // in the workflow than they can currently go, so set the
    // state of the workflow to be what we expect it should be
    else
    {
        _outgoingSynchronizeEventArgs.StateToGoTo = expectedElement.StateName;
    }
}

The above code populates the _outgoingSynchronizeEventArgs data member with the information needed for the Workflow Manager to synchronize the Navigational Workflow. The _outgoingSynchronizeEventArgs data member maps to the OutgoingSynchronizeMessageEventArgs property. This property gets passed into the OnOutgoingSynchronize Local Service method, which when called raises the OutgoingSynchronizeReceived event that the Workflow Manager is listening for. All of this happens from the SynchronizeWorkflow method inside the WorkflowManager.

Error Pages

So far, we have established that every ASPX web page (that is part of the natural workflow, i.e.,~ not an error page) maps to a unique StateActivity within the Navigational Workflow, and that synchronization is used to ensure that we are on the correct page. Inevitably though, an error will occur sometimes in our applications, and our Navigational Workflow needs to be prepared to handle them.

When an error occurs, the host application determines what kind of error it is, and then raises the Error event inside the Navigational Workflow, and indicates to it what type of error has occurred. The Navigational Workflow then returns the error page to navigate to. The following code is an example of how the host application raises the Error event inside the Navigational Workflow and indicates to it that the type of error is "Unknown":

string pageToGoTo = 
    _workflowManager.Error(DateSiteNavigation.ErrorType.Unknown);
    
_workflowManager.ManagedRedirect(pageToGoTo);

The Workflow Manager executes the following code to raise the Error event inside the Navigational Workflow to obtain the error page to navigate to:

public string Error(ErrorType errorType)
{
    // prepare external data exchange arguments
    ErrorEventArgs args = 
        new ErrorEventArgs(this.CurrentWorkflowInstanceId, errorType);

    // make local service call into Navigational Workflow
    _navigationService.OnError(args);

    // execute the workflow on the current thread and 
    // return the page to go to
    return GetPageToGoTo(args.InstanceId);
}

The first concept to recognize when dealing with error pages is that an error page is always a valid page to be on regardless of what State the Navigational Workflow is in. The Error event is defined in the Local Service as:

event EventHandler<ErrorEventArgs> Error;

To quote Popeye The Sailor Man, "I am what I am and that's all that I am." Every page in our workflow, whether it be a navigational page or an error page, knows about itself. It does not know anything about the other pages, but it knows what its responsibilities are. That said, an error page knows it is an error page, and it understands the concept that it is always a valid page to be on in terms of navigation. Therefore, all error pages make sure to not call the synchronize workflow method.

Double-clicking the EventDrivenActivity called errorEvent inside of the Navigational Workflow opens the following in the designer:

eventError Screenshot

The first Activity is a HandleExternalEventActivity called handleError. If we investigate its properties, we will see that it is configured to listen for the Error event, and that it binds the argument e to the Navigational Workflow property ErrorEventArgs:

handleErrorProperties Screenshot

After the Error event is raised, the CallExternalMethodActivity called callErrorOnPageToGoTo executes. Let's take a look at its properties:

callErrorOnPageToGoTo Screenshot

Notice that the properties are almost identical to the those of the CallExternalMethodActivity discussed in the section: The Previous and Next Events: How Navigation Works. The only difference is, the MethodInvoking property calls the method InitializeErrorMessage before the Local Service method fires. This method is responsible for initializing the PageToGoToEventArgs with the name of the ASPX error page to go to:

private void InitializeErrorMessage(object sender, EventArgs e)
{
    /* 109 */ // initialize the outgoing args to send to the host application
    
    /* 110 */ _pageToGoToEventArgs = 
                new PageToGoToEventArgs(
                    this.WorkflowInstanceId, GetErrorPageToGoTo());
}

The GetErrorPageToGoTo method on line 110 gets the name of the ASPX error page to go to:

private string GetErrorPageToGoTo()
{
    // query the page to go to from the navigation section
    string errorName = _errorEventArgs.ErrorType.GetErrorName();
    string pageToGoTo = NavigationSection.Current.Errors[errorName].PageName;
    return pageToGoTo;
}

This method obtains the name of the error from the ErrorEventArgs. From there, it proceeds to retrieve the error page name from the web.config using NavigationSection. The web.config section that contains the error name to error page mapping looks like:

<errors>
    <error errorName="generic" pageName="genericError.aspx" />
    <error errorName="unknown" pageName="unknownError.aspx" />
</errors>  

Persistence and Long Running Workflows

Most real world scenarios have a requirement that a user be able to start a new workflow, stop, and then continue at some later point in time. That later point in time could be hours, days, or even months later. We have already discussed Creating and Starting a New Navigational Workflow; now, we will cover how to rehydrate a navigational workflow from the point the user left off at. How to setup the WWF Persistence Service database tables will be covered in the Running the Code section. There is one additional table that is used to store dating profile data such as eye color, hair color, height, etc. This table is called the Profile table. It is also the table that stores the unique workflow instance ID for each user that creates a dating record. That same workflow instance ID maps to a record in the out-of-the-box WWF database table called InstanceState. The WWF table called InstanceState is where Windows Workflow Foundation stores data about workflows that have been persisted. It stores information such as the current StateActivity the workflow is in, private data members, etc.

The host application rehydrates workflows by calling the Rehydrate method of the Workflow Manager. The Rehydrate method takes the ID of the workflow instance to be rehydrated as its parameter. Below is the implementation of the Rehydrate method:

public string Rehydrate(Guid instanceId)
{
    try
    {
        // prepare external data exchange arguments
        ExternalDataEventArgs args = 
            new ExternalDataEventArgs(instanceId);

        // make local service call into Navigation Workflow 
        // to retrieve step user left off at
        _navigationService.OnRehydrated(args);

        // put the workflow instance id in session 
        this.CurrentWorkflowInstanceId = instanceId;

        // execute the workflow on the current 
        // thread and return the page to go to
        return GetPageToGoTo(args.InstanceId);
    }
    catch (Exception ex)
    {
        // the given workflow instance could not be rehydrated
        return GetStartPage();
    }
}

The Navigational Workflow listens for this event. Double-clicking the eventRehydrated EventDrivenActivity opens up the below in the designer:

eventRehydrated Screenshot

The HandleExternalEventActivity called handleRehydrated listens for the Rehydrated event. Once received, the CallExternalMethodActivity callRehydrated executes. If you look at the properties of this Activity, you will notice it is exactly the same as in the section The Previous and Next Events: How Navigation Works:

callRehydrated Screenshot

The reason it is the same has to do with what happens when you rehydrate a workflow instance in WWF: When you rehydrate a workflow instance, WWF retrieves the workflow instance from the database for the duration required to process the request made to it. Part of the information that is retrieved is the current StateActivity of the workflow. Our Navigational Workflow uses this to obtain the ASPX page from the web.config that maps to the current StateActivity. It is this page that is returned to the Workflow Manager and then to the host application. After the instance finishes processing the request, WWF will save it back into the persistence store.

Skipping Between Pages

Since every web page is associated with a unique StateActivity, we are actually skipping between StateActivitys in the Navigational Workflow to achieve skipping between web pages. Most real-world scenarios require a user to fill out a web form following some predefined navigational order. Once the user has gone through the predefined steps, they should (in certain circumstances) be allowed to skip back and forth between pages they have already been on to edit information. To allow this, our navigational workflow listens to the following events:

event EventHandler<ExternalDataEventArgs> SkipToBasics;
event EventHandler<ExternalDataEventArgs> SkipToAppearance;
event EventHandler<ExternalDataEventArgs> SkipToLifestyle;
event EventHandler<ExternalDataEventArgs> SkipToInterests;
event EventHandler<ExternalDataEventArgs> SkipToComplete;

The EventDrivenActivitys that encapsulate these events are defined at the workflow level:

skipToStates Screenshot

Double-clicking on the EventDrivenActivity eventSkipToLifestyle opens up the following in the designer:

eventSkipToLifestyle Screenshot

Once the HandlExternalEventActivity called handleSkipToLifestyle receives the SkipToLifestyle event, control transfers to the IfElseActivity. The IfElseBranchActivity on the left uses a code condition to check and see if skipping to the LifestyleState is a valid navigational move. This is where the _heaviestWeight variable comes into play. Each StateActivity has a weight that is associated with it, and as the navigational workflow transitions to States with heavier weights, it stores that weight in the _heaviestWeight variable.

According to the web.config, the Lifestyle State has a weight of 30:

<element stateName="LifestyleState" pageName="lifestyle.aspx" weight="30" />

Skipping to the Lifestyle state is valid if 30 is less than or equal to the heaviest known weight. The CodeCondition method that determines if skipping to the LifeStyle State is valid is defined as:

private void CanSkipToLifestyleState(object sender, ConditionalEventArgs e)
{
    int lifestyleWeight = 
        NavigationSection.Current.Elements[LifestyleState.Name].Weight;
        
    e.Result = lifestyleWeight <= _heaviestWeight;
}

If the Code Condition evaluates to true, then the Navigational Workflow transitions to the Lifestyle State; otherwise, the IfElseBranchActivity on the right executes. Examining the properties of the CallExternalMethodActivity callSkipToLifestyleOnPageToGoTo reveals that the Navigational Workflow is preparing to return an error page as the page to go to. This illustrates an example of how to redirect the host application to an error page from the Navigational Workflow. The example in the section Error Pages illustrated how to redirect to an error page from the host application. Below are the properties of callSkipToLifestyleOnPageToGoTo:

callSkipToLifestyleOnPageToGoTo Screenshot

Investigating the InitializeCannotSkipToStateErrorMessage reveals which error page the Navigational Workflow is going to return:

private void InitializeCannotSkipToStateErrorMessage(object sender, EventArgs e)
{
    // get the error page to go to
    string pageToGoTo = 
        NavigationSection.Current.
            Errors[ErrorType.Generic.GetErrorName()].PageName;

    // initialize the outgoing args to send to the host application
    _pageToGoToEventArgs = 
        new PageToGoToEventArgs(this.WorkflowInstanceId, pageToGoTo);
}

The GenericError is defined in the web.config as:

<error errorName="generic" pageName="genericError.aspx" />

What this means is that the user will get redirected to genericError.aspx whenever they attempt to skip to a web page they have not yet been on. The "natural flow" of the Navigational Workflow is to transition to and from StateActivitys using the Previous and Next events. The skip events are used as an example to simulate one type of real-world scenario.

Running the Code

World Wide Dating requires Microsoft SQL Server 2005 (any edition). The required SQL scripts are in a folder called "database setup procedure" inside the DateSite project. The setup procedure is as follows:

  1. Create a database called Dating
  2. Run Profile_Schema.sql
  3. Run SqlPersistenceService_Schema.sql (WWF script)
  4. Run SqlPersistenceService_Logic.sql (WWF script)

Alternatively, the composite.sql file contains all the necessary SQL scripts bunched into one file. After the database has been setup, open up the DateSite Solution and run the DateSite project.

Note About Different SQL Server 2005 Versions

Many people have posted messages regarding the following error:

navigationService = _workflowRuntime.GetService() as NavigationService 

System.NullReferenceException: Object reference not set to an 
instance of an object

The most common cause of this error is an incorrect connection string. World Wide Dating was coded using SQL Server 2005 Developer Edition, and assumes the Dating database is located on the server named localhost. If you are getting this error, you may need to modify your database connection string to point to the proper database server.

Happy Dating!

History

  • 21/10/2008: Updated Running the Code section.
  • 17/08/2008: Correction made to the Navigational Logic and Rules section.
  • 22/05/2008: Updated article diagrams.
  • 08/05/2008: Formatted code for proper printing.
  • 29/04/2008: Initial release.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Pero Matić
Software Developer (Senior) Plan A Software
United States United States
No Biography provided

You may also be interested in...

Comments and Discussions

 
GeneralRe: Basic understanding of the workflow is wrong Pinmembermaniprevo24-Dec-09 18:45 
GeneralHello Pinmemberzensim20-Nov-09 5:41 
GeneralNullReferenceException Pinmemberavidie23-Sep-09 5:30 
GeneralRe: NullReferenceException Pinmembermhbearlover12-Aug-10 13:03 
GeneralRe: NullReferenceException Pinmembermhbearlover27-Aug-10 14:51 
QuestionPUBLISH ... Object reference not set to an instance of an object Pinmemberiuri_figueiredo29-Jul-09 4:34 
AnswerRe: PUBLISH ... Object reference not set to an instance of an object PinmemberPero Matić29-Jul-09 6:00 
QuestionRe: PUBLISH ... Object reference not set to an instance of an object Pinmemberiuri_figueiredo29-Jul-09 6:07 
AnswerRe: PUBLISH ... Object reference not set to an instance of an object PinmemberPero Matić29-Jul-09 6:28 
QuestionRe: PUBLISH ... Object reference not set to an instance of an object Pinmemberiuri_figueiredo29-Jul-09 6:35 
AnswerRe: PUBLISH ... Object reference not set to an instance of an object PinmemberPero Matić29-Jul-09 7:05 
GeneralRe: PUBLISH ... Object reference not set to an instance of an object Pinmemberiuri_figueiredo29-Jul-09 23:18 
GeneralAmazing Article Pinmemberjenisan29-Apr-09 8:42 
GeneralRe: Amazing Article PinmemberPero Matic29-Apr-09 9:29 
GeneralWWF DB scripts PinmemberSalam Y. ELIAS28-Oct-08 5:50 
GeneralRe: WWF DB scripts PinmemberPero Matic28-Oct-08 8:47 
GeneralDatabase connection PinmemberE! Ray K2-Oct-08 14:46 
GeneralRe: Database connection PinmemberPero Matic3-Oct-08 5:38 
GeneralRe: Database connection PinmemberE! Ray K4-Oct-08 15:15 
GeneralThanks for your work PinmemberHarry Sun27-Aug-08 21:11 
GeneralRe: Thanks for your work PinmemberPero Matic27-Aug-08 23:28 
General_navigationService = _workflowRuntime.GetService<inavigationservice>() as NavigationService;</inavigationservice> Pinmemberomairh7424-Jun-08 10:40 
GeneralRe: _navigationService = _workflowRuntime.GetService() as NavigationService; PinmemberPero Matic24-Jun-08 12:27 
GeneralRe: _navigationService = _workflowRuntime.GetService() as NavigationService; PinmemberGUI Developer7-Oct-08 13:38 
GeneralRe: _navigationService = _workflowRuntime.GetService() as NavigationService; PinmemberGUI Developer7-Oct-08 13:56 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140926.1 | Last Updated 21 Oct 2008
Article Copyright 2008 by Pero Matić
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid