Dialogs Using Dialogs and Modal Windows in a Xamarin.Mac application Contents This article will cover the following topics in detail: Introduction to Dialogs Adding a Modal Window to a Project Creating a Custom Sheet Loading UI from a XIB Opening and Closing the Sheet Using the Sheet Creating a Preferences Dialog Displaying a New Preference View Displaying the First View and Switching Views Displaying the Preference Window The Open Dialog The Print and Page Setup Dialogs The Save Dialog Overview When working with C# and .NET in a Xamarin.Mac application, you have access to the same Dialogs and Modal Windows 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 Modal Windows (or optionally create them directly in C# code). A dialog appears in response to a user action and typically provides ways users can complete the action. A dialog requires a response from the user before it can be closed. Windows can be used in a Modeless state (such as a text editor that can have multiple documents open at once) or Modal (such as an Export dialog that must be dismissed before the application can continue). In this article, we'll cover the basics of working with Dialogs and Modal Windows 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. Introduction to Dialogs A dialog appears in response to a user action (such as saving a file) and provides a way for users to complete that action. A dialog requires a response from the user before it can be closed. According to Apple, there are three ways to present a Dialog: Document Modal - A Document Modal dialog prevents the user from doing anything else within a given document until it is dismissed. App Modal - An App Modal dialog prevents the user from interacting with the application until it is dismissed. Modeless A Modeless Dialog enables users to change settings in the dialog while still interacting with the document window. Modal Window Any standard NSWindow can be used as a customized dialog by displaying it modally: Document Modal Dialog Sheets A Sheet is a modal dialog that is attached to a given document window, preventing users from interacting with the window until they dismiss the dialog. A Sheet is attached to the window from which it emerges and only one sheet can be open for a window at any one time. Preferences Windows A Preferences Window is a modeless dialog that contains the application's settings that the user changes infrequently. Preferences Windows often include a Toolbar that allows the user to switch between different groups of settings: Open Dialog The Open Dialog gives users a consistent way to find and open an item in an application: Print and Page Setup Dialogs OS X provides standard Print and Page Setup Dialogs that your application can display so that users can have a consistent printing experience in every application they use. The Print Dialog can be displayed as both a free floating dialog box: Or it can be displayed as a Sheet: The Page Setup Dialog can be displayed as both a free floating dialog box: Or it can be displayed as a Sheet: Save Dialogs The Save Dialog gives users a consistent way to save an item in an application. The Save Dialog has two states: Minimal (also known as Collapsed): And the Expanded state: The Minimal Save Dialog can also be displayed as a Sheet: As can the Expanded Save Dialog: For more information, see the Dialogs section of Apple's OS X Human Interface Guidelines Adding a Modal Window to a Project Aside from the main document window, a Xamarin.Mac application might need to display other types of windows to the user, such as Preferences or Inspector Panels. To add a new window, do the following: 1. In the Solution Explorer, right-click on the Project and select Add > New File... 2. In the New File dialog box, select Xamarin.Mac > Cocoa Window with Controller: 3. Enter CustomDialog for the Name and click the New button. 4. Double-click the CustomDialog.xib file to open it for editing in Interface Builder: 5. Design your interface: 6. Wire-up any Actions and Outlets: 7. Save your changes and return to Xamarin Studio to sync with Xcode. Add the following code to the MainWindow.cs file to open the window modally: [Export ("showDialog:")] void ShowDialog (NSObject sender) { // Load the new window var dialog = new CustomDialogController (); // Display the window modally NSApplication.SharedApplication.RunModalForWindow (dialog.Window); } Next, add this code to the CustomDialog.cs file to close the Modal Dialog: [Export ("dialogClose:")] void DialogClose (NSObject sender) { NSApplication.SharedApplication.StopModal (); this.Close (); } [Export ("dialogCancel:")] void DialogCancel (NSObject sender) { NSApplication.SharedApplication.AbortModal (); this.Close (); } The NSApplication.SharedApplication.StopModal () method is used for the OK action of the dialog, the NSApplication.SharedApplication.AbortModal () method is used for the Cancel action. We can run our application and display the custom dialog: For more information about using windows in a Xamarin.Mac application, please see our Working with Windows documentation. Creating a Custom Sheet A Sheet is a modal dialog that is attached to a given document window, preventing users from interacting with the window until they dismiss the dialog. A Sheet is attached to the window from which it emerges and only one sheet can be open for a window at any one time. To create a Custom Sheet in Xamarin.Mac, let's do the following: 1. In the Solution Explorer, right-click on the Project and select Add > New File... 2. In the New File dialog box, select Xamarin.Mac > Cocoa Window: 3. Enter LoginSheet for the Name and click the New button. 4. Edit the LoginSheet.cs file and change the class definition to the following: public partial class LoginSheet : NSPanel 5. Double-click the CustomDialog.xib file to open it for editing in Interface Builder: 6. Delete the existing Window. 7. Drag a Panel from the Library Inspector to the Interface Editor: 8. Switch to the Identity Inspector and enter LoginSheet for the Class: 9. Switch to the Attribute Inspector and make all of the windows properties look like the following: 10. Design your user interface: 11. Open the Assistant View, select the LoginScreen.h file and create an outlet for your sheet's window: 12. Create any required Actions and Outlets for your UI elements. 13. Save your changes and return to Xamarin Studio to sync changes. 14. In the Solution Explorer, right-click on the Project and select Add > New File... 15. In the New File dialog box, select General > Empty Class: 16. Enter LoginSheetController for the Name and click the New button. Next, edit the LoginSheetController.cs file and make it look like the following: using System; using Foundation; using AppKit; namespace MacWindows { public class LoginSheetController : NSObject { #region Computed Properties [Export("window")] public LoginSheet Window { get; set;} [Outlet] public NSTextField loginPassword { get; set; } [Outlet] public NSTextField loginUser { get; set; } public bool Canceled { get; set;} public string UserID { get { return loginUser.StringValue; } set { loginUser.StringValue = value; } } public string Password { get { return loginPassword.StringValue; } set { loginPassword.StringValue = value; } } #endregion #region Constructors public LoginSheetController () { // Load the .xib file for the sheet NSBundle.LoadNib ("LoginSheet", this); } #endregion #region Public Methods public void ShowSheet(NSWindow inWindow) { NSApplication.SharedApplication.BeginSheet (Window, inWindow); } public void CloseSheet() { NSApplication.SharedApplication.EndSheet (Window); Window.Close(); } #endregion #region Button Handlers [Export ("loginCancel:")] void LoginCancel (NSObject sender) { Canceled = true; CloseSheet(); RaiseLoginCanceled (); } [Export ("loginOK:")] void LoginOK (NSObject sender) { Canceled = false; CloseSheet(); RaiseLoginRequested (); } #endregion #region Events public delegate void LoginCanceledDelegate(); public event LoginCanceledDelegate LoginCanceled; internal void RaiseLoginCanceled() { if (this.LoginCanceled != null) { this.LoginCanceled(); } } public delegate void LoginRequestedDelegate(string userID, string password); public event LoginRequestedDelegate LoginRequested; internal void RaiseLoginRequested() { if (this.LoginRequested != null) { this.LoginRequested(UserID, Password); } } #endregion } } Before we use our new sheet, let's look at a few important concepts here: Loading UI from a XIB Before you can load a User Interface from a .xib file, the class that you are attempting to load from must inherit from NSObject (or from a base class that does) and there must a window property to accept the loaded UI. The following code does this: public class LoginSheetController : NSObject ... [Export("window")] public LoginSheet Window { get; set;} ... public LoginSheetController () { // Load the .xib file for the sheet NSBundle.LoadNib ("LoginSheet", this); } Because LoginSheetController class spawns our LoginSheet panel, it becomes the target of any Outlets or Actions that we defined in Interface Builder, not the LoginSheet class. So we need to define our Outlets or Actions in the LoginSheetController.cs file: [Outlet] public NSTextField loginPassword { get; set; } [Outlet] public NSTextField loginUser { get; set; } ... [Export ("loginCancel:")] void LoginCancel (NSObject sender) { ... } [Export ("loginOK:")] void LoginOK (NSObject sender) { ... } Opening and Closing the Sheet We use the NSApplication.SharedApplication object to display our sheet on a given window and to detach it when we are finished: public void ShowSheet(NSWindow inWindow) { NSApplication.SharedApplication.BeginSheet (Window, inWindow); } public void CloseSheet() { NSApplication.SharedApplication.EndSheet (Window); Window.Close(); } Note that we still have to call the Close method on the Sheet's Window to actually remove it from the screen. Using the Sheet With the Sheet's design created in Interface Builder, wired-up to the required Actions and Outlets and our controller class created, we are ready to use the sheet. The following code displays the Sheet attached to a window: var sheet = new LoginSheetController (); sheet.LoginRequested += (userID, password) => { Console.WriteLine("User ID: {0}, Password: {1}", userID, password); }; // Where "this" is an NSWindow sheet.ShowSheet (this); If we run our application and open the Sheet, it will be attached to the window: Note: If your Sheet appears detached from your window, double check that you have all of the properties in the Attribute Inspector set exactly as shown in the code above. Creating a Preferences Dialog To add a new window, do the following: 1. In the Solution Explorer, right-click on the Project and select Add > New File... 2. In the New File dialog box, select Xamarin.Mac > Cocoa Window with Controller: 3. Enter PreferencesWindow for the Name and click the New button. 4. Edit the PreferencesWindow.cs file and change the class definition to the following: public partial class PreferencesWindow : NSPanel 5. Double-click the PreferencesWindow.xib file to open it for editing in Interface Builder: 6. Design your interface using a by adding a Toolbar and several Toolbar Items to act as tabs: 7. For each Toolbar Item make sure that you check Selectable and give it a unique Identifier: 8. Ensure that your Toolbar isn't customizable: 9. Lock the size of your Window: 10. Add a Custom View just below the Toolbar, make it fill the rest of the content area, and set it to shrink and grow with the window: 11. Wire-up Actions for all of your Toolbar Items and create an Outlet for the Toolbar and the Custom View: 12. Save your changes and return to Xamarin Studio to sync with Xcode. Now we will need to create custom views for each of our Toolbar Items. We'll be keeping these in separate files to making them easy to maintain so you'll need to do the follow steps for each tab needed: 1. In the Solution Explorer, right-click on the Project and select Add > New File... 2. In the New File dialog box, select Xamarin.Mac > Cocoa Window with Controller: 3. Enter PreferencesGlobal for the Name and click the New button. 4. Double-click the PreferencesGlobal.xib file to open it for editing in Interface Builder and make the view the size of the panelContainer we created above: 5. Design your UI for the given preference panel: 6. Wire-up and needed Actions and Outlets. 7. Save your changes and return to Xamarin Studio to sync with Xcode. 8. Repeat for each Toolbar Item. Next, let's modify the PreferencesWindow.cs file to automatically select the first Toolbar item and display that's items information when the window is first opened. We'll also add the code to display each tab. Make the PreferencesWindow.cs file look like the following: using System; using Foundation; using AppKit; using CoreGraphics; namespace MacWindows { public partial class PreferencesWindow : NSPanel { #region Private Variables private NSViewController _subviewController = null; private NSView _subview = null; #endregion #region Constructors public PreferencesWindow (IntPtr handle) : base (handle) { } [Export ("initWithCoder:")] public PreferencesWindow (NSCoder coder) : base (coder) { } #endregion #region Private Methods private void ShowPanel(NSViewController controller) { // Is there a view already being displayed? if (_subview != null) { // Yes, remove it from the view _subview.RemoveFromSuperview (); // Release memory _subview = null; _subviewController = null; } // Save values _subviewController = controller; _subview = controller.View; // Define frame and display _subview.Frame = new CGRect (0, 0, panelContainer.Frame.Width, panelContainer.Frame.Height); panelContainer.AddSubview (_subview); } #endregion #region Override Methods public override void AwakeFromNib () { base.AwakeFromNib (); // Automatically select the first item mainToolbar.SelectedItemIdentifier = "global"; ShowPanel (new preferenceGlobalController ()); } #endregion #region Toolbar Handlers [Export ("preferencesProfile:")] void PreferencesProfile (NSObject sender) { mainToolbar.SelectedItemIdentifier = "profile"; ShowPanel (new PreferencesProfileController ()); } [Export ("preferencesGlobal:")] void PreferencesGlobal (NSObject sender) { mainToolbar.SelectedItemIdentifier = "global"; ShowPanel (new preferenceGlobalController ()); } [Export ("preferencesKeyboard:")] void PreferencesKeyboard (NSObject sender) { mainToolbar.SelectedItemIdentifier = "keyboard"; ShowPanel (new PreferencesKeyboardController ()); } [Export ("preferencesVIOP:")] void PreferencesVOIP (NSObject sender) { mainToolbar.SelectedItemIdentifier = "voip"; ShowPanel (new PreferencesVOIPController ()); } #endregion } } Before we use the preference window, let's look at a few important concepts here: Displaying a New Preference View When the user selects a different preference tab (Toolbar Item) we need to switch the view displayed to the one that the user is working with. The following code handles that: private NSViewController _subviewController = null; private NSView _subview = null; ... private void ShowPanel(NSViewController controller) { // Is there a view already being displayed? if (_subview != null) { // Yes, remove it from the view _subview.RemoveFromSuperview (); // Release memory _subview = null; _subviewController = null; } // Save values _subviewController = controller; _subview = controller.View; // Define frame and display _subview.Frame = new CGRect (0, 0, panelContainer.Frame.Width, panelContainer.Frame.Height); panelContainer.AddSubview (_subview); } It looks to see it we already have a view displayed, if so it removes it from the screen. Next it takes the view that has been passed in (as loaded from a View Controller) resizes it to fit in the Content Area and adds it to the content for display. Displaying the First View and Switching Views When the Preference Window is first displayed, we need to select the first Toolbar Item and display the first item's view. This code handles that: // Automatically select the first item mainToolbar.SelectedItemIdentifier = "global"; ShowPanel (new preferenceGlobalController ()); Note that global is the unique Identifier that we assigned the Toolbar Item in Interface Builder. As each Toolbar Item is selected by the user, the following Actions handle switching the view: [Export ("preferencesProfile:")] void PreferencesProfile (NSObject sender) { mainToolbar.SelectedItemIdentifier = "profile"; ShowPanel (new PreferencesProfileController ()); } [Export ("preferencesGlobal:")] void PreferencesGlobal (NSObject sender) { mainToolbar.SelectedItemIdentifier = "global"; ShowPanel (new preferenceGlobalController ()); } [Export ("preferencesKeyboard:")] void PreferencesKeyboard (NSObject sender) { mainToolbar.SelectedItemIdentifier = "keyboard"; ShowPanel (new PreferencesKeyboardController ()); } [Export ("preferencesVIOP:")] void PreferencesVOIP (NSObject sender) { mainToolbar.SelectedItemIdentifier = "voip"; ShowPanel (new PreferencesVOIPController ()); } Displaying the Preference Window Add the following code to AppDelegate.cs display your preference window: [Export("applicationPreferences:")] void ShowPreferences (NSObject sender) { var preferences = new PreferencesWindowController (); preferences.Window.MakeKeyAndOrderFront (this); } If we run the code and select the Preferences... from the Application Menu, the window will be displayed: For more information on working with Windows and Toolbars, please see our Windows and Toolbars documentation. The Open Dialog The Open Dialog gives users a consistent way to find and open an item in an application. To display an Open Dialog in a Xamarin.Mac application, use the following code: 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) { var path = url.Path; // Create a new window to hold the text var newWindowController = new MainWindowController (); newWindowController.Window.MakeKeyAndOrderFront (this); // Load the text into the window var window = newWindowController.Window as MainWindow; window.Text = File.ReadAllText(path); window.SetTitleWithRepresentedFilename (Path.GetFileName(path)); window.RepresentedUrl = url; } } In the above code, we are opening a new document window to display the contents of the file. You'll need to replace this code with functionality is required by your application. The following properties are available when working with a NSOpenPanel: CanChooseFiles - If true the user can select files. CanChooseDirectories - If true the user can select directories. AllowsMultipleSelection - If true the user can select more than one file at a time. ResolveAliases - If true selecting and alias, resolves it to the original file's path. The RunModal () method displays the Open Dialog and allow the user to select files or directories (as specified by the properties) and returns 1 if the user clicks the Open button. The Open Dialog returns the user's selected files or directories as an array of URLs in the URL property. If we run the program and select the Open... item from the File menu, the following is displayed: The Print and Page Setup Dialogs OS X provides standard Print and Page Setup Dialogs that your application can display so that users can have a consistent printing experience in every application they use. The following code will show the standard Print Dialog: public bool ShowPrintAsSheet { get; set;} = true; ... [Export ("showPrinter:")] void ShowDocument (NSObject sender) { var dlg = new NSPrintPanel(); // Display the print dialog as dialog box if (ShowPrintAsSheet) { dlg.BeginSheet(new NSPrintInfo(),this,this,null,new IntPtr()); } else { if (dlg.RunModalWithPrintInfo(new NSPrintInfo()) == 1) { var alert = new NSAlert () { AlertStyle = NSAlertStyle.Critical, InformativeText = "We need to print the document here...", MessageText = "Print Document", }; alert.RunModal (); } } } If we set the ShowPrintAsSheet property to false, run the application and display the print dialog, the following will be displayed: If set the ShowPrintAsSheet property to true, run the application and display the print dialog, the following will be displayed: The following code will display the Page Layout Dialog: [Export ("showLayout:")] void ShowLayout (NSObject sender) { var dlg = new NSPageLayout(); // Display the print dialog as dialog box if (ShowPrintAsSheet) { dlg.BeginSheet (new NSPrintInfo (), this); } else { if (dlg.RunModal () == 1) { var alert = new NSAlert () { AlertStyle = NSAlertStyle.Critical, InformativeText = "We need to print the document here...", MessageText = "Print Document", }; alert.RunModal (); } } } If we set the ShowPrintAsSheet property to false, run the application and display the print layout dialog, the following will be displayed: If set the ShowPrintAsSheet property to true, run the application and display the print layout dialog, the following will be displayed: For more information about working with the Print and Page Setup Dialogs, please see Apple's NSPrintPanel, NSPageLayout and Introduction to Printing documentation. The Save Dialog The Save Dialog gives users a consistent way to save an item in an application. The following code will show the standard Save Dialog: public bool ShowSaveAsSheet { get; set;} = true; ... [Export("saveDocumentAs:")] void ShowSaveAs (NSObject sender) { var dlg = new NSSavePanel (); dlg.Title = "Save Text File"; if (ShowSaveAsSheet) { dlg.BeginSheet(mainWindowController.Window,(result) => { var alert = new NSAlert () { AlertStyle = NSAlertStyle.Critical, InformativeText = "We need to save the document here...", MessageText = "Save Document", }; alert.RunModal (); }); } else { if (dlg.RunModal () == 1) { var alert = new NSAlert () { AlertStyle = NSAlertStyle.Critical, InformativeText = "We need to save the document here...", MessageText = "Save Document", }; alert.RunModal (); } } } If we set the ShowSaveAsSheet property to false, run the application and select Save As... from the File menu, the following will be displayed: The user can expand the dialog: If we set the ShowSaveAsSheet property to true, run the application and select Save As... from the File menu, the following will be displayed: The user can expand the dialog: For more more information on working with the Save Dialog, please see Apple's NSSavePanel documentation. Summary This article has taken a detailed look at working with Modal Windows, Sheets and the standard system Dialog Boxes in a Xamarin.Mac application. We saw the different types and uses of Modal Windows, Sheets and Dialogs, how to create and maintain Modal Windows and Sheets in Xcode's Interface Builder and how to work with Modal Windows, Sheets and Dialogs in C# code.
© Copyright 2024