静态 class 到依赖注入

Static class to dependency injection

我尝试开始工作的应用程序使用 MVVM。这个 post 的最大部分是对我尝试过的和我得到的工作的解释。问题在 post 的底部附近。使用的 Localizer class 仅用作示例,可以轻松替换为另一个 class.

我有一个 class library 和一个 Localizer class。此 class 的目的是即时更改应用程序的语言,而无需重新启动应用程序。 `Localizer 在使用之前必须被实例化,但是一旦实例化,就应该可以在整个应用程序中使用。 (class 使用应用程序资源对应用程序进行本地化。)

我的 第一种方法 我能想到的是用 public static void Initialize 方法将 Localizer 变成 public static class。这样我就可以像这样初始化 Localizer

Localizer.Initialize(/* Needed arguments here */);

在应用程序级别,在我的 class 库或这样的应用程序中的任何地方使用它

string example = Localizer.GetString(/* A key in the resource dictionary */);

考虑到 class 库是我写的(只有我有源码),其他不知道源码的人使用(他们只知道 class 库是什么可以),我必须在某种 "How to use this class library" 中明确声明他们需要在应用程序级别调用 Localizer.Initialize 才能在他们的应用程序中的任何地方使用它。

在做了一些研究之后,很多人表示这是一种不好的做法,并建议调查什么 依赖注入 (DI) 和控制反转 (IoC),所以我做到了。我了解到 DI 的做法与我的第一种方法大致相同,但删除了静态内容,使用 Localizer.Initialize 作为构造函数并将实例化的 class 注入我的其他 classes.

所以 第二种方法 是依赖注入,这就是我卡住的地方。我设法让我的应用程序使用单个 MainWindowViewMainWindowViewModel 进行编译,代码如下:

protected override void OnStartup(StartupEventArgs e)
{
    ILocalizer localizer = new Localizer(Current.Resources, System.Reflection.Assembly.GetExecutingAssembly().GetName().Name, "Languages", "Language", "en");

    var mainWindowViewModel = new MainWindowViewModel(localizer);

    var mainWindowView = new MainWindowView { DataContext = mainWindowViewModel };

    mainWindowView.Show();

    base.OnStartup(e);
}

上面的代码所做的是,将 localizer 注入 MainWindowViewModel。这样就不会向后面的 MainWindowView 代码添加额外的代码,并且视图模型绑定到视图。

MainWindowViewModel中构造函数是这样的(注意消息框在别处被调用但是被移到这里以最小化代码):

ILocalizer _localizer;

public MainWindowViewModel( ILocalizer localizer)
{
    _localizer = localizer;

    MessageBox.Show(_localizer.GetString(/* A key in the resource dictionary */));
}

以上代码仍在编译中,运行 没有异常。当我的 class library 中有 UserControls 并且视图和视图模型也需要 localizer 实例时,就会出现问题。

我想我有一个解决方案,当我在我的应用程序程序集中有一个 UserControl 但感觉它比我使用 static class 时更多 'complex'。我通常只是将 UserControl 的视图模型绑定到它后面的代码中的视图。这样我就可以像这样 <local:UserControl1 /> 简单地将 UserControl 添加到我的 .xaml 代码中,而不需要很多额外的操作。这样视图模型 parent 视图模型就不必关心 child 视图模型了。

使用 DI,我会在我的 parent 中做类似的事情(child 与前面的代码块相同)

查看

<n:UserControl1 DataContext="{Binding UC1ViewModel}" />

视图模型

public UserControl1ViewModel UC1ViewModel { get; set; }
ILocalizer _localizer;

public MainWindowViewModel(ILocalizer localizer)
{
    _localizer = localizer;
    UC1ViewModel  = new UserControl1ViewModel(localizer);
}

以上仍然一切正常,到目前为止没有问题。唯一改变的是 DataContext 设置在 parents 视图中,DataContext 的内容设置在 parent 的视图模型中。

问题 我的 class library 里也有几个 UserControlsclass library 的用户可以使用它们,但他们不能更改它们。这些UserControls大多是一些固定的pages,显示人、车等信息,其用意是,比如人名标签是英文的"Name" , "Naam" 荷兰语等(它们都在视图中声明并且工作正常)但后面的代码中也有文本必须本地化,这就是我被卡住的地方。

我是否应该像在我的应用程序程序集中处理 UserControl 一样处理问题?如果假设在单个 parent 视图中使用了 20 多个 UserControls,这感觉真的适得其反。

我也觉得我没有 100% 正确地实施 DI。

查看这篇关于 WPF 应用程序本地化的文章:

http://www.codeproject.com/Articles/299436/WPF-Localization-for-Dummies

您的本地化可以通过您需要支持的每种语言的资源程序集来处理,并且将在 运行 时根据当前文化或后备文化使用正确的语言。您的视图模型可以引用资源,不应该关心特定的语言环境。

问题

DI 并不像您说的那么简单。有一些 DI 框架可以处理 DI 问题,它们是成熟的软件。

由于 DI 应该 的工作方式

,如果不设计 DI 容器,你就不能真正自己做 DI

DI 解决了一些问题,其中几个主要问题是:

  • IoC - 通过在组件外部移动解决方案和提供依赖项来确保组件不会紧密耦合 classes

  • 生命周期范围 - 确保组件具有明确定义的 lifetime/lifecycle 并且它们在应用程序的关键点被正确实例化和处置

看起来怎么样?

你甚至不应该看到容器! - 你应该只看到组件依赖关系,其余的应该看起来很神奇...

DI容器应该是非常透明的。您的组件和服务应该通过指定依赖项(在它们的构造函数中)来简单地需要它们的依赖项

我现在的问题是什么?

您不希望使用如下代码手动连接子依赖项:

public MainWindowViewModel(ILocalizer localizer)
{
    _localizer = localizer;
    UC1ViewModel  = new UserControl1ViewModel(localizer); // <-- ouch
}

上面有很多问题:

  1. 你让 MainWindowViewModel 负责创建 UC1ViewModel 和管理对象的生命周期(这并不总是一件坏事,因为有时你想管理特定组件中对象的生命周期)

  2. 您正在将 MainWindowViewModel 的实现耦合到 UserControl1ViewModel 的构造函数实现 - 如果您需要 UserControl1ViewModel 中的另一个依赖项,突然您必须更新MainWindowViewModel 注入该依赖项,提示大量重构。这是因为您是自己实例化类型,而不是让容器来实例化它。

容器如何防止上述代码?

对于任何容器,您都应该注册组件

容器将跟踪可能的组件和服务列表,并使用此注册表来解决依赖关系。

它还跟踪依赖生命周期(单例、实例化等)

好的,我已经注册了所有内容,接下来呢?

一旦你注册了所有的依赖项,你就可以从容器中解析你的根组件。这被称为组合根,应该是您的应用程序(通常是主视图或主方法)的 'entry point'。

容器应该负责为一切连接并创建依赖关系,这些依赖来自那个组合根。

示例:

(伪代码)

public class ApplicationBootstrapper
{
    private IContainer _container;

    public ApplicationBootstrapper() {
        _container = new SomeDIContainer();

        _container.Register<SomeComponent>().AsSingleton(); // Singleton instance, same instance for every resolve
        _container.Register<SomeOtherComponent>().AsTransient(); // New instance per resolve
        // ... more registration code for all your components
        // most containers have a convention based registration
        // system e.g. _container.Register().Classes().BasedOn<ViewModelBase> etc

        var appRoot = _container.Resolve<MainWindowViewModel>();
        appRoot.ShowWindow();
    }
}

现在,当您的应用程序运行时,所有依赖项都被注入根目录以及根目录的所有依赖项等等

您的 MainWindowViewModel 可以这样指定对 UC 的依赖:

public MainWindowViewModel(UC1ViewModel vm)
{
}

请注意 MainWindowViewModel 如何不再需要 ILocalizer 实例,它将被解析并注入到 UC1ViewModel 中(除非您当然需要它)。

两点注意事项

  • 您应该传递容器实例。如果您在应用程序启动以外的任何地方引用应用程序代码中的容器,您可能做错了什么

  • 依赖项的延迟解析通常是通过工厂(专门设计用于代表您的组件从容器中解析的类型)实现的。工厂应该被注入到组件中,然后组件可以调用工厂来获取它需要的实例。这也允许您将参数传递给依赖项。

  • 使用 SOLID 原则,依赖于抽象而不是具体的 classes。通过这种方式,如果您决定改变某些东西的工作方式,更换组件会容易得多(您只需更改注册代码以使用实现相同接口的不同具体 class,等等,无需重构应用程序)

其他

这绝不是 DI 的简明视图,有很多需要考虑的地方,但希望它能帮助您入门。正如 Steven 提到的,如果您计划重新分发库,您应该阅读最佳实践。

dos/dont 上的原 post 在这里:

Dependency Inject (DI) "friendly" library

您应该使用哪个 DI 容器?

世界尽在掌握。我是 Castle Windsor 的粉丝 - 它不是最快的(我想不出我写过的应用程序在哪里我需要组件分辨率来快速忍者......),但它肯定是功能齐全的。

更新:几个我没有真正解决的非查询

插件

Castle Windsor 具有内置的插件功能 - 因此您可以将 DLL 放到您的应用程序目录中,通过向容器注册组件来为您的应用程序添加功能。不确定这是否适用于你的 UC class 库(你可以让应用程序依赖它,除非它实际上需要是一个插件)

其他

也有相当多的 MVVM 框架在 view/viewmodel 分辨率上有几种不同的方法(视图模型优先、视图优先、混合方法)。

如果您还没有使用其中之一(听起来不像您在使用),您可能需要考虑使用其中之一来帮助指导您构建应用程序。