UWP アプリの最小化によるSuspendを考慮したバックグラウンド処理

# 実現したい内容

UWPアプリでバックグラウンド処理を用意し、一定時間間隔でインターネットから画像ファイルを取得し壁紙に設定する。アプリが最小化されSuspend(中断)しても、バックグラウンド処理を継続する。アプリが最小化から復帰したら、バックグラウンド処理から最新の画像ファイルの情報を取得し、UIを更新する。

基本のお作法は、[MS文書](https://learn.microsoft.com/en-us/windows/uwp/launch-resume/create-and-register-a-background-task) に記された内容に従い、周辺のコード群を補足する。

:::note
一定時間間隔での処理の実行は、`ThreadPoolTimer`を利用できるが、UIを最小化するとthreadが`Suspended`になり止まる。止めないためには、バックグラウンド処理での実装が必要。
:::

:::note
バックグラウンド処理は、長時間の実行を想定するので、out-of-processのバックグラウンド処理として実装する。
:::

# UI側の処理(MainPage)

- バックグラウンド処理の登録
- バックグランド処理完了時の処理
- UI最小化によるSuspendからの復帰まわりの処理
- UI側とバックグラウンド側との情報のやりとり

## バックグラウンド処理の登録

- 時間トリガを作成してバックグラウンド処理を登録。
- 完了時の`Completed`イベントにハンドラを登録。ここでは`OnCompleted`を呼び出す。
- バックグラウンド処理に情報を引き渡すために、`ApplicationData.Current.LocalSettings`にデータを設定。大域変数で情報を引き渡すことが出来る相手ではない。

```C#:バックグラウンド処理の登録
private BackgroundTaskRegistration _BGTask = null;
private String _BGTaskName = "VolcanoWallpaperBGTask";
// "バックグラウンド処理のプロジェクト名.バックグラウンド処理のクラス名"
private String _BGTaskEntryPoint = "VolcanoWallpaperBG.VolcanoWallpaperBGTask";

private void RegisterBGTimerTask(int hour)
{
    // 引数チェックなど
    ...

    if (_BGTask != null)
    {
        UnregisterBGTimerTask();
    }

    uint triggerInterval = (uint)hour * 60;
    IBackgroundTrigger trigger = new TimeTrigger(triggerInterval, false);

    var builder = new BackgroundTaskBuilder();
    builder.Name = _BGTaskName;
    builder.TaskEntryPoint = _BGTaskEntryPoint;
    builder.AddCondition(new SystemCondition(SystemConditionType.InternetAvailable));
    builder.SetTrigger(trigger);            

    BackgroundTaskRegistration task = builder.Register();
    _BGTask = task;

    task.Completed += new BackgroundTaskCompletedEventHandler(OnCompleted);

    ApplicationData.Current.LocalSettings.Values["currentVolcanoCode"] = _CurrentVolcano.Code;
}
```

二重登録にならないよう、登録解除処理を準備。

```C#:バックグラウンド処理の登録解除
private void UnregisterBGTimerTask()
{
    _BGTask?.Unregister(false);
    _BGTask = null;
}
```

## バックグラウンド処理完了時の処理

バックグラウンド処理が完了時に発生する`OnCompleted`イベントで呼び出される処理を準備。アプリのUI部とは別スレッドになるので、`Dispatcher.RunAsync()`でUIを更新。UIが最小化していないときは、こちらが呼び出される。

```C#:バックグラウンド処理が完了したときに呼び出される処理
private async void OnCompleted(IBackgroundTaskRegistration task, BackgroundTaskCompletedEventArgs args)
{
    Debug.WriteLine(String.Format("Background task completed: {0} {1}",task.TaskId, task.Name));
    await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
    {
        UpdateUI();
    });                        
}
```
## UIの最小化解除時のイベントハンドラの登録

`Application.Current.Resuming`イベントにハンドラを登録。UIの最小化が解除されるときに呼び出される。ここでは、`AppResuming`を呼び出す。

```C#:MainPageでイベントの登録
public MainPage()
{
    this.InitializeComponent();

    Application.Current.Suspending += new SuspendingEventHandler(AppSuspending);
    Application.Current.Resuming += new EventHandler<Object>(AppResuming);
}
```

対の`Application.Current.Suspending`イベントも登録しているが、本稿では未使用。

## UIの最小化が解除されたときの処理

アプリのUIの最小化が解除されると呼び出される。最小化中にもバックグラウンド処理が動作し、最小化前とは状態が変化しているので、最新の状態でUIを更新する。

```C#:アプリのSuspendが解除されたときの処理
private void AppResuming(Object sender, Object e)
{
    if (_BGTask != null)
    {
        UpdateUI();
    }         
}
```

## バックグラウンド処理からの情報の受け取り

バックグラウンド処理からは、`ApplicationData.Current.LocalSettings`を介して情報を受け取る。これらを元にUIを更新する。

```C#:UIの更新処理
public void UpdateUI()
{
    // バックグラウンド処理からの情報の受け取り
    String folder = (String)ApplicationData.Current.LocalSettings.Values["imgFolder"];
    String imgFname = (String)ApplicationData.Current.LocalSettings.Values["imgFilename"];
    String wallpaperFname = (String)ApplicationData.Current.LocalSettings.Values["wallpaperFilename"];

    // UIの更新処理
    ...
}
```

# バックグラウンド側の処理

UI側のUWPプロジェクトとは別に、Windowsランタイムコンポーネントのプロジェクトとして作成する。`ApplicationData.Current.LocalSettings`を介して、UI側に情報を引き渡す。

```C#:バックグラウンド処理の本体
public sealed class VolcanoWallpaperBGTask : IBackgroundTask
{
    private IBackgroundTaskInstance _TaskInstance;
    private BackgroundTaskDeferral _Deferral;

    public async void Run(IBackgroundTaskInstance taskInstance)
    {
        _TaskInstance = taskInstance;
        _Deferral = taskInstance.GetDeferral();
        taskInstance.Canceled += new BackgroundTaskCanceledEventHandler(OnCanceled);

        // 処理の本体
        ...

        // UI側に引き渡す情報の設定
        ApplicationData.Current.LocalSettings.Values["imgFolder"] = vi.Folder;
        ApplicationData.Current.LocalSettings.Values["imgFilename"] = vi.ImageFilename;
        ApplicationData.Current.LocalSettings.Values["wallpaperFilename"] = _FileManager.WallpaperName;

        _Deferral.Complete();
    }

    private void OnCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
    {
        _Deferral.Complete();
    }
}
```

# 補足

アプリのUI側とバックグラウンド処理は別プロジェクトになるので、両者で使用する処理はSharedのプロジェクトを設けて共用する。例えば、UIからの指示でもバックグラウンド処理でも画像を取得する機能を設ける場合、画像を取得する処理はSharedのプロジェクトで実装する。