.NET5のWPFアプリでインタラクティブなトースト通知を実装する

## つくったもの

昔作っていたWPFアプリ（.NET5）に、パッケージ化やら証明書やらのメンドクサイ作業一切なしでトースト通知を実装することができました。
＃アプリ自体の説明は[こちら](https://qiita.com/ambleside138/items/e05e6d1fad91dd3c05c0)
＃リポジトリは[ここ](https://github.com/ambleside138/TimeRecorder)
![toast.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77732/90c152fa-fdbc-b823-9b5d-9eb660e03681.gif)


## 実装のきっかけ　

最近、UWPアプリからしか使えなかった機能がクラシックデスクトップアプリ（WinForm, WPF）でも使えるようになってきてます。トースト通知もそのひとつのようで。@okazukiさんのtweetでAPIの仕様変更を知りました。

https://twitter.com/okazuki/status/1382558920728711168

今回手を加えたアプリを作った当時（2020年夏頃）、せっかくだからトーストも実装したいなぁ〜と考えてはいたのですがあまりのめんどくささに断念してました。

＃ちなみにこんな実装が必要だったらしい...↓

https://qiita.com/tera1707/items/fddb6e01ffcdf73d8dd4


というわけで、どのくらい実装がラクになっているのか、以前開発したWPFアプリにトースト通知機能を実装して確認してみます。


## トースト通知を表示するところまで

まずは単純なメッセージを表示するだけの機能を実装してみます。

1. NuGetで必要なパッケージを追加
Microsoft.Toolkit.Uwp.Notificationsパッケージを追加。

1. プロジェクトのターゲットフレームワークをWin10向けに変更　**←大事！**
最初これを忘れていてしばらくつまづいてました...
[公式の記事](https://docs.microsoft.com/ja-jp/windows/uwp/design/shell/tiles-and-notifications/send-local-toast?tabs=desktop)でも言及されてますが、変更せずに「net5.0-windows」のままになっているとToastContentBuilderのShowメソッド定義がない、とコンパイル時点で怒られます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77732/a53faf13-8635-8da2-06b5-2ea3711abaf6.png)

1. トースト通知する処理を実装
メッセージを表示するならこれだけでOK

```csharp
new ToastContentBuilder()
     .AddText("⏰ TimeRecorder ⏰")
     .AddText("作業タスクが設定されていません")
     .Show();
```

表示イメージ　↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77732/54d4d69a-c693-6293-3269-8df93dfaf18d.png)

すごい！めちゃ簡単！

## 対話型のトースト実装

実はここまでの機能（メッセージの表示のみ）だと、WinFormsのNotifyIcon.ShowBalloonTipを使ってすでに実装できてました。
↓WinFormsのNotifyIconを利用した通知
![BaloonTooltip.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77732/85360c05-537c-ba99-9b30-ed6683971d04.png)

せっかく純正Toastが使えるようになったので、トーストコントロール内で項目の選択をしてアプリ側で対応する処理を行う対話型の実装をしてみます。

### UI構築
トーストにはいろいろなコンテンツを埋め込むことができるようで、リファレンスをざっと確認すると

- テキスト
- 画像
- コンボボックス
- ボタン
- 通知音

など、結構自由にカスタマイズできるよう。
今回の要件に合わせてカスタマイズすると、以下のようなソースになりました。

```NotificationService.cs
public void PutInteractor(IEnumerable<WorkTaskWithTimesDto> workTasks)
{
    var title = "お知らせ";
    var content = "作業タスクが設定されていません";
    var attribute = "TimeRecorder ⏰ 工数管理";

    var selector = new ToastSelectionBox(_SelectionTaskKey);
    
    // 表示できる項目数に上限があるよう
    foreach(var itm in workTasks.Take(5)
                                .Select(t => new ToastSelectionBoxItem(t.TaskId.Value.ToString(), $"[{t.ProcessName}] {t.Title}")))
    {
        selector.Items.Add(itm);
    }

    selector.DefaultSelectionBoxItemId = selector.Items.FirstOrDefault()?.Id ?? "";

    new ToastContentBuilder()
         .SetToastScenario(ToastScenario.Reminder)
         .AddText(title)
         .AddText(content)
         .AddAttributionText(attribute)
         .AddToastInput(selector)
         .AddButton(new ToastButton()
                        .SetContent("開始")
                        .AddArgument(_ActionTypeCode, _ActionTypeStartTask))
         .AddButton(new ToastButtonSnooze())
         .Show();
}

```

ポイントとしては３点。

- コンボボックスはToastSelectionBoxのItemsに項目を追加することで実装可能
- ユーザが選択した内容を特定するために各コンテンツにキーを付与しておく
- 「再通知」するにはToastScenario.Reminderの指定が必要

上記のソースだとこんなイメージのトースト通知になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77732/c7ff2029-94b4-fec2-2e2d-a5fa80b56ff3.png)



### トーストコントロール内でのユーザー操作を検知する

ToastNotificationManagerCompat.OnActivatedイベントをリッスンしておくことでトーストコントロール上のユーザ操作を検知することができます。App.xaml.csとかで購読開始して、ユーザ操作を検知したらその内容に合わせて必要な処理を行うよう実装します。
＃イベントはUIスレッド以外で処理されるようなので、UIを触る場合はDispatcherを経由する必要があります。

```NotificationService.cs
public void Setup()
{
    // Listen to notification activation
    ToastNotificationManagerCompat.OnActivated += toastArgs =>
    {
        // Obtain the arguments from the notification
        ToastArguments args = ToastArguments.Parse(toastArgs.Argument);

        // Obtain any user input (text boxes, menu selections) from the notification
        ValueSet userInput = toastArgs.UserInput;

        // Need to dispatch to UI thread if performing UI operations
        Application.Current.Dispatcher.Invoke(delegate
        {
            if(args.TryGetValue(_ActionTypeCode, out string value))
            {
                switch(value)
                {
                    case _ActionTypeStartTask:
                        userInput.TryGetValue(_SelectionTaskKey, out object key);
                        StartSelectedWorkTask(key?.ToString());
                        break;
                }
            }
            else
            {
                ((MainWindow)Application.Current.MainWindow).ShowWindow();
            }
        });
    };
}

private void StartSelectedWorkTask(string workTaskId)
{
    var contents = MainWindowViewModel.Instance.Contents.OfType<WorkUnitRecorderViewModel>().First();

    var selectedTask = contents.PlanedTaskCards.FirstOrDefault(c => c.Dto.TaskId.Value.ToString() == workTaskId);
    selectedTask?.StartOrStopWorkTask();
}
```

ポイントは２点。

- ToastArgumentsにはどのボタンを押下したかの情報が格納されている  
　⇒ 今回の場合、開始ボタンを押下するとKey=_ActionTypeCode, Value=_ActionTypeStartTaskの情報が格納される
- ValueSetにはコンボボックスで選択した項目の情報が格納されている  
　⇒ 今回の場合、選択したタスクのIDが格納される 


## おわりに
ターゲットフレームワークの変更は必要でしたが、それ以外は数十行のコードを書くだけでトースト通知が実装できました！去年の状況からは想像もできない簡単さですねぇ。
一時期は「クラシックデスクトップアプリ」なんて呼ばれてしまっていたWPFですが、Microsoftの方針転換のおかげで最新機能が簡単に実装できるようになってきてます。WPF、まだまだイケるぞ！


## 参考サイト

https://docs.microsoft.com/ja-jp/windows/uwp/design/shell/tiles-and-notifications/send-local-toast?tabs=desktop

https://docs.microsoft.com/ja-jp/windows/uwp/design/shell/tiles-and-notifications/adaptive-interactive-toasts?tabs=builder-syntax#selection-input