WPF: привязка ContextMenu к команде MVVM - PullRequest
24 голосов
/ 27 августа 2010

Допустим, у меня есть окно со свойством, возвращающим команду (на самом деле это 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 Error: 4: Не удалось найти источник для привязки со ссылкой «ElementName = myWindow».BindingExpression: Path = МояКоманда;DataItem = NULL;Целевым элементом является «MenuItem» (Name = '');Целевым свойством является 'Command' (тип 'ICommand')

Почему?И как мне это исправить?Использование DataContext не вариант, так как эта проблема возникает далеко вниз по визуальному дереву, где DataContext уже содержит отображаемые фактические данные.Я уже пытался использовать {RelativeSource FindAncestor, ...} вместо этого, но это выдает похожее сообщение об ошибке.

Ответы [ 6 ]

40 голосов
/ 03 сентября 2014

Проблема в том, что 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>
16 голосов
/ 08 августа 2012

Ура для web.archive.org ! Вот недостающее сообщение в блоге :

Привязка к пункту меню в контекстном меню 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. Затем вы можете передать этот контекст вниз через ваши пункты меню.

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

8 голосов
/ 19 февраля 2014

Я обнаружил, что это не работает для меня из-за вложенности пункта меню, что означает, что мне пришлось перебрать дополнительный «Родитель», чтобы найти 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, который привязывается к 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>
6 голосов
/ 27 августа 2010

См. эту статью от Джастина Тейлора для обходного пути.

Обновление
К сожалению, указанный блог больше не доступен. Я попытался объяснить процесс в другом SO-ответе. Его можно найти здесь .

4 голосов
/ 10 мая 2012

Исходя из ответа 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>
2 голосов
/ 06 апреля 2017

Если (как и я) у вас есть отвращение к уродливым сложным выражениям привязки, вот простое решение этой проблемы с выделенным кодом.Этот подход все еще позволяет вам сохранять чистые объявления команд в вашем 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;
        } 
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...