Синхронизация свойства SelectedPath с SelectedItem в WPF TreeView

Я пытаюсь создатьSelectedPath свойство (например, в моей модели представления), которое синхронизируется с WPFTreeView, Теория заключается в следующем:

Всякий раз, когда выбранный элемент в древовидном представлении изменяется (SelectedItem имущество/SelectedItemChanged событие), обновитеSelectedPath свойство для хранения строки, представляющей весь путь к выбранному узлу дерева.Всякий раз, когдаSelectedPath изменено свойство, найдите узел дерева, указанный строкой пути, разверните весь путь до этого узла дерева и выберите его после отмены выбора ранее выбранного узла.

Чтобы сделать все это воспроизводимым, предположим, что все узлы дерева имеют типDataNode (см. ниже), что у каждого узла дерева есть имя, которое является уникальным среди дочерних элементов его родительского узла, и что разделитель пути представляет собой одну прямую косую черту./

ОбновлениеSelectedPath недвижимость вSelectedItemChange Событие не является проблемой - следующий обработчик событий работает безупречно:

void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs e)
{
    DataNode selNode = e.NewValue as DataNode;
    if (selNode == null) {
        vm.SelectedPath = null;
    } else {
        vm.SelectedPath = selNode.FullPath;
    }
}

Однако я не могу заставить работать наоборот. Следовательно, мой вопрос, основанный на приведенном ниже обобщенном и свернутом примере кода:Как мне сделать WPF 's TreeView уважать мой программный выбор предметов?

Теперь, как далеко я зашел? Прежде всего, TreeViewSelectedItem имущество только для чтения, поэтому его нельзя установить напрямую. Я нашел и прочитал многочисленные вопросы SO, обсуждающие это всесторонне (такие какэтот,этот или жеэтот), а также ресурсы на других сайтах, таких какэтот пост,Эта статья или жеэтот пост

Почти все эти ресурсы указывают на определение стиля дляTreeViewItem это связываетTreeViewItemIsSelected свойство к эквивалентному свойству базового объекта узла дерева из модели представления. Иногда (например,Вот а такжеВот), иногда привязка выполняется в двух направлениях (например,Вот а такжеВот) Это'односторонняя привязка. Я неЯ не вижу смысла в том, чтобы сделать это односторонним связыванием (если пользовательский интерфейс древовидного представления каким-то образом отменяет выбор элемента, это изменение, конечно, должно быть отражено в базовой модели представления), поэтому я реализовал двустороннюю версию. (То же самое обычно предлагается дляIsExpandedпоэтому я также добавил свойство для этого.)

ЭтоTreeViewItem стиль ям с помощью:


    
    

Я подтвердил, что этот стиль действительно применяется (если я добавлю сеттер, чтобы установить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 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 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 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 RootNodes {
            get {
                return rootNodes;
            }
        }

        private DataNode NodeByPath(string path)
        {
            if (path == null) {
                return null;
            } else {
                string[] levels = selectedPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
                IEnumerable 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


    
        
            
            
        

        
            
                
                    
                    
                
            
            
                
                    
                
            
        
        
    

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 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)

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