如何将 ViewModel(包括分隔符)正确绑定到 WPF 的菜单?

How to correctly bind a ViewModel (which Include Separators) to WPF's Menu?

我正在使用 MVVM,我想将我的 MenuViewModels 列表数据绑定到我的主要菜单。它由一组菜单项和分隔符组成。

这是我的 MenuItemViewModel 代码:

public interface IMenuItemViewModel
{
}

[DebuggerDisplay("---")]
public class SeparatorViewModel : IMenuItemViewModel
{
}

[DebuggerDisplay("{Header}, Children={Children.Count}")]
public class MenuItemViewModel : IMenuItemViewModel, INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public MenuItemViewModel(string header, ICommand command, ImageSource imageSource)
    {
        Header = header;
        Command = command;
        ImageSource = imageSource;

        Children = new List<IMenuItemViewModel>();
    }

    public string Header { get; private set; }
    public ICommand Command { get; private set; }

    public ImageSource ImageSource { get; private set; }

    public IList<IMenuItemViewModel> Children { get; private set; }
}

我的主要 window 看起来像这样:

<Window.Resources>
    <HierarchicalDataTemplate DataType="{x:Type ViewModel:MenuItemViewModel}"
        ItemsSource="{Binding Children}">
        <MenuItem Header="{Binding Header}"
                  Command="{Binding Command}"/>
    </HierarchicalDataTemplate>

    <DataTemplate DataType="{x:Type ViewModel:SeparatorViewModel}">
        <Separator />
    </DataTemplate>
</Window.Resources>

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

应该是很简单的东东。不幸的是,要么菜单项看起来不对,要么分隔符是空的 menuItem(取决于我的尝试)。

那么,如何让我的 Menu 找到我的两个 DataTemplates

解决了我自己的问题

在网上搜索了几个小时后,我发现很多例子反对 WPF 的自然意图,但none 与它一起工作。

以下是 Menu 控制而不是对抗它的方法...

一点背景知识

WPF 的 Menu 控件在绑定到 POCO 集合时,通常 自动为您创建 MenuItem 个对象,使用 ItemsSource 属性.

但是,此默认行为可以被覆盖!方法如下...

解决方案

首先,您必须创建一个派生自 ItemContainerTemplateSelector 的 class。或者使用我创建的简单 class:

public class MenuItemContainerTemplateSelector : ItemContainerTemplateSelector
{
    public override DataTemplate SelectTemplate(object item, ItemsControl parentItemsControl)
    {
        var key = new DataTemplateKey(item.GetType());
        return (DataTemplate) parentItemsControl.FindResource(key);
    }
}

其次,您必须将对 MenuItemContainerTemplateSelector class 的引用添加到您的 Windows resources 对象,如下所示:

<Window.Resources>
    <Selectors:MenuItemContainerTemplateSelector x:Key="_menuItemContainerTemplateSelector" />

第三,您必须在 MenuMenuItem 上设置两个属性(UsesItemContainerTemplateItemContainerTemplateSelector(在 HierarchicalDataTemplate).

像这样:

    <HierarchicalDataTemplate DataType="{x:Type ViewModel:MenuItemViewModel}"
        ItemsSource="{Binding Children}">
        <MenuItem Header="{Binding Header}"
                  Command="{Binding Command}"
                  UsesItemContainerTemplate ="true"
                  ItemContainerTemplateSelector=
                  "{StaticResource _menuItemContainerTemplateSelector}"/>
    </HierarchicalDataTemplate>

    <Menu DockPanel.Dock="Top"
          ItemsSource="{Binding MenuItems}"
          UsesItemContainerTemplate="True"
          ItemContainerTemplateSelector=
          "{StaticResource _menuItemContainerTemplateSelector}">
    </Menu>

为什么有效

出于优化目的,Menu 使用 UsesItemContainerTemplate 标志(默认值为 false)跳过 DataTemplate 查找,仅 returns 一个普通的 MenuItem 对象。因此,我们需要将此值设置为 true,然后我们的 ItemContainerTemplateSelector 会按预期工作。

编码愉快!

没有 TemplateSelector 的解决方案:

提供 ItemContainerTemplates 而不是 DataTemplates:

<ContextMenu ItemsSource="{Binding Path=MenuItems}" UsesItemContainerTemplate="True">
              <ContextMenu.Resources>
                <ResourceDictionary>
                  <ItemContainerTemplate DataType="{x:Type ViewModel:MenuItemViewModel }">
                    <MenuItem Header="{Binding Path=Header}" Command="{Binding Path=Command}" UsesItemContainerTemplate="True">
                      <MenuItem.Icon>
                        <Image Source="{Binding Path=ImageSource}"/>
                      </MenuItem.Icon>
                    </MenuItem>
                  </ItemContainerTemplate>
                  <ItemContainerTemplate DataType="{x:Type ViewModel:SeparatorViewModel}">
                    <Separator >
                      <Separator.Style>
                        <Style TargetType="{x:Type Separator}" BasedOn="{StaticResource ResourceKey={x:Static MenuItem.SeparatorStyleKey}}"/>
                      </Separator.Style>
                    </Separator>
                  </ItemContainerTemplate>
                </ResourceDictionary>
              </ContextMenu.Resources>
            </ContextMenu>

备注:

  • 我没试过儿童
  • 分隔符styled wrong:我不得不手动重新应用样式

另一种方法是:

  • 在您的菜单项 ViewModel 上有一个布尔值 属性,指示某个项目是否为分隔符
  • 使用基于此 属性 的触发器来更改 MenuItemControlTemplate,以便它使用 Separator 控件来代替

像这样:

<Menu ItemsSource="{Binding MenuItems}">
    <Menu.Resources>
        <Style TargetType="{x:Type MenuItem}">
            <Setter Property="Header" Value="{Binding Header}" />
            <Setter Property="Command" Value="{Binding Command}" />
            <Style.Triggers>
                <DataTrigger Binding="{Binding IsSeparator}" Value="True">
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="{x:Type MenuItem}">
                                <Separator />
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </DataTrigger>
            </Style.Triggers>
        </Style>

        <HierarchicalDataTemplate DataType="{x:Type ViewModel:MenuItemViewModel}"
                                  ItemsSource="{Binding Children}" />
    </Menu.Resources>
</Menu>