Параметр WPF CommandParameter NULL, коли вперше викликається CanExecute


86

Я зіткнувся з проблемою з WPF та командами, які прив’язані до кнопки всередині DataTemplate елемента ControlControl. Сценарій цілком прямий. 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>

Отже, як ви бачите, у мене є список об’єктів Коментарі. Я хочу, щоб CommandParameter команди DeleteCommentCommand був прив'язаний до об'єкта Command.

Тож, думаю, моє запитання: чи хтось раніше стикався з цією проблемою? CanExecute викликається моєю командою, але параметр завжди NULL з першого разу - чому це?

Оновлення: Я зміг трохи звузити проблему. Я додав порожній Debug ValueConverter, щоб я міг вивести повідомлення, коли CommandParameter пов'язаний з даними. Виявляється, проблема полягає в тому, що метод CanExecute виконується до того, як CommandParameter прив'язаний до кнопки. Я намагався встановити CommandParameter перед Command (як запропоновано), але він все одно не працює. Будь-які поради щодо того, як ним керувати.

Оновлення2: Чи є спосіб виявити, коли прив’язка «зроблена», щоб я міг змусити переоцінити команду? Також - чи проблема в тому, що у мене є кілька кнопок (по одній для кожного елемента в ItemsControl), які прив'язуються до одного і того ж екземпляра командного об'єкта?

Оновлення3: Я завантажив репродукцію помилки на свій SkyDrive: http://cid-1a08c11c407c0d8e.skydrive.live.com/self.aspx/Code%20samples/CommandParameterBinding.zip


У мене точно така ж проблема з ListBox.
Хаді Ескандарі

На даний момент існує відкритий звіт про помилку щодо 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
}

Цей перетворювач значень приймає будь-яку кількість параметрів і передає перший із них назад як перетворене значення. При використанні у 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
Це спрацювало для мене. Я не розумію чому. Хтось може пояснити?
TJKjaer

Чи не працює це через те, що CommandParameter зв’язаний перед Command? Сумніваюся, вам знадобиться конвертер ...
MBoros

2
Це не рішення. Це хак? Що, чорт візьми, відбувається? Це раніше працювало?
Йорданія

Ідеально, працює для мене! Магія знаходиться в рядку <Binding />, що призводить до оновлення прив'язки команди при зміні шаблону даних (який прив'язаний до параметру команди)
Андреас Калер,

56

У мене виникла така сама проблема під час спроби прив’язки до команди на моїй моделі перегляду.

Я змінив його для використання відносного прив'язки джерела, а не посилання на елемент за назвою, і це зробило трюк. Прив'язка параметрів не змінилася.

Старий код:

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}"

42
Переміщення CommandParameter перед Command - найкраща відповідь у цій темі.
BSick7

6
Переміщення порядку атрибутів нам не допомогло. Я був би здивований, якби це вплинуло на порядок виконання.
Jack Ukleja

3
Не знаю, чому це працює. Відчувається, що не слід, але цілком так.
RMK

1
У мене була та сама проблема - RelativeSource не допомогло, змінивши порядок атрибутів. Дякуємо за оновлення!
Грант Крофтон,

14
Як людина, яка релігійно використовує розширення для автоматичного прикрашення XAML (розділити атрибути по рядках, виправити відступи, змінити порядок атрибутів), пропозиція змінити порядок CommandParameterта Commandлякає мене.
Guttsy

29

Я виявив, що порядок, у якому я встановлюю Command і CommandParameter, має різницю. Встановлення властивості Command призводить до негайного виклику CanExecute, тому ви хочете, щоб CommandParameter вже був встановлений на той момент.

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

Ви, схоже, припускаєте, що кнопка ніколи не вмикається, що дивно, оскільки я очікував би, що CommandParameter буде встановлено незабаром після властивості Command у вашому прикладі. Чи викликає CommandManager.InvalidateRequerySugnged () причину, щоб кнопку активували?


3
Спробував встановити CommandParameter перед Command - все ще виконує CanExecute, але все одно передає NULL ... Облом - але дякую за підказку. Крім того, виклик CommandManager.InvalidateRequerySugnged (); не робить ніякої різниці.
Jonas Follesø

CommandManager.InvalidateRequerySugnged () вирішив подібну проблему для мене. Дякую!
MJS

13

Я запропонував ще один варіант вирішення цього питання, яким хотів поділитися. Оскільки метод 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}" />

Сподіваюсь, це, можливо, допоможе комусь із цим питанням.


Чудово зроблено, дякую. Я не можу повірити, що M $ не виправив цього через 8 років. Жахливий!
McGarnagle

8

Це стара тема, але оскільки Google привів мене сюди, коли у мене виникла ця проблема, я додаю те, що працювало для мене для DataGridTemplateColumn за допомогою кнопки.

Змініть прив'язку з:

CommandParameter="{Binding .}"

до

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

Не знаю, чому це працює, але це зробило для мене.


Я спробував обидві вищезазначені відповіді, але ця спрацювала лише для мене. Здається, це внутрішня проблема самого контролю, а не прив'язки, але все одно багато людей отримали це, працюючи з вищезазначеними відповідями. Дякую!
Джавідан

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

Можливо, ви зможете скористатися моїм, CommandParameterBehaviorякий я розмістив на форумах Prism вчора. Це додає відсутність поведінки, коли зміна CommandParameterпричини Commandперезапиту.

Тут є деяка складність, спричинена моїми спробами уникнути витоку пам’яті, викликаного, якщо ви телефонуєте PropertyDescriptor.AddValueChangedбез подальшого дзвінка PropertyDescriptor.RemoveValueChanged. Я намагаюся це виправити, відмінивши реєстратор обробника, коли екмент вивантажується.

Ймовірно, вам доведеться видалити IDelegateCommandречі, якщо ви не використовуєте Prism (і хочете внести ті самі зміни, що і я в бібліотеку Prism). Також зауважте, що ми зазвичай RoutedCommandтут не використовуємо s (ми використовуємо 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();
            }

        }
    }
}

натрапив на ваш звіт про помилку при підключенні. Будь-який шанс ви могли б оновити свою публікацію тут своїм останнім кодом цього? чи з тих пір ви знайшли кращу роботу?
Маркус Хюттер,

Більш простим рішенням може бути спостереження властивості CommandParameter, використовуючи прив’язку замість дескриптора властивості. Інакше чудове рішення! Це насправді виправляє основну проблему, а не просто вводить незручний хак чи обхідний шлях.
Себастьян Неграс,

1

Існує відносно простий спосіб "виправити" цю проблему за допомогою 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) У захищеній віртуальній порожнечі 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 слід запускати кожного разу, коли інтерфейс відображає елементи, прив'язані до відповідного DelegateCommand.

Бережись, Джо

арбітраж на gmail


1

Прочитавши кілька хороших відповідей на подібні запитання, я трохи змінив у вашому прикладі 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.InvalidateRequerySugged в потоці графічного інтерфейсу

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

Я виявив, що DispatcherPriority.Normalце занадто високо, щоб працювати надійно (або взагалі, у моєму випадку). Використання DispatcherPriority.Loadedпрацює добре і здається більш доцільним (тобто явно вказує, що делегат не можна викликати, поки фактично не завантажені елементи інтерфейсу, пов'язані з моделлю подання).
Пітер Дуніхо,

0

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

CommandParameter = "{Прив'язка RelativeSource = {RelativeSource AncestorType = ContextMenu}, Path = PlacementTarget.SelectedItem, Mode = TwoWay}"


Я точно так само роблю в поданні списку. У цьому випадку це ItemsControl, тому немає очевидних властивостей "прив'язуватись" (у візуальному дереві). Думаю, мені доведеться знайти спосіб виявити, коли прив'язка зроблена, і переоцінити CanExecute (бо CommandParameter прив'язується, просто до пізнього часу)
Jonas Follesø


0

Деякі з цих відповідей стосуються прив'язки до DataContext для отримання самої команди, але питання полягало в тому, щоб CommandParameter був нульовим, коли він не повинен бути. Ми також це пережили. На перший погляд, ми знайшли дуже простий спосіб змусити це працювати в нашому 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;
        }
    }

-1

Це довгий постріл. для налагодження цього ви можете спробувати:
- перевірити подію PreviewCanExecute.
- використовуйте snoop / wpf mol, щоб зазирнути всередину і подивитися, що таке параметр команди.

HTH,


Спробував використовувати Snoop - але насправді важко налагодити, оскільки це лише NULL, коли він спочатку завантажений. Якщо я запустив на ній Snoop, то Command і CommandParameter обидва seth ... Це пов’язано з використанням команд у DataTemplate.
Jonas Follesø

-1

КомандаManager.InvalidateRequerySugnged працює і для мене. Я вважаю, що в наступному посиланні йдеться про подібну проблему, і M $ dev підтвердив обмеження в поточній версії, а командаManager.InvalidateRequerySugnged є обхідним шляхом. http://social.expression.microsoft.com/Forums/en-US/wpf/thread/c45d2272-e8ba-4219-bb41-1e5eaed08a1f/

Важливим є час виклику командиManager.InvalidateRequerySugnged. До цього слід звернутися після повідомлення про відповідну зміну значення.


це посилання вже не дійсне
Пітер Дуніхо,

-2

Поряд із пропозицією Еда Болла щодо встановлення CommandParameter перед Command , переконайтесь, що ваш метод CanExecute має параметр типу об’єкта .

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

Сподіваюся, це заважає комусь витратити величезну кількість часу, який я зробив, щоб зрозуміти, як отримати SelectedItems як параметр CanExecute

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.