【问题标题】:Synchronizing a SelectedPath property with the SelectedItem in WPF's TreeView将 SelectedPath 属性与 WPF TreeView 中的 SelectedItem 同步
【发布时间】:2012-11-26 13:00:34
【问题描述】:

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

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

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

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

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 定义一个样式,该样式将TreeViewItemIsSelected 属性绑定到视图模型中底层树节点对象的等效属性。有时(例如herehere),绑定是双向的,有时(例如herehere)它是单向绑定。我没有看到将其作为单向绑定的意义(如果树视图 UI 以某种方式取消选择该项目,那么该更改当然应该反映在底层视图模型中),所以我已经实现了双向绑定版本。 (IsExpanded 通常也建议这样做,所以我也为此添加了一个属性。)

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

<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,所有的树视图项都会以红色背景显示)。

这里是简化和概括的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;
    }
}

如您所见,每个节点都有一个名称,一个对其父节点的引用(如果有的话),它懒惰地初始化它的子节点,但只有一次,它有一个IsSelected 和一个IsExpanded 属性,两者都从INotifyPropertyChanged 接口触发PropertyChanged 事件。

因此,在我的视图模型中,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 实例。尽管如此,当将TextBox 绑定到视图模型的SelectedPath 属性时,我可以运行我的应用程序并看到以下行为:

  • type /0 => item /0 被选中并展开
  • type /0/1/2 => item /0 保持选中状态,但 item /0/1/2 被展开。

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

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

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

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


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

【问题讨论】:

    标签: wpf treeview selecteditem


    【解决方案1】:

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

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

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

    【讨论】:

    • 我通过按 Tab 键使 TextBox 失去焦点,这应该聚焦 TreeView(但没有选择其他任何内容)。将通过一个额外的按钮尝试您的解决方案。
    • 好吧,这似乎奏效了。所以我的选择相关代码完全没有问题。问题只是在更改选择时通过将焦点切换到树视图来更改选择...有趣且出乎意料。谢谢:-)
    猜你喜欢
    • 2013-02-22
    • 2011-11-01
    • 1970-01-01
    • 2010-12-15
    • 2011-09-12
    • 1970-01-01
    • 2013-11-02
    • 1970-01-01
    • 2014-09-05
    相关资源
    最近更新 更多