65.9K
CodeProject 正在变化。 阅读更多。
Home

MVVM 动态命令

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.56/5 (5投票s)

2009年4月24日

CPOL

6分钟阅读

viewsIcon

63349

downloadIcon

1433

在模型-视图-视图模型 (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 文件夹中。

为了简单起见,演示假设超链接可以调用两个命令之一:CompleteTransactionReviewAccount。这些命令的 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 使用了一个名为 StringConverterIValueConverter。此转换器只是在警报文本后添加一个空格,以将其与超链接分开。演示应用程序使用的值转换器位于 ViewModel/ValueConverters 文件夹中。

演练

应用程序在 App.xaml.cs 中初始化。对 OnStartup() 处理程序的重写创建了视图模型并向其中添加了三个警报。前两个警报调用不同的 ICommands;第三个警报不调用命令。

此时,我们实例化了一个名为 CommandConverterIValueConverter

/* 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 的名称并将其传递给 CommandConverterCommandConverter 创建指定的 ICommand 对象并将其传递回超链接的 Command 属性。当超链接被点击时,命令被调用。

征求意见

这个解决方案有效,但坦率地说,它感觉有点像一个变通方案。我并不完全适应将 IValueConverter 推到这个解决方案所达到的程度。但是有没有一个更简单、更优雅的解决方案呢?

以下是我认为本文中描述的解决方案的一些优点:

  • ICommands 的创建集中在一处。
  • 该解决方案与 WPF 加载图像的方式非常相似。

以下是我认为它的一些缺点:

  • 该解决方案可能以 WPF 设计者不打算的方式使用 IValueConverters
  • 您必须知道在值转换器中查找 ICommand 实例化。

最后一点最让我担心。在传统的 MVVM 中,我可以在命令属性中找到所有命令。这个解决方案要求我到处寻找它们,而值转换器并不是我期望找到 ICommand 实例化的第一个地方。
那么,文章中的解决方案是实现动态命令的最佳方法,还是可以通过一两行非常巧妙的 XAML 来实现?我期待您的评论!

历史

  • 2009 年 4 月 24 日:首次发布
© . All rights reserved.