WPF CommandParameter é NULL pela primeira vez que CanExecute é chamado

86

Eu tive um problema com o WPF e os comandos vinculados a um botão dentro do DataTemplate de um ItemsControl. O cenário é bastante simples. O ItemsControl está vinculado a uma lista de objetos e quero poder remover cada objeto da lista clicando em um botão. O Botão executa um Comando e o Comando cuida da exclusão. O CommandParameter está vinculado ao objeto que desejo excluir. Assim eu sei o que o usuário clicou. Um usuário só deve ser capaz de excluir seus "próprios" objetos - portanto, preciso fazer algumas verificações na chamada "CanExecute" do Comando para verificar se o usuário tem as permissões corretas.

O problema é que o parâmetro passado para CanExecute é NULL na primeira vez que é chamado - então não consigo executar a lógica para habilitar / desabilitar o comando. No entanto, se eu ativá-lo sempre e clicar no botão para executar o comando, o CommandParameter é passado corretamente. Isso significa que a vinculação ao CommandParameter está funcionando.

O XAML para ItemsControl e DataTemplate se parece com isto:

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

Como você pode ver, tenho uma lista de objetos Comentários. Eu quero que o CommandParameter do DeleteCommentCommand seja vinculado ao objeto Command.

Portanto, acho que minha pergunta é: alguém já experimentou esse problema antes? CanExecute é chamado no meu comando, mas o parâmetro é sempre NULL na primeira vez - por que isso?

Atualização: consegui reduzir um pouco o problema. Eu adicionei um Debug ValueConverter vazio para que eu pudesse enviar uma mensagem quando o CommandParameter estiver vinculado aos dados. Acontece que o problema é que o método CanExecute é executado antes que CommandParameter seja vinculado ao botão. Eu tentei definir o CommandParameter antes do comando (como sugerido) - mas ainda não funciona. Alguma dica sobre como controlá-lo.

Update2: Existe alguma maneira de detectar quando a ligação está "concluída", para que eu possa forçar a reavaliação do comando? Além disso - é um problema que eu tenho vários botões (um para cada item no ItemsControl) que se ligam à mesma instância de um objeto de comando?

Update3: Eu carreguei uma reprodução do bug no meu SkyDrive: http://cid-1a08c11c407c0d8e.skydrive.live.com/self.aspx/Code%20samples/CommandParameterBinding.zip

Jonas Follesø
fonte
Eu tenho exatamente o mesmo problema, com um ListBox.
Hadi Eskandari,
Há um relatório de bug aberto no WPF para esse problema: github.com/dotnet/wpf/issues/316
UuDdLrLrSs

Respostas:

14

Me deparei com um problema semelhante e resolvi usando meu confiável 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
}

Este conversor de valor pega qualquer número de parâmetros e passa o primeiro deles de volta como o valor convertido. Quando usado em uma MultiBinding em seu caso, tem a seguinte aparência.

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

Você terá que adicionar TriggerConverter como um recurso em algum lugar para que isso funcione. Agora, a propriedade Command não é definida antes que o valor para CommandParameter esteja disponível. Você pode até mesmo vincular a RelativeSource.Self e CommandParameter em vez de. para obter o mesmo efeito.

David Liersch
fonte
2
Isso funcionou para mim. Eu não entendo o porquê. Alguém pode explicar?
TJKjaer
Não funciona porque o CommandParameter está vinculado antes do comando? Duvido que você precise do conversor ...
MBoros
2
Isso não é uma solução. Isso é um hack? O que diabos está acontecendo? Isso costumava funcionar?
Jordan,
Perfeito, funciona para mim! A mágica está na linha <Binding />, que faz com que o vínculo do comando seja atualizado quando o modelo de dados muda (que é vinculado ao parâmetro do comando)
Andreas Kahler
56

Eu estava tendo o mesmo problema ao tentar vincular a um comando no meu modelo de exibição.

Eu mudei para usar uma associação de origem relativa em vez de me referir ao elemento pelo nome e isso resolveu o problema. A vinculação de parâmetros não mudou.

Código Antigo:

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

Novo Código:

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

Update : Acabei de me deparar com esse problema sem usar ElementName, estou vinculando a um comando em meu modelo de exibição e meu contexto de dados do botão é meu modelo de exibição. Nesse caso, tive que simplesmente mover o atributo CommandParameter antes do atributo Command na declaração de Button (em XAML).

CommandParameter="{Binding Groups}"
Command="{Binding StartCommand}"
Travis Weber
fonte
42
Mover o CommandParameter na frente do Command é a melhor resposta neste tópico.
BSick7
6
Mover a ordem dos atributos não nos ajudou. Eu ficaria surpreso se tivesse algum efeito na ordem de execução.
Jack Ukleja,
3
Eu não sei porque isso funciona. Parece que não deveria, mas é totalmente verdade.
RMK de
1
Eu tive o mesmo problema - RelativeSource não ajudou, mudando a ordem dos atributos sim. Obrigado pela atualização!
Grant Crofton
14
Como uma pessoa que religiosamente usa extensões para automaticamente embelezar XAML (atributos dividido em todas as linhas, correção recuo, atributos Reordenar) a proposta de alterar a ordem de CommandParametere Commandme assusta.
Guttsy
29

Descobri que a ordem em que defino Command e CommandParameter faz diferença. Definir a propriedade Command faz com que CanExecute seja chamado imediatamente, portanto, você deseja que CommandParameter já esteja definido nesse ponto.

Descobri que mudar a ordem das propriedades no XAML pode realmente ter um efeito, embora não tenha certeza de que isso resolverá seu problema. Mas vale a pena tentar.

Você parece estar sugerindo que o botão nunca é habilitado, o que é surpreendente, pois eu esperaria que o CommandParameter fosse definido logo após a propriedade Command em seu exemplo. Chamar CommandManager.InvalidateRequerySuggested () faz com que o botão seja habilitado?

Ed Ball
fonte
3
Tentei definir o CommandParameter antes do Command - ainda executa CanExecute, mas ainda passa em NULL ... Que chatice - mas obrigado pela dica. Além disso, chamar CommandManager.InvalidateRequerySuggested (); não faz nenhuma diferença.
Jonas Follesø
CommandManager.InvalidateRequerySuggested () resolveu um problema semelhante para mim. Obrigado!
MJS de
13

Eu encontrei outra opção para contornar esse problema que gostaria de compartilhar. Como o método CanExecute do comando é executado antes que a propriedade CommandParameter seja definida, criei uma classe auxiliar com uma propriedade anexada que força o método CanExecute a ser chamado novamente quando a ligação muda.

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);
    }
}

E então no botão que você deseja vincular um parâmetro de comando a ...

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

Espero que isso talvez ajude alguém com o problema.

Ed Downs
fonte
Muito bem, obrigado. Não acredito que M $ não consertou isso depois de 8 anos. Turrível!
McGarnagle
8

Este é um tópico antigo, mas como o Google me trouxe aqui quando tive esse problema, adicionarei o que funcionou para mim para um DataGridTemplateColumn com um botão.

Altere a ligação de:

CommandParameter="{Binding .}"

para

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

Não sei por que funciona, mas funcionou para mim.

Simon Smith
fonte
Tentei as duas respostas de pontuação alta acima, mas esta só funcionou para mim. Parece que é um problema interno de controle em si, não a ligação, mas ainda assim muitas pessoas conseguiram trabalhar com as respostas acima. Obrigado!
Javidan
6

Recentemente me deparei com o mesmo problema (para mim era para os itens de menu em um menu de contexto), mas embora possa não ser uma solução adequada para cada situação, encontrei uma maneira diferente (e muito mais curta!) De resolver isso problema:

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

Ignorando a Tagsolução alternativa baseada em - para o caso especial de menu de contexto, a chave aqui é vincular o CommandParameterregularmente, mas vincular o Commandcom o adicional IsAsync=True. Isso atrasará um pouco a vinculação do comando real (e, portanto, sua CanExecutechamada), de modo que o parâmetro já estará disponível. Isso significa, porém, que por um breve momento, o estado ativado pode estar errado, mas para o meu caso, isso era perfeitamente aceitável.

Ralf Stauder
fonte
5

Você pode usar o CommandParameterBehaviorque eu postei nos fóruns do Prism ontem. Ele adiciona o comportamento ausente onde uma mudança na CommandParametercausa doCommand com que seja novamente consultado.

Há alguma complexidade aqui causada por minhas tentativas de evitar o vazamento de memória causado se você ligar PropertyDescriptor.AddValueChangedsem chamar mais tardePropertyDescriptor.RemoveValueChanged . Tento consertar isso cancelando o registro do manipulador quando o ekement é descarregado.

Você provavelmente precisará remover o IDelegateCommandmaterial, a menos que esteja usando o Prism (e queira fazer as mesmas alterações que eu fiz na biblioteca do Prism). Observe também que geralmente não usamos RoutedCommands aqui (usamos Prism DelegateCommand<T>para praticamente tudo), então, por favor, não me responsabilize se meu apelo para CommandManager.InvalidateRequerySuggesteddesencadear algum tipo de cascata de colapso de função de onda quântica que destrua o universo conhecido ou qualquer coisa.

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();
            }

        }
    }
}
Swythan
fonte
encontrei o seu relatório de bug no Connect. Alguma chance de você atualizar sua postagem aqui com o último código disso? ou desde então você encontrou uma solução melhor?
Markus Hütter
Uma solução mais fácil pode ser observar a propriedade CommandParameter usando uma associação em vez de um descritor de propriedade. Caso contrário, uma ótima solução! Na verdade, este corrige o problema subjacente em vez de apenas introduzir um hack ou uma solução alternativa estranha.
Sebastian Negraszus
1

Há uma maneira relativamente simples de "consertar" esse problema com DelegateCommand, embora exija a atualização da fonte DelegateCommand e a recompilação do Microsoft.Practices.Composite.Presentation.dll.

1) Baixe o código-fonte do Prism 1.2 e abra o CompositeApplicationLibrary_Desktop.sln. Aqui está um projeto Composite.Presentation.Desktop que contém a fonte DelegateCommand.

2) No evento público EventHandler CanExecuteChanged, modifique para ler da seguinte forma:

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) Sob o vazio virtual protegido OnCanExecuteChanged (), modifique-o da seguinte forma:

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

4) Recompile a solução e navegue até a pasta Debug ou Release onde residem as DLLs compiladas. Copie Microsoft.Practices.Composite.Presentation.dll e .pdb (se desejar) para onde você faz referência aos assemblies externos e, em seguida, recompile seu aplicativo para obter as novas versões.

Depois disso, CanExecute deve ser disparado sempre que a IU renderiza elementos vinculados ao DelegateCommand em questão.

Se cuida Joe

árbitro no gmail

Joe Bako
fonte
1

Depois de ler algumas boas respostas para perguntas semelhantes, mudei levemente em seu exemplo o DelegateCommand para fazê-lo funcionar. Ao invés de usar:

public event EventHandler CanExecuteChanged;

Eu mudei para:

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

Removi os dois métodos a seguir porque tinha preguiça de corrigi-los

public void RaiseCanExecuteChanged()

e

protected virtual void OnCanExecuteChanged()

E isso é tudo ... isso parece garantir que CanExecute será chamado quando o Binding mudar e após o método Execute

Ele não será acionado automaticamente se ViewModel for alterado, mas conforme mencionado neste thread, é possível chamar CommandManager.InvalidateRequerySuggested no thread da GUI

Application.Current?.Dispatcher.Invoke(DispatcherPriority.Normal, (Action)CommandManager.InvalidateRequerySuggested);
kkCosmo
fonte
Descobri que DispatcherPriority.Normalé muito alto para funcionar de forma confiável (ou em tudo, no meu caso). Usar DispatcherPriority.Loadedfunciona bem e parece mais apropriado (ou seja, indica explicitamente que o delegado não deve ser invocado até que os elementos de interface do usuário associados ao modelo de exibição tenham de fato sido carregados).
Peter Duniho
0

Ei Jonas, não tenho certeza se isso vai funcionar em um modelo de dados, mas aqui está a sintaxe de ligação que eu uso em um menu de contexto ListView para pegar o item atual como um parâmetro de comando:

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


fonte
Eu faço exatamente a mesma coisa na minha visualização de lista. Nesse caso, é um ItemsControl, portanto não há nenhuma propriedade óbvia com a qual "ligar" (na árvore visual). Acho que tenho que encontrar uma maneira de detectar quando a vinculação está concluída e reavaliar CanExecute (porque CommandParameter é vinculado, apenas tarde)
Jonas Follesø
0

Algumas dessas respostas são sobre a vinculação ao DataContext para obter o próprio Comando, mas a questão era sobre o CommandParameter ser nulo quando não deveria ser. Nós também experimentamos isso. Seguindo um palpite, encontramos uma maneira muito simples de fazer isso funcionar em nosso ViewModel. Isso é especificamente para o problema nulo de CommandParameter relatado pelo cliente, com uma linha de código. Observe o 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;
        }
    }
TravisWhidden
fonte
-1

É um tiro longo. para depurar isso, você pode tentar:
- verificar o evento PreviewCanExecute.
- use snoop / wpf mole para espiar dentro e ver o que o parâmetro de comando é.

HTH,

Dennis
fonte
Tentei usar o Snoop - mas é realmente difícil de depurar porque só é NULL quando é carregado inicialmente. Se eu executar o Snoop nele, o Command e CommandParameter serão ambos seth ... Tem a ver com o uso de comandos no DataTemplate.
Jonas Follesø
-1

O commandManager.InvalidateRequerySuggested funciona para mim também. Acredito que o link a seguir fala sobre um problema semelhante, e M $ dev confirmou a limitação na versão atual, e o commandManager.InvalidateRequerySuggested é a solução alternativa. http://social.expression.microsoft.com/Forums/en-US/wpf/thread/c45d2272-e8ba-4219-bb41-1e5eaed08a1f/

O importante é o tempo de invocação do commandManager.InvalidateRequerySuggested. Isso deve ser chamado depois que a mudança de valor relevante for notificada.


fonte
esse link não é mais válido
Peter Duniho
-2

Ao lado da sugestão de Ed Ball sobre a configuração de CommandParameter antes de Command , certifique-se de que seu método CanExecute tenha um parâmetro de tipo de objeto .

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

Espero que isso evite que alguém gaste tanto tempo que eu gastei tentando descobrir como receber SelectedItems como parâmetro CanExecute

Julio Nobre
fonte