I'm trying my hands on an event aggregator and I'm storing actions in a wrapper class where I store the actual action method as a Delegate
, and the class the action method is in as a WeakReference
.
When I want to call the event handlers I first check if the WeakReference.IsAlive is true. If it is then I call it, otherwise I remove it from the collection because it means the object to which it belongs has been nulled/garbage collected.
However, when I create an anonymous action and add that to my event aggregator it is always alive, even if I null the parent class (and forcebly run the Garbage Collector).
How can I get IsAlive on the anonymous method to be set to FALSE?
void AddHandler<TEvent>(Action<TEvent> callback) {
InternalHandler handler = new InternalHandler(callback);
// Store the handler somewhere to use the callback later, if it still alive
}
class InternalHandler {
WeakReference _reference;
Delegate _method;
public InternalHandler(Delegate handler) {
_reference = new WeakReference(handler.Target);
Type messageType = handler.Method.GetParameters()[0].ParameterType;
Type delegateType = typeof(Action<,>).MakeGenericType(handler.Target.GetType(), messageType);
_method = Delegate.CreateDelegate(delegateType, handler.Method);
}
bool IsAlive => _reference != null && _reference.IsAlive;
bool Invoke(object data) {
if (!IsAlive) return false;
if (_reference.Target != null) _method.DynamicInvoke(_reference.Target, data);
return true;
}
}
And in the test application
TempObject t = new TempObject();
// Some time later
t = null;
GC.Collect();
class TempObject {
public TempObject() {
myHandler.AddHandler<SomeObject>(o => {
// Some code with a breakpoint
});
}
}
After having called GC.Collect()
and I send out a new event, the breakpoint in the anonymous method in t
is still called!
How can I get a proper WeakReference
to an anonymous method?
Bellow I assume that you don't reference any instance variables (so, don't use "this") in your anonymous TempObject
handler, because such assumption leads to observed behavior.
To fully understand the reason, the easiest method is to look up the code which is generated by C# compiler for anonymous method you mentioned. The code is rather unreadable because of the names of classes and variables used, but here is a bit prettified version:
class TempObject {
public TempObject(Handlers handlers) {
handlers.AddHandler<object>(GeneratedClass._staticAction ?? (GeneratedClass._staticAction = GeneratedClass._staticField.Handler));
}
[CompilerGenerated]
[Serializable]
private sealed class GeneratedClass {
public static readonly TempObject.GeneratedClass _staticField;
public static Action<object> _staticAction;
static GeneratedClass() {
TempObject.GeneratedClass._staticField = new TempObject.GeneratedClass();
}
public GeneratedClass() {
}
internal void Handler(object o) {
Console.WriteLine(o);
}
}
}
You see here that compiler generated new class (named GeneratedClass
here), which has one static field with the reference to an instance of this class, and also another static field where reference to your anonymous handler is cached. So, your anonymous delegate is really instance method (named Handler) of an instance of class GeneratedClass
, which instance is stored in static field.
At this point you should already realize why you observe such behavior. Your reference is
_reference = new WeakReference(handler.Target);
And handler.Target in this case references to the static field which is never set to null, so is never garbage collected.
Another way to intuitively understand it is that you never use anything related to an instance of TempObject
in your anonymous handler, and so your anonymous method is basically a reference to static method (conceptually), not to an instance method. So it actually does not have any relation to TempObject
instances and it's lifetime is not related to lifetime of TempObjects
. So basically it's the same as:
class TempObject {
public TempObject(Handlers handlers) {
handlers.AddHandler<object>(Handler);
}
private static void Handler(object arg) {
Console.WriteLine(arg);
}
}
And of course it's not surprising that your method won't work for static methods as expected (actually for static method, handler.Target will be null, so your code will fail I suppose).
Now let's change this a bit:
class TempObject {
public TempObject(Handlers handlers) {
handlers.AddHandler<object>(o => {
Console.WriteLine(o + this.Name);
});
}
public string Name { get; set; }
}
This time your handler does reference "this" and so it is related to an instance of TempObject
class. Compiler will generate different code for this sitatation which I won't show here, but the net result will be that in this case your WeakReference
will not be alive when TempObject is garbage collected, so will work as expected.