WPF MVVM communication between View Model [closed]
Asked Answered
S

4

10

I am working on WPF MVVM application wherein I have 2 views View1 and View2 with their respective ViewModels. Now, I want on click of a button in View1 would close View1 and open View2 using ViewModel1. Also, I want to pass some data say a instance of person class to ViewModel2 when opening from ViewModel1 which would be used to display information in View2.

What is the best and possibly the simplest way to achieve this inside ViewModels only, I would want to avoid writing code for navigation in code behind.

Stevens answered 22/5, 2014 at 5:26 Comment(0)
T
4

How about using the Mediator pattern (for example see technical-recipes.com or John Smith) or weak events? Afaik several MVVM frameworks/libs (like PRISM, Caliburn.Micro, MVVMCross) already come with the infrastructure code for these. There are also separate libraries that are independent of any specific MVVM framework, like Appccelerate EventBroker which can help you achieve something along the lines of what you want.

With events, however, I wonder whether you require some feedback on whether the event was "correctly" handled or not. There are ways to achieve this (altering the value of the event args, handling the events sync, after raising the event, checking the value of the event args), but they are not as concise as a method's return value or a method throwing an exception.

EDIT: sorry I just realized that the second view/ViewModel is not open, yet. So my "solution" is not (that simply) applicable. You need to pass the instruction "up" in the view model tree, maybe even to the root, where you can instantiate and show the new view model (show in a new window or as a ContentControl in an existing view?)

Transitive answered 22/5, 2014 at 5:42 Comment(1)
This suggestion worked for me. The Mediator pattern is straightforward and practical way of enabling communication between view models. An example implementation is given here: technical-recipes.com/2016/…Inchoative
F
31

I created this Messenger class to handle communication between ViewModels.

Register for an added person object in MainViewModel:

Messenger.Default.Register<Person>(this, AddPersonToCollection, Context.Added);

To notify all registered ViewModels about the added person from the CreatePersonViewModel:

Messenger.Default.Send(person, Context.Added);

Source code:

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

namespace Application.Messaging
{
    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);
                }
            }
        }
    }
}
Finella answered 28/5, 2014 at 11:4 Comment(5)
I really like your solution. Combining generic message types with optional context objects is awesome. As I cannot decide upon a MVVM framework yet and maybe never will, I found you solution and used it in my hobby project. But there is one problem with the MessengerKey: It does not consider the generic type of the message. Thus, if you register two different message types to the same recipient with same context, then the second registration will fail (but not throw an exception). I solved it locally for me by extending the MessengerKey with the type of the message.Bulletproof
@Yeah69 can you share this MessengerKey extension you created?Charpentier
@Conrad: yes, sure. I made a github repo: github.com/Yeah69/MessengerPatternBulletproof
This is working brilliantly! Just a quick question, how would I use the Register when the method takes no arguments ? private void MyMethod()Digamma
How can you check if a class has been registered already?Robinrobina
T
4

How about using the Mediator pattern (for example see technical-recipes.com or John Smith) or weak events? Afaik several MVVM frameworks/libs (like PRISM, Caliburn.Micro, MVVMCross) already come with the infrastructure code for these. There are also separate libraries that are independent of any specific MVVM framework, like Appccelerate EventBroker which can help you achieve something along the lines of what you want.

With events, however, I wonder whether you require some feedback on whether the event was "correctly" handled or not. There are ways to achieve this (altering the value of the event args, handling the events sync, after raising the event, checking the value of the event args), but they are not as concise as a method's return value or a method throwing an exception.

EDIT: sorry I just realized that the second view/ViewModel is not open, yet. So my "solution" is not (that simply) applicable. You need to pass the instruction "up" in the view model tree, maybe even to the root, where you can instantiate and show the new view model (show in a new window or as a ContentControl in an existing view?)

Transitive answered 22/5, 2014 at 5:42 Comment(1)
This suggestion worked for me. The Mediator pattern is straightforward and practical way of enabling communication between view models. An example implementation is given here: technical-recipes.com/2016/…Inchoative
C
2

Use a tiny dedicated Light Message Bus. It is not a part of any MVVM framework, so one can use it independently. Very very easy to install and use.

Usage guidelines

Carinthia answered 26/3, 2017 at 19:5 Comment(0)
P
1

I ended up adapting Dalstroem's solution slightly. This helped me solve two problems:-

Problem #1: Each recipient could only register for one message per context

Solution - Include the Type as part of the dictionary key (as suggested by Dima above).

Problem #2: My xUnit tests kept failing inconsistently

Solution - Change the Messenger from a singleton. Instead, inject the messenger into the ViewModels.

Also, crucially, change the Dictionary to be a non-static member. Otherwise you'll run into all sorts of problems with parallel tests.

Adapted solution:

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

namespace Application.Messaging
{
    public class Messenger
    {
        private readonly ConcurrentDictionary<MessengerKey, object> RecipientDictionary = new ConcurrentDictionary<MessengerKey, object>();

        public Messenger()
        {
        }

        public void Register<T>(object recipient, Action<T> action)
        {
            Register(recipient, action, null);
        }

        public void Register<T>(object recipient, Action<T> action, object context)
        {
            var key = new MessengerKey(recipient, typeof(T), context);
            RecipientDictionary.TryAdd(key, action);
        }

        public void Unregister<T>(object recipient, Action<T> action)
        {
            Unregister(recipient, action, null);
        }

        public void Unregister<T>(object recipient, Action<T> action, object context)
        {
            object removeAction;
            var key = new MessengerKey(recipient, typeof(T), context);
            RecipientDictionary.TryRemove(key, out removeAction);
        }

        public void UnregisterAll()
        {
            RecipientDictionary.Clear();
        }

        public void Send<T>(T message)
        {
            Send(message, null);
        }

        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 RecipientDictionary where r.Key.Context == null select r;
            }
            else
            {
                // Get all recipients where the context is matching.
                result = from r in RecipientDictionary 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 Type MessageType { get; private set; }
            public object Context { get; private set; }

            public MessengerKey(object recipient, Type messageType, object context)
            {
                Recipient = recipient;
                MessageType = messageType;
                Context = context;
            }

            protected bool Equals(MessengerKey other)
            {
                return Equals(Recipient, other.Recipient) 
                    && Equals(MessageType, other.MessageType)
                    && Equals(Context, other.Context) ;
            }

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

            public override int GetHashCode()
            {
                unchecked
                {
                    return ((Recipient != null ? Recipient.GetHashCode() : 0) * 397) 
                        ^ ((MessageType != null ? MessageType.GetHashCode() : 0) * 397)
                        ^ (Context != null ? Context.GetHashCode() : 0);
                }
            }
        }
    }
}
Paramorphism answered 6/7, 2021 at 15:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.