Vacation Request 2.0 - Windows Workflow Foundation (WF) and DotNetNuke

A DotNetNuke module communicating with a WF workflow hosted in the Open Source project IWebWF

Introduction

Windows WorkFlow (WF) is a powerful framework for creating enterprise level workflows. Such power also brings great complexity. Much of WF is "roll your own", you are provided the core components but much of the required elements are up to you to provide. There are many ways of hosting workflows, this article describes hosting workflows using .asmx web services.

This example project shows a complete WF solution composed of a DotNetNuke module communicating with a WF workflow hosted in the Open Source project IWebWF (IWebWF.com). The project is called Vacation Requests and it allows users to make requests for time off from work. The requests will be processed by the workflow using the following business rules:

This article examines the project using the following outline:

Overview of Vacation Request 2.0

The following diagram shows the basic structure of the Vacation Request application:

The "front-end" of the application resides in a module running in the DotNetNuke website. The "back-end" processing for the application resides in the workflow running in the IWebWF website. The DotNetNuke module and the workflow communicate using web services.

The DotNetNuke module exposes the following web services:

The workflow exposes these web services:

WF workflows can be hosted in a number of different ways. A workflow could even be hosted in the DotNetNuke website. This design was chosen because it allows the solution to be scaled by adding multiple instances of the IWebWF application. In addition, the workflow components and persistence services can be resource intensive. It is desirable to off-load this from the DotNetNuke website.

Windows Communication Foundation (WCF) can be used instead of the .asmx web services described in this solution. While this works, WCF introduces complications when constructing the WCF services that provide unnecessary challenges, such as needing complex configuration files to bind the protocol and the transport.

Set-up

To set-up the application, you need to install IWebWF and DotNetNuke. You can download IWebWF from: http://www.codeplex.com/IWebWF and DotNetNuke from: http://DotNetNuke.com.

IWebWF

After installing IWebWF, log in as the administrator and configure and test the Email settings.

Unzip the files from the VacationRequestWorkflow.zip file and place the VacationRequest.dll file in the "Bin" directory and the VacationRequest.asmx file in the "Webservice" directory.

DotNetNuke

Install the Vacation Request_02.00.00_Install.zip module using the normal DotNetNuke module installation process (if using DNN4 Run LinqPrep first).



Log in as an administrator and click the [Set Webservice URL] link and set the web service link to point to the VacationRequest.asmx page in the IWebWF site.



The [Edit Users Vacation Days] (from the main page of the module) allows the administrator to set the available days for users.

Also, ensure the DNN admin account has an email address and the SMTP settings are configured in the DNN site.

Make a Request

A vacation request starts in the DNN module. It makes a web service call to the workflow web service. The DNN module creates a random number and saves it in the database as a CheckDigit. The RequestID, the CheckDigit, the Portal administrator and users email addresses are passed to the workflow web service.

Creating a proxy in DNN to call an external web service

To allow a DNN module to call an external web service and change the address dynamically, a VacationWebService project is created that contains a normal .asmx web proxy that connects to the workflow web service in the IWebWF site.

The project is compiled and the VacationWebService.dll file that is created, is placed in the "Bin" directory of the DNN site. This allows you to instantiate the class with code such as this:
VacationRequestWorkflow_WebService wsVacationRequest = new VacationRequestWorkflow_WebService();
and alter the web service address dynamically with code like this:
// Set the address to the web service      
wsVacationRequest.Url = GetWebServiceURL();
The following, shows the complete code to create a random CheckDigit code and call the workflow web service:
// Create a record
VacationRequestDAL VacationRequestDAL = new VacationRequestDAL();
VacationRequest VacationRequest = new VacationRequest();
VacationRequest.Approved = false;
VacationRequest.CheckDigit = GetRandomNumber();
VacationRequest.Complete = false;
VacationRequest.CreatedDate = DateTime.Now;
VacationRequest.DateRequested = Convert.ToDateTime(txtRequestedDate.Text);
VacationRequest.DaysRequested = Convert.ToInt32(txtRequestedDays.Text);
VacationRequest.LastActiveDate = DateTime.Now;
VacationRequest.NeedsApproval = false;
VacationRequest.UserID = UserId;
VacationRequest.WorkflowInstanceId = "";

VacationRequestDAL.VacationRequests.InsertOnSubmit(VacationRequest);
VacationRequestDAL.SubmitChanges();

// Reference to the web service            
VacationRequestWorkflow_WebService wsVacationRequest = new VacationRequestWorkflow_WebService();
// Enable cookies            
wsVacationRequest.CookieContainer = new System.Net.CookieContainer();
// Set the address to the web service           
wsVacationRequest.Url = GetWebServiceURL();
// Call the method to start the workflow            
Guid WorkflowInstanceID = wsVacationRequest.StartWorkflow(VacationRequest.RequestID,
    VacationRequest.CheckDigit, GetLocalWebserviceURL(), PortalSettings.Email, UserInfo.Email);

// Update the WorkflowInstanceID
UpdateWorkflowInstanceID(VacationRequest.RequestID, WorkflowInstanceID);

pnlVacationRequest.Visible = false;
lblError.Text = "Request submitted. You will receive an email with a response";

VacationRequest Workflow

The VacationRequest workflow has an interface that defines the web services that it exposes (StartWorkflow and ApproveRequest):

        public interface IVacationRequest
	{
        Guid StartWorkflow(int parmRecordID, int parmCheckDigit, string parmWebService, 
            string parmAdministratorEmailstring, string parmEmployeeEmail);

        bool ApproveRequest(int parmRecordID, int parmCheckDigit, bool parmApproval);
	}

The VacationRequestWorkflow.cs file contains the workflow logic for the workflow.

The StartVacationRequest activity is bound to the StartWorkflow web method that starts the workflow. After the workflow is started, the wsGetUserDetails activity calls the GetUserDetails web service in the DNN site.

The workflow is able to set the web address dynamically by wiring-up a method to the Invoking event and setting the url of the .asmx web proxy that points to the web service in the DNN module.

// Set the URL to the web service to the URL that was passed whe the Workflow was started
wsVacationRequest.WebService objWebService = (wsVacationRequest.WebService)e.WebServiceProxy;
objWebService.Url = WebService; 

The workflow calls this web service method in the DNN site which returns information about the vacation request:

        public UserDetails GetUserDetails(int RecordID, int CheckDigit)
        {
            UserDetails UserDetails = new UserDetails();

            VacationRequestDAL VacationRequestDAL = new VacationRequestDAL();

            // Search for a matching record
            var VacationRequestsResult = (from VacationRequests in VacationRequestDAL.VacationRequests
                                          where VacationRequests.RequestID == RecordID &
                                          VacationRequests.CheckDigit == CheckDigit &
                                          VacationRequests.CheckDigit != 0
                                          select VacationRequests).FirstOrDefault();

            // If the record is found return the result
            if (VacationRequestsResult != null)
            {
                var VacationDaysResult = (from Days in VacationRequestDAL.VacationDays
                                          where Days.UserID == VacationRequestsResult.UserID
                                          select Days).FirstOrDefault();

                UserDetails.RequestedDate = VacationRequestsResult.DateRequested;
                UserDetails.DaysRequested = VacationRequestsResult.DaysRequested;
                UserDetails.VacationDays = VacationDaysResult.VacationDays;
            }
            else
            {
                // Set the values so that the Workflow service will know the request is bad
                // The workflow will receive these values and terminate the workflow
                UserDetails.RequestedDate = new DateTime(1900,1,1);
                UserDetails.DaysRequested = 0;
                UserDetails.VacationDays = 0;
            }

            return UserDetails;
        } 

After the workflow receives the information about the vacation request, the process reaches the ifElseActivity_ProcessRequest decision point. There are three possible activity groups that could be processed based on the values received from the DNN web service.

When you right-click on the TerminateWorkflowBranch...

and select Properties, you will be able to see the see the rule conditions that determine the execution of that branch (the days requested, and the vacation days are 0).

The same process can be performed to see the rule conditions for the NeedsApprovalBranch (the days requested is more than 3 days, or the days requested is more than the vacation days, or the vacation days minus the days requested would be less than 0).

(see this article for more information on using the rules engine)


The ApprovedBranch does not have a rule condition and will be executed by default if the rule conditions for TerminateWorkflowBranch and NeedsApprovalBranch are not met (the ifElseActivity branches are evaluated from left-to-right).

Approve a Request

If the days requested and the vacation days are 0 the workflow will terminate. If a request is for less than 4 days and there are sufficient days available, the request is automatically approved. An email is sent to the user, and the workflow terminates.

For all other situations, the NeedsApprovalBranch branch is executed. This branch contains activities that call the web service in the DNN site to update the record. It sets the record as needing approval, and sends an email to the administrator indicating that there is a record that needs approval.

The workflow then proceeds to the WaitForApproval activity. This activity contains the ApprovalWebService group.

The WaitForApproval activity is a WhileActivity that will stay in a loop until the Code Condition is met. It will not break out of the loop until the value of CheckIsTimeToStop method returns true.

The wsApprovalRequest_Input activity is the first activity in the ApprovalWebService group. The wsApprovalRequest_Input activity is bound to the ApproveRequest web method in the workflow. It will now wait for a web service call from the DNN site to approve the request.

DNN Module

When logged into the DNN site as an administrator, the [Approve Requests] link will take you to the approval page.

The following code creates a new random CheckDigit and calls the workflow web service:

            bool boolApproval = false;
            string strCommandArgument = (string)e.CommandArgument;
            int intRecordID = Convert.ToInt32(e.CommandName);

            int CheckDigit = GetRandomNumber();
            Guid WorkflowInstanceID = UpdateCheckDigitAndGetWorkflowInstanceID(intRecordID, CheckDigit);

            if (strCommandArgument == "Approve")
            {
                // Approve
                boolApproval = true;
            }
            else
            {
                // Decline
                boolApproval = false;
            }

            // Reference to the web service            
            VacationRequestWorkflow_WebService wsVacationRequest = new VacationRequestWorkflow_WebService();
            // Enable cookies            
            wsVacationRequest.CookieContainer = new System.Net.CookieContainer();
            // Set the address to the web service           
            wsVacationRequest.Url = GetWebServiceURL();
            // Create a URI            
            Uri VacationRequestUri = new Uri(wsVacationRequest.Url); 
            // Enable cookies          
            wsVacationRequest.CookieContainer = new System.Net.CookieContainer();    
            // Use the URI to obtain a collection of the cookies    
            CookieCollection mycollection = wsVacationRequest.CookieContainer.GetCookies(VacationRequestUri);           
            // Add the current WorkflowInstanceId to the cookie collection       
            // that will be passed to the web service          
            wsVacationRequest.CookieContainer.SetCookies       
                (               
                VacationRequestUri,
                String.Format("{0}={1}", "WF_WorkflowInstanceId", WorkflowInstanceID.ToString()) 
                );
            // Call the method on the workflow            
            wsVacationRequest.ApproveRequest(intRecordID, CheckDigit, boolApproval);

Vacation Request Workflow

The workflow receives the approval call from the DNN site and processes the activities in the ApprovalProcess activity group. The wsUpdateVacationRequestApproval activity calls the following UpdateVacationRequest web service method in the DNN site to update the record:

        public bool UpdateVacationRequest(int RecordID, int CheckDigit, int VacationDays, bool NeedsApproval, bool Approved, bool Complete)
        {
            VacationRequestDAL VacationRequestDAL = new VacationRequestDAL();

            // Search for a matching record
            var VacationRequestsResult = (from VacationRequests in VacationRequestDAL.VacationRequests
                                          where VacationRequests.RequestID == RecordID &
                                          VacationRequests.CheckDigit == CheckDigit &
                                          VacationRequests.CheckDigit != 0
                                          select VacationRequests).FirstOrDefault();

            // Only update if record is found
            if (VacationRequestsResult != null)
            {
                VacationRequestsResult.Approved = Approved;
                VacationRequestsResult.Complete = Complete;
                VacationRequestsResult.NeedsApproval = NeedsApproval;
                VacationRequestsResult.LastActiveDate = DateTime.Now;
                // Set Check Digit to 0 so that the record can not be updated by web services
                VacationRequestsResult.CheckDigit = 0;

                // Update Vacation Days
                var VacationDaysResult = (from Days in VacationRequestDAL.VacationDays
                                          where Days.UserID == VacationRequestsResult.UserID
                                          select Days).FirstOrDefault();

                VacationDaysResult.VacationDays = VacationDays;

                // Commit changes
                VacationRequestDAL.SubmitChanges();
            }

            return true;
        }

In the workflow, the isTimeToStop value is set to true which causes the CheckIsTimeToStop method to return true.

        #region CheckIsTimeToStop
        // This method will return the value of isTimeToStop
        // The value of isTimeToStop will be set by other
        // methods in the workflow
        private void CheckIsTimeToStop(object sender, ConditionalEventArgs e)
        {
            e.Result = !(isTimeToStop);
        }
        #endregion
This causes WaitForApproval activity to stop and the workflow terminates. The process is complete.

Logging

When workflows are running it is hard to know what is going on without logging.

The DNN module provides some logging by clicking on the [History] link (when logged in as an administrator).

The VacationRequest workflow project contains a reference to the IWebCore project that allows it to easily log actions with code such as this:

        #region WriteToIWebWFLog
        private void WriteToIWebWFLog(string LogEvent)
        {
            ApplicationLog.AddToLog(string.Format("VacationRequest Event - WorkflowInstanceID: {0} Event: {1}", 
                this.WorkflowInstanceId.ToString(), LogEvent));
        }

        private void WriteErrorToIWebWFLog(string Error)
        {
            ApplicationLog.AddToLog(string.Format("VacationRequest Error - WorkflowInstanceID: {0} Error: {1}", 
                this.WorkflowInstanceId.ToString(), Error));
        }
        #endregion

The IWebCore project also allows emails to be sent using code such as this:

        #region SendEmailtoEmployee
        private void SendEmailtoEmployee()
        {
            try
            {
                Email Email = new Email();
                string strEmailMessage = String.Format("Your Vacation Request for {0} {1}.", 
                    objUserDetails.RequestedDate.ToShortDateString(), (Approval) ? "has been approved" : "has been declined");

                Email.SendMail(EmployeeEmail, "", "", "", "Email From VacationRequest",
                    strEmailMessage, "");
                WriteToIWebWFLog(strEmailMessage);
            }
            catch (Exception ex)
            {
                WriteErrorToIWebWFLog(String.Format("{0} - {1} ", ex.Message, ex.StackTrace));
            }
        }
        #endregion

The logged events show in the administration page in the IWebWF website.

In addition, the workflow status page in the IWebWF website shows any currently persisted workflows. These are workflows that are waiting for some sort of action before they can terminate. The persistence service automatically saves the state of idled workflows and automatically reactivates them when needed. This allows long running workflows to survive during server restarts.

Clicking on a workflow GUID on the Status page displays the workflow name and it's activities.

Security

Windows workflow allows you to design security in any way you prefer. This example uses the following security design:

Summary

The question remains, why would you want to go through all this trouble? This example barely qualifies as a justifiable use of WF. The reasons to use workflow rather than simple procedural code is:

In addition, using IWebWF eliminates a lot of unnecessary work.

[Back to: The ADefWebserver DotNetNuke HELP WebSite]


 DotNetNuke Powered!DotNetNuke is a registered trademark of DotNetNuke Corporation.