CanExecuteが最初に呼び出されたときにWPFCommandParameterがNULLになる


86

ItemsControlのDataTemplate内のボタンにバインドされているWPFとコマンドで問題が発生しました。シナリオは非常に単純です。ItemsControlはオブジェクトのリストにバインドされており、ボタンをクリックしてリスト内の各オブジェクトを削除できるようにしたいと思います。ボタンはコマンドを実行し、コマンドが削除を処理します。CommandParameterは、削除するオブジェクトにバインドされています。そうすれば、ユーザーが何をクリックしたかがわかります。ユーザーは自分の「自分の」オブジェクトのみを削除できるはずです。そのため、コマンドの「CanExecute」呼び出しでいくつかのチェックを行って、ユーザーが適切な権限を持っていることを確認する必要があります。

問題は、CanExecuteに渡されるパラメーターが最初に呼び出されたときにNULLであるため、コマンドを有効/無効にするロジックを実行できないことです。ただし、常に有効にしてからボタンをクリックしてコマンドを実行すると、CommandParameterが正しく渡されます。つまり、CommandParameterに対するバインディングが機能しているということです。

ItemsControlとDataTemplateのXAMLは次のようになります。

<ItemsControl 
    x:Name="commentsList"
    ItemsSource="{Binding Path=SharedDataItemPM.Comments}"
    Width="Auto" Height="Auto">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <Button                             
                    Content="Delete"
                    FontSize="10"
                    Command="{Binding Path=DataContext.DeleteCommentCommand, ElementName=commentsList}" 
                    CommandParameter="{Binding}" />
            </StackPanel>                       
         </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

ご覧のとおり、Commentsオブジェクトのリストがあります。DeleteCommentCommandのCommandParameterをCommandオブジェクトにバインドしたいと思います。

だから私の質問は、誰かが以前にこの問題を経験したことがあるかということだと思います。CanExecuteがコマンドで呼び出されますが、パラメーターは最初は常にNULLです-それはなぜですか?

更新:問題を少し絞り込むことができました。CommandParameterがデータバインドされているときにメッセージを出力できるように、空のDebugValueConverterを追加しました。問題は、CommandParameterがボタンにバインドされる前にCanExecuteメソッドが実行されることです。コマンドの前にCommandParameterを設定しようとしましたが(提案されているように)、それでも機能しません。それを制御する方法に関するヒント。

Update2:バインディングが「完了」したことを検出して、コマンドの再評価を強制できるようにする方法はありますか?また、コマンドオブジェクトの同じインスタンスにバインドする複数のボタン(ItemsControlの各アイテムに1つ)があるのは問題ですか?

Update3と:私は私のSkyDriveにバグの再現をアップロードしていますhttp://cid-1a08c11c407c0d8e.skydrive.live.com/self.aspx/Code%20samples/CommandParameterBinding.zip


私はリストボックスでまったく同じ問題を抱えています。
Hadi Eskandari 2015年

この問題については、現在WPFに対して開かれているバグレポートがあります:github.com/dotnet/wpf/issues/316
UuDdLrLrSs

回答:


14

私は同様の問題に遭遇し、信頼できるTriggerConverterを使用してそれを解決しました。

public class TriggerConverter : IMultiValueConverter
{
    #region IMultiValueConverter Members

    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        // First value is target value.
        // All others are update triggers only.
        if (values.Length < 1) return Binding.DoNothing;
        return values[0];
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    #endregion
}

この値コンバーターは、任意の数のパラメーターを受け取り、それらの最初のパラメーターを変換された値として返します。あなたのケースでマルチバインディングで使用すると、次のようになります。

<ItemsControl 
    x:Name="commentsList"
    ItemsSource="{Binding Path=SharedDataItemPM.Comments}"
    Width="Auto" Height="Auto">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <Button                             
                    Content="Delete"
                    FontSize="10"
                    CommandParameter="{Binding}">
                    <Button.Command>
                        <MultiBinding Converter="{StaticResource TriggerConverter}">
                            <Binding Path="DataContext.DeleteCommentCommand"
                                     ElementName="commentsList" />
                            <Binding />
                        </MultiBinding> 
                    </Button.Command>
                </Button>
            </StackPanel>                                       
         </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

これを機能させるには、どこかにリソースとしてTriggerConverterを追加する必要があります。これで、CommandParameterの値が使用可能になる前ではなく、Commandプロパティが設定されます。の代わりにRelativeSource.SelfとCommandParameterにバインドすることもできます。同じ効果を達成するために。


2
これは私のために働いた。私はなぜなのか理解していない。誰か説明できますか?
TJKjaer 2013年

CommandParameterがコマンドの前にバインドされるため、機能しませんか?私は...あなたはコンバータを必要とすることはないだろう
MBoros

2
これは解決策ではありません。これはハックですか?一体何が起こっているのですか?これは以前は機能していましたか?
ヨルダン

完璧です、私のために働きます!魔法は<Binding />行にあり、データテンプレートが変更されたときにコマンドバインディングが更新されます(コマンドパラメーターにバインドされています)
Andreas Kahler 2017年

56

ビューモデルのコマンドにバインドしようとしたときに、これと同じ問題が発生していました。

要素を名前で参照するのではなく、相対的なソースバインディングを使用するように変更しましたが、これでうまくいきました。パラメータバインディングは変更されませんでした。

古いコード:

Command="{Binding DataContext.MyCommand, ElementName=myWindow}"

新しいコード:

Command="{Binding DataContext.MyCommand, RelativeSource={RelativeSource AncestorType=Views:MyView}}"

更新:ElementNameを使用せずにこの問題に遭遇しました。ビューモデルのコマンドにバインドしており、ボタンのデータコンテキストはビューモデルです。この場合、(XAMLで)Button宣言のCommand属性の前にCommandParameter属性を移動するだけで済みました。

CommandParameter="{Binding Groups}"
Command="{Binding StartCommand}"

42
コマンドの前にCommandParameterを移動することが、このスレッドの最良の答えです。
BSick7 2011

6
属性の順序を移動しても役に立ちませんでした。それが実行の順序に影響を与えたとしたら、私は驚きます。
Jack Ukleja 2011年

3
なぜこれが機能するのかわかりません。すべきではないように感じますが、完全にそうです。
RMK 2012年

1
私は同じ問題を抱えていました-RelativeSourceは役に立ちませんでした、属性の順序を変更することは役に立ちました。更新していただきありがとうございます!
グラントクロフトン2012

14
拡張機能を宗教的に使用してXAMLを自動的に美化する(行間で属性を分割する、インデントを修正する、属性を並べ替える)人として、順序を変更するという提案は私CommandParameterCommand怖がらせます。
Guttsy 2014

29

CommandとCommandParameterを設定する順序が異なることがわかりました。Commandプロパティを設定すると、CanExecuteがすぐに呼び出されるため、その時点でCommandParameterがすでに設定されている必要があります。

XAMLでプロパティの順序を切り替えると、実際に効果があることがわかりましたが、問題が解決するかどうかはわかりません。ただし、試してみる価値はあります。

例のCommandプロパティの直後にCommandParameterが設定されると予想されるため、ボタンが有効にならないことを示唆しているようです。これは驚くべきことです。CommandManager.InvalidateRequerySuggested()を呼び出すと、ボタンが有効になりますか?


3
コマンドの前にCommandParameterを設定しようとしました-それでもCanExecuteを実行しますが、それでもNULLを渡します...残念-しかし、ヒントに感謝します。また、CommandManager.InvalidateRequerySuggested();を呼び出します。違いはありません。
ジョナスFollesø

CommandManager.InvalidateRequerySuggested()は、私にとって同様の問題を解決しました。ありがとう!
MJS

13

共有したいこの問題を回避するための別のオプションを考え出しました。CommandParameterプロパティが設定される前にコマンドのCanExecuteメソッドが実行されるため、バインディングが変更されたときにCanExecuteメソッドが再度呼び出されるように強制するプロパティが添付されたヘルパークラスを作成しました。

public static class ButtonHelper
{
    public static DependencyProperty CommandParameterProperty = DependencyProperty.RegisterAttached(
        "CommandParameter",
        typeof(object),
        typeof(ButtonHelper),
        new PropertyMetadata(CommandParameter_Changed));

    private static void CommandParameter_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var target = d as ButtonBase;
        if (target == null)
            return;

        target.CommandParameter = e.NewValue;
        var temp = target.Command;
        // Have to set it to null first or CanExecute won't be called.
        target.Command = null;
        target.Command = temp;
    }

    public static object GetCommandParameter(ButtonBase target)
    {
        return target.GetValue(CommandParameterProperty);
    }

    public static void SetCommandParameter(ButtonBase target, object value)
    {
        target.SetValue(CommandParameterProperty, value);
    }
}

次に、コマンドパラメータをバインドするボタンで...

<Button 
    Content="Press Me"
    Command="{Binding}" 
    helpers:ButtonHelper.CommandParameter="{Binding MyParameter}" />

これがおそらく他の誰かの問題に役立つことを願っています。


よくできました、ありがとう。M $が8年経ってもこれを修正していないなんて信じられません。ひどい!
McGarnagle 2016

8

これは古いスレッドですが、この問題が発生したときにGoogleが私をここに連れてきたので、ボタン付きのDataGridTemplateColumnで機能したものを追加します。

バインディングを次の場所から変更します。

CommandParameter="{Binding .}"

CommandParameter="{Binding DataContext, RelativeSource={RelativeSource Self}}"

なぜそれが機能するのかわかりませんが、私にとってはうまくいきました。


私は上記の両方のハイスコアの答えを試しましたが、これは私のためだけに機能しました。拘束力ではなく、コントロール自体の内部的な問題のようですが、それでも多くの人が上記の答えでそれを機能させました。ありがとう!
Javidan 2017

6

私は最近同じ問題に遭遇しました(私にとってはコンテキストメニューのメニュー項目でした)、それはすべての状況に適した解決策ではないかもしれませんが、これを解決する別の(そしてはるかに短い!)方法を見つけました問題:

<MenuItem Header="Open file" Command="{Binding Tag.CommandOpenFile, IsAsync=True, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}" CommandParameter="{Binding Name}" />

Tagコンテキストメニューの特殊なケースのベースの回避策を無視すると、ここで重要なのはCommandParameter定期的にバインドすることですCommandが、を追加のでバインドしIsAsync=Trueます。これにより、実際のコマンド(したがってそのCanExecute呼び出し)のバインドが少し遅れるため、パラメーターは既に使用可能になります。これは、少しの間、有効状態が間違っている可能性があることを意味しますが、私の場合、それは完全に受け入れられました。


5

昨日PrismフォーラムCommandParameterBehaviorに投稿した私のものを利用できるかもしれません。それは、原因への変更が原因である欠落した動作を追加しますCommandParameterCommandが再クエリされる。

PropertyDescriptor.AddValueChanged後で呼び出さずに呼び出した場合に発生するメモリリークを回避しようとしたため、ここにはいくつかの複雑さがありますPropertyDescriptor.RemoveValueChanged。ekementがアンロードされたときにハンドラーの登録を解除することで、これを修正しようとしています。

IDelegateCommandPrismを使用していない限り(そしてPrismライブラリに私と同じ変更を加えたい場合を除いて)、おそらくそれらを削除する必要があります。また、RoutedCommandここでは通常sを使用しないことに注意してください(DelegateCommand<T>ほとんどすべてにPrismを使用します)。したがってCommandManager.InvalidateRequerySuggested、既知の宇宙などを破壊するある種の量子波動関数崩壊カスケードを開始するという私の呼び出しが発生した場合、私に責任を負わせないでください。

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;

namespace Microsoft.Practices.Composite.Wpf.Commands
{
    /// <summary>
    /// This class provides an attached property that, when set to true, will cause changes to the element's CommandParameter to 
    /// trigger the CanExecute handler to be called on the Command.
    /// </summary>
    public static class CommandParameterBehavior
    {
        /// <summary>
        /// Identifies the IsCommandRequeriedOnChange attached property
        /// </summary>
        /// <remarks>
        /// When a control has the <see cref="IsCommandRequeriedOnChangeProperty" />
        /// attached property set to true, then any change to it's 
        /// <see cref="System.Windows.Controls.Primitives.ButtonBase.CommandParameter" /> property will cause the state of
        /// the command attached to it's <see cref="System.Windows.Controls.Primitives.ButtonBase.Command" /> property to 
        /// be reevaluated.
        /// </remarks>
        public static readonly DependencyProperty IsCommandRequeriedOnChangeProperty =
            DependencyProperty.RegisterAttached("IsCommandRequeriedOnChange",
                                                typeof(bool),
                                                typeof(CommandParameterBehavior),
                                                new UIPropertyMetadata(false, new PropertyChangedCallback(OnIsCommandRequeriedOnChangeChanged)));

        /// <summary>
        /// Gets the value for the <see cref="IsCommandRequeriedOnChangeProperty"/> attached property.
        /// </summary>
        /// <param name="target">The object to adapt.</param>
        /// <returns>Whether the update on change behavior is enabled.</returns>
        public static bool GetIsCommandRequeriedOnChange(DependencyObject target)
        {
            return (bool)target.GetValue(IsCommandRequeriedOnChangeProperty);
        }

        /// <summary>
        /// Sets the <see cref="IsCommandRequeriedOnChangeProperty"/> attached property.
        /// </summary>
        /// <param name="target">The object to adapt. This is typically a <see cref="System.Windows.Controls.Primitives.ButtonBase" />, 
        /// <see cref="System.Windows.Controls.MenuItem" /> or <see cref="System.Windows.Documents.Hyperlink" /></param>
        /// <param name="value">Whether the update behaviour should be enabled.</param>
        public static void SetIsCommandRequeriedOnChange(DependencyObject target, bool value)
        {
            target.SetValue(IsCommandRequeriedOnChangeProperty, value);
        }

        private static void OnIsCommandRequeriedOnChangeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (!(d is ICommandSource))
                return;

            if (!(d is FrameworkElement || d is FrameworkContentElement))
                return;

            if ((bool)e.NewValue)
            {
                HookCommandParameterChanged(d);
            }
            else
            {
                UnhookCommandParameterChanged(d);
            }

            UpdateCommandState(d);
        }

        private static PropertyDescriptor GetCommandParameterPropertyDescriptor(object source)
        {
            return TypeDescriptor.GetProperties(source.GetType())["CommandParameter"];
        }

        private static void HookCommandParameterChanged(object source)
        {
            var propertyDescriptor = GetCommandParameterPropertyDescriptor(source);
            propertyDescriptor.AddValueChanged(source, OnCommandParameterChanged);

            // N.B. Using PropertyDescriptor.AddValueChanged will cause "source" to never be garbage collected,
            // so we need to hook the Unloaded event and call RemoveValueChanged there.
            HookUnloaded(source);
        }

        private static void UnhookCommandParameterChanged(object source)
        {
            var propertyDescriptor = GetCommandParameterPropertyDescriptor(source);
            propertyDescriptor.RemoveValueChanged(source, OnCommandParameterChanged);

            UnhookUnloaded(source);
        }

        private static void HookUnloaded(object source)
        {
            var fe = source as FrameworkElement;
            if (fe != null)
            {
                fe.Unloaded += OnUnloaded;
            }

            var fce = source as FrameworkContentElement;
            if (fce != null)
            {
                fce.Unloaded += OnUnloaded;
            }
        }

        private static void UnhookUnloaded(object source)
        {
            var fe = source as FrameworkElement;
            if (fe != null)
            {
                fe.Unloaded -= OnUnloaded;
            }

            var fce = source as FrameworkContentElement;
            if (fce != null)
            {
                fce.Unloaded -= OnUnloaded;
            }
        }

        static void OnUnloaded(object sender, RoutedEventArgs e)
        {
            UnhookCommandParameterChanged(sender);
        }

        static void OnCommandParameterChanged(object sender, EventArgs ea)
        {
            UpdateCommandState(sender);
        }

        private static void UpdateCommandState(object target)
        {
            var commandSource = target as ICommandSource;

            if (commandSource == null)
                return;

            var rc = commandSource.Command as RoutedCommand;
            if (rc != null)
            {
                CommandManager.InvalidateRequerySuggested();
            }

            var dc = commandSource.Command as IDelegateCommand;
            if (dc != null)
            {
                dc.RaiseCanExecuteChanged();
            }

        }
    }
}

connectでバグレポートに出くわしました。この最後のコードでここに投稿を更新できる可能性はありますか?それとも、より良い回避策を見つけましたか?
マルクスヒュッター

より簡単な解決策は、プロパティ記述子の代わりにバインディングを使用してCommandParameterプロパティを監視することです。そうでなければ素晴らしい解決策です!これは、厄介なハッキングや回避策を導入するだけでなく、根本的な問題を実際に修正します。
SebastianNegraszus19年

1

DelegateCommandでこの問題を「修正」する比較的簡単な方法がありますが、DelegateCommandソースを更新し、Microsoft.Practices.Composite.Presentation.dllを再コンパイルする必要があります。

1)Prism 1.2ソースコードをダウンロードし、CompositeApplicationLibrary_Desktop.slnを開きます。ここに、DelegateCommandソースを含むComposite.Presentation.Desktopプロジェクトがあります。

2)パブリックイベントEventHandler CanExecuteChangedの下で、次のように変更して読み取ります。

public event EventHandler CanExecuteChanged
{
     add
     {
          WeakEventHandlerManager.AddWeakReferenceHandler( ref _canExecuteChangedHandlers, value, 2 );
          // add this line
          CommandManager.RequerySuggested += value;
     }
     remove
     {
          WeakEventHandlerManager.RemoveWeakReferenceHandler( _canExecuteChangedHandlers, value );
          // add this line
          CommandManager.RequerySuggested -= value;
     }
}

3)保護された仮想void OnCanExecuteChanged()の下で、次のように変更します。

protected virtual void OnCanExecuteChanged()
{
     // add this line
     CommandManager.InvalidateRequerySuggested();
     WeakEventHandlerManager.CallWeakReferenceHandlers( this, _canExecuteChangedHandlers );
}

4)ソリューションを再コンパイルしてから、コンパイルされたDLLが存在するDebugフォルダーまたはReleaseフォルダーに移動します。Microsoft.Practices.Composite.Presentation.dllと.pdb(必要な場合)を外部アセンブリを参照する場所にコピーしてから、アプリケーションを再コンパイルして新しいバージョンをプルします。

この後、UIが問題のDelegateCommandにバインドされた要素をレンダリングするたびにCanExecuteを起動する必要があります。

気をつけて、ジョー

Gmailでrefereejoe


1

同様の質問に対するいくつかの良い答えを読んだ後、私はあなたの例でDelegateCommandを少し変更して機能させました。使用する代わりに:

public event EventHandler CanExecuteChanged;

私はそれを次のように変更しました:

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

怠惰すぎて修正できなかったため、次の2つの方法を削除しました

public void RaiseCanExecuteChanged()

そして

protected virtual void OnCanExecuteChanged()

これで、Bindingが変更されたときとExecuteメソッドの後にCanExecuteが呼び出されるようになります。

ViewModelが変更された場合は自動的にトリガーされませんが、このスレッドで説明されているように、GUIスレッドでCommandManager.InvalidateRequerySuggestedを呼び出すことで可能になります

Application.Current?.Dispatcher.Invoke(DispatcherPriority.Normal, (Action)CommandManager.InvalidateRequerySuggested);

私はそれDispatcherPriority.Normalが高すぎて確実に機能しないことを発見しました(または私の場合はまったく)。使用DispatcherPriority.Loadedは適切に機能し、より適切と思われます(つまり、ビューモデルに関連付けられたUI要素が実際に読み込まれるまでデリゲートが呼び出されないことを明示的に示しています)。
Peter Duniho 2016年

0

Jonasさん、これがデータテンプレートで機能するかどうかはわかりませんが、ListViewコンテキストメニューで現在のアイテムをコマンドパラメーターとして取得するために使用するバインディング構文は次のとおりです。

CommandParameter = "{Binding RelativeSource = {RelativeSource AncestorType = ContextMenu}、Path = PlacementTarget.SelectedItem、Mode = TwoWay}"


リストビューでもまったく同じことをします。この場合、それはItemsControlであるため、(ビジュアルツリー内で)「バインド」する明確なプロパティはありません。私は私が行っているバインドするときに検出する方法を見つけなければならないと思い、そして(CommandParameterがバインドされますので、ちょうど後半に)CanExecuteを再評価
ジョナスFollesø


0

これらの回答のいくつかは、コマンド自体を取得するためにDataContextにバインドすることに関するものですが、問題は、CommandParameterがnullであるべきではないのにnullであるということでした。私たちもこれを経験しました。すぐに、ViewModelでこれを機能させる非常に簡単な方法を見つけました。これは特に、1行のコードでお客様から報告されたCommandParameternullの問題に対するものです。Dispatcher.BeginInvoke()に注意してください。

public DelegateCommand<objectToBePassed> CommandShowReport
    {
        get
        {
            // create the command, or pass what is already created.
            var command = _commandShowReport ?? (_commandShowReport = new DelegateCommand<object>(OnCommandShowReport, OnCanCommandShowReport));

            // For the item template, the OnCanCommand will first pass in null. This will tell the command to re-pass the command param to validate if it can execute.
            Dispatcher.BeginInvoke((Action) delegate { command.RaiseCanExecuteChanged(); }, DispatcherPriority.DataBind);

            return command;
        }
    }

-1

そのロングショット。これをデバッグするには、次のことを試してください。
-PreviewCanExecuteイベントを確認します。
-snoop / wpf molを使用して内部を確認し、コマンドパラメーターが何であるかを確認します。

HTH、


Snoopを使用してみましたが、最初にロードされたときはNULLしかないため、デバッグは非常に困難です。その上でSnoopを実行すると、CommandとCommandParameterの両方が設定されます... DataTemplateでコマンドを使用する必要があります。
ジョナスFollesø

-1

commandManager.InvalidateRequerySuggestedは私にも機能します。次のリンクで同様の問題について説明していると思います。M$ devは現在のバージョンでの制限を確認しており、commandManager.InvalidateRequerySuggestedが回避策です。http://social.expression.microsoft.com/Forums/en-US/wpf/thread/c45d2272-e8ba-4219-bb41-1e5eaed08a1f/

重要なのは、commandManager.InvalidateRequerySuggestedを呼び出すタイミングです。これは、関連する値の変更が通知された後に呼び出す必要があります。


そのリンクは無効になりました
Peter Duniho 2016年

-2

Commandの前にCommandParameterを設定するというEdBallの提案に加えて、CanExecuteメソッドにオブジェクトタイプのパラメーターがあることを確認してください。

private bool OnDeleteSelectedItemsCanExecute(object SelectedItems)  
{
    // Your goes heres
}

誰かがSelectedItemsをCanExecuteパラメーターとして受け取る方法を理解するために私が行った膨大な時間を費やすことを防ぐことを願っています

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.