microsoft / WindowsAppSDK

The Windows App SDK empowers all Windows desktop apps with modern Windows UI, APIs, and platform features, including back-compat support, shipped via NuGet.
https://docs.microsoft.com/windows/apps/windows-app-sdk/
MIT License
3.78k stars 319 forks source link

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

Closed hulumane closed 2 years ago

hulumane commented 4 years ago
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

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...

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 #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!!

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

riverar commented 3 years ago

The review link does not work for non-FTEs and taints the open source spirit of the project/workflow here. Recommend attaching files as a workaround.

Some early feedback:

  1. It doesn’t appear apps will receive notifications until they notify at least once due to the placement of the registration logic. I was a bit surprised by this, instead expecting to see the registration behavior in a static constructor or module constructor. The impact here is that an app can’t receive notifications from the cloud until they toast once.

  2. Registration of unpackaged desktop apps entails creating a fake GUID to serve as the application’s AUMID. I have several concerns about this:

    • SHA1 algorithm is used to hash the process image path
      • Clash potential for portable applications on removable media seems high (e.g. E:\app.exe)
      • Potential incorrect behavior for applications on removable media (e.g. dynamically assigned E:\ changes to F:)
      • Algorithm is no longer approved for use in FIPS-compliant environments (can probably explain this away as it’s not used for encryption)
    • Apps that use versioning in their install path will lose their registration every update (see: electron apps) and cannot clean up previous registrations.
  3. There's no mention of how this works in 10X containers.

andrewleader commented 3 years ago

Thanks Rafael!! I fixed the link (included the documentation PDF files). I need to talk with our docs team about seeing if we can open up certain doc branches to the public so we can just share the actual docs!

Responding to those additional points...

  1. This proposal doesn't include push (cloud) notifications, plus to receive push notifications, apps have to make an API call anyways (to obtain the push channel), so we can register the app there. But push will be covered in a separate spec and likely requires even more work like a UWP sidecar (we're still figuring out a plan)... for toasts we've always had "Nitro" down-level to use.

  2. Excellent concerns! We'll take these all into consideration, clearly we need to do something slightly different.

  3. For 10X containers, I'll sync up with the rest of the Reunion team. Good callout.

hulumane commented 3 years ago

1) For the Push concern, we could technically abstract the registration call behind channel creation. We still need to think about it a little more though. 2) For the registration, I agree with some of these concerns. Thanks for bringing it up. I think it is valuable to provide a way to unregister. Otherwise we end up with stale orphans in our DB. Also instead of a hash, maybe we could simply use the file path which is guaranteed to be unique? The goal initially was to abstract away all of this for developers but clearly there is a cost here and we need to rethink our approach.

riverar commented 3 years ago

@hulumane File paths aren't guaranteed to be unique, sadly. Consider the removable device scenario, where the path could be reused by multiple apps (e.g. R:\tool.exe). Probably rare though.

andrewleader commented 3 years ago

Documenting the Electron EXE case for future reference... indeed the EXE path does change over app versions... tracing the EXE back to the shortcut and using the shortcut would work though as that stays stable (of course the app could rename their app's display name which would change the shortcut path... should see if electron typically assigns the AUMID property on the shortcut).

image

After updating to 1.0.1...

image

Changing the name indeed (obviously) changes the shortcut path. Interesting to note the local data path (up till the version) remains constant. It seems to use the subsequent value seen further below within package.json for maker_squirrel... I wonder if that also gets set as the AUMID of the shortcut or what... either way that value seems unique and constant, and possibly the earlier AppData path is constant and unique...

image

image

Indeed the AUMID gets set, and that config value seems to get used, not sure where the second "ElectronApp" comes from though... (easy way to view AUMIDs is to CTRL+R and execute shell:appsfolder, change the display to detailed list, and press ALT+V, "Choose details…", select AppUserModelId

image

They seem to use the "product name" and sanitize it for spaces when generating the AUMID, so changing the app name results in a new AUMID (also note that forge's installer doesn't handle deleting the previous shortcut, but that's a separate issue for forge not us).

image

image

riverar commented 3 years ago

@andrewleader Portable unpackaged desktop apps may not have a shortcut to leverage, right? (I believe the shortcut dependency was one of the pain points identified in this proposal.)

The Taskbar notification area anchors to image path. But to address some of the pitfalls to that approach, it also provides developers with a method of registering with an ID they control. Perhaps a similar approach can be adopted here?

andrewleader commented 3 years ago

Yes, shortcuts aren't required, but when present they provide some more options for a more stable app identity (for example for Electron apps, that resolves the problem with the EXE path changing on upgrade, because the shortcut remains stable and actually has the AUMID property set).

That's super useful to know how taskbar notification area identifies apps! Indeed an optional option for developers to register an ID might be necessary. I'd like to keep things as easy and seamless as possible for developers, which is why I'm searching for solutions to avoid that and use existing ways to uniquely identify apps.

mdtauk commented 3 years ago

How will Adaptive Cards play into this new API?

uBadRequest commented 3 years ago

What about ScheduledToastNotifications? Currently, that doesn't even work if the win32 application has never called the CreateToastNotifier().show() method. A work around my app in production uses is, if its the first time the app has run, it calls .show() and then immediately calls .hide() on a blank notification, just so the scheduled notifications work.

ScheduledToastNotification toast = new ScheduledToastNotification(toastContent.GetXml(), DateTime.Now.AddMinutes(60));

DesktopNotificationManagerCompat.CreateToastNotifier().AddToSchedule(toast);
andrewleader commented 3 years ago

@mdtauk We don't have any plans to support Adaptive Cards in toast notifications today. Do you or your company have a need/requirement for this and if so can you explain why and what end-user experience or developer benefit you're hoping to achieve?

@uBadRequest I wasn't aware of this issue, thank you for reporting that, I'll double check to see if our new solution for toasts works with scheduled toast notifications out-of-the-gate (I probably haven't seen this issue myself since in my test apps I've usually sent a local toast first).

psmulovics commented 3 years ago

@andrewleader we also looking to have something like adaptive cards or even something more complex as a need; I'm happy to take it offline with you

andrewleader commented 3 years ago

The updated Notifications Toolkit preview is available to try for C# apps (WPF/Winforms, MSIX, or UWP)! Shortcuts are no longer needed!! Documentation and instructions here: https://docstaging.z5.web.core.windows.net/aleader/toolkit-7/design/shell/tiles-and-notifications/send-local-toast.html

If you run into any problems or find anything that could be better or doesn't work for your scenarios, please let us know (if your app requires running as admin, there are some known problems with activation there that we haven't fixed yet). If the preview works well for you, please let us know about that too!

stevewri commented 3 years ago

See above issue, something to keep track of context of any Reunion notification work.

ForNeVeR commented 3 years ago

@andrewleader, while browsing a section for Win32 C++ apps on the address you provided, I can see that shortcuts are still required:

If you're using classic Win32 (or if you support both), you have to declare your Application User Model ID (AUMID) and toast activator CLSID (the GUID from step # 4) on your app's shortcut in Start.

Could you please elaborate under what conditions was the requirement lifted? I assume, certain Windows update is required?

andrewleader commented 3 years ago

@ForNeVeR this feature is still in proposal state for Project Reunion and hasn't been implemented, but when it is, shortcuts won't be required in any scenario, including support back to version 1809 of Windows 10.

In a couple days, we're going to be releasing the updated version of the Windows Community Toolkit which includes support for the shortcut-less notification experience for C# apps, and along with that we're publishing a docs article that might help you try this now for C++ apps. I'll let you know here when that's available!

Do you have a C++ app?

ForNeVeR commented 3 years ago

@andrewleader, I am considering using the new toast notifications instead of the older XP-styled ones in a large Java application that has a C++ bootstrapper (so we coud use pure C++ solution based on WRL, but it would be harder for me to push WinRT/UWP-based solution).

Main problem with XP-style notifications is that they force us to add a tray notification icon, which is otrherwise completely unused by the app, and is defunct and confusing for the users (as a free bonus, the new notiifcations are much more functional, but that's not our main focus — though we may try to use some of the features such as clickable links).

And, yes, the custom shortcut requirement is one of the reasons we weren't able to bring the new notifications into the app: we have a complex deployment system with custom updates, application being sometimes distributed as a ZIP archive, and such, so we aren't always controlling the shortcuts.

hulumane commented 2 years ago

The WinAppSDK now implements a Toast Notification featureset that supports all Win32 apps (Packaged and Unpackaged). https://github.com/microsoft/WindowsAppSDK/pull/2192