Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Rehosting WorkflowDesigner

0.00/5 (No votes)
2 May 2019 1  
Rehosting of WorkflowDesigner in WPF and Windows Forms application, and invoking the workflow

Introduction

This article shows how to create and execute workflows. It uses Microsoft's System.Activities.Presentation.WorkflowDesigner class for the visual design of workflows, and the System.Activities.Presentation.WorkflowInvoker class to execute the workflow. A WPF as well as a WinForm application is provided.

Workflow design:

Workflow execution:

This article also shows how to create and execute rules. It uses Microsoft's System.Workflow.Activities.Rules.Design.RuleSetDialog class for the visual design of rules, and the System.Workflow.Activities.Rules.RuleExecution class to execute the rules.

Rules design:

Rules execution:

Background

Microsoft's Rehosting the Workflow Designer article describes how to use the WorkflowDesigner class. This article describes how to bundle the toolbox, the designer, and the properties pane into one user control, and how to use this user control in a WPF and a WinForm application.

Using the Code

There are three projects in this solution:

  1. WFLibrary: a library containing the WPF user control WFDesigner and the plain old CLR object Order
  2. WPFApp: a WPF application to design and run workflows, and to design and run rules
  3. WinFormApp: a WinForm application to design and run workflows

WFLibrary

The WFDesigner user control consists of a grid with three columns: the toolbox, the designer, and the properties view. It is not possible to add the WorkflowDesigner in the visual designer, or in the XAML, it has to be added by code in the constructor.

Public Sub New()
    ' This call is required by the designer.
    InitializeComponent()
    ' Add any initialization after the InitializeComponent() call.
    Dim dm = New DesignerMetadata()
    dm.Register()
    Dim tc = GetToolboxControl()
    Grid.SetColumn(tc, 0)
    grid1.Children.Add(tc)
    RecreateDesigner(GetActivityEmpty)
End Sub

The DesignerMetadata.Register method is needed to register designer metadata for Microsoft.Activities.Design.

ToolboxControl

The GetToolboxControl function creates an instance of the System.Activities.Presentation.Toolbox.ToolboxControl class. The following statements are added to the toolbox: If, ForEach, Sequence, Switch, While, Assign, InvokeMethod, Delay, Rethrow, Throw.
The toolbox control is added to the first column of the grid: Grid.SetColumn(tc, 0)

Load Empty Workflow From Local Resource

The project contains the empty workflow ActivityEmpty.xaml, with just one Sequence activity. This file's build action is Resource. The function GetActivityEmpty reads this file:

Private Function GetActivityEmpty() As String
    Dim assembly = Me.GetType().Assembly
    Dim manifestResourceStream = assembly.GetManifestResourceStream
                                 (assembly.GetName.Name & ".g.resources")
    Dim resourceReader = New Resources.ResourceReader(manifestResourceStream)
    Dim resourceEnumerator = resourceReader.GetEnumerator
    While resourceEnumerator.MoveNext
        If resourceEnumerator.Key = "activityempty.xaml" Then
            Dim streamReader = New StreamReader(CType(resourceEnumerator.Value, Stream))
            Return streamReader.ReadToEnd
        End If
    End While
End Function

WorkflowDesigner

The RecreateDesigner method creates an instance of the System.Activities.Presentation.WorkflowDesigner class:

Private Sub RecreateDesigner(text As String)
    WD = New WorkflowDesigner
    AddHandler WD.ModelChanged, AddressOf OnModelChanged
    WD.Text = text
    If WD.Text > "" Then
        WD.Load()
    End If
    WfDesignerBorder.Child = WD.View
    WfPropertyBorder.Child = WD.PropertyInspectorView
End Sub

The WorkflowDesigner displays the workflow (in XAML) that is set on the Text property. It provides two views: the designer view (.View) is displayed in the middle column, and the property view (.PropertyInspectorView) is displayed in the right column. It is not possible to change the Text property. Therefore, the WorkflowDesigner has to be recreated, each time another workflow must be displayed. That is why the user control's Text property, calls the RecreateDesigner method:

Public Property Text As String
    Get
        WD.Flush()
        Return WD.Text
    End Get
    Set(value As String)
        RecreateDesigner(value)
    End Set
End Property

ModelChanged Event

The user control handles the WorkflowDesigner's ModelChanged event, and raises its own ModelChanged event. Therefore, a client application may be informed, when the workflow changes.

 Private Sub RecreateDesigner(text As String)
     ...
     AddHandler WD.ModelChanged, AddressOf OnModelChanged
     ...
 End Sub

Private Sub OnModelChanged(sender As Object, e As EventArgs)
     RaiseEvent ModelChanged(Me, e)
 End Sub

Validation Errors

The user control exposes the readonly property NrValidationErrors. Therefore, a client application may check if the workflow is valid. Unfortunately, this information is retrieved through reflection, because it is private in the WorkflowDesigner. That means, this implementation might not work in a future version of WorkflowDesigner.

Public ReadOnly Property NrValidationErrors As Integer
    Get
        Dim prop = WD.GetType.GetProperty("ValidationService", -1)
        Dim srv = prop.GetValue(WD)
        prop = srv.GetType.GetProperty("ValidationErrors", -1)
        Dim dict = prop.GetValue(srv)
        prop = dict.GetType.GetProperty("Count", -1)
        Return CInt(prop.GetValue(dict))
    End Get
End Property

WPFApp

This WPF application has three tabs:

  1. Design: Contains a WFDesigner user control and a text box.

    It provides three menus:
    • New: Creates a new empty workflow
    • Open: Opens a workflow from a .xaml file
    • Save: Saves a workflow to a .xaml file
  2. Run: Contains two property grids displaying on the left the order before the workflow execution and on the right the order after the workflow execution.

    It provides one menu:
    • Run: Executes the workflow defined in the first tab.
  3. Rules: Contains two property grids displaying on the left the order before the rules execution and on the right the order after the rules execution.

    It provides three menus:
    • New: Creates a new rules definition
    • Open: Opens a rules definition from a .rules file
    • Run: Executes the rules defined in the .rules file

    The RuleSetDialog class is used to define the rules. The RuleExecution class is used to execute the rules:

    Private Sub MenuItem_RulesRun_Click(sender As Object, e As RoutedEventArgs)
        OrderOut = OrderIn.Copy
        If File.Exists(TextBoxRulesFilePath.Text) Then
            Dim ruleSet = RulesLoad(TextBoxRulesFilePath.Text)
            Dim validation = New RuleValidation(OrderOut.GetType(), Nothing)
            Dim engine = New RuleExecution(validation, OrderOut)
            ruleSet.Execute(engine)
            PropertyGridRulesOrderOut.SelectedObject = OrderOut
            PropertyGridRulesOrderOut.Update()
            MsgBox("ok")
        Else
            MsgBox("Must specify rules.")
        End If
    End Sub
    

WinFormApp

This Windows Forms application is like the WPFApp without the third tab (the Rules functionality). A WPF control may not be added as is to a Windows Form. The ElementHost class is needed to interface a Windows Form with a WPF user control. In the constructor of the form, the method InitWFDesigner is called. However, events from WPF controls can be consumed directly like the ModelChanged event.

Private Sub WFDesigner_ModelChanged(sender As Object, e As System.EventArgs)
    ToolStripMenuItemSave.Enabled = True
    RemoveHandler WFDesigner1.ModelChanged, AddressOf WFDesigner_ModelChanged
End Sub

Private Sub InitWFDesigner()
    Dim elemHost = New ElementHost
    elemHost.Dock = DockStyle.Fill
    PanelDesign.Controls.Add(elemHost)
    WFDesigner1 = New WFLibrary.WFDesigner
    AddHandler WFDesigner1.ModelChanged, AddressOf WFDesigner_ModelChanged
    elemHost.Child = WFDesigner1
    MaximizeBox = True
End Sub

Walkthroughs

Open and compile WorkflowTest.sln with Visual Studio.

Workflow with WPF

  1. Startup project is WPFApp. Press F5 to run it.
  2. Open a workflow through File/Open, choose WFLibrary/Activity1.xaml. The workflow designer appears:

  3. Change to tab "Run".
  4. Run the workflow through File/Run. Notice that the Status changes to Ready. This is because the expression is: IF order.GetEUREquivalent() < 10 Then order.Status = StatusEnum.Settled Else order.Status = StatusEnum.Ready:

  5. Change the amount in the left Order to 9 and rerun the workflow with File/Run. The Status in the right Order should change to Settled, because 9 < 10.

  6. Change to tab "Design" and modify the workflow, for example make the condition order.GetEUREquivalent() < 9.
  7. Rerunning the workflow, should produce Status = Ready.
  8. You may add other assignments, if statements, etc., and see the effect when running the workflow.
  9. You may create from scratch a new workflow through File/New.

Rules

  1. Change to tab "Rules".
  2. Open a set of rules through File/Open, choose WFLibrary/Rules1.rules. The rule set designer opens:

  3. Click Cancel.
  4. Run the set of rules through File/Run. Notice that the Status changes to Ready. This is because the condition is: IF order.GetEUREquivalent() < 10, the Then Action is: order.Status = StatusEnum.Settled and the Else Action is: order.Status = StatusEnum.Ready:

  5. Change the amount in the left Order to 9 and rerun the rules with File/Run. The Status in the right Order should change to Settled, because 9 < 10.

  6. Open again the set of rules through File/Open and modify the rule, for example, make the condition order.GetEUREquivalent() < 9.
  7. Rerunning the rule, should produce Status = Ready.
  8. You may add other rules, and see the effect when running the set of rules.
  9. You may create from scratch a new set of rules through File/New.

Workflow with WinForm

You may start the WinFormApp, and follow the same steps as for the WPFApp. The WinFormApp has only the Workflow functionality, it does not have the Rules functionality.

Points of Interest

  • Bundling the WorkflowDesigner with toolbox and properties pane into a WPF user control in WFLibrary.WFDesigner
  • Executing a workflow with WorkflowInvoker in WPFApp.MainWindow.MenuItem_Run_Click() and WinFormApp.Form1.ToolStripMenuItemRun_Click()
  • Reading local resource files in WFLibrary.WFDesigner.GetActivityEmpty()
  • Creating and showing a set of rules using RuleSetDialog in WPFApp.MainWindow.MenuItem_RulesOpen_Click()
  • Executing a set of rules using RuleExecution in WPFApp.MainWindow.MenuItem_RulesRun_Click()
  • Hosting a WPF control in a WinForm application with ElementHost in WinFormApp.Form1.InitWFDesigner
  • Consuming events from a WPF control in WinFormApp.Form1.WFDesigner_ModelChanged()
  • Using the WPF PropertyGrid from Extended.Wpf.Toolkit 3.5.0 Nuget package in WPFApp.MainWindow.xaml. There is no native WPF property grid as in WinForms

History

  1. WorkflowDesigner bundled in WFLibrary.WFDesigner, and using WFDesigner in WPF application and WinFormApplication. Executing workflow with WorkflowInvoker. Editing set of rules with RuleSetDialog and executing a set of rules with RuleExecution.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here