7

UI から削除されたコマンド ソースで CanExecute が呼び出される理由を理解しようとしています。以下は、デモンストレーション用の単純化されたプログラムです。

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Height="350" Width="525">
    <StackPanel>
        <ListBox ItemsSource="{Binding Items}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel>
                        <Button Content="{Binding Txt}" 
                                Command="{Binding Act}" />
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <Button Content="Remove first item" Click="Button_Click"  />
    </StackPanel>
</Window>

分離コード:

public partial class MainWindow : Window
{
    public class Foo
    {
        static int _seq = 0;
        int _txt = _seq++;
        RelayCommand _act;
        public bool Removed = false;

        public string Txt { get { return _txt.ToString(); } }

        public ICommand Act
        {
            get
            {
                if (_act == null) {
                    _act = new RelayCommand(
                        param => { },
                        param => {
                            if (Removed)
                                Console.WriteLine("Why is this happening?");
                            return true;
                        });
                }
                return _act;
            }
        }
    }

    public ObservableCollection<Foo> Items { get; set; }

    public MainWindow()
    {
        Items = new ObservableCollection<Foo>();
        Items.Add(new Foo());
        Items.Add(new Foo());
        Items.CollectionChanged += 
            new NotifyCollectionChangedEventHandler(Items_CollectionChanged);
        DataContext = this;
        InitializeComponent();
    }

    void Items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.Action == NotifyCollectionChangedAction.Remove)
            foreach (Foo foo in e.OldItems) {
                foo.Removed = true;
                Console.WriteLine("Removed item marked 'Removed'");
            }
    }

    void Button_Click(object sender, RoutedEventArgs e)
    {
        Items.RemoveAt(0);
        Console.WriteLine("Item removed");
    }
}

「最初の項目を削除」ボタンを 1 回クリックすると、次の出力が得られます。

Removed item marked 'Removed'
Item removed
Why is this happening?
Why is this happening?

"なぜこうなった?" ウィンドウの空の部分をクリックするたびに印刷され続けます。

なぜこうなった?また、削除されたコマンド ソースで CanExecute が呼び出されないようにするには、どうすればよいですか?

注: RelayCommand はここにあります。

マイケル・エデンフィールドの質問への回答:

Q1:削除されたボタンで CanExecute が呼び出されたときのコールスタック:

WpfApplication1.exe!WpfApplication1.MainWindow.Foo.get_Act.AnonymousMethod__1(オブジェクト パラメータ) 30 行目 WpfApplication1.exe!WpfApplication1.RelayCommand.CanExecute(オブジェクト パラメータ) 41 行目 + 0x1a バイト PresentationFramework.dll!MS.Internal.Commands.CommandHelpers.CanExecuteCommandSource (System.Windows.Input.ICommandSource commandSource) + 0x8a バイト PresentationFramework.dll!System.Windows.Controls.Primitives.ButtonBase.UpdateCanExecute() + 0x18 バイト PresentationFramework.dll!System.Windows.Controls.Primitives.ButtonBase.OnCanExecuteChanged(object sender, System.EventArgs e) + 0x5 バイト PresentationCore.dll!System.Windows.Input.CommandManager.CallWeakReferenceHandlers(System.Collections.Generic.List handlers) + 0xac バイト PresentationCore.dll!System.Windows.Input.CommandManager.RaiseRequerySuggested(オブジェクト obj) + 0xf バイト

Q2:また、(最初のボタンだけでなく) リストからすべてのボタンを削除すると、この問題は引き続き発生しますか?

はい。

4

2 に答える 2

3

問題は、コマンド ソース (つまり、ボタン) がバインドされているコマンドのサブスクライブを解除しないため、コマンド ソースがなくなった後も、起動CanExecuteChangedするたびCommandManager.RequerySuggestedに起動することです。CanExecute

これを解決するために に実装IDisposableRelayCommand、必要なコードを追加して、モデル オブジェクトが削除され、UI から削除されるたびに、そのすべての で Dispose() が呼び出されるようにしましたRelayCommand

これは変更されたものRelayCommandです (オリジナルはここにあります):

public class RelayCommand : ICommand, IDisposable
{
    #region Fields

    List<EventHandler> _canExecuteSubscribers = new List<EventHandler>();
    readonly Action<object> _execute;
    readonly Predicate<object> _canExecute;

    #endregion // Fields

    #region Constructors

    public RelayCommand(Action<object> execute)
        : this(execute, null)
    {
    }

    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");

        _execute = execute;
        _canExecute = canExecute;
    }

    #endregion // Constructors

    #region ICommand

    [DebuggerStepThrough]
    public bool CanExecute(object parameter)
    {
        return _canExecute == null ? true : _canExecute(parameter);
    }

    public event EventHandler CanExecuteChanged
    {
        add
        {
            CommandManager.RequerySuggested += value;
            _canExecuteSubscribers.Add(value);
        }
        remove
        {
            CommandManager.RequerySuggested -= value;
            _canExecuteSubscribers.Remove(value);
        }
    }

    public void Execute(object parameter)
    {
        _execute(parameter);
    }

    #endregion // ICommand

    #region IDisposable

    public void Dispose()
    {
        _canExecuteSubscribers.ForEach(h => CanExecuteChanged -= h);
        _canExecuteSubscribers.Clear();
    }

    #endregion // IDisposable
}

上記を使用する場合は常に、インスタンス化されたすべての RelayCommands を追跡して、必要なDispose()ときに呼び出すことができるようにします。

Dictionary<string, RelayCommand> _relayCommands 
    = new Dictionary<string, RelayCommand>();

public ICommand SomeCmd
{
    get
    {
        RelayCommand command;
        string commandName = "SomeCmd";
        if (_relayCommands.TryGetValue(commandName, out command))
            return command;
        command = new RelayCommand(
            param => {},
            param => true);
        return _relayCommands[commandName] = command;
    }
}

void Dispose()
{
    foreach (string commandName in _relayCommands.Keys)
        _relayCommands[commandName].Dispose();
    _relayCommands.Clear();
}
于 2012-04-24T07:56:45.800 に答える
0

トリガーしているように見えるラムダ式とイベントの使用には、既知の問題があります。これを「バグ」と呼ぶのはためらっていますが、これが意図した動作であるかどうかを知るのに十分なほど内部の詳細を理解していないためですが、直感に反しているように思えます。

ここでの重要な指標は、コール スタックのこの部分です。

PresentationCore.dll!System.Windows.Input.CommandManager.CallWeakReferenceHandlers(
   System.Collections.Generic.List handlers) + 0xac bytes 

「弱い」イベントは、ターゲット オブジェクトを存続させないイベントをフックする方法です。イベント ハンドラーとしてラムバ式を渡しているため、ここで使用されているため、メソッドを含む「オブジェクト」は内部的に生成された匿名オブジェクトです。add問題は、イベントのハンドラーに渡されるオブジェクトが、イベントに渡されるものと同じ式のインスタンスではなくremove、機能的に同一のオブジェクトであるため、イベントからサブスクライブされていないことです。

次の質問で説明されているように、いくつかの回避策があります。

ラムダで使用する弱いイベント ハンドラー モデル

C# でのラムダによるイベントのフック解除

イベント ハンドラーとしてラムダを使用すると、メモリ リークが発生する可能性がありますか?

あなたの場合、最も簡単な方法は、CanExecute および Execute コードを実際のメソッドに移動することです。

if (_act == null) {
  _act = new RelayCommand(this.DoCommand, this.CanDoCommand);
}

private void DoCommand(object parameter)
{
}

private bool CanDoCommand(object parameter)
{
    if (Removed)
      Console.WriteLine("Why is this happening?");
    return true;
}

または、オブジェクトをラムダから一度構築Action<>して委任し、それらを変数に格納し、それらを作成時に使用するように手配できる場合、同じインスタンスが使用されるように強制されます。IMO、あなたのケースでは、おそらく必要以上に複雑です。Func<>RelayCommand

于 2012-04-23T16:23:14.750 に答える