【问题标题】:Notify parent ViewModel of changes in child ViewModels通知父 ViewModel 子 ViewModel 的变化
【发布时间】:2015-07-22 17:10:56
【问题描述】:

我在这里阅读了几篇文章,这些文章描述了如何收听发出的通知。但是:我仍然无法将这些应用到我的应用程序中。

我目前有一个包含多个“页面”的应用程序。

其中一个页面包含一个 WPF Treeview 控件以及几个 ViewModel 和数据模型。

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; }
    }
}

此 ViewModel 包含 DriveTreeViewItemViewModels 并通过 DataContext 绑定到 UserControl(“页面”)。

Drive- 和 DirectoryTreeViewItemViewModel 类包含一些属性,但在其他方面基于 TreeViewItemViewModel,您可以在此处查看:

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
}

我已按照http://www.codeproject.com/Articles/26288/Simplifying-the-WPF-TreeView-by-Using-the-ViewMode 中描述的教程和想法进行操作,到目前为止一切正常。

我的问题是:我想将字符串“selected”作为属性添加到 FoldersSearchViewModel,其中将包含所选子 ViewModel 的路径。 DriveTreeViewItemViewModel 和 DirectoryTreeViewItemViewModel 各有一个“ Path" 属性,它包含子节点的完整路径。

所以:一旦 OnPropertyChanged("IsSelected") 被调用,我想通知 FoldersSearchViewModel 并让该方法将 Path-property 从选定的 TreeViewItemViewModel 复制到新的“selected”(字符串)属性中。


我可以通过在构造函数中将 FoldersSearchViewModel 对象传递给孩子和孩子的孩子等来实现这一点 - 但是没有更好的方法吗?我想我应该将 FoldersSearchViewModel 挂钩到每个节点和子节点的 PropertyChanged 事件,但我想知道在这种情况下有 MVVM 经验的人会怎么做。

顺便说一句:我可以使用 WPF Treeview.SelectedItem 来获取当前选定的 TreeViewItemViewModel,但这听起来不对,因为我想将视图、模型和视图模型分开。

P.s.:我尝试阅读和使用MVVM in WPF - How to alert ViewModel of changes in Model... or should I?,但遗憾的是它似乎并没有解决我的问题。

非常感谢任何帮助!

【问题讨论】:

    标签: wpf mvvm treeview


    【解决方案1】:

    我的工作方式是,如上所述,Messenger 服务是一个单例。我也使用 DI,因此需要使用它的 VM 会将 IMessengerService 实例注入其中。

    IMessengerService 看起来像:

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

    消息“参数”类在应用程序范围内可用,因此您可能有类似的内容:

    public class FolderOpened : IMessageBase
    {
    }
    

    因此,FolderOpened 类在整个应用程序中都可用,显然它是在编译时定义的。

    任何关心此消息的客户端都将在其 VM 构造函数中订阅该消息:

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

    发件人是否已经“注册”没关系,信使只是基于消息类类型。

    任何人都可以随时发送消息:

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

    YMMV,但我的信使会自动断开已处置/不存在的订阅者,但实际上,正确的方法是让 VM 在其终结器或处置方法中取消订阅。

    这样就解决了吗?

    【讨论】:

    • 是的,确实如此。我正在使用类似的东西;但是:我没有发送消息对象,而是发送了一个类似于 propertyName 的键以及相关对象。我确实相信,从长远来看,实现 IMessageBase 的消息会更好,因为它们可以保存任意数量的单独属性。谢谢 - 我想我现在明白如何让它工作了(虽然我不使用依赖注入)。
    【解决方案2】:

    MVVM 方法是使用信使/事件聚合器模式并广播事件。

    【讨论】:

    • Hm.. 所以我应该创建一个 Messenger 类,订阅 FoldersSearchViewModel 以接收通知,并让 TreeViewItemViewModel 在事件发生更改时通知所有订阅的成员?我目前遇到的一个问题是:如果处置了 TreeViewItemViewModel - 我在哪里处理从 Messenger 类取消注册?我已经读过,该方法没有被可靠地调用,这可能会留下空对象,或者 - 更糟糕的是 - 我的应用程序中的内存泄漏。你有什么建议吗?伪代码或方案会很有帮助。
    • 您不会为每个项目注册一个事件,您只是注册一般事件。所以以资源管理器为例。树视图窗格可能会注册“SelectionChanged”、“ItemOpened”和“ItemClosed”事件,并发送一个“EventArgs”类型类作为参数(特定于消息)任何对这些事件感兴趣的人都会订阅。确实不需要取消订阅信使事件,因为它不绑定到任何控件,而是应用程序范围的单例。如果您将引用存储在“ItemClosed”处理程序中,您将破坏模型:)。
    • 嗯.. EventMessenger 确实是一个单身人士。它包含一个通知订阅者的方法,并传递事件和源对象(当前)。但是为了知道通知谁,我必须注册潜在的收件人,不是吗?如果我必须为它们订阅一个或多个事件,如果 ViewModel 被破坏,我可能不得不在某个时候取消订阅它们——或者我错了吗?是否有教程或其他内容可以显示这种信使模式的工作原理?谢谢! :)
    猜你喜欢
    • 1970-01-01
    • 2017-03-14
    • 2021-11-09
    • 1970-01-01
    • 1970-01-01
    • 2017-11-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多