Slanje poruka unutar Silverlight aplikacije pomoću Mediator patterna
U izradi Silverlight (ili WPF, WinForms…) aplikacija, često se javlja potreba za komunikacijom različitih dijelova aplikacije, poput različitih User Control-a, instanciranih objekata i slično. Najbolje da jednim primjerom objasnim problematiku: za izradu Silverlight aplikacije za praćenje riječkog karnevala (izrađeno 2008., nadam se da je još online!) morao sam implementirati "pametno" sučelje, koje ispisuje informacije o trenutnoj skupini koja prolazi, označiti ju na listi, i još par sitnica. Te informacije sam dobivao preko web servisa u lokalni "provider" podataka, i pomoću evenata/delegata u kodu se predplaćivao na taj "providera" i slao potrebne objekte svim pretplatnicima na evente. Kako se sučelje sastoji od dosta user controla i custom controla, orkestracija pretplate prilikom starta aplikacije odnosno kreiranja objekata je bila relativno kompleksna. Na kraju izrade bilo mi je jasno da to i nije nabolje rješenje i da mora postojati jednostavniji način. Nedugo potom sam naišao na jedan design pattern, za koji sam znao da postoji ali ga nikada nisam imao priliku iskoristiti, pretpostavljam zbog samom načina rada web aplikacija. Radi se o Mediator patternu, koji definira komunikaciju između objekata.
Jedan od mojih prethodnih blog postova objašnjava izradu ViewModela, prema Model-View-ViewModel patternu. Mediator pattern se savršeno uklapa u naš ViewModel, stoga ćemo iskoristiti MVVM i dodati potrebne klase.
U svojoj osnovi, stvar ovako funkcionira: objekti se pretplate (Subscribe(tip poruke)) na određene poruke Mediator objektu (MessageGateway u primjeru). Ti objekti mogu i poslati poruku (Publish(ChatMessage)) Mediator objektu, koji potom prosljeđuje poruku svim pretplatnicima koji su se pretplatili na taj tip poruke (ReceiveMessage(poruka)).[more]
Naša mediator klasa:
public class MessageGateway { private static MessageGateway gateway; /// <summary> /// Singleton instanca MessageGateway objekta /// </summary> /// <value>Statična MessageGateway instanca</value> public static MessageGateway Instance { get { if(gateway==null) gateway=new MessageGateway(); return gateway; } } /// <summary> /// Lista pretplatnika na tipove poruka; svaki tip poruke moze imati vise pretplatnika /// </summary> private Dictionary<Type,List<WeakReference>> subscribers; /// <summary> /// Pretplata na specifičnu poruku /// </summary> /// <param name="receiver" />Pretplatnik</param> /// <param name="message" />Tip poruke</param> public void Subscribe(IReceiver receiver,Type messageType) { if(messageType!=typeof(MessageBase) && !messageType.IsSubclassOf(typeof(MessageBase))) throw new ArgumentException("Parametar mora biti tipa MessageBase","messageType"); if(subscribers==null) subscribers=new Dictionary<Type,List<WeakReference>>(); if(!subscribers.ContainsKey(messageType)) subscribers.Add(messageType, new List<weakreference>()); subscribers[messageType].Add(new WeakReference(receiver)); } /// <summary> /// Objava poruke svim pretplatnicima /// </summary> /// <param name="message" />Poruka</param> public void Publish(MessageBase message) { if (subscribers == null) return; foreach(Type type in subscribers.Keys) { if(message.GetType() == type || message.GetType().IsSubclassOf(type)) { foreach (var reference in subscribers[type]) { if (reference.IsAlive && reference.Target != null) ((IReceiver)reference.Target).ReceiveMessage(message); } } } } }
Subscribe: svaki objekt koji se želi pretplatiti na poruke mora implementirati IReceiver interface. On mu omogućava primanje poruka. Metoda interno koristi Dictionary, gdje je za svaki ključ (Type, tip poruke), vezana lista pretplatnika. Ako ključ ne postoji, on se dodaje u Dictionary, kreira nova lista, i na kraju dodaje referenca (WeakReference) pretplatnika.
Publish: u Dictionaryu pretplatnika (nevezano, ali zar slicni takav dictionary sa tipovima nema i IoC kontejner?) se traže pretplatnici na određeni tip poruke, i svakom pretplatniku onda se šalje ta poruka. Ako se netko pretplatio na baznu klasu poruke (MessageBase), dobiti će sve poruke, jer sve poruke nasljeđuju baznu poruku. Logično, zar ne;)?
IReceiver interface koji moraju implementirati objekti koji žele dobivati poruke je jednostavan, kao i bazna MessageBase klasa:
public interface IReceiver { void ReceiveMessage(MessageBase messsage); } public abstract class MessageBase { public MessageBase(object sender) { Sender = sender; } public object Sender { get; protected set; } } /// <summary> /// Primjer implementacije poruke /// </summary> public class ChatMessage : MessageBase { public ChatMessage(object sender, string text) : base(sender) { Text = text; } public string Text { get; private set; } }
Bazna ViewModel klasa, koju nasljeđuju sve ViewModel klase, sadrži logiku pretplate, primanja i slanja poruka mediator objektu:
public abstract class ViewModelBase : INotifyPropertyChanged, IReceiver { protected void Subscribe(Type messageType) { MessengerInstance.Subscribe(this,messageType); } protected void Publish(MessageBase message) { MessengerInstance.Publish(message); } private MessageGateway MessengerInstance { get { return MessageGateway.Instance; } } public virtual void ReceiveMessage(MessageBase messsage) { } public event PropertyChangedEventHandler PropertyChanged; protected virtual void RaisePropertyChanged(string propertyName) { if (this.PropertyChanged != null) this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } }
kao što je vidljivo iz primjera, ViewModelBase apstraktna klasa ujedno je i IReceiver, znači se može pretplatiti na primanje poruka.
Da bi pretplatili na određene poruke, dovoljno je dodati u konstruktor našeg ViewModel-a (npr. DashboardViewModel, koji nasljeđuje ViewModelBase):
Subscribe(typeof(ChatMessage));
Za primanje poruka napravimo override ReceiveMessage metode:
public override void ReceiveMessage(MessageBase messsage) { var chatmsg=(ChatMessage)message; // operacije nad primljenom porukom }
Primjer slanja nove poruke:
Publish(new ChatMessage(this,"neki text"));
Cijeli projekt možete i skinuti lokalno, pa će sama implementacija biti jasnija, nadam se.
Čisto za informaciju, slična arhitektura i način slanja poruka se koristi i u Service Bus frameworcima, pogodnim za SOA arhitekturu. Microsoft nema svoj ServiceBus framework (osim promoviranja JBOWS!), za razliku od Java svijeta, gdje su oni dosta popularni u enterprise primjeni. Bitna razlika od ove implementacije je asinkroni način slanja poruka putem Queue (MSMQ uglavnom), kojeg u ovom primjeru nema (sve je sinkrono, nema “reda” poruka i enkapsuliranja u transakcije), kao i “broker” objekt koji može sadržavati određenu logiku koja određuje kome se mora poslati koja poruka ovisno o danim parametrima. Dva popularna open source frameworka u .NET svijetu su nServiceBus i MassTransit, a uskoro se nadam da ću imati prilike i pozabaviti se i njima, nakon čeka naravno slijedi blog osvrt!