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

1

7

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);
        }
    }
}
Schutz answered 3/9, 2014 at 8:47 Comment(2)
Have you checked the NewListenerList in the WeakEventManager to see what listeners are still referencedNightstick
Looks a bit complicated checking that method, however I tried with a memory-profiler and seems never called (not sure, though).Schutz
G
6

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.

Grimes answered 27/3, 2015 at 2:20 Comment(8)
Okay, but that's still strange. Wouldn't be simpler keeping a WeakReference of each sender and throw it away when the sender will dead? Seriously, I can't see any good reason to do that.Schutz
I don't see any problem: I done something similar for a weak multi-listeners observable source. Simply register a collection of listeners instead a single one. I started from this excellent article: blog.stephencleary.com/2009/07/nitoweakreference-and.htmlSchutz
I mean what you say, but -again- I don't see the problem. The manager acts as a proxy-listener for the instance raising an event, right? Thus, the "AddHandler" method should subscribe the event and expose a weak-collection for the listeners. Then, an event is raised by the instance, and it's received by the proxy. Now, since any CLR event inherits EventArgs, it carries the "sender" field to the proxy. At this, point, why the proxy does not uses this "sender" reference when forward the event to the real-listeners?Schutz
This is not related to the many different sources hold by the manager: don't mix the source table (which is the first arg of the AddHandler) with the event arg! There's no need to hold the "sender" datum within the manager.Schutz
I added some sample pseudo-code to the post to better explain what's happening. I don't believe there is any way to retrieve the registered source at point of interest A, other than relying on the sender parameter being the same. (And I'm deleting the previous comments, as I think they're not relevant anymore after this edit.)Grimes
I'm sure that you're right about what is really coded, but I'm wondering why they didn't provide a better solution. I added a hard-link working solution for clarity: just switch to weak references and reflection (not trivial, though) and the game is on. I have only a guess: they thought to give the handler always the owning instance, maybe for some legacy compatibility. However, I'd throw an exception in the current WeakEventManager if a different sender is not supported, instead of silently avoiding the handling.Schutz
The main different between your solution and the actual implementation is that you create a new proxy for each added handler, whereas the WeakEventManager is a singleton. I can only guess why they've done it this way, but I wouldn't be surprised if it's for performance reasons. Could be any other reason as well though...Grimes
My example relies on a static manager: a singleton would be the same thing. Internally, the original could create its own private proxies, and the external code does not realize that. Performances may also be a point, but I bet that this solution was chosen as a compromise between the Winforms and the WPF worlds. The IPropertyChanged comes from the first one...Schutz

© 2022 - 2024 — McMap. All rights reserved.