附加行为中的模式






4.90/5 (5投票s)
基于模式在 WPF 中开发附加行为。
摘要
本文是对一个反复出现的附加行为模式的小调查。我希望通过基于多个示例实现的定义 ASR 和 AR 模式,为我们提供一种更好地理解这些内容的方法。模式本身远非一项突破性的科学技术。它只是对一个反复出现的主题进行的回顾。但是,给事物命名并指出它们的共同结构应该能让我们更好地理解它们。
关于模式的讨论可能对您来说很无聊。在这种情况下,可以查看本文附加的附加行为和示例集。也许其中有更令您感兴趣的内容。
引言
我接到了一个任务,需要实现几乎所有应用程序如今都具备的拖放功能。所需的工作流程是这样的:
- 用户从 Windows 资源管理器中拖动文件到应用程序 X 上
- 期望:应用程序 X 打开被拖放的文件并显示其内容
我对使用 WPF 和 MVVM 来实现这一点很感兴趣,因此我猜测这可以通过附加行为来实现。因此,我首先研究了基于事件的解决方案,然后将其转换为附加行为。我仔细研究了附加行为,直到能够确信它支持命令绑定,能够将 `RoutedCommand` 或委托(`RelayCommand`)命令作为放置事件的处理程序。每个步骤的解决方案都基于我在网上找到的另外三个解决方案 [1][2][3](分别由其作者提供)。是 WPF MSDN 论坛上的 Kane Nguyen [3] 帮助我完成了附加放置行为的最终版本。
思考这些发展让我对一个开发模式 Attach-Subscribe-Response (ASR) 产生了思考,这个模式似乎在附加行为中一贯地反复出现。我并非声称这是附加行为的唯一模式,也并非声称我发明了什么。我只是在尝试将模式与一些示例分开。仅仅给它命名并理解其抽象含义应该能让我们尽早决定某个事物是否可以通过附加行为来实现。
我将在下面进一步讨论这种模式以及我从中学习到的其他内容。接下来的小节将让您了解我是如何开发 `DropCommand` 行为的。这与 ASR 模式一起,应该对您在开发自己的附加行为以处理控件 X、事件 Y 和(命令)绑定 Z 时有所帮助。
文件拖放操作的基于事件的方法
一些研究 [1] 表明,将文件拖放到应用程序窗口上是一个相当简单的问题(一旦你在网上找到解决方案 - 哈哈)。在 WPF 中,实际上只需设置 `AllowDrop="True"` 属性,并在希望接受文件放置的 UI 上定义相应的路由事件处理程序 `Drop="DropEventHandler"`。事件处理程序本身也不是太复杂。
private void DropEventHandler(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop))
{
string[] droppedFilePaths =
e.Data.GetData(DataFormats.FileDrop, true) as string[];
foreach (string droppedFilePath in droppedFilePaths)
{
MessageBox.Show(string.Format("Opening file '{0}'",
droppedFilePath), "Debug Info", MessageBoxButton.OK);
}
}
}
如果您想看到这一点,请下载并测试本节开头附加的示例。
带有委托命令的放置附加行为
在本节中,我将使用一个放置附加行为向您展示如何接收放置事件并将其转换为委托命令。如果您对 `RoutedCommands` 感兴趣,请参阅下一节。
本节的示例应用程序在 `App.xaml.cs` 文件的 `Application_Startup` 方法中启动。在这里,我们构造带有 `MainWindow` 的 `ViewModel` 并向用户显示窗口。
`MainWindow` 与前一个版本中的 `MainWindow` 相同 - 除了我们不再在代码隐藏文件中使用事件。相反,我们使用附加行为 `behav:DropFileCommand.DropCommand="{Binding LoadFileCommand}"`,它在接收到附加控件的放置事件时调用 `LoadFileCommand`。
我以前在这里停止分析,因为我不明白放置事件如何能触发命令,或者执行其他操作,例如将项目带入视图。但是请耐心听我说,并记住上一节中的示例。
如果我们同意我们确实想将 [2] 初始事件转换为命令,并使用命令绑定在 `ViewModel` 中执行实际处理,那么就可以导出上一节事件处理的等效附加行为。
放置事件 -> 执行命令 -> 在 ViewModel 中处理
Attach
查看 `DropFileCommand.cs` 文件,我们会发现附加是通过一个 `static ICommand` 字段和相应的 setter 和 getter 方法实现的。
private static readonly DependencyProperty DropCommandProperty = DependencyProperty.RegisterAttached(
"DropCommand",
typeof(ICommand),
typeof(DropFileCommand),
new PropertyMetadata(null, OnDropCommandChange));
public static void SetDropCommand(DependencyObject source, ICommand value)
{
source.SetValue(DropCommandProperty, value);
}
public static ICommand GetDropCommand(DependencyObject source)
{
return (ICommand)source.GetValue(DropCommandProperty);
}
这被称为附加依赖属性 [6],您可以在 Visual Studio 2010 中通过输入 'propa
' 并按 TAB 键来生成它(如果尚未提供下拉菜单)。这就像依赖属性将 `ViewModel` 连接到 `View` 一样,是维系事物的基础。
Subscribe (订阅)
现在,在绑定时,`DropCommandProperty` 被绑定到 `ViewModel`,后者又调用 `OnDropCommandChange` 方法。
private static void OnDropCommandChange(DependencyObject d, DependencyPropertyChangedEventArgs
{
UIElement uiElement = d as UIElement // Remove the handler if it exist to avoid memory leaks
uiElement.Drop -= UIElement_Drop;
var command = e.NewValue as ICommand;
if (command != null)
uiElement.Drop += UIElement_Drop;
}
`OnDropCommandChange` 方法在参数 `DependencyObject d` 中看到了行为所附加的 UI 元素。我们使用此行为将 `UIElement_Drop` 方法注册到我们所附加 UI 元素的 `drop` 事件上。
这段代码...
uiElement.Drop += UIElement_Drop;
...基本上是在告诉计算机每次发生放置事件时调用 `UIElement_Drop` 方法。
响应
查看 `UIElement_Drop` 方法,我们可以看到:
UIElement uiElement = sender as UIElement;
if (uiElement == null)
return;
ICommand dropCommand = DropFileCommand.GetDropCommand(uiElement);
if (dropCommand == null)
return;
if (e.Data.GetDataPresent(DataFormats.FileDrop))
{
string[] droppedFilePaths =
e.Data.GetData(DataFormats.FileDrop, true) as string[];
foreach (string droppedFilePath in droppedFilePaths)
{
if (dropCommand is RoutedCommand)
(dropCommand as RoutedCommand).Execute(droppedFilePath, uiElement);
else
dropCommand.Execute(droppedFilePath);
}
调用者可以从 `sender` 参数中提取。该信息用于检索生成此事件实例的确切命令绑定(毕竟,这些都是 `static` 方法)。内部的 `if` 块与上一节的处理方式相似 - 这里决定性的区别在于,我们调用的是 `RoutedCommand`,或者,如我们目前的情况,是 `delegate`(`RelayCommand`)命令。
命令绑定在 XAML 中定义,并调用 `LoadFile` 方法。
带有委托命令或路由命令的放置附加行为
本示例中的路由命令绑定与上一个示例略有不同。
没有 `RelayCommand` 类,因为 `RoutedCommand` 类和 `RoutedUICommand` 类是 .NET 标准框架的一部分。`App` 类中的 `InitMainWindowCommandBinding` 方法用于将 `static` 的 `AppCommand.LoadFile` `RoutedUICommand` 绑定到 `ViewModel` 中的 `LoadFile` 方法。此绑定将 `RoutedUICommand` 链接到每次调用命令时执行的方法。
附加行为通过 `MainWindow` XAML 中的以下绑定表达式绑定到 `AppCommand.LoadFile` `RoutedUICommand`:
behav:DropFileCommand.DropCommand="cmd:AppCommand.LoadFile"
否则,该行为与上一节的行为完全相同。唯一的区别是我们调用 `UIElement_Drop` 方法的 `if` 语句中的 `(dropCommand as RoutedCommand).Execute(droppedFilePath, uiElement);` 分支(感谢 Kane Nguyen 分享此细节)。
附加行为模式
回顾 `DropFileCommand.cs` 文件(以及我上面给出的描述),我们可以看到附加行为可以分为三个部分(或三个处理阶段)。
Attach
Subscribe (订阅)
响应
我意识到这三个部分是反复出现的主题。此外,现在我清楚了,我们可以通过先实现基于事件的方法(Response
),然后实现 Attach
部分,最后在 Subscribe
部分将它们整合起来,来开发这种附加行为。
下面是描绘附加行为的各个部分的概述。
我在这里的主要挑战是掌握所有这些项目:UI 控件提供的事件、属性和方法,以及 WPF 中的正确响应公式(尽管响应也可以导致属性或方法的操纵,如下一节所示)。
ASR 示例
滚动到列表视图行为
Josh Smith 发布了一个附加行为,用于在通过 `ViewModel` 选择 `TreeView` 项时将其带入视图 [7]。
此附加行为的实现也展示了 Attach-Subscribe-Response (ASR) 模式。但这一次,Response 不是执行命令,而是执行 `TreeViewItem` 的方法。
存在类似的问题,例如,在一个长时间运行的进程结束时滚动 `listview` 到其最后一个条目,当您比较 Josh 的解决方案和本节中的示例解决方案(BringSelectedListViewItemIntoView
)时,您会发现 Josh 的解决方案被用作模板(几乎字面意思上是将 `TreeView` 替换为 `ListView`,将 `TreeViewItem` 替换为 `ListViewItem`)。
此处示例的处理方法在 [8] 中有更详细的解释。
查看 `BringSelectedListViewItemIntoView` 类,可以看到我们再次有三个部分的代码。Attach
部分是 `IsBroughtIntoViewWhenSelected` 附加属性。Subscribe
部分是 `OnIsBroughtIntoViewWhenSelectedChanged` 方法中的事件挂钩。Response
部分是 `OnListViewItemSelected` 方法中的事件处理。
拉伸列表视图列
04_BringListViewItemIntoView.zip 示例还包含一个附加行为,该行为将 `ListView` 的列拉伸以占据可用空间(ListViewColumns.cs)。这同样是此处讨论的 ASR 模式的一个绝佳示例。请注意,附加行为订阅了两个事件:`Loaded` 和 `SizeChanged`。每个事件都由不同的 Response 处理。
这意味着该模式可以扩展,使我们有一个控件,n
个事件订阅,以及至少(如果不是更多)n
个不同的 Response。
非 ASR 示例
对话框关闭器
DialogCloser
[9] 附加行为表明我们不限于 ASR。此附加行为(AB)仅包含一个附加依赖属性和一个在附加依赖属性更改时执行的方法。因此,我将这种模式命名为 AR(Attach - Respond)模式。
令人惊讶的是,此附加行为不订阅任何事件,但仍然可以通过响应 `ViewModel` 发起的附加依赖属性的更改来做有用的事情。此 AB 表明我们不限于在附加属性所在的视图中发生的事件。我们还可以响应 `ViewModel` 中的更改(通过附加依赖属性的 `OnChanged` 方法)。
ASR 模式通常用于将数据或状态从 `View` 传输到 `ViewModel`(尽管也可以从 `ViewModel` 到 `View`),而 `DialogCloser` 模式则提供了一个通过附加行为将状态从 `ViewModel` 传输到 `View` 的示例。
我学到的东西
我在此次开发中学到的是,`ICommand` 依赖项或附加属性的作者应始终确保检查实际绑定的命令类型,并执行相应的 `Execute` 方法,而不是只支持一种 `ICommand` 类。所需的工作量大致如下:
// Check whether this attached behaviour is bound to a RoutedCommand
if (dropCommand is RoutedCommand)
{
// Execute the routed command
(dropCommand as RoutedCommand).Execute(droppedFilePath, uiElement);
}
else
{
// Execute the Command as bound delegate
dropCommand.Execute(droppedFilePath);
}
...并且是值得的。我学到的第二件事是从基于事件的代码开发附加行为的模式。作为初学者,我更倾向于尝试这样做,而不是使用我尚不完全理解的更复杂的库 [5]。别误会我的意思 - 我每天都在使用开源库。但我也在可能的情况下开发自己的解决方案。对于附加行为,这似乎是我的方式,因为我最终得到了简单、非通用、易于维护和应用的解决方案。
阅读有关路由事件和命令等之间的差异 [4] 使我能够更好地论证或反驳在特定实现中使用 `RoutedCommand`。
参考文献
- [1] 通过路由事件实现拖放操作的描述
WPF 中的拖放文件 (kirupa)
http://www.kirupa.com/blend_wpf/drag_drop_files_wpf_pg1.htm - [2] 附加放置行为的第一个版本
MVVM 中的拖放(带有 ScatterView)(Dave Clemmer)
http://stackoverflow.com/questions/1034374/drag-and-drop-in-mvvm-with-scatterview - [3] 改进的、接近最终版本的附加放置行为 (Kane Nguyen)
通过附加行为执行路由命令
http://social.msdn.microsoft.com/Forums/de-DE/wpf/thread/21bed380-c485-44fb-8741-f9245524d0ae - [4] 进一步阅读:理解 WPF 中的路由事件和命令 (Brian Noyes)
http://msdn.microsoft.com/en-us/magazine/cc785480.aspx
关于该主题的讨论串
http://social.msdn.microsoft.com/Forums/de-DE/wpf/thread/7c3eb6b0-765d-4c0e-96e7-8faf8ad93669 - [5] AttachedCommandBehavior V2 又名 ACB
http://marlongrech.wordpress.com/2008/12/13/attachedcommandbehavior-v2-aka-acb/
gong-wpf-dragdrop
http://code.google.com/p/gong-wpf-dragdrop/ - [6] 附加属性概述
http://msdn.microsoft.com/en-us/library/ms749011.aspx - [7] WPF 附加行为简介 (Josh Smith)
https://codeproject.org.cn/Articles/28959/Introduction-to-Attached-Behaviors-in-WPF - [8] 用于观察复杂算法进度的 MultiProgressViewModel
https://codeproject.org.cn/Articles/317199/A-MultiProgressViewModel-to-observe-progress-in-co - [9] 使用 WPF 和 MVVM 关闭窗口和应用程序
https://codeproject.org.cn/Articles/413517/Closing-Windows-and-Applications-with-WPF-and-MVVM
历史
- 2012 年 7 月 17 日 - 初稿
- 2012 年 7 月 21 日 - 添加了概述图并删除了几处拼写错误