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

Posted

技术标签:

【中文标题】如何在动态创建的 ContextMenu 中添加水平分隔符?【英文标题】:How to add horizontal separator in a dynamically created ContextMenu? 【发布时间】:2011-06-16 23:06:24 【问题描述】:

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

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); 

【问题讨论】:

【参考方案1】:

我这样做过一次,并使用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 可以访问它,您可以将它放在应用程序中您想要的任何位置。

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

【讨论】:

感谢您的想法。但是,它还不起作用。我还不能插入分隔符。我将值更改为 'Value="DynamicResource MenuSeparatorTemplate"' 并能够调试解决方案。结果是分隔符不可见,并且该区域在空菜单项上具有翻转状态。我想知道是否可以修复。 您需要创建MenuSeparatorTemplate。由于它没有被创建,所以没有显示任何内容。 如何从 datatrigger 访问通用分隔符模板? 我添加了 ControlTemplate 并将其命名为“MenuSeparatorTemplate”。但是,当我尝试打开上下文菜单时,解决方案崩溃了。我相信这是非常接近和很好的解决方案。我只是有一些语法问题。理想情况下,我希望能够访问在整个项目中使用的通用分隔符样式。 @vladc77:我在答案中添加了一个示例 ControlTemplate。我已经测试了代码,它运行良好,没有任何崩溃。【参考方案2】:

编辑:

我对这个问题的第一个回答虽然确实有效,但并不遵循 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" 
;

行为:

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


原答案:

或者,不是让 ContextMenu 绑定到命令集合,而是将其绑定到 FrameworkElements 集合,然后您可以将 MenuItems 或 Separators 直接添加到集合中,并让 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);

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

【讨论】:

感谢您分享这项技术。 如果您想保持模型-视图-分离,这不是一个好主意。 不知道为什么。此代码位于 ViewModel 中,因此与 View 是分开的。 True 包含与 View 相关的 UI 控件,如果您想将这些控件排除在 ViewModel 之外,您可以将所有功能放入一个行为中,并将其绑定到菜单项的命令列表,并使用分隔符的虚拟命令。 投了反对票,因为您不应该将 MenuItems 放在 ViewModel 中。它是 ViewModel,而不是视图。【参考方案3】:

我已经修改了上面 Rachel 提供的解决方案以更正分隔符样式。我意识到这篇文章已经过时了,但仍然是 Google 上最好的结果之一。在我的情况下,我将它用于 Menu 和 ContextMenu,但同样应该工作。

XAML

<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>

Without Separator Style Change

With Separator Style Change

【讨论】:

欢迎来到 Stack Overflow!似乎这几乎是一个答案,所以我建议删除关于它作为评论的部分,并添加更多上下文以使其成为一个完整的解决方案。 我用x:Null作为一个值来表示分隔符。但是,如果用户设法准确单击分隔符程序就会崩溃。显然它试图在空值上调用Command。我创建了特殊的静态值而不是 null:`x:Static MyItem.MySpecialStatic 并且它起作用了。【参考方案4】:

使用 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;
        
    

【讨论】:

不要将视图元素放在视图模型中,更不用说模型了。【参考方案5】:

要为 MVVM 正确执行此操作,您必须定义自己的项目接口 (fe IMenuItem),为 Menu创建派生类> / ContextMenuMenuItem,在这些类中覆盖以下虚拟保护方法:

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

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

现在,如果您要绑定从 Menu/ContextMenu 派生的新的 ItemsSource 属性 带有 IMenuItem 集合的控件,它会向您显示预期的结果,而无需在 ViewModel 端查看 View-stuff。

【讨论】:

以上是关于如何在动态创建的 ContextMenu 中添加水平分隔符?的主要内容,如果未能解决你的问题,请参考以下文章

C#.NET中如何设置CONTEXTMENU菜单项的点击事件

如何使用下面的类从web服务中动态读取下面给出的JSON数据,并为收到的数据创建水平列表视图。

SwiftUI contextMenu - 如何添加红色(危险)动作[重复]

动态壁纸水波纹效果

获取 ListItem ContextMenu 中的选定项目

以编程方式添加 MahApps.Metro 上下文菜单