静态类到依赖注入的转变

7

我试图使应用程序使用MVVM。这篇文章的最大部分是关于我尝试过什么以及我已经实现了什么的解释。问题在文章底部。使用的Localizer类只是此处的一个示例,可以轻松替换为另一个类。

我有一个类库,其中包含一个名为Localizer的类。该类的目的是可以在应用程序运行时更改应用程序的语言,而无需重新启动应用程序。`Localizer必须实例化才能使用,但一旦实例化,就可以在整个应用程序中使用。(该类使用应用程序资源本地化应用程序。)

我想到的第一种方法是将Localizer作为public static class,并具有public static void Initialize方法。这样,我可以像这样初始化Localizer

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

在应用程序级别使用它,并将其用于我的类库或应用程序中,就像这样

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

考虑到类库是由我编写(只有我拥有源代码),而其他人使用这个类库却不了解源代码(他们只知道该类库能做什么),因此我需要在某种“如何使用这个类库”的说明中明确声明,在应用程序级别上调用Localizer.Initialize以便在他们的整个应用程序中都可以使用它。
经过一些研究,很多人表示这是一种不好的做法,并建议研究依赖注入(DI)和控制反转(IoC),所以我也这样做了。我了解到DI基本上与我的第一种方法相同,但是去除了静态内容,将Localizer.Initialize用作构造函数并将实例化的类注入到我的其他类中。
所以第二种方法是依赖注入,这就是我卡住的地方。我设法让我的应用程序仅使用一个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 */));
}

上面的代码仍然可以编译并正常运行,没有出现异常。当我在我的类库中有一个需要localizer实例的视图和视图模型时,问题就会发生。
我认为当我在我的应用程序集中有一个UserControl时,我有一个解决方案,但感觉比使用静态类更加复杂。通常,我将一个UserControl的视图模型绑定到它的代码后面的视图上。这样,我就可以像这样简单地将UserControl添加到我的.xaml代码中,例如,而不需要额外的麻烦。这样,视图模型的父视图模型就不必考虑子视图模型。
使用DI,我会在我的父级中这样做(子级与上一个代码块中的相同):
视图
<n:UserControl1 DataContext="{Binding UC1ViewModel}" />

视图模型

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

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

目前还没有出现任何问题,一切都运行良好。唯一变化的是DataContext在父视图中设置,而DataContext的内容在父视图的视图模型中设置。

问题 我在我的class library中还有几个UserControls。这些可以被class library的用户使用,但不能更改它们。其中大部分UserControls是一些固定的pages,用于显示关于人、汽车等信息的信息。意图是例如,人名标签在英语中为“Name”,在荷兰语中为“Naam”等(所有标签都在视图中声明并且正常工作),但是代码背后也有文本需要本地化,这就是我卡住的地方。

我应该像在我的应用程序程序集中处理UserControl一样处理此问题吗?如果在单个父视图中使用20多个这样的UserControls,这会感觉非常低效。

我还觉得我没有完全正确地实现DI。


2
你考虑过使用SimpleInjector吗?https://simpleinjector.codeplex.com/ 它是一个NuGet包,非常容易使用,并且能够完全满足你的需求。 - Arie
你没有正确使用 DI!我马上回答! - Charleh
1
由于您的库是一个“可重用库”,因此您应该确切地了解如何构建DI友好型库。尽管Simple Injector(可能任何DI容器)很容易使用,但不要试图在类库中使用它。这会使重复使用变得更加困难,并且从未必要。 - Steven
"我通常只是将UserControl的视图模型绑定到其代码后面的视图中。" 摊手。 - user1228
我实际上更喜欢你自己的解决方案而不是依赖注入。如果你有很多类,依赖注入只会增加代码量。拥有一个静态类使得访问变得非常简单,只要它有一个简单的工作。例如像Logger.LogError这样的东西就是你原始示例的完美用例。 - rollsch
2个回答

7

问题

依赖注入(DI)并不像你所想象的那么简单。有些 DI 框架可以处理 DI 的问题,它们是成熟的软件。

如果要正确使用 DI,你不能自己实现 DI,除非设计一个 DI 容器。

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

  • 控制反转(IoC)- 通过将依赖项的解析和提供移到组件类之外来确保组件没有紧密耦合。

  • 生命周期范围 - 确保组件具有良好定义的生命周期,并在应用程序的关键点正确地实例化和处置它们。

看起来是什么样子?

你甚至不应该看到容器! - 你只应该看到组件的依赖关系,其他部分都应该像魔术一样……

DI 容器应该非常透明。你的组件和服务应该通过指定其构造函数中依赖项的方式来获取它们的依赖项。

我的当前问题是什么?

你不想像这样手动编写子依赖项的代码进行连接:

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

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

  2. 你将 MainWindowViewModel 的实现与 UserControl1ViewModel 的构造函数实现耦合在一起 - 如果你需要在 UserControl1ViewModel 中添加另一个依赖项,则突然间你必须更新 MainWindowViewModel 以注入该依赖项,这会导致大量重构。这是因为你自己实例化了类型,而不是让容器来实现。

容器如何防止上述代码出现? 任何容器都应该注册组件。
容器将跟踪可能的组件和服务列表,并使用此注册表来解决依赖关系。
它还跟踪依赖项的生命周期(单例、实例等)。 好的,我已经注册了所有内容,接下来呢? 一旦您注册了所有依赖项,就可以从容器中解析出根组件。这被称为组合根,应该是您的应用程序的“入口点”(通常是主视图或主方法)。
容器应该负责连接和创建从该组合根衍生的所有依赖项。 示例: (伪代码)
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原则,依赖于抽象而不是具体类。这样,如果你决定更改某个东西的工作方式,就可以更轻松地交换组件(只需更改注册代码以使用实现相同接口的不同具体类,即可完成操作),而不需要重构应用程序。 - 其他信息。
这绝不是 DI 的简要概述,需要考虑很多因素,但希望能为你入门提供帮助。正如 Steven 所提到的,如果您计划重新分发库,则应阅读最佳实践。

关于“可做/不可做”的原始帖子在此处:Dependency Inject (DI) "friendly" library

应该使用哪个 DI 容器?

世界是你的,我喜欢 Castle Windsor - 它不是最快的(我想不出我编写的任何应用程序需要组件解析非常快...),但它肯定具有完整的功能。

更新:我没有真正回答的一些问题

插件

Castle Windsor 内置插件功能 - 因此,您可以将 DLL 放入应用程序目录中,通过向容器注册组件来添加功能,从而使应用程序添加功能。不确定是否适用于您的 UC 类库(除非它需要实际成为插件,否则您可以让应用程序依赖于它)

其他东西

还有很多MVVM框架,采用了不同的视图/视图模型解决方案(视图模型优先、视图优先、混合方法等)。

如果您尚未使用其中之一,您可能希望考虑使用其中之一来帮助指导您构建应用程序结构(听起来似乎您还没有使用它们)。


这是一个很好的依赖注入应该如何工作的例子。我认为你可以把它变成一篇博客/教程。惊讶它没有更多的投票。 - rollsch

-1

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

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

您可以通过资源程序集来处理本地化,每种语言都需要支持相应的资源程序集,并且根据当前文化或回退文化在运行时使用正确的资源程序集。您的视图模型可以引用资源,并且不必关心特定的区域设置。


使用的 Localizer 类仅在此处用作示例,可以轻松替换为另一个类。我也已经更新了我的答案。抱歉让您感到困惑。 - Krowi

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