在应用了 HierarchicalDataTemplate 的 WPF TreeView 中绑定 SelectedItem
Posted
技术标签:
【中文标题】在应用了 HierarchicalDataTemplate 的 WPF TreeView 中绑定 SelectedItem【英文标题】:Binding SelectedItem in a HierarchicalDataTemplate-applied WPF TreeView 【发布时间】:2012-06-19 10:16:22 【问题描述】:我有一个数据绑定TreeView
,我想绑定SelectedItem
。 This attached behavior 在没有 HierarchicalDataTemplate
的情况下可以完美运行,但附加的行为只能以一种方式(UI 到数据)起作用,因为现在 e.NewValue
是 MyViewModel
而不是 TreeViewItem
。
这是来自附加行为的代码 sn-p:
private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
var item = e.NewValue as TreeViewItem;
if (item != null)
item.SetValue(TreeViewItem.IsSelectedProperty, true);
这是我的TreeView
定义:
<Window xmlns:interactivity="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity">
<TreeView ItemsSource="Binding MyItems" VirtualizingStackPanel.IsVirtualizing="True">
<interactivity:Interaction.Behaviors>
<behaviors:TreeViewSelectedItemBindingBehavior SelectedItem="Binding SelectedItem, Mode=TwoWay" />
</interactivity:Interaction.Behaviors>
<TreeView.Resources>
<HierarchicalDataTemplate DataType="x:Type local:MyViewModel" ItemsSource="Binding Children">
<TextBlock Text="Binding Name"/>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
</Window>
如果我可以在附加的行为方法OnSelectedItemChanged
中获得对TreeView
的引用,也许我可以使用this question 中的答案来获得TreeViewItem
,但我不知道如何到达那里。有谁知道如何以及它是正确的方式吗?
【问题讨论】:
【参考方案1】:这是上述附加行为的改进版本。它完全支持双向绑定,还可以与HeriarchicalDataTemplate
和TreeView
s 一起使用,其中它的项目是虚拟化的。请注意,虽然要找到需要选择的“TreeViewItem”,但它会实现(即创建)虚拟化的TreeViewItem
s,直到找到正确的。这可能是大型虚拟树的性能问题。
/// <summary>
/// Behavior that makes the <see cref="System.Windows.Controls.TreeView.SelectedItem" /> bindable.
/// </summary>
public class BindableSelectedItemBehavior : Behavior<TreeView>
/// <summary>
/// Identifies the <see cref="SelectedItem" /> dependency property.
/// </summary>
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register(
"SelectedItem",
typeof(object),
typeof(BindableSelectedItemBehavior),
new UIPropertyMetadata(null, OnSelectedItemChanged));
/// <summary>
/// Gets or sets the selected item of the <see cref="TreeView" /> that this behavior is attached
/// to.
/// </summary>
public object SelectedItem
get
return this.GetValue(SelectedItemProperty);
set
this.SetValue(SelectedItemProperty, value);
/// <summary>
/// Called after the behavior is attached to an AssociatedObject.
/// </summary>
/// <remarks>
/// Override this to hook up functionality to the AssociatedObject.
/// </remarks>
protected override void OnAttached()
base.OnAttached();
this.AssociatedObject.SelectedItemChanged += this.OnTreeViewSelectedItemChanged;
/// <summary>
/// Called when the behavior is being detached from its AssociatedObject, but before it has
/// actually occurred.
/// </summary>
/// <remarks>
/// Override this to unhook functionality from the AssociatedObject.
/// </remarks>
protected override void OnDetaching()
base.OnDetaching();
if (this.AssociatedObject != null)
this.AssociatedObject.SelectedItemChanged -= this.OnTreeViewSelectedItemChanged;
private static Action<int> GetBringIndexIntoView(Panel itemsHostPanel)
var virtualizingPanel = itemsHostPanel as VirtualizingStackPanel;
if (virtualizingPanel == null)
return null;
var method = virtualizingPanel.GetType().GetMethod(
"BringIndexIntoView",
BindingFlags.Instance | BindingFlags.NonPublic,
Type.DefaultBinder,
new[] typeof(int) ,
null);
if (method == null)
return null;
return i => method.Invoke(virtualizingPanel, new object[] i );
/// <summary>
/// Recursively search for an item in this subtree.
/// </summary>
/// <param name="container">
/// The parent ItemsControl. This can be a TreeView or a TreeViewItem.
/// </param>
/// <param name="item">
/// The item to search for.
/// </param>
/// <returns>
/// The TreeViewItem that contains the specified item.
/// </returns>
private static TreeViewItem GetTreeViewItem(ItemsControl container, object item)
if (container != null)
if (container.DataContext == item)
return container as TreeViewItem;
// Expand the current container
if (container is TreeViewItem && !((TreeViewItem)container).IsExpanded)
container.SetValue(TreeViewItem.IsExpandedProperty, true);
// Try to generate the ItemsPresenter and the ItemsPanel.
// by calling ApplyTemplate. Note that in the
// virtualizing case even if the item is marked
// expanded we still need to do this step in order to
// regenerate the visuals because they may have been virtualized away.
container.ApplyTemplate();
var itemsPresenter =
(ItemsPresenter)container.Template.FindName("ItemsHost", container);
if (itemsPresenter != null)
itemsPresenter.ApplyTemplate();
else
// The Tree template has not named the ItemsPresenter,
// so walk the descendents and find the child.
itemsPresenter = container.GetVisualDescendant<ItemsPresenter>();
if (itemsPresenter == null)
container.UpdateLayout();
itemsPresenter = container.GetVisualDescendant<ItemsPresenter>();
var itemsHostPanel = (Panel)VisualTreeHelper.GetChild(itemsPresenter, 0);
// Ensure that the generator for this panel has been created.
#pragma warning disable 168
var children = itemsHostPanel.Children;
#pragma warning restore 168
var bringIndexIntoView = GetBringIndexIntoView(itemsHostPanel);
for (int i = 0, count = container.Items.Count; i < count; i++)
TreeViewItem subContainer;
if (bringIndexIntoView != null)
// Bring the item into view so
// that the container will be generated.
bringIndexIntoView(i);
subContainer =
(TreeViewItem)container.ItemContainerGenerator.
ContainerFromIndex(i);
else
subContainer =
(TreeViewItem)container.ItemContainerGenerator.
ContainerFromIndex(i);
// Bring the item into view to maintain the
// same behavior as with a virtualizing panel.
subContainer.BringIntoView();
if (subContainer == null)
continue;
// Search the next level for the object.
var resultContainer = GetTreeViewItem(subContainer, item);
if (resultContainer != null)
return resultContainer;
// The object is not under this TreeViewItem
// so collapse it.
subContainer.IsExpanded = false;
return null;
private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
var item = e.NewValue as TreeViewItem;
if (item != null)
item.SetValue(TreeViewItem.IsSelectedProperty, true);
return;
var behavior = (BindableSelectedItemBehavior)sender;
var treeView = behavior.AssociatedObject;
if (treeView == null)
// at designtime the AssociatedObject sometimes seems to be null
return;
item = GetTreeViewItem(treeView, e.NewValue);
if (item != null)
item.IsSelected = true;
private void OnTreeViewSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
this.SelectedItem = e.NewValue;
为了完整起见,更高级的是GetVisualDescentants
的实现:
/// <summary>
/// Extension methods for the <see cref="DependencyObject" /> type.
/// </summary>
public static class DependencyObjectExtensions
/// <summary>
/// Gets the first child of the specified visual that is of tyoe <typeparamref name="T" />
/// in the visual tree recursively.
/// </summary>
/// <param name="visual">The visual to get the visual children for.</param>
/// <returns>
/// The first child of the specified visual that is of tyoe <typeparamref name="T" /> of the
/// specified visual in the visual tree recursively or <c>null</c> if none was found.
/// </returns>
public static T GetVisualDescendant<T>(this DependencyObject visual) where T : DependencyObject
return (T)visual.GetVisualDescendants().FirstOrDefault(d => d is T);
/// <summary>
/// Gets all children of the specified visual in the visual tree recursively.
/// </summary>
/// <param name="visual">The visual to get the visual children for.</param>
/// <returns>All children of the specified visual in the visual tree recursively.</returns>
public static IEnumerable<DependencyObject> GetVisualDescendants(this DependencyObject visual)
if (visual == null)
yield break;
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(visual); i++)
var child = VisualTreeHelper.GetChild(visual, i);
yield return child;
foreach (var subChild in GetVisualDescendants(child))
yield return subChild;
【讨论】:
如何使用 GetVisualDescendant 方法?我添加了对 PresentationFramework 的引用,但仍然无法使用?我错过了什么? GetVisualDescendant 方法是在拖放工具中使用的扩展方法implementation,反正我就是在那儿找到的。 像魅力一样工作。扩展 TreeView 控件的不良 mvvm 功能的非常好的解决方案。 @peter 请查看 Xtr 的 avove 评论中的链接 @bitbonk,请原谅我的愚蠢。我已经看到了那个链接,但是把那个代码放在哪里?【参考方案2】:如果您像我一样发现 this answer 有时会因为 itemPresenter
为空而崩溃,那么对该解决方案的修改可能对您有用。
将OnSelectedItemChanged
更改为此(如果尚未加载树,则等待树 加载并重试):
private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
Action<TreeViewItem> selectTreeViewItem = tvi2 =>
if (tvi2 != null)
tvi2.IsSelected = true;
tvi2.Focus();
;
var tvi = e.NewValue as TreeViewItem;
if (tvi == null)
var tree = ((BindableTreeViewSelectedItemBehavior) sender).AssociatedObject;
if (!tree.IsLoaded)
RoutedEventHandler handler = null;
handler = (sender2, e2) =>
tvi = GetTreeViewItem(tree, e.NewValue);
selectTreeViewItem(tvi);
tree.Loaded -= handler;
;
tree.Loaded += handler;
return;
tvi = GetTreeViewItem(tree, e.NewValue);
selectTreeViewItem(tvi);
【讨论】:
【参考方案3】:我知道这是个老问题,但也许对其他人有帮助。我结合了Link的代码
现在看起来:
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;
using System.Windows.Media;
namespace Behaviors
public class BindableSelectedItemBehavior : Behavior<TreeView>
#region SelectedItem Property
public object SelectedItem
get return (object)GetValue(SelectedItemProperty);
set SetValue(SelectedItemProperty, value);
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register("SelectedItem", typeof(object), typeof(BindableSelectedItemBehavior), new UIPropertyMetadata(null, OnSelectedItemChanged));
private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
// if binded to vm collection than this way is not working
//var item = e.NewValue as TreeViewItem;
//if (item != null)
//
// item.SetValue(TreeViewItem.IsSelectedProperty, true);
//
var tvi = e.NewValue as TreeViewItem;
if (tvi == null)
var tree = ((BindableSelectedItemBehavior)sender).AssociatedObject;
tvi = GetTreeViewItem(tree, e.NewValue);
if (tvi != null)
tvi.IsSelected = true;
tvi.Focus();
#endregion
#region Private
private void OnTreeViewSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
SelectedItem = e.NewValue;
private static TreeViewItem GetTreeViewItem(ItemsControl container, object item)
if (container != null)
if (container.DataContext == item)
return container as TreeViewItem;
// Expand the current container
if (container is TreeViewItem && !((TreeViewItem)container).IsExpanded)
container.SetValue(TreeViewItem.IsExpandedProperty, true);
// Try to generate the ItemsPresenter and the ItemsPanel.
// by calling ApplyTemplate. Note that in the
// virtualizing case even if the item is marked
// expanded we still need to do this step in order to
// regenerate the visuals because they may have been virtualized away.
container.ApplyTemplate();
var itemsPresenter =
(ItemsPresenter)container.Template.FindName("ItemsHost", container);
if (itemsPresenter != null)
itemsPresenter.ApplyTemplate();
else
// The Tree template has not named the ItemsPresenter,
// so walk the descendents and find the child.
itemsPresenter = FindVisualChild<ItemsPresenter>(container);
if (itemsPresenter == null)
container.UpdateLayout();
itemsPresenter = FindVisualChild<ItemsPresenter>(container);
var itemsHostPanel = (Panel)VisualTreeHelper.GetChild(itemsPresenter, 0);
// Ensure that the generator for this panel has been created.
#pragma warning disable 168
var children = itemsHostPanel.Children;
#pragma warning restore 168
for (int i = 0, count = container.Items.Count; i < count; i++)
var subContainer = (TreeViewItem)container.ItemContainerGenerator.
ContainerFromIndex(i);
if (subContainer == null)
continue;
subContainer.BringIntoView();
// Search the next level for the object.
var resultContainer = GetTreeViewItem(subContainer, item);
if (resultContainer != null)
return resultContainer;
else
// The object is not under this TreeViewItem
// so collapse it.
//subContainer.IsExpanded = false;
return null;
/// <summary>
/// Search for an element of a certain type in the visual tree.
/// </summary>
/// <typeparam name="T">The type of element to find.</typeparam>
/// <param name="visual">The parent element.</param>
/// <returns></returns>
private static T FindVisualChild<T>(Visual visual) where T : Visual
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(visual); i++)
Visual child = (Visual)VisualTreeHelper.GetChild(visual, i);
if (child != null)
T correctlyTyped = child as T;
if (correctlyTyped != null)
return correctlyTyped;
T descendent = FindVisualChild<T>(child);
if (descendent != null)
return descendent;
return null;
#endregion
#region Protected
protected override void OnAttached()
base.OnAttached();
AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged;
protected override void OnDetaching()
base.OnDetaching();
if (AssociatedObject != null)
AssociatedObject.SelectedItemChanged -= OnTreeViewSelectedItemChanged;
#endregion
【讨论】:
有时我们到达var itemsHostPanel = (Panel)VisualTreeHelper.GetChild(itemsPresenter, 0);
,而itemPresenter
为空。有什么想法吗?
@Killercam 你打败了我 :) 我猜你在 Gemini 中发现了与我现在正在调查的相同的错误......
问题似乎是 GetVisualChild 并不总是有效,因为可视化树在调用时并不总是完全加载。
嗨@TimJones,我想我已经通过稍微重写这个行为来解决这个问题。我很想看看你的解决方案,你合并到 Gemini 了吗?
我已将它合并到 Gemini 中,并且我还在下面添加了它作为附加答案 (***.com/a/27447702/208817)。以上是关于在应用了 HierarchicalDataTemplate 的 WPF TreeView 中绑定 SelectedItem的主要内容,如果未能解决你的问题,请参考以下文章
应用程序调试 - 是啥决定了我的应用程序在后台停留多长时间?
在拒绝 iOS 应用程序后,Apple 为重新提交应用程序提供了多长时间?
如何知道在 django 中使用了哪个版本的应用程序 [重复]
在应用程序被杀死并重新启动后,Android 应用程序黑暗主题消失了