WPF 使用 Windows 10 APIs - 3

本篇介紹 Bridge WPF 專案整合 Windows Notification Service(WNS),並處理相關的事件。

對於 WNS 運作方式,可參考 Universal App - 整合 Windows Notification Service (WNS) for ServerUniversal App - 整合 Windows Notification Service (WNS) for Client 的介紹,瞭解 WNS 與 UWP 的整合。

根據 Toast notifications from desktop apps 的介紹,由於 Desktop apps(Desktop Bridge and classic Win32) 因爲架構不同,整合有不同的選擇,但 COM activator 來處理 Windows 10 Notifications 是最好的選擇,因爲所有的功能都有支援,如下圖: 加上官方有寫好的 library: Send a local toast notification from destkop C# appsSend a local toast notification from destkop C++ WRL apps 更方便我們完成整合。

需注意:

  • Desktop Bridge apps 的整合會更接近 UWP,用戶點擊 toast 時,要能啓動 app 並帶入 toast 中的參數;
  • Classic Win32 apps 需設定 AUMID 來傳送 toast,或搭配 CLSID 設定捷徑。
    • 可利用亂數 GUID 來設定 GUID CLSID;
    • 切勿新增 COM Server / COM activator;
    • 可以設定 stub COM CLSID,讓 Action Center 保存您的通知;
    • 只能使用 protocol 類型的 toast,因爲 stub COM CLSID 會中斷其他任何 toast 的啓動。因此要更新 App 讓它支援 protocol 啓動;

以 Desktop Bridge (WPF) 程式爲例,整合 WNS 需要以下步驟:

  1. 建立並完成 Desktop Bridge 的基本設定,可參考WPF 使用 Windows 10 APIs - 1
  2. 修改 WPF 專案檔,加入:<TargetPlatformVersion /> 設定預計支援 Windows 10 的最小版本,如下:
    <TargetFrameworkVersion>...</TargetFrameworkVersion>
    <TargetPlatformVersion>10.0.10240.0</TargetPlatformVersion>
  3. 加入必要的參考:Windows.DataWindows.UI,如下圖:
    如果您已經加入過 Program Files (x86)\Windows Kits\10\UnionMetadata\Windows.winmdWindows\Microsoft.NET\Framework\v4.0.30319\System.Runtime.WindowsRuntime.dll 的話,不需要在加入 Windows.UI 與 Window.Data 因爲 Windows.winmd 已經有了。
  4. 下載 DesktopNotificationManagerCompat.cs file from GitHub 並加入到專案中;該 compact library 簡化複雜 desktop application 整合 notifications 的方式,並加入處理 notifications 與 apps 之間的互動;
  5. 實作 activator 處理用戶點擊 toast 時與 App 的互動。
    延申 NotificationActivator 類別與加入 3 個必要的屬性宣告,並為 App 加入 unique GUID CLSID,GUID 可以是亂數產生的。 CLSID(class identify) 用在向 Action Center 讓他知道收到 toast 對應到哪一個 COM activate。
    // GUID CLSID 必須是唯一值。
    [ClassInterface(ClassInterfaceType.None)]
    [ComSourceInterfaces(typeof(INotificationActivationCallback))]
    [Guid("replaced-with-your-guid-C173E6ADF0C3"), ComVisible(true)]
    public class MyNotificationActivator : NotificationActivator
    {
        public override void OnActivated(string invokedArgs, NotificationUserInput userInput, string appUserModelId)
        {
            // 負責處理收到 OnActivated 的事件
        }
    }
  6. 向 notification platform 注冊,這裏使用 Desktop Bridge 爲例,如果您是 classic Win32 請參考 Register with notification platform
    Package.appxmanifest 加入 xmlns:comxmlns:desktop 的宣告,並設定 windows.comServer(com extension) 與 windows.toastNotificaiton (desktop extension),兩者要記得使用與上一步的 GUID 一致。
    <Package
      ...
      xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
      xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
      IgnorableNamespaces="... com desktop">
      ...
      <Applications>
        <Application>
          ...
          <Extensions>
    
            <!--Register COM CLSID LocalServer32 registry key-->
            <com:Extension Category="windows.comServer">
              <com:ComServer>
               <!-- YourProject\YourProject.exe 需注意專案輸出的目錄,不可以用 $targetnametoken$.exe  -->
                <com:ExeServer Executable="YourProject\YourProject.exe" Arguments="-ToastActivated" DisplayName="Toast activator">
                  <com:Class Id="replaced-with-your-guid-C173E6ADF0C3" DisplayName="Toast activator"/>
                </com:ExeServer>
              </com:ComServer>
            </com:Extension>
    
            <!--Specify which CLSID to activate when toast clicked-->
            <desktop:Extension Category="windows.toastNotificationActivation">
              <desktop:ToastNotificationActivation ToastActivatorCLSID="replaced-with-your-guid-C173E6ADF0C3" /> 
            </desktop:Extension>
    
          </Extensions>
        </Application>
      </Applications>
     </Package>
  7. 注冊 AUMID 與 COM Server:
    AUMID 的取得可參考:Find the AUMID (Application User Model ID) of an installed UWP app,如果您沒有特別設定通常是 {Package family name}!App
    protected override void OnStartup(StartupEventArgs e)
    {
      // Register AUMID and COM server (for Desktop Bridge apps, this no-ops)
      DesktopNotificationManagerCompat.RegisterAumidAndComServer<MyNotificationActivator>("{AUMID}");
    
      // Register COM server and activator type
      DesktopNotificationManagerCompat.RegisterActivator<MyNotificationActivator>();
    }
    需注意:
    • 如果抓不到 AUMID,請先安裝一次您的 App;
    • 如果同時支援 Desktop Bridge 與 classic Win32 apps 可以隨時使用 RegisterAumidAndComServer 方法;如果是 Desktop Bridge 只需要在 OnStartup() 時使用;AUMID 在注冊 COM Server 時會一起被記錄在 LocalServer32 的 register key 裏面;
    • 不管是 Desktop Bridge 或 classic Win32 apps 都需要使用 DesktopNotificationManagerCompat.RegisterActivator 注冊 notification action type (例如上面範例的 MyNotificationActivator);
  8. DesktopNotificationManagerCompat 包裝了發送 ToastNotifier 幫忙發送 toast 訊息,減少我們自己撰寫 toast 的 XML 造成的錯誤;或者選擇安裝 Notifications library 也可以方便使用。詳細使用方式可參考 Step 7: Send a notification
  9. 在 MyNotificationActivator.OnActivated 與 App.OnStartup 加入處理 notification 的機制:
    開發過 UWP 熟悉 toast 被用戶點擊時會啓動 app,而存在兩個狀況:app 未開啓時會觸發 OnLaunch;app 已開啓時會觸發 OnActivated。同樣第以 WPF 爲例:
    • app 正開著,會進入:NotificationActivator.OnActivated;
    • app 未被開啓,會進入 App.OnStartup 事件,並帶入啓動 toast 中夾帶 -ToastActivated 的參數;接著在呼叫 NotificationActivator.OnActivated;
    利用兩段代碼片段來説明:
    // The GUID CLSID must be unique to your app. Create a new GUID if copying this code.
    [ClassInterface(ClassInterfaceType.None)]
    [ComSourceInterfaces(typeof(INotificationActivationCallback))]
    [Guid("replaced-with-your-guid-a3af-C173E6ADF0C3"), ComVisible(true)]
    public class MyNotificationActivator : NotificationActivator
    {
        public override void OnActivated(string invokedArgs, NotificationUserInput userInput, string appUserModelId)
        {
            // 要記得使用 Application.Current.Dispatcher 包裝要操作的 UI 事件;
            // 因爲 com activator 是在另一個 thread
            Application.Current.Dispatcher.Invoke(() =>
            {
                // 檢查是否有 window 在前景顯示
                OpenWindowIfNeeded();
            });
        }
    
        private void OpenWindowIfNeeded()
        {
            // 檢查是否有 window 被開啓,如果沒有則需要建立一個;
            // == 0 的狀況會發生在 toast 被點擊但 app 還沒有被開啓的時候;
            if (App.Current.Windows.Count == 0)
            {
                new MainWindow().Show();
            }
    
            // 啓動 window 讓 window 被系統 focus 跑到第一個
            App.Current.Windows[0].Activate();
    
            // 設定 window 要顯示的大小
            App.Current.Windows[0].WindowState = WindowState.Normal;
        }
    }
    protected override void OnStartup(StartupEventArgs e)
    {
      // 利用 DesktopNotificationManagerCompat 注冊 AUMID 到 COM Server
      DesktopNotificationManagerCompat.RegisterAumidAndComServer("{AUMID}");
      // 利用 DesktopNotificationManagerCompat 注冊處理 COM Server 的 Activator
      DesktopNotificationManagerCompat.RegisterActivator();
    
      // 如果 App 被啓動是來自於 toast 被 click 的話,就會帶入 -ToastActivated 的參數
      // -ToastActivated 也是我們在 Package.appxmanifest 宣告的 activator 參數
      if (e.Args.Contains("-ToastActivated"))
      {
         // 如果是來自 toast 啓動 app 最後會走進 注冊的 OnActivated 中
         // 由於上方的範例改由 OnActivated 判讀是否需要產生 window 這裏就不需要了
      }
      else
      {
        // 一般啓動 app 需要顯示 window
        // 在 App.xaml 中務移除 StartupUri 以便預設情況下不會創建 window,因爲我們需要自己控制。而且有時候我們只希望在沒有 window 下處理任務。
        new MainWindow().Show();
      }
    
      base.OnStartup(e);
    }
    需注意 Desktop apps 的 foreground 與 background 啓動的識別機制一樣,都是透過 COM activator 的呼叫。 因此,apps 收到 toast 時要顯示 window 或是當作 worker 只處理任務是由您寫的 code 來決定。 即使設定 toast 中的 ActivationType 是 Background 也無效果。
  10. 清除 toast 或是指定特定 tag 的 taost:
    // Remove the toast with tag "Message2"
    DesktopNotificationManagerCompat.History.Remove("Message2");
    
    // Clear all toasts
    DesktopNotificationManagerCompat.History.Clear();
    建議使用 DesktopNotificationHistoryCompat 包裝好的 methods 來操作 toast,省去 AUMID 的使用。

上面介紹讓 Desktop bridge 或 Win32 app 能夠整合 toast,接著繼續介紹整合 Windows Notification Service。

如何拿到 WNS 的 server 端,可參考 Universal App - 整合 Windows Notification Service (WNS) for Server

下面以 WPF 爲例介紹取得 WNS channel:

private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    try
    {
        // 與 UWP 一樣使用 PushNotificationChannelManager 拿到 channel
        var channel = await PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync();
        channel.PushNotificationReceived += Channel_PushNotificationReceived;
        Debug.WriteLine(channel.Uri);
    }
    catch (Exception ex)
    {
        Debug.WriteLine(ex);
    }
}

private void Channel_PushNotificationReceived(PushNotificationChannel sender, PushNotificationReceivedEventArgs args)
{
    // 可根據自己的需要,處理收到的訊息,但需注意這裏的寫法只有 app 在前景時有用;
    // 可以寫到 BackgroundTask 包裝起來;
    switch (args.NotificationType)
    {
        case PushNotificationType.Raw:
            break;
        case PushNotificationType.Tile:
            break;
        case PushNotificationType.TileFlyout:
            break;
        case PushNotificationType.Toast:
            break;
    }
    
    args.Cancel = true;
}

[補充]

  • DesktopNotificationManagerCompat 該檔案幫忙寫好處理注冊 COM 與接受訊息時,呼叫 kernal32.dll 來轉發到注冊的 *.exe,並套入既有 Application 的 life cycle。

[範例程式]

DotblogsSampleCode/Samples/34-WpfPushNotification/

======

隨著微軟陸續開發 How x86 emulation works on ARM 的方向,加上 Bridge Desktop apps 與 PWA apps 不斷地在 Microsoft Store 上架,發現除了 UWP 之外還有其他開發方式,可以讓 Apps 在 Windows 10 設備上執行。

Desktop Bridge 是延續企業軟體最好的機制,因此幫忙研究一些可能用到的技術,希望對大家有所幫忙。謝謝。

References