Methods of WPF – WF Data Exchange, Direct WPF – WF Data Binding






4.92/5 (19 votes)
Small tutorial for WPF - WF data exchange and databinding implementation in VS2010.
Contents
- Introduction
- Run a workflow in a WPF application
- Databinding of FrameworkElement and workflow variables
- Resume bookmark from WPF
- Control UI state from the workflow
- Create WPF controls in the workflow
- Error handling
Introduction
If you want to use WF 4.0 in a WPF based Desktop application, you must ensure data exchange between the workflow and WPF controls. In this article (wrote in tutorial form), I would like to discuss some tips of data exchange implementations; first of all, «direct» data binding between WPF and WF. I hope that your feedback will help me improve and extend the proposed solutions.
This tutorial demonstrates the creation of simple WPF/WF applications with «direct» data binding between the WPF window and the WF variables, in six steps. The resulting application code is available in the download link above.
Run a workflow in a WPF application
- Create a “WpfApplication1” project using the WPF Application template.
- Add the “ActivityLibrary1” project using the Workflow Activity Library template.
- Add “Flowchart1.xaml” to ActivityLibrary1 using the “Add>New Item…” menu and a Workflow Flowchart template. Add a “
WriteLine
” activity withText
="Flowchart1 completed" toFlowchart1
using Workflow Designer: - Add
System.Activities
andActivityLibrary1
references to “WpfApplication1”. Add aWorkflowInstance
member to Window1.xaml.cs: - Add a
StackPanel
with a "Run Flowchart1" button to Window1.xaml: - Add a click event handler to Window1.xaml.cs:
WorkflowInstance _instance;
<StackPanel>
<Button Content="Run Flowchart1"
HorizontalAlignment="Left" Name="button1"
Margin="20" Click="button1_Click" />
</StackPanel>
private void button1_Click(object sender, RoutedEventArgs e)
{
_instance = new WorkflowInstance(new Flowchart1());
_instance.Run();
}
Compile and start a solution WpfApplication1. Now we can start a workflow instance using a button click and check the “Flowchart1 completed" text in the output window. We completed here a simple but important preparation for the more interesting thing!
Databinding of FrameworkElement and workflow variables
- Add a “
variable1
” string variable with a "MyVariable" default value to theFlowchart1
workflow in the workflow designer. - Create an interface
IContextProvider
inActivityLibrary1
using the “Add>Class” menu and the interface template
.
- Add a
SetDataContext
workflow element toActivityLibrary1
using “Add>New Item…” and the Workflow > Workflow Element template. Set it as the first activity inFlowchart1
. Note: It is very important that theSetDataContext
activity does not have arguments. Only in this case will it have the same context as the workflow. - Add a 5 sec delay activity to
Flowchart1
afterSetDataContext
: - Add a “
_workflowContext
” member to Window1.xaml.cs: - Add the
IContextProvider
interface to ourWindow1
class – we will store the workflow context in it: Note: we need aDispatcher
to setWindow1.DataContext
from theSetDataContext
activity because a workflow will not be run on a GUI thread. - Add a workflow instance
OnCompleted
event handler tobutton1_Click
. RemoveWindow1.DataContext
in it. After the workflow is completed, the context will be disposed and cannot be used in WFP! - Add a
IContextProvider
implementation (Window1
) to Extensions before the instance is run for providing access to this interface from the workflow activities: - Add a
TextBox
to Window1.xaml. Add a binding to the text withvariable1
: Now, after starting the workflow, it will set the “MyVariable” text in theTextBox
. After a 5 sec delay, the workflow will be completed,DataContext
ofWindow1
will be removed, and theTextBox
will be again empty.
using System.Activities;
namespace ActivityLibrary1
{
public interface IContextProvider
{
WorkflowDataContext WorkflowContext { get; set; }
}
}
public class SetDataContext: CodeActivity
{
protected override void Execute(CodeActivityContext context)
{
IContextProvider rootDataContext = context.GetExtension<icontextprovider>();
rootDataContext.WorkflowContext = context.DataContext;
}
}
WorkflowDataContext _workflowContext;
public partial class Window1 : Window, IContextProvider
public WorkflowDataContext WorkflowContext
{
get { return _workflowContext; }
set
{
this.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
(Action)delegate() { this.DataContext = value; });
_workflowContext = value;
}
}
Note: we can use the DataContext
of the child activities in the same way.
_instance.OnCompleted = delegate(WorkflowCompletedEventArgs we)
{
this.Dispatcher.Invoke(DispatcherPriority.Normal,
(Action)delegate() { this.DataContext = null; });
};
__instance.Extensions.Add(this);
<TextBox Margin="20" Text="{Binding variable1, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Left" Name="textBox1" VerticalAlignment="Top"/>
Now after starting the workflow, it will set the “MyVariable” text in the TextBox
. After a 5 sec delay, the workflow will be completed, the DataContext
of Window1
will be removed, and TextBox
will be empty again.
Resume bookmark from WPF
Okay, but maybe we want to control the workflow from the WPF window. In this case, we need to supply some events in the GUI:
- Add a “
ReadBookmark
” activity toActivityLibrary1
using “Add>New Item…” and the Workflow Element Template. Add a string “Command” input argument. Set the input command as the result of the activity in theContinue()
method. Build ActivityLibrary1. - Add a Pick activity to
Flowchart1
with aPickBranch
instead of a 5 sec delay. AddReadBookmark
activities as a trigger toPickBranch
. Set theCommand
argument ofReadBookmark
to a value “stop". - Now, the workflow will wait and resume the “stop” bookmark to continue. We can resume this bookmark with a new
Window1
button “Stop Flowchart1”: - For the control workflow state, add a
_isWorkflowStarted bool
member to Window1.xaml.cs. Set_isWorkflowStarted
totrue
before the workflow is started: - Add a window closed event handler and terminate the workflow in it, if workflow has started:
public class ReadBookmark<t> : NativeActivity<t>
{
// Arguments:
public InArgument<string> Command { get; set; }
protected override void Execute(ActivityExecutionContext context)
{
context.CreateNamedBookmark(Command.Get(context),
new BookmarkCallback(this.Continue));
}
void Continue(ActivityExecutionContext context,
Bookmark bookmark, object obj)
{
this.Result.Set(context, Command.Get(context));
}
}
<Button Content="Stop Flowchart1"
HorizontalAlignment="Left" Margin="20"
Name="button2" VerticalAlignment="Top"
Click="button2_Click" />
private void button2_Click(object sender, RoutedEventArgs e)
{
_instance.ResumeBookmark("stop", null);
}
_isWorkflowStarted = true;
_instance.Run();
And, false
when the workflow is completed:
_instance.OnCompleted = delegate(WorkflowCompletedEventArgs we)
{
this.Dispatcher.Invoke(DispatcherPriority.Normal, (Action)delegate()
{
this.DataContext = null; });
_isWorkflowStarted = false;
};
private void Window_Closed(object sender, EventArgs e)
{
if (_instance!=null && _isWorkflowStarted==true)
_instance.Terminate("Main Window closed");
}
Now, after starting the workflow ,we see the “MyVariable” text. After this, click the Stop button. The “stop” bookmark will be resolved and flowchart1
will be completed. The DataContext
of Window1
will be removed and TextBox
will be empty again.
Control UI state from the workflow
Now, we will carefully enable the state of the buttons:
- Set the initial
IsEnabled
state of the stop button tofalse
. - Add the
UpdateUIState()
method to theIContextProvider
interface. - Add an implementation of
UpdateUIState()
to theWindow1
class. Set the stop buttonIsEnabled
property to depend on the created workflow bookmarks: - Add the “
UpdateUI
” activity toActivityLibrary1
using “Add>New Item…” and the Workflow Element Template. Build ActivityLibrary1. - Add the
UpdateUI
code activity toFlowchart1
after aPick
activity: - Update UI using
ReadBookmark.Execute
: - Disable
button1
when workflow starts and enable it if the workflow has completed.
<Button Content="Stop Flowchart1" IsEnabled="False">
public interface IContextProvider
{
WorkflowDataContext WorkflowContext { get; set; }
void UpdateUIState();
}
public void UpdateUIState()
{
this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (Action)delegate()
{
button2.IsEnabled = false;
foreach (BookmarkInfo bookmark in _instance.GetAllBookmarks())
{
switch (bookmark.BookmarkName)
{
case "stop":
button2.IsEnabled = true;
break;
}
}
});
}
public class UpdateUI: CodeActivity
{
protected override void Execute(CodeActivityContext context)
{
IContextProvider rootDataContext = context.GetExtension<IContextProvider>();
rootDataContext.UpdateUIState();
}
}
//update UI
IContextProvider rootDataContext = context.GetExtension<IContextProvider>();
rootDataContext.UpdateUIState();
private void button1_Click(object sender, RoutedEventArgs e)
{
button1.IsEnabled = false;
//…
_instance.OnCompleted = delegate(WorkflowCompletedEventArgs we)
{
this.Dispatcher.Invoke(DispatcherPriority.Normal, (Action)delegate()
{
this.DataContext = null;
button1.IsEnabled = true;
});
_isWorkflowStarted = false;
}
};
Now after application starts, the button stop is disabled. Only when the workflow is started and the bookmark “stop” is created, will the stop button be enabled. Great!
Create WPF controls in a workflow
Of course, we can create some GUI direct in our workflow:
- Add the WpfCustomControlLibrary1 project using the “Add>New Project .. ” menu and the Window WPF Custom Control Library template. Add “WorkflowWindow” using the “Add>New Item…” and the WPF Window template. Add a reference to
System.Activities
to the WpfCustomControlLibrary1 project. Add a “Workflow
” property to “WorkflowWindow
”: - Add a
StackPanel
with aTextBox
to “WorkflowWindow.xaml” with binding tovariable1
: - Add a “Close” button to “WorkflowWindow.xaml”. Close the window by clicking this button. Add a Closed handler to “WorkflowWindow.xaml”. Resume the “close” bookmark on window close:
- Add a workflow instance to the extensions on workflow start in the
button1_Click()
method of Window1.cs: - Add a reference to
WpfCustomControlLibrary1
in ActivityLibrary1. - Create an “
OpenWindow
”WorkflowElement
inActivityLibrary1
using “Add>New Item …” and the Workflow>Workflow Element template. It will create aWorkflowWindow
, set itsWorkflow
property to the workflow instance, set theWorkflowWindow
DataContext
to the workflow context, and show the window as a dialog: - Add an “Open” button to Window1.xaml. Resume the “open” bookmark on the “open” button click. Update the button in
UpdateUIState()
: - Add a
_command
string variable toFlowchart1workflow
. Add a PickBranch with theReadBookmark
activity as theTrigger
to thePick
activity using the workflow designer. SetReadBookmark.Command
to “open”. SetReadBookmark.Result
to_command
. - Add a
FlowSwitch
with the expression “_command
” and two cases toFlowchart1
. If the_command
variable is “open”, then switch to theOpenWindow
activity. In the default case (_command
variable is “stop”), switch to theWriteLine
activity withText
="Flowchart1 completed”. - Add a
Pick
activity toFlowchart1
after theOpenWindow
activity. Add aPickBranch
activity withReadBookmark
activities asTrigger
. Set theCommand
argument ofReadBookmark
to the value “close" and save the result in the_command
variable. Add aFlowStep
from this activity to the firstPick
activity inFlowchart1
.
public WorkflowInstance Workflow { get; set; }
<TextBox Margin="20" Text="{Binding variable1}" HorizontalAlignment="Left"/>
Note: to avoid conflicts between Workflow and WPF code we make separate projects.
Note: We use the dialog window for simplicity; for real applications, WPF controls can be used, of course.
void OnClosed(Object sender, EventArgs e)
{
Workflow.ResumeBookmark("close", null);
}
<Button Content="Close" Margin="20"
HorizontalAlignment="Left" Click="button1_Click" />
private void button1_Click(object sender, RoutedEventArgs e)
{
Close();
}
_instance.Extensions.Add(_instance);
public class OpenWindow: CodeActivity
{
protected override void Execute(CodeActivityContext context)
{
IContextProvider rootDataContext = context.GetExtension<IContextProvider>();
WorkflowInstance instance = context.GetExtension<WorkflowInstance>();
Application.Current.Dispatcher.BeginInvoke(
DispatcherPriority.Normal, (Action)delegate()
{
WorkflowWindow1 window = new WorkflowWindow1();
window.DataContext = rootDataContext.WorkflowContext;
window.Workflow = instance;
window.ShowDialog();
});
}
}
<Button Content="Open" IsEnabled="False"
HorizontalAlignment="Left" Margin="20"
Name="button3" Click="button3_Click" />
private void button3_Click(object sender, RoutedEventArgs e)
{
_instance.ResumeBookmark("open", null);
}
public void UpdateUIState()
{
this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (Action)delegate()
{
button2.IsEnabled = false;
button3.IsEnabled = false;
foreach (BookmarkInfo bookmark in
_instance.GetAllBookmarks(new TimeSpan(0, 0, 1)))
{
switch (bookmark.BookmarkName)
{
case "stop":
button2.IsEnabled = true;
break;
case "open":
button3.IsEnabled = true;
break;
}
}
});
}
<p:Pick>
<p:PickBranch>
<p:PickBranch.Trigger>
<a:ReadBookmark x:TypeArguments="x:String"
Command="["stop"]"
Result="[_command]" />
</p:PickBranch.Trigger>
</p:PickBranch>
<p:PickBranch>
<p:PickBranch.Trigger>
<a:ReadBookmark x:TypeArguments="x:String"
Command="["open"]" Result="[_command]" />
</p:PickBranch.Trigger>
</p:PickBranch>
</p:Pick>
Now, on the Open button click, we can open a WPF window from the workflow and change variable1
in this window.
Error handling
Advanced error handling lets you quickly find and correct system errors. Full error handling depends on the requirements (use cases) of the concrete application, but this minimal error handling has to be implemented:
- We use
Application.Current.Dispatcher
to invoke the necessary methods on the GUI thread. We can handle all unhandled Dispatcher exceptions in a special handler: - Unhandled exceptions from the workflow can be handled in
OnUnhandledException
:
void Dispatcher_UnhandledException(object sender,
DispatcherUnhandledExceptionEventArgs e)
{
// TODO: error handling
}
_instance.OnUnhandledException = delegate(WorkflowUnhandledExceptionEventArgs we)
{
// TODO: error handling
};