Xamarin.Forms is primarily known as a framework to build cross-platform mobile apps while it actually covers a far wider range of platforms, including Windows and macOS. This post will show how this can be utilized to create applications that life in the tray bar for Windows and macOS and share a single code base written in Xamarin.Forms. If you want to get started right away, you can just install my template from NuGet using the dotnet CLI:
Before we start, why are we doing this?
There are certainly other solutions to write tray applications across multiple platforms. You could use Electron or write dedicated native or Xamarin applications for each platform. Compared to these approaches, Xamarin.Forms provides some unique benefits that come quite handy for this use case:
- All code can be written in C# or F#. Whether it is shared code or something platform specific.
- The UI can get written in either C# or XAML. While Xamarin.Forms does not use the same XAML dialect known from WPF or UWP, most code can get ported quite easily which would allow to port an existing Windows only tray application to macOS.
- Xamarin provides full access to the platform SDKs which allows for scenarios where other frameworks like Electron require extensions written in the native languages. This can be specially useful when writing applications that interact with the system.
You need a device for each platform to write the startup code, platform specific adjustments or simply test the application.
- Visual Studio 2019 with the “.NET desktop development” and “Mobile development with .NET” workloads
- An icon in the .ico format and resolution 32*32 for Windows and 18*18 for macOS.
Anatomy of a Xamarin.Forms tray application
A tray application written in Xamarin.Forms is quite similar to a traditional Xamarin.Forms app. All shared code sits in a shared .NETStandard project, for example the main page which is shown when opening the application from the tray bar. Xamarin.Forms normally does all the setup to correctly show the content in the application, this is the part we have to do by our own.
The whole setup can be separated in three steps:
- Setting up the project with the shared Xamarin.Forms content
- Building the macOS tray icon (or menu bar item to be more precisely)
- Building the Windows tray icon
After doing these steps, you will have a project where you can develop the application as you would do with any Xamarin.Forms application.
Setting up the project and shared content
The sample code for this part can be found here. Besides the described content, it contains a more extensive sample calculator page to present the UI capabilities of Xamarin.Forms.
Get started by creating a solution with three projects:
- .NETStandard 2.0: A shared project which will host all shared ui and logic.
NuGet: Xamarin.Forms (4.3.*+)
- Xamarin.macOS: The MacOS project which will contain the setup logic for macOS and platform specific logic.
References: The shared project, NuGet: Xamarin.Forms (4.3.*+)
- WPF: The Windows project which will contain the setup logic for macOS and platform specific logic. You cant use .NET Core here currently since Xamarin.Forms is not compatible currently, but a PR is already on its way!
References: The shared project, NuGet: Xamarin.Forms (4.3.*+), Xamarin.Forms.Platform.WPF (4.3.*+)
Make sure, that all Xamarin.Forms packages are the same version.
Why do we chose WPF instead of UWP? Chosing UWP would have the benefit, that the platform implementation for Xamarin.Forms is way more mature compared to WPF, which is only community supported. I tried both and while we can directly implement the tray bar functionality in WPF, UWP requires a WPF or WinForms host that launches the UWP application. This makes the WPF tray application way more flexible and easier to setup.
As any normal Xamarin.Forms application, our shared project needs to contain a class that inherits the Xamarin.Forms.Application class, where we can do our application setup and assign the MainPage. If you want to have navigation in your application, set a NavigationPage as MainPage.
The sample code for the windows platform can be found here.
The Windows implementation requires the following steps:
- Add an icon in the .ico format and 32*32 resolution to the application Resources (Properties/Resources.resx).
- Add a reference to the PresentationFramework. It contains the NotifyIcon class we will utilize to create the tray icon.
- Remove the MainWindow.xaml and .xaml.cs and remove StartupUri=”MainWindow.xaml” from the App.xaml file.
The whole setup is done in the App.cs of the WPF application. I will go trough the steps to create, toggle and exit the application now but you can just head to the complete sample if you want, the code is quite self explanatory.
Start by initializing Xamarin.Forms and create the NotifyIcon instance. We add event handler to MouseUp and MouseMove to correctly toggle the window later. Besides the NotifyIcon, we create a context menu which will open when we right click the tray icon and we can use to exit the application. When exiting, we dispose everything and exit the application.
When toggling the window for the first time, we have to initially create the Window, since we are working with Xamarin.Forms we can utilize the FormsApplicationPage here and call LoadApplication with a new instance of our Application implementation.
Now that the window is initialized, we can toggle the visibility based on the current visibility of the window. When the window gets shown, we should set position based on where top-left corner of the window should be. We can just assume, that the taskbar is positioned at the bottom and the tray icons will sit at the right side. The sample repository contains more advanced code which calculates the position based on the tray icon position and taskbar orientation.
When toggling the window we can call the lifecycle events of the Application implementation: SendStart, SendSleep and SendResume.
We registered multiple event handlers to call the toggle method.
- When the tray icon is left clicked (right click will open the context menu by default).
- When the close button of the window is clicked: We intercept the close method to avoid exiting the application and toggle the window instead.
- When clicked outside the window (see below).
The macOS specific code can be found here.
macOS requires some preparation as well:
- Since we don't want to show the initial window from the storyboard, uncheck the Is Initial Controller from the start window.
- A tray application normally does not show up in the dock. To get this behavior, we have to modify the Info.plist and set the application as agent.
- Add the icon in the .ico format with a 18*18 resolution to the Resources folder with the build action “BundleResource”.
Now that the project is set up, we can write the startup logic:
As on Windows, we initialize Xamarin.Forms but this time we have to set our Application implementation as the application instance ourself. On Windows, this was done by the FormsApplicationPage we utilized. Next up is the setup of the status item.
On macOS, we retrieve the status bar instance and create a status bar item for it. The button will only raise its Activated event on left clicks by default, but we can change that behavior by calling SendActionOn to subscribe for the “other” click. Besides the status item we already create the context menu which will contain the close button.
In the activated event handler the actual event has to be retrieved first and can then get evaluated to show the window or opening the menu.
When finally showing the window, on macOS we have to create the ViewController from the Xamarin.Forms page our self by using the Xamarin.Forms CreateViewController method. These methods to create native views from Xamarin.Forms pages exist for all platforms to allow to embed these pages into native Xamarin applications, as we do now. Another drawback of creating the ViewController our self is that the navigation bar is not created correctly. If you need a navigation bar, you can create your own window with a custom navigation bar and wrap the created view in there. A sample for this is included in the repository.
Opening the created ViewController is way easier on macOS compared to Windows as we can just use the NSPopover class which provides parameters to easily position the popover under the menu item.
When creating or reusing the ViewController we can call the Application lifecycle events for the start and resume of the application. To correctly raise the sleep lifecycle event we have to implement a custom NSPopoverDelegate where we can override DidClose to call SendSleep.
Thats it! If you have any questions or if I missed anything, feel free to reach out to me on Twitter.