2008-12-02 13 views
70

Ich habe ein Problem mit WPF und Befehle, die an eine Schaltfläche innerhalb der DataTemplate eines ItemsControl gebunden sind, aufgetreten. Das Szenario ist ziemlich geradlinig. Das ItemsControl ist an eine Liste von Objekten gebunden, und ich möchte in der Lage sein, jedes Objekt in der Liste durch Klicken auf eine Schaltfläche zu entfernen. Der Button führt einen Befehl aus und der Befehl kümmert sich um den Löschvorgang. Der CommandParameter ist an das Objekt gebunden, das ich löschen möchte. Auf diese Weise weiß ich, worauf der Benutzer geklickt hat. Ein Benutzer sollte nur in der Lage sein, seine "eigenen" Objekte zu löschen. Daher muss ich im "CanExecute" -Aufruf des Befehls prüfen, ob der Benutzer die richtigen Berechtigungen hat.WPF CommandParameter ist NULL zum ersten Mal CanExecute heißt

Das Problem ist, dass der an CanExecute übergebene Parameter beim ersten Aufruf NULL ist - also kann ich die Logik nicht ausführen, um den Befehl zu aktivieren/deaktivieren. Wenn ich es jedoch immer aktiviert habe und dann auf die Schaltfläche zum Ausführen des Befehls klicke, wird CommandParameter korrekt übergeben. Das bedeutet also, dass die Bindung an das CommandParameter funktioniert.

Die XAML für das Itemscontrol und der Datatemplate sieht wie folgt aus:

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

So wie du, ich habe eine Liste von Objekten Kommentare sehen können. Ich möchte, dass der CommandParameter des DeleteCommentCommand an das Command-Objekt gebunden wird.

Also ich denke, meine Frage ist: habe jemand dieses Problem schon einmal erlebt? CanExecute wird auf meinem Befehl aufgerufen, aber der Parameter ist beim ersten Mal immer NULL - warum ist das?

Update: Ich konnte das Problem etwas eingrenzen. Ich habe einen leeren Debug ValueConverter hinzugefügt, so dass ich eine Nachricht ausgeben konnte, wenn der CommandParameter Daten gebunden ist. Das Problem besteht darin, dass die CanExecute-Methode ausgeführt wird, bevor der CommandParameter an die Schaltfläche gebunden ist. Ich habe versucht, das CommandParameter vor dem Befehl zu setzen (wie vorgeschlagen) - aber es funktioniert immer noch nicht. Irgendwelche Tipps, wie man es kontrolliert.

Update2: Gibt es eine Möglichkeit zu erkennen, wenn die Bindung "erledigt" ist, so dass ich eine erneute Auswertung des Befehls erzwingen kann? Außerdem - ist es ein Problem, dass ich mehrere Schaltflächen (eine für jedes Element im ItemsControl) habe, die an dieselbe Instanz eines Befehlsobjekts binden?

Update3: Ich habe eine Reproduktion des Fehlers zu meinem SkyDrive hochgeladen: http://cid-1a08c11c407c0d8e.skydrive.live.com/self.aspx/Code%20samples/CommandParameterBinding.zip

+0

Ich habe genau dasselbe Problem, mit einer ListBox. –

Antwort

12

Ich stolperte über ein ähnliches Problem und löste es mit meinem trustey 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 
} 

Dieser Wertkonverter akzeptiert eine beliebige Anzahl von Parametern und übergibt die erste davon als umgewandelten Wert. Wenn es in einem MultiBinding in Ihrem Fall verwendet wird, sieht es folgendermaßen aus.

Sie müssen TriggerConverter als Ressource hinzufügen, damit dies funktioniert. Jetzt wird die Command-Eigenschaft nicht festgelegt, bevor der Wert für das CommandParameter verfügbar geworden ist. Sie könnten sogar an RelativeSource.Self und CommandParameter binden. um den gleichen Effekt zu erzielen.

+1

Dies funktionierte für mich. Ich verstehe nicht warum. Kann mir jemand erklären? –

+0

Funktioniert es nicht, weil CommandParameter vor dem Befehl gebunden ist? Ich bezweifle, dass Sie den Konverter benötigen würden ... – MBoros

+1

Dies ist keine Lösung. Das ist ein Hack? Was zur Hölle ist los? Das hat früher funktioniert? – Jordan

27

ich gefunden habe, dass die Reihenfolge, in der ich gesetzt Befehl und Command einen Unterschied macht. Das Festlegen der Eigenschaft Command bewirkt, dass CanExecute sofort aufgerufen wird, sodass CommandParameter bereits an diesem Punkt festgelegt werden soll.

Ich habe festgestellt, dass die Reihenfolge der Eigenschaften in der XAML tatsächlich eine Wirkung haben kann, obwohl ich nicht zuversichtlich bin, dass es Ihr Problem lösen wird. Es ist jedoch einen Versuch wert.

Sie scheinen darauf hinzuweisen, dass die Schaltfläche nie aktiviert wird, was überraschend ist, da ich erwarten würde, dass CommandParameter kurz nach der Command-Eigenschaft in Ihrem Beispiel festgelegt wird. Führt das Aufrufen von CommandManager.InvalidateRequerySuggested() dazu, dass die Schaltfläche aktiviert wird?

+2

Versucht, das CommandParameter vor dem Command zu setzen - führt immer noch CanExecute aus, aber übergibt immer noch NULL ... Bummer - aber danke für den Tipp. Außerdem wird CommandManager.InvalidateRequerySuggested(); macht keinen Unterschied. –

+0

CommandManager.InvalidateRequerySuggested() löste ein ähnliches Problem für mich. Vielen Dank! – MJS

-1

Es ist eine Totale. um dies zu debuggen, können Sie versuchen:
- Überprüfen des PreviewCanExecute-Ereignisses.
- Verwenden Sie Snoop/WPF Maulwurf, um nach innen zu sehen und zu sehen, was der Befehlsparameter ist.

HTH,

+0

Mit Snoop versucht - aber es ist wirklich schwer zu debuggen, da es nur NULL ist, wenn es anfänglich geladen wird. Wenn ich Snoop darauf starte, sind Command und CommandParameter beide seth ... Es hat mit der Verwendung von Befehlen in DataTemplate zu tun. –

0

Hey Jonas, nicht sicher, ob dies in einer Datenvorlage arbeiten, aber hier ist die Bindung Syntax I in einem Listview-Kontextmenü verwenden, um das aktuelle Element als Befehlsparameter zu greifen:

Command = "{Binding Relative = {Relative AncestorType = ContextMenu}, Path = PlacementTarget.SelectedItem, Mode = TwoWay}"

+0

Ich mache genau das gleiche in meiner Listenansicht. In diesem Fall ist es ein ItemsControl, also gibt es keine offensichtliche Eigenschaft, gegen die "gebunden" werden kann (in der visuellen Struktur).Ich denke, ich muss einen Weg finden, zu erkennen, wann die Bindung gemacht wird, und CanExecute neu bewerten (weil CommandParameter gebunden wird, nur zu spät) –

-1

Die commandManager.InvalidateRequerySuggested funktioniert auch für mich. Ich glaube, der folgende Link spricht über ein ähnliches Problem, und M $ Dev bestätigte die Einschränkung in der aktuellen Version, und der BefehlManager.InvalidateRequerySuggested ist die Problemumgehung. http://social.expression.microsoft.com/Forums/en-US/wpf/thread/c45d2272-e8ba-4219-bb41-1e5eaed08a1f/

Was ist wichtig das Timing des Aufrufs des BefehlsManager.InvalidateRequerySuggested. Dies sollte aufgerufen werden, nachdem die relevante Wertänderung gemeldet wurde.

+0

dieser Link ist nicht mehr gültig –

4

Sie können möglicherweise meine CommandParameterBehavior verwenden, die ich gestern an die Prism forums gepostet habe. Es fügt das fehlende Verhalten hinzu, bei dem eine Änderung an CommandParameter dazu führt, dass die Command erneut abgefragt wird.

Es gibt einige Komplexität hier verursacht durch meine Versuche, den Speicherverlust zu vermeiden, wenn Sie PropertyDescriptor.AddValueChanged aufrufen, ohne später PropertyDescriptor.RemoveValueChanged aufzurufen. Ich versuche das zu beheben, indem ich den Handler abmelden, wenn das Ekement entladen wird.

Sie müssen wahrscheinlich die IDelegateCommand Sachen entfernen, wenn Sie Prism verwenden (und die gleichen Änderungen wie ich an der Prism-Bibliothek vornehmen möchten). Beachten Sie auch, dass wir RoutedCommand hier nicht verwenden (wir verwenden Prism's DelegateCommand<T> für so ziemlich alles) also bitte nicht mich verantwortlich, wenn mein Anruf zu CommandManager.InvalidateRequerySuggested eine Art Quantenwellenfunctionskollapskaskade auslöst, die das bekannte Universum zerstört oder etwas.

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

     } 
    } 
} 
+0

kam über Ihren Fehlerbericht bei connect. Könntest du deinen Beitrag hier mit deinem letzten Code aktualisieren? oder hast du seither eine bessere arbeit gefunden? –

1

ist es eine relativ einfache Möglichkeit, dies zu "reparieren" noch existiert Problem mit DelegateCommand, obwohl das Aktualisieren der DelegateCommand-Quelle und das erneute Kompilieren von Microsoft.Practices.Composite.Presentation.dll erforderlich ist.

1) Laden Sie den Quellcode von Prism 1.2 herunter und öffnen Sie CompositeApplicationLibrary_Desktop.sln. Hier ist ein Composite.Presentation.Desktop-Projekt, das die DelegateCommand-Quelle enthält.

2) Im Rahmen der öffentlichen Veranstaltung Eventhandler CanExecuteChanged, ändern wie folgt lauten:

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) Unter geschützt virtual void OnCanExecuteChanged(), ändern Sie es wie folgt:

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

4) Kompilieren Sie die Lösung erneut und navigieren Sie dann zum Debug- oder Release-Ordner, in dem die kompilierten DLLs gespeichert sind. Kopieren Sie die Microsoft.Practices.Composite.Presentation.dll und .pdb (wenn Sie möchten), wo Sie auf Ihre externen Assemblies verweisen, und kompilieren Sie Ihre Anwendung neu, um die neuen Versionen zu ziehen.

Danach sollte CanExecute jedes Mal ausgelöst werden, wenn die UI Elemente rendert, die an den betreffenden DelegateCommand gebunden sind.

Achten Sie darauf, Joe

refereejoe bei gmail

47

ich das gleiche Problem wurde, während auf einen Befehl auf meiner Ansicht Modell zu binden versucht.

Ich änderte es, um eine relative Quellbindung zu verwenden, anstatt auf das Element nach Name zu verweisen, und das hat den Trick gemacht. Die Parameterbindung hat sich nicht geändert.

Alter Code:

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

Neuer Code:

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

aktualisieren: Ich kam gerade über diese Frage ohne Elementnamen zu verwenden, ich bin auf dem View-Modell auf einen Befehl Bindung und Mein Datenkontext der Schaltfläche ist mein Ansichtsmodell. In diesem Fall musste ich einfach das CommandParameter-Attribut vor dem Command-Attribut in der Button-Deklaration (in XAML) verschieben.

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

Das CommandParameter vor dem Befehl zu verschieben ist die beste Antwort auf diesen Thread. – BSick7

+4

Verschieben der Reihenfolge der Attribute hat uns nicht geholfen. Ich wäre überrascht, wenn es Auswirkungen auf die Reihenfolge der Ausführung hätte. – Schneider

+2

Ich weiß nicht, warum das funktioniert. Es fühlt sich an, als ob es nicht sollte, aber es tut es total. – RMK

13

Ich weiß, dass dieses Thema etwas alt ist, aber ich habe mit einer anderen Option kommen, um dieses Problem zu arbeiten, die ich teilen wollte. Da die CanExecute-Methode des Befehls ausgeführt wird, bevor die CommandParameter-Eigenschaft festgelegt wird, habe ich eine Hilfsklasse mit einer angefügten Eigenschaft erstellt, die erzwingt, dass die CanExecute-Methode erneut aufgerufen wird, wenn sich die Bindung ändert.

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

Und dann auf die Schaltfläche Sie einen Befehlsparameter zu binden ...

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

Ich hoffe, das vielleicht jemand anderes mit dem Thema hilft.

+0

Diese Workaround hat es funktioniert für mich, danke! –

+0

Schön gemacht, danke. Ich kann nicht glauben, dass M $ das nach 8 Jahren nicht behoben hat. Turrible! – McGarnagle

-1

Neben Ed Ball's suggestion auf Einstellung Command vor Befehl, stellen Sie sicher, dass Ihre CanExecute Methode einen Parameter von Objekt Typ hat.

privaten Bool OnDeleteSelectedItemsCanExecute (Objekt SelectedItems)
{

// Your goes heres 

}

Hoffe, dass es jemand verbringen die riesige Menge an Zeit verhindert ich tat, um herauszufinden, wie SelectedItems als CanExecute Parameter

erhalten
1

Nachdem ich einige gute Antworten auf ähnliche Fragen gelesen habe, habe ich in Ihrem Beispiel den DelegateCommand leicht geändert, damit es funktioniert. Anstelle der Verwendung von:

public event EventHandler CanExecuteChanged; 

habe ich es zu:

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

Ich entfernte die folgenden zwei Methoden, weil ich zu faul war, um sie zu beheben

public void RaiseCanExecuteChanged() 

und

protected virtual void OnCanExecuteChanged() 

Und das ist alles ... das scheint zu en Sie sicher, dass CanExecute wenn die Bindung Änderungen aufgerufen wird, und nach der Methode Execute

Es ist nicht automatisch ausgelöst wird, wenn das Ansichtsmodell geändert wird, aber wie in diesem Thread möglich erwähnt durch die CommandManager.InvalidateRequerySuggested auf dem GUI-Thread rufen

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

Ich habe festgestellt, dass "DispatcherPriority.Normal" zu hoch ist, um zuverlässig (oder überhaupt, in meinem Fall) zu arbeiten. Die Verwendung von 'DispatcherPriority.Loaded' funktioniert gut und scheint geeigneter zu sein (d. H. Weist explizit darauf hin, dass der Delegat erst dann aufgerufen werden soll, wenn die dem Ansichtsmodell zugeordneten UI-Elemente tatsächlich geladen wurden). –

0

Einige dieser Antworten beziehen sich auf das Binden an den DataContext, um das Command selbst zu erhalten, aber die Frage war, dass CommandParameter null ist, wenn es nicht sein sollte. Das haben wir auch erlebt. Kurz gesagt, wir haben einen sehr einfachen Weg gefunden, dies in unserem ViewModel zum Laufen zu bringen. Dies ist speziell für das CommandParameter-Nullproblem, das vom Kunden gemeldet wird, mit einer Codezeile. Beachten Sie den 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; 
     } 
    } 
6

Dies ist ein alter Thread, aber da Google mich hierher gebracht, wenn ich dieses Problem habe, werde ich hinzufügen, was mit einem Knopf für einen Datagridtemplatecolumn für mich gearbeitet.

aus der Bindung ändern:

CommandParameter="{Binding .}" 

zu

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

nicht sicher, warum es funktioniert, aber es hat für mich.

+0

Ich habe beide Highscore-Antworten oben versucht, aber dieses eine funktionierte nur für mich. Es scheint, es ist das interne Problem der Kontrolle selbst nicht die Bindung, aber immer noch viele Menschen haben es funktioniert mit obigen Antworten. Vielen Dank! – Javidan