Click here to Skip to main content
15,885,032 members
Articles / Web Development / ASP.NET

Navigational Workflows Unleashed in WWF/ASP.NET 3.5

Rate me:
Please Sign up or sign in to vote.
4.97/5 (42 votes)
21 Oct 2008CPOL18 min read 224.9K   1.6K   165  
Case-study on the internals of a Navigational Workflow engine for a fictional dating website called “World Wide Dating”.
using System;
using DateSiteNavigation;
using System.Workflow.Runtime;
using System.Workflow.Activities;
using System.Workflow.Runtime.Hosting;

/// <summary>
/// Wrapper class used by the asp.net host application to communicate
/// with the navigational workflow.
/// </summary>
public class WorkflowManager
{
    private string _pageToGoTo;
    private string _stateToGoTo;
    private bool _isSynchronized;    
    
    private WorkflowRuntime _workflowRuntime;
    private NavigationService _navigationService;
    
    public const string WorkflowInstanceIdKey = "WorkflowInstanceId";
    public const string WorkflowRuntimeKey = "WorkflowRuntime";
    public const string LocalServicesKey = "LocalServices";

    private readonly System.Web.HttpApplicationState Application;
    private readonly System.Web.SessionState.HttpSessionState Session;
    private readonly System.Web.HttpRequest Request;
    private readonly System.Web.HttpResponse Response;

    /// <summary>
    /// Constructor that initializes all variables used to communicate with
    /// the navigational workflow.
    /// </summary>
    /// <param name="application"></param>
    /// <param name="session"></param>
    /// <param name="request"></param>
    /// <param name="response"></param>
    public WorkflowManager(System.Web.HttpApplicationState application, System.Web.SessionState.HttpSessionState session, System.Web.HttpRequest request, System.Web.HttpResponse response)
    {
        // get reference to asp.net members
        this.Application = application;
        this.Session = session;
        this.Request = request;
        this.Response = response;

        // get workflow references
        _pageToGoTo = string.Empty;
        _workflowRuntime = GetWorkflowRuntime();
        _navigationService = _workflowRuntime.GetService<INavigationService>() as NavigationService;

        // add event handlers to receive messages from local service
        _navigationService.PageToGoToReceived += new EventHandler<PageToGoToEventArgs>(_navigationService_PageToGoToReceived);
        _navigationService.OutgoingSynchronizeReceived += new EventHandler<OutgoingSynchronizeEventArgs>(_navigationService_OutgoingSynchronizeReceived);
    }

    /// <summary>
    /// Gets or sets the workflow instance id currently in use.
    /// </summary>
    public Guid CurrentWorkflowInstanceId
    {
        get { return Session[WorkflowInstanceIdKey] == null ? Guid.Empty : (Guid)Session[WorkflowInstanceIdKey]; }
        set { Session[WorkflowInstanceIdKey] = value; }
    }

    /// <summary>
    /// Gets the name of the current state activity for the
    /// current workflow instance in use.
    /// </summary>
    public string CurrentWorkflowState
    {
        get 
        {
            // check if the current workflow instance id is empty
            if (this.CurrentWorkflowInstanceId == Guid.Empty)
            {
                return string.Empty;
            }

            // get a reference to the current state machine workflow instance
            StateMachineWorkflowInstance instance = new StateMachineWorkflowInstance(_workflowRuntime, this.CurrentWorkflowInstanceId);

            // return the current state name
            return instance.CurrentStateName;
        }
    }

    /// <summary>
    /// Get the name of the page the user is currently on. This method is 
    /// independent of the page they should be on.
    /// </summary>
    /// <returns></returns>
    public string GetCurrentPageName()
    {
        return System.IO.Path.GetFileName(Request.PhysicalPath);
    }

    /// <summary>
    /// Creates a new workflow instance and returns the page name 
    /// of the initial state to go to (which is default.aspx).
    /// </summary>
    /// <returns></returns>
    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);
    }

    /// <summary>
    /// Ensures the current workflow is synchronized, meaning the page the user is on is
    /// the page they should be on. To test that this works, you can hardcode the url to
    /// skip to a different page than the next logical one in the workflow. HOWEVER, our
    /// workflow is also self healing, and if the page the user goes to, whether hacked
    /// or natural, the workflow will kick off its self healing mechanism (if possible)
    /// and auto-transition its state to the state that matches the given page name.
    /// </summary>
    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);
        }
    }

    /// <summary>
    /// Raise the previous event inside the navigational workflow 
    /// and then return the page to go to.
    /// </summary>
    /// <returns></returns>
    public string Previous()
    {
        // prepare external data exchange arguments
        ExternalDataEventArgs args = new ExternalDataEventArgs(this.CurrentWorkflowInstanceId);

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

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

    /// <summary>
    /// Raise the next event inside the navigational workflow
    /// and then return the page to go to.
    /// </summary>
    /// <returns></returns>
    public string Next()
    {
        // prepare external data exchange arguments
        ExternalDataEventArgs args = new ExternalDataEventArgs(this.CurrentWorkflowInstanceId);

        // make local service call into Navigation Workflow
        _navigationService.OnNext(args);

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

    /// <summary>
    /// Rehydrate the workflow with the given instance id by raising the rehydrate 
    /// event inside the navigational workflow and then return the page to go to.
    /// </summary>
    /// <param name="instanceId"></param>
    /// <returns></returns>
    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();
        }
    }

    /// <summary>
    /// Raise the error event of the given type inside the navigational workflow
    /// and then return the page to go to.
    /// </summary>
    /// <param name="errorType"></param>
    /// <returns></returns>
    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);
    }

    /// <summary>
    /// Raise the skip to basics event inside the navigational workflow
    /// and then return the page to go to. 
    /// </summary>
    /// <returns></returns>
    public string SkipToBasics()
    {
        // prepare external data exchange arguments
        ExternalDataEventArgs args = new ExternalDataEventArgs(this.CurrentWorkflowInstanceId);

        // make local service call into Navigation Workflow
        _navigationService.OnSkipToBasics(args);

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

    /// <summary>
    /// Raise the skip to appearance event inside the navigational workflow
    /// and then return the page to go to. 
    /// </summary>
    /// <returns></returns>
    public string SkipToAppearance()
    {
        // prepare external data exchange arguments
        ExternalDataEventArgs args = new ExternalDataEventArgs(this.CurrentWorkflowInstanceId);

        // make local service call into Navigation Workflow
        _navigationService.OnSkipToAppearance(args);

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

    /// <summary>
    /// Raise the skip to lifestyle event inside the navigational workflow
    /// and then return the page to go to. 
    /// </summary>
    /// <returns></returns>
    public string SkipToLifestyle()
    {
        // prepare external data exchange arguments
        ExternalDataEventArgs args = new ExternalDataEventArgs(this.CurrentWorkflowInstanceId);

        // make local service call into Navigation Workflow
        _navigationService.OnSkipToLifestyle(args);

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

    /// <summary>
    /// Raise the skip to interests event inside the navigational workflow
    /// and then return the page to go to. 
    /// </summary>
    /// <returns></returns>
    public string SkipToInterests()
    {
        // prepare external data exchange arguments
        ExternalDataEventArgs args = new ExternalDataEventArgs(this.CurrentWorkflowInstanceId);

        // make local service call into Navigation Workflow
        _navigationService.OnSkipToInterests(args);

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

    /// <summary>
    /// Raise the skip to complete event inside the navigational workflow
    /// and then return the page to go to. 
    /// </summary>
    /// <returns></returns>
    public string SkipToComplete()
    {
        // prepare external data exchange arguments
        ExternalDataEventArgs args = new ExternalDataEventArgs(this.CurrentWorkflowInstanceId);

        // make local service call into Navigation Workflow
        _navigationService.OnSkipToComplete(args);

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

    /// <summary>
    /// Return the starting page of all navigational workflows.
    /// </summary>
    /// <returns></returns>
    public string GetStartPage()
    {
        // this is a special function in that it knows where to look in the web.config to retrieve the initial page
        return NavigationSection.Current.Elements["InitialState"].PageName;
    }

    /// <summary>
    /// Calls Response.Redirect on the given page to go to.
    /// </summary>
    /// <param name="pageToGoTo"></param>
    public void ManagedRedirect(string pageToGoTo)
    {
        if (pageToGoTo != null && pageToGoTo != string.Empty)
        {
            Response.Redirect(pageToGoTo);
        }
    }

    /// <summary>
    /// Get a reference to the workflow runtime.
    /// </summary>
    /// <returns></returns>
    private WorkflowRuntime GetWorkflowRuntime()
    {
        WorkflowRuntime workflowRuntime = Application[WorkflowRuntimeKey] as WorkflowRuntime;
        return workflowRuntime;
    }

    /// <summary>
    /// Get a reference to the manual workflow scheduler service.
    /// </summary>
    /// <returns></returns>
    private ManualWorkflowSchedulerService GetWorkflowSchedulerService()
    {
        ManualWorkflowSchedulerService scheduler = _workflowRuntime.GetService(typeof(ManualWorkflowSchedulerService)) as ManualWorkflowSchedulerService;
        return scheduler;
    }

    /// <summary>
    /// Run the workflow with the given instance id on the current thread. 
    /// </summary>
    /// <param name="instanceId"></param>
    /// <returns></returns>
    private bool RunWorkflow(Guid instanceId)
    {
        ManualWorkflowSchedulerService scheduler = GetWorkflowSchedulerService();
        return scheduler.RunWorkflow(instanceId);
    }

    /// <summary>
    /// Runs the given workflow and returns the page to go to.
    /// </summary>
    /// <param name="instanceId"></param>
    /// <returns></returns>
    private string GetPageToGoTo(Guid instanceId)
    {
        // execute the workflow on the current asp.net thread
        RunWorkflow(instanceId);

        // return where the user should go
        return _pageToGoTo;
    }

    /// <summary>
    /// After calling OnPageToGoTo, which the navigational workflow is listening
    /// for, the workflow with then raise the event MessageReceived to return the
    /// page to go to to the host application.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void _navigationService_PageToGoToReceived(object sender, PageToGoToEventArgs e)
    {
        // get the page to go to
        _pageToGoTo = e.PageToGoTo;
    }

    /// <summary>
    /// After calling OnIncomingSynchronize, which the navigational workflow is listening
    /// for, the workflow will then raise the event OutgoingSynchronize to return the result to the host 
    /// application. It is here we capture if the workflow is synchronzied and the state to go to.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void _navigationService_OutgoingSynchronizeReceived(object sender, OutgoingSynchronizeEventArgs e)
    {
        _isSynchronized = e.IsSynchronized;
        _stateToGoTo = e.StateToGoTo;
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

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


Written By
Founder Turing Inc.
United States United States

Comments and Discussions