如何从层次数据模板项获取TreeViewItem?

36

我有一个TreeView,它使用HierarchicalDataTemplate来绑定数据。

它长这样:

<TreeView x:Name="mainTreeList" ItemsSource="{Binding MyCollection}>
  <TreeView.Resources>
    <HierarchicalDataTemplate 
     DataType="{x:Type local:MyTreeViewItemViewModel}" 
     ItemsSource="{Binding Children}">
      <!-- code code code -->
    </HierarchicalDataTemplate>
  </TreeView.Resources>
</TreeView>

现在,我想要从代码后台(code-behind)中获取当前选定的TreeViewItem。然而,如果我使用以下代码:

this.mainTreeList.SelectedItem;

选中的项是类型为MyTreeViewItemViewModel。但我想获取“parent”或“bound”的TreeViewItem,并且不会将其传递给我的TreeViewItemModel对象(甚至不知道该如何传递)。

我该怎么做?


@romkyns:什么?这个问题与跟踪所选项目毫无关系 - H.B.
@romkyns:嗯,你本来就不应该得到TreeViewItem,所以它是否难以实现并不重要。 - H.B.
@H.B. 可能确实如此...也许你可以帮我在不使用TreeViewItem的情况下完成这个任务? - Roman Starkov
@H.B. 我现在直接问问题了:访问TreeViewItems是否有问题?,期待您的回答。 - Roman Starkov
12个回答

32
TreeViewItem item = (TreeViewItem)(mainTreeList
    .ItemContainerGenerator
    .ContainerFromIndex(mainTreeList.Items.CurrentPosition));

对我而言不起作用,因为在使用HierarchicalDataTemplate的树形视图中,mainTreeList.Items.CurrentPosition始终为-1。

以下方法同样也不起作用,因为在使用HierarchicalDataTemplate的树形视图中,mainTreeList.Items.CurrentItem始终为null。

TreeViewItem item = (TreeViewItem)mainTreeList
    .ItemContainerGenerator
    .ContainerFromItem(mainTreeList.Items.CurrentItem);

相反地,我必须在路由的TreeViewItem.Selected事件中设置最后选择的TreeViewItem,该事件会向上传递到树形视图(由于我们使用HierarchicalDataTemplate,在设计时TreeViewItem本身不存在)。

可以在XAML中通过以下方式捕获事件:

<TreeView TreeViewItem.Selected="TreeViewItemSelected" .../> 

然后在事件中可以这样设置最后选定的TreeViewItem:

    private void TreeViewItemSelected(object sender, RoutedEventArgs e)
    {
        TreeViewItem tvi = e.OriginalSource as TreeViewItem;

        // set the last tree view item selected variable which may be used elsewhere as there is no other way I have found to obtain the TreeViewItem container (may be null)
        this.lastSelectedTreeViewItem = tvi;

        ...
     }

2
我认为问题在于您正在使用mainTreeList.Items,这仅适用于树视图第一级中的项目 - 每个树视图节点(TreeViewItem)都有自己的Items集合 - 与Bea的示例不同,您引用了一个仅具有一级项目的ListBox。因此,您需要获取当前选定的TreeViewItem的引用,而我发现在使用HierarchicalDataTemplate时唯一的方法是捕获TreeViewItem.Selected事件,就像我的答案中所述。 - markmnl
顺便提一下,Bea也有一个TreeView的解决方案 - 但那比我上面的方法要麻烦得多,因为它涉及等待UI线程完成所有任务,然后假设TreeViewItems已经被创建:http://bea.stollnitz.com/blog/?p=59 - markmnl
如何在代码中添加事件:ctLayersTree.AddHandler(TreeViewItem.SelectedEvent, <your handler>) - Roman Starkov
1
说实话,这解决了。WPF是一个可怕的、庞大的、难以理解的混乱怪物。这里有很多聪明人,却不能轻松地找到一个简单的方法来获取一个大而臃肿的可视控件上的项目名称,即使他们找到了答案,也只是一个事件。 - j riv
@LelaDax 需要一些时间来适应,但我很高兴我做到了,并且现在总体上喜欢XAML :) 注意:这种情况很不寻常 - 你为什么要尝试使用Visual?在XAML中的绑定应该可以避免这种需要。 - markmnl
显示剩余3条评论

24

根据Bea Stollnitz的博客文章,尝试以下方法:

TreeViewItem item = (TreeViewItem)(mainTreeList
    .ItemContainerGenerator
    .ContainerFromIndex(mainTreeList.Items.CurrentPosition));

可以了,谢谢。虽然有点不同,CurrentItem返回一个对象而ContainerFromIndex需要一个整数,所以我不得不使用CurrentIndex或其他什么东西,但没关系。 - Razzie
啊,我刚刚从Bea的博客上复制粘贴,然后改了一些东西。我没有注意到那个错误 :) 不过,我会更新我的答案。 - Cameron MacFarland
10
无论我选择了什么,我的 CurrentPosition 始终为0。你提供的博客文章是关于列表而不是树形结构 - 或许这就是原因所在。链接的博客文章中没有涉及到 HierarchicalDataTemplate - Roman Starkov
1
它已经工作了一段时间,但是现在CurrentPosition总是0。 - zionpi

10

我遇到了同样的问题。 我需要获取TreeViewItem以便我可以选择它。 然后我意识到我可以在我的ViewModel中添加一个属性IsSelected,然后将其绑定到TreeViewItems的IsSelectedProperty。 这可以通过ItemContainerStyle实现:

<TreeView>
            <TreeView.ItemContainerStyle>
                <Style TargetType="{x:Type TreeViewItem}">
                    <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
                    <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
                </Style>
            </TreeView.ItemContainerStyle>
</TreeView>

现在如果我想要在树形视图中选择一个项目,我只需要直接调用我的ViewModel类上的IsSelected属性。

希望能对某些人有所帮助。


2
在我看来,这是使用树视图的最佳方式。项目必须是视图模型,以便您可以使用它们的属性进行操作。否则,您最终将不得不做出各种花哨的手段,以使本应简单的行为正常工作。 - Jason Ridge
1
我同意,但问题是如何获取_TreeViewItem_而不是它包装的对象。 - markmnl
是的,这确实是你应该做的方式。但是为每个东西都创建一个ViewModel非常麻烦,特别是对于小型基本UI来说。 - Roman Starkov

7
TreeViewItem item = (TreeViewItem)(mainTreeList
    .ItemContainerGenerator
    .ContainerFromIndex(mainTreeList.Items.CurrentPosition)); gives first item in the TreeView because CurrentPosition is always 0.

如何?
TreeViewItem item = (TreeViewItem)(mainTreeList
    .ItemContainerGenerator
    .ContainerFromItem(mainTreeList.SelectedItem)));

这对我来说效果更好。


你可以简化最后一个示例。只需使用 ...ItemGenerator.ContainerFromItem(mainTreeList.SelectedItem) 即可。如果您正在使用一个或多个 HierarchicalDataTemplate,这也适用,而第一个示例则不行。我允许自己编辑了你的答案,希望你没关系。 - René
不起作用,mainTreeList.SelectedItemis null是多余的,mainTreeList.SelectedItem和TreeViewItem项是相同的。 - Ejrr1085

4
受 Fëanor 的回答启发,我尝试让 TreeViewItem 对于每个已创建的数据项都易于访问。
这个想法是在视图模型中添加一个类型为 TreeViewItem 的字段,并通过接口公开它。每当创建 TreeViewItem 容器时,TreeView 就会自动填充该字段。
这是通过对 TreeView 进行子类化并将事件附加到 ItemContainerGenerator 来完成的,在创建 TreeViewItem 时记录它。需要注意的是,由于 TreeViewItem 是懒加载的,因此有时可能确实无法获得它。
自发布此答案以来,我已经进一步开发并在一个项目中长期使用它。迄今为止没有问题,除了这违反了 MVVM(但对于简单情况也可以节省大量样板代码)。源代码在这里用法 选择所选项的父项并将其折叠,确保其在视图中可见:
...
var selected = myTreeView.SelectedItem as MyItem;
selected.Parent.TreeViewItem.IsSelected = true;
selected.Parent.TreeViewItem.IsExpanded = false;
selected.Parent.TreeViewItem.BringIntoView();
...

声明:

<Window ...
        xmlns:tvi="clr-namespace:TreeViewItems"
        ...>
    ...
    <tvi:TreeViewWithItem x:Name="myTreeView">
        <HierarchicalDataTemplate DataType = "{x:Type src:MyItem}"
                                  ItemsSource = "{Binding Children}">
            <TextBlock Text="{Binding Path=Name}"/>
        </HierarchicalDataTemplate>
    </tvi:TreeViewWithItem>
    ...
</Window>

class MyItem : IHasTreeViewItem
{
    public string Name { get; set; }
    public ObservableCollection<MyItem> Children { get; set; }
    public MyItem Parent;
    public TreeViewItem TreeViewItem { get; set; }
    ...
}

代码

public class TreeViewWithItem : TreeView
{
    public TreeViewWithItem()
    {
        ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
    }

    private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
    {
        var generator = sender as ItemContainerGenerator;
        if (generator.Status == GeneratorStatus.ContainersGenerated)
        {
            int i = 0;
            while (true)
            {
                var container = generator.ContainerFromIndex(i);
                if (container == null)
                    break;

                var tvi = container as TreeViewItem;
                if (tvi != null)
                    tvi.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;

                var item = generator.ItemFromContainer(container) as IHasTreeViewItem;
                if (item != null)
                    item.TreeViewItem = tvi;

                i++;
            }
        }
    }
}

interface IHasTreeViewItem
{
    TreeViewItem TreeViewItem { get; set; }
}

检查这条路非常有趣,也很有启发性,让我了解了WPF的工作原理(也被称为黑魔法),但是赏金答案告诉我如何正确地做到这一点。 这不是一个好的方法(请恕我直言),因为项目会被重新生成,而且您会不断添加处理程序,而且您永远不会删除那些处理程序,因为没有“已删除”的生成器状态。 我们有路由事件,我们应该学习并使用它们。 - L.Trabacchin
@L.Trabacchin 我完全同意我的方法有些hacky。说实话,对于复杂的用户界面,最好的选择几乎肯定是放弃访问TreeViewItems的想法;那不是WPF的使用方式。我们应该访问我们的ViewModel实例而不是TreeViewItems。我不记得为什么有赏金的答案对我来说行不通了,但它看起来比我的方法少一些hacky的感觉。 - Roman Starkov
当然,另一种很好的方法是使用视图模型,但这取决于我们想要实现什么,对我来说,路由事件效果更好,因为我试图扩展树形视图以实现多选树形视图,但我希望代码尽可能少,因为那可能会导致维护和错误,并且希望它能独立于使用的模型而工作,我大部分都做到了,现在有一些缺陷,需要将所选项目公开以供其他控件访问...我希望我知道更多 :) 谢谢! - L.Trabacchin

2
尝试像这样做:

试试以下方法:

    public bool UpdateSelectedTreeViewItem(PropertyNode dateItem, ItemsControl itemsControl)
    {
        if (itemsControl == null || itemsControl.Items == null || itemsControl.Items.Count == 0)
        {
            return false;
        }
        foreach (var item in itemsControl.Items.Cast<PropertyNode>())
        {
            var treeViewItem = itemsControl.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
            if (treeViewItem == null)
            {
                continue;
            }
            if (item == dateItem)
            {
                treeViewItem.IsSelected = true;
                return true;
            }
            if (treeViewItem.Items.Count > 0 && UpdateSelectedTreeViewItem(dateItem, treeViewItem))
            {
                return true;
            }
        }
        return false;
    }

2

我将William的递归搜索修改为更紧凑的版本:

public TreeViewItem GetTreeViewItemFromObject(ItemContainerGenerator container, object targetObject) {
    if (container.ContainerFromItem(targetObject) is TreeViewItem target) return target;
    for (int i = 0; i < container.Items.Count; i++)
        if ((container.ContainerFromIndex(i) as TreeViewItem)?.ItemContainerGenerator is ItemContainerGenerator childContainer)
            if (GetTreeViewItemFromObject(childContainer, targetObject) is TreeViewItem childTarget) return childTarget;
    return null;
}

通过提供TreeView实例的ItemContainerGenerator和目标数据对象,可以调用它:

TreeViewItem tvi = GetTreeViewItemFromObject(treeView.ItemContainerGenerator, targetDataObject);

1

当使用'ItemContainerGenerator.ContainerFromItem'或'ItemContainerGenerator.ItemContainerGenerator'方法时,无法通过树状视图对象找到与之相关联的TreeViewItem,这是因为树状视图项控制器和树状视图项数据被分开了。

为了在树状视图中定位TreeViewItem,需要创建一个递归函数来使用'ItemContainerGenerator.ContainerFromItem'和'ItemContainerGenerator.ItemContainerGenerator'方法。

递归函数的示例代码可能如下所示:

private TreeViewItem GetTreeViewItemFromObject(ItemContainerGenerator container, object tvio)
{
    var item = container.ContainerFromItem(tvio) as TreeViewItem;
    if (item != null)
    {
        return item;
    }

    for (int i = 0; i < container.Items.Count; i++)
    {
        var subContainer = (TreeViewItem)container.ContainerFromIndex(i);
        if (subContainer != null)
        {
            item = GetTreeViewItemFromObject(subContainer.ItemContainerGenerator, tvio);
            if (item != null)
            {
                return item;
            }
        }
    }

    return null;
}

被调用的代码为:

var target = GetTreeViewItemFromObject(treeView.ItemContainerGenerator, item);

只有在树形视图的'ItemContainerGenerator.Status'为'ContainersGenerated'时,递归函数才能正常工作。因此,在初始化视图期间,'GetTreeViewItemFromObject'无法正常工作。


0

这里是解决方案。 rtvEsa 是树形视图。 HierarchicalDataTemplate 是树形视图模板。 Tag 使当前项的实际使用成为可能。它不是所选项,而是在使用 HierarchicalDataTemplate 的树控件中的当前项。

Items.CurrentItem 是内部树集合的一部分。 您可以获取许多不同的数据。例如,Items.ParenItem。

  <HierarchicalDataTemplate ItemsSource="{Binding ChildItems}">

  <TextBox Tag="{Binding ElementName=rtvEsa, Path=Items.CurrentItem }" />


0
如果你需要在子元素的子元素中查找项目,你可能需要像这样使用递归。
public bool Select(TreeViewItem item, object select) // recursive function to set item selection in treeview
{
    if (item == null)
        return false;
    TreeViewItem child = item.ItemContainerGenerator.ContainerFromItem(select) as TreeViewItem;
    if (child != null)
    {
        child.IsSelected = true;
        return true;
    }
    foreach (object c in item.Items)
    {
        bool result = Select(item.ItemContainerGenerator.ContainerFromItem(c) as TreeViewItem, select);
        if (result == true)
            return true;
    }
    return false;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接