2010-06-12 16 views
7

Mam tricky problem, gdzie mam wiążącą się ContextMenu do zestawu ICommand pochodzące z A obiektów oraz ustawianie Command i CommandParameter właściwości na każdym MenuItem poprzez stylu:ICommand.CanExecute przepuszcza zerowy mimo CommandParameter jest ustawiony

<ContextMenu 
    ItemsSource="{Binding Source={x:Static OrangeNote:Note.MultiCommands}}"> 
    <ContextMenu.Resources> 
     <Style 
      TargetType="MenuItem"> 
      <Setter 
       Property="Header" 
       Value="{Binding Path=Title}" /> 
      <Setter 
       Property="Command" 
       Value="{Binding}" /> 
      <Setter 
       Property="CommandParameter" 
       Value="{Binding Source={x:Static OrangeNote:App.Screen}, Path=SelectedNotes}" /> 
... 

Jednak podczas gdy zostaje przekazany zestaw zaznaczonych nut tak, jak powinien, ICommand.CanExecute(object) (wywoływane podczas tworzenia menu) otrzymuje pomijanie. Sprawdziłem i wybrana kolekcja notatek została poprawnie utworzona przed wywołaniem (w rzeczywistości przypisano jej wartość w deklaracji, więc nigdy nie jest to null). Nie mogę się domyślić, dlaczego CanEvaluate przechodzi pomyślnie null.

+0

miałem dokładnie ten sam problem. Moim rozwiązaniem było powiązanie komendy po parametrze komend, po prostu ustawiając parametr polecenia setter przed komendą ustawiającą i nagle związany parametr został przekazany do pierwszego wywołania 'CanExecute'. – Cubinator73

Odpowiedz

7

Ustaliłem, że są co najmniej dwa błędy w ContextMenu, które powodują, że jego wywołania CanExecute mogą być niewiarygodne w różnych okolicznościach. Wywołuje CanExecute natychmiast po ustawieniu polecenia. Późniejsze rozmowy są nieprzewidywalne i na pewno nie są wiarygodne.

Spędziłem całą noc, próbując wyśledzić dokładne warunki, w których zawiedzie i szuka obejścia. W końcu zrezygnowałem i przełączyłem się na programy obsługi kliknięć, które uruchamiały żądane polecenia.

Zrobiłem, że jednym z moich problemów było to, że zmiana DataContext w ContextMenu może spowodować wywołanie CanExecute przed związaniem nowego Command lub CommandParameter.

Najlepszym rozwiązaniem znam tego problemu jest wykorzystanie własnych dołączonych właściwości Command and CommandBinding zamiast przy użyciu wbudowanych w te:

  • Gdy załączony właściwość poleceń jest ustawiony, zapisz się do Kliknij zdarzenia DataContextChanged w menuItem, a także zasubskrybuj CommandManager.RequerySuggested.

  • Po zmianie DataContext, RequerySuggested przychodzi, czy też swoich dwóch dołączonych właściwości zmian, zaplanować pracę dyspozytora za pomocą Dispatcher.BeginInvoke że wezwie swój CanExecute() i aktualizować IsEnabled na MenuItem.

  • Po uruchomieniu zdarzenia Click, wykonaj czynność CanExecute, a jeśli się uda, wywołaj Execute().

Wykorzystanie jest jak regularny Command and CommandParameter, ale przy użyciu załączonego właściwości zamiast:

<Setter Property="my:ContexrMenuFixer.Command" Value="{Binding}" /> 
<Setter Property="my:ContextMenuFixer.CommandParameter" Value="{Binding Source=... }" /> 

To rozwiązanie działa i omija wszelkie problemy z błędami w obsłudze contextMenu za CanExecute.

Mam nadzieję, że pewnego dnia Microsoft rozwiąże problemy z ContextMenu i to obejście nie będzie już konieczne. Mam sprawę repro siedzącą gdzieś tutaj, którą zamierzam złożyć w Connect. Być może powinienem dostać piłkę i faktycznie to zrobić.

Co to jest RequerySuggested i dlaczego go używać?

Mechanizm RequerySuggested to metoda RoutedCommand do sprawnej obsługi ICommand.CanExecuteChanged.W świecie innym niż RoutedCommand, każda ICommand ma własną listę subskrybentów CanExecuteChanged, ale dla RoutedCommand każdy klient subskrybujący ICommand.CanExecuteChanged będzie faktycznie subskrybował CommandManager.RequerySuggested. Ten prostszy model oznacza, że ​​za każdym razem, gdy CanExecute RoutedCommand może się zmienić, wystarczy wywołać CommandManager.InvalidateRequerySuggested(), co spowoduje to samo, co uruchomienie ICommand.CanExecuteChanged, ale zrobi to dla wszystkich RoutedCommands jednocześnie i wątku tła. Ponadto wywołania RequerySuggested są łączone razem, więc jeśli pojawi się wiele zmian, CanExecute musi zostać wywołane tylko jeden raz.

Powody, dla których poleciłem, aby zasubskrybować CommandManager.RequerySuggested zamiast ICommand.CanExecuteChanged, to: 1. Nie potrzebujesz kodu do usuwania starej subskrypcji i dodawania nowej za każdym razem, gdy zmieni się wartość Twojej właściwości Załączonej komendy i 2. CommandManager.RequerySuggested ma wbudowaną funkcję słabej referencji, która pozwala ustawić obsługę zdarzeń i nadal być zbierane śmieci. Postępując tak samo z ICommand, musisz wdrożyć swój własny słaby mechanizm referencyjny.

Z drugiej strony, jeśli subskrybujesz CommandManager.RequerySuggested zamiast ICommand.CanExecuteChanged, otrzymasz aktualizacje tylko dla RoutedCommands. Korzystam wyłącznie z RoutedCommands, więc nie jest to dla mnie problemem, ale powinienem był wspomnieć, że jeśli korzystasz z regularnych poleceń ICommands, powinieneś rozważyć wykonanie dodatkowej pracy polegającej na słabym subskrybowaniu ICommand.CanExecutedChanged. Zauważ, że jeśli to zrobisz, nie musisz także subskrybować RequerySuggested, ponieważ RoutedCommand.add_CanExecutedChanged już to robi.

+0

Wow, to skomplikowane rozwiązanie dla raczej prostej rzeczy do zrobienia. Kilka pytań: jak używać CommandManager.RequerySuggested (to zdarzenie statyczne, co dokładnie mam sprawdzić?) I jaka jest trzecia dołączona właściwość, o której wspomniałeś, oprócz Command i CommandParameter? – devios1

+1

Och, widzę, ta "dołączona własność tylko do użytku wewnętrznego" ... czy nie mogę po prostu zapisać się do DataContextChanged? – devios1

+0

Mam to działa! w00t! Dzięki :) Ciągle ciekawi RequerySuggested tho ... co to dokładnie jest? – devios1

8

Wierzę, że to jest związane z connect problem zalogować tutaj:

https://connect.microsoft.com/VisualStudio/feedback/details/504976/command-canexecute-still-not-requeried-after-commandparameter-change?wa=wsignin1.0

Moje obejście jest następujący:

  1. Utwórz klasę statyczną z dołączonym właściwość zależność parametru komenda bound
  2. Utwórz niestandardowy interfejs do ręcznego podnoszenia CanExecuteChanged na niestandardowej komendzie
  3. Implementuj interfejs w każdym poleceniu, które musi wiedzieć o zmianach parametrów.

    public interface ICanExecuteChanged : ICommand 
    { 
        void RaiseCanExecuteChanged(); 
    } 
    
    public static class BoundCommand 
    { 
        public static object GetParameter(DependencyObject obj) 
        { 
         return (object)obj.GetValue(ParameterProperty); 
        } 
    
        public static void SetParameter(DependencyObject obj, object value) 
        { 
         obj.SetValue(ParameterProperty, value); 
        } 
    
        public static readonly DependencyProperty ParameterProperty = DependencyProperty.RegisterAttached("Parameter", typeof(object), typeof(BoundCommand), new UIPropertyMetadata(null, ParameterChanged)); 
    
        private static void ParameterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 
        { 
         var button = d as ButtonBase; 
         if (button == null) 
         { 
          return; 
         } 
    
         button.CommandParameter = e.NewValue; 
         var cmd = button.Command as ICanExecuteChanged; 
         if (cmd != null) 
         { 
          cmd.RaiseCanExecuteChanged(); 
         } 
        } 
    } 
    

realizacja poleceń:

public class MyCustomCommand : ICanExecuteChanged 
    { 
     public void Execute(object parameter) 
     { 
      // Execute the command 
     } 

     public bool CanExecute(object parameter) 
     { 
      Debug.WriteLine("Parameter changed to {0}!", parameter); 
      return parameter != null; 
     } 

     public event EventHandler CanExecuteChanged; 

     public void RaiseCanExecuteChanged() 
     { 
      EventHandler temp = this.CanExecuteChanged; 
      if (temp != null) 
      { 
       temp(this, EventArgs.Empty); 
      } 
     } 
    } 

Xaml Zastosowanie:

<Button Content="Save" 
     Command="{Binding SaveCommand}" 
     my:BoundCommand.Parameter="{Binding Document}" /> 

Jest to najprostsza poprawka mogłem wymyślić i to działa uczta dla wdrożeń typu MVVM. Można również wywołać funkcję CommandManager.InvalidateRequerySuggested() w zmianie parametru BoundCommand tak, aby działała również z RoutedCommands.

+0

Pracowałem dla mnie dobrze – Artiom

1

Natknąłem się na tę sytuację na DataGrid, gdzie potrzebowałem menu kontekstowego, aby rozpoznać, czy włączyć lub wyłączyć określone polecenia w zależności od wybranego wiersza. Odkryłem, że tak obiekt przekazany do polecenia ma wartość zerową i że został wykonany tylko raz dla wszystkich wierszy, niezależnie od tego, czy nastąpiła zmiana czy nie.

To, co zrobiłem, to zadzwonić pod numer RaiseCanExecuteChanged w sprawie konkretnych poleceń, które wyzwalałyby włączenie lub wyłączenie w wybranym zdarzeniu wyboru siatki.


private void MyGrid_OnSelectionChanged(object sender, SelectionChangedEventArgs e) 
{ 
    VM.DeleteItem.RaiseCanExecuteChanged(); 
} 

Wiązanie polecenie przypisania

VM.DeleteItem 
    = new OperationCommand((o) => MessageBox.Show("Delete Me"), 
          (o) => (myGrid.SelectedItem as Order)?.InProgress == false); 

Wynik

Jeżeli InProgress jest polecenie true usunięcie nie jest włączona

enter image description here

XAML

<DataGrid AutoGenerateColumns="True" 
     Name="myGrid" 
     ItemsSource="{Binding Orders}" 
     SelectionChanged="MyGrid_OnSelectionChanged"> 
    <DataGrid.ContextMenu> 
     <ContextMenu> 
      <MenuItem Header="Copy" Command="{Binding CopyItem}"/> 
      <MenuItem Header="Delete" Command="{Binding DeleteItem}" /> 
     </ContextMenu> 
    </DataGrid.ContextMenu> 
</DataGrid>