WPF MVVM中使用Messenger进行通信(通过消息加载VM)

4

背景

我正在使用MVVM模式编写WPF应用程序。我在各种教程中学习到使用Messenger在ViewModel之间通信。我正在使用本帖子代码部分(感谢@Dalstroem WPF MVVM communication between View Model和Pluralsight的Gill Cleeren)提供的Messenger类实现。

由于我的应用程序需要大量的Views/VMs,每个ViewModel都是在需要View时实例化并随后释放的(view-first,VM作为View的DataContext指定)。

问题

每个ViewModel的构造函数根据需要加载资源(Commands、Services等),并注册感兴趣的消息。从先前存在的ViewModel发送的消息不会被新的ViewModel接收。

因此,我无法使用Messenger类在ViewModel之间通信。

想法

我看到一些示例使用一个ViewModelLocator来预先实例化所有的ViewModel。当创建View时,视图只需从VML中获取现有的ViewModel。这种方法意味着消息将始终被接收并在每个ViewModel中可用。我的担忧是,对于30多个需要使用大量数据的ViewModel,如果每个View都被使用(没有资源被释放),我的应用程序会变得缓慢。

我考虑过找到一种方法来存储消息,并随后将所有消息重新发送给任何注册的接收者。如果实现了这个方法,我可以在注册了每个ViewModel的消息后调用一个重新发送的方法。我有几个担忧,包括随时间累积的消息。

我不确定我做错了什么或者是否有我不知道的方法。

代码

public class Messenger
{
    private static readonly object CreationLock = new object();
    private static readonly ConcurrentDictionary<MessengerKey, object> Dictionary = new ConcurrentDictionary<MessengerKey, object>();

    #region Default property

    private static Messenger _instance;

    /// <summary>
    /// Gets the single instance of the Messenger.
    /// </summary>
    public static Messenger Default
    {
        get
        {
            if (_instance == null)
            {
                lock (CreationLock)
                {
                    if (_instance == null)
                    {
                        _instance = new Messenger();
                    }
                }
            }

            return _instance;
        }
    }

    #endregion

    /// <summary>
    /// Initializes a new instance of the Messenger class.
    /// </summary>
    private Messenger()
    {
    }

    /// <summary>
    /// Registers a recipient for a type of message T. The action parameter will be executed
    /// when a corresponding message is sent.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="recipient"></param>
    /// <param name="action"></param>
    public void Register<T>(object recipient, Action<T> action)
    {
        Register(recipient, action, null);
    }

    /// <summary>
    /// Registers a recipient for a type of message T and a matching context. The action parameter will be executed
    /// when a corresponding message is sent.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="recipient"></param>
    /// <param name="action"></param>
    /// <param name="context"></param>
    public void Register<T>(object recipient, Action<T> action, object context)
    {
        var key = new MessengerKey(recipient, context);
        Dictionary.TryAdd(key, action);
    }

    /// <summary>
    /// Unregisters a messenger recipient completely. After this method is executed, the recipient will
    /// no longer receive any messages.
    /// </summary>
    /// <param name="recipient"></param>
    public void Unregister(object recipient)
    {
        Unregister(recipient, null);
    }

    /// <summary>
    /// Unregisters a messenger recipient with a matching context completely. After this method is executed, the recipient will
    /// no longer receive any messages.
    /// </summary>
    /// <param name="recipient"></param>
    /// <param name="context"></param>
    public void Unregister(object recipient, object context)
    {
        object action;
        var key = new MessengerKey(recipient, context);
        Dictionary.TryRemove(key, out action);
    }

    /// <summary>
    /// Sends a message to registered recipients. The message will reach all recipients that are
    /// registered for this message type.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="message"></param>
    public void Send<T>(T message)
    {
        Send(message, null);
    }

    /// <summary>
    /// Sends a message to registered recipients. The message will reach all recipients that are
    /// registered for this message type and matching context.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="message"></param>
    /// <param name="context"></param>
    public void Send<T>(T message, object context)
    {
        IEnumerable<KeyValuePair<MessengerKey, object>> result;

        if (context == null)
        {
            // Get all recipients where the context is null.
            result = from r in Dictionary where r.Key.Context == null select r;
        }
        else
        {
            // Get all recipients where the context is matching.
            result = from r in Dictionary where r.Key.Context != null && r.Key.Context.Equals(context) select r;
        }

        foreach (var action in result.Select(x => x.Value).OfType<Action<T>>())
        {
            // Send the message to all recipients.
            action(message);
        }
    }

    protected class MessengerKey
    {
        public object Recipient { get; private set; }
        public object Context { get; private set; }

        /// <summary>
        /// Initializes a new instance of the MessengerKey class.
        /// </summary>
        /// <param name="recipient"></param>
        /// <param name="context"></param>
        public MessengerKey(object recipient, object context)
        {
            Recipient = recipient;
            Context = context;
        }

        /// <summary>
        /// Determines whether the specified MessengerKey is equal to the current MessengerKey.
        /// </summary>
        /// <param name="other"></param>
        /// <returns></returns>
        protected bool Equals(MessengerKey other)
        {
            return Equals(Recipient, other.Recipient) && Equals(Context, other.Context);
        }

        /// <summary>
        /// Determines whether the specified MessengerKey is equal to the current MessengerKey.
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj)) return false;
            if (ReferenceEquals(this, obj)) return true;
            if (obj.GetType() != GetType()) return false;

            return Equals((MessengerKey)obj);
        }

        /// <summary>
        /// Serves as a hash function for a particular type. 
        /// </summary>
        /// <returns></returns>
        public override int GetHashCode()
        {
            unchecked
            {
                return ((Recipient != null ? Recipient.GetHashCode() : 0) * 397) ^ (Context != null ? Context.GetHashCode() : 0);
            }
        }
    }
}

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

更新

我的应用程序的架构方式是使用了一个ViewModel和我的MainWindow,它作为一种基本的外壳。它提供一个主要布局和几个用于导航、登录/注销等控件。

所有后续的视图都显示在MainWindow内部的ContentControl中(占用大部分窗口空间)。ContentControl绑定到我的"MainWindowViewModel"的"CurrentView"属性。MainWindowViewModel实例化了一个我为选择和返回适当的视图而创建的自定义导航服务,以更新我的"CurrentView"属性。

这种架构可能是不正常的,但我不确定如何在不使用TabControl之类的现成工具的情况下完成导航。

想法

借鉴@axlj的想法,我可以将一个ApplicationState对象作为"MainWindowViewModel"的属性。使用我的Messenger类,在向MainWindow中注入新视图时,我可以pub一个ApplicationState消息。每个视图的ViewModel当然会sub这个消息,并在创建时立即获得状态。如果任何ViewModel对它们的ApplicationState副本进行更改,则会发布一条消息。然后MainWindowViewModel通过其订阅进行更新。

2个回答

1
"...并注册感兴趣的消息。先前存在的ViewModel发送的消息不会被新的ViewModel接收到。因此,我无法使用我的Messenger类在ViewModel之间进行通信。"
"你的VM们为什么需要知道历史消息呢?"
"一般来说,消息传递应该是发布/订阅模式;消息被发布(“pub”),任何对特定消息感兴趣的人都可以订阅(“sub”)以接收这些消息。发布者不应关心消息的处理方式——这由订阅者决定。"
"如果您有某些奇怪的业务需求需要了解以前的消息,那么您应该创建自己的消息队列机制(即将它们存储在数据库中,并根据日期时间检索它们)。”

并不是发布消息的虚拟机关心消息的去向,而是订阅消息的虚拟机在原始消息发布时不存在。基本上,需要从一个虚拟机传递一些有关应用程序状态的信息(用户信息、正在处理的实体标识等),它们在同一时间不存在。 - WyattE
@WyattE,这种类型的信息通常在创建VM时注入,你不会保存历史消息并在VM创建时回溯搜索它们。对于一般应用程序状态,请将其保存在单例中,你可以通过定位器或解析器获取它。 - slugster

1
我建议不要“存储消息” - 即使您找出了恢复消息的良好模式,您仍将得到难以测试的逻辑。这实际上表明您的视图模型需要过多地了解应用程序状态。
在视图模型定位器的情况下 - 良好设计的视图模型定位器可能会延迟加载视图模型,这将使您处于与现在相同的位置。
选项1
相反,尽可能使用UserControls和DependencyProperties。
选项2
如果您的视图确实是真正的视图,则考虑单例上下文类,该类维护必要的状态并将其注入到您的视图模型中。此方法的好处是,您的上下文类可以实现INotifyPropertyChanged,并且任何更改都会自动传播到使用视图。
选项3
如果您正在导航视图之间,则可能需要实现类似于here中描述的导航服务。
interface INavigationService(string location, object parameter) {}

在这种情况下,您的参数被视为状态对象。新的视图模型从您要导航离开的视图接收模型数据。
这篇博客文章很有帮助,可以解释何时使用视图模型和用户控件的最佳实践。

就应用程序状态的数量而言,每个ViewModel实际上只需要知道一个起点。例如,我的30个视图中的5个可能都需要包含一些属性的自定义类对象的相同实例。所有30个视图都需要访问某些用户配置文件信息(另一个类)。我的视图确实是视图,我只需要它们感觉无缝,因为每个视图可能会对相关信息执行任务。关于存储状态的“单例上下文类”,它是否有效地执行了存储用于通知ViewModels应用程序状态的消息的相同操作? - WyattE
诚然,上下文类比消息集合更美观,但它只是在ViewModels之外存储应用程序状态的另一种方式,对吧? - WyattE
最重要的是,您将视图模型与需要知道如何传递数据、担心顺序、接受哪些消息、忽略哪些消息等分离开来。 - AJ X.
感谢提供额外的信息/想法。如果您不介意,请看一下我更新的问题,其中包含一些信息和想法。 - WyattE

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接