2017年10月28日 星期六

C# HttpClient WebAPI : 15. 呼叫 Web API 的處理進度事件回報與強制取消用法

通常,若我們需要從 Web API 取得大量的資料到用戶端的時候,或者因為網路品質不好,導致資料回傳時間過長,我們希望提供當時取得網路資源的進度資訊給使用者知道,在這個練習中,我們將會嘗試從網路上下載一個圖片檔案,並且顯示這個圖片的完整取得進度百分比。
另外,我們也提供了一項功能,那就是當使用者在取得資料的過程中,使用者可以決定是否要終止此次的網路存取行為,為了要能夠讓使用者可以操作這樣效果,所以,我們在用戶端這裡,會適時地暫停一小段時間,讓使用者可以決定是否有取消這次存取行為。

了解更多關於 [HttpClient Class] 的使用方式
了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式


呼叫 Web API 的處理進度事件回報與強制取消用法

這個 ReportProgress 方法,將是進行 HttpClient 讀取資料的時候,要回報進度的委派方法,您也可以視為他是的 Call Back 方法。
在 DownloadImageAsync 方法中,我們需要三個參數,第一個是下載圖片的檔案名稱,第二個是要回報進度的 IProgress,第三個則是用來取消 HttpClient 動作的 CancellationToken 類別物件。
在我們進行取得圖片的資源的時候,使用這個 await client.GetAsync(fooFullUrl, HttpCompletionOption.ResponseHeadersRead); 敘述,其中, HttpCompletionOption.ResponseHeadersRead列舉值,指示 HttpClient 應該儘快的回傳任何回應內容,而不是要把所有的內容都放置到緩衝區內,才結束這個方法執行。透過這個列舉值的使用,我們可以提早得到這個 Http 回應的 Header,並且可以知道檔案的全部大小。
在我們執行相關 Http 請求動作並且得到 HttpResponseMessage 物件之後,接著判斷此次的請求動作的狀態碼是成功的,我們就可以透過 var total = response.Content.Headers.ContentLength.HasValue ? response.Content.Headers.ContentLength.Value : -1L; 取得這個圖片檔案的全部檔案大小數值。
在進行將 Http 回傳的圖片內容,將其寫入到本機檔案中,我們使用了 await response.Content.ReadAsStreamAsync() 表示式,每次讀取一定大小的資料(我們這裡,每次讀取 4096 Bytes),接著寫入到檔案,然後反覆這樣的動作,直到所有的內容都成功寫入到檔案內。當每個小區塊資料寫入到檔案之後,我們就會執行傳入參數的IProgress物件,在這裡使用 progress.Report((totalRead * 1d) / (total * 1d) * 100); 表示式來執行,這樣,我們在呼叫這個方法的地方,就可以反映出現在下載完成百分比進度。
另外,在這個反覆讀取網路資源小區塊資料,且入到檔案的反覆過程中,我們看到了一個表示式 token.ThrowIfCancellationRequested();,當這個 token 物件所歸屬的 CancellationTokenSource 類別物件,執行了 cts.Cancel(); 方法,那麼,當執行 token.ThrowIfCancellationRequested(); 的時候,就會產生例外異常,當然,也就取消了這次的 Web API 呼叫。
因為我們下載的圖片檔案沒有很大,因此,每讀、寫一次 Stream 區塊,我們就執行 await Task.Delay(200); 敘述,暫停個 0.2 秒鐘。
private static void ReportProgress(double obj)
{
    Console.WriteLine($"下載完成進度 {obj}");
}

private static async Task<APIResult> DownloadImageAsync(string filename,
    IProgress<double> progress, CancellationToken token)
{
    string ImgFilePath = $"My_{filename}";
    ImgFilePath = Path.Combine(Environment.CurrentDirectory, ImgFilePath);
    APIResult fooAPIResult;
    using (HttpClientHandler handler = new HttpClientHandler())
    {
        using (HttpClient client = new HttpClient(handler))
        {
            try
            {
                #region 呼叫遠端 Web API
                string FooUrl = $"http://vulcanwebapi.azurewebsites.net/Datas/";
                HttpResponseMessage response = null;

                #region  設定相關網址內容
                var fooFullUrl = $"{FooUrl}{filename}";

                response = await client.GetAsync(fooFullUrl, 
                    HttpCompletionOption.ResponseHeadersRead);
                #endregion
                #endregion

                #region 處理呼叫完成 Web API 之後的回報結果
                if (response != null)
                {
                    if (response.IsSuccessStatusCode == true)
                    {
                        #region 狀態碼為 OK
                        var total = response.Content.Headers.ContentLength.HasValue ? response.Content.Headers.ContentLength.Value : -1L;
                        var canReportProgress = total != -1 && progress != null;
                        using (var filestream = File.Open(ImgFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite))
                        {
                            using (var stream = await response.Content.ReadAsStreamAsync())
                            {
                                var totalRead = 0L;
                                var buffer = new byte[4096];
                                var isMoreToRead = true;

                                do
                                {
                                    token.ThrowIfCancellationRequested();

                                    var read = await stream.ReadAsync(buffer, 0, buffer.Length);

                                    if (read == 0)
                                    {
                                        isMoreToRead = false;
                                    }
                                    else
                                    {
                                        await filestream.WriteAsync(buffer, 0, read);

                                        totalRead += read;

                                        if (canReportProgress)
                                        {
                                            progress.Report((totalRead * 1d) / (total * 1d) * 100);
                                        }
                                    }
                                    // 故意暫停,讓使用者可以取消下載
                                    await Task.Delay(200);
                                } while (isMoreToRead);
                            }
                        }
                        fooAPIResult = new APIResult
                        {
                            Success = true,
                            Message = string.Format("Error Code:{0}, Error Message:{1}", response.StatusCode, response.Content),
                            Payload = ImgFilePath,
                        };
                        #endregion
                    }
                    else
                    {
                        fooAPIResult = new APIResult
                        {
                            Success = false,
                            Message = string.Format("Error Code:{0}, Error Message:{1}", response.StatusCode, response.RequestMessage),
                            Payload = null,
                        };
                    }
                }
                else
                {
                    fooAPIResult = new APIResult
                    {
                        Success = false,
                        Message = "應用程式呼叫 API 發生異常",
                        Payload = null,
                    };
                }
                #endregion
            }
            catch (Exception ex)
            {
                fooAPIResult = new APIResult
                {
                    Success = false,
                    Message = ex.Message,
                    Payload = ex,
                };
            }
        }
    }

    return fooAPIResult;
}

觸發的 Web API 動作

這個範例中,將會指向 URL http://vulcanwebapi.azurewebsites.net/Datas/vulcan.png,此時,將會向 Web API 伺服器要求回傳該圖片回到用戶端上。

進行測試

在程式進入點函式,我們建立一個 CancellationTokenSource 型別的物件,接著,取得 CancellationTokenSource.Token,這個是屬於 CancellationToken 的物件,我們需要將這個 CancellationToken 物件傳送到下載圖片的 DownloadImageAsync 方法內。
此時,我們另外啟動一個新的執行緒,這個執行緒並不做任何事情,只是等候使用者按下任一按鍵,若使用者按下了 C 這個按鍵,則 cts.Cancel(); 將會執行,而當時的 HttpClient 抓取圖片動作,將會取消。
static async Task Main(string[] args)
{
    CancellationTokenSource cts = new CancellationTokenSource();
    CancellationToken token = cts.Token;

    Thread t1 = new Thread(() =>
    {
        if (Console.ReadKey(true).KeyChar.ToString().ToUpperInvariant() == "C")
            cts.Cancel();
    });

    t1.Start();

    var progressIndicator = new Progress<double>(ReportProgress);

    var fooResult = await DownloadImageAsync("vulcan.png", progressIndicator, token);
    if (fooResult.Success == true)
    {
        Process myProcess = new Process();
        try
        {
            // true is the default, but it is important not to set it to false
            myProcess.StartInfo.UseShellExecute = true;
            myProcess.StartInfo.FileName = fooResult.Payload.ToString();
            myProcess.Start();
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }
        Console.WriteLine($"Press any key to Exist...{Environment.NewLine}");
        Console.ReadKey();
    }
    else
    {
        Console.WriteLine($"使用者中斷下載作業 {fooResult.Message} {Environment.NewLine}"); 
        Console.WriteLine($"Press any key to Exist...{Environment.NewLine}");
        Console.ReadKey();
    }
}

執行結果

這個測試將會輸出底下內容
下載完成進度 5.10764588152406
下載完成進度 7.6614688222861
下載完成進度 10.2152917630481
下載完成進度 12.7691147038102
下載完成進度 15.3229376445722
下載完成進度 17.8767605853342
下載完成進度 20.4305835260963
下載完成進度 22.9844064668583
下載完成進度 25.5382294076203
下載完成進度 28.0920523483824
下載完成進度 30.6458752891444
下載完成進度 33.1996982299064
下載完成進度 35.7535211706684
下載完成進度 38.3073441114305
下載完成進度 40.8611670521925
下載完成進度 43.4149899929545
下載完成進度 45.9688129337166
下載完成進度 48.5226358744786
下載完成進度 51.0764588152406
下載完成進度 53.6302817560027
下載完成進度 56.1841046967647
下載完成進度 58.7379276375267
下載完成進度 61.2917505782888
下載完成進度 63.8455735190508
下載完成進度 66.3993964598128
下載完成進度 68.9532194005749
下載完成進度 71.5070423413369
下載完成進度 74.0608652820989
下載完成進度 76.614688222861
下載完成進度 79.168511163623
下載完成進度 81.722334104385
下載完成進度 84.276157045147
下載完成進度 86.8299799859091
下載完成進度 89.3838029266711
下載完成進度 91.9376258674332
下載完成進度 94.4914488081952
下載完成進度 97.0452717489572
下載完成進度 99.5990946897192
下載完成進度 100
Press any key to Exist...

HTTP 傳送與接收原始封包

讓我們來看看,這個 Web API 的呼叫動作中,在請求 (Request) 與 反應 (Response) 這兩個階段,會在網路上傳送了那些 HTTP 資料
  • 請求 (Request)
GET http://vulcanwebapi.azurewebsites.net/Datas/vulcan.png HTTP/1.1
Host: vulcanwebapi.azurewebsites.net
Connection: Keep-Alive
  • 反應 (Response)
HTTP/1.1 200 OK
Content-Length: 160387
Content-Type: image/png
Last-Modified: Sun, 08 Oct 2017 16:09:22 GMT
Accept-Ranges: bytes
ETag: "1d3404fcd99d783"
Server: Kestrel
X-Powered-By: ASP.NET
Set-Cookie: ARRAffinity=9d3635139ab6649f453417d1e9047b7ed7a79b7bef031b04afeb6a2c58b33d4e;Path=/;HttpOnly;Domain=vulcanwebapi.azurewebsites.net
Date: Mon, 23 Oct 2017 02:22:35 GMT

 PNG
 

IHDR              u         pHYs          +      tIME         8ǫe     tEXtAuthor    H    tEXtDescription      !#   
tEXtCopyr

*** FIDDLER: RawDisplay truncated at 128 characters. Right-click to disable truncation. ***

相關文章索引

C# HttpClient WebAPI 系列文章索引

了解更多關於 [HttpClient Class] 的使用方式
了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式



關於 Xamarin 在台灣的學習技術資源

Xamarin 實驗室 粉絲團
歡迎加入 Xamarin 實驗室 粉絲團,在這裡,將會經常性的貼出各種關於 Xamarin / Visual Studio / .NET 的相關消息、文章、技術開發等文件,讓您可以隨時掌握第一手的 Xamarin 方面消息。
Xamarin.Forms @ Taiwan
歡迎加入 Xamarin.Forms @ Taiwan,這是台灣的 Xamarin User Group,若您有任何關於 Xamarin / Visual Studio / .NET 上的問題,都可以在這裡來與各方高手來進行討論、交流。
Xamarin 實驗室 部落格
Xamarin 實驗室 部落格 是作者本身的部落格,這個部落格將會專注於 Xamarin 之跨平台 (Android / iOS / UWP) 方面的各類開技術探討、研究與分享的文章,最重要的是,它是全繁體中文。
Xamarin.Forms 系列課程
Xamarin.Forms 系列課程 想要快速進入到 Xamarin.Forms 的開發領域,學會各種 Xamarin.Forms 跨平台開發技術,例如:MVVM、Prism、Data Binding、各種 頁面 Page / 版面配置 Layout / 控制項 Control 的用法等等,千萬不要錯過這些 Xamarin.Forms 課程


沒有留言:

張貼留言