Workflow as a Page Flow Engine


You could also use Windows Workflow Foundation and ASP.NET to create a page flow engine. Many web applications have navigation frameworks that drive the order in which various pages are displayed. These frameworks may range from simple and inflexible to somewhat complex and dynamic. Just as Windows Workflow Foundation is provided so that developers aren’t reinventing the wheel every time a process engine is needed, the same technology can be used to build a flexible and dynamic ASP.NET page flow engine.

Microsoft has stated that it plans on releasing a set of classes for ASP.NET page flow sitting on Windows Workflow Foundation. However, these plans have not been finalized (as of this book’s writing). Therefore, you might want to build your own library of page flow code. Before developing any code, however, you need to understand the characteristics of a good page flow engine.

Model-View-Controller

In GUI-based applications, a common approach to maintaining a flexible code base that is free from ties to the user interface is the Model-View-Controller (MVC) pattern. This pattern prescribes the following three layers:

  • The model - This is the piece of the architecture that represents your business logic. It should know nothing about the front end of your application. The model commonly contains domain-specific entities such as a Customer or Order class and other supporting types. In addition, any business processes defined in Windows Workflow Foundation is considered part of the model. Finally, data access code, although generally not considered business or domain-specific logic, is part of or below the model.

  • The view - This is the front end. It is the interface the user interacts with and sees on his or her screen. Therefore, in ASP.NET, the ASPX markup is the view. A typical view contains obvious controls, such as text boxes, buttons, and drop-down lists. Although the view contains these data-entry mechanisms, the controller is actually responsible for obtaining the data and passing it to the model.

  • The controller - This is the layer between the model and the view that passes information about events generated in the view to the model. Therefore, the controller, not unlike the view, should generally not contain important business logic. To relate this to ASP.NET, the controller is implemented as code-beside classes, because these classes contain all the event handlers for controls in the ASPX files.

Given this knowledge about the MVC pattern, building a page flow engine with Windows Workflow Foundation tends to be a little more straightforward. As mentioned, any workflow is part of the model because it is the business logic, and the front end is still considered the view. Where it gets interesting is in the controller layer. The controller needs to watch for user interactions with the page and pass that information to the workflow. At that point, the workflow does whatever work it needs to do and then tells the controller it is ready for its next step or piece of information. The controller should then be responsible for taking the response from a workflow and translating it to the page that needs to be displayed to the user next.

This means that each page in an ASP.NET application should have absolutely no knowledge of the order in which it will be displayed. Conversely, the workflow should not directly know that it is controlling an ASP.NET page flow. This type of pattern enables you to swap the front end while using the same back-end logic. For example, consider an organization whose workflow is responsible for enrolling new customers in a rewards program. The workflow should be usable in an ASP.NET web application or a call center’s Windows Forms application. Implementing the MVC pattern to create a page flow engine is not necessarily hard. However, you need to take appropriate precautions to ensure application boundaries are not crossed, thereby weakening the flexibility of an application.

Building a Page Flow Application

This section follows the development of a simple ASP.NET application responsible for enrolling employees in various company programs, such as insurance and 401(k). The ASP.NET application needs to include various forms for collecting information about a specific program and then passing the data to the controller. In addition, a workflow needs to be developed for dictating the order in which data is collected from the user. For example, employees will be asked on the first page what type of employee they are (full or part time), how long they have been with the company, and whether they want to enroll in a dental insurance program. Depending on the answers to these questions, the users may or may not see certain forms during their session.

To allow the workflow to communicate with the controller, the application will use the out-of-the-box CallExternalMethod and HandleExternalEvent activities. The instances of these activities in the workflow will point to a page flow service interface developed for this example. Figure 13-1 shows a partial screen shot of the completed workflow. The logic for the retirement plan includes an IfElse activity that checks to see whether the employee is eligible to enroll based on employee type and tenure. If the employee is eligible, a CallExternalMethod activity is executed that tells the web application to forward the user to the retirement plan enrollment page. After user input is collected, a HandleExternalEvent activity captures this event, and the workflow execution is resumed. Next, logic is implemented that checks to see whether the employee opted to enroll in the retirement plan, and if so, code is called that enrolls the user. The workflow proceeds from here to display other enrollment forms, if applicable.

image from book
Figure 13-1

The first step in writing the page flow code for this example is developing the communication plumbing, which includes the local communication service interface, the service class itself, and the event arguments classes. The following code shows the interface:

   [ExternalDataExchange] [CorrelationParameter("stepName")] public interface IPageService {     [CorrelationInitializer]     void AdvancePageFlow(string stepName,         Dictionary<string, object> dataFromWorkflow);     [CorrelationAlias("stepName", "e.StepName")]     event EventHandler<PageFlowEventArgs> StepCompleted; } 

The first thing that happens in this interface is that the workflow invokes the AdvancePageFlow method, which passes a parameter that represents the name of the next step. (How this determines which page gets called is discussed in the next paragraph.) The second parameter passed to this method holds any parameters that the workflow wants to pass to the outside world. There is also an event called StepCompleted, which is raised by the controller when a page is submitted by the end user.

Notice the correlation attributes applied to various elements of the interface. The interface itself is decorated with the CorrelationParameter attribute, which tells the workflow runtime the field to use so that the correct HandleExternalEvent is called when an event is raised from the controller. The AdvancePageFlow method is decorated with the CorrelationInitializer attribute. This tells the workflow runtime that when this method is called, the correlation token is initialized, and the value for stepName should be noted and used for correlating events. Finally, the event is decorated with the CorrelationAlias attribute, which maps the stepName parameter to a property in the event’s PageFlowEventArgs instance.

The following code shows the implementation of the communication service. The interesting piece of code in this class is the AdvancePageFlow method. When this method is called from the workflow, any subscribers to the PageAdvanceCommandReceived event are notified and passed an instance of the AdvanceFlowEventArgs class, which contains any parameters passed from the workflow as well as the next step name.

  public class PageService : IPageService {     public void AdvancePageFlow(string stepName,         Dictionary<string, object> dataFromWorkflow)     {         if (this.PageAdvanceCommandReceived != null)             this.PageAdvanceCommandReceived(null,                 new AdvanceFlowEventArgs(dataFromWorkflow, stepName));     }     public void RaiseStepCompleted(PageFlowEventArgs e)     {         if (this.StepCompleted != null)             this.StepCompleted(null, e);     }     public event EventHandler<PageFlowEventArgs> StepCompleted;     public event EventHandler<AdvanceFlowEventArgs> PageAdvanceCommandReceived; } 

That’s really about it for the interesting code on the workflow side of things. Now code needs to be written that manipulates the display of ASP.NET web forms (also known as the controller). To serve the purpose of the controller, a class called WorkflowHelper is implemented. This class uses the singleton pattern because it is a single-purpose class that does not contain any instance-specific data.

The following code shows the first part of the class, which includes the class constructor and properties:

  public class WorkflowHelper {     // static members     private static WorkflowHelper instance = null;     // instance members     private Dictionary<string, string> pageMappings =         new Dictionary<string, string>();     private AutoResetEvent waitHandle = new AutoResetEvent(false);     // constructors     private WorkflowHelper()     {         // these should be configurable!         pageMappings.Add("401kEnrollment", "RetirementPlan.aspx");         pageMappings.Add("DentalEnrollment", "Dental.aspx");         pageMappings.Add("Done", "ThankYou.aspx");     }     // properties     public static WorkflowHelper Instance     {         get         {             if (instance == null) instance = new WorkflowHelper();             return instance;         }     }     public Dictionary<string, object> DataFromWorkflow     {         get         {             return HttpContext.Current.Session["DataFromWorkflow"]                 as Dictionary<string, object>;         }     }     public WorkflowRuntime WorkflowRuntime     {         get         {             WorkflowRuntime runtime =                 HttpContext.Current.Application["WorkflowRuntime"]                     as WorkflowRuntime;             if (runtime == null)             {                 // create the runtime and add the page flow service                 runtime = new WorkflowRuntime();                 ManualWorkflowSchedulerService schedulerService =                     new ManualWorkflowSchedulerService();                 runtime.AddService(schedulerService);                 ExternalDataExchangeService dataExchangeService =                     new ExternalDataExchangeService();                 runtime.AddService(dataExchangeService);                 PageService pageService = new PageService();                 pageService.PageAdvanceCommandReceived += new                     EventHandler<AdvanceFlowEventArgs>(PageAdvanceCommandReceived);                 dataExchangeService.AddService(pageService);                 runtime.StartRuntime();                 // add the workflow runtime instance to the application state                 HttpContext.Current.Application["WorkflowRuntime"] = runtime;             }             return runtime;         }     }     private PageService PageService     {         get         {             return this.WorkflowRuntime.GetService(typeof(PageService))                 as PageService;         }     }     private ManualWorkflowSchedulerService SchedulerService     {         get         {             return this.WorkflowRuntime.GetService(                 typeof(ManualWorkflowSchedulerService))                     as ManualWorkflowSchedulerService;         }     }     private Guid CurrentWorkflowInstanceId     {         get         {             HttpCookie instanceIdCookie =                 HttpContext.Current.Request.Cookies["InstanceId"] as HttpCookie;             if (instanceIdCookie != null)                 return new Guid(instanceIdCookie.Value);             throw new ApplicationException("No instance has been started.");         }     }     ... } 

The only thing going on in this constructor is that keys and values are added to a class-level Dictionary<string, string> member. This object is responsible for holding mappings of command names to URLs. This mapping allows the workflow and the ASP.NET application to remain loosely coupled. However, in a real-world application, these values should be contained in a configuration file for easy modification later.

The class properties include the singleton instance, a property called DataFromWorkflow that holds any parameters passed from the workflow’s previous step, properties for runtime services, and a property that retrieves the workflow instance ID from the user’s cookies. In addition, there is a property called WorkflowRuntime that retrieves a reference to a single instance of the WorkflowRuntime class. First, the get accessor checks to see whether an instance of the WorkflowRuntime class has been stored in the Application object. If not, a new instance is created, and the required runtime services are added. Finally, the new WorkflowRuntime object is stored in the globally available application state.

Notice that a handler is added to the PageAdvanceCommandReceived event of the PageService. This is so that when the workflow makes an external method call to the local communication service, this event is raised, and the application’s controller knows to redirect the user to a new page. (This event’s event-handler method is discussed in more detail later.)

The following code shows the remainder of the WorkflowHelper class:

  public void StartWorkflow(Dictionary<string, object> parms) {     this.SubmitData(parms, null); } public void SubmitData(string stepName, Dictionary<string, object> parms) {     this.SubmitData(parms, stepName); } private void SubmitData(Dictionary<string, object> parms, string stepName) {     WorkflowInstance instance;     if (stepName == null)     {         instance = this.WorkflowRuntime.CreateWorkflow(             typeof(Enrollment.EnrollmentWorkflow), parms);         instance.Start();         // add the workflow's instance id to the user's cookies         HttpContext.Current.Response.Cookies.Add(             new HttpCookie("InstanceId", instance.InstanceId.ToString()));     }     else     {         instance =             this.WorkflowRuntime.GetWorkflow(this.CurrentWorkflowInstanceId);         this.PageService.RaiseStepCompleted(             new PageFlowEventArgs(instance.InstanceId, stepName, parms));     }     // run the workflow on the same thread as the request     this.SchedulerService.RunWorkflow(instance.InstanceId); } private void PageAdvanceCommandReceived(object sender, AdvanceFlowEventArgs e) {     HttpContext.Current.Session["DataFromWorkflow"] = e.Parameters;     string nextUrl = this.pageMappings[e.StepName];     HttpContext.Current.Response.Redirect(nextUrl, false); } 

In this code, the third SubmitData overload is responsible for either starting a new workflow instance or obtaining a reference to an existing one and resuming its execution. Whether a new instance is starting or an existing instance is continuing, the ManualWorkflowSchedulerService is used because this is an ASP.NET application and the workflow should be executing on the same thread as the web request.

The last method in this class, PageAdvanceCommandReceived, is responsible for processing the page flow when the workflow makes an external method call to the local communication class, PageService. This event-handler method first grabs a reference to the parameters passed from the workflow and places the object in the user’s ASP.NET session. Next, a URL is obtained by using the command-mapping Dictionary object introduced earlier. This URL is then used to redirect the user to a new page by calling the HttpResponse’s Redirect method. It is extremely important that the Redirect method accept a Boolean as its second argument; in this case, it is always passed a value of false. This tells the Redirect method to not end the execution of the current thread before the redirection is performed. If this value is not explicitly passed, the current thread’s execution would be terminated, and the workflow would not have a chance to continue. This means that the first page would be correctly shown using a redirection, but after that page was submitted, nothing would happen because the workflow can never progress to the next HandleExternalEvent activity.

At this point, the web forms can be implemented. Remember, each form should have no idea which order it is displayed in except for the initial form that starts the workflow. This makes sense in this example because the data collection screen is always displayed first and starts the workflow. The following is a portion of the code-beside class for the data collection screen. As you can see, there is not much going on here, which is actually good. The majority of the logic is contained in the model, which is where it should be. First, a Person object is created that represents the employee and his or her relevant data. This object is then added to a Dictionary instance. Finally, the parameters object is passed to the WorkflowHelper’s StartWorkflow method. The controller and model then decide what happens next.

  protected void btnSubmit_Click(object sender, EventArgs e) {     Person person = new Person(txtFirstName.Text,         (EmployeeType)Convert.ToInt32(ddlEmployeeType.SelectedValue),         (Tenure)Convert.ToInt32(ddlTenure.SelectedValue));     Dictionary<string, object> parms = new Dictionary<string, object>();     parms.Add("Person", person);     WorkflowHelper.Instance.StartWorkflow(parms); } 

The following block of code is taken from the retirement plan form’s code-beside class. This code is similar to the data collection form’s code, except that the button event handlers call the SubmitData method of WorkflowHelper, which takes a string parameter that represents the current step’s name. This string is eventually passed to the workflow so that the correct HandleExternalEvent activity is executed. There is also a second method that enables the user to opt out of the retirement plan. (The dental-plan code is very similar and thus is not displayed here.)

  protected void btnSubmit_Click(object sender, EventArgs e) {     Dictionary<string, object> parms = new Dictionary<string, object>();     parms.Add("OptIn", true);     parms.Add("Amount", txtAmount.Text);     WorkflowHelper.Instance.SubmitData("401kEnrollment", parms); } protected void btnCancel_Click(object sender, EventArgs e) {     Dictionary<string, object> parms = new Dictionary<string, object>();     parms.Add("OptIn", false);     WorkflowHelper.Instance.SubmitData("401kEnrollment", parms); } 

Finally, the following code is found in the last page of the workflow, ThankYou.aspx. This code is interesting because it is finally using the ability to retrieve parameters passed from the workflow to the controller. A BulletedList control is used to display a list of all the programs an employee has enrolled in during the lifecycle of the workflow. Keep in mind that this list could be empty, depending on the employee’s eligibility for programs or choices for enrollment.

  protected void Page_Load(object sender, EventArgs e) {     List<string> enrollments =         WorkflowHelper.Instance.DataFromWorkflow["Enrollments"]             as List<string>;     this.BulletedList1.DataSource = enrollments;     this.BulletedList1.DataBind(); } 

The page flow application in this example illustrates a great way to use Windows Workflow Foundation to control the UI of an application - more specifically, an ASP.NET application. However, there are a few things that could have been done to make this code more generic and usable for any page flow scenario. The code as shown is not too far from this, but it could benefit from greater configurability. For example, the command-to-URL mapping should decidedly be implemented in a configuration file. In addition, the type of workflow created should be configurable.

Another thing to keep in mind with this type of application is that not only could a sequential workflow be used, but a state-machine workflow would also work just fine. Using a state-machine workflow would allow users to be directed back and forth to different web forms in any order dictated by the workflow logic. This might be useful in a bug tracking or approval scenario when certain steps may be repeated depending on events external to the workflow.



Professional Windows Workflow Foundation
Professional Windows Workflow Foundation
ISBN: 0470053860
EAN: 2147483647
Year: 2004
Pages: 118
Authors: Todd Kitta

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net