A DotNetNuke module communicating with a WF workflow hosted in the Open Source project IWebWF
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:
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.
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.
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.
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.
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";
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).
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.
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);
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); } #endregionThis causes WaitForApproval activity to stop and the workflow terminates. The process is complete.
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.
Windows workflow allows you to design security in any way you prefer. This example uses the following security design:
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™ is a registered trademark of DotNetNuke Corporation.