2012-04-23 4 views
6

Próbuję zrozumieć, dlaczego CanExecute jest wywoływana ze źródła polecenia, które zostało usunięte z interfejsu użytkownika. Tutaj jest uproszczony program wykazać:Dlaczego program CanExecute jest wywoływany po usunięciu źródła poleceń z interfejsu użytkownika?

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

Code-tył:

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

Po kliknięciu "Usuń pierwsza pozycja" przycisk jeden raz, otrzymuję ten wynik:

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

"Dlaczego tak się dzieje?" będzie drukowany za każdym razem, gdy kliknę na pustą część okna.

Dlaczego tak się dzieje? I co mogę lub powinienem zrobić, aby zapobiec wywoływaniu CanExecute na usuniętych źródłach poleceń?

Uwaga: RelayCommand można znaleźć here.

Odpowiedzi na pytania Michael Edenfield:

Q1: callstack kiedy CanExecute jest wywoływany na usuniętym przyciskiem:

WpfApplication1.exe WpfApplication1.MainWindow.Foo.get_Act.AnonymousMethod__1 (object param) Linia 30 WpfApplication1.exe! WpfApplication1.RelayCommand.CanExecute (parametr obiektu) Linia 41 + 0x1a bajtów PresentationFramework.dll! MS.Internal.Commands.CommandHelpers.CanExecuteCommandSource (Syste m.Windows.Input.ICommandSource commandSource) + 0x8a bajtów PresentationFramework.dll! System.Windows.Controls.Primitives.ButtonBase.UpdateCanExecute() + 0x18 bajtów PresentationFramework.dll! System.Windows.Controls.Primitives.ButtonBase.OnCanExecuteChanged (obiekt sender, System.EventArgs e) + 0x5 bajtów PresentationCore.dll! System.Windows.Input.CommandManager.CallWeakReferenceHandlers (System.Collections.Generic.List)) + 0xac bajtów PresentationCore.dll! System.Windows.Input.CommandManager. RaiseRequerySuggested (object obj) + 0xf bajtów

Q2: (? nie tylko pierwszy) także, czy to dzieje się zachować, jeśli usunąć wszystkie przyciski z listy

Tak.

+0

Tęsknię za RelayCommand. Co to jest? – Gqqnbig

+0

Dodałem link do implementacji RelayCommand. –

+0

Czy próbowałeś sprawdzić dzwonek podczas wydarzenia i zobaczyć, co go spowodowało? Czy to też się dzieje, jeśli usuniesz * wszystkie * przycisków z listy (nie tylko pierwszego?) –

Odpowiedz

2

Problem jest, że źródłem komend (tzn przycisk) nie wypisany z CanExecuteChanged polecenia jest związany, tak, że gdy CommandManager.RequerySuggested pożary, CanExecute pożary, a także długo po źródłem polecenia nie ma.

Aby rozwiązać ten problem, że realizowane IDisposable na RelayCommand i dodano wymagane kodu, tak, że gdy obiekt modelu usuwa się, a więc wyjmuje się z interfejsu użytkownika, usunąć() jest wywoływana na całej jego RelayCommand.

to zmodyfikowany RelayCommand (oryginał jest 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 
} 

Gdziekolwiek używam powyższego śledzić wszystkie wystąpienia RelayCommands więc mogę powołać Dispose() gdy nadejdzie czas:

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

Znany jest problem z użyciem wyrażenia lambda i zdarzeń, które wydają się być wyzwalane. Waham się nazwać go "błędem", ponieważ nie rozumiem wewnętrznych szczegółów wystarczająco, by wiedzieć, czy to zamierzone zachowanie, ale z pewnością wydaje mi się to sprzeczne z intuicją.

Kluczem tutaj jest wskazanie tej części swojego stosu wywołań:

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

„słaby” wydarzenia są sposobem spięcie zdarzenia, które nie utrzymują żywy obiekt docelowy; jest używany tutaj, ponieważ przekazujesz wyrażenie lamba jako procedurę obsługi zdarzeń, więc "obiekt", który zawiera metodę, jest wewnętrznie wygenerowanym anonimowym obiektem.Problem polega na tym, że obiekt przekazywany do obsługi add dla zdarzenia nie jest tym samym wystąpieniem wyrażenia, co zdarzenie, które jest przekazywane do zdarzenia remove, jest to tylko funkcjonalnie identyczny obiekt, więc nie jest on anulowany z wydarzenia .

Istnieje kilka obejścia, jak opisano w następujących kwestiach:

Weak event handler model for use with lambdas

UnHooking Events with Lambdas in C#

Can using lambdas as event handlers cause a memory leak?

Twoim przypadku najłatwiej jest przenieść CanExecute i wykonanie kodu na rzeczywisty metody:

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

Alternatywnie, jeśli możesz zaaranżować swój obiekt, aby utworzyć Action<> i Func<> delegatów z lambdas, przechowuj je w zmiennych i używaj ich podczas tworzenia swojego RelayCommand, zmusi to samo wystąpienie do użycia. IMO, dla twojej sprawy, która prawdopodobnie jest bardziej skomplikowana niż powinna.

+0

Tworzenie nieanonimowych metod i przekazywanie ich jako parametrów konstruktora RelayCommand niczego nie zmienia. Zrobiłem kilka badań i wydaje się, że problem polega na tym, że źródło poleceń (przycisk) nadal zasubskrybowało CanExecuteChanged (tj. Przycisk automatycznie łączy się z wydarzeniem, ale nie anuluje subskrypcji. –

Powiązane problemy