6
\$\begingroup\$

I have the need to programatically create a context menu using names for the menu items which aren't known until the program loads user data.

Here's how I have implemented it. I'm not happy with the cast to get the menu item text to find out which item they have clicked on. Is there a better way that I'm missing?

public partial class Form1 : Form
{
    private readonly string[] cards = new string[] {"HBOS", "EGG", "RBOS"};

    ContextMenu mnuContextMenu = new ContextMenu();

    private void Form1_Load(object sender, EventArgs e)
    {
        ContextMenu = mnuContextMenu;

        SetupRightClickmenu();
    }

    private void SetupRightClickmenu()
    {
        MenuItem bank1MenuItem = new MenuItem(BankNames.Bank1);
        MenuItem bank2MenuItem = new MenuItem(BankNames.Bank2);
        MenuItem bank3MenuItem = new MenuItem(BankNames.Bank3);
        MenuItem cashMenuItem = new MenuItem("Cash");
        MenuItem creditCardMenuItem = new MenuItem("Credit Card");

        bank1MenuItem.Click += bank1MenuItem_Click;
        bank2MenuItem.Click += bank2MenuItem_Click;
        bank3MenuItem.Click += bank3MenuItem_Click;
        cashMenuItem.Click += cashMenuItem_Click;

        mnuContextMenu.MenuItems.Add(bank1MenuItem);
        mnuContextMenu.MenuItems.Add(bank2MenuItem);
        mnuContextMenu.MenuItems.Add(bank3MenuItem);
        mnuContextMenu.MenuItems.Add(cashMenuItem);
        mnuContextMenu.MenuItems.Add(creditCardMenuItem);


        MenuItem creditCardSubMenuItem;

        foreach(string card in cards)
        {
            creditCardSubMenuItem = new MenuItem();
            creditCardSubMenuItem.Text = card;
            creditCardMenuItem.MenuItems.Add(creditCardSubMenuItem);
            creditCardSubMenuItem.Click += creditCardSubMenuItem_Click;
        }
    }

    void creditCardSubMenuItem_Click(object sender, EventArgs e)
    {
        MenuItem menuItem = (MenuItem) sender;

        MessageBox.Show("You clicked on " + menuItem.Text);
    }

    void cashMenuItem_Click(object sender, EventArgs e)
    {
        MessageBox.Show("You clicked on cash");
    }

    void bank3MenuItem_Click(object sender, EventArgs e)
    {
        MessageBox.Show("You clicked on bank 3");
    }

    void bank2MenuItem_Click(object sender, EventArgs e)
    {
        MessageBox.Show("You clicked on bank 2");
    }

    void bank1MenuItem_Click(object sender, EventArgs e)
    {
        MessageBox.Show("You clicked on bank 1");
    }
}

In the actual version the cards array is received from the users data.

\$\endgroup\$
  • \$\begingroup\$ I believe what you are looking for is dependency injection. \$\endgroup\$ – IAbstract Aug 9 '11 at 13:32
  • \$\begingroup\$ @IAbstract actually I think he's just curious how to get rid of the cast from sender to a specific type, which would mean altering the event signature and wouldn't be related to DI, though it's not entirely clear so I'm not certain \$\endgroup\$ – Jimmy Hoffa Aug 9 '11 at 16:22
  • \$\begingroup\$ For the click event handlers, are you actually just displaying a message box for all in exactly the same way and nothing else? If so, it would probably be better to just use the creditCardSubMenuItem_Click() method for all of them as none of the others does anything significantly different. \$\endgroup\$ – Jeff Mercado Aug 10 '11 at 0:54
  • \$\begingroup\$ The system allows the users to enter transactions against accounts. The accounts can either be to one of 3 bank accounts, the petty cash account or a credit card. The credit cards are created by the users. They can add or delete them as they need to. Right now there is no way to move a transaction from one account to another.I just need a way to give them a context menu when they right click on the transaction which has the list of accounts and existing credit cards. All I want from the menu is to know the name of the credit card they have selected so I can move the transaction to that card. \$\endgroup\$ – user476683 Aug 10 '11 at 12:53
  • \$\begingroup\$ @Jeff Mercado Those messages boxes are just temporary. \$\endgroup\$ – user476683 Aug 10 '11 at 12:54
5
\$\begingroup\$

You can use the Tag property to store the card. Its normal to store the context object in the Tag.

that way you can make the text whatever you like eg..

creditCardSubMenuItem.Text = string.Format("The card {0}", card);
creditCardSubMenuItem.Tag= card;
| improve this answer | |
\$\endgroup\$
  • 1
    \$\begingroup\$ The Tag property is still of type object, so he'll need to cast it to get it a MenuItem out of it. There's also little need to put the object firing the event in the Tag, since it'll always be available as sender anyway. \$\endgroup\$ – Adam Lear Aug 10 '11 at 3:33
  • 1
    \$\begingroup\$ I think his problem isn't so much the cast, but that he is having to use the text on the UI as a way to find out what was selected, which is ugly. The 'cast' is just adding to the noise of what seems to be a bad way of doing things. \$\endgroup\$ – Keith Nicholas Aug 10 '11 at 21:30
  • \$\begingroup\$ Oh, I misread your suggestion. I thought you were putting the menu item in the tag for some reason. Derp. My bad. \$\endgroup\$ – Adam Lear Aug 10 '11 at 21:35
2
\$\begingroup\$

Though there are other things in this I am not sure are done in an ideal fashion, I'm not sure the whole scope so I will stick to just what you asked about:

This cast from sender to a specific type which is the only type that will fire the event, is the standard way of doing exactly what you are doing with it. The only other way people do this, is to use the as which will return a null if the cast fails, but this should only be used when there's a chance of a different type than expected. If you know for certain that event handler will only be subscribed to an object of type MenuItem, then your cast is absolutely the correct way to do it.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ generally you wouldn't EVER use the Text of a menu item to find out your context though. Hence my answer. Especially if translation is involved \$\endgroup\$ – Keith Nicholas Aug 10 '11 at 1:32
0
\$\begingroup\$

You can do something like this. Here, I've sub-classed MenuItem and overridden OnClick event. When a menu is clicked, I cook up an event args and pass it back to main form. If you are okay with casting 'sender' object, you can get rid of MenuActivatedEventArgs class. This code actually compiles as I'm pasting it from my VisualStudio editor. Ask me if you are not clear on any line.

This probably may not the best way of doing it, but this is how I love to implement when things go crazy. I separate them out and look for places where I can apply some clean-coding and refactoring.

using System;
using System.Windows.Forms;

namespace ContextMenu {
    public partial class Form1 : Form
    {
        // info text for message box
        private const string InfoText = "You clicked on {0}";
        private readonly string[] MyCards = new[] { "HBOS", "EGG", "RBOS" };

        public Form1()
        {
            InitializeComponent();
        }

        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
            // set the context menu
            ContextMenu = new System.Windows.Forms.ContextMenu();
            // setup menuitems
            SetupRightClickMenu();
        }

        private void SetupRightClickMenu()
        {
            // add first few menu items which don't have any submenuitems
            ContextMenu.MenuItems.AddRange(
                new[]
                    {
                        CreateAndHookUpMenuItem("Bank 1"),
                        CreateAndHookUpMenuItem("Bank 2"),
                        CreateAndHookUpMenuItem("Bank 3"),
                        CreateAndHookUpMenuItem("Cash"),

                    });

            // create creditcard menu item
            var creditCardMenuItem = CreateAndHookUpMenuItem("Credit Card") ;

            // add cards to creditcard menu item
            foreach (var card in MyCards)
                creditCardMenuItem.MenuItems.Add(CreateAndHookUpMenuItem(card));

            // finally, add creditcard menu item to the context menu
            ContextMenu.MenuItems.Add(creditCardMenuItem);
        }

        private MenuItem CreateAndHookUpMenuItem(string title)
        {
            var menuItem = new MyMenuItem(title);
            menuItem.OnMenuActivated += MyMenuItem_OnMenuActivated;
            return menuItem;
        }


        private void MyMenuItem_OnMenuActivated(object sender, MenuActivatedEventArgs e)
        {
            MessageBox.Show(String.Format(InfoText, e.ItsMenuItem.Text));
        }
    }


    class MyMenuItem : MenuItem
    {
        public MyMenuItem(string title) : base(title) { }

        // this can be just of type EventArgs but if you hate casting, this is one way of doing it
        public event EventHandler<MenuActivatedEventArgs> OnMenuActivated;

        protected override void OnClick(EventArgs e)
        {
            base.OnClick(e);
            InvokeMenuActivatedEvent();
        }

        private void InvokeMenuActivatedEvent()
        {
            var handler = OnMenuActivated;
            if (handler != null)
            {
                OnMenuActivated.Invoke(this, new MenuActivatedEventArgs(this));
            }
        }
    }

    class MenuActivatedEventArgs : EventArgs
    {
        public MyMenuItem ItsMenuItem { get; private set; }

        public MenuActivatedEventArgs(MyMenuItem item)
        {
            ItsMenuItem = item;
        }
    }
}
| improve this answer | |
\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.