WPF application patterns continued: convention over configuration
In my previous post on WPF application patterns, I went over a few of the most common patterns available, and gave some pointers on how to implement them in WPF. I mentioned how you could use a dependency injection framework to decouple the implementation of your views and controllers from each other. However, the problem when using dependency injection out of the box is that you need an interface to bind to.
Soon, you end up with this:
And this kind of initialization code (using Ninject1, but it’s straightforward to understand):
class ApplicationModule : StandardModule { public override void Load() { Bind<IAccountController>().To<AccountController>(); Bind<IAddProductController>().To<AddProductController>(); Bind<ICategoryController>().To<CategoryController>(); Bind<ICategoryListController>().To<CategoryListController>(); Bind<IEditProductController>().To<EditProductController>(); Bind<IMainWindowController>().To<MainWindowController>(); Bind<IProductController>().To<ProductController>(); Bind<IProductListController>().To<ProductListController>(); Bind<ISettingsController>().To<SettingsController>(); Bind<ISummaryController>().To<SummaryController>(); } }
You’re flooded with interfaces that will be, most of the time, empty (because, remember, you’re using WPF features such as data binding and commanding to control your view, thus removing the need for much specific wiring code beside what will be in the base IView and IController interfaces). Remember that the example above only has 10 views in total, which means it’s a pretty damn small application, and you have to double the initialization code because I just wrote it for the controllers (you need the same thing for the views).
It’s tedious, and quite repetitive, and you know how lazy developers are.
Ruby On Rails made popular the “convention over configuration” paradigm, which previously didn’t have much spotlight. We can get on that bandwagon to remove some of our work.
More specifically, because we’re nice and obedient developers, we know that our controllers and views will be called “FooView” and “BarController“. So instead of getting a view or a controller based on its interface, we could get it based on its “name” (where the name here is “Foo” or “Bar“).
We’re therefore trading this code:
class AccountController : IAccountController { public void Execute() { IAccountView view = KernelContainer.Kernel.Get<IAccountView>(); } }
For this code:
class AccountController : IAccountController { public void Execute() { IAccountView view = (IAccountView)viewFactory.Create("Account"); } }
By the way, notice how most MVC/MVP frameworks for web applications already work like this.
Anyway, this is not too bad, given that we’re doing this so that we can remove most of our interfaces, and change the initialization code to something much simpler and scalable. In our case, the initialization code could be as follows… First, bind all classes called “SomethingController” or “SomethingView” to themselves:
class ApplicationModule : StandardModule { public override void Load() { foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) { foreach (Type type in assembly.GetTypes()) { if (typeof(IView).IsAssignableFrom(type) && type.Name.EndsWith("View")) Bind(type).ToSelf().Using<TransientBehavior>(); } } } }
Then, implement the IViewFactory by building the full type name of the requested view, and then asking the IOC container to build it for you (as opposed to using Activator.CreateInstance, so that any dependencies are correctly injected):
class ViewFactory : IViewFactory { #region IViewFactory Members public IView Create(string viewName) { Type viewType = GetViewType(viewName); IView view = (IView)KernelContainer.Kernel.Get(viewType); return view; } #endregion }
This code will stay pretty much the same as the application grows, unlike the other code, and you can switch modules for your unit tests. Of course, you may want to have a more solid test to know what type should be considered a view, or have better control over which assemblies are reflected, etc. This is just example code. Remember also that this is only executed once at startup, so unless you have severe startup performance requirements, the cost shouldn’t be noticeable. Otherwise, you could always generate the code during build and compile that instead.
Now, I know, I know, my original code is not dependency injection, because I’m fetching the IAccountView myself from the IOC container (so this is really a Service Locator pattern). You would preferably have this instead:
class AccountController : IAccountController { private IAccountView mView; public AccountController(IAccountView view) { mView = view; } public void Execute() { // Do something with the view. } }
That is, get the view injected into the controller.
However, you can’t do that half the time. The goal of a controller is not only to process user actions on a view, it’s also to choose what view to show in the first place (which means you don’t always have a 1 on 1 relationship between controllers and views). You may therefore have some logic code in there that, depending on the context (is the user logged in, does he belong to the correct group, has he been banned, etc.), will show different views (redirect to the login view, display the regular user or administrator account view, display an error message, etc.). You don’t want all possible views injected in your controller, because any of those views could have a costly constructor itself. So you really need to fetch the view yourself programmatically.
What you would get injected, on the other hand, is the view factory that I use in my modified code:
class AccountController : IAccountController { public AccountController(IViewFactory viewFactory) { IAccountView view = (IAccountView)viewFactory.Create("Account"); } }
And as I mentioned earlier, it’s probable that your IAccountView is empty, so you can actually get away with just using IView, thus removing the cast.
Now we have a nice way to manage our views and controllers, and how to make them relate to each other. In future posts, we’ll look at other implementation details of WPF applications, such as giving controllers some context and handling user actions.
1 Because it’s a cool IOC framework, with a super cool name and logo!