Josh,
I’ve never lost the functionality of RequerySuggested in any UI when following the View, ViewModel pattern we use, even if I run GC.Collect.
Karl
Josh,
I’ve attached a sample project that uses RelayCommand. Rename extension to .zip.
I has a button to cause a GC.Collect
I’ve pressed it 50 times and no disconnect occurs in the other Button that uses RequerySuggested to test if the TextBox is not empty.
Your thoughts?
Karl
You've done something different from the unit test. Your delegate is
to a member, while the unit test uses a lambda. I don't see how that
would make a difference in behavior, though. The reference is going
in the wrong direction to root the delegate here, I would think? It's
late, and I've not gotten much sleep lately, so my mind is reeling
trying to puzzle this one through. But I suspect there's a difference
here somewhere that's significant. I'm willing to bet that the typical
usage is fine here (like you've demonstrated), but some atypical usage
can result in this behavior, and that explains why no one's reported
an issue yet. IOW, I don't think there's a bug in RelayCommand or WPF,
but rather in the calling code. I'm sure that after reading this,
someone else will explain what it is, and I'll feel stupid for not
being able to puzzle it through tonight, but I'm just to exhausted to
care ;).
--
Quidquid latine dictum sit, altum sonatur.
- Whatever is said in Latin sounds profound.
War is peace. Freedom is slavery. Bugs are features.
Thanks for your feedback.
I'll change the application over to use a Lambda and see if that changes the
landscape.
I quit using Lambda's for relay command a while back because they sit around
in memory. Not sure why they seem to float around in the heap.
Karl
Still works with Lambda's.
() => canCancelCalled = true
I see what you're implying now; barren for sure.
-Andrew
Josh,
I think that's a common issue when dealing with weak events - the WeakEvent implementation I'm using here even explicitly tests for closures because they are being garbage collected right away
(fyi - it's this one here: http://www.codeproject.com/KB/cs/WeakEvents.aspx).
Regarding Andrew's scenario, I'd say this is perfectly legal, anyway - after all, it's the idea behind having weak events that listener can in fact be garbage collected. Regarding closures, on the other hand, it is indeed a limitation.
We might be able to test for this with a bit of reflection, however, as the closures are decorated with a CompilerGeneratedAttribute:
//directly taken from Daniel's WeakEventImplementation
MethodInfo method = GetMethodFromCaller();
if (method.DeclaringType.GetCustomAttributes(typeof (CompilerGeneratedAttribute), false).Length != 0)
{
throw new ArgumentException("Cannot create weak event to anonymous method with closure.");
}
...I haven't dug deep on this yet, but could have a look at a test for the command later this afternoon.
Cheers,
Philipp
From an old internal thread w/ Varsha, the lead on Tree
Services.
“RoutedCommands don’t have their
own CanExecuteChanged handler storage. They use CommandManager.RequerySuggested
instead” …. So I don’t think RelayCommand is too far
off..
------------ now back to your scenario ------------------
The scenario where nothing holds a reference to a ViewModel exposing a command, is not common … and the patterns I use for commands is delayed instantiation and single instance, so my commands are always members of the ViewModel exposing them; so I use some thing similar to RelayCommand.
The way I see it, the choices were:
1.- A very likely leak on a common scenario, if I created a simple (member store) backing up CanExecuteChanged… and then forgot to -= .
2.- The issue you describe in a very unlikely scenario where nothing holds a reference to a VM exposing a Command.. and the CanExecute code is collected.
3.- Adding complexity if I created a store that was weak in my command… and then possibly having another non-deterministic scenario not much different from the one I was trying to solve in #2...
In the end, I opted for #2 because of the odds; I knew how easy it is for me to leak memory.. and could not afford to do #1. I think the scenario where nothing references the VM that exposes the command (and references it via member) was very unlikely.. So far, it is working for me. NOT claiming that to be the answer; just re-visiting the trade-off ( of a leak)..
The WPF team created pseudo-code for DelegateCommand once … when we considered adding it to WPF 4. The pseudocode had CanExecuteChanged in a backed store.. but I can’t see if that was a weak reference or not ( it is not in the pseudo code I looked at) so if it helps, the WPF team was going towards 1 or 3 above (from pseudo I can’t tell if reference was weak or not)..
Sorry if this does not help. Looking forward to better answer; and most important looking for others to high light valid scenarios where this issue does happen; I can read the example below, but that sounds unlikely, since you have a class that is not referenced; in MVVM, the VM is always referenced (else nothing works)..
Looking in Reflector, it looks like all three implementations of
ICommandSource hold the event handler itself in DP storage using
UncommonField<T>:
private static readonly UncommonField<EventHandler>
CanExecuteChangedHandler;
private void HookCommand(ICommand command)
{
EventHandler handler = new EventHandler
(OnCanExecuteChanged);
CanExecuteChangedHandler.SetValue(this, handler);
command.CanExecuteChanged += handler;
UpdateCanExecute();
}
private void UnhookCommand(ICommand command)
{
EventHandler handler = CanExecuteChangedHandler.GetValue
(this);
if (handler != null)
{
command.CanExecuteChanged -= handler;
CanExecuteChangedHandler.ClearValue(this);
}
UpdateCanExecute();
}
Keeping a reference to your delegate in RelayCommand would certainly
fix the issue.. I need some coffee to think through this some more..
mark
And to further draw out, it only happens to user handlers which are
trying to watch the CanExecute state - WPF controls maintain their own
reference to the handler so Button, MenuItem and Hyperlink do not have
this problem - even with RelayCommand. It doesn't appear that
RoutedCommand does anything to stop this behavior - I can reproduce
the unit test with a RoutedCommand as well. If there is no other
reference to the handler it's collected.
The question really is, how common is this scenario? I can't recall
ever need to wire up a handler for CanExecute on a command myself..
mark
Sorry, had to be said.
--
I have been wondering how behaviors (like those published by the Expression
team) handle the CanExecute (if at all) though. My guess right now is that
they don't observe it at all.
Need to check that.
Cheers,
Laurent
-----Original Message-----
From: wpf-di...@googlegroups.com [mailto:wpf-di...@googlegroups.com]
On Behalf Of Mark Smith
Sent: Tuesday, September 08, 2009 2:56 PM
To: wpf-di...@googlegroups.com
Subject: [WPF Disciples] Re: RelayCommand: memory leak vs. behavior bug
-Andrew
----- Original Message ----
From: Mark Smith <mark....@gmail.com>
To: wpf-di...@googlegroups.com
Sent: Tuesday, September 8, 2009 7:16:45 AM
Subject: [WPF Disciples] Re: RelayCommand: memory leak vs. behavior bug
target.CanExecuteChanged += (s, a) => canExecuteChangedWasCalled =
true;target.CanExecuteChanged += eh;
it will work, because it eliminates the unreferenced delegate problem.
IMO, the original code in question is incorrect because it hooks the RelayCommand.CanExecuteChanged event but never unhooks it. Since that event is not documented as a WeakEvent (even though it really IS a WeakEvent based on its proxying for CommandManager.RequerySuggested), the caller should treat it as a strong event, even in the context of this TestMethod, and make sure to unhook it. Once that happens, a reference will be created and the problem will go away.
I would kick this bug back as INVALID and require the caller to write:
EventHandler eh = (s, a) => canExecuteChangedWasCalled = true;
target.CanExecuteChanged += eh;
target.CanExecuteChanged -= eh;
Also, I would definitely make the documentation clear on how this event is working so that anyone listening knows that they need to otherwise hold a reference to the delegate so that it doesn't get collected.