
In Windows Workflow 4, basic re-hosting of Microsoft’s Workflow Designer WPF control is a simple task. Microsoft has provided a set of controls that can be inserted directly into your own application. This article will guide you through creating your own workflow editor application by re-hosting the System.Activities.Presentation.WorkflowDesigner WPF control.
Create a new WPF application project in Visual Studio. The project must target .NET Framework v4 or above to allow it to target the Workflow 4 (WF4) architecture.
Add references to the following assemblies in your project:
System.Activities
System.Activities.Presentation
System.Activities.Core.Presentation
Code language: plaintext (plaintext)
Next, we need to add the editing controls to the main window. Open the MainWindow.xaml file and
add a namespace declaration in the opening ‘Window’ tag:
xmlns:toolbox="http://schemas.microsoft.com/netfx/2010/xaml/activities/presentation/toolbox"
Code language: HTML, XML (xml)
Define the initial root layout inside the as follows:
<Grid Name="MainGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="4*" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<GridSplitter Grid.Row="0" Grid.Column="0" Width="5" VerticalAlignment="Stretch" />
<GridSplitter Grid.Row="0" Grid.Column="1" Width="5" VerticalAlignment="Stretch" />
<ScrollViewer Grid.Row="0" Grid.Column="0">
<toolbox:ToolboxControl Name="MainToolbox"
ScrollViewer.CanContentScroll="True" />
</ScrollViewer>
</Grid>
Code language: HTML, XML (xml)
This gives us a 3 column layout in which a ‘toolbox’ control has been added to the left-hand pane, the workflow designer will be added to the centre pane once we’ve initialised it, and a ‘properties’ control will be added to the right-hand pane. (The GridSplitter controls just allow the widths of each pane to be changed by the user at runtime.)
Switch to the code-behind file for the main window now. If you want, you can add the additional using directives we’ll need ahead of time (alternatively, you can add them as needed when you paste in the code.)
using Microsoft.Win32;
using System.Activities;
using System.Activities.Core.Presentation;
using System.Activities.Presentation;
using System.Activities.Presentation.Toolbox;
using System.Activities.Statements;
Code language: C# (cs)
Add a class-level variable for the workflow designer:
private WorkflowDesigner _editor;
Code language: C# (cs)
Now let’s register the designer metadata (only needs doing once) and then instantiate and add the workflow designer to the window. In the main window code behind file, alter the constructor and add a method to initialise the workflow designer like this:
private string _existingFile = "";
public MainWindow()
{
this.InitializeComponent();
// Register the runtime metadata for the designer.
new DesignerMetadata().Register();
this.NewDesigner();
this._editor.Load(new ActivityBuilder() { Implementation = new Sequence() });
}
private void NewDesigner()
{
this._existingFile = "";
if (this._editor != null)
{
this.MainGrid.Children.Remove(this._editor.View);
this.MainGrid.Children.Remove(this._editor.PropertyInspectorView);
}
// Instiantiate the workflow designer, setting the initial layout to a sequence.
this._editor = new WorkflowDesigner();
this._editor.Load(new ActivityBuilder() { Implementation = new Sequence() });
// Add the designer to the middle column in the main grid layout.
Grid.SetColumn(this._editor.View, 1);
this.MainGrid.Children.Add(this._editor.View);
// Add the property inspector for the designer to the right-hand column
// of the grid.
Grid.SetColumn(this._editor.PropertyInspectorView, 2);
this.MainGrid.Children.Add(this._editor.PropertyInspectorView);
this._editor.Load(new ActivityBuilder() { Implementation = new Sequence() });
}
Code language: C# (cs)
The workflow designer is added to the window layout in a separate method because you can only call the Load method of the designer once in an object lifecycle (meaning that you will need to instantiate a new instance if a another workflow is being loaded.)
Use of the ActivityBuilder class as the root activity in the designer promotes better compatibility when editing workflows in Visual Studio, so I’d recommend its use even though you don’t have to do so.
Next, let’s initialise the toolbox with the different types of workflow activity that can be used:
private void AddToolToCategory(ToolboxCategory category, Type tool)
{
if (category == null) throw new ArgumentNullException("category");
if (tool == null) throw new ArgumentNullException("tool");
string displayName;
string[] splitName = tool.Name.Split('`');
if (splitName.Length == 1) displayName = tool.Name;
else displayName = string.Format("{0}<>", splitName[0]);
category.Add(
new ToolboxItemWrapper(
tool.FullName, tool.Assembly.FullName, null, displayName));
}
private ToolboxCategory GetToolboxCategory(ToolboxControl target, string category)
{
if (target.Categories.Count > 0)
{
foreach (ToolboxCategory existing in target.Categories)
{
if (existing.CategoryName.Equals(
category,
StringComparison.OrdinalIgnoreCase))
{
return existing;
}
}
}
ToolboxCategory newCategory = new ToolboxCategory(category);
target.Categories.Add(newCategory);
return newCategory;
}
private void InitialiseToolbox()
{
this.MainToolbox.Categories.Clear();
ToolboxCategory category =
this.GetToolboxCategory(this.MainToolbox, "Flow Control");
this.AddToolToCategory(category, typeof(ForEach<>));
this.AddToolToCategory(category, typeof(If));
this.AddToolToCategory(category, typeof(Parallel));
this.AddToolToCategory(category, typeof(ParallelForEach<>));
this.AddToolToCategory(category, typeof(DoWhile));
this.AddToolToCategory(category, typeof(Pick));
this.AddToolToCategory(category, typeof(PickBranch));
this.AddToolToCategory(category, typeof(Sequence));
this.AddToolToCategory(category, typeof(Switch<>));
this.AddToolToCategory(category, typeof(While));
category = this.GetToolboxCategory(this.MainToolbox, "Flowchart");
this.AddToolToCategory(category, typeof(Flowchart));
this.AddToolToCategory(category, typeof(FlowDecision));
this.AddToolToCategory(category, typeof(FlowSwitch<>));
category = this.GetToolboxCategory(this.MainToolbox, "Runtime");
this.AddToolToCategory(category, typeof(Persist));
this.AddToolToCategory(category, typeof(TerminateWorkflow));
category = this.GetToolboxCategory(this.MainToolbox, "Primitives");
this.AddToolToCategory(category, typeof(Assign));
this.AddToolToCategory(category, typeof(Delay));
this.AddToolToCategory(category, typeof(InvokeMethod));
this.AddToolToCategory(category, typeof(WriteLine));
category = this.GetToolboxCategory(this.MainToolbox, "Transactions");
this.AddToolToCategory(category, typeof(CancellationScope));
this.AddToolToCategory(category, typeof(CompensableActivity));
this.AddToolToCategory(category, typeof(Compensate));
this.AddToolToCategory(category, typeof(Confirm));
this.AddToolToCategory(category, typeof(TransactionScope));
category = this.GetToolboxCategory(this.MainToolbox, "Collections");
this.AddToolToCategory(category, typeof(AddToCollection<>));
this.AddToolToCategory(category, typeof(ClearCollection<>));
this.AddToolToCategory(category, typeof(ExistsInCollection<>));
this.AddToolToCategory(category, typeof(RemoveFromCollection<>));
category = this.GetToolboxCategory(this.MainToolbox, "Error Handling");
this.AddToolToCategory(category, typeof(Rethrow));
this.AddToolToCategory(category, typeof(Throw));
this.AddToolToCategory(category, typeof(TryCatch));
}
Code language: C# (cs)
Call the InitialiseToolbox() method at the bottom of the constructor and that’s it, we’ve got a functional workflow editor!
We’ve now got a very basic re-hosted WF4 workflow designer. It isn’t much use yet though, because there isn’t any ability to load or save workflows in the application yet. We’ll add this functionality next.
Let’s modify our single-row main grid so that it has two rows. That way we can host New, Open and Save buttons for managing workflow files in our application. (Note that all the original controls have to be moved down to the 2nd row (row-index=1) of the grid layout.)
<Grid Name="MainGrid">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="4*" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ScrollViewer Grid.Row="1" Grid.Column="0">
<toolbox:ToolboxControl Name="MainToolbox"
ScrollViewer.CanContentScroll="True" />
</ScrollViewer>
<StackPanel Grid.Row="0" Grid.Column="0"
Grid.ColumnSpan="3" Orientation="Horizontal" Margin="5,5,5,5">
<Button Content=" New " Click="NewButton_Click" Margin="5,5,5,5" />
<Button Content=" Open " Click="OpenButton_Click" Margin="5,5,5,5 "/>
<Button Content=" Save " Click="SaveButton_Click" Margin="5,5,5,5 "/>
</StackPanel>
<GridSplitter Grid.Row="1" Grid.Column="0" Width="5" VerticalAlignment="Stretch" />
<GridSplitter Grid.Row="1" Grid.Column="1" Width="5" VerticalAlignment="Stretch" />
</Grid>
Code language: C# (cs)
We’ll also need to insert the workflow designer and the properties viewer in the second row of the grid so add the following in the main window constructor at the appropriate places (just after the equivalent set column index method calls):
Grid.SetRow(this._editor.View, 1);
Grid.SetRow(this._editor.PropertyInspectorView, 1);
Code language: C# (cs)
Event handlers for our new buttons need to be added to the code behind file:
private string _existingFile = "";
private void NewButton_Click(object sender, RoutedEventArgs e)
{
this.NewDesigner();
this._editor.Load(new ActivityBuilder() { Implementation = new Sequence() });
}
private void OpenButton_Click(object sender, RoutedEventArgs e)
{
OpenFileDialog open = new OpenFileDialog();
open.Title = "Select a workflow file to load...";
open.Filter = "Windows Workflow Files (*.xaml)|*.xaml|All Files (*.*)|*.*";
open.FilterIndex = 0;
open.CheckFileExists = true;
open.Multiselect = false;
bool? outcome = open.ShowDialog();
if (outcome != null && (bool)outcome)
{
this.NewDesigner();
this._editor.Load(open.FileName);
this.existingFile = open.FileName;
}
}
private void SaveButton_Click(object sender, RoutedEventArgs e)
{
SaveFileDialog save = new SaveFileDialog();
save.Title = "Select the file to save the workflow to...";
save.Filter = "Windows Workflow Files (*.xaml)|*.xaml|All Files (*.*)|*.*";
save.FilterIndex = 0;
save.CheckPathExists = true;
save.AddExtension = true;
save.OverwritePrompt = true;
if (!string.IsNullOrWhiteSpace(this.existingFile))
{
save.FileName = this.existingFile;
}
bool? outcome = save.ShowDialog();
if (outcome != null && (bool)outcome)
{
this._editor.Save(save.FileName);
MessageBox.Show("The workflow was saved to " + save.FileName, "Saved",
MessageBoxButton.OK, MessageBoxImage.Information);
}
}
Code language: C# (cs)
We now have a bespoke application that allows us to create, load and save XAML workflows for Windows Workflow 4. This solution is fairly limited at present though, and would need some enhancing to be a workable solution in the real world. For example, how would you use activities you developed yourself or that are provided by 3rd parties, and how would you know if your workflow is valid and can be run?
These additional features and more will be covered in other articles.