我试图创建一个与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的问题,并对此进行了深入讨论(例如this,this或this),以及其他站点(例如this blogpost,this article或this blogpost)上的资源。几乎所有这些资源都指向为
TreeViewItem
定义样式,该样式将TreeViewItem
的IsSelected
属性绑定到视图模型中基础树节点对象的等效属性。有时(例如here和here)绑定是双向的,有时(例如here和here)是单向绑定。我不认为这是单向绑定的要点(如果树视图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
属性,两者都从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
实例。尽管如此,将TextBox
绑定到视图模型的SelectedPath
属性时,我仍可以运行我的应用程序并看到以下行为:类型
/0
=>已选择并扩展项目/0
类型
/0/1/2
=>项/0
保持选中状态,但项/0/1/2
被展开。同样,当我第一次将选定的路径设置为
/0/1
时,该项目会被正确选择并展开,但是对于任何后续路径值,这些项目只会被展开,而从未被选中。经过一段时间的调试,我认为问题是
SelectedPath
行中prevSel.IsSelected = false;
setter的递归调用,但是添加一个标志,该标志将阻止执行该命令时执行setter代码,似乎没有完全改变程序的行为。那么,我在这里做错了什么?我看不出我在做什么与所有这些博文中所建议的有所不同。是否需要以某种方式将新选择的项目的新
IsSelected
值通知给TreeView?为方便起见,构成独立的最小示例的所有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;
}
}
}
数据源
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中选择某些内容(然后整个选择过程就搞砸了)。
我要做的是在表单底部添加一个按钮。该按钮什么也不做,但是单击时,TextBox失去焦点。现在一切正常。
我使用.Net 4.5在VS2012中进行了编译
关于wpf - 在WPF的TreeView中将SelectedPath属性与SelectedItem同步,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/13778515/