Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use Dependency Injection with Conductors in Caliburn.Micro

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?

like image 287
Nestor Avatar asked Apr 27 '16 19:04

Nestor


People also ask

How do you add dependency injection?

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.

Which is the right way to inject the dependency?

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.

Where do we use dependency injection?

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.


2 Answers

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.

like image 82
Nkosi Avatar answered Nov 02 '22 15:11

Nkosi


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,

  • You are decoupling the resolving, navigation and initialization of view models(screens) from the requestor(other view models or services).
  • Unit testable, you can mock away everything not concerning your view models, navigation can be tested separately.
  • Extensible. Additional navigation requirements like life cycle management, navigating back and forth between different views and such can be easily implemented by extending the Navigator.
  • Adaptability - Can be adapted to different IoC or even without one by not altering any of your view models.
like image 35
Mat J Avatar answered Nov 02 '22 13:11

Mat J