将SelectedPath属性与WPF的TreeView中的SelectedItem同步

时间:2012-12-08 14:42:18

标签: wpf treeview selecteditem

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

  • 每当更改树视图中的选定项(SelectedItem属性/ SelectedItemChanged事件)时,更新SelectedPath属性以存储表示所选树的整个路径的字符串节点
  • 每当更改SelectedPath属性时,找到路径字符串指示的树节点,展开该树节点的整个路径,并在取消选择先前选择的节点后选择它。

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

更新SelectedPath事件中的SelectedItemChange属性不是问题 - 以下事件处理程序可以正常运行:

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尊重我对程序选择的项目?

现在,我到底有多远?首先,TreeView的SelectedItem property是只读的,因此无法直接设置。我已经找到并阅读了许多深入讨论此问题的SO问题(例如thisthisthis),以及其他网站上的资源,例如this blogpostthis articlethis blogpost

几乎所有这些资源都指向TreeViewItem定义一种样式,它将TreeViewItem的{​​{1}}属性绑定到视图模型中基础树节点对象的等效属性。有时(例如herehere),绑定是双向的,有时(例如herehere)它是单向绑定。我没有看到使这个单向绑定的意义(如果树视图UI以某种方式取消选择项目,那个更改当然应该反映在底层视图模型中),所以我实现了双向版。 (IsSelected通常建议使用相同的内容,因此我也为此添加了一个属性。)

这是我正在使用的IsExpanded样式:

TreeViewItem

我已确认实际应用了此样式(如果我添加了一个setter来将<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> 属性设置为Background,则所有树视图项都会显示为红色背景。

这是简化的和通用的Red类:

DataNode

如您所见,每个节点都有一个名称,对其父节点的引用(如果有的话),它懒惰地初始化其子节点,但只有一次,它有一个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; } } 和一个{{1 }}属性,它们都从IsSelected接口触发IsExpanded事件。

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

INotifyPropertyChanged

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")); } } } 实例。尽管如此,我可以运行我的应用程序,并在将NodeByPath绑定到视图模型的DataNode属性时看到以下行为:

  • type TextBox =&gt;项目SelectedPath已选中并展开
  • type /0 =&gt;项目/0仍处于选中状态,但项目/0/1/2已展开。

同样,当我首先将所选路径设置为/0时,该项目会被正确选择和展开,但对于任何后续路径值,这些项目只会展开,永远不会被选中。

调试一段时间后,我认为问题是/0/1/2行中/0/1 setter的递归调用,但添加了一个阻止执行的标志执行该命令时的setter代码似乎根本没有改变程序的行为。

所以,我在这里做错了什么?我没有看到我在做什么不同于所有这些博客中提出的建议。是否需要以某种方式通知TreeView有关新选择项目的新SelectedPath值的信息?

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


DataNode.cs

prevSel.IsSelected = false;

DataSource.cs

IsSelected

ViewModel.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;
        }
    }
}

Window1.xaml

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();
                }
            }
        }
    }
}

Window1.xaml.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);
            }
        }
    }
}

1 个答案:

答案 0 :(得分:1)

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

我所做的是在表单底部添加一个按钮。该按钮不执行任何操作,但单击时TextBox失去焦点。现在一切都很完美。

我使用.Net 4.5

在VS2012中编译了它