web-dev-qa-db-fra.com

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

J'ai rencontré un problème avec WPF et les commandes 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 s'occupe de la suppression. Le CommandParameter est lié à l'objet que je veux supprimer. De cette façon, je sais sur quoi l'utilisateur a cliqué. Un utilisateur ne doit pouvoir supprimer que ses "propres" objets - j'ai donc besoin de faire quelques vérifications dans l'appel "CanExecute" de la commande pour vérifier que l'utilisateur a les bonnes autorisations.

Le problème est que le paramètre transmis à 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 le rend toujours activé, 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 une liste d'objets Commentaires. Je veux que le CommandParameter de 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 Debug ValueConverter vide pour que je puisse sortir 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 ne soit lié au bouton. J'ai essayé de définir le CommandParameter avant la commande (comme suggéré) - mais cela ne fonctionne toujours pas. Des conseils sur la façon de le contrôler.

Update2: Existe-t-il un moyen de détecter le moment où la liaison est "terminée", afin que je puisse forcer la réévaluation de la commande? Aussi - est-ce un problème que j'ai plusieurs boutons (un pour chaque élément du ItemsControl) qui se lient à la même instance d'un objet Command?

Update3: J'ai téléchargé une reproduction du bogue sur mon SkyDrive: http://cid-1a08c11c407c0d8e.skydrive.live.com/ self.aspx/Code% 20samples/CommandParameterBinding.Zip

80
Jonas Follesø

Je suis tombé sur un problème similaire et 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 n'importe quel nombre 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. Désormais, la propriété Command n'est pas définie avant que la valeur du CommandParameter ne soit disponible. Vous pouvez même vous lier à RelativeSource.Self et CommandParameter au lieu de. pour obtenir le même effet.

14
David Liersch

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

Je l'ai changé pour utiliser une liaison de source relative plutôt que de faire référence à l'élément par son nom et cela a fait l'affaire. 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 rencontrer ce problème sans utiliser ElementName, je 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, je devais simplement déplacer l'attribut CommandParameter avant l'attribut Command dans la déclaration Button (en XAML).

CommandParameter="{Binding Groups}"
Command="{Binding StartCommand}"
51
Travis Weber

J'ai constaté que l'ordre dans lequel j'ai défini Command et CommandParameter fait une différence. La définition de la propriété Command provoque l'appel immédiat de CanExecute, vous souhaitez donc que CommandParameter soit déjà défini à ce point.

J'ai constaté que le changement de l'ordre des propriétés dans le XAML peut en fait avoir un effet, bien que je ne sois pas convaincu que cela résoudra votre problème. Cela vaut la peine d'essayer, cependant.

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

28
Ed Ball

Je sais que ce fil est un peu ancien, mais j'ai trouvé une autre option pour contourner ce problème que je voulais partager. Étant donné que 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'assistance avec une propriété attachée qui force la méthode CanExecute à être appelée à nouveau lorsque la liaison change.

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 puis sur le bouton que vous souhaitez lier un paramètre de commande à ...

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

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

13
Ed Downs

Il s'agit d'un ancien fil, mais puisque Google m'a amené ici quand j'ai eu ce problème, je vais ajouter ce qui a fonctionné pour moi pour un DataGridTemplateColumn avec un bouton.

Modifiez la liaison de:

CommandParameter="{Binding .}"

à

CommandParameter="{Binding DataContext, RelativeSource={RelativeSource Self}}"

Je ne sais pas pourquoi cela fonctionne, mais ça l'a fait pour moi.

6
Simon Smith

Vous pourrez peut-être utiliser mon CommandParameterBehavior que j'ai posté sur le forums Prism hier. Il ajoute le comportement manquant lorsqu'une modification de CommandParameter entraîne une nouvelle requête de Command.

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

Vous devrez probablement supprimer les éléments IDelegateCommand sauf si vous utilisez Prism (et souhaitez apporter les mêmes modifications que moi à la bibliothèque Prism). Notez également que nous n'utilisons généralement pas RoutedCommands ici (nous utilisons DelegateCommand<T> pour à peu près tout) alors ne me tenez pas responsable si mon appel à CommandManager.InvalidateRequerySuggested déclenche une sorte de cascade d'effondrement quantique de la fonction d'onde qui détruit l'univers connu ou quoi que ce soit.

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

        }
    }
}
5
Swythan

Il existe un moyen relativement simple de "résoudre" ce problème avec DelegateCommand, bien qu'il nécessite la mise à jour de la source DelegateCommand et la recompilation de Microsoft.Practices.Composite.Presentation.dll.

1) Téléchargez le code source de Prism 1.2 et ouvrez CompositeApplicationLibrary_Desktop.sln. Vous trouverez ici un projet Composite.Presentation.Desktop qui contient la source DelegateCommand.

2) Sous l'événement public EventHandler CanExecuteChanged, modifiez pour lire comme suit:

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) Sous OnCanExecuteChanged () vide virtuel protégé, modifiez-le comme suit:

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

4) Recompilez la solution, puis accédez au dossier Debug ou Release dans lequel vivent les DLL compilées. Copiez le fichier Microsoft.Practices.Composite.Presentation.dll et .pdb (si vous le souhaitez) à l'endroit où vous référencez vos assemblys externes, puis recompilez votre application pour extraire les nouvelles versions.

Après cela, CanExecute doit être déclenché chaque fois que l'interface utilisateur rend les éléments liés à la DelegateCommand en question.

Prends soin de toi, Joe

refereejoe chez gmail

1
Joe Bako

Après avoir lu quelques bonnes réponses à des questions similaires, j'ai légèrement changé dans votre exemple le DelegateCommand pour le faire fonctionner. À la place d'utiliser:

public event EventHandler CanExecuteChanged;

Je l'ai changé en:

public event EventHandler CanExecuteChanged
{
    add { CommandManager.RequerySuggested += value; }
    remove { CommandManager.RequerySuggested -= value; }
}

J'ai supprimé les deux méthodes suivantes parce que j'étais trop paresseux pour les corriger

public void RaiseCanExecuteChanged()

et

protected virtual void OnCanExecuteChanged()

Et c'est tout ... cela semble garantir que CanExecute sera appelé lorsque la liaison change et après la méthode Execute

Il ne se déclenchera pas automatiquement si le ViewModel est modifié mais comme mentionné dans ce thread possible en appelant le CommandManager.InvalidateRequerySuggested sur le thread GUI

Application.Current?.Dispatcher.Invoke(DispatcherPriority.Normal, (Action)CommandManager.InvalidateRequerySuggested);
1
kkCosmo

Certaines de ces réponses concernent la liaison au DataContext pour obtenir la commande elle-même, mais la question était que le CommandParameter était nul alors qu'il ne devrait pas l'être. Nous l'avons également vécu. Sur une intuition, nous avons trouvé un moyen très simple de faire fonctionner cela dans notre ViewModel. Ceci est spécifiquement pour le problème null CommandParameter signalé par le client, avec une ligne de code. Notez Dispatcher.BeginInvoke ().

public DelegateCommand<objectToBePassed> CommandShowReport
    {
        get
        {
            // create the command, or pass what is already created.
            var command = _commandShowReport ?? (_commandShowReport = new DelegateCommand<object>(OnCommandShowReport, OnCanCommandShowReport));

            // For the item template, the OnCanCommand will first pass in null. This will tell the command to re-pass the command param to validate if it can execute.
            Dispatcher.BeginInvoke((Action) delegate { command.RaiseCanExecuteChanged(); }, DispatcherPriority.DataBind);

            return command;
        }
    }
0
TravisWhidden

J'ai récemment rencontré le même problème (pour moi, c'était pour les éléments de menu dans un menu contextuel), même si ce n'est peut-être pas une solution adaptée à chaque situation, j'ai trouvé une manière différente (et beaucoup plus courte!) De résoudre ce problème. problème:

<MenuItem Header="Open file" Command="{Binding Tag.CommandOpenFile, IsAsync=True, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}" CommandParameter="{Binding Name}" />

Ignorant la solution de contournement basée sur Tag pour le cas spécial du menu contextuel, la clé ici est de lier le CommandParameter régulièrement, mais de lier le Command avec le IsAsync=True. Cela retardera un peu la liaison de la commande réelle (et donc son appel CanExecute), donc le paramètre sera déjà disponible. Cela signifie, cependant, que pendant un bref instant, l'état activé pourrait être faux, mais pour mon cas, c'était parfaitement acceptable.

0
Ralf Stauder

J'ai enregistré cela en tant que bogue contre WPF dans .Net 4.0, car le problème existe toujours dans la version bêta 2.

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

0
Swythan