Cutting CAB The Other Way
The Composite UI Application Block (CAB) is a pretty nice piece of technology that allows you to separate user interface code from user interface process code, following some variant of the Model View Controller (MVC) pattern. The QuickStarts that ship with CAB demonstrate how to use different features of the block, but interestingly none of the samples show you how to split a CAB application across multiple libraries along layer boundaries.
The largest, integrated QuickStart (BankTeller) show you how to split a CAB application into multiple libraries for purposes of extending the application. While this is a very important scenario in its own right, this implementation bundles Views and Controllers together into the same library.
In my opinion, Views belong to the UI layer, and Controllers belong in the UI Process layer just below it. To make that distinction clear, each layer should be implemented in a separate assembly, just like I did in my old MSDN article Easy UI Testing: Isolate Your UI Code Before It Invades Your Business Layer. That article uses the UIP Application Block, but the guiding principle is the same.
My goal is to implement all the Views in one Visual Studio project, and Controllers in a separate library project. Additionally, the Controller project should have no knowledge of the View project or Windows Forms in general. One of the main motivations of doing this is to enable unit testing of the Controller library.
CAB needs something to initialize its framework, including ObjectBuilder. In a Windows Forms application, this is done by deriving from FormShellApplication<TWorkItem, TShell> and calling the Run method:
public class MyShellApplication :
FormShellApplication<MyWorkItem, MyForm>
{
[STAThread]
static void Main()
{
new MyShellApplication().Run();
}
protected override void AfterShellCreated()
{
base.AfterShellCreated();
this.RootWorkItem.Run();
}
}
The single call to RootWorkItem.Run is the only Controller-like code residing in the UI layer. Since the UI project is a Windows Forms executable, a bit of code is needed to bootstrap CAB before delegating control to the Controller layer.
MyForm is just a Windows Form containing a single DeckWorkspace called mainWorkspace_. MyWorkItem is the root WorkItem for the application and is defined in the Controller library:
public class MyWorkItem : WorkItem
{
protected override void OnRunStarted()
{
base.OnRunStarted();
IMyView mv = this.Items.Get<IMyView>("IMyView");
if (mv == null)
{
mv = this.Items.AddNew<IMyView>("IMyView");
}
mv.Message = "Hello World";
this.Workspaces["mainWorkspace_"].Show(mv);
}
}
Although this code looks deceptively simple, there's a lot of indirection going on here. I tend to view a CAB WorkItem as a participant in the Application Controller design pattern (described in Patterns of Enterprise Application Architecture), so it's the WorkItem's responsibility to create and show Views. Since the Controller has no knowledge of the UI layer, it's necessary to work with Views in an abstract fashion; hence, I've defined the IMyView interface:
public interface IMyView
{
string Message
{
get;
set;
}
}
To get or create a particular View, MyWorkItem uses its Items collection to get or create an instance of IMyView. The AddNew<T> method uses Object Builder to create a new instance of the requested type, but in itself it doesn't know how to create an instance of IMyView; after all, IMyView is an interface.
Fortunately, CAB provides access to Object Builder through CabApplication.AddBuilderStrategies, so I can define a map from IMyView to an implementation by overriding this method in MyShellApplication:
protected override void AddBuilderStrategies
(Builder builder)
{
base.AddBuilderStrategies(builder);
TypeMappingPolicy policy =
new TypeMappingPolicy(typeof(MyView),
"IMyView");
builder.Policies.Set<ITypeMappingPolicy>(policy,
typeof(IMyView), "IMyView");
}
In Object Builder, you can add policies that guide how objects are created. One type of policy that can be added is ITypeMappingPolicy, which is used by Object Builder to map from one type to another; in this case from IMyView to MyView. The TypeMappingPolicy class is used to define the destination type, and is then added to the collection of policies together with the originally requested type.
You may have noticed that in both MyWorkItem.OnRunStarted and MyShellApplication.AddBuilderStrategies I'm using a string with the value "IMyView". When you request an instance of a type from Object Builder, this request is qualified not only by the type, but also by a name. The name can be omitted, but this will cause Object Builder to assign a new Guid as a name, and this makes type mapping impossible, since type mapping works on both type and name, and when you define the map you need to know the name in advance. My convention is just to used the interface name as the name, but any string can be used as long as it's well-known.
In my example, I've hard-coded the type map into the executable, but in a production application, this should rather be defined in the application configuration file.
MyView is the Windows Forms-based implementation of IMyView:
public partial class MyView : UserControl, IMyView
{
public MyView()
{
InitializeComponent();
}
#region IMyView Members
public string Message
{
get { return this.messageLabel_.Text; }
set { this.messageLabel_.Text = value; }
}
#endregion
}
Notice that MyView is simply a UserControl with a single label control called messageLabel_. It implements IMyView, and since MyShellApplication maps from IMyView to MyView, when MyWorkItem requests an instance of IMyView, Object Builder returns an instance of MyView.
To reiterate: MyForm, MyShellApplication and MyView are defined in the UI project. IMyView and MyWorkItem are defined in the Controller project. The UI project is a Windows Forms executable, which has a reference to the Controller project. The controller project, on the other hand, is a library project which has no knowledge of either the UI Project or Windows Forms in general (it has no reference to System.Windows.Forms or Microsoft.Practices.CompositeUI.WinForms).
As I initially hinted, the main purpose of cutting a CAB-based application along the lines of layer boundaries is to enable unit testing of the Controller library, and I'll demonstrate how to do this in my next post.
Update: The unit testing post is now published.
Comments
- Anonymous
May 04, 2006
In my previous post I demonstrated how to create an application based on the Composite UI Application...