Синхронизация свойства SelectedPath с SelectedItem в WPF TreeView
Я пытаюсь создатьSelectedPath
свойство (например, в моей модели представления), которое синхронизируется с WPFTreeView
, Теория заключается в следующем:
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 учитывал мой программный выбор элементов?
Теперь, как далеко я зашел? Прежде всего, TreeViewSelectedItem
свойство только для чтения, поэтому его нельзя установить напрямую. Я нашел и прочитал многочисленные вопросы SO, обсуждающие это всесторонне (такие какэто, это или жеэто), а также ресурсы на других сайтах, таких какэтот пост, эта статья или жеэтот пост.
Почти все эти ресурсы указывают на определение стиля дляTreeViewItem
это связываетTreeViewItem
«sIsSelected
свойство к эквивалентному свойству базового объекта узла дерева из модели представления. Иногда (например,Вот а такжеВот), иногда привязка выполняется в двух направлениях (например,Вот а такжеВот) это односторонняя привязка. Я не вижу смысла делать это односторонним связыванием (если пользовательский интерфейс древовидного представления каким-то образом отменяет выбор элемента, это изменение, конечно, должно отражаться в базовой модели представления), поэтому я реализовал двустороннее связывание. версия. (То же самое обычно предлагается для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>
Я подтвердил, что этот стиль действительно применяется (если я добавлю сеттер, чтобы установить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
свойство view-модели:
/0
=> предмет/0
выбран и расширентип/0/1/2
=> предмет/0
остается выбранным, но пункт/0/1/2
расширяется.Точно так же, когда я впервые устанавливаю выбранный путь в/0/1
этот элемент будет правильно выбран и расширен, но для любых последующих значений пути элементы будут только расширяться, а не выделяться.
После некоторой отладки яподумал проблема была в рекурсивном вызовеSelectedPath
сеттер вprevSel.IsSelected = false;
строка, но добавление флага, который бы препятствовал выполнению установочного кода во время выполнения этой команды, похоже, не изменило поведение программы вообще.
Так,что я тут не так делаю? Я не понимаю, где я делаю что-то отличное от того, что предлагается во всех этих постах. Нужно ли как-то уведомлять 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();
}
}