Skip to content

Proposal: Converged and simplified toast notifications API across Win32 and UWP #137

@hulumane

Description

@hulumane
Summary Status

Customer / job-to-be-done: App developers using WPF, WinForms, or Win32 C++ need to keep their users informed via toasts regardless of whether their app is currently open

Problem: WPF apps must use the Community Toolkit to send notifications, C++ and Electron apps have told us they hate needing to have a shortcut and setting up notifications is difficult and they also have to use raw XML

How does our solution make things easier? All types of apps will benefit from the easier experience WPF apps currently have through the Toolkit (no more shortcut, zero configuration work to send a notification, easy activation). WPF app developers will benefit from only having to reference and update one single NuGet package (Reunion) instead of two (Reunion+Toolkit)

✅ Problem validation
🔄 Docs
❌ Dev spec
❌ Solution validation
❌ Implementation
❌ Samples

Target customer

Packaging App types
✅ Packaged apps
✅ Unpackaged apps
✅ WPF
✅ Win32 (C++)
✅ WinForms
✅ Console
✅ UWP
⚠ Cross-plat (Electron, MAUI, React Native, etc)
⚠ PowerShell

While this work should be consumable by cross-platform platforms to improve experiences there, we should have a separate feature specifically for ensuring notifications are built in "natively" and developers don't have to do additional custom work to access the Windows native layer.

Customer's job-to-be-done's

Job-to-be-done Validated?
Need to inform users about important events ✅ Validated
Sometimes need to show images in notifications ✅ Validated
Sometimes need buttons/inputs ✅ Validated
Occasionally need really rich visual content and interactivity ✅ Validated

Problems that exist today

Problem Validated?
Start menu shortcut: Unpackaged apps need a Start menu shortcut and this is very painful for developers ✅ People hate this. Hands down top feedback
Don't want to use MSIX: Switching to MSIX just to simply use notifications is too difficult/painful, I want to use it from my unpackaged app ✅ Heard this from multiple customers
Manual config: Unpackaged apps need to manually config their COM server with their shortcut, this is very painful for developers ✅ People struggle and get caught up here
Sending differences: UWP and Win32 MSIX/sparse apps simply call CreateToastNotifier()... Win32 apps have to call CreateToastNotifier("MyAumid") ✅ We've had several customers ask/be confused about which API they should use
XML is difficult: Programmatically building up an XML payload via code is difficult ✅ C# apps love using the Toolkit for this reason (has nearly half a million downloads now)
Sending verbosity: The APIs to send a simple toast are quite verbose and require talking to a number of different classes and objects. Couldn't it be easier? ✅ Several customers have asked us this specific question: why can't it be just one line of code
No HTTP images for unpackaged apps: Unpackaged apps have to download the image to disk ✅ Several complaints about this
Activation of elevated process: When using toasts from an unpackaged elevated app, COM server doesn't activate as elevated ✅ Heard several reports of this problem

Summary

Along with simplifying the APIs for UWP apps, these new simplified APIs will also work for all Win32 apps, AND they will work down-level too, so that developers (UWP or Win32) can use them instantly! There will be no requirement to change existing code to the new APIs, and existing SDKs that worked before will still work with your apps.

To make the registration experience seamless for Win32 apps, we'll take on the heavy lifting of registering a Win32 app down-level, using their existing app assets (app name, icon). Developers simply have to call our new simplified APIs, and regardless of their app type, toasts will automatically work!

Quick links

Rationale

  • See the problems today section above.
  • Win32 app developers find the shortcut requirement the most frustrating/painful experience about using toasts today.
  • Win32 toasts should be actionable from the action centre. A large set of win32 developers today display toasts that cannot be actioned on causing user dissatisfaction. We should make it easier for developers to properly support activation.
  • We should not have to document and tell developers which technique to use to activate toasts based on OS versioning. The process should be abstracted and simplified behind a well know set of common APIs.
  • Developers need not have to go and jump through hoops to display toasts using methodologies like Nitro. For example, Nitro puts a shortcut dependency on app developers which causes painpoints in packaging and deploying these shortcuts.
  • Only 50% of Win32 developers are happy with today's documentation for Toasts and Activations.
  • There are corner cases that don't work with COM servers that run under an elevated context: They need to setup COM access privileges and do special stuff for their scenarios to work when the APIs should be handling this for them.

Scope

Capability Priority
Common set of APIs across all app types to register and display toast notifications Must
Easy, simple, and straightforward to use Must
Full activation support for toasts when the app is closed for all App Types (Win32 and UWP) Must
Can build toasts with rich UI functionality (Icons, themes, App names) Must
Can build toasts using objects/builder syntax rather than XML documents Must
Activation of apps running as admin "just works" Must
PowerShell and Python scripts can easily use toasts Must
Electron apps can easily use toasts Must
"Portable" apps (ones that just run from EXE, not installed) are supported Must
Same toast content builders must work on ASP.NET web servers for push notifications Must
Scheduled toasts work Must
Renaming your app doesn't cause you to lose your current notifications Should
HTTP images supported for all app types Should
Support toast collections Could
Support multi-user apps Could

API experience

Here's a look at what (UWP) developers do today, compared to what we're proposing (any app type) developers will do using Reunion...

Today Tomorrow (Reunion)
Install Toolkit Notifications library Install Reunion library
Create a ToastContentBuilder, add their content Create a NotificationBuilder, add their content, call Show()
Create a ToastNotification using the XML from ToastContentBuilder
Create a ToastNotifier using ToastNotificationManager
Show the notification using ToastNotifier

Today

var content = new ToastContentBuilder() // Toolkit Notifications library
    .AddText("Hello from UWP!")
    .GetToastContent();

var notif = new ToastNotification(content.GetXml()); // Platform APIs

ToastNotificationManager.CreateToastNotifier().Show(notif); // Platform APIs

Tomorrow (Reunion)*

new NotificationBuilder() // Reunion library
    .AddText("Hello from WPF!")
    .Show();

To receive activation, we haven't been able to get a 100% converged experience...

  • UWP apps: They would still use App.xaml.cs OnActivated as they do today
  • Win32 MSIX: They would first have to add a COM server registration in their app manifest (maybe tooling can eliminate this somehow), and then they subscribe to ToastNotificationManagerInterop.OnActivated
  • Win32 normal/sparse apps: They subscribe to ToastNotificationManagerInterop.OnActivated

Sending toast API experience

First, developers would install the Reunion NuGet package.

Then, we're bringing in the toast XML object model that the Toolkit Notifications library has, so that you can have everything you need to easily construct toasts within one library! No manipulating XML necessary :) There will be a new ToastNotificationBuilder class, which allows you to create a toast using zero XML, set all the properties on it, and show it without calling the lengthy call-chain soup that today is ToastNotificationManager.CreateToastNotifier().Show()!

// Construct the notification and show it!
new NotificationBuilder()
    .AddLaunchArgs("picOfHappyCanyon")
    .AddText("Andrew sent you a picture")
    .AddText("Check this out, Happy Canyon in Utah!")
    .Show();

Receiving activation API experience

UWP apps

UWP apps would receive activation as they do today, within their App.xaml.cs OnActivated method.

protected override void OnActivated(IActivatedEventArgs e)
{
    // Handle toast activation
    if (e is ToastNotificationActivatedEventArgs toastActivationArgs)
    {
        // Obtain the arguments from the toast
        string args = toastActivationArgs.Argument;

        // Obtain any user input (text boxes, menu selections) from the toast
        ValueSet userInput = toastActivationArgs.UserInput;
 
        // TODO: Show the corresponding content
    }
}

Win32 MSIX apps

First, in your Package.appxmanifest, add:

  1. Declaration for xmlns:com
  2. Declaration for xmlns:desktop
  3. In the IgnorableNamespaces attribute, com and desktop
  4. com:Extension for the COM activator using a new GUID of your choice. Be sure to include the Arguments="-ToastActivated" so that you know your launch was from a toast
  5. desktop:Extension for windows.toastNotificationActivation to declare your toast activator CLSID (the GUID from Update issue templates #4 above).

Then, in your app's startup code (App.xaml.cs OnStartup for WPF), subscribe to the OnActivated event.

// Listen to activation
AppLifecycle.OnActivated += AppLifecycle_OnActivated;

private void AppLifecycle_OnActivated(IActivatedEventArgs e)
{
    // Handle notification activation
    if (e is NotificationActivatedEventArgs toastActivationArgs)
    {
        // Obtain the arguments from the toast
        string args = toastActivationArgs.Argument;

        // Obtain any user input (text boxes, menu selections) from the toast
        ValueSet userInput = toastActivationArgs.UserInput;
 
        // TODO: Show the corresponding content
    }
}

Win32 or sparse apps

In your app's startup code (App.xaml.cs OnStartup for WPF), subscribe to the OnActivated event.

// Listen to activation
AppLifecycle.OnActivated += AppLifecycle_OnActivated;

private void AppLifecycle_OnActivated(IActivatedEventArgs e)
{
    // Handle notification activation
    if (e is NotificationActivatedEventArgs toastActivationArgs)
    {
        // Obtain the arguments from the toast
        string args = toastActivationArgs.Argument;

        // Obtain any user input (text boxes, menu selections) from the toast
        ValueSet userInput = toastActivationArgs.UserInput;
 
        // TODO: Show the corresponding content
    }
}

What about existing SDKs? Please don't break those!

I fully agree with you. If you're using a SDK that returns an XmlDocument or a ToastNotification, you'll still be able to use those, we'll provide APIs to allow you to pass those through to the new APIs.

API definitions

NOTE: These are OUTDATED, haven't been updated to the new Builder style

Introduce a new class...

Microsoft.UI.Notifications.ToastNotificationManagerInterop

Methods

Method Description Return type Min supported build Supported app types
CreateToastNotifier() Creates and initializes a new instance of the ToastNotifier that lets you raise a toast notification. Windows.UI.Notifications.ToastNotifier 10240 All three
GetToastCollectionManager() Creates a ToastCollectionManager that you can use to save, update, and clear notification groups. Windows.UI.Notifications.ToastCollectionManager 15063 All three
CreateToastNotifierForToastCollectionAsync(string collectionId) Creates and initializes a new instance of the ToastNotifier that lets you raise a toast notification within the specified toast collection. Note that the platform API is called GetToastNotifierForToastCollectionIdAsync, I changed "Get" to "Create" and dropped "Id" as it seems excessively verbose. IAsyncOperation< Windows.UI.Notifications.ToastNotifier> 15063 All three
GetHistoryForToastCollectionAsync(string collectionId) Gets the notification history for the specified toast collection. Note that I dropped the "Id" from the platform API since it seemed excessively verbose. IAsyncOperation< Microsoft.UI.Notifications.ToastNotificationHistoryInterop> 15063 All three

Omitted methods

We're explicitly omitting a few methods from the platform ToastNotificationManager and ToastNotificationManagerForUser...

Method Reason
CreateToastNotifier(string appId) Only used by multi-app packages, which are rare or non-existent anymore... if we have requests for this we can always easily add it at any point in time
GetDefault() 99% of apps are single-user apps, making 99% of developers always call GetDefault() is annoying.
GetForUser(Windows.System.User user) Do we need to support MUA apps?
GetTemplateContent(ToastTemplateType) These toast templates are from Windows 8, Windows 10 now uses ToastGeneric which we have builder classes for and this method is meaningless.
GetToastCollectionManager(string appId) Only used by multi-app packages

Properties

Property Description Return type Min supported build Supported app types
History Gets the ToastNotificationHistoryInterop object. Microsoft.UI.Notifications.ToastNotificationHistoryInterop 10240 All three

Events

Event Description Args type Min supported build Supported app types
OnActivated Event that is fired when a toast notification or action on a toast is clicked. This is not supported on UWP apps and will throw an exception if called from UWP. Win32 MSIX/sparse apps must first add values in their app manifest before calling this. (Microsoft.UI.Notifications. ToastNotificationActivatedEventArgsInterop e) 10240 Win32 MSIX/sparse and Win32 (not UWP)

And add another new class...

Microsoft.UI.Notifications.ToastNotificationHistoryInterop

Method Description Return type Min supported build Supported app types
Clear() Removes all notifications sent by this app from action center. void 10240 All three
GetHistory() Gets the collection of toasts currently in Action Center. Note: Should we change the name History? It's wonky, implies it'd include dismissed toasts. IReadOnlyList< Windows.UI.Notifications.ToastNotification> 10240 All three
Remove(string tag) Removes an individual toast, with the specified tag label, from action center. void 10240 All three
Remove(string tag, string group) Removes a toast notification from the action using the notification's tag and group labels. void 10240 All three
RemoveGroup(string group) Removes a group of toast notifications, identified by the specified group label, from action center. void 10240 All three

Omitted methods

We're explicitly omitting a few methods from the platform ToastNotificationHistory...

Method Reason
Clear(string appId) Only used by multi-app packages
GetHistory(string appId) Only used by multi-app packages
Remove(string tag, string group, string appId) Only used by multi-app packages
Remove(string group, string appId) Only used by multi-app packages

And one more new class... (unfortunately we can't just instantiate ToastNotificationActivatedEventArgs).

Microsoft.UI.Notifications.ToastNotificationActivatedEventArgsInterop

This class will only be used by Win32 MSIX/sparse and Win32 apps... there's nothing stopping UWP apps from using it, but it just won't ever do anything or be sent to them.

Or ideally we should have a converged activation experience with the rest of Reunion...

Properties

Property Description Return type Min supported build Supported app types
Argument Gets the arguments that were originally specified on the toast corresponding to which action was taken on the toast. string 10240 All three
UserInput Gets the user inputs the user provided on the toast notification ValueSet 10240 All three

Implementation details

When the developer calls ToastNotificationManagerInterop.CreateToastNotifier(), we'll handle the differences between UWP/Win32 MSIX/spase and Win32.

If running with identity: We simply call CreateToastNotifier()

If not running with identity (Win32): We'll first register the app by obtaining its display name and icon and using the EXE path for the AUMID, and then we'll call CreateToastNotifier(aumid).

We'll do the same forking logic for when they access .History.

Uniquely and consistently obtaining an app's identity

We need to be able to uniquely identify (and consistently re-identify) a Win32 app so that we can register it with a stable identity.

Scenarios we should support are...

  1. EXE closed and re-opened
  2. App has two EXEs (like Notepad), regardless of which one runs they both should resolve to the same identity
  3. EXE's path changes upon app upgrade (Electron cases, see comment about 4 comments down)
  4. "Portable" apps where they aren't installed and EXE path might change

In all those cases, we should hopefully be able to keep the same identity.

How NotificationIcon (taskbar notification icons) identifies apps

Taskbar either uses hWnd + uId (where uId is an integer) or guidItem to uniquely identify the notification icon in the system tray (docs). There's a code sample here. You can only set hWnd and leave uId as default 0, that's the most minimal. It doesn't automatically infer your hWnd though.

Open questions

Would appreciate community feedback on any of the following!!

  • Are Win32 apps okay with the same display assets they use on taskbar being used for toasts? Or do they want a way to specify custom display name/icons?
  • Should the new OnActivated event be added to a DesktopNotificationManagerInterop class? It's not usable by UWP at all, and won't do anything on UWP (since UWP activation goes through the OnActivated App.xaml.cs method). Or maybe we just throw an exception when running on UWP to let the developer know to not use it (in addition to having an IntelliSense comment).
  • Do we need to support multi-user apps?
  • Should methods be called CreateToastNotifier() or GetToastNotifier()? For the new toast notifier for toast collections, we went with Get... should we converge but possibly make it slightly tougher to switch? Existing apps shouldn't need to switch to these new APIs anyways though.
  • Converged activation experience across all of Reunion?
  • Should notifications from the dev-deployed version of the app be separate from notifications from the Installer-installed version of their app?
    • One reason for NO is that app data itself is usually shared across the dev debugging version and the installed version of the app. For example, apps simply save their data to the AppData folder, picking a unique name, and they probably don't pick a different name for debug vs production installed. Electron's "userData" folder is just AppData[AppDisplayName], so it's the same across debug deployed and installed version.

Important Notes

PM gathered feedback from 12 developers on GitHub who used our Desktop notifications library today in Win32 non-packaged apps to learn what their pain points are, what approaches they would prefer, etc
• Overwhelmingly, people’s biggest pain point was needing to create the shortcut (only 2 found that easy)
• About 50/50 were happy with current COM activation.
• About 50/50 were happy with the current documentation experience
• When asked whether they’d prefer COM or EXE activation, of those who responded, most (4 developers) said COM, only one said EXE, two said in-memory callback
• Handling activation of COM server from an elevated process is a challenge/problem today and something we need to fix
• One developer was a scripting developer, we can’t forget the PowerShell community (there’s a BurntToast library for sending toasts via PowerShell, we can update that to use the new registration).

A sample of the docs for sending toasts are available here: Internal link / Public link

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions