这是使用命令模式的正确方法吗?
Is this the right way to use the Command Pattern?
与另一个问题相关:How to inject an action into a command using Ninject?
根据对上述问题的评论,我认为我只需要创建一些命令 classes 并将它们注入我的视图模型,以便视图的控件只需要绑定给他们。我在概念上同意并理解其中的好处。此外,我希望使用 Ninject、DI 和构造函数注入尽可能干净。
遵循这些重要的规则,这是我到目前为止所得到的。
创建类别命令
public class CreateCategoryCommand : ICommand {
public CreateCategoryCommand(CreateCategoryView view) {
if(view == null) throw new ArgumentNullException("view");
this.view = view;
}
public bool CanExecute(object parameter) { return true; }
public event EventHandler CanExecuteChanged;
public void Execute(object parameter) { view.Show(); }
private readonly CreateCategoryView view;
}
CategoriesManagementViewModel
public class CategoriesManagementViewModel {
public CategoriesManagementViewModel(ICommand createCommand) {
if (createCommand == null) throw new ArgumentNullException("createCommand");
this.createCommand = createCommand;
}
public ICommand CreateCommand { get { return createCommand; } }
private readonly ICommand createCommand;
}
所以现在当 CategoriesManagementView 被初始化时,它被构造函数注入 CategoriesManagementViewModel,而后者又被构造函数注入CreateCategoryCommand,它又通过 CreateCategoryView 构造函数注入,因此没有冗余依赖,也没有任何循环依赖。
现在,当我CategoriesManagementView.CreateButton时,它将触发绑定CategoriesManagementViewModel.CreateCommand,这将显示CreateCategoryView 给用户,这个视图应该有自己的正确命令,并以相同的方式注入。
最后,这会使 RelayCommand class 变得无用...
是吗?
首先,我同意 RelayCommand
和 DelegateCommand
等是实现违反 SOLID 原则的命令的方法,因此您在这里用单独的 class 替换它们的解决方案是正确的。这样做还可以让您的 ViewModel 更干净。
就是说,您在 ViewModels 层(CreateCategoryCommand
)中有一个 class 并且了解视图层中的具体内容(CreateCategoryView
). ViewModels 层中的任何内容都不应直接引用 Views 层中的任何内容。
这样想象 - 您已经将层分离到不同的 dll 中 - Views.dll、ViewModels.dll、Models.dll、DataLayer.dll。如果您的 ViewModels 中的某些内容引用了您的 Views 中的具体内容,并且显然您的 Views 将引用 ViewModels(必要时),那么您就会遇到循环引用问题。
解决方案是让您的 View 对象实现一个接口(接口隔离原则),如 IDialog
或 IUiDisplay
(根据您想要的抽象程度选择名称),并让您的命令依赖于该接口,而不是直接的具体类型,如下所示:
浏览量:
public class CreateCategoryView : ..., IUiDisplay
{
...
}
在视图模型中:
public interface IUiDisplay
{
void Show();
}
public class CreateCategoryCommand : ICommand
{
public CreateCategoryCommand(IUiDisplay uiDisplay) {
if(display == null) throw new ArgumentNullException("uiDisplay");
this.display = uiDisplay;
}
private readonly IUiDisplay display;
...
}
现在,您的命令不再直接依赖于更高层的具体(因此它现在是可模拟和可测试的!)。现在,您可以让 DI/IOC 将命令依赖项解析为要注入的特定视图 class。 (我个人会在命令中注入一个视图工厂,并且只懒惰地创建视图,但这是一个不同的讨论)。
一个相关的注释 - 如果您通过直接让它们实现 ICommand
来实现命令,那么您将重复自己很多次(DRY)。我的建议是创建一个实现 ICommand
要求的抽象基础 class(CommandBase
或其他)。您会发现从它派生的所有命令只会覆盖 Execute()
,有时会覆盖 CanExecute()
。这使您不必在每个命令中实现事件(以及引发事件的代码),并且在许多情况下使您不必实现 CanExecute
因为大多数命令只是 return true
.
与另一个问题相关:How to inject an action into a command using Ninject?
根据对上述问题的评论,我认为我只需要创建一些命令 classes 并将它们注入我的视图模型,以便视图的控件只需要绑定给他们。我在概念上同意并理解其中的好处。此外,我希望使用 Ninject、DI 和构造函数注入尽可能干净。
遵循这些重要的规则,这是我到目前为止所得到的。
创建类别命令
public class CreateCategoryCommand : ICommand {
public CreateCategoryCommand(CreateCategoryView view) {
if(view == null) throw new ArgumentNullException("view");
this.view = view;
}
public bool CanExecute(object parameter) { return true; }
public event EventHandler CanExecuteChanged;
public void Execute(object parameter) { view.Show(); }
private readonly CreateCategoryView view;
}
CategoriesManagementViewModel
public class CategoriesManagementViewModel {
public CategoriesManagementViewModel(ICommand createCommand) {
if (createCommand == null) throw new ArgumentNullException("createCommand");
this.createCommand = createCommand;
}
public ICommand CreateCommand { get { return createCommand; } }
private readonly ICommand createCommand;
}
所以现在当 CategoriesManagementView 被初始化时,它被构造函数注入 CategoriesManagementViewModel,而后者又被构造函数注入CreateCategoryCommand,它又通过 CreateCategoryView 构造函数注入,因此没有冗余依赖,也没有任何循环依赖。
现在,当我CategoriesManagementView.CreateButton时,它将触发绑定CategoriesManagementViewModel.CreateCommand,这将显示CreateCategoryView 给用户,这个视图应该有自己的正确命令,并以相同的方式注入。
最后,这会使 RelayCommand class 变得无用...
是吗?
首先,我同意 RelayCommand
和 DelegateCommand
等是实现违反 SOLID 原则的命令的方法,因此您在这里用单独的 class 替换它们的解决方案是正确的。这样做还可以让您的 ViewModel 更干净。
就是说,您在 ViewModels 层(CreateCategoryCommand
)中有一个 class 并且了解视图层中的具体内容(CreateCategoryView
). ViewModels 层中的任何内容都不应直接引用 Views 层中的任何内容。
这样想象 - 您已经将层分离到不同的 dll 中 - Views.dll、ViewModels.dll、Models.dll、DataLayer.dll。如果您的 ViewModels 中的某些内容引用了您的 Views 中的具体内容,并且显然您的 Views 将引用 ViewModels(必要时),那么您就会遇到循环引用问题。
解决方案是让您的 View 对象实现一个接口(接口隔离原则),如 IDialog
或 IUiDisplay
(根据您想要的抽象程度选择名称),并让您的命令依赖于该接口,而不是直接的具体类型,如下所示:
浏览量:
public class CreateCategoryView : ..., IUiDisplay
{
...
}
在视图模型中:
public interface IUiDisplay
{
void Show();
}
public class CreateCategoryCommand : ICommand
{
public CreateCategoryCommand(IUiDisplay uiDisplay) {
if(display == null) throw new ArgumentNullException("uiDisplay");
this.display = uiDisplay;
}
private readonly IUiDisplay display;
...
}
现在,您的命令不再直接依赖于更高层的具体(因此它现在是可模拟和可测试的!)。现在,您可以让 DI/IOC 将命令依赖项解析为要注入的特定视图 class。 (我个人会在命令中注入一个视图工厂,并且只懒惰地创建视图,但这是一个不同的讨论)。
一个相关的注释 - 如果您通过直接让它们实现 ICommand
来实现命令,那么您将重复自己很多次(DRY)。我的建议是创建一个实现 ICommand
要求的抽象基础 class(CommandBase
或其他)。您会发现从它派生的所有命令只会覆盖 Execute()
,有时会覆盖 CanExecute()
。这使您不必在每个命令中实现事件(以及引发事件的代码),并且在许多情况下使您不必实现 CanExecute
因为大多数命令只是 return true
.