Simple Injector在BaseClass中注入多个依赖项

时间:2015-02-03 09:48:21

标签: c# mvvm dependency-injection inversion-of-control simple-injector

我有一个BaseViewModel,它由多个ViewModel类继承。在我的BaseViewModel中,我有几个从ViewModel注入的依赖项。现在,如果我需要在BaseViewModel中添加新的依赖项,我需要更改继承BaseViewModel的所有VM。请告诉我如何在Simple Injector中处理它。以下是我的代码结构:

如何让我的基类注入独立,以便我不需要在所有继承的类中进行更改?

代码:

public class BaseViewModel
{
    protected readonly IAESEnDecrypt AESEnDecrypt;
    protected readonly IDataService DataService;
    protected readonly INavigationService NavigateToPage;
    public BaseViewModel(INavigationService nav, IDataService data, IAESEnDecrypt encrypt)
    {
        AESEnDecrypt= encrypt;
        NavigateToPage = nav;
        DataService = data;
    }
}


public class ViewModel
{
   public ViewModel(INavigationService nav, IDataService data, IAESEnDecrypt encrypt) : base (nav, data, encrypt)
   {

   }
}

My BaseViewModel包含以下一些接口,其实现通过构造函数注入:

- NavigationService
- DataService
- GeoLocationService
- SmartDispatcher
- MessageBus which implement Message Aggregator

它还包含一些常见属性作为静态变量,其数据在整个应用程序中使用,如UserDetails。并且还包含CancellationToken,IsBusy以显示进度条。

BaseViewModel还包含HandleException方法,该方法处理来自所有ViewModel的所有传入异常。 还包含一些在Si等所有视图中使用的常用命令 gnoutCommand,NavigationBar命令。

实际上它已经开始包含各种ViewModel中使用的各种常用方法。

请建议我如何重构此代码?

3 个答案:

答案 0 :(得分:8)

你的最后一句:

  

实际上它已经开始包含各种ViewModel

中使用的各种常用方法

准确描述您的问题!正如史蒂文已经描述的那样,您通过一个基类构建了几乎完整的应用程序。从而侵犯了你现在正在经历的Open-Closed principle

诀窍是围绕非常小的SOLID ViewModels设计应用程序,您可以在运行时编写应用程序。通过拆分ViewModel并使用UserControl作为您的视图,您可以为用户创建大型复杂视图,同时您仍然可以从使用SOLID设计中获得所有好处。那么让我们来看看你实现的一些不同的接口以及你在基类中“处理”的一些函数:

<强>的NavigationService

这听起来像是一个控制应用程序流程的服务。这听起来像你的主视图(模型)。您可以创建一个MainViewModel作为单个属性,假设CurrentView。假设您正在使用WPF,通常会将此属性绑定到ContentControl。此控件的内容可以是从单个TextBlock到完整UserControl的所有内容。 UserControls仍然可能非常复杂,因为它们可能由多个子用户控件组成,依此类推。使用MVVM框架(例如Caliburn MicroMVVM Light)可选,但会派上用场。

它也可以是具有某种回调或委托功能的应用程序全局服务,以执行到某个视图(模型)的导航。在任何情况下,你的应用程序的基础结构部分都应该拥有它自己的类,不应该被放在基类中。

<强>的DataService

单个数据服务是我工作超过10年的方式。每当我把头靠在墙上时。有一个时间点,您需要一些特殊的东西,这些东西不包含在您的数据服务中,您可能会通过完整的代码库进行正确的调整。说到开放封闭原则......

比我了解了Command / Handler和Query / Handler模​​式。您可以阅读有关此herehere的信息。在所有需要数据的地方使用此模式,您只需注入正确的IQueryHandler&lt;,&gt;并在那里使用它。并非每个视图(模型)都需要数据,当然也不需要相同的数据。那么为什么要使用全局DataService呢?这也将改善您对DBContext对象的生命周期管理。

<强> HandleException

为什么您的基类负责处理viewmodel的异常?基类知道这些异常的含义是什么?基类有什么作用?记录异常,向用户显示一条消息(什么样的消息?)并静默继续?让应用程序在3分钟后崩溃并让用户不知道发生了什么? I.M.O.如果您没想到它们会被抛出,则不应该捕获异常。比在应用程序级别记录异常(例如在Main中),向用户显示“对不起”消息并关闭应用程序。如果你期望一个例外,那就在那里处理然后处理。

<强>的UserDetails

问自己一个问题,你的40个ViewModel中有多少人确实需要这些信息?如果所有40个人都需要这些信息,那么您的设计还有其他问题。如果没有,只在实际使用它们的ViewModel中注入这些细节(甚至更好IUserContext)。

如果您将其用于某种身份验证,请考虑使用包装任务的装饰器,他们需要获得执行权限。

<强> IsBusyIndi​​cator

再次说明:你是否在每个ViewModel中都需要这个?我想不是。我认为此外,向用户显示忙碌指示符是View的责任,而不是ViewModel,并且随着任务的长度确定您是否需要显示此内容,请将其作为任务的责任(假设您正在查看您也可以通过使用已经提到的Command / Handler模​​式以SOLID方式执行任务。

使用WPF,您可以定义一个可以绑定到视图的Dependency Property,从而显示某种忙碌指示符。现在只需在需要显示它的任务中注入ShowBusyIndicatorService。或者将所有(冗长的)任务包装在ShowBusyIndicatorDecorator中。

<强>设计

现在让我们看一下您可以定义的一些简单接口来构建View(模型)。假设我们决定让每个ViewModel负责一个任务,我们定义以下(典型的LoB)任务:

  • 显示(任何类型)数据
  • 选择或选择数据
  • 编辑数据

可以将单个任务拆分为“显示单个数据类型(实体)的数据”。现在我们可以定义以下接口:

  • IView<TEntity>
  • ISelect<TEntity>
  • IEdit<TEntity>

对于每种接口类型,您将根据您的语义首选项创建处理器/服务或DialogHandler,这将执行典型的MVVM操作,例如查找相应的视图并将其绑定到viewmodel并以某种方式显示(模式窗口,注入它作为一些内容控制中的用户控制等。)。

通过在您需要导航或显示不同视图的“父”ViewModel中注入此单个Processor / Service或DialogHandler,您可以通过一行代码显示任何类型的实体,并将职责转移到下一个ViewModel

我现在在项目中使用这3个接口,我真的可以做我过去可以做的一切,但现在以SOLID方式。我的EditProcessor,接口和viewmodel看起来像这样,从所有不那么有趣的东西中删除。我正在使用Caliburn Micro进行ViewModel-View Binding。

public class EditEntityProcessor : IEditEntityProcessor
{
    private readonly Container container;
    private readonly IWindowManager windowManager;

    public EditEntityProcessor(Container container, IWindowManager windowManager)
    {
        this.container = container;
        this.windowManager = windowManager;
    }

    public void EditEntity<TEntity>(TEntity entity) where TEntity : class
    {
        // Compose type
        var editEntityViewModelType = 
        typeof(IEntityEditorViewModel<>).MakeGenericType(entity.GetType());

        // Ask S.I. for the corresponding ViewModel, 
        // which is responsible for editing this type of entity
        var editEntityViewModel = (IEntityEditorViewModel<TEntity>)
                 this.container.GetInstance(editEntityViewModelType);

        // give the viewmodel the entity to be edited
        editEntityViewModel.EditThisEntity(entity);

        // Let caliburn find the view and show it to the user
        this.windowManager.ShowDialog(editEntityViewModel);
    }
}

public interface IEntityEditorViewModel<TEntity> where TEntity : class
{
    void EditThisEntity(TEntity entity);
}

public class EditUserViewModel : IEntityEditorViewModel<User>
{
    public EditUserViewModel(
        ICommandHandler<SaveUserCommand> saveUserCommandHandler,
        IQueryHandler<GetUserByIdQuery, User> loadUserQueryHandler)

    {
        this.saveUserCommandHandler = saveUserCommandHandler;
        this.loadUserQueryHandler = loadUserQueryHandler;
    }

    public void EditThisEntity(User entity)
    {
        // load a fresh copy from the database
        this.User = this.loadUserQueryHandler.Handle(new GetUserByIdQuery(entity.Id));
    }

    // Bind a button to this method
    public void EndEdit()
    {
        // Save the edited user to the database
        this.saveUserCommandHandler.Handle(new SaveUserCommand(this.User));
    }

    //Bind different controls (TextBoxes or something) to the properties of the user
    public User User { get; set; }
}

IView<User>现在可以使用以下代码编辑当前所选用户:

// Assuming this property is present in IView<User>
public User CurrentSelectedUser { get; set; }

public void EditUser()
{    
    this.editService.EditEntity(this.CurrentSelectedUser);
}

请注意,通过使用此设计,您可以将ViewModel包装在装饰器中,以执行横切关注,例如日志记录,身份验证等。

所以这是一个很长的答案,简短的答案是:松散基类,它咬你,它会咬你越来越难!

答案 1 :(得分:4)

首先防止使用此基类。这个基类是一个很大的代码气味,结果就是你目前的痛苦。这样的基类将违反单一责任原则(SRP),并且将作为所有派生视图模型的大帮助类,或者甚至看起来您正在将横切关注点放在那里。基类甚至可能隐藏您的视图模型违反SRP的事实。他们可能做得太多了;责任太多了。

相反,请尝试执行以下操作:

  • 将横切关注问题从基类转移到装饰器中,或者找到另一种方法来应用横切关注点。
  • 将相关的依赖项组合在一起成为aggregate service,并将此类聚合服务注入到您的视图模型中。

在设计良好的应用程序中,几乎不需要具有依赖性的基类。

如果你无法改变你的设计(但请看看它;你将在没有基类的情况下处于更好的地方),你可以恢复显式属性注入。 Simple Injector不是开箱即用的,而是文档describes如何做到这一点。

基本上,它归结为编写自定义IPropertySelectionBehavior,将BaseViewModel的构造函数依赖项移动到公共属性并使用自定义属性标记它们。

但同样,只使用属性注入作为最后的手段。物业注入只会隐藏设计问题;它不会解决它。

答案 2 :(得分:0)

你可以使用ServiceLocator(反)模式使注射独立, HOWEVER 你不应该这样做,因为它违反了SOLID的原则Mark Seemann - Service Locator violates SOLID

您应该坚持在构造函数中添加依赖项,因为这符合SOLID OO设计原则。