我有一个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中使用的各种常用方法。
请建议我如何重构此代码?
答案 0 :(得分:8)
你的最后一句:
实际上它已经开始包含各种ViewModel
中使用的各种常用方法
准确描述您的问题!正如史蒂文已经描述的那样,您通过一个基类构建了几乎完整的应用程序。从而侵犯了你现在正在经历的Open-Closed principle。
诀窍是围绕非常小的SOLID ViewModels设计应用程序,您可以在运行时编写应用程序。通过拆分ViewModel并使用UserControl
作为您的视图,您可以为用户创建大型复杂视图,同时您仍然可以从使用SOLID设计中获得所有好处。那么让我们来看看你实现的一些不同的接口以及你在基类中“处理”的一些函数:
<强>的NavigationService 强>
这听起来像是一个控制应用程序流程的服务。这听起来像你的主视图(模型)。您可以创建一个MainViewModel
作为单个属性,假设CurrentView
。假设您正在使用WPF,通常会将此属性绑定到ContentControl。此控件的内容可以是从单个TextBlock
到完整UserControl
的所有内容。 UserControls仍然可能非常复杂,因为它们可能由多个子用户控件组成,依此类推。使用MVVM框架(例如Caliburn Micro或MVVM Light)可选,但会派上用场。
它也可以是具有某种回调或委托功能的应用程序全局服务,以执行到某个视图(模型)的导航。在任何情况下,你的应用程序的基础结构部分都应该拥有它自己的类,不应该被放在基类中。
<强>的DataService 强>
单个数据服务是我工作超过10年的方式。每当我把头靠在墙上时。有一个时间点,您需要一些特殊的东西,这些东西不包含在您的数据服务中,您可能会通过完整的代码库进行正确的调整。说到开放封闭原则......
比我了解了Command / Handler和Query / Handler模式。您可以阅读有关此here和here的信息。在所有需要数据的地方使用此模式,您只需注入正确的IQueryHandler&lt;,&gt;并在那里使用它。并非每个视图(模型)都需要数据,当然也不需要相同的数据。那么为什么要使用全局DataService呢?这也将改善您对DBContext对象的生命周期管理。
<强> HandleException 强>
为什么您的基类负责处理viewmodel的异常?基类知道这些异常的含义是什么?基类有什么作用?记录异常,向用户显示一条消息(什么样的消息?)并静默继续?让应用程序在3分钟后崩溃并让用户不知道发生了什么?
I.M.O.如果您没想到它们会被抛出,则不应该捕获异常。比在应用程序级别记录异常(例如在Main
中),向用户显示“对不起”消息并关闭应用程序。如果你期望一个例外,那就在那里处理然后处理。
<强>的UserDetails 强>
问自己一个问题,你的40个ViewModel中有多少人确实需要这些信息?如果所有40个人都需要这些信息,那么您的设计还有其他问题。如果没有,只在实际使用它们的ViewModel中注入这些细节(甚至更好IUserContext
)。
如果您将其用于某种身份验证,请考虑使用包装任务的装饰器,他们需要获得执行权限。
<强> IsBusyIndicator 强>
再次说明:你是否在每个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的事实。他们可能做得太多了;责任太多了。
相反,请尝试执行以下操作:
在设计良好的应用程序中,几乎不需要具有依赖性的基类。
如果你无法改变你的设计(但请看看它;你将在没有基类的情况下处于更好的地方),你可以恢复显式属性注入。 Simple Injector不是开箱即用的,而是文档describes如何做到这一点。
基本上,它归结为编写自定义IPropertySelectionBehavior
,将BaseViewModel
的构造函数依赖项移动到公共属性并使用自定义属性标记它们。
但同样,只使用属性注入作为最后的手段。物业注入只会隐藏设计问题;它不会解决它。
答案 2 :(得分:0)
你可以使用ServiceLocator
(反)模式使注射独立, HOWEVER 你不应该这样做,因为它违反了SOLID的原则。 Mark Seemann - Service Locator violates SOLID
您应该坚持在构造函数中添加依赖项,因为这符合SOLID OO设计原则。