如何在Caliburn.Micro中使用Conductors实现依赖注入

18

我有时使用Caliburn.Micro创建应用程序。

使用最简单的BootStrapper,我可以像这样使用IoC容器(SimpleContainer):

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);
}

所以在Configure方法中,我可以像这样添加和注册我的ViewModels:
container.PerRequest<MyMainViewModel>();

当请求时,我的ViewModel的构造函数可以有一个由IoC容器注入的参数:

public MyMainViewModel(IWindowManager windowManager)
{
  //do the init
}

当我调用DisplayRootViewFor<MyMainViewModel>()时,它按预期工作。

但是,如果我想创建一些更多的逻辑并使用Conductor会发生什么?

在示例中,作者使用了一个简单的、无IoC实现的“方便”方式:

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());
    }
}

因此,他们实例化ViewModels,而不是从IoC容器请求实例。

在这种情况下,依赖注入的正确使用方式是什么?

我有另一个具有以下构造函数的ViewModel:

public MySecondViewModel(MyParamClass input)
{
  //do the work
}

我应该像这样修改代码吗?
在 Configure 方法中:
simpleContainer.PerRequest<MyParamClass>(); //How could it be different every time?

在导体中:
public void ShowPageOne() 
{
   ActivateItem(IoC.Get<MySecondViewModel>());
}

此外,这种做法是否被允许或违反DI规则:
protected override object GetInstance(Type serviceType, string key) 
{
  if(serviceType==typeof(MySecondViewModel))
    return new MySecondViewModel(new MyParamClass(2));
  return _container.GetInstance(serviceType, key);
}

我可以看到使用DI,ViewModels应该由IoC容器提供而不是手动创建(更不用说所需的参数 - 它在容器内部)。

那么你能给一些提示如何使用conductor实现IoC模式吗?


我也在寻找解决这个问题的最佳实践...我能告诉你的是,从IoC直接初始化ViewModels并不好...而且我想手动实例化它们也不是那么好(参见:https://stackoverflow.com/a/17373957/2830676)。我想最好的方法是从构造函数获取它,但它会有一个问题,即它会过早地实例化=[ - Yitzchak
1
一些IoC容器允许您在T已注册的情况下依赖于Func<T>。如果您的容器是这种情况,您可以将其用于DI,但仍然只有在实际需要时才实例化视图模型。 - Dirk
@Yitzchak注入工厂委托,以允许依赖项实例化被延迟。有关更多详细信息,请检查提交的答案。 - Nkosi
3个回答

5

最简单和直接的方法是遵循显式依赖原则

因此,假设

public MySecondViewModel(MyParamClass input) {
  //do the work
}

并且它及其依赖已在容器中注册,
simpleContainer.PerRequest<MyParamClass>();
simpleContainer.PerRequest<MySecondViewModel>();

MainViewModel 的调节器可以依赖于一个委托(工厂),该委托可在需要时用于解析依赖关系。

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);
    }
}

尽管没有得到适当的文档支持,SimpleContainer允许以Func<TDependency>形式注入工厂委托(源代码),用于延迟解析/实例化注入的依赖项。您可以利用该功能仅在实际需要时解决依赖关系。

我喜欢你的回答,感觉这就是我需要的答案,但如果您提供一个注入工厂的示例(即使它非常简单),它将更有助于我(和其他人)理解。 - Yitzchak
1
如果您查看MainModel的构造函数,您将看到Func <MySecondViewModel> mySecondViewModelFactory。该函数委托充当工厂。 - Nkosi

3
我通常的做法是引入一个Navigator,并将其与一个单例的ShellView(作为我们的指挥)和IOC container实例相结合。一个简单的导航API可能如下所示,

简单实现:

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);
    }
}

如果需要更灵活的替代方案,可以改进此模式,引入导航请求的概念,将初始化屏幕和屏幕本身以及根据需要激活它的所有细节封装起来。

稍微扩展实现

为此模式设计一个NavigationRequest,例如:

public interface INavigationRequest<out T>
{
    T Screen { get; }
    void Go();
}

更新 INavigator 以返回此请求。

public interface INavigator
{
    INavigationRequest<T> To<T>();
}

为您的ShellViewModel提供一份类似于以下合同:

public interface IShell : IConductActiveItem
{

}

实现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);
            }
        }
    }

一旦这个基础架构建立起来,你可以通过在需要时将INavigator注入到视图模型中来使用它。

通过提供附加的工具函数的扩展方法,可以扩展这个基本的架构,比如说你想在导航到视图模型时传递参数。你可以按照以下方式引入附加的服务:

/// <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);
}

提供相同的实用方法。
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();
    }
}

使用方法:

可以使用简洁的流畅 API 调用:

public void GoToProfile()
{
   //Say, this.CurrentUser is UserProfile 
   //and UserDetailsViewModel implements IAcceptArguments<UserProfile>
   _navigator.To<UserDetailsViewModel>().WithArguments(this.CurrentUser).Go();
}

您可以根据需要扩展此内容,以满足您的要求。这种架构的主要优点是:

  • 将视图模型(屏幕)的解析、导航和初始化与请求者(其他视图模型或服务)分离。
  • 可单元测试,可以轻松模拟不涉及视图模型的所有内容,导航可以单独测试。
  • 可扩展。通过扩展导航器,可以轻松实现其他导航需求,如生命周期管理,在不同视图之间来回导航等。
  • 适应性强 - 可以适应不同的IoC甚至在不更改任何视图模型的情况下进行适应。

我相信答案会对其他人有所帮助,但这不是我要找的。我不需要导航,我需要在同一个屏幕上显示几个视图模型,并且只需要延迟它们的实例化方式。感谢您详细而好的回答。 - Yitzchak

2

解决方案

我认为最好的解决方案是传递一个知道如何创建我的子视图模型的工厂。然后父视图模型将调用该工厂。


成就:

  • 只有在需要时才实例化子视图模型(惰性)
  • 您可以从父视图模型和/或注入中传递参数
  • 您可以使用模拟工厂为父视图模型编写单元测试。这使您能够测试父视图模型是否真正创建了您的子视图模型,而无需实际创建它们。

编辑:感谢@Nkosi的回答,使用Caliburn.Micro可以简单地注入惰性视图模型(类似于工厂)。使用此注入可获得最佳效果。


SimpleContainer 可以注入工厂委托,从而允许延迟依赖项实例化。请查看我提交的答案以获取更多详细信息。 - Nkosi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接