This is the third 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 showed a wizard style application.
This post follows Part II, starting the application and the adapter. In that post we started our shell and explained how the adapter communicates with a workflow instance, how it can react to commands (normal RoutedUI events from wpf controls) and react to events by the command service.
We will now continue, by looking at a simple view.
View responsibility
Let's first look at how we perceive a view in the MVC paradigm.
A view should be nothing more than the visualization of your data. The only authority it has, is the authority to decide how to represent a piece of data on the screen. That means it should not contain any business logic. Be very strict about this: the responsibility of a view is the visualization of data.
So, let's take a look at a common scenario where these lines may blur.
Take a list of products and let's say that if we have a new product-line that has been introduced within the past month, we want to use another background color, to alert our customers to this new hot product.
We could solve this in our binding perhaps (let's just assume that is easy), but we should not do that. That would mean the view is deciding when a product is new and hot. It should not.
The only thing the view should do is create the two visual representations of products and use a datatemplate selector to decide which is hot or not. The datatemplate selector could be injected by our controller. Another way to solve this, is for the controller to put this information in the ViewModel itself. Like, add a boolean 'new' which the view uses.
If you do not do it this way, and you are embedding logic inside of your view, you will quickly end up with scattered logic, never knowing where something is defined. Changing rules becomes hard and your application will break at some point.
Now, I understand, and have done many times, that sometimes you just do not have the time to do it right. But always remember that in the long run, you will get burned. Try to setup a situation where it is easy to do the right thing, by making it easy to use datatemplate selectors or use the viewmodel.
View decoupling
MVC advocates not letting your view have any knowledge whatsoever of the controller. It does this, because tight coupling of the view to the controller will destroy maintainability and flexibility. If you tight couple, you are unable to swap controllers or views. Most importantly, if you couple the view to the controller (by making it call specific methods on the controller), it becomes harder to maintain/refactor.
There are certainly approaches that do couple view to controller. If you look at the very powerful Caliburn framework, you will see that the framework has 'action messages' that directly call methods on the controller. I have yet to work with that extensively, so I can not be sure, but it feels to me there should be a very explicit layer between view and controller, which defines how the view will communicate with the controller.
Our goals in this project are to use the tools WPF provides us to communicate with the rest of the system. We do so with Commands.
A command can be seen as a message that is passed upward (and downward) the visual tree. Since our adapter lives just above the view and is part of the visual tree, it will have the opportunity to react to the command.
When building a view, you should also explicitly define all the interactions that view expects to have with the outside world. Do that in a static class like so:
public static class ImportantWizardInteractions
{
public static readonly RoutedUICommand Next;
public static readonly RoutedUICommand Back;
public static readonly RoutedUICommand GotoClientScreen;
public static readonly RoutedUICommand GotoAdresScreen;
public static readonly RoutedUICommand GotoRoleScreen;
public static readonly RoutedUICommand GotoCarScreen;
public static readonly RoutedUICommand Save;
public static readonly RoutedUICommand SaveYes;
public static readonly RoutedUICommand SaveNo;
static ImportantWizardInteractions()
{
Next = new RoutedUICommand("Next", "Next", typeof(ImportantWizardInteractions));
Back = new RoutedUICommand("Back", "Back", typeof(ImportantWizardInteractions));
GotoClientScreen = new RoutedUICommand("GotoClientScreen", "GotoClientScreen", typeof(ImportantWizardInteractions));
GotoAdresScreen = new RoutedUICommand("GotoAdresScreen", "GotoAdresScreen", typeof(ImportantWizardInteractions));
GotoRoleScreen = new RoutedUICommand("GotoRoleScreen", "GotoRoleScreen", typeof(ImportantWizardInteractions));
GotoCarScreen = new RoutedUICommand("GotoCarScreen", "GotoCarScreen", typeof(ImportantWizardInteractions));
Save = new RoutedUICommand("Save", "Save", typeof(ImportantWizardInteractions));
SaveYes = new RoutedUICommand("SaveYes", "SaveYes", typeof(ImportantWizardInteractions));
SaveNo = new RoutedUICommand("SaveNo", "SaveNo", typeof(ImportantWizardInteractions));
}
}
By being explicit about your interactions like this, you will be able to unit test more easily as well.
Use in your view like this:
<Button Command="{x:Static local:ImportantWizardInteractions.GotoClientScreen}">Client</Button>
A command is great for buttons and other stuff, but how do you do for instance communicate that a customer was selected from a listview?
- well, you bind to a selectedCustomer property hopefully, and when the customer was selected, that property changed on the viewmodel. The controller might pick that up.
- More explicitly though: do use the SelectedItemChanged event and use the codebehind of your view as a translation layer to talk to the outside world:
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// send a command
CustomerQueueInteractions.SelectNewCustomer.Execute(e.AddedItems, this);
}
You can call me on that. It's not a very elegant solution. I'd rather be able to do away with the codebehind of a view entirely. But using the codebehind is actually fine: it is part of the view, and it should not be allowed to do anything else than to act as a translator for view specific things to commands.
So, how to show a view
Well, building a view is nothing else than just deriving from usercontrol and doing your thing. Using commands and going wild on the visuals. (try to animate everything!!! your client loves it).
It depends now how you want to show it.
- Let's say your building a project where you don't care about fancy composition and pluggable modules in your application, and you just want your shell to show your view. The shell might have the following code:
<c:GenericWorkflowAdapter WorkflowController="{x:Type logic:AControllerForYourView}" >
<v:YourView/>
</c:GenericWorkflowAdapter>
I am assuming you do want a controller around your view.
Here a controller is instantiated and it's content is set to your view. Easy.
- Let's say we want our controller to choose what view it uses. That seems to me to be the nicest way to go about it. We will again put a controller in the visual tree, but will not set a view already:
- <c:GenericWorkflowAdapter WorkflowController="{x:Type logic:ImportantClientWizard}" />
Then, in the workflow, we might use some fancy logic to determine which view we will show (perhaps looking at the role of the user). To actually set a view, we will use the SetMainContentActivit