|
If you've ever used Windows Workflow Foundation then you probably know that one of the most powerful features of WF is that you can track pretty much every single thing that takes place within a workflow with an excruciating amount of detail. This is especially handy in state machine workflows for being able to track when state transitions occur. Also, the ability to create your own tracking events (User Events) adds even more power to the already extremely useful system.
Out of the box, you can get a tracking service that logs the events to a SQL Server database. There are also code samples in the SDK that show you how to log all workflow activity to the console. I decided that I wanted to be able to bind my GUI to the list of events that have taken place throughout the lifetime of a given workflow instance. To do this, I wrote my own custom Workflow Tracking Service and added it to the workflow runtime instance as shown below:
_trackService = new ContinuousTrackingService();
_wfRuntime.AddService(_trackService);
Once the tracking service is in place, you're pretty much done. The WF runtime takes care of calling your service, obtaining a tracking channel, and trackintg things according to the tracking profile you provide. In my case, I wrote a channel that responds to the Send method call on the base tracking channel implementation, and add the tracking event to a published ContinuousCollection.
Once the workflow instance has started, my service is asked for a channel and then any time any event that the workflow runtime thinks is significant that matches the service's tracking profile (e.g. does it track just user data, or just activity data, or only workflow data, or some combination of those?) the channel's Send method is called.
With that in place, I can bind a listbox in my UI to a history of workflow tracking events like this:
_historyListBox.DataContext = _trackingService.TrackingLog;
And then really all I need to do is create a couple of data templates to display the right type of tracking entry (these are basic toss-outs that just make sure the system is working, your templates would obviously be much nicer looking):
<DataTemplate x:Key="userTrackingItemTemplate"
DataType="{x:Type tracking:UserTrackingRecord}">
<StackPanel Orientation="Horizontal">
<TextBlock Margin="0,0,8,0" Text="User Track:"/>
<TextBlock Text="{Binding UserData}"/>
<TextBlock Text="{Binding ActivityType}"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="workflowTrackingItemTemplate"
DataType="{x:Type tracking:WorkflowTrackingRecord}">
<StackPanel Orientation="Horizontal">
<TextBlock Margin="0,0,8,0" Text="WF Status:" />
<TextBlock Text="{Binding TrackingWorkflowEvent}"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="activityTrackingItemTemplate"
DataType="{x:Type tracking:ActivityTrackingRecord}">
<StackPanel Orientation="Horizontal">
<TextBlock Margin="0,0,8,0" Text="Activity Type:" />
<TextBlock Text="{Binding ActivityType}" Margin="0,0,8,0"/>
<TextBlock Margin="0,0,8,0" Text="Status:" />
<TextBlock Text="{Binding ExecutionStatus}"/>
</StackPanel>
</DataTemplate>
With WPF, if you want a list box to dynamically choose which data template it is going to display based on some information contained within the row being displayed, then you need to create your own DataTemplateSelector. Here is the one I used:
public class TrackingLogDataTemplateSelector : DataTemplateSelector
{
public override System.Windows.DataTemplate SelectTemplate(object item,
System.Windows.DependencyObject container)
{
if (item is UserTrackingRecord)
return Application.Current.MainWindow.FindResource(
"userTrackingItemTemplate") as DataTemplate;
else if (item is WorkflowTrackingRecord)
return Application.Current.MainWindow.FindResource(
"workflowTrackingItemTemplate") as DataTemplate;
else if (item is ActivityTrackingRecord)
return Application.Current.MainWindow.FindResource(
"activityTrackingItemTemplate") as DataTemplate;
else
return base.SelectTemplate(item, container);
}
}
Even cooler than just binding my GUI to the entire tracking log is the ability to write continuous queries against the tracking log and then bind the GUI to the dynamically updated result set from those queries. Take this query, for example:
ContinuousCollection<TrackingRecord> filteredTrackingLog =
from trackingRecord in _trackingService.TrackingLog
where trackingRecord.EventDateTime >= targetTime
select trackingRecord;
This query retrieves all events that took place within the workflow after a given target time. The possibilities for using queries against the tracking log are limited only by your imagination and the information contained within the tracking records.
So that's about it. If you like this idea, let me know and I will put the source code to the ContinuousTrackingService up on the CLINQ CodePlex site.