如何在动态创建的ContextMenu中添加水平分隔符?

22

我在互联网上寻找解决方案,但是在我的示例中找不到它。我需要在从代码后端生成的上下文菜单项之间添加一个分隔符。我尝试使用以下代码行添加它,但没有成功。

this.Commands.Add(new ToolStripSeparator()); 
我想知道是否有人能帮忙。提前感谢您。 上下文菜单 XAML:
<Style x:Key="DataGridCellStyle" TargetType="{x:Type DataGridCell}">
    <Setter Property="ContextMenu">
        <Setter.Value>
            <ContextMenu ItemsSource="{Binding Commands}">
                <ContextMenu.ItemContainerStyle>
                    <Style TargetType="{x:Type MenuItem}">
                        <Setter Property="Command" Value="{Binding}" />
                        <Setter Property="Header" Value="{Binding Path=Text}" />
                        <Setter Property="CommandParameter" Value="{Binding Path=Parameter}" />
                    </Style>
                </ContextMenu.ItemContainerStyle>
            </ContextMenu>
        </Setter.Value>
    </Setter>

添加了一个方法的 C# 代码:

this.Commands = new ObservableCollection<ICommand>();
        this.Commands.Add(MainWindow.AddRole1);
        this.Commands.Add(MainWindow.AddRole2);
        this.Commands.Add(MainWindow.AddRole3);
        this.Commands.Add(MainWindow.AddRole4);
        //this.Add(new ToolStripSeparator()); 
        this.Commands.Add(MainWindow.AddRole5);
        this.Commands.Add(MainWindow.AddRole6);
        this.Commands.Add(MainWindow.AddRole7); 
6个回答

52

我曾经这样做过,并使用null作为我的分隔符。然后,从XAML中,我样式化了模板,以便在数据上下文为空时使用分隔符。

代码后台:

this.Commands.Add(MainWindow.AddRole4);
this.Add(null); 
this.Commands.Add(MainWindow.AddRole5);

XAML 大概长这个样子:

<ContextMenu.ItemContainerStyle>
    <Style TargetType="{x:Type MenuItem}">
        <Setter Property="Command" Value="{Binding}" />
        <Setter Property="Header" Value="{Binding Path=Text}" />
        <Setter Property="CommandParameter" Value="{Binding Path=Parameter}" />

        <Style.Triggers>
            <DataTrigger Binding="{Binding }" Value="{x:Null}">
                <Setter Property="Template" Value="{StaticResource MenuSeparatorTemplate}" />
            </DataTrigger>
        </Style.Triggers>
    </Style>
</ContextMenu.ItemContainerStyle>

希望我有正确的语法 - 我没有IDE来验证代码

编辑

这是上下文菜单分隔符的示例模板。我将其放在ContextMenu.Resources中,尽管您可以将其放在应用程序中的任何位置,只要上下文菜单可以访问它即可。

<ContextMenu.Resources>
    <ControlTemplate x:Key="MenuSeparatorTemplate">
        <Separator />
    </ControlTemplate>
</ContextMenu.Resources>

谢谢你的建议。然而,它还没有起作用。我还不能插入分隔符。我将值更改为 'Value="{DynamicResource MenuSeparatorTemplate}"' 并成功调试了解决方案。结果是分隔符不可见,并且该区域在空菜单项上有悬停状态。我想知道是否有可能修复这个问题。 - vladc77
你需要创建 MenuSeparatorTemplate。因为它还没有被创建,所以什么也没有显示出来。 - Rachel
我添加了 ControlTemplate 并将其命名为 'MenuSeparatorTemplate'。但是,当我尝试打开上下文菜单时,解决方案崩溃了。我相信它非常接近,并且是一个很好的解决方案。我只是有一些语法问题。理想情况下,我希望能够访问在整个项目中使用的通用分隔符样式。 - vladc77
1
@vladc77:我在答案中添加了一个ControlTemplate的示例。我已经测试了代码,没有任何崩溃问题。 - Rachel
是的,分隔符看起来很奇怪 - 它横跨整个菜单(不应覆盖左侧的图标区域),并且比普通分隔符要厚得多。除此之外,这个解决方案看起来很有前途。 - Adrian S
显示剩余3条评论

14

编辑:

我的第一个回答虽然有效,但并没有遵循MVVM设计原则。我现在提供一种MVVM方法,并将原始答案留在下面供参考。

您可以创建一个行为来解决这个问题。

XAML:

(该段内容无需翻译)
<Menu>
    <MenuItem Header="_File" menu:MenuBehavior.MenuItems="{Binding Path=MenuItemViewModels, Mode=OneWay}">

    </MenuItem>
</Menu>

视图模型:

public IEnumerable<MenuItemViewModelBase> MenuItemViewModels => new List<MenuItemViewModelBase>
{
    new MenuItemViewModel { Header = "Hello" },
    new MenuItemSeparatorViewModel(),
    new MenuItemViewModel { Header = "World" }
};

Behavior:

public class MenuBehavior
{
    public static readonly DependencyProperty MenuItemsProperty =
        DependencyProperty.RegisterAttached("MenuItems",
            typeof(IEnumerable<MenuItemViewModelBase>), typeof(MenuBehavior),
            new FrameworkPropertyMetadata(MenuItemsChanged));

    public static IEnumerable<MenuItemViewModelBase> GetMenuItems(DependencyObject element)
    {
        if (element == null)
        {
            throw (new ArgumentNullException("element"));
        }
        return (IEnumerable<MenuItemViewModelBase>)element.GetValue(MenuItemsProperty);
    }

    public static void SetMenuItems(DependencyObject element, IEnumerable<MenuItemViewModelBase> value)
    {
        if (element == null)
        {
            throw (new ArgumentNullException("element"));
        }
        element.SetValue(MenuItemsProperty, value);
    }

    private static void MenuItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var menu = (MenuItem)d;

        if (e.OldValue != e.NewValue)
        {
            menu.ItemsSource = ConvertViewModelsToFrameworkElements((IEnumerable<MenuItemViewModelBase>)e.NewValue);
        }
    }

    private static IEnumerable<FrameworkElement> ConvertViewModelsToFrameworkElements(IEnumerable<MenuItemViewModelBase> viewModels)
    {
        var frameworkElementList = new List<FrameworkElement>();

        foreach (var viewModel in viewModels)
        {
            switch (viewModel)
            {
                case MenuItemViewModel mi:
                    frameworkElementList.Add(new MenuItem
                    {
                        Header = mi.Header,
                        Command = mi.Command,
                        Icon = mi.Icon
                    });
                    break;

                case MenuItemSeparatorViewModel s:
                    frameworkElementList.Add(new Separator());
                    break;
            }
        }
        return frameworkElementList;
    }
}

类:

public class MenuItemViewModelBase
{
}

public class MenuItemViewModel : MenuItemViewModelBase
{
    public object Header { get; set; }
    public ICommand Command { get; set; }
    public object Icon { get; set; }
}

public class MenuItemSeparatorViewModel : MenuItemViewModelBase
{
}

最初的回答:

或者,不要让您的上下文菜单绑定到命令集合,而是将其绑定到FrameworkElements集合,然后您可以直接向集合添加MenuItem或Separator,让Menu控件完成所有模板化操作...

<Style x:Key="DataGridCellStyle" TargetType="{x:Type DataGridCell}">
    <Setter Property="ContextMenu">
        <Setter.Value>
            <ContextMenu ItemsSource="{Binding Commands}" />
        </Setter.Value>
    </Setter>
</Style>

C#:

this.Commands = new ObservableCollection<FrameworkElement>();

this.Commands.Add(new MenuItem {Header = "Menuitem 2", Command = MainWindow.AddRole1});
this.Commands.Add(new MenuItem {Header = "Menuitem 2", Command = MainWindow.AddRole2});
this.Commands.Add(new MenuItem {Header = "Menuitem 3", Command = MainWindow.AddRole3});
this.Commands.Add(new MenuItem {Header = "Menuitem 4", Command = MainWindow.AddRole4});

this.Commands.Add(new Separator);

this.Commands.Add(new MenuItem {Header = "Menuitem 5", Command = MainWindow.AddRole5});
this.Commands.Add(new MenuItem {Header = "Menuitem 6", Command = MainWindow.AddRole6});
this.Commands.Add(new MenuItem {Header = "Menuitem 7", Command = MainWindow.AddRole7});

我在我的应用程序中使用了这种方法 - 这种分隔符看起来更好。

最初的回答


17
如果你想保持模型-视图分离,这并不是一个好主意。 - H.B.
1
不确定为什么。这段代码位于ViewModel中,因此与View分开。它确实包含与View相关的UI控件,如果您想将这些控件保留在ViewModel之外,可以将所有功能放入行为中,并将其绑定到菜单项命令列表,其中分隔符使用虚拟命令。 - samneric
7
因为你不应该在 ViewModel 中放置 MenuItems,所以被 downvote 了。ViewModel 是 ViewModel,不是 view。 - aaronburro

12
我已修改Rachel提供的解决方案,以更正分隔符样式。我知道这篇文章很旧,但仍然是谷歌搜索结果中最热门的之一。在我的情况下,我是将其用于菜单而非上下文菜单,但是同样的方法应该适用。
<Menu ItemsSource="{Binding MenuItems}">
    <Menu.Resources>
        <ControlTemplate x:Key="MenuSeparatorTemplate">
            <Separator>
                <Separator.Style>
                    <Style TargetType="{x:Type Separator}" BasedOn="{StaticResource ResourceKey={x:Static MenuItem.SeparatorStyleKey}}"/>
                </Separator.Style>
            </Separator>
        </ControlTemplate>
        <Style TargetType="{x:Type MenuItem}">
            <Setter Property="Header" Value="{Binding MenuItemHeader}" />
            <Setter Property="Command" Value="{Binding MenuItemCommand}" />
            <Setter Property="CommandParameter" Value="{Binding MenuItemCommandParameter}" />
            <Setter Property="ItemsSource" Value="{Binding MenuItemCollection}" />
            <Style.Triggers>
                <DataTrigger Binding="{Binding }" Value="{x:Null}">
                    <Setter Property="Template" Value="{StaticResource MenuSeparatorTemplate}" />
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </Menu.Resources>
</Menu>

不更改分隔符样式

更改分隔符样式


1
欢迎来到 Stack Overflow!看起来这几乎是一个答案,所以我建议删除关于它是评论的部分,并添加更多上下文使其成为完整的解决方案。 - Brett DeWoody
我使用{x:Null}作为值来表示分隔符。但是,如果用户恰好点击分隔符,则程序会崩溃。显然,它试图在空值上调用“Command”。我创建了一个特殊的静态值代替null:{x:Static MyItem.MySpecialStatic},这样就可以正常工作了。 - IgorStack

1
使用ItemTemplateSelector:
public class MenuItemTemplateSelector : DataTemplateSelector
{
    public DataTemplate SeparatorTemplate { get; set; }

    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        var menuItem = container.GetVisualParent<MenuItem>();
        if (menuItem == null)
        {
            throw new Exception("Unknown MenuItem type");
        }

        if (menuItem.DataContext == null)
        {
            return SeparatorTemplate;
        }

        return menuItem.ItemTemplate;
    }
}

Xaml:
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}"

                                ItemsSource="{Binding Path=ViewContentMenuItems}" >
                                <ContextMenu.ItemTemplateSelector>
                                    <templateSelectors:MenuItemTemplateSelector>
                                        <templateSelectors:MenuItemTemplateSelector.SeparatorTemplate>
                                            <DataTemplate>
                                                <Separator />
                                            </DataTemplate>
                                        </templateSelectors:MenuItemTemplateSelector.SeparatorTemplate>
                                    </templateSelectors:MenuItemTemplateSelector>
                                </ContextMenu.ItemTemplateSelector>
                            </ContextMenu>

在模型中:
public ObservableCollection<MenuItem> ViewContentMenuItems
    {
        get
        {
            var temp = new ObservableCollection<MenuItem>();
            temp.Add(null);
            temp.Add(CreateFolderMenuItem);
            return temp;
        }
    }
private MenuItem CreateFolderMenuItem
    {
        get
        {
            var createFolderMenuItem = new MenuItem()
            {
                Header = "New Folder",
                Icon = new Image
                {
                    Source = new BitmapImage(new Uri("/icons/folderWinCreate.png", UriKind.Relative)),
                    Height = 16,
                    Width = 16
                }
            };

            Message.SetAttach(createFolderMenuItem, "CreateDocumentsFolder");//Caliburn example
            return createFolderMenuItem;
        }
    }

1
不要在视图模型中放置视图元素,更不要放在模型中。 - aaronburro

0
WPF提供了您所需的功能 - 它被称为“分隔符”:
this.Commands.Add(new Separator()); 

-1

要正确地实现 MVVM,您必须定义自己的项接口(例如 IMenuItem),为菜单 / 上下文菜单菜单项创建派生类,在这些类中覆盖以下虚拟受保护方法:

ItemsControl.PrepareContainerForItemOverride
ItemsControl.ClearContainerForItemOverride
ItemsControl.GetContainerForItemOverride
ItemsControl.IsItemItsOwnContainerOverride

确保这些方法为IMenuItem类型的项目创建容器,该容器派生自您新创建的MenuItem类型,并绑定所有必要的属性。在此,您可以区分不同类型的IMenuItem以显示普通项、分隔符或其他内容。对于未知类型,请调用基本实现。

现在,如果您将您新创建的Menu/ContextMenu控件的ItemsSource属性与IMenuItem集合绑定,它将显示您期望的结果,而无需在ViewModel端了解View-stuff。


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