Prism Commands
1.命令(Commanding)
1.1. ViewModel的作用:
- ViewModel不仅提供在视图中显示或编辑的数据,还可能定义一个或多个用户可以执行的动作或操作。
- 这些用户可以通过用户界面(UI)执行的动作或操作通常被定义为命令(Commands)。
1.2. 命令(Commands)的作用:
- 命令提供了一种方便的方式来表示可以轻松绑定到UI控件的动作或操作。
- 它们封装了实现动作或操作的实际代码,并有助于将其与视图中的实际视觉表示分离。
1.3. 命令的触发方式:
- 用户与视图交互时,可以通过多种方式视觉上表示和调用命令。
- 大多数情况下,命令是由鼠标点击触发的,但它们也可以由快捷键按下、触摸手势或其他输入事件触发。
- 视图中的控件与ViewModel的命令数据绑定,因此用户可以使用控件定义的任何输入事件或手势来调用它们。
- 视图中的UI控件与命令之间的交互可以是双向的,即命令可以在用户与UI交互时被调用,UI也可以根据底层命令的启用或禁用状态自动启用或禁用。
1.4. 命令对象的实现:
- ViewModel可以将命令实现为命令对象(一个实现了
ICommand
接口的对象)。 - 视图与命令的交互可以声明性地定义,而不需要在视图的代码后台文件中编写复杂的事件处理代码。
- 例如,某些控件天生支持命令,并提供了一个可以与ViewModel提供的
ICommand
对象数据绑定的Command属性。 - 在其他情况下,可以使用命令行为将控件与ViewModel提供的命令方法或命令对象关联起来。
1.5. ICommand接口的实现:
- 实现
ICommand
接口是直接的。 - Prism框架提供了
DelegateCommand
这个ICommand
接口的实现,你可以在你的应用程序中直接使用它。
DelegateCommand
可以在Prism.Core Nuget包的Prism.Commands
命名空间中找到。
2.创建一个委托命令对象
如何在MVVM模式中使用
DelegateCommand
来实现命令,以及如何将这些命令绑定到UI控件上。
2.1. DelegateCommand
类封装了两个委托,这两个委托分别对应于ViewModel中的Execute
和CanExecute
方法。这意味着,当你在UI中触发一个命令时,DelegateCommand
会调用ViewModel中相应的方法。
2.2. 你可以通过DelegateCommand
类的构造函数来指定这些委托。例如,下面的代码示例展示了如何通过指定委托到ViewModel中的Submit
和CanSubmit
方法来构造一个代表提交命令的DelegateCommand
实例。然后,这个命令通过一个只读属性暴露给视图,该属性返回对DelegateCommand
的引用。
public class ArticleViewModel
{
public DelegateCommand SubmitCommand { get; private set; }
public ArticleViewModel()
{
SubmitCommand = new DelegateCommand<object>(Submit, CanSubmit);
}
void Submit(object parameter)
{
// 实现逻辑
}
bool CanSubmit(object parameter)
{
return true;
}
}
2.3. 当DelegateCommand
对象的Execute
方法被调用时,它会通过你在构造函数中指定的委托将调用转发到ViewModel类中的方法。同样,当CanExecute
方法被调用时,会调用ViewModel类中的对应方法。CanExecute
方法的委托在构造函数中是可选的。如果没有指定,DelegateCommand
将始终返回true
作为CanExecute
的结果。
2.4. DelegateCommand
类是一个泛型类型。类型参数指定了传递给Execute
和CanExecute
方法的命令参数的类型。在上面的例子中,命令参数是object
类型。Prism还提供了一个非泛型的DelegateCommand
类,用于不需要命令参数的情况,定义如下:
public class ArticleViewModel
{
public DelegateCommand SubmitCommand { get; private set; }
public ArticleViewModel()
{
SubmitCommand = new DelegateCommand(Submit, CanSubmit);
}
void Submit()
{
// 实现逻辑
}
bool CanSubmit()
{
return true;
}
}
DelegateCommand
避免使用值类型作为参数,如果需要使用值类型参数时,应该如何处理以避免潜在的问题:
- DelegateCommand避免使用值类型:
DelegateCommand
这个类在设计时故意不使用值类型(如int
、double
、bool
等)作为参数类型。这是因为ICommand
接口接受的是一个对象类型的参数。- 值类型参数可能导致意外行为:如果在XAML初始化时,命令绑定调用了
CanExecute(null)
,并且T
是一个值类型,那么可能会导致一些意料之外的行为。这是因为值类型在默认情况下会被初始化为它们的默认值(如int
的默认值是0),这可能会导致命令的执行逻辑出现问题。- 默认值问题:考虑过使用
default(T)
作为解决方案,但最终被拒绝了。原因是实现者将无法区分一个有效的值和一个默认值。例如,如果T
是int
,那么default(int)
是0,但实现者可能无法确定0是一个有效的参数值还是一个默认值。- 使用可空类型:如果你确实需要使用值类型作为参数,那么必须将其转换为可空类型(nullable)。这可以通过使用
DelegateCommand<Nullable<int>>
或者使用简写形式?
语法(DelegateCommand<int?>
)来实现。这样,就可以区分一个有效的值和一个未赋值(null)的情况。
3.在视图中调用 ViewModel 提供的 DelegateCommands
3.1. 视图与命令对象的关联:在视图中,可以通过 Command 属性将控件与 ViewModel 提供的命令对象关联起来。这适用于 WPF、Xamarin.Forms 和 UWP 控件,它们可以通过数据绑定轻松地与命令对象关联。
3.2. 数据绑定示例:提供了一个按钮的示例代码,展示了如何将按钮的 Command 属性绑定到 ViewModel 中的 SubmitCommand 命令上。代码如下:
<Button Command="{Binding SubmitCommand}" CommandParameter="OrderId"/>
这里,{Binding SubmitCommand}
表示将按钮的 Command 属性绑定到 ViewModel 的 SubmitCommand 属性上,CommandParameter="OrderId"
表示传递一个参数 “OrderId” 给命令的 Execute 方法。
3.3. 命令参数:可以通过 CommandParameter 属性可选地定义一个命令参数。预期的参数类型在 DelegateCommand 的泛型声明中指定。当用户与控件交互时,控件会自动调用目标命令,如果提供了命令参数,它将作为参数传递给命令的 Execute 方法。
3.4. 自动调用命令:在上述示例中,当按钮被点击时,它会自动调用 ViewModel 中的 SubmitCommand。此外,如果指定了 CanExecute 委托,按钮会在 CanExecute 返回 false 时自动禁用,在返回 true 时启用。
4.向UI发送变化通知
4.1. RaiseCanExecuteChanged方法:
当你需要手动更新绑定到UI元素的状态时,可以使用这个方法。例如,当IsEnabled
属性的值发生变化时,在属性的setter中调用RaiseCanExecuteChanged
来通知UI状态变化。
private bool _isEnabled;
public bool IsEnabled
{
get { return _isEnabled; }
set
{
SetProperty(ref _isEnabled, value);
SubmitCommand.RaiseCanExecuteChanged();
}
}
4.2. ObservesProperty方法:
当命令需要在属性值变化时发送通知时,可以使用这个方法。使用ObservesProperty
方法时,每当提供的属性值变化,DelegateCommand
会自动调用RaiseCanExecuteChanged
来通知UI状态变化。
public class ArticleViewModel : BindableBase
{
private bool _isEnabled;
public bool IsEnabled
{
get { return _isEnabled; }
set { SetProperty(ref _isEnabled, value); }
}
public DelegateCommand SubmitCommand { get; private set; }
public ArticleViewModel()
{
SubmitCommand = new DelegateCommand(Submit, CanSubmit).ObservesProperty(() => IsEnabled);
}
void Submit()
{
// 实现逻辑
}
bool CanSubmit()
{
return IsEnabled;
}
}
注意:使用
ObservesProperty
方法时,可以链式注册多个属性进行观察。例如:ObservesProperty(() => IsEnabled).ObservesProperty(() => CanSave)
。
4.3. ObservesCanExecute方法:
如果你的CanExecute
是简单布尔属性的结果,你可以消除声明CanExecute
委托的需要,改用ObservesCanExecute
方法。ObservesCanExecute
不仅会在注册的属性值变化时向UI发送通知,还会使用同一个属性作为实际的CanExecute
委托。
public class ArticleViewModel : BindableBase
{
private bool _isEnabled;
public bool IsEnabled
{
get { return _isEnabled; }
set { SetProperty(ref _isEnabled, value); }
}
public DelegateCommand SubmitCommand { get; private set; }
public ArticleViewModel()
{
SubmitCommand = new DelegateCommand(Submit).ObservesCanExecute(() => IsEnabled);
}
void Submit()
{
// implement logic
}
}
警告:不要尝试链式注册
ObservesCanExecute
方法。只能为CanExecute
委托观察一个属性。
5.在命令中使用异步方法
在现代编程中,使用async
和await
调用异步方法是很常见的需求。虽然许多人的第一反应是需要一个AsyncCommand
,但实际上这种假设是错误的。ICommand
本质上是同步的,Execute
和CanExecute
委托应该被视为事件。这意味着使用async void
作为命令的语法是完全有效的。文中提供了两种使用DelegateCommand
与异步方法的方法。
选项 1:
public class ArticleViewModel
{
public DelegateCommand SubmitCommand { get; private set; }
public ArticleViewModel()
{
SubmitCommand = new DelegateCommand(Submit);
}
async void Submit()
{
await SomeAsyncMethod();
}
}
在这个选项中,Submit
方法被声明为async void
,这意味着它可以包含await
语句来调用异步方法。DelegateCommand
在执行时会调用这个Submit
方法。
选项 2:
public class ArticleViewModel
{
public DelegateCommand SubmitCommand { get; private set; }
public ArticleViewModel()
{
SubmitCommand = new DelegateCommand(async () => await Submit());
}
Task Submit()
{
return SomeAsyncMethod();
}
}
在这个选项中,Submit
方法被声明为返回Task
的普通方法,而DelegateCommand
的构造函数中使用了lambda表达式async () => await Submit()
来包装这个方法调用。这样,当DelegateCommand
被执行时,它会调用这个lambda表达式,该表达式会异步地调用Submit
方法。
这两种方法都允许你在DelegateCommand
中使用异步方法,同时保持ICommand
的同步特性。选择哪种方法取决于你的具体需求和偏好。