2010-08-27 17 views
17

Diciamo che ho una finestra con una proprietà che restituisce un comando (infatti, è un controllo utente con un comando in una classe ViewModel, ma manteniamo le cose il più semplici possibile per riprodurre il problema).WPF: associare un ContextMenu a un comando MVVM

le seguenti opere:

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

ma il seguente non funziona.

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

Il messaggio di errore che ottengo è

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

Perché? E come posso risolvere questo? L'utilizzo di DataContext non è un'opzione, poiché questo problema si verifica in fondo alla struttura visiva in cui DataContext contiene già i dati effettivi visualizzati. Ho già provato a utilizzare {RelativeSource FindAncestor, ...} invece, ma questo produce un messaggio di errore simile.

+0

+1 per la modifica con la soluzione, si dovrebbe fare un separato risposta – jan

+0

@jan: Buona idea, fatto. – Heinzi

risposta

16

Il problema è che il ContextMenu non nella struttura ad albero visuale, in modo da avere sostanzialmente dire al menu contestuale su quale contesto dati da utilizzare.

Check out this blogpost con una soluzione molto bella di Thomas Levesque.

Crea un proxy di classe che eredita Freezable e dichiara una proprietà di dipendenza Data.

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

Poi si può essere dichiarata in XAML (su un posto nella struttura ad albero visuale in cui si conosce il DataContext corretto):

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

e utilizzata nel menu contestuale al di fuori della struttura ad albero visuale:

<ContextMenu> 
    <MenuItem Header="Test" Command="{Binding Source={StaticResource Proxy}, Path=Data.MyCommand}"/> 
</ContextMenu> 
+0

Questo __finally__ ha funzionato dopo aver provato circa 10 approcci diversi (da SO e altrove). Grazie mille per questa risposta pulita e abbastanza semplice, ma davvero fantastica! :) – Yoda

+0

Questa è la ** soluzione migliore ** – n00b101

+0

Questa è una soluzione molto bella. Rendo fortemente digitato i miei proxy di legame (la proprietà Data e la proprietà di dipendenza non sono typeof (oggetto) ma typeof (MyViewModel) .In questo modo c'è intellisense migliore dove devo legare tramite il proxy. – Michael

6

Vedere l'articolo this di Justin Taylor per una soluzione alternativa.

Aggiornamento
Purtroppo, il blog di riferimento non è più disponibile. Ho cercato di spiegare il procedimento in un'altra risposta SO. Può essere trovato here.

+0

Ho postato il post sul blog mancante come un'altra risposta. – mydogisbox

+0

@mydogisbox +1 perfetto! – HCL

4

Sulla base di HCLs answer, questo è quello che ho finito per usare:

<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

Funziona davvero? Ho cercato di farlo funzionare, e usando snoop sembra che il comando venga valutato una volta e mai aggiornato. PlacementTarget è null fino a quando il menu di scelta rapida non viene effettivamente attivato, a quel punto Parent.PlacementTarget.Tag è valido ma il comando non viene mai aggiornato dinamicamente (da quello che posso vedere in Snoop) – nrjohnstone

+0

questa è in realtà l'unica cosa che funziona per me e io ho provato come 10-15 suggerimenti da tutto questo sito. –

13

Viva web.archive.org! Ecco 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

Stranamente, ho impostato il 'DataContext' di' MenuItem' e non funziona. Non appena l'ho modificato per essere impostato su 'ContextMenu' come hai descritto, ha iniziato a funzionare. Grazie per aver postato questo. –

7

ho scoperto che non funzionava per me a causa della voce di menu essere annidati, il che significa che ho dovuto attraversare di un "genitore" in più per trovare il PlacementTarget.

Un modo migliore è quello di trovare il ContextMenu stesso come RelativeSource e quindi legarsi al target di posizionamento di quello. Inoltre, poiché il tag è la finestra stessa e il tuo comando è nel viewmodel, devi avere anche il set DataContext.

ho finito con qualcosa di simile

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

Ciò significa che se si finisce con un menu contestuale complicato con sottomenu ecc .. non c'è bisogno di continuare ad aggiungere "Parent" a ciascuno livelli comandi.

- EDIT -

è venuto anche con questa alternativa per impostare un tag su ogni ListBoxItem che si lega alla finestra/Usercontrol. Ho finito per fare questo perché ogni ListBoxItem era rappresentato dal proprio ViewModel ma avevo bisogno dei comandi di menu da eseguire tramite il ViewModel di livello superiore per il controllo, ma passavo il loro elenco ViewModel come parametro.

<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

Se (come me) si ha un'avversione verso espressioni di rilegatura complesse brutte, ecco una semplice soluzione codificata per questo problema. Questo approccio consente comunque di mantenere pulite le dichiarazioni di comando nel tuo XAML.

XAML:

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

codice dietro:

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; 
     } 
    } 
} 
Problemi correlati