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

WPF 附加行为简介

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (99投票s)

2008年8月30日

CPOL

5分钟阅读

viewsIcon

795469

downloadIcon

9682

解释了附加行为的概念,并展示了如何在 MVVM 模式的上下文中实现它们。

AttachedBehavior.jpg

引言

本文解释了什么是附加行为,以及如何在 WPF 应用程序中实现它们。本文的读者应该对 WPF、XAML、附加属性和 Model-View-ViewModel (MVVM) 模式有所了解。我强烈建议您也阅读我的文章《使用 ViewModel 模式简化 WPF TreeView》,因为本文的材料是对该文章中介绍的材料的扩展。

背景

早在 2008 年 5 月,我曾发表过一篇题为《使用 ViewModel 模式简化 WPF TreeView》的文章。那篇文章侧重于 MVVM 模式。今天早上,我醒来时发现一位名叫 Pascal Binggeli 的开发者在那篇文章的消息板上提出了一个非常好的问题

Pascal 想知道,当 ViewModel 对象选择了一个 TreeViewItem 时,如何将其滚动到 TreeView 控件的可视区域内。这看起来很简单,但经过仔细检查,它并不像乍一看那样直接。目标和问题在于找到调用所选 TreeViewItemBringIntoView() 方法的正确位置,以避免违反 MVVM 模式的原则。

例如,假设用户在一个 TreeView 中搜索一个显示文本与用户定义的搜索字符串匹配的项。当搜索逻辑找到匹配项时,匹配的 ViewModel 对象会将其 IsSelected 属性设置为 true。然后,通过数据绑定的魔力,与该 ViewModel 对象关联的 TreeViewItem 将进入选中状态(即,其 IsSelected 属性也被设置为 true)。但是,该 TreeViewItem 不一定在视图中,这意味着用户将看不到匹配其搜索字符串的项。Pascal 希望在 ViewModel 确定 TreeViewItem 处于选中状态时,将其带入视图。

ViewModel 对象不知道 TreeViewItem 的存在,也不知道它与它们绑定,所以不能期望 ViewModel 对象来将 TreeViewItem 带入视图。现在的问题是,当 ViewModel 强制选中 TreeViewItem 时,谁负责将其带入视图?

我们当然不希望将这段代码放入 ViewModel 中,因为它会在 ViewModel 对象和视觉元素之间引入人为的、不必要的耦合。我们也不希望将其放入每个绑定到 ViewModel 的 TreeView 的代码隐藏文件中,因为这会重新引入我们最初通过使用 ViewModel 来避免的一些问题。我们可以创建一个 TreeViewItem 子类,使其在被选中时具有内置的带入视图的支持,但在 WPF 世界中,对于一个轻量级的问题来说,这绝对是一个重大的解决方案。

我们如何以轻量级且可重用的方式优雅地解决这个问题?

附加行为

上述问题的解决方案是使用附加行为。将行为附加到对象上,仅仅意味着让该对象执行一些它本身不会做的事情。以下是我在我写的《在 WPF TreeView 中使用 CheckBoxes》一文中关于附加行为的解释:

其思想是在元素上设置一个附加属性,以便您可以从公开附加属性的类中访问该元素。一旦该类拥有了对元素的访问权,它就可以挂接元素上的事件,并响应这些事件的触发,让元素执行它通常不会执行的操作。这是创建和使用子类的一种非常方便的替代方法,并且非常适合 XAML。

在那篇文章中,演示应用程序以复杂的方式使用附加行为,但在本文中,我们将保持简单。背景和理论就说到这里,让我们看看如何创建一个解决我们朋友 Pascal 提出的问题的附加行为。

演示

本文的演示应用程序(可在页面顶部下载)使用了“使用 ViewModel 模式简化 WPF TreeView”一文提供的文本搜索演示。我做了一些修改,例如在 TreeView 中添加了更多项,增加了字体大小,并添加了一个附加行为。附加行为位于一个名为 TreeViewItemBehavior 的新静态类中。该类公开了一个名为 IsBroughtIntoViewWhenSelected 的布尔附加属性,可以设置在 TreeViewItem 上。下面是 TreeViewItemBehavior 类:

/// <summary>
/// Exposes attached behaviors that can be
/// applied to TreeViewItem objects.
/// </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.
        if (!Object.ReferenceEquals(sender, e.OriginalSource))
            return;

        TreeViewItem item = e.OriginalSource as TreeViewItem;
        if (item != null)
            item.BringIntoView();
    }

    #endregion // IsBroughtIntoViewWhenSelected
}

上面看到的附加行为基本上只是一个挂接 TreeViewItemSelected 属性的巧妙方法,当事件触发时,对该项调用 BringIntoView()。这个谜题的最后一块是看看 TreeViewItemBehavior 类如何获得对 TreeView 中每个 TreeViewItem 的引用。我们通过向应用于 TreeView 中每个项的 Style 添加一个 Setter 来实现这一点,如下所示:

 <TreeView.ItemContainerStyle>
  <Style TargetType="{x:Type TreeViewItem}">
    <!--
    This Setter applies an attached behavior to all 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>

当演示应用程序加载时,搜索文本将自动设置为字母 Y。点击几次“查找”按钮,您会看到每次选择一个项时,该项都将包含字母 Y 并滚动到视图中。它在被选中时滚动到视图中的事实意味着附加行为正在正常工作。

结论

通过挂接对象的事件并在事件触发时执行某些操作,绝不是什么突破性的创新。从这个意义上说,附加行为只是做同样的老事情的另一种方式。然而,这项技术的重要性在于它有一个名字,这可能是任何设计模式最重要的方面。此外,您可以创建附加行为并将其应用于任何元素,而无需修改系统的任何其他部分。这是对 Pascal Binggeli 提出的问题以及许多其他问题的清晰解决方案。它是一个非常有用的工具,可以放在您的工具箱里。

参考文献

修订历史

  • 2008 年 8 月 30 日 – 创建了本文。
© . All rights reserved.