web-dev-qa-db-ja.com

CanExecuteが最初に呼び出されたとき、WPF CommandParameterはNULLです

ItemsControlのDataTemplate内のButtonにバインドされている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がデータバインドされているときにメッセージを出力できるように、空のDebug ValueConverterを追加しました。問題は、CommandParameterがボタンにバインドされる前にCanExecuteメソッドが実行されることです。コマンドの前にCommandParameterを設定しようとしました(推奨)-しかし、まだ機能しません。それを制御する方法に関するヒント。

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

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

80
Jonas Follesø

同様の問題に出くわし、信頼できる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にバインドすることもできます。同じ効果を達成します。

14
David Liersch

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

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

古いコード:

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

新しいコード:

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

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

CommandParameter="{Binding Groups}"
Command="{Binding StartCommand}"
51
Travis Weber

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

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

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

28
Ed Ball

私はこのスレッドが古いことを知っていますが、共有したいこの問題を回避する別のオプションを考え出しました。 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}" />

これがおそらく他の誰かがこの問題を解決することを願っています。

13
Ed Downs

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

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

CommandParameter="{Binding .}"

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

なぜそれが機能するのかはわかりませんが、私には役立ちました。

6
Simon Smith

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

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

おそらく、Prismを使用していない限り、IDelegateCommandを削除する必要があります(そして、Prismライブラリに私と同じ変更を加えたい場合)。また、ここでは通常RoutedCommandsを使用しないことに注意してください(ほとんどすべてに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();
            }

        }
    }
}
5
Swythan

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

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

気をつけて、ジョー

gmailの審判員

1
Joe Bako

同様の質問に対するいくつかの良い答えを読んだ後、私はあなたの例で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);
1
kkCosmo

これらの回答のいくつかは、DataContextにバインドしてCommand自体を取得することに関するものですが、疑問はCommandParameterが本来あるべきではないときにnullであるということでした。これも経験しました。ちょっと考えてみると、これをViewModelで機能させる非常に簡単な方法を見つけました。これは、顧客から報告されたCommandParameter null問題に特化しており、1行のコードです。 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;
        }
    }
0
TravisWhidden

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

<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呼び出し)のバインディングが少し遅延するため、パラメーターは既に使用可能になっています。ただし、これは、しばらくの間、使用可能状態が間違っている可能性があることを意味しますが、私の場合、これは完全に受け入れられました。

0
Ralf Stauder

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

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

0
Swythan