2009-03-19 20 views
17

Tengo un problema al vincular un comando en un menú contextual en un control de usuario que está en una página de pestañas. La primera vez que uso el menú (haga clic con el botón derecho en la pestaña) funciona muy bien, pero si cambio de pestaña, el comando utilizará la instancia de conexión de datos que se utilizó la primera vez.El menú de contexto de WPF no se enlaza al elemento de datos correcto

Si pongo un botón que está unido a la orden en el control de usuario funciona como se esperaba ...

¿Puede alguien decirme lo que estoy haciendo mal ??

Este es un proyecto de prueba que expone el problema:

App.xaml.cs:

public partial class App : Application 
{ 
    protected override void OnStartup(StartupEventArgs e) 
    { 
     base.OnStartup(e); 

     CompanyViewModel model = new CompanyViewModel(); 
     Window1 window = new Window1(); 
     window.DataContext = model; 
     window.Show(); 
    } 
} 

Window1.xaml:

<Window x:Class="WpfApplication1.Window1" 
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:vw="clr-namespace:WpfApplication1" 
Title="Window1" Height="300" Width="300"> 

    <Window.Resources> 
    <DataTemplate x:Key="HeaderTemplate"> 
     <StackPanel Orientation="Horizontal"> 
      <TextBlock Text="{Binding Path=Name}" /> 
     </StackPanel> 
    </DataTemplate> 
    <DataTemplate DataType="{x:Type vw:PersonViewModel}"> 
     <vw:UserControl1/> 
    </DataTemplate> 

</Window.Resources> 
<Grid> 
    <TabControl ItemsSource="{Binding Path=Persons}" 
       ItemTemplate="{StaticResource HeaderTemplate}" 
       IsSynchronizedWithCurrentItem="True" /> 
</Grid> 
</Window> 

UserControl1.xaml:

<UserControl x:Class="WpfApplication1.UserControl1" 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    MinWidth="200"> 
    <UserControl.ContextMenu> 
     <ContextMenu > 
      <MenuItem Header="Change" Command="{Binding Path=ChangeCommand}"/> 
     </ContextMenu> 
    </UserControl.ContextMenu> 
    <Grid> 
     <Grid.ColumnDefinitions> 
      <ColumnDefinition Width="100" /> 
      <ColumnDefinition Width="*" /> 
     </Grid.ColumnDefinitions> 
     <Label Grid.Column="0">The name:</Label> 
     <TextBox Grid.Column="1" Text="{Binding Path=Name, UpdateSourceTrigger=PropertyChanged}" /> 
    </Grid> 
</UserControl> 

Compa nyViewModel.cs:

public class CompanyViewModel 
{ 
    public ObservableCollection<PersonViewModel> Persons { get; set; } 
    public CompanyViewModel() 
    { 
     Persons = new ObservableCollection<PersonViewModel>(); 
     Persons.Add(new PersonViewModel(new Person { Name = "Kalle" })); 
     Persons.Add(new PersonViewModel(new Person { Name = "Nisse" })); 
     Persons.Add(new PersonViewModel(new Person { Name = "Jocke" })); 
    } 
} 

PersonViewModel.cs:

public class PersonViewModel : INotifyPropertyChanged 
{ 
    Person _person; 
    TestCommand _testCommand; 

    public PersonViewModel(Person person) 
    { 
     _person = person; 
     _testCommand = new TestCommand(this); 
    } 
    public ICommand ChangeCommand 
    { 
     get 
     { 
      return _testCommand; 
     } 
    } 
    public string Name 
    { 
     get 
     { 
      return _person.Name; 
     } 
     set 
     { 
      if (value == _person.Name) 
       return; 
      _person.Name = value; 
      OnPropertyChanged("Name"); 
     } 
    } 
    void OnPropertyChanged(string propertyName) 
    { 
     PropertyChangedEventHandler handler = this.PropertyChanged; 
     if (handler != null) 
     { 
      var e = new PropertyChangedEventArgs(propertyName); 
      handler(this, e); 
     } 
    } 
    public event PropertyChangedEventHandler PropertyChanged; 
} 

TestCommand.cs:

public class TestCommand : ICommand 
{ 
    PersonViewModel _person; 
    public event EventHandler CanExecuteChanged; 

    public TestCommand(PersonViewModel person) 
    { 
     _person = person; 
    } 
    public bool CanExecute(object parameter) 
    { 
     return true; 
    } 
    public void Execute(object parameter) 
    { 
     _person.Name = "Changed by command"; 
    } 
} 

Person.cs:

public class Person 
{ 
    public string Name { get; set; } 
} 

Respuesta

22

La clave es recordar aquí es context me nus no son parte del árbol visual.

Por lo tanto, no heredan la misma fuente que el control al que pertenecen para el enlace. La forma de lidiar con esto es vincularse al objetivo de ubicación del ContextMenu mismo.

<MenuItem Header="Change" Command="{Binding 
    Path=PlacementTarget.ChangeCommand, 
    RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContextMenu}}}" 
/> 
+0

Hola Cameron. ¿Cree que su técnica aquí está de alguna manera relacionada con el problema que he descrito aquí: http://stackoverflow.com/questions/833607/wpf-why-do-contextmenu-items-work-for-listbox-but-not- control de elementos ... No estoy vinculando a un comando, pero tengo la sospecha de que es un problema relacionado. –

+2

No estoy convencido por esta respuesta. Los enlaces de comando SI funcionan para el elemento de menú (sabe que tiene que vincular el modelo de vista) ... el problema es que los elementos de menú no se vuelven a vincular cuando el contexto de datos cambia debido a la pestaña de conmutación. Si se debe a que no forman parte del árbol visual, ¿cómo es que funciona por primera vez? – Schneider

+0

@Schneider: No dije que los enlaces en un menú no funcionaran, solo que no heredan el contexto de datos de sus padres como era de esperar. Diría que el motor de enlace de WPF establece el contexto cuando el menú se abre por primera vez y luego no lo actualiza cuando cambia la pestaña. –

8

La manera más limpia que he encontrado que se unen a los comandos de elementos del menú contextual implica el uso de una clase llamada CommandReference. Puede encontrarlo en el kit de herramientas de MVVM en Codeplex al WPF Futures.

El XAML podría tener este aspecto:

<UserControl x:Class="View.MyView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
       xmlns:vm="clr-namespace:ViewModel;assembly=MyViewModel" 
       xmlns:mvvm="clr-namespace:ViewModelHelper;assembly=ViewModelHelper" 
      <UserControl.Resources> 
       <mvvm:CommandReference x:Key="MyCustomCommandReference" Command="{Binding MyCustomCommand}" /> 

       <ContextMenu x:Key="ItemContextMenu"> 
        <MenuItem Header="Plate"> 
         <MenuItem Header="Inspect Now" Command="{StaticResource MyCustomCommandReference}" 
           CommandParameter="{Binding}"> 
         </MenuItem> 
        </MenuItem> 
       </ContextMenu> 
    </UserControl.Resources> 

MyCustomCommand es un RelayCommand en el modelo de vista. En este ejemplo, ViewModel se adjuntó al contexto de datos de la vista en el código subyacente.

Nota: este XAML se copió de un proyecto en funcionamiento y se simplificó con fines ilustrativos. Puede haber errores tipográficos u otros errores menores.

+1

¿Has probado esto con un RelayCommand con un delegado CanExecute, CyberMonk? Descubrí que CommandReference pasa el parámetro null al parámetro CanExecute, aunque el método Execute obtiene el valor correcto. Me está impidiendo usarlo ahora mismo. –

+0

OK, esto puede funcionar pero ¿alguien puede explicar por qué es necesario? ¿Por qué los enlaces en ContextMenus solo se ejecutan una vez? – Schneider

+0

Puedo verificar que esto funcione ... las explicaciones son bienvenidas :) – Schneider

0

me encontré con este método usando la propiedad Tag muy útil cuando se une a un menú contextual en el interior de una plantilla de control:

http://blog.jtango.net/binding-to-a-menuitem-in-a-wpf-context-menu

Esto hace que sea posible unir a cualquier contexto de datos disponible para el control desde el que se abrió el menú contextual. El menú contextual puede acceder al control cliqueado a través de "PlacementTarget". Si la propiedad Tag del control cliqueado está vinculada a un contexto de datos deseado, el enlace a "PlacementTarget.Tag" desde el menú contextual lo lanzará directamente a ese contexto de datos.

1

Prefiero otra solución. Agregue el evento de menú contextual del cargador.

<ContextMenu Loaded="ContextMenu_Loaded"> 
    <MenuItem Header="Change" Command="{Binding Path=ChangeCommand}"/> 
</ContextMenu> 

Asigna el contexto de datos dentro del evento.

private void ContextMenu_Loaded(object sender, RoutedEventArgs e) 
{ 
    (sender as ContextMenu).DataContext = this; //assignment can be replaced with desired data context 
} 
4

Tuve el mismo problema recientemente con un ContextMenu ubicado en un ListBox. Intenté vincular un comando al modo MVVM sin código subyacente. Finalmente me rendí y le pedí ayuda a un amigo. Encontró una solución ligeramente retorcida pero concisa. Pasa el ListBox en el DataContext del ContextMenu y luego encuentra el comando en el modelo de vista accediendo al DataContext del ListBox. Esta es la solución más simple que he visto hasta ahora. Sin código personalizado, sin etiqueta, solo XAML puro y MVVM.

He publicado una muestra totalmente funcional en Github. Aquí hay un extracto del XAML.

<Window x:Class="WpfListContextMenu.MainWindow" 
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
     Title="MainWindow" Height="350" Width="268"> 
    <Grid> 
    <DockPanel> 
     <ListBox x:Name="listBox" DockPanel.Dock="Top" ItemsSource="{Binding Items}" DisplayMemberPath="Name" 
       SelectionMode="Extended"> 
     <ListBox.ContextMenu> 
      <ContextMenu DataContext="{Binding Path=PlacementTarget, RelativeSource={RelativeSource Self}}"> 
      <MenuItem Header="Show Selected" Command="{Binding Path=DataContext.ShowSelectedCommand}" 
         CommandParameter="{Binding Path=SelectedItems}" /> 
      </ContextMenu> 
     </ListBox.ContextMenu> 
     </ListBox> 
    </DockPanel> 
    </Grid> 
</Window> 
0

sé que esto ya es una entrada antigua, pero me gustaría añadir otra solución para aquellos que están buscando diferentes maneras de hacerlo.

No pude hacer la misma solución en mi caso, ya que estaba tratando de hacer otra cosa: abrir el menú contextual con un clic del mouse (como una barra de herramientas con un submenú adjunto) y también vincular comandos a mi modelo. Como estaba utilizando un desencadenador de eventos, el objeto PlacementTarget era nulo.

Esta es la solución que encontré para hacer que funcione sólo con XAML:

<!-- This is an example with a button, but could be other control --> 
<Button> 
    <...> 

    <!-- This opens the context menu and binds the data context to it --> 
    <Button.Triggers> 
    <EventTrigger RoutedEvent="Button.Click"> 
     <EventTrigger.Actions> 
     <BeginStoryboard> 
      <Storyboard> 
      <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="ContextMenu.DataContext"> 
       <DiscreteObjectKeyFrame KeyTime="0:0:0" Value="{Binding}"/> 
      </ObjectAnimationUsingKeyFrames> 
      <BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="ContextMenu.IsOpen"> 
       <DiscreteBooleanKeyFrame KeyTime="0:0:0" Value="True"/> 
      </BooleanAnimationUsingKeyFrames> 
      </Storyboard> 
     </BeginStoryboard> 
     </EventTrigger.Actions> 
    </EventTrigger> 
    </Button.Triggers> 

    <!-- Here it goes the context menu --> 
    <Button.ContextMenu> 
    <ContextMenu> 
     <MenuItem Header="Item 1" Command="{Binding MyCommand1}"/> 
     <MenuItem Header="Item 2" Command="{Binding MyCommand2}"/> 
    </ContextMenu> 
    </Button.ContextMenu> 

</Button> 
Cuestiones relacionadas