【问题标题】:WPF MVVM Communication with Messenger (post-message load of VM)WPF MVVM 与 Messenger 通信(VM 的消息后负载)
【发布时间】:2016-11-16 18:38:54
【问题描述】:

背景

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

由于我的应用需要大量的 Views/VM,每个 ViewModel 在需要 View 时实例化并随后处理(view-first,VM 指定为 View 的 DataContext)。

问题

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

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

想法

我见过的一些示例使用了一个预先实例化所有 ViewModel 的 ViewModelLocator。创建视图时,只需从 VML 中提取现有的 ViewModel。这种方法意味着消息将始终在每个 ViewModel 中被接收和可用。我担心的是,使用 30 多个 ViewModel 时都会加载大量数据,我的应用会随着每个 View 的使用而延长使用时间(没有资源被释放)。

我考虑找到一种方法来存储消息,然后将所有消息重新发送给任何注册的收件人。如果实现,这将允许我在每个 ViewModel 中注册消息后调用各种 Resend 方法。我对这种方法有一些顾虑,包括随着时间的推移消息的积累。

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

代码

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

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

更新

我的应用程序的架构方式是与我的 MainWindow 一起使用的 ViewModel,它用作一种基本的 shell。它提供了一个主要布局,其中包含一些导航和登录/注销等控件。

所有后续视图都显示在 MainWindow 内的 ContentControl 内(占据了大部分窗口空间)。 ContentControl 绑定到我的“MainWindowViewModel”的“CurrentView”属性。 MainWindowViewModel 实例化了我创建的自定义导航服务,目的是选择并返回适当的视图以更新我的“CurrentView”属性。

这种架构可能是非正统的,但我不确定在不使用 TabControl 等开箱即用的东西的情况下通常如何完成导航。

想法

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

【问题讨论】:

    标签: c# wpf mvvm communication messenger


    【解决方案1】:

    我建议不要“存储消息”——即使您制定了恢复消息的良好模式,您仍然会得到难以测试的逻辑。这确实表明您的视图模型需要了解太多关于应用程序状态的信息。

    在视图模型定位器的情况下 - 一个设计良好的视图模型定位器可能会延迟加载视图模型,这将使​​您留在您现在所在的位置。

    选项 1

    相反,请考虑尽可能使用 UserControls 和 DependencyProperties。

    选项 2

    如果您的视图实际上是真正的视图,那么请考虑一个维护必要状态并将其注入您的视图模型的单例上下文类。这种方法的好处是您的上下文类可以实现INotifyPropertyChanged,并且任何更改都会自动传播到您的消费视图。

    选项 3

    如果您在视图之间导航,您可能希望实现类似于here 描述的导航服务。

    interface INavigationService(string location, object parameter) {}
    

    在这种情况下,您的参数被视为您的状态对象。新的视图模型从您要离开的视图接收模型数据。

    blog post 有助于解释何时使用视图模型和用户控件的最佳实践。

    【讨论】:

    • 就应用程序状态的数量而言,每个 ViewModel 只需要知道一个起点即可。例如,我的 30 个视图中的 5 个可能都需要包含一些属性的自定义类对象的相同实例。所有 30 个都需要访问一些用户个人资料信息(另一类)。我的观点绝对是观点,我只需要他们感觉无缝,因为每个人都可以对相关信息执行任务。关于存储状态的“单例上下文类”——这与存储用于通知 ViewModel 应用程序状态的消息不一样吗?
    • 诚然,上下文类比消息集合漂亮得多,但它只是在 ViewModel 之外存储应用程序状态的另一种方式,对吧?
    • 最重要的是,您将视图模型与需要知道如何传递数据、担心顺序、接受哪些消息、忽略哪些消息等分开。
    • 感谢您提供更多信息/想法。如果您不介意看一下,我已经用一些信息和想法更新了我的问题。
    【解决方案2】:

    ...并注册感兴趣的消息。从先前存在的 ViewModel 发送的消息不会被新的 ViewModel 接收。因此,我无法使用我的 Messenger 类在 ViewModel 之间进行通信。

    为什么您的虚拟机需要了解历史消息?

    通常消息传递应该是 pub/sub;消息被发布(“pub”)并且任何可能对特定消息感兴趣的人订阅(“sub”)以接收这些消息。发布者不应该关心如何处理消息 - 这取决于订阅者。

    如果您有一些模糊的业务案例需要了解以前的消息,那么您应该创建自己的消息队列机制(即将它们存储在数据库中并根据日期时间检索它们)。

    【讨论】:

    • 发布消息的虚拟机并不关心消息会发生什么,而是在发布原始消息时订阅的虚拟机并不存在。基本上,关于应用程序状态的一些信息(用户信息、正在处理的实体的标识符等)需要从一个 VM 传递到另一个 VM,并且它们不会同时存在。
    • @WyattE 这类信息通常会在创建 VM 时注入,您不会保留历史消息并在创建 VM 时返回并搜索它们。对于一般应用程序状态,请将其保存在一个单例中,您可以通过定位器或解析器获取它。
    猜你喜欢
    • 2011-03-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-05-10
    • 2011-05-08
    • 1970-01-01
    • 2010-11-13
    相关资源
    最近更新 更多