Patch available: Automatically call RaiseCanExecuteChanged() when CommandParameter changes

Topics: Prism v1
Feb 17, 2009 at 11:13 AM
Hi,

We keep running up against an issue in WPF where changing the CommandParameter of an ICommandSource (e.g. Button or MenuItem) doesn't cause a call to CanExecute with the new parameter. Often we find that even if we never change the CommandParameter,  the Command gets set first so we only get a call to CanExecute with a null parameter. It's also very annoying if the CommandParameter comes from a binding.

I've written what I think is a fix in the form of an attached behavior that you can set on the Button, etc that will listen for changes to the CommandParameter and call RaiseCanExecuteChanged if the command is a DelegateCommand.

Usage is like:
    <Button Command="{Binding ...}" 
CommandParameter="{Binding ...}"
prismCommands:CommandParameterBehavior.IsCommandRequeriedOnChange="true" />
You can, of course, set this in an application wide style for the various controls you care about.

Would you be interested in including this upstream? I'll post the patch below.
N.B. To get it to work, I had to create an interface for the non-generic bits of DelegateCommand<T> (i.e. RaiseCanExecuteChanged).
Feb 17, 2009 at 11:20 AM
Edited Feb 17, 2009 at 11:24 AM
Edit:I've had to strip out all the xml comments, as they were messing up the formatting. If you want them, I have written some!

Firstly, I had to add an IDelegateCommand interface, and make DelegateCommand<T> inherit from that (not shown).
    public interface IDelegateCommand : ICommand, IActiveAware
{
void RaiseCanExecuteChanged();
}
The important class, though, is the one below with the attached behavior.

Note that I've tried to handle the Unloaded event sensibly to avoid the memory leak that would otherwise be caused by using PropertyDescriptor.AddValueChanged. I'm pretty sure that works, but I've not tested it in anything other than a trivial scenario.
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;

namespace Microsoft.Practices.Composite.Wpf.Commands
{
public static class CommandParameterBehavior
{
public static readonly DependencyProperty IsCommandRequeriedOnChangeProperty =
DependencyProperty.RegisterAttached("IsCommandRequeriedOnChange",
typeof(bool),
typeof(CommandParameterBehavior),
new UIPropertyMetadata(false, new PropertyChangedCallback(OnIsCommandRequeriedOnChangeChanged)));

public static bool GetIsCommandRequeriedOnChange(DependencyObject target)
{
return (bool)target.GetValue(IsCommandRequeriedOnChangeProperty);
}

public static void SetIsCommandRequeriedOnChange(DependencyObject target, bool value)
{
target.SetValue(IsCommandRequeriedOnChangeProperty, value);
}

private static void OnIsCommandRequeriedOnChangeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is ICommandSource))
return;

if (!(d is FrameworkElement || d is FrameworkContentElement))
return;

if ((bool)e.NewValue)
{
HookCommandParameterChanged(d);
}
else
{
UnhookCommandParameterChanged(d);
}

UpdateCommandState(d);
}

private static PropertyDescriptor GetCommandParameterPropertyDescriptor(object source)
{
return TypeDescriptor.GetProperties(source.GetType())["CommandParameter"];
}

private static void HookCommandParameterChanged(object source)
{
var propertyDescriptor = GetCommandParameterPropertyDescriptor(source);
propertyDescriptor.AddValueChanged(source, OnCommandParameterChanged);

// N.B. Using PropertyDescriptor.AddValueChanged will cause "source" to never be garbage collected,
// so we need to hook the Unloaded event and call RemoveValueChanged there.
HookUnloaded(source);
}

private static void UnhookCommandParameterChanged(object source)
{
var propertyDescriptor = GetCommandParameterPropertyDescriptor(source);
propertyDescriptor.RemoveValueChanged(source, OnCommandParameterChanged);

UnhookUnloaded(source);
}

private static void HookUnloaded(object source)
{
var fe = source as FrameworkElement;
if (fe != null)
{
fe.Unloaded += OnUnloaded;
}

var fce = source as FrameworkContentElement;
if (fce != null)
{
fce.Unloaded += OnUnloaded;
}
}

private static void UnhookUnloaded(object source)
{
var fe = source as FrameworkElement;
if (fe != null)
{
fe.Unloaded -= OnUnloaded;
}

var fce = source as FrameworkContentElement;
if (fce != null)
{
fce.Unloaded -= OnUnloaded;
}
}

static void OnUnloaded(object sender, RoutedEventArgs e)
{
UnhookCommandParameterChanged(sender);
}

static void OnCommandParameterChanged(object sender, EventArgs ea)
{
UpdateCommandState(sender);
}

private static void UpdateCommandState(object target)
{
var commandSource = target as ICommandSource;

if (commandSource == null)
return;

var rc = commandSource.Command as RoutedCommand;
if (rc != null)
{
CommandManager.InvalidateRequerySuggested();
}

var dc = commandSource.Command as IDelegateCommand;
if (dc != null)
{
dc.RaiseCanExecuteChanged();
}

}
}
}
Apr 8, 2010 at 6:08 PM

There's a relatively simple way to "fix" this problem with DelegateCommand, though it requires updating the DelegateCommand source and re-compiling the Microsoft.Practices.Composite.Presentation.dll.

1)  Download the Prism 1.2 source code and open the CompositeApplicationLibrary_Desktop.sln.  In here is a Composite.Presentation.Desktop project that contains the DelegateCommand source.

2) Under the public event EventHandler CanExecuteChanged, modify to read as follows:

    public event EventHandler CanExecuteChanged
    {
         add
         {
              WeakEventHandlerManager.AddWeakReferenceHandler( ref _canExecuteChangedHandlers, value, 2 );
              // add this line
              CommandManager.RequerySuggested += value;
         }
         remove
         {
              WeakEventHandlerManager.RemoveWeakReferenceHandler( _canExecuteChangedHandlers, value );
              // add this line
              CommandManager.RequerySuggested -= value;
         }
    }

3) Under protected virtual void OnCanExecuteChanged(), modify it as follows:

    protected virtual void OnCanExecuteChanged()
    {
         // add this line
         CommandManager.InvalidateRequerySuggested();
         WeakEventHandlerManager.CallWeakReferenceHandlers( this, _canExecuteChangedHandlers );
    }

4) Recompile the solution, then navigate to either the Debug or Release folder where the compiled DLLs live.  Copy the Microsoft.Practices.Composite.Presentation.dll and .pdb (if you wish) to where you references your external assemblies, and then recompile your application to pull the new versions.

After this, CanExecute should be fired every time the UI renders elements bound to the DelegateCommand in question.

Take care,
Joe

refereejoe at gmail

Apr 9, 2010 at 9:13 AM

Thanks Joe. I notice that most other ICommand implementations do effectively the same thing (i.e. forward CanExecuteChanged through to CommandManager.RequerySuggested).

I think if you're doing that, there's no need to leave the existing implementation (using _canExecuteChangedHandlers) in place, as it will be handled by the CommandManager. With both implementations in place the handlers will get called twice when you call OnCanExecuteChanged, won't they?

The downside of this approach, IIUC, is that the CanExecuteChanged handlers will get called (more or less) every time the UI renders any element bound to any command, though, won't they? Some of our CanExecute methods are a bit slow (very bad design ,I know. I promise I'll fix them soon!) so calling them more often than necessary could be "unfortunate".

BTW: As it turns out, my implementation above wasn't good enough, as the Loaded & Unloaded events don't come in pairs (the Loaded event is often fired more than once), so you can end up with multiple event subscriptions (e.g. for MenuItems, IIRC). If anyone is interested I can post an updated version. Given the amount of code necessary to do it my way, I'm tempted to switch to the CommandManager.InvalidateRequerySuggested solution. :-)