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:
WFLibrary
: a library containing the WPF user control WFDesigner
and the plain old CLR object Order
WPFApp
: a WPF application to design and run workflows, and to design and run rules 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()
InitializeComponent()
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:
- 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
- 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.
- 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
- Startup project is
WPFApp
. Press F5 to run it. - Open a workflow through File/Open, choose WFLibrary/Activity1.xaml. The workflow designer appears:
- Change to tab "Run".
- 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
:
- 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
.
- Change to tab "Design" and modify the workflow, for example make the condition
order.GetEUREquivalent() < 9
. - Rerunning the workflow, should produce
Status = Ready
. - You may add other assignments,
if
statements, etc., and see the effect when running the workflow. - You may create from scratch a new workflow through File/New.
Rules
- Change to tab "Rules".
- Open a set of rules through File/Open, choose WFLibrary/Rules1.rules. The rule set designer opens:
- Click Cancel.
- 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
:
- 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
.
- Open again the set of rules through File/Open and modify the rule, for example, make the condition
order.GetEUREquivalent() < 9
. - Rerunning the rule, should produce
Status = Ready
. - You may add other rules, and see the effect when running the set of rules.
- 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
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
.