Search code examples
delphinotificationswindows-runtime

How to receive Activated and Dismissed messages for Windows Toast notifications


I'm making a library for Delphi to implement Windows 11 toast notifications easier using the interfaces from Winapi.UI.Notifications.

I want to to post custom notifications to the action center using a IToastNotifier to show the notifications and to be able to to receive events for when when the notification is dismissed or activated (clicked on). Thankfully, the IToastNotification has the add_Activated(), add_Dismissed() and add_Failed() methods to register a callback for when those events are triggered.

In C#, it's rather easy to add them, like so:

public void ShowToastNotification()
{
    // Construct the toast content with data bound fields
    var content = new ToastContentBuilder()
        .AddText("Notification title")
        .GetToastContent();

    // Generate the toast notification
    var toast = new ToastNotification(content.GetXml());
    toast.Activated += Toast_Activated;

    // Show the toast notification to the user
    ToastNotificationManager.CreateToastNotifier().Show(toast);
}

private void Toast_Activated(ToastNotification sender, object args)
{
    ShowMessageDialog($"Toast activated!");
}

To do the same thing in Delphi, I wrote the following code snippet:

unit Unit1;
interface

uses
  Winapi.Windows, Vcl.Forms, Winapi.ui.Notifications,
  Winapi.CommonTypes, Winapi.Winrt, Vcl.Dialogs, Winapi.DataRT;

type
  TToastActivatedHandler = class(TInterfacedObject, TypedEventHandler_2__IToastNotification__IInspectable)
    procedure Invoke(sender: IToastNotification; args: IInspectable); safecall;
  end;
  TToastDismissHandler = class(TInterfacedObject, TypedEventHandler_2__IToastNotification__IToastDismissedEventArgs)
    procedure Invoke(sender: IToastNotification; args: IToastDismissedEventArgs); safecall;
  end;

  TForm1 = class(TForm)
    procedure FormCreate(Sender: TObject);
  end;

var
  Form1: TForm1;
  ActivateHandle: TToastActivatedHandler;
  DismissHandle: TToastDismissHandler;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
var
  HStr: HSTRING;
  S:  string;
  Instance: IInspectable;
  Notifier: IToastNotifier;
begin
  // Notification maanger
  const xml = '<toast activationType="protocol">'+
  ' <visual>'+
  '     <binding template="ToastGeneric">'+
  '         <text>Hello world!</text>'+
  '         <text>This is a test notification.</text>'+
  '         <image src="C:\Windows\System32\@facial-recognition-windows-hello.gif" placement="hero" alt=""/>'+
  '     </binding>'+
  ' </visual>'+
  '</toast>';

  // Create XML
  S := 'Windows.Data.Xml.Dom.XmlDocument';
  WindowsCreateString(PChar(S), Length(S), HStr);
  try
    RoActivateInstance(HStr, Instance);
  finally
    WindowsDeleteString(HStr);
  end;

  // Load XML
  WindowsCreateString(PChar(xml), Length(xml), HStr);
  try
    (Instance as Xml_Dom_IXmlDocumentIO).LoadXml( HStr );
  finally
    WindowsDeleteString(HStr);
  end;

  // Create interfaces
  S := 'App.Test';
  WindowsCreateString(PChar(S), Length(S), HStr);
  try
    Notifier := TToastNotificationManager.CreateToastNotifier(HStr);
  finally
    WindowsDeleteString(HStr);
  end;
  const Notification = TToastNotification.CreateToastNotification( Instance as Xml_Dom_IXmlDocument );

  // Create activator
  ActivateHandle := TToastActivatedHandler.Create;
  DismissHandle := TToastDismissHandler.Create;

  // Prepare
  Notification.add_Activated( ActivateHandle );
  Notification.add_Dismissed( DismissHandle );

  // Show
  Notifier.Show( Notification );
end;

{ TToastActivatedHandler2 }
procedure TToastDismissHandler.Invoke(sender: IToastNotification;
  args: IToastDismissedEventArgs);
begin
  ShowMessage('Toast was dismissed!');
end;

{ TToastActivatedHandler }
procedure TToastActivatedHandler.Invoke(sender: IToastNotification;
  args: IInspectable);
begin
  ShowMessage('Toast was activated');
end;

end.

The following notification appears:

Image of shown notification

But the Invoke() method never gets called when I click on the notification, and I'm not sure why. I also tried the add_Dismissed() one, but the same problem occurs for It as well.

I'm completely out of ideas on what to try. There must be a way to have the invoke method called, but I'm unsure on how this may be accomplished.

Note: The unit file is not needed to run the code above, I only took the snippets from It that are needed to show this example. But the full library can be found on GitHub here.


Solution

  • I've figured out the issue!

    It seems that interfaces rely very heavily on their serialized GUIDs, and without the proper IDs added to a class, the registration will fail.

    Since TypedEventHandler_2__IToastNotification__IToastDismissedEventArgs is a child interface of TypedEventHandler_2__IToastNotification__IToastDismissedEventArgs_Delegate_Base, I assumed It would inherit It's GUID as well, but It seems that's not the case.

    The fix is really easy to apply, just add the TypedEventHandler_2__IToastNotification__IInspectable_Delegate_Base and TypedEventHandler_2__IToastNotification__IToastDismissedEventArgs_Delegate_Base interface classes to the TToastActivatedHandler and TToastDismissedHandler object respectively.

    Here is the correct declaration:

    TToastActivatedHandler = class(TInterfacedObject, TypedEventHandler_2__IToastNotification__IInspectable,
        TypedEventHandler_2__IToastNotification__IInspectable_Delegate_Base)
      public
        procedure Invoke(sender: IToastNotification; args: IInspectable); safecall;
      end;
      TToastDismissedHandler = class(TInterfacedObject, TypedEventHandler_2__IToastNotification__IToastDismissedEventArgs,
        TypedEventHandler_2__IToastNotification__IToastDismissedEventArgs_Delegate_Base)
      public
        procedure Invoke(sender: IToastNotification; args: IToastDismissedEventArgs); safecall;
      end;