如何将一个 ViewModel 的 ObservableCollection 绑定到一个 MenuItem?

16

当我使用ObservableCollection来绑定菜单项时,只有“内部”区域的菜单项可以被点击:

alt text 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>
由于ObservableCollection中的每个ViewModel都具有Title和IdCode属性,因此上述代码乍一看似乎可以正常工作。 然而,问题在于DataTemplate中的MenuItem实际上是嵌套在另一个MenuItem内部(好像它被绑定了两次),因此在具有Background="Red"的上述DataTemplate中,存在一个红色框在每个菜单项内部,并且只有此区域可以被点击,而不是整个菜单项区域本身(例如,如果用户点击打勾标记所在的区域或内部可点击区域的左侧或右侧,则什么也不会发生,如果没有单独的颜色,这非常令人困惑)。 如何将菜单项绑定到ViewModels的ObservableCollection以使每个MenuItem内部的整个区域都可点击?

更新:

因此,我根据下面的建议做出了以下更改,现在有:

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

我的DataTemplate中只有TextBlock,但我仍然无法"为整个MenuItem着色",而只能为TextBlock着色:

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

我把命令绑定放到了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>
4个回答

37

我发现在使用MVVM与MenuItems一起时非常具有挑战性。我的应用程序的其余部分使用DataTemplates将View与ViewModel配对,但由于您描述的原因,这种方法似乎无法与菜单一起工作。以下是我最终解决该问题的方法。我的View看起来像这样:

<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。下面是该接口的代码:

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 Items,这是获取子菜单的方法。同时,IsSeparator是在菜单中定义分隔符的一种方式(另一个棘手的小技巧)。您可以在xaml中看到它如何使用DataTrigger来更改样式为现有的分隔符样式,如果IsSeparator为true。以下是MainViewModel如何定义MainMenu属性(视图绑定到该属性):

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

这看起来效果很不错。我想你可以使用ObservableCollection用于MainMenu。我实际上正在使用MEF从各个部件组合菜单,但在此之后,项目本身就是静态的(尽管每个菜单项的属性并非如此)。我还使用了一个实现IMenuItem的AbstractMenuItem类,它是一种帮助类,用于在各个部分中实例化菜单项。

更新:

关于您的颜色问题,这个主题是否有所帮助?


1
+1 - 非常好的完整示例,包括分隔符和其他所有内容。 - Matt DeKrey
我有一个非常相似的设计,除了显示分隔符之外,其他都正常。如果我将模板更改为<Label>Separator</Label>,那么我会在应该出现分隔符的地方看到“Separator”。但是当我尝试像您的答案中那样使用模板时,什么也没有显示。我尝试了<Separator BorderThickness="100" BorderBrush="Black"></Separator>,然后分隔符就可见了,但我想要默认样式,宽度由菜单宽度动态设置。我应该在哪里定义SeparatorStyleKey?我在网上搜索过,但找不到任何有用的信息...谢谢! - Dina

14
不要将 MenuItem 放在 DataTemplate 中。DataTemplate 定义了 MenuItem 的内容。相反,通过 ItemContainerStyle 指定 MenuItem 的其他属性。
<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>
此外,还要看一下HierarchicalDataTemplate

你的意思是在Menu.ItemContainerStyle中定义标题/颜色,然后在DataTemplate中放置一个HierarchicalDataTemplate来定义Command和CommandParameter吗? - Edward Tanguay
+1 - HierarchicalDataTemplates使得整个问题几乎变得微不足道。 - Matt Jordan

2

以下是我如何制作菜单的方法。这可能不完全符合您的需求,但我认为它非常接近。

  <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。
我通过SumMenuItemStyle样式控制SubMenuItems的显示方式。每个SubMenuItems都有自己的MenuName属性、ICommand类型的Command属性,以及可能还有另一个SubMenuItems集合。
结果是我能够将所有的菜单信息存储在数据库中,并在运行时动态切换显示哪些菜单。整个菜单项区域都可以点击并正确显示。
希望这可以帮到您。

2

只需将您的数据模板设计成文本块(或者是包含图标和文本块的堆栈面板)。


好的,太棒了,它起作用了(我以为我已经尝试过了),但现在我必须想办法将命令连接到TextBlock上,因为它没有Command属性,所以我不能使用我的DelegateCommand,你是使用AttachedBehaviors还是其他什么东西? - Edward Tanguay
已经根据上面的编辑进行了修改,发布了截图,但仍然无法正常工作 :-( - Edward Tanguay
嗯,你可能需要将其绑定到 TextBlock 的标记上,然后编写一个 OnApplyTemplate 处理程序或其他东西,并向上遍历树到 MenuItem。超级 hacky 和非 WPF'y,但有时你必须这样做。 - Ana Betts
谢谢,编辑ItemContainerStyle对我很有帮助。(使用了Kent Boogaart的例子。) - Judah Gabriel Himango

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接