I am having a really strange problem with the Messenger
system in MVVM Light. It\'s hard to explain, so here is small program that demonstrates the issue:
I agree, the behavior of this program is really strange.
I tried it myself and as you already figured out the problem is somehow related to this line:
var x = target;
I have no idea why this line causes any trouble but you might consider this workaround:
class Program
{
static void Main(string[] args)
{
var prog = new Program();
var recipient = new object();
prog.RegisterMessageA(recipient);
prog.RegisterMessageB(recipient);
prog.SendMessage("First Message");
GC.Collect();
prog.SendMessage("Second Message");
}
public void RegisterMessageA(object target)
{
Messenger.Default.Register(target, (Message msg) =>
{
Console.WriteLine(msg.Name + " received by A");
var x = msg.Target;
});
}
public void RegisterMessageB(object target)
{
Messenger.Default.Register(target, (Message msg) =>
{
Console.WriteLine(msg.Name + " received by B");
});
}
public void SendMessage(string name)
{
Messenger.Default.Send(new Message { Name = name });
}
class Message : MessageBase //part of the MVVM Light framework
{
public string Name { get; set; }
}
}
MessageBase is a class from the MVVM Light Framework which offers the possibility to retrieve the target from the message itself.
But I'm not sure if this is what you're trying to achieve...
Well, I now understand why it's happening (I believe, anyway). I've reproduced it in a shorter form which doesn't use lambda expressions, and then I'll explain why the lambdas are important.
using System;
using GalaSoft.MvvmLight.Messaging;
class Program
{
static void Main(string[] args)
{
Receiver r1 = new Receiver("r1");
Receiver r2 = new Receiver("r2");
var recipient = new object();
Messenger.Default.Register<object>(recipient, r1).ShowMessage;
Messenger.Default.Register<object>(recipient, r2).ShowMessage;
GC.Collect();
Messenger.Default.Send(recipient, null);
// Uncomment one of these to see the relevant message...
// GC.KeepAlive(r1);
// GC.KeepAlive(r2);
}
}
class Receiver
{
private string name;
public Receiver(string name)
{
this.name = name;
}
public void ShowMessage(object message)
{
Console.WriteLine("message received by {0}", name);
}
}
Basically, the messenger only keeps a weak reference to the message handler. (Also to the recipient, but that's not a problem here.) More specifically, it appears to have a weak reference to the handler's target object. It doesn't seem to care about the delegate object itself, but the target is important. So in the above code, when you keep a Receiver
object alive, the delegate which has that object as a target is still used. However, when the target is allowed to be garbage collected, the handler using that object is not used.
Now let's look at your two handler:
public void RegisterMessageA(object target)
{
Messenger.Default.Register(target, (Message msg) =>
{
Console.WriteLine(msg.Name + " received by A");
var x = target;
});
}
This lambda expression captures the target
parameter. In order to capture it, the compiler generates a new class - so RegisterMessageA
is effectively:
public void RegisterMessageA(object target)
{
GeneratedClass x = new GeneratedClass();
x.target = target;
Messenger.Default.Register(x.target, x.Method);
}
private class GeneratedClass
{
public object target;
public void Method(Message msg)
{
Console.WriteLine(msg.Name + " received by A");
var x = target;
}
}
Now, there's nothing other than the delegate which keeps that instance of GeneratedClass
alive. Compare that with your second handler:
public void RegisterMessageB(object target)
{
Messenger.Default.Register(target, (Message msg) =>
{
Console.WriteLine(msg.Name + " received by B");
});
}
Here, there are no captured variables, so the compiler generates code a bit like this:
public void RegisterMessageB(object target)
{
Messenger.Default.Register(target, RegisterMessageB_Lambda);
}
private static void RegisterMessageB_Lambda(Message msg)
{
Console.WriteLine(msg.Name + " received by B");
}
Here it's a static method, so there's no delegate target at all. If the delegate captured this
, it would be generated as an instance method. But the important point is that there's no need to generate an extra class... so there's nothing to be garbage collected.
I haven't looked into exactly how MvvmLight is doing this - whether it's just got a weak reference to the delegate, and that the CLR is treating that in some special way, or whether MvvmLight is separating the target from the delegate itself. Either way, I hope that explains the behaviour you're seeing. In terms of how to fix whatever problem you're seeing with real code - basically you'll need to make sure you keep a strong reference to whatever delegate target you need.
EDIT: Okay, it looks like it's now due to WeakActionGeneric and its base class WeakAction. I don't know whether this behaviour is the expected behaviour (by the author), but that's the code responsible :)