Windows Workflow Foundation: Everything About Re-Hosting the Workflow Designer

 

Vihang Dalal
Software Development Engineer Test
Microsoft Corporation

May 2006

Applies to:
   Windows Workflow Foundation beta 2.2
   Microsoft Visual C# Version 2.0
   Visual Studio 2005

Summary: The workflow designer surface that shows in Visual Studio 2005 when the Visual Studio 2005 extensions for Windows Workflow Foundation are installed is a redistributable component. This article discusses how this component can be hosted in a separate application. A sample application that demonstrates the concepts is described in the article. (18 printed pages)

This article was written using beta 2.2 and may need to be changed for compatibility with later versions of Windows Workflow Foundation.

Download the code sample, WorkflowDesignerControl.exe.

Contents

Introduction
Re-Hosting Classes
Features and Custom Services
Conclusion
For More Information

Introduction

This article has been written for developers who use Windows Workflow Foundation (WF). It will help them to understand the workflow designer re-hosting APIs, the extensibility hooks, and best practices in re-hosting the workflow designer outside of the Visual Studio 2005 IDE.

What is Designer Re-hosting?

Designer Re-hosting is visualization of workflows outside of Visual studio.

Why would you use Designer Rehosting?

Designer Re-hosting can be used to:

  • Customize views to your needs/choices. You can choose to provide as much or as little functionality.
  • Embed workflow Designer in your existing application.

When would you use Re-hosting?

You can Re-host the designer for Design Time and Runtime experiences.

  • Design Time:
    • To view, create, and modify workflows.
  • Runtime:
    • To view the current state of an executing workflow by utilizing the tracking information.

The sample demonstrates the ability to have a self-contained application (outside of Visual Studio 2005) that can be used to design and develop workflows using the workflow designer. The System.Workflow.ComponentModel library provides a set of APIs to achieve this and the goal is to demonstrate their use and best practices and patterns that can be used to leverage the workflow designer in applications.

This article assumes the reader has basic understanding of C#, the .NET Framework, and WF.

The article will start off with a surface level overview of various parts to complete the workflow designer re-hosting story. Later it will dig into details of integrating advanced functionality such as development and deployment.

Re-Hosting Classes

There are two parts to designing a workflow:

  • The workflow object model, which describes the logical execution of the activities.
  • A set of designer components that map to individual activities.

When re-hosting the designer in your applications, the developer is responsible for managing both parts in order to view and modify the workflow.

The classes DesignSurface, WorkflowView, and WorkflowDesignerLoader make up the designer re-hosting surface and help in managing the designer components aspects of your workflow. A workflow author can completely re-host the workflow designer outside of Visual studio using these classes.

Figure 1 shows a re-hosted workflow designer in a Windows Forms application.

Aa480213.wfdsgnrehst01(en-us,MSDN.10).gif

Figure 1

The DesignSurface uses the WorkflowDesignerLoader to create the activity tree and its corresponding designer components tree. The WorkflowView represents the default view of the designer associated with the RootComponent in the activity tree.

The class DesignSurface provides a self-contained environment to display the designer components. This is ultimately what the user perceives as a designer. This class is used to access the root designer associated with the workflow definition.

The class WorkflowDesignerLoader is an implementation of a designer loader that developers will subclass to implement custom loading and unloading logic. In the loading method, called PerformLoad(), developers are responsible for creating the workflow object model and designer component hierarchies. Inside the unloading method, PerformFlush(), developers are responsible for saving the workflow object model to a serialized workflow definition. This definition can be stored in the file systems, DB, and so on. This logic is unique to your re-hosting application.

The class WorkflowView implements the control that renders a visual representation of the process flow described in the workflow definition. It uses the DesignSurface to retrieve the designer components that visually represent the workflow definition. WorkflowView offers a rich set of user interface functionality needed by the activity designers for rendering and for responding to various Windows-generated events. For example, developers can subscribe to OnLayout() and OnPaint() events to perform look and feel operations in the application. In addition, methods like SaveWorkflowAsImage() can be used to snapshot the visual representation of the workflow and save it as an image

The following example shows code that demonstrates the basic steps to re-host the workflow designer in Windows forms. This is from the Loader.cs and WorkflowDesignerControl.cs source file.

  • Create custom WorkflowDesignerLoader:

    Override PerformLoad() and PerformFlush() and implement custom loading and unloading logic.

    internal sealed class WorkflowLoader : WorkflowDesignerLoader
    {
       protected override void PerformLoad(IDesignerSerializationManager serializationManager)
          {
             base.PerformLoad(serializationManager);
             // Implement the logic to read from the serialized state,
             // create the activity tree and the corresponding designer
             // tree and add it to the designer host
          }
    
       protected override void PerformFlush(IDesignerSerializationManager manager)
          {
             // Implement the logic to save the activity tree to a
             // serialized state along with any code beside elements 
          }
    }
    
  • Load the workflow view into Windows forms.

    // Create new Design surface and loader (created as shown above)
    
    DesignSurface designSurface = new DesignSurface();
    WorkflowLoader loader = new WorkflowLoader();
    
    // load the design surface using the loader. This will call the
    // PerformLoad method on the loader, create the activity tree and
    // add the corresponding designer components to the designer host
    
    designSurface.BeginLoad(loader);
    
    // Get the designer host and retrieve the corresponding root component
    IDesignerHost designerHost = designSurface.GetService(typeof(IDesignerHost)) as IDesignerHost;
    if (designerHost != null && designerHost.RootComponent != null)
    {
       // Get the designer associated with the root component
       IRootDesigner rootDesigner =    designerHost.GetDesigner(designerHost.RootComponent) as IRootDesigner;
       if (rootDesigner != null)
       {
          this.designSurface = designSurface;
          this.loader = loader;
    
             // Assign the default view of the rootdesigner to WorkflowView
             this.workflowView = rootDesigner.GetView(ViewTechnology.Default) as          WorkflowView;
    
             // Add the workflow view control to winforms app
             this.workflowViewSplitter.Panel1.Controls.Add(this.workflowView);
             this.workflowView.Focus();
             this.propertyGrid.Site = designerHost.RootComponent.Site;
       }
     }
    

The DesignerHost class acts as a service container and service provider that various services are added to and accessed. Developers add custom implementation of a service or use the default one that is provided. Once the design surface is loaded, using the WorkflowView control, the user can drag drop activities into it and define a workflow. The interaction between the DesignSurface class, the WorkflowDesignerLoader class, and the WorkflowView is summarized in figure 2.

Aa480213.wfdsgnrehst02(en-us,MSDN.10).gif

Figure 2

Later the features that have been implemented and integrated into this sample will be explained in detail. The details will define the services required in order for them to work.

Features and Custom Services

The System.Workflow.ComponentModel provides some services by default. These include implementations of IIdentifierCreationService, IReferenceService, IworkflowCompilerOptionsService, and so on. For advance features, custom services need to be implemented which are explained below.

  1. Context Menu And Common Designer Features:

    As the name explains, a context menu is the menu that appears in a particular context. You can see the context menu when you right click on a particular designer. This will show you a list of all menu items for that designer. The context menu support is integrated using the WorkflowMenuCommandService.

    • WorkflowMenuCommandService:

      The WorkflowMenuCommandService class inherits from the MenuCommandService class and is used to add handlers for menu commands and to define verbs. These verbs are displayed when the user brings up the context menu for the designer.

      The following code snippet shows how you add verbs using this class.

      // Create a new context menu
      ContextMenu contextMenu = new ContextMenu();
      Dictionary<CommandID, string> selectionCommands = new Dictionary<CommandID, string>();
      
      // Add the required commands
      selectionCommands.Add(WorkflowMenuCommands.Cut, "Cut");
      selectionCommands.Add(WorkflowMenuCommands.Copy, "Copy");
      selectionCommands.Add(WorkflowMenuCommands.Paste, "Paste");
      selectionCommands.Add(WorkflowMenuCommands.Delete, "Delete");
      foreach (CommandID id in selectionCommands.Keys)
      {
            MenuCommand command = FindCommand(id);
            if (command != null)
             {
               // For each command create a new menu item and add an
               MenuItem menuItem = new MenuItem(selectionCommands[id], new          EventHandler(OnMenuClicked));
               menuItem.Tag = command;
               contextMenu.MenuItems.Add(menuItem);
            }
      }
      
      // Handle the event when the MenuItem is clicked
      private void OnMenuClicked(object sender, EventArgs e)
      {
       // Retrieve the menu item that was clicked
       MenuItem menuItem = sender as MenuItem;
       if (menuItem != null && menuItem.Tag is MenuCommand)
            {
               // invoke the command corresponding to the menu item clicked
               MenuCommand command = menuItem.Tag as MenuCommand;
               command.Invoke();
            }
      }
      

      Figure 3 shows the menu commands that have been added using the previous code string. This menu is activated by right clicking on the background of the workflow designer.

      Aa480213.wfdsgnrehst03(en-us,MSDN.10).gif

      Figure 3

      Other common designer features such as cut / copy / paste / delete, pan / zoom in / zoom out, expand / collapse, and so on, that are present in the toolstrip menu, are also integrated with the help of menu command service. This makes use of the command ID fields that are defined in the WorkflowMenuCommands class. Each command ID corresponds to a command function provided by the workflow designers.

      When the user clicks on any of the commands, the following code gets called to execute the command.

      // Invoke the standard command with the command id of the command clicked
      this.workflowDesignerControl1.InvokeStandardCommand(WorkflowMenuCommands.Expand);
      
      public void InvokeStandardCommand(CommandID cmd)
         {
            IMenuCommandService menuService =
            GetService(typeof(IMenuCommandService)) as IMenuCommandService;
            if (menuService != null)
            menuService.GlobalInvoke(cmd);
         }
      
  2. Code Beside Generation:

    Code beside generation is one of the important features supported by the sample. This includes generating fields, properties (.NET properties as well as Depedency Properties), and event handlers whenever the user performs corresponding action in the designer. Code beside generation support requires two services to be created by the developer; these are included in the sample. They are MemberCreationService and EventBindingService, both of which are implemented as a class in the sample and are described in detail later in this article.

    • MemberCreationService:

      The MemberCreationService class inherits from the IMemberCreationService interface that defines methods to dynamically create, update, and remove code elements for a type at design time. Methods are also provided that allow you to display code at design time. The MemberCreationService class instance is added into the loaderhost and directly interacts with the code compile unit that is added into the type provider. Whenever the user performs any action that requires code generation, the call is intercepted by the MemberCreationService and performs the appropriate action. One important thing to note here is that the MemberCreationService class is used to handle only the generation of member fields and properties. Eventhandler generation is managed by the EventBindingService class.

      The following is a sample code from the CreateField() method of the MemberCreationService class which demonstrates creating and adding new fields to the workflow.

      // prepare field
      CodeMemberField field = new CodeMemberField();
      field.Name = fieldName;
      field.Type = GetCodeTypeReference(className, type);
      field.UserData["UserVisible"] = true;
      field.Attributes = attributes;
      
      // Get the type declaration from code compile unit
      CodeTypeDeclaration typeDeclaration = GetCodeTypeDeclFromCodeCompileUnit(nsName, className);
      
      //push the field into the code compile unit
      typeDeclaration.Members.Insert(index, field);
      
      // refresh the code compile unit
      TypeProvider typeProvider = (TypeProvider)this.serviceProvider.GetService(typeof(ITypeProvider));
      typeProvider.RefreshCodeCompileUnit(this.ccu,new EventHandler(RefreshCCU)); 
      

      This code compile unit is then flushed into the C# file and is compiled along with the XAML file. The MemberCreationService class is fairly sophisticated in that it has the functionality to check if a field already exists before creating one. It also has the functionality to modify existing fields and properties as well as updating the type name.

    • EventBindingService:

      The EventBindingService class implements the IEventBindingService interface and provides the service for registering event handlers for component events; that is, it provides a way to link an event handler with a component event from designer code. EventBindingService is required for the event properties of an activity to show up in the property browser. Without EventBindingService the event properties (for e.g. ExecuteCode for CodeActivity) will not appear in the property browser.

      The EventBindingService class provides the EventPropertyDescriptor class, which is used to configure the event with an event handler method name.

      The EventBindingService class has a GetEvent() method that is used to obtain an EventDescriptor object for an event of a component. The GetEventProperty() and GetEventProperties() methods of the EventBindingService class return a PropertyDescriptor for each EventDescriptor passed to either method. Each PropertyDescriptor returned from GetEventProperty() or GetEventProperties() has a custom type converter, which converts the handler name to a string value that indicates the name of the event-handler method to link the event with. This value is set using the SetValue() method of the EventPropertyDescriptor class.

      The EventBindingService class instance is added into the loaderhost and also interacts with the code compile unit that is added into the type provider. Whenever the user performs any action that requires event handler generation, the call is intercepted by EventBindingService and performs the appropriate action.

      The following code snippet shows how to create and add a event handler using EventBindingService.

      // create code member method
      CodeMemberMethod method = new CodeMemberMethod();
      method.Name = methodName;
      method.Parameters.AddRange(paramCollection);
      method.ReturnType = new CodeTypeReference(returnType);
      method.Attributes = modifiers;
      
      //push the method into the code compile unit
      typeDeclaration.Members.Insert(index, method);
      
      // refresh the code compile unit
      TypeProvider typeProvider = (TypeProvider)this.serviceProvider.GetService(typeof(ITypeProvider));
      typeProvider.RefreshCodeCompileUnit(this.ccu,new EventHandler(RefreshCCU)); 
      

      When the code compile unit is flushed into the C# file, the following code gets emitted.

      public partial class Workflow1 : SequentialWorkflowActivity
      {
              private void EventHandler(object sender, System.EventArgs e)
              {
              }
      }
      

      The XAML file has the following code.

      <SequentialWorkflowActivity x:Class="foo.Workflow1" x:Name="Workflow1" Completed="EventHandler" xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" xmlns="https://schemas.microsoft.com/winfx/2006/xaml/workflow" />
      
  3. ToolBoxService:

    The Toolbox is used to display all the components (activities in our case) that can be dropped into the designer. Toolbox integration is achieved with the help of a ToolBoxService class.

    • ToolBoxService:

      The Toolbox Service inherits from the IToolboxService interface and provides properties and methods for adding toolbox items, serializing and de-serializing toolbox items, retrieving toolbox state information, and managing toolbox state. The toolbox service reads the list of activities and the assemblies in which they reside from a text file and adds them to the toolbox.

      Figure 4 shows the activities that are added to the toolbox using the toolbox service.

      Aa480213.wfdsgnrehst04(en-us,MSDN.10).gif

      Figure 4

      Toolbox Service also supports the adding of custom activities to the toolbox. The activities from the toolbox can now be dropped onto the workflow design surface and can be configured as required.

  4. ActivityBind Dialog:

    Activity bind is an important feature that is regularly used in Workflow models. It helps the workflow model developer to bind an activity's property to a workflow property, field, indexer, method, event, or another activity's property. This allows the passing of values from one activity to another. In the designer this can be achieved using the activity bind dialog. The activity bind dialog lists all the fields, properties, methods, and activities present in the workflow, and the user can bind to any field, property, or method as long as the source and the destination types match. The activity bind dialog also allows the user to create a new field or a property to bind to if the user chooses to do so. Activity bind dialog integration is achieved through the IPropertyValueUIService interface.

    • PropertyValueUIService:

      The PropertyValueUIService class inherits from the IPropertyValueUIService interface. The PropertyValueUIService class provides a PropertyValueUIItem associated with a property of the activity. In this case, PropertyValueUIItem is a blue image, as shown in figure 5.

      Aa480213.wfdsgnrehst05(en-us,MSDN.10).gif

      Figure 5

      Note: The custom PropertyValueUIItem also requires property browser to be closely integrated with the designer. This is achieved by setting the Site property of the property grid to the Site of the RootComponent associated with the DesignerHost.

      this.propertyGrid.Site = designerHost.RootComponent.Site;
      

      When the user double clicks on the blue image, the activity bind dialog is brought up, as shown in figure 6.

      Using this dialog, the property can now be bound to an existing property, a new field, or a new property. Once the binding is complete in the dialog and the developer clicks the OK button, the properties are shown with the binding complete. This is shown in figure 6.

      The interaction in the activity bind dialog causes the following code to be emitted.

      public System.EventHandler NewField = default(System.EventHandler);
      
      <SequentialWorkflowActivity x:Class="foo.Workflow1" Initialized="{ActivityBind /Self,Path=NewField}" x:Name="Workflow1" xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" xmlns="https://schemas.microsoft.com/winfx/2006/xaml/workflow" />
      

      Aa480213.wfdsgnrehst06(en-us,MSDN.10).gif

      Figure 6

      Note:   In case the workflow model developer decides to bind to a new field or a property, MemberCreationService comes into play and handles the code generation part.

  5. Rules Dialog:

    The integration of a rules execution engine is a very important feature for a workflow platform. Rules capabilities allow developers to easily incorporate declarative rules at any point in their workflow, thereby providing a seamless developer experience between workflow and rules modeling. The workflow designer exposes rules technology in two ways: through conditions on activities in which the rule condition dialog is displayed, and through the forward chaining ruleset dialog on the Policy activity.

    Figure 7 shows the rule condition dialog in the re-hosted designer:

    Aa480213.wfdsgnrehst07(en-us,MSDN.10).gif

    Figure 7

    Once the rule dialog is up, you can configure the rule as you would in Visual Studio 2005. Once the rule is configured, it is saved in a file with the extension .rules in the same directory as the XAML file. The format of this file is also XAML.

    One important thing to note here is the ability to use activities in rule condition. In the previous example we use the following condition in the rule.

    this.whileActivity1.Description == "Test Description"
    

    This requires some additional logic to be incorporated in the code. The rules dialog shows all the members in the type that is currently being designed. This type is provided to it by the type provider. The type provider has a partial class that represents the code beside. However, when we add a WhileActivity, it gets added in the markup (XAML file). Hence we need a way to convert the XAML code to a code compile unit and add it to the type provider. There are helper methods that deserialize the XAML, convert it to a code compile unit having a partial class, and add it to the type provider. The type provider automatically merges the two partial classes and shows all the members in the type that is being designed.

    The rule set dialog also works in a similar way. The ruleset with all the rules is saved in the .rules file and can be edited as and when required.

    Figure 8 shows Rule Set Editor dialog integrated with re-hosted designer.

    Aa480213.wfdsgnrehst08(en-us,MSDN.10).gif

    Figure 8

  6. State Machine Features:

    Another important feature supported by the re-hosting sample is retaining the layout of states in state machine workflow, as well as rendering the states on the design surface by reading the layout information from the layout file. The following code demonstrates how to save and load the layout information for state machine workflow.

    // Save the layout in case of State Machine Workflow
    string layoutFile = Path.Combine(Path.GetDirectoryName(this.xoml), Path.GetFileNameWithoutExtension(this.xoml) + ".layout");
    ActivityDesigner rootdesigner = host.GetDesigner(rootActivity) as ActivityDesigner;
    XmlWriter layoutwriter = XmlWriter.Create(layoutFile);
    IList errors = null;
    SaveDesignerLayout(layoutwriter, rootdesigner, out errors);
    layoutwriter.Close();
    
    // Load the layout
    string layoutFile = Path.Combine(Path.GetDirectoryName(this.xoml), Path.GetFileNameWithoutExtension(this.xoml) + ".layout");
    if (File.Exists(layoutFile))
       {
          IList loaderrors = null;
          using (XmlReader xmlReader = XmlReader.Create(layoutFile))
             LoadDesignerLayout(xmlReader, out loaderrors);
       }
    

    One important note to keep in mind while loading the layout information from the layout file is to make sure that the operation is carried out in the OnEndLoad() method of the loader. This is because the layout information is always applied to the designer component, which is only created after the PerformLoad() method finishes.

    Figure 9 shows a re-hosted state machine workflow with the layout information applied to the states.

    Aa480213.wfdsgnrehst09(en-us,MSDN.10).gif

    Figure 9

  7. Other Features:

    The sample also includes integration of other common workflow features.

    • New / Open / Save / Save As

      The designer sample has features to create new workflow, open existing workflows, and save the workflows.

    • Page Setup / Print Preview / Print

      Workflow provides its own page setup dialog that can be displayed to the end user to configure the page setup before printing.

      Figure 10 shows how the page setup dialog is displayed.

      Aa480213.wfdsgnrehst10(en-us,MSDN.10).gif

      Figure 10

      The System.Workflow.ComponentModel provides classes to do the page setup for the workflow.

      The following code snippet is used to display the dialog.

      WorkflowView workflowView = this.workflowDesignerControl1.WorkflowView;
      if (null != workflowView)
         {
            WorkflowPageSetupDialog pageSetupDialog = new 
            WorkflowPageSetupDialog(this.workflowDesignerControl1.Workflow
            View as IServiceProvider);
         if (DialogResult.OK == pageSetupDialog.ShowDialog())
             workflowView.PerformLayout();
         }
      

      The workflow can also be displayed in the print-preview mode using the workflowView.PrintPreviewMode() API, as shown in figure 11.

      Aa480213.wfdsgnrehst11(en-us,MSDN.10).gif

      Figure 11

    • Compile Workflow.

      The workflow can also be compiled using the workflow compiler, as shown in the following code snippet.

      WorkflowCompiler compiler = new WorkflowCompiler();
      WorkflowCompilerParameters parameters = new WorkflowCompilerParameters();
      
      // adding the code beside file and XAML file to the arraylist of files to 
      // be compiled
      ArrayList files = new ArrayList();
      files.Add(this.loader.Xoml);
      files.Add(codeBesideFile);
      
      // adding the rules file to the resources
      string resources = @"/resource:" + rulesFile + "," + this.NameSpace + "." + this.TypeName + "." + "rules";
      parameters.CompilerOptions += resources;
      
      parameters.OutputAssembly = "CustomWorkflow" + Guid.NewGuid().ToString() + ".dll";
      
      // Compile the workflow
      WorkflowCompilerResults results = compiler.Compile(parameters, strArr);
      

Conclusion

Windows Workflow Foundation provides a rich set of APIs that can be easily leveraged to create a simple or fully functioanal advanced stand alone application to host the workflow designer and common functionalities. Re-hosting of the workflow designer outside of Visual Studio 2005 will help you to build your custom workflow design surface on top of WF. By re-hosting the workflow designer and providing your customers with specialized high functionality activity packages, you can give your end-users a specialized design environment for constructing workflows. This would provide an intuitive environment in which they can create and modify workflows that are specific to their business domain. The technology also allows seamless integration of rules support in the workflow.

For More Information

This document along with the following resources should help developers who are new to workflow technology learn about the technology and quickly become productive with its use.

  • MSDN Workflow Site:
    • Numerous documents and links to webcasts and labs.
  • SDK Samples:
    • Designer Hosting
    • Outlook Workflow Wizard
  • MSDN Workflow Forum:
    • For questions related to Designer hosting or WF in general, please visit this discussion forum.

 

About the author

Vihang Dalal works for Microsoft Corporation in Redmond, Washington where he is a Software Development Engineer in Test. He has worked on the Windows Workflow Foundation team since January 2005.