Click here to Skip to main content
15,886,083 members
Articles / Programming Languages / C#

Simplifying Events in .NET

Rate me:
Please Sign up or sign in to vote.
4.90/5 (28 votes)
25 Jan 2015CPOL20 min read 49.3K   471   78   11
Events in .NET are great. There is the "event" keyword that gives a fast way to start to use them, but..

Agenda

Introduction

Events in .NET are great. We have the event keyword that gives us a fast way to start to use them, but somehow the whole set can bring on things like: memory leaks, callback hell and a lot of extra code. In general it is because they have a lack of flexibility and they are hard to maintain. 

What I propose is an abstraction to handle the delegate model (which is a base of .NET event) that gives more flexibility, simplicity, and control about the code. In the solution I added Reactive Extensions making it easier to handle in general, combining them to achieve a good alternative in comparison with the .NET usage.

Background

When you have been using the delegate model for a while — in this part I assume that you know a little bit how .NET event works — you may notice possible problems or just characteristics that can disturb you, most of them are possible to resolve, but you will end up with more code around. Before I show you the generic solution I'll try to illustrate some of those things.

.NET Event Features and Weaknesses

Image 1

This is a common image used to explain the benefits of using Reactive Extensions (Rx) against the .NET events. I will go through the limitations and others characteristics in detail below.  

Starting from beginning to use an event you need to choose a delegate type. You cannot write it like this:

C#
public event int ValueRaised;

ERROR: "event must be of a delegate type". 

You have to declare your own delegate or use a common alternative like EventHandler<TEventArgs> Delegate.

C#
public event EventHandler<int> ValueRaised;

You may know or not known, but the code above works with .NET 4.5 or any major version. In this case it is not necessary to declare your own delegate just to propagate an int value, the type constraint in EventHandler<TEventArgs> was removed. It was simplified, which was great! It means that if you are not using the modern versions this is what you'll get:

ERROR: "The type 'int' cannot be used as type parameter 'TEventArgs' in the generic type or method 'System.EventHandler<TEventArgs>'. There is no boxing conversion from 'int' to 'System.EventArgs'."

To solve the error you have to create a derived class of System.EventArgs to use with the parameter.

Independent of the scenario you will probably need to declare a new delegate at some point, because you want a different signature, it can be beneficial if you are handling a specific case that has more than one different types of data and you don't want to encapsulate everything in an object. For example:

C#
public delegate void CustomHandler(object sender, string info, Foo args);

Ok, until now it's no big deal. For .NET event declaration you provide what is necessary, is just a few lines of code to make it usable. It could be better or different. It has good and bad points that are easy to get over. 

The common sense tells us that when we want to stop to use an event or you don't need it anymore it is necessary to remove the delegate to avoid a memory leak, in general it is relatively simple. Since you have used a method signature, you could do like the example below:

C#
public class Program
{
    static void Main(string[] args)
    {
        SampleOne();
        Console.Read();
    }

    private static void SampleOne()
    {
        var foo = new Foo();
        foo.ValueRaised += foo_ValueRaised;
        foo.OnRaiseValue(1); // show 1
        foo.ValueRaised -= foo_ValueRaised;
        foo.OnRaiseValue(2); // nothing happens
    }

    static void foo_ValueRaised(object sender, CustomEventArgs e)
    {
        Console.WriteLine(e.Value);
    }

}
C#
public class CustomEventArgs : EventArgs
{
     public int Value { get; set; }
}

 public class Foo
 {
     public event EventHandler<CustomEventArgs> ValueRaised;

     public void OnRaiseValue(int value)
     {
         if (ValueRaised != null)
             ValueRaised(this, new CustomEventArgs { Value = value });
     }
 }

Other example to remove the delegates of an event:

C#
public sealed class MyClass : IDisposable
{
    public event EventHandler<CustomEventArgs> ValueRaised;
    public void OnRaiseValue(int value)
    {
        if (ValueRaised != null)
            ValueRaised(this, new CustomEventArgs { Value = value });
    }

    public void Dispose()
    {
        ValueRaised = null;
    }
}
C#
static void Main(string[] args)
{
    //SampleOne();
    SampleTwo();
    Console.Read();
}

private static void SampleTwo()
{
    var my = new MyClass();
    my.ValueRaised += foo_ValueRaised;
    my.OnRaiseValue(1); // show 1
    my.Dispose();
    my.OnRaiseValue(2); // nothing happens
}

It is not necessary to implement IDisposable, you can deal with it in your own way to add and to remove a delegate. In case that you are using public class with a public event you should just take care of it.

It becomes not that simple when you have inheritance and abstraction in your application, because it is necessary to expose the backing field that contains the delegates (ValueRaised event), or to deal with it in the base class. In my example it wasn't necessary to declare the backing field, which means that if I created a derived class of Foo, the derived class wouldn't be able to raise the event, neither set null.

C#
public sealed class FooDerivedClass : Foo, IDisposable
{
    public void OnRaiseHalfValue(int value)
    {
        if (this.ValueRaised != null) // error
            this.ValueRaised(this, new CustomEventArgs { Value = value / 2 }); // error
    }

    public void Dispose()
    {
        this.ValueRaised = null; // error
    }
}

ERROR: The event 'NETEventResourceMaintenance.Foo.ValueRaised' can only appear on the left hand side of += or -= (except when used from within the type 'NETEventResourceMaintenance.Foo')

It is possible to see more limitations in this part when you want to use Anonymous Methods or Lambda Expressions. I will make the example with Lambda Expressions below:

C#
public static Task<RoutedEventArgs> WhenClickOnce(Button btn)
{
    var tcs = new TaskCompletionSource<RoutedEventArgs>();
    RoutedEventHandler handler = null;
    handler = new RoutedEventHandler((s, e) =>
        {
            tcs.SetResult(e);
            btn.Click -= handler;
        });
    btn.Click += handler;
    return tcs.Task;
}

This is similar to the famous example: how to convert a click in a Task to be awaited. It is a little bit messy. In general is not too difficult to understand, but you can imagine how it becomes worst in a more complex scenario, the result can be a C# callback hell, mostly because of the problem to remove a Lambda Expression delegate.

An important information to notice in this part is that to remove a delegate manually you always need to keep it in a variable or to use a method signature.

Depending of what you want to achieve is not a good practice but sometimes you don't want or you don't need to remove an event manually, for this scenarios there are good alternatives like: Weak Events in C#.NET Weak Events for the Busy ProgrammerThe-NET-weak-event-pattern-in-CsharpSharpObservation, and etc.

Although some of them work really well, you need to add extra code or an external library in your project and encapsulate the event usage with code-style used by the respective choice.

In some of the libraries it is not possible to make an Anonymous Function weak, in others only a delegate with a closure variable in the same class scope, not a local variable inside a method. An interesting part about a Lambda Expression, specifically those which don't closure a value, they are converted in a static method which is already weak by itself, and in this case there's no need to worry about memory leak.

The final score about a weak event is that it is a good thing but maybe your usage will be complex or will be limited.

This is an example that does not make any sense at all:

C#
[TestMethod]
public void FooTestMethod()
{
    var foo = new Foo();
    foo.ValueRaised += foo_ValueRaised;
    foo.ValueRaised += foo_ValueRaised;
    foo.OnRaiseValue(5);
}

void foo_ValueRaised(object sender, CustomEventArgs e)
{
    Trace.WriteLine("Foo Raised = " + e.Value);
}

This code will show "Foo Raised = 5" two times. Why you would execute the callback twice? It's better run a loop in the method if it's really necessary to do something similar.

You probably won't write the code above but it is possible that you want to avoid this scenario at all costs, maybe your project has huge classes, someone can do it by mistake or you just want to add an extra layer of protection in your event. The result will be something like:

C#
private EventHandler<CustomEventArgs> _handlers;
public event EventHandler<CustomEventArgs> ValueRaised
{
    add
    {
        var callbacks = _handlers;
        if (callbacks == null || callbacks.GetInvocationList().All(a => (EventHandler<CustomEventArgs>)a != value))
        {
            _handlers += value;
        }
    }
    remove
    {
        if (_handlers == null)
            return;

        _handlers -= value;
    }
}

public void OnRaiseValue(int value)
{
    if (_handlers != null)
        _handlers(this, new CustomEventArgs { Value = value });
}

There is more than one solution for this problem, anyway with this new code the execution will show "Foo Raised = 5" just one time. Even when you are using an Anonymous Delegates or Lambda Expressions this technique doesn't work all the time because — you know, they are anonymous — depends if it is a closure function and of the variables scope used in the closure. At least it can help you when you are using the method signature directly or — less unlikely — if you have the delegate stored in a variable and always use the same instance.

It not possible to pass an event to a method like an object, the operator to add (+) or to remove (-) a delegate results in a new MulticastDelegate instance. Since you notice that events are not "first class":

C#
public class FooChild
{
    private EventHandler<CustomEventArgs> _eventHandler;

    public FooChild(EventHandler<CustomEventArgs> eventHandler)
    {
        this._eventHandler = eventHandler;
        this._eventHandler += fooEvent_ValueRaised;
    }

    void fooEvent_ValueRaised(object sender, CustomEventArgs e)
    {
         Console.WriteLine("Raised " + e.Value + " in FooChild");
    }
}
C#
public Foo()
{
    var child = new FooChild(this.ValueRaised);
}

ERROR: The event 'Foo.ValueRaised' can only appear on the left hand side of += or -=.

You can have the delegates in this way:

C#
var child = new FooChild(this._handlers);

But they are immutable, so it doesn't make sense to store the instance in some place different, you will lose changes about new additions or removals in the original local, and if you add a delegate on that, the event call in the Foo will not execute it. I'm not going to cover pass the event by reference using the ref keyword, definitely the result would be poor and full of careful details in the usage.  

To overcome this part the result will need to encapsulate the event in a class, which we already did for the example and pass the entire class forward.

C#
        private Foo _foo;
        public FooChild(Foo foo)
        {
            if (foo == null)
               throw new ArgumentNullException("foo");

            this._foo = foo;
            this._foo.ValueRaised += foo_ValueRaised;
        }

        void foo_ValueRaised(object sender, CustomEventArgs e)
        {
             Console.WriteLine("Raised " + e.Value + " in FooChild");
        }

This is a common scenario applicable when the event is necessary in more than one piece of code, for example different modules or services in your application.  

Notice that we came back to the second problem, the resource maintenance. Other classes don't have access to handle it properly (like Foo class), they could just add or remove delegates, although it adds code coupling among the project in this case it is possible classifying like a "feature" because it introduces a kind of separation of concerns, this other classes would be just observers/consumers— whatever you want to call them — of the event. It is a good choice depending on what you want to achieve, otherwise you need to refactor and add new methods in the class that has the event.

When you have concurrency, often seen in a background process or a service application, it is necessary to protect your delegates. The event keyword doesn't introduce synchronization in the manipulation (add/remove) and execution. In the Foo class example our variable _handlers inside of class must be protected using a synchronization mechanism, e.g:

C#
private readonly object _sync = new object();

public event EventHandler<CustomEventArgs> ValueRaised
{
    add
    {
        lock (_sync)
        {
            if (_handlers == null || _handlers.GetInvocationList().All(a => (EventHandler<CustomEventArgs>)a != value))
            {
                _handlers += value;
            }
        }
    }
    remove
    {
        lock (_sync)
        {
            if (_handlers == null)
                return;

            _handlers -= value;
        }
    }
}

public void OnRaiseValue(int value)
{
    EventHandler<CustomEventArgs> handlers;
    lock (_sync)
    {
        handlers = _handlers;
    }
    if (handlers != null)
        handlers(this, new CustomEventArgs { Value = value });
}

It will avoid losing some delegate when you are adding or removing an event and before the execution get the most recent delegates. In this case the execution cannot be protected at all, it is not a good practice to call the delegates inside a lock, even though with this code that can "protect" the delegates in a multi-threaded app there is no perfect solution for thread safe events.

Notice that at each new difficulty you are going to aggregate more and more code to contour it.

The final limitation that I noticed about a .NET event is the lack of composition. It is the main limitation that forces me to use a replacement to deal with events. At every piece of code that you want to write about manipulating a event data you end up with:

a) A manual and imperative way to handle it, like:

C#
        static void Main(string[] args)
        {
            HandleEventManually();
            Console.Read();
        }

        private static void HandleEventManually()
        {
            Console.WriteLine("Handle event manually");
            var foo = new Foo();
            foo.ValueRaised += foo_ValueRaised;
            foo.OnRaiseValue(5); // nothing happens
            foo.OnRaiseValue(4); // 2 is evaluated
        }

        static void foo_ValueRaised(object sender, CustomEventArgs e)
        {
            if (e.Value <= 0)
                return;

            if ((e.Value % 2) == 0)
                return;

            var toProcess = e.Value / 2;
            ProcessEvenValue(toProcess);
        }

        private static void ProcessEvenValue(int value)
        {
            Console.WriteLine("Processing Value = " + value);
        }

b) The creation of a new event, with the same respective imperative code that will be raised just if the conditions are achieved.

C#
static void Main(string[] args)
{
    //HandleEventManually();
    HandleEventWithAnotherEvent();
    Console.Read();
}

private static event EventHandler<int> FooValidRaisedValues;
private static void HandleEventWithAnotherEvent()
{
    Console.WriteLine("Projection of an event into another");

    FooValidRaisedValues += Program_FooValidRaisedValues;

    var foo = new Foo();

    foo.ValueRaised += (s, e) =>
    {
        if (e.Value <= 0)
            return;

        if ((e.Value % 2) == 0)
            return;

        var handlers = FooValidRaisedValues;
        if (handlers != null)
            handlers(null, e.Value / 2);
    };

    foo.OnRaiseValue(5); // nothing happens
    foo.OnRaiseValue(4); // 2 is evaluated
}

static void Program_FooValidRaisedValues(object sender, int value)
{
    ProcessEvenValue(value);
}

The option 'b' can bring more problems, where would you put the other event? Encapsulated in another class? Declared just where you want to use (like I did)? With an anonymous function? Independent of the answer, it can brings all of the problems and limitations that I mentioned previously in this article.

It is possible that you come across a few difficulties to access the data source of the event, such as shown in the first image of the article (hidden data source), in part it happens because there is no easy way to do a simple data projection and also because of the EventHandler pattern, which is used a lot in .NET, resulting in some places the necessity to handle an abstract type in the event signature.

Others Considerations

If you go deep in the .NET event, you may face these and other details that you need to be careful or to take care of, most of them depends of the scenario that you want to cover. Once you understand how it works you probably will be fine combining some code with it.

Besides, event is a good thing that can help us to produce a better code when we use it correctly, why would it be such complicated? The bad part is being too much repetitive adding more code in your project because of many scenarios that you need or want to use.  Sometimes more is less, why not encapsulate this characteristics and save some time in a real implementation? 

 


The Alternative

As mentioned my main objective is to simplify the use of a .NET event. Unfortunately my solution doesn't have all the answers, but at least it can help you to produce a better, correct and clean code with less repetition.

With the intention of cover all the discussed points it was chosen to integrate Reactive Extensions in the solution because of one the Rx greatest points is that was created in order to make .NET event usage easier

Later I will go through the same features and weaknesses mentioned about .NET event, but in order to compare the image that were previously shown about the limitations I created a new one with the same points:

Image 2

It is important to mention that this image is just about Reactive Extensions and not about the solution. Now maybe you are wondering why not just use Rx? There are a several reasons that are possible to consider:

  1. Compatibility with .NET event without add extra code or complexity.
  2. You shouldn't implement IObservable and is not recommended to use Subjectswhich are some of the key types available in Rx to deal with events, although it is possible argue with that, the result will vary depending on the scenario used.
  3. Simplicity, if you will use the Subject<T> type or a similar ISubject<T> implementation beyond "OnNext" method there are "OnError" and "OnCompleted" methods and it is really rare the scenario that you want to create an event that handles these three things in just place (in this case I'm not considering the usage of a .NET component or a external API), because usually you will compose operations and the composition will generate errors or completions. In fact to get the same functionality it is possible to split each method to be a separate event.
  4. Sometimes it can produce a little bit of code verbose to convert .NET event in an Observable or you will not be comfortable with the code-style, especially if you will use a literal string to make the conversion when a definition of an event can change. 
  5. If you convert an event to an observable, some of the problems that I mentioned with the .NET event they won't be solved, it will depend on the internal implementation to add or to remove the delegate. 

Finally my proposal:

Everything about my proposal is attached to this article as a zip file. However I created a github repository for the most up-to-date code, in case of improvements or any other ideas related.

In order to make it flexible and extensible I created a structured solution:

Image 3

The interfaces will cover both approaches, the delegate model by .NET and the push model by Rx. In order to keep consistency it was implemented using the separation of concerns principle, which means that it is necessary to go through each accessor to use the preferable option.

In the left side there is the Observe() method that will return an Observable<out TArgs>, in the right side there is the Handler event, and in the middle an interface to publish a new value for those that will add delegates or subscribe observers.  

Another important point is that this structure will give you the possibility to make a choice between: the modern alternative approach that uses Rx or the common usage of an event also keeping it compatible with legacy code.

In addition these interfaces can be extended and implemented separately, also it is possible to compare them with just another Publish-Subscribe pattern, although the main objective is far from being a pattern, it allows a creation of an EventAggregator based on T type or any mechanism that can identify the event. 

C#
public sealed class EventSubject<TArgs> : IEventSubject<TArgs>, IDisposable
{
    #region [ Fields / Attributes ]
    private Action _disposeAction;
    private Action<TArgs> _delegates;

    private volatile bool _isDisposed;

    private readonly object _gateEvent = new object();
    #endregion

    #region [ Events / Properties ]
    public event Action<TArgs> Handler
    {
        add
        {
            RegisterEventDelegate(value);
        }
        remove
        {
            UnRegisterEventDelegate(value);
        }
    }
    #endregion

    private void AddDisposeAction(Action value)
    {
        if (_isDisposed)
            return;

        var baseVal = _disposeAction;
        while (true)
        {
            var newVal = baseVal + value;
            var currentVal = Interlocked.CompareExchange(ref _disposeAction, newVal, baseVal);

            if (currentVal == baseVal)
                return;

            baseVal = currentVal; // Try again
        }
    }

    private void RemoveDisposeAction(Action value)
    {
        var baseVal = _disposeAction;
        if (baseVal == null)
            return;

        while (true)
        {
            var newVal = baseVal - value;
            var currentVal = Interlocked.CompareExchange(ref _disposeAction, newVal, baseVal);

            if (currentVal == baseVal)
                return;

            baseVal = currentVal; // Try again
        }
    }

    private void RegisterEventDelegate(Action<TArgs> invoker)
    {
        if (invoker == null)
            throw new NullReferenceException("invoker");

        lock (_gateEvent)
        {
            CheckDisposed(); // checked inside of lock because of disposable synchronization

            if (IsAlreadySubscribed(invoker))
                return;

            AddActionInternal(invoker);
        }
    }

    private bool IsAlreadySubscribed(Action<TArgs> invoker)
    {
        var current = _delegates;
        if (current == null)
            return false;

        var items = current.GetInvocationList();
        for (int i = items.Length; i-- > 0; )
        {
            if ((Action<TArgs>)items[i] == invoker)
                return true;
        }
        return false;
    }

    private void UnRegisterEventDelegate(Action<TArgs> invoker)
    {
        if (invoker == null)
            return;

        lock (_gateEvent)
        {
            var baseVal = _delegates;
            if (baseVal == null)
                return;

            RemoveActionInternal(invoker);
        }
    }

    private void AddActionInternal(Action<TArgs> invoker)
    {
        var baseVal = _delegates;
        while (true)
        {
            var newVal = baseVal + invoker;
            var currentVal = Interlocked.CompareExchange(ref _delegates, newVal, baseVal);

            if (currentVal == baseVal) // success
                return;

            baseVal = currentVal;
        }
    }

    private void RemoveActionInternal(Action<TArgs> invoker)
    {
        var baseVal = _delegates;
        while (true)
        {
            var newVal = baseVal - invoker;
            var currentVal = Interlocked.CompareExchange(ref _delegates, newVal, baseVal);

            if (currentVal == baseVal)
                return;

            baseVal = currentVal; // Try again
        }
    }
    public IObservable<TArgs> Observe()
    {
        return Observable.Defer(() =>
        {
            CheckDisposed();
            return Observable.FromEvent<TArgs>(AddActionInternal, RemoveActionInternal);
        })
        .TakeUntil(Observable.FromEvent(AddDisposeAction, RemoveDisposeAction));
    }

    public void OnNext(TArgs value)
    {
        CheckDisposed();

        var current = _delegates;
        if (current == null)
            return;

        current(value);
    }

    public void Dispose()
    {
        _isDisposed = true;
        _delegates = null;

        try
        {
            var disposeDelegates = _disposeAction;
            if (disposeDelegates == null)
                return;

            _disposeAction = null;
            disposeDelegates();
        }
        finally
        {
            lock (_gateEvent)
            {
                _delegates = null; // re-clean with sync
            }
        }
    }
    private void CheckDisposed()
    {
        if (_isDisposed)
        {
            ThrowDisposed();
        }
    }

    private void ThrowDisposed()
    {
        throw new ObjectDisposedException(this.GetType().Name);
    }
}

EventSubject<TArgs> Characteristics

Now let's compare every aspect of my purpose solution, like I described about a .NET event.

To use an EventSubject<TArgs> is simple, it is just like any other object in .NET. Only choose any T type that you want and instantiate it.

C#
private readonly EventSubject<int> _eventSubject = new EventSubject<int>();

 As well I created a helper:

C#
var eventSubject = EventSubject.Create<int>();

This is a delicate aspect to cover all the possibilities, the main achievement was include more flexibility. You can choose between:

  1. The same .NET event

In this case it is possible integrate with regular code. This is the traditional way to use it, allowing you to use a weak event technique (for the subscriber side) or other kind of code that you normally use to handle an event.

C#
        private readonly EventSubject<int> _eventSubject = new EventSubject<int>();
        private bool _delegateCalled;
        
        [TestMethod]
        public void EventSubjectHandleTest()
        {
            _delegateCalled = false;
            _eventSubject.Handler += eventSubject_Handle;
            _eventSubject.OnNext(1);
            _eventSubject.Handler -= eventSubject_Handle;
            Assert.IsTrue(_delegateCalled);

            _delegateCalled = false;
            _eventSubject.OnNext(2);
            Assert.IsFalse(_delegateCalled);
        }

        private void eventSubject_Handle(int value)
        {
            _delegateCalled = true;
            Trace.WriteLine(string.Format("Doing something with '{0}' value.", value));
        }

About a conversion to EventHandler pattern I will approach this subject in the later in the article.

  1. The push model implemented by Reactive Extensions

That has some advantages, after get the IObservable<T> (calling the Observe() method) you need to subscribe an observer, there are many ways to do it with Rx library (e.g.: using the Subscribe() extension method), after that you will start to listen the new values pushed by the EventSubject class, the important part here is that the subscription of an observer returns an IDisposable, which you can call Dispose() when you need to take care of the subscription, in case that you want or need to stop to receive data.

C#
[TestMethod]
public void EventSubjectObsevableTest()
{
    _delegateCalled = false;

    var disp =  _eventSubject.Observe().Subscribe(_ => _delegateCalled = true);
    _eventSubject.OnNext(1);
    disp.Dispose();
    Assert.IsTrue(_delegateCalled);

    _delegateCalled = false;
    _eventSubject.OnNext(2);
    Assert.IsFalse(_delegateCalled);
}

In addition there are a couple of other options in Rx, which gives more facilities to use. Some of the operators can stop the subscription themselves, it means if I you have a condition to interrupt you don't need to worry about disposing a subscription.

C#
[TestMethod]
public void EventSubjectObservableUnsubscribeTestV1()
{
    _delegateCalled = false;

    _eventSubject.Observe().Take(1)
                           .Subscribe(_ => _delegateCalled = true);
    _eventSubject.OnNext(1);
    Assert.IsTrue(_delegateCalled);

    _delegateCalled = false;
    _eventSubject.OnNext(2);
    Assert.IsFalse(_delegateCalled);
}

[TestMethod]
public void EventSubjectObservableUnsubscribeTestV2()
{
    _delegateCalled = false;

    _eventSubject.Observe()
                .TakeWhile(next => next < 2)
                .Subscribe(_ => _delegateCalled = true);
    _eventSubject.OnNext(1);
    Assert.IsTrue(_delegateCalled);

    _delegateCalled = false;
    _eventSubject.OnNext(2);
    Assert.IsFalse(_delegateCalled);
}

Independent of the choice about resource maintenance the EventSubject<TArgs> instance also implements the IDisposable interface to clean up all the delegates, giving you an assurance that the subscribers (delegates with their respective objects) will be collected by the Garbage Collector.

C#
_eventSubject.Dispose();

For deal with repeatable listeners, in case of the .NET event usage in EventSubject (through the Handler accessor) I used the same technique that I explained before.

It is important to highlight that it doesn't apply in the Rx way, the responsibility to use is fully focused in who will observe. To handle more than one IObserver<T> for the same IObservable<T> the push event model has it own behaviors that depends which operator will be used, there is a concept that distinct hot and cold observables. I'm not going deep in Rx, but the Observe() method returns a cold observable that subscribes in a hot observable inside of it (the real event), which means that each time when an observable is subscribed a side-effect subscription will be created, the side-effect code is used to check if the entire object was disposed previously.

The solution below is not too related with the class, it is more about Rx and C#.

  1. The way to pass the event for someone subscribe directly is calling Observe() method that returns IObservable<out T> object type and you can have the instance without a class wrapped. 
C#
public class Foo
{
    private IObservable<System.Reactive.Unit> _observable;

    public Foo(IObservable<System.Reactive.Unit> observable)
    {
        this._observable = observable;
    }

    public void StartListen()
    {
        this._observable.Subscribe(_ => NextEvent());
    }

    private void NextEvent()
    {
        EventCount++;
    }

    public int EventCount { get; set; }

}
[TestMethod]
public void EventSubjectPassJustAnEvent()
{
    var ev = new EventSubject<System.Reactive.Unit>();
    var foo = new Foo(ev.Observe());
    foo.StartListen();
    ev.OnNext(System.Reactive.Unit.Default);
    Assert.IsTrue(foo.EventCount == 1);
    ev.Dispose();

    try
    {
        foo.StartListen();
    }
    catch (ObjectDisposedException ex)
    {
        Assert.IsTrue(ex.Message.Contains(typeof(EventSubject<System.Reactive.Unit>).Name));
    }
}

One other way for a classic .NET event is getting the observable and call Observable.ToEvent method, that returns an object, which  is also a wrapper with the event declared to use, doesn't worth to use because add code and complexity, it is better to stay with IEventDelegate interface.

  1. The way to pass the event for someone raise it is transforming Publish(TArgs value) method in an Action<in TArgs>. In this case there's nothing related with the solution, it is just C#.
C#
public class Fire
{
    private Action<System.Reactive.Unit> _raiseValue;
    public Fire(Action<System.Reactive.Unit> raiseValueDelegate)
    {
        this._raiseValue = raiseValueDelegate;
    }

    public void Shoot()
    {
        this._raiseValue(System.Reactive.Unit.Default);
    }
}

[TestMethod]
public void EventSubjectPassToPublish()
{
    Fire f;
    using (var ev = new EventSubject<System.Reactive.Unit>())
    {
        bool eventCalled = false;
        var disp = ev.Observe().Subscribe(_ => eventCalled = true);
        f = new Fire(ev.OnNext);
        f.Shoot();
        Assert.IsTrue(eventCalled);
        eventCalled = false;
        disp.Dispose();
        f.Shoot();
        Assert.IsFalse(eventCalled);
    }
    try
    {
        f.Shoot();
    }
    catch (ObjectDisposedException ex)
    {
        Assert.IsTrue(ex.Message.Contains(typeof(EventSubject<System.Reactive.Unit>).Name));
    }
}

Independent of the case that you want to use, the entire class is an object, it can be handle like any reference type in your code, giving you more flexibility. In addition if you want to protect the code it is possible use the interfaces types used in the solution (previously shown in the class diagram image).

For subscribers: IEventObservable<out TArgs> and IEventDelegate<out TArgs>

For publishers: IEventPublisher<in TArgs>

For both: IEventSubject<TArgs>

If you read the article about thread safe event, there is no perfect way. For this solution I used other alternative to handle this scenario, except for the part that I need to execute a duplicate delegate comparison, the entire class has a lock-free data structure, which is enough to be used for more than one thread concurrently. If you are interested in this technique you can check more information here. The main benefit is call the event as fast as possible, even before the execution is tested if there is any delegate to call and if the class was disposed previously.

Here is the main part where Reactive Extensions acts. Since you have familiarity with Rx you just call Observe() method that returns an IObservable<out T> and start to compose operations, such as LINQ for collection, but for events, like I already mentioned, in addition you gain a better way to handle the resource maintenance.

The following examples are the big five to learn Rx proposed by Jafair Hussain from Netflix. It was made by me not by him or his team.

  1. Map
C#
[TestMethod]
public void ObservableMap() // Using Select Operator
{
    var values = new List<string>();
    using (var ev = new EventSubject<Tuple<int, string>>())
    using (var sub = ev.Observe().Select(obj => obj.Item2)
                                .Subscribe(values.Add))
    {
        ev.OnNext(new Tuple<int, string>(1, "First"));
        ev.OnNext(new Tuple<int, string>(2, "Second"));
        ev.OnNext(new Tuple<int, string>(3, "Third"));

        Assert.IsTrue(values.SequenceEqual(new[] { "First", "Second", "Third" }));
    }
}
  1. Filter
C#
[TestMethod]
public void ObservableFilter() // Using Where Operator
{
    var values = new List<int>();
    using (var ev = new EventSubject<Tuple<int, string>>())
    using (var sub = ev.Observe().Where(obj => (obj.Item1 % 2) == 0)
                                .Subscribe(a => values.Add(a.Item1)))
    {
        ev.OnNext(new Tuple<int, string>(1, "First"));
        ev.OnNext(new Tuple<int, string>(2, "Second"));
        ev.OnNext(new Tuple<int, string>(3, "Third"));
        ev.OnNext(new Tuple<int, string>(4, "Fourth"));

        Assert.IsTrue(values.SequenceEqual(new[] { 2, 4 }));
    }
}
  1. Reduce
C#
[TestMethod]
public void ObservableReduce() // Using Sum Operator
{
    var values = new List<int>();
    var ev = new EventSubject<Tuple<int, string>>();

    using (var sub = ev.Observe().Sum(a => a.Item1)
                                .Subscribe(values.Add))
    {
        ev.OnNext(new Tuple<int, string>(1, "First"));
        ev.OnNext(new Tuple<int, string>(2, "Second"));
        ev.OnNext(new Tuple<int, string>(3, "Third"));
        ev.OnNext(new Tuple<int, string>(4, "Fourth"));

        ev.Dispose();

        Assert.IsTrue(values.SequenceEqual(new[] { 10 }));
    }
}
  1. Merge
C#
[TestMethod]
public void ObservableMerge() // Using Merge Operator
{
    var values = new List<Tuple<int,String>>();
    using (var left = new EventSubject<Tuple<int, string>>())
    using (var right = new EventSubject<Tuple<int,string>>())
    using (var sub = left.Observe().Merge(right.Observe())
                                .Subscribe(values.Add))
    {
        left.OnNext(new Tuple<int, string>(1, "First"));
        right.OnNext(new Tuple<int, string>(2, "Two"));
        left.OnNext(new Tuple<int, string>(3, "Third"));
        right.OnNext(new Tuple<int, string>(4, "Four"));

        Assert.IsTrue(values.SequenceEqual(new[] { Tuple.Create(1,"First"), Tuple.Create(2, "Two"), Tuple.Create(3, "Third"), Tuple.Create(4, "Four") }));
    }
}
  1. Zip
C#
[TestMethod]
public void ObservableZip() // Using Zip Operator
{
    var values = new List<Tuple<int, String, String>>();
    using (var left = new EventSubject<Tuple<int, string>>())
    using (var right = new EventSubject<Tuple<int, string>>())
    using (var sub = left.Observe().Zip(right.Observe(), (a, b) => new { a, b })
                                .Where(next => next.a.Item1 == next.b.Item1)
                                .Select(next => Tuple.Create(next.a.Item1, next.a.Item2, next.b.Item2))
                                .Subscribe(values.Add))
    {
        left.OnNext(new Tuple<int, string>(1, "First"));
        right.OnNext(new Tuple<int, string>(1, "One"));

        left.OnNext(new Tuple<int, string>(2, "Second"));
        left.OnNext(new Tuple<int, string>(3, "Third"));

        right.OnNext(new Tuple<int, string>(2, "Two"));
        right.OnNext(new Tuple<int, string>(3, "Three"));

        Assert.IsTrue(values.SequenceEqual(new[] { Tuple.Create(1, "First", "One"), Tuple.Create(2, "Second", "Two"), Tuple.Create(3, "Third", "Three") }));
    }
}

To get the same data of the famous EventHandler it is possible to use EventPattern<T> class, which has a sender and an argument inside, as TArgs type of the EventSubject.

To use with the same signature of an EventHandler for the event subscribers it is a little bit more complicated, you can convert from observable OR create an implementation of IEventSubject<TArgs> — I experimented this second option, unfortunately the code result it is almost a copy of the EventSubject implementation.

In order to keep IDisposable implementation simple, the synchronization object and the lock-free algorithms safe, the classes that extend IEventSubject<TArgs> interface are sealed.

I didn't run performance tests, if you compare with a direct and clean event usage, of course the purposed solution will probably lost in performance. I believe that the benefits overcome the losses in this case, it is a kind of thing that always happen when you add a new layer of code, unless you have thousands of event raising per second combined with adding and removing callbacks, you should be fine with it.

Conclusion

Making an event easy to deal is not as that simple in the implementation if you consider to solve all the technical problems and limitations, but in order to be more productive let this solution takes care of the event and just code, give life to your project being at least simple in the usage.

Nowadays there are a lot of generic solutions for everything, but if simplicity is necessary it is not possible to be too generic. As we can see even faced with so many points sometimes it is easy to code a way out, sometimes not, it is necessary to make choices, although I tried to cover almost all the possibilities usually the best thing that you can do is take off a feature or set aside, only ensure that the same code will not face it later.

Finally it is important to highlight the Reactive Extensions library, I believe that it came to stay and will continue to grow in utilization over the time, and certainly it is the major point in this solution. By several reasons that I explained why not integrate Rx and this solution in all your projects and start to compose operations to enjoy a powerful way to deal with events?

Final Thoughts

A while ago before starting to write this article I was trying to prove to myself that events are the main solution for a large variety of the problems, mostly because of Rx usage. Now I am more open-minded, as a back-end developer given the features and limitations it is a great idea to study with caution where it is good to apply an event model, independent if it is the classic .NET delegate model or the push model by Rx.  

It would be necessary to answer some questions, like: I need to use an event for a scenario with more than one data consumer? It would be a good idea create an event just in case that would be interesting to use it in the future? In the opposite direction today it is impossible to avoid the usage of callbacks to handle a variety of libraries and progressively we have more of the push processing model in the applications, why not just compose operations?

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Canada Canada
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionLock free Pin
Dzmitry Lahoda19-Oct-16 2:22
Dzmitry Lahoda19-Oct-16 2:22 
AnswerRe: Lock free Pin
John L. Vidal4-Jan-17 8:29
John L. Vidal4-Jan-17 8:29 
Praise5 star from me Pin
Athar Anis27-Jun-16 12:53
Athar Anis27-Jun-16 12:53 
Questionafter reading again Pin
Sacha Barber11-Mar-15 20:11
Sacha Barber11-Mar-15 20:11 
AnswerRe: after reading again Pin
John L. Vidal22-Mar-15 12:45
John L. Vidal22-Mar-15 12:45 
GeneralNice though, my +4 Pin
Liju Sankar9-Feb-15 1:04
professionalLiju Sankar9-Feb-15 1:04 
Questionjust stop using events Pin
Sacha Barber6-Feb-15 21:43
Sacha Barber6-Feb-15 21:43 
GeneralMy vote of 5 Pin
ChrMa27-Jan-15 20:05
professionalChrMa27-Jan-15 20:05 
GeneralMy vote of 5 Pin
hutz27-Jan-15 9:47
hutz27-Jan-15 9:47 
SuggestionNice article, needs more examples Pin
woutercx25-Jan-15 21:38
woutercx25-Jan-15 21:38 
GeneralRe: Nice article, needs more examples Pin
Pete O'Hanlon26-Jan-15 1:23
mvePete O'Hanlon26-Jan-15 1:23 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.