0

I have read a few articles on here, that describe how to listen to notifications raised. However: I am still having trouble to apply those to my application.

I currently have an application with several "pages".

One of the pages contains a WPF Treeview control in it along with several ViewModels and data models.

public class FoldersSearchViewModel
{
    private ReadOnlyCollection<DriveTreeViewItemViewModel> _drives;

    public FoldersSearchViewModel(string[] logicalDrives)
    {
        _drives = new ReadOnlyCollection<DriveTreeViewItemViewModel>(
            Environment.GetLogicalDrives()
            .Select(s => new DriveInfo(s))
            .Where(di => di.IsReady)
            .Select(di => new DriveTreeViewItemViewModel(di))
            .ToList()
        );
    }

    public ReadOnlyCollection<DriveTreeViewItemViewModel> Drives
    {
        get { return _drives; }
    }
}

This ViewModel contains DriveTreeViewItemViewModels and is bound via DataContext to the UserControl ("page").

The Drive- and DirectoryTreeViewItemViewModel classes contain a few attributes, but are otherwise based on TreeViewItemViewModel, which you can see here:

public class TreeViewItemViewModel : INotifyPropertyChanged
{
    #region Data

    static readonly protected TreeViewItemViewModel DummyChild = new TreeViewItemViewModel();

    readonly ObservableCollection<TreeViewItemViewModel> _children;
    readonly TreeViewItemViewModel _parent;

    bool _isExpanded;
    bool _isSelected;

    #endregion // Data

    #region Constructors

    protected TreeViewItemViewModel(TreeViewItemViewModel parent, bool lazyLoadChildren)
    {
        _parent = parent;

        _children = new ObservableCollection<TreeViewItemViewModel>();

        if (lazyLoadChildren)
            _children.Add(DummyChild);
    }

    // This is used to create the DummyChild instance.
    private TreeViewItemViewModel()
    {
    }

    #endregion // Constructors

    #region Presentation Members

    #region Children

    /// <summary>
    /// Returns the logical child items of this object.
    /// </summary>
    public ObservableCollection<TreeViewItemViewModel> Children
    {
        get { return _children; }
    }

    #endregion // Children

    #region HasLoadedChildren

    /// <summary>
    /// Returns true if this object's Children have not yet been populated.
    /// </summary>
    public bool HasDummyChild
    {
        get { return this.Children.Count == 1 && this.Children[0] == DummyChild; }
    }

    #endregion // HasLoadedChildren

    #region IsExpanded

    /// <summary>
    /// Gets/sets whether the TreeViewItem 
    /// associated with this object is expanded.
    /// </summary>
    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;

            // Lazy load the child items, if necessary.
            if (this.HasDummyChild)
            {
                this.Children.Remove(DummyChild);
                this.LoadChildren();
            }
        }
    }

    #endregion // IsExpanded

    #region IsSelected

    /// <summary>
    /// Gets/sets whether the TreeViewItem 
    /// associated with this object is selected.
    /// </summary>
    public bool IsSelected
    {
        get { return _isSelected; }
        set
        {
            if (value != _isSelected)
            {
                _isSelected = value;
                this.OnPropertyChanged("IsSelected");
            }
        }
    }

    #endregion // IsSelected

    #region LoadChildren

    /// <summary>
    /// Invoked when the child items need to be loaded on demand.
    /// Subclasses can override this to populate the Children collection.
    /// </summary>
    protected virtual void LoadChildren()
    {
    }

    #endregion // LoadChildren

    #region Parent

    public TreeViewItemViewModel Parent
    {
        get { return _parent; }
    }

    #endregion // Parent

    #endregion // Presentation Members

    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
    {
        if (this.PropertyChanged != null)
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

    #endregion // INotifyPropertyChanged Members
}

I have followed the tutorial and ideas described in http://www.codeproject.com/Articles/26288/Simplifying-the-WPF-TreeView-by-Using-the-ViewMode and everything works great so far.

My problem is: I would like to add a string "selected" as an attribute to FoldersSearchViewModel, which would contain the path of the selected child ViewModel. The DriveTreeViewItemViewModel and the DirectoryTreeViewItemViewModel each have a "Path" property, that contains the full path to the child.

So: once OnPropertyChanged("IsSelected") is called, I would like to notify FoldersSearchViewModel about it and have the method copy the Path-property from the selected TreeViewItemViewModel into the new "selected"(string) attribute.


I could achieve this by passing the FoldersSearchViewModel-object to the children and children's children etc. in the constructor - but is there no better way of doing this? I suppose I should hook the FoldersSearchViewModel to the PropertyChanged-event of every node and sub-node, but I would like to know what someone with experience in MVVM would do in such a case.

By the way: I could use the WPF Treeview.SelectedItem to get the currently selected TreeViewItemViewModel, but that does not sound right since I want to keep the view, the models and the viewmodels separate.

P.s.: I tried reading and making use of MVVM in WPF - How to alert ViewModel of changes in Model... or should I?, but sadly it does not seem to solve my problem.

Any help is greatly appreciate!

Community
  • 1
  • 1
Igor
  • 1,582
  • 6
  • 19
  • 49

2 Answers2

0

The MVVM way to do it would be to use a messenger / event aggregator pattern and broadcast an event.

SledgeHammer
  • 7,338
  • 6
  • 41
  • 86
  • Hm.. so I should create a Messenger class, subscribe FoldersSearchViewModel to receive notifications and have the TreeViewItemViewModel notify all subscribed members when an event is changed? A problem that I currently have, is: if a TreeViewItemViewModel is disposed - where do I handle the unregistring from the Messenger-class? I have read, that the method is not called reliably, which might leave null-objects or - even worse - memory leaks in my application. Do you have a suggestion? Pseudo-code or a scheme would be very helpful. – Igor Jul 23 '15 at 09:39
  • You don't register an event for each item, you just register general events. So take Explorer for example. The tree view pane might register "SelectionChanged", "ItemOpened" and "ItemClosed" events and send an "EventArgs" type class as a param (message specific) Anybody who is interested in those events would subscribe. There really isn't a need to unsubscribe from a messenger event since its not tied to any control, but rather an app wide singleton. If you store references in a "ItemClosed" handler, you're breaking the model :). – SledgeHammer Jul 23 '15 at 15:00
  • Hm.. well the EventMessenger is indeed a singleton. It contains a method to NotifySubscribers and passes the event and the source-object (currently). But in order to know whom to notify, I do have to register the potential recipients, do I not? And if I have to subscribe them to one or more events, I might have to unsubscribe them at some point if the ViewModel is destroyed - or am I wrong? Is there a tutorial or something, that would show how exactly this messenger pattern works? Thanks! :) – Igor Jul 23 '15 at 15:59
0

The way mine works is, the Messenger service is a singleton as discussed. I also use DI, so a VM that needs to use it gets the IMessengerService instance injected into it.

IMessengerService looks like:

public interface IMessengerService : IServiceBase
{
    Message<T> GetMessage<T>() where T : IMessageBase;
}

Message "param" classes are available application wide, so you might have something like:

public class FolderOpened : IMessageBase
{
}

So, the FolderOpened class is available throughout the application, its defined at compile time obviously.

Any client that would care about this message will subscribe to the message in its VM constructor:

_messenger.GetMessage().Handler += ...

It doesn't matter if the sender has "registered" it yet, the messenger is just based on the message class type.

At any time, anybody can send the message:

_messenger.GetMessage().SendMessage(...);

YMMV, but my messenger will automatically disconnect disposed / non existant subscribers, but really, the correct way would be for a VM to unsubscribe in its finalizer or dispose method.

Does that clear it up?

SledgeHammer
  • 7,338
  • 6
  • 41
  • 86
  • Yeah, it does. I am using something similar; however: I did not have message-objects, that I was sending, but instead I sent a propertyName-like key as well as the related object. I do believe, that in the long run Messages, that implement IMessageBase, would be better, because they can hold any amount of separate attributes. Thanks - I think I now understand how to make it work (though I do not use dependence injection). – Igor Jul 24 '15 at 04:29