Why WeakEventManager does not fire an event when the sender is not the nominal?

2.6k views Asked by At

I don't like off-the-standard pattern, but I was making a quick test on my app, and I bumped against this strange behavior.

Consider a normal class exposing an event, here the very common PropertyChanged, but I think could be any other.

The subscriber chooses to subscribe the event via the WeakEventManager helper. Now, the "odd" thing is the actual sender reference: as long the instance is the same as was used on the subscription, everything goes fine. However, when you use another object, no notification will be issued.

Again, that's NOT a good pattern, but I wonder whether there is any good reason for this limitation, or rather that is a kind a bug. More a curiosity than a real need.

class Class1
{
    static void Main(string[] args)
    {
        var c = new MyClass();

        WeakEventManager<INotifyPropertyChanged, PropertyChangedEventArgs>.AddHandler(
            c,
            "PropertyChanged",
            Handler
            );

        c.ActualSender = c;
        c.Number = 123;  //will raise

        c.ActualSender = new Class1();
        c.Number = 456;  //won't raise

        Console.ReadKey();
    }

    static void Handler(object sender, PropertyChangedEventArgs e)
    {
        Console.WriteLine("Handled!");
    }
}

class MyClass : INotifyPropertyChanged
{
    public object ActualSender { get; set; }


    private int _number;
    public int Number
    {
        get { return this._number; }
        set
        {
            if (this._number != value)
            {
                this._number = value;
                this.OnPropertyChanged("Number");
            }
        }
    }


    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged(
        string name
        )
    {
        this.PropertyChanged(
            this.ActualSender, 
            new PropertyChangedEventArgs(name)
            );
    }
}

EDIT: here is a rough way to achieve the expected behavior (hard-links for sake of simplicity).

class Class1
{
    static void Main(string[] args)
    {
        var cx = new MyClass();
        var cy = new MyClass();

        Manager.AddHandler(cx, Handler1);
        Manager.AddHandler(cx, Handler2);
        Manager.AddHandler(cy, Handler1);
        Manager.AddHandler(cy, Handler2);

        cx.ActualSender = cx;
        cx.Number = 123;

        cx.ActualSender = new Class1();
        cx.Number = 456;

        cy.ActualSender = cy;
        cy.Number = 789;

        cy.ActualSender = new Class1();
        cy.Number = 555;

        Console.ReadKey();
    }

    static void Handler1(object sender, PropertyChangedEventArgs e)
    {
        var sb = new StringBuilder();
        sb.AppendFormat("Handled1: {0}", sender);

        var c = sender as MyClass;
        if (c != null) sb.AppendFormat("; N={0}", c.Number);
        Console.WriteLine(sb.ToString());
    }

    static void Handler2(object sender, PropertyChangedEventArgs e)
    {
        var sb = new StringBuilder();
        sb.AppendFormat("Handled2: {0}", sender);

        var c = sender as MyClass;
        if (c != null) sb.AppendFormat("; N={0}", c.Number);
        Console.WriteLine(sb.ToString());
    }
}

static class Manager
{
    private static Dictionary<object, Proxy> _table = new Dictionary<object, Proxy>();

    public static void AddHandler(
        INotifyPropertyChanged source,
        PropertyChangedEventHandler handler
        )
    {
        var p = new Proxy();
        p._publicHandler = handler;
        source.PropertyChanged += p.InternalHandler;
        _table[source] = p;
    }

    class Proxy
    {
        public PropertyChangedEventHandler _publicHandler;
        public void InternalHandler(object sender, PropertyChangedEventArgs args)
        {
            this._publicHandler(sender, args);
        }
    }
}
1

There are 1 answers

8
Steven On

I haven't found any documentation that says anything about this, but you can look at the WeakEventManager source code to see why this happens.

The manager keeps a table mapping the registered source objects to its handlers. Note that this source object is the one you pass in when adding the handler.

When the manager receives an event, it looks up the relevant handlers from this table, using the sender of the event as key. Obviously, if this sender is not the same as the one that is registered, the expected handlers will not be found.


Edit

Below is some pseudo-code to illustrate.

public class PseudoEventManager : IWeakEventListener
{
    private static PseudoEventManager _instance = new PseudoEventManager();

    private readonly Dictionary<object, List<object>> _handlerTable 
                             = new Dictionary<object, List<object>>();

    public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
    {
        foreach (var handler in _handlerTable[sender]) // point of interest A
            //invoke handler
    }

    public static void AddHandler(object source, object handler)
    {
        if (!_instance._handlerTable.ContainsKey(source)) 
            _instance._handlerTable.Add(source, new List<object>()); //point of interest B
        _instance._handlerTable[source].Add(handler);
        //attach to event
    }
}

When adding a handler, the source you pass in gets added to a lookup table. When an event is received, this table is queried for the sender of this event, to get the relevant handlers for this sender/source.

In your sample, the source you're listening on is c, which the first time is also the value for ActualSender. Thus, the sender of the event is the same as the registered source, which means the handler is correctly found and called.

The second time however, the ActualSender is a different instance than c. The registered source doesn't change, but the value of the sender parameter is now different! Hence it won't be able to retrieve the handlers, and nothing gets called.