97 votes

WPF CommandParameter est NULL la première fois que CanExecute est appelé

J'ai rencontré un problème avec WPF et les commandes qui sont liées à un bouton dans le DataTemplate d'un ItemsControl. Le scénario est assez simple. Le ItemsControl est lié à une liste d'objets, et je veux pouvoir supprimer chaque objet de la liste en cliquant sur un bouton. Le bouton exécute une commande et la commande se charge de la suppression. Le CommandParameter est lié à l'objet que je veux supprimer. Ainsi, je sais sur quoi l'utilisateur a cliqué. Un utilisateur ne doit pouvoir supprimer que ses "propres" objets. Je dois donc effectuer quelques vérifications dans l'appel "CanExecute" de la commande pour m'assurer que l'utilisateur dispose des bonnes autorisations.

Le problème est que le paramètre passé à CanExecute est NULL la première fois qu'il est appelé - je ne peux donc pas exécuter la logique pour activer/désactiver la commande. Cependant, si je fais en sorte qu'elle soit toujours activée, puis que je clique sur le bouton pour exécuter la commande, le CommandParameter est transmis correctement. Cela signifie donc que la liaison avec le CommandParameter fonctionne.

Le XAML pour le ItemsControl et le DataTemplate ressemble à ceci :

<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>

Comme vous pouvez le voir, j'ai donc une liste d'objets Commentaires. Je veux que le CommandParameter de la commande DeleteCommentCommand soit lié à l'objet Command.

Je suppose donc que ma question est la suivante : quelqu'un a-t-il déjà rencontré ce problème ? CanExecute est appelé sur ma commande, mais le paramètre est toujours NULL la première fois - pourquoi ?

Mise à jour : J'ai pu réduire un peu le problème. J'ai ajouté un ValueConverter Debug vide afin de pouvoir afficher un message lorsque le CommandParameter est lié aux données. Il s'avère que le problème est que la méthode CanExecute est exécutée avant que le CommandParameter soit lié au bouton. J'ai essayé de placer le CommandParameter avant la commande (comme suggéré), mais cela ne fonctionne toujours pas. Avez-vous des conseils sur la façon de contrôler ce problème ?

Mise à jour2 : Existe-t-il un moyen de détecter quand la liaison est "terminée", afin de pouvoir forcer la réévaluation de la commande ? Par ailleurs, le fait que plusieurs boutons (un pour chaque élément du ItemsControl) soient liés à la même instance d'un objet de commande pose-t-il un problème ?

Mise à jour3 : J'ai téléchargé une reproduction du bug sur mon SkyDrive : http://cid-1a08c11c407c0d8e.skydrive.live.com/self.aspx/Code%20samples/CommandParameterBinding.zip

0 votes

J'ai exactement le même problème, avec une ListBox.

1 votes

Il existe actuellement un rapport de bogue ouvert contre WPF pour ce problème : github.com/dotnet/wpf/issues/316

64voto

Travis Weber Points 371

J'ai rencontré le même problème en essayant de me lier à une commande sur mon modèle de vue.

Je l'ai modifié pour qu'il utilise une liaison de source relative plutôt que de faire référence à l'élément par son nom et cela a fonctionné. La liaison des paramètres n'a pas changé.

Ancien code :

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

Nouveau code :

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

Mise à jour : Je viens de tomber sur ce problème sans utiliser ElementName, je me lie à une commande sur mon modèle de vue et mon contexte de données du bouton est mon modèle de vue. Dans ce cas, j'ai dû simplement déplacer l'attribut CommandParameter avant l'attribut Command dans la déclaration du bouton (en XAML).

CommandParameter="{Binding Groups}"
Command="{Binding StartCommand}"

47 votes

Déplacer le CommandParameter devant la Command est la meilleure réponse de ce fil.

6 votes

Le déplacement de l'ordre des attributs ne nous a pas aidés. Je serais surpris que cela ait un effet sur l'ordre d'exécution.

4 votes

Je ne sais pas pourquoi ça marche. On dirait que ça ne devrait pas, mais ça marche.

34voto

Ed Ball Points 1341

J'ai constaté que l'ordre dans lequel je définis Command et CommandParameter fait une différence. La définition de la propriété Command entraîne l'appel immédiat de CanExecute. Il faut donc que CommandParameter soit déjà défini à ce moment-là.

J'ai constaté que le fait de changer l'ordre des propriétés dans le XAML peut avoir un effet, même si je ne suis pas sûr que cela résoudra votre problème. Mais cela vaut la peine d'essayer.

Vous semblez suggérer que le bouton n'est jamais activé, ce qui est surprenant, car je m'attendrais à ce que le CommandParameter soit défini peu après la propriété Command dans votre exemple. L'appel à CommandManager.InvalidateRequerySuggested() entraîne-t-il l'activation du bouton ?

3 votes

J'ai essayé de définir le paramètre CommandParameter avant la commande - il exécute toujours CanExecute, mais il transmet toujours NULL... Dommage, mais merci pour l'astuce. De même, l'appel de CommandManager.InvalidateRequerySuggested() ; ne fait aucune différence.

0 votes

CommandManager.InvalidateRequerySuggested() a résolu un problème similaire pour moi. Merci !

18voto

Ptahhotep Points 86

Je suis tombé sur un problème similaire et je l'ai résolu en utilisant mon fidèle 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
}

Ce convertisseur de valeur prend un nombre quelconque de paramètres et renvoie le premier d'entre eux comme valeur convertie. Lorsqu'il est utilisé dans un MultiBinding, dans votre cas, il ressemble à ce qui suit.

<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>

Vous devrez ajouter TriggerConverter comme ressource quelque part pour que cela fonctionne. Maintenant, la propriété Command est définie pas avant que la valeur du CommandParameter soit disponible. Vous pourriez même lier RelativeSource.Self et CommandParameter au lieu de . pour obtenir le même effet.

2 votes

Ça a marché pour moi. Je ne comprends pas pourquoi. Quelqu'un peut-il m'expliquer ?

0 votes

Cela ne fonctionne pas parce que le CommandParameter est lié avant la commande ? Je doute que vous ayez besoin du convertisseur...

2 votes

Ce n'est pas une solution. C'est un piratage ? Qu'est-ce qui se passe, bon sang ? Ça marchait avant ?

17voto

Ed Downs Points 21

J'ai trouvé une autre option pour contourner ce problème et je voulais la partager. Comme la méthode CanExecute de la commande est exécutée avant que la propriété CommandParameter ne soit définie, j'ai créé une classe d'aide avec une propriété attachée qui oblige la méthode CanExecute à être appelée à nouveau lorsque la liaison est modifiée.

public static class ButtonHelper
{
    public static DependencyProperty CommandParameterProperty = DependencyProperty.RegisterAttached(
        "CommandParameter",
        typeof(object),
        typeof(ButtonHelper),
        new PropertyMetadata(CommandParameter_Changed));

    private static void CommandParameter_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var target = d as ButtonBase;
        if (target == null)
            return;

        target.CommandParameter = e.NewValue;
        var temp = target.Command;
        // Have to set it to null first or CanExecute won't be called.
        target.Command = null;
        target.Command = temp;
    }

    public static object GetCommandParameter(ButtonBase target)
    {
        return target.GetValue(CommandParameterProperty);
    }

    public static void SetCommandParameter(ButtonBase target, object value)
    {
        target.SetValue(CommandParameterProperty, value);
    }
}

Et ensuite sur le bouton auquel vous voulez lier un paramètre de commande...

<Button 
    Content="Press Me"
    Command="{Binding}" 
    helpers:ButtonHelper.CommandParameter="{Binding MyParameter}" />

J'espère que cela aidera peut-être quelqu'un d'autre à résoudre ce problème.

1 votes

Bien fait, merci. Je n'arrive pas à croire que M$ n'ait pas corrigé ce problème après 8 ans. C'est terrible !

5voto

Swythan Points 88

Vous pouvez utiliser mon CommandParameterBehavior que j'ai posté sur le Forums sur le prisme hier. Il ajoute le comportement manquant lorsqu'une modification de l'option CommandParameter causer le Command pour être réinterrogé.

Il y a une certaine complexité ici causée par mes tentatives d'éviter la fuite de mémoire causée si vous appelez PropertyDescriptor.AddValueChanged sans appeler ensuite PropertyDescriptor.RemoveValueChanged . J'essaie de résoudre ce problème en désenregistrant le gestionnaire lorsque l'événement est déchargé.

Vous aurez probablement besoin de retirer le IDelegateCommand à moins que vous n'utilisiez Prism (et que vous vouliez faire les mêmes changements que moi dans la bibliothèque de Prism). Notez également que nous n'utilisons généralement pas RoutedCommand ici (nous utilisons l'option de Prism DelegateCommand<T> pour à peu près tout) alors ne me tenez pas pour responsable si mon appel à CommandManager.InvalidateRequerySuggested déclenche une sorte de cascade d'effondrement d'ondes quantiques qui détruit l'univers connu ou autre.

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();
            }

        }
    }
}

0 votes

Je suis tombé sur votre rapport de bug à connect. Pourriez-vous mettre à jour votre message ici avec votre dernier code à ce sujet ? Ou avez-vous depuis trouvé une meilleure solution ?

0 votes

Une solution plus simple peut consister à observer la propriété CommandParameter en utilisant une liaison au lieu d'un descripteur de propriété. Sinon, c'est une excellente solution ! Celle-ci corrige réellement le problème sous-jacent au lieu d'introduire un hack ou une solution de contournement gênante.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X