Click here to Skip to main content
14,451,065 members

Weak Events in .NET using Reactive Extensions (Rx)

Rate this:
5.00 (8 votes)
Please Sign up or sign in to vote.
5.00 (8 votes)
12 Feb 2016CPOL
Subscribe weakly to an event using Reactive Extensions

Introduction

Reactive Extensions (Rx) are a very powerful tool created to aid the programmer dealing with asynchronous streams and has a large amount of built in functionality that will help you to create Linq queries of data streams in almost any situation. However, one thing is missing in the toolbox and that is the ability to subscribe weakly to an event.

The method I will use is described in the article Weak Events in .NET, the easy Way by Samuel Jack. This tip is only a different and hopefully cleaner way of implementing the same method. I should mention that the code Samuel provides in his article has a typo which you will need to correct if you want to Subscribe weakly to an event (I do point out how to correct this at the end of this tip).

Just in case you are unfamiliar with weak events, I would recommend you to first read the Weak References documentation provided by Microsoft, followed by the article Weak Events in C# by Daniel Grunwald.

This is a really small project written as a Console application, and if you wonder why the file size is almost 8 MB, it's because it contains the full Rx framework. I will thus use the Reactive Extension library to create a weak event listener, basically extending the library with a new method called SubscribeWeakly.

Create the IObservable

To be able to subscribe to an event either weakly or normally using Rx, you will need to first create the IObservable that will basically tell Rx how to wrap the event handler in a IObservable<T>. To subscribe to a property that implements INotifyPropertyChanged, you must create a method similar to the one below:

public static IObservable<EventPattern<PropertyChangedEventArgs>> ObserveOn<TSource,
TProperty>(this TSource collection, Expression<Func<TSource, TProperty>> propertyExpr)
 where TSource : INotifyPropertyChanged
{
    Contract.Requires(collection != null);
    Contract.Requires(propertyExpr != null);

    var body = propertyExpr.Body as MemberExpression;

    if (body == null)
        throw new ArgumentException
        ("The specified expression does not reference a property.", "property");

    var propertyInfo = body.Member as PropertyInfo;

    if (propertyInfo == null)
        throw new ArgumentException
        ("The specified expression does not reference a property.", "property");

    string propertyName = propertyInfo.Name;

    return Observable.FromEventPattern
     <PropertyChangedEventHandler, PropertyChangedEventArgs>(
        h => h.Invoke,
            handler => collection.PropertyChanged += handler,
            handler => collection.PropertyChanged -= handler)
    .Where(evt => evt.EventArgs.PropertyName == propertyName);
}

At first glance, the method call can seem quite complex as I thought the first time I saw an Expression tree. The code is a pretty straight forward way of enabling you to write out the property name with the help of intellisense as this:

var someclass = new PropertyExampleClass();
someclass.ObserveOn(o => o.Name);

Admittedly, I could create the same functionality using a string for the property name (which would make the code a lot shorter). The reason for using Expression tree is to reduce the possibility of errors caused by incorrectly spelling the property name.

Similar to the PropertyChanged implementation, I will now create a way to subscribe to a CollectionChanged event. This part is however directly linked to a property that implements NotifyCollectionChanged (typically an ObservableCollection), so there is no need for an Expression tree:

public static IObservable<EventPattern<NotifyCollectionChangedEventArgs>>
       ObserveOn(this INotifyCollectionChanged collection)
{
    return Observable.FromEventPattern<NotifyCollectionChangedEventHandler,
                      NotifyCollectionChangedEventArgs>(
        handler => (sender, e) => handler(sender, e),
        handler => collection.CollectionChanged += handler,
        handler => collection.CollectionChanged -= handler);
}

Please note that if you are using a different version of Rx than I have (Release 2.2.5), the function FromEventPattern might take different parameters.

Subscribe to the Event

The ObserveOn methods makes it really easy to add an event handler via the Subscribe method by writing the following code:

var collection = new ObservableCollection<object>();
collection.ObserveOn().Subscribe(delegate
{ Console.WriteLine("Event Received By Strong Subscription"); });

The problem with that code is that it creates a strong reference between the property and the event handler, which was exactly what I wanted to avoid.

In order to achieve the separation between the property and where the event subscriber lives, you need to use the WeakReferance and the method IsAlive to determine if the event should be disposed of. In Samuel Jacks code, he uses a static method as the event handler delegate and calls it within the class that will subscribe to the event weakly. By using the static method, he ensures that the event handler wouldn't create a strong reference to the class. This is a bit of cheating, as we do now have a single instance static method that will never ever be garbage collected, but will allow the rest of the class to be cleaned up.

His method does works very well, and it is relatively easy to implement. However, whenever I design some extension methods, I make a point of creating them as similar to the original event as possible. This will hopefully reduce any wrong usage of it, as you would only need to know how to use the original method to be able to use the extension.

In this case, I want to create a method called SubscribeWeakly that is used exactly the same as the Subscribe method in Rx. So my solution was to create the SubscribeWeakly extension by the following code:

public static IDisposable SubscribeWeakly<T>
(this IObservable<T> observable, Action<T> onNext) where T :class
{
    IDisposable Result = null;
    WeakSubscriberHelper<T> SubscriptionHelper =
    new WeakSubscriberHelper<T>(observable, ref Result, onNext);
    return Result;
}

For the SubscribeWeakly to be as general as possible, I use the generic T where T is of type class. You should also note that the IObservable<T> will have an event handler that returns Action<T>. If you don't do this, you would have to create a SubscribeWeakly method to all different argument types, which would also include custom arguments.

As you probably already noticed, it creates a helper class called WeakSubscriberHelper<T>, which contains the static handler and the call to it. We also preserve the call value of class T.

private class WeakSubscriberHelper<T> where T :class
{
    public WeakSubscriberHelper(IObservable<T> observable,
           ref IDisposable Result, Action<T> eventAction)
    {
       Result = observable.InternalSubscribeWeakly
                (eventAction, WeakSubscriberHelper<T>.StaticEventHandler);
    }

    public static void StaticEventHandler(Action<T> subscriber, T item)
    {
        subscriber(item);
    }
}

Note that this implementation is different from Samuel's, he is using the static void to do the eventhandling by passing the argument T directly to it. I use the static method to pass in a WeakReferance to the Action<T> subscriber, and invoke that item T that came from the subscribe within InternalSubscribeWeakly. It is very important that the Action<T> in the static void is a WeakReferance, otherwise the subscription will create a strong reference, hence it will not be a weak event listener.

The code inside InternalSubscribeWeakly method is almost the same as the original implementation by Samuel Jack, with just a slight twist as a mentioned before, I set the onNext as the WeakReferance. This also makes much more sense than manually specifying the class that contains the method.

I did, for the sake of completion, also include the same implementation as Samuel did, where you had to specify the class that should have a weak reference. That in turn, meant that the internal call had to contain two weak references instead of one, so I'd advise against using it though.

private static IDisposable InternalSubscribeWeakly
<TEventPattern, TEvent>(this IObservable<TEventPattern> observable,
TEvent Weak_onNext, Action<TEvent, TEventPattern> onNext)
where TEvent : class
{
    if (onNext.Target != null)
        throw new ArgumentException("onNext must refer to a static method,
        or else the subscription will still hold a strong reference to target");

    // Is the eventhandler alive?
    var Weak_onNextReferance = new WeakReference(Weak_onNext);

    IDisposable subscription = null;
    subscription = observable.Subscribe(item =>
    {
        var current_onNext = Weak_onNextReferance.Target as TEvent;
        if (current_onNext != null)
        {
            onNext(current_onNext, item);
        }
        else
        {
            subscription.Dispose();
        }
    });
    return subscription;
}

This is really all you need to do, so it's time to test the implementation.

Test and How to (Not) Use It

To test that it actually works as predicted, I perform the same that's as Samual Jack did:

class Program
   {
       static void Main(string[] args)
       {
           var collection = new ObservableCollection<object>();

           var strongSubscriber = new StrongSubscriber();
           strongSubscriber.Subscribe(collection);

           var weakSubscriber = new WeakSubscriber();
           weakSubscriber.Subscribe(collection);

           collection.Add(new object());

           strongSubscriber = null;
           weakSubscriber = null;

           GC.Collect();
           Console.WriteLine("Full collection completed");

           collection.Add(new object());

           Console.Read();
       }
   }

   public class StrongSubscriber
   {
       public void Subscribe(ObservableCollection<object> collection)
       {
           collection.ObserveOn().Subscribe(HandleEvent);
       }

       private void HandleEvent(EventPattern<NotifyCollectionChangedEventArgs> item)
       {
           Console.WriteLine("Event Received By Strong Subscription");
       }

   }

   public class WeakSubscriber
   {

       public void Subscribe(ObservableCollection<object> collection)
       {
           collection.ObserveOn().SubscribeWeakly(HandleEvent);
       }

       private void HandleEvent(EventPattern<NotifyCollectionChangedEventArgs> item)
       {
           Console.WriteLine("Event received by Weak subscription");
       }
   }

For the record, the typo that Samuel made inside his article is that he forgot to write the static keyword for the HandleEvent void:

private class WeakSubscriber
{
    public void Subscribe(ObservableCollection<object> collection)
    {
        collection.ObserveCollectionChanged().SubscribeWeakly
                   (this, (target, item) => target.HandleEvent(item));
    }

    private static void HandleEvent(EventPattern<NotifyCollectionChangedEventArgs> item)
    {
        Console.WriteLine("Event received by Weak subscription");
    }
}

There is a bit of a catch by using the SubscribeWeakly method that you must know. You cannot write an inline subscription like the one below:

public void Subscribe(ObservableCollection<object> collection)
{
    collection.ObserveOn().SubscribeWeakly(delegate
     { Console.WriteLine("Event Received By weak Subscription"); });
}

or by this method (which is actually the same):

public void Subscribe(ObservableCollection<object> collection)
{
    collection.ObserveOn().SubscribeWeakly(x=>
    {
        Console.WriteLine("Event received by Weak subscription");
    });
}

This would essentially create the delegate where the collection lives, and not in the class that makes the subscription. It is, however, possible to pass the HandleEvent with a delegate call and it will still remain a weak event:

public void Subscribe(ObservableCollection<object> collection)
{
    collection.ObserveOn().SubscribeWeakly(x=>HandleEvent(x));
}

private void HandleEvent(EventPattern<NotifyCollectionChangedEventArgs> item)
{
    Console.WriteLine("Event received by Weak subscription");
}

The above code will behave in the exact same manner as the one below:

public class WeakSubscriber
{
    public void Subscribe(ObservableCollection<object> collection)
    {
        collection.ObserveOn().SubscribeWeakly(HandleEvent);
    }

    private void HandleEvent(EventPattern<NotifyCollectionChangedEventArgs> item)
    {
        Console.WriteLine("Event received by Weak subscription");
    }
}

In short, you would have to make sure that the EventHandler lives in the class you wish to have the weak event subscription. This should follow by the implementation details, but I repeat it here to make sure you get it.

Epilog

This is one of my first adventures into the world of Rx and I find it amazing, but the journey to create this application would have taken me much longer if not for the assistance of some very professional Code Project members that answer questions in their own time, as well as people writing short tips and articles on the subject sharing their knowledge with me.

History

  • 12th February, 2016: Initial version

License

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

Share

About the Author

Kenneth Haugland
Engineer
Norway Norway
I hope that you like the stuff I have created and if you do wish to say thank you then a donation is always appreciated.
You can donate here[^].

Comments and Discussions

 
QuestionNot working as intended? Pin
Member 1073981029-Jun-16 8:26
MemberMember 1073981029-Jun-16 8:26 
QuestionI can't see why/how is useful to use that Pin
Member 1234420022-Feb-16 15:05
MemberMember 1234420022-Feb-16 15:05 
AnswerRe: I can't see why/how is useful to use that Pin
Kenneth Haugland23-Feb-16 3:09
professionalKenneth Haugland23-Feb-16 3:09 
QuestionDo you use ReactiveUI? Pin
angel.machin16-Feb-16 1:25
Memberangel.machin16-Feb-16 1:25 
AnswerRe: Do you use ReactiveUI? Pin
Kenneth Haugland16-Feb-16 9:30
professionalKenneth Haugland16-Feb-16 9:30 
QuestionGood Pin
Sacha Barber12-Feb-16 21:02
MemberSacha Barber12-Feb-16 21:02 
AnswerRe: Good Pin
Kenneth Haugland12-Feb-16 22:13
professionalKenneth Haugland12-Feb-16 22:13 
GeneralRe: Good Pin
Sacha Barber13-Feb-16 1:26
MemberSacha Barber13-Feb-16 1:26 
GeneralRe: Good Pin
Kenneth Haugland13-Feb-16 2:14
professionalKenneth Haugland13-Feb-16 2:14 
GeneralRe: Good Pin
Pete O'Hanlon13-Feb-16 6:03
communityengineerPete O'Hanlon13-Feb-16 6:03 
GeneralRe: Good Pin
Kenneth Haugland13-Feb-16 19:28
professionalKenneth Haugland13-Feb-16 19:28 
GeneralRe: Good Pin
Sacha Barber13-Feb-16 22:14
MemberSacha Barber13-Feb-16 22:14 
AnswerRe: Good Pin
Kenneth Haugland13-Feb-16 19:27
professionalKenneth Haugland13-Feb-16 19:27 
GeneralRe: Good Pin
Sacha Barber13-Feb-16 22:12
MemberSacha Barber13-Feb-16 22:12 
GeneralRe: Good Pin
Kenneth Haugland14-Feb-16 12:45
professionalKenneth Haugland14-Feb-16 12:45 
AnswerRe: Good Pin
Kenneth Haugland16-Feb-16 7:57
professionalKenneth Haugland16-Feb-16 7:57 
QuestionNice one Pin
Pete O'Hanlon12-Feb-16 11:26
communityengineerPete O'Hanlon12-Feb-16 11:26 
AnswerRe: Nice one Pin
Kenneth Haugland12-Feb-16 22:19
professionalKenneth Haugland12-Feb-16 22:19 

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.

Tip/Trick
Posted 12 Feb 2016

Tagged as

Stats

27.2K views
190 downloads
18 bookmarked