ClickOnce に関するまとめ

## はじめに ##

このところ ClickOnce に関連する機能拡張は行われていませんが、windows プラットフォームで .NET Framework アプリケーションの自動更新を行う手段としては今でも有用です。ClickOnce 標準の更新ダイアログを表示しない運用が必要になったのを機に、各種設定をまとめてみました。

## アプリケーション起動時の自動更新 ##

ClickOnce 標準の自動更新機能によって更新が行われるかどうかは、発行設定の内容と起動方法によって決まります。プログラムコードで更新を制御するには、発行設定の `アプリケーションの更新プログラムを確認する` を無効にします。

### 発行設定 ###

* アプリケーションの更新プログラムを確認する
  * このオプションを有効にすると、ClickOnce 標準の自動更新機能によって更新が行われます。
  * `アプリケーションの開始前に行う` を選択した場合、アプリケーションが起動される前に更新チェックが行われます。更新バージョンが存在する場合、更新ダイアログが表示されます。
  * `アプリケーションの開始後に行う` を選択した場合、アプリケーションが起動された後でバックグラウンドで更新チェックが行われます。更新バージョンが存在する場合、次回起動時に更新ダイアログが表示されます。**新バージョン発行直後の一回目の起動は旧バージョンのままであることに注意が必要です。**すぐに最新バージョンを使用させる必要があるアプリケーションの場合、後述する ApplicationDeployment クラスを用いて、更新バージョンが存在する場合は再起動を促すように実装したほうがよいでしょう。
  * `アプリケーションが実行されるたびに確認する` を選択した場合、アプリケーションを起動するたびに更新チェックが行われます。更新ダイアログでキャンセルボタンが押された場合、7日間更新チェックが行われなくなります。
  * `確認する間隔` を選択した場合、最後の更新チェック日時から指定期間経過した後の初めての起動時に更新チェックが行われます。この間隔は、更新ダイアログでキャンセルボタンが押された場合の次の更新チェックまでの間隔にも適用されます。
* このアプリケーションに最低限必要なバージョン
  * 現在のバージョンがこのバージョンよりも古い場合、更新ダイアログによる確認が行われずに強制的に更新されるようになります。最新バージョンと同じバージョンを最低バージョンに設定すると、常に強制的に最新バージョンをダウンロードさせることができます。
  * ここで指定したバージョンは更新チェックを行ったときに初めてクライアントに読み込まれます。そのため、更新チェックを行わない設定になっている場合は更新チェックを行うまで強制ダウンロードは行われません。

### 起動方法 ###

* ショートカットアイコン（appref-ms）から起動する
  * ClickOnce アプリケーションをインストールすると、スタートメニューにショートカットアイコンが登録されます。上記の発行設定はこのショートカットから起動した場合に適用されます。
* WEBページ（publish.htm）からインストールする
  * 上記の発行設定は適用されません。更新バージョンが存在する場合は無条件に更新されます。「起動」ではなく「インストール」ですから当然ですね。

### その他の条件 ###

* 更新処理に失敗すると、`アプリケーションの更新プログラムを確認する` の設定に関わらず次回起動時に更新チェックが行われ、ClickOnce 標準の自動更新機能によって更新が行われます。

### 設定例 ###

| ケース | アプリケーションの更新プログラムを確認する | このアプリケーションに最低限必要なバージョン |
|:--|:-:|:-:|
| 常に最新バージョンを利用させたい | 開始前に確認する | 発行バージョンと同じバージョンを指定する |
| 特定バージョンよりも古いバージョンを切り捨てたい | 開始前に確認する | その特定バージョンを指定する |
| とにかく起動時間を短くしたい | 開始後に確認する or 確認しない（起動後に更新を促したければ要コーディング） | 任意 |
| プログラムコードで制御したい | 確認しない | 任意 |

## 更新バージョンの確認 ##

アプリケーションの更新バージョンが存在するかどうかを確認するには、ApplicationDeployment クラスの次のメソッドを呼び出します。

* CheckForUpdate メソッド
  * 更新バージョンが存在するかどうかを bool 値で返します。更新バージョン情報は ApplicationDeployment クラスのプロパティから取得できます。
* CheckForUpdateAsync メソッド
  * 更新バージョンが存在するかどうかを非同期処理で取得します。Task 方式ではなく、イベントコールバック方式の非同期処理です。進捗通知イベントと完了通知イベントが定義されています。
  * 更新バージョン情報は完了通知イベント（ CheckForUpdateCompleted ）で取得できます。
* CheckForDetailedUpdate メソッド
  * 更新バージョン情報を取得します。
  * 更新バージョンが存在するのであればそのバージョン番号を知りたいという場合、CheckForUpdate メソッドよりもこちらのメソッドのほうが分かりやすく実装できます。
  * 引数の persistUpdateCheckResult に false を指定すると、ClickOnce 標準のダイアログを非表示にできます。
  
## 更新バージョン情報 ##

更新バージョン情報は ApplicationDeployment クラスのプロパティから取得できます。
CheckForUpdateCompleted イベントのイベントデータである DeploymentProgressChangedEventArgs クラスと、CheckForDetailedUpdate メソッドの戻り値である UpdateCheckInfo クラスにも同名の同じ役割のプロパティが定義されています。

| プロパティ | 説明 |
|:--|:--|
| UpdateAvailable | 現在のバージョンよりも新しいバージョンが発行されているかどうか。**このプロパティが false を返す場合、以降のプロパティの値を取得しようとすると InvalidOperationException がスローされます。** |
| AvailableVersion | 更新バージョンのバージョン番号。 |
| IsUpdateRequired | 更新が要求されているかどうか。現在のバージョンが MinimumRequiredVersion よりも古い場合に true を返します。 |
| MinimumRequiredVersion | 発行設定の「このアプリケーションに最低限必要なバージョン」で指定されているバージョン。 |
| UpdateSizeBytes | 更新バージョンのサイズ。 |


## 更新 ##

アプリケーションのバージョンを更新するには、ApplicationDeployment クラスの次のメソッドを呼び出します。

* Update メソッド
  * 最新バージョンに更新します。
  * 既に最新バージョンがインストールされている場合は更新処理は行われず、戻り値は false を返します。
* UpdateAsync メソッド
  * 非同期処理で最新バージョンに更新します。Task 方式ではなく、イベントコールバック方式の非同期処理です。進捗通知イベントと完了通知イベントが定義されています。
  * 既に最新バージョンがインストールされている場合は更新処理は行われませんが、完了通知イベント（ UpdateCompleted ）では更新されたかどうかを取得できません。

## ダウンロードグループ ##

発行設定でアプリケーションファイルに対して `ダウンロードグループ` を設定しておくと、アプリケーション本体と分離して任意のタイミングでダウンロードさせることができます。アプリケーション本体のファイルサイズを小さくすることによって更新処理にかかる時間を短縮したり負荷を軽減したりすることを目的とした機能です。

### 遅延ダウンロードの仕組み ###

ClickOnce では PC のローカルドライブにアプリケーションファイルがダウンロードされますが、保持されるのはカレントバージョンとその直前のバージョンのみです。バージョンアップが行われると、カレントバージョンのファイルが一つ前のバージョンとして退避され、最新バージョンのファイルがカレントバージョンとしてダウンロードされます。このときダウンロードグループが設定されているファイルはダウンロードされません。カレントバージョンのフォルダにはファイルが存在しない状態になりますので、この状態でアプリケーションがそれらのファイルを参照しようとすると例外が発生したりします。参照される前にダウンロードしておく必要があります。

| ダウンロードグループ | カレントバージョン | 直前バージョン |
|:-:|:-:|:-:|
| 設定なし | 1.1.0.0 | 1.0.0.0 |
| 設定あり | 1.1.0.0 | 1.0.0.0 |

バージョン更新によってアプリケーション本体の最新バージョンがダウンロードされると…

| ダウンロードグループ | カレントバージョン | 直前バージョン |
|:-:|:-:|:-:|
| 設定なし | 1.2.0.0 | 1.1.0.0 |
| 設定あり | ファイルなし | 1.1.0.0 |

ダウンロードグループのダウンロードを行うと…

| ダウンロードグループ | カレントバージョン | 直前バージョン |
|:-:|:-:|:-:|
| 設定なし | 1.2.0.0 | 1.1.0.0 |
| 設定あり | 1.2.0.0 | 1.1.0.0 |

### 実装方法 ###

ダウンロードグループを設定したファイルをダウンロードするには、ApplicationDeployment クラスの次のメソッドを呼び出します。ClickOnce 標準の自動更新機能にはダウンロードグループのダウンロード機能はありません。ダウンロードグループを利用したければ、ダウンロードグループのダウンロード処理だけはコーディングで実装する必要があります。

* IsFileGroupDownloaded メソッド
  * 指定されたダウンロードグループのファイルがダウンロード済かどうかを取得します。
* DownloadFileGroup メソッド
  * 指定されたダウンロードグループのファイルをダウンロードします。
  * 既に最新バージョンがインストールされている場合は更新処理は行われませんが、戻り値はないため判断できません。
* DownloadFileGroupAsync メソッド
  * 指定されたダウンロードグループのファイルを非同期処理でダウンロードします。Task 方式ではなく、イベントコールバック方式の非同期処理です。進捗通知イベントと完了通知イベントが定義されています。
  * 既に最新バージョンがインストールされている場合は更新処理は行われませんが、完了通知イベント（ UpdateCompleted ）では更新されたかどうかを取得できません。

基幹業務システムなどではメニューや機能が階層化されていることが多いですので、次の階層に遷移する前にダウンロードグループがダウンロードされているかどうかを IsFileGroupDownloaded で確認し、ダウンロードされていなければ DownloadFileGroup または DownloadFileGroupAsync メソッドでダウンロードしてから遷移するのがシンプルな方法です。

このような方法が採用できないケースでは、AppDomain クラスの AssemblyResolve イベントを利用する方法が採られることが多いです。このイベントは参照しようとしたアセンブリが見つからないときに発生します。イベントデータからアセンブリ名を取得し、そのアセンブリに対応するダウンロードグループをダウンロードするように実装します。アセンブリ名からダウンロードグループを判別する仕組みは個別に実装する必要があります。なお、当然ながらこの方法は対象ファイルがアセンブリファイルである場合のみです。

## おわりに ##

ClickOnce の設定はやや直感的でない表現があってとっつきにくい印象はありますが、適切に設定すればいろいろなケースに対応できます。ただ、ApplicationDeployment クラスはかなり以前（.NET Framework 2.0 時代？）に作られたものだけあって、非同期処理をはじめ、設計が古いように感じます。今回調べた内容をもとに汎用クラスを作成してみようと考えています。