Tuesday, February 19, 2008

This is the second of a series about using Workflow Foundation to control your UI Logic in a WPF application. The full table of contents:

  • Workflow as controller: Introducing <M,V,C> where M: ViewModel, V : WPF, C : WF
  • Part II, starting the application, and the adapter
  • Intermezzo: new sample application
  • Part III, your first view
  • Part IV, decoupling view from controller
  • Part V, marshalling commands from WPF to WF
  • Part VI, Injecting a controller in a subview / workspace
  • Part VII, IOC on the cheap: injecting and retrieving objects
  • Part VIII, Broadcasting for all to see
  • I thought it best to just put out that TOC, to force myself to actually write these short posts ;-)

    Recap

    In the previous post, the complete solution was presented. I am presenting a solution to use workflow as the controller part in your MVC inspired WPF application. It is inspired on the thought that you do not need complex frameworks, because WPF already gives you great power (routed eventing, resources). So, no IOC is used, no event aggregator etcetera: it's taken care of by WPF and WF, a natural fit.
    The solution is very decoupled and I feel it's a great advantage to be able to visual your control logic.

    Starting the application / Shell

    The term 'Shell' is used to indicate a startable 'host' for your application. In WPF, that is probably your App.xaml view. In there, you point to a startupUri of a window. We do not need anything different for our application, but we do need to start the workflow runtime.

    I have chosen not to build a generic application.start method, because I am still thinking about threading. For now, I have chosen to use the ManualWorkflowSchedulerService to let the workflow instances do it's thing. Normally the workflow runtime uses a workerthread to execute the workflow instances on the background. That means that when you send a command to the workflow, it will be run in the background. That sounds great, but will give you some pain when changing data that is bound to the UI thread. For this first version, I did not want that pain, so I used the ManualWorkerThreadScheduler. Now, the workflow instance will do nothing, until we explicitly donate our (UI)thread to it.

    Starting the runtime is simple:

      1         public App()
      2         {
      3             // start a workflow runtime
      4             workflowRuntime = new WorkflowRuntime();
      5
      6             ManualWorkflowSchedulerService manualSvc = new ManualWorkflowSchedulerService(false);
      7             workflowRuntime.AddService(manualSvc);
      8
      9             ExternalDataExchangeService dataSvc = new ExternalDataExchangeService();
    10             workflowRuntime.AddService(dataSvc);
    11             dataSvc.AddService(new CommandService(workflowRuntime));    // add our generic communication service
    12
    13
    14
    15             workflowRuntime.StartRuntime();
    16             workflowRuntime.WorkflowTerminated += new EventHandler<WorkflowTerminatedEventArgs>(workflowRuntime_WorkflowTerminated);
    17             workflowRuntime.WorkflowAborted += new EventHandler<WorkflowEventArgs>(workflowRuntime_WorkflowAborted);
    18             workflowRuntime.WorkflowCompleted += new EventHandler<WorkflowCompletedEventArgs>(workflowRuntime_WorkflowCompleted);
    19
    20
    21             ControllersAdapters.WorkflowRuntimeHolder.SetCurrentRuntime(workflowRuntime);
    22
    23             this.Exit += new ExitEventHandler(App_Exit);
    24         }

    At line 7, the ManualWorkflowSchedulerService is indeed added to the runtime.
    At line 11, our own communicaton class (CommandService) is added to the runtime. You can interpret the runtime as a global object container: when we ever want to use that commandService singleton, we can just ask the runtime for it.
    Lines 15 through 18 hookup some eventhandlers to certain events. We'll cover them next.
    Line 21 sets this runtime at a static propery for the controllerAdapters to fetch. A quick and dirty solution.

    The events that we subscribe to are handled as follows:

      1         void workflowRuntime_WorkflowCompleted(object sender, WorkflowCompletedEventArgs e)
      2         {
      3             ICommandService cmdsvc = workflowRuntime.GetService(typeof(ICommandService)) as ICommandService;
      4
      5             cmdsvc.SendMessage(new InstanceWasRemovedMessage(e.WorkflowInstance.InstanceId));
      6         }
      7
      8         void workflowRuntime_WorkflowAborted(object sender, WorkflowEventArgs e)
      9         {
    10             ICommandService cmdsvc = workflowRuntime.GetService(typeof(ICommandService)) as ICommandService;
    11
    12             cmdsvc.SendMessage(new InstanceWasRemovedMessage(e.WorkflowInstance.InstanceId));
    13         }
    14
    15         void workflowRuntime_WorkflowTerminated(object sender, WorkflowTerminatedEventArgs e)
    16         {
    17             ICommandService cmdsvc = workflowRuntime.GetService(typeof(ICommandService)) as ICommandService;
    18
    19             cmdsvc.SendMessage(new InstanceWasRemovedMessage(e.WorkflowInstance.InstanceId));
    20         }
    21
    22         void App_Exit(object sender, ExitEventArgs e)
    23         {
    24             workflowRuntime.StopRuntime();
    25         }

    As you can see, I fetch the command service from the runtime, and ask it to send a message. The commandservice will 'broadcast' this message to all living controller adapters. When a workflow is finished, either by termination or just because it finished it's process, we need to let the adapter know so that it can unsubscribe from events from the commandservice.

    The adapter

    The GenericWorkflowAdapter is a WPF control that handles the communication between WPF and WF. We will see pieces of it in the upcoming posts, but we'll need to go into a little more detail here.

        /// <summary>
        /// This is a WPF type that can be placed anywhere in your UI tree. It can be configured with a workflow type.
        /// When it is, it will instantiate the Workflow.
        /// This adapter will then be able to pick up WPF Command (RoutedUI) and send them to the workflow, as well
        /// as listen to events coming from the runtime, the commandsvc and the workflow instance
        /// </summary>
        public class GenericWorkflowAdapter : ContentControl, IWeakEventListener
        {
               ...
        }

    As you can see, it is a contentControl. The workflowcontroller is able to place an arbitrary view as it's content.
    It has one property: WorkflowControllerProperty, typeof(Type), which will fire off the SetWorkflowController method when it is set.

      1         private void SetWorkflowController(Type type)
      2         {
      3             // actually start the controller!
      4             instance = runtime.CreateWorkflow(type);
      5             instance.Start();
      6
      7             // allow it to do it's thing
      8             threadSvc.RunWorkflow(instance.InstanceId);
      9
    10             Debug.WriteLine(String.Format("Adapter has started workflow instance {0}, of type {1}", instance.InstanceId, type.ToString()));
    11
    12
    13             // we will filter commands to only manage commands that we have defined in our workflow
    14             // so we have to walk recursively through all activities
    15             IEnumerable<System.Workflow.ComponentModel.Activity> flattenedActivities =
    16                 (instance.GetWorkflowDefinition() as System.Workflow.ComponentModel.CompositeActivity).EnabledActivities.
    17                 SelectRecursiveSimple(activity => (activity is System.Workflow.ComponentModel.CompositeActivity) ?
    18                     ((System.Workflow.ComponentModel.CompositeActivity)activity).EnabledActivities :
    19                     new System.Collections.ObjectModel.ReadOnlyCollection<System.Workflow.ComponentModel.Activity>(new List<System.Workflow.ComponentModel.Activity>()))
    20             ;
    21
    22             // let's get the handlecommands
    23             var commands = flattenedActivities.Where(act => act is HandleCommand).Select(act => ((HandleCommand)act).CommandName)
    24             ;
    25
    26             implementedCommands = new ReadOnlyCollection<string>(commands.ToList());
    27
    28             SetupCommandSinks();
    29         }

    As you can see, this method actually goes into the workflowruntime and ask for it to spin up a workflowinstance. Then it donates it's thread to actually 'run' the instance. The workflow instance probably has initialization code attached to it. That code get's run at this point.

    At line 15, I use Linq to go through every activity that is defined in the workflow and look at the HandleCommand activities. These are activities that wait for a command and act upon it. I need to know which commands this workflow might respond to, so I create a readonly collection from this. Later, we will only let the adapter pass commands that are actually implemented by the workflow!

    At line 28, there is a call to setup the command sinks:

            private void SetupCommandSinks()
            {
                // set up command sinks
                CommandManager.AddExecutedHandler(this, CmdExecuted);
                CommandManager.AddCanExecuteHandler(this, CmdCanExecute);
            }

    Here you see the simple code that will register this contentcontrol to handle RoutedUICommands from WPF. As you can probably guess, when a command reaches these handlers, they will be filtered by the 'implementedCommands' collection we defined earlier on, and if they are implemented AND they are currently enabled, the command is posted to the workflow.

    I have setup two events: the lost and gotFocus events, to also send commands to the workflow. If the workflow chooses to do so, it can handle these. I use them to remove and add options to the menu shell.

    The last thing to cover, is the ReceiveWeakEvent method. This adapter will register itself at the commandService, and the commandService will subscribe the adapter to a few events. It uses weakevents to do so, so that the lifespan of this adapter is not tied into the commandService (which will live forever).
    There are a whole host of message that can be sent in the system, and the ReceiveWeakEventMethod will implement different behavior for all of them. It will look at at the arguments that were passed, and check for a specific type.

    (I might refactor that, to actually put the logic into the messages).

    That's it for now, in the next post we will actually get our hands dirty and put together our first application!

    Comments are closed.