2012-04-23 7 views
6

Estoy tratando de entender por qué se invoca CanExecute en un origen de comando que se ha eliminado de la interfaz de usuario. Aquí es un programa simplificado para demostrar:¿Por qué se invoca CanExecute después de eliminar el origen del comando de la interfaz de usuario?

<Window x:Class="WpfApplication1.MainWindow" 
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
     Height="350" Width="525"> 
    <StackPanel> 
     <ListBox ItemsSource="{Binding Items}"> 
      <ListBox.ItemTemplate> 
       <DataTemplate> 
        <StackPanel> 
         <Button Content="{Binding Txt}" 
           Command="{Binding Act}" /> 
        </StackPanel> 
       </DataTemplate> 
      </ListBox.ItemTemplate> 
     </ListBox> 
     <Button Content="Remove first item" Click="Button_Click" /> 
    </StackPanel> 
</Window> 

de código subyacente:

public partial class MainWindow : Window 
{ 
    public class Foo 
    { 
     static int _seq = 0; 
     int _txt = _seq++; 
     RelayCommand _act; 
     public bool Removed = false; 

     public string Txt { get { return _txt.ToString(); } } 

     public ICommand Act 
     { 
      get 
      { 
       if (_act == null) { 
        _act = new RelayCommand(
         param => { }, 
         param => { 
          if (Removed) 
           Console.WriteLine("Why is this happening?"); 
          return true; 
         }); 
       } 
       return _act; 
      } 
     } 
    } 

    public ObservableCollection<Foo> Items { get; set; } 

    public MainWindow() 
    { 
     Items = new ObservableCollection<Foo>(); 
     Items.Add(new Foo()); 
     Items.Add(new Foo()); 
     Items.CollectionChanged += 
      new NotifyCollectionChangedEventHandler(Items_CollectionChanged); 
     DataContext = this; 
     InitializeComponent(); 
    } 

    void Items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) 
    { 
     if (e.Action == NotifyCollectionChangedAction.Remove) 
      foreach (Foo foo in e.OldItems) { 
       foo.Removed = true; 
       Console.WriteLine("Removed item marked 'Removed'"); 
      } 
    } 

    void Button_Click(object sender, RoutedEventArgs e) 
    { 
     Items.RemoveAt(0); 
     Console.WriteLine("Item removed"); 
    } 
} 

Cuando pulso en el "Eliminar primer elemento" botón una vez, me sale esta salida:

Removed item marked 'Removed' 
Item removed 
Why is this happening? 
Why is this happening? 

"¿Por qué está pasando esto?" sigue imprimiéndose cada vez que hago clic en alguna parte vacía de la ventana.

¿Por qué sucede esto? ¿Y qué puedo hacer o debo hacer para evitar que CanExecute se invoque en los orígenes de comandos eliminados?

Nota: RelayCommand se puede encontrar here.

Las respuestas a las preguntas Michael Edenfield:

P1: pila de llamadas cuando CanExecute se invoca en el botón eliminado:

WpfApplication1.exe WpfApplication1.MainWindow.Foo.get_Act.AnonymousMethod__1 (parámetro de objeto) Línea 30 WpfApplication1.exe! WpfApplication1.RelayCommand.CanExecute (parámetro de objeto) Línea 41 + 0x1a bytes PresentationFramework.dll! MS.Internal.Commands.CommandHelpers.CanExecuteCommandSource (Syste m.Windows.Input.ICommandSource commandSource) + 0x8a bytes PresentationFramework.dll! System.Windows.Controls.Primitives.ButtonBase.UpdateCanExecute() + 0x18 bytes PresentationFramework.dll! System.Windows.Controls.Primitives.ButtonBase.OnCanExecuteChanged (object remitente, System.EventArgs e) + 0x5 bytes PresentationCore.dll! System.Windows.Input.CommandManager.CallWeakReferenceHandlers (System.Collections.Generic.List handlers) + 0xac bytes PresentationCore.dll! System.Windows.Input.CommandManager. RaiseRequerySuggested (objeto obj) + 0xf bytes

P2: (? no sólo el primero) Además, tiene esto siguen ocurriendo si elimina todos los botones de la lista

Sí.

+0

Extraño RelayCommand. ¿Qué es esto? – Gqqnbig

+0

Agregué un enlace a una implementación de RelayCommand. –

+0

¿Ha intentado verificar la pila de llamadas durante el evento y ver qué lo activó? Además, ¿esto sigue sucediendo si elimina * todos * los botones de la lista (no solo el primero?) –

Respuesta

2

La cuestión es que la fuente de comandos (es decir, el botón) no lo hace estar inscrita en CanExecuteChanged del comando que está obligado a, por lo que cada vez que CommandManager.RequerySuggested incendios, CanExecute incendios, así, mucho después de que la fuente de mando se ha ido.

Para resolver este I implementado IDisposable en RelayCommand, y se añade el código necesario para que cada vez que se elimina un objeto del modelo, y así se elimina de la interfaz de usuario, Dispose() se invoca en toda su RelayCommand.

Este es el modificado RelayCommand (el original es here):

public class RelayCommand : ICommand, IDisposable 
{ 
    #region Fields 

    List<EventHandler> _canExecuteSubscribers = new List<EventHandler>(); 
    readonly Action<object> _execute; 
    readonly Predicate<object> _canExecute; 

    #endregion // Fields 

    #region Constructors 

    public RelayCommand(Action<object> execute) 
     : this(execute, null) 
    { 
    } 

    public RelayCommand(Action<object> execute, Predicate<object> canExecute) 
    { 
     if (execute == null) 
      throw new ArgumentNullException("execute"); 

     _execute = execute; 
     _canExecute = canExecute; 
    } 

    #endregion // Constructors 

    #region ICommand 

    [DebuggerStepThrough] 
    public bool CanExecute(object parameter) 
    { 
     return _canExecute == null ? true : _canExecute(parameter); 
    } 

    public event EventHandler CanExecuteChanged 
    { 
     add 
     { 
      CommandManager.RequerySuggested += value; 
      _canExecuteSubscribers.Add(value); 
     } 
     remove 
     { 
      CommandManager.RequerySuggested -= value; 
      _canExecuteSubscribers.Remove(value); 
     } 
    } 

    public void Execute(object parameter) 
    { 
     _execute(parameter); 
    } 

    #endregion // ICommand 

    #region IDisposable 

    public void Dispose() 
    { 
     _canExecuteSubscribers.ForEach(h => CanExecuteChanged -= h); 
     _canExecuteSubscribers.Clear(); 
    } 

    #endregion // IDisposable 
} 

Dondequiera que uso de lo anterior, el seguimiento de todos RelayCommands instanciados así que pueden invocar Dispose() cuando llegue el momento:

Dictionary<string, RelayCommand> _relayCommands 
    = new Dictionary<string, RelayCommand>(); 

public ICommand SomeCmd 
{ 
    get 
    { 
     RelayCommand command; 
     string commandName = "SomeCmd"; 
     if (_relayCommands.TryGetValue(commandName, out command)) 
      return command; 
     command = new RelayCommand(
      param => {}, 
      param => true); 
     return _relayCommands[commandName] = command; 
    } 
} 

void Dispose() 
{ 
    foreach (string commandName in _relayCommands.Keys) 
     _relayCommands[commandName].Dispose(); 
    _relayCommands.Clear(); 
} 
0

Existe un problema conocido con el uso de la expresión lambda y los eventos que pareces estar desencadenando. Dudo en llamarlo un "error" porque no entiendo los detalles internos lo suficiente como para saber si este es el comportamiento previsto, pero ciertamente me parece contraintuitivo.

La indicación clave aquí es esta parte de su pila de llamadas:

PresentationCore.dll!System.Windows.Input.CommandManager.CallWeakReferenceHandlers(
    System.Collections.Generic.List handlers) + 0xac bytes 

eventos "débiles" son una manera de conectar eventos que no guarda el objeto de destino con vida; se está utilizando aquí porque está pasando una expresión lamba como controlador de eventos, por lo que el "objeto" que contiene el método es un objeto anónimo generado internamente.El problema es que el objeto que se pasa al controlador add para su evento no es la misma instancia de una expresión que la que se pasa al evento remove, es solo un objeto funcionalmente idéntico, por lo que no se anulará la suscripción a su evento .

Hay varias soluciones, como se describe en las siguientes preguntas:

Weak event handler model for use with lambdas

UnHooking Events with Lambdas in C#

Can using lambdas as event handlers cause a memory leak?

para su caso lo más fácil es mover su CanExecute y ejecutar código en real métodos:

if (_act == null) { 
    _act = new RelayCommand(this.DoCommand, this.CanDoCommand); 
} 

private void DoCommand(object parameter) 
{ 
} 

private bool CanDoCommand(object parameter) 
{ 
    if (Removed) 
     Console.WriteLine("Why is this happening?"); 
    return true; 
} 

Alternativamente, si puede organizar su objeto para construir delegados Action<> y Func<> una vez, almacenarlos en variables y usarlos al crear su RelayCommand, obligará a la misma instancia a ser utilizada. OMI, para su caso probablemente sea más complejo de lo que necesita ser.

+0

Crear métodos no anónimos y pasarlos como parámetros del constructor de RelayCommand no cambia nada. Investigué un poco y parece que el problema es que el origen del comando (el botón) todavía está suscrito a CanExecuteChanged (es decir, el botón se engancha automáticamente al evento, pero no se anula la suscripción). –

Cuestiones relacionadas