Introduction
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"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:onyx="http://schemas.onyx.com/2009/fx/presentation"
xmlns:ui="clr-namespace:Simple"
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:
- If the ViewElement implements an interface, that interface is available as a service.
- If the ViewElement implements IServiceProvider, GetService will be called on the ViewElement next.
- 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.