images

Most of the time you will author new activities and workflows using the workflow designer that is integrated into Visual Studio. However, you may need to enable end users of your application to also design activities and workflows. You can allow them to load and customize existing Xaml files or to create new ones.

The workflow designer is not limited to use only within the Visual Studio environment. WF provides the classes necessary to host this same designer within your applications. This chapter is all about hosting this designer. After a brief overview of the major workflow designer components, you will implement a simple application that hosts the workflow designer. In subsequent sections, you will build upon the application, adding new functionality with each section.

Understanding the Workflow Designer Components

The workflow designer functionality is provided in a set of classes that is located in the System.Activities.Presentation namespace and its associated child namespaces. Here are the additional child namespaces that you are likely to use when you host the designer:

images

The workflow designer is architected in layers, with components in each layer interacting with those above or beneath it. It might be helpful to keep these layers in mind as you explore the classes that you will use to host the designer.

  • Visual: This includes the visual controls that a developer uses to interact with the activity definition. The visual components interact with the ModeItem tree.
  • ModelItem tree: This is a tree of ModelItem objects that sit between the visual elements and the in-memory representation of the activity definition. They track changes to the underlying definition.
  • Activity definition: This is the in-memory tree of activities that have been defined.
  • MetadataStore: An in-memory store of attributes that is used to associate activities with their designers.

In the sections that follow, I review the most important classes that you are likely to encounter when you develop your own designer host application.

Understanding the WorkflowDesigner Class

To host the workflow designer, you create an instance of the WorkflowDesigner class. Here are the most important members of this class:

images

After creating the WorkflowDesigner instance, you add the control referenced by the View property to a WPF window in order to display the designer canvas. This property is a UIElement, which is a WPF control that can be placed directly on a window or within another control. In a like manner, the PropertyInspectorView property references a WPF control that you can also add to your WPF application. It provides the property grid, which allows you to change property values for the currently selected activity in the designer. The two controls are automatically linked, so there’s no need for additional code to set the current selection for the property grid.

To initialize the designer with a definition, you use one of the overloads of the Load method. One overload of this method allows you to specify the file name of the Xaml file containing the definition. With another overload, you pass an object that represents the definition. This object could be a single activity or an entire tree of activities that you constructed in code. You can also set the Text property to the Xaml string representing the definition and then invoke the Load method without any parameters. This last option is useful if you are persisting the definition in a database or another medium instead of directly in the file system.

To save the current definition, you can invoke the Save method, passing the name of the file to use when saving. But that method is useful only if you are saving the definition directly to the file system. You can also retrieve the Xaml string from the Text property and save it yourself (to the file system, to a database, and so on).

images Tip Remember to invoke the Flush method before you access the Text property; otherwise, you may not retrieve the latest version of the definition.

Understanding the ToolboxControl

The WorkflowDesigner class provides the designer canvas and a coordinated properties grid. It doesn’t automatically provide a Toolbox containing the activities that can be dropped onto the canvas. However, WF does provide these classes that allow you to easily construct your own Toolbox:

  • ToolboxControl: A WPF UIElement that visually represents the Toolbox
  • ToolboxCategory: Defines a single activity category in the Toolbox
  • ToolboxItemWrapper: Defines a single tool entry in the Toolbox

These classes are located in the System.Activities.Presentation.Toolbox namespace. To create a Toolbox, you first create an instance of the ToolboxControl and add it to the WPF window. You then create the categories and individual items that you want to show in the Toolbox. Since you are implementing your own designer host application, you have complete freedom as to the set of activities that you show, as well as how you organize them into categories. You don’t have to include all of the standard activities or use the standard categories.

Each Toolbox category is represented by an instance of the ToolboxCategory class. After creating a new instance, you use the Add method of this class to add the individual items in that category. Each item is represented by an instance of the ToolboxItemWrapper class. Several constructor overloads are provided for this class, allowing you to specify varying degrees of information for each tool.

You must provide the type of the item and a display name. The item type is the type of Activity that you want to add when someone drags this tool to a workflow definition. You can optionally specify a bitmap file name if you want to customize the image that is shown for the item in the Toolbox.

images Note If you don’t provide a bitmap file name for a ToolboxItemWrapper, the tool will be shown with a default image. This applies to the standard activities as well as any custom activities that you develop. It would be convenient to have the ability to load a bitmap from a resource that is packaged with the assembly. Unfortunately, this class only supports loading a bitmap image directly from a file. It doesn’t support loading an image that you obtained in some other way.

The images that are associated with the standard activities in Visual Studio are not available. For some reason, Microsoft chose to package these images in Visual Studio assemblies rather than in an assembly that is easily redistributable. So, for logistical or licensing reasons, you can’t directly use the images that are associated with the standard WF activities.

Defining New Activities

To use the workflow designer, it must always have a root object. Loading an existing Xaml definition satisfies this requirement since the definition contains one or more activities with a single root activity.

But what do you load into the designer if you want to create a new activity? The answer to this question is not as obvious as you might think. You might look at a Xaml file and notice that the root is the Activity element. So, your first attempt might be to create an instance of an empty Activity class and load that into the designer. The problem is that the Activity class is abstract and doesn’t have a public constructor. The same applies to frequently used child classes like CodeActivity and NativeActivity. You can create an instance of other activities, such as the Sequence class, and load them directly into the designer. But then every new activity that you design has the Sequence activity as its root. And the Sequence activity supports the entry of variables but doesn’t support any way to define arguments.

To solve this problem, you need to load an instance of a special class named ActivityBuilder (found in the System.Activities namespace). This class is designed for the sole purpose of defining new activities. When loaded into the designer, this class provides an empty canvas, waiting for you to add your chosen root activity. It also supports the definition of arguments. Here are the most important properties of this class:

images

I said that this class supports the definition of arguments, but from the list of supported properties, it may not be obvious where they are defined. Arguments are maintained in the Properties collection as DynamicActivityProperty instances. You will use this property later in the chapter to display a list of defined arguments.

images Warning The Name property of the ActivityBuilder class is used to set the x:Class attribute. This defines the class name that is compiled into a CLR type when you are using Visual Studio. Since you are not compiling a definition into a CLR type, you might think that you don’t need to provide a value for this property. However, you should always provide a value for the Name property. If you don’t, you will be unable to successfully provide default values for any arguments. If you attempt to define default argument values, you will be unable to load or run the serialized Xaml. The best practice is to always provide a value for the Name property. The initial value that you provide can be changed in the designer.

Understanding the EditingContext

The WorkflowDesigner supports a Context property that is an instance of the EditingContext class. This object is your interface to the current internal state of the designer. It allows you to access state that is shared between the designer and the host application. For example, you can use the Context property to identify the activity that is currently selected in the designer. It also provides access to services that have been loaded into the designer. These services provide functionality that is shared between the designer and the host application or by individual activity designers. Here are the most important properties of this class:

images

Context.Items

The ContextItemManager (the Context.Items property) provides access to individual objects that derive from the base ContextItem class. Here are the most important members of this class:

images

Each object in the Context.Items collection serves a different purpose and maintains a different type of shared state. Here are the standard objects that are available—you won’t necessarily need to access all of these objects in a typical designer application:

images

Context.Services

The Context.Services property is an instance of a ServiceManager object. This object provides access to a set of standard services that are automatically loaded when you create an instance of the designer. As you will see later in the chapter, you can also add your own custom services to provide optional functionality. The ServiceManager class provides these members:

images

Here are some of the standard services that are available when you self-host the designer:

images

Providing Designer Metadata

As you learned in Chapter 15, there are two ways to associate a designer with a particular activity:

  • You can apply the Designer attribute directly to the activity.
  • You can use the MetadataStore class at runtime.

If you opted for the second option when you implemented your custom activities and designers, you need to use the MetadataStore class to associate each activity with its designer. If you are using the standard WF activities (this is very likely), you also need to use the DesignerMetadata class (located in the System.Activities.Core.Presentation namespace). This class supports a single Register method that associates each standard WF activity with its designer. You only need to invoke this method once during the lifetime of your application.

You can also use the MetadataStore class to associate custom designers with the standard activities. Providing your own metadata entries overrides the defaults that are provided when you invoke the DesignerMetadata.Register method.

The Self-hosting Designer Workflow

If you are developing an application to host the workflow designer, you obviously need to create or maintain activity definitions outside Visual Studio. And your application will be designed to satisfy the particular requirements of your end users. That means your application will likely look much different from the one that I might implement (or the one that is presented in the remainder of this chapter). But in general, you might want to consider these steps when you decide to self-host the workflow designer:

  1. Design a WPF application that can support the designer visual components.
  2. Create an instance of the WorkflowDesigner class and add the object referenced by the View property to a WPF window to display the main designer canvas.
  3. Add the object referenced by the PropertyInspectorView property to a WPF window to provide property grid functionality.
  4. Register metadata for the standard and custom activities.
  5. Create and manage a Toolbox containing activities that can be dropped onto the designer canvas.
  6. Add logic to initialize a new activity definition. Also add logic to save and load activity definitions to a file or other persistence medium.
  7. Add logic to react to the currently selected item by providing context menus, context Toolbox support, and so on.
  8. Provide custom validation logic and display validation errors.

Implementing a Simple Workflow Designer

In the remainder of this chapter, you will implement a WPF application that hosts the workflow designer. The example application will begin with minimal functionality that will be enhanced in the sections that follow this one. Each section builds upon the code that was developed in the previous section.

images Note The examples in this chapter all build upon each other and assume that you are implementing the code for each section in order. Each section presents only the necessary changes to the existing code. If you prefer to jump directly to the finished product, I suggest you download the sample code for this book from the Apress site.

Creating the Application

Create a new project using the WPF Application project template. Name the project DesignerHost, and add it to a new solution that is named for this chapter. Add these assembly references to the project:

  • System.Activities
  • System.Activities.Presentation
  • System.Activities.Core.Presentation

All the components and code that you will add to this application will go into the MainWindow class that was added by the new project template. The MainWindow.xaml file contains the WPF markup for the window, and the MainWindow.xaml.cs file contains the C# code.

Declaring the Window Layout

Before you can implement any code for the application, you need to lay out the controls of the MainWindow class. Double-click the MainWindow.xaml file in the Solution Explorer to open it in Design View. Design View in this case is a split screen that includes a WPF designer and an XML editor showing the Xaml markup. Here is the complete markup that you need to add to this file:

<Window x:Class="DesignerHost.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Workflow Designer Host" Height="600" Width="800"
        Closing="Window_Closing">
    <Grid>

I’ve included a handler for the Closing event of the window (shown in the previous code). You will add code to this handler to test for any unsaved changes to the current definition. The window is organized into three columns and three rows. The leftmost column will be used for a Toolbox of activities, and the rightmost column is for the properties grid. The middle column is reserved for the workflow designer canvas. The first row is used for a menu, while the second row is the main area for the designer, Toolbox, and properties grid. The third row is reserved for a message area.

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="2*" />
            <ColumnDefinition Width="5*"/>
            <ColumnDefinition Width="2*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="25" />
            <RowDefinition />
            <RowDefinition Height="75" />
        </Grid.RowDefinitions>

A menu is defined that allows the user to select the main operations of the application. Handlers are defined for all the menu items, so you’ll need to include code for these event handlers even though you won’t fully implement them immediately.

        <Menu Width="Auto" Grid.Row="0">
            <MenuItem Header="File" Name="menuFile">
                <MenuItem Header="New" Name="menuNew"
                    Click="menuNew_Click" />
                <MenuItem Header="Open..." Name="menuOpen"
                    Click="menuOpen_Click" />
                <MenuItem Header="Save" Name="menuSave"
                    Click="menuSave_Click" />
                <MenuItem Header="Save As..." Name="menuSaveAs"
                    Click="menuSaveAs_Click" />
                <Separator />
                <MenuItem Header="Add Reference..." Name="menuAddReference"
                    Click="menuAddReference_Click"  />
                <Separator />
                <MenuItem Header="Run..." Name="menuRun"
                    Click="menuRun_Click" />
                <Separator/>
                <MenuItem Header="Exit" Name="menuExit"
                    Click="menuExit_Click" />
            </MenuItem>
        </Menu>

I’ve included a Border control as a placeholder for each of the three main designer areas. This provides a subtle visual separation between the controls and also simplifies the code that populates these areas with the designer controls. The markup also includes a ListBox control that occupies the final row (the message area).

The markup also includes GridSplitter controls between the three main sections. These controls allow you to drag the gray bar between the sections to resize the Toolbox or the properties grid.

        <Border Name="toolboxArea" Grid.Column="0" Grid.Row="1"
            Margin="2"  BorderThickness="1" BorderBrush="Gray"  />
        <GridSplitter Grid.Column="0" Grid.Row="1" Width="5"
            ResizeDirection="Columns" HorizontalAlignment="Right"
            VerticalAlignment="Stretch" Background="LightGray" />

        <Border Name="designerArea" Grid.Column="1" Grid.Row="1"
            Margin="2" BorderThickness="1" BorderBrush="Gray" />

        <Border Name="propertiesArea" Grid.Column="2" Grid.Row="1"
            Margin="2" BorderThickness="1" BorderBrush="Gray"  />
        <GridSplitter Grid.Column="2" Grid.Row="1" Width="5"
            ResizeDirection="Columns" HorizontalAlignment="Left"
            VerticalAlignment="Stretch"  Background="LightGray" />

        <ListBox Name="messageListBox" Grid.Column="0" Grid.Row="2"
            Grid.ColumnSpan="3" SelectionMode="Single"/>

    </Grid>
</Window>

The project won’t build at this point since the markup is referencing event handlers that you haven’t implemented. You’ll turn your attention to the code next.

Implementing the Application

Open the MainWindow class in Code View. This should open the MainWindow.xaml.cs file in the code editor. Here is the initial implementation of this class:

using System;
using System.Activities;
using System.Activities.Core.Presentation;
using System.Activities.Core.Presentation.Factories;
using System.Activities.Presentation;
using System.Activities.Presentation.Metadata;
using System.Activities.Presentation.Model;
using System.Activities.Presentation.Services;
using System.Activities.Presentation.Toolbox;
using System.Activities.Presentation.Validation;
using System.Activities.Presentation.View;
using System.Activities.Statements;
using System.Activities.XamlIntegration;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using Microsoft.Win32;

The code includes a number of namespaces that you won’t initially need. However, you will need them by the time you complete this chapter, so go ahead and include all of them now.

namespace DesignerHost
{
    public partial class MainWindow : Window
    {
        private WorkflowDesigner wd;
        private ToolboxControl toolboxControl;
        private String currentXamlPath;
        private String originalTitle;
        private Boolean isModified = false;

        private HashSet<Type> loadedToolboxActivities =
            new HashSet<Type>();

        public MainWindow()
        {
            InitializeComponent();

            originalTitle = this.Title;

During construction of the window, the metadata for the standard activities is registered using the DesignerMetadata class. A Toolbox containing a subset of the standard activities is then constructed. Finally, an instance of the designer is initialized with a call to the private InitializeDesigner method, and a new empty workflow is loaded by calling the StartNewWorkflow method.

            //register designers for the standard activities
            DesignerMetadata dm = new DesignerMetadata();
            dm.Register();

            //toolbox
            toolboxControl = CreateToolbox();
            toolboxArea.Child = toolboxControl;

            InitializeDesigner();
            StartNewWorkflow();
        }

The InitializeDesigner method creates a new instance of the designer and wires it up to the designated areas on the window. An event handler is added to the ModelChanged event of the designer. This will notify you whenever any changes are made to the current definition. As you will see later in the code, the InitializeDesigner method is called each time you need to clear the designer and begin a new activity definition. It is not possible to actually clear the designer or load a new definition once one has already been loaded.

        private void InitializeDesigner()
        {
            //cleanup the previous designer
            if (wd != null)
            {
                wd.ModelChanged -= new EventHandler(Designer_ModelChanged);
            }

            //designer
            wd = new WorkflowDesigner();
            designerArea.Child = wd.View;

            //property grid
            propertiesArea.Child = wd.PropertyInspectorView;

            //event handler
            wd.ModelChanged += new EventHandler(Designer_ModelChanged);
        }

The StartNewWorklfow handles the task of preparing the designer with a new ActivityBuilder instance. Loading this object presents the user with a clean canvas that they can use to define their own activity.

        private void StartNewWorkflow()
        {
            wd.Load(new ActivityBuilder
            {
                Name = "Activity1"
            });
            currentXamlPath = null;
            isModified = false;
        }

        private void menuNew_Click(object sender, RoutedEventArgs e)
        {
            if (IsCloseAllowed())
            {
                InitializeDesigner();
                StartNewWorkflow();
                UpdateTitle();
            }
        }

The handlers for the Save, SaveAs, and Open menu items will be fully implemented later in the chapter. However, they must be defined as shown here in order to successfully build the project:

        private void menuSave_Click(object sender, RoutedEventArgs e)
        {
        }

        private void menuSaveAs_Click(object sender, RoutedEventArgs e)
        {
        }

        private void menuOpen_Click(object sender, RoutedEventArgs e)
        {
        }

        private void menuExit_Click(object sender, RoutedEventArgs e)
        {
            if (IsCloseAllowed())
            {
                isModified = false;
                this.Close();
            }
        }

        private void Window_Closing(object sender, CancelEventArgs e)
        {
            if (!IsCloseAllowed())
            {
                e.Cancel = true;
            }
        }

The Run and Add Reference menu item handlers will also be implemented later in the chapter. The Run handler will contain logic that allows you to execute the current definition using the WorkflowInvoker class. The Add Reference handler will be used to load additional activities from a referenced assembly.

The handler for the ModelChanged event simply updates the title to indicate that there are unsaved changes.

        private void menuRun_Click(object sender, RoutedEventArgs e)
        {
        }

        private void menuAddReference_Click(object sender, RoutedEventArgs e)
        {
        }

        private void Designer_ModelChanged(object sender, EventArgs e)
        {
            isModified = true;
            UpdateTitle();
        }

The CreateToolbox method creates a Toolbox containing a small subset of the standard activities. There’s nothing special about the activities that I’ve chosen. Once you’ve completed the examples in this chapter, feel free to change the list of activities that are loaded to your preferred set of activities.

Each set of activities is organized into a named category. Each defined category is then passed to the CreateToolboxCategory private method to construct a ToolboxCategory object. These categories are then added to an instance of the ToolboxControl.

        private ToolboxControl CreateToolbox()
        {
            Dictionary<String, List<Type>> activitiesToInclude =
                new Dictionary<String, List<Type>>();

            activitiesToInclude.Add("Basic", new List<Type>
            {
                typeof(Sequence),
                typeof(If),
                typeof(While),
                typeof(Assign),
                typeof(WriteLine)
            });

            activitiesToInclude.Add("Flowchart", new List<Type>
            {
                typeof(Flowchart),
                typeof(FlowDecision),
                typeof(FlowSwitch<>)
            });

You may notice that instead of adding the ForEach<T> activity, the code references a class named ForEachWithBodyFactory<T>. The ParallelForEach<T> activity has also been replaced with a different class name (ParallelForEachWithBodyFactory<T>). These are the classes that are actually included in the Visual Studio Toolbox. They are activity templates that add the primary activity (for example ForEach<T>) and also add an ActivityAction<T> as a child of the primary activity. The ActivityAction<T> is needed to provide access to the argument that represents each item in the collection.

            activitiesToInclude.Add("Collections", new List<Type>
            {
                typeof(ForEachWithBodyFactory<>),
                typeof(ParallelForEachWithBodyFactory<>),
                typeof(ExistsInCollection<>),
                typeof(AddToCollection<>),
                typeof(RemoveFromCollection<>),
                typeof(ClearCollection<>),
            });

            activitiesToInclude.Add("Error Handling", new List<Type>
            {
                typeof(TryCatch),
                typeof(Throw),
                typeof(TransactionScope)
            });

            ToolboxControl tb = new ToolboxControl();
            foreach (var category in activitiesToInclude)
            {
                ToolboxCategory cat = CreateToolboxCategory(
                    category.Key, category.Value, true);
                tb.Categories.Add(cat);
            }

            return tb;
        }

The CreateToolboxCategory method is invoked to create a new ToolboxCategory instance. The code creates ToolboxItemWrapper instances for each activity that was defined for the category. The null value that is passed to the ToolboxItemWrapper constructor is where you would pass the file name of a bitmap to associate with the toolbox item. Since the code passes null, a default image will be shown.

        private ToolboxCategory CreateToolboxCategory(
            String categoryName, List<Type> activities, Boolean isStandard)
        {
            ToolboxCategory tc = new ToolboxCategory(categoryName);

            foreach (Type activity in activities)
            {
                if (!loadedToolboxActivities.Contains(activity))
                {
                    //cleanup the name of generic activities
                    String name;
                    String[] nameChunks = activity.Name.Split('`'),
                    if (nameChunks.Length == 1)
                    {
                        name = activity.Name;
                    }
                    else
                    {
                        name = String.Format("{0}<>", nameChunks[0]);
                    }

                    ToolboxItemWrapper tiw = new ToolboxItemWrapper(
                        activity.FullName, activity.Assembly.FullName,
                            null, name);
                    tc.Add(tiw);

                    if (isStandard)
                    {
                        loadedToolboxActivities.Add(activity);
                    }
                }
            }

            return tc;
        }

        private void UpdateTitle()
        {
            String modified = (isModified ? "*" : String.Empty);
            if (String.IsNullOrEmpty(currentXamlPath))
            {
                this.Title = String.Format("{0} - {1}{2}",
                    originalTitle, "unsaved", modified);
            }
            else
            {
                this.Title = String.Format("{0} - {1}{2}",
                    originalTitle,
                    System.IO.Path.GetFileName(currentXamlPath), modified);
            }
        }

        private Boolean IsCloseAllowed()
        {
            Boolean result = true;

            if (isModified)
            {
                if (MessageBox.Show(this,
                    "Are you sure you want to lose unsaved changes?",
                    "Unsaved Changes",
                    MessageBoxButton.YesNo, MessageBoxImage.Warning)
                        == MessageBoxResult.No)
                {
                    result = false;
                }
            }

            return result;
        }
    }
}

Testing the Application

You should now be able to build the project and run it. Figure 17-1 shows the initial view of the DesignerHost project when it is first started. Feel free to give the designer a try by dropping a few activities onto the canvas.

images

Figure 17-1. Initial view of DesignerHost

With the relatively small amount of code that you added, the application already has these capabilities:

  • A fully functional workflow designer canvas including expand and collapse, zoom controls, and so on
  • An integrated, context-sensitive properties grid
  • A functional custom Toolbox
  • Add, delete, move, cut, copy, and paste activities
  • The ability to set property values including expressions
  • The ability to validate the definition and highlight errors
  • The ability to add variables and arguments
  • The ability to clear the designer and start a new definition (the New menu option)

Figure 17-2 shows the designer after I added Sequence, Assign, and WriteLine activities. To enter the expressions for the Assign properties, I first added an Int32 variable named count that is scoped by the Sequence activity.

images

Figure 17-2. DesignerHost with activities

images Note One thing that is missing is IntelliSense when you are entering expressions. Unfortunately, Microsoft didn’t provide an easy way to implement this in your own application.

Executing the Workflow

In this section, you will enhance the application to support the execution of the current workflow definition. This allows the user to declare a workflow and immediately execute it as a preliminary test.

Modifying the Application

To add this functionality, you need to provide an implementation for the menuRun_Click event handler. Here is the code that you need to add to this method:

        private void menuRun_Click(object sender, RoutedEventArgs e)
        {
            try
            {

The code first calls the Flush method on the WorkflowDesigner object. Doing this ensures that the WorkflowDesigner.Text property contains Xaml that includes all of the most recent changes to the definition. The ActivityXamlServices class is used to deserialize the Xaml into a DynamicActivity instance that can be passed to the WorkflowInvoker class for execution.

Prior to execution, the standard Console is routed to a StringWriter. This allows the code to intercept and display the results that would normally go to the console.

                wd.Flush();
                Activity activity = null;
                using (StringReader reader = new StringReader(wd.Text))
                {
                    activity = ActivityXamlServices.Load(reader);
                }

                if (activity != null)
                {
                    StringBuilder sb = new StringBuilder();
                    using (StringWriter writer = new StringWriter(sb))
                    {
                        Console.SetOut(writer);
                        try
                        {
                            WorkflowInvoker.Invoke(activity);
                        }
                        catch (Exception exception)
                        {
                            MessageBox.Show(this,
                                exception.Message, "Exception",
                                MessageBoxButton.OK, MessageBoxImage.Error);
                        }

                        finally
                        {
                            MessageBox.Show(this,
                                sb.ToString(), "Results",
                                MessageBoxButton.OK, MessageBoxImage.Information);
                        }
                    }

                    StreamWriter standardOutput =
                        new StreamWriter(Console.OpenStandardOutput());
                    Console.SetOut(standardOutput);
                }
            }
            catch (Exception exception)
            {
                MessageBox.Show(this,
                    exception.Message, "Outer Exception",
                    MessageBoxButton.OK, MessageBoxImage.Error);
            }
        }

Testing the Application

You can now rebuild and run the DesignerHost project. You can perform a preliminary test by just dropping a WriteLine activity onto the canvas and setting the Text property to a string of your choosing. If you select the Run option from the menu, you should see the expected results.

To perform a slightly more ambitious test that also includes a variable and an argument, I declared a simple workflow using these steps:

  1. Add a While activity.
  2. Declare an Int32 variable named count that is scoped by the While activity.
  3. Add an Int32 input argument named ArgMax. Set the default value of this argument to 5.
  4. Set the While.Condition property to count < ArgMax.
  5. Add a Sequence to the While.Body.
  6. Add an Assign activity to the Sequence activity. Set the Assign.To property to count and the Assign.Value property to count + 1.
  7. Add a WriteLine immediately below the Assign activity. Set the Text property to "Count is" + count.ToString().

Figure 17-3 shows the completed workflow definition. Figure 17-4 shows the results when I select the Run option from the menu.

images

Figure 17-3. About to run a workflow

images

Figure 17-4. Workflow results

Loading and Saving the Definition

The next enhancement you will make is to add support for loading and saving the definition. The Save and SaveAs menu options will be implemented to save the definition to a Xaml file. The Open menu option will allow you to locate an existing Xaml file and load it into the designer.

Modifying the Application

You will make changes to the event handlers for the Save, SaveAs, and Open menu items that were defined but originally did not contain any code. The code that you add to these handlers will invoke new private methods that perform the actual I/O operations. Here is the code that you need to add to the three event handlers:

        private void menuSave_Click(object sender, RoutedEventArgs e)
        {
            if (String.IsNullOrEmpty(currentXamlPath))
            {
                menuSaveAs_Click(sender, e);
            }
            else
            {
                wd.Flush();
                SaveXamlFile(currentXamlPath, wd.Text);
                isModified = false;
                UpdateTitle();
            }
        }

        private void menuSaveAs_Click(object sender, RoutedEventArgs e)
        {
            SaveFileDialog dialog = new SaveFileDialog();
            dialog.AddExtension = true;
            dialog.CheckPathExists = true;
            dialog.DefaultExt = ".xaml";
            dialog.Filter = "Xaml files (.xaml)|*xaml|All files|*.*";
            dialog.FilterIndex = 0;
            Boolean? result = dialog.ShowDialog(this);
            if (result.HasValue && result.Value)
            {
                wd.Flush();
                currentXamlPath = dialog.FileName;
                SaveXamlFile(currentXamlPath, wd.Text);
                isModified = false;
                UpdateTitle();
            }
        }

        private void menuOpen_Click(object sender, RoutedEventArgs e)
        {
            if (!IsCloseAllowed())
            {
                return;
            }

            OpenFileDialog dialog = new OpenFileDialog();
            dialog.AddExtension = true;
            dialog.CheckPathExists = true;
            dialog.DefaultExt = ".xaml";
            dialog.Filter = "Xaml files (.xaml)|*xaml|All files|*.*";
            dialog.FilterIndex = 0;
            Boolean? result = dialog.ShowDialog(this);
            if (result.HasValue && result.Value)
            {
                String markup = LoadXamlFile(dialog.FileName);
                if (!String.IsNullOrEmpty(markup))
                {
                    InitializeDesigner();
                    wd.Text = markup;
                    wd.Load();
                    isModified = false;
                    currentXamlPath = dialog.FileName;
                    UpdateTitle();
                }
                else
                {
                    MessageBox.Show(this,
                        String.Format(
                            "Unable to load xaml file {0}", dialog.FileName),
                        "Open File Error",
                        MessageBoxButton.OK, MessageBoxImage.Error);
                }
            }
        }

Here are the new private methods that you need to add to the MainWindow class:

        private String LoadXamlFile(String path)
        {
            String markup = null;
            try
            {
                using (FileStream stream = new FileStream(path, FileMode.Open))
                {
                    using (StreamReader reader = new StreamReader(stream))
                    {
                        markup = reader.ReadToEnd();
                    }
                }
            }
            catch (Exception exception)
            {
                Console.WriteLine("LoadXamlFile exception: {0}:{1}",
                    exception.GetType(), exception.Message);
            }
            return markup;
        }

        private void SaveXamlFile(String path, String markup)
        {
            try
            {
                using (FileStream stream = new FileStream(path, FileMode.Create))
                {
                    using (StreamWriter writer = new StreamWriter(stream))
                    {
                        writer.Write(markup);
                    }
                }
            }
            catch (Exception exception)
            {
                Console.WriteLine("SaveXamlFile exception: {0}:{1}",
                    exception.GetType(), exception.Message);
            }
        }

Testing the Application

You can now rebuild the project and run it. All of the existing functionality should work in the same way as it did previously. However, you should now be able to use the Save, SaveAs, and Open operations to save your work and load existing Xaml files.

Displaying Validation Errors

The design canvas displays red error indicators on any activity that has a validation error. This works well, but you might also want to list all validation errors in one place. You can accomplish this by developing a custom service that implements the IValidationErrorService interface.

This interface (found in the System.Activities.Presentation.Validation namespace) defines a single ShowValidationErrors method that you must implement in your class. This method is passed a list of ValidationErrorInfo objects. Each object is an error or warning that has been detected. Once your custom service is registered with the EditingContext, the ShowValidationErrors method will be invoked to notify you of any errors or warnings.

Implementing the ValidationErrorService

Add a new C# class to the DesignerHost project, and name it ValidationErrorService. The plan is to display any validation errors and warnings in the ListBox that were added to the bottom of the window. To accomplish this, the constructor of this class is passed a reference to this ListBox. Here is the complete code that you need for this class:

using System;
using System.Activities.Presentation.Validation;
using System.Collections.Generic;
using System.Windows.Controls;

namespace DesignerHost
{
    public class ValidationErrorService : IValidationErrorService
    {
        private ListBox lb;

        public ValidationErrorService(ListBox listBox)
        {
            lb = listBox;
        }

        public void ShowValidationErrors(IList<ValidationErrorInfo> errors)
        {
            lb.Items.Clear();
            foreach (ValidationErrorInfo error in errors)
            {
                if (String.IsNullOrEmpty(error.PropertyName))
                {
                    lb.Items.Add(error.Message);
                }
                else
                {
                    lb.Items.Add(String.Format("{0}: {1}",
                        error.PropertyName,
                        error.Message));
                }
            }
        }
    }
}

Modifying the Application

To use the new service in the DesignerHost application, you first need to add a member variable for the service to the top of the MainWindow class like this:

namespace DesignerHost
{
    public partial class MainWindow : Window
    {
        private ValidationErrorService errorService;

Next, create a new instance of this service in the constructor of the MainWindow class. This must be done before the call to the InitializeDesigner method as shown here:

        public MainWindow()
        {
            InitializeComponent();

            errorService = new ValidationErrorService(this.messageListBox);

            InitializeDesigner();
            StartNewWorkflow();
        }

Finally, you need to add code to the InitializeDesigner method to publish this new service to the EditingContext of the designer. Calling the Publish method makes this new service available to the designer. You can add the call to the Publish method to the end of the InitializeDesigner method as shown here:

        private void InitializeDesigner()
        {

            wd.Context.Services.Publish<IValidationErrorService>(errorService);
        }

Testing the Application

Rebuild the project and run it to test the new functionality. To see the new service in action, you simply need to add an activity and generate a validation error. For example, you can add a Sequence activity and an Assign activity as its child. Since the Assign.To and Assign.Value properties require values, this should immediately generate the errors that are shown at the bottom of Figure 17-5. Correcting any validation errors clears the ListBox.

images

Figure 17-5. Workflow with validation errors

Adding Activities to the Toolbox

The designer application currently supports a subset of the standard activities in the Toolbox. In this section, you will enhance the application to support additional activities. You will add code to the Add Reference menu item handler that allows the user to select an assembly containing additional activities. Any activities that are found in the assembly are added to the Toolbox, making them available for use in the designer.

Additionally, you will enhance the code that loads an existing Xaml file to look for any activities that are not already in the Toolbox. Any new activities will be added to a new category and shown at the top of the Toolbox. This provides the user with access to any activities that are already in use in the loaded definition.

Modifying the Application

Begin by declaring two new member variables at the top of the MainWindow class. The autoAddedCategory will be reused each time a Xaml file is loaded to support any new activities that are found in the definition. The referencedCategories dictionary will be used to track the Toolbox categories for any assemblies that are manually referenced.

namespace DesignerHost
{
    public partial class MainWindow : Window
    {

        private ToolboxCategory autoAddedCategory;
        private Dictionary<String, ToolboxCategory> referencedCategories =
            new Dictionary<String, ToolboxCategory>();

Next, add code to the StartNewWorkflow method to call a new private method. You will implement this new method later in the code. This new method will remove the Toolbox category that contains automatically added activities.

        private void StartNewWorkflow()
        {

            RemoveAutoAddedToolboxCategory();
        }

Modify the event handler for the Open menu item (menuOpen_Click) to call a new method named AutoAddActivitiesToToolbox. This method will inspect the newly loaded definition looking for activities that are not already in the Toolbox. The call to this method should be added after the designer has been initialized with the new definition. I’ve included the entire method to show you the context of where this line should be added:

        private void menuOpen_Click(object sender, RoutedEventArgs e)
        {
            if (!IsCloseAllowed())
            {
                return;
            }

            OpenFileDialog dialog = new OpenFileDialog();
            dialog.AddExtension = true;
            dialog.CheckPathExists = true;
            dialog.DefaultExt = ".xaml";
            dialog.Filter = "Xaml files (.xaml)|*xaml|All files|*.*";
            dialog.FilterIndex = 0;
            Boolean? result = dialog.ShowDialog(this);

            if (result.HasValue && result.Value)
            {
                String markup = LoadXamlFile(dialog.FileName);
                if (!String.IsNullOrEmpty(markup))
                {
                    InitializeDesigner();
                    wd.Text = markup;
                    wd.Load();
                    isModified = false;
                    currentXamlPath = dialog.FileName;
                    UpdateTitle();

                    AutoAddActivitiesToToolbox();
                }
                else
                {
                    MessageBox.Show(this,
                        String.Format(
                            "Unable to load xaml file {0}", dialog.FileName),
                        "Open File Error",
                        MessageBoxButton.OK, MessageBoxImage.Error);
                }
            }
        }

You need to provide an implementation for the Add Reference menu item handler that was previously empty:

        private void menuAddReference_Click(object sender, RoutedEventArgs e)
        {
            OpenFileDialog dialog = new OpenFileDialog();
            dialog.AddExtension = true;
            dialog.CheckPathExists = true;
            dialog.DefaultExt = ".dll";
            dialog.Filter = "Assemblies (.dll)|*dll|All files|*.*";
            dialog.FilterIndex = 0;
            Boolean? result = dialog.ShowDialog(this);

            if (result.HasValue && result.Value)
            {
                AddReferencedActivitiesToToolbox(dialog.FileName);
            }
        }

Finally, you need to add these new private methods to the MainWindow class. The AutoAddActivitiesToToolbox method finds any activities in the newly loaded definition that are not already in the Toolbox. There are two namespaces that are ignored during this process. If a new activity is found but is in one of these namespaces, it is not added to the Toolbox. These are the namespaces that are used for expression support, and you generally don’t want to add these activities to the Toolbox.

        private void AutoAddActivitiesToToolbox()
        {
            ModelService ms = wd.Context.Services.GetService<ModelService>();
            IEnumerable<ModelItem> activities =
                ms.Find(ms.Root, typeof(Activity));

            List<String> namespacesToIgnore = new List<string>
            {
                "Microsoft.VisualBasic.Activities",
                "System.Activities.Expressions"
            };

            HashSet<Type> activitiesToAdd = new HashSet<Type>();
            foreach (ModelItem item in activities)
            {
                if (!loadedToolboxActivities.Contains(item.ItemType))
                {
                    if (!namespacesToIgnore.Contains(item.ItemType.Namespace))
                    {
                        if (!activitiesToAdd.Contains(item.ItemType))
                        {
                            activitiesToAdd.Add(item.ItemType);
                        }
                    }
                }
            }

            RemoveAutoAddedToolboxCategory();

            if (activitiesToAdd.Count > 0)
            {
                ToolboxCategory autoCat = CreateToolboxCategory(
                    "Auto", activitiesToAdd.ToList<Type>(), false);
                CreateAutoAddedToolboxCategory(autoCat);
            }
        }

        private void RemoveAutoAddedToolboxCategory()
        {
            if (autoAddedCategory != null)
            {
                toolboxControl.Categories.Remove(autoAddedCategory);
                autoAddedCategory = null;
            }
        }

        private void CreateAutoAddedToolboxCategory(ToolboxCategory autoCat)
        {
            //add this category to the top of the list
            List<ToolboxCategory> categories = new List<ToolboxCategory>();
            categories.Add(autoCat);
            categories.AddRange(toolboxControl.Categories);
            toolboxControl.Categories.Clear();
            foreach (var cat in categories)
            {
                toolboxControl.Categories.Add(cat);
            }

            autoAddedCategory = autoCat;
        }

The AddReferencedActivitiesToToolbox method loads the specified assembly and looks for any activities. Any activities that are found are added to a new Toolbox category that is named for the assembly.

        private void AddReferencedActivitiesToToolbox(String assemblyFileName)
        {
            try
            {
                HashSet<Type> activitiesToAdd = new HashSet<Type>();

                Assembly asm = Assembly.LoadFrom(assemblyFileName);
                if (asm != null)
                {
                    if (referencedCategories.ContainsKey(asm.GetName().Name))
                    {
                        return;
                    }

                    Type[] types = asm.GetTypes();
                    Type activityType = typeof(Activity);

                    foreach (Type t in types)
                    {
                        if (activityType.IsAssignableFrom(t))
                        {
                            if (!activitiesToAdd.Contains(t))
                            {
                                activitiesToAdd.Add(t);
                            }
                        }
                    }
                }

                if (activitiesToAdd.Count > 0)
                {
                    ToolboxCategory cat = CreateToolboxCategory(
                        asm.GetName().Name, activitiesToAdd.ToList<Type>(), false);
                    toolboxControl.Categories.Add(cat);
                    referencedCategories.Add(asm.GetName().Name, cat);
                }
            }
            catch (Exception exception)
            {
                MessageBox.Show(this,
                    exception.Message, "Exception",
                    MessageBoxButton.OK, MessageBoxImage.Error);
            }
        }

Testing the Application

You should now be ready to rebuild the project and run it. To test the new functionality, you need one or more custom activities. You can use the custom activities that you developed in Chapter 16 for this purpose. Copy these assemblies that you implemented and built in Chapter 16 to the indebug folder of the DesignerHost project. This assumes that you are currently building for debug.

  • ActivityLibrary.dll
  • ActivityLibrary.Design.dll

images Note Please refer to Chapter 16 for detailed information on these custom activities.

Now use the Add Reference menu option of the designer application to add a reference to the ActivityLibrary.dll that you just copied. You need to reference the assembly that is in the same folder as the DesignerHost project; otherwise, .NET won’t be able to resolve the reference correctly. After adding the reference to the assembly, you should see a list of custom activities appear at the bottom of the Toolbox. This is shown in Figure 17-6 under the ActivityLibrary category.

images

Figure 17-6. Toolbox with custom activities

You should now be able to drag the custom activities to the canvas. For example, you can follow these steps to produce the workflow shown in Figure 17-7.

  1. Add a MySequence activity to the empty workflow. Set the Condition property to True.
  2. Add an OrderScope activity as a child of the MySequence activity. Set the OrderId property to 1.
  3. Add an OrderAddItems activity as a child of the OrderScope. Set the Items property to new System.Collections.Generic.List (Of Int32) From {1, 2, 3}.
  4. Save the workflow definition to a file named CustomActivityTest.xaml. You are saving the definition so you can load it to test the automatic activity detection that occurs when loading an existing definition.
images

Figure 17-7. Workflow using custom activities

To test the automatic activity detection, close and restart the DesignerHost project. Doing so ensures that any custom activities that were previously loaded are now cleared. Now open the workflow file that you previously saved (CustomActivityTest.xaml). You should now see that the Auto category in the Toolbox has been populated with the custom activities that you referenced in the workflow, as shown in Figure 17-8.

images

Figure 17-8. Auto Toolbox category

Providing Designer Metadata

The DesignerHost application currently relies upon the standard activity designer metadata that was provided by the DesignerMetadata class. When you called the Register method of this class, the associations between the standard activities and their designers were added to the in-memory MetadataStore object.

In this section, you will modify the application to override the default designer for the While activity. In Chapter 16, you developed a custom MyWhile activity that just happens to match the signature of the standard While activity. You will now instruct the workflow designer to use the custom designer that you developed for this activity with the standard While activity.

The point of all of this is not to provide a better designer for the While activity. The standard designer for this activity works just fine. The point is to demonstrate how you can override the designer for any activity, even those that you didn’t author yourself. This demonstrates one of the key benefits of hosting the workflow designer yourself: you have much greater control over the design experience.

Referencing the Custom Designer

The custom designer that you will use is packaged in the ActivityLibrary.Design assembly that you developed in Chapter 16. You can update the DesignerHost project with a reference to this project in two ways. You can browse to this assembly (which should now be in your indebug folder) and add a direct assembly reference. Or, you can add the existing ActivityLibrary.Design and ActivityLibrary projects from Chapter 16 to your solution for this chapter and build them as part of the solution. You would then need to modify the DesignerHost project with a project reference to the ActivityLibrary.Design project.

Modifying the Application

To override the designer that is used for the While activity, you need to add this small amount of code to the end of the MainWindow constructor:

        public MainWindow()
        {

            //override designer for the standard While activity
            AttributeTableBuilder atb = new AttributeTableBuilder();
            atb.AddCustomAttributes(typeof(While), new DesignerAttribute(
                typeof(ActivityLibrary.Design.MyWhileDesigner)));
            MetadataStore.AddAttributeTable(atb.CreateTable());
        }

Testing the Application

Rebuild the solution, and run the DesignerHost project. Add a While activity to the empty designer canvas. You should see the custom designer that you developed in Chapter 16 instead of the standard designer, as shown in Figure 17-9. For your reference, Figure 17-3 shows the standard designer for the While activity.

images

Figure 17-9. While activity with custom designer

Tracking the Selected Activity

In many cases, you may need to know which activity is currently selected and make decisions based on that selection. In this section, you will enhance the DesignerHost application to adjust the Toolbox based on the currently selected activity.

The current code includes the three flowchart-related activities in the list of standard activities that are added to the Toolbox. However, the FlowDecision and FlowSwitch<T> make sense only if the currently selected activity is a Flowchart activity. These two activities can be added directly to a Flowchart activity only. The code that you will add in this section adds or removes the Toolbox items for the FlowDecision and FlowSwitch<T> activities depending on the currently selected activity in the designer.

Modifying the Application

First, add the declaration for these new member variables to the top of the MainWindow class:

namespace DesignerHost
{
    public partial class MainWindow : Window
    {

        private ToolboxCategory flowchartCategory;
        private List<ToolboxItemWrapper> flowchartActivities =
            new List<ToolboxItemWrapper>();

Add new code to the end of the InitializeDesigner method. The new code adds a subscription to the Selection object in the EditingContext. Each time this object changes, the private OnItemSelected method will be executed.

        private void InitializeDesigner()
        {

            wd.Context.Items.Subscribe<Selection>(OnItemSelected);
        }

Add the new OnItemSelected method. This method determines whether the currently selected activity is a Flowchart. If it is, the other flowchart-related activities are added to the flowchart category of the Toolbox. If the selection is not a Flowchart activity, the activities are removed from the category.

        private void OnItemSelected(Selection item)
        {
            ModelItem mi = item.PrimarySelection;
            if (mi != null)
            {
                if (flowchartCategory != null && wd.ContextMenu != null)
                {
                    if (mi.ItemType == typeof(Flowchart))
                    {
                        //add the flowchart-only activities
                        foreach (var tool in flowchartActivities)
                        {
                            if (!flowchartCategory.Tools.Contains(tool))
                            {
                                flowchartCategory.Tools.Add(tool);
                            }
                        }
                    }
                    else
                    {
                        //remove the flowchart-only activities
                        foreach (var tool in flowchartActivities)
                        {
                            flowchartCategory.Tools.Remove(tool);
                        }
                    }
                }
            }
        }

You also need to add code to the existing CreateToolbox method. The new code saves the flowchart category and the FlowDecision and FlowSwitch<T> Toolbox items in the new member variables. This allows the code in the OnItemSelected method (shown previously) to easily modify the category object.

        private ToolboxControl CreateToolbox()
        {

            ToolboxControl tb = new ToolboxControl();
            foreach (var category in activitiesToInclude)
            {
                ToolboxCategory cat = CreateToolboxCategory(
                    category.Key, category.Value, true);
                tb.Categories.Add(cat);

                if (cat.CategoryName == "Flowchart")
                {
                    flowchartCategory = cat;
                    foreach (var tool in cat.Tools)
                    {
                        if (tool.Type == typeof(FlowDecision) ||
                            tool.Type == typeof(FlowSwitch<>))
                        {
                            flowchartActivities.Add(tool);
                        }
                    }
                }
            }

            return tb;
        }

Testing the Application

Rebuild and run the DesignerHost project. To test the new functionality, add a Flowchart as the root of the workflow. All the flowchart-related activities are shown in the Toolbox. Now add a WriteLine to the Flowchart you just added. Notice that when the WriteLine activity is selected, the FlowDecision and FlowSwitch<T> activities are removed from the Toolbox, as shown in Figure 17-10.

images

Figure 17-10. Flowchart with context-sensitive Toolbox

If you select the root Flowchart activity, all the flowchart-related activities are added back to the Toolbox.

Modifying the Context Menu

Since the application is now capable of reacting to the currently selected activity, it makes sense to enhance the context menu. The goal of the changes that you are about to make is to add a new context menu item when a Flowchart activity is selected. The new context menu will allow the user to add a FlowDecision activity to the Flowchart. When any other type of activity is selected, the context menu item will be removed. These changes also demonstrate how you can programmatically add activities to the model.

Modifying the Application

Begin this set of changes by adding a new member variable to the top of the MainWindow class, as shown here:

namespace DesignerHost
{
    public partial class MainWindow : Window
    {

        private MenuItem miAddFlowDecision;

Next, modify the MainWindow constructor to call a new CreateContextMenu method. This call should be done before the call to the InitializeDesigner method, as shown here:

        public MainWindow()
        {

            //create a context menu item
            CreateContextMenu();

            InitializeDesigner();
            StartNewWorkflow();

        }

Modify the OnItemSelected method that you recently added. If the selected activity is a Flowchart, the code adds the new context menu item to the ContextMenu property of the WorkflowDesigner. If any other activity type has been selected, the context menu item is removed. I’ve highlighted the new code in the listing that follows:

        private void OnItemSelected(Selection item)
        {
            ModelItem mi = item.PrimarySelection;
            if (mi != null)
            {
                if (flowchartCategory != null && wd.ContextMenu != null)
                {
                    if (mi.ItemType == typeof(Flowchart))
                    {

                        //add the flowchart-only activities
                        foreach (var tool in flowchartActivities)
                        {
                            if (!flowchartCategory.Tools.Contains(tool))
                            {
                                flowchartCategory.Tools.Add(tool);
                            }
                        }

                        if (!wd.ContextMenu.Items.Contains(miAddFlowDecision))
                        {
                            wd.ContextMenu.Items.Add(miAddFlowDecision);
                        }
                    }
                    else
                    {
                        //remove the flowchart-only activities
                        foreach (var tool in flowchartActivities)
                        {
                            flowchartCategory.Tools.Remove(tool);
                        }
                        wd.ContextMenu.Items.Remove(miAddFlowDecision);
                    }
                }
            }
        }

Finally, add these two new methods to the MainWindow class. The CreateContextMenu is invoked to create the context menu item. It assigns the AddFlowDecision_Click handler to the Click event of this new menu item. The code in the event handler first verifies that the currently selected activity is a Flowchart. If it is, a new FlowDecision activity instance is created and added to the Nodes property of the Flowchart activity.

        private void CreateContextMenu()
        {
            miAddFlowDecision = new MenuItem();
            miAddFlowDecision.Header = "Add FlowDecision";
            miAddFlowDecision.Name = "miAddFlowDecision";
            miAddFlowDecision.Click +=
                new RoutedEventHandler(AddFlowDecision_Click);
        }

        private void AddFlowDecision_Click(object sender, RoutedEventArgs e)
        {
            ModelItem selected =
                wd.Context.Items.GetValue<Selection>().PrimarySelection;
            if (selected != null)
            {
                if (selected.ItemType == typeof(Flowchart))
                {
                    ModelProperty mp = selected.Properties["Nodes"];
                    if (mp != null)
                    {
                        mp.Collection.Add(new FlowDecision());
                    }
                }
            }
        }

Testing the Application

After rebuilding the DesignerHost project, you can run it and test the new functionality. Add a Flowchart as the root activity. Right-click to open the context menu for this activity. You should see the new Add FlowDecision menu item. If you select this menu item, a new FlowDecision activity should be added to the Flowchart. Figure 17-11 shows the new context menu item.

images

Figure 17-11. Flowchart with context menu

Once the FlowDecision has been added, select it and right-click to open the context menu. The Add FlowDecision menu item should now be removed.

Locating the Arguments

At some point, you may need to inspect the input and output arguments for the workflow definition. Their location within the activity model is not necessarily intuitive. They are actually located in the Properties collection of the root activity. To retrieve them, you first retrieve the ModelService from the EditingContext of the designer. You can then use the Root property of the ModelService to navigate to the Properties collection. This is a collection of DynamicActivityProperty objects. Each input or output argument is in this collection along with any other properties.

To demonstrate this, you will modify the handler for the Run menu item to display any arguments that have default values assigned to them.

Modifying the Application

The only necessary change is to add code to the menuRun_Click event handler to access the arguments. Here is the enhanced code for this handler with the new code highlighted:

        private void menuRun_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                wd.Flush();
                Activity activity = null;
                using (StringReader reader = new StringReader(wd.Text))
                {
                    activity = ActivityXamlServices.Load(reader);
                }

                if (activity != null)
                {
                    //list any defined arguments
                    messageListBox.Items.Clear();
                    ModelService ms =
                        wd.Context.Services.GetService<ModelService>();
                    ModelItemCollection items =
                        ms.Root.Properties["Properties"].Collection;
                    foreach (var item in items)
                    {
                        if (item.ItemType == typeof(DynamicActivityProperty))
                        {
                            DynamicActivityProperty prop =
                                item.GetCurrentValue() as DynamicActivityProperty;
                            if (prop != null)
                            {
                                Argument arg = prop.Value as Argument;
                                if (arg != null)
                                {
                                    messageListBox.Items.Add(String.Format(
                                        "Name={0} Type={1} Direction={2} Exp={3}",
                                            prop.Name, arg.ArgumentType,
                                            arg.Direction, arg.Expression));
                                }
                            }
                        }
                    }


        }

Testing the Application

Rebuild and run the DesignerHost project to see the new functionality in action. To demonstrate this, you can use the same set of activities that you used in a previous example. Please follow these steps to define the test workflow:

  1. Add a While activity.
  2. Declare an Int32 variable named count that is scoped by the While activity.
  3. Add an Int32 input argument named ArgMax. Set the default value of this argument to 5.
  4. Set the While.Condition property to count < ArgMax.
  5. Add a Sequence to the While.Body.
  6. Add an Assign activity to the Sequence activity. Set the Assign.To property to count and the Assign.Value property to count + 1.
  7. Add a WriteLine immediately below the Assign activity. Set the Text property to "Count is" + count.ToString().

Now, select the Run menu item to execute the workflow using the default value of 5 for the ArgMax argument. In addition to the correct results from the execution of the workflow, you should also see the details of the input argument shown in the ListBox at the bottom of the window, as shown in Figure 17-12.

images

Figure 17-12. Displaying runtime arguments

Summary

This chapter focused on hosting the workflow designer in your own application. The chapter began with a simple application that hosted the major designer components. This same example application was enhanced in subsequent sections of this chapter.

You enhanced this application with the ability to load, save, and run workflow definitions as well as display validation errors. Other enhancements included the ability to reference activities in other assemblies and to automatically populate the Toolbox with activities that are found in loaded definitions. You saw how to override the activity designer that is assigned to standard activities and to locate any arguments in the workflow model. The application was also enhanced to provide a context-sensitive menu and Toolbox.

In the next chapter, you will learn how to execute some WF 3.x activities in the WF 4 environment.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.119.170