在 MVVM 模式下使用 ICommand






4.85/5 (4投票s)
如何根据 MVVM 模式创建和使用命令
范围
在本文中,我们将展示 `ICommand` 接口的基本实现,以根据 MVVM 模式管理 UI 无关命令的定义,从而实现该模式本身建立的典型的 `View` 和 `Model` 分离。
引言
正如我们在之前的文章中所述(参见 MVVM 模式的基础概述和示例),MVVM 的基本范例是将应用程序的图形部分(通常是呈现数据的部分)与其业务逻辑(即查找、初始化和公开数据的代码)分开。在上面链接的文章中,我们看到了如何针对对象的属性来实现这一点,引入了一个中间层(所谓的 `ViewModel`),它负责将数据(Model 的属性)提供给 `View`(UI)。在这里,我们将看到关于命令的相同内容,或者如何克服事件驱动的逻辑(比如按钮的逻辑)来实现用户可点击对象与实际执行代码之间的完全分离。
为此,准备一个合适的环境将是明智的,即一个包含要呈现的数据的类,一个用于呈现其数据的中间类,以及一个视图,在本例中也将是 WPF 视图。
一个基本数据类
正如我们在过去的示例中所做的那样,我们将开始编写一个简单的类来表示我们的核心数据。在这种情况下,示例将非常简单,设计一个公开两个属性的元素:一个文本,由名为 `Text` 的属性标识,以及一个背景,也有一个相应的属性。开始设想如何使用这样的类,我们可以预期将其绑定到 `TextBox`,更具体地说,是将它们的 `Text` 属性用于内容,将 `Background` 属性用于背景颜色相互连接。
Public Class ItemData
Dim _text As String
Dim _backg As Brush = Nothing
Public Property Text As String
Get
Return _text
End Get
Set(value As String)
_text = value
End Set
End Property
Public Property Background As Brush
Get
Return _backg
End Get
Set(value As Brush)
_backg = value
End Set
End Property
Public Sub New()
_text = "RANDOM TEXT"
_backg = New SolidColorBrush(Colors.Green)
End Sub
End Class
在其构造函数中,该类仅使用 "RANDOM TEXT" 的值初始化其 `Text` 属性,并使用绿色的 `SolidColorBrush` 作为 `Background`。展望其使用,我们知道——如前一篇文章所述——设置我们 `View` 的 `DataContext` 将是合适的。我们必须创建一个将用作 `ViewModel` 的类,以便——给定一个 `ItemData`——它将公开其基本属性(或者至少是我们真正想在操作区域中使用的那些属性)。
ViewModel,第一个版本
让我们从一个非常小的 `ViewModel` 开始,小到足以初始化 `DataContext`。让我们看一下一个适合此目的的类,它具有创建新 `ItemData` 的功能,并通过属性公开 `ItemData` 的属性。正如我们所见,任何 `ViewModel` 的核心都是实现 `INotifyPropertyChanged` 接口,以跟踪属性修改,从而实现有效的数据绑定。
Imports System.ComponentModel
Public Class ItemDataViewModel
Implements INotifyPropertyChanged
Dim _item As New ItemData
Public Property Text As String
Get
Return _item.Text
End Get
Set(value As String)
_item.Text = value
NotifyPropertyChanged()
End Set
End Property
Public Property Background As Brush
Get
Return _item.Background
End Get
Set(value As Brush)
_item.Background = value
NotifyPropertyChanged("Background")
End Set
End Property
Public Event PropertyChanged As PropertyChangedEventHandler _
Implements INotifyPropertyChanged.PropertyChanged
Private Sub NotifyPropertyChanged(Optional propertyName As String = "")
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
End Class
在该类中,我们看到第一步是通过创建一个 `ItemData` 的新实例来完成的:调用其构造函数,其基本属性将按照我们上面的说明进行初始化。然后,我们将创建用于公开“子属性”的属性,即我们的数据源属性。在这种情况下,我将 `ViewModel` 的属性与它们在基类中的名称保持一致。通过它们,使用 `Get`/`Set`,将进行实际的数据修改。
视图,第一个版本
最后,一个基本的 `View`:如前所述,我们将把我们的 `ViewModel` 设置为我们 Window 的 `DataContext`。我们将在 Window 上放置一个 `TextBox`,并将其绑定到 `ViewModel` 公开的属性。我们可以这样写:
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation
[This link is external to TechNet Wiki. It will open in a new window.] "
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml
[This link is external to TechNet Wiki. It will open in a new window.] "
xmlns:local="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:ItemDataViewModel/>
</Window.DataContext>
<Grid>
<TextBox HorizontalAlignment="Left" Height="25" Margin="30,53,0,0"
Text="{Binding Text}" Background="{Binding Background}"
VerticalAlignment="Top" Width="199"/>
</Grid>
</Window>
请注意本地命名空间的声明,它引用了应用程序本身:没有它,我们就无法将我们的 `ItemDataViewModel` 类引用为 `Window.DataContext`,因为它定义在我们的项目中。接下来,我们设置我们的 `TextBox` 绑定,分别绑定到 `Text` 和 `Background` 属性。源属性显然源自我们的 `ViewModel`。此时,我们拥有了产生第一个结果所需的一切。即使不执行我们的代码,由于绑定效果,我们也会看到 IDE 向我们展示一个按预期修改的 Window。即,一个包含 "RANDOM TEXT" 文本、背景为绿色的 `TextBox`。
现在我们有了一个有用的基础来开始讨论命令。
ICommand 接口
命令的通用范围是将其代码的语义与使用它的对象分开。一方面,我们将有一个命令的图形表示(例如,一个 `Button`),而在我们的 `ViewModel` 中将包含实际要执行的指令集——当开发人员设定的条件满足时——当用户向图形命令发送输入时。在我们的示例中,我们将用一些子类来丰富我们的 `ViewModel`,以公开命令并将它们绑定到 `View`。但没有阻止——如果有人愿意这样做——创建专用类,并在 ViewModel 中引用它们。
`ICommand` 实现包含四个基本元素:一个构造函数、`CanExecute()` 和 `Execute()` 函数,以及 `CanExecuteChanged` 事件的管理。其最基本示例如下所示:
Public Class Command
Implements ICommand
Private _action As Action
Sub New(action As action)
_action = action
End Sub
Public Function CanExecute(parameter As Object) As Boolean Implements ICommand.CanExecute
Return True
End Function
Public Event CanExecuteChanged(sender As Object, e As EventArgs) _
Implements ICommand.CanExecuteChanged
Public Sub Execute(parameter As Object) Implements ICommand.Execute
_action()
End Sub
End Class
我们定义了一个名为 `Command` 的类,它使用其核心元素实现了 `ICommand`。Action 变量代表方法的封装(委托)。换句话说,当我们调用 `Command` 的构造函数时,我们必须告诉它必须调用哪个 `sub` 或 `function`。为此,我们将向它传递一个指向所需 `Delegate` 的引用。该引用将在调用 `Execute()` 方法时执行。`CanExecute()` 方法用于定义何时可以执行我们的命令。在我们的示例中,我们将始终返回 `True`,因为我们只是希望它始终可用。
定义了一个基本实现后,我们可以将其集成到我们的 `ViewModel` 中:在我们的示例中,我们假设我们想引入一个命令,通过该命令我们将把元素的背景切换为 `red`。在我们的 `View` 中,此功能将委托给按钮的点击事件。我们可以按如下方式修改我们的 `ViewModel`:
Imports System.ComponentModel
Public Class ItemDataViewModel
Implements INotifyPropertyChanged
Dim _item As New ItemData
Public Property Text As String
Get
Return _item.Text
End Get
Set(value As String)
_item.Text = value
NotifyPropertyChanged()
End Set
End Property
Public Property Background As Brush
Get
Return _item.Background
End Get
Set(value As Brush)
_item.Background = value
NotifyPropertyChanged("Background")
End Set
End Property
Public Event PropertyChanged As PropertyChangedEventHandler _
Implements INotifyPropertyChanged.PropertyChanged
Private Sub NotifyPropertyChanged(Optional propertyName As String = "")
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
Dim _cmd As New Command(AddressOf MakeMeRed)
Public ReadOnly Property FammiRosso As ICommand
Get
Return _cmd
End Get
End Property
Private Sub MakeMeRed()
Me.Background = New SolidColorBrush(Colors.Red)
End Sub
Public Class Command
Implements ICommand
Private _action As Action
Sub New(action As action)
_action = action
End Sub
Public Function CanExecute(parameter As Object) As Boolean _
Implements ICommand.CanExecute
Return True
End Function
Public Event CanExecuteChanged(sender As Object, e As EventArgs) _
Implements ICommand.CanExecuteChanged
Public Sub Execute(parameter As Object) Implements ICommand.Execute
_action()
End Sub
End Class
End Class
请注意,我如何将 `Command` 类插入为子类(这不是强制性程序,只是对我来说有助于展示一个不太分散的示例)。`ViewModel` 声明了一个变量 `_cmd`,类型为新的 `Command`,并向其传递了一个委托(`AddressOf`)指向前面的 `sub MakeMeRed`。这个 `sub` 做什么?很简单,正如可以看到的,它获取当前对象的 `Background` 属性(相同的底层 `ItemData` 属性通过它公开),并将其设置为红色的 `Brush`。作为 MVVM 规则,命令也必须作为属性公开:因此,我创建了 `FammiRosso` 属性,它将我们的 `Command` 公开给 `ViewModel` 外部。
我们可以继续在 `View` 中引入我们的 `Command`。让我们按如下方式修改我们的 XAML:
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation
[This link is external to TechNet Wiki. It will open in a new window.] "
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml
[This link is external to TechNet Wiki. It will open in a new window.] "
xmlns:local="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:ItemDataViewModel/>
</Window.DataContext>
<Grid>
<TextBox HorizontalAlignment="Left" Height="25" Margin="30,53,0,0"
Text="{Binding Text}" Background="{Binding Background}"
VerticalAlignment="Top" Width="199"/>
<Button Content="Button" Command="{Binding FammiRosso}"
HorizontalAlignment="Left" Height="29" Margin="30,99,0,0"
VerticalAlignment="Top" Width="199"/>
</Grid>
</Window>
在 `Button` 控件中,我们可以通过 `Command` 属性绑定命令。请注意此过程有多快:由于我们 Window 的 `DataContext` 是 `ItemDataViewModel`,我们可以简单地写 `Command="{Binding FammiRosso}"` 来绑定这两个。如果我们现在运行我们的示例,我们将首先看到一个背景为绿色的 `TextBox`,这是由 `ItemData` 构造函数引起的。点击我们的 `Button` 将导致控件背景发生变化。
再次强调 UI 和底层逻辑之间的独立性很重要。通过常规的事件驱动编程逻辑,我们应该在 Window 的代码隐藏中编写代码,而借助 MVVM,定义 UI 的文件可以与工作逻辑分开,避免在仅更改其中一个时修改两个文件的必要性(除非更改发生在两个文件之间的基本接触点上,比如属性名,例如。在这种情况下,进行调整将更加容易和快速)。
参数化命令
在任何情况下,`static` 命令可能不足以满足操作需求。考虑一个命令,它必须根据另一个控件的状态执行不同的操作。这时就需要创建参数化命令,该命令可以接收 UI(或任何其他对象)的数据,并根据这些数据执行各种不同的任务。
在我们的示例中,我们想实现一个额外的按钮。这个新按钮也将更改我们的 `TextBox` 的背景,但它是从用户将在 `TextBox` 中输入的颜色中选择的。`ItemData` 的 `Text` 属性必须作为参数传递给我们的命令。参数化命令的基本实现与我们上面看到的没有太大区别。我们可以将其概括为:
Public Class ParamCommand
Implements ICommand
Private _action As Action(Of String)
Sub New(action As Action(Of String))
_action = action
End Sub
Public Function CanExecute(parameter As Object) As Boolean Implements ICommand.CanExecute
Return True
End Function
Public Event CanExecuteChanged(sender As Object, _
e As EventArgs) Implements ICommand.CanExecuteChanged
Public Sub Execute(parameter As Object) Implements ICommand.Execute
_action(parameter.ToString)
End Sub
End Class
请注意,与之前的实现(除了类名)的唯一区别在于 `Action` 委托的类型规范。现在我们有了 `Action(Of String)`,无论是在变量声明中,还是在构造函数中,以及在 `Execute()` 方法的执行语法中。即,我们声明一个我们想要的任何类型的委托(我选择 `String` 将很快显现),它将引用一个具有相同签名的函数。
与上一个示例一样,我们需要在我们的 `ItemDataViewModel` 中实现我们的 `Command`,并通过属性将其公开。将我们的参数化命令类添加到 `ViewModel` 中,我们将得到:
Imports System.ComponentModel
Public Class ItemDataViewModel
Implements INotifyPropertyChanged
Dim _item As New ItemData
Public Property Text As String
Get
Return _item.Text
End Get
Set(value As String)
_item.Text = value
NotifyPropertyChanged()
End Set
End Property
Public Property Background As Brush
Get
Return _item.Background
End Get
Set(value As Brush)
_item.Background = value
NotifyPropertyChanged("Background")
End Set
End Property
Public Event PropertyChanged As PropertyChangedEventHandler _
Implements INotifyPropertyChanged.PropertyChanged
Private Sub NotifyPropertyChanged(Optional propertyName As String = "")
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
Dim _cmd As New Command(AddressOf MakeMeRed)
Dim _cmdP As New ParamCommand(AddressOf EmitMessage)
Public ReadOnly Property FammiRosso As ICommand
Get
Return _cmd
End Get
End Property
Public ReadOnly Property Message As ICommand
Get
Return _cmdP
End Get
End Property
Private Sub MakeMeRed()
Me.Background = New SolidColorBrush(Colors.Red)
End Sub
Private Sub EmitMessage(message As String)
Try
Me.Background = New SolidColorBrush(CType_
(ColorConverter.ConvertFromString(message), Color))
Catch
MakeMeRed()
End Try
MessageBox.Show(message)
End Sub
Public Class Command
Implements ICommand
Private _action As Action
Sub New(action As action)
_action = action
End Sub
Public Function CanExecute(parameter As Object) _
As Boolean Implements ICommand.CanExecute
Return True
End Function
Public Event CanExecuteChanged(sender As Object, e As EventArgs) _
Implements ICommand.CanExecuteChanged
Public Sub Execute(parameter As Object) Implements ICommand.Execute
_action()
End Sub
End Class
Public Class ParamCommand
Implements ICommand
Private _action As Action(Of String)
Sub New(action As Action(Of String))
_action = action
End Sub
Public Function CanExecute(parameter As Object) As Boolean _
Implements ICommand.CanExecute
Return True
End Function
Public Event CanExecuteChanged(sender As Object, e As EventArgs) _
Implements ICommand.CanExecuteChanged
Public Sub Execute(parameter As Object) Implements ICommand.Execute
_action(parameter.ToString)
End Sub
End Class
End Class
请注意 `ParamCommand` 的声明,`_cmd` 变量的声明,以及对 `EmitMessage` 委托的引用,这是一个接收 `String` 类型参数的子程序。该例程将尝试将参数转换为颜色,将其转换为画笔并将其分配给 `ItemData` 的 `Background` 属性。同时,它将弹出一个 `MessageBox`,其中包含传递的 `String` 参数。我们将通过 `Message` 属性公开我们的 `Command`。现在应该清楚的是,为了实现我们设想的目的,传递给例程的参数必须是公开的 `Text` 属性,使其由用户输入,被参数化命令接收,并在委托例程的上下文中进行使用。
为了实现这样的绑定,我们在 View 中实现了一个新的 `Button`。最终的 XAML 将变成:
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation
[This link is external to TechNet Wiki. It will open in a new window.] "
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml
[This link is external to TechNet Wiki. It will open in a new window.] "
xmlns:local="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:ItemDataViewModel/>
</Window.DataContext>
<Grid>
<TextBox HorizontalAlignment="Left" Height="25" Margin="30,53,0,0"
Text="{Binding Text}" Background="{Binding Background}"
VerticalAlignment="Top" Width="199"/>
<Button Content="Button" Command="{Binding FammiRosso}"
HorizontalAlignment="Left" Height="29" Margin="30,99,0,0"
VerticalAlignment="Top" Width="199"/>
<Button Content="Button" Command="{Binding Message}"
CommandParameter="{Binding Text}" HorizontalAlignment="Left"
Margin="30,133,0,0" VerticalAlignment="Top" Width="199" Height="29"/>
</Grid>
</Window>
请注意,与之前一样,命令公开属性绑定到 Button 的 `Command` 属性。要传递我们的参数,只需在 `Button` 上设置 `CommandParameter` 属性,并引用要链接/绑定的属性即可。在我们的例子中,`CommandParameter="{Binding Text}"` 指定传递给命令的参数必须在绑定的 `Text` 属性中找到。
再次运行我们的代码,我们可以(从 `System.Windows.Media.Colors` 类中,例如 `Red`、`Black`、`Green` 等)在我们的 `TextBox` 中键入不同的字符串,每次点击按钮,都会看到 `TextBox` 的 `Background` 属性如何被修改(以及 `MessageBox` 弹出)。如果用户输入无效输入,正如我们在 `EmitMessage()` 代码中可以看到的,指定的颜色将是红色。
历史
- 2015 年 1 月 10 日:CodeProject 的首次发布