How to use Dependency Injection with Conductors in Caliburn.Micro
Asked Answered
S

3

18

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?

Salmonella answered 27/4, 2016 at 19:58 Comment(3)
I'm also searching for the best practice for that problem... All I can tell that it's not good to initiate the ViewModels from IoC directly... And I guess it's also not so good to instantiate them manually (see: https://mcmap.net/q/742153/-inject-eventaggregator-into-viewmodel-with-caliburn-micro). I guess the best way is to get it from the constructor, but it has that problem that it gets instantiated too soon =[Pane
Some IoC containers enable you to depend on a Func<T> if T is registered. If that's the case with your container, you could use is for DI yet still only instantiate the viewmodel when it's actually required.Chickadee
@Pane inject factory delegate that will allow for the dependency instantiation to be deferred. Check submitted answer for more details.Koball
K
5

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.

Koball answered 21/7, 2018 at 14:18 Comment(2)
I like your answer, I feel that this is the answer I need, but it can help me (and others) more if you provide an example how to inject the factory (even though it is very simple, it will provide a complete answer)Pane
@Pane If you look at the constructor of the MainModel you will see the Func<MySecondViewModel> mySecondViewModelFactory. That function delegate acts as the factory.Koball
R
3

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.
Ringster answered 20/7, 2018 at 9:7 Comment(1)
I’m sure answer will help others, but it’s not what I searched for. I don’t need navigation, I need to show several view models in the same screen, and just needed a way to delay their instantiation. Thanks for your good and detailed answer.Pane
P
2

Solution

I believe that the best solution is to pass a factory that know how to create my child View Models. And the parent View Model will call the factory.


Achievements:

  • You instantiate the child View Model only when needed (lazy)
  • You can pass parameters from parent View Model and\or from injection
  • You can write unit tests for your parent View Model with mocked factory. That enables you to test that parent view model created your child View Models without really creating them.

EDIT: Thanks to @Nkosi answer, there is a simple way to inject lazy view models (factory like) with Caliburn.Micro :) Use my answer with this injection for best results.

Pane answered 19/7, 2018 at 15:58 Comment(1)
SimpleContainer can inject factory delegate that will allow for the dependency instantiation to be deferred. Check my submitted answer for more details.Koball

© 2022 - 2024 — McMap. All rights reserved.