我正在尝试创建与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
的{{1}}属性绑定到视图模型中基础树节点对象的等效属性。有时(例如here和here),绑定是双向的,有时(例如here和here)它是单向绑定。我没有看到使这个单向绑定的意义(如果树视图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
属性时看到以下行为:
TextBox
=&gt;项目SelectedPath
已选中并展开/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);
}
}
}
}
答案 0 :(得分:1)
我无法重现您描述的行为。您发布的与TreeView无关的代码存在问题。 TextBox默认的UpdateSourceTrigger是LostFocus,因此TreeView只有在TextBox失去焦点后才会受到影响,但是你的示例中只有两个控件,所以要让TextBox失去焦点,你必须在TreeView中选择一些东西(然后整个选择过程搞砸了) )。
我所做的是在表单底部添加一个按钮。该按钮不执行任何操作,但单击时TextBox失去焦点。现在一切都很完美。
我使用.Net 4.5
在VS2012中编译了它