c# - data - WPF: Vinculando un ContextMenu a un comando MVVM
mvvm datatemplate (6)
¡Hurra para web.archive.org ! Aquí está la entrada del blog que falta :
Enlace a un elemento de menú en un menú contextual de WPF
Miércoles 29 de octubre de 2008 - jtango18
Debido a que ContextMenu en WPF no existe dentro del árbol visual de su página / ventana / control per se, el enlace de datos puede ser un poco complicado. He buscado alta y baja en la web para esto, y la respuesta más común parece ser "solo hágalo en el código de atrás". ¡INCORRECTO! No entré al maravilloso mundo de XAML para volver a hacer las cosas en el código que hay detrás.
Aquí está mi ejemplo de que le permitirá enlazar a una cadena que existe como una propiedad de su ventana.
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>
La parte importante es la etiqueta en el botón (aunque puede configurar fácilmente el DataContext del botón). Esto almacena una referencia a la ventana principal. El ContextMenu es capaz de acceder a esto a través de su propiedad PlacementTarget. A continuación, puede pasar este contexto a través de los elementos de su menú.
Admito que esta no es la solución más elegante del mundo. Sin embargo, es mejor que configurar cosas en el código detrás. Si alguien tiene una forma aún mejor de hacerlo, me encantaría escucharlo.
Digamos que tengo una ventana con una propiedad que devuelve un comando (de hecho, es un UserControl con un comando en una clase de ViewModel, pero seamos lo más simples posible para reproducir el problema).
Los siguientes trabajos:
<Window x:Class="Window1" ... x:Name="myWindow">
<Menu>
<MenuItem Command="{Binding MyCommand, ElementName=myWindow}" Header="Test" />
</Menu>
</Window>
Pero lo 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 recibo es
System.Windows.Data Error: 4: No se puede encontrar el origen para el enlace con la referencia ''ElementName = myWindow''. BindingExpression: Path = MyCommand; DataItem = null; el elemento objetivo es ''MenuItem'' (Nombre = ''''); la propiedad de destino es ''Comando'' (tipo ''ICommand'')
¿Por qué? ¿Y cómo soluciono esto? El uso del DataContext
no es una opción, ya que este problema ocurre en el árbol visual donde el DataContext ya contiene los datos reales que se muestran. Ya intenté usar {RelativeSource FindAncestor, ...}
lugar, pero eso produce un mensaje de error similar.
Basándome en la respuesta de HCL , 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>
Descubrí que no funcionaba para mí debido a que el elemento del menú estaba anidado, lo que significa que tuve que buscar un "Padre" adicional para encontrar el Objetivo de ubicación.
Una mejor manera es encontrar el ContextMenu como el RelativeSource y luego enlazarlo al destino de la ubicación. Además, dado que la etiqueta es la ventana en sí misma, y su comando está en el modelo de vista, también debe tener configurado el DataContext.
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 terminas con un menú de contexto complicado con submenús, etc., no necesitas seguir agregando "Padres" a los Comandos de cada nivel.
- EDITAR -
También se me ocurrió esta alternativa para establecer una etiqueta en cada ListBoxItem que se enlaza con Window / Usercontrol. Terminé haciendo esto porque cada ListBoxItem estaba representado por su propio ViewModel pero necesitaba que los comandos del menú se ejecutaran a través del ViewModel de nivel superior para el control, pero pasé la 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>
El problema es que ContextMenu no está en el árbol visual, por lo que básicamente tiene que decirle al menú contextual sobre qué contexto de datos usar.
Echa un vistazo a este 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));
}
Luego se puede declarar en el XAML (en un lugar en el árbol visual donde se conoce el DataContext correcto):
<Grid.Resources>
<local:BindingProxy x:Key="Proxy" Data="{Binding}" />
</Grid.Resources>
Y usado en el menú contextual fuera del árbol visual:
<ContextMenu>
<MenuItem Header="Test" Command="{Binding Source={StaticResource Proxy}, Path=Data.MyCommand}"/>
</ContextMenu>
Si (como yo) tiene una aversión a las expresiones de enlace complejas y feas, aquí hay una solución de código subyacente simple para este problema. Este enfoque aún le permite mantener declaraciones de comandos limpias en su XAML.
XAML:
<ContextMenu ContextMenuOpening="ContextMenu_ContextMenuOpening">
<MenuItem Command="Save"/>
<Separator></Separator>
<MenuItem Command="Close"/>
...
Código detrá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;
}
}
}