【问题标题】:How to use Dependency Injection with Conductors in Caliburn.Micro如何在 Caliburn.Micro 中对导体使用依赖注入
【发布时间】:2016-04-27 19:58:38
【问题描述】:

我有时使用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 方法中,我可以像这样添加和注册我的 ViewModel:

container.PerRequest<MyMainViewModel>();

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

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

当我打电话给DisplayRootViewFor&lt;MyMainViewModel&gt;()时,它按预期工作

但是,如果我打算创建更多逻辑并使用Conductor,会发生什么?

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

为了让这个示例尽可能简单,我什至没有使用 带有引导程序的 IoC 容器。让我们看看 首先是 ShellViewModel。它继承自 Conductor 并实现为 如下:

public class ShellViewModel : Conductor<object> {
    public ShellViewModel() {
        ShowPageOne();
    }

    public void ShowPageOne() {
        ActivateItem(new PageOneViewModel());
    }

    public void ShowPageTwo() {
        ActivateItem(new PageTwoViewModel());
    }
}

因此他们实例化 ViewModel,而不是从 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,ViewModel 应该由 IoC 容器提供,而不是手动创建(更不用说所需的参数 - 它在容器内)。

那么您能否提供一些提示如何使用导体实现 IoC 模式?

【问题讨论】:

  • 我也在寻找解决这个问题的最佳实践......我只能说直接从 IoC 启动 ViewModel 并不好......而且我想它也不太好手动实例化它们(参见:stackoverflow.com/a/17373957/2830676)。我想最好的方法是从构造函数中获取它,但是它的问题是它被实例化得太快了=[
  • 如果 T 已注册,某些 IoC 容器使您能够依赖 Func&lt;T&gt;。如果您的容器是这种情况,您可以将 is 用于 DI,但仍仅在实际需要时实例化视图模型。
  • @Yitzchak 注入工厂委托,允许延迟依赖实例化。查看提交的答案以获取更多详细信息。

标签: c# dependency-injection caliburn.micro


【解决方案1】:

最简单直接的方法是关注The Explicit Dependency Principle

所以假设

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&lt;TDependency&gt; 的形式注入工厂委托 (Source Code),以延迟解析/实例化注入的依赖项。只有在实际需要时,您才能利用该功能来解决您的依赖关系。

【讨论】:

  • 我喜欢你的回答,我觉得这是我需要的答案,但是如果你提供一个如何注入工厂的例子,它可以帮助我(和其他人)更多(即使它很简单,它将提供完整的答案)
  • @Yitzchak 如果您查看MainModel 的构造函数,您将看到Func&lt;MySecondViewModel&gt; mySecondViewModelFactory。该函数委托充当工厂。
【解决方案2】:

我通常这样做的方式是引入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();
}

这可以根据您的要求进行扩展。像这样的架构的主要优点是,

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

【讨论】:

  • 我确信答案会对其他人有所帮助,但这不是我搜索的内容。我不需要导航,我需要在同一个屏幕上显示几个视图模型,只需要一种方法来延迟它们的实例化。感谢您的详细解答。
【解决方案3】:

解决方案

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


成就:

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

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

【讨论】:

  • SimpleContainer 可以注入工厂委托,这将允许延迟依赖实例化。查看我提交的答案以了解更多详细信息。
猜你喜欢
  • 2015-07-16
  • 1970-01-01
  • 2010-10-10
  • 1970-01-01
  • 2011-11-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-01-29
相关资源
最近更新 更多