commandmanager - WPF CommandParameter es NULL la primera vez que se llama CanExecute
mvvm wpf (14)
Además de la sugerencia de Ed Ball de configurar CommandParameter antes de Comando , asegúrese de que su método CanExecute tenga un parámetro de tipo de objeto .
bool privado 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
Me he encontrado con un problema con WPF y comandos que están vinculados a un botón dentro de la DataTemplate de un ItemsControl. El escenario es bastante directo. 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. 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 tenga 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 hago siempre habilitado, y luego hago clic en el botón para ejecutar el comando, CommandParameter se pasa correctamente. Entonces 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>
Entonces, como pueden ver, tengo una lista de objetos Comentarios. Quiero que CommandParameter de DeleteCommentCommand se vincule al objeto Command.
Así que supongo que mi pregunta es: ¿Alguien ha experimentado este problema antes? Se llama a CanExecute en mi comando, pero el parámetro siempre es NULL la primera vez, ¿por qué?
Actualización: Pude reducir el problema un poco. Agregué un Debug ValueConverter vacío para poder dar salida a un mensaje cuando CommandParameter está enlazado a datos. Resulta que el problema es que el método CanExecute se ejecuta antes de que CommandParameter se vincule al botón. Intenté configurar CommandParameter antes del comando (como se sugiere), pero todavía no funciona. Cualquier consejo sobre cómo controlarlo.
Actualización2: ¿Hay alguna forma de detectar cuándo se realiza el enlace, de modo que pueda forzar la reevaluación del comando? Además, ¿es un problema que tengo varios botones (uno para cada elemento en ItemsControl) que se unen a la misma instancia de un objeto de comando?
Actualización3: he cargado una reproducción del error en mi SkyDrive: http://cid-1a08c11c407c0d8e.skydrive.live.com/self.aspx/Code%20samples/CommandParameterBinding.zip
Algunas de estas respuestas se refieren al enlace al DataContext para obtener el Comando en sí, pero la pregunta era que CommandParameter era nulo cuando no debería ser. También experimentamos esto. En 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;
}
}
Descubrí que el orden en el que configuro Command y CommandParameter hace una diferencia. Establecer la propiedad Command hace que CanExecute se invoque inmediatamente, por lo que desea que CommandParameter ya esté configurado en ese punto.
Descubrí que cambiar el orden de las propiedades en XAML puede tener un efecto real, aunque no estoy seguro de que resuelva su problema. Pero vale la pena intentarlo.
Pareces estar sugiriendo que el botón nunca se habilita, lo que es sorprendente, ya que esperaría que el parámetro de comando se establezca poco después de la propiedad de comando en tu ejemplo. ¿La llamada a CommandManager.InvalidateRequerySuggested () hace que el botón se habilite?
Después de leer algunas buenas respuestas a preguntas similares, modifiqué ligeramente 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 solucionarlos
public void RaiseCanExecuteChanged()
y
protected virtual void OnCanExecuteChanged()
Y eso es todo ... esto parece garantizar que se invocará CanExecute cuando cambie la vinculación 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 subproceso, al invocar CommandManager.InvalidateRequerySuggested en el hilo de la GUI
Application.Current?.Dispatcher.Invoke(DispatcherPriority.Normal, (Action)CommandManager.InvalidateRequerySuggested);
El comandoManager.InvalidateRequerySuggested también funciona para mí. Creo que el siguiente enlace habla sobre un problema similar, y M $ dev confirmó la limitación en la versión actual, y el comando Manager.InvalidateRequerySuggested es la solución alternativa. http://social.expression.microsoft.com/Forums/en-US/wpf/thread/c45d2272-e8ba-4219-bb41-1e5eaed08a1f/
Lo importante es el tiempo de invocación del comandoManager.InvalidateRequerySuggested. Esto debe invocarse después de notificar el cambio de valor relevante.
Es posible que pueda usar mi CommandParameterBehavior
que CommandParameterBehavior
en los foros de Prism ayer. Agrega el comportamiento que falta cuando un cambio en CommandParameter
hace que el Command
se vuelva a consultar.
Aquí hay una cierta complejidad causada por mis intentos de evitar la pérdida de memoria provocada si llama a PropertyDescriptor.AddValueChanged
sin llamar más tarde a PropertyDescriptor.RemoveValueChanged
. Intento solucionarlo anulando el registro del manejador cuando se descarga el ekement.
Probablemente necesites eliminar el IDelegateCommand
menos que estés usando Prism (y quieras hacer los mismos cambios que yo en la biblioteca de Prism). También tenga en cuenta que generalmente no usamos RoutedCommand
aquí (utilizamos DelegateCommand<T>
Prism para prácticamente todo), así que no me haga responsable si mi llamada a CommandManager.InvalidateRequerySuggested
desencadena algún tipo de cascada de colapso de fusió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();
}
}
}
}
Es una posibilidad remota. para depurar esto puedes probar:
- verificar el evento PreviewCanExecute.
- usa mole snoop / wpf para mirar dentro y ver qué es el parámetro de comando.
HTH,
Estaba teniendo este mismo problema al intentar enlazar a un comando en mi modelo de vista.
Lo cambié para usar un enlace de fuente relativo en lugar de referirme al elemento por nombre y eso fue lo que hizo. El enlace de los 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 : Acabo de enterarme de 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, tuve que mover simplemente el atributo CommandParameter antes del atributo Command en la declaración Button (en XAML).
CommandParameter="{Binding Groups}"
Command="{Binding StartCommand}"
Este es un hilo viejo, pero como Google me trajo aquí cuando tuve este problema, agregaré lo que funcionó para mí para un DataGridTemplateColumn con un botón.
Cambiar la vinculación de:
CommandParameter="{Binding .}"
a
CommandParameter="{Binding DataContext, RelativeSource={RelativeSource Self}}"
No estoy seguro de por qué funciona, pero lo hizo por mí.
Hay una manera relativamente simple de "solucionar" este problema con DelegateCommand, aunque requiere actualizar el origen de 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, modifíquelo para que se lea de la siguiente manera:
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) En 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) Recompile la solución, luego navegue a la carpeta Depurar o Liberar donde viven las DLL compiladas. Copie Microsoft.Practices.Composite.Presentation.dll y .pdb (si lo desea) a donde hace referencia a sus ensamblajes externos, y luego recompile su aplicación para extraer las nuevas versiones.
Después de esto, CanExecute debe activarse cada vez que la UI represente elementos vinculados al DelegateCommand en cuestión.
Cuídate, Joe
refereejoe en gmail
He registrado esto como un error contra WPF en .Net 4.0, ya que el problema todavía existe en Beta 2.
https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=504976
Hola Jonas, no estoy seguro si esto funcionará en una plantilla de datos, pero aquí está la sintaxis de enlace que uso en un menú de contexto de ListView para tomar el elemento actual como un parámetro de comando:
CommandParameter = "{Enlace RelativeSource = {RelativeSource AncestorType = ContextMenu}, Path = PlacementTarget.SelectedItem, Mode = TwoWay}"
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 valor toma cualquier número de parámetros y pasa el primero de ellos como el 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 un recurso en alguna parte para que esto funcione. Ahora la propiedad Command se establece no antes de que el valor para CommandParameter esté disponible. Incluso podría enlazar a RelativeSource.Self y CommandParameter en lugar de a. para lograr el mismo efecto
Sé que este hilo es algo viejo, pero he encontrado otra opción para solucionar este problema que quería compartir. Como 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 invocarse 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 que desea vincular un parámetro de comando para ...
<Button
Content="Press Me"
Command="{Binding}"
helpers:ButtonHelper.CommandParameter="{Binding MyParameter}" />
Espero que esto ayude a alguien más con el problema.