将 SelectedPath 属性与 WPF 的 TreeView 中的 SelectedItem 同步 [英] Synchronizing a SelectedPath property with the SelectedItem in WPF's TreeView

查看:18
本文介绍了将 SelectedPath 属性与 WPF 的 TreeView 中的 SelectedItem 同步的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试创建一个与 WPF TreeView 同步的 SelectedPath 属性(例如在我的视图模型中).理论如下:

I am trying to create a SelectedPath property (e.g. in my view-model) that is synchronized with a WPF TreeView. The theory is as follows:

  • 每当树视图中的选定项发生更改(SelectedItem 属性/SelectedItemChanged 事件)时,更新 SelectedPath 属性以存储字符串表示到所选树节点的整个路径.
  • 每当 SelectedPath 属性改变时,找到路径字符串指示的树节点,展开到该树节点的整个路径,并在取消选择之前选择的节点后选择它.
  • Whenever the selected item in the tree view is changed (SelectedItem property/SelectedItemChanged event), update the SelectedPath property to store a string that represents the whole path to the selected tree node.
  • Whenever the SelectedPath property is changed, find the tree node indicated by the path string, expand the whole path to that tree node, and select it, after de-selecting the previously selected node.

为了使所有这些可重现,让我们假设所有树节点都是 DataNode 类型(见下文),每个树节点都有一个在其子节点中唯一的名称父节点,并且路径分隔符是单个正斜杠 /.

In order to make all of this reproducible, let us assume that all tree nodes are of type DataNode (see below), that every tree node has a name that is unique among the children of its parent node, and that the path separator be a single forward slash /.

更新 SelectedItemChange 事件中的 SelectedPath 属性不是问题 - 以下事件处理程序完美无缺:

Updating the SelectedPath property in the SelectedItemChange event is not a problem - the following event handler works flawlessly:

void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
    DataNode selNode = e.NewValue as DataNode;
    if (selNode == null) {
        vm.SelectedPath = null;
    } else {
        vm.SelectedPath = selNode.FullPath;
    }
}

但是,我无法让相反的方式正常工作.因此,基于以下通用化和最小化的代码示例,我的问题是:如何让 WPF 的 TreeView 尊重我对项目的编程选择?

However, I fail to make the other way round work properly. Hence, my question, based on the generalized and minimized code sample below, is: How do I make WPF's TreeView respect my programmatical selection of items?

现在,我走了多远?首先,TreeView的SelectedItem 属性 是只读的,所以不能直接设置.我发现并阅读了许多深入讨论这个问题的 SO 问题(例如 这个这个this),以及其他网站上的资源,例如这篇博文, 这篇文章这篇博文.

Now, how far have I come? First of all, TreeView's SelectedItem property is read-only, so it cannot be set directly. I have found and read numerous SO questions discussing this in-depth (such as this, this or this), and also resources on other sites, such as this blogpost, this article or this blogpost.

几乎所有这些资源都指向为 TreeViewItem 定义一个样式,该样式将 TreeViewItemIsSelected 属性绑定到底层的等效属性视图模型中的树节点对象.有时(例如此处here),绑定是双向的,有时(例如 这里此处)这是一种单向绑定.我不认为将其设为单向绑定的意义(如果树视图 UI 以某种方式取消选择该项目,则该更改当然应反映在底层视图模型中),因此我实现了双向绑定版本.(通常建议对 IsExpanded 使用相同的方法,因此我还为此添加了一个属性.)

Almost all of these resources point to defining a style for TreeViewItem that binds TreeViewItem's IsSelected property to an equivalent property of the underlying tree node object from the view-model. Sometimes (e.g. here and here), the binding is made two-way, sometimes (e.g. here and here) it's a one-way binding. I don't see the point in making this a one-way-binding (if the tree view UI somehow deselects the item, that change should of course be reflected in the underlying view-model), so I have implemented the two-way version. (The same is usually suggested for IsExpanded, so I have also added a property for that.)

这是我使用的 TreeViewItem 样式:

This is the TreeViewItem style I'm using:

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

我已经确认实际应用了这种样式(如果我添加一个 setter 将 Background 属性设置为 Red,则所有树视图项都会显示为红色背景).

I have confirmed that this style is actually applied (if I add a setter to set the Background property to Red, all the tree view items do appear with a red background).

这里是简化和通用的 DataNode 类:

And here is the simplified and generalized DataNode class:

public class DataNode : INotifyPropertyChanged
{
    public DataNode(DataNode parent, string name)
    {
        this.parent = parent;
        this.name = name;
    }

    private readonly DataNode parent;

    private readonly string name;

    public string Name {
        get {
            return name;
        }
    }

    public override string ToString()
    {
        return name;
    }


    public string FullPath {
        get {
            if (parent != null) {
                return parent.FullPath + "/" + name;
            } else {
                return "/" + name;
            }
        }
    }

    protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        if (PropertyChanged != null) {
            PropertyChanged(this, e);
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private DataNode[] children;

    public IEnumerable<DataNode> Children {
        get {
            if (children == null) {
                children = DataSource.GetChildNodes(FullPath).Select(s => new DataNode(this, s)).ToArray();
            }

            return children;
        }
    }

    private bool isSelected;

    public bool IsSelected {
        get {
            return isSelected;
        }
        set {
            if (isSelected != value) {
                isSelected = value;
                OnPropertyChanged(new PropertyChangedEventArgs("IsSelected"));
            }
        }
    }

    private bool isExpanded;

    public bool IsExpanded {
        get {
            return isExpanded;
        }
        set {
            if (isExpanded != value) {
                isExpanded = value;
                OnPropertyChanged(new PropertyChangedEventArgs("IsExpanded"));
            }
        }
    }

    public void ExpandPath()
    {
        if (parent != null) {
            parent.ExpandPath();
        }
        IsExpanded = true;
    }
}

如您所见,每个节点都有一个名称,对其父节点(如果有)的引用,它会延迟初始化其子节点,但只初始化一次,并且它有一个 IsSelected 和一个IsExpanded 属性,这两个属性都会从 INotifyPropertyChanged 接口触发 PropertyChanged 事件.

As you can see, each node has a name, a reference to its parent node (if any), it initializes its child nodes lazily, but only once, and it has an IsSelected and an IsExpanded property, both of which trigger the PropertyChanged event from the INotifyPropertyChanged interface.

因此,在我的视图模型中,SelectedPath 属性实现如下:

So, in my view-model, the SelectedPath property is implemented as follows:

    public string SelectedPath {
        get {
            return selectedPath;
        }
        set {
            if (selectedPath != value) {
                DataNode prevSel = NodeByPath(selectedPath);
                if (prevSel != null) {
                    prevSel.IsSelected = false;
                }

                selectedPath = value;

                DataNode newSel = NodeByPath(selectedPath);
                if (newSel != null) {
                    newSel.ExpandPath();
                    newSel.IsSelected = true;
                }

                OnPropertyChanged(new PropertyChangedEventArgs("SelectedPath"));
            }
        }
    }

NodeByPath 方法正确(我已经检查过)检索任何给定路径字符串的 DataNode 实例.尽管如此,当将 TextBox 绑定到视图模型的 SelectedPath 属性时,我可以运行我的应用程序并看到以下行为:

The NodeByPath method correctly (I've checked this) retrieves the DataNode instance for any given path string. Nonetheless, I can run my application and see the following behavior, when binding a TextBox to the SelectedPath property of the view-model:

  • type /0 => item /0 被选中并展开
  • type /0/1/2 => 项目 /0 保持选中状态,但项目 /0/1/2 被展开.
  • type /0 => item /0 is selected and expanded
  • type /0/1/2 => item /0 remains selected, but item /0/1/2 gets expanded.

同样,当我第一次将所选路径设置为 /0/1 时,该项目会被正确选择和展开,但对于任何后续路径值,这些项目只会被展开,从未被选中.

Similarly, when I first set the selected path to /0/1, that item gets correctly selected and expanded, but for any subsequent path values, the items only get expanded, never selected.

调试一段时间后,我认为问题是prevSel.IsSelected = false;SelectedPath setter的递归调用行,但是添加一个标志来阻止在执行该命令时执行 setter 代码似乎根本没有改变程序的行为.

After debugging for a while, I thought the problem was a recursive call of the SelectedPath setter in the prevSel.IsSelected = false; line, but adding a flag that would prevent the execution of the setter code while that command is being executed did not seem to change the behaviour of the programme at all.

那么,我在这里做错了什么?我看不出我在做什么与所有这些博客文章中建议的不同.是否需要以某种方式通知 TreeView 新选定项的新 IsSelected 值?

So, what am I doing wrong here? I don't see where I'm doing something different than what is suggested in all of those blogposts. Does the TreeView need to be notified somehow about the new IsSelected value of the newly selected item?

为方便起见,构成自包含的最小示例的所有 5 个文件的完整代码(在此示例中数据源显然返回虚假数据,但它返回一个常量树,因此使上述测试用例可重现):

For your convencience, the full code of all 5 files that constitute the self-contained, minimal example (the data source obviously returns bogus data in this example, yet it returns a constant tree and hence makes the test cases indicated above reproducible):

DataNode.cs

using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.Linq;

namespace TreeViewTest
{
    public class DataNode : INotifyPropertyChanged
    {
        public DataNode(DataNode parent, string name)
        {
            this.parent = parent;
            this.name = name;
        }

        private readonly DataNode parent;

        private readonly string name;

        public string Name {
            get {
                return name;
            }
        }

        public override string ToString()
        {
            return name;
        }


        public string FullPath {
            get {
                if (parent != null) {
                    return parent.FullPath + "/" + name;
                } else {
                    return "/" + name;
                }
            }
        }

        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            if (PropertyChanged != null) {
                PropertyChanged(this, e);
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private DataNode[] children;

        public IEnumerable<DataNode> Children {
            get {
                if (children == null) {
                    children = DataSource.GetChildNodes(FullPath).Select(s => new DataNode(this, s)).ToArray();
                }

                return children;
            }
        }

        private bool isSelected;

        public bool IsSelected {
            get {
                return isSelected;
            }
            set {
                if (isSelected != value) {
                    isSelected = value;
                    OnPropertyChanged(new PropertyChangedEventArgs("IsSelected"));
                }
            }
        }

        private bool isExpanded;

        public bool IsExpanded {
            get {
                return isExpanded;
            }
            set {
                if (isExpanded != value) {
                    isExpanded = value;
                    OnPropertyChanged(new PropertyChangedEventArgs("IsExpanded"));
                }
            }
        }

        public void ExpandPath()
        {
            if (parent != null) {
                parent.ExpandPath();
            }
            IsExpanded = true;
        }
    }
}


DataSource.cs

using System;
using System.Collections.Generic;

namespace TreeViewTest
{
    public static class DataSource
    {
        public static IEnumerable<string> GetChildNodes(string path)
        {
            if (path.Length < 40) {
                for (int i = 0; i < path.Length + 2; i++) {
                    yield return (2 * i).ToString();
                    yield return (2 * i + 1).ToString();
                }
            }
        }
    }
}


ViewModel.cs

using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.Linq;

namespace TreeViewTest
{
    public class ViewModel : INotifyPropertyChanged
    {
        private readonly DataNode[] rootNodes = DataSource.GetChildNodes("").Select(s => new DataNode(null, s)).ToArray();

        public IEnumerable<DataNode> RootNodes {
            get {
                return rootNodes;
            }
        }

        private DataNode NodeByPath(string path)
        {
            if (path == null) {
                return null;
            } else {
                string[] levels = selectedPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
                IEnumerable<DataNode> currentAvailable = rootNodes;
                for (int i = 0; i < levels.Length; i++) {
                    string node = levels[i];
                    foreach (DataNode next in currentAvailable) {
                        if (next.Name == node) {
                            if (i == levels.Length - 1) {
                                return next;
                            } else {
                                currentAvailable = next.Children;
                            }
                            break;
                        }
                    }
                }

                return null;
            }
        }

        private string selectedPath;

        public string SelectedPath {
            get {
                return selectedPath;
            }
            set {
                if (selectedPath != value) {
                    DataNode prevSel = NodeByPath(selectedPath);
                    if (prevSel != null) {
                        prevSel.IsSelected = false;
                    }

                    selectedPath = value;

                    DataNode newSel = NodeByPath(selectedPath);
                    if (newSel != null) {
                        newSel.ExpandPath();
                        newSel.IsSelected = true;
                    }

                    OnPropertyChanged(new PropertyChangedEventArgs("SelectedPath"));
                }
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            if (PropertyChanged != null) {
                PropertyChanged(this, e);
            }
        }
    }
}


Window1.xaml

<Window x:Class="TreeViewTest.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="TreeViewTest" Height="450" Width="600"
    >
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <TreeView ItemsSource="{Binding RootNodes}" SelectedItemChanged="TreeView_SelectedItemChanged">
            <TreeView.Resources>
                <Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource {x:Type TreeViewItem}}">
                    <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
                    <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
                </Style>
            </TreeView.Resources>
            <TreeView.ItemTemplate>
                <HierarchicalDataTemplate ItemsSource="{Binding Children}">
                    <TextBlock Text="{Binding .}"/>
                </HierarchicalDataTemplate>
            </TreeView.ItemTemplate>
        </TreeView>
        <TextBox Grid.Row="1" Text="{Binding SelectedPath, Mode=TwoWay}"/>
    </Grid>
</Window>


Window1.xaml.cs

using System;
using System.Windows;

namespace TreeViewTest
{
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();

            DataContext = vm;
        }

        void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
        {
            DataNode selNode = e.NewValue as DataNode;
            if (selNode == null) {
                vm.SelectedPath = null;
            } else {
                vm.SelectedPath = selNode.FullPath;
            }
        }

        private readonly ViewModel vm = new ViewModel();
    }
}

推荐答案

我无法重现您描述的行为.您发布的与 TreeView 无关的代码存在问题.TextBox 默认 UpdateSourceTrigger 是 LostFocus 因此 TreeView 仅在 TextBox 失去焦点后才会受到影响,但您的示例中只有两个控件,因此要使 TextBox 失去焦点,您必须在 TreeView 中选择某些内容(然后整个选择过程就搞砸了).

I could not reproduce the behavior you described. There is a problem with the code you posted unrelated to TreeView. The TextBox default UpdateSourceTrigger is LostFocus therefore the TreeView is affected only after the TextBox loses focus but there are only two controls in your example so to make the TextBox lose focus you have to select something in the TreeView (then the entire selection process is messed up).

我所做的是在表单底部添加一个按钮.该按钮什么都不做,但在单击 TextBox 时会失去焦点.现在一切正常.

What I did was to add a button at the bottom of the form. The button does nothing but when clicked the TextBox loses focus. Everything works perfectly now.

我使用 .Net 4.5 在 VS2012 中编译它

I compiled it in VS2012 using .Net 4.5

这篇关于将 SelectedPath 属性与 WPF 的 TreeView 中的 SelectedItem 同步的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆