In need of an English translation? Please drop me a note.
Voor ons huidige project zochten we naar een goed ontkoppeling tussen de user interface en de communicatie met hardware. Omdat het een WPF applicatie is en we gebruik maken van MVVM draait alles in hetzelfde applicatie. De hardware willen we real time uitlezen en reageren op het gedrag.
Er werd dus nagedacht over het gebruik van Events. Dat is een prima oplossing voor “Loose coupling”. Maar hierbij is de ontkoppeling maar één kant op. De ‘luisteraar’ kent de verstrekker van de gebeurtenissen want hij moet zich daarop abonneren.
Omdat we MVVMLight gebruiken kregen we de mogelijkheid om nog verder te gaan met ontkoppeling. MVVMLight bezit namelijk ook een Messagebus.
De messagebus kennen we vooral vanuit SOA en Enterprise architectuur:
Maar hier hebben we te maken met messsages die In Process verstuurd worden. Hierbij zijn de volgende klassen te herkennen:
- De boodschap, de Message
- Diegene die ‘m verstuurt, de Sender
- Diegene die zich abonneert op dit type bericht, de Target of algemener, de Receiver
- De Messenger
Bij MVVMLight is dit opgelost met een aantal objecten die globaal beschikbaar zijn en via locking threadsafe zijn gemaakt. Hierdoor wordt het mogelijk om overal in de applicatie boodschappen te verzenden en eventueel te ontvangen.
Een boodschap is in het algemeen een overerving van MessageBase, een standaard klasse beschikbaar in MVVMLight:
public class DeviceMessage : MessageBase { public DeviceMessage(string message) { Message = message; } public string Message { get; private set; } public override string ToString() { return "Device message: " + Message; } }
Hier hebben we een DeviceMessage die één property genaamd Message bezit.
Een message wordt verstuurd via de Send methode op de static Messenger:
Messenger.Default.Send<DeviceMessage> (new DeviceMessage("Time on device: " + DateTime.Now.ToString()));
Meer is er niet nodig om een boodschap op de Messagebus te zetten. In dit voorbeeld kan ik berichten met een datum/tijd verwachten. Zo kan ik ook vanuit een ViewModel juist een start- of stopcommando doorgeven aan een bepaald apparaat (of beter: een instanciering van iets wat de interface van dat apparaat implementeert). Ik heb geen kennis van de luisteraar, ik weet niet eens of die wel bestaat.
Hoe wordt het bericht ontvangen? Via een constructor of initialisatie heeft een andere partij zich geabonneerd op deze berichten:
Messenger.Default.Register<DeviceMessage> (this, true, m => { Message = m.Message; });
In dit geval wordt de boodschap in op een ViewModel de m.Message uitgelezen en op het scherm getoond (via een bindable property).
Maar er is ook een bonus. De MessageBus is globaal beschikbaar en iedereen kan zich abonneren op berichten. MVVMLight biedt de mogelijkheid om met een Message ook een filtertoken op te nemen.
Messenger.Default.Send<SwitchMessage> (new SwitchMessage(this, "AnotherDevice"), "DeviceSwitchToken");
In dit geval kunnen meerdere partijen zich abonneren op een SwitchMessage. Maar door de meegestuurde “DeviceSwitchToken” zal wellicht niet iedereen de SwitchMessage verwerken, ze wachten op zo’n bericht vergezeld met een ander token.
En het is ook mogelijk om zich juist op een scala van berichten te abonneren. Het is mogelijk om alle overervingen van een baseclass te ontvangen:
public class LogProvider : ILogProvider { public void Initialize() { // log all messages Messenger.Default.Register<MessageBase>(this, true, m => { Debug.WriteLine( String.Format( "At {0} we received: {1}", DateTime.Now.ToShortTimeString(), m)); }); } }
Hier heb ik een logger geabonneerd op alle overervingen van MessageBase.
Door de totale ontkoppeling van de zenders en ontvangers kan je het overzicht verliezen. Maar juist door deze logger heb ik een mooie tijdslijn van wat er onderling verstuurd is en kan ik analyseren wat er mis is gegaan. Daarom implementeer ik ook de ToString() zodat ik de messages hun eigen te loggen boodschap kan laten beschrijven.
Ik kan ook als een soort van watchdog een pulse afgeven. Als ik niet de gewenste antwoorden ontvang kan ik ‘alarm slaan’. En dit kan ik doen zonder dat ik de daadwerkelijke implementatie van de te controleren instanties niet ken.
Er zijn in MVVMLight nog een aantal andere boodschappen mogelijk. Ik wil er nog één aanstippen.
MVVMLight kent ook een generic message. Deze bevat standaard een Content property waarin een andere klasse gestopt kan worden. In bovenstaande voorbeelden is steeds gebruik gemaakt van een MessageBase overerving met extra properties. Met de generic oplossing kunnen die properties in de Content geplaatst worden. De Content zier er dan zo uit:
public class SwitchContent { public SwitchContent(int id, string message) { ID = id; Message = message; } public int ID { get; private set; } public string Message { get; private set; } }
De message wordt een stuk eenvoudiger:
public class GenericSwitchMessage : GenericMessage<SwitchContent> { public GenericSwitchMessage(SwitchContent switchParameter) : base(switchParameter) { } public override string ToString() { return String.Format( "Switch message: sender{0}; target={1}", Sender, Target); } }
Het te verzenden bericht wordt nu gecombineerd met de context:
Messenger.Default.Send<GenericSwitchMessage> (new GenericSwitchMessage(new SwitchContent (1, "Hallo")));
En het uitluisten ziet er ook wat rustiger uit:
public class LogProvider : ILogProvider { public void Initialize() { // log all messages Messenger.Default.Register<GenericSwitchMessage>(this, true, m => { // m.Content.ID // m.Content.Message }); } }
Conclusie:
De MVVMLight Messenger biedt een fraaie oplossing voor de totale ontkoppeling. Er zijn ook andere implementaties beschikbaar (iedere MVVM implementatie lijkt er wel één te bieden) dus het is eenvoudig om er aan te komen en om het eens uit te proberen.