Reputation: 139
I'm experimenting with using Caliburn and MEF in a WPF application. My MEF knowledge is sketchy at best.
Here's the Bootstrapper:
class Bootstrapper : BootstrapperBase
{
public Bootstrapper()
{
Initialize();
}
protected override void OnStartup(object sender, StartupEventArgs e)
{
DisplayRootViewFor<IShell>();
}
protected override void Configure()
{
//An aggregate catalog that combines multiple catalogs
var catalog = new AggregateCatalog();
//Adds all the parts found in the same assembly as the Program class
catalog.Catalogs.Add(new AssemblyCatalog(typeof(Bootstrapper).Assembly));
//Create the CompositionContainer with the parts in the catalog
container = new CompositionContainer(catalog);
var batch = new CompositionBatch();
batch.AddExportedValue<IWindowManager>(new WindowManager());
batch.AddExportedValue<IEventAggregator>(new EventAggregator());
batch.AddExportedValue(container);
container.Compose(batch);
}
protected override object GetInstance(Type serviceType, string key)
{
string contract = string.IsNullOrEmpty(key) ? AttributedModelServices.GetContractName(serviceType) : key;
var exports = container.GetExportedValues<object>(contract);
if (exports.Any())
return exports.First();
throw new Exception(string.Format("Could not locate any instances of contract {0}.", contract));
}
protected override IEnumerable<object> GetAllInstances(Type serviceType)
{
return container.GetExportedValues<object>(AttributedModelServices.GetContractName(serviceType));
}
protected override void BuildUp(object instance)
{
container.SatisfyImportsOnce(instance);
}
}
Here's the shellview:
<Window x:Class="MefCaliburn.ShellView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MefCaliburn"
mc:Ignorable="d"
Title="ShellView" Height="300" Width="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="20"/>
<RowDefinition/>
<RowDefinition Height="100"/>
</Grid.RowDefinitions>
<ContentControl Grid.Row="0" x:Name="Menu"></ContentControl>
<ContentControl Grid.Row="1"></ContentControl>
<ContentControl Grid.Row="2" x:Name="Messages"></ContentControl>
</Grid>
The IShell interface:
public interface IShell
{
}
Here's my shell view model:
namespace MefCaliburn
{
[Export(typeof(IShell))]
public class ShellViewModel : PropertyChangedBase, IShell
{
private readonly IEventAggregator _events;
UserViewModel uvm;
[ImportingConstructor]
public ShellViewModel(MenuViewModel menu, MessagesViewModel mess, IEventAggregator eventaggregator)
{
Messages = mess;
Menu = menu;
_events = eventaggregator;
}
public MessagesViewModel Messages
{
get; set;
}
public MenuViewModel Menu
{
get; set;
}
public void LaunchUserViewModel()
{
uvm = new UserViewModel();
}
}
}
So when the Boostrapper overridden method
protected override void OnStartup(object sender, StartupEventArgs e)
{
DisplayRootViewFor<IShell>();
}
gets called, my ShellViewModel constructor is called and the Menu and Messages view models are Injected. This is an example of dependency injection, correct?
In my case the Menu & Messages view models are created along with the shell. But if a new view model
[Export(typeof(UserViewModel))]
public class UserViewModel : PropertyChangedBase
{
private readonly IEventAggregator _events;
[ImportingConstructor]
public UserViewModel(IEventAggregator eventaggregator)
{
_events = eventaggregator;
}
}
is created when the user presses a button on the MenuView.xaml
<UserControl x:Class="MefCaliburn.MenuView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:MefCaliburn"
xmlns:cal="http://www.caliburnproject.org"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<StackPanel Orientation="Horizontal">
<Button Content="ONE" cal:Message.Attach="[Event Click] = [Action LaunchUserViewModel()]"></Button>
<Button Content="TWO"></Button>
</StackPanel>
</Grid>
This UserViewModel will be publishing events to the MessagesViewModel, I'm going to need the IEventAggregator. Yes, I can just pass it in explicitly to the constructor, but what I'm trying to understand is why MEF won't inject it.
Am I trying to use MEF in a manner it's not intended? Is MEF only meant to be used when the application starts up for those view models that are known to be required?
Upvotes: 3
Views: 1308
Reputation: 3915
You are a little confused. First of all I think that you don't really want to be using MEF
in the first place, but I will get to that later. You try to create a new viewmodel in your LaunchUserViewModel
. The whole idea of MEF
and Dependency Injection
is to get rid of it. As you can see here new UserViewModel()
when you try to create a new instance you have to supply all the required parameters. But the whole idea is to let the framework do the work and supply them for us. So first of all we have to get rid of the new
keyword and think about instantiating it differently. Let's look at your example:
This is ok. You Export a module of certain type.
[Export(typeof(IUserViewModel))]
public class UserViewModel : PropertyChangedBase
{
private readonly IEventAggregator _events;
[ImportingConstructor]
public UserViewModel(IEventAggregator eventaggregator)
{
_events = eventaggregator;
}
}
public interface IUserViewModel {
void SomeMethod();
}
So what do we usually do when we export something? We import it somewhere else:
[Export(typeof(IShell))]
public class ShellViewModel : PropertyChangedBase, IShell
{
[Import]
public IUserViewModel Uvm { get; set; }
public void LaunchUserViewModel()
{
Uvm.SomeMethod(); // Your Uvm is already created
}
}
As you can see we completed removed new
keyword. This means that the ShellViewModel
doesn't know what class exactly it's gonna import. And that's the beauty of it. You can swap implementations any time you want, and you don't have to change ShellViewModel
What happens here is AddExportedValue<IEventAggregator>(new EventAggregator());
notifies MEF
, hey, here's a class that something might want. Later when you do [ImportingConstructor]
, MEF
looks for the classes that the constructor needs and if it has them, it injects them for you.
Now, I think you just wanted Dependency Injection
and got into the wrong framework. Although MEF
allows you to use DI
, it is much more powerful. It uses a concept of modules so that you can build modular applications. It is very useful when you want to allow people to write plugins for your application.
What it would look like is:
You would only show plugins what you want them to show, so you would create a project called API
which would store only contracts(interfaces). Then you would have you program's Core
and users would supply Plugins
. Users would only see API
, not Core
, and you would load their plugins knowing that they actually implement API
interfaces you told them to implement.
You don't need MEF
to use Dependency Injection
. Caliburn.Micro
has built in container, but you can also use Simple Injector, Ninject, and many more.
I hope it makes sense. It's a vast subject.
EDIT:
I created a very basic project that uses Caliburn.Micro
with Dependecy injection. Maybe it will help you to understand it better.
Upvotes: 3
Reputation: 4664
First of all, I highly recommend you give up MEF and use an IoC container that's much less clunky and easier to use (like SimpleInjector). But if you want to go MEF route you will need to export the EventAggregator inside your boostrapper class as following:
private CompositionContainer _container;
protected override void Configure()
{
_container = new CompositionContainer(new ApplicationCatalog());
var batch = new CompositionBatch();
batch.AddExportedValue<IWindowManager>(new WindowManager());
batch.AddExportedValue<IEventAggregator>(new EventAggregator());
batch.AddExportedValue(_container);
_container.Compose(batch);
}
Upvotes: 3