我正在尝试创建一个SelectedPath
与 WPF 同步的属性(例如在我的视图模型中)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
属性是只读的,所以不能直接设置。我发现并阅读了许多深入讨论此问题的 SO 问题(例如this、this或this),以及其他站点上的资源,例如this blogpost、this article或this blogpost。
几乎所有这些资源都指向定义一种样式,将's 的属性TreeViewItem
绑定到视图模型中底层树节点对象的等效属性。有时(例如here和here),绑定是双向的,有时(例如here和here)它是单向绑定。我没有看到将其设为单向绑定的意义(如果树视图 UI 以某种方式取消选择该项目,则该更改当然应该反映在底层视图模型中),因此我已经实现了双向绑定版本。(通常建议使用相同的方法,因此我还为此添加了一个属性。)TreeViewItem
IsSelected
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
任何给定路径字符串的实例。尽管如此,当将 a 绑定TextBox
到SelectedPath
视图模型的属性时,我可以运行我的应用程序并看到以下行为:
- type
/0
=> item/0
被选中并展开 - type
/0/1/2
=> item/0
保持选中状态,但 item/0/1/2
被扩展。
同样,当我第一次将选定路径设置为 时/0/1
,该项目会被正确选择和扩展,但对于任何后续路径值,项目只会被扩展,从未被选中。
调试了一段时间后,我认为SelectedPath
问题是该行中setter的递归调用prevSel.IsSelected = false;
,但是添加一个标志会阻止在执行该命令时执行setter代码似乎并没有改变程序的行为一点也不。
那么,我在这里做错了什么?我看不出我在哪里做的事情与所有这些博文中的建议不同。是否需要以某种方式通知 TreeViewIsSelected
新选择项的新值?
为了您的方便,构成独立的最小示例的所有 5 个文件的完整代码(在此示例中数据源显然返回虚假数据,但它返回一个常量树,因此使上述测试用例可重现):
数据节点.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;
}
}
}
数据源.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();
}
}
}
}
}
视图模型.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();
}
}