How to: Create a Custom Tracking Participant

Workflow tracking provides visibility into the status of workflow execution. The workflow runtime emits tracking records that describe workflow lifecycle events, activity lifecycle events, bookmark resumptions, and faults. These tracking records are consumed by tracking participants. Windows Workflow Foundation (WF) includes a standard tracking participant that writes tracking records as Event Tracing for Windows (ETW) events. If that does not meet your requirements, you can also write a custom tracking participant. This tutorial step describes how to create a custom tracking participant and tracking profile that capture the output of WriteLine activities so that they can be displayed to the user.

To create the custom tracking participant

  1. Right-click NumberGuessWorkflowHost in Solution Explorer and choose Add, Class. Type StatusTrackingParticipant into the Name box, and click Add.

  2. Add the following using (or Imports) statements at the top of the file with the other using (or Imports) statements.

    Imports System.Activities.Tracking  
    Imports System.IO  
    
    using System.Activities.Tracking;  
    using System.IO;  
    
  3. Modify the StatusTrackingParticipant class so that it inherits from TrackingParticipant.

    Public Class StatusTrackingParticipant  
        Inherits TrackingParticipant  
    
    End Class  
    
    class StatusTrackingParticipant : TrackingParticipant  
    {  
    }  
    
  4. Add the following Track method override. There are several different types of tracking records. We are interested in the output of WriteLine activities, which are contained in activity tracking records. If the TrackingRecord is an ActivityTrackingRecord for a WriteLine activity, the Text of the WriteLine is appended to a file named after the InstanceId of the workflow. In this tutorial, the file is saved to the current folder of the host application.

    Protected Overrides Sub Track(record As TrackingRecord, timeout As TimeSpan)  
        Dim asr As ActivityStateRecord = TryCast(record, ActivityStateRecord)  
    
        If Not asr Is Nothing Then  
            If asr.State = ActivityStates.Executing And _  
            asr.Activity.TypeName = "System.Activities.Statements.WriteLine" Then  
    
                'Append the WriteLine output to the tracking  
                'file for this instance.  
                Using writer As StreamWriter = File.AppendText(record.InstanceId.ToString())  
                    writer.WriteLine(asr.Arguments("Text"))  
                    writer.Close()  
                End Using  
            End If  
        End If  
    End Sub  
    
    protected override void Track(TrackingRecord record, TimeSpan timeout)  
    {  
        ActivityStateRecord asr = record as ActivityStateRecord;  
    
        if (asr != null)  
        {  
            if (asr.State == ActivityStates.Executing &&  
                asr.Activity.TypeName == "System.Activities.Statements.WriteLine")  
            {  
                // Append the WriteLine output to the tracking  
                // file for this instance  
                using (StreamWriter writer = File.AppendText(record.InstanceId.ToString()))  
                {  
                    writer.WriteLine(asr.Arguments["Text"]);  
                    writer.Close();  
                }  
            }  
        }  
    }  
    

    When no tracking profile is specified, the default tracking profile is used. When the default tracking profile is used, tracking records are emitted for all ActivityStates. Because we only need to capture the text one time during the lifecycle of the WriteLine activity, we only extract the text from the ActivityStates.Executing state. In To create the tracking profile and register the tracking participant, a tracking profile is created that specifies that only WriteLine ActivityStates.Executing tracking records are emitted.

To create the tracking profile and register the tracking participant

  1. Right-click WorkflowHostForm in Solution Explorer and choose View Code.

  2. Add the following using (or Imports) statement at the top of the file with the other using (or Imports) statements.

    Imports System.Activities.Tracking  
    
    using System.Activities.Tracking;  
    
  3. Add the following code to ConfigureWorkflowApplication just after the code that adds the StringWriter to the workflow extensions and before the workflow lifecycle handlers.

    'Add the custom tracking participant with a tracking profile  
    'that only emits tracking records for WriteLine activities.  
    Dim query As New ActivityStateQuery()  
    query.ActivityName = "WriteLine"  
    query.States.Add(ActivityStates.Executing)  
    query.Arguments.Add("Text")  
    
    Dim profile As New TrackingProfile()  
    profile.Queries.Add(query)  
    
    Dim stp As New StatusTrackingParticipant()  
    stp.TrackingProfile = profile  
    
    wfApp.Extensions.Add(stp)  
    
    // Add the custom tracking participant with a tracking profile  
    // that only emits tracking records for WriteLine activities.  
    StatusTrackingParticipant stp = new StatusTrackingParticipant  
    {  
        TrackingProfile = new TrackingProfile  
        {  
            Queries =
            {  
                new ActivityStateQuery  
                {  
                    ActivityName = "WriteLine",  
                    States = { ActivityStates.Executing },  
                    Arguments = { "Text" }  
                }  
            }  
        }  
    };  
    
    wfApp.Extensions.Add(stp);  
    

    This tracking profile specifies that only activity state records for WriteLine activities in the Executing state are emitted to the custom tracking participant.

    After adding the code, the start of ConfigureWorkflowApplication will look like the following example.

    Private Sub ConfigureWorkflowApplication(wfApp As WorkflowApplication)  
        'Configure the persistence store.  
        wfApp.InstanceStore = store  
    
        'Add a StringWriter to the extensions. This captures the output  
        'from the WriteLine activities so we can display it in the form.  
        Dim sw As New StringWriter()  
        wfApp.Extensions.Add(sw)  
    
        'Add the custom tracking participant with a tracking profile  
        'that only emits tracking records for WriteLine activities.  
        Dim query As New ActivityStateQuery()  
        query.ActivityName = "WriteLine"  
        query.States.Add(ActivityStates.Executing)  
        query.Arguments.Add("Text")  
    
        Dim profile As New TrackingProfile()  
        profile.Queries.Add(query)  
    
        Dim stp As New StatusTrackingParticipant()  
        stp.TrackingProfile = profile  
    
        wfApp.Extensions.Add(stp)  
    
        'Workflow lifecycle handlers...  
    
    private void ConfigureWorkflowApplication(WorkflowApplication wfApp)  
    {  
        // Configure the persistence store.  
        wfApp.InstanceStore = store;  
    
        // Add a StringWriter to the extensions. This captures the output  
        // from the WriteLine activities so we can display it in the form.  
        StringWriter sw = new StringWriter();  
        wfApp.Extensions.Add(sw);  
    
        // Add the custom tracking participant with a tracking profile  
        // that only emits tracking records for WriteLine activities.  
        StatusTrackingParticipant stp = new StatusTrackingParticipant  
        {  
            TrackingProfile = new TrackingProfile  
            {  
                Queries =
                {  
                    new ActivityStateQuery  
                    {  
                        ActivityName = "WriteLine",  
                        States = { ActivityStates.Executing },  
                        Arguments = { "Text" }  
                    }  
                }  
            }  
        };  
    
        wfApp.Extensions.Add(stp);  
    
        // Workflow lifecycle handlers...  
    

To display the tracking information

  1. Right-click WorkflowHostForm in Solution Explorer and choose View Code.

  2. In the InstanceId_SelectedIndexChanged handler, add the following code immediately after the code that clears the status window.

    'If there is tracking data for this workflow, display it  
    'in the status window.  
    If File.Exists(WorkflowInstanceId.ToString()) Then  
        Dim status As String = File.ReadAllText(WorkflowInstanceId.ToString())  
        UpdateStatus(status)  
    End If  
    
    // If there is tracking data for this workflow, display it  
    // in the status window.  
    if (File.Exists(WorkflowInstanceId.ToString()))  
    {  
        string status = File.ReadAllText(WorkflowInstanceId.ToString());  
        UpdateStatus(status);  
    }  
    

    When a new workflow is selected in the workflow list, the tracking records for that workflow are loaded and displayed in the status window. The following example is the completed InstanceId_SelectedIndexChanged handler.

    Private Sub InstanceId_SelectedIndexChanged(sender As Object, e As EventArgs) Handles InstanceId.SelectedIndexChanged  
        If InstanceId.SelectedIndex = -1 Then  
            Return  
        End If  
    
        'Clear the status window.  
        WorkflowStatus.Clear()  
    
        'If there is tracking data for this workflow, display it  
        'in the status window.  
        If File.Exists(WorkflowInstanceId.ToString()) Then  
            Dim status As String = File.ReadAllText(WorkflowInstanceId.ToString())  
            UpdateStatus(status)  
        End If  
    
        'Get the workflow version and display it.  
        'If the workflow is just starting then this info will not  
        'be available in the persistence store so do not try and retrieve it.  
        If Not WorkflowStarting Then  
            Dim instance As WorkflowApplicationInstance = _  
                WorkflowApplication.GetInstance(WorkflowInstanceId, store)  
    
            WorkflowVersion.Text = _  
                WorkflowVersionMap.GetIdentityDescription(instance.DefinitionIdentity)  
    
            'Unload the instance.  
            instance.Abandon()  
        End If  
    End Sub  
    
    private void InstanceId_SelectedIndexChanged(object sender, EventArgs e)  
    {  
        if (InstanceId.SelectedIndex == -1)  
        {  
            return;  
        }  
    
        // Clear the status window.  
        WorkflowStatus.Clear();  
    
        // If there is tracking data for this workflow, display it  
        // in the status window.  
        if (File.Exists(WorkflowInstanceId.ToString()))  
        {  
            string status = File.ReadAllText(WorkflowInstanceId.ToString());  
            UpdateStatus(status);  
        }  
    
        // Get the workflow version and display it.  
        // If the workflow is just starting then this info will not  
        // be available in the persistence store so do not try and retrieve it.  
        if (!WorkflowStarting)  
        {  
            WorkflowApplicationInstance instance =  
                WorkflowApplication.GetInstance(this.WorkflowInstanceId, store);  
    
            WorkflowVersion.Text =  
                WorkflowVersionMap.GetIdentityDescription(instance.DefinitionIdentity);  
    
            // Unload the instance.  
            instance.Abandon();  
        }  
    }  
    

To build and run the application

  1. Press Ctrl+Shift+B to build the application.

  2. Press Ctrl+F5 to start the application.

  3. Select a range for the guessing game and the type of workflow to start, and click New Game. Enter a guess in the Guess box and click Go to submit your guess. Note that the status of the workflow is displayed in the status window. This output is captured from the WriteLine activities. Switch to a different workflow by selecting one from the Workflow Instance Id combo box and note that the status of the current workflow is removed. Switch back to the previous workflow and note that the status is restored, similar to the following example.

    Note

    If you switch to a workflow that was started before tracking was enabled no status is displayed. However if you make additional guesses, their status is saved because tracking is now enabled.

    Please enter a number between 1 and 10
    Your guess is too high.
    Please enter a number between 1 and 10
    

    Note

    This information is useful for determining the range of the random number, but it does not contain any information about what guesses have been previously made. This information is in the next step, How to: Host Multiple Versions of a Workflow Side-by-Side.

    Make a note of the workflow instance id, and play the game through to its completion.

  4. Open Windows Explorer and navigate to the NumberGuessWorkflowHost\bin\debug folder (or bin\release depending on your project settings). Note that in addition to the project executable files there are files with guid filenames. Identify the one that corresponds to the workflow instance id from the completed workflow in the previous step and open it in Notepad. The tracking information contains information similar to the following.

    Please enter a number between 1 and 10
    Your guess is too high.
    Please enter a number between 1 and 10
    Your guess is too high.
    Please enter a number between 1 and 10
    

    In addition to the absence of the user's guesses, this tracking data does not contain information about the final guess of the workflow. That is because the tracking information consists only of the WriteLine output from the workflow, and the final message that is displayed is done so from the Completed handler after the workflow completes. In next step of the tutorial, How to: Host Multiple Versions of a Workflow Side-by-Side, the existing WriteLine activities are modified to display the user's guesses, and an additional WriteLine activity is added that displays the final results. After these changes are integrated, How to: Host Multiple Versions of a Workflow Side-by-Side demonstrates how to host multiple versions of a workflow at the same time.