94

ItemsControl の DataTemplate 内のボタンにバインドされている WPF とコマンドで問題が発生しました。シナリオは非常に簡単です。ItemsControl はオブジェクトのリストにバインドされており、ボタンをクリックしてリスト内の各オブジェクトを削除できるようにしたいと考えています。Button は Command を実行し、Command は削除を処理します。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>

ご覧のとおり、コメント オブジェクトのリストがあります。DeleteCommentCommand の CommandParameter を Command オブジェクトにバインドする必要があります。

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

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

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

Update3:バグの複製を SkyDrive にアップロードしました: http://cid-1a08c11c407c0d8e.skydrive.live.com/self.aspx/Code%20samples/CommandParameterBinding.zip

4

15 に答える 15

58

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

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

古いコード:

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}"
于 2010-10-28T21:39:55.323 に答える
32

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

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

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

于 2008-12-03T04:59:55.017 に答える
18

同様の問題に出くわし、信頼できる 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
}

この値コンバーターは、任意の数のパラメーターを受け取り、最初のパラメーターを変換後の値として返します。あなたのケースで MultiBinding で使用すると、次のようになります。

<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 にバインドすることもできます。同じ効果を達成するために。

于 2009-02-05T16:22:50.030 に答える
15

この問題を回避する別の方法を思いついたので、共有したいと思います。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}" />

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

于 2014-07-10T06:16:59.670 に答える
10

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

バインディングを次のように変更します。

CommandParameter="{Binding .}"

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

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

于 2016-08-15T18:46:40.347 に答える
5

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

PropertyDescriptor.AddValueChanged後でPropertyDescriptor.RemoveValueChanged. _ ekement がアンロードされたときにハンドラーを登録解除することで、これを修正しようとしています。

IDelegateCommandPrism を使用していない限り (そして、Prism ライブラリに私と同じ変更を加えたい場合は) 、おそらくそれらを削除する必要があります。また、ここでは一般的に s を使用しないことに注意してくださいRoutedCommand(ほとんどすべての場合に Prism を使用します) ため、既知の宇宙などを破壊するある種の量子波動関数の崩壊カスケードがDelegateCommand<T>への呼び出しによって引き起こされたとしても、私に責任を負わせないでください。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();
            }

        }
    }
}
于 2009-02-18T15:34:52.913 に答える
1

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

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

2) public イベント 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 (必要な場合) をコピーし、アプリケーションを再コンパイルして新しいバージョンをプルします。

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

気をつけて、ジョー

gmailの審判員

于 2010-04-08T18:01:27.613 に答える
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);
于 2015-11-29T10:41:34.337 に答える
0

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

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

于 2008-12-04T14:26:06.403 に答える
0

この問題は Beta 2 にも存在するため、.Net 4.0 の WPF に対するバグとしてこれを記録しました。

https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=504976

于 2009-11-26T14:18:40.777 に答える
-1

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

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

于 2008-12-22T05:57:30.130 に答える
-1

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

HTH、

于 2008-12-04T04:55:53.200 に答える