将 SelectedPath 属性与 WPF 的 TreeView 中的 SelectedItem 同步 [英] Synchronizing a SelectedPath property with the SelectedItem in WPF's TreeView
问题描述
我正在尝试创建一个与 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 theSelectedPath
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
定义一个样式,该样式将 TreeViewItem
的 IsSelected
属性绑定到底层的等效属性视图模型中的树节点对象.有时(例如此处和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屋!