I have a MEF container which contains hundreds of classes. What is a good way to pass messages between different classes?
I would prefer a solution that will work with any Dependency Injection (DI) container, including Unity, Castle Windsor, etc.
Note: This is a "share your knowledge, Q&A-style" entry.
This event publisher allows any class from the MEF container to send a message to any other class in the MEF container.
This code has been battle proven over a number of years, and has proven to be particularly useful when using WPF / MVVM.
It's a one-to-many subscription, so once the message is sent out, it is received by any listener that is observing messages of that custom type.
This example is for MEF, but it is also applicable to any other Dependency Injection (DI) container such as Unity, Castle Windsor, etc. If you convert EventPublisher
to a singleton, you can use it with normal C# (i.e. not using a DI container). Let me know if you want me to post the code.
This code is nothing new: there are hundreds of other implementations of event publishers in the open source community, e.g. in MVVM Light. However, this example uses such a small amount of code that it's possible to see how it works under the hood, by single stepping in the debugger.
Add the boiler plate code to your project (see below).
Create your custom event type. This can be a class, a struct, or even an enum, e.g.:
public enum NavigationType
{
Unknown = 0,
MyOption1,
MyOption2
}
... then, I can import the eventPublisher
into any class, like so:
[ImportingConstructor]
public BrokerOrderSearchResultViewModel(
IEventPublisher<NavigationType> eventPublisher,
)
{
_eventPublisher = eventPublisher;
...
... in the constructor, I can then subscribe to events of type NavigationType
:
_eventPublisher.GetEvent<NavigationType>().Subscribe(o =>
{
Console.Write(o);
});
... and anywhere else, I can push events out, which will be received in the subscription:
_eventPublisher.Publish(NavigationType.MyOption1);
Add the Reactive Extensions (RX) NuGet package to your project.
Create this interface:
public interface IEventPublisher
{
IObservable<TEvent> GetEvent<TEvent>();
void Publish<TEvent>(TEvent sampleEvent);
}
public interface IEventPublisher<in T>
{
IObservable<TEvent> GetEvent<TEvent>() where TEvent : T;
void Publish<TEvent>(TEvent sampleEvent) where TEvent : T;
}
... with this implementation:
// NOTE: This class must be a singleton (there should only ever
// be one copy; this happens automatically in any dependency injection
// container). This class is the central dictionary that routes events
// of any incoming type, to all listeners for that same type.
[Export(typeof (IEventPublisher))]
public class EventPublisher : IEventPublisher
{
private readonly ConcurrentDictionary<Type, object> _subjects;
public EventPublisher()
{
_subjects = new ConcurrentDictionary<Type, object>();
}
public IObservable<TEvent> GetEvent<TEvent>()
{
return (ISubject<TEvent>)_subjects.GetOrAdd(typeof(TEvent), t => new Subject<TEvent>());
}
public void Publish<TEvent>(TEvent sampleEvent)
{
object subject;
if (_subjects.TryGetValue(typeof (TEvent), out subject))
{
((ISubject<TEvent>)subject).OnNext(sampleEvent);
}
// Could add a lock here to make it thread safe, but in practice,
// the Dependency Injection container sets everything up once on
// startup and it doesn't change from that point on, so it just
// works.
}
}
// NOTE: There can be many copies of this class, one for
// each type of message. This happens automatically in any
// dependency injection container because its a <T> class.
[Export(typeof (IEventPublisher<>))]
public class EventPublisher<T> : IEventPublisher<T>
{
private readonly IEventPublisher _eventPublisher;
[ImportingConstructor]
public EventPublisher(IEventPublisher eventPublisher)
{
_eventPublisher = eventPublisher;
}
public IObservable<TEvent> GetEvent<TEvent>() where TEvent : T
{
return _eventPublisher.GetEvent<TEvent>();
}
public void Publish<TEvent>(TEvent sampleEvent) where TEvent : T
{
_eventPublisher.Publish(sampleEvent);
}
}
This code shows how simple it is to send an event from any class to any other class.
As shown, you need to create a new custom type in order to send a message. The type can be an enum, a struct, or a class. If the type is a class or a struct, it can contain any number of properties. If a message is sent out using a specific custom type, all subscribers listening to messages of that type will receive it. You can create many custom types, one for each flavour of event you need to communicate with.
Behind the scenes, all the code is doing is keeping a dictionary of your custom types. On a send, it looks up the appropriate subscribers in the dictionary, then sends the message using Reactive Extensions (RX). All subscribers listening to that type will then receive the message.
Sometimes, if there are too many events flying everywhere, it's difficult to see which classes are communicating with which other classes. In this case, it's simple: you can use "Find in Files" to find all classes that contain the string IEventPublisher<NavigationType>
, which ends up listing all of the classes that are either sending or listening to an event of our custom type NavigationType
.
Beware: this code is not a silver bullet. It is a bad code smell to rely on events too much, as the class hierarchy should be composed in such a way that classes should not be dependent on their parents. For more information, study the SOLID principles, in particular, the LSP. However, sometimes use of events are unavoidable, as we have no choice but to cross the class hierarchy.
Currently, this Event Publisher does not implement IDisposable. It should.
Use EventAggregator
if you're not looking to do something overly elaborate.
http://blogs.msdn.com/b/gblock/archive/2009/02/23/event-aggregation-with-mef-with-and-without-eventaggregator.aspx
And a way to bring this into your project the MEFfy way:
https://msdn.microsoft.com/en-us/library/microsoft.practices.prism.mefextensions.events.mefeventaggregator(v=pandp.50).aspx
You could also write your own EventAggregator patter (per M. Fowler), but then you would have to take into consideration cleanly removing subscribed handlers, which will most likely lead you into the land of weak references and the horrors (or not) that lie there.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With