WPF: привязка ContextMenu к команде MVVM
Допустим, у меня есть окно со свойством, возвращающим команду (на самом деле это UserControl с командой в классе ViewModel, но давайте сделаем все как можно проще, чтобы воспроизвести проблему).
Следующие работы:
<Window x:Class="Window1" ... x:Name="myWindow">
<Menu>
<MenuItem Command="{Binding MyCommand, ElementName=myWindow}" Header="Test" />
</Menu>
</Window>
Но следующее не работает.
<Window x:Class="Window1" ... x:Name="myWindow">
<Grid>
<Grid.ContextMenu>
<ContextMenu>
<MenuItem Command="{Binding MyCommand, ElementName=myWindow}" Header="Test" />
</ContextMenu>
</Grid.ContextMenu>
</Grid>
</Window>
Я получаю сообщение об ошибке:
System.Windows.Data Ошибка: 4: не удается найти источник для привязки со ссылкой "ElementName=myWindow". BindingExpression:Path= МояКоманда; DataItem= NULL; Целевым элементом является "MenuItem" (Name=''); Свойство target - "Command" (тип "ICommand")
Зачем? И как мне это исправить? С использованием DataContext
это не вариант, так как эта проблема возникает далеко вниз по визуальному дереву, где DataContext уже содержит отображаемые фактические данные. Я уже пытался использовать {RelativeSource FindAncestor, ...}
вместо этого, но это приводит к аналогичному сообщению об ошибке.
6 ответов
Проблема в том, что ContextMenu находится не в визуальном дереве, поэтому вам в основном нужно сообщить контекстному меню, какой контекст данных использовать.
Проверьте этот пост с очень хорошим решением Томаса Левеска.
Он создает класс Proxy, который наследует Freezable и объявляет свойство зависимостей Data.
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));
}
Затем он может быть объявлен в XAML (в месте в визуальном дереве, где известен правильный DataContext):
<Grid.Resources>
<local:BindingProxy x:Key="Proxy" Data="{Binding}" />
</Grid.Resources>
И используется в контекстном меню за пределами визуального дерева:
<ContextMenu>
<MenuItem Header="Test" Command="{Binding Source={StaticResource Proxy}, Path=Data.MyCommand}"/>
</ContextMenu>
Ура для http://web.archive.org/! Вот недостающее сообщение в блоге:
Привязка к MenuItem в контекстном меню WPF
Среда, 29 октября 2008 г. - jtango18
Поскольку Context Menu в WPF не существует внутри визуального дерева вашей страницы / окна / элемента управления как такового, привязка данных может быть немного сложнее. Я искал в Интернете все выше и ниже, и наиболее распространенный ответ, похоже, "просто сделайте это в коде позади". НЕПРАВИЛЬНО! Я не вошел в удивительный мир XAML, чтобы вернуться к тому, чтобы что-то делать в коде.
Вот мой пример, который позволит вам связать строку, которая существует как свойство вашего окна.
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>
Важной частью является тег на кнопке (хотя вы также можете легко установить DataContext для кнопки). Здесь хранится ссылка на родительское окно. Context Menu может получить к нему доступ через свойство PlacementTarget. Затем вы можете передать этот контекст через ваши пункты меню.
Я признаю, что это не самое элегантное решение в мире. Тем не менее, это лучше, чем установка кода в коде позади. Если у кого-то есть еще лучший способ сделать это, я бы хотел это услышать.
Я обнаружил, что это не работает для меня из-за вложенности пункта меню, что означает, что мне пришлось перебрать лишнего "Родителя", чтобы найти PlacementTarget.
Лучший способ - найти сам ContextMenu как RelativeSource, а затем просто привязать его к цели размещения. Кроме того, поскольку тег является самим окном, а ваша команда находится в модели представления, вам также нужно установить DataContext.
Я закончил с чем-то вроде этого
<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>
Это означает, что если у вас получится сложное контекстное меню с подменю и т. Д., Вам не нужно добавлять "родительский элемент" для команд каждого уровня.
-- РЕДАКТИРОВАТЬ --
Также предложили эту альтернативу, чтобы установить тег для каждого ListBoxItem, который привязывается к Window/Usercontrol. Я закончил тем, что сделал это, потому что каждый ListBoxItem был представлен их собственной ViewModel, но мне нужно было, чтобы команды меню выполнялись через ViewModel верхнего уровня для элемента управления, но передавали их ViewModel списка в качестве параметра.
<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>
Исходя из ответа HCL, вот что я в итоге использовал:
<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>
Ответ в 2020 году:
Я оставляю этот ответ здесь для всех, кто задавал этот вопрос в Google, так как это первый результат поиска, который появляется. Это сработало для меня и проще, чем другие предлагаемые решения:
<MenuItem Command="{Binding YourCommand}" CommandTarget="{Binding Path=PlacementTarget, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
Как описано здесь:
Если (как и я) у вас есть отвращение к уродливым сложным выражениям привязки, вот простое решение этой проблемы. Этот подход все еще позволяет вам сохранять чистые объявления команд в вашем XAML.
XAML:
<ContextMenu ContextMenuOpening="ContextMenu_ContextMenuOpening">
<MenuItem Command="Save"/>
<Separator></Separator>
<MenuItem Command="Close"/>
...
Код позади:
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;
}
}
}