Question

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. ItemsControl est lié à une liste d'objets et je souhaite 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. De cette façon, je sais sur quoi l'utilisateur a cliqué. Un utilisateur ne devrait pouvoir supprimer que son & Quot; propre & Quot; objets - il me faut donc effectuer quelques vérifications dans le fichier & "CanExecute &"; appel de la commande pour vérifier que l'utilisateur dispose des autorisations appropriées.

Le problème est que le paramètre transmis à CanExecute est NULL lors de son premier appel - je ne peux donc pas exécuter la logique pour activer / désactiver la commande. Cependant, si je l'active toujours, puis que je clique sur le bouton pour exécuter la commande, le CommandParameter est transmis correctement. Cela signifie donc que la liaison avec CommandParameter fonctionne.

Le code XAML pour ItemsControl et DataTemplate se présente comme 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"
                    Command="{Binding Path=DataContext.DeleteCommentCommand, ElementName=commentsList}" 
                    CommandParameter="{Binding}" />
            </StackPanel>                       
         </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

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

Donc, je suppose 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 cerner un peu le problème. J'ai ajouté un Debug ValueConverter vide afin de pouvoir afficher un message lorsque le CommandParameter est lié aux données. Le problème est que la méthode CanExecute est exécutée avant que CommandParameter ne soit lié au bouton. J'ai essayé de définir CommandParameter avant la commande (comme suggéré) - mais cela ne fonctionne toujours pas. Des conseils sur la façon de le contrôler.

Update2: Y a-t-il un moyen de détecter le moment où la liaison est & terminée & "; afin de pouvoir forcer la réévaluation de la commande?" De plus, est-ce un problème que j'ai plusieurs boutons (un pour chaque élément dans le contrôle des éléments) qui se lient à la même instance d'un objet de commande?

Update3: j'ai importé une copie du bogue sur mon lecteur SkyDrive: http://cid-1a08c11c407c0d8e.skydrive.live.com/self.aspx/Code%20samples/CommandParameterBinding.zip

Était-ce utile?

La solution

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 en tant que valeur convertie. Lorsqu'il est utilisé dans une liaison multiple 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 en tant que ressource quelque part pour que cela fonctionne. Désormais, la propriété Command n'est pas définie avant que la valeur de CommandParameter soit disponible. Vous pouvez même vous lier à RelativeSource.Self et CommandParameter au lieu de. pour obtenir le même effet.

Autres conseils

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

Je l'ai modifié pour utiliser une liaison source relative plutôt que de faire référence à l'élément par son nom, ce qui a permis de résoudre le problème. 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 tout juste de rencontrer ce problème sans utiliser ElementName, je suis lié à une commande de mon modèle de vue et le contexte de données de ce bouton correspond à 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}"

J'ai constaté que l'ordre dans lequel je règle Command et ParameterParameter fait une différence. La définition de la propriété Command entraîne l’appel immédiat de CanExecute. Vous souhaitez donc que CommandParameter soit déjà défini à ce stade.

J'ai constaté que le fait de changer l'ordre des propriétés dans le code XAML pouvait avoir un effet, bien que je ne sois pas sûr 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 CommandParameter soit défini peu de temps après la propriété Command de votre exemple. L’appel de CommandManager.InvalidateRequerySuggested () entraîne-t-il l’activation du bouton?

Je sais que ce fil de discussion est un peu vieux, mais j'ai proposé une autre option pour contourner ce problème que je souhaitais partager. Étant donné que la méthode CanExecute de la commande est exécutée avant la définition de la propriété CommandParameter, j'ai créé une classe d'assistance 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);
    }
}

Ensuite, sur le bouton, 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 le problème.

Il s'agit d'un vieux fil de discussion, mais comme Google m'a amené ici avec ce problème, j'ajouterai ce qui a fonctionné pour moi pour un DataGridTemplateColumn avec un bouton.

Modifier la liaison de:

CommandParameter="{Binding .}"

à

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

Je ne sais pas pourquoi cela fonctionne, mais cela a fonctionné pour moi.

Vous pourrez peut-être utiliser le CommandParameterBehavior que j'ai publié sur Forums Prism hier. Il ajoute le comportement manquant dans lequel une modification du CommandParameter a pour effet de requérir la requête Command .

Les tentatives que j'ai faites pour éviter la fuite de mémoire si vous appelez PropertyDescriptor.AddValueChanged sans appeler ultérieurement PropertyDescriptor.RemoveValueChanged ont été quelque peu complexes. J'essaie de résoudre ce problème en annulant l'enregistrement du gestionnaire lorsque l'ekement est déchargé.

Vous aurez probablement besoin de supprimer le IDelegateCommand à moins que vous n'utilisiez Prism (et que vous souhaitiez apporter les mêmes modifications que moi à la bibliothèque Prism). Notez également que nous n'utilisons généralement pas les RoutedCommand (nous utilisons le DelegateCommand < T > de Prism pour à peu près tout), veuillez donc ne pas me tenir responsable si mon appel to CommandManager.InvalidateRequerySuggested déclenche une sorte de cascade d'effondrement d'effondrement d'onde quantique 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();
            }

        }
    }
}

Il existe un moyen relativement simple de "réparer" ce problème avec DelegateCommand, bien que cela 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. Voici un projet Composite.Presentation.Desktop contenant la source DelegateCommand.

2) Sous l'événement public EventHandler CanExecuteChanged, modifier 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 void virtuel protégé OnCanExecuteChanged (), 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 se trouvent les DLL compilées. Copiez les fichiers 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.

Ensuite, CanExecute doit être lancé à chaque fois que l'interface utilisateur restitue les éléments liés au DelegateCommand en question.

Prenez soin de vous, Joe

arbitrerjoe à gmail

Après avoir lu quelques bonnes réponses à des questions similaires, j'ai légèrement modifié dans votre exemple le DelegateCommand pour que cela fonctionne. Au lieu 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 résoudre

public void RaiseCanExecuteChanged()

et

protected virtual void OnCanExecuteChanged()

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

Il ne se déclenchera pas automatiquement si le ViewModel est modifié, mais comme mentionné dans ce fil est possible en appelant le CommandManager.InvalidateRequerySuggested sur le fil de l'interface graphique

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

Hé, Jonas, je ne sais pas si cela fonctionnera dans un modèle de données, mais voici la syntaxe de liaison que j'utilise dans un menu contextuel ListView pour saisir l'élément actuel en tant que paramètre de commande:

CommandParameter = & {{Reliure RelativeSource = {RelativeSource AncestorType = ContextMenu}, Path = PlacementTarget.SelectedItem, Mode = TwoWay} " <

Je l'ai enregistré comme un 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

Certaines de ces réponses concernent la liaison au DataContext pour obtenir la commande elle-même, mais la question concernait le fait que le paramètre CommandParameter était nul alors qu'il ne devrait pas l'être. Nous avons également vécu cela. De manière intuitive, nous avons trouvé un moyen très simple de faire en sorte que cela fonctionne dans notre ViewModel. Cela concerne spécifiquement le problème null de CommandParameter signalé par le client, avec une ligne de code. Notez le 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;
        }
    }

J'ai récemment rencontré le même problème (pour moi, c'était pour les éléments de menu dans un menu contextuel), bien que ce ne soit 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:

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

En ignorant la solution de contournement basée sur la balise Tag pour le cas particulier du menu contextuel, la clé consiste à lier régulièrement le CommandParameter , mais la liaison à la Command avec le IsAsync = True supplémentaire. Cela retardera la liaison de la commande réelle (et donc de son appel CanExecute ), de sorte que le paramètre sera déjà disponible. Cela signifie cependant que, pendant un bref instant, l'état activé pourrait être erroné, mais pour mon cas, cela était parfaitement acceptable.

C'est un long shot. pour déboguer ceci, vous pouvez essayer:
- vérification de l'événement PreviewCanExecute.
- Utilisez snoop / wpf mole pour jeter un coup d'œil à l'intérieur et voir ce qu'est le paramètre de commande.

HTH,

Le commandManager.InvalidateRequerySuggested fonctionne également pour moi. Je crois que le lien suivant parle d'un problème similaire, et M $ dev a confirmé la limitation dans la version actuelle, et le commandManager.InvalidateRequerySuggested est la solution de contournement. http: // social .expression.microsoft.com / Forums / fr-fr / wpf / thread / c45d2272-e8ba-4219-bb41-1e5eaed08a1f /

Ce qui est important, c'est le moment d'appeler le commandManager.InvalidateRequerySuggested. Ceci doit être invoqué une fois que le changement de valeur correspondant est notifié.

À côté de suggestion d'Ed Ball sur la définition de CommandParameter avant Commande , assurez-vous que votre méthode CanExecute a un paramètre de type object .

private bool OnDeleteSelectedItemsCanExecute(object SelectedItems)  
{
    // Your goes heres
}

J'espère que cela empêchera quelqu'un de passer autant de temps que moi à comprendre comment recevoir SelectedItems en tant que paramètre CanExecute

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top