Lose Kommunikation zwischen ViewModels an Hand eines Beispiels und verwendbarem Framework.

Wer ernsthafte WPF bzw. Silverlight Anwendungen entwickelt, der wird um das Design Pattern MVVM (oder ein angepasstes UI-Pattern) nicht herum kommen. Eine Frage, die sich immer wieder stellt, ist die, wie denn dann die einzelnen ViewModels miteinander kommunizieren.

Eine einfache Lösung für dieses Problem ist der Einsatz eines Patterns á la Mediator. Nachfolgend möchte ich ein einfaches Beispiel dafür zeigen, wie eine lose Kommunikation zwischen unterschiedlichen ViewModels (oder generell Objekten) implementiert werden kann.

Um Nachrichten zu versenden, müssen Empfänger an einer zentralen Stelle registriert werden. Zusätzlich muss es möglich sein, Registrierungen zu entfernen und schlussendlich, Nachrichten zu senden. Abbilden lässt sich dies im ersten Schritt durch ein Interface:

interface IMessenger
{
    void Register<TNotification>(object recipient, Action<TNotification> action);
    void Register<TNotification>(object recipient, string identCode, Action<TNotification> action);
    void Send<TNotification>(TNotification notification);
    void Send<TNotification>(TNotification notification, string identCode);
    void Unregister<TNotification>(object recipient);
}

Dieses Interface wird nun entsprechend implementiert.

public class Messenger : IMessenger
{
    private static Messenger instance;
    private static object lockObject = new object();
    private Dictionary<Type, List<ActionIdentifier>> references = new Dictionary<Type,List<ActionIdentifier>>();
    private Messenger() { }
    public static Messenger Instance
    {
        get
        {
            lock (lockObject)
            {
                if (instance == null)
                    instance = new Messenger();
                return instance;
            }
        }
    }

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

    public void Register<TNotification>(object recipient, string identCode, Action<TNotification> action)
    {
        Type messageType = typeof(TNotification);
        if (!references.ContainsKey(messageType))
            references.Add(messageType, new List<ActionIdentifier>());
        ActionIdentifier actionIdent = new ActionIdentifier();
        actionIdent.Action = new WeakReferenceAction<TNotification>(recipient, action);
        actionIdent.IdentificationCode = identCode;
        references[messageType].Add(actionIdent);
    }

    public void Send<TNotification>(TNotification notification)
    {
        Type type = typeof(TNotification);
        List<ActionIdentifier> typeActionIdentifiers = references[type];
        foreach (ActionIdentifier ai in typeActionIdentifiers)
        {
            IActionParameter actionParameter = ai.Action as IActionParameter;
            if (actionParameter != null)
                actionParameter.ExecuteWithParameter(notification);
            else
                ai.Action.Execute();
        }
    }

    public void Send<TNotification>(TNotification notification, string identCode)
    {
        Type type = typeof(TNotification);
        List<ActionIdentifier> typeActionIdentifiers = references[type];
        foreach (ActionIdentifier ai in typeActionIdentifiers)
        {
            if (ai.IdentificationCode == identCode)
            {
                IActionParameter actionParameter = ai.Action as IActionParameter;
                if (actionParameter != null)
                    actionParameter.ExecuteWithParameter(notification);
                else
                    ai.Action.Execute();
            }
        }
    }

    public void Unregister<TNotification>(object recipient)
    {
        bool lockTaken = false;
        try
        {
            Monitor.Enter(references, ref lockTaken);
            foreach (Type targetType in references.Keys)
            {
                foreach (ActionIdentifier wra in references[targetType])
                {
                    if (wra.Action != null && wra.Action.Target == recipient)
                        wra.Action.Unload();
                }
            }
        }
        finally
        {
            if (lockTaken)
                Monitor.Exit(references);
        }
    }
}

Was geschieht hier? Für jeden Notification-Typ können hier eine oder mehrere Instanzen registriert werden. Dies kann wahlweise mit Hilfe eines Identification Codes oder aber auch ohne passieren. Durch den Identification Code ist es möglich, eine Registrierung für spezielle Fälle zu unternehmen.

Per Unregister kann eine Registrierung für eine Instanz wieder entfernt werden. Per Send wird nun an alle Instanzen, welche die Kritierien (Notification-Typ, Identification Code) erfüllen, gesendet und werden dort abgearbeitet. Wie die Abarbeitung aussieht bleibt dem Ziel überlassen, da eine entsprechende Methode definiert werden muss, die genau dafür zuständig ist.

Des weiteren kommen einige Hilfsklassen zum Einsatz. So auch die Klasse ActionIdentifier. Diese schafft ein Mapping zwischen der auszuführenden Action (inklusive dem Ziel-Objekt), als auch einem etwaigen vorhandenen Identification Codes.

public class ActionIdentifier
{
    public WeakReferenceAction Action { get; set; }
    public string IdentificationCode { get; set; }
}

Diese Hilfsklasse stellt die Eigenschaft Action vom Typ WeakReferenceAction nach außen. Diese kann eine WeakReference auf das Zielobjekt halten, als auch eine Referenz auf die auszuführende Action, die ausgeführt wird, wenn eine Benachrichtigung für das eingetragene Zielolbjekt erhalten wird.

public class WeakReferenceAction
{
    private WeakReference target;
    private Action action;
    public WeakReferenceAction(object target, Action action)
    {
        this.target = new WeakReference(target);
        this.action = action;
    }
    public WeakReference Target
    {
        get
        {
            return target;
        }
    }
    public void Execute()
    {
        if (action != null && target != null && target.IsAlive)
            action.Invoke();
    }
    public void Unload()
    {
        target = null;
        action = null;
    }
}

public class WeakReferenceAction<T> : WeakReferenceAction, IActionParameter
{
    private Action<T> action;
    public WeakReferenceAction(object target, Action<T> action)
        : base (target, null)
    {
        this.action = action;
    }
    public void Execute()
    {
        if (action != null && Target != null && Target.IsAlive)
            action(default(T));
    }
    public void Execute(T parameter)
    {
        if (action != null && Target != null && Target.IsAlive)
            this.action(parameter);
    }
    public Action<T> Action
    {
        get
        {
            return action;
        }
    }
    #region IActionParameter Members
    public void ExecuteWithParameter(object parameter)
    {
        this.Execute((T)parameter);
    }
    #endregion
}

Mit diesen einfachen Klassen ist das Hilfswerk für eine übergreifende Kommunikation zwischen Objekten gegeben. Diese muss nur noch in die jeweiligen ViewModels implementiert werden. Dazu ist es notwendig, beim Empfänger eine Registrierung vorzunehmen:

public class SecondViewModel : ViewModelBase
{
    private string message;
    public SecondViewModel()
    {
        Messenger.Instance.Register<string>(this, Notify);
    }
    public string Message
    {
        get
        {
            return message;
        }
        set
        {
            if (message == value)
                return;
            message = value;
            RaisePropertyChanged("Message");
        }
    }
    public void Notify(string message)
    {
        Message = message;
    }
}

Ein anderes ViewModel muss nun eine Notification auslösen, die genau dieser Registrierung entspricht:

public class FirstViewModel : ViewModelBase
{
    private RelayCommand sendMessageCommand;
    private string message;
    public ICommand SendMessageCommand
    {
        get
        {
            if (sendMessageCommand == null)
                sendMessageCommand = new RelayCommand(SendMessage);
            return sendMessageCommand;
        }
    }
    private void SendMessage(object message)
    {
        Messenger.Instance.Send<string>(Message);
    }
    public string Message
    {
        get { return message; }
        set
        {
            if (message == value)
                return;
            message = value;
            RaisePropertyChanged("Message");
        }
    }
}

Die verwendete Klasse ViewModelBase macht nichts weiter, als das Interface INotifyPropertyChanged zu implementieren und eine Methode RaisePropertyChanged zur Verfügung zu stellen.

Im angehängten Beispiel kann man sich ein laufendes Beispiel dazu ansehen. Natürlich bestehen für diese Variante noch einige Verbesserungen an (beispielsweise das korrekte Entfernen der Referenzen). Über weitere Hinweise und Verbesserungsvorschläge würde ich mich freuen.

Download WPF Messaging Demo

Verbesserungen und Erweiterungen zu diesem Beitrag finden sich im zweiten Teil.

Über den Autor

Norbert Eder

Ich bin ein leidenschaftlicher Softwareentwickler und Fotograf. Mein Wissen und meine Gedanken teile ich nicht nur hier im Blog, sondern auch in Fachartikeln und Büchern.

10 Kommentare

  • hi Norbert,
    Gute Variante vom mvvm messenger, aber hat einen Bug, und zwar schau dir folgenden unittest an:

    Messenger.Instance.Register<string>(this, new Action<string>((a) => Console.WriteLine(a)));
    Messenger.Instance.Send<string>("Test");
    Messenger.Instance.Unregister<string>(this);
    Messenger.Instance.Send<string>("Test");

    Obwohl ich Unregister aufrufe, wird die registrierte Action 2x aufgerufen, also auch nachdem unregister aufgerufen wurde.

    ?

  • Danke für die Rückmeldung. Es hat leider einen Fehler beim Verwerfen der Registrierung gegeben, dadurch blieb der Empfänger weiterhin erhalten. Das Beispielprojekt wurde entsprechend angepasst.

    Zusätzlich gab es eine kleine Erweiterung. Das Aufheben einer Registrierung kann nun auch über den Identification Code passieren.

  • Hallo Norbert,
    ich weiß der Blogeintrag ist schon etwas älter.

    Vielleicht verstehe ich das Beispiel falsch, aber so wie ich jetzt dein Beispiel verstanden habe muss immer ein Sender und ein Empfänger festgelegt werden – man muss sich daher vorher überlegen welche ViewModels miteinander Daten austauschen müssen. Ist das korrekt so?

    lg
    Markus

      • Hallo Norbert,

        danke für deine rasche Antwort, ich denke jetzt verstehe ich das.
        Die ViewModels müssen die für sie wichtigen Messages empfangen können, von wem die Messages kommen ist egal, weil der Absender einfach nur eine Message absendet – wer diese dann empfängt ist dem Absender egal.
        Die „Typen“ der Messages müssen zusammenpassen.

        lg
        Markus

  • Hallo Norbert,

    leider muss ich nochmal nachhaken.
    Du verwendest in deinem Demo-Projekt eine Klasse „RelayCommand“, diese scheint mittels ICommand-Schnittstelle das Ausführen / übermitteln der Actions zu übernehmen.

    Ist diese Klasse dafür notwendig? Du erwähnst diese nämlich in deinem Beitrag nicht, daher vermute ich mal, dass es einen anderen Lösungsweg gibt?

    lg
    Markus