mirror of
https://github.com/LANCommander/Notify.NET.git
synced 2026-08-01 03:08:24 -04:00
382 lines
12 KiB
Markdown
382 lines
12 KiB
Markdown
|
|
# Notify.NET
|
||
|
|
|
||
|
|
A cross-platform .NET Standard library for displaying OS notifications. Provides a single
|
||
|
|
fluent API that dispatches to the native notification system on each supported platform.
|
||
|
|
|
||
|
|
| Platform | Backend | Minimum OS |
|
||
|
|
|----------|---------|------------|
|
||
|
|
| Windows | [WinToastLib](https://github.com/mohabouje/WinToast) via a thin C++ wrapper DLL | Windows 8 |
|
||
|
|
| Linux | libnotify via P/Invoke | Any distribution with a D-Bus notification daemon |
|
||
|
|
| macOS | UNUserNotificationCenter via a thin Objective-C wrapper dylib | macOS 10.14 (Mojave) |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Installation
|
||
|
|
|
||
|
|
```
|
||
|
|
dotnet add package Notify.NET
|
||
|
|
```
|
||
|
|
|
||
|
|
The NuGet package includes the pre-compiled native libraries for all supported platforms.
|
||
|
|
No separate native installation is required.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Quick start
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
using Notify.NET.Builder;
|
||
|
|
using Notify.NET.Extensions;
|
||
|
|
|
||
|
|
using var service = ServiceCollectionExtensions.CreateNotificationService(opts =>
|
||
|
|
{
|
||
|
|
opts.AppName = "My App";
|
||
|
|
opts.AppUserModelId = "MyCompany.MyApp"; // Windows only; ignored on other platforms
|
||
|
|
});
|
||
|
|
|
||
|
|
if (service.IsSupported)
|
||
|
|
{
|
||
|
|
long id = await service.ShowAsync(
|
||
|
|
NotificationBuilder.Create("Hello")
|
||
|
|
.WithBody("Notify.NET is working.")
|
||
|
|
.Build());
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Creating the service
|
||
|
|
|
||
|
|
### Without a DI container
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
using var service = ServiceCollectionExtensions.CreateNotificationService(opts =>
|
||
|
|
{
|
||
|
|
opts.AppName = "My App";
|
||
|
|
opts.AppUserModelId = "MyCompany.MyApp";
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
`CreateNotificationService` selects the correct implementation for the current OS
|
||
|
|
automatically. On unsupported platforms it returns a no-op service where `IsSupported`
|
||
|
|
is `false`.
|
||
|
|
|
||
|
|
### With Microsoft.Extensions.DependencyInjection
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
services.AddNotifications(opts =>
|
||
|
|
{
|
||
|
|
opts.AppName = "My App";
|
||
|
|
opts.AppUserModelId = "MyCompany.MyApp";
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
This registers `INotificationService` as a singleton. Resolve it normally:
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
var service = provider.GetRequiredService<INotificationService>();
|
||
|
|
```
|
||
|
|
|
||
|
|
### Checking platform support
|
||
|
|
|
||
|
|
Always check `IsSupported` before calling `ShowAsync`. On unsupported platforms or when
|
||
|
|
initialisation fails (e.g. the user denied notification permission on macOS), `ShowAsync`
|
||
|
|
throws `PlatformNotSupportedException`.
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
if (!service.IsSupported)
|
||
|
|
{
|
||
|
|
Console.WriteLine("Notifications are not available on this platform.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Builder API
|
||
|
|
|
||
|
|
All notification configuration is done through `NotificationBuilder`. The `Build()` call
|
||
|
|
produces an immutable `NotificationRequest` that can be passed to `ShowAsync`.
|
||
|
|
|
||
|
|
### Title and body
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
var request = NotificationBuilder.Create("Title goes here")
|
||
|
|
.WithBody("Optional body text goes here.")
|
||
|
|
.Build();
|
||
|
|
```
|
||
|
|
|
||
|
|
### Image
|
||
|
|
|
||
|
|
Pass an absolute path to an image file. Relative paths are resolved to absolute paths
|
||
|
|
at call time; if the file does not exist the notification is shown without an image
|
||
|
|
rather than failing.
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
var request = NotificationBuilder.Create("New photo")
|
||
|
|
.WithBody("A photo has arrived.")
|
||
|
|
.WithImage("/home/user/photos/latest.jpg")
|
||
|
|
.Build();
|
||
|
|
```
|
||
|
|
|
||
|
|
Supported formats depend on the platform (PNG and JPEG work on all three).
|
||
|
|
|
||
|
|
### Action buttons
|
||
|
|
|
||
|
|
Up to five action buttons can be added. Each button has a label and an optional click
|
||
|
|
callback that receives the notification ID.
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
var request = NotificationBuilder.Create("Update available")
|
||
|
|
.WithBody("Version 2.0 is ready.")
|
||
|
|
.AddButton("Install now", id => Installer.Run())
|
||
|
|
.AddButton("Remind me later", id => ScheduleReminder())
|
||
|
|
.AddButton("Skip this version", null)
|
||
|
|
.Build();
|
||
|
|
```
|
||
|
|
|
||
|
|
### Lifecycle callbacks
|
||
|
|
|
||
|
|
Register delegates for specific notification events:
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
var request = NotificationBuilder.Create("Download complete")
|
||
|
|
.WithBody("report.pdf has been saved.")
|
||
|
|
.OnActivated(id => OpenFile("report.pdf"))
|
||
|
|
.OnDismissed((id, reason) =>
|
||
|
|
{
|
||
|
|
if (reason == DismissReason.UserCancelled)
|
||
|
|
Console.WriteLine("User dismissed the notification.");
|
||
|
|
})
|
||
|
|
.OnFailed(id => Console.WriteLine($"Notification {id} could not be displayed."))
|
||
|
|
.Build();
|
||
|
|
```
|
||
|
|
|
||
|
|
For more complex cases, implement `INotificationHandler` and attach it with `WithHandler`:
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
public sealed class DownloadHandler : INotificationHandler
|
||
|
|
{
|
||
|
|
public void OnActivated(long id) => OpenDownloadFolder();
|
||
|
|
public void OnButtonActivated(long id, int i) => HandleButton(i);
|
||
|
|
public void OnDismissed(long id, DismissReason r) { }
|
||
|
|
public void OnFailed(long id) => LogError(id);
|
||
|
|
}
|
||
|
|
|
||
|
|
var request = NotificationBuilder.Create("Download complete")
|
||
|
|
.WithHandler(new DownloadHandler())
|
||
|
|
.Build();
|
||
|
|
```
|
||
|
|
|
||
|
|
`WithHandler` takes precedence over any delegate callbacks registered on the same builder.
|
||
|
|
|
||
|
|
### Urgency
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
// Low priority — the platform may suppress or delay it
|
||
|
|
NotificationBuilder.Create("FYI").WithUrgency(NotificationUrgency.Low)
|
||
|
|
|
||
|
|
// Critical — bypasses Do Not Disturb where the platform supports it
|
||
|
|
NotificationBuilder.Create("Disk full").WithUrgency(NotificationUrgency.Critical)
|
||
|
|
|
||
|
|
// Windows-specific scenarios
|
||
|
|
NotificationBuilder.Create("Meeting in 5 minutes").WithUrgency(NotificationUrgency.Reminder)
|
||
|
|
NotificationBuilder.Create("Incoming call").WithUrgency(NotificationUrgency.Alarm)
|
||
|
|
```
|
||
|
|
|
||
|
|
| Value | Windows | Linux | macOS |
|
||
|
|
|-------|---------|-------|-------|
|
||
|
|
| `Normal` | Default scenario | Normal urgency | Active interruption level |
|
||
|
|
| `Low` | Default scenario | Low urgency | Passive interruption level |
|
||
|
|
| `Critical` | Default scenario | Critical urgency | Critical interruption level (macOS 12+) |
|
||
|
|
| `Alarm` | Alarm scenario | Critical urgency | Critical interruption level (macOS 12+) |
|
||
|
|
| `Reminder` | Reminder scenario | Normal urgency | Active interruption level |
|
||
|
|
|
||
|
|
### Audio
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
NotificationBuilder.Create("Alert").WithAudio(NotificationAudio.Silent)
|
||
|
|
NotificationBuilder.Create("Alarm").WithAudio(NotificationAudio.Loop) // Windows only
|
||
|
|
```
|
||
|
|
|
||
|
|
| Value | Windows | Linux | macOS |
|
||
|
|
|-------|---------|-------|-------|
|
||
|
|
| `Default` | System notification sound | Controlled by daemon | System notification sound |
|
||
|
|
| `Silent` | No sound | No sound | No sound |
|
||
|
|
| `Loop` | Sound loops until dismissed | Treated as default | Treated as default |
|
||
|
|
|
||
|
|
### Expiration
|
||
|
|
|
||
|
|
Sets how long the notification is visible before it auto-dismisses. Pass `null` or omit
|
||
|
|
the call to use the platform default.
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
NotificationBuilder.Create("Reminder")
|
||
|
|
.WithExpiration(TimeSpan.FromSeconds(10))
|
||
|
|
.Build();
|
||
|
|
```
|
||
|
|
|
||
|
|
Note: `UNUserNotificationCenter` on macOS does not expose a per-notification timeout API;
|
||
|
|
this value is stored in the request but has no effect on macOS.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Showing and hiding notifications
|
||
|
|
|
||
|
|
`ShowAsync` returns a `long` identifier for the notification. Pass this to `HideAsync`
|
||
|
|
to remove it programmatically before the user interacts with it.
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
long id = await service.ShowAsync(request);
|
||
|
|
|
||
|
|
// Remove it after two seconds
|
||
|
|
await Task.Delay(TimeSpan.FromSeconds(2));
|
||
|
|
await service.HideAsync(id);
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Callback threading
|
||
|
|
|
||
|
|
Callbacks are fired on a background thread:
|
||
|
|
|
||
|
|
- **Windows** — callbacks arrive on a WinRT thread-pool thread.
|
||
|
|
- **Linux** — callbacks arrive on the GLib main loop thread.
|
||
|
|
- **macOS** — callbacks arrive on a background GCD thread managed by
|
||
|
|
`UNUserNotificationCenter`.
|
||
|
|
|
||
|
|
If you need to update UI elements from a callback, marshal the call to your UI thread
|
||
|
|
(e.g. `Dispatcher.InvokeAsync` on WPF, `Control.Invoke` on WinForms, or
|
||
|
|
`MainThread.BeginInvokeOnMainThread` on MAUI).
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## INotificationService interface
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
public interface INotificationService : IDisposable
|
||
|
|
{
|
||
|
|
// False if the platform is unsupported or initialisation failed.
|
||
|
|
bool IsSupported { get; }
|
||
|
|
|
||
|
|
// Shows a notification. Returns the notification ID on success.
|
||
|
|
// Throws PlatformNotSupportedException if IsSupported is false.
|
||
|
|
Task<long> ShowAsync(NotificationRequest request,
|
||
|
|
CancellationToken cancellationToken = default);
|
||
|
|
|
||
|
|
// Removes the notification from the notification centre.
|
||
|
|
Task HideAsync(long notificationId,
|
||
|
|
CancellationToken cancellationToken = default);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Dispose the service when your application exits to release the native resources
|
||
|
|
(WinToastLib STA thread on Windows, GLib main loop on Linux, UNUserNotificationCenter
|
||
|
|
cleanup on macOS).
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Platform notes
|
||
|
|
|
||
|
|
### Windows
|
||
|
|
|
||
|
|
- Requires Windows 8 or later. `IsSupported` returns `false` on earlier versions.
|
||
|
|
- The `AppUserModelId` must match an application shortcut in the Start Menu. The native
|
||
|
|
wrapper creates this shortcut automatically on first run when it does not already exist,
|
||
|
|
but the shortcut creation requires that the process can write to the user's
|
||
|
|
`%APPDATA%\Microsoft\Windows\Start Menu\Programs` folder.
|
||
|
|
- `WinToastWrapper.dll` is loaded at runtime from `runtimes/win-<arch>/native/` relative
|
||
|
|
to the entry assembly. For published single-file applications, ensure the DLL is
|
||
|
|
published alongside the executable.
|
||
|
|
- Toast callbacks are delivered on a WinRT thread-pool thread, not the STA thread. The
|
||
|
|
library handles this internally.
|
||
|
|
|
||
|
|
### Linux
|
||
|
|
|
||
|
|
`libnotify` must be installed at runtime. Install it via your package manager:
|
||
|
|
|
||
|
|
```
|
||
|
|
# Debian / Ubuntu
|
||
|
|
sudo apt install libnotify4
|
||
|
|
|
||
|
|
# Fedora / RHEL
|
||
|
|
sudo dnf install libnotify
|
||
|
|
|
||
|
|
# Arch
|
||
|
|
sudo pacman -S libnotify
|
||
|
|
```
|
||
|
|
|
||
|
|
A running D-Bus notification daemon is required (GNOME Shell, KDE Plasma, and most other
|
||
|
|
desktop environments provide one). Notifications will be silently dropped if no daemon is
|
||
|
|
running. `IsSupported` reflects whether `notify_init()` succeeded, not whether a daemon
|
||
|
|
is present.
|
||
|
|
|
||
|
|
Image support via `gdk-pixbuf` requires `libgdk-pixbuf-2.0` to be installed, which is
|
||
|
|
typically included as a dependency of `libnotify4`.
|
||
|
|
|
||
|
|
### macOS
|
||
|
|
|
||
|
|
- Requires macOS 10.14 (Mojave) or later.
|
||
|
|
- On first use, macOS presents an authorisation dialog asking the user to allow
|
||
|
|
notifications. `MNW_Initialize` (called from the `MacOSNotificationService` constructor)
|
||
|
|
blocks until the user responds. `IsSupported` is `false` if permission was denied.
|
||
|
|
- The user can revoke permission at any time in System Settings > Notifications. Subsequent
|
||
|
|
calls to `ShowAsync` will fail silently (the OS will not display the notification and no
|
||
|
|
callback fires) until permission is re-granted.
|
||
|
|
- Action button callbacks and dismiss callbacks require the process to remain running after
|
||
|
|
the notification is shown, because `UNUserNotificationCenterDelegate` delivers responses
|
||
|
|
to the live process. If the process exits before the user interacts, the callbacks are
|
||
|
|
never fired.
|
||
|
|
- Non-bundled processes (bare `dotnet` CLI applications) can display banners but may not
|
||
|
|
always receive action callbacks on all OS versions. Wrap the application as an `.app`
|
||
|
|
bundle or sign it with an appropriate entitlement for reliable callback delivery in
|
||
|
|
production.
|
||
|
|
- The notification body-tap and button-tap events on macOS are terminal events — the
|
||
|
|
`OnDismissed` callback is not fired after the user activates a notification or clicks
|
||
|
|
a button (unlike Windows, where WinToastLib always fires the dismissed event after any
|
||
|
|
interaction).
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Building the native libraries from source
|
||
|
|
|
||
|
|
Pre-built native binaries are included in the NuGet package. You only need to build from
|
||
|
|
source if you are making changes to the native wrapper code.
|
||
|
|
|
||
|
|
### Windows (WinToastWrapper.dll)
|
||
|
|
|
||
|
|
Requires Visual Studio 2022 with the "Desktop development with C++" workload.
|
||
|
|
|
||
|
|
```
|
||
|
|
msbuild native\WinToastWrapper\WinToastWrapper.vcxproj /p:Configuration=Release /p:Platform=x64
|
||
|
|
msbuild native\WinToastWrapper\WinToastWrapper.vcxproj /p:Configuration=Release /p:Platform=Win32
|
||
|
|
msbuild native\WinToastWrapper\WinToastWrapper.vcxproj /p:Configuration=Release /p:Platform=ARM64
|
||
|
|
```
|
||
|
|
|
||
|
|
Output is written to `runtimes\win-<arch>\native\WinToastWrapper.dll`.
|
||
|
|
|
||
|
|
### macOS (libMacNotifyWrapper.dylib)
|
||
|
|
|
||
|
|
Requires Xcode command-line tools (`xcode-select --install`).
|
||
|
|
|
||
|
|
```
|
||
|
|
make -C native/MacNotifyWrapper install
|
||
|
|
```
|
||
|
|
|
||
|
|
This cross-compiles both an `arm64` (Apple Silicon, macOS 11+) and an `x86_64` (Intel,
|
||
|
|
macOS 10.14+) slice and copies them to `runtimes/osx-arm64/native/` and
|
||
|
|
`runtimes/osx-x64/native/` respectively.
|
||
|
|
|
||
|
|
To also produce a universal binary:
|
||
|
|
|
||
|
|
```
|
||
|
|
make -C native/MacNotifyWrapper universal
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## License
|
||
|
|
|
||
|
|
MIT. See [LICENSE](LICENSE) for details.
|
||
|
|
WinToastLib is copyright (c) mohabouje, also distributed under the MIT License.
|