views:

6207

answers:

9

I have run into an issue with WPF and Commands that are bound to a Button inside the DataTemplate of an ItemsControl. The scenario is quite straight forward. The ItemsControl is bound to a list of objects, and I want to be able to remove each object in the list by clicking a Button. The Button executes a Command, and the Command takes care of the deletion. The CommandParameter is bound to the Object I want to delete. That way I know what the user clicked. A user should only be able to delete their "own" objects - so I need to do some checks in the "CanExecute" call of the Command to verify that the user has the right permissions.

The problem is that the parameter passed to CanExecute is NULL the first time it's called - so I can't run the logic to enable/disable the command. However, if I make it allways enabled, and then click the button to execute the command, the CommandParameter is passed in correctly. So that means that the binding against the CommandParameter is working.

The XAML for the ItemsControl and the DataTemplate looks like this:

<ItemsControl 
    x:Name="commentsList"
    ItemsSource="{Binding Path=SharedDataItemPM.Comments}"
    Width="Auto" Height="Auto">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <Button                             
                    Content="Delete"
                    FontSize="10"
                    Command="{Binding Path=DataContext.DeleteCommentCommand, ElementName=commentsList}" 
                    CommandParameter="{Binding}" />
            </StackPanel>           
         </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

So as you can see I have a list of Comments objects. I want the CommandParameter of the DeleteCommentCommand to be bound to the Command object.

So I guess my question is: have anyone experienced this problem before? CanExecute gets called on my Command, but the parameter is always NULL the first time - why is that?

Update: I was able to narrow the problem down a little. I added an empty Debug ValueConverter so that I could output a message when the CommandParameter is data bound. Turns out the problem is that the CanExecute method is executed before the CommandParameter is bound to the button. I have tried to set the CommandParameter before the Command (like suggested) - but it still doesn't work. Any tips on how to control it.

Update2: Is there any way to detect when the binding is "done", so that I can force re-evaluation of the command? Also - is it a problem that I have multiple Buttons (one for each item in the ItemsControl) that bind to the same instance of a Command-object?

Update3: I have uploaded a reproduction of the bug to my SkyDrive: http://cid-1a08c11c407c0d8e.skydrive.live.com/self.aspx/Code%20samples/CommandParameterBinding.zip

+1  A: 

I have found that the order in which I set Command and CommandParameter makes a difference. Setting the Command property causes CanExecute to be called immediately, so you want CommandParameter to already be set at that point.

I have found that switching the order of the properties in the XAML can actually have an effect, though I'm not confident that it will solve your problem. It's worth a try, though.

You seem to be suggesting that the button never becomes enabled, which is surprising, since I would expect the CommandParameter to be set shortly after the Command property in your example. Does calling CommandManager.InvalidateRequerySuggested() cause the button to become enabled?

Ed Ball
Tried setting the CommandParameter before the Command - still executes CanExecute, but still passing in NULL... Bummer - but thanks for the tip. Also, calling CommandManager.InvalidateRequerySuggested(); does not make any difference.
Jonas Follesø
CommandManager.InvalidateRequerySuggested() solved a similar issue for me. Thanks!
MJS
A: 

Its a long shot. to debug this you can try:
- checking the PreviewCanExecute event.
- use snoop/wpf mole to peek inside and see what the commandparameter is.

HTH,

Dennis Roche
Tried using Snoop - but it's really hard to debug as it's only NULL when it's initially loaded. If i run Snoop on it the Command and CommandParameter are both seth... It has to do with using Commands in DataTemplate.
Jonas Follesø
A: 

Hey Jonas, not sure if this will work in a data template, but here is the binding syntax I use in a ListView Context menu to grab the current item as a command parameter:

CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=ContextMenu}, Path=PlacementTarget.SelectedItem, Mode=TwoWay}"

I do the exact same thing in my list view. In this case it is a ItemsControl so there is no obvious property to "bind up" against (in the visual tree). I guess I have to find a way to detect when binding is done, and re-evaluate CanExecute (because CommandParameter gets bound, just to late)
Jonas Follesø
A: 

The commandManager.InvalidateRequerySuggested works for me as well. I believe the following link talks about similar problem, and M$ dev confirmed the limitation in the current version, and the commandManager.InvalidateRequerySuggested is the workaround. http://social.expression.microsoft.com/Forums/en-US/wpf/thread/c45d2272-e8ba-4219-bb41-1e5eaed08a1f/

What important is the timing of invoking the commandManager.InvalidateRequerySuggested. This should be invoked after the relevant value change is notified.

+2  A: 

I stumbled upon a similar problem and solved it using my trusty TriggerConverter.

public class TriggerConverter : IMultiValueConverter
{
    #region IMultiValueConverter Members

    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        // First value is target value.
        // All others are update triggers only.
        if (values.Length < 1) return Binding.DoNothing;
        return values[0];
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    #endregion
}

This value converter takes any number of parameters and passes the first of them back as the converted value. When used in a MultiBinding in your case it looks like the following.

<ItemsControl 
    x:Name="commentsList"
    ItemsSource="{Binding Path=SharedDataItemPM.Comments}"
    Width="Auto" Height="Auto">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <Button                             
                    Content="Delete"
                    FontSize="10"
                    CommandParameter="{Binding}">
                    <Button.Command>
                        <MultiBinding Converter="{StaticResource TriggerConverter}">
                            <Binding Path="DataContext.DeleteCommentCommand"
                                     ElementName="commentsList" />
                            <Binding />
                        </MultiBinding> 
                    </Button.Command>
                </Button>
            </StackPanel>                                       
         </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

You will have to add TriggerConverter as a resource somewhere for this to work. Now the Command property is set not before the value for the CommandParameter has become available. You could even bind to RelativeSource.Self and CommandParameter instead of . to achieve the same effect.

Ptahhotep
+1  A: 

You may be able to use my CommandParameterBehavior that I posted to the Prism forums yesterday. It adds the missing behaviour where a change to the CommandParameter cause the Command to be re-queried.

There's some complexity here caused by my attempts to avoid the memory leak caused if you call PropertyDescriptor.AddValueChanged without later calling PropertyDescriptor.RemoveValueChanged. I try and fix that by unregistering the handler when the ekement is unloaded.

You'll probably need to remove the IDelegateCommand stuff unless you're using Prism (and want to make the same changes as me to the Prism library). Also note that we don't generally use RoutedCommands here (we use Prism's DelegateCommand<T> for pretty much everything) so please don't hold me responsible if my call to CommandManager.InvalidateRequerySuggested sets off some sort of quantum wavefuntion collapse cascade that destroys the known universe or anything.

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;

namespace Microsoft.Practices.Composite.Wpf.Commands
{
    /// <summary>
    /// This class provides an attached property that, when set to true, will cause changes to the element's CommandParameter to 
    /// trigger the CanExecute handler to be called on the Command.
    /// </summary>
    public static class CommandParameterBehavior
    {
        /// <summary>
        /// Identifies the IsCommandRequeriedOnChange attached property
        /// </summary>
        /// <remarks>
        /// When a control has the <see cref="IsCommandRequeriedOnChangeProperty" />
        /// attached property set to true, then any change to it's 
        /// <see cref="System.Windows.Controls.Primitives.ButtonBase.CommandParameter" /> property will cause the state of
        /// the command attached to it's <see cref="System.Windows.Controls.Primitives.ButtonBase.Command" /> property to 
        /// be reevaluated.
        /// </remarks>
        public static readonly DependencyProperty IsCommandRequeriedOnChangeProperty =
            DependencyProperty.RegisterAttached("IsCommandRequeriedOnChange",
                                                typeof(bool),
                                                typeof(CommandParameterBehavior),
                                                new UIPropertyMetadata(false, new PropertyChangedCallback(OnIsCommandRequeriedOnChangeChanged)));

        /// <summary>
        /// Gets the value for the <see cref="IsCommandRequeriedOnChangeProperty"/> attached property.
        /// </summary>
        /// <param name="target">The object to adapt.</param>
        /// <returns>Whether the update on change behavior is enabled.</returns>
        public static bool GetIsCommandRequeriedOnChange(DependencyObject target)
        {
            return (bool)target.GetValue(IsCommandRequeriedOnChangeProperty);
        }

        /// <summary>
        /// Sets the <see cref="IsCommandRequeriedOnChangeProperty"/> attached property.
        /// </summary>
        /// <param name="target">The object to adapt. This is typically a <see cref="System.Windows.Controls.Primitives.ButtonBase" />, 
        /// <see cref="System.Windows.Controls.MenuItem" /> or <see cref="System.Windows.Documents.Hyperlink" /></param>
        /// <param name="value">Whether the update behaviour should be enabled.</param>
        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();
            }

        }
    }
}
Swythan
A: 

I've logged this as a bug against WPF in .Net 4.0, as the problem still exists in Beta 2.

https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=504976

Swythan
A: 

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

Joe Bako
A: 

I was having this same issue while trying to bind to a command on my view model.

I changed it to use a relative source binding rather than referring to the element by name and that did the trick. Parameter binding didn't change.

Old Code:

Command="{Binding DataContext.MyCommand, ElementName=myWindow}"

New Code:

Command="{Binding DataContext.MyCommand, RelativeSource={RelativeSource AncestorType=Views:MyView}}"
Travis Weber