【发布时间】: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 问题(例如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 属性,两者都从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