【翻译】WPF 中附加行为的介绍 Introduction to Attached Behaviors in WPF
作者:Josh Smith 2008.08.30
原文地址:https://www.codeproject.com/Articles/28959/Introduction-to-Attached-Behaviors-in-WPF
Explains the concept of attached behaviors and shows how to use them in the context of the MVVM pattern.
解释附加行为的概念并展示如何在 MVVM 模式上下文中使用它们。
[Demo 下载] Download demo project (requires Visual Studio 2008) – 21.3 KB
引言 [Introduction]
This article explains what an attached behavior is, and how you can implement them in a WPF application. Readers of this article should be somewhat familiar with WPF, XAML, attached properties, and the Model-View-ViewModel (MVVM) pattern. I highly recommend that you also read my ‘Simplifying the WPF TreeView by Using the ViewModel Pattern’ article, because the material here is an extension of the material presented in it.
本文解释了什么是附加行为,以及您如何在 WPF 应用程序中实现它们。本文的读者需要稍微熟悉 WPF、XAML、附加属性、以及 MVVM 模式。我强烈建议您也阅读下我的文章《Simplifying the WPF TreeView by Using the ViewModel Pattern(通过使用 MVVM 模式来简化 WPF 的 TreeView)》,因为这里的素材就是对其中提及的素材的一个拓展。
背景 [Background]
Back in May of 2008, I published an article called ‘Simplifying the WPF TreeView by Using the ViewModel Pattern’. That article focused on the MVVM pattern. This morning, I woke up to find that a fellow by the name of Pascal Binggeli had asked an excellent question on that article’s message board.
回想 2008 年 5 月,我发布了一篇文章叫作《Simplifying the WPF TreeView by Using the ViewModel Pattern(通过使用 MVVM 模式来简化 WPF 的 TreeView)》的。那篇文章关注的是 MVVM 模式。今天早上,我醒来时发现一个叫 Pascal Binggeli 的家伙在那篇文章的留言板问了 一个极好的问题 。
Pascal wanted to know how to scroll a
TreeViewItem
into the viewable area of theTreeView
control when its associated ViewModel object selects it. That seems simple enough, but upon further examination, it is not quite as straightforward as one might initially expect. The objective, and problem, is to find the proper place to put code that callsBringIntoView()
on the selectedTreeViewItem
, such that the principles of the MVVM pattern are not violated.
Pascal 想知道怎样让一个 TreeViewItem
在与它关联的 ViewModel 对象中选中它时将它滚动到 TreeView
的可视区。这似乎足够简单,但在进一步检查之后,它似乎不像最初的期望那么简单。目标和问题是找到合适的地方放置代码来对选中的 TreeViewItem
调用 BringIntoView()
,同时不违反 MVVM 模式的理念。
For example, suppose that the user searches through a
TreeView
for an item whose display text matches a user-defined search string. When the search logic finds a matching item, the matching ViewModel object will have itsIsSelected
property set totrue
. Then, via the magic of data binding, theTreeViewItem
associated with that ViewModel object enters into the selected state (i.e., itsIsSelected
property is set totrue
, too). However, thatTreeViewItem
will not necessarily be in view, which means the user will not see the item that matches their search string. Pascal wanted aTreeViewItem
brought into view when the ViewModel determines that it is in the selected state.
例如,假设用户从一个 TreeView
中搜索显示文本匹配用户自定义搜索字符的一项。当搜索逻辑找到一个匹配项,ViewModel 中的匹配对象会将其 IsSelected
属性设置为 true
。然后,通过神奇的数据绑定,和这个 ViewModel 中的对象关联的 TreeViewItem
进入被选中的状态(比如,它的 IsSelected
属性也被设为 true
)。然而,这个 TreeViewItem
不一定在视野中,这意味着用户将看不到匹配他搜索字符串的项。Pascal 想要 TreeViewItem
在 ViewModel 设定它为被选中状态时被带到视野中。
The ViewModel objects have no idea that a
TreeViewItem
exists, and is bound to them, so it does not make sense to expect the ViewModel objects to bringTreeViewItem
s into view. The question becomes, now, who is responsible for bringing aTreeViewItem
into view when the ViewModel forces it to be selected?
ViewModel 对象不知道 TreeViewItem
的存在,也不能约束他们,所以期望 ViewModel 对象把 TreeViewItem
带到视野中是没有意义的。现在问题就变成了,当 ViewModel 设置一个 TreeViewItem
为被选中时谁负责将其带到视野中。
We certainly do not want to put that code into the ViewModel because it introduces an artificial, and unnecessary, coupling between a ViewModel object and a visual element. We do not want to put that code in the code-behind of every place a
TreeView
is bound to a ViewModel, because it reintroduces some of the problems that we avoid by using a ViewModel in the first place. We could create aTreeViewItem
subclass that has built-in support for bringing itself into view when selected, but, in the WPF world, that is definitely a heavy-handed solution to a lightweight problem.
我们当然不想将代码放到 ViewModel 里,因为这将在 ViewModel 对象和视觉元素之间引入人工的不必要的耦合。我们也不想把代码放在每个放置了受 ViewModel 约束的 TreeView
的后置代码中,因为这将重新引入一些我们起初通过使用 ViewModel 避免了的问题。我们可以创建一个 TreeViewItem
子类,该类拥有当被选中时将自己带到视野中的内建支持,但是,在 WPF 的世界中,这肯定就是杀鸡用牛刀了。
How can we elegantly solve this problem in a lightweight and reusable way?
我们怎样通过一个轻量的、可复用的方式优雅地解决这个问题呢?
附加行为 [Attached Behaviors]
The solution to the problem explained above is to use an attached behavior. Attaching a behavior to an object simply means making the object do something that it would not do on its own. Here is the explanation of attached behaviors that I wrote in my ‘Working with CheckBoxes in the WPF TreeView’ article:
The idea is that you set an attached property on an element so that you can gain access to the element from the class that exposes the attached property. Once that class has access to the element, it can hook events on it and, in response to those events firing, make the element do things that it normally would not do. It is a very convenient alternative to creating and using subclasses, and is very XAML-friendly.
上面阐述的问题的解决方案就是使用 附加行为。附加行为到一个对象简单来说就是让一个对象做一些它之前自己不会做的事情。我把对附加行为的解释写在了我的文章《Working with CheckBoxes in the WPF TreeView(在 WPF 的 TreeView 中使用 CheckBoxes)》中:
这个点子就是,你在一个元素上设置一个附加属性,那么你就可以从暴露这个附加属性的类中获得该元素的访问。一旦那个类有权限访问那个元素,它就能在其上挂钩事件,响应这些事件的触发,使该元素做出它通常不会做的事情。创建和使用子类是个非常方便的选择,并且对 XAML 是非常友好的。
In that article, the demo application uses attached behaviors in complicated ways, but in this article, we will keep it simple. Enough with the background and theory, let’s see how to create an attached behavior that solves the problem posed by our friend Pascal.
在那篇文章中,Demo 程序以一种复杂的方式使用附加行为,但在这篇文章中,我们会让其简单。背景和理论足够了,让我们看看怎样创建一个附加行为来解决我们的朋友 Pascal 发布的问题吧。
论证 [Demonstration]
This article’s demo app, which is available for download at the top of this page, uses the Text Search demo provided by the ‘Simplifying the WPF TreeView by Using the ViewModel Pattern’ article. I made a few changes, such as adding more items to the
TreeView
, increasing the font size, and adding an attached behavior. The attached behavior is in a new static class calledTreeViewItemBehavior
. That class exposes aBoolean
attached property that can be set on aTreeViewItem
, calledIsBroughtIntoViewWhenSelected
. Here is theTreeViewItemBehavior
class:
这篇文章的 Demo 程序(在本页顶部可供下载)使用了文章《Simplifying the WPF TreeView by Using the ViewModel Pattern》中提供的 “Text Search” 示例程序。我做了些修改,例如,往 TreeView
中添加了更多的项,增大了字体大小,添加了附加行为。附加行为在一个叫做 TreeViewItemBehavior
的新的静态类中。这个类暴露了一个可以被设置到 TreeViewItem
的 Boolean
类型的附加属性,叫作 IsBroughtIntoViewWhenSelected
。这就是 TreeViewItemBehavior
类:
/// <summary> /// Exposes attached behaviors that can be applied to TreeViewItem objects. /// 暴露可被应用到 TreeViewItem 对象的附加行为。 /// </summary> public static class TreeViewItemBehavior { #region IsBroughtIntoViewWhenSelected public static bool GetIsBroughtIntoViewWhenSelected(TreeViewItem treeViewItem) { return (bool)treeViewItem.GetValue(IsBroughtIntoViewWhenSelectedProperty); } public static void SetIsBroughtIntoViewWhenSelected(TreeViewItem treeViewItem, bool value) { treeViewItem.SetValue(IsBroughtIntoViewWhenSelectedProperty, value); } public static readonly DependencyProperty IsBroughtIntoViewWhenSelectedProperty = DependencyProperty.RegisterAttached( "IsBroughtIntoViewWhenSelected", typeof(bool), typeof(TreeViewItemBehavior), new UIPropertyMetadata(false, OnIsBroughtIntoViewWhenSelectedChanged)); static void OnIsBroughtIntoViewWhenSelectedChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e) { TreeViewItem item = depObj as TreeViewItem; if (item == null) return; if (e.NewValue is bool == false) return; if ((bool)e.NewValue) item.Selected += OnTreeViewItemSelected; else item.Selected -= OnTreeViewItemSelected; } static void OnTreeViewItemSelected(object sender, RoutedEventArgs e) { // Only react to the Selected event raised by the TreeViewItem whose IsSelected property was modified. // Ignore all ancestors who are merely reporting that a descendant's Selected fired. // 只对 IsSelected 属性被修改的 TreeViewItem 触发的 Selected 事件作出反应。 // 忽略所有只是报告子孙的 Selected 被触发的祖先。 if (!Object.ReferenceEquals(sender, e.OriginalSource)) return; TreeViewItem item = e.OriginalSource as TreeViewItem; if (item != null) item.BringIntoView(); } #endregion // IsBroughtIntoViewWhenSelected }
The attached behavior seen above is basically just a fancy way of hooking the
Selected
property of aTreeViewItem
and, when the event is raised, callingBringIntoView()
on the item. The final piece of this puzzle is seeing how theTreeViewItemBehavior
class gets a reference to everyTreeViewItem
in theTreeView
. We accomplish that by adding aSetter
to theStyle
applied to every item in theTreeView
, as seen below:
上述附加行为从根本上来说只是一种挂钩 TreeViewItem
的 Selected
属性的一种有趣的方式,当事件被触发,就在该项上调用 BringIntoView()
。这个披萨的最后一块就是看看 TreeViewItemBehavior
类如何获得 TreeView
中的每个 TreeViewItem
。我们通过给应用到 TreeView
中的每一项的 Style
添加一个 Setter
来达成,如下所示:
<TreeView.ItemContainerStyle> <Style TargetType="{x:Type TreeViewItem}"> <!-- This Setter applies an attached behavior to all TreeViewItems. --> <!-- 这个 Setter 给所有 TreeViewItems 应用附加行为 --> <Setter Property="local:TreeViewItemBehavior.IsBroughtIntoViewWhenSelected" Value="True"/> <!-- These Setters bind a TreeViewItem to a PersonViewModel. --> <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" /> <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" /> <Setter Property="FontWeight" Value="Normal" /> <Style.Triggers> <Trigger Property="IsSelected" Value="True"> <Setter Property="FontWeight" Value="Bold" /> </Trigger> </Style.Triggers> </Style> </TreeView.ItemContainerStyle>
When the demo application loads up, the search text will be set to the letter Y automatically. Click the Find button a few times, and you will see that each time an item is selected, it will contain the letter Y and will scroll into view. The fact that it scrolls into view upon being selected means that the attached behavior is working properly.
当示例程序载入后,搜索文本会被自动设置为字母 Y。点击 Find 按钮几次,你会看到每次选中了一项,它包含了字母 Y 并且会滚动到视野中。一旦被选中就会滚动到视野中这个事实意味着附加行为工作正常。
结论 [Conclusion]
Hooking an event on an object and doing something when it fires is certainly not a breakthrough innovation, by any stretch of the imagination. In that sense, attached behaviors are just another way to do the same old thing. However, the importance of this technique is that it has a name, which is probably the most important aspect of any design pattern. In addition, you can create attached behaviors and apply them to any element without having to modify any other part of the system. It is a clean solution to the problem raised by Pascal Binggeli, and many, many other problems. It\’s a very useful tool to have in your toolbox.
无论你怎么想,在对象上挂钩事件并且在触发时做些事情当然不是一个突破性的创新。从这个层面来说,附加行为只是做相同旧事的另一种方式。然而,这个技术的重要之处就是它有个名称,这可能是任何设计模式的重要层面。而且,你可以创建附加行为并将它们应用到任何元素而无需修改系统的任何部分。它是对 Pascal Binggeli 提出的问题以及其它很多很多问题的一个清爽的解决方案,是您工具箱中非常有用的工具。
参考 [References]
- The Attached Behavior Pattern – John Gossman
- Simplifying the WPF TreeView by Using the ViewModel Pattern – Josh Smith
- Working with CheckBoxes in the WPF TreeView – Josh Smith
版本历史 [Revision History]
- August 30, 2008 – Created the article.
许可证 [License]
This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)
这篇文章,包括任何附带的源码和文件,在 The Code Project Open License (CPOL) 下被许可。
资源
2021年10月19日 更新:
[dlgcy] 附加属性帮助类修改为较通用的版本(添加支持 DataGridRow):https://gitee.com/dlgcy/WPFTemplateLib/blob/master/WpfHelpers/BringIntoViewBehavior.cs
发表评论