MVVM 动态命令






4.56/5 (5投票s)
在模型-视图-视图模型 (MVVM) 架构中实现动态 WPF ICommands。

引言
Model-View-ViewModel (MVVM) 模式在 WPF 社区中非常流行,以至于对许多开发人员来说,这就是 WPF 的开发方式。它具有简单直观的优点,如果您已经了解 Model-View-Controller,那么学习起来也很容易。
在 MVVM 中,命令是直观且优雅的。命令封装在 ICommand
对象中,该对象作为视图模型中的属性公开。绑定到此类命令就像这样简单:
<Button Command=”MyCommand”>Click Me</Button>
如果按钮及其命令绑定可以在设计时在 XAML 中定义,那么这种方法非常有效。但是,如果按钮以及其将绑定到的命令的规范是在运行时动态加载的呢?
本文解释了实现动态命令所涉及的问题,并提供了解决方案。我并不认为该解决方案是最佳实践。事实上,发表本文的原因之一是征求有关解决该问题的其他方法的评论。如果从文章的评论中出现了更好的解决方案,我将更新文章(当然会注明出处)以实现该解决方案。
背景
假设您正在创建一个金融规划师用于管理其客户账户的应用程序。主窗口包含一个 ListBox
,用于显示账户的警报。每个警报都包含一个超链接,允许规划师打开一个窗口,以便她/他可以采取所需的任何操作。例如,如果客户账户余额为负,规划师可能需要打开一个窗口来查看账户,以了解发生了什么。
超链接由应用程序在运行时生成,每个超链接都需要调用应用程序中可用的不同 ICommand
。这意味着我们无法在设计时绑定超链接。
我们将要解决的问题是“我们如何实现超链接功能,因为我们无法在设计时绑定它们的命令?”
演示应用程序
演示应用程序模拟了用法场景中描述并显示在本文顶部的 ListBox
。演示应用程序的主窗口 MainWindow.xaml 位于 View 文件夹中。
为了简单起见,演示假设超链接可以调用两个命令之一:CompleteTransaction
或 ReviewAccount
。这些命令的 ICommand
类位于 ViewModel/Commands 文件夹中。
演示应用程序的领域模型(在 Domain 文件夹中)由单个对象组成——一个 Alert
。一个 alert
对象具有三个 string
属性:
AlertText
:警报文本LinkText
:超链接文本LinkCommand
:超链接应调用的命令名称
警报列表由视图模型作为 ObservableCollection
暴露。
ListBox
使用 DataTemplate
实现,允许我们包含一个超链接
<ListBox Grid.Row="1" Margin="10,0,10,10" ItemsSource="{Binding Alerts}">
<ListBox.Resources>
<vm:PaddedStringConverter x:Key="StringConverter" />
</ListBox.Resources>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=AlertText,
Converter={StaticResource StringConverter}}">
<Hyperlink Command="{Binding Path=LinkCommand,
Converter={StaticResource CommandConverter}}">
<TextBlock Text="{Binding Path=LinkText}"/>
</Hyperlink>
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
实现相当简单。请注意,TextBlock
使用了一个名为 StringConverter
的 IValueConverter
。此转换器只是在警报文本后添加一个空格,以将其与超链接分开。演示应用程序使用的值转换器位于 ViewModel/ValueConverters 文件夹中。
演练
应用程序在 App.xaml.cs 中初始化。对 OnStartup()
处理程序的重写创建了视图模型并向其中添加了三个警报。前两个警报调用不同的 ICommands
;第三个警报不调用命令。
此时,我们实例化了一个名为 CommandConverter
的 IValueConverter
/* We instantiate our CommandConverter here, so that we can
* pass the view model to it. Then we add it to the application
* resource dictionary, where it is available to XAML. */
// Create and initialize main window
Resources.Add("CommandConverter", new CommandConverter(viewModel));
MainWindow mainWindow = new MainWindow();
mainWindow.DataContext = viewModel;
mainWindow.Show();
我们将在应用程序后面使用此转换器来获取我们的动态命令。我们通过代码而不是标记实例化此转换器,因为它需要对应用程序视图模型的引用。我将在后面解释原因。主窗口显示后,WPF 接管并显示 ListBox
。XAML 是不言自明的。
动态命令
这引出了核心问题:我们如何动态地将超链接绑定到其命令?通常,当我们布局窗口时,我们预先知道控件将调用哪个命令。这就是使 MVVM 易于实现的原因——我们只需将控件的 Command
属性绑定到公开所需命令的视图模型属性。
但在这种情况下,我们不知道在设计时超链接将调用哪个命令。我们只知道适当的 ICommand
的名称将包含在 Alert.LinkCommand
属性中。因此,我们需要某种方法从 ICommand
的名称获取实际的 ICommand
本身。
CommandConverter
演示实现了一个 IValueConverter
,称为 CommandConverter
,以解决问题。CommandConverter
接受一个表示 ICommand
名称的 string
,并返回具有该名称的 ICommand
// Create command
switch (commandName)
{
case "CompleteTransaction":
command = new CompleteTransaction(m_ViewModel);
break;
case "ReviewAccount":
command = new ReviewAccout(m_ViewModel);
break;
default:
throw new ArgumentException("Invalid ICommand name passed in.");
}
CommandConverter
的核心是一个简单的 switch
语句,它实例化并返回适当的 ICommand
对象。
您可能会注意到 CommandConverter
将视图模型引用 (m_ViewModel
) 作为成员变量持有,并将其注入到 ICommands
中。ICommands
将该引用作为成员变量持有,但它们不使用它。
视图模型引用旨在促进 ICommands
和视图模型之间的通信。ICommands
在演示应用程序中不调用视图模型,但在我的生产应用程序中它们经常这样做,因此我通常会将视图模型引用注入到所有新的 ICommands
中。
演示中包含该功能是为了展示视图模型引用如何注入到 CommandConverter
中,以及从那里如何注入到 ICommand
对象中。需要视图模型注入也解释了为什么 CommandConverter
是在代码中实例化的,而不是简单地在 MainWindow.xaml 中声明为 XAML 资源。
结果如下
<Hyperlink Command="{Binding Path=LinkCommand,
Converter={StaticResource CommandConverter}}">
WPF 从 LinkCommand
属性中读取 ICommand
的名称并将其传递给 CommandConverter
。CommandConverter
创建指定的 ICommand
对象并将其传递回超链接的 Command
属性。当超链接被点击时,命令被调用。
征求意见
这个解决方案有效,但坦率地说,它感觉有点像一个变通方案。我并不完全适应将 IValueConverter
推到这个解决方案所达到的程度。但是有没有一个更简单、更优雅的解决方案呢?
以下是我认为本文中描述的解决方案的一些优点:
ICommands
的创建集中在一处。- 该解决方案与 WPF 加载图像的方式非常相似。
以下是我认为它的一些缺点:
- 该解决方案可能以 WPF 设计者不打算的方式使用
IValueConverters
。 - 您必须知道在值转换器中查找
ICommand
实例化。
最后一点最让我担心。在传统的 MVVM 中,我可以在命令属性中找到所有命令。这个解决方案要求我到处寻找它们,而值转换器并不是我期望找到 ICommand
实例化的第一个地方。
那么,文章中的解决方案是实现动态命令的最佳方法,还是可以通过一两行非常巧妙的 XAML 来实现?我期待您的评论!
历史
- 2009 年 4 月 24 日:首次发布