2010-08-27 19 views
17

Digamos que tengo una ventana con una propiedad que devuelve un comando (de hecho, es un UserControl con un comando en una clase ViewModel, pero mantengamos todo lo simple posible para reproducir el problema).WPF: Vinculando un ContextMenu a un comando MVVM

las siguientes obras:

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

pero el siguiente no funciona.

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

El mensaje de error que consigo es

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

¿Por qué? ¿Y cómo soluciono esto? No es posible utilizar el DataContext, ya que este problema se produce en el árbol visual donde DataContext ya contiene los datos reales que se muestran. Ya traté de usar {RelativeSource FindAncestor, ...} en su lugar, pero eso produce un mensaje de error similar.

+0

+1 para la edición con su solución, usted debe hacer una respuesta separada – jan

+0

@jan: Buena idea, hecho. – Heinzi

Respuesta

16

El problema es que no es el ContextMenu en el árbol visual, por lo que, básicamente, tienen que contar el menú de contexto sobre el cual contexto de datos para su uso.

Echa un vistazo a this blogpost con una muy buena solución de Thomas Levesque.

Crea un Proxy de clase que hereda Freezable y declara una propiedad de dependencia de datos.

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

A continuación, se puede declarar en el XAML (en un lugar en el árbol visual donde se conoce la DataContext correcta):

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

y usados ​​en el menú de contexto fuera del árbol visual:

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

Esto __finally__ funcionó después de probar con 10 enfoques diferentes (de SO y de otros lugares). ¡Muchas gracias por esta respuesta limpia y simple, pero tan increíble! :) – Yoda

+0

Esta es la ** mejor solución ** – n00b101

+0

Esa es una muy buena solución. Hago que mis proxies vinculantes estén fuertemente tipados (la propiedad de datos y la propiedad de dependencia no son typeof (objeto) sino typeof (MyViewModel). De esta manera hay mejor intellisense donde tengo que enlazar a través del proxy. – Michael

6

Vea el artículo this de Justin Taylor para una solución alternativa.

actualización
Lamentablemente, el blog de referencia sin más está disponible. Intenté explicar el procedimiento en otra respuesta SO. Se puede encontrar here.

+0

Publiqué la publicación de blog faltante como otra respuesta. – mydogisbox

+0

@mydogisbox +1 ¡perfecto! – HCL

4

Basado en HCLs answer, esto es lo que terminé usando:

<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

¿Esto realmente funciona? He intentado que esto funcione, y al usar Snoop parece que el comando se evalúa una vez y nunca se actualiza. PlacementTarget es nulo hasta que el menú contextual esté realmente activado, momento en el que Parent.PlacementTarget.Tag es válido pero el comando nunca se actualiza dinámicamente (según lo que puedo ver en Snoop) – nrjohnstone

+0

esto es lo único que funciona para mí y para mí He intentado como 10-15 sugerencias de todo este sitio. –

13

Viva web.archive.org! Aquí es 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

Por extraño que parezca, he establecido 'DataContext' del' MenuItem' y no funciona. Tan pronto como lo cambié para establecerlo en 'ContextMenu' como lo describió, comenzó a funcionar. Gracias por publicar esto. –

7

descubrí que no estaba funcionando para mí debido a la opción de menú está anidado, lo que significaba que tenía que atravesar por un "padre" extra para encontrar la PlacementTarget.

Una mejor manera es encontrar el ContextMenu en sí mismo como el RelativeSource y luego simplemente vincular al destino de colocación de eso. Además, dado que la etiqueta es la ventana en sí, y su comando está en el modelo de vista, también necesita tener el DataContext configurado.

que terminó con algo como esto

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

Lo que esto significa es que si usted termina con un menú de contexto complicado, con submenús etc .. no es necesario seguir sumando "Padre" a cada Comandos de niveles.

- EDITAR -

también ocurrió con esta alternativa para fijar una etiqueta en cada ListBoxItem que se une a la ventana/Usercontrol. Terminé haciendo esto porque cada ListBoxItem estaba representado por su propio ViewModel, pero necesitaba los comandos de menú para ejecutarlos a través del ViewModel de nivel superior para el control, pero pasé su lista ViewModel como parámetro.

<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

Si (como yo) tiene una aversión a las expresiones de enlace complejo feo, aquí hay una solución simple de código subyacente a este problema. Este enfoque aún le permite mantener declaraciones de comando limpias en su XAML.

XAML:

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

Código atrás:

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; 
     } 
    } 
} 
Cuestiones relacionadas