web-dev-qa-db-ja.com

Josh SmithによるRelayCommandの実装に欠陥がありますか?

参照 JoshSmithの記事WPFApps With the Model-View-ViewModel Design Pattern 、具体的にはRelayCommandの実装例(図3)を検討してください。 (この質問については、記事全体を読む必要はありません。)

一般的に、実装は優れていると思いますが、CanExecuteChangedCommandManagerイベントへのRequerySuggestedサブスクリプションの委任について質問があります。 RequerySuggestedのドキュメント は次のように述べています。

このイベントは静的であるため、弱参照としてのみハンドラーを保持します。このイベントをリッスンするオブジェクトは、ガベージコレクションを回避するために、イベントハンドラーへの強力な参照を保持する必要があります。これは、プライベートフィールドを用意し、このイベントにアタッチする前または後にハンドラーを値として割り当てることで実現できます。

しかし、RelayCommandのサンプル実装は、サブスクライブされたハンドラーに対してそのようなものを維持していません。

public event EventHandler CanExecuteChanged
{
    add { CommandManager.RequerySuggested += value; }
    remove { CommandManager.RequerySuggested -= value; }
}
  1. これにより、弱参照がRelayCommandのクライアントにリークし、RelayCommandのユーザーがCanExecuteChangedの実装を理解し、ライブ参照を維持する必要がありますか?
  2. もしそうなら、例えば、RelayCommandサブスクライバーの潜在的な時期尚早なGCを軽減するために、CanExecuteChangedの実装を次のように変更することは理にかなっていますか。

    // This event never actually fires.  It's purely lifetime mgm't.
    private event EventHandler canExecChangedRef;
    public event EventHandler CanExecuteChanged
    {
        add 
        { 
            CommandManager.RequerySuggested += value;
            this.canExecChangedRef += value;
        }
        remove 
        {
            this.canExecChangedRef -= value;
            CommandManager.RequerySuggested -= value; 
        }
    }
    
41
Greg D

私もこの実装に欠陥があると思いますイベントハンドラへの弱参照を確実にリークするためです。これは実際には非常に悪いことです。
MVVMLightツールキットとそこに実装されているRelayCommandを使用しており、記事と同じように実装されています。
次のコードはOnCanExecuteEditChangedを呼び出すことはありません。

private static void OnCommandEditChanged(DependencyObject d, 
                                         DependencyPropertyChangedEventArgs e)
{
    var @this = d as MyViewBase;
    if (@this == null)
    {
        return;
    }

    var oldCommand = e.OldValue as ICommand;
    if (oldCommand != null)
    {
        oldCommand.CanExecuteChanged -= @this.OnCanExecuteEditChanged;
    }
    var newCommand = e.NewValue as ICommand;
    if (newCommand != null)
    {
        newCommand.CanExecuteChanged += @this.OnCanExecuteEditChanged;
    }
}

ただし、このように変更すると、機能します。

private static EventHandler _eventHandler;

private static void OnCommandEditChanged(DependencyObject d,
                                         DependencyPropertyChangedEventArgs e)
{
    var @this = d as MyViewBase;
    if (@this == null)
    {
        return;
    }
    if (_eventHandler == null)
        _eventHandler = new EventHandler(@this.OnCanExecuteEditChanged);

    var oldCommand = e.OldValue as ICommand;
    if (oldCommand != null)
    {
        oldCommand.CanExecuteChanged -= _eventHandler;
    }
    var newCommand = e.NewValue as ICommand;
    if (newCommand != null)
    {
        newCommand.CanExecuteChanged += _eventHandler;
    }
}

唯一の違いは? CommandManager.RequerySuggestedのドキュメントに示されているように、イベントハンドラーをフィールドに保存しています。

9
Daniel Hilgarth

Joshの コメント 彼の " ルーティングされたコマンドの理解 "の記事で答えを見つけました:

[...] CanExecuteChangedイベントでWeakEventパターンを使用する必要があります。これは、視覚要素がそのイベントをフックし、アプリがシャットダウンするまでコマンドオブジェクトがガベージコレクションされない可能性があるため、メモリリークの可能性が非常に高いためです。 [...]

WPF CanExecuteChangedは愚かで自分自身をフック解除するため、Visuals実装者は登録されたハンドラーを弱く保持する必要があるという議論のようです。これは、すでにこれを行っているCommandManagerに委任することで最も簡単に実装できます。おそらく同じ理由で。

44
David Schmitt

リフレクターによると、それはRoutedCommandクラスでも同じように実装されているので、大丈夫だと思います... WPFチームの誰かがミスをしない限り;)

7
Thomas Levesque

欠陥があると思います。

イベントをCommandManagerに再ルーティングすることにより、次の動作が得られます。

これにより、WPFコマンドインフラストラクチャは、組み込みコマンドを要求するたびに実行できるかどうかをすべてのRelayCommandオブジェクトに要求します。

ただし、CanExecuteステータスを再評価するために、単一のコマンドにバインドされているすべてのコントロールに通知する場合はどうなりますか?彼の実装では、CommandManagerに移動する必要があります。つまり、

アプリケーション内のすべてのコマンドバインディングが再評価されます

これには、豆の山に関係のないもの、CanExecuteの評価に副作用(データベースアクセスや長時間実行タスクなど)があるもの、収集されるのを待っているものなどが含まれます...それはスレッジハンマーを使用するようなものですフリッゲンネイルを打ち込む。

これを行うことの影響を真剣に検討する必要があります。

6
user1228

私はここで要点を見逃しているかもしれませんが、以下はコンストラクターのイベントハンドラーへの強い参照を構成していませんか?

    _canExecute = canExecute;           
0
Lazarus