Pregunta

Tengo un problema al vincular un comando en un menú contextual en un control de usuario que está en una página de pestaña. La primera vez que uso el menú (clic derecho en la pestaña) funciona muy bien, pero si cambio de pestaña, el comando usará la instancia de databound que se usó la primera vez.

Si pongo un botón que está vinculado al comando en el control del usuario, funciona como se espera ...

¿Alguien puede decirme qué 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>

CompanyViewModel.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; }
}
¿Fue útil?

Solución

La clave para recordar aquí es que los menús contextuales 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}}}"
/>

Otros consejos

La forma más limpia que he encontrado para vincular comandos a elementos del menú contextual implica usar una clase llamada CommandReference. Puede encontrarlo en el kit de herramientas MVVM en Codeplex en WPF Futures .

El XAML podría verse así:

<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 ViewModel. 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 de trabajo y se simplificó para ilustración. Puede haber errores tipográficos u otros errores menores.

Recientemente tuve el mismo problema con un ContextMenu ubicado en un ListBox. Traté de vincular un comando a la manera MVVM sin ningún código subyacente. Finalmente me di por vencido y le pedí ayuda a un amigo. Encontró una solución ligeramente retorcida pero concisa. Está pasando 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.

Publiqué una muestra completamente funcional en Github . Aquí hay un extracto de 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>

Prefiero otra solución. Agregar evento de cargador de menú contextual.

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

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

Encontré este método usando la propiedad Tag muy útil cuando se vincula desde un menú contextual en lo profundo de una plantilla de control:

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

Esto permite enlazar a cualquier contexto de datos disponible para el control desde el que se abrió el menú contextual. El menú contextual puede acceder al control seleccionado a través de & Quot; PlacementTarget & Quot ;. Si la propiedad Tag del control seleccionado está vinculada a un contexto de datos deseado, se vincula a & Quot; PlacementTarget.Tag & Quot; desde el interior del menú contextual lo lanzará directamente a ese contexto de datos.

Sé que esto ya es una publicación antigua, pero me gustaría agregar otra solución para aquellos que buscan diferentes formas de hacerlo.

No pude hacer la misma solución para trabajar 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 para mi modelo. Como estaba usando un activador de eventos, el objeto PlacementTarget era nulo.

Esta es la solución que encontré para que funcione solo usando 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>
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top