WPF CommandParameter es NULL la primera vez que se llama a CanExecute

86

Me he encontrado con un problema con WPF y los comandos que están vinculados a un botón dentro del DataTemplate de un ItemsControl. El escenario es bastante sencillo. ItemsControl está vinculado a una lista de objetos, y quiero poder eliminar cada objeto de la lista haciendo clic en un botón. El botón ejecuta un comando y el comando se encarga de la eliminación. El parámetro CommandParameter está vinculado al objeto que quiero eliminar. De esa manera sé en qué hizo clic el usuario. Un usuario solo debería poder eliminar sus "propios" objetos, por lo que necesito hacer algunas comprobaciones en la llamada "CanExecute" del comando para verificar que el usuario tiene los permisos correctos.

El problema es que el parámetro pasado a CanExecute es NULL la primera vez que se llama, por lo que no puedo ejecutar la lógica para habilitar / deshabilitar el comando. Sin embargo, si lo habilito siempre y luego hago clic en el botón para ejecutar el comando, el parámetro CommandParameter se pasa correctamente. Eso significa que el enlace contra CommandParameter está funcionando.

El XAML para ItemsControl y DataTemplate se ve así:

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

Como puede ver, tengo una lista de objetos Comentarios. Quiero que el parámetro CommandParameter del DeleteCommentCommand esté vinculado al objeto Command.

Así que supongo que mi pregunta es: ¿alguien ha experimentado este problema antes? CanExecute recibe una llamada en mi comando, pero el parámetro siempre es NULL la primera vez, ¿por qué?

Actualización: pude reducir un poco el problema. Agregué un Debug ValueConverter vacío para poder generar un mensaje cuando CommandParameter está vinculado a datos. Resulta que el problema es que el método CanExecute se ejecuta antes de que CommandParameter esté vinculado al botón. Intenté configurar el parámetro CommandParameter antes del comando (como se sugiere), pero todavía no funciona. Cualquier consejo sobre cómo controlarlo.

Update2: ¿Hay alguna forma de detectar cuando el enlace está "hecho", de modo que pueda forzar la reevaluación del comando? Además, ¿es un problema que tenga varios botones (uno para cada elemento en ItemsControl) que se unen a la misma instancia de un objeto Command?

Update3: He subido una reproducción del error a mi SkyDrive: http://cid-1a08c11c407c0d8e.skydrive.live.com/self.aspx/Code%20samples/CommandParameterBinding.zip

Jonas Follesø
fuente
Tengo exactamente el mismo problema, con ListBox.
Hadi Eskandari
Actualmente hay un informe de error abierto contra WPF para este problema: github.com/dotnet/wpf/issues/316
UuDdLrLrSs

Respuestas:

14

Me encontré con un problema similar y lo resolví usando mi confiable 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
}

Este convertidor de valores toma cualquier número de parámetros y devuelve el primero de ellos como valor convertido. Cuando se usa en un MultiBinding en su caso, tiene el siguiente aspecto.

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

Tendrá que agregar TriggerConverter como recurso en algún lugar para que esto funcione. Ahora, la propiedad Command se establece no antes de que el valor del CommandParameter esté disponible. Incluso podría enlazar a RelativeSource.Self y CommandParameter en lugar de. para lograr el mismo efecto.

David Liersch
fuente
2
Esto funcionó para mí. No entiendo porque. ¿Alguien puede explicarlo?
TJKjaer
¿No funciona porque el parámetro CommandParameter está vinculado antes que el comando? Dudo que necesite el convertidor ...
MBoros
2
Esta no es una solución. ¿Esto es un truco? ¿Qué diablos está pasando? ¿Esto solía funcionar?
Jordania
¡Perfecto, funciona para mí! La magia está en la línea <Binding />, que hace que el enlace de comando se actualice cuando cambia la plantilla de datos (que está vinculada al parámetro de comando)
Andreas Kahler
56

Estaba teniendo este mismo problema al intentar vincularme a un comando en mi modelo de vista.

Lo cambié para usar un enlace de origen relativo en lugar de referirme al elemento por su nombre y eso funcionó. La vinculación de parámetros no cambió.

Código antiguo:

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

Nuevo código:

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

Actualización : me encontré con este problema sin usar ElementName, estoy vinculando un comando en mi modelo de vista y mi contexto de datos del botón es mi modelo de vista. En este caso, simplemente tuve que mover el atributo CommandParameter antes del atributo Command en la declaración del botón (en XAML).

CommandParameter="{Binding Groups}"
Command="{Binding StartCommand}"
Travis Weber
fuente
42
Mover el parámetro CommandParameter delante del comando es la mejor respuesta en este hilo.
BSick7
6
Mover el orden de los atributos no nos ayudó. Me sorprendería que tuviera algún efecto en el orden de ejecución.
Jack Ukleja
3
No sé por qué funciona esto. Parece que no debería, pero lo hace totalmente.
RMK
1
Tuve el mismo problema: RelativeSource no ayudó, cambiar el orden de los atributos sí. ¡Gracias por la actualización!
Grant Crofton
14
Como persona que religiosamente usa extensiones para embellecer automáticamente XAML (dividir atributos en líneas, corregir sangrías, reordenar atributos), la propuesta de cambiar el orden de CommandParametery Commandme asusta.
Guttsy
29

Descubrí que el orden en el que configuro Command y CommandParameter marca la diferencia. Establecer la propiedad Command hace que CanExecute se llame inmediatamente, por lo que desea que CommandParameter ya esté configurado en ese punto.

Descubrí que cambiar el orden de las propiedades en el XAML puede tener un efecto, aunque no estoy seguro de que resuelva su problema. Sin embargo, vale la pena intentarlo.

Parece que está sugiriendo que el botón nunca se habilita, lo cual es sorprendente, ya que esperaría que CommandParameter se establezca poco después de la propiedad Command en su ejemplo. ¿Llamar a CommandManager.InvalidateRequerySuggested () hace que el botón se habilite?

Ed Ball
fuente
3
Intenté configurar el parámetro CommandParameter antes del comando; todavía ejecuta CanExecute, pero sigue pasando NULL ... Bummer, pero gracias por el consejo. Además, llamar a CommandManager.InvalidateRequerySuggested (); no hace ninguna diferencia.
Jonas Follesø
CommandManager.InvalidateRequerySuggested () resolvió un problema similar para mí. ¡Gracias!
MJS
13

Se me ocurrió otra opción para solucionar este problema que quería compartir. Debido a que el método CanExecute del comando se ejecuta antes de que se establezca la propiedad CommandParameter, creé una clase auxiliar con una propiedad adjunta que obliga al método CanExecute a ser llamado nuevamente cuando cambia el enlace.

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

Y luego, en el botón al que desea vincular un parámetro de comando ...

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

Espero que esto ayude a alguien más con el problema.

Ed Downs
fuente
Bien hecho, gracias. No puedo creer que M $ no haya solucionado esto después de 8 años. Turrible!
McGarnagle
8

Este es un hilo antiguo, pero como Google me trajo aquí cuando tuve este problema, agregaré lo que funcionó para mí para DataGridTemplateColumn con un botón.

Cambiar el enlace de:

CommandParameter="{Binding .}"

a

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

No estoy seguro de por qué funciona, pero a mí me funcionó.

Simon Smith
fuente
Probé las dos respuestas de puntaje alto anteriores, pero esta solo funcionó para mí. Parece que es un problema interno del control en sí mismo, no el enlace, pero aún así muchas personas lo lograron funcionar con las respuestas anteriores. ¡Gracias!
Javidan
6

Recientemente me encontré con el mismo problema (para mí fue para los elementos del menú en un menú contextual), y aunque puede que no sea una solución adecuada para cada situación, encontré una forma diferente (¡y mucho más corta!) De resolver esto. problema:

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

Ignorando la Tagsolución alternativa basada en -para el caso especial del menú contextual, la clave aquí es enlazar CommandParameterregularmente, pero enlazar Commandcon el adicional IsAsync=True. Esto retrasará un poco la vinculación del comando real (y por lo tanto su CanExecutellamada), por lo que el parámetro ya estará disponible. Esto significa, sin embargo, que por un breve momento, el estado habilitado podría estar equivocado, pero para mi caso, eso fue perfectamente aceptable.

Ralf Stauder
fuente
5

Es posible que pueda usar mi CommandParameterBehaviorque publiqué en los foros de Prism ayer. Agrega el comportamiento faltante donde un cambio en la CommandParametercausa Commandse vuelve a consultar.

Aquí hay cierta complejidad causada por mis intentos de evitar la pérdida de memoria causada si llamas PropertyDescriptor.AddValueChangedsin llamar más tarde PropertyDescriptor.RemoveValueChanged. Intento solucionarlo anulando el registro del controlador cuando se descarga el ekement.

Probablemente necesitará eliminar las IDelegateCommandcosas a menos que esté usando Prism (y quiera hacer los mismos cambios que yo en la biblioteca de Prism). También tenga en cuenta que generalmente no usamos RoutedCommands aquí (usamos Prism's DelegateCommand<T>para casi todo) así que por favor no me hagan responsable si mi llamada a CommandManager.InvalidateRequerySuggesteddesencadena algún tipo de cascada de colapso de función de onda cuántica que destruye el universo conocido o cualquier cosa.

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
fuente
encontró su informe de error en connect. ¿Alguna posibilidad de que pueda actualizar su publicación aquí con su último código de esto? ¿O has encontrado una solución mejor?
Markus Hütter
Una solución más sencilla puede ser observar la propiedad CommandParameter utilizando un enlace en lugar de un descriptor de propiedad. De lo contrario, ¡una gran solución! Este realmente soluciona el problema subyacente en lugar de simplemente introducir un truco o una solución incómoda.
Sebastian Negraszus
1

Hay una forma relativamente sencilla de "solucionar" este problema con DelegateCommand, aunque requiere actualizar la fuente DelegateCommand y volver a compilar Microsoft.Practices.Composite.Presentation.dll.

1) Descargue el código fuente de Prism 1.2 y abra CompositeApplicationLibrary_Desktop.sln. Aquí hay un proyecto Composite.Presentation.Desktop que contiene la fuente DelegateCommand.

2) En el evento público EventHandler CanExecuteChanged, modifique para que se lea como sigue:

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) Bajo el vacío virtual protegido OnCanExecuteChanged (), modifíquelo de la siguiente manera:

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

4) Vuelva a compilar la solución, luego navegue a la carpeta Debug o Release donde se encuentran las DLL compiladas. Copie Microsoft.Practices.Composite.Presentation.dll y .pdb (si lo desea) donde hace referencia a sus ensamblados externos y luego vuelva a compilar su aplicación para extraer las nuevas versiones.

Después de esto, CanExecute debe activarse cada vez que la interfaz de usuario muestra elementos vinculados al DelegateCommand en cuestión.

Cuídate Joe

árbitrojoe en gmail

Joe Bako
fuente
1

Después de leer algunas buenas respuestas a preguntas similares, cambié un poco en su ejemplo DelegateCommand para que funcione. En lugar de usar:

public event EventHandler CanExecuteChanged;

Lo cambié a:

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

Eliminé los siguientes dos métodos porque era demasiado vago para arreglarlos

public void RaiseCanExecuteChanged()

y

protected virtual void OnCanExecuteChanged()

Y eso es todo ... esto parece garantizar que se llamará a CanExecute cuando cambie el enlace y después del método Execute

No se activará automáticamente si se cambia el modelo de vista, pero como se menciona en este hilo, es posible llamar al CommandManager.InvalidateRequerySuggested en el hilo de la GUI

Application.Current?.Dispatcher.Invoke(DispatcherPriority.Normal, (Action)CommandManager.InvalidateRequerySuggested);
kkCosmo
fuente
Descubrí que DispatcherPriority.Normales demasiado alto para funcionar de manera confiable (o en absoluto, en mi caso). Usar DispatcherPriority.Loadedfunciona bien y parece más apropiado (es decir, indica explícitamente que no se debe invocar al delegado hasta que los elementos de la interfaz de usuario asociados con el modelo de vista se hayan cargado).
Peter Duniho
0

Hola Jonas, no estoy seguro de si esto funcionará en una plantilla de datos, pero aquí está la sintaxis de enlace que uso en un menú contextual de ListView para tomar el elemento actual como parámetro de comando:

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


fuente
Hago exactamente lo mismo en mi vista de lista. En este caso, es un ItemsControl, por lo que no hay una propiedad obvia contra la que "vincularse" (en el árbol visual). Supongo que tengo que encontrar una manera de detectar cuándo se realiza el enlace y volver a evaluar CanExecute (porque CommandParameter se enlaza, solo demasiado tarde)
Jonas Follesø
0

Algunas de estas respuestas tratan sobre la vinculación al DataContext para obtener el comando en sí, pero la pregunta era sobre el parámetro CommandParameter siendo nulo cuando no debería serlo. También experimentamos esto. Por una corazonada, encontramos una manera muy simple de hacer que esto funcione en nuestro ViewModel. Esto es específicamente para el problema nulo de CommandParameter informado por el cliente, con una línea de código. Tenga en cuenta el 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;
        }
    }
TravisWhidden
fuente
-1

Es una posibilidad remota. para depurar esto, puede intentar:
- comprobar el evento PreviewCanExecute.
- use snoop / wpf mole para mirar dentro y ver cuál es el parámetro de comando.

HTH,

Dennis
fuente
Intenté usar Snoop, pero es realmente difícil de depurar ya que solo es NULL cuando se carga inicialmente. Si ejecuto Snoop en él, Command y CommandParameter son ambos seth ... Tiene que ver con el uso de Comandos en DataTemplate.
Jonas Follesø
-1

El commandManager.InvalidateRequerySuggested también funciona para mí. Creo que el siguiente enlace habla de un problema similar, y M $ dev confirmó la limitación en la versión actual, y el commandManager.InvalidateRequerySuggested es la solución. http://social.expression.microsoft.com/Forums/en-US/wpf/thread/c45d2272-e8ba-4219-bb41-1e5eaed08a1f/

Lo importante es el momento en que se invoca el comando commandManager.InvalidateRequerySuggested. Esto debe invocarse después de que se notifique el cambio de valor relevante.


fuente
ese enlace ya no es válido
Peter Duniho
-2

Además de la sugerencia de Ed Ball sobre la configuración de CommandParameter antes de Command , asegúrese de que su método CanExecute tenga un parámetro de tipo de objeto .

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

Espero que evite que alguien pase la gran cantidad de tiempo que hice para descubrir cómo recibir SelectedItems como parámetro CanExecute

Julio Nobre
fuente