Как я могу привязать ObservableCollection ViewModels к MenuItem?

Когда я привязываю элементы меню к ObservableCollection, кликабельна только «внутренняя» область MenuItem:

http://tanguay.info/web/external/mvvmMenuItems.png

В моем представлении есть это меню:

<Menu>
    <MenuItem 
        Header="Options" ItemsSource="{Binding ManageMenuPageItemViewModels}"
              ItemTemplate="{StaticResource MainMenuTemplate}"/>
</Menu>

Затем я связываю его с помощью этого DataTemplate:

<DataTemplate x:Key="MainMenuTemplate">
    <MenuItem
        Header="{Binding Title}" 
        Command="{Binding DataContext.SwitchPageCommand,
        RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Menu}}}" 
        Background="Red"
        CommandParameter="{Binding IdCode}"/>
</DataTemplate>

Поскольку каждая ViewModel в ObservableCollection ManageMenuPageItemViewModels имеет свойства Title и IdCode, приведенный выше код с первого взгляда работает нормально.

ОДНАКО проблема в том, что MenuItem в DataTemplate на самом деле внутри другого MenuItem (как если бы он привязывается дважды), так что в приведенном выше шаблоне данных с Background = "Red" есть красное поле внутри каждого пункта меню , и можно щелкнуть только эту область, а не всю область элемента меню (например, если пользователь щелкает область, где стоит галочка, или справа или слева от внутренней интерактивной области, то ничего не происходит, что, если вы не иметь отдельного цвета очень сбивает с толку.)

Как правильно привязать MenuItems к ObservableCollection ViewModels, чтобы можно было нажимать на всю область внутри каждого MenuItem?

ОБНОВИТЬ:

Итак, я внес следующие изменения, основываясь на приведенном ниже совете, и теперь у меня есть следующее:

http://tanguay.info/web/external/mvvmMenuItemsYellow.png

У меня есть только TextBlock внутри моего DataTemplate, но я все еще не могу «раскрасить весь MenuItem», а только TextBlock:

<DataTemplate x:Key="MainMenuTemplate">
    <TextBlock Text="{Binding Title}"/>
</DataTemplate>

И я поместил привязку Command в Menu.ItemContainerStyle, но теперь они не срабатывают:

<Menu DockPanel.Dock="Top">
    <Menu.ItemContainerStyle>
        <Style TargetType="MenuItem">
            <Setter Property="Background" Value="Yellow"/>
            <Setter Property="Command" Value="{Binding DataContext.SwitchPageCommand,
        RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Menu}}}"/>
            <Setter Property="CommandParameter" Value="{Binding IdCode}"/>
        </Style>
    </Menu.ItemContainerStyle>
    <MenuItem 
        Header="MVVM" ItemsSource="{Binding MvvmMenuPageItemViewModels}"
              ItemTemplate="{StaticResource MainMenuTemplate}"/>
    <MenuItem 
        Header="Application" ItemsSource="{Binding ApplicationMenuPageItemViewModels}"
              ItemTemplate="{StaticResource MainMenuTemplate}"/>
    <MenuItem 
        Header="Manage" ItemsSource="{Binding ManageMenuPageItemViewModels}"
              ItemTemplate="{StaticResource MainMenuTemplate}"/>
</Menu>

person Edward Tanguay    schedule 01.07.2009    source источник


Ответы (4)


Я обнаружил, что использовать MVVM с MenuItems очень сложно. Остальная часть моего приложения использует DataTemplates для сопряжения View с ViewModel, но это, похоже, не работает с Menus именно по причинам, которые вы описали. Вот как я в итоге решил это. Мой вид выглядит так:

<DockPanel>
<Menu DockPanel.Dock="Top" ItemsSource="{Binding Path=(local:MainViewModel.MainMenu)}">
    <Menu.ItemContainerStyle>
        <Style>
            <Setter Property="MenuItem.Header" Value="{Binding Path=(contracts:IMenuItem.Header)}"/>
            <Setter Property="MenuItem.ItemsSource" Value="{Binding Path=(contracts:IMenuItem.Items)}"/>
            <Setter Property="MenuItem.Icon" Value="{Binding Path=(contracts:IMenuItem.Icon)}"/>
            <Setter Property="MenuItem.IsCheckable" Value="{Binding Path=(contracts:IMenuItem.IsCheckable)}"/>
            <Setter Property="MenuItem.IsChecked" Value="{Binding Path=(contracts:IMenuItem.IsChecked)}"/>
            <Setter Property="MenuItem.Command" Value="{Binding}"/>
            <Setter Property="MenuItem.Visibility" Value="{Binding Path=(contracts:IMenuItem.Visible), 
                Converter={StaticResource BooleanToVisibilityConverter}}"/>
            <Setter Property="MenuItem.ToolTip" Value="{Binding Path=(contracts:IMenuItem.ToolTip)}"/>
            <Style.Triggers>
                <DataTrigger Binding="{Binding Path=(contracts:IMenuItem.IsSeparator)}" Value="true">
                    <Setter Property="MenuItem.Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="{x:Type MenuItem}">
                                <Separator Style="{DynamicResource {x:Static MenuItem.SeparatorStyleKey}}"/>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </Menu.ItemContainerStyle>
</Menu>
</DockPanel>

Если вы заметили, я определил интерфейс под названием IMenuItem, который является ViewModel для MenuItem. Вот код для этого:

public interface IMenuItem : ICommand
{
    string Header { get; }
    IEnumerable<IMenuItem> Items { get; }
    object Icon { get; }
    bool IsCheckable { get; }
    bool IsChecked { get; set; }
    bool Visible { get; }
    bool IsSeparator { get; }
    string ToolTip { get; }
}

Обратите внимание, что IMenuItem определяет элементы IEnumerable, поэтому вы получаете подменю. Кроме того, IsSeparator - это способ определения разделителей в меню (еще один сложный маленький трюк). Вы можете увидеть в xaml, как он использует DataTrigger для изменения стиля на существующий стиль разделителя, если IsSeparator истинно. Вот как MainViewModel определяет свойство MainMenu (к которому привязывается представление):

public IEnumerable<IMenuItem> MainMenu { get; set; }

Кажется, это хорошо работает. Я предполагаю, что вы могли бы использовать ObservableCollection для MainMenu. На самом деле я использую MEF для составления меню из частей, но после этого сами элементы статичны (хотя свойства каждого элемента меню не являются). Я также использую класс AbstractMenuItem, который реализует IMenuItem и является вспомогательным классом для создания экземпляров пунктов меню в различных частях.

ОБНОВЛЕНИЕ:

Что касается вашей проблемы с цветом, помогает ли эта ветка?

person Scott Whitlock    schedule 01.07.2009
comment
+1 - очень хорошо для полного примера с разделителями и всем остальным. - person Matt DeKrey; 16.12.2010
comment
У меня очень похожий дизайн и все работает, кроме показа разделителя. Если я изменю шаблон на ‹Label› Separator ‹/Label›, то я увижу разделитель там, где должен быть разделитель. Но когда я пытаюсь использовать шаблон, как в вашем ответе, ничего не отображается. Я попробовал ‹Separator BorderThickness = 100 BorderBrush = Black› ‹/Separator›, и тогда разделитель стал видимым, но мне нужен стиль по умолчанию с шириной, динамически устанавливаемой шириной меню. Должен ли я где-то определять SeparatorStyleKey? Я искал в Интернете, но не нашел ничего, что помогло бы ... Спасибо! - person Dina; 18.01.2015

Не вставляйте MenuItem в DataTemplate. DataTemplate определяет содержимое MenuItem. Вместо этого укажите посторонние свойства для MenuItem через ItemContainerStyle:

<Menu>
    <Menu.ItemContainerStyle>
        <Style TargetType="MenuItem">
            <Setter Property="Header" Value="{Binding Title}"/>
            ...
        </Style>
    </Menu.ItemContainerStyle>
    <MenuItem 
        Header="Options" ItemsSource="{Binding ManageMenuPageItemViewModels}"
              ItemTemplate="{StaticResource MainMenuTemplate}"/>
</Menu>

Также обратите внимание на HierarchicalDataTemplates.

person Kent Boogaart    schedule 01.07.2009
comment
вы имеете в виду определить заголовок / цвет в Menu.ItemContainerStyle, а затем внутри DataTemplate поместить HierarchicalDataTemplate, который определяет Command и CommandParameter? - person Edward Tanguay; 01.07.2009
comment
Спасибо, это именно то, что я искал. Работает отлично. Спасибо! - person Judah Gabriel Himango; 05.05.2010
comment
+1 - HierarchicalDataTemplates делают эту проблему почти тривиальной. - person Matt Jordan; 25.12.2010

Вот как я делал свои меню. Возможно, это не совсем то, что вам нужно, но я думаю, что это довольно близко.

  <Style x:Key="SubmenuItemStyle" TargetType="MenuItem">
    <Setter Property="Header" Value="{Binding MenuName}"></Setter>
    <Setter Property="Command" Value="{Binding Path=MenuCommand}"/>
    <Setter Property="ItemsSource" Value="{Binding SubmenuItems}"></Setter>
  </Style>

  <DataTemplate DataType="{x:Type systemVM:TopMenuViewModel}" >
    <Menu>
      <MenuItem Header="{Binding MenuName}"         
                    ItemsSource="{Binding SubmenuItems}" 
                    ItemContainerStyle="{DynamicResource SubmenuItemStyle}" />
    </Menu>
  </DataTemplate>

    <Menu DockPanel.Dock="Top" ItemsSource="{Binding Menus}" />

TopMenuViewModel - это набор меню, которые будут отображаться в строке меню. Каждый из них содержит MenuName, которое будет отображаться, и коллекцию SubMenuItems, которую я установил как ItemsSource.

Я контролирую способ отображения SubMenuItems с помощью стиля SumMenuItemStyle. Каждый SubMenuItem имеет собственное свойство MenuName, свойство Command типа ICommand и, возможно, другую коллекцию SubMenuItems.

В результате я могу хранить всю информацию о меню в базе данных и динамически переключать, какие меню отображаются во время выполнения. Вся область элемента меню активна и отображается правильно.

Надеюсь это поможет.

person RB Davidson    schedule 01.07.2009

Просто сделайте свой DataTemplate TextBlock (или, возможно, панелью стека со значком и TextBlock).

person Ana Betts    schedule 02.07.2009
comment
хорошо, это здорово, это работает (я думал, что пробовал это), но теперь мне нужно как-то подключить команду к TextBlock, у нее нет атрибута Command, я не могу использовать свою DelegateCommand, что вы использовали AttachedBehaviors или что-то другое? - person Edward Tanguay; 02.07.2009
comment
отредактировал соответственно выше, выложил скриншот, все еще не работает :-( - person Edward Tanguay; 02.07.2009
comment
Хммм, возможно, вам придется привязать его к тегу TextBlock, затем написать обработчик OnApplyTemplate или что-то в этом роде и пройти дерево обратно до MenuItem. Супер хакерский и не-WPF'ы, но иногда приходится. - person Ana Betts; 02.07.2009
comment
Спасибо, редактирование ItemContainerStyle у меня сработало. (Использовал пример Кента Бугарта.) - person Judah Gabriel Himango; 05.05.2010