M-V-VM: simple view provider
That project, named Ran Toolkit, has been developed for about 4 years, and gave me lots of satisfactions, but:
- has grown uncontrolled, introducing unwanted complexity…the typical big ball of mud;
- has a double face: it’s a m-v-vm toolkit and an UI composition toolkit, and if you want one you have to get the other…and vice versa…not the best;
So far, so good…
Obviously that small application evolved and start asking for some UI composition feature, so I decided to give to the Ran Toolkit a new life adding a new kid to the Radical family: Radical.Windows.Presentation (not yet on nuget).
So what?
The aims of Radical.Windows.Presentation are:
- provide a m-v-vm toolkit;
- provide a UI composition toolkit;
- remove, totally remove, the coupling between the two toolkits letting the developer choose what he needs, without compromises;
With simplicity in mind the first thing I approached is the Inversion of Control container support, specifically how to help the developer to resolve views and view models.
I can state that there is a tight coupling between views and view models, at least most of the times for every single view there is a view model, what a better place to introduce conventions?
The view resolver does a really simple assumption, well someone else does it in the end, the view resolver says: given a view named MySampleView I’ll assume, by default, that the view model will be a type in the same namespace as the view and with the same name as the view with a “Model” suffix, so in this case the view model name will be MySampleViewModel.public interface IViewResolver
{
T GetView() where T : DependencyObject;
}
Obviously Radical.Windows.Presentation provides a default implementation of the IViewResolver interface, something like this:
Well…what’s going on here?public T GetView() where T : DependencyObject var viewModelType = this.conventions.ResolveViewModelType( typeof( T ) );
{
var view = this.container.GetService();
if( !this.conventions.ViewHasDataContext( view ) )
{
var viewModel = this.container.GetService( viewModelType );
this.conventions.AttachViewToViewModel( view, viewModel );
this.conventions.SetViewDataContext( view, viewModel );
}
return view;
}
First of all a couple of notes about involved actors:
- container is the Inversion of Control container, in this case it is just an IServiceProvider instance and GetService
is an extension method that “hides” all the ugliness of type casting; - conventions is a really simple component that handles conventions, that the end user can change to satisfy his own needs;
Q: Why we ask it to the conventions handler?The second assumption we do here is: if the resolver view already has a DataContext property set it’s a singleton view and has been already resolved once, so do not do anything else and return the view to the caller.
A: Basically the problem is that a DependencyObject does not have a DataContext property, DataContext is exposed by FrameworkElement and FrameworkContentElement that are not in the same inheritance chain so we cannot query just a single base class; This tells me that other controls (such as third party controls) can behave in a different manner and the end user needs to be able to plug his own logic.
On the other end if the view is not a singleton or if it is the first time we resolve that view we:
- ask to the conventions handler, given the view type, to tell us which is the view model type;
- resolve the view model using the Inversion of Control container;
- attach the view to the view model…what the hell is this? we’ll see it in a minute;
- set the DataContext of the view to the resolved view model instance, we use the conventions handler for the reasons explained above;
Why do we attach the view to the view model?
Well, this is one of the decisions driven by the simplicity pillar: in every desktop application you have, at one point, to deal with the view, as the following really frequent sample demonstrates:
Dialogs: you need to request, from a view model, to open a dialog to ask something to the user (e.g. choose a file on disk); you’ll try to do everything respecting the pattern, so you send a “ChooseFileRequest” message using your favorite message broker but at one point you realize that you need to give to the dialog instance its own owner, otherwise you start facing really cool behaviors where dialogs are dialogs but are not topmost and cannot be brought on top… :-/Radical.Windows.Presentation introduce a really simple way to achieve that, just fire your message in the usual way and introduce in the message the concept of “owner”; for example if the calling view model is the designated owner you can write something like this:
Now what you can do in the message handler is something like:var message = new ChooseFileRequest() { Owner = this; } this.broker.Broadcast( message );
where FindWindow is an extension method provided by the infrastructure whose role is to find the window instance that is hosting the given view model…how? simply using the conventions handler, asking it the current view associated with the view model and reverse walking the visual tree until it finds the Window object.var window = message.Owner.FindWindow(); var view = viewResolver.GetView(); view.Owner = window; view.ShowDialog();
Easy…isn’t it?
.m