Подтвердить что ты не робот

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, ...} вместо этого, но это дает аналогичное сообщение об ошибке.

4b9b3361

Ответ 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;
        } 
    }
}