Table of Contents
Introduction
For those of you that know me, you may have noticed that I have not been producing my usual tirade of articles, and be slightly worried about me, fear not, I am not dead, I have just been very, very busy indeed.
What on, I hear you ask. Well, some of you may also know that I absolutely love WPF and that some time ago I wrote a pretty well received ModelView-ViewModel (MVVM) framework called Cinch, which I presented here at CodeProject a while back. At the time, I was pretty happy with that framework, as it seemed to do all the common MVVM tasks such as OpenFileDialog/MessageBox/SaveFileDialog show popups etc., but as we software engineers/developers know, things change quickly in our world, and occasionally something quite nice comes along (that last thing was LINQ for me), but now it's MEF. For those of you that have not heard of MEF, it stands for Managed Extensibility Framework. So with this in mind, and the fact that there was now better support for attached properties in the form of Blend Behaviours/TargettedTriggers, I thought it may be time to revisit Cinch and give it an update.
So that is precisely what I set out to do, and I have been working hard on that the past month or so, and I have now finished bringing Cinch bang up to date, and am totally stoked with the results. For those of you that have not read my original Cinch articles, you can find a list of those right here.
One thing I should mention is that Cinch V1 was targeted at WPF only. With Cinch V2, this has now changed, and Cinch V2 works equally well with Silverlight (version 4 or above) as it does with WPF. Cinch V2 is a pretty big framework that tries to deal with a lot of different things; as such, there are a lot of classes, and I think it may all get a bit confusing for you (even with documentation such as this) without a compatibility matrix. In fact, I feel this compatibility matrix is so important it will be included in the introduction of every Cinch V2 article.
The compatibility matrix shows a list of classes along with their general work area, and whether they are compatible with WPF or SL or both.
Work Area | Class Name | WPF | Silverlight (4 or above) | Both |
Business objects | EditableValidatingObject.cs | | | Yes |
Business objects | ValidatingObject.cs | | | Yes |
Business objects | DataWrapper.cs | | | Yes |
Commands | EventToCommandArgs.cs | | | Yes |
Commands | SimpleCommand.cs | | | Yes |
Commands | WeakEventHandlerManager.cs | | | Yes |
Events | CloseRequestEventArgs.cs | | | Yes |
Events | UICompletedEventArgs.cs | | | Yes |
WeakEvents | WeakEvent.cs | | | Yes |
WeakEvents | WeakEventHelper.cs | | | Yes |
WeakEvents | WeakEventProxy.cs | | | Yes |
Extension Methods | DispatcherExtensions.cs | Yes | | |
Extension Methods | GenericListExtensions.cs | | Yes | |
Interactivity Actions | CommandDrivenGoToStateAction.cs | | | Yes |
Interactivity Behaviours | FocusBehaviourBase.cs | Yes | | |
Interactivity Behaviours | NumericTextBoxBehaviour.cs | Yes | | |
Interactivity Behaviours | SelectorDoubleClickCommandBehavior.cs | Yes | | |
Interactivity Behaviours | TextBoxFocusBehavior.cs | Yes | | |
Interactivity Triggers | CompletedAwareCommandTrigger.cs | | | Yes |
Interactivity Triggers | CompletedAwareGotoStateCommandTrigger.cs | | | Yes |
Interactivity Triggers | EventToCommandTrigger.cs | | | Yes |
Messager Mediator | MediatorMessageSinkAttribute.cs | | | Yes |
Messager Mediator | MediatorSingleton.cs | | | Yes |
Services Implementation | ChildWindowService.cs | | Yes | |
Services Implementation | SLMessageBoxService.cs | | Yes | |
Services Implementation | ViewAwareStatus.cs | | | Yes |
Services Implementation | ViewAwareStatusWindow.cs | Yes | | |
Services Implementation | VSMService.cs | | | Yes |
Services Implementation | WPFMessageBoxService.cs | Yes | | |
Services Implementation | WPFOpenFileService.cs | Yes | | |
Services Implementation | WPFSaveFileService.cs | Yes | | |
Services Implementation | WPFUIVisualizerService.cs | Yes | | |
Services Interfaces | IChildWindowService.cs | | Yes | |
Services Interfaces | IMessageBoxService.cs | | Yes | |
Services Interfaces | IViewAwareStatus.cs | | | Yes |
Services Interfaces | IViewAwareStatusWindow.cs | Yes | | |
Services Interfaces | IVSM.cs | | | Yes |
Services Interfaces | IMessageBoxService.cs | Yes | | |
Services Interfaces | IOpenFileService.cs | Yes | | |
Services Interfaces | ISaveFileService.cs | Yes | | |
Services Interfaces | IUIVisualizerService.cs | Yes | | |
Services Test Implementations | TestChildWindowService.cs | | Yes | |
Services Test Implementations | TestMessageBoxService.cs | | Yes | |
Services Test Implementations | TestViewAwareStatus.cs | | | Yes |
Services Test Implementations | TestViewAwareStatusWindow.cs | Yes | | |
Services Test Implementations | TestVSMService.cs | | | Yes |
Services Test Implementations | TestMessageBoxService.cs | Yes | | |
Services Test Implementations | TestOpenFileService.cs | Yes | | |
Services Test Implementations | TestSaveFileService.cs | Yes | | |
Services Test Implementations | TestUIVisualizerService.cs | Yes | | |
Threading | AddRangeObservableCollection.cs (this is specific SL implementation) | | Yes | |
Threading | AddRangeObservableCollection.cs (this is specific WPF implementation) | Yes | | |
Threading | BackgroundTaskManager.cs | | | Yes |
Threading | ISynchronizationContext.cs | | | Yes |
Threading | UISynchronizationContext.cs | | | Yes |
Threading | ApplicationHelper.cs | Yes | | |
Threading | DispatcherNotifiedObservableCollection.cs | Yes | | |
Menus | CinchMenuItem.cs | | | Yes |
Utilities | ArgumentValidator.cs | | | Yes |
Utilities | IWeakEventListener.cs (this is a System class missing from Silverlight, so I created it) | | Yes | |
Utilities | ObservableHelper.cs | | | Yes |
Utilities | PropertyChangedEventManager.cs (this is a System class missing from Silverlight, so I created it) | | Yes | |
Utilities | PropertyObserver.cs | | | Yes |
Utilities | BindingEvaluator.cs | Yes | | |
Utilities | ObservableDictionary.cs | Yes | | |
Utilities | TreeHelper.cs | Yes | | |
Validation | RegexRule.cs | | | Yes |
Validation | Rule.cs | | | Yes |
Validation | SimpleRule.cs | | | Yes |
ViewModels | EditableValidatingViewModelBase.cs | | | Yes |
ViewModels | IViewStatusAwareInjectionAware.cs | | | Yes |
ViewModels | ValidatingViewModelBase.cs | | | Yes |
ViewModels | ViewMode.cs | | | Yes |
ViewModels | ViewModelBase.cs | | | Yes |
ViewModels | ViewModelBaseSLSpecific.cs | | Yes | |
ViewModels | ViewModelBaseWPFSpecific.cs | Yes | | |
Workspaces | ChildWindowResolver.cs | | Yes | |
Workspaces | CinchBootStrapper.cs (Silverlight version) | | Yes | |
Workspaces | CinchBootStrapper.cs (WPF version) | Yes | | |
Workspaces | PopupNameToViewLookupKeyMetadataAttribute.cs | | | Yes |
Workspaces | IWorkspaceAware.cs | Yes | | |
Workspaces | MockView.cs | Yes | | |
Workspaces | NavProps.cs | Yes | | |
Workspaces | PopupResolver.cs | Yes | | |
Workspaces | ViewnameToViewLookupKeyMetadataAttribute.cs | Yes | | |
Workspaces | ViewResolver.cs | Yes | | |
Workspaces | WorkspaceData.cs | Yes | | |
Now that I have shown you what classes will work with WPF/Silverlight, let's get on with the rest of this article, shall we? But first, here are the links to the old Cinch V1 articles.
In case you missed Cinch V1, and have an interest in MVVM, I would strongly recommend that you read all the Cinch V1 articles first, as it will give you a much deeper understanding of the content that will be presented in these Cinch V2 articles.
Cinch V1 Article Links
Some of you may never have seen the old Cinch V1 articles, so I will also include a list of these here, as where the Cinch V2 still uses the same functionality as Cinch V1, I will be redirecting people to these articles.
What's New
I have received a lot of emails about Cinch V1 from a lot of people that are using it, and people are generally very positive and grateful for the framework, so why would I think about changing it all? Well, the simple answer is that better things come along, and I like to move with the times, and it is my framework, so I feel if I want to change it, I am allowed to, so I did.
Question: If I am using Cinch V1, will my codebase still work?
Answer: Most of the core Cinch V1 classes and functionality has been maintained, but there are several areas of the codebase where I had to make breaking changes or I thought up a much better way of dealing with things that forced my hand into making a breaking change. In most cases, I am pretty confident that these issues will be easily resolvable. So what are the things that have changed? Well, they are as follows:
- Service injection / IOC
- BackgroundTaskManager
- Logging
- MVVM Menus
- Commands
- Workspaces
- Attached Behaviors
- View LifeCycle DPs
- Key Gestures Firing Commands
This may seem a lot, but I am sure once you see what you can do with the new framework, you will realise why I had to make these changes. To be honest, I am so much happier with the new framework than the old one, that I do not mind these breakages. "You can't make an omelet without breaking some eggs".
What Will Break If I am Currently Use Cinch V1
As just stated, the following areas will break:
Services
The way services are made available is totally different. It used to use the Unity IOC container with the option to supply a different IOC container. Now it makes exclusive use of MEF.
As such, there is no longer a service resolver on ViewModelBase
, all services are expected to be passed into as ViewModel constructors. The loss of the service resolver is something I am quite happy about actually, as it kind of broke the single responsibility pattern. This article is all about how the new services work, so by the end of this article, you will be more informed of the new methodology employed by Cinch V2.
BackgroundTaskManager
I realised that I never allowed for any state object to be passed into the background worker Func<T>
delegate, so I have made a few cosmetic changes to the BackgroundTaskManager<T>
which is now BackgroundTaskManager<TArg, TResult>
to accommodate this. You will notice that there is a new WorkerArgument
property which is of type object
, and the new work Func
delegate now looks like this: Func<TArg, TResult>
, where the WorkerArgument
is cast to the type of TArg
internally by the BackgroundTaskManager<TArg, TResult>
code.
Cinch Logging
Cinch V1 provided some basic logging using log4Net and the Simple Logging Facade (SLF). I had a think about this and I felt that logging within a framework was not that great an idea, as I was essentially forcing people to use log4Net for Cinch alone, and then they would also have to add their own logging requirements on top of that. So after much to'ing and fro'ing, I took the decision to remove logging for Cinch V2; if you think this was a bad idea, please let me know and I can add it back in.
MVVM Menu Items
These work the same way as I discussed in the Cinch V1 article CinchIII.aspx#WPFMenuItems but Cinch V1 was solely aimed at WPF, and with Cinch V2, we now have Silverlight support, so it did not seem that right to keep the MVVM menus called WPFMenuItem
, so all that has changed is that the name of the class has been changed to something more suitable for a cross technology framework, and the new name is CinchMenuItem
.
Commanding A.K.A. SimpleCommand
Cinch V1 introduced a simple delegate style ICommand
implementation, which was fine at the time, but with Cinch V2, SimpleCommand
has received some serious loving, and now expects two generic arguments. SimpleCommand
now looks like this: SimpleCommand<T1,T2>
where T1
is the ICommand.CanExecute(Object parameter)
parameter type (so this is really CanExecute(T1 parameter)
) and T2
is the ICommand.Execute(Object parameter)
parameter type (so this is really Execute(T2 parameter)
. There is also much more to this new SimpleCommand
implementation. I will discuss this in further articles.
Workspaces
Cinch V1 kind of provides workspaces via DataTemplate
s. I have revisited this area and made the DataTemplate
s work in a view first scenario which keeps the view designer friendly and Blendable. The new workspace management also allows some contextual data to be sent to the view.
Attached Behaviours
Cinch V1 had a bunch of Attached Behaviours such as:
NumericTextBox
SelectorDoubleClick
EventCommander
SingleEventCommand
In Cinch V2, these have all been migrated into Blend interactivity based Behaviors/Triggers, which aids in greater Blendability.
View Lifecycle Events
Cinch V1 had two Attached DPs that could be used to fire ViewModel ICommand
s when various View lifecycle events (such as Loaded/Unloaded) occurred.
LifetimeEvent
UserControlLifeTimeEvent
In Cinch V2, these two Attached DPs have been replaced with a UI Service called ViewAwareStatus
, which we will be discussing in this and the next article.
Key Gestures Firing ICommands in ViewModel
Cinch V1 had Attached DPs that could be used to fire ViewModel ICommand
s when various key gestures (such as CTRL + F1) occurred.
In Cinch V2, there is no support at all for this, as this comes out of the box with WPF 4/.NET 4.0.
That I think completes what will break; there is a fair bit there, but I honestly believe it will not be that painful to migrate across to Cinch V2, and in all honesty, I am just so pleased about how V2 has worked out that I consider these few breakages to be worth it.
Cinch V2 Article Links
So that is what the article roadmap looks like, I guess it is now time to dive into the guts of this article, so let's go:
MEFedMVVM and ViewModel/Service Resolution
Now for those that know me, you will know that I like code, and that I like to create my own code, and occasionally, I do work well with others (unlike Baboons who do not work well with other, cheeky Baboons).
So with that in mind, I just need to tell you a quick story before we continue.
Around the time that I decided to start a rewrite of Cinch, I knew I wanted to use MEF, and I started looking into it, and about 4 days later, Marlon Grech (C# MVP / WPF Disciples founder) sent an email to the group about a cool MEF ViewModel resolver project he had made, and a few days later, Glenn Block (Microsoft MEF Project Manager) also emailed the group stating he had come up with a cool MEF ViewModel resolver project.
Interesting stuff....options, options, options.
Now what I wanted for Cinch V2 was no dependencies other than the standard Microsoft ones, so I set about examining these two libraries and crafted my own MEF enabled ViewModel resolver project, which I did actually finish, which was largely based on Glenn Block's offering which he is calling "Brook", cheers Glenn.
So then what happened is, I kept destroying my brain by reading and re-reading Marlon's implementation, and in the end, I just felt that Marlon's implementation was closer to what I was trying to do, and then I got another email from Marlon stating he was giving a talk on his library, which I attended.
During that talk, Marlon showed that you could basically add a reference to his library and a couple of attributes, and you got:
- View first (which is designer/Blend friendly, which in my opinion is what we should all be aiming for when doing MVVM)
- View-ViewModel wire up
- Design time services for displaying design time data
- Runtime services for displaying runtime data
- Nice extension points that would easily facilitate the Strategy pattern and therefore Mocks for unit testing
- Support for multiple XAP files (when working on large Silverlight projects, this is a must I would think)
- The ability to swap in your own container resolution strategy if you do not like how Marlon does things (but that will not happen, you will like how he does things, because it is the right way he does it, in my opinion)
So in the end, after much soul searching, I decided that I would take a dependency on Marlon's work (which he is calling MefedMVVM) for Cinch V2. This is not something I have taken lightly, but once you see what Marlon's library does, I am sure you will all agree that it is worth it.
The remainder of this article will be dedicated to having a look at what it means to use Marlon's MefedMVVM within the context of Cinch V2.
What's MEF
Just before we get into the guts of Marlon's MefedMVVM superbness, I feel it may actually be a good idea to go into MEF a bit; after all, MefedMVVM is entirely based on MEF, so some understanding of MEF is required.
Probably one of the best places to start with MEF is the MEF CodePlex site, which is available using any of the MEF links within this article.
I have taken the following excerpt straight from the MEF CodePlex site: http://mef.codeplex.com/wikipage?title=Overview&referringTitle=Home.
What is MEF?
The Managed Extensibility Framework (or MEF for short) simplifies the creation of extensible applications. MEF offers discovery and composition capabilities that you can leverage to load application extensions.
What problems does MEF solve?
MEF presents a simple solution for the runtime extensibility problem. Until now, any application that wanted to support a plug-in model needed to create its own infrastructure from scratch. Those plug-ins would often be application-specific and could not be reused across multiple implementations.
- MEF provides a standard way for the host application to expose itself and consume external extensions. Extensions, by their nature, can be reused amongst different applications. However, an extension could still be implemented in a way that is application-specific. Extensions themselves can depend on one another and MEF will make sure they are wired together in the correct order (another thing you won't have to worry about).
- MEF offers a set of discovery approaches for your application to locate and load available extensions.
- MEF allows tagging extensions with additional metadata which facilitates rich querying and filtering.
How does MEF work?
Roughly speaking, MEF's core is comprised of a catalog and a CompositionContainer. A catalog is responsible for discovering extensions and the container coordinates creation and satisfies dependencies.
- MEF's first-class citizen is a ComposablePart (see Parts). A composable part offers up one or more Exports, and may also depend on one or more externally provided services or Imports. A composable part also manages an instance, which can be an object instance of a given type (it is in the default MEF implementation). MEF, however, is extensible, and additional ComposablePart implementations can be provided as long as they adhere to the Import/Export contracts.
- Exports and imports each have a Contract. Contracts are the bridge between exports and imports. An export contract can consist of further metadata that can be used to filter on its discovery. For example, it might indicate a specific capability that the export offers.
- MEF's container interacts with Catalogs to have access to composable parts. The container itself resolves a part's dependencies and exposes exports to the outside world. You're free to add composable part instances directly to the container if you wish.
- A ComposablePart returned by a catalog will likely be an extension to your application. It might have imports (dependencies) on components the host application offers, and it's likely to export others.
- The default MEF composable part implementation uses attribute-based metadata to declare exports and imports. This allows MEF to determine which parts, imports, and exports are available completely through discovery.
This diagram tries to illustrate some of the inner workings of how MEF works.
A Quick Word on Catalogs
With MEF, there are many forms of Catalogs, all doing slightly different things, in order to resolver the Parts (Imports/Exports). Let us examine a few of these Catalog types, shall we?
- AssemblyCatalog: Used to discover all the exports in a given assembly.
- DirectoryCatalog: Used to discover all the exports in all the assemblies in a directory.
- AggregateCatalog: When AssemblyCatalog and DirectoryCatalog are not enough individually and a combination of catalogs is needed, then an application can use an AggregateCatalog. An AggregateCatalog combines multiple catalogs into a single catalog. A common pattern is to add the current executing assembly, as well as a directory catalog of third-party extensions. You can pass in a collection of catalogs to the AggregateCatalog constructor, or you can add directly to the
Catalogs
collection, i.e., catalog.Catalogs.Add(...)
. - TypeCatalog: Used to discover all the exports in a specific set of types.
- DeploymentCatalog (SL only): MEF in Silverlight includes the DeploymentCatalog for dynamically downloading remote XAPs.
So once you have an idea of what your catalog setup will be, you must then configure the CompositionContainer, which is where all the current parts are made available.
That was a whirlwind tour of how MEF works, so now we begin our deep dark dive into the inner workings of Marlon's MefedMVVM within the context of Cinch V2.
Introducing MefedMVMM
As I previously stated, Cinch V2 has a dependency on the most excellent work of Marlon Grech's MefedMVVM offering. You know even if you do not end up liking or using Cinch V2, you should really take the time out to get to know MefedMVVM as it is simply awesome.
So what is Marlon trying to do with MefedMVVM, and how is it that Cinch V2 is able to use it so easily? Well, what Marlon set out to do was create a library that could be used with any other MVVM framework with a minimum of fuss.
Has he achieved this? Hell, yeah. It is literally a piece of piss to use MefedMVVM with Cinch V2, a single reference, a few attributes, and an Attached DP in the View's XAML, and it just works.
And recall from what I said earlier, this is what you get out of the box when using MefedMVVM and therefore also with Cinch V2.
- View first (which is designer/Blend friendly, which in my opinion is what we should all be aiming for when doing MVVM)
- View-ViewModel wire up
- Design time services for displaying design time data
- Runtime services for displaying runtime data
- Nice extension points that would easily facilitate the Strategy pattern and therefore Mocks for unit testing
- Support for multiple XAP files (when working on large Silverlight projects, this is a must I would think)
- The ability to swap in your own container resolution strategy if you do not like how Marlon does things (but that will not happen, you will like how he does things, because it is it right way he does it, in my opinion)
That is a lot of bang for your buck there man.....I take my hat off to Marlon, massive Kudos to him, he has done an absolutely sterling job and it is just so easy to use. Big thanks Marlon.
Now that you know that Cinch V2 is using MefedMVVM, let's get into how this all works, shall we?
The Basic Idea
The basic idea behind MefedMVVM is that the following bases should be covered:
- The View should be able to locate its ViewModel by using a single Attached DP
- The ViewModel will be found and shall have all its required services (constructor parameters) injected automatically by the ViewModel resolution Attached DP from step 1
- If the View is being viewed inside of Blend at design time, design time services are used to provide design time data
Couple of Things
I just want to discuss a few of the things I just mentioned in a bit more detail.
Blend vs. VS2010
That is the basic idea anyway. Some of you may note that I talk about Blend design time support, and not VS2010. This is the case; as far as I know, there is no support for VS2010 design time data, and MefedMVVM really only works in Blend. I am unable to verify this, as VS2010 does not seem to even be able to open any View that uses the Blend "System.Windows.Interactivity.dll", which in my opinion is just nuts. I could be wrong on that one though.
Design Time Data
As some of you may be aware, Blend does allow you to create design time data, using the d:DataContext
design time attributes, where Blend will create a design time ViewModel for you based on your actual ViewModel.
Karl Shifflett has a nice post on this: http://karlshifflett.wordpress.com/2009/10/28/ddesigninstance-ddesigndata-in-visual-studio-2010-beta2/
Now this is OK I guess, but as far as I know, there are some pretty serious restrictions on this, such as:
- All properties must be Read/Write (getter/setter)
- The ViewModel in question must have a default constructor
- Obfuscates the XAML with loads of design time markup
Now that may suit you, but I tell you what, it certainly does not sit well with me. I do not want to have a default constructor that allows my ViewModel to be created possibly in an invalid state. OK, I could call another constructor from the default one, but what about Read/Write properties, I may not want that.
Now I am not slogging anything off here, I think Microsoft is doing a grand job of listening to the community and providing some tooling to allow design time data. It is just for me, I would prefer not to have default constructors and I do not want all my properties to be Read/Write.
So what's another choice; well, what MefedMVVM does is introduce the idea of UI Services that can be used to provide data. These UI Services can be used either at runtime/design-time or both.
The beauty of this approach is:
- That you do not have a default constructor on your ViewModel.
- There is no design time markup in the View's XAML.
- All your properties can be designed with Read or Read/Write access.
- You are binding to your ViewModel the same way at design time, as you are at runtime; it is the same ViewModel that will have either design time or runtime services injected depending on whether you are at design time or runtime.
Anyway, that should give you a bit of an insight as to how MefedMVVM goes about its business. Let's crack on then, shall we?
View-ViewModel Resolution
One of the most important things you need to consider when wanting to use design time data and MefedMVVM (and therefore Cinch V2) is the View first approach which allows designers to be able to actually see what the finished article will look like. As I previously stated, MefedMVVM allows for this by using an Attached Property that allows the view to find its ViewModel dynamically.
Let's have a look at that, shall we? Within MefedMVVM, there is an Attached DP called ViewModel
which is located in the ViewModelLocator
class. All we need to do is use that Attached DP as shown below in this example View:
<UserControl x:Class="CinchV2DemoWPF.ImageLoaderView"
xmlns:CinchV2="clr-namespace:Cinch;assembly=Cinch.WPF"
xmlns:meffed="http:\\www.codeplex.com\MEFedMVVM"
meffed:ViewModelLocator.ViewModel="ImageLoaderViewModel">
</UserControl>
Let us continue the path down from the ViewModel
DP and see what MefedMVVM is really doing for us. Here is what the actual DP code looks like:
public static readonly DependencyProperty ViewModelProperty =
DependencyProperty.RegisterAttached("ViewModel", typeof(string), typeof(ViewModelLocator),
new PropertyMetadata((string)String.Empty,
new PropertyChangedCallback(OnViewModelChanged)));
public static string GetViewModel(DependencyObject d)
{
return (string)d.GetValue(ViewModelProperty);
}
public static void SetViewModel(DependencyObject d, string value)
{
d.SetValue(ViewModelProperty, value);
}
private static void OnViewModelChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
try
{
string vmContractName = (string)e.NewValue;
var element = d as FrameworkElement;
if (!String.IsNullOrEmpty(vmContractName) && element != null)
{
ViewModelRepository.AttachViewModelToView(vmContractName, element);
}
}
catch (Exception ex)
{
Debug.WriteLine("Error while resolving ViewModel. " + ex);
}
}
It can be seen that it uses the value of the property to use as a ViewModel contract name, and it then makes a call to the ViewModelRepository.AttachViewModelToView
method passing in both the ViewModel contract name and the actual view element. Carrying on down the trail, let's look at the ViewModelRepository.AttachViewModelToView
method, which looks like this:
public static void AttachViewModelToView( string vmContract, FrameworkElement view)
{
var vmExport = Instance.Resolver.GetViewModelByContract(vmContract, view);
if (vmExport != null)
{
Debug.WriteLine("Attaching ViewModel " +
vmExport.Metadata[ExportViewModel.NameProperty]);
if ((bool)vmExport.Metadata[ExportViewModel.IsDataContextAwareProperty])
ViewModelRepository.Instance.dataContextAwareVMInitializer.CreateViewModel(
vmExport, view);
else
ViewModelRepository.Instance.basicVMInitializer.CreateViewModel(vmExport, view);
}
else
{
RegisterMissingViewModel(vmContract, view);
}
}
It can be seen that this code is now attempting to actually get a resolved (MEF Export) ViewModel instance by using the Instance.Resolver.GetViewModelByContract()
method. And once it has an instance, it will call either of the two Initializer CreateViewModel()
methods. More on that in just a minute; for now, I just want to talk about how the ViewModel Export is resolved in the first place. As I just stated, MefedMVVM does this using the Instance.Resolver.GetViewModelByContract()
, so let's have a look at that.
public Export GetViewModelByContract(string vmContractName, object contextToInject)
{
if(Container == null)
return null;
var viewModelTypeIdentity = AttributedModelServices.GetTypeIdentity(typeof(object));
var requiredMetadata = new Dictionary<string, Type>();
requiredMetadata[ExportViewModel.NameProperty] = typeof(string);
requiredMetadata[ExportViewModel.IsDataContextAwareProperty] = typeof(bool);
var definition = new ContractBasedImportDefinition(
vmContractName, viewModelTypeIdentity,
requiredMetadata, ImportCardinality.ZeroOrMore, false,
false, CreationPolicy.Any);
SetContextToExportProvider(contextToInject);
var vmExports = Container.GetExports(definition);
SetContextToExportProvider(null);
var vmExport = vmExports.FirstOrDefault(
e => e.Metadata[ExportViewModel.NameProperty].Equals(vmContractName));
if (vmExport != null)
return vmExport;
return null;
}
As we can see, we are doing some sort of query here where we are using metadata (which is something that comes out of the box with MEF) to query for a particular Export where the NameProperty == the incoming ViewModel name (which was from the original View attached DP (meffed:ViewModelLocator.ViewModel="ImageLoaderViewModel"
)). That's cool, we now have an Export (ViewModel) that matches our original string where we specified what ViewModel we wanted. Seems all cool, doesn't it? But wait a minute, we had a lookup string on the View, but how does that get us a ViewModel exactly?
Well, the answer to that lies in the use of a custom Export attribute such as this one:
[ExportViewModel("ImageLoaderViewModel")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class ImageLoaderViewModel : ViewModelBase
{
}
Where the ExportViewModelAttribute
is available inside of MefedMVVM, where it inherits from the MEF ExportAttribute
, so has all the good stuff you get when using the standard MEF ExportAttribute
, such as metadata, which is how the code shown above managed to query the Exports metadata to find the Export whose Exports metadata Name value == the requested ViewModel.
Note: One thing to note is that you must also decide what instancing you would like to use for the ViewModel when it gets MEFed. Say you want separate instances (I would say this is the most common requirement), you would use [PartCreationPolicy(CreationPolicy.NonShared)]
, otherwise would you go for [PartCreationPolicy(CreationPolicy.Shared)]
which would give you the same singleton shared VM each time.
In practical terms, all you need to do is use the meffed:ViewModelLocator.ViewModel
attached DP in your View, and mark up your ViewModel with the MefedMVVM ExportViewModelAttribute
, and decide what instancing you need for the Exported ViewModel (Singleton (Shared), or new instance (NonShared)) job done.
Service Resolution
So by now you must have realised that both MefedMVVM and Cinch V2 make extensive use of services. These services may be data services (provide data either at design time or runtime), or may be core framework services such as MessageBox/OpenFile/SaveFile services.
It doesn't really matter actually, the treatment of them is the same within MefedMVVM (and therefore Cinch V2). All we are really looking to achieve is that the ViewModel that MefedMVVM constructs for the View (as outlined above) has the correct services passed in as constructor parameters.
Note: This is a departure from how services worked in Cinch V1, there is no longer any IOC container or ServiceResolver. MefedMVVM deals with all of this for us.
So how does it do that exactly? Well, as before, there is a special attribute called ExportServiceAttribute
which can be used to mark up your services. Let's look at a couple of examples.
Common Core Service Example
Assume you have a Core common service which has a service contract that looks like this:
using System;
using System.Windows;
namespace Cinch
{
public interface IOpenFileService
{
String FileName { get; set; }
String Filter { get; set; }
String InitialDirectory { get; set; }
bool? ShowDialog(Window owner);
}
}
Which can then be used for the actual service implementation like this:
using System;
using System.Collections.Generic;
using System.Windows;
using Microsoft.Win32;
using System.ComponentModel.Composition;
using MEFedMVVM.ViewModelLocator;
namespace Cinch
{
[PartCreationPolicy(CreationPolicy.Shared)]
[ExportService(ServiceType.Both, typeof(IOpenFileService))]
public class WPFOpenFileService : IOpenFileService
{
.....
.....
}
}
See how we simply use the MefedMVVM ExportServiceAttribute
and tell it that this is a common service that should be used as both design time and runtime? The other thing of note here is that since we are using MEF, we can make use of the standard MEF PartCreationPolicyAttribute
to specify the lifecycle and instancing of the part. In this case, CreationPolicy.Shared
is used, so there will only be one instance that is shared to all importers.
Design Time vs. Runtime Service Example
Considering a different example, let's say we have the need to fetch some data (say from a WCF service or the file system, whatever really) and we would like to be able to visualize design time data for this service, we may start out with a service contract that looks like this:
public interface IImageProvider
{
void FetchImages(string imagePath, Action<List<ImageData>> callback);
}
Where we then have a runtime service implementation that looks like this (note the ServiceType
is set to ServiceType.Runtime
, and that this time we do not want shared services, so we simply use the CreationPolicy.NonShared
to ensure each ViewModel gets its own copy):
[PartCreationPolicy(CreationPolicy.NonShared)]
[ExportService(ServiceType.Runtime, typeof(IImageProvider))]
public class RunTimeImageProvider : IImageProvider
{
}
We would then have the same service definition but for design time, either in the main Assembly
, or in a totally different Assembly
. In the demo apps associated with Cinch V2, I am using totally separate design time Assemblies that are not even referenced by the main app, and design time data is still supported by MefedMVVM, thanks in part to how Blend works, but more on that in just a minute. For now, the important thing to note is that the design service looks the same as the runtime service but has a ServiceType.Designtime
set on it.
[PartCreationPolicy(CreationPolicy.NonShared)]
[ExportService(ServiceType.DesignTime, typeof(IImageProvider))]
public class DesigntimeImageProvider : IImageProvider
{
}
So that is pretty much all we need to do to create Core services and Design time/Runtime services. Easy, isn't it?
But I guess you lot want to know how all this works. Exactly how does MefedMVVM know what services to inject into the resolved ViewModel constructor? Well, the answer to that partially lies in understanding how Blend works (remember MefedMVVM is really aimed at working with Blend).
How does Blend work when you load a solution into it? Well, what happens is Blend has an AppDomain
that knows about all the Assemblies in the current solution file. So you can exploit that, and see if any of these Assemblies holds a reference to the MefedMVVM DLL, and if it does, it is a candidate to examine for Export parts (Services/ViewModels). And this is exactly what MefedMVVM does, so basically, when you add a reference to the MefedMVVM DLL, you are allowing MefedMVVM to include the Assembly
that references MefedMVVM as part of the overall CompositionContainer
that contains the Export
s.
And that is how you are able to have a totally separate DLL for design time services that the main app does not even hold a reference to.
That is one part of the puzzle that explains how the correct DLLs are examined by MefedMVVM in the first place. But how does MefedMVVM know which of the two services to inject into the resolved ViewModel? Well, that is done using two things:
- A MefedMVVM bootstrapper
- A special MefedMVVM Catalog called
MEFedMVVMCatalog
Let's examine the inner workings of these two items, shall we?
The MefedMVVM Bootstrapper
The bootstrapper is responsible for setting up either a design time composer or a runtime composer and adding these to the MEFedMVVMCatalog
. Here is the relevant code from the MefedMVVM bootstrapper:
public static CompositionContainer EnsureLocatorBootstrapper()
{
IComposer composer = null;
if (Designer.IsInDesignMode)
{
if (designTimeComposer == null)
designTimeComposer = new DefaultDesignTimeComposer();
composer = designTimeComposer;
}
else
{
if (runtimeComposer == null)
runtimeComposer = new DefaultRuntimeComposer();
composer = runtimeComposer;
}
var catalog = composer.InitializeContainer();
MEFedMVVMExportProvider provider =
new MEFedMVVMExportProvider(MEFedMVVMCatalog.CreateCatalog(catalog));
CompositionContainer container = new CompositionContainer(provider);
provider.SourceProvider = container;
return container;
}
Can you see that there is a DefaultDesignTimeComposer
? Let's have a look at that:
public class DefaultDesignTimeComposer : IComposer
{
#region IComposer Members
public ComposablePartCatalog InitializeContainer()
{
return GetCatalog();
}
#endregion
private AggregateCatalog GetCatalog()
{
IList<AssemblyCatalog> assembliesLoadedCatalogs =
(from assembly in AppDomain.CurrentDomain.GetAssemblies()
where assembly.GetReferencedAssemblies().Where(
x => x.Name.Contains("MEFedMVVM.WPF")).Count() > 0 ||
assembly.ManifestModule.Name == "MEFedMVVM.WPF.dll"
select new AssemblyCatalog(assembly)).ToList();
if (assembliesLoadedCatalogs.Where(x => x.Assembly.ManifestModule.Name
!= "MEFedMVVM.WPF.dll").Count() == 0)
{
Debug.WriteLine("No assemblies found for Design time. Quick tip... ");
return null;
}
var catalog = new AggregateCatalog();
foreach (var item in assembliesLoadedCatalogs)
catalog.Catalogs.Add( item);
return catalog;
}
}
And here is the DefaultRuntimeComposer
:
public class DefaultRuntimeComposer : IComposer
{
#region IComposer Members
public ComposablePartCatalog InitializeContainer()
{
return GetCatalog();
}
#endregion
private AggregateCatalog GetCatalog()
{
var catalog = new AggregateCatalog();
var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
var extensionPath = String.Format(@"{0}\Extensions\", baseDirectory);
catalog.Catalogs.Add(new DirectoryCatalog(baseDirectory));
catalog.Catalogs.Add(new DirectoryCatalog(baseDirectory, "*.exe"));
if (Directory.Exists(extensionPath))
catalog.Catalogs.Add(new DirectoryCatalog(extensionPath));
return catalog;
}
}
So you can see that different things are loaded dependant on whether you are in design time or runtime. One thing I really like is that if you are not happy with how this works, Marlon has given you the ability to swap in your own IComposer
implementations into the LocatorBootstrapper
, which allows you to be in control of how the design time/runtime catalogs get created.
But that still doesn't answer how MefedMVVM knows which services (design time or runtime) to inject into the ViewModels. So how does that work? That is done by a special catalog called MEFedMVVMCatalog
.
MEFedMVVMCatalog
The retrieval of design time or runtime exports is done by the MEFedMVVMCatalog
, which works as follows:
public class MEFedMVVMCatalog : ComposablePartCatalog
{
private readonly ComposablePartCatalog _inner;
private readonly IQueryable<ComposablePartDefinition> _partsQuery;
public MEFedMVVMCatalog(ComposablePartCatalog inner, bool designTime)
{
_inner = inner;
if (designTime)
_partsQuery = inner.Parts.Where(p => p.ExportDefinitions.Any(
ed => !ed.Metadata.ContainsKey("IsDesignTimeService") ||
ed.Metadata.ContainsKey("IsDesignTimeService") &&
(ed.Metadata["IsDesignTimeService"].Equals(ServiceType.DesignTime) ||
ed.Metadata["IsDesignTimeService"].Equals(ServiceType.Both))));
else
_partsQuery = inner.Parts.Where(p => p.ExportDefinitions.Any(
ed => !ed.Metadata.ContainsKey("IsDesignTimeService") ||
ed.Metadata.ContainsKey("IsDesignTimeService") &&
(ed.Metadata["IsDesignTimeService"].Equals(ServiceType.Runtime) ||
ed.Metadata["IsDesignTimeService"].Equals(ServiceType.Both))));
}
public override IQueryable<ComposablePartDefinition> Parts
{
get
{
return _partsQuery;
}
}
public static MEFedMVVMCatalog CreateCatalog(ComposablePartCatalog inner)
{
return new MEFedMVVMCatalog(inner, Designer.IsInDesignMode);
}
}
Which if we just go back and have a look at the MefedMVVM LocatorBootstrapper
again,
var catalog = composer.InitializeContainer();
MEFedMVVMExportProvider provider =
new MEFedMVVMExportProvider(MEFedMVVMCatalog.CreateCatalog(catalog));
CompositionContainer container = new CompositionContainer(provider);
provider.SourceProvider = container;
return container;
We can see this code has a direct impact on what is added to the CompositionContainer
, such that when the CompositionContainer
is queried, we know we are either getting design time Exports or runtime Exports or both.
And that is how design time / runtime or shared services are resolved by MefedMVVM.
View Context
I have one last story for you. Whilst MefedMVVM was being developed, Marlon, Glenn Block (Microsoft MEF Program Manager) and I were having some quite lengthy discussions about how to handle ViewModels that needed to link to the View. We all spent a great deal of email time on this and many ideas were bounced around, then finally, Glenn let the cat out of the bag, and told Marlon and I that we could do what we were asking about using something called an ExportProvider
.
Now going back to the MEF overview image I included at the start of this article:
You can see that CustomExportProvider
is something that the CompositionContainer
can make use of. There is not that much information about creating custom ExportProviders out there, Glenn has one good blog post, where he talks about this.
Parts in MEF carry exports and imports. During composition, the container composes parts and satisfies imports. In order to do this, it queries a series of export providers as can be seen in the diagram below.
If you take a look at the ExportProvider API, you'll see the following:
At first glance, you may be thinking, wow that looks anything but simple. Majority of these methods are different ways for specifying a set of exports to retrieve, a format to return them, and whether or not it is a single item or a collection that is returned. The GetExport
/ GetExports
methods return lazy instantiated objects which are of type Export
. The GetExportedObject
/ GetExportedObjects
method returns the actual instances that the Exports create.
Fortunately about 95% of the methods are syntactic sugar around one core method, which is the only method you need to implement when authoring a custom ExportProvider.
That method takes an ImportDefinition
and returns a collection of Exports.
ImportDefinition
You can think of the ImportDefinition
as similar to a SQL Where
clause. It specifies a filter for which Exports to return. The ImportDefinition
has two main components. The constraint is an Expression<Func<ExportDefinition, bool>>
and represents the export filter. cardinality
is an enum which specifies the cardinality of the exports, it can be ZeroOrOne
(one max, but zero is allowed), ExactlyOne
, or ZeroOrMore
(a collection of 0 to N). We'll hold of on talking about the other params for now.
These definitions come from several places. Parts carry import definitions; for example, when you decorate a Part with one or more Import attributes, it will have ImportDefinition
s created for each Import as it is picked up by the catalog. The ExportProvider
has several public methods that accept definitions as parameters. As the container is an ExportProvider
, this means definitions may be passed in directly through its methods. Finally, if any of the overloaded GetExport
(s)/GetExportObject
(s) methods on the ExportProvider
that do not accept an ImportDefinition
are called, internally an ImportDefinition
will be created.
For example, the snippet below illustrates creating a definition that matches on all exports that use the convention "Service" as a suffix.
This is a very simple constraint, but you can let your mind run wild as to what you can do through an expression.
All italic text taken from Glenn Block's blog post: http://blogs.msdn.com/b/gblock/archive/2008/12/25/using-exportprovider-to-customize-container-behavior-part-i.aspx up on date 13/06/2010.
Now you may be asking just what the heck has that got to do with creating ViewModels? Well, recall I started this section by stating that Marlon, Glenn Block (Microsoft MEF Program Manager), and I were all looking for a nice way to make a View aware service that could be a regular Export using MEF, but this time, it would have the View injected into it as part of its creation. So to do this, you need to create a custom ExportProvider
and associate that with your CompositionContainer
somehow. In the case of MefedMVVM, that link is done through the use of the MEFedMVVMExportProvider
which gets passed the MEFedMVVMCatalog
which we discussed in the last subsection.
Recall these few lines:
var catalog = composer.InitializeContainer();
MEFedMVVMExportProvider provider =
new MEFedMVVMExportProvider(MEFedMVVMCatalog.CreateCatalog(catalog));
CompositionContainer container = new CompositionContainer(provider);
provider.SourceProvider = container;
return container;
See the MEFedMVVMExportProvider
in there? Let's have a closer look at that, shall we? The complete code for the MEFedMVVMExportProvider
class is shown below.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.Composition.Hosting;
using System.ComponentModel.Composition.Primitives;
using MEFedMVVM.Services.Contracts;
namespace MEFedMVVM.ViewModelLocator
{
public class MEFedMVVMExportProvider : ExportProvider, IDisposable
{
private CatalogExportProvider _exportProvider;
public MEFedMVVMExportProvider(ComposablePartCatalog catalog)
{
_exportProvider = new CatalogExportProvider(catalog);
_exportProvider.ExportsChanged += (s, e) => OnExportsChanged(e);
_exportProvider.ExportsChanging += (s, e) => OnExportsChanging(e);
}
public ExportProvider SourceProvider
{
get
{
return _exportProvider.SourceProvider;
}
set
{
_exportProvider.SourceProvider = value;
}
}
protected override System.Collections.Generic.IEnumerable<Export> GetExportsCore(
ImportDefinition definition, AtomicComposition atomicComposition)
{
var exports = _exportProvider.GetExports(definition, atomicComposition);
return exports.Select(export =>
new Export(export.Definition, () => GetValue(export)));
}
private object GetValue(Export innerExport)
{
var value = innerExport.Value;
var context = value as IContextAware;
if (context != null)
{
context.InjectContext(_context);
}
return value;
}
private object _context;
public void SetContextToInject(object context)
{
_context = context;
}
#region IDisposable Members
public void Dispose()
{
_exportProvider.Dispose();
}
#endregion
}
}
The area of particular interest here is the SetContextToInject(object context)
method and the GetExportsCore(ImportDefinition definition, AtomicComposition atomicComposition)
and GetValue(Export innerExport)
methods. Glenn's blog explains the GetExportsCore(..)
method, but what about the SetContextToInject(object context)
? What is that doing for us, and how does it work?
OK, so here is what happens. Within the MefedMVVMResolver.GetViewModelByContract(string vmContractName, object contextToInject)
(which we talked about a while back), there is a bit of code, where we call a method called SetContextToExportProvider(object contextToInject)
which is used to inject the MEFedMVVMExportProvider
using the SetContextToInject(object context)
method, with the current View, just before trying to satisfy the creation of a ViewModel from the CompositionContainer
.
MEFedMVVMResolver code excerpt:
SetContextToExportProvider(contextToInject);
var vmExports = Container.GetExports(definition);
SetContextToExportProvider(null);
...
...
internal void SetContextToExportProvider(object contextToInject)
{
if (Container.Providers != null && Container.Providers.Count >= 1)
{
foreach (var item in Container.Providers)
{
var mefedProvider = item as MEFedMVVMExportProvider;
if (mefedProvider != null)
mefedProvider.SetContextToInject(contextToInject);
}
}
}
We now have some context (the actual View) within the MEFedMVVMExportProvider
, so when we attempt to get an Export through the CompositionContainer
, we will have the context (the View just added to MEFedMVVMExportProvider
) injected into the service just before the Export is created. This is only done for Exports that implement IContextAware
.
Have a look at this section of the MEFedMVVMExportProvider
again. It may be clearer what is going on now. Basically, it boils down to this: a View in injected into the MEFedMVVMExportProvider
just before a ViewModel export is created. Then for any of the ViewModel services that implement IContextAware
, the available context object (the View) is injected into the service prior to the service export being created to satisfy the import for the ViewModel being created.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.Composition.Hosting;
using System.ComponentModel.Composition.Primitives;
using MEFedMVVM.Services.Contracts;
namespace MEFedMVVM.ViewModelLocator
{
public class MEFedMVVMExportProvider : ExportProvider, IDisposable
{
.......
.......
.......
.......
protected override System.Collections.Generic.IEnumerable<Export> GetExportsCore(
ImportDefinition definition, AtomicComposition atomicComposition)
{
var exports = _exportProvider.GetExports(definition, atomicComposition);
return exports.Select(export =>
new Export(export.Definition, () => GetValue(export)));
}
private object GetValue(Export innerExport)
{
var value = innerExport.Value;
var context = value as IContextAware;
if (context != null)
{
context.InjectContext(_context);
}
return value;
}
private object _context;
public void SetContextToInject(object context)
{
_context = context;
}
.......
.......
.......
.......
}
}
That's It ....For Now
Anyway, that is all for now. Hope you enjoyed it, but there will be more, much more....But for now, I am off on a well earned holiday for two weeks where I plan on drinking myself into a coma and eating three herds of buffalos. When I come back all refreshed, I will write the rest of the articles, but if you find you just can not wait and wish to explore the codebase for Cinch V2, go ahead. One thing I should mention again is that if you have any deep MEF related questions, you should direct those to Marlon Grech either by using his blog C# Disciples, or by using the MefedMVVM CodePlex site. Any other Cinch V2 questions will be answered by the next Cinch V2 articles.