Синхронизация свойства 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();
    }
}

Ответы на вопрос(1)

Ваш ответ на вопрос