2010-08-27 11 views
17

Angenommen, ich habe ein Fenster mit einer Eigenschaft, die einen Befehl zurückgibt (in der Tat ist es ein UserControl mit einem Command in einer ViewModel-Klasse), aber lassen Sie uns die Dinge so einfach wie möglich halten Problem).WPF: Binden eines ContextMenu an einen MVVM-Befehl

Die folgenden Werke:

<Window x:Class="Window1" ... x:Name="myWindow"> 
    <Menu> 
     <MenuItem Command="{Binding MyCommand, ElementName=myWindow}" Header="Test" /> 
    </Menu> 
</Window> 

aber die folgenden funktioniert nicht.

<Window x:Class="Window1" ... x:Name="myWindow"> 
    <Grid> 
     <Grid.ContextMenu> 
      <ContextMenu> 
       <MenuItem Command="{Binding MyCommand, ElementName=myWindow}" Header="Test" /> 
      </ContextMenu>    
     </Grid.ContextMenu> 
    </Grid> 
</Window> 

Die Fehlermeldung, die ich bekommen ist

System.Windows.Data Error: 4 : Cannot find source for binding with reference 'ElementName=myWindow'. BindingExpression:Path=MyCommand; DataItem=null; target element is 'MenuItem' (Name=''); target property is 'Command' (type 'ICommand')

Warum? Und wie repariere ich das? Die Verwendung der DataContext ist keine Option, da dieses Problem weit unten in der visuellen Struktur auftritt, wo der DataContext bereits die tatsächlichen Daten enthält, die angezeigt werden. Ich habe bereits versucht, stattdessen {RelativeSource FindAncestor, ...} zu verwenden, aber das ergibt eine ähnliche Fehlermeldung.

+0

+1 für die Bearbeitung mit Ihrer Lösung, sollten Sie es machen eine separate Antwort – jan

+0

@jan: Gute Idee, fertig. – Heinzi

Antwort

16

Das Problem ist, dass die ContextMenu es nicht in der visuellen Struktur, so dass Sie im Grunde das Kontextmenü über Kontext, welche Daten zu verwenden, zu sagen haben.

Check out this blogpost mit einer sehr schönen Lösung von Thomas Levesque.

Er erstellt eine Klasse Proxy, die Freezable erbt und eine Datenabhängigkeitseigenschaft deklariert.

public class BindingProxy : Freezable 
{ 
    protected override Freezable CreateInstanceCore() 
    { 
     return new BindingProxy(); 
    } 

    public object Data 
    { 
     get { return (object)GetValue(DataProperty); } 
     set { SetValue(DataProperty, value); } 
    } 

    public static readonly DependencyProperty DataProperty = 
     DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null)); 
} 

Dann kann es in der XAML (in der visuellen Struktur auf einen Ort, an dem die richtige Datacontext bekannt ist) erklärt werden:

<Grid.Resources> 
    <local:BindingProxy x:Key="Proxy" Data="{Binding}" /> 
</Grid.Resources> 

Und im Kontextmenü außerhalb der visuellen Struktur verwendet:

+0

Dieses __finally__ funktionierte, nachdem ich ungefähr 10 verschiedene Annäherungen (von SO und von anderswo) versuchte. Vielen Dank für diese saubere und ziemlich einfache, aber so tolle Antwort! :) – Yoda

+0

Dies ist die ** beste Lösung ** – n00b101

+0

Das ist eine sehr schöne Lösung. Ich mache meine Binding-Proxies stark typisiert (Data-Eigenschaft und Abhängigkeitseigenschaft sind nicht Typeof (Objekt), sondern Typeof (MyViewModel). Auf diese Weise gibt es bessere Intellisense, wo ich über den Proxy binden muss. – Michael

6

Siehe Artikel this Artikel von Justin Taylor für eine Problemumgehung.

Aktualisieren
Leider ist das referenzierte Blog nicht mehr verfügbar. Ich habe versucht, das Vorgehen in einer anderen SO-Antwort zu erklären. Es kann here gefunden werden.

+0

Ich habe den fehlenden Blogbeitrag als weitere Antwort gepostet. – mydogisbox

+0

@mydogisbox +1 perfekt! – HCL

4

Basierend auf HCLs answer, das ist, was ich am Ende mit:

<Window x:Class="Window1" ... x:Name="myWindow"> 
    ... 
    <Grid Tag="{Binding ElementName=myWindow}"> 
     <Grid.ContextMenu> 
      <ContextMenu> 
       <MenuItem Command="{Binding Parent.PlacementTarget.Tag.MyCommand, 
              RelativeSource={RelativeSource Self}}" 
          Header="Test" /> 
      </ContextMenu> 
     </Grid.ContextMenu> 
    </Grid> 
</Window> 
+1

Funktioniert das tatsächlich? Ich habe versucht, das funktioniert, und mit Snoop scheint es, dass der Befehl einmal ausgewertet und nie tatsächlich aktualisiert wird. PlacementTarget ist null, bis das Kontextmenü tatsächlich aktiviert ist. An diesem Punkt ist Parent.PlacementTarget.Tag gültig, aber der Befehl wird nie dynamisch aktualisiert (was ich in Snoop sehen kann). – nrjohnstone

+0

Das ist eigentlich das einzige, was für mich und ich funktioniert habe versucht wie 10-15 Vorschläge von überall auf dieser Seite. –

13

Hurray für web.archive.org! Hier ist the missing blog post:

Binding to a MenuItem in a WPF Context Menu

Wednesday, October 29, 2008 — jtango18

Because a ContextMenu in WPF does not exist within the visual tree of your page/window/control per se, data binding can be a little tricky. I have searched high and low across the web for this, and the most common answer seems to be “just do it in the code behind”. WRONG! I didn’t come in to the wonderful world of XAML to be going back to doing things in the code behind.

Here is my example to that will allow you to bind to a string that exists as a property of your window.

public partial class Window1 : Window 
{ 
    public Window1() 
    { 
     MyString = "Here is my string"; 
    } 

    public string MyString 
    { 
     get; 
     set; 

    } 
} 

    <Button Content="Test Button" Tag="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}}}"> 
     <Button.ContextMenu> 
      <ContextMenu DataContext="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Self}}" > 
       <MenuItem Header="{Binding MyString}"/> 
      </ContextMenu> 
     </Button.ContextMenu> 
    </Button> 

The important part is the Tag on the button(although you could just as easily set the DataContext of the button). This stores a reference to the parent window. The ContextMenu is capable of accessing this through it’s PlacementTarget property. You can then pass this context down through your menu items.

I’ll admit this is not the most elegant solution in the world. However, it beats setting stuff in the code behind. If anyone has an even better way to do this I’d love to hear it.

+0

Seltsamerweise hatte ich den 'DataContext' des' MenuItem' gesetzt und es funktioniert nicht. Sobald ich es so änderte, dass es auf dem ContextMenu wie beschrieben eingestellt wurde, fing es an zu arbeiten. Danke für das Posten. –

7

Ich fand heraus, es war nicht für mich arbeitet aufgrund des Menüpunkt verschachtelt werden, was bedeutet, ich hatte ein extra „Eltern“ zu durchqueren bis finden die PlacementTarget.

Ein besserer Weg ist, das ContextMenu selbst als RelativeSource zu finden und dann nur an das Placement-Ziel zu binden. Da das Tag auch das Fenster selbst ist und Ihr Befehl sich im View-Modell befindet, muss auch der DataContext gesetzt sein.

endete ich mit etwas nach oben wie diese

<Window x:Class="Window1" ... x:Name="myWindow"> 
... 
    <Grid Tag="{Binding ElementName=myWindow}"> 
     <Grid.ContextMenu> 
      <ContextMenu> 
       <MenuItem Command="{Binding PlacementTarget.Tag.DataContext.MyCommand, 
              RelativeSource={RelativeSource Mode=FindAncestor,                       
                      AncestorType=ContextMenu}}" 
          Header="Test" /> 
      </ContextMenu> 
     </Grid.ContextMenu> 
    </Grid> 
</Window> 

Was dies bedeutet, dass, wenn Sie mit einem komplizierten Kontextmenü mit Untermenü am Ende etc .. Sie müssen zu jedem nicht halten den Zusatz „Parent“ Ebenen Befehle.

- EDIT -

kamen Auch bei dieser Alternative auf einen Tag auf jeder ListBoxItem einzustellen, die an die Fenster/Usercontrol bindet. Ich tat dies schließlich, weil jedes ListBoxItem durch ein eigenes ViewModel repräsentiert wurde, aber ich benötigte die Menübefehle, um über das Toplevel ViewModel für das Control auszuführen, aber übergab die Liste ViewModel als Parameter.

<ContextMenu x:Key="BookItemContextMenu" 
      Style="{StaticResource ContextMenuStyle1}"> 

    <MenuItem Command="{Binding Parent.PlacementTarget.Tag.DataContext.DoSomethingWithBookCommand, 
         RelativeSource={RelativeSource Mode=FindAncestor, 
         AncestorType=ContextMenu}}" 
       CommandParameter="{Binding}" 
       Header="Do Something With Book" /> 
    </MenuItem>> 
</ContextMenu> 

... 

<ListView.ItemContainerStyle> 
    <Style TargetType="{x:Type ListBoxItem}"> 
     <Setter Property="ContextMenu" Value="{StaticResource BookItemContextMenu}" /> 
     <Setter Property="Tag" Value="{Binding ElementName=thisUserControl}" /> 
    </Style> 
</ListView.ItemContainerStyle> 
2

Wenn (wie ich) Sie eine Abneigung gegen hässliche komplexe Bindungsausdrücke haben, hier ist eine einfache Code-Behind-Lösung für dieses Problem. Mit diesem Ansatz können Sie weiterhin saubere Befehlsdeklarationen in Ihrem XAML beibehalten.

XAML:

<ContextMenu ContextMenuOpening="ContextMenu_ContextMenuOpening"> 
    <MenuItem Command="Save"/> 
    <Separator></Separator> 
    <MenuItem Command="Close"/> 
    ... 

-Code hinter:

private void ContextMenu_ContextMenuOpening(object sender, ContextMenuEventArgs e) 
{ 
    foreach (var item in (sender as ContextMenu).Items) 
    { 
     if(item is MenuItem) 
     { 
      //set the command target to whatever you like here 
      (item as MenuItem).CommandTarget = this; 
     } 
    } 
} 
Verwandte Themen