静态类到依赖注入

Kro*_*owi 5 c# wpf dependency-injection mvvm

我正在尝试上班的应用程序使用MVVM.这篇文章的最大部分是对我尝试过的内容和我工作的内容的解释.问题接近帖子的底部.使用的Localizer类仅在此处用作示例,并且可以很容易地用另一个类替换.

我有class library一个Localizer班级.此类的目的是即时更改应用程序的语言,而无需重新启动应用程序.`Localizer必须先实例化才能使用,但一旦实例化,就应该可以在整个应用程序中使用.(该类使用应用程序资源来本地化应用程序.)

我的第一个方法我能想到的是制作的Localizer一个public static class带一个public static void Initialize方法.这样我可以初始化Localizer这样的

Localizer.Initialize(/* Needed arguments here */);
Run Code Online (Sandbox Code Playgroud)

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

string example = Localizer.GetString(/* A key in the resource dictionary */);
Run Code Online (Sandbox Code Playgroud)

考虑到类库是由我编写的(只有我有源代码)并且被其他人使用,他们对源代码一无所知(他们只知道类库可以做什么),我必须在某种情况下明确说明他们需要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);
}
Run Code Online (Sandbox Code Playgroud)

什么上面的代码确实是,就是注入localizerMainWindowViewModel.这样就不会在MainWindowView后面的代码中添加额外的代码,并且视图模型绑定到该视图.

MainWindowViewModel构造函数中是这样的(请注意,消息框在其他地方调用,但在此处移动以最小化代码):

ILocalizer _localizer;

public MainWindowViewModel( ILocalizer localizer)
{
    _localizer = localizer;

    MessageBox.Show(_localizer.GetString(/* A key in the resource dictionary */));
}
Run Code Online (Sandbox Code Playgroud)

上面的代码仍然编译并运行正常,没有例外.当我UserControlsclass library视图和视图模型中也需要localizer实例时,会出现问题.

我想我有一个解决方案,当我UserControl在我的应用程序组件中有一个但感觉它更复杂'然后当我使用一个static class.我通常只是UserControl在其后面的代码中将a的视图模型绑定到视图中.这样我就可以简单地添加UserControl到我的.xaml代码中,<local:UserControl1 />而不需要额外的喧嚣.这样,视图模型父视图模型不必关心子视图模型.

使用DI我会在父母身上做这样的事情(孩子将与前一段代码相同):

视图

<n:UserControl1 DataContext="{Binding UC1ViewModel}" />
Run Code Online (Sandbox Code Playgroud)

视图模型

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

public MainWindowViewModel(ILocalizer localizer)
{
    _localizer = localizer;
    UC1ViewModel  = new UserControl1ViewModel(localizer);
}
Run Code Online (Sandbox Code Playgroud)

以上仍然工作正常,到目前为止没有问题.唯一改变的DataContext是在父视图DataContext中设置,并在父视图模型中设置内容.

这个问题 我也有几个UserControls在我的class library.这些可以由用户使用,class library但他们不能改变它们.其中大多数UserControls是固定的pages,显示有关人,汽车等的信息.意图是,例如,具有该人名称的标签是英文的"姓名",荷兰语的"Naam"等(这是所有在视图中声明并且工作正常)但是后面的代码中还有文本必须本地化,这就是我被困住的地方.

我应该像处理UserControl应用程序程序集中的方法一样处理问题吗?如果UserControls在单一父视图中使用20多个这样的话,这会产生相反的效果.

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

Cha*_*leh 5

问题

DI并不像你看起来那么简单.有DI框架可以解决DI问题,它们是成熟的软件.

你自己无法真正做到无DI由于道路设计DI容器DI 应该工作

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

  • IoC - 通过在组件类之外移动解析和提供依赖关系来确保组件不紧密耦合

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

它看起来怎么样?

你甚至不应该看到容器! - 你应该只看到组件依赖,其余应该看起来像魔术......

DI容器应该非常透明.您的组件和服务应该仅通过指定依赖项(在其构造函数中)来要求它们的依赖项

我目前的问题是什么?

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

public MainWindowViewModel(ILocalizer localizer)
{
    _localizer = localizer;
    UC1ViewModel  = new UserControl1ViewModel(localizer); // <-- ouch
}
Run Code Online (Sandbox Code Playgroud)

上面有很多问题:

  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();
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,当您的应用程序运行时,所有依赖项都会注入根目录和根目录的所有依赖项,依此类推

MainWindowViewModel然后,您可以指定对UC的依赖关系:

public MainWindowViewModel(UC1ViewModel vm)
{
}
Run Code Online (Sandbox Code Playgroud)

注意MainWindowViewModel不再需要一个ILocalizer实例,它将被解析并注入到UC1ViewModel你身上(除非你需要它).

几点要注意

  • 你应该通过周围的容器的一个实例.如果您在应用程序启动期间的任何地方引用应用程序代码中的容器,那么您可能做错了什么

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

  • 使用SOLID原则,取决于抽象而非具体类.这样,如果您决定更改某些工作方式,则更换组件会更容易(您只需将注册代码更改为使用实现相同界面的其他具体类,等等,不要重构应用程序)

还要别的吗

这绝不是DI的简洁视图,需要考虑很多,但希望它能让你开始.正如Steven所说,如果您计划重新分发库,您应该阅读最佳实践.

关于dos/dont的原始帖子在这里:

依赖注入(DI)"友好"库

你应该使用哪个DI容器?

世界是你的牡蛎.我是温莎城堡的粉丝 - 它不是最快的(我不会想到我写过的应用程序,我需要组件分辨率才能快速忍者......),但它肯定是功能齐全的.

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

插件

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

其他的东西

还有很多MVVM框架在view/viewmodel解析上有几种不同的方法(viewmodel-first,view-first,hybrid methods).

您可能需要考虑使用其中一个来帮助指导您构建应用程序(如果您尚未使用它)(听起来不像您).