WPF: привязка ContextMenu к команде MVVM
Скажем, у меня есть Window с возвращающим свойство Command (на самом деле это UserControl с Command в классе 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, ...}
вместо этого, но это дает аналогичное сообщение об ошибке.
Ответы
Ответ 1
Проблема заключается в том, что ContextMenu это не в визуальном дереве, поэтому вы в основном должны сообщать контекстному меню о том, какой контекст данных использовать.
Просмотрите этот blogpost с очень приятным решением Томаса Левеска.
Он создает класс Proxy, который наследует Freezable и объявляет свойство зависимостей данных.
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>
Ответ 2
Ура для web.archive.org! Вот отсутствующий пост в блоге:
Привязка к элементу MenuItem в контекстном меню WPF
Среда, 29 октября 2008 г. - jtango18
Так как ContextMenu в 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 кнопки). В нем содержится ссылка на родительское окно. ContextMenu способен к доступу к этому через его свойство PlacementTarget. Затем вы можете передать этот контекст вниз через пункты меню.
Я признаю, что это не самое элегантное решение в мире. Тем не менее, это превосходит настройку в коде позади. Если у кого есть даже лучший способ сделать это Id люблю его слышать.
Ответ 3
Я узнал, что это не работает для меня из-за того, что элемент меню является вложенным, что означает, что мне пришлось пройти дополнительный "родительский", чтобы найти 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>
Это означает, что если вы закончите сложное контекстное меню с подменю и т.д., вам не нужно добавлять "родительский" к командам каждого уровня.
- EDIT -
Также появилась эта альтернатива для установки тега на каждый элемент ListBoxItem, который привязывается к окну /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>
Ответ 4
См. эту статью от Джастина Тейлора для обходного пути.
Обновление
К сожалению, ссылка на блог больше недоступна. Я попытался объяснить ход в другом SO-ответе. Здесь можно найти .
Ответ 5
На основе ответа HCLs, вот что я в итоге использовал:
<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>
Ответ 6
Если (как и я) у вас есть отвращение к уродливым сложным выражениям привязки, вот простое решение для решения этой проблемы. Этот подход по-прежнему позволяет сохранять чистые декларации команд в вашем 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;
}
}
}