Menus

Using menus in a Xamarin.Mac application

PDF for offline use
Sample Code:
Related Articles:
Related SDKs:

Let us know how you feel about this

Translation Quality


0/250

This article covers working with Cocoa Menus in a Xamarin.Mac application. It covers creating and maintaining menus and menu items in Xcode and Interface builder, how to expose those items to code using Outlets and Actions, enabling and disabling menus and menu items and finally responding to menu items in C# code.

Contents

This article will cover the following topics in detail:

Overview

When working with C# and .NET in a Xamarin.Mac application, you have access to the same Cocoa Menus that a developer working in in Objective-C and Xcode does. Because Xamarin.Mac integrates directly with Xcode, you can use Xcode's Interface Builder to create and maintain your Menu Bars, Menus and Menu Items (or optionally create them directly in C# code).

Menus are a integral part of a Mac application's user experience and commonly appear in various parts of the user interface:

  • The Application's Menu Bar - This is the main menu that appears at the top of the screen for every Mac Application.
  • Contextual Menus - These appear when the user right-clicks or left-control-clicks an item in a window.
  • The Status Bar - This is the area at the far right side of the Application Menu Bar that appears at the top of the screen (to the left of the menu bar clock) and grows to the left as items are added to it.
  • Dock Menu - The menu for each application in the dock that appears when the user right-clicks or left-control-clicks the application's icon, or when the user left-clicks the icon and holds the mouse button down.
  • Pop-up Button and Pull-Down Lists - A Pop-up Button displays a selected item and presents a list of options to select from when clicked by the user. A Pull-Down list is a type of Pop-up Button usually used for selecting commands specific to the context of the current task. Both can appear anywhere in a window.

In this article, we'll cover the basics of working with Cocoa Menu Bars, Menus and Menu Items in a Xamarin.Mac application. It is highly suggested that you work through the Hello, Mac article first, specifically the Introduction to Xcode and Interface Builder and Outlets and Actions sections, as it covers key concepts and techniques that we'll be using in this article.

You may want to take a look at the Exposing C# classes / methods to Objective-C section of the Xamarin.Mac Internals document as well, it explains the Register and Export commands used to wire-up your C# classes to Objective-C objects and UI Elements.

The Application's Menu Bar

Unlike applications running on the Windows OS where every window can have it's own menu bar attached to it, every application running on macOS has a single Menu Bar that runs along the top of the screen that's used for every window in that application:

Items on this Menu Bar are activated or deactivated based on the current context or state of the application and it's user interface at any given moment. For example: if the user selects a text field, items on the Edit menu will be come enabled such as Copy and Cut.

According to Apple and by default, all macOS application's have a standard set of Menus and Menu Items that appear in the Application's Menu Bar:

  • The Apple Menu - This menu provides access to system wide items that are available to the user at all times, regardless of what application is running. These items cannot be modified by the developer.
  • The App Menu - This menu displays the application's name in bold and helps the user identify what application is currently running. It contains items that apply to the application as a whole and not a given document or process such as quitting the application.
  • The File Menu - Items used to create, open or save documents that your application works with. If your application is not document based, this menu can be renamed or removed.
  • The Edit Menu - Holds command such as Cut, Copy and Paste and are used to edit or modify elements in the applications user interface.
  • The Format Menu - If the application works with text, this menu holds commands to adjust the formatting of that text.
  • The View Menu - Holds command that affect how content is displayed (viewed) in the application's user interface.
  • Application Specific Menus - These are any menus that are specific to your applications (such as Bookmarks for a web browser). They should appear between the View and Windows menus on the bar.
  • The Windows Menu - Contains commands for working with windows in your application, as well as a list of current open windows.
  • The Help Menu - If your application provides onscreen help, the Help menu should be the right-most menu on the bar.

For more information about the Application Menu Bar and standard Menus and Menu Items, please see Apple's Menu Bar Menus section of the OS X Human Interface Guidelines.

The Default Application Menu Bar

Whenever you create a new Xamarin.Mac project, you automatically get a standard, default Application Menu Bar that has the typical items that a macOS application would normally have (as discussed in the section above). Your application's default Menu Bar is defined in the Main.storyboard file (along with the rest of your app's UI) under the Project in the Solution Explorer:

Double-click the Main.storyboard file to open it for editing in Xcode's Interface Builder and you'll be presented with the Menu Editor Interface:

From here we can click on items such as the Open menu item in the File menu and edit or adjust it's properties in the Attributes Inspector:

We'll get into adding, editing and deleting Menus and Items later in this article. For now we just want to see what Menus and Menu Items are available by default and how they have been automatically exposed to code via a set of predefined Outlets and Actions (for more information see our Outlets and Actions documentation).

For example, if we click on the Connection Inspector for the Open menu item we can see it is automatically wired-up the openDocument: Action:

If you select the First Responder in the Interface Hierarchy and scroll down in the Connection Inspector, and you will see the definition of the openDocument: Action that the Open menu item is attached to (along with several other default Actions for the application that are and are not automatically wired-up to controls):

Why is this important? In the next section will see how these automatically defined Actions work with other Cocoa user interface elements to automatically enable and disable menu items, as well as, provide built-in functionality for the items.

Later we'll be using these built-in Actions to enable and disable items from code and provide our own functionality when they are selected.

Built-In Menu Functionality

If you were the run a newly created Xamarin.Mac application before adding any UI items or code, you'll notice that some items are automatically wired-up and enabled for you (with fully functionality automatically built-in), such as the Quit item in the App Menu:

While other Menu Items, such as Cut, Copy and Paste are not:

Let's stop the application and double-click the Main.storyboard file in the Solution Explorer to open it for editing in Xcode's Interface Builder. Next, drag a Text View from the Library onto the Window's View Controller in the Interface Editor:

In the Constraint Editor let's pin the Text View to the Window's edges and set it where it grows and shrinks with the window by clicking all four Red I-Beams at the top of the editor and clicking the Add 4 Constraints button:

Save your changes to the user interface design and switch back the Visual Studio for Mac to synchronize the changes with your Xamarin.Mac project. Now start the application, type some text into the text view, select it and open the Edit menu:

Notice how the Cut, Copy and Paste are automatically enabled and fully functional, all without writing a single line of code.

What's going on here? Remember the built-in predefine Actions that come wired-up to the default Menu Items (as presented above), most of the Cocoa User Interface elements that are part of macOS have built in hooks to specific Actions (such as copy:). So when they are added to a window, active and selected, the corresponding Menu Item or Items attached to that Action are automatically enable. If the user selects that Menu Item, the functionality built into the UI element is called and executed, all without developer intervention.

Enabling and Disabling Menus and Items

By default, every time a user event occurs, NSMenu automatically enables and disables each visible Menu and Menu Item based on the context of the application. There are three ways to enable/disable an item:

  • Automatic Menu Enabling - A Menu Item is enabled if NSMenu can find an appropriate object that responds to the Action that the item is wired-up to. For example, the Text View above that had a built in hook to the copy: Action.
  • Custom Actions & validateMenuItem: - For any menu item that is bound to a Window or View Controller Custom Action, you can add the validateMenuItem: Action and manually enable or disable menu items.
  • Manual Menu Enabling - You manually set the Enabled property of each NSMenuItem to enable or disable each item in a menu individually.

To choose a system, set the AutoEnablesItems property of a NSMenu where true is automatic (the default behavior) and false is manual.

⚠️

NOTE: If you choose to use Manual Menu Enabling, none of the menu items, even those controlled by Application Kit classes like NSTextView, are updated automatically. You will be responsible for enabling and disabling all items by hand in code.

Using validateMenuItem

As stated above, for any menu item that is bound to a Window or View Controller Custom Action, you can add the validateMenuItem: Action and manually enable or disable menu items.

In the following example, the Tag property will be used to decide the type of Menu Item that will be enabled/disabled by the validateMenuItem: Action based on the state of selected text in a NSTextView. The Tag property has been set in Interface Builder for each Menu Item:

And the following code added to the View Controller:

[Action("validateMenuItem:")]
public bool ValidateMenuItem (NSMenuItem item) {

    // Take action based on the Menu Item type
    // (As specified in its Tag)
    switch (item.Tag) {
    case 1:
        // Wrap menu items should only be available if
        // a range of text is selected
        return (TextEditor.SelectedRange.Length > 0);
    case 2:
        // Quote menu items should only be available if
        // a range is NOT selected.
        return (TextEditor.SelectedRange.Length == 0);
    }

    return true;
}

When this code is run, and no text is selected in the NSTextView, the two wrap Menu Items are disabled (even though they are wired to Actions on the View Controller):

If a section of text is selected and the Menu reopened, the two wrap Menu Items will be available:

Enabling and Responding to Menu Items in Code

As we have seen above, just by adding specific Cocoa User Interface elements to our UI design (such as a Text Field), several of the default Menu Items will be enabled and function automatically, without having to write any code. Next let's look at adding our own C# code to our Xamarin.Mac project to enable a Menu Item and provide functionality when the user selects it.

For example, let say we want the user to be able to use the Open item in the File menu to select a folder. Since we want this to be an application wide function and not limited to a give window or UI element, we're going to add the code to handle this to our Application Delegate.

In the Solution Explorer, double-click the AppDelegate.CS file to open it for editing:

Add the following code below the DidFinishLaunching method:

[Export ("openDocument:")]
void OpenDialog (NSObject sender)
{
    var dlg = NSOpenPanel.OpenPanel;
    dlg.CanChooseFiles = false;
    dlg.CanChooseDirectories = true;

    if (dlg.RunModal () == 1) {
        var alert = new NSAlert () {
            AlertStyle = NSAlertStyle.Informational,
            InformativeText = "At this point we should do something with the folder that the user just selected in the Open File Dialog box...",
            MessageText = "Folder Selected"
        };
        alert.RunModal ();
    }
}

Let's run the application now and open the File menu:

Notice that the Open menu item is now enabled. If we select it, the Open Dialog Box will be displayed:

If we click the Open button, our alert message will be displayed:

The key line here was [Export ("openDocument:")], it tells NSMenu that our AppDelegate has a method void OpenDialog (NSObject sender) that responds to the openDocument: Action. If you'll remember above, the Open menu item is automatically wired-up to this Action by default in Interface Builder:

Next let's look at creating our own Menu, Menu Items and Actions and responding to them in code.

Working with the Open Recent Menu

By default, the File menu contains an Open Recent item that keeps track of the last several files that the user has opened with your app. If you are creating a NSDocument based Xamarin.Mac app, this menu will be handled for you automatically. For any other type of Xamarin.Mac app, you will be responsible for managing and responding to this menu item manually.

To manually handle the Open Recent menu, you will first need to inform it that a new file has been opened or saved using the following:

// Add document to the Open Recent menu
NSDocumentController.SharedDocumentController.NoteNewRecentDocumentURL(url);

Even though your app is not using NSDocuments, you still use the NSDocumentController to maintain the Open Recent menu by sending a NSUrl with the location of the file to the NoteNewRecentDocumentURL method of the SharedDocumentController.

Next, you need to override the OpenFile method of the App Delegate to open any file that the user selects from the Open Recent menu. For example:

public override bool OpenFile (NSApplication sender, string filename)
{
    // Trap all errors
    try {
        filename = filename.Replace (" ", "%20");
        var url = new NSUrl ("file://"+filename);
        return OpenFile(url);
    } catch {
        return false;
    }
}

Return true if the file can be opened, else return false and a built-in warning will be displayed to the user that the file could not be opened.

Because the filename and path returned from the Open Recent menu, might include a space, we need to properly escape this character before creating a NSUrl or we will get an error. We do that with the following code:

filename = filename.Replace (" ", "%20");

Finally, we create a NSUrl that points to the file and use a helper method in the App Delegate to open a new window and load the file into it:

var url = new NSUrl ("file://"+filename);
return OpenFile(url);

To pull everything together, let's take a look at an example implementation in an AppDelegate.cs file:

using AppKit;
using Foundation;
using System.IO;
using System;

namespace MacHyperlink
{
    [Register ("AppDelegate")]
    public class AppDelegate : NSApplicationDelegate
    {
        #region Computed Properties
        public int NewWindowNumber { get; set;} = -1;
        #endregion

        #region Constructors
        public AppDelegate ()
        {
        }
        #endregion

        #region Override Methods
        public override void DidFinishLaunching (NSNotification notification)
        {
            // Insert code here to initialize your application
        }

        public override void WillTerminate (NSNotification notification)
        {
            // Insert code here to tear down your application
        }

        public override bool OpenFile (NSApplication sender, string filename)
        {
            // Trap all errors
            try {
                filename = filename.Replace (" ", "%20");
                var url = new NSUrl ("file://"+filename);
                return OpenFile(url);
            } catch {
                return false;
            }
        }
        #endregion

        #region Private Methods
        private bool OpenFile(NSUrl url) {
            var good = false;

            // Trap all errors
            try {
                var path = url.Path;

                // Is the file already open?
                for(int n=0; n<NSApplication.SharedApplication.Windows.Length; ++n) {
                    var content = NSApplication.SharedApplication.Windows[n].ContentViewController as ViewController;
                    if (content != null && path == content.FilePath) {
                        // Bring window to front
                        NSApplication.SharedApplication.Windows[n].MakeKeyAndOrderFront(this);
                        return true;
                    }
                }

                // Get new window
                var storyboard = NSStoryboard.FromName ("Main", null);
                var controller = storyboard.InstantiateControllerWithIdentifier ("MainWindow") as NSWindowController;

                // Display
                controller.ShowWindow(this);

                // Load the text into the window
                var viewController = controller.Window.ContentViewController as ViewController;
                viewController.Text = File.ReadAllText(path);
                viewController.SetLanguageFromPath(path);
                viewController.View.Window.SetTitleWithRepresentedFilename (Path.GetFileName(path));
                viewController.View.Window.RepresentedUrl = url;

                // Add document to the Open Recent menu
                NSDocumentController.SharedDocumentController.NoteNewRecentDocumentURL(url);

                // Make as successful
                good = true;
            } catch {
                // Mark as bad file on error
                good = false;
            }

            // Return results
            return good;
        }
        #endregion

        #region Actions
        [Export ("openDocument:")]
        void OpenDialog (NSObject sender)
        {
            var dlg = NSOpenPanel.OpenPanel;
            dlg.CanChooseFiles = true;
            dlg.CanChooseDirectories = false;

            if (dlg.RunModal () == 1) {
                // Nab the first file
                var url = dlg.Urls [0];

                if (url != null) {
                    // Open the document in a new window
                    OpenFile (url);
                }
            }
        }
        #endregion
    }
}

Based on the requirements of your app, you might not want the user to open the same file in more than one window at the same time. In our example app, if the user chooses a file that is already open (either from the Open Recent or Open.. menu items), the window that contains the file is brought to the front.

To accomplish this, we used the following code in our helper method:

var path = url.Path;

// Is the file already open?
for(int n=0; n<NSApplication.SharedApplication.Windows.Length; ++n) {
    var content = NSApplication.SharedApplication.Windows[n].ContentViewController as ViewController;
    if (content != null && path == content.FilePath) {
        // Bring window to front
        NSApplication.SharedApplication.Windows[n].MakeKeyAndOrderFront(this);
        return true;
    }
}

We designed our ViewController class to hold the path to the file in its Path property. Next, we loop through all currently open windows in the app. If the file is already open in one of the windows, its is brought to the front of all other windows using:

NSApplication.SharedApplication.Windows[n].MakeKeyAndOrderFront(this);

If no match is found, a new window is opened, the file loaded and the file is noted in the Open Recent menu:

// Get new window
var storyboard = NSStoryboard.FromName ("Main", null);
var controller = storyboard.InstantiateControllerWithIdentifier ("MainWindow") as NSWindowController;

// Display
controller.ShowWindow(this);

// Load the text into the window
var viewController = controller.Window.ContentViewController as ViewController;
viewController.Text = File.ReadAllText(path);
viewController.SetLanguageFromPath(path);
viewController.View.Window.SetTitleWithRepresentedFilename (Path.GetFileName(path));
viewController.View.Window.RepresentedUrl = url;

// Add document to the Open Recent menu
NSDocumentController.SharedDocumentController.NoteNewRecentDocumentURL(url);

Working with Custom Window Actions

Just like the built-in First Responder Actions that come prewired to standard Menu Items, you can create new, custom Actions and wire them to Menu Items in Interface Builder.

First, define a custom Action on one of your App's Windows Controllers. For example:

[Action("defineKeyword:")]
public void defineKeyword (NSObject sender) {
    // Preform some action when the menu is selected
    Console.WriteLine ("Request to define keyword");
}

Next, double-click the App's Storyboard file in the Solution Explorer to open it for editing in Xcode's Interface Builder. Select the First Responder under the Application Scene, then switch to the Attribute Inspector:

Click the Plus Button (+) at the bottom of the Attribute Inspector to add a new custom action:

Give it the same name as the custom Action that you created on your Window Controller:

Control-click and drag from a Menu Item to the First Responder under the Application Scene. From the Popup List, select the new Action you just created (defineKeyword: in this example):

Save the changes to the Storyboard and return to Visual Studio for Mac to sync the changes. If you run the App, the Menu Item that you connected the custom Action to will automatically be enabled/disabled (based on the Window with the Action being open) and selecting the Menu Item will fire off the Action:

Adding, Editing and Deleting Menus

As we have seen in the previous sections, a Xamarin.Mac application comes with a preset number of standard, default Menus and Menus Items that specific UI controls will automatically activate and respond to. We have also seen how to add code to our application that will also enable and respond to these default items.

In this section we will look at removing menu items that we don't need, reorganizing menus and adding new Menus, Menu Items and Actions.

Double-click the Main.storyboard file in the Solution Explorer to open it for editing:

For our specific Xamarin.Mac application we are not going to be using the default View menu so we are going to remove it. In the Interface Hierarchy select the View menu item that is a part of the main menu bar:

Press the Delete or Backspace key to delete the menu. Next, we aren't going to be using all of the items in the Format menu and we want to move the items we are going to use out from under the sub menus. In the Interface Hierarchy select the following menu items:

Drag the items under the parent Menu from the sub menu where they currently are:

Your Menu should now look like:

Next let's drag the Text sub menu out from under the Format menu and place it on the Main Menubar between the Format and Window menus:

Let's go back under the Format menu and delete the Font sub menu item. Next, select the Format Menu and rename it Font:

Next, let's create a custom menu of predefine phrases that will automatically get appended to the text in the Text View when they are selected. In the search box at the bottom on the Library Inspector type in menu, this will make it easier to find and work with all of the menu UI elements:

Now let's do the following to create our menu:

  1. Drag a Menu Item from the Library Inspector onto the Menubar between the Text and Window menus:

  2. Rename the item Phrases:

  3. Next drag a Menu from the Library Inspector:

  4. Drop then Menu on the new Menu Item we just created and change it's name to Phrases:

  5. Now let's rename the three default Menu Items Address, Date and Greeting:

  6. Let's add a fourth Menu Item by dragging a Menu Item from the Library Inspector and calling it Signature:

  7. Save the changes to the Menubar.

Now let's create a set of custom Actions so that our new Menu Items are exposed to C# code. In Xcode let's switch to the Assistant view:

Let's do the following:

  1. Control-drag from the Address Menu Item to the AppDelegate.h file.
  2. Switch the Connection type to Action:

  3. Enter a Name of phraseAddress and press the Connect button to create the new Action:

  4. Repeat the above steps for the Date, Greeting and Signature Menu Items:

  5. Save the changes to the Menubar.

Next we need to create an Outlet for our Text View so that we can adjust it's content from code. Select the ViewController.h file in the Assistant Editor and create a new Outlet called documentText:

Return to Visual Studio for Mac to sync the changes from Xcode. Next edit the ViewController.cs file and make it look like the following:

using System;

using AppKit;
using Foundation;

namespace MacMenus
{
    public partial class ViewController : NSViewController
    {
        #region Application Access
        public static AppDelegate App {
            get { return (AppDelegate)NSApplication.SharedApplication.Delegate; }
        }
        #endregion

        #region Computed Properties
        public override NSObject RepresentedObject {
            get {
                return base.RepresentedObject;
            }
            set {
                base.RepresentedObject = value;
                // Update the view, if already loaded.
            }
        }

        public string Text {
            get { return documentText.Value; }
            set { documentText.Value = value; }
        } 
        #endregion

        #region Constructors
        public ViewController (IntPtr handle) : base (handle)
        {
        }
        #endregion

        #region Override Methods
        public override void ViewDidLoad ()
        {
            base.ViewDidLoad ();

            // Do any additional setup after loading the view.
        }

        public override void ViewWillAppear ()
        {
            base.ViewWillAppear ();

            App.textEditor = this;
        }

        public override void ViewWillDisappear ()
        {
            base.ViewDidDisappear ();

            App.textEditor = null;
        }
        #endregion
    }
}

This exposes the text of our Text View outside of the ViewController class and informs the App Delegate when the Window gains or loses focus. Now edit the AppDelegate.cs file and make it look like the following:

using AppKit;
using Foundation;
using System;

namespace MacMenus
{
    [Register ("AppDelegate")]
    public partial class AppDelegate : NSApplicationDelegate
    {
        #region Computed Properties
        public ViewController textEditor { get; set;} = null;
        #endregion

        #region Constructors
        public AppDelegate ()
        {
        }
        #endregion

        #region Override Methods
        public override void DidFinishLaunching (NSNotification notification)
        {
            // Insert code here to initialize your application
        }

        public override void WillTerminate (NSNotification notification)
        {
            // Insert code here to tear down your application
        }
        #endregion

        #region Custom Actions
        [Export ("openDocument:")]
        void OpenDialog (NSObject sender)
        {
            var dlg = NSOpenPanel.OpenPanel;
            dlg.CanChooseFiles = false;
            dlg.CanChooseDirectories = true;

            if (dlg.RunModal () == 1) {
                var alert = new NSAlert () {
                    AlertStyle = NSAlertStyle.Informational,
                    InformativeText = "At this point we should do something with the folder that the user just selected in the Open File Dialog box...",
                    MessageText = "Folder Selected"
                };
                alert.RunModal ();
            }
        }

        partial void phrasesAddress (Foundation.NSObject sender) {

            textEditor.Text += "Xamarin HQ\n394 Pacific Ave, 4th Floor\nSan Francisco CA 94111\n\n";
        }

        partial void phrasesDate (Foundation.NSObject sender) {

            textEditor.Text += DateTime.Now.ToString("D");
        }

        partial void phrasesGreeting (Foundation.NSObject sender) {

            textEditor.Text += "Dear Sirs,\n\n";
        }

        partial void phrasesSignature (Foundation.NSObject sender) {

            textEditor.Text += "Sincerely,\n\nKevin Mullins\nXamarin,Inc.\n";
        }
        #endregion
    }
}

Here we've made the AppDelegate a partial class so that we can use the Actions and Outlets that we defined in Interface Builder. We also expose a textEditor to track which Window is currently in focus.

The following methods are used to handle our new, custom Menu and Menu Items:

partial void phrasesAddress (Foundation.NSObject sender) {

    if (textEditor == null) return;
    textEditor.Text += "Xamarin HQ\n394 Pacific Ave, 4th Floor\nSan Francisco CA 94111\n\n";
}

partial void phrasesDate (Foundation.NSObject sender) {

    if (textEditor == null) return;
    textEditor.Text += DateTime.Now.ToString("D");
}

partial void phrasesGreeting (Foundation.NSObject sender) {

    if (textEditor == null) return;
    textEditor.Text += "Dear Sirs,\n\n";
}

partial void phrasesSignature (Foundation.NSObject sender) {

    if (textEditor == null) return;
    textEditor.Text += "Sincerely,\n\nKevin Mullins\nXamarin,Inc.\n";
}

Now if we run our application, all of the items in the Phrase menu will be active and will add the give phrase to the Text View when selected:

Now that we have the basics of working with the Application Menu Bar down, let's look at creating a custom Contextual Menu.

Creating Menus from Code

In addition to creating Menus and Menu Items with Xcode's Interface Builder, there might be times when a Xamarin.Mac app needs to create, modify or remove a Menu, Sub Menu or Menu Item from code.

In the following example, a class is created to hold the information about the Menu Items and Sub Menus that will be dynamically created on-the-fly:

using System;
using System.Collections.Generic;
using Foundation;
using AppKit;

namespace AppKit.TextKit.Formatter
{
    public class LanguageFormatCommand : NSObject
    {
        #region Computed Properties
        public string Title { get; set; } = "";
        public string Prefix { get; set; } = "";
        public string Postfix { get; set; } = "";
        public List<LanguageFormatCommand> SubCommands { get; set; } = new List<LanguageFormatCommand>();
        #endregion

        #region Constructors
        public LanguageFormatCommand () {

        }

        public LanguageFormatCommand (string title)
        {
            // Initialize
            this.Title = title;
        }

        public LanguageFormatCommand (string title, string prefix)
        {
            // Initialize
            this.Title = title;
            this.Prefix = prefix;
        }

        public LanguageFormatCommand (string title, string prefix, string postfix)
        {
            // Initialize
            this.Title = title;
            this.Prefix = prefix;
            this.Postfix = postfix;
        }
        #endregion
    }
}

Adding Menus and Items

With this class defined, the following routine will parse a collection of LanguageFormatCommandobjects and recursively build new Menus and Menu Items by appending them to the bottom of the existing Menu (created in Interface Builder) that has been passed in:

private void AssembleMenu(NSMenu menu, List<LanguageFormatCommand> commands) {
    NSMenuItem menuItem;

    // Add any formatting commands to the Formatting menu
    foreach (LanguageFormatCommand command in commands) {
        // Add separator or item?
        if (command.Title == "") {
            menuItem = NSMenuItem.SeparatorItem;
        } else {
            menuItem = new NSMenuItem (command.Title);

            // Submenu?
            if (command.SubCommands.Count > 0) {
                // Yes, populate submenu
                menuItem.Submenu = new NSMenu (command.Title);
                AssembleMenu (menuItem.Submenu, command.SubCommands);
            } else {
                // No, add normal menu item
                menuItem.Activated += (sender, e) => {
                    // Apply the command on the selected text
                    TextEditor.PerformFormattingCommand (command);
                };
            }
        }
        menu.AddItem (menuItem);
    }
}

For any LanguageFormatCommand object that has a blank Title property, this routine creates a Separator Menu Item (a thin gray line) between Menu sections:

menuItem = NSMenuItem.SeparatorItem;

If a title is provided, a new Menu Item with that title is created:

menuItem = new NSMenuItem (command.Title);

If the LanguageFormatCommand object contains child LanguageFormatCommand objects, a Sub Menu is created and the AssembleMenu method is recursively called to build out that Menu:

menuItem.Submenu = new NSMenu (command.Title);
AssembleMenu (menuItem.Submenu, command.SubCommands);

For any new Menu Item that does not have Sub Menus, code is added to handle the Menu Item being selected by the user:

menuItem.Activated += (sender, e) => {
    // Do something when the menu item is selected
    ...
};

Testing the Menu Creation

With all of the above code in place, if the following collection of LanguageFormatCommand objects were created:

// Define formatting commands
FormattingCommands.Add(new LanguageFormatCommand("Stong","**","**"));
FormattingCommands.Add(new LanguageFormatCommand("Emphasize","_","_"));
FormattingCommands.Add(new LanguageFormatCommand("Inline Code","`","`"));
FormattingCommands.Add(new LanguageFormatCommand("Code Block","```\n","\n```"));
FormattingCommands.Add(new LanguageFormatCommand("Comment","<!--","-->"));
FormattingCommands.Add (new LanguageFormatCommand ());
FormattingCommands.Add(new LanguageFormatCommand("Unordered List","* "));
FormattingCommands.Add(new LanguageFormatCommand("Ordered List","1. "));
FormattingCommands.Add(new LanguageFormatCommand("Block Quote","> "));
FormattingCommands.Add (new LanguageFormatCommand ());

var Headings = new LanguageFormatCommand ("Headings");
Headings.SubCommands.Add(new LanguageFormatCommand("Heading 1","# "));
Headings.SubCommands.Add(new LanguageFormatCommand("Heading 2","## "));
Headings.SubCommands.Add(new LanguageFormatCommand("Heading 3","### "));
Headings.SubCommands.Add(new LanguageFormatCommand("Heading 4","#### "));
Headings.SubCommands.Add(new LanguageFormatCommand("Heading 5","##### "));
Headings.SubCommands.Add(new LanguageFormatCommand("Heading 6","###### "));
FormattingCommands.Add (Headings);

FormattingCommands.Add(new LanguageFormatCommand ());
FormattingCommands.Add(new LanguageFormatCommand("Link","[","]()"));
FormattingCommands.Add(new LanguageFormatCommand("Image","![](",")"));
FormattingCommands.Add(new LanguageFormatCommand("Image Link","[ ![](",")](LinkImageHere)"));

And that collection passed to the AssembleMenu function (with the Format Menu set as the base), the following dynamic Menus and Menu Items would be created:

Removing Menus and Items

If you need to remove any Menu or Menu Item from the app's User Interface, you can use the RemoveItemAt method of the NSMenu class simply by giving it the zero based index of the item to remove.

For example, to remove the Menus and Menu Items created by the routine above, you could use the following code:

public void UnpopulateFormattingMenu(NSMenu menu) {

    // Remove any additional items
    for (int n = (int)menu.Count - 1; n > 4; --n) {
        menu.RemoveItemAt (n);
    }
}

In the case of the code above, the first four Menu Items are created in Xcode's Interface Builder and aways available in the app, so they are not removed dynamically.

Contextual Menus

Contextual Menus appear when the user right-clicks or left-Control-clicks an item in a window. By default, several of the UI elements built into macOS already have contextual menus attached to them (such as the Text View). However, there might be times when we want to create our own custom Contextual Menus for a UI element that we have added to a window.

Let's edit our Main.storyboard file in Xcode and add a Window window to our design, set its Class to NSPanel in the Identity Inspector, Add a new Assistant item to the Window menu and attach it to the new window using a Show Segue:

Let's do the following:

  1. Drag a Label from the Library Inspector onto the Panel window and set its text to Property:

  2. Next drag a Menu from the Library Inspector onto the View Controller in the View Hierarchy and rename the three default menu items Document, Text and Font:

  3. Now control-drag from the Property Label onto the Menu:

  4. From the popup dialog, select Menu:

  5. From the Identity Inspector, set the View Controller's class to PanelViewController:

  6. Switch back to Visual Studio for Mac to sync, then return to Interface Builder.
  7. Switch to the Assistant Editor and select the PanelViewController.h file.
  8. Create an Action for the Document menu item called propertyDocument:

  9. Repeat creating Actions for the remaining menu items:

  10. Finally create an Outlet for the Property Label called propertyLabel:

  11. Save your changes and return to Visual Studio for Mac to sync with Xcode.

Edit the PanelViewController.cs file and add the following code:

partial void propertyDocument (Foundation.NSObject sender) {
    propertyLabel.StringValue = "Document";
}

partial void propertyFont (Foundation.NSObject sender) {
    propertyLabel.StringValue = "Font";
}

partial void propertyText (Foundation.NSObject sender) {
    propertyLabel.StringValue = "Text";
}

Now if we run the application and right-click on the Property label in the Panel, we'll see our custom Contextual Menu. If we select and item from the menu, the label's value will change:

Next let's look at creating Status Bar Menus.

Status Bar Menus

Status Bar Menus display a collection of status menu items that provide interaction with or feedback to the user, such as a menu or an image reflecting an application’s state. An application's Status Bar Menu is enabled and active even if the application is running in the background. The system-wide Status Bar resides at the right side of the Application Menu Bar and is the only Status Bar currently available in macOS.

Let's edit our AppDelegate.cs file and make the DidFinishLaunching method look like the following:

public override void DidFinishLaunching (NSNotification notification)
{
    // Create a Status Bar Menu
    NSStatusBar statusBar = NSStatusBar.SystemStatusBar;

    var item = statusBar.CreateStatusItem (NSStatusItemLength.Variable);
    item.Title = "Text";
    item.HighlightMode = true;
    item.Menu = new NSMenu ("Text");

    var address = new NSMenuItem ("Address");
    address.Activated += (sender, e) => {
        PhraseAddress(address);
    };
    item.Menu.AddItem (address);

    var date = new NSMenuItem ("Date");
    date.Activated += (sender, e) => {
        PhraseDate(date);
    };
    item.Menu.AddItem (date);

    var greeting = new NSMenuItem ("Greeting");
    greeting.Activated += (sender, e) => {
        PhraseGreeting(greeting);
    };
    item.Menu.AddItem (greeting);

    var signature = new NSMenuItem ("Signature");
    signature.Activated += (sender, e) => {
        PhraseSignature(signature);
    };
    item.Menu.AddItem (signature);
}

The NSStatusBar statusBar = NSStatusBar.SystemStatusBar; gives us access to the system-wide status bar. The var item = statusBar.CreateStatusItem (NSStatusItemLength.Variable); creates a new Status Bar Item. From there we create a Menu and a number of Menu Items and attach the menu to the Status Bar Item we just created.

If we run the application, the new Status Bar Item will be displayed. Selecting an item from the menu will change the text in the Text View:

Next, let's look at creating custom Dock Menu Items.

Custom Dock Menus

The Dock Menu appears for you Mac application when the user right-clicks or control-left-clicks the application's icon in the Dock:

Let's create a custom Dock Menu for our application by doing the following:

  1. In Visual Studio for Mac, right-click on the application's project and select Add > New File... From the New File dialog box, select Xamarin.Mac > Empty Interface Definition, use DockMenu for the Name and click the New button to create the new DockMenu.xib file:

  2. In the Solution Explorer, double-click the DockMenu.xib file to open it for editing in Xcode. Create a new Menu with the following items: Address, Date, Greeting and Signature

  3. Next, let's connect our new menu items to our existing Actions that we created for our custom menu in the Adding, Editing and Deleting Menus section above. Switch to the Connection Inspector and select the First Responder in the Interface Hierarchy. Scroll down and find the phraseAddress: Action. Drag a line from the circle on that Action to the Address Menu Item:

  4. Repeat for all of the other Menu Items attaching them to their corresponding Actions:

  5. Next, select the Application in the Interface Hierarchy. In the Connection Inspector, drag a line from the circle on the dockMenu Outlet to the Menu we just created:

  6. Save your changes and switch back to Visual Studio for Mac to sync with Xcode.
  7. Double-click the Info.plist file to open it for editing:

  8. Click the Source tab at the bottom of the screen:

  9. Click Add new entry, click the Green Plus, set the property name as AppleDockMenu and the value of DockMenu (the name of our new .xib file without the extension):

Now if we run our application and right-click on it's icon in the Dock, our new menu items will be displayed:

If we select one of the custom items from the menu, the text in our Text View will be modified.

Pop-up Button and Pull-Down Lists

A Pop-up Button displays a selected item and presents a list of options to select from when clicked by the user. A Pull-Down list is a type of Pop-up Button usually used for selecting commands specific to the context of the current task. Both can appear anywhere in a window.

Let's create a custom Pop-up Button for our application by doing the following:

  1. Edit the Main.storyboard file in Xcode and drag a Popup Button from the Library Inspector onto the Panel window we created in the Contextual Menus section:

  2. Add a new Menu Item and set the titles of the Items in the Popup to: Address, Date, Greeting and Signature

  3. Next, let's connect our new menu items to our existing Actions that we created for our custom menu in the Adding, Editing and Deleting Menus section above. Switch to the Connection Inspector and select the First Responder in the Interface Hierarchy. Scroll down and find the phraseAddress: Action. Drag a line from the circle on that Action to the Address Menu Item:

  4. Repeat for all of the other Menu Items attaching them to their corresponding Actions:

  5. Save your changes and switch back to Visual Studio for Mac to sync with Xcode.

Now if we run our application and select an item from the popup, the text in our Text View will change:

You can create and work with Pull-Down Lists in the exact same way as Pop-up Buttons. Instead of attaching to existing Actions, you could create your own custom Actions just like we did for our Contextual Menu in the Contextual Menus section.

Summary

In this article has taken a detailed look at working with Menus and Menu Items in a Xamarin.Mac application. First we examined the Application's Menu Bar, then we looked at creating Contextual Menus, next we examined Status Bar Menus and Custom Dock Menus. Finally, we covered Pop-up Menus and Pull-Down Lists.

Xamarin Workbook

If it's not already installed, install the Xamarin Workbooks app first. The workbook file should download automatically, but if it doesn't, just click to start the workbook download manually.