Onyx provides a framework that aids in the creation of WPF applications that follow the Model-View-ViewModel design pattern. The goal when developing with Onyx is to create XAML based Views that have no code in the codebehind file (remember, this is a goal, not a requirement, and one should be pragmatic). In addition, the ViewModel should contain no code that ties it to any specific UI or UI features that are not easily unit tested.

Unfortunately, it's not always easy to follow the M-V-VM pattern. Sometimes you need to synchronize state between the View and the ViewModel for which data binding doesn't work. For example, you can't bind the selected items in a multi-select ListBox to a collection in the ViewModel just through standard data binding. Worse, there's often a need to do something in response to user input that would make your ViewModel tightly coupled to the UI and hinder unit testing. For example, in response to the user initiating a Save command you may need to display a SaveFileDialog. If you follow any WPF discussions about M-V-VM you can probably think of several dozen other examples of things that people have struggled with.

Usually, when someone asks a question about how to accomplish something that's difficult to do while using WPF and M-V-VM, the experts recommend one of two approaches. The first is to be creative with the WPF framework. Often advanced techniques such as "attached behaviors" can go a long ways towards providing a solution. Though these approaches work, and may even be the best answer in many cases, they require advanced techniques and out-of-the-box thinking. Worse, sometimes it's not obvious how you'd use these techniques to solve a specific problem, especially in a clean and reusable manner. In those cases, the second solution proposed by the experts is to use an interface. This is a well known strategy for general decoupling of code, and is used in non-UI centric code rather extensively. This is a solution that should always work. However, it's not without it's own set of problems, which the WPF experts generally just ignore, leaving solutions up to the inquirer.

When you want to use the interface approach to decoupling, the biggest problem that must be solved is how to supply an interface to the ViewModel. There's two very well known patterns that one can employ here: ServiceLocator and DependencyInjection. By default, Onyx uses ServiceLocator, though it's possible to modify the behavior to use DependencyInjection. For the rationale behind this, see Why ServiceLocator.

Let's take a high level view of how Onyx works. Note that we'll be working backwards from how you'd likely develop using Onyx if you're following a TDD approach, but this is probably the best way to understand how to use Onyx.

The first thing to look at is how you associate a ViewModel with a View when using Onyx. There's several approaches to doing this, but the Onyx has a preferred method. Here's a snippet of XAML taken directly from the "Simple" sample application that illustrates how it's done.

<Window x:Class="Simple.MainWindow"
        Title="Simple" Height="300" Width="300"
        onyx:View.Model="{x:Type ui:MainWindowViewModel}">

The onyx:View.Model property is used to specify the ViewModel that is associated with a View. Any object may be specified here, but if you specify a Type, as was done here, then the ViewModel will be constructed for you. How the instance is constructed is a little complex, and can even be modified by setting View.ViewModelActivator to a custom IViewModelActivator, but for now we'll concentrate on the default behavior in the most typical usage. In our "Simple" sample application the MainWindowViewModel has a public constructor that takes a single argument of type View. This is the constructor that's used to construct our ViewModel in this case.

Where does the View that's passed to the constructor come from? There's a static method, View.GetView, which can be called to obtain the View associated with any DependencyObject. If a View isn't currently associated with the DependencyObject, one will be created and associated at that time. The View instance provides two important things: a reference to the ViewElement (the DependencyObject that the View is associated with), and an implementation of IServiceProvider. While it's possible for a ViewModel to interact with the ViewElement, this generally would cause a tight coupling, and so you should be very careful about doing this. Instead, you generally should interact via services provided through the View's IServiceProvider implementation. For example, the "Simple" sample application displays a message box to the user by obtaining an IDisplayMessage service from the View.

Where do the services available from the View come from? They come from one of three places, in the following order:
  1. If the ViewElement implements an interface, that interface is available as a service.
  2. If the ViewElement implements IServiceProvider, GetService will be called on the ViewElement next.
  3. The OnyxContainer associated with the View instance is the final source for the service.

The OnyxContainer is populating with common services when the View is constructed. You can even add your own common services by subscribing to the View.ViewCreated event. All of this provides you with a great deal of flexibility and power when it comes to registering services that will be found by the View provided to the ViewModel. By default, Onyx provides several common services based on the type of the ViewElement.

Last edited Mar 9, 2009 at 11:45 PM by wekempf, version 4


No comments yet.