Geeks With Blogs

Josh Reuben
Workflow 4 State Machine – Distributed Tracking Visualization
 
·        Disclaimer: This post requires an understanding of WF 4.0.1 , WCF and WPF.
·        Before I explain the how, look at the following image for the what: I am debugging a REMOTE workflow STATEMACHINE instance, and from tracking information sent from server, I am able to highlight the current state in my client!
 
 
Visual Workflow Tracking Sample – a Big Ball of Mud
·        Workflow 4 provides the System.Activities.Presentation.WorkflowDesigner class which encapsulates a WPF control for displaying a visualization of Workflow XAML.
·        If you download the WF 4 samples from http://www.microsoft.com/downloads/en/details.aspx?FamilyID=35ec8682-d5fd-4bc3-a51a-d8ad115a8792&displaylang=en  and navigate to the \WF_WCF_Samples\WF\Application\VisualWorkflowTracking folder, you will find a toy example of using this control.
·        I say toy because it is a single tier, Big Ball of Mud – (see http://en.wikipedia.org/wiki/Big_ball_of_mud ). All the code is tangled up in the code-behind of a WPF UserControl (which is written like a WinForm control …).
·        In any real world app, the Workflow would run hosted in the server and the visualization would of course run in the client – a distributed app.
·        This would require tracking information to be serialized and sent over the wire whenever a server-side TrackingParticipent raised an event.
·        In order for the Server to send add-hoc messages to the client, I opted for a PubSub (http://en.wikipedia.org/wiki/PubSub ) architecture – I leveraged Yuval Loewy's ServiceModelEx – downloadable from the IDesign site: http://idesign.net/idesign/DesktopDefault.aspx?tabindex=5&tabid=11
 
 
Solution Structure
·        The Solution should consist of the following projects:
·        1) A Class Library containing the infrastructure
·        2) A Class library containing the Workflow XAML definitions + any custom activity classes
·        3) A Console App for hosting the PubSub service
·        4) A Console App for hosting the Workflow and publishing tracking events
·        5) A Windows Application for hosting the WorkflowDesigner and subscribing to tracking events
·        Note: for demo purposes I used console apps – I would recommend hosting under AppFabric.
·        Note: see my previous blog post on how to configure these projects to run CLR 4.0.1 - http://geekswithblogs.net/JoshReuben/archive/2011/05/30/workflow-4.0.1-statemachine---youve-got-to-work-to-make.aspx - ITS MANDATORY FOR WF STATEMACHINES.
 
DataEvents
·        The first step is to define the notification events that will bubble up from the TrackingParticipant and be published from the Workflow Runtime host.
 
·        I admit here I took a shortcut – My EventArgs derived classes are also DataContract classes. It would be trivial to separate this out to DTO classes using AutoMapper (http://automapper.codeplex.com ), but this is not relevant to the kernel of this scenario. I do recommend that you do this for correct design.
·        These should be serializable via DataContractSerializer – this requires a slight finickery, as the WorkflowDesigner is coupled to Activity and SourceLocation classes from the Workflow API, and these are not inherently serializable. (Share my pain of attempting to pass an Activity across the wire here - http://social.msdn.microsoft.com/Forums/en-US/wfprerelease/thread/b5f3baba-e94f-4db1-8b55-62a72e9ad5bb - the XamlXml zoo is poorly documented: XamlWriter, ActivityXamlServices, XamlXmlWriter, XamlServices etc.).
·        So anyway, whenever the TrackingParticipant Track overload received a TrackingRecord that was either an ActivityStateRecord or a StateMachineStateRecord, I wanted to bubble up an event and serialize a DataContract instance for publishing à hence TrackingEventArgs and StateChangeEventArgs classes (I will explain the different payloads further on).
·        I also wanted the Workflow Runtime host to publish a notification on Workflow instance start and completion / cancellation – hence TrackingBeginEventData and TrackingEndEventData – these are just empty DTOs – but in future, I may add some payload.
·        Here is TrackingEventArgs, in all its non- separation of concerns glory, taking a shortcut of mish-moshing DataContract and EventArgs (POC cringe!). the constructor is making sure that all serializable DataMember flagged properties are set.
    [DataContract]
    public class TrackingEventArgs : EventArgs
    {
        [DataMember]
        public string Record { getset; }
        [DataMember]
        public TimeSpan Timeout { getset; }
       
        [DataMember]
        public string ActivityId { getset; }
        [DataMember]
        public string ActivityState { getset; }
        [DataMember]
        public string DisplayName { getset; }
 
        private static string GetState(TrackingRecord trackingRecord)
        {
            
            var stateRecord = trackingRecord as ActivityStateRecord;
            return (stateRecord == null)? string.Empty :stateRecord.State;
            
        }
 
 
        public TrackingEventArgs(TrackingRecord trackingRecord, TimeSpan timeout, string activityId, string displayName)
        {
            if (trackingRecord != null) Record = trackingRecord.ToString();
            Timeout = timeout;
 
            ActivityId = activityId;
            DisplayName = displayName;
            ActivityState = GetState(trackingRecord);
 
        }
    }
 
 
PubSub
·        ServiceModelEx PubSub provides a decoupling layer of indirection, subscription and publication mediation, and manages subscription lists.
·        To Managing Transient Subscriptions – use ServiceModelEx ISubscriptionService – general purpose - does not specify callback contract. Derive from ISubscriptionService and specify the desired callback contract. Don’t add operations – uses SubscriptionManager<T> to maintain an internal shared dictionary of transient subscribers – populated via reflection. To subscribe to a specific event, pass event name. To subscribe to all events, pass null. Note: for publishing, need to mark callback contract with ServiceContractAttribute.
 
 
    [ServiceContract(CallbackContract = typeof(IMyEvents))]
    public interface IMySubscriptionService : ISubscriptionService
    { } 
 
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
    public class MySubscriptionService : SubscriptionManager<IMyEvents>, IMySubscriptionService
    { }
 
·        Publishing service - should support the same events contract as the subscribers. To use - Derive from PublishService<T> and use the derived static FireEvent(params object[] args) method to deliver messages to subscribers – it extracts method name from incoming message headers.
·        I have 4 publication events that come from the Workflow host (OnTrackingEvent, OnStateChangeEvent, OnWorkflowBeginEvent and OnWorkflowEndEvent) + 1 publication event (OnLoadWorkflowEvent) that will come from the client to kick things off and let the Workflow host know which workflow to load:
 
 
    [ServiceContract]
    public interface IMyEvents
    {
       
 
        [OperationContract(IsOneWay = true)]
        void OnLoadWorkflowEvent(string workflowName, Dictionary<string,string> activityNames);
 
        [OperationContract(IsOneWay = true)]
        void OnTrackingEvent(TrackingEventArgs trackingEventArgs);
 
        [OperationContract(IsOneWay = true)]
        void OnStateChangeEvent(StateChangeEventArgs stateChangeEventArgs);
 
        [OperationContract(IsOneWay = true)]
        void OnWorkflowBeginEvent(TrackingBeginEventData trackingBeginEventData);
 
        [OperationContract(IsOneWay = true)]
        void OnWorkflowEndEvent(TrackingEndEventData trackingEndEventData);
    }
 
 
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
    public class MyPublishService : PublishService<IMyEvents>, IMyEvents
    {
       
        public void OnTrackingEvent(TrackingEventArgs trackingEventArgs)
        {
            FireEvent(trackingEventArgs);
        }
 
        public void OnStateChangeEvent(StateChangeEventArgs stateChangeEventArgs)
        {
            FireEvent(stateChangeEventArgs);
        }
 
 
        public void OnWorkflowBeginEvent(TrackingBeginEventData trackingBeginEventData)
        {
            FireEvent(trackingBeginEventData);
        }
 
        public void OnWorkflowEndEvent(TrackingEndEventData trackingEndEventData)
        {
            FireEvent(trackingEndEventData);
        }
 
 
        public void OnLoadWorkflowEvent(string workflowName, Dictionary<stringstring> activityNames)
        {
            FireEvent(workflowName, activityNames);
        }
    }
 
WCF Proxy
·        Have you ever used Project > Add Service Reference to generate a WCF client proxy? Forget about it! I leveraged ServiceModelEx ClientBase<T> and DuplexClientBase<T> to internally leverage ChannelFactory:
·        EventsProxy allows code to publish to the PubSub ServiceBus. Its constructors such up the PubSub address and the rest is just calls to the typed channel.
    class EventsProxy : ClientBase<IMyEvents>, IMyEvents
    {
        public EventsProxy()
        { }
 
        public EventsProxy(string endpointConfigurationName)
            : base(endpointConfigurationName)
        { }
 
        public EventsProxy(string endpointConfigurationName, string remoteAddress)
            : base(endpointConfigurationName, remoteAddress)
        { }
 
        public EventsProxy(string endpointConfigurationName, EndpointAddress remoteAddress)
            : base(endpointConfigurationName, remoteAddress)
        { }
 
        public EventsProxy(Binding binding, EndpointAddress remoteAddress)
            : base(binding, remoteAddress)
        { }
 
       
        public void OnTrackingEvent(TrackingEventArgs trackingEventArgs)
        {
            Channel.OnTrackingEvent(trackingEventArgs);
        }
 
        public void OnStateChangeEvent(StateChangeEventArgs stateChangeEventArgs)
        {
            Channel.OnStateChangeEvent(stateChangeEventArgs);
        }
 
        public void OnLoadWorkflowEvent(string workflowName, Dictionary<stringstring> activityNames)
        {
            Channel.OnLoadWorkflowEvent(workflowName, activityNames);
        }
 
        public void OnWorkflowEndEvent(TrackingEndEventData trackingEndEventData)
        {
            Channel.OnWorkflowEndEvent(trackingEndEventData);
        }
 
        public void OnWorkflowBeginEvent(TrackingBeginEventData trackingBeginEventData)
        {
            Channel.OnWorkflowBeginEvent(trackingBeginEventData);
        }
    }
 
·        SubscriptionServiceProxy allows code to subscribe to PubSub callbacks:
    class SubscriptionServiceProxy : DuplexClientBase<IMySubscriptionService>, IMySubscriptionService
    {
        public SubscriptionServiceProxy(InstanceContext inputInstance)
            : base(inputInstance)
        { }
 
        public SubscriptionServiceProxy(InstanceContext inputInstance, string endpointConfigurationName)
            : base(inputInstance, endpointConfigurationName)
        { }
 
        public SubscriptionServiceProxy(InstanceContext inputInstance, string endpointConfigurationName, string remoteAddress)
            : base(inputInstance, endpointConfigurationName, remoteAddress)
        { }
 
        public SubscriptionServiceProxy(InstanceContext inputInstance, string endpointConfigurationName, EndpointAddress remoteAddress)
            : base(inputInstance, endpointConfigurationName, remoteAddress)
        { }
 
        public SubscriptionServiceProxy(InstanceContext inputInstance, Binding binding, EndpointAddress remoteAddress)
            : base(inputInstance, binding, remoteAddress)
        { }
 
        public void Subscribe(string eventOperation)
        {
            Channel.Subscribe(eventOperation);
        }
        public void Unsubscribe(string eventOperation)
        {
            Channel.Unsubscribe(eventOperation);
        }
    }
·        These are wrapped by a "one size fits all" proxy:
 
 public class PubSubProxy : IMyEvents
    {
        readonly SubscriptionServiceProxy _subscriptionServiceProxy;
        private readonly EventsProxy _pubServiceProxy;
 
·        This proxy class functions as a singleton that retries calls to the PubSub. I'm not going to delve into Fault Contracts and Exception Shielding in this post.
 
        private PubSubProxy()
        {
            var context = new InstanceContext(this);
            _subscriptionServiceProxy = new SubscriptionServiceProxy(context);
            _pubServiceProxy = new EventsProxy();
 
        }
 
        public static PubSubProxy CreateInstance(int retryTimeout, int retryCount = 0)
        {
            var proxy = new PubSubProxy();
            var isConnected = false;
            while (!isConnected)
            {
                try
                {
                    proxy.Subscribe(string.Empty); // avoid persistent subscription
                    isConnected = true;
                }
                catch (EndpointNotFoundException)
                {
                    Thread.Sleep(retryTimeout);
                }
                retryCount--;
                if (retryCount == 0) break;
            }
 
            return (isConnected) ? proxy : null;
        }
 
·        It wraps subscription:
 
 
        public void Subscribe(string eventName)
        {
            _subscriptionServiceProxy.Subscribe(eventName);
        }
        public void Unsubscribe(string eventName)
        {
            _subscriptionServiceProxy.Unsubscribe(eventName);
        }
·        And it exposes a simple delegate pattern for invoking publication responses:
 
 
        public Action<TrackingEventArgs> HandleTrackingEvent { getset; }
        public void OnTrackingEvent(TrackingEventArgs trackingEventArgs)
        {
            if (HandleTrackingEvent != null) HandleTrackingEvent(trackingEventArgs);
        }
 
        public Action<StateChangeEventArgs> HandleStateChangeEvent { getset; }
        public void OnStateChangeEvent(StateChangeEventArgs stateChangeEventArgs)
        {
            if (HandleStateChangeEvent != null) HandleStateChangeEvent(stateChangeEventArgs);
        }
 
        public Action HandleWorkflowBeginEvent { getset; }
        public void OnWorkflowBeginEvent(TrackingBeginEventData t)
        {
            if (HandleWorkflowBeginEvent != null) HandleWorkflowBeginEvent();
        }
 
        public Action HandleWorkflowEndEvent { getset; }
        public void OnWorkflowEndEvent(TrackingEndEventData t)
        {
            if (HandleWorkflowEndEvent != null) HandleWorkflowEndEvent();
        }
 
        public Action<stringDictionary<stringstring>> HandleLoadWorkflowEvent { getset; }
        public void OnLoadWorkflowEvent(string workflowName, Dictionary<stringstring> activityNames)
        {
            if (HandleLoadWorkflowEvent != null) HandleLoadWorkflowEvent(workflowName, activityNames);
        }
·        So we have distributed communication framework for tracking event notifications.
Tracking Runtime Event Publication Pump
·        Next, we will dig into the Workflow runtime execution host and the visual tracking participant.
·        This core of this system works on Workflow Tracking to provide visibility into workflow execution– tracking instruments a workflow to emit records reflecting key events during the execution. You can read about it here: http://msdn.microsoft.com/en-us/library/ee513992.aspx
 
·        To participate in Tracking, the VisualTrackingParticipant class derives from TrackingParticipant and overrides the Track method.
·        The WorkflowExecutionHost class wraps an execution context for running a Workflow instance – it can be WorkflowInvoker or WorkflowApplication. At some point I may subclass WorkflowServiceHost and merge this stuff in, then create a custom WorkflowServiceHostFactory.
·         
 
·        the VisualTrackingParticipant class derives from TrackingParticipant. It exposes 2 events for bubbling up tracking info. It receives a dictionary of activity id's from the client (via the WorkflowRuntimeHost) when the client publishes a  LoadWorkflowEvent. VisualTrackingParticipant overrides the Track event, and based upon the TrackingRecord raises the appropriate event.
internal class VisualTrackingParticipant : TrackingParticipant
    {
        internal event EventHandler<TrackingEventArgs> TrackingRecordReceived;
        internal event EventHandler<StateChangeEventArgs> StateChangeReceived;
        // for client side debugging synchronization
        internal Dictionary<stringstring> ActivityIdToDisplayNameMap { getset; }
 
        protected override void Track(TrackingRecord record, TimeSpan timeout)
        {
·        Based on the TrackingRecord subtype, the appropriate event is raised:
·        for StateMachineStateRecord, I just pass the State DisplayName ;
            
                if (record is StateMachineStateRecord && StateChangeReceived != null)
                {
                    var stateMachineStateRecord = record as StateMachineStateRecord;
                    StateChangeReceived(thisnew StateChangeEventArgs(stateMachineStateRecord.StateName));
                    
                }
 
·        for ActivityStateRecord, I first retrieve its client mapping Activity id – we will discuss this soon.
 
                else if (record is ActivityStateRecord && TrackingRecordReceived != null)
                {
                    var activityStateRecord = record as ActivityStateRecord;
 
                    if (!activityStateRecord.Activity.TypeName.Contains("System.Activities.Expressions"))
                    {
 
                        var currentActivityId = activityStateRecord.Activity.Id;
                        if (ActivityIdToDisplayNameMap.ContainsKey(currentActivityId))
                        {
                            var displayName = ActivityIdToDisplayNameMap[currentActivityId];
                            TrackingRecordReceived(thisnew TrackingEventArgs(
                                                            record,
                                                            timeout,
                                                            currentActivityId, displayName
                                                            )
 
                                );
                        }
 
 
 
                    }
                }
·        The WorkflowExecutionHost exposes a RunWorkflow method which is called from PubSub subscription - a notification from the client. I pass in the workflow file name, and the pesky client activity id map (which will soon be explained). I should also pass as parameters the Workflow execution parameters and timeout – but this is trivial. I instantiate a WorkflowInvoker / WorkflowApplication from the root Activity of the Workflow definition. I then spin up a Task to publish a "begin" notification, invoke the Workflow , and then publish a workflow "completion" notification. (I would leverage tracking events for the completion event if using WorkflowApplication).
 
 
        public void RunWorkflow(string workflowFileName, Dictionary<string,string> activityIdToDisplayNameMap)
        {
            var runtimeRootActivity = ActivityXamlParser.GetRuntimeExecutionRoot(workflowFileName);
            var instance = new WorkflowInvoker(runtimeRootActivity);
            InitTracking(instance, activityIdToDisplayNameMap);
            Task.Factory.StartNew(() =>
                {
                    // publish  event
                    PublishInvokationBeginEvent();
 
                    //Invoking the Workflow Instance with Input Arguments and timeout
                    var wfParams = new Dictionary<stringobject> { { "decisionVar"true } };
 

                    var wfTimeout = new TimeSpan(1, 0, 0);

 
                    instance.Invoke(wfParams , wfTimeout );
 
                    // publish  event
                    PublishInvokationCompletionEvent();
                }
            );
        }
 
·        The WorkflowExecutionHost establishes a Tracking Event Pump – create a TrackingParticipant (in the real world use DI) and add it to the Runtime host extensions. any events coming out of the TrackingParticipant are then attached and used to publish PubSub notifications.
 
private void InitTracking(WorkflowInvoker instance, Dictionary<stringstring> activityIdToDisplayNameMap)
        {
            var visualTrackingParticipant = VisualTrackingParticipant.CreateInstance();
            visualTrackingParticipant.ActivityIdToDisplayNameMap = activityIdToDisplayNameMap;
            EstablishTrackingEventPump(visualTrackingParticipant);
            instance.Extensions.Add(visualTrackingParticipant);
        }
 
        private void EstablishTrackingEventPump(VisualTrackingParticipant visualTrackingParticipant)
        {
            //As the tracking events are received
            visualTrackingParticipant.StateChangeReceived
                += (trackingParticpant, stateChangeEventArgs) =>
                    {
                        var stateName = stateChangeEventArgs.StateName;
                        if (!string.IsNullOrEmpty(stateName))
                        {
                            Debug.WriteLine(String.Format("State Change Tracking Record Received: {0} ", stateName));
                        }
 
                        // pass activity in event
                        PublishStateChangeEvent(stateChangeEventArgs);
                    };
 
            visualTrackingParticipant.TrackingRecordReceived
                += (trackingParticpant, trackingEventArgs) =>
                {
                    var activityId = trackingEventArgs.ActivityId;
                    if (!string.IsNullOrEmpty(activityId))
                    {
                        Debug.WriteLine(String.Format("Activity Tracking Record Received for ActivityId: {0}, record: {1} ", activityId, trackingEventArgs.Record));
                    }
 
                    // pass activity in event
                    PublishTrackingEvent(trackingEventArgs);
 
 
                };
        }
The WorkflowExecutionHost bubbles up events for the PubSub
 
 
 
        public event EventHandler<TrackingEventArgs> TrackingEvent;
        public event EventHandler<StateChangeEventArgs> StateChangeEvent;
        public event EventHandler<TrackingEventArgs> InvokationBeginEvent;
        public event EventHandler<TrackingEventArgs> InvokationCompletionEvent;
 
        private void PublishStateChangeEvent(StateChangeEventArgs stateChangeEventArgs)
        {
            var handler = StateChangeEvent;
            if (handler != null) handler(this, stateChangeEventArgs);
        }
 
        private void PublishTrackingEvent(TrackingEventArgs trackingEventArgs)
        {
            var handler = TrackingEvent;
            if (handler != null) handler(this, trackingEventArgs);
        }
 
        private void PublishInvokationCompletionEvent()
        {
            var handler = InvokationCompletionEvent;
            if (handler != null) handler(thisnull);
        }
 
        private void PublishInvokationBeginEvent()
        {
            var handler = InvokationBeginEvent;
            if (handler != null) handler(thisnull);
        }
 
 
Debugger Management
·        So far we have seen our PubSub client / server infrastructure and our Workflow Runtime Host that bubbles up PubSub publications from Tracking Participation. All nice and good. But how does the WorkflowDesigner know which Activity to highlight?
·        This is where the Workflow API gets ugly: WorkflowDesigner exposes a DebugManagerView property – which implements System.Activities.Presentation.Debug.IDesignerDebugView. Before being able to break at an Activity, the DebugManagerView must register the SourceLocation of for the set of Activity objects it contains. It does this via the opaque System.Activities.Debugger.SourceLocationProvider class CollectMapping method, which creates a Dictionary of mappings of the root Activity and its descendants to their corresponding SourceLocation objects.
·        What is a SourceLocation?You would think thatit would be a pointer to a DependencyObject in the WorkflowDesigner.View WPF logical tree, but you would be disappointed. It is a class that contains the StartLine, EndLine, StartColumn (+ magic offsets!) and EndColumn of the Activity's position in the Workflow XAML XML file!
·        The following classes encapsulate this behavior:
 
·        The InstanceDebugger class is responsible for managing SourceLocation mappings - retrieving this dictionary and parsing the mapping for Activity Ids which can be used for correlation with Tracking events received from the server.
        private static Dictionary<objectSourceLocation> InitSourceLocationMapping(string workflowFileName, Activity designerRootActivity, Activity runtimeRootActivity)
        {
           var map = new Dictionary<objectSourceLocation>();
 
            SourceLocationProvider.CollectMapping(
                runtimeRootActivity,
                designerRootActivity,
                map,
                workflowFileName
                );
            return map;
 
        }
 
 
        internal static SourceLocation GetActivitySourceLocation(TrackingEventArgs trackingEventArgs)
        {
            var sourceLocation = _activityIdToSourceLocationMap
                .Where(kvp => kvp.Key == trackingEventArgs.ActivityId)
                .Select(kvp => kvp.Value)
                    .FirstOrDefault();
            return sourceLocation;
        }
 
 //  input designerRootActivity from client,   output published to server then to TrackingParticipant to filter tracking
        
        internal static Dictionary<stringstring> InitMappings(string workflowFileName, Activity designerRootActivity, ServiceManager designerServiceManager)
        {
            var runtimeRootActivity = ActivityXamlParser.GetRootRuntimeWorkflowElement(workflowFileName);
 
            //Mapping between the Object and Line No.
            var instanceSourceLocationMap = InitSourceLocationMapping(workflowFileName, designerRootActivity, runtimeRootActivity);
            _designerSourceLocationMap = InitSourceLocationMapping(workflowFileName, designerRootActivity, designerRootActivity);
 
            //Mapping between the Object and the Instance Id
            _activityIdToSourceLocationMap = BuildActivityIdToLocationMap(instanceSourceLocationMap);
 
            return BuildActivityIdToDisplayNameMap(instanceSourceLocationMap);
        }
·        The DesignerDebugger class is responsible for updating SourceLocation values retrieved from the InstanceDebugger:
 
// Notify the DebuggerService of the new sourceLocationMapping.
        // When rootInstance == null, it'll just reset the mapping.
        internal static void UpdateDebuggerViewLocations(IDesignerDebugView designerDebugView)
        {
            var debuggerService = designerDebugView as DebuggerService;
            if (debuggerService != null)
            {
                debuggerService.UpdateSourceLocations(InstanceDebugger.DesignerSourceLocationMap);
 
            }
        }
 

 

        internal static void RefreshDebuggerLocation(IDesignerDebugView designerDebugView, TrackingEventArgs trackingEventArgs, string workflowFilename = "")
        {
            designerDebugView.CurrentLocation  = InstanceDebugger.GetNewDebuggerLocation(trackingEventArgs, workflowFilename);
        }
 
 
·        The TrackingVisualizer class is just a façade over these 2 classes – a level of indirection
 
Workflow Designer Host Control
 
·        The last abstraction to take care of is the WorkflowDesignerHost UserControl, which wraps the WorkflowDesigner class.
 
 
·        Its markup contains a Grid to which we will add the WorkflowDesigner.View UIElement
 
  <Grid Name="RehostGrid" />
 
·        It exposes 3 properties for extracting data needed for initializing an Activity to SourceLocation map and for publishing a request to start a Workflow instance on the server:
 
        public Activity RootActivity { getprivate set; }
 
        public string WorkflowFileName { get { return _workflowFilename; } }
 
        public ServiceManager DesignerServiceManager
        {
            get { return _workflowDesigner.Context.Services; }
        }
 
·        It has a method for loading a Workflow definition from file to display and initializing its public properties:
 
 
        public void LoadWorkflowDesigner(string workflowFilename)
        {
            _workflowFilename = workflowFilename;
            (new DesignerMetadata()).Register();
 
            _workflowDesigner = new WorkflowDesigner();
            RehostGrid.Children.Clear();
            RehostGrid.Children.Add(_workflowDesigner.View);
            _workflowDesigner.Load(workflowFilename);
 
            RootActivity = DesignerHelper.GetWorkflowRoot(_workflowDesigner);
            _isStateMachine = (RootActivity is StateMachine);
        }
·        And it has a set of methods for responding to subscriptions – these are run under the Dispatcher (for WPF UI thread affinity when coming in from the subscription callback thread) and call into the TrackingVisualizer façade , passing in the WorkflowDesigner.DebugManagerView to update its current SourceLocation!
        public void SubscribeInstanceInvokationBeginEvent()
        {
            TrackingVisualizer.UpdateDebuggerViewLocations(_workflowDesigner.DebugManagerView);
        }
 
        public void SubscribeInstanceInvokationCompletionEvent()
        {
            Dispatcher.Invoke(
                DispatcherPriority.Render,
                (Action)(() =>                
                    TrackingVisualizer.RefreshDebuggerLocation(_workflowDesigner.DebugManagerView, _workflowFilename)
                    ));
        }
 
        public void SubscribeTrackingEvent(TrackingEventArgs trackingEventArgs)
        {
            Dispatcher.Invoke(
               DispatcherPriority.Render,
               (Action)(() =>
                   TrackingVisualizer.AddSourceLocation(trackingEventArgs)
                       ));
 
        }
 
·        And that completes the Infrastructure – now we will see how to use the API:
 
PubSub Host
·        In this console App, we setup our App.Config file System.ServiceModel\Services section to add service endpoints for the Publication and the Subscription contracts:
            <service name="Infra.PubSub.MyPublishService">
                <endpoint address="" binding="netTcpBinding" contract="Infra.PubSub.IMyEvents">
                    <identity>
                        <dns value="localhost"/>
                    </identity>
                </endpoint>
                <host>
                    <baseAddresses>
                        <add baseAddress="net.tcp://localhost:8742/Design_Time_Addresses/PubSubHost/MyPublishService/"/>
                    </baseAddresses>
                </host>
            </service>
                         <service name="Infra.PubSub.MySubscriptionService">
                                 <endpoint address="" binding="netTcpBinding" contract="Infra.PubSub.IMySubscriptionService">
                                          <identity>
                                                   <dns value="localhost"/>
                                          </identity>
                                  </endpoint>
                                  <host>
                                          <baseAddresses>
                                                  <add baseAddress="net.tcp://localhost:8743/Design_Time_Addresses/PubSubHost/MySubscriptionService/"/>
                                          </baseAddresses>
                                  </host>
                         </service>
·        In Program.Main, instantiate the PubSub services using ServiceModelEx ServiceHost<T>
        static void Main(string[] args)
        {
            Console.WriteLine("PubSub");
            var pubHost = new ServiceHost<MyPublishService>();
            var subHost = new ServiceHost<MySubscriptionService>();
            pubHost.EnableMetadataExchange(true);
            subHost.EnableMetadataExchange(true);
            
            pubHost.Open();
            subHost.Open();
            
            Console.WriteLine(pubHost.State);
            Console.WriteLine(subHost.State);
            
            Console.ReadLine();
        }
 
Workflow Runtime Host
·        In this console App, we setup our App.Config file System.ServiceModel\Client section to add client & callback endpoints for the Publication and the Subscription contracts:
        <endpoint address="net.tcp://localhost:8742/Design_Time_Addresses/PubSubHost/MyPublishService/" binding="netTcpBinding" contract="Infra.PubSub.IMyEvents"/>
                         <endpoint address="net.tcp://localhost:8743/Design_Time_Addresses/PubSubHost/MySubscriptionService/" binding="netTcpBinding" contract="Infra.PubSub.IMySubscriptionService"/>
 
 
·        In Program.Main, instantiate the PubSubProxy and the WorkflowExecutionHost, attach handlers for publishing events out of the WorkflowExecutionHost through the proxy, and subscribe to client events to start a Workflow instance.
 
    class Program
    {
        public static PubSubProxy Proxy { getprivate set; }
 
        static void Main(string[] args)
        {
            Console.WriteLine("Workflow Runtime");
            Proxy = PubSubProxy.CreateInstance(1000);
            var workflowExecutionHost = new WorkflowExecutionHost();
            HandlePublishingServerEvents(workflowExecutionHost);
            SubscribeToClientEvents(workflowExecutionHost);
            Console.ReadLine();
        }
 
        private static void SubscribeToClientEvents(WorkflowExecutionHost workflowExecutionHost)
        {
            Proxy.HandleLoadWorkflowEvent = (s, d) =>
            {
                workflowExecutionHost.RunWorkflow(s, d);
            };
        }
 
        private static void HandlePublishingServerEvents(WorkflowExecutionHost workflowExecutionHost)
        {
            workflowExecutionHost.InvokationBeginEvent += (s, te) => Proxy.Publish("OnWorkflowBeginEvent");
            workflowExecutionHost.TrackingEvent += (s, te) => Proxy.PublishTrackingEvent(te);
            workflowExecutionHost.StateChangeEvent += (s, se) => Proxy.PublishStateChangeEvent(se);
            workflowExecutionHost.InvokationCompletionEvent += (s, te) => Proxy.Publish("OnWorkflowEndEvent");
        }
    }
 
 
Debugger Visualizer Client
·        In this Windows App, we setup our App.Config file System.ServiceModel\Client section to add client & callback endpoints for the Publication and the Subscription contracts – this is exactly the same as for the Workflow Host Console app – see above.
·        In the WPF App.xaml.cs, instantiate the proxy:
 
public partial class App
    {
        public PubSubProxy Proxy { get;  private set; }
 
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);
            Proxy = PubSubProxy.CreateInstance(1000);
        }
    }
·        In the main Window.xaml, add a Menu and add a XAML namespace mapping + the WorkflowDesignerHost UserControl.
        <Menu x:Name="rootMenu" VerticalAlignment="Top" Grid.ColumnSpan="2">
            <MenuItem Header="_File" x:Name="fileMenu">
                <MenuItem Header="_Open Workflow..."  Click="OpenWorkflowClick"  />
                <MenuItem Header="_Open StateMachine..." Click="OpenStateMachineClick" />
                <MenuItem Header="Set Location"  Click="SetLocationClick" />
                <MenuItem Header="Select State"  Click="SelectStateClick" />
                <MenuItem Header="_Run Workflow..."  Click="RunLoadedWorkflowClick"  />
                <MenuItem Header="_Exit" Click="ExitClick"/>
            </MenuItem>
        </Menu>
 <CodeFlow:WorkflowDesignerHost x:Name="workflowDesignerHost" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
·        In the Window.xaml.cs code-behind, implement the MenuItem event handlers (You should leverage MVVM commands for a real world app, instead of code behind events):
 
 
        private void RunLoadedWorkflowClick(object sender, RoutedEventArgs e)
        {
            try
            {                
                PubSubClient.RunWorkflow(workflowDesignerHost);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "Error"MessageBoxButton.OK, MessageBoxImage.Error);
            }
        }
 
        private void ExitClick(object sender, RoutedEventArgs e)
        {
            Application.Current.Shutdown();
        }
 
        private void OpenWorkflowClick(object sender, RoutedEventArgs e)
        {
            _workflowFileName = "Workflow.xaml";
            workflowDesignerHost.LoadWorkflowDesigner(wdir + _workflowFileName);
        }
 
        private void OpenStateMachineClick(object sender, RoutedEventArgs e)
        {
            _workflowFileName = "StateMachineWorkflow.xaml";
            workflowDesignerHost.LoadWorkflowDesigner(wdir + _workflowFileName);
        }
·         RunLoadedWorkflowClick callsPubSubClient RunWorkflow(workflowDesignerHost) – implement PubSubClient to run a Workflow as follows:
·        Subscribe to server events that come down the PubSub – hook them up to WorkflowDesignerHost methods for updating the debugger.
·        Initialize an Activity to SourceLocation tracking map via the TrackingVisualizer façade
·        Publish a LoadWorkflowEvent, notifying the workflow runtime host of the workflow to load and the activity location map.
 
 
    internal static class PubSubClient
    {
        private static readonly PubSubProxy Proxy;
        
 
        static PubSubClient()
        {
            Proxy = ((App)Application.Current).Proxy;
            
        }
 
        internal static void RunWorkflow(WorkflowDesignerHost designerHost)
        {
            SubscribeToServerEvents(designerHost);
            var workflowFileName = designerHost.WorkflowFileName;
            var activityIdToDisplayNameMap = TrackingVisualizer.InitMappings(
                designerHost.WorkflowFileName,
                designerHost.RootActivity,
                designerHost.DesignerServiceManager);
 
            PublishClientEvents(workflowFileName, activityIdToDisplayNameMap);
 
       }
 
        private static void PublishClientEvents(string workflowFileName, Dictionary<stringstring> activityIdToDisplayNameMap)
        {
            Proxy.PublishLoadWorkflowEvent(workflowFileName, activityIdToDisplayNameMap);
        }
 
       
        private static void SubscribeToServerEvents(WorkflowDesignerHost designerHost)
        {
            Proxy.HandleWorkflowBeginEvent = () => designerHost.SubscribeInstanceInvokationBeginEvent();
            Proxy.HandleTrackingEvent = te => designerHost.SubscribeTrackingEvent (te);
            Proxy.HandleStateChangeEvent = se => designerHost.SubscribeStateChangeEvent(se);
            Proxy.HandleWorkflowEndEvent = () => designerHost.SubscribeInstanceInvokationCompletionEvent();
        }
    }
 
Test the Application
·        The infrastructure is complete.
·        Run the application. Select a Workflow to load – it displays in the Debugger. Execute it – the current Activity is highlighted.
·        Now try load a State Machine Workflow. Things do not go as expected! The StateMachine Activity is highlighted, then all of a sudden the WorkflowDesigner traverse down into a State and highlights its nested Delay Activity. The State is never highlighted, ruining the visual experience, as it jumps up & down the designer navigation breadcrumbs.
 
·        How to correct this behavior? Read on.
 
State: WF – WTF ?!?
·        It turns out SourceLocationProvider.CollectMapping does not support WF 4.0.1 States!
·        We are trying to get locations of States and highlight them in WorkflowDesigner.
·        CollectMapping is returning all other activity types, including those within states; but it does not return locations for states.
·        Investigation of State reveals it is not an Activity - http://msdn.microsoft.com/en-us/library/system.activities.statements.state.aspx - it inherits from Object ! BAH HUMBUG!!!
·        The state obviously has a location on the designer (I can see it in the XAML ViewState)
              <Statex:Name="__ReferenceID1"DisplayName="State2"sap:VirtualizedContainerService.HintSize="113.6,60.8">
               <sap:WorkflowViewStateService.ViewState>
                  <scg3:Dictionaryx:TypeArguments="x:String, x:Object">
                    <av:Pointx:Key="ShapeLocation">23.2,209.6</av:Point>
                    <av:Sizex:Key="ShapeSize">113.6,60.8</av:Size>
                    <x:Booleanx:Key="IsPinned">False</x:Boolean>
                  </scg3:Dictionary>
                </sap:WorkflowViewStateService.ViewState>
·        I can get States from designer using the ModelingService.I can retrieve the viewstate programatically - from here I can retrieve its ShapeLocation.
            var viewStateService = _workflowDesigner.Context.Services.GetService<ViewStateService>();
            var modelService = _workflowDesigner.Context.Services.GetService<ModelService>();
            var modelItems = modelService.Find(modelService.Root, typeof(State));
            var activities = WorkflowInspectionServices
                .GetActivities(_rootActivity)
                .Where(a => a.GetType().FullName == "System.Activities.Statements.InternalState");
 
            foreach (var modelItem in modelItems)
            {
                var state = modelItem.GetCurrentValue() asState;
                var stateId = activities
                    .Where(a => a.DisplayName == state.DisplayName)
                    .Select(a => a.Id)
                    .First();
                var viewState = viewStateService.RetrieveAllViewState(modelItem);
                var shapeLocation = (Point)viewState["ShapeLocation"] ;
            }
·       However, this is just a Point. How do I get a SourceLocation? (linestart, columnStart, lineEnd, columnEnd)?
·        Using reflection I can see that SourceLocationProvider.TryGetSourceLocation uses
AttachablePropertyServices.TryGetProperty<int>(obj, XamlDebuggerXmlReader.StartLineName, out startLine)
·        to get lineStart.
·       I can see that AttachablePropertyServices.CopyPropertiesTo gives me an array containing HintSize and ViewState, which dont have anything to do with SourceLocation fields! This approach is a dead end.
·        I couldn’t even use Snoop (http://snoopwpf.codeplex.com/ ) to see what was going in inside the WorkflowDesigner – CLR 4.0.1 caused it to hang!
·        Luckily I was able to leverage the Visual Studio WPF Visualizer to drill down into Visual Tree, until I found the ShapeBorder Border and the relevant nested TextBlock containing the State's DisplayName !
·    
    Here is the relevant dynamic XAML  Visual Tree, way down:
 
·        For State Machine Tracking Visualization, a work-around is in order:
·        As WorkflowDesigner.View is a WPF UIElement, I can use VisualTreeHelper – see http://krishnabhargav.blogspot.com/2008/10/visual-tree-helper-methods.html for generic recursive traversal method GetChildrenOfType<T>.
·        I can then reach into the View and manually set the style of the Border for the correct state:
privatevoid SelectState(string stateName)
        {
            var borders = GetChildrenOfType<Border>(RehostGrid).Where(b => b.Name == "ShapeBorder").ToList();
            foreach (var b in borders)
            {
                var isRoot = GetChildrenOfType<TextBlock>(b).Where(t => t.Text == "StateMachine").Any();
                var containsTargetState = GetChildrenOfType<TextBlock>(b).Where(t => t.Text == stateName).Any();
 
                if (!isRoot && containsTargetState)
                {
                    b.BorderBrush = newSolidColorBrush(Colors.Yellow);
                    b.BorderThickness = newThickness(4);
                }
                else
                {
                    b.BorderBrush = newSolidColorBrush(Colors.White);
                    b.BorderThickness = newThickness(0);
                }
 
            }
        }
·        As you recall, For StateMachineStateRecord , I am just pumping the State DisplayName in my publication, ignoring the Activity Id to SourceLocation mapping.
·        I added a Subscription for the StateChangeEvent:
 
 
        public void SubscribeStateChangeEvent(StateChangeEventArgs se)
        {
            Dispatcher.Invoke(
                DispatcherPriority.Render,
                (Action)(() => SelectState(se.StateName)
                ));
        }
 
·       As well as a short circuit for SubscribeTrackingEvent:
_isStateMachine = (RootActivity is StateMachine);
 if (_isStateMachine) return;
 
 
 
 
 
 
   It works !!!!!!!!!! – I've hacked the WorkflowDesigner to support State Debugging !
 
Enjoy!
 
 

 

Posted on Tuesday, June 7, 2011 6:56 PM Workflow | Back to top


Comments on this post: Workflow 4.0.1 StateMachine - Distributed Tracking Visualization

# re: Workflow 4.0.1 StateMachine - Distributed Tracking Visualization
Requesting Gravatar...
Great Article! I have been looking at the same MSDN samples and wondering if I could separate the debugging stuff and the designer host as well as implement the visual tracking remotely.

Is is possible to post he sample code.

Regards,

Mark
Left by Mark on Sep 08, 2012 3:31 AM

# re: Workflow 4.0.1 StateMachine - Distributed Tracking Visualization
Requesting Gravatar...
Bloody hell! That's one amazing job!
Left by Frank on Sep 20, 2012 10:55 PM

# re: Workflow 4.0.1 StateMachine - Distributed Tracking Visualization
Requesting Gravatar...
I wish I had seen this a few days ago. Fighting the same fight. Only difference is I went with SignalR instead of pub sub. I am still trying to reconcile the workflow activityids with the designer id's to connect to a sourcelocation.

Also, the fact that the designer chokes on c# expressions is really annoying. Beginning to think this whole idea is a dead end.
Left by Philip on Oct 18, 2012 4:48 AM

# re: Workflow 4.0.1 StateMachine - Distributed Tracking Visualization
Requesting Gravatar...
The idea really works well. It's a significant process after all. - Morgan Exteriors
Left by Jaime Payne on Dec 31, 2016 4:35 AM

Your comment:
 (will show your gravatar)


Copyright © JoshReuben | Powered by: GeeksWithBlogs.net