Подтвердить что ты не робот

WPF CommandParameter NULL первый раз, когда вызывается CanExecute

У меня возникла проблема с WPF и командами, привязанными к Button внутри DataTemplate элемента ItemsControl. Сценарий довольно прямолинейный. Элемент ItemsControl привязан к списку объектов, и я хочу, чтобы удалить каждый объект в списке, нажав кнопку. Кнопка выполняет команду, и команда позаботится об удалении. CommandParameter привязан к объекту, который я хочу удалить. Таким образом, я знаю, что пользователь нажал. Пользователь должен только удалять свои "собственные" объекты, поэтому мне нужно выполнить некоторые проверки в вызове "CanExecute" команды, чтобы убедиться, что пользователь имеет права доступа.

Проблема в том, что параметр, переданный в CanExecute, является NULL при первом вызове, поэтому я не могу запустить логику, чтобы включить/отключить эту команду. Однако, если я сделаю это всегда включенным, а затем нажмите кнопку, чтобы выполнить команду, CommandParameter передается правильно. Таким образом, это означает, что привязка к CommandParameter работает.

XAML для ItemsControl и DataTemplate выглядит следующим образом:

<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. Я хочу, чтобы CommandParameter команды DeleteCommentCommand был связан с объектом Command.

Итак, я думаю, мой вопрос: кто-нибудь испытал эту проблему раньше? CanExecute вызывается в моей команде, но параметр всегда имеет значение NULL в первый раз - почему?

Обновление: Я смог немного уменьшить проблему. Я добавил пустой Debug ValueConverter, чтобы я мог выводить сообщение, когда CommandParameter привязан к данным. Оказывается, проблема заключается в том, что метод CanExecute выполняется до того, как CommandParameter привязан к кнопке. Я попытался установить CommandParameter перед Command (как предлагается), но он все равно не работает. Любые советы о том, как управлять им.

Update2: Есть ли способ обнаружить, когда привязка "выполнена", чтобы я мог заставить переоценку команды? Кроме того, проблема в том, что у меня есть несколько кнопок (по одному для каждого элемента в ItemsControl), которые привязаны к одному экземпляру командного объекта?

Update3: Я загрузил репликацию ошибки на свой SkyDrive: http://cid-1a08c11c407c0d8e.skydrive.live.com/self.aspx/Code%20samples/CommandParameterBinding.zip

4b9b3361

Ответ 1

Я наткнулся на подобную проблему и решил ее использовать мой надежный 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 в качестве ресурса где-нибудь, чтобы это работало. Теперь свойство Command установлено не ранее, чем значение для CommandParameter стало доступным. Вы можете даже привязываться к RelativeSource.Self и CommandParameter, а не к. для достижения такого же эффекта.

Ответ 2

У меня была эта же проблема при попытке привязки к команде в моей модели представления.

Я изменил его, чтобы использовать относительную привязку источника, а не ссылаться на элемент по имени, и это сделало трюк. Связывание параметров не изменилось.

Старый код:

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

Новый код:

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

Обновление: я просто столкнулся с этой проблемой, не используя ElementName, я привязываюсь к команде в моей модели представления, а мой контекст данных кнопки - моя модель представления. В этом случае мне пришлось просто переместить атрибут CommandParameter перед атрибутом Command в объявлении Button (в XAML).

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

Ответ 3

Я обнаружил, что порядок, в котором я устанавливаю Command и CommandParameter, имеет значение. Установка свойства Command вызывает немедленное вызов CanExecute, поэтому вы хотите, чтобы CommandParameter уже был установлен в этой точке.

Я обнаружил, что изменение порядка свойств в XAML может действительно иметь эффект, хотя я не уверен, что он решит вашу проблему. Однако стоит попробовать.

Кажется, вы предлагаете, чтобы кнопка никогда не включалась, что удивительно, поскольку я ожидал, что CommandParameter будет установлен сразу после свойства Command в вашем примере. Вызывает ли вызов CommandManager.InvalidateRequerySposed(), чтобы кнопка включалась?

Ответ 4

Я знаю, что эта ветка вроде старой, но я придумал другой вариант, чтобы обойти эту проблему, которую я хотел разделить. Поскольку метод CanExecute команды запускается до того, как установлен свойство CommandParameter, я создал вспомогательный класс со связанным свойством, который заставляет метод 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}" />

Я надеюсь, что это, возможно, поможет кому-то другому в этом вопросе.

Ответ 5

Это старый поток, но поскольку Google привел меня сюда, когда у меня возникла эта проблема, я добавлю, что сработало для меня для DataGridTemplateColumn с помощью кнопки.

Измените привязку:

CommandParameter="{Binding .}"

to

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

Не знаю, почему это работает, но это было для меня.

Ответ 6

Возможно, вы сможете использовать мой CommandParameterBehavior который я опубликовал вчера на форумах Prism. Он добавляет недостающее поведение, когда изменение CommandParameter приводит к повторному CommandParameter Command.

Здесь есть некоторая сложность, вызванная моими попытками избежать утечки памяти, вызванной, если вы вызываете PropertyDescriptor.AddValueChanged без последующего вызова PropertyDescriptor.RemoveValueChanged. Я пытаюсь исправить это, отменив регистрацию обработчика при выгрузке ekement.

Вам, вероятно, потребуется удалить материал IDelegateCommand если вы не используете Prism (и не хотите вносить те же изменения, что и я, в библиотеку Prism). Также обратите внимание, что мы обычно не используем 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 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 
        /// <see cref="System.Windows.Controls.Primitives.ButtonBase.CommandParameter" /> property will cause the state of
        /// the command attached to it <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();
            }

        }
    }
}

Ответ 7

Там относительно простой способ "исправить" эту проблему с DelegateCommand, хотя он требует обновления источника DelegateCommand и повторной компиляции Microsoft.Practices.Composite.Presentation.dll.

1) Загрузите исходный код Prism 1.2 и откройте CompositeApplicationLibrary_Desktop.sln. Здесь представлен проект Composite.Presentation.Desktop, содержащий источник DelegateCommand.

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) Перекомпилируйте решение, затем перейдите в папку Debug или Release, в которой собраны скомпилированные библиотеки DLL. Скопируйте файлы Microsoft.Practices.Composite.Presentation.dll и .pdb(если хотите) туда, где вы ссылаетесь на свои внешние сборки, а затем перекомпилируйте приложение, чтобы вытащить новые версии.

После этого CanExecute следует запускать каждый раз, когда пользовательский интерфейс отображает элементы, связанные с рассматриваемой делегацией.

Позаботьтесь, Джо

refereejoe в gmail

Ответ 8

После прочтения некоторых хороших ответов на подобные вопросы, я немного изменил в вашем примере команду DelegateCommand, чтобы заставить ее работать. Вместо использования:

public event EventHandler CanExecuteChanged;

Я изменил его на:

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

Я удалил следующие два метода, потому что я был слишком ленив, чтобы исправить их

public void RaiseCanExecuteChanged()

и

protected virtual void OnCanExecuteChanged()

И что все... это, кажется, гарантирует, что CanExecute будет вызываться, когда Binding изменится и после метода Execute

Он не будет автоматически запускаться, если ViewModel изменен, но, как упоминалось в этом потоке, можно вызвать, вызвав CommandManager.InvalidateRequerySposed по потоку GUI

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

Ответ 9

Привет, Джонас, не уверен, что это будет работать в шаблоне данных, но вот синтаксис привязки, который я использую в контекстном меню ListView, чтобы захватить текущий элемент в качестве параметра команды:

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

Ответ 11

Некоторые из этих ответов связаны с привязкой к DataContext для получения самой команды, но вопрос был в том, что CommandParameter имеет значение null, когда этого не должно быть. Мы также испытали это. На догадках мы нашли очень простой способ заставить это работать в нашей ViewModel. Это специально для нулевой проблемы CommandParameter, указанной клиентом, с одной строкой кода. Обратите внимание на 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;
        }
    }

Ответ 12

Недавно я столкнулся с той же проблемой (для меня это было для пунктов меню в контекстном меню), хотя это не может быть подходящим решением для каждой ситуации, я нашел другой (и намного короче!) Способ решения этой проблемы. проблема:

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

Игнорирование обходного пути для Tag -based для особого случая контекстного меню, ключом здесь является регулярное связывание CommandParameter, но связывание Command с дополнительным IsAsync=True. Это немного задержит привязку фактической команды (и, следовательно, ее вызова CanExecute), поэтому параметр уже будет доступен. Это означает, однако, что на короткое время включенное состояние может быть неправильным, но для моего случая это было совершенно приемлемо.

Ответ 13

Его длинный выстрел. Чтобы отладить это, вы можете попробовать:
- проверка события PreviewCanExecute.
- используйте snoop/wpf mole, чтобы заглянуть внутрь и посмотреть, что такое командный параметр.

НТН,

Ответ 14

Для меня также работает commandManager.InvalidateRequeryS Suggest. Я полагаю, что следующая ссылка говорит о подобной проблеме, а M $dev подтвердила ограничение в текущей версии, а commandManager.InvalidateRequerySposed - обходной путь. http://social.expression.microsoft.com/Forums/en-US/wpf/thread/c45d2272-e8ba-4219-bb41-1e5eaed08a1f/

Какое значение имеет время вызова команды commandManager.InvalidateRequerySposed. Это следует вызывать после уведомления соответствующего изменения значения.

Ответ 15

Помимо предложения Эд Болла о настройке CommandParameter до Command, убедитесь, что у вашего метода CanExecute есть параметр типа объекта.

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

Надеюсь, это предотвратит трату огромного количества времени, которое я потратил, чтобы выяснить, как получить SelectedItems в качестве параметра CanExecute.