0

I have a TreeView in my View that is databound to a list of root Nodes in my ViewModel. Those root Nodes can have child Nodes. All nodes are of the same type and have the property IsSelected that is bound to the IsChecked dependency property of a CheckBox that's contained in the respective TreeViewItem. That CheckBox has set IsThreeState to false.

public class Node : PropertyChangedBase, INode
{
    private bool? _isSelected;
    private IList<INode> _nodes;
    private INode _parent;

    public Node()
    { }

    public bool? IsSelected
    {
        get { return _isSelected; }
        set
        {
            if (_SetField(ref _isSelected, value))
            {
                _OnIsSelectedChanged();
            }
        }
    }
    public IList<INode> Nodes
    {
        get { return _nodes; }
        set { _SetField(ref _nodes, value); }
    }
    public INode Parent
    {
        get { return _parent; }
        set { _SetField(ref _parent, value); }
    }

    private void _OnIsSelectedChanged()
    {
        if (IsSelected.HasValue)
        {
            if (IsSelected.Value)
            {
                if (Parent != null)
                {
                    // Set IsSelected on all parenting nodes to:
                    //  - true, if all of their immediate child packages have been selected
                    //  - null, else
                }

                if (Nodes != null && Nodes.Count > 0)
                {
                    // Prevent running this method again by circumventing setting the property
                    _SetField(ref _isSelected, null);
                }
            }
            else
            {
                if (Parent != null)
                {
                    // Set IsSelected of the parent to null
                }

                if (Nodes != null)
                {
                    // Set IsSelected = false on all child nodes
                }
            }
        }
        else if (Parent != null)
        {
            // Set IsSelected on all parenting nodes to:
            //  - true, if all of their immediate child packages have been selected
            //  - null, else
        }
    }
}

PropertyChangedBase is a base class implementing INotifyPropertyChanged. It's been designed after this SO answer. If the set value actually changes, _SetField(ref object, object) returns true and notifies about the property change.

If the user clicks a CheckBox, that change should propagate the parent node's (up to the root node) IsSelected property and to the child node's IsSelected property, too. After the propagation of all Properties finished, I want to fire an event. But only when no further node will be changed. I then want to do something in the ViewModel, that takes some time, so it would be bad performance-wise if the event would fire with each changed property.

The behaviour should be the following:

  • If a node's IsSelected gets set to true or null, the parent node's IsSelected gets set to null if not all of the node's sibling's IsSelected are set to true or null (what then propagates up the tree).
  • If a node's IsSelected gets set to true or null, the parent node's IsSelected gets set to true if all of the node's sibling's IsSelected are set to true or null (what then propagates up the tree).
  • If a node's IsSelected gets set to false, all of its immediate child node's IsSelected get set to false, too (what then propagates down the tree).
  • A node set to null means that not all of its immediate child nodes have been selected.

So how can I achieve firing the PropertyChanged event (or another I'd implement) only after the last node has been changed?

Tim
  • 157
  • 1
  • 11
  • 1
    The simplest pseudocode I can think of to explain the concept of what you want to achieve is: doStuffRecursive(); fireEvent();. The trouble is I can't think of a way to "detect" whenStuffRecursiveFinished. I would have thought of some sort of count (know how many nodes you are supposed to load for a given request) that you decrement with Interlocked.Decrement() and when 0. But I believe this would get quite complicated fast and there are better ways through WPF mechanisms – Alexandru Clonțea Sep 21 '18 at 19:51
  • @AlexandruClonțea of course. DoStuffRecursive then fireEvent. If I have a public method on my `Node` that sets `IsSelected` without calling `_OnIsSelectedChanged`, I can change all nodes in one method firing the event afterwards. I'd have to lock, though, to prevent calling `_OnIsSelectedChanged` twice at the same time. I'll try that. – Tim Sep 22 '18 at 10:18
  • 1
    @AlexandruClonțea thank you for the inspiration. I ended up doing exactly this: raise `IsSelectedChangedPropagationStarted`, perform the propagation, raise `IsSelectedChangedPropagationCompleted`. Works like a charme. I can't believe, I didn't came up with this by myself. Sometimes you just need a little push in the right direction. Thanks a lot again. – Tim Sep 30 '18 at 17:09
  • Glad to hear it! If you can, add a more complete answer, it might be helpful for other people in the future! Once gain, congratulations! – Alexandru Clonțea Oct 01 '18 at 08:27

1 Answers1

1

I ended up doing what Alexandru suggested.

I introduced two events IsSelectedChangedPropagationStarted & IsSelectedChangedPropagationCompleted the first being raised before handling the selection and the latter being raised upon completion. The class looks similar to this now:

public class Node : PropertyChangedBase, INode
{
    // #### Attributes
    private bool? _isSelected;
    private IList<INode> _nodes;
    private INode _parent;

    // #### Constructor
    public Node()
    { }

    // #### Properties
    public bool? IsSelected
    {
        get { return _isSelected; }
        set
        {
            if (_SetField(ref _isSelected, value))
            {
                _OnIsSelectedChanged();
            }
        }
    }
    public IList<INode> Nodes
    {
        get { return _nodes; }
        set { _SetField(ref _nodes, value); }
    }
    public INode Parent
    {
        get { return _parent; }
        set { _SetField(ref _parent, value); }
    }

    // #### Events
    public event EventHandler IsSelectedChangedPropagationStarted;
    public event EventHandler IsSelectedChangedPropagationCompleted;

    // #### Instance Methods
    private void _OnIsSelectedChanged()
    {
        IsSelectedChangedPropagationStarted?.Invoke(this, EventArgs.Empty);

        if (IsSelected.HasValue)
        {
            if (IsSelected.Value)
            {
                RecursivelySetAllParents();

                if (Nodes != null && Nodes.Count > 0)
                {
                    // Prevent running this method again by circumventing setting the property
                    _SetField(ref _isSelected, null);
                }
            }
            else
            {
                if (Parent != null)
                {
                    // Set IsSelected of the parent to null
                }

                RecursivelySetAllChildren();
            }
        }
        else if (Parent != null)
        {
            // Set IsSelected on all parenting nodes to:
            //  - true, if all of their immediate child packages have been selected
            //  - null, else
        }

        IsSelectedChangedPropagationCompleted?.Invoke(this, EventArgs.Empty);
    }
}

By implementing custom EventArgs I could also tell the listeners if something at all has changed so they can act accordingly.

Tim
  • 157
  • 1
  • 11