How to add horizontal separator in a dynamically created ContextMenu?

27,680

Solution 1

EDIT:

My first answer to this question, though it actually worked, does not follow the MVVM design principle. I am now providing an MVVM approach and leaving the original answer below for reference.

You can create a behavior to solve this problem.

XAML:

<Menu>
    <MenuItem Header="_File" menu:MenuBehavior.MenuItems="{Binding Path=MenuItemViewModels, Mode=OneWay}">

    </MenuItem>
</Menu>

ViewModel:

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

Classes:

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

Original Answer:

Or, instead of having your ContextMenu bind to a collection of commands, bind it to a collection of FrameworkElements then you can add either MenuItems or Separators directly to the collection and let the Menu control do all the templating....

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

Just used this approach in my app - the separator looks better this way also.

Solution 2

I did this once and used a null as my separator. From the XAML, I then styled the template to use a separator if the datacontext was null

Code behind:

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

XAML was something like this:

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

Hope I got the syntax right - I don't have an IDE on this machine to verify the code

EDIT

Here is an example template for the context menu separator. I am putting it in ContextMenu.Resources, although you could put this anywhere you want in your application as long as the ContextMenu can access it.

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

Solution 3

I have modified the solution provided by Rachel above to correct the Separator style. I realize this post is old, but still one of the top results on Google. In my situation, I was using it for a Menu vs a ContextMenu, but the same should work.

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

Solution 4

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

In model:

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;
        }
    }
Share:
27,680
vladc77
Author by

vladc77

Updated on February 02, 2020

Comments

  • vladc77
    vladc77 over 4 years

    I was looking for the solution on the internet but was not able to find it within my sample. I need to add a separator between Context menu item that are generated from code behind. I tried to add it with such code lines like below but without success.

    this.Commands.Add(new ToolStripSeparator()); 
    

    I am wondering if someone can help. Thank you in advance.

    Context Menu 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# that added in the method:

    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); 
    
  • vladc77
    vladc77 over 13 years
    Thank you for the idea. However, it does not work yet. I cannot insert separator yet. I changed value to be 'Value="{DynamicResource MenuSeparatorTemplate}"' and was able to debug the solution. The result is that separator is not visible and that area has rollover state over empty menuitem. I am wondering if it is possible to fix.
  • Rachel
    Rachel over 13 years
    You need to create the MenuSeparatorTemplate. Since it isn't created, nothing is being shown.
  • vladc77
    vladc77 over 13 years
    How to access generic Separator template from datatrigger?
  • vladc77
    vladc77 over 13 years
    I added ControlTemplate and named it 'MenuSeparatorTemplate'. However, the solution is crashing when I am trying to open context menu. I believe it is very close and great solution. I just have some syntax problem. Ideally, I'd like to be able to access generic separator style that used througout the project.
  • Rachel
    Rachel over 13 years
    @vladc77: I added an example ControlTemplate to the answer. I've tested the code and it works fine without any crashes.
  • vladc77
    vladc77 over 13 years
    Grear. It works. However, the style is different from hormal generic separator style. There is no kind of 3D effect. Do you have any ideas why it can be missed? Thank you again.
  • vladc77
    vladc77 over 13 years
    I just draw separator there without adding <Separator /> code line. It works great now. I am just wondering why its style missed.
  • vladc77
    vladc77 almost 13 years
    Thank you for sharing this technique.
  • H.B.
    H.B. over 11 years
    Not such a great idea if you want to maintain a model-view-separation though.
  • samneric
    samneric over 11 years
    Not sure why. This code is located in the ViewModel so is separate from the View. True is contains View-related UI controls and if you wanted to keep those out of the ViewModel you could put all the functionality into a behavior and have that bind to a list of commands for the menu items with a dummy command for the separator.
  • aaronburro
    aaronburro almost 8 years
    Downvoted because you shouldn't put MenuItems in a ViewModel. It's a ViewModel, not a view.
  • aaronburro
    aaronburro almost 8 years
    Do not put View elements in a View Model, much less a model.
  • Adrian S
    Adrian S about 7 years
    Yes, the separator looks weird - it goes across the entire menu (it should not cover the icon area on the left) and it is much thicker than a normal separator. Other than this, the solution looks promising.
  • Brett DeWoody
    Brett DeWoody over 6 years
    Welcome to Stack Overflow! Seems this is almost an answer, so I'd recommend removing the part about it being a comment, and add more context to make it a complete solution.
  • IgorStack
    IgorStack over 4 years
    I used {x:Null} as a value to indicate separator. However if user manage to click exactly on the separator program crashed. Apparently it tried to invoke Command on the null value. I created special static value instead of null: `{x:Static MyItem.MySpecialStatic} and it worked.