I'm encountering a memory retention problem related to the usage of WeakEventManager. After profiling my application using DotMemoryProfiler, I discovered that whenever I add a handler using WeakEventManager.AddHandler, it results in adding an instance to the ConditionalWeakTable<object, object>, and the entry is retained unless the handler is removed manually (even removing in destructor won’t work, you got to have a function to remove it explicitly).
From the code below, you can see that calling the Detach method manually vs calling it via the destructor makes a difference. When it is called manually, the ConditionalWeakTable is properly collected and marked as "Dead" in the DotMemoryProfiler snapshot. However, when it is called from the destructor (~Subscriber), the ConditionalWeakTable survives. Note that the snapshot is taken after Detach is executed (as the sequence is printed in the console). I also ensure that it is run in Release Mode with "Suppress JIT optimization" option unticked.
Here's a comparison of DotMemory snapshots showing the survived object:
You might think that the surviving bytes aren’t many, but it is causing an issue in our real application because we extensively use WeakEventManager and unsubscribe it via destructor, resulting in the retention of a substantial amount of memory (more than 12MB). Here's the screenshot showing an example that it can retain such a huge amount of memory in our real application.
To provide more context, here's the relevant code snippet:
class Program
{
static void Main()
{
var isolator = new Action(() => {
var publisher = new Publisher();
var subscriber = new Subscriber();
subscriber.Init(publisher);
MemoryProfiler.GetSnapshot();
//subscriber.Detach(publisher); // detach it manually won't cause issue
});
isolator();
for (int i = 0; i < 10; i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
MemoryProfiler.GetSnapshot();
Console.WriteLine("2nd snapshot captured");
}
private static void EventHandlerMethod(object sender, EventArgs e) { }
}
public class Publisher : INotifyPropertyChanged
{
private string name;
public string Name
{
get => name;
set
{
name = value;
OnPropertyChanged(nameof(Name));
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public class Subscriber : IWeakEventListener
{
Publisher _publisher;
public void Init(Publisher publisher)
{
_publisher = publisher;
WeakEventManager<Publisher, PropertyChangedEventArgs>.AddHandler(publisher, "PropertyChanged", OnPublisherPropertyChanged);
}
public void Detach(Publisher publisher)
{
WeakEventManager<Publisher, PropertyChangedEventArgs>.RemoveHandler(publisher, "PropertyChanged", OnPublisherPropertyChanged);
Console.WriteLine("Detach called");
}
~Subscriber()
{
Console.WriteLine("Destructor called");
Detach(_publisher);
}
private void OnPublisherPropertyChanged(object sender, PropertyChangedEventArgs e) { }
public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
{
return true;
}
}
This is the Console output for both scenarios:
- Call Detach via destructor
Destructor called
Detach called
2nd snapshot captured
- Disable destructor and call Detach manually
Detach called
2nd snapshot captured
Note that in both cases, the Detach method is called. YES, it is called! But why the RemoveHandler doesn’t work if we call it from the destructor?
My questions:
- Why does the ConditionalWeakTable retain memory even after removing event handlers/GC when it is called via the destructor?
- How to workaround to clear it effectively?


WeakEventManagerwas designed to work in an exclusive thread. For example, to register handlers in a UI thread.Internal implementation of this class will create a new instance for each managed thread (due to static field marked with
ThreadStaticattribute).Therefore, to work properly with
WeakEventManager, it is necessary to ensure that each invocation to the methods of this class will come from the same thread.The easiest way to do this is to use an instance of
Dispatcherclass, which will be run in a dedicated thread (just like we do in any UI application when we want to synchronize with a UI thread).After that, we can synchronize with this thread anywhere in the code
With this usage, there is no need to unsubscribe from the event in the destructor, because the internal method
DoCleanupwill do it for us, which scans all added handlers wrapped withWeakReference.Your case. In the code there is a subscription to an event in thread
1, but it is unsubscribed in thread2. That is, you are subscribing to another instance ofWeakEventManager, but this is half of the problem. The main problem is that you do not have a dispatcher running for the thread in which you useWeakEventManager. Starting the removal of handlers that have been collected byGCis done through the dispatcher, without the dispatcher running you will get a memory leak. The process is initiated either after adding a new listener or if no valid handler is found when the event is triggered