2010-06-12 29 views
7

Tôi có một vấn đề khó khăn mà tôi đang ràng buộc một ContextMenu đến một tập hợp các đối tượng ICommand -derived, và thiết lập các CommandCommandParameter tính trên mỗi MenuItem qua một phong cách:ICommand.CanExecute được thông qua rỗng mặc dù CommandParameter được thiết lập

<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}" /> 
... 

Tuy nhiên, trong khi được chuyển qua tập hợp các ghi chú đã chọn như cần, ICommand.CanExecute(object) (được gọi khi trình đơn được tạo) được chuyển thành giá trị rỗng. Tôi đã kiểm tra và bộ sưu tập ghi chú đã chọn được khởi tạo đúng cách trước khi cuộc gọi được thực hiện (trên thực tế nó được gán một giá trị trong khai báo của nó, vì vậy nó không bao giờ là null). Tôi không thể tìm ra lý do tại sao CanEvaluate được thông qua null.

+0

tôi đã có vấn đề chính xác như vậy. Giải pháp của tôi là liên kết lệnh sau tham số lệnh bằng cách đặt setter của tham số lệnh trước khi setter của lệnh và đột nhiên paramter bị ràng buộc được chuyển tới cuộc gọi đầu tiên của 'CanExecute'. – Cubinator73

Trả lời

7

Tôi đã xác định rằng có ít nhất hai lỗi trong ContextMenu khiến các cuộc gọi CanExecute của nó không đáng tin cậy trong các trường hợp khác nhau. Nó gọi CanExecute ngay lập tức khi Command được thiết lập. Các cuộc gọi sau này là không thể đoán trước và chắc chắn không đáng tin cậy.

Tôi đã dành cả đêm khi cố gắng theo dõi các điều kiện chính xác mà theo đó nó sẽ không thành công và tìm cách giải quyết. Cuối cùng tôi đã từ bỏ và chuyển sang Trình xử lý nhấp chuột đã kích hoạt các lệnh mong muốn.

Tôi đã xác định rằng một trong những vấn đề của tôi là việc thay đổi DataContext của ContextMenu có thể làm cho CanExecute được gọi trước khi Command hoặc CommandParameter mới bị ràng buộc.

Giải pháp tốt nhất mà tôi biết cho vấn đề này là sử dụng tài sản gắn liền của riêng bạn cho chỉ huy và CommandBinding thay vì sử dụng được xây dựng trong những:

  • Khi thuộc tính chỉ huy kết nối của bạn được thiết lập, đăng ký với Các sự kiện Click và DataContextChanged trên MenuItem, và cũng đăng ký CommandManager.RequerySuggested.

  • Khi DataContext thay đổi, RequerySuggested xuất hiện hoặc một trong hai thuộc tính đính kèm của bạn thay đổi, lên lịch điều phối bằng Dispatcher.BeginInvoke sẽ gọi CanExecute() và cập nhật IsEnabled trên MenuItem.

  • Khi sự kiện Nhấp chuột kích hoạt, thực hiện điều CanExecute và nếu nó trôi qua, hãy gọi Execute().

Cách sử dụng cũng giống như lệnh thường xuyên và CommandParameter, nhưng sử dụng tài sản gắn liền thay vì:

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

giải pháp này hoạt động và bỏ qua tất cả các vấn đề với các lỗi trong việc xử lý CanExecute ContextMenu của.

Hy vọng một ngày nào đó Microsoft sẽ khắc phục sự cố với ContextMenu và cách giải quyết này sẽ không còn cần thiết nữa. Tôi có một trường hợp repro ngồi quanh đây ở đâu đó mà tôi định gửi đến Connect. Có lẽ tôi nên vào bóng và thực sự làm điều đó.

RequerySuggested là gì và tại sao lại sử dụng nó?

Cơ chế RequerySuggested là cách của RoutedCommand xử lý hiệu quả ICommand.CanExecuteChanged.Trong thế giới phi RoutedCommand, mỗi ICommand có danh sách các thuê bao riêng của mình để CanExecuteChanged, nhưng đối với RoutedCommand, bất kỳ khách hàng nào đăng ký vào ICommand.CanExecuteChanged sẽ thực sự đăng ký CommandManager.RequerySuggested. Mô hình đơn giản này có nghĩa là bất kỳ thời gian nào mà CanExecute của RoutedCommand có thể thay đổi, tất cả những gì cần thiết là gọi CommandManager.InvalidateRequerySuggested(), sẽ thực hiện những việc tương tự như bắn ICommand.CanExecuteChanged nhưng thực hiện nó cho tất cả RoutedCommands cùng một lúc và trên một luồng nền. Ngoài ra, các lời gọi RequerySuggested được kết hợp với nhau để nếu có nhiều thay đổi xảy ra thì CanExecute chỉ cần được gọi một lần.

Lý do tôi đề nghị bạn đăng ký CommandManager.RequerySuggested thay vì ICommand.CanExecuteChanged là: 1. Bạn không cần mã để xóa đăng ký cũ và thêm mã mới mỗi khi giá trị của thuộc tính Command của bạn thay đổi thay đổi , và 2. CommandManager.RequerySuggested có một tính năng tham chiếu yếu được xây dựng trong đó cho phép bạn thiết lập xử lý sự kiện của bạn và vẫn được thu gom rác thải. Làm tương tự với ICommand yêu cầu bạn phải thực hiện cơ chế tham chiếu yếu của riêng bạn.

Mặt trái của điều này là nếu bạn đăng ký CommandManager.RequerySuggested thay vì ICommand.CanExecuteChanged là bạn sẽ chỉ nhận được bản cập nhật cho RoutedCommands. Tôi sử dụng RoutedCommands độc quyền vì vậy đây không phải là một vấn đề đối với tôi, nhưng tôi nên đã đề cập rằng nếu bạn sử dụng ICommands thường xuyên đôi khi bạn nên xem xét làm công việc phụ của yếu đăng ký vào ICommand.CanExecutedChanged. Lưu ý rằng nếu bạn làm điều này, bạn không cần phải đăng ký vào RequerySuggested là tốt, kể từ RoutedCommand.add_CanExecutedChanged đã làm điều này cho bạn.

+0

Wow đây là một giải pháp phức tạp cho một điều khá đơn giản muốn làm. Vài câu hỏi: làm thế nào để sử dụng CommandManager.RequerySuggested (đó là một sự kiện tĩnh, chính xác những gì tôi kiểm tra trong nó?), Và tài sản đính kèm thứ ba bạn đề cập đến, bên cạnh Command và CommandParameter là gì? – devios1

+1

Ồ, tôi thấy, điều này "nội bộ chỉ sử dụng tài sản đính kèm" ... tôi không thể chỉ cần đăng ký DataContextChanged? – devios1

+0

Đã làm việc !! w00t! Cảm ơn :) Vẫn tò mò về RequerySuggested tho ... chính xác là gì? – devios1

8

Tôi tin rằng đây là liên quan đến vấn đề kết nối đăng nhập ở đây:

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

workaround của tôi là như sau:

  1. Tạo một lớp tĩnh với một tài sản phụ thuộc kèm theo một tham số lệnh ràng buộc
  2. Tạo giao diện tùy chỉnh để tăng CanExecuteThay đổi theo cách thủ công trên lệnh tùy chỉnh
  3. Thực hiện giao diện trong mỗi lệnh cần biết về các thay đổi tham số.

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

lệnh thực hiện:

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 Cách sử dụng:

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

Đây là việc sửa chữa đơn giản nhất tôi có thể đưa ra và nó hoạt động một điều trị cho việc triển khai phong cách MVVM. Bạn cũng có thể gọi CommandManager.InvalidateRequerySuggested() trong tham số BoundCommand thay đổi để rằng nó cũng làm việc với RoutedCommands.

+0

Làm việc tốt cho tôi – Artiom

1

Tôi gặp phải tình huống này trên DataGrid nơi tôi cần trình đơn ngữ cảnh để nhận biết có bật hay tắt các lệnh cụ thể tùy thuộc vào hàng đã chọn. Những gì tôi thấy là có đối tượng được truyền cho lệnh là null và nó chỉ được thực hiện một lần cho tất cả các hàng bất kể có thay đổi hay không.

Điều tôi đã làm là gọi RaiseCanExecuteChanged trên các lệnh cụ thể sẽ kích hoạt bật hoặc tắt trong sự kiện đã chọn thay đổi của lưới.


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

Lệnh ràng buộc phân

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

quả

Trường hợp InProgresstrue xóa lệnh không được kích hoạt

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> 
Các vấn đề liên quan