Контекстное меню WPF не привязывается к правому элементу привязки данных

У меня проблема при связывании команды в контекстном меню в элементе управления, который находится на странице вкладки. В первый раз, когда я использую меню (щелкните вкладку правой кнопкой мыши), оно отлично работает, но если я переключу вкладку, команда будет использовать экземпляр привязки данных, который использовался в первый раз.

Если я помещаю кнопку, привязанную к команде в пользовательском элементе управления, она работает, как ожидалось ...

Может кто-нибудь подскажет, что я делаю не так ??

Это тестовый проект, который выявляет проблему:

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

person Community    schedule 19.03.2009    source источник


Ответы (7)


Здесь важно помнить, что контекстные меню не являются частью визуального дерева.

Следовательно, они не наследуют тот же источник, что и элемент управления, которому они принадлежат для привязки. Способ справиться с этим - привязать к цели размещения самого ContextMenu.

<MenuItem Header="Change" Command="{Binding 
    Path=PlacementTarget.ChangeCommand, 
    RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContextMenu}}}"
/>
person Cameron MacFarland    schedule 19.03.2009
comment
Привет, Кэмерон. Как вы думаете, ваша техника здесь как-то связана с проблемой, которую я описал здесь: stackoverflow.com/questions/833607/ ... Я не привязываюсь к команде, но у меня есть подозрение, что это родственная проблема. - person Drew Noakes; 07.05.2009
comment
Меня этот ответ не убеждает. Связывание команд ДЕЙСТВИТЕЛЬНО работает для элемента меню (он знает, что должен связывать модель представления) ... проблема в том, что элементы меню не связываются повторно, когда текст данных изменяется из-за переключения вкладки. Если это из-за того, что они не являются частью визуального дерева, почему это работает с первого раза? - person Jack Ukleja; 22.02.2010
comment
@Schneider: Я не говорил, что привязки в меню не работают, просто они не наследуют свой текст данных от своего родителя, как вы ожидаете. Я бы сказал, что механизм привязки WPF устанавливает контекст при первом открытии меню, а затем не обновляет его при изменении вкладки. - person Cameron MacFarland; 22.02.2010
comment
Что чертовски раздражает, не интуитивно понятно и нигде не задокументировано! :) Как вы говорите, это должен быть особый случай привязки, когда создается контекстное меню и дальше ничего ... - person Jack Ukleja; 23.02.2010
comment
@Schnieder: Добро пожаловать в WPF! : D - person Cameron MacFarland; 23.02.2010
comment
Я все равно хотел бы найти исчерпывающее объяснение этого от команды WPF и т. Д. Могу я предложить вам вставить абзац в свой ответ, объясняющий, что ПОТОМУ ЧТО его нет в визуальном дереве, контексте данных и, следовательно, привязки не обновляются, когда содержимое в презентация контента изменяется из-за выбора вкладки (при условии, что это является причиной проблемы) - person Jack Ukleja; 23.02.2010
comment
WPF 4.0: ‹ContextMenu DataContext = {Binding PlacementTarget.DataContext, RelativeSource = {RelativeSource Self}}› (намного короче). - person JoanComasFdz; 17.04.2012
comment
@JoanComasFdz: Это делает решение намного более аккуратным. Означает, что вам не нужно настраивать каждую привязку команд MenuItem. Спасибо! - person quarkonium; 28.06.2013

Самый чистый способ привязки команд к пунктам контекстного меню, который я нашел, включает использование класса CommandReference. Вы можете найти его в наборе инструментов MVVM на Codeplex по адресу WPF Futures.

XAML может выглядеть так:

<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 - это RelayCommand на ViewModel. В этом примере ViewModel был прикреплен к тексту данных представления в коде программной части.

Примечание: этот XAML был скопирован из рабочего проекта и упрощен для иллюстрации. Возможны опечатки или другие мелкие ошибки.

person CyberMonk    schedule 28.01.2010
comment
Вы пробовали это с RelayCommand с делегатом CanExecute, CyberMonk? Я обнаружил, что CommandReference получает значение null для параметра CanExecute, хотя методу Execute передается правильное значение. Это мешает мне использовать его прямо сейчас. - person Matt Hamilton; 12.02.2010
comment
Хорошо, это может сработать, но может ли кто-нибудь объяснить, зачем это нужно? Почему привязки к ContextMenus запускаются только один раз? - person Jack Ukleja; 22.02.2010
comment
Спасибо. Привязка CommandParameter может иметь вид {Binding PlacementTarget.DataContext, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}; поскольку MenuItem имеет нулевой DataContext. - person alexei; 07.06.2012
comment
Также обратите внимание на проблему с преждевременным сбором обработчиков в реализации CommandReference (здесь и здесь). - person alexei; 07.06.2012

Недавно у меня была такая же проблема с ContextMenu, расположенным в ListBox. Я попытался связать команду способом MVVM без какого-либо кода программной части. В конце концов я сдался и попросил друга о помощи. Он нашел немного запутанное, но краткое решение. Он передает ListBox в DataContext ContextMenu, а затем находит команду в модели представления, обращаясь к DataContext ListBox. Это самое простое решение, которое я видел до сих пор. Ни пользовательского кода, ни тега, только чистый XAML и MVVM.

Я разместил полностью рабочий образец на Github. Вот выдержка из 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>
person Manuel Laflamme    schedule 03.12.2011

Проверьте: ContextMenuServiceExtensions.DataContext

person Tim Valentine    schedule 21.04.2011

Я предпочитаю другое решение. Добавить событие загрузчика контекстного меню.

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

Назначьте контекст данных внутри события.

private void ContextMenu_Loaded(object sender, RoutedEventArgs e)
{
    (sender as ContextMenu).DataContext = this; //assignment can be replaced with desired data context
}
person Ahmed Yasin Koçulu    schedule 27.11.2011

Я нашел этот метод, использующий свойство Tag, очень полезным при привязке из контекстного меню глубоко внутри шаблона элемента управления:

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

Это позволяет выполнить привязку к любому тексту данных, доступному для элемента управления, из которого было открыто контекстное меню. Контекстное меню может получить доступ к выбранному элементу управления через «PlacementTarget». Если свойство Tag элемента управления, по которому был выполнен щелчок, привязано к желаемому контексту данных, привязка к «PlacementTarget.Tag» из контекстного меню приведет вас непосредственно к этому контексту данных.

person Christian Myksvoll    schedule 26.10.2011
comment
Ссылка мертва :( - person FrankM; 06.04.2020

Я знаю, что это уже старый пост, но я хотел бы добавить еще одно решение для тех, кто ищет разные способы сделать это.

Я не мог заставить такое же решение работать в моем случае, так как я пытался сделать что-то еще: открыть контекстное меню щелчком мыши (точно так же, как панель инструментов с прикрепленным к ней подменю), а также привязать команды к моей модели. Поскольку я использовал триггер события, объект PlacementTarget имел значение NULL.

Это решение, которое я нашел, чтобы заставить его работать только с использованием 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>
person rodrigogq    schedule 14.11.2017