5

MVVM 設計パターンを使用してアプリケーションを構築しており、ApplicationCommands クラスで定義された RoutedUICommands を利用したいと考えています。View の CommandBindings プロパティ (UserControl を参照) は DependencyProperty ではないため、ViewModel で定義された CommandBindings を View に直接バインドすることはできません。すべての ViewModel が CommandBindings の ObservableCollection を持つことを保証する ViewModel インターフェイスに基づいて、これをプログラムでバインドする抽象 View クラスを定義することで、これを解決しました。これはすべて正常に機能しますが、一部のシナリオでは、異なるクラス (View と ViewModel) で定義されているロジックを同じコマンドで実行したいと考えています。たとえば、ドキュメントを保存するとき。

ViewModel では、コードはドキュメントをディスクに保存します。

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

一見したところ、前のコードだけで十分ですが、ドキュメントがバインドされている View の TextBox は、フォーカスを失ったときにのみ Source を更新します。ただし、Ctrl+S を押すと、フォーカスを失うことなくドキュメントを保存できます。これは、ソースで更新された変更の前にドキュメントが保存され、事実上変更が無視されることを意味します。ただし、UpdateSourceTrigger を PropertyChanged に変更することは、パフォーマンス上の理由から実行可能なオプションではないため、保存する前に別の方法で強制的に更新する必要があります。そこで、PreviewExecuted イベントを使用して、次のように PreviewExecuted イベントで強制的に更新できるようにしようと考えました。

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

ただし、ハンドラーを PreviewExecuted イベントに割り当てると、Handled プロパティを明示的に false に設定した場合でも、イベントが完全にキャンセルされるようです。したがって、前のコード サンプルで定義した executeSave イベント ハンドラは実行されなくなります。cb.PreviewExecuted を cb.Executed に変更すると、両方のコード実行されますが、正しい順序ではないことに注意してください。

イベントを処理済みとしてマークしない限り、PreviewExecuted と Executed にハンドラーを追加して順番に実行できるはずなので、これは .Net のバグだと思います。

誰でもこの動作を確認できますか? それとも私が間違っていますか?このバグの回避策はありますか?

4

2 に答える 2

3

EDIT 2:ソースコードを見ると、内部的には次のように機能しているようです:

  1. ユーザー入力 (マウスまたはキーボード) に反応する呼び出しUIElementCommandManager.TranslateInput()
  2. 次に、入力に関連付けられたコマンドを探して、さまざまなレベルをCommandManager通過します。CommandBindings
  3. コマンドが見つかると、そのCanExecute()メソッドが呼び出され、返された場合trueExecuted()が呼び出されます。
  4. 各メソッドが本質的に同じことを行う場合、プロセスを開始した上で、RoutedCommand添付されたイベントCommandManager.PreviewCanExecuteEventand CommandManager.CanExecuteEvent(またはPreviewExecutedEventand ExecutedEvent)のペアが発生します。UIElementこれで第 1 フェーズは終了です。
  5. UIElementには、これら 4 つのイベント用に登録されたクラス ハンドラがあり、これらのハンドラは単純にCommandManager.OnCanExecute()andを呼び出しますCommandManager.CanExecute()(プレビュー イベントと実際のイベントの両方に対して)。
  6. 登録されたハンドラーが呼び出されるのは、ここCommandManager.OnCanExecute()とメソッドだけです。何も見つからない場合、イベントはの親に転送され、コマンドが処理されるか、ビジュアル ツリーのルートに到達するまで、新しいサイクルが開始されます。CommandManager.OnExecute()CommandBindingCommandManagerUIElement

CommandBinding クラスのソース コードを見ると、CommandBinding を介して PreviewExecuted および Executed イベントに登録したハンドラーを呼び出す OnExecuted() メソッドがあります。そこにそのビットがあります:

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

これにより、PreviewExecuted ハンドラーが返された直後にイベントが処理されるように設定されるため、Executed は呼び出されません。

EDIT 1: CanExecute & PreviewCanExecute イベントを見ると、重要な違いがあります:

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

ここで Handled を true に設定することは条件付きであるため、CanExecute を続行するかどうかを決定するのはプログラマーです。PreviewCanExecute ハンドラーで CanExecuteRoutedEventArgs の CanExecute を true に設定しないと、CanExecute ハンドラーが呼び出されます。

Preview イベントのContinueRoutingプロパティについて - false に設定すると、Preview イベントがそれ以上ルーティングされなくなりますが、次のメイン イベントにはまったく影響しません。

ハンドラーが CommandBinding を通じて登録されている場合にのみ、この方法で機能することに注意してください。

それでも PreviewExecuted と Executed の両方を実行したい場合は、次の 2 つのオプションがあります。

  1. Execute()PreviewExecuted ハンドラー内からルーティングされたコマンドのメソッドを呼び出すことができます。考えてみると、PreviewExecuted が終了する前に Executed ハンドラーを呼び出すと、同期の問題が発生する可能性があります。私には、これは良い方法とは思えません。
  2. CommandManager.AddPreviewExecutedHandler()静的メソッドを使用して、PreviewExecuted ハンドラーを個別に登録できます。これは UIElement クラスから直接呼び出され、CommandBinding は関与しません。EDIT 2: Look at the point 4 at the beginning of the post - these are the events we're adding the handlers for.

見た目からして、わざとこうしている。なんで?推測することしかできません...

于 2010-02-22T16:22:03.610 に答える
1

欠落しているContinueRoutingの動作を取得するために、次の回避策を作成します。

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;
    }
}
于 2010-02-23T10:12:04.947 に答える