2012-04-23 4 views
6

UIから削除されたコマンドソースでCanExecuteが呼び出される理由を理解しようとしています。ここで実証するための単純化されたプログラムです:コマンドソースが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"); 
    } 
} 

私は「最初の項目を削除」ボタンを一度クリックすると、私はこの出力を得る:

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

「なぜこれが起こっているのですか?」ウィンドウの空の部分をクリックするたびに印刷され続けます。

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

注: RelayCommandはhereです。マイケルEdenfieldの質問に

回答:

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(Syste + 0x8 bytes PresentationFramework.dll!System.Windows.Controls.Primitives.ButtonBase.OnCanExecuteChanged(Object() System.Windows.Input.CommandManager.CallWeakReferenceHandlers(System.Collections.Generic.Listハンドラー)+ 0xac bytes PresentationCore.dll!System.Windows.Input.CommandManager。 RaiseRequerySuggested(オブジェクトobj)+ 0xFのバイト

Q2:リストからすべてのボタンを削除する場合も、これは(?ちょうど最初ではない)が起こっておくん

はい。

+0

私はRelayCommandが恋しい。これは何ですか? – Gqqnbig

+0

RelayCommandの実装へのリンクを追加しました。 –

+0

あなたはイベント中にコールスタックをチェックし、それをトリガしたものを見てみましたか?また、リストから*すべての*ボタンを削除しても、これは起こり続けますか? –

答えて

2

問題は、コマンドソースがなくなっているずっと後たびCommandManager.RequerySuggested火災、CanExecute火災だけでなく、そのようコマンドソース(すなわち、ボタンが)、それがバインドされているコマンドのCanExecuteChangedから解除しないということです。 I)はRelayCommandIDisposableを実施し、モデルオブジェクトが削除されるたびように必要なコードを追加し、そうUI、廃棄(から除去され、これを解決するために

は、そのすべてのRelayCommandに 呼び出されます。

これは(オリジナルhereである)変性RelayCommandである:私は上記を使用する場合はいつでも時間が来るとき、私はDispose()を呼び出すことができるよう

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を追跡:

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

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

ここで重要な兆候は、あなたのコールスタックのこの部分です:

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

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

次の質問で説明したように、いくつかの回避策があります:あなたのケースのために

Weak event handler model for use with lambdas

UnHooking Events with Lambdas in C#

Can using lambdas as event handlers cause a memory leak?

最も簡単なあなたのCanExecuteを移動し、実際にコードを実行することです方法:

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<>Func<>の代理人をlambdaから一度構築し、それらを変数に格納し、RelayCommandを作成するときに使用すると、同じインスタンスが強制的に使用されます。あなたのケースでは、おそらくもっと複雑なIMOです。

+0

非匿名メソッドを作成し、それらをRelayCommandのコンストラクタのパラメータとして渡しても何も変更されません。私はいくつかの調査を行っており、コマンドソース(ボタン)がまだCanExecuteChangedに登録されているという問題があるようです(つまり、ボタンは自動的にイベントにフックしますが、登録解除はしません)。 –

関連する問題