Friday, October 28, 2005

A K2.net 2003 Looping Pattern

Its about time...

Ever so often a student ask me about some K2.net 2003 best practices. Mostly there's a hidden question behind the obvious... how to solve a specific business problem using this great workflow engine's process mapper.

I've decided to document a view of my own experiences. This month I'll overview a solution a client requested regarding scheduled document checking in a SharePoint document library. Since the rest of the solution was all done in SharePoint 2003 and K2.net 2003, I was eager to implement this solution of checking a bunch of documents using the exact same technologies.

As with all best pratices and patterns, note:

  • Its a recommended, repeatable suggestion for solving a common, repeated problem. Its not a rule or the only way to solve the problem.
Problem:

My client's got a document library with hundreds of documents. The document library contains various document properties (meta data) - one being the date that a document is valid until. Once a document's valid until date is reached, certain actions need to take place. These actions already exist in another K2 process.

Solution:

Its a simple problem, and writing some kind of process that will (1) start periodically, (2) load some configuration, (3) load all the documents from the document library that's about to expire, (4) loop them and, for each, call a process that will take some actions on them using the K2.net IPC event. This process is then scheduled to run periodically (using the standard Windows task scheduler I gave my client the control).

Lets look at the K2.net process I've mapped out for this:



The first activity merely loads some configuration settings. The only configuration I load here is the DayOffset used for determining when a document is expired.

The second activity connects to the SharePoint document library and loads all the documents that is about to expire based on the configuration settings from the first activity.

  • To enable my client to control the process (and other processes part of this solution) I've build a simple key/value custom list in SharePoint that allows them to control processes related settings. This is a great way to hide the actual process from the user but give them the power to control the process without knowing how to configure, build and export K2.net processes when they want to make changes.

The list of documents that should be processed are loaded into a K2.net XML datafield by the Get Expired Documents activity. The XML data field, ExpiredDocuments, will be the iteration that will be looped in the K2.net process. It contains a list of document URLs of the expired documents based on my criteria loaded in the first activity.

The other important K2.net datafield it the DocumentCounter datafield. Its the iterator of the loop.

The third activity is the most important part of this pattern. It is the looping structure. Using a combination of a process datafield as the iterator, a XML process datafield as the iteration and line rules as looping conditions, you can build pretty much any looping structure in K2.net.

This activity will loop the list of document URLs - every time selecting the next document from the XML process datafield and making it the current document. The DocumentCounter is updated and the MustLoop datafield indicates if the loop should terminate. This is all done within a single server event in the main looping control activity.

public void Main(ServerEventContext K2)
{
K2.Synchronous = true;
// get the list of expired
documents from the xml datafield string DocsField;
DocsField =
K2.ProcessInstance.XmlFields["ExpiredDocuments"].Value;

// create a xml document and load the xml datafield
System.Xml.XmlDocument oXmlDoc = new System.Xml.XmlDocument();
oXmlDoc.LoadXml(DocsField);

// select the
document element at the current position

int DocumentCounter =
(int)K2.ProcessInstance.DataFields["DocumentCounter"].Value;

System.Xml.XmlNode oXmlDocument =
oXmlDoc.SelectSingleNode("Documents/Document[position() = " +
DocumentCounter.ToString() + "]");

// if there's a document in this position set the 'CurrentDocumentURL' datafield,
// set the 'MustLoop' datafield to true to enter the loop and
// increment the 'DocumentCounter' datafield
// ELSE
// exit the loop by setting 'MustLoop' to false
if (oXmlDocument != null)
{
K2.ProcessInstance.DataFields["CurrentDocumentURL"].Value =
oXmlDocument.SelectSingleNode("DocumentURL").InnerText;

((int)K2.ProcessInstance.DataFields["DocumentCounter"].Value)++;

K2.ProcessInstance.DataFields["MustLoop"].Value = true;
}
else
{
K2.ProcessInstance.DataFields["MustLoop"].Value = false;
}
}

Since the architecture of K2.net does not allow me to create a code block that will span multiple events (never mind multiple activities), I need to build a loop over activities by using a combination of lines and a iterator to keep track of the current document being processed. I will use the DocumentCounter process datafield that will keep the value of the current position within the element in the ExpiredDocuments XML datafield. Everytime the Loop activity runs, I select the next document from the XML list using a simple XPath query:

Documents/Document[position() = '1 based index']

This query will return null into the XmlNode object if there is not a element at the given position (in my solution this indicates that I've iterated all the nodes in the element and that the loop should terminate).

From here the process will either flow into the loop if there's another document to be processed or the loop with terminate if all documents was processed by using the MustLoop process datafield based on the line rules leaving the Loop activity.

By using XML process datafields in K2.net, you can build and loop lists of items with ease. And don't be scrared to dive into the code level of K2.net - its THE most powerfull feature of K2.net - having the .NET class libraries right there for you to write realy powerful workflow.

If you have to allow a mortal, um, non-developer to configure the process, just take your code and hide it in an assembly. Have the non-developer call your code using the Assembly Wizard event thats part of K2.net 2003 SP2a.