アプリが中断されてもファイルのダウンロードを継続するには?[ユニバーサルWindowsアプリ開発]WinRT/Metro TIPS

Windows.Networking.BackgroundTransfer名前空間が提供するAPIを使って、アプリが中断中でもダウンロードを継続させる方法を解説する。

» 2015年07月15日 05時00分 公開
[山本康彦BluewaterSoft/Microsoft MVP for Windows Platform Development]
WinRT/Metro TIPS
業務アプリInsider/Insider.NET

powered by Insider.NET

「WinRT/Metro TIPS」のインデックス

連載目次

 Windowsランタイムアプリでは、大きなファイルをダウンロードしている最中にアプリが中断されるとダウンロードも中断してしまうし、アプリが終了させられてしまうとダウンロードも停止してしまう。アプリが中断/終了してもダウンロードを継続できたらよいのにと思ったことはないだろうか? Windows.Networking.BackgroundTransfer名前空間に用意されているAPIを使えば、それが可能なのだ。アプリが終了させられた後でもダウンロードは継続し、次に起動されたときに後続する処理を実行できるのである。

 本稿では、HTTPプロトコルによるダウンロードをバックグラウンドで継続させる方法と、ダウンロード中の進捗(しんちょく)状況を取得する方法を解説する。なお、本稿のサンプルは「Windows Store app samples:MetroTips #109」からダウンロードできる。

事前準備

 ユニバーサルプロジェクトを使ってWindows 8.1/Windows Phone 8.1用のユニバーサルWindowsアプリを開発するには、以下の開発環境が必要である。本稿では、無償のVisual Studio Community 2013 with Update 4を使っている。

  • SLAT対応のPC*1
  • 2014年4月のアップデート*2適用済みの64bit版Windows 8.1 Pro版以上*3
  • Visual Studio 2013 Update 2(またはそれ以降)*4を適用済みのVisual Studio 2013(以降、VS 2013)*5

*1 SLAT対応ハードウエアは、Windows Phone 8.1エミュレーターの実行に必要だ。ただし未対応でも、ソースコードのビルドと実機でのデバッグは可能だ。SLAT対応のチェック方法はMSDNブログの「Windows Phone SDK 8.0 ダウンロードポイント と Second Level Address Translation (SLAT) 対応PCかどうかを判定する方法」を参照。なお、SLAT対応ハードウエアであっても、VM上ではエミュレーターが動作しないことがあるのでご注意願いたい。

*2 事前には「Windows 8.1 Update 1」と呼ばれていたアップデート。スタート画面の右上に検索ボタンが(環境によっては電源ボタンも)表示されるようになるので、適用済みかどうかは簡単に見分けられる。ちなみに公式呼称は「the Windows RT 8.1, Windows 8.1, and Windows Server 2012 R2 update that is dated April, 2014」というようである。

*3 Windows Phone 8.1エミュレーターを使用しないのであれば、32bit版のWindows 8.1でもよい。

*4 マイクロソフトのダウンロードページから誰でも入手できる(このURLはUpdate 4のもの)。

*5 本稿に掲載したコードを試すだけなら、無償のExpressエディションやCommunityエディションで構わない。Visual Studio Express 2013 with Update 4 for Windows(製品版)はマイクロソフトのページから無償で入手できる。Expressエディションはターゲットプラットフォームごとに製品が分かれていて紛らわしいが、Windowsランタイムアプリの開発には「for Windows」を使う(「for Windows Desktop」はデスクトップで動作するアプリ用)。また、Visual Studio Community 2013 with Update 4(製品版)もマイクロソフトのページから無償で入手できる。Communityエディションは本稿執筆時点では英語版だけなので、同じ場所にあるVisual Studio 2013 Language Packの日本語版を追加インストールし、[オプション]ダイアログで言語を切り替える必要がある。


サンプルコードについて

 Visual Studio 2013 Update 2(Update 3/4も)では、残念なことにVB用のユニバーサルプロジェクトのテンプレートは含まれていない*6。そのため、本稿で紹介するVBのコードはユニバーサルプロジェクトではなく、PCL(ポータブルクラスライブラリ)を使ったプロジェクトのものである。

*6 VB用のユニバーサルプロジェクトは、2015年の夏にリリースされるといわれているVisual Studio 2015(開発コード「Visual Studio 14」)からの提供となるようだ。プレビュー版で、すでに共有プロジェクトは利用可能になっている。「特集:次期Visual Studioの全貌を探る:Visual Basic 14の新機能ベスト10〜もう「VBだから」とは言わせない!」参照。


BackgroundTransferで可能なこと

 Windows.Networking.BackgroundTransfer名前空間に用意されているAPIを利用すると、次のようなことが可能だ。

  • バックグラウンドでのダウンロード(HTTP/HTTPS/FTP)
  • バックグラウンドでのアップロード(HTTP/HTTPS)
  • 進捗状況の表示

 本稿では、HTTPのダウンロードと進捗状況を取得する方法を解説する。別途公開のサンプルコードには、取得した進捗状況をプログレスバーやテキストボックスに表示する実装も入っているので、ぜひご覧いただきたい(次の画像)。

別途公開のサンプルコードを実行している様子
別途公開のサンプルコードを実行している様子 別途公開のサンプルコードを実行している様子
上はWindows 8.1、下はWindows Phone 8.1エミュレーターでの実行である。本稿では、このUIの作り方やプログレスバーへの表示方法などは説明しない。別途公開のサンプルコードをご覧いただきたい。

using/Import宣言を追加する

 以降に示すコードは、ソースファイルの先頭に次のようなusing/Import宣言が必要である。

using System;
using System.Linq;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.Networking.BackgroundTransfer;
using Windows.Storage;

Imports Windows.Networking.BackgroundTransfer

以降のコードで必要となるusing/Import宣言(上:C#/下:VB)
これらの宣言でテンプレートから生成されたソースコードに欠けているものは、追加しておいてほしい。

ダウンロードを開始するには?

 まず、BackgroundDownloaderオブジェクト(Windows.Networking.BackgroundTransfer名前空間)のCreateDownloadメソッドに、ダウンロードしたいファイルを示すUriオブジェクト(System名前空間)と保存先のStorageFileオブジェクト(Windows.Storage名前空間)を渡して、DownloadOperation(Windows.Networking.BackgroundTransfer名前空間)オブジェクトを得る。得られたDownloadOperationオブジェクトのStartAsyncメソッドを呼び出すと、バックグラウンドでのダウンロードが始まる。それらのコードを「StartDownloadAsync」というメソッドにまとめると、次のようになる。

// バックグラウンドダウンロードを開始する
public static async Task StartDownloadAsync(Uri uri)
{
  // 保存先のファイル
  StorageFile destFile = ……省略……

  // バックグラウンドダウンローダーのオブジェクトを作る
  BackgroundDownloader downloader = new BackgroundDownloader();

  // URLと保存先ファイルを指定してダウンロード操作オブジェクトを作る
  DownloadOperation download = downloader.CreateDownload(uri, destFile);

  // 必要に応じてダウンロード操作オブジェクトにオプションを設定する
  // 例:従量制課金接続ではダウンロードを中断する
  // download.CostPolicy = BackgroundTransferCostPolicy.UnrestrictedOnly;
  // 注:エミュレーターでは従量制課金接続(のエミュレーション)しか使えないので、
  //     このオプションを付けているとダウンロードできない

  // ダウンロード操作オブジェクトにバックグラウンドでのダウンロードを開始させる
  // 注:StartAsyncはワーカースレッドで呼び出す(そうしないとブロックされる)
  await Task.Run(() => download.StartAsync());
}

' バックグラウンドダウンロードを開始する
Public Shared Async Function StartDownloadAsync(uri As Uri) As Task
  ' 保存先のファイル
  Dim destFile As StorageFile = ……省略……

  ' バックグラウンドダウンローダーのオブジェクトを作る
  Dim downloader As BackgroundDownloader = New BackgroundDownloader()

  ' URLと保存先ファイルを指定してダウンロード操作オブジェクトを作る
  Dim download As DownloadOperation = downloader.CreateDownload(uri, destFile)

  ' 必要に応じてダウンロード操作オブジェクトにオプションを設定する
  ' 例:従量制課金接続ではダウンロードを中断する
  ' download.CostPolicy = BackgroundTransferCostPolicy.UnrestrictedOnly
  ' 注:エミュレーターでは従量制課金接続(のエミュレーション)しか使えないので、
  '     このオプションを付けているとダウンロードできない

  ' ダウンロード操作オブジェクトにバックグラウンドダウンロードを開始させる
  ' 注:StartAsyncはワーカースレッドで呼び出す(そうしないとブロックされる)
  Await Task.Run(Function() download.StartAsync())
End Function

StartDownloadAsyncメソッド(上:C#/下:VB)
ダウンロードしたいURLからUriオブジェクトを作り、それを引数としてこのメソッドを呼び出せば、バックグラウンドでのダウンロードが開始される。しばらくしてから保存先のファイルを確認してみよう。ファイルがダウンロードされているはずだ。

 このコードだけでダウンロードできるのだが、このままでは進捗状況が分からない。少なくともダウンロードの完了が分からなければ、次の処理に移れないだろう。

開始したダウンロード操作を得るには?

 上のStartDownloadAsyncメソッドは値を返していないことに気付かれただろう。開始したダウンロード操作(=DownloadOperationオブジェクト)を得るにはどうしたらよいのだろうか?

 BackgroundDownloaderオブジェクトのCreateDownloadメソッドを呼び出したときの返値を保持しておいてもよさそうだ。しかし、アプリが終了させられて次に起動したときには、その方法は使えない。汎用的にいつでも使える方法は、BackgroundDownloaderクラスのGetCurrentDownloadsAsyncメソッドでDownloadOperationオブジェクトを列挙する方法である。最新のDownloadOperationオブジェクトだけを取り出すコードを「GetLastDownloadAsync」というメソッドにまとめると、次のようになる。

// 現在進行中のダウンロード操作(複数あるときは最後のもの)を得る
public static async Task<DownloadOperation> GetLastDownloadAsync()
{
  var downloads 
    = await BackgroundDownloader.GetCurrentDownloadsAsync();
  return downloads.LastOrDefault();
}

' 現在進行中のダウンロード(複数あるときは最後のもの)を得る
Public Shared Async Function GetLastDownloadAsync() As Task(Of DownloadOperation)
  Dim downloads _
    = Await BackgroundDownloader.GetCurrentDownloadsAsync()
  Return downloads.LastOrDefault()
End Function

GetLastDownloadAsyncメソッド(上:C#/下:VB)
本稿では、同時に進行するダウンロードは一つだけだと想定している。複数のダウンロードを同時に進行する場合は、GetCurrentDownloadsAsyncメソッドで得られたDownloadOperationオブジェクトのコレクションの中から目的のものを見つけ出す必要がある。それには、DownloadOperationオブジェクトのRequestedUriプロパティ(ダウンロードしているURLを表すUriオブジェクト)とResultFile(保存先のファイルを表すWindows.Storage.Streams名前空間のIStorageFileオブジェクト)が役立つだろう。

ダウンロード状況を確認するには?〜その1:ポーリング

 進行中の状況を確認する簡単な方法は、ポーリング(=定期的に確認すること)である。先のGetLastDownloadAsyncメソッドで取得したダウンロード操作オブジェクトのProgressプロパティのStatusプロパティを見ればよい。そのコードは、次の「GetStatusWithoutAttachAsync」メソッドのようになる。

// 現在進行中のダウンロード操作の状態を得る
public static async Task<BackgroundTransferStatus> GetStatusWithoutAttachAsync()
{
  var download = await GetLastDownloadAsync();
  if (download == null)
    return BackgroundTransferStatus.Idle;

  return download.Progress.Status;
}

' 現在進行中のダウンロード操作の状態を得る
Public Shared Async Function GetStatusWithoutAttachAsync() As Task(Of BackgroundTransferStatus)
  Dim download = Await GetLastDownloadAsync()
  If (download Is Nothing) Then
    Return BackgroundTransferStatus.Idle
  End If
  Return download.Progress.Status
End Function

GetStatusWithoutAttachAsyncメソッド(上:C#/下:VB)
このメソッドが返す値はBackgroundTransferStatus列挙体であり、次のような値をとる。
  • Idle: アイドル状態。
  • Running: 転送は現在進行中。
  • PausedByApplication: 転送操作を一時停止中(アプリからの指示)。
  • PausedCostedNetwork: 転送操作を一時停止中(ネットワークコストのポリシーによる)。
  • PausedNoNetwork: 転送操作を一時停止中(ネットワーク切断による)。
  • Completed: 転送操作はすでに完了。
  • Canceled: 転送操作はキャンセルされた。
  • Error: 転送操作の実行中にエラーが発生した。
  • PausedSystemPolicy: 転送操作を一時停止中(システムポリシーによる、Phone専用)。

 そして、ダウンロード操作の結果を知りたい場合には、ダウンロード操作オブジェクトのAttachAsyncメソッドを呼び出してからStatusプロパティを見る。この場合には、ダウンロード操作が終了するまで結果が返ってこないことに注意が必要だ。そのコードは、次の「GetAttachedStatusAsync」メソッドのようになる。

// 現在進行中のダウンロード操作にアタッチしてその結果を得る
public static async Task<BackgroundTransferStatus> GetAttachedStatusAsync()
{
  var download = await GetLastDownloadAsync();
  if (download == null)
    return BackgroundTransferStatus.Idle;

  // ダウンロードが進行中の場合は、終了するまで待たされる
  var attachedDownload = await download.AttachAsync();
  return attachedDownload.Progress.Status;
}

' 現在進行中のダウンロード操作にアタッチしてその結果を得る
Public Shared Async Function GetAttachedStatusAsync() As Task(Of BackgroundTransferStatus)
  Dim download = Await GetLastDownloadAsync()
  If (download Is Nothing) Then
    Return BackgroundTransferStatus.Idle
  End If

  ' ダウンロードが進行中の場合は、終了するまで待たされる
  Dim attachedDownload = Await download.AttachAsync()
  Return attachedDownload.Progress.Status
End Function

GetAttachedStatusAsyncメソッド(上:C#/下:VB)
AttachAsyncメソッド呼び出して得られるダウンロード操作オブジェクトは、ダウンロードが終了したものになる。ダウンロードの状態を取得するというよりも、終了を待つためのメソッドになっている。

 アタッチした場合には、ダウンロードが終わるまでメソッドから返ってこない。ポーリングするロジックは、そのことを利用して次のように書けばよいだろう。

  1. StartDownloadAsyncメソッドを呼び出してダウンロードを開始する
  2. 定期的にGetStatusWithoutAttachAsyncメソッドを呼び出すポーリングを開始する
  3. GetAttachedStatusAsyncメソッドを呼び出す。結果が返ってきたらポーリングを停止する

ダウンロード状況を確認するには?〜その2:イベントハンドラー

 コーディングが少々複雑になるが、イベントハンドラーを使えばポーリングするよりもきれいなコードになる。

 まず、イベントを処理するハンドラーメソッドを二つ記述する。進捗を受ける「ProgressHandler」メソッドと、ダウンロードが終了したときに呼び出される「CompletedHandler」だ(次のコード)。

// ダウンロード操作の進捗によって呼び出されるハンドラー
private async void ProgressHandler(
                    IAsyncOperationWithProgress<DownloadOperation, DownloadOperation> asyncInfo,
                    DownloadOperation progressInfo)
{
  // ダウンロード操作の状態
  BackgroundTransferStatus status = progressInfo.Progress.Status;

  // ダウンロード済みデータ量とファイル全体のサイズ
  ulong received = progressInfo.Progress.BytesReceived;
  ulong totalSize = progressInfo.Progress.TotalBytesToReceive;
  // 注:TotalBytesToReceiveは取得できないこともある

  await this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority .Normal, () =>
  {
    // UIを操作する
    ……省略……
  });
}

// ダウンロード操作が終了したときに呼び出されるハンドラー
private async void CompletedHandler(
                    IAsyncOperationWithProgress<DownloadOperation, DownloadOperation> asyncInfo,
                    AsyncStatus asyncStatus)
{
  switch (asyncStatus)
  {
    case AsyncStatus.Completed: 
      // ダウンロードが成功したときの処理
      var op = asyncInfo.GetResults();
      var file = op.ResultFile; // ダウンロードしたファイル
      ulong totalSize = op.Progress.TotalBytesToReceive; // ファイルサイズ
      await this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
      {
        // UIを操作する
        ……省略……
      });
      break;
    case AsyncStatus.Canceled:
      // ダウンロードがキャンセルされたときの処理を書く
      break;
    case AsyncStatus.Error:
      // ダウンロードが失敗したときの処理を書く
      break;
    default:
      break;
  }
}

' ダウンロード操作の進捗によって呼び出されるハンドラー
Private Async Sub ProgressHandler(
    asyncInfo As IAsyncOperationWithProgress(Of DownloadOperation, DownloadOperation),
    progressInfo As DownloadOperation
  )

  ' ダウンロード操作の状態
  Dim status As BackgroundTransferStatus = progressInfo.Progress.Status

  ' 受信済みデータ量とファイル全体のサイズ
  Dim received As ULong = progressInfo.Progress.BytesReceived
  Dim totalSize As ULong = progressInfo.Progress.TotalBytesToReceive
  ' 注:TotalBytesToReceiveは取得できないこともある

  Await Me.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal,
    Sub()
      ' UIを操作する
      ……省略……
    End Sub)
  End Sub

' ダウンロード操作が終了したときに呼び出されるハンドラー
Private Async Sub CompletedHandler(
    asyncInfo As IAsyncOperationWithProgress(Of DownloadOperation, DownloadOperation),
    asyncStatus As AsyncStatus
  )

  Select Case asyncStatus
    Case asyncStatus.Completed
      ' ダウンロードが成功したときの処理
      Dim op = asyncInfo.GetResults()
      Dim file = op.ResultFile ' ダウンロードしたファイル
      Dim totalSize As ULong = op.Progress.TotalBytesToReceive ' ファイルサイズ
      Await Me.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal,
        Sub()
          ' UIを操作する
          ……省略……
        End Sub)

    Case asyncStatus.Canceled
      ' ダウンロードがキャンセルされたときの処理を書く

    Case asyncStatus.Error
      ' ダウンロードが失敗したときの処理を書く

  End Select
End Sub

ダウンロード操作のイベントを受けるハンドラーメソッドの概要(上:C#/下:VB)
このメソッドは、ページかユーザーコントロールの中に書く(Dispatcherを使ってUIを操作するため)。

 上のハンドラーを、ダウンロード開始直後にダウンロード操作オブジェクトへセットしてやればよい。そのコードを「SetDownloadHandlerAsync」メソッドとしてまとめると、次のようになる。

// 進行中/終了時のハンドラーをセットする
public static async Task SetDownloadHandlerAsync(
  AsyncOperationProgressHandler<DownloadOperation, DownloadOperation> progressHandler = null,
  AsyncOperationWithProgressCompletedHandler<DownloadOperation, DownloadOperation> completedHandler = null)
{
  var download = await GetLastDownloadAsync();
  if (download == null)
    return;

  // 目的のダウンロード操作にアタッチする
  IAsyncOperationWithProgress<DownloadOperation, DownloadOperation>
    operation = download.AttachAsync();

  // 進捗ハンドラーをセット
  if (progressHandler != null)
    operation.Progress = progressHandler;

  // 終了ハンドラーをセット
  if (completedHandler != null)
    operation.Completed = completedHandler;
}

' 進行中/終了時のハンドラーをセットする
Public Shared Async Function SetDownloadHandlerAsync(
  Optional progressHandler _
    As AsyncOperationProgressHandler(Of DownloadOperation, DownloadOperation) = Nothing,
  Optional completedHandler _
    As AsyncOperationWithProgressCompletedHandler(Of DownloadOperation, DownloadOperation) = Nothing
  ) _
  As Task

  Dim download = Await GetLastDownloadAsync()
  If (download Is Nothing) Then
    Return
  End If

  ' 目的のダウンロード操作にアタッチする
  Dim operation _
    As IAsyncOperationWithProgress(Of DownloadOperation, DownloadOperation) _
    = download.AttachAsync()

  ' 進捗ハンドラーをセット
  If (progressHandler IsNot Nothing) Then
    operation.Progress = progressHandler
  End If

  ' 終了ハンドラーをセット
  If (completedHandler IsNot Nothing) Then
    operation.Completed = completedHandler
  End If
End Function

SetDownloadHandlerAsyncメソッド(上:C#/下:VB)

 最後に、ダウンロードを開始する「StartDownloadAsync」メソッドを改修しよう。ダウンロードを開始した直後に、上の「SetDownloadHandlerAsync」メソッドを呼び出すようにするのだ。

// バックグラウンドダウンロードを開始する(改修版)
public static async Task StartDownloadAsync(Uri uri,
  AsyncOperationProgressHandler<DownloadOperation, DownloadOperation>
    progressHandler = null, 
  AsyncOperationWithProgressCompletedHandler<DownloadOperation, DownloadOperation>
    completedHandler = null)
{
  ……省略……
  await Task.Run(() => { return download.StartAsync(); });

  // 進捗と終了時のイベントハンドラーをセットする
  await SetDownloadHandlerAsync(progressHandler, completedHandler);
}

' バックグラウンドダウンロードを開始する(改修版)
Public Shared Async Function StartDownloadAsync(
  uri As Uri,
  Optional progressHandler _
    As AsyncOperationProgressHandler(Of DownloadOperation, DownloadOperation) = Nothing,
  Optional completedHandler _
    As AsyncOperationWithProgressCompletedHandler(Of DownloadOperation, DownloadOperation) = Nothing
  ) _
  As Task

  ……省略……
  Await Task.Run(Function() download.StartAsync())

  ' 進捗と終了時のイベントハンドラーをセットする
  Await SetDownloadHandlerAsync(progressHandler, completedHandler)

End Function

ハンドラーをセットするように改修したStartDownloadAsyncメソッド(上:C#/下:VB)
太字の部分を追加した。引数を追加し、SetDownloadHandlerAsyncメソッドの呼び出しを末尾に追加しただけである。
このメソッドを呼び出すときにハンドラーも渡せば、作成したダウンロード操作にそのハンドラーを設定してくれる。
別途公開のサンプルでは、[START]ボタンのクリック時にこのメソッドを呼び出している。

 これで、ダウンロードの進捗と終了時にハンドラーメソッドが呼び出されるようになった。本稿で示したハンドラーメソッドの中では、UIを操作することも可能である。

次の起動時にダウンロードを継続するには?

 実は、何もしなくてもダウンロードは継続している。あるいは、アプリが終了されてから次に起動されるまでの間に、ダウンロードが完了しているかもしれない。従って、次に起動されたときにやるべきことは、ダウンロード操作オブジェクトの状態を取得することである。

 ポーリングで状態を見る場合は、アプリの開始時、またはページやユーザーコントロールがロードされたときに、前に示した2と3だけを行えばよい(ダウンロードはすでに開始しているので1は不要)。

 2. 定期的にGetStatusWithoutAttachAsyncメソッドを呼び出すポーリングを開始する

 3. GetAttachedStatusAsyncメソッドを呼び出す。結果が返ってきたらポーリングを停止する

 ハンドラーを使う場合は、アプリの開始時、またはページやユーザーコントロールがロードされたときに、ハンドラーをセットすればよい。具体的には、前述の「SetDownloadHandlerAsync」メソッドを呼び出すだけである。このときすでにダウンロードが完了していた場合には、直ちに終了時のハンドラーが呼び出される。

 例えば別途公開のサンプルコードでは、ユーザーコントロールのロード時のコードは次のようになっている。サンプルということで、ポーリングする技法とハンドラーを使う方法を混在させているが、実際にはどちらか一方のみを使ってほしい。

async void MyUserControl0_Loaded(object sender, RoutedEventArgs e)
{
  ……省略……

  // ハンドラーを使う場合
  // アプリ開始時や画面表示時などで、ハンドラーを結び付ける
  await DownloaderSample.SetDownloadHandlerAsync(ProgressHandler, CompletedHandler);

  // 注意:
  // CompletedHandlerを結び付けたときに、すでに前回のダウンロードが終わっていた場合は、
  // 直ちにCompletedHandlerが呼び出され、以下で取得する状態はIdleになってしまう。
  // 前回のダウンロードが終わっていた場合のポーリングによる結果を正しく見るには、
  // 上のハンドラーを結び付けているコードをコメントアウトすること。

  // ポーリングする場合(その1):
  // まず、アタッチせずに状態を見てみる
  var status = await DownloaderSample.GetStatusWithoutAttachAsync();
  this.StatusText.Text += string.Format("アタッチせずに取得した状態(ロード時): {0} {1}\r\n",
                                        DateTime.Now.ToString("ss.fff"), status.ToString());
  ……省略……
  // 注:上はサンプルということで1回しか見ていないが、
  //     実際にはタイマーを利用するなどして定期的にチェックする

  // ポーリングする場合(その2):
  // アイドル状態でなかったときは、アタッチして終了状態を取得する(終了するまで返ってこない)
  if (status != BackgroundTransferStatus.Idle)
  {
    try
    {
      status = await DownloaderSample.GetAttachedStatusAsync();
      this.StatusText.Text += string.Format("アタッチして取得した状態(ロード時): {0} {1}\r\n",
                                            DateTime.Now.ToString("ss.fff"), status.ToString());
    }
    catch (Exception ex)
    {
      // ダウンロードがキャンセルされたときなど、例外が出る
      ……省略……
    }
    ……省略……
  }
}

Private Async Sub MyUserControl0_Loaded(sender As Object, e As RoutedEventArgs)
  ……省略……

  ' ハンドラーを使う場合
  ' アプリ開始時や画面表示時などで、ハンドラーを結び付ける
  Await DownloaderSample.SetDownloadHandlerAsync(AddressOf ProgressHandler, AddressOf CompletedHandler)

  ' 注意:
  ' CompletedHandlerを結び付けたときに、すでに前回のダウンロードが終わっていた場合は、
  ' 直ちにCompletedHandlerが呼び出され、以下で取得する状態はIdleになってしまう。
  ' 前回のダウンロードが終わっていた場合のポーリングによる結果を正しく見るには、
  ' 上のハンドラーを結び付けているコードをコメントアウトすること。

  ' ポーリングする場合(その1):
  ' まず、アタッチせずに状態を見てみる
  Dim status = Await DownloaderSample.GetStatusWithoutAttachAsync()
  Me.StatusText.Text &= String.Format("アタッチせずに取得した状態(ロード時): {0} {1}" & vbCrLf, 
                                      DateTime.Now.ToString("ss.fff"), status.ToString())
  ……省略……
  ' 注:上はサンプルということで1回しか見ていないが、
  '     実際にはタイマーを利用するなどして定期的にチェックする

  ' ポーリングする場合(その2):
  ' アイドル状態でなかったときは、アタッチして終了状態を取得する(終了するまで返ってこない)
  If (status <> BackgroundTransferStatus.Idle) Then
    Try
      status = Await DownloaderSample.GetAttachedStatusAsync()
      Me.StatusText.Text &= String.Format("アタッチして取得した状態(ロード時): {0} {1}" & vbCrLf, 
                                          DateTime.Now.ToString("ss.fff"), status.ToString())

    Catch ex As Exception
      ' ダウンロードがキャンセルされたときなど、例外が出る
      ……省略……
    End Try
    ……省略……
  End If
End Sub

次に起動されたときのコード例(上:C#/下:VB)
サンプルということで、ポーリングする技法とハンドラーを使う方法を混在させているが、実際にはどちらか一方のみを使ってほしい。
このコードの実行例は、次の画像を見ていただきたい。

次に起動されたときの実行例
次に起動されたときの実行例 次に起動されたときの実行例
上はWindows 8.1、下はWindows Phone 8.1エミュレーターでの実行である。
ユーザーコントロールがロードされたときに上記のコードのようにして状態をチェックしている。最初に得られる状態は「Running」だったり「PausedNoNetwork」だったりとバラバラであるが、停止中であっても自動的に再開される(アプリ側で何かをする必要はない)。
下側のテキストボックスの表示で行頭が「progress:」または「completed:」で始まっている行は、ハンドラーメソッドからの出力である。この画像は、ダウンロード開始直後にアプリを終了させ、直ちに起動したときのものだ。「progress:」の行の末尾に進捗のパーセンテージを表示しているのだが、それが途中から始まっているとお分かりいただけるだろうか?
なお、このような状況を確認するには、それなりに大きなファイル(数十MBytes〜数百MBytes程度)をダウンロードする必要がある。ダウンロードさせてもらうWebサイトに過剰な負荷を掛けないような配慮をお願いしたい。
本稿では、このUIの作り方やプログレスバーへの表示方法などは説明していない。別途公開のサンプルコードをご覧いただきたい。

ダウンロードを途中でキャンセルするには?

 ダウンロード操作オブジェクトにアタッチする際にキャンセルトークンを渡せばよい。そのコードを「CancelDownloadAsync」メソッドとしてまとめると、次のようになる。

// 現在進行中のダウンロードをキャンセルする(停止状態のダウンロードもキャンセルできる)
public static async Task CancelDownloadAsync()
{
  var download = await GetLastDownloadAsync();
  if (download == null)
    return;

  try
  {
    // キャンセルトークンを用意する
    System.Threading.CancellationTokenSource canceledToken
      = new System.Threading.CancellationTokenSource();
    canceledToken.Cancel();

    // ダウンロード操作オブジェクトにアタッチする際にキャンセルトークンを渡す
    await download.AttachAsync().AsTask(canceledToken.Token);
  }
  catch (TaskCanceledException)
  {
    // (正常=キャンセルに成功するとTaskCanceledExceptionが出る)
  }
}

' 現在進行中のダウンロードをキャンセルする(停止状態のダウンロードもキャンセルできる)
Public Shared Async Function CancelDownloadAsync() As Task
  Dim download = Await GetLastDownloadAsync()
  If (download Is Nothing) Then
    Return
  End If

  Try
    ' キャンセルトークンを用意する
    Dim canceledToken As System.Threading.CancellationTokenSource _
      = New System.Threading.CancellationTokenSource()
    canceledToken.Cancel()

    ' ダウンロード操作オブジェクトにアタッチする際にキャンセルトークンを渡す
    Await download.AttachAsync().AsTask(canceledToken.Token)
  Catch ex As TaskCanceledException
    ' (正常=キャンセルに成功するとTaskCanceledExceptionが出る)
  End Try
End Function

CancelDownloadAsyncメソッド(上:C#/下:VB)
別途公開のサンプルでは、[CANCEL]ボタンのクリック時にこのメソッドを呼び出している。

まとめ

 バックグラウンドでのダウンロードを開始するのは簡単だ。その後、ダウンロード状況や結果を取得するのが少々面倒なのである。このバックグラウンド転送機能については、次のドキュメントも参照してほしい。

「WinRT/Metro TIPS」のインデックス

WinRT/Metro TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。