2010-02-17 30 views
5

Tôi đang xây dựng một ứng dụng bằng cách sử dụng mẫu thiết kế MVVM và tôi muốn sử dụng các RoutedUICommands được định nghĩa trong lớp ApplicationCommands. Vì thuộc tính CommandBindings của View (đọc UserControl) không phải là DependencyProperty nên chúng ta không thể ràng buộc CommandBindings được định nghĩa trong ViewModel cho View trực tiếp. Tôi đã giải quyết điều này bằng cách định nghĩa một lớp View trừu tượng liên kết với chương trình này, dựa trên giao diện ViewModel, đảm bảo mọi ViewModel có một ObservableCollection của CommandBindings. Điều này tất cả các công trình tốt, tuy nhiên, trong một số kịch bản tôi muốn thực hiện logic được định nghĩa trong các lớp khác nhau (View và ViewModel) cùng một lệnh. Ví dụ, khi lưu một tài liệu.RoutedUICommand PreviewExecuted Bug?

Trong ViewModel mã tiết kiệm tài liệu vào đĩa:

private void InitializeCommands() 
{ 
    CommandBindings = new CommandBindingCollection(); 
    ExecutedRoutedEventHandler executeSave = (sender, e) => 
    { 
     document.Save(path); 
     IsModified = false; 
    }; 
    CanExecuteRoutedEventHandler canSave = (sender, e) => 
    { 
     e.CanExecute = IsModified; 
    }; 
    CommandBinding save = new CommandBinding(ApplicationCommands.Save, executeSave, canSave); 
    CommandBindings.Add(save); 
} 

Thoạt nhìn mã trước đó là tất cả tôi muốn làm, nhưng TextBox trong Xem mà tài liệu được ràng buộc, chỉ cập nhật Nguồn của nó khi nó mất tiêu điểm của nó. Tuy nhiên, tôi có thể lưu tài liệu mà không bị mất tiêu điểm bằng cách nhấn Ctrl + S. Điều này có nghĩa là tài liệu được lưu trước các thay đổi được cập nhật trong nguồn, bỏ qua hiệu quả các thay đổi. Nhưng kể từ khi thay đổi UpdateSourceTrigger thành PropertyChanged không phải là một lựa chọn khả thi vì lý do hiệu suất, cái gì khác phải buộc cập nhật trước khi lưu. Vì vậy, tôi nghĩ, cho phép sử dụng các sự kiện PreviewExecuted để buộc các cập nhật trong trường hợp PreviewExecuted, như vậy:

//Find the Save command and extend behavior if it is present 
foreach (CommandBinding cb in CommandBindings) 
{ 
    if (cb.Command.Equals(ApplicationCommands.Save)) 
    { 
     cb.PreviewExecuted += (sender, e) => 
     { 
      if (IsModified) 
      { 
       BindingExpression be = rtb.GetBindingExpression(TextBox.TextProperty); 
       be.UpdateSource(); 
      } 
      e.Handled = false; 
     }; 
    } 
} 

Tuy nhiên, gán một handler cho sự kiện PreviewExecuted dường như để hủy bỏ sự kiện này hoàn toàn, ngay cả khi tôi thiết lập một cách rõ ràng Xử lý bất động sản để sai. Vì vậy, eventhandler executeSave mà tôi đã định nghĩa trong mẫu mã trước đó không được thực hiện nữa. Lưu ý rằng khi tôi thay đổi cb.PreviewExecuted thành cb.Executed cả hai đoạn mã làm thực thi, nhưng không theo đúng thứ tự.

Tôi nghĩ đây là lỗi trong .Net, vì bạn có thể thêm trình xử lý vào PreviewExecuted và Executed và yêu cầu chúng được thực thi theo thứ tự, miễn là bạn không đánh dấu sự kiện là đã xử lý.

Có ai có thể xác nhận hành vi này không? Hoặc là tôi sai? Có cách giải quyết cho lỗi này không?

Trả lời

3

EDIT 2: Từ cách nhìn vào mã nguồn có vẻ như nội nó hoạt động như thế:

  1. Các UIElement cuộc gọi CommandManager.TranslateInput() trong phản ứng để người dùng nhập vào (chuột hoặc bàn phím).
  2. CommandManager sau đó đi qua CommandBindings trên các cấp khác nhau để tìm lệnh được liên kết với đầu vào.
  3. Khi lệnh được tìm thấy, phương thức CanExecute() được gọi và nếu nó trả về true thì gọi là Executed().
  4. Trong trường hợp RoutedCommand mỗi phương pháp làm essencially điều tương tự - nó làm nảy sinh một cặp của các sự kiện gắn liền CommandManager.PreviewCanExecuteEventCommandManager.CanExecuteEvent (hoặc PreviewExecutedEventExecutedEvent) trên UIElement mà bắt đầu quá trình. Điều đó kết thúc giai đoạn đầu tiên.
  5. Giờ đây, UIElement có các trình xử lý lớp đã đăng ký cho bốn sự kiện đó và các trình xử lý này chỉ cần gọi CommandManager.OnCanExecute()CommandManager.CanExecute() (cho cả sự kiện xem trước và thực tế).
  6. Chỉ có ở đây trong các phương thức CommandManager.OnCanExecute()CommandManager.OnExecute() nơi các trình xử lý được đăng ký với số CommandBinding được gọi. Nếu không tìm thấy sự kiện, hãy chuyển sự kiện CommandManager lên đến phụ huynh của UIElement và chu kỳ mới bắt đầu cho đến khi lệnh được xử lý hoặc gốc của cây trực quan được tiếp cận.

Nếu bạn nhìn vào mã nguồn lớp CommandBinding có được OnExecuted() phương pháp mà có trách nhiệm kêu gọi các bộ xử lý bạn đăng ký PreviewExecuted và thực hiện các sự kiện thông qua CommandBinding. Có một chút ở đó:

PreviewExecuted(sender, e); 
e.Handled = true; 

điều này đặt sự kiện như được xử lý ngay sau khi trình xử lý PreviewExecuted của bạn trở về và do đó thực thi không được gọi.

EDIT 1: Nhìn vào CanExecute & PreviewCanExecute sự kiện có sự khác biệt quan trọng:

PreviewCanExecute(sender, e); 
    if (e.CanExecute) 
    { 
    e.Handled = true; 
    } 

thiết lập Cầm true là có điều kiện ở đây và vì vậy nó là các lập trình viên người quyết định có hay không để tiến hành CanExecute . Đơn giản chỉ cần không đặt CanExecute của CanExecuteRoutedEventArgs thành true trong trình xử lý PreviewCanExecute của bạn và trình xử lý CanExecute sẽ được gọi.

Khi đến ContinueRouting thuộc tính của sự kiện Xem trước - khi được đặt thành false, nó sẽ ngăn sự kiện Xem trước định tuyến thêm nhưng không ảnh hưởng đến sự kiện chính sau đây theo bất kỳ cách nào.

Lưu ý rằng nó chỉ hoạt động theo cách này khi trình xử lý được đăng ký thông qua CommandBinding.

Nếu bạn vẫn muốn có cả hai PreviewExecuted và đã thi để chạy bạn có hai lựa chọn:

  1. Bạn có thể có thể gọi Execute() phương pháp của lệnh chuyển từ bên trong handler PreviewExecuted. Chỉ cần suy nghĩ về nó - bạn có thể chạy vào các vấn đề đồng bộ hóa như bạn đang gọi xử lý thực thi trước khi PreviewExecuted kết thúc. Với tôi điều này không giống như một cách tốt để đi.
  2. Bạn có thể đăng ký trình xử lý PreviewExecuted riêng thông qua phương thức tĩnh CommandManager.AddPreviewExecutedHandler(). Điều này sẽ được gọi trực tiếp từ lớp UIElement và sẽ không liên quan đến CommandBinding. EDIT 2: Look at the point 4 at the beginning of the post - these are the events we're adding the handlers for.

Từ ngoại hình - nó được thực hiện theo cách này theo mục đích. Tại sao? Người ta chỉ có thể đoán ...

+0

Cốt truyện dày ... Vì vậy, tôi nhìn vào mã nguồn mà bạn đề cập và họ làm như vậy điều trong OnCanExecute với PreviewCanExecute. Tuy nhiên, có một sự khác biệt quan trọng giữa các CanExecuteRoutedEventArgs từ OnCanExecute và ExecutedRoutedEventArgs từ OnExecuted. Như bạn mong đợi các CanExecuteRoutedEventArgs chứa một tài sản ContinueRouting mà không chính xác điều đó, nhưng đối với một số lý do ExecutedRoutedEventArgs phải làm mà không có. Tôi thực sự thực sự không thể có được đầu của tôi xung quanh sự lựa chọn này từ Microsoft. – elmar

+0

Tôi nghĩ rằng ContinueRouting không tham gia vào quá trình đó - hãy xem EDIT 2 của tôi trong bài đăng. Tại sao họ lại làm theo cách này ...Nhìn vào hai phần của phương thức CommandBinding.OnExecuted(), chúng hầu như giống hệt nhau - nó có thể là trường hợp điển hình của sao chép/dán :) và sau đó nó là một lỗi. Nghiêm túc mà nói, tôi không nghĩ đó là trường hợp. Tôi thực sự muốn biết lý do của họ đằng sau điều này là gì. –

1

tôi xây dựng các workaround sau đây, để có được những hành vi ContinueRouting mất tích:

foreach (CommandBinding cb in CommandBindings) 
{ 
    if (cb.Command.Equals(ApplicationCommands.Save)) 
    { 
     ExecutedRoutedEventHandler f = null; 
     f = (sender, e) => 
     { 
      if (IsModified) 
      { 
       BindingExpression be = rtb.GetBindingExpression(TextBox.TextProperty); 
       be.UpdateSource(); 

       // There is a "Feature/Bug" in .Net which cancels the route when adding PreviewExecuted 
       // So we remove the handler and call execute again 
       cb.PreviewExecuted -= f; 
       cb.Command.Execute(null); 
      } 
     }; 
     cb.PreviewExecuted += f; 
    } 
} 
Các vấn đề liên quan