I know I know, I am way behind on my InfoPath Integration series that I started several weeks back, but still, I'd like to go off on a tangent here on a somewhat tricky methodology for accomplishing something that is probably pretty common in a lot of workflow scenarios. That scenario is this: Lets say we have a list of zero to many items, where it is unknown how many items may be in said list but we need to do some sort of workflow process on each item in the list. Specifically, we will use an InfoPath template with a repeating table as our example. In this InfoPath template, the user may add as many rows as they like to the repeating table which we will call 'Child' for simplicity sake. Each 'Child' row has a 'Name' and a 'Description' field. In our workflow process, we need to loop through each 'Child' row and create a smart object for it, so it is persisted to a data store.
I am going to make a couple of assumptions here before I get started with my example. I am assuming there is an InfoPath form with the 'Child' repeating table present as defined above, and there is a simple smart object defined with a 'Name' and 'Description' property. I am also assuming that the InfoPath form is integrated with the process which we will be using as an example, with the process being started on submission of the Infopath form.
Given our assumptions, the first step to simulating a For-Each loop is to drag out a Smart Object event which is going to be our action taken on each iteration of the loop. Map this Smart Object event to our 'Child' smart object's 'Create' method. Leave the input and output parameters blank for now, we'll need to extract them in our loop. What we are doing here is providing the action to take place on every iteration of the loop, which in this case is to create a 'Child' smart object:
No we need to go about telling the activity that it is to be executed for each child row in the InfoPath form. It is not very intuitive to do this, which is why I'm blogging about it in great detail. I'll try and explain each step as best I can. The first step is to bring up the activity's 'Destination Rule' wizard:
Once the rule wizard starts, we actually need to GO BACK a screen (Click the 'Back' button), so we can tell the wizard we want to run in advanced mode:
Now that we are in advanced mode, click 'Next' and we are presented with a list of options for specifying when instances of this activity should be created. We want to specify 'Plan per slot (no destinations)', which means that this activity is not going to an actual destination and that we will be creating x number of instances of the activity:
Click 'Next' and we are presented with a special screen based on our previous selection of 'no destinations'. In this screen, we are being asked to specify how many instances of the activity we will be creating. The first option assumes we know the exact number of slots to create, which we don't since we are relying on the InfoPath repeating table to tell us this. The second option allows us to specify any repeating field (be it a repeating xml node or a smart object list) to iterate through, creating an instance of the activity for each iteration. This is exactly what we want, so select this option. In the field entry, specify the repeating 'Child' node from our InfoPath form which instructs it to create an activity instance for each row in this repeating table:
Click 'Finish' to finalize the destination rule wizard. What would happen now if we ran this process is that for each 'Child' row inserted into the InfoPath form, there will be a blank smart object record inserted into the Smart Box database. Why will the smart object values be blank? We never mapped the 'Create' method's input parameters to the values being extracted from the child node iteration. Now there is good news, bad news and worse news here: The good news is that all of the data from the child node of the current iteration is available in the 'ActivityDestinationInstance.InstanceData' field:
The bad news is that, as you can see, there is no design-time context for the underlying data in this field so we can't use the interface to map values to the Create method. No problem, we can always create our data fields manually in server code, parsing the individual data fields from the InstanceData into our own data fields. Here is the worse news. The InstanceData field concatenates the data of its repeating node into one giant non-delimited string, making it very difficult to extract individual values. I am currently working with the product support team to see if there is a better way to get at our iteration data, but for now this is only a viable solution if you either have just one child value or if you can make assumptions on data locations and sizes in your InstanceData. I'll post an update here if and when I learn a more effective way to get at the iteration data.
Previously, I identified a couple of specific technical problems that I'd like to stumble through solutions for. One of the technical problems I described like this:
'Submitting one order form may spawn multiple processes that can be acted on simultaneously by different users to accomplish one final result. How will we have one process split into many, passing along the common information necessary to the child processes?'
I suppose I will clarify this description a little bit before I jump into the solution. One InfoPath form containing some common user-entered data starts the master process. Submitting this form starts three other processes that have their own InfoPath forms that need the common information pre-populated from the first InfoPath form. So, to do a proof of concept for this issue, we'll need a total of four InfoPath forms and four processes defined in a K2 workflow project (Master, Child1, Child2, Child3). Let's start out with the InfoPath forms:
Create an InfoPath form named 'Master', and add two text fields that will represent the common data. We can just call them 'Id' and 'Name':
Create the next three InfoPath forms named 'Child1', 'Child2' and 'Child3'. You can put any other information you want in them, as long as you include the 'CommonFields' section as it was defined in the Master form:
Now that we have our InfoPath templates defined, we need to consume them in K2 Processes. Start Visual Studio and create a new empty K2 project:
Add four processes to the project named 'Master', 'Child1', 'Child2' and 'Child3':
Let's start with defining each child process. They will simply create a task list item pointing to their corresponding InfoPath form which will have the header somehow pre-populated with data entered in the Master form. First things first, though, we need to integrate the process with its InfoPath form. Drag out an InfoPath Integration process wizard and follow the steps necessary to integrate the InfoPath form and create a simple client event (I've blogged this process before if you get stuck, you can read it here, though that was blogged long before I semi-knew what I was doing). Duplicate the process for all three child processes so that they look as simple as this:
Note that we did nothing with pre-populating the data in the InfoPath form yet, we'll tackle that later. For now, we can move on to our Master process. We'll need to integrate the Master InfoPath form with this process similarly to how we did it with the Child processes, the only difference being that we need to ensure that the Master form starts the process, by filling in this portion of the integration wizard:
This will make it so that any time this specific form is submitted, it will automatically start this Master process. So now we come to the heart of the matter - how do we invoke the child processes all at once? Thankfully, the developers of this product anticipated this very need and supplies a wizard that will make this challenge a breeze. Drag out the 'IPC Event' wizard:
In the wizard, we'll need to specify which process to invoke. Let's start out invoking our Child1 process. It also asks us if it is Synchronous or Asynchronous - basically whether or not we need a response from this child process before we can continue. In our case, we just want to kick off the process and we don't care what it does so we will chose Asynchronous:
We also have the ability here to specify a new folio, if we want the child folio to match something that we are gathering in the parent process. Be warned, though, that this folio definition will be overridden by any folio definition in the Child process (defined in the InfoPath Integration wizard). Clicking next brings us to a simple security question - who to call the process as. Now normally, I would expect to use 'Integrated Windows' here so that the process is invoked by whomever invoked the Master process. However, for reasons I don't yet understand, this never worked successfully for me and I've had to use the 'Impersonate Originator' option:
Clicking next again brings us to a screen that is very interesting to us, given the challenge we are trying to overcome. It is asking us what data we want to pass to the child from the parent! That is perfect! Or is it... Let's see how it handles our specific request of passing the 'CommonFields' data from the Master form to the 'CommonFields' data of the child. Add a new process field mapping:
Use the object browser to drill into our Master infopath form XML representation and select the 'CommonFields' section:
And drag it onto the Field Mapping form:
Now use the object browser via ellipsis next to the Child process field name to browse to the 'CommonFields' data in the Child InfoPath form:
And drag it onto the Field Mapping form... uh oh, it won't let us! Looks like we are asking it to do something a little more complex than what it was designed for, which is understandable as moving one piece of xml between documents might not always be very straight forward, particularly when namespaces are involved. Looks like we'll have to do some of this ourselves via code. Bring up the Child Process object browser again, and add a new xml field called 'ParentCommonFields'. We'll use that as the temporary container for the common field data:
Drag it onto the Field Mapping form:
Something else I learned the hard way about this process is that if you specify a portion of an xml document, as we did when we specified 'CommonFields' in the Master document, it will send the data as a list of field values as opposed to the XML. As such, we'll just send the whole XML over and parse out the CommonFields section in the Child process logic. So revisit the Parent field value and browse to the Master xml document:
Drag it onto the Field Mapping form:
Repeat this IPC event process for all three Child processes so your Master process looks like this in the end:
Now we need to revisit each Child process and add some logic to copy the parent common fields into the child common fields InfoPath xml. The only way to do this is with some server side code, so drag out a new 'Default Server Event (Code)' wizard:
And connect it up before the InfoPath client event:
View the code of this Server Event (Right-Click - View Code - Event Item) and add a 'using System.Xml' to the top to make our XML references cleaner. Now, what we need to do is load up a DOM with the Child InfoPath form XML so we can navigate to its 'CommonFields' section and replace it with the 'ParentCommonFields' value. The caveat here is that the Parent Common Fields XML may have a different namespace associated with it, so we'll need to load that up in a DOM too so we can identify and replace any occurrences of an incorrect namespace:
XmlDocument docChild = new XmlDocument();
docChild.LoadXml(K2.ProcessInstance.XmlFields["Child1"].Value);
XmlNamespaceManager nsMgrChild = new XmlNamespaceManager(docChild.NameTable);
nsMgrChild.AddNamespace("my", docChild.DocumentElement.GetNamespaceOfPrefix("my"));
XmlDocument docParent = new XmlDocument();
docParent.LoadXml(K2.ProcessInstance.XmlFields["ParentCommonFields"].Value);
XmlNamespaceManager nsMgrParent = new XmlNamespaceManager(docParent.NameTable);
nsMgrParent.AddNamespace("my", docParent.DocumentElement.GetNamespaceOfPrefix("my"));
docChild.SelectSingleNode("//my:CommonFields", nsMgrChild).InnerXml = docParent.SelectSingleNode("//my:CommonFields", nsMgrParent).InnerXml;
K2.ProcessInstance.XmlFields["Child1"].Value = docChild.InnerXml.Replace(nsMgrParent.LookupNamespace("my"), nsMgrChild.LookupNamespace("my"));
Let's deploy this thing, and when it is done, we will visit the SharePoint list associated with our 'Master' form, add an item, and see if we get tasks generated for each 'Child' form. Opening up those tasks should show the Master Common Field data in the header. Don't forget to give yourself process start rights for the master and child processes via the Workspace! If it doesn't work as expected or I glossed over an important detail, please leave me a comment and I'll try to help as best i can.
The K2 [blackpearl] product integrates very well with InfoPath, as any sales-pitch and pre-made tutorial will inform you. I was very impressed with how easily I was able to use InfoPath as the user interface to my simple workflow processes, but I feel that every new technology looks good when you take it through its expected scenarios. As such, what I am going to try and do for you in the next series of posts is describe my experiences implementing K2 [blackpear] InfoPath integration in the real-world, where the business requirements don't always exactly match the InfoPath integration tutorials. What I hope for you to get out of this series is the scenarios where InfoPath integration may be more complex than it would seem on the surface, where it will be a quick-win, and some tricks I stumble through along the way to get over some of the hurdles that present themselves.
Here is some background on the project:
Users will be interfacing with SharePoint 2007 to create service request orders, which are sent to a service provider team for processing. There are four distinct services with their own order forms, and each of these order forms can be either filled out from scratch and submitted ad-hoc, or submitted through an automated process where most of the order form data is pre-filled.
Here are some of the key business requirements that I am going to cover in detail:
There are multiple ways to start the same process. That is, it is possible for a user to submit an order at any time (ad-hoc) or to approve and submit an order that is automatically generated for them (outside of this system). This might be tricky because from what I've seen, a process can be tied to only one view of one InfoPath form for process initiation. The ad-hoc and automated order forms will be different views at the least, maybe even completely different InfoPath forms... how will we get one process to start off of the submission of either one of these forms?
Submitting one order form may spawn multiple processes that can be acted on simultaneously by different users to accomplish one final result. How will we have one process split into many, passing along the common information necessary to the child processes?
Since there are a number of activities related to one request, the requestor would like to see the progress being made on each activity. The default task list webpart in SharePoint only shows tasks assigned to the user that is currently logged in. How will we change this so that it shows all tasks associated with the process that they initiated?
All of the various order forms share a certain subset of data, lending themselves to a hierarchical use of content types to represent a base order form and then each specific order form derived from it. I did not see any options in my InfoPath integration experimentation to indicate that the integrated form can be turned into a content type that is based on another content type. How will we get our InfoPath form data to be tied to a base content type?
The task list needs to show various information that exists within the InfoPath form data (such as client name, due date, etc.). The default task list can only show a static set of data, and the InfoPath integration wizard didn't seem to have a screen to promote fields to columns (a feature available when publishing InfoPath forms via InfoPath 2007). How (and where) will I be able to show InfoPath form data as columns associated with a task?
Users want to have buttons or links to click in the InfoPath forms to submit their actions with one click. The InfoPath Integration tutorials all use a drop down list of actions that the user selects from and then clicks a submit button. How will I get the actions related to the task into buttons or links that know what to do when they are clicked?
If you are interested in seeing the solutions to any of the above problems, keep an eye on this blog as I will be posting solutions as I stumble through them over the next few days.
While this is a bit of a departure from the theme of my blog, the Coding Challenge recently held by Clarity Consulting for its employees is an event that I feel is worth blogging about. The reason it is of particular interest to me is that behind my calm, mild-mannered demeanor I am a fiercely competitive person - not from the standpoint that I must win everything I compete in but in that I simply love the thrill of the competition itself. There is also the fact that on any given work day, I could go home thinking I must be the greatest developer in the world or go home thinking that I must have chosen the wrong profession; outside of my grades in college (which mean absolutely nothing in the real world) I have no way to judge or rank my developer skills. Thusly, I was very excited to hear about an opportunity to test my abilities against a number of other Clarity consultants, and proceeded to completely ignore preparing in any way, preferring to 'stumble through' it as I do everything in my life (not that there was much to prepare for, really, as you'll see).
Come time for the challenge, I learned that there were going to be just three questions - an 'easy' one, a 'medium' one and a 'hard' one and we would gain points according to how quickly we submitted a working solution to the problem. Nobody knew what the questions would be until the challenge started, and the timer for the question would begin right when you read it. The solution would consist of only one class file (either VB or C#) with a pre-determined public method signature that can accept a number of different parameters and return results in a pre-determined format for testing. After submission, the class would be subjected to a number of tests consisting of various parameter values and the results would be validated (I am assuming an exception would count as failing the test as well). If any one test failed, you would score a big fat zero for that solution. This brings me to my one gripe with the challenge: you could have written the greatest algorithm in the world faster than anybody else but forgot to handle one outside test case (such as null parameter) and you'd get no credit at all! I had hoped you'd get points per test case that succeeded, but such was not the case. Turns out, though, that the test cases weren't as hard-core as I thought, as I built my method without any error handling, bounds checking or null handling and manage to score full points so I guess I shouldn't gripe too much. I would, however, still submit that the test cases should've been tougher as a 'good' developer wouldn't have forgotten to check for null parameter values like I forgot to do.
While I won't post my results here, I will say that I was both humbled by what the other Clarity consultants were capable of and emboldened by my own moderate success. My experience was somewhat tainted by the fact that I had a specific time I needed to leave by and since we started the challenge very late I was severely handicapped by time. Not that I am making excuses here, mind you, I have no confidence that I would've done any more had I another 50 hours much less another 50 minutes, but I was very disappointed to leave the challenge in progress. I am very much looking forward to stumbling through the next tech challenge, I'll make sure I have no responsibilities afterwards this time and therefore, no more pathetic excuses!
All in all, it was a very fun event and is a good example of one of the many things Clarity does to keep a level of excitement and interest in the workplace.
Technorati Tags:
ASP.NET,
K2,
Blackpearl
While doing a little requirements gathering for a workflow project that will be leveraging the K2 Blackpearl product, I came across a few requirements that were not features we'd get 'out of the box' in Blackpearl. Specifically, the project required a stand-alone user role manager outside of the one provided within the K2 workspace and an enhanced task list containing features that do not exist in the default task list web part.
I am going to start out by discussing how to leverage Blackpearl's management API to accomplish at least part of a custom user role manager. The goal here is to provide a site where authorized users can see all of the workflow roles defined for workflows, and add users from Active Directory to those roles. We'll need to be able to add new roles, rename roles, and remove users from roles as well. These roles can be used by workflow processes to assign tasks to groups of users, so they appear on the task list for every user belonging to the group until one of the users opens the task. The reason for using these roles instead of Active Directory groups is that, ostensibly, you can open up the role management to end users instead of relying on network administrators to maintain your Active Directory groups. While Blackpearl provides an interface for maintaining these roles out of the box, it currently resides within their 'Workspace' which has a ton of other functionality that we'd like to insulate our end users from.
So, where do we start? Let's create a basic C# ASP.NET application in Visual Studio, and make a reference to the following .NET assemblies:
SourceCode.Security.UserRoleManager.Management
SourceCode.Workflow.Management
These assemblies are inter-related, and together provide the API for maintaining workflow roles. The core class in this solution will be the 'UserRoleManager', which is used to query and manipulate the blackpearl environment. In order for it to work, though, we need to connect it to our workflow server. This will require a connection string to your workflow server, which is easily obtained via the object browser (Select the 'Change Server' option from the 'Environment' sub menu and you'll get your current connection string). By the way, all of this code is going to be going into the 'Page_Load' event of our default page - I've added only a grid view (named GridView1) to the page which we'll use to see our output from our API calls. Here is the logic for instantiating the user role manager API:
UserRoleManager managerAPI = new UserRoleManager();
// Connect to environment
managerAPI.Connection = managerAPI.CreateConnection();
managerAPI.Connection.Open("[host server connection string here, can get it from object browser]");
// END connect to environment
Now we are connected to our environment, and yes, in a real-world scenario you'd want to wrap this in a try-catch with a finally block to close the connection, just like you would with any data connection. I'm just a lazy blogger and I'm not gonna write all that too. Now the API becomes very straight-forward to use. I want to first put all of the existing roles into our grid view, so I will simply write this:
// List all roles
Role[] roles = managerAPI.GetRoles();
this.GridView1.DataSource = roles;
this.GridView1.DataBind();
// END List all roles
Executing this, you should see all of the roles you have previously defined appear in your grid view. Doesn't get any more straight forward than that... updating roles is just as intuitive as well. To update the description of one of the roles we've just retrieved (we'll use the first role retrieved for simplicity sake), simply follow this logic:
// Append a '7' to the first role's description (update test)
Role updateRole = roles[0];
updateRole.Description += "7";
managerAPI.UpdateRole(updateRole);
// END Append a '7' to the first role's description (update test)
Now not everything is all pretty and nice with the user role manager API, there is a bit of complexity to maintaining the users of each role that feels quite over the top. I was expecting users to be a child collection of each role, but that was not the case. Users are made available from the Role object via the 'GetData' method, which returns some simple XML containing role information, users, groups and smart objects belonging to this role. This same XML schema needs to be followed whenever you want to add/update role users vial the 'LoadData' method. Here is a quick example of the code required to add myself to a new role named 'Test':
// Add a new role
Role newRole = new Role("Test");
newRole.LoadData("<role name=\"" + newRole.Name + "" guid=\"" + newRole.Guid + "\" description=\"\" isdynamic=\"false\"><include><users><user name=\"K2:Domain\\user\" /></users><groups /><smartobjects /></include><exclude><users /><groups /><smartobjects /></exclude></role>");
managerAPI.CreateRole(newRole);
// END add new role
Pretty weird, huh? Though it does allow for a bit of flexibility, particularly in using smart objects to populate roles (a topic for another day), I prefer making children as properties of the parent class for the sake of readability. Ah well, it may just be a coding style conflict. Never the less, it all works as advertised and in learning this it makes the visual elements of a custom user role manager the most complicated part of development; the logic to process the user's requests will be a snap.
Since 'Smart Objects' are one of the key new features of K2 blackpearl over K2 2003, I've decided that I need to spend more time stumbling through them and their capabilities. I'm still a little confused as to when, why and how to use them, my previous postings on smart objects exhibited only a very narrow scope of what they are used for and they were pretty straight-forward at that. So here is my goal: Every workflow that will be created for my current client will need to have the same sort of information attached to it that go beyond the normal process data; things like the current branch that the workflow belongs to would be a nice piece of information to have for reporting purposes. I think a SmartObject would be suitable for that, a 'StandardInformation' smart object that gets attached to every workflow and gets the current branch from... well... somewhere I don't know, we'll probably just hard-code it at first just to prove out the concept.
Let's get started then, shall we? I already have a K2 workflow Visual Studio project with a few processes, it doesn't matter what the processes do as long as they actually work, as the feature we are focusing on here is SmartObject integration. Add a new SmartObject to the project and name it 'StandardInformation':
The SmartObject editor opens up in simple mode by default, with pre-populated CRUD methods which I guess we'll keep. More importantly, though, is that we add some properties that define this object. As mentioned earlier, we need a 'Branch' property, but every SmartObject definition must also have a key property and 'Branch' certainly does not fit the bill so we'll add an 'ID' property as well:
I hope to explore the 'Associations' and 'Additional Settings' menus in the future, but they are more advanced than what we are trying to accomplish with this SmartObject so we'll leave them alone for now. Similarly, I should explore 'Advanced Mode' a little more, but there is nothing there that is necessary at this time, so add that to my ever-growing list of things to explore later. For the time being, let's save off this SmartObject as-is and open up any old process to see how we can meld the two together. I am going to use my 'Assign to Group' process which is a POC for assigning tasks to Active Directory groups and Workspace roles and allowing a single user to take ownership of said task via the 'Claim' action. FYI, assigning tasks to Active Directory groups does not currently put a task on the worklist of every user within the group, this is a bug that will be resolved with Service Pack 1, due out by the end of the year or so:
Now, in staying true to the theme of this blog, I currently have no idea how to do what we are trying to do with the SmartObject... I see two SmartObject-centric options in Visual Studio - there is the 'SmartObject Event' available in the toolbox:
and the 'SmartObject Association' icon in the upper-right corner of the canvas:
Which to use, and when? Well, let's just blunder on through and see what happens... we'll start with the 'SmartObject Event' by dragging it onto the canvas from the toolbox:
Right off the bat, the wizard's welcome screen tells us that we can use this wizard to 'call smart object methods' and 'bind process data to smart object method inputs'... sounds kind of like what we'd want to do. Maybe we can use this wizard to call the 'Create' method of our smart object, and pass in the branch name from a process data field? Sounds like a plan, lets cancel out of this wizard for now and add a process-level field called 'Branch' which will hard code to 'Chicago' for now:
Drag the SmartObject event out onto the canvas again and click 'Next' to begin the process. The first dialog prompts us for the smart object method that will be called in this event. We can use the object browser to drag out the 'Create' method of our standard information SmartObject:
Click 'Next' and it prompts us to specify values for the 'Create' method parameters. We only want to specify a value for 'Branch', because 'ID' is an autonumber (it will be automatically generated) so click the 'Branch' property and then click 'Assign'. Drag in our 'Branch' process level data field and click 'OK' to accept it:
Click 'Next' and it asks us to supply a data field for the 'ID' return value of the Create method... we don't really care about the ID, so just say 'Next' and 'Finish'. Hook up the default activity with our SmartObject event somewhere in the workflow process so we can see if it works:
Deploy everything and start up this workflow via the Workspace. So now how are we going to actually see the smart objects that have been created? That is where the reports come into play... open up the workspace and go to the 'Reports Designer'. Select the 'Reports' tab and then click 'Create Report'. Name the report 'Test' and make it a simple Tabular Report:
Click 'Next' and we are now presented with a huge array of data source categories to drill through. What we want is to drill through the Workflow Reports, Workflow Solutions, [project name], [process name] categories and select the process instances. After selecting the process, the right side of the screen will be populated with the possible data in that process. Why don't we see our SmartObject in there? Well, I think that is where that other SmartObject canvas tool comes into play... let's revisit our workflow, and click the 'SmartObject Association' tool in the upper right hand corner of the canvas:
What this wizard is asking us is what SmartObjects can be associated with this process and whether or not they should be created within the workflow and/or within reporting. we definitely want to create the one for reporting, so it will show up in the data sources as expected when creating our reports. Blow through this wizard, associating it with our smart object (shouldn't be any tough questions here) and re-deploy the process. Get back into our report data sources, and this time we see the smart object as an option:
Notice that you see 'StandardInformation' out there twice in my screen shot... that is because I forgot I had been playing around with a SmartObject named 'StandardInformation' previously, and while I had deleted it from my project, there is no way to delete it from the blackpearl server (being a neat freak, I really really hate this). I ended up creating another SmartObject named 'Standard Information' and re-associated everything to get it to work. Anyway, get through the rest of the report wizard and make sure you include the 'branch' property somewhere in your report, to prove that it is getting out there correctly. When you view your report, it should look something like this:
Despite the issues I had with the duplicate SmartObject, I think we've proved out our goals coming into this thing... Every workflow can be associated with that same SmartObject, allowing them all to have additional properties (Branch, in this case) associated with them for reporting purposes. I believe that this is something that would not be possible in K2 2003 without some custom queries against the K2 database, which probably is not a very fun thing to do. One weakness of K2 2003 that still exists in blackpearl is that these custom properties are still not available to display on the worklist web part. That would be an extremely useful feature that to the best of my knowledge is not available without doing some custom coding.
A key feature of workflows that needs to be implemented in any technical workflow solution is escalations, that is, redirecting a task to a different user or sending notifications to various system users if the task goes un-addressed for too long. Today, I'd like to take a look at how the K2 blackpearl product handles escalations in the following scenario:
We will create a simple task, called 'Escalating task', and put it on a specific users task list. If the task goes un-addressed after one minute, that owner of the task will be sent an e-mail reminder to address the task. If the task goes un-addressed for five minutes, then it will be moved from the current work list to a different user's work list.
Let's get started by adding a new Process to a workflow project (see previous posts on blackpearl if you need more direction on how to do this). Within this process, drag out a 'Default Client Event' wizard to the canvas. We won't make this event actually do anything important, but the wizard requires an action of some sort so simply make it navigate to my company's home page at www.claritycon.com:
Click 'Next' and specify that we would NOT like notification of the event (don't want to get our escalation e-mails mixed up with notification e-mails). Click 'Next' again and blow through the Action wizards and accept their default 'Task Completed' action recommendation, but do not create the line for this outcome (we aren't going to care what happens when the user closes this task, that isn't what we are testing here).
For the Destination User, specify someone that you can log in as to view their task list, just so we can be sure it is appearing as expected when we run this process. Finish the wizard, and our client event will appear within a new activity on the canvas. Within this activity, select the 'Activity Escalations' icon from the activity strip (it looks like a clock). This brings us to the Activity Escalations dialog, where we can specify as many different escalations as this activity requires. In our case, that will be two - the e-mail escalation and the redirect escalation. Lets define the e-mail escalation first by clicking the 'Add' button:
Click 'Next' and specify that we want an 'e-mail' action template:
Now the process is asking us for specifics on when this escalation should take place. We want it to happen one minute after the task has been assigned to the user, so select 'Escalate After' - 1 Minute:
Click 'Next' again and we are required to fill in some information about the e-mail we are sending out. It doesn't really matter who it is from or what it says, as long as it is directed to the owner of this task. That is accomplished by clicking the 'Destination User' as the recipient, which becomes available when you uncheck 'Specify'... or so I thought! Why the heck can't we select 'Destination user'? Bug! Bug! Bug! I screamed in my head, but no, this is by design and I will explain why... by default, only one instance of an activity is created that is owned by the server, and various destination users simply get links to it from their respective work lists. So telling it to send an e-mail to its 'destination user' is invalid at this point, because the task is technically global, and has no idea what destination users may be linking to it. To change this behavior so that the activity is created for the actual destination user, we need to re-visit the 'Destination Rule' on the activity strip (looks like a cluster of three people). After clicking the destination rule icon, immediately click 'Back' to get the welcome screen which allows us to run in advanced mode:
Click 'Next' after checking the advanced mode option and we are presented with a 'Destination Rule Options' screen... the default option here is 'Plan just once', which is not what we want. We need to specify one of the options under the 'Plan per destination' section to create the activity for each destination. I'm not yet sure what the difference is between the two options here, for our simple test I'm sure 'All at once' will be fine:
Click 'Next' and we get a custom dialog based on the selection we just made... in our case, it is asking for parameters on how to handle 'slots' (instances) of this activity. Since we are assigning the activity to one (and only one) user at a time, these options don't really apply to us so we can accept the defaults:
Now we get to the 'Destination Sets' dialog, which provides further customization on specifying who will be receiving the activity. Again, this is all overkill for our simple test, so just create one destination set named 'Default' and put our destination user into it:
Click 'Finish' and our activity is now setup to create specific instances for the destination users. Now, let's get back into our e-mail escalation and we see that this time, Destination User is enabled:
Finish the wizard and our e-mail escalation is complete. Now, lets create our redirect escalation by clicking 'Add' again on the Activity Escalations dialog:
Click 'Next' and this time, specify the 'Redirect' template:
Click 'Next' and specify that we want this to escalate after five minutes:
Click 'Next' and we need to now specify who to redirect the task to:
Click 'Finish' and then 'Finish' again to complete our escalation definitions. Deploy this process, and start it up via workspace. Visit the destination user's task list and notice the 'Escalating task':
After one minute, check the destination user's e-mail for the reminder. After five minutes, refresh the task list and note that the task no longer appears:
Log in as the escalation destination user and see that their work list now has the task:

We left off previously with a workflow that can automatically assign itself to an appropriate user after a set amount of time passes, effectively simulating a reoccurring workflow. There was one major weakness, however, and that is that the time used to determine when to escalate the activity is based on the number of days/hours/minutes since the activity was last completed. While this may be due to the design of the workflow, it lends itself to a good topic for a blog, and that is manually manipulating the code generated by blackpearl workflow items. What I want to do is modify the escalation rule that we defined previously to execute every five minutes to instead escalate at the top of the hour every hour, regardless of when the last process was closed by the user. Using our current rules, if the user closed the activity at 11:05am, it would escalate again at 12:05pm (if we made it escalate every hour). What I want it to do, is escalate at 12:00 no matter when during the 11 hour it was closed. Lets take a closer look at how we will accomplish this by opening the workflow we created in the previous post and opening its 'Activity Escalations' dialog:
In this dialog, we get the option to select an escalation and view its rule code:
Select this option, and we are presented with the Windows Workflow representation of this escalation rule:
We can double click the one code item in this workflow to bring us to its source code:
Right away, we can see the logic that sets the K2 Escalation rule equal to whatever values were entered into the escalation configuration dialog (The K2.Configuration items). Instead of pulling the values from the configuration, we'll calculate the values based on our business rule, that is, since it should always escalate at the top of the hour, we will set 'EscalateMinutes' equal to however many minutes are left in the current hour:
private void EscalateAfterRule_ExecuteCode(object sender, EventArgs e)
{
int EscalateDays = 0;
int EscalateHours = 0;
int EscalateMinutes = 60 - DateTime.Now.Minute;
int EscalateSeconds = 0;
int EscalateRepeatedTimes = K2.Configuration.EscalateRepeatedTimes;
K2.SetEscalationRule(EscalateDays, EscalateHours, EscalateMinutes, EscalateSeconds, EscalateRepeatedTimes);
}
Save the changes and deploy the workflow, start it up and make sure it appears in the service account's task list:
Now we'll have to wait until the top of the hour to see if it automatically escalates to my task list.
*** Time Passes ***
Bam! There it is in my task list, though oddly enough, the 'Event Start Date' is still the same as the original event start date. I assumed it would update to the time at the top of the hour when it was redirected, but that is a minor issue considering it actually arrived at the top of the hour.
One of the workflow tasks presented to me that required a proof of concept was to enable a workflow process that automatically assigns itself to the appropriate person annually, monthly, weekly or daily. As usual, I stumbled through a solution without actually doing any research on whether or not there is a 'correct' or 'recommended' way to do this, so please feel free to critique what I've done here or use it as a K2 blackpearl learning exercise in how not to do things. First things first, we need to create our process. Right click your workflow project and add a new process, I've aptly named mine 'AutomaticWorkflow':
What we want to do with our nice blank-slate workflow is define a client event that will put an appropriately named task onto a service account's task list which will escalate to the appropriate owner's task list at the correct time (after a year, month, week, whatever). The reason we assign it to the service account is simply so that the workflow task is alive and kicking, waiting for the appropriate time to become 'active' by assigning itself to a real user. We have to trust that the service account user will never go in and close, reassign or otherwise tamper with this activity while it is hanging around on their task list waiting to escalate. This workflow also needs logic to re-assign the workflow back to the service account when the 'real' user finishes it, resetting its internal escalation clock so it will escalate to the real user again at the appropriate time. Let's get started with this workflow by dragging out a 'Default Client Event' activity wizard from the toolbar to our blank canvas:
Doing so will begin the default client event wizard which will ask us most of the important questions about our little workflow. Click 'Next' to start the wizard, and we are presented with the 'Event Name and Forms' dialog. We don't really care what this event is doing or what it is named, so just make it a web page action that opens up, oh, I dunno, my company's homepage:
Click 'Next' and we can ignore the next dialog which I believe will e-mail the recipient of the activity if you check the box. Click 'Next' again and we come to the 'Configure Actions' dialog. This is where we specify the various things that the user can do to this workflow, which for our purposes we will only allow them to close it. This wizard has a handy feature that automatically adds the 'Complete Task' action if we don't specify any actions, so you can go ahead and click 'Next' without specifying anything, and answer 'yes' to the dialog that asks if it should create the default action:
We are then presented with the 'Configure Outcomes' screen, which details the various states the activity can be in. Since we chose to automatically create the default action, it ties a default 'Task Completed' outcome in to that action so we can go ahead and click 'Next' without fiddling with anything:
Now the wizard is asking for the 'Destination User' of this activity, which is the account to assign the activity to once it starts. We want to specify our blackpearl service account here, so that the activity is placed in its task list by default:
Click 'Next' and then 'Finish' to complete the wizard and your canvas should now look like this:
Connect the 'Start' box with our activity (rename the activity to 'Automatic Activity' so we can identify it when it appears on the task list) and connect the 'Task Completed' arrow back to the activity, like this:
What this means is that whenever a user completes this activity, it will revert back to its default state of being assigned to blackpearl service account with the escalation counter reset. Escalation counter, you say? What's up with that? Well, I was just about to get into that... an activity can have 'escalations' assigned to it, which mean that on or after a certain amount of time, the activity can perform a number of actions including being moved from the current destination user to a new destination user, effectively moving it from the current task list to another user's task list. To accomplish this, we need to run the 'Activity Escalations' dialog, which is available from the activity's 'activity strip', accessible by clicking the little dropdown arrow next to the activity's name:
In the activity strip, select the 'Activity Escalations' icon, which resembles a little clock. This brings up the following Escalations dialog:
One activity can have multiple escalations, but in our case, we only need one: Activate Activity. Click the 'Add' button and name the new escalation:
Click 'Next' and we are provided with a list of possible escalation actions. The action we want in this case is 'Redirect', which will redirect the activity to a different user:
Click 'Next' and we are presented with the 'Rule Details' dialog. This is where we specify when the activity should be escalated, and while we have a number of options here, for the purposes of this blog (where we demand results fast), we will make the activity escalate every 5 minutes:
Something to note here is that the time specified here is based on the tie time that the process started - so the way we have things set up, the process starts whenever the user closes the current activity. This can lead to some inaccuracies when trying to make annual/monthly/weekly repeating workflows, because if you set up the time to be every 7 days (weekly) expecting it to appear every Monday, if the user closes it once on a Tuesday then the next escalation of the workflow will be seven days from that Tuesday, which would be the following Tuesday. I didn't see any options to do things like we see in Outlook scheduler, like 'every week on Monday' or 'January 1st every year' which would be very useful (at least in the approach I am taking to the problem). Click 'Next' and it is now asking us for the 'Redirect Action', which is basically asking who do we assign this activity to when it escalates. In our case, this will be the actual user or group that needs to address the task, I've assigned it to myself because it will help prove that the escalation is working:
Click 'Finish' and then 'Finish' again to complete the escalation definition. We will now deploy the process to the server, and once that is complete, we'll need to use the Workspace to manually start up the process as we did not tie this process into any server events:
Immediately after starting the process, we should see it on the service account's task list:
Now, we will wait five minutes and we should see it removed from the service account's task list:
and added to the 'real' user's task list (note the user names at the top of the screen shots):
I will now complete the task:
which will remove it from my task list:
And put it back on the service account's task list:
We can wait another five minutes to prove that it will escalate to me again.
There you have it, a workflow that automatically assigns itself to a specific user at a somewhat specific time. I'd like to see more options on the escalation time spans, but the way we defined this workflow comes fairly close to addressing the business need. It looks as if there is a way to manually manipulate the code generated by the escalation, which is likely what I'd need to do to add more complex time spans to the escalation logic. We will take a look at that feature in my next post.
Technorati Tags:
LINQ,
Visual Studio,
C#
At the very beginning of my blogging days, I mentioned 'LINQ' as a technology that I was very interested in Stumbling Through... then I never mentioned it again. Well, never fear all you 'Stumbling Through' fans (yes, both of you), I haven't forgotten this interesting bit of technology and have decided to work it into our People - Addresses project that we started up for Stumbling Through WPF (take a look at my post history for more information on this project). Here is the plan:
We currently have three collections defined - People, Addresses and States. Addresses are children of People, while States are kind of a lookup table. What we'll do is load the records for these collections from xml files using LINQ, with the interesting caveat of including the full state name as a read-only property in the address object even though it resides in the States collection (the address object currently has only the state abbreviation). Let's get started by creating two xml files: People.xml and States.xml. Here is the data for each:
People.xml
<?xml version="1.0" encoding="utf-8" ?>
<People>
<Person FirstName="Test" LastName="Guy">
<Address City="Chicago" State="IL" />
<Address City="Mundelein" State="FL" />
<Address City="Bartlett" State="CA" />
</Person>
<Person FirstName="Another" LastName="One">
<Address City="Chicago" State="IL" />
<Address City="Mundelein" State="FL" />
<Address City="Lviv" State="CA" />
</Person>
<Person FirstName="Someone" LastName="Else">
<Address City="Huntley" State="IL" />
<Address City="Addison" State="IL" />
<Address City="Bartlett" State="CA" />
</Person>
</People>
States.xml
<?xml version="1.0" encoding="utf-8" ?>
<States>
<State Name="Illinois" Abbreviated="IL"/>
<State Name="Florida" Abbreviated="FL"/>
<State Name="California" Abbreviated="CA"/>
</States>
We'll include these in our 'Stumbling Through' project under a 'Data' folder. Make sure that each file has the 'Copy if New'flag set and Build Action is 'None', so it will be copied into a data folder relative to the executable (it'll make it easier to find the data at run-time). Now I want to modify the 'Load' methods of each collection to pull their data from these XML files instead of inserting hard-coded items. We'll start with 'StateCollection.LoadAll', as it will probably be the simplest. Here is the full code for the StateCollection class, I'll explain the 'LoadAll' method afterwards:
using System.Collections.ObjectModel;
using System.Linq;
using System.Xml.Linq;
namespace StumblingThroughWPFPartI
{
public class StateCollection : ObservableCollection<State>
{
public void LoadAll()
{
this.Clear();
XElement xmlElement = XElement.Load("Data/States.xml");
foreach (State state in
from xmlState in xmlElement.Descendants("State")
select new State(xmlState.Attribute("Abbreviated").Value, xmlState.Attribute("Name").Value)
)
{
this.Add(state);
}
}
}
}
Wow, that is some crazy syntax in the 'LoadAll' method... what does it mean? Firstly, we clear any items in the the list. Then, we load up an XElement (Xml.Linq namespace) with the data in our external xml file. We then iterate through each state identified in the xml file, instantiate a state object using the element's attribute values in the constructor, and add the state instance to our collection.
Something else interesting with the LINQ syntax is that if we wanted to populate our State object properties after instantiating it (that is, there was an empty constructor), we could substitute the following for our previous 'foreach' statement:
foreach (State state in
from xmlState in xmlElement.Descendants("State")
select new State()
{
Abbreviated = xmlState.Attribute("Abbreviated").Value,
Name = xmlState.Attribute("Name").Value
}
)
This instantiates the State object, and then sets its 'Abbreviation' and 'Name' properties all in the LINQ statement!
I am very impressed with LINQ so far, even if the syntax is a little confusing (why 'from' element 'in' source? Just to avoid confusion with 'for'?). Let's move on to our 'PersonCollection.LoadAll', which should be just as simple:
using System.Collections.ObjectModel;
using System.Linq;
using System.Xml.Linq;
namespace StumblingThroughWPFPartI
{
public class PersonCollection : ObservableCollection<Person>
{
public void LoadAll()
{
this.Clear();
XElement xmlElement = XElement.Load("Data/People.xml");
foreach (Person person in
from xmlPerson in xmlElement.Descendants("Person")
select new Person(xmlPerson.Attribute("FirstName").Value, xmlPerson.Attribute("LastName").Value)
)
{
this.Add(person);
}
}
}
}
Nothing new here, just using the 'Person' object and xml data. Let's see how 'AddressCollection.LoadByPerson' will work out:
using System.Collections.ObjectModel;
using System.Linq;
using System.Xml.Linq;
using System.Collections.Generic;
namespace StumblingThroughWPFPartI
{
public class AddressCollection : ObservableCollection<Address>
{
public void LoadByPerson(Person parent)
{
this.Clear();
XElement xmlElement = XElement.Load("Data/People.xml");
IEnumerable<XElement> xmlParentElement = from item in xmlElement.Descendants("Person")
where item.Attribute("FirstName").Value.Equals(parent.FirstName)
where item.Attribute("LastName").Value.Equals(parent.LastName)
select item;
xmlElement = xmlParentElement.First();
foreach (Address address in
from xmlAddress in xmlElement.Descendants("Address")
select new Address(xmlAddress.Attribute("City").Value, xmlAddress.Attribute("State").Value)
)
{
this.Add(address);
}
}
}
}
All right, things got a little hairy here. After clearing the list, we need to first find the parent element in the xml data because we are loading all addresses for a given person. So I load all people, then set an enumerable list (the default return type for a LINQ query) equal to the person element that has both a first and last name equal to the first and last name of the parent person (obviously, we should use a better key here but we'll take what we got for now). We then set our element equal to the first item in said enumerable list, which should have one and only one item in it if we found our parent correctly. Once we get the parent, then the rest of the LINQ stuff looks like what we did for the other load methods.
So far, everything has gone pretty smoothly, but we really haven't tried to do anything complicated that utilizes one of the features of LINQ that I find most intriguing. Let's remedy that by combining our StateCollection object with the xml query used to load an address collection. I want to grab the state name based on the state abbreviation, and stick it into the address as a read-only property. We'll first need to modify our Address class' fields, constructor and properties to contain a 'StateName' value (read only property). Here is the code for the new Address class:
using System;
using System.ComponentModel;
namespace StumblingThroughWPFPartI
{
public class Address : INotifyPropertyChanged, IDataErrorInfo
{
String _city;
String _state;
String _stateName;
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(System.ComponentModel.PropertyChangedEventArgs e)
{
PropertyChanged(this, e);
}
public Address(String city, String state, String stateName)
{
_city = city;
_state = state;
_stateName = stateName;
}
public String City
{
get
{
return _city;
}
set
{
_city = value;
}
}
public String State
{
get
{
return _state;
}
set
{
_state = value;
}
}
public String StateName
{
get
{
return _stateName;
}
}
public string Error
{
get
{
return null;
}
}
public string this[string name]
{
get
{
string result = null;
if (name.Equals("City"))
{
if (this._city.Trim().Length == 0)
result = "Please specify a city";
}
else if (name.Equals("State"))
{
if (this._state.Trim().Length == 0)
result = "Please specify a state";
}
return result;
}
}
}
}
With that out of the way, our code will now no longer compile because the 'AddressCollection.LoadByPerson' LINQ statement that instantiates the Address object now requires the 'StateName' parameter in its constructor. How do we get it? Well, here is the code, which I will explain afterwards:
public void LoadByPerson(Person parent)
{
this.Clear();
StateCollection allStates = new StateCollection();
allStates.LoadAll();
XElement xmlElement = XElement.Load("Data/People.xml");
IEnumerable<XElement> xmlParentElement = from item in xmlElement.Descendants("Person")
where item.Attribute("FirstName").Value.Equals(parent.FirstName)
where item.Attribute("LastName").Value.Equals(parent.LastName)
select item;
xmlElement = xmlParentElement.First();
foreach (Address address in
from xmlAddress in xmlElement.Descendants("Address")
join state in allStates on address.Attribute("State").Value equals state.Abbreviation
select new Address(xmlAddress.Attribute("City").Value, xmlAddress.Attribute("State").Value, state.Name)
)
{
this.Add(address);
}
}
What we do now after clearing the list, is load all of our states into a collection utilizing the methods we already have in place. We then get the parent 'Person' element same as before. Now, when we are looping through each address to instantiate an object for it, we join to our States collection by matching the 'State' attribute in the xml of the address to the 'Abbreviated' property of the state objects. After doing so, the 'state' LINQ element will be the state with the addresses abbreviation, and we can use its Name to pass along to our address constructor. To prove whether this is working, lets modify our front-end bindings so that the list box that used to be displaying the address' 'City' is now displaying its 'StateName'. Run the application and we get... an error? What the heck? This worked fine in a different project, but now I am getting:
Argument '3': cannot convert from 'lambda expression' to 'System.Func<System.Xml.Linq.XElement,string>'
Well now, let's try and figure out what is going wrong here. It is clearly an issue with our join, but what? Oh, duh, my bad... it should join on xmlAddress.Attribute("State"), not address.Attribute("State")... address is our object instance, it doesn't have an 'Attribute' property. Could have given me better feedback than that! Change our foreach to this:
foreach (Address address in
from xmlAddress in xmlElement.Descendants("Address")
join state in allStates
on xmlAddress.Attribute("State").Value equals state.Abbreviation
select new Address(xmlAddress.Attribute("City").Value, xmlAddress.Attribute("State").Value, state.Name)
)
Run the application and we get this:
Now THAT'S what I'm talking about! This sort of functionality is nothing new to object-oriented programming, but never before has it been so integrated into the language and provided such short and intellisense-enabled coding. Including custom collections in a query on XML (that could have just as easily been a database, by the way) is very powerful and can help streamline many collection-oriented architectures with various data sources which probably makes up a good chunk of the architectures out there. I'm sure LINQ can be useful in any other situation as well, maybe not as a replacement for SQL (though definitely can replace XPATH for THIS developer), but at least as a common query language/routine for inter-object queries and interfaces to business objects. I hope I impressed some of the basics of LINQ to you while giving you a real-world (albeit simple) example of its use. If not, well, I hope my stumbling through i