This is the fourth of a series about using Workflow Foundation to control your UI Logic in a WPF application. The full table of contents:
Workflow as controller: Introducing <M,V,C> where M: ViewModel, V : WPF, C : WF Part II, starting the application, and the adapter Intermezzo: new sample application Part III, your first view Part IV, decoupling view from controller Part V, marshalling commands from WPF to WF Part VI, Injecting a controller in a subview / workspace Part VII, IOC on the cheap: injecting and retrieving objects Part VIII, Broadcasting for all to see Recap
In the first post, the complete solution was presented. I am presenting a solution to use workflow as the controller part in your MVC inspired WPF application. It is inspired on the thought that you do not need complex frameworks, because WPF already gives you great power (routed eventing, resources). So, no IOC is used, no event aggregator etcetera: it's taken care of by WPF and WF, a natural fit.
The solution is very decoupled and I feel it's a great advantage to be able to visual your control logic.
In the previous post, I talked about the various ways to show a view, and actually already talked about the decoupling mechanism: commands.
I'm very lucky to have received some good comments from Wekemf about tight coupling. I urge you to read those comments and maybe chime in.
In this post I will not return on that subject, but will quickly address two important activities that help in configuring your system quickly: the SetMainContent activity and the SetDataContext activity and sending WPF commands to WF's HandleCommand.
SetMainContent activity
A controller adapter is a normal WPF contentcontrol. It has the job to participate in the visual tree on behalf of our workflow controller class. To actually attach a view to it, we need to set it's content property.
As shown in the previous post, you can just set on in xaml yourself, but it's more logical to let the workflow decide on the view. Ofcourse, the best approach is totally up to you.
I usually use the stateinitalization activity to set up a view for us. I drag in the SetMainContent activity, and choose a type from the references assemblies.
If it weren't for this step, the controller assembly would not need a reference to the view assembly at all. I found it very cool to be able to select a type with the typebrowser and just have it show up.
The typebrowser is located in an assembly I have put in the externalAssemblies folder. It is a project, not started by me. The code did not work when I got my hands on it, but I managed to fix it by using a hammer. Check out this post to learn more about this great design-time experience!
If you have a business need to decouple even further, you would need to adjust the SetMainContent activity, and instead of sending a real Type, send a string key or whatever. Then you would create some mapping functionality to map that key to the actual view.
When the adapter get's notified by the SetMainContentMessage that it needs to set a content, it will just create the view (using reflection) and place it as it's own content.
SetDataContext activity
I do not like MVP at all, where the presenter talks back to the view directly (using an interface or something). I feel it's way too 'pushy' and way too much work. I believe in databinding (especially WPF bindings, I think Microsoft got it right this time). You view should just bind to your domain objects. In many cases, it's better to create a wrapper for the domainobjects, so you have the opportunity to supply some shortcut properties or view specific stuff: you might have a list of products, and you want the view to display the sum of the prices. That is a great opportunity for the viewmodel to expose a 'Sum' property that the view can simply bind to.
The object that is used as a ViewModel should live with the controller who will be able to communicate with it.
I usually create an internal public class, simply called ViewModel and have the controller inject that class with domain objects.
The Set DataContext activity is very similar to the SetMainContent activity, in that it let's the adapter know it has to set a datacontext on itself.
You configure the SetDataContext activity simply by choosing a field or property of your controller.
In small sample applications, I have used the 'invoking' event, to hook up some code that actually initializes the ViewModel object.
Sending WPF commands to the Workflow: HandleCommandActivity
The HandleCommandActivity is really what makes using the solution so easy. I have blogged about it already extensively, and I will just summarize here:
Workflow has a difficult communication story. You need to define your incoming and outgoing calls in an ExternalDataExchangeService. Then you have to hook up events in your workflow to listen to incoming calls/events. It is not possible to listen to the same events in two different states, without using the very difficult CorrelationTechnique.
This is not necessary for our usage. I have created the HandleCommand activity to just listen to a queue with a specific name. That name is defined by the command we are listening to. So, if you want your workflow to react when you send it the string 'workflowRules', you would just drag in the HandleCommand and configure the Command property to read 'workflowRules'. No need to setup a special event for it.
The commandService class has a PostCommand method, that you can call to put a message on the queue. That's all there is to it.
So, when we receive a WPF command, we cast it to a RoutedUI command. The commandname is used to form a SimpleCommandMessage which can be used as input to the PostCommand method.
1 #region command sinks
2 private void CmdExecuted(object sender, ExecutedRoutedEventArgs e)
3 {
4 string commandname = (e.Command as RoutedUICommand).Name;
5
6 PostCommand(commandname, e.Parameter);
7
8 }
9
10 private void PostCommand(string commandname, object Parameter)
11 {
12 if (implementedCommands.Contains(commandname))
13 {
14 commandSvc.PostCommand(new SimpleCommandMessage(instance.InstanceId, commandname, Parameter));
15 }
16 }
17
18 private void CmdCanExecute(object sender, CanExecuteRoutedEventArgs e)
19 {
20 string commandName = (e.Command as RoutedUICommand).Name;
21
22 if (implementedCommands.Contains(commandName))
23 {
24 e.CanExecute = commandSvc.CanExecute(new SimpleCommandMessage(instance.InstanceId, commandName));
25 }
26 }
27
28 #endregion
As you can see, I first check if the workflow even implements such a command. If not, it would be too expensive to send it to the workflow.
Also, check out the CmdCanExecute method. It actually makes it possible for the workflow to put rules on the HandleCommand activity that are used to figure out if a command can be executed. For instance, if you are not authorized to do something, the command was never in CanExecute, so the button that hooks up to it was always dimmed!
I hope that clears up some questions. Let me know what you think!