WPF Context Menu

TextBox has a default context menu. I would like to add an item to it. OK, that means cloning the default one, and adding an extra item to that.

I'd like to reuse some code here. I have five textboxes. Each needs the additional item on its context menu. The item needs act on the textbox that was clicked. I know "copy and paste" is the recommended method of code reuse in WPF, but if possible I'd prefer not to define five menus in XAML and five commands in the code behind.

Is there any reasonably clean and quick way to do this in WPF?

public partial class MyGhastlyView
{
    /* blah blah */

    private void MenuCut_Click(object sender, RoutedEventArgs e)
    {
        try
        {
            (sender as MenuItem).GetPlacementTarget<TextBox>().Cut();
        }
        catch (Exception)
        {
        }
    }

    /* blah blah */
}

public static class FurshlugginerExtensions
{
    public static bool TryGetPlacementTarget<TTargetType>(this MenuItem mi, 
        out TTargetType target) where TTargetType : class
    {
        target = null;

        var cm = mi.GetContextMenu();

        if (null != cm)
        {
            target = cm.PlacementTarget as TTargetType;
        }

        return null != target;
    }

    public static TTargetType GetPlacementTarget<TTargetType>(this MenuItem mi) 
        where TTargetType : class
    {
        var cm = mi.GetContextMenu();

        return (cm == null)
                ? null
                : cm.PlacementTarget as TTargetType;
    }

    public static ContextMenu GetContextMenu(this MenuItem mi)
    {
        var logicalParent = LogicalTreeHelper.GetParent(mi);

        if (logicalParent is ContextMenu)
        {
            return logicalParent as ContextMenu;
        }
        else if (logicalParent is MenuItem)
        {
            return (logicalParent as MenuItem).GetContextMenu();
        }

        return null;
    }
}

UPDATE

What I'm looking for turns out to be a RoutedUICommand, with some futzing around in XAML. It knows what you clicked on (with some Kafkaesque exceptions due to event bubbling -- but can just set the CommandParameter on the ContextMenu).

Upvotes: 1

Views: 1724

Answers (2)

Jeff
Jeff

Reputation: 2505

Unfortunately, ContextMenuOpening event will not work here. For whatever reason, TextBox does not expose its context menu, and is always null unless you set it with your own. Perhaps it simply pops a private menu on right mouse click.

Charles Petzold speaks about that with RichTextBox here. (Both TextBox and RichTextBox derive from TextBoxBase, which appears to define that behavior)

It seems you will have to create your own, and duplicate the existing items.

Several articles demonstrate exactly this, like the one here.

Hope this helps.


EDIT:

However if you insist on editing the current menu, it appears someone has done so here (using an extension method and reflection).

After further investigation of the above attempt, it seems that the author is creating an instance of an EditorContextMenu (private class which derives from ContextMenu in System.Windows.Documents) and assigning it to the TextBox ContextMenu property, then adding the parameter menu items to the newly created menu. In effect, overriding the current menu. While you do get the original implementation, I am not sure I would favor this solution.


EDIT 2:

The following code will create only one instance of custom menu, bind Ctrl-D to the textboxes, along with the correlating ContextMenu item.

    public static RoutedCommand ItemActionCommand = new RoutedCommand();

    public MainWindow()
    {
        InitializeComponent();

        CommandBinding commandBinding = new CommandBinding(ItemActionCommand, new ExecutedRoutedEventHandler(ItemActionCommandEventHandler));
        KeyBinding keyBinding = new KeyBinding(ItemActionCommand, new KeyGesture(Key.D, ModifierKeys.Control));

        MenuItem item = new MenuItem();
        item.Click += CustomContextMenuItem_Click;  // not really necessary
        item.Header = "Custom Menu Item";
        item.InputGestureText = "Ctrl+D";
        item.Command = ItemActionCommand;

        ContextMenu menu = new ContextMenu();
        menu.Items.Add(item);

        Grid container = new Grid();
        this.Content = container;

        for (int i = 0; i < 5; i++)
            container.Children.Add(this.CreateTextBox("Value: " + i.ToString(), (i + 1) * 30.0d, menu, commandBinding, keyBinding));
    }

    private void ItemActionCommandEventHandler(object sender, ExecutedRoutedEventArgs e)
    {
        TextBox textBox = e.Source as TextBox;
        Debug.Assert(textBox != null);
        // perform actions against textbox here
    }

    private void CustomContextMenuItem_Click(object sender, RoutedEventArgs e)
    {
        MenuItem item = sender as MenuItem;
        Debug.Assert(item != null);
        TextBox textBox = ((ContextMenu)item.Parent).PlacementTarget as TextBox;
        Debug.Assert(textBox != null);
        // no need to do anything here since the command handler above will fire
        // but for the sake of completeness
    }

    private TextBox CreateTextBox(string text, double topOffset, ContextMenu menu, CommandBinding commandBinding, KeyBinding keyBinding)
    {
        TextBox textbox = new TextBox();
        textbox.HorizontalAlignment = HorizontalAlignment.Center;
        textbox.VerticalAlignment = VerticalAlignment.Top;
        textbox.Margin = new Thickness(0.0d, topOffset, 0.0d, 0.0d);
        textbox.CommandBindings.Add(commandBinding);
        textbox.InputBindings.Add(keyBinding);
        textbox.ContextMenu = menu;
        textbox.Width = 150.0d;
        textbox.Height = 25.0d;
        textbox.Text = text;
        return textbox;
    }

Screenshot:

ctx menu

Upvotes: 3

user3411327
user3411327

Reputation: 1041

It is possible with an AttachedProperty and the handling of the ContextMenuOpening event. Look here and here. Should take around 100 lines of code and one line in xaml.

For completenes sake:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace WpfApplication1
{
    public class CustomMenuAction
    {
        public static bool GetHasMenuItemAction(DependencyObject obj)
        {
            return (bool)obj.GetValue(HasMenuItemActionProperty);
        }

        public static void SetHasMenuItemAction(DependencyObject obj, bool value)
        {
            obj.SetValue(HasMenuItemActionProperty, value);
        }

        // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty HasMenuItemActionProperty =
            DependencyProperty.RegisterAttached("HasMenuItemAction", typeof(bool), typeof(CustomMenuAction), new PropertyMetadata(default(bool),OnPropertyChanged));

        private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
           if((bool)e.NewValue)
           {
               var textbox = d as TextBox;
               if(textbox != null)
               {
                   textbox.ContextMenu = GetCustomContextMenu();
                   textbox.ContextMenuOpening += textbox_ContextMenuOpening;
               }
           }
        }

        private static ContextMenu GetCustomContextMenu()
        {
            var contextMenu = new ContextMenu();
            var standardCommands = GetStandardCommands();
            foreach (var item in standardCommands)
            {
                contextMenu.Items.Add(item);
            }
            return contextMenu;
        }

        private static IList<MenuItem> GetStandardCommands()
        {
            //From https://stackoverflow.com/a/210981/3411327
            List<MenuItem> standardCommands = new List<MenuItem>();
            MenuItem item = new MenuItem();
            item.Command = ApplicationCommands.Cut;
            standardCommands.Add(item);
            item = new MenuItem();
            item.Command = ApplicationCommands.Copy;
            standardCommands.Add(item);
            item = new MenuItem();
            item.Command = ApplicationCommands.Paste;
            standardCommands.Add(item);
            return standardCommands;
        }


        static void textbox_ContextMenuOpening(object sender, ContextMenuEventArgs e)
        {
            //From MSDN example: http://msdn.microsoft.com/en-us/library/bb613568.aspx
            var textbox = e.Source as TextBox;
            ContextMenu cm = textbox.ContextMenu;
            foreach (MenuItem mi in cm.Items)
            {
                if ((String)mi.Header == "Item4") return;
            }
            MenuItem mi4 = new MenuItem();
            mi4.Header = "Item4";
            mi4.Click += (o, args) =>
                {
                    var menuItem = o as MenuItem;
                    MessageBox.Show(menuItem.Header.ToString(), textbox.Text);
                };
            textbox.ContextMenu.Items.Add(mi4);
        }   
    }
}

<TextBox namespace:CustomMenuAction.HasMenuItemAction="True"></TextBox>

Upvotes: 1

Related Questions