13

ビュー モデルのコンテキスト メニュー コマンドに問題があります。

ビュー モデル内の各コマンドに ICommand インターフェイスを実装し、ビュー (MainWindow) のリソース内に ContextMenu を作成し、MVVMToolkit の CommandReference を使用して現在の DataContext (ViewModel) コマンドにアクセスします。

アプリケーションをデバッグすると、ウィンドウの作成時を除いてコマンドの CanExecute メソッドが呼び出されていないように見えるため、Context MenuItems が期待どおりに有効化または無効化されていません。

実際のアプリケーションを示す簡単なサンプル (ここに添付) を作成し、以下に要約します。どんな助けでも大歓迎です!

これはビューモデルです

namespace WpfCommandTest
{
    public class MainWindowViewModel
    {
        private List<string> data = new List<string>{ "One", "Two", "Three" };

        // This is to simplify this example - normally we would link to
        // Domain Model properties
        public List<string> TestData
        {
            get { return data; }
            set { data = value; }
        }

        // Bound Property for listview
        public string SelectedItem { get; set; }

        // Command to execute
        public ICommand DisplayValue { get; private set; }

        public MainWindowViewModel()
        {
            DisplayValue = new DisplayValueCommand(this);
        }

    }
}

DisplayValueCommand は次のとおりです。

public class DisplayValueCommand : ICommand
{
    private MainWindowViewModel viewModel;

    public DisplayValueCommand(MainWindowViewModel viewModel)
    {
        this.viewModel = viewModel;
    }

    #region ICommand Members

    public bool CanExecute(object parameter)
    {
        if (viewModel.SelectedItem != null)
        {
            return viewModel.SelectedItem.Length == 3;
        }
        else return false;
    }

    public event EventHandler CanExecuteChanged;

    public void Execute(object parameter)
    {
        MessageBox.Show(viewModel.SelectedItem);
    }

    #endregion
}

最後に、ビューは Xaml で定義されます。

<Window x:Class="WpfCommandTest.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfCommandTest"
    xmlns:mvvmtk="clr-namespace:MVVMToolkit"
    Title="Window1" Height="300" Width="300">

    <Window.Resources>

        <mvvmtk:CommandReference x:Key="showMessageCommandReference" Command="{Binding DisplayValue}" />

        <ContextMenu x:Key="listContextMenu">
            <MenuItem Header="Show MessageBox" Command="{StaticResource showMessageCommandReference}"/>
        </ContextMenu>

    </Window.Resources>

    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>

    <Grid>
        <ListBox ItemsSource="{Binding TestData}" ContextMenu="{StaticResource listContextMenu}" 
                 SelectedItem="{Binding SelectedItem}" />
    </Grid>
</Window>
4

5 に答える 5

21

CanExecuteChangedウィルの答えを完成させるために、イベントの「標準」実装を次に示します。

public event EventHandler CanExecuteChanged
{
    add { CommandManager.RequerySuggested += value; }
    remove { CommandManager.RequerySuggested -= value; }
}

(ジョシュ・スミスのRelayCommandクラスより)

ところで、おそらくRelayCommandorの使用を検討する必要DelegateCommandがあります。ViewModel のすべてのコマンドに対して新しいコマンド クラスを作成するのはすぐに飽きてしまうでしょう...

于 2010-04-06T20:41:25.730 に答える
4

CanExecute のステータスがいつ変更されたかを追跡し、ICommand.CanExecuteChanged イベントを発生させる必要があります。

また、それが常に機能するとは限らないことに気付くかもしれません。このような場合CommandManager.InvalidateRequerySuggested()、コマンド マネージャーをお尻に蹴り飛ばすために を呼び出す必要があります。

これに時間がかかりすぎる場合は、この質問への回答を確認してください。

于 2010-04-06T20:32:15.740 に答える
2

迅速な返信ありがとうございます。このアプローチは、ウィンドウ内の標準のボタン (DataContext を介してビュー モデルにアクセスできる) にコマンドをバインドしている場合に機能します。ICommand 実装クラスで提案したように、または RelayCommand と DelegateCommand を使用して CommandManager を使用すると、CanExecute が非常に頻繁に呼び出されることが示されています。

ただし、ContextMenu の CommandReference を介して同じコマンドをバインドしても、同じようには動作しません。

同じ動作を行うには、CommandReference 内に Josh Smith の RelayCommand の EventHandler も含める必要がありますが、その際、OnCommandChanged メソッド内のコードをコメント アウトする必要があります。なぜそこにあるのか完全にはわかりませんが、おそらくイベントメモリリークを防いでいるのでしょう(推測で!)?

  public class CommandReference : Freezable, ICommand
    {
        public CommandReference()
        {
            // Blank
        }

        public static readonly DependencyProperty CommandProperty = DependencyProperty.Register("Command", typeof(ICommand), typeof(CommandReference), new PropertyMetadata(new PropertyChangedCallback(OnCommandChanged)));

        public ICommand Command
        {
            get { return (ICommand)GetValue(CommandProperty); }
            set { SetValue(CommandProperty, value); }
        }

        #region ICommand Members

        public bool CanExecute(object parameter)
        {
            if (Command != null)
                return Command.CanExecute(parameter);
            return false;
        }

        public void Execute(object parameter)
        {
            Command.Execute(parameter);
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        private static void OnCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            CommandReference commandReference = d as CommandReference;
            ICommand oldCommand = e.OldValue as ICommand;
            ICommand newCommand = e.NewValue as ICommand;

            //if (oldCommand != null)
            //{
            //    oldCommand.CanExecuteChanged -= commandReference.CanExecuteChanged;
            //}
            //if (newCommand != null)
            //{
            //    newCommand.CanExecuteChanged += commandReference.CanExecuteChanged;
            //}
        }

        #endregion

        #region Freezable

        protected override Freezable CreateInstanceCore()
        {
            throw new NotImplementedException();
        }

        #endregion
    }
于 2010-04-06T21:38:51.847 に答える
1

ただし、ContextMenu の CommandReference を介して同じコマンドをバインドしても、同じようには機能しません。

これは CommandReference 実装のバグです。この2点から次のようになります。

  1. ICommand.CanExecuteChanged の実装者は、ハンドラーへの弱い参照のみを保持することをお勧めします (この回答を参照してください)。
  2. ICommand.CanExecuteChanged のコンシューマーは (1) を予期する必要があるため、ICommand.CanExecuteChanged に登録するハンドラーへの強い参照を保持する必要があります。

RelayCommand と DelegateCommand の一般的な実装は (1) に従います。CommandReference 実装は、newCommand.CanExecuteChanged をサブスクライブする場合、(2) に従いません。そのため、ハンドラー オブジェクトが収集され、その後 CommandReference は、それが期待していた通知を受け取りません。

修正は、CommandReference でハンドラーへの強い参照を保持することです。

    private EventHandler _commandCanExecuteChangedHandler;
    public event EventHandler CanExecuteChanged;

    ...
    if (oldCommand != null)
    {
        oldCommand.CanExecuteChanged -= commandReference._commandCanExecuteChangedHandler;
    }
    if (newCommand != null)
    {
        commandReference._commandCanExecuteChangedHandler = commandReference.Command_CanExecuteChanged;
        newCommand.CanExecuteChanged += commandReference._commandCanExecuteChangedHandler;
    }
    ...

    private void Command_CanExecuteChanged(object sender, EventArgs e)
    {
        if (CanExecuteChanged != null)
            CanExecuteChanged(this, e);
    }

同じ動作を行うには、CommandReference 内に Josh Smith の RelayCommand の EventHandler も含める必要がありますが、その際、OnCommandChanged メソッド内のコードをコメント アウトする必要があります。なぜそこにあるのか完全にはわかりませんが、おそらくイベントメモリリークを防いでいるのでしょう(推測で!)?

サブスクリプションを CommandManager.RequerySuggested に転送するというアプローチもバグを解消することに注意してください (最初から参照されていないハンドラーはもうありません) が、CommandReference 機能が不利になります。CommandReference が関連付けられているコマンドは、(CommandManager に依存して再クエリ要求を発行する代わりに) CanExecuteChanged を直接発生させることができますが、このイベントは飲み込まれ、CommandReference にバインドされたコマンド ソースに到達することはありません。これは、newCommand.CanExecuteChanged をサブスクライブすることによって CommandReference が実装される理由についての質問にも答える必要があります。

更新: CodePlex で問題を提出しました

于 2012-06-07T00:30:45.730 に答える