(WF4) Visual Workflow Tracking and WorkflowInspectionServices

Over time a few folks have asked on the forum for information about writing rehosted visual workflow debugger applications (a.k.a. workflow simulator, a.k.a. visual workflow tracking…).

Many of these folks have already seen the Visual Workflow Tracking sample (MSDN), and even read at least one of Kushal’s related blog posts (“Debugging in Workflow 4.0” , “VisualWorkflowTracking aka Workflow Simulator”, and “Visual Workflow Tracking with Step Service”). But since it’s still a reasonably popular subject to ask questions on it seems like a good time to rehash it, and add a little information on very closely related subjects.

 

1 – General Limitations

At first glance (at the pictures) the Visual Workflow Tracking samples might appear to show a debugging experience extremely similar to the workflow debugging experience Visual Studio. Now actually it is different, and while it is true that the two scenarios rely on some of the same code, one significant difference to be aware of is that Visual Studio is literally debugging the activities as you run because it has information about which activity is executing gained because it is attaching as a debugger. This is immediately different from the Visual Workflow Tracking sample which is working based on a completely different source of information about workflow execution, namely data from the Workflow 4.0 ‘Tracking‘ feature (for a very quick intro on the Tracking feature see here, but please note that it refers to Beta 1 of .Net 4.0, not the final release. Also see the MSDN description which refers to the actual release). This means you might not be able to easily implement every debugging feature you are familiar with such as breakpoints, or if you can implement them that they may end up needing to behave slightly different to the Visual Studio behavior.

Now most parts of the sample code are useful no matter you get information about the currently executing state of the workflow – if you get creative it could be something completely different from workflow tracking. However, there is one more limitation of the sample code, which many people will want to overcome. The sample itself contains a workflow which executed live and in the same process as the visual workflow tracker. In many cases customers are interested in doing something which is not the same, what they actually want to do is allow a) visualizing a workflow which ran in a different process (e.g. on a server machine) or b) replay of a workflow which finished executing some time in the past.

The impact of this is that instead of working from an Activity object, which is available as part of the TrackingRecord (on the derived subclasses) received by Participants, as the initial input into figuring out which activity is executing and needs displaying in the debugger, instead one must work with whatever Activity tracking data the TrackingParticipant saves to a database or log, which would typically include not the Activity object itself (this wouldn’t do what we want), instead it would usually include information such as an Activity ID.

 

2 - Activity IDs

OK, that’s great but what is an Activity ID? Well, it’s actually a special  string. Here’s a concrete example that I totally made up and might even look wrong:

“0.1.2.1.4”

The important part of the example which isn’t wrong is that an Activity ID is just a series of numbers – numbers of significance! The numbers are in fact a path in your Activity tree. This is basically just like a file system path, “C:\foo\bar\temp\readme.txt”, so it refers to a unique entity within a tree, and a given Activity ID always refers to a particular Activity as long as you are talking about the same exact workflow.

Yes, I need to clarify that. It has to be the same exact workflow (workflow structure, not workflow instance) for two reasons.

First reason:
Look at the Activity ID string again. “0.1.2.1.4” is really, when interpreted as a suffix instead of a complete string, just a relative path – it’s relative to some root activity.

Second reason:
Think about the numbers! How is “0” actually going to map to an Activity in a workflow tree? It’s going to work by position. If you change the number of activities in your workflow, or change the workflow’s Activity tree structure in any way, this is going to render all of your Activity IDs worthless.

A note to NativeActivity subclassers: how is position of activities in a workflow determined? It’s determined by the Children collection which you create in CacheMetadata. And by the way, if you’re not overriding CacheMetadata, you probably should be [why here, more or less].

So, is it safe to rely on there being a consistent scheme which maps Activity IDs to Activities? The answer is mostly yes, but the onus on you is to make sure you have the right workflow tree for applying this mapping. Or if you have anything special, such as a wrapper activity which ends up giving you different IDs at runtime and debugging time, you might need to do some fixups.

 

3 – Actually using an Activity ID to get an Activity

Intuitively the process should be simple, you can guess that this general outline should work:

  1. Figure out which XAML file the Activity ID is for. Hopefully you saved some data which lets you determine this!
  2. Load the XAML File. Now you have a root Activity.
  3. Somehow walk the Activity tree looking for a matching ID.

Sadly there is a bit of devil in the details. (Step 1 is fine, the issues are in 2 and 3.)

The main issue in Step 2 is that for our later requirements of getting XAML line numbers to be met, we will need to load the XAML file in a way which is compatible with SourceLocationProvider.CollectMapping(). As explained in the linked thread, the only way I know of which is actually compatible is calling WorkflowDesigner.Load(string) passing the name of a physical file on disk, and the file should also need the correct XamlDebuggerXmlReader attribute contained in its XAML. If you don’t have such a file available, or it’s missing the attribute a messy workaround to create one is to load your XAML temporarily into a WorkflowDesigner(), and then save it to disk with WorkflowDesigner.Save().

The main issue in Step 3 is that Activities don’t have IDs until the entire workflow tree is known and in some sense ‘prepared to run’. A workflow isn’t prepared to run until the root Activity is known. Consider this example:

Activity i = new If();
Activity s = new Sequence { Activities = { i } };

Which of these 2 activities is the root of the workflow? Trick question, neither! It turns out that the next line of code is

Activity p = new Parallel { Activities = { s } };

and this is followed by WorkflowInvoker.Invoke(p);

The point of this example is to show you that Activities’ location in a Workflow really is not known until the workflow to be run is also known.

The good news, now that I’ve beaten that point to death, is that we don’t actually have to run a workflow using WorkflowInvoker.Invoke in order to prepare it for running in the sense that it will have Activity IDs. And this is where WorkflowInspectionServices come in (if you can still remember the post title).

 

4 – The rather useful class WorkflowInspectionServices

WIS has two APIs which I would consider using here. Let’s start with number 1:

WorkflowInspectionServices.GetActivities(Activity) – this will give you a list (well, IEnumerable<>) of all the child activities of any workflow in the tree. We could start from the root and work our way down, but why do that when we have API number 2:

WorkflowInspectionServices.Resolve(Activity root, string id) – woohoo! Looks like this actually just straight-up solves the exact problem we were trying to solve, and saves us all the trouble of walking the tree ourselves. Great!

And yes, I believe (but am too lazy to verify right now) that either of these do put the Activity into the ‘nearly runnable’ state where all the metadata is cached, and Activity IDs are fully generated. (Assuming I am right so far, WorkflowInspectionServices.CacheMetadata() probably also has that effect.)

(Aside – you may have inferred from this that good things do not happen if you try to use a single Activity object in more than one workflow simultaneously. Yes indeed.)

 

5 – Back to visual workflow debugging – nearly done!

Yes, we’re really nearly done. We have the Activity, and the Activity ID. All we need now is a line number… wait… do we even need that? You know, maybe not. For purposes of highlighting an activity, that information could be good enough, if we are willing to write our own Adorner code. But you might want line numbers anyway… for displaying the XAML or something… or utilizing DebugService. The good news is at this point you really should be able to just follow the sample. (Kick up a constructive fuss if it doesn’t work!)

 

Please use the comments form below if you have questions, bouquets or brickbats!
Tim

Comments

  • Anonymous
    June 09, 2011
    Hi Tim, I'm not sure if my last blog comment got in or not; the short of it is - good post. I do have one question.  I'm using DebuggerService.InsertBreakPoint() to create a debug breakpoint.  According to one of Kushal's posts ((blogs.msdn.com/.../visualworkflowtrackingwithstepservice.aspx)), this is what is needed to get the little red circle debug adornment.  I find this does work if my activity's designer is baed on ActivityDesigner.  If, however, my activity's designer is based on WorkflowViewElement, then I don't see the little red circle adornment.  How do I get this to appear for activity's whose designer is based on WorkflowViewElement? Thanks, Notre

  • Anonymous
    June 14, 2011
    Hi Notre, The idea of WorkflowViewElement is it doesn't tie you to being an activity - you can use it for anything other than activities, which is great if the default behavior for activities just doesn't make sense for that element. The idea with ActivityDesigner on the other hand is that it includes lots of default behavior appropriate for activities, and makes creating a custom designer consistent with the built-in activity designers of VS very easy. So, if you want the default behavior that other activities share, like debugging adorners you should probably be subclassing ActivityDesigner - or you should be stuck with replicating any Activity specific functionality of the ActivityDesigner class you want, such as debugging adorners. (It makes sense to me for this to be Activity specific because debugging or tracking concept isn't defined for non-activities.) Tim

  • Anonymous
    June 15, 2011
    Hi Tim, Hmm, this is interesting.  I always though that WorkflowViewElement was used exclusively as the basis of an activity designer - for those designers where the activity author didn't want all the 'border' UI stuff that comes along with Activity Designer.  Can you give an example of where else WorkflowViewElement is used, other than as the basis of an activity designer?  I know one example, of sorts - the FlowDecision and FlowSwitch flowchart items are not true activities and use WorkflowViewElement as the basis of their 'activity' designers. I did manage to replicace the debugger adornments from ActivityDesigner on my WorkflowViewElement based designers. Thanks, Notre

  • Anonymous
    June 15, 2011
    The comment has been removed

  • Anonymous
    August 30, 2011
    @Notre (or Tim): is there any guidance you can point out/provide to replicate the adornment functionality? I used Reflector extensively to try to figure out 1) why stream-loaded XAML workflows (i.e.: not loaded from the filesystem) cannot be adorned, which led to trying to figure out 2) why adornments require SourceLocations (starting/ending rows/columns).  I concluded that the devs wrote the code in the context of the debugger, which is usually run against files, and coupled the concepts unnecessarily. I suppose if the adornments require visual styles only present in the Visual Studio (debugger) SKU, that's fine, but if I want to be able to re-host the designer without having to install Visual Studio, and without having my workflow persistence store be file-based, and still run/adorn workflows, I've got to do the adornments myself.