0

I try to apply MVVM for treeview by refer to Josh's tutorial https://www.codeproject.com/Articles/26288/Simplifying-the-WPF-TreeView-by-Using-the-ViewMode

Here is my full source code

TreeNode.cs

public class TreeNode
{
    private ObservableCollection<TreeNode> _children = new ObservableCollection<TreeNode>();

    public ObservableCollection<TreeNode> Children
    {
        get { return _children; }
        set { _children = value; }
    }

    public string Name { get; set; }

    public string ID { get; set; }

}

TreeNodeViewModel.cs

public class TreeNodeViewModel : INotifyPropertyChanged
{
    private ObservableCollection<TreeNodeViewModel> _children;
    private TreeNodeViewModel _seletected;
    private TreeNodeViewModel _parent;
    private TreeNode _node;

    bool _isExpanded;
    bool _isSelected;



    public TreeNodeViewModel(TreeNode node)
        : this(node, null)
    {
    }

    private TreeNodeViewModel(TreeNode node, TreeNodeViewModel parent)
    {
        _node = node;
        _parent = parent;

        _children = new ObservableCollection<TreeNodeViewModel>(
                (from child in _node.Children
                 select new TreeNodeViewModel(child, this))
                 .ToList<TreeNodeViewModel>());
    }

    public ObservableCollection<TreeNodeViewModel> Children
    {
        get { return _children; }
        set { _children = value; }
    }

    public string Name
    {
        get { return _node.Name; }
        set { _node.Name = value; }
    }
    public TreeNodeViewModel Selected
    {
        get { return _seletected; }
        set { _seletected = value; }
    }


    public bool IsExpanded
    {
        get { return _isExpanded; }
        set
        {
            if (value != _isExpanded)
            {
                _isExpanded = value;
                this.OnPropertyChanged("IsExpanded");
            }

            // Expand all the way up to the root.
            if (_isExpanded && _parent != null)
                _parent.IsExpanded = true;
        }
    }

    public bool IsSelected
    {
        get { return _isSelected; }
        set
        {
            if (value != _isSelected)
            {
                _isSelected = value;
                this.OnPropertyChanged("IsSelected");
                if (_isSelected) { _seletected = this; }
            }

        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

TreeViewViewModel.cs

public class TreeViewViewModel
{
    readonly ObservableCollection<TreeNodeViewModel> _firstLevel;
    readonly TreeNodeViewModel _rootNode;

    private ICommand _addCommand;

    public TreeViewViewModel(TreeNode rootNode)
    {
        _rootNode = new TreeNodeViewModel(rootNode);

        _firstLevel = new ObservableCollection<TreeNodeViewModel>(_rootNode.Children);

        _addCommand = new AddCommand(this);
    }

    public ObservableCollection<TreeNodeViewModel> FirstLevel
    {
        get { return _firstLevel; }
    }

    public ICommand AddCommand
    {
        get { return _addCommand; }
    }
}

AddCommand.cs

public class AddCommand : ICommand
{
    private TreeViewViewModel _TreeView;

    public AddCommand(TreeViewViewModel treeView)
    {
        _TreeView = treeView;
    }

    public event EventHandler CanExecuteChanged;

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public void Execute(object parameter)
    {
        //Show Selected item ????
        MessageBox.Show(_TreeView.????);




    }
}

My goal is when I click to add command, It will show selected item name and parent selected item name. But the problem is there is no anything in TreeViewViewModel to access to TreeNodeViewModel.

It's was prevent by middle class TreeViewViewModel to access to TreeNodeViewModel's property when live in Josh's world

Bruce
  • 519
  • 6
  • 23
  • "show" - in the view that binds to the TreeViewViewModel? – asaf92 Jun 12 '20 at 10:59
  • BTW your TreeViewViewModel (bad name, should simply be TreeViewModel) doesn't have a SelectedItem property – asaf92 Jun 12 '20 at 11:00
  • I created a property like a SelectedItem in TreeViewViewModel but then how to I handle it? Because selected item is in the TreeNodeViewModel, So I can't access to it – Bruce Jun 12 '20 at 11:07
  • You said that you have a `SelectedItem` property is in the tree VM but you can't access it because it's in the TreeNode VM? The command is in the tree VM, and the tree VM is passed to the command class (which shouldn't exist but that's a whole other issue), so you can access it from the command execution method. – asaf92 Jun 12 '20 at 11:25
  • @asaf92 I still not understand your ideal. How can I access to selected item from treeVM. I can add attribute SelectedNode but how can it refect it to selected node in TReeNode VM – Bruce Jun 12 '20 at 11:30
  • You can do it many ways, one of them is the way @GazTheDestroyer suggested. Another would be to have the TreeViewViewModel subscribe to events on every node that is fired by the nodes when the selection status changes. It doesn't really matter. – asaf92 Jun 12 '20 at 11:34
  • I would probably use a behavior to set an attached dependency property, bind that to a property in the window viewmodel. https://stackoverflow.com/questions/1000040/data-binding-to-selecteditem-in-a-wpf-treeview – Andy Jun 12 '20 at 14:05

3 Answers3

1

As per my comment on your previous question. Add a SelectedNode to TreeViewViewModel and set it in your Node Selected setter:

private TreeNodeViewModel(TreeNode node, TreeNodeViewModel parent, TreeViewViewModel root)
{
    _node = node;
    _parent = parent;  
    _root = root;
    //snip rest
}

public bool IsSelected
{
    get { return _isSelected; }
    set
    {
        if (value != _isSelected)
        {
            _isSelected = value;
            this.OnPropertyChanged("IsSelected");
            if (_isSelected) { _root.SelectedNode = this; }
        }

    }
}
GazTheDestroyer
  • 20,722
  • 9
  • 70
  • 103
  • How can I pass root to third parameter constructor? From the TreeViewViewModel, We just only run into by public TreeNodeViewModel(TreeNode node), There is no parameter for root node, if I change signature of this parameter, I got a lot of issue – Bruce Jun 12 '20 at 11:21
  • TreeViewViewModel root what exactly root here, root here should be TreeNodeViewModel, if it's a TreeNodeViewModel, no SelectedNode. If it's a TreeViewViewModel, there is no root (we only has public TreeNodeViewModel _rootNode;) – Bruce Jun 12 '20 at 11:29
  • You need to change the signature of the constructor and pass the TreeViewViewModel as a dependency in order for this to work. What do you mean by "a lot of issues"? – asaf92 Jun 12 '20 at 11:35
  • Ignore the naming. It should be "tree" instead of "root" – asaf92 Jun 12 '20 at 11:35
  • i mean which is a root to put to constructor from treeviewviewmodel, there only onething to put to this constructor is TreeNodeViewmodel _root but it a treenode, not treeview – Bruce Jun 12 '20 at 12:14
  • Hi gazthedestroyer, can you help give me more detail about third parameter in constructor, what should i put to this parameter from treeviewviewmodel – Bruce Jun 12 '20 at 14:15
0

If you are concerned with you application's performance, then always implement INotifyPropertyChanged on your binding source - even if you are not raising the PropertyChanged event (because the property won't change).

In your case, TreeNode serves as a binding source for the TreeView. Therefore TreeNode should implement INotifyPropertyChanged.

Since IsSelected and IsExpanded are definitely attributes of a node, they should be members of TreeNode. This makes TreeNodeViewModel redundant. Removing TreeNodeViewModel will also remove the duplicate properties e.g., Children or Name and their awkward initialization.
For this reason I merged both classes using only the TreeNode and the TreeViewModel in my examples.

Also check the proper implementation of the ICommand.CanExecuteChanged event. I have fixed it in my examples.

Instead of passing string values of members or types to methods use nameof. I also fixed this where you invoke OnPropertyChanged. For the most optimal implementation of the OnPropertyChanged event invocator see Microsoft Docs: INotifyPropertyChanged.PropertyChanged.

The manipulation of the tree e.g., add/remove items to/from the tree, should always happen centralized in the class that manages the tree e.g., TreeViewModel. I've implemented corresponding methods in this class. The commands now call this methods to manipulate the tree.

The solution to your problem is to add a SelectedNode property to the class that manages the tree, which is in your case the TreeViewModel. The TreeViewModel listens to a SelctedChanged event of the TreeNode to update the TreeViewModel.SelectedNode property.

TreeNode.cs

public class TreeNode : INotifyPropertyChanged
{
    public event EventHandler SelectedChanged;
    public event PropertyChangedEventHandler PropertyChanged;

    private ObservableCollection<TreeNode> _children;
    private TreeNode _parent;
    private TreeNode _selectedTreeNode;
    private string _name;
    private string _id;
    private bool _isExpanded;
    private bool _isSelected;

    public TreeNode() => this(null);

    public TreeNode(TreeNode parent)
    {
        _parent = parent;    
        _children = new ObservableCollection<TreeNodeViewModel>();
    }

    public ObservableCollection<TreeNode> Children
    {
        get => _children; 
        set
        {
            if (value != _children)
            {
                _children = value;
                OnPropertyChanged(nameof(this.Children));
            }
        }
    }

    public TreeNode Parent
    {
        get => _parent; 
        set
        {
            if (value != _parent)
            {
                _parent = value;
                OnPropertyChanged(nameof(this.Parent));
            }
        }
    }

    public string Name
    {
        get => _name; 
        set
        {
            if (value != _name)
            {
                _name = value;
                OnPropertyChanged(nameof(this.Name));
            }
        }
    }

    public string Id
    {
        get => _id; 
        set
        {
            if (value != _id)
            {
                _id = value;
                OnPropertyChanged(nameof(this.Id));
            }
        }
    }

    public bool IsExpanded
    {
        get => _isExpanded; 
        set
        {
            if (value != _isExpanded)
            {
                _isExpanded = value;
                OnPropertyChanged(nameof(this.IsExpanded));
            }

            // Expand all the way up to the root.
            if (_isExpanded && _parent != null)
                _parent.IsExpanded = true;
        }
    }

    public bool IsSelected
    {
        get => _isSelected; 
        set
        {
            if (value != _isSelected)
            {
                _isSelected = value;
                OnPropertyChanged(nameof(this.IsSelected));

                OnSelectionChanged();
            }
        }
    }

    private void OnSelectionChanged()
    {
        this.SelectedChanged?.Invoke(this, EventArgs.Empty);
    }

    private void OnPropertyChanged(string propertyName)
    {
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

TreeViewModel.cs

public class TreeViewModel : INotifyPropertyChanged
{
    private readonly ObservableCollection<TreeNode> _firstLevel;
    private readonly TreeNode _rootNode;    
    private TreeNode _selectedTreeNode;
    private ICommand _addCommand;
    private ICommand _removeCommand;

    public TreeViewModel(TreeNode rootNode)
    {
        _rootNode = rootNode;

        _firstLevel = new ObservableCollection<TreeNode>(_rootNode.Children);

        _addCommand = new AddCommand(this);
        _removeCommand = new RemoveCommand(this);
    }

    public void AddNode(TreeNode treeNode)
    {
        treeNode.SelectedChanged += OnTreeNodeSelectedChanged;
        this.FirstLevel.Add(treeNode);
    }

    public void RemoveNode(TreeNode treeNode)
    {
        treeNode.SelectedChanged -= OnTreeNodeSelectedChanged;
        this.FirstLevel.Remove(treeNode);
    }

    public void OnTreeNodeSelectedChanged(object sender, EventArgs e)
    {
        var treeNode = sender as TreeNode;
        if (treeNode.isSelected)
        {
            this.SelectedNode = treeNode;
        }
    }

    private void OnPropertyChanged(string propertyName)
    {
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    public TreeNode RootNode => _rootNode; 

    public TreeNode SelectedNode
    {
        get => _selectedNode; 
        set
        {
            if (value != _selectedNode)
            {
                _selectedNode = value;
                OnPropertyChanged(nameof(this.SelectedNode));
            }
        }
    }

    public ObservableCollection<TreeNode> FirstLevel => _firstLevel; 

    public ICommand AddCommand => _addCommand;     

    public ICommand RemoveCommand => _removeCommand; 
}

AddCommand.cs

public class AddCommand : ICommand
{
    private TreeViewModel _tree;

    public AddCommand(TreeViewModel tree)
    {
        _tree = tree;
    }

    public event EventHandler CanExecuteChanged
    {
        add => CommandManager.RequerySuggested += value;
        remove => CommandManager.RequerySuggested -= value;
    }

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public void Execute(object parameter)
    {
        _tree.AddNode(new TreeNode(_tree.RootNode));

        //Show Selected item's name
        MessageBox.Show(_tree.SelectedNode.Name);
    }
}

RemoveCommand.cs

public class AddCommand : ICommand
{
    private TreeViewModel _tree;

    public AddCommand(TreeViewModel tree)
    {
        _tree = tree;
    }

    public event EventHandler CanExecuteChanged
    {
        add => CommandManager.RequerySuggested += value;
        remove => CommandManager.RequerySuggested -= value;
    }

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public void Execute(object parameter)
    {
        _tree.RemoveNode(parameter as TreeNode);
    }
}
BionicCode
  • 1
  • 4
  • 28
  • 44
0

The MVVM pure methodology for binding to the selected item in a TreeView is using a Behavior class

public class perTreeViewHelper : Behavior<TreeView>
{
    public object BoundSelectedItem
    {
        get => GetValue(BoundSelectedItemProperty);
        set => SetValue(BoundSelectedItemProperty, value);
    }

    public static readonly DependencyProperty BoundSelectedItemProperty =
        DependencyProperty.Register("BoundSelectedItem",
            typeof(object),
            typeof(perTreeViewHelper),
            new FrameworkPropertyMetadata(null,
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                OnBoundSelectedItemChanged));

    private static void OnBoundSelectedItemChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
    {
        if (args.NewValue is perTreeViewItemViewModelBase item)
        {
            item.IsSelected = true;
        }
    }

    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged;
    }

    protected override void OnDetaching()
    {
        AssociatedObject.SelectedItemChanged -= OnTreeViewSelectedItemChanged;
        base.OnDetaching();
    }

    private void OnTreeViewSelectedItemChanged(object obj, RoutedPropertyChangedEventArgs<object> args)
    {
        BoundSelectedItem = args.NewValue;
    }
}

More details of its usage on my blog post.

Peregrine
  • 4,287
  • 3
  • 17
  • 34