I sometimes use Caliburn.Micro to create applications.
Using the simplest BootStrapper, I can use IoC container (SimpleContainer) like this:
private SimpleContainer _container = new SimpleContainer();
protected override object GetInstance(Type serviceType, string key) {
return _container.GetInstance(serviceType, key);
}
protected override IEnumerable<object> GetAllInstances(Type serviceType) {
return _container.GetAllInstances(serviceType);
}
protected override void BuildUp(object instance) {
_container.BuildUp(instance);
}
So in the Configure
method I can add and register my ViewModels like this:
container.PerRequest<MyMainViewModel>();
My ViewModel's constructor can have a parameter that is injected by the IoC container when requested:
public MyMainViewModel(IWindowManager windowManager)
{
//do the init
}
It works as expected, when I call DisplayRootViewFor<MyMainViewModel>()
But what happens, if I intend to create some more logic and use a Conductor?
In the examples, the authors use a simple, IoC-free implementation for "convenience":
In order to keep this sample as simple as possible, I’m not even using an IoC container with the Bootstrapper. Let’s look at the ShellViewModel first. It inherits from Conductor and is implemented as follows:
public class ShellViewModel : Conductor<object> { public ShellViewModel() { ShowPageOne(); } public void ShowPageOne() { ActivateItem(new PageOneViewModel()); } public void ShowPageTwo() { ActivateItem(new PageTwoViewModel()); } }
So they instantiate the ViewModels, instead of requesting an instance from the IoC container.
What would be the proper use of Dependency Injection in this case?
I have another ViewModel that has a constructor like this:
public MySecondViewModel(MyParamClass input)
{
//do the work
}
Should I modify the code like this:
In the Configure
method:
simpleContainer.PerRequest<MyParamClass>(); //How could it be different every time?
In the conductor:
public void ShowPageOne()
{
ActivateItem(IoC.Get<MySecondViewModel>());
}
Also, is this allowed or it violates the rules of DI:
protected override object GetInstance(Type serviceType, string key)
{
if(serviceType==typeof(MySecondViewModel))
return new MySecondViewModel(new MyParamClass(2));
return _container.GetInstance(serviceType, key);
}
I can see that using DI, the ViewModels should be provided by the IoC container and not created manually (not to mention the required parameter - which is inside the container).
So can you give some hint how to implement the IoC pattern with conductors?
Dependency Injection is done by supplying the DEPENDENCY through the class's constructor when creating the instance of that class. The injected component can be used anywhere within the class. Recommended to use when the injected dependency, you are using across the class methods.
Constructor injection should be the main way that you do dependency injection. It's simple: A class needs something and thus asks for it before it can even be constructed. By using the guard pattern, you can use the class with confidence, knowing that the field variable storing that dependency will be a valid instance.
More specifically, dependency injection is effective in these situations: You need to inject configuration data into one or more components. You need to inject the same dependency into multiple components. You need to inject different implementations of the same dependency.
The simplest and most straight forward approach would be to follow The Explicit Dependency Principle
So assuming
public MySecondViewModel(MyParamClass input) {
//do the work
}
And that it and its dependencies are registered with the container,
simpleContainer.PerRequest<MyParamClass>();
simpleContainer.PerRequest<MySecondViewModel>();
the MainViewModel
conductor can depend on a delegate (factory) that can be used to resolve the dependency when needed.
public class MainViewModel : Conductor<object> {
//...
private readonly Func<MySecondViewModel> mySecondViewModelFactory;
public MyMainViewModel(IWindowManager windowManager, Func<MySecondViewModel> mySecondViewModelFactory) {
this.mySecondViewModelFactory = mySecondViewModelFactory;
//...do the init
}
public void ShowPageOne() {
var item = mySecondViewModelFactory(); //invoke factory
ActivateItem(item);
}
}
While not properly documented, the SimpleContainer
allows for the injection of factory delegates (Source Code) in the form of Func<TDependency>
for deferred resolution/instantiation of injected dependencies. You can take advantage of that feature to resolve your dependencies only when they are actually needed.
The way I usually do this is to introduce a Navigator
and couple it with a singleton ShellView(which will be our conductor) and the IOC container
instance. A simplistic navigation api might look like,
Simple implementation:
public interface INavigator
{
void Navigate<T>();
}
public class Navigator : INavigator
{
private ShellViewModel _shellview;
public Navigator(ShellViewModel shellview) //where ShellViewModel:IConductor
{
_shellview = shellview;
}
public void Navigate<T>()
{
//you can inject the IOC container or a wrapper for the same from constructor
//and use that to resolve the vm instead of this
var screen = IoC.Get<T>();
_shellview.ActivateItem(screen);
}
}
For a more flexible alternative, you can improve on this pattern to introduce the concept of a navigation request, encapsulating all the details regarding initializing the screen and the screen itself and activate it as required.
A little Extended Implementation
For such a pattern, design a NavigationRequest
such as,
public interface INavigationRequest<out T>
{
T Screen { get; }
void Go();
}
Update the INavigator
to return this request.
public interface INavigator
{
INavigationRequest<T> To<T>();
}
Provide a contract for your ShellViewModel similar to
public interface IShell : IConductActiveItem
{
}
Implement the INavigator
:
public class MyApplicationNavigator : INavigator
{
private readonly IShell _shell;
public MyApplicationNavigator(IShell shell)
{
_shell = shell;
}
public INavigationRequest<T> To<T>()
{
return new MyAppNavigationRequest<T>(() => IoC.Get<T>(), _shell);
}
/// <summary>
/// <see cref="MyApplicationNavigator"/> specific implementation of <see cref="INavigationRequest{T}"/>
/// </summary>
/// <typeparam name="T">Type of view model</typeparam>
private class MyAppNavigationRequest<T> : INavigationRequest<T>
{
private readonly Lazy<T> _viemodel;
private readonly IShell _shell;
public MyAppNavigationRequest(Func<T> viemodelFactory, IShell shell)
{
_viemodel = new Lazy<T>(viemodelFactory);
_shell = shell;
}
public T Screen { get { return _viemodel.Value; } }
public void Go()
{
_shell.ActivateItem(_viemodel.Value);
}
}
}
Once this infrastructure is in place, you can consume it by injecting INavigator
to the view models as required.
This basic architecture can be extended by way of extension methods for providing additional utility functions, say you want to pass arguments to the view models while navigating to them. You can introduce additional services as follows,
/// <summary>
/// Defines a contract for View models that accept parameters
/// </summary>
/// <typeparam name="T">Type of argument expected</typeparam>
public interface IAcceptArguments<in T>
{
void Accept(T args);
}
Provide utility methods for the same,
public static class NavigationExtensions
{
public static INavigationRequest<T> WithArguments<T, TArgs>(this INavigationRequest<T> request, TArgs args) where T : IAcceptArguments<TArgs>
{
return new NavigationRequestRequestWithArguments<T, TArgs>(request, args);
}
}
internal class NavigationRequestRequestWithArguments<T, TArgs> : INavigationRequest<T> where T : IAcceptArguments<TArgs>
{
private readonly INavigationRequest<T> _request;
private readonly TArgs _args;
public NavigationRequestRequestWithArguments(INavigationRequest<T> request, TArgs args)
{
_request = request;
_args = args;
}
public T Screen { get { return _request.Screen; } }
public void Go()
{
_request.Screen.Accept(_args);
_request.Go();
}
}
Usage:
This can be consumed using a concise fluent api:
public void GoToProfile()
{
//Say, this.CurrentUser is UserProfile
//and UserDetailsViewModel implements IAcceptArguments<UserProfile>
_navigator.To<UserDetailsViewModel>().WithArguments(this.CurrentUser).Go();
}
This can be extended upon as much as required as your requirements go. The main advantages of an architecture like this are,
IoC
or even without one by not altering any of your view models.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With