Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

Document approval workflow system

4.09/5 (26 votes)
24 Mar 2007CPOL6 min read 4   9.4K  
Generic, full cycle of document approval workflow system, using Windows Workflow Foundation (WF).

Screenshot - wwf_document.gif

The purpose of the article

Demonstrate, a quick view of workflow foundation, and highlight the basic knowledge that

we should know before create first useful workflow application,

and highlight a design guide lines to create reusable workflow applications.

Finally, to taste the workflow through a sample project that is generic and useful for any

document workflow system, from the client application to administration application using

database.

Workflow foundation, in brief:

We can see the workflow foundation, simply as a collection of activities that can work

together through conditions and rules for specific purpose.

Workflow foundation is not if else blocks with loop blocks, and if it was implemented as

this simple algorithm, it will cost us a lot for no benefits. The bottom line of using

workflow foundation is, simplifying a

complex algorithm to simple visual editable, maintainable workflow,

Replaced my old design, with workflow:

I used the command patterns a lot in my work, as an encapsulation of the request in

single object called command, that is initialized with parameters ,

then execute method will be invoked to retrieve the result, or to do the specific tasks,

But I always have a problem in sharing the properties, information and resources, or the

result(s) of the other commands,

Some commands were waiting until other commands finished,and others will begins from

the ending point of others,

To solve this problem I always use a mediator which in most cases is CommandManager

class that has a collection of a commands, and knows the order of execution of all

commands,and share all parameters, through the container scope, and each command has

a reference to the command manager,

so it can access all the properties of all other command very easy, or setting any

property for other command or the out client application.

Finally

to make the design good, I always inherit all the commands from CommandBase

abstracted class to give me the contract of all commands,so the CommandManager can

execute and initialize all childs commands.

The funny thing is,

that each time I try to create the command objects, I found myself made the same

generic pattern,and wondering if there any way to visualize these library for better use?

Once I saw the sequential workflow activities first time, I said "Aha"

That is exactly what I want, commands (Activities ), has a container command manager

(SequentialWorkflowActivity),and all commands has the same interface or base class

(Activity), and all commands can get the data through contacting the command manager

Sequential workflow

Finally visualizing every thing for quick development, and for better maintainability it is a

great stuff.

What impressed me in workflow foundation?

Sure every thing in workflow foundation impressed me, But the Idea of Adding service(s)

in runtime, then using this service in the workflow through referencing the service

using the public method GetService of WorkflowRuntime object, or through special

activities like CallExternalMethodActivity, HandleExternalEventActivity, it is

fantastic Idea,

because simply you can separate all the abstracted classes and interfaces to a

separate assembly, then make the workflow and the core application reference that

assembly, so you increased the level of reusability of workflow, because the workflow was

coded against the interface(s) but you can in runtime register any concrete class, that

implement this interface,

you can integrate the wonderful Provider pattern that introduced in .Net 2 with the

wonderful workflow foundation through registering any service in web.config, in workflow

section (this topic is beyond this simple project) Once you understand how to use, the

custom service with in the workflow You will understand all

other services, including "out of box" services like WorkflowPersistenceService,

WorkflowSchedulerService, TrackingService,..

Basics that you should know,

Before developing, the first practical application,

as I mentioned, using workflow as a bunch of "if else" statements, is not good idea at

all, and you should know the minimum information about workflow You should know

• How to pass parameters to workflow and to retrieve parameters, after the workflow

completed.

• How to create a custom activity, to work for specific task.

• How to use custom service, and consuming this service inside the workflow.

• How to pass data from host application to workflow or from workflow to application

through workflow processing. I think that is the minimum that you should know to create

a useful workflow application For advanced work with the workflow you may need to now,

• Using WorkflowPersistenceService, and Load and Unload to database,

• Using Transaction scope for data consistency.

• Create fault handler, in case of errors happened inside the workflow. Sue that is a lot to

mention about, the workflow specially if you, use the workflow in Asp.NET applications or

in webservice,

Or you think to update simple windows form after completing the workflow

or in specific events. But that is not the purpose of this article.

Design guide lines to work with workflow foundation

I tried to apply the workflow with objects that I already wrote before using my skills In

object oriented programming, using the workflow but I found it is quite difficult to use the

workflow, without any preparation to my code, exactly like first time I tried to use Unit

test to test "not testable " applications

I found that I have to make my code testable through dependency injection and

constructor injection and mockobjects,

Now To use the workflow you have to change your way in developing, So instead using

100% object oriented programming you should move object behaviors from concrete

classes to services,

so your code will be more service oriented

Steps for preparing your code, before using workflow:

Basic 3 steps

1) Separate all object behaviors to Services.

2) Extract the Interface for these services (user refactoring in voisual stodio 5) , so all

your services should implement services interfaces

3) Separate these interfaces to separate assembly

Consider this simple example

Assume you always use a class called

Customer, has these properties

ID, Name, Address,

and has these methods

Add(), Update(), Delete(), Attach(), Register(), Unregister(),..

To use the workflow , you should separate this object state and Identitylike like ID, Name,

Address And move the behavior (public methods, events,..) to CustomerService that

Implement ICustomerService.

In this case you can use the workflow very easy with your prepared code.

In source sample,

you will see the Document Object is inherited from the IDocumentService, and both of

them used for describing the whole document process

The Sample project in Brief

The Solution sample is 3 basic projects and other 2 projects for common objects and

interfaces.

The Idea of the Application is

The user will create a document with his client application,

the user still can update his document as long as is in Open sate.

Then the user will kick the document to workflow, then depending on the document

properties , the workflow will choose the Administrator (s), that is delegated to approve

this document, and will save the data to database and will wait until the Administrator to

complete the cycle.

The Administrator after the login screen (sample will work without password) will see all

documents that are waiting for him,

and then he will approve or Reject the document,

so the document will be flagged as approved or to open state again to the user that can

update the document or delete it or to re submit again to the workflow,

Client Application Sample

Image 2

Create document inside the main form

private void btnCreateDocument_Click(object sender, EventArgs e)
     {
         if (ValidateForm())
         {
             Document _document = new Document();
             _document.Category = (Category)this.cmbCategory.SelectedItem;
             _document.Title = this.txtTitle.Text.Trim();
             _document.Description = this.txtDescription.Text.Trim();
             _document.Depositor = User.CurrentUser.UserName;
             _service.CreateDocument(_document);
             ConfirmSuccess();
         }
         else {
             MessageBox.Show("Please complete, the form's fields", "Validation, Error", MessageBoxButtons.OK
                        , MessageBoxIcon.Error);
         }
     }

IDocument services class diagram
Image 3


document Database Diagram
Image 4

Document services class diagram


Image 5


[Serializable]
   public class DocumentService : IDocumentService
   {
       public Document GetDocumentByID(int id)
       {
           return DocumentDalc.GetDocumentByID(id);
       }

       public Documents GetCreatedDocuments(string depositor, Status status)
       {
           return DocumentDalc.GetAllCreatedDocuments(depositor, status);
       }

       public Documents GetCreatedDocuments(string depositor)
       {
           return DocumentDalc.GetAllCreatedDocuments(depositor);
       }

       public Documents GetAllNeedToApproveDocuments(string userName)
       {
           return DocumentDalc.GetAllNeedToApproveDocuments(userName);

       }
       public DocumentWorkflowItem GetWorkflowItem(Document document)
       {
           DocumentWorkflowItem wfItem = new DocumentWorkflowItem(document);
           return wfItem;
       }

       #region IDocumentService Members

       public void CreateDocument(Document document)
       {
           DocumentDalc.CreateDocumentRecord(document);
           if (this.Created != null) Created.Invoke(this, new DocumentEventArguments(document));
       }

       public void UpdateDocument(Document document)
       {
           DocumentDalc.UpdateDocumentRecord(document);
           if (this.Updated != null) Updated.Invoke(this, new DocumentEventArguments(document));

       }

       public void ArchiveDocument(Document document)
       {
           document.Status = Status.Archived;
           DocumentDalc.UpdateDocumentRecord(document);
           if (this.Archived != null) Archived.Invoke(this, new DocumentEventArguments(document));

       }

       public void DeleteDocument(Document document)
       {
           document.Status = Status.Deleted;
           DocumentDalc.UpdateDocumentRecord(document);
           if (this.Deleted != null) Deleted.Invoke(this, new DocumentEventArguments(document));

       }

       public void CreateInWorkflowDocumentForUser(Document document, string approvalUser, Guid workflowID)
       {
           DocumentDalc.CreateInWorkflowDocumentRecordForUser(document, workflowID, approvalUser);
           if (this.SentToWorkflow != null) SentToWorkflow.Invoke(this, new DocumentEventArguments(document));

       }

       public void CreateInWorkflowDocumentForGroup(Document document, string approvalGroup, Guid workflowID)
       {
           DocumentDalc.CreateInWorkflowDocumentRecordForGroup(document, workflowID, approvalGroup);
           if (this.SentToWorkflow != null) SentToWorkflow.Invoke(this, new DocumentEventArguments(document));

       }

       public void ApproveDocumentWorkflow(Document document, string approvalUser, Guid workflowID)
       {
           DocumentDalc.CreateApprovedDocumentRecordForUser(document, workflowID, approvalUser);
           if (this.Approved != null) Approved.Invoke(this, new DocumentEventArguments(document));

       }


       public void RejectDocumentWorkflow(Document document, string approvalUser, Guid workflowID)
       {
           DocumentDalc.CreateRejectedDocumentRecordForUser(document, workflowID, approvalUser);
           if (this.Rejected != null) Rejected.Invoke(this, new DocumentEventArguments(document));
       }


       #endregion

       #region IDocumentService Members



       public event EventHandler<externalexternaldocumenteventargument __designer:dtid="1688849860263990" /> RaiseApproveDocumentWorkflowEvent;
       public event EventHandler<externalexternaldocumenteventargument /> RaiseRejectDocumentWorkflowEvent;

       #endregion
       public void RaiseApproveDocumentWorkflow(DocumentWorkflowItem wfItem)
       {
           ExternalExternalDocumentEventArgument arg = new ExternalExternalDocumentEventArgument(wfItem.WorkflowID, wfItem.Document, User.CurrentUser);
           if (RaiseApproveDocumentWorkflowEvent != null)
           {
               RaiseApproveDocumentWorkflowEvent.Invoke(this, arg);
           }


       }

       public void RaiseRejectDocumentWorkflow(DocumentWorkflowItem wfItem)
       {
           ExternalExternalDocumentEventArgument arg = new ExternalExternalDocumentEventArgument(wfItem.WorkflowID, wfItem.Document, User.CurrentUser);
           if (this.RaiseRejectDocumentWorkflowEvent != null)
           {
               RaiseRejectDocumentWorkflowEvent.Invoke(this, arg);
           }
       }

       #region IDocumentService Members


       public event EventHandler<documenteventarguments /> Created;

       public event EventHandler<documenteventarguments /> Updated;

       public event EventHandler<documenteventarguments /> Archived;

       public event EventHandler<documenteventarguments /> Deleted;

       public event EventHandler<documenteventarguments /> SentToWorkflow;

       public event EventHandler<documenteventarguments /> Approved;

       public event EventHandler<documenteventarguments /> Rejected;

       #endregion
   }

Administration Application Program

Image 6
private void approveToolStripMenuItem_Click(object sender, EventArgs e)
{
    _documentID = int.Parse(dgrdDocuments.CurrentRow.Cells["ID"].Value.ToString());
    DialogResult result = MessageBox.Show("Are you sure that you want to Approve this document", "confirmation", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
    Document doc = null;
    if (result == DialogResult.Yes)
    {
        doc = _service.GetDocumentByID(this._documentID);
        if (doc.Status != DocumentBaseLibrary.Status.InWorkflow)
        {
            MessageBox.Show(string.Format("This document {0} is not in Workflow state", doc), "Error", MessageBoxButtons.OK
                , MessageBoxIcon.Error);
            return;
        }

        Approve(doc);
    }
}
  private void Approve(Document doc)
{
   DocumentWorkflowItem wfItem=_service.GetWorkflowItem(doc);

   // Get the type of the workflow
   Type type = typeof(DocumentWorkflow.DocumentWorkflow);

   // Start the workflow instance
   Guid id = wfItem.WorkflowID;
   try
   {
       WorkflowInstance inst = theWorkflowRuntime.GetWorkflow(id);
       inst.Load();
       this._service.RaiseApproveDocumentWorkflow(wfItem);

   }
   catch(Exception ex)
   {
       MessageBox.Show("The Document cannot be loaded. " + ex.ToString(),
           "Administration", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
   }
}

Conclusion

Workflow foundation, it take off completely the burden of all complexity of the

Application rules and decisions

But, to achieve the maximum reusability, you have to

prepare your code by creating services , and separate abstracted services in separate

assemblies

License

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