2017年10月27日 星期五

C# HttpClient WebAPI : 14. 呼叫 Web API 的逾時時間限制用法

當我們在進行電腦資源存取的時候,例如:檔案、資料庫、網路、記憶體、系統資源等等,需要特別小心操作,最保險的方式就是將這些存取資源的敘述,使用 try 將這些敘述包含在內,這樣,當有任何例外異常發生的時候,就可以捕捉到這些例外異常問題。
可是,有個問題是比較棘手的,那就是發生逾期的問題,在我們進行 HttpClient 的相關請求操作的時候,也許是因為網路品質、斷線、伺服器的效能或者後端程式碼撰寫品質不佳影響,會造成用戶端長時間無法得到結果。
若我們使用 HttpClient 的時候,若沒有特別指定 HttpClient.Timeout 屬性值, 他的預設逾期時間.aspx)為 100 秒,這樣的時間,對於一般的程式應用似乎有點過長,因此,在這篇文章中,我們將來學習 HttpClient 逾期的程式碼設計方式。

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


呼叫 Web API 的逾時時間限制用法

在我們所設計的逾期 Web API,只要您呼叫這個 Web API,他將會延遲5秒鐘之後,才會將結果回傳到用戶端;因此,在 JsonPutAsync 方法中,我們將會接受到一個秒數參數,讓我們可以設定 HttpClient 的逾期時間。我們使用這樣的敘述 client.Timeout = TimeSpan.FromSeconds(sec); 來設定這個 HttpClient 各種操作的最大逾期時間。
若進行 HttpClient 的網路存取操作的時候,若得到回傳結果的時間,大於這裡設定的逾期時間,這個時候 HttpClient 物件將會丟出例外異常,底下是這時間逾期的呼叫堆疊範例。
   於 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   於 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   於 System.Net.Http.HttpClient.<FinishSendAsyncBuffered>d__58.MoveNext()
--- 先前擲回例外狀況之位置中的堆疊追蹤結尾 ---
   於 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   於 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   於 System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   於 LongTimeAccess.Program.<JsonPutAsync>d__1.MoveNext() 於 D:\Vulcan\GitHub\CSharpNotes\WebAPI\LongTimeAccess\Program.cs: 行 77
而這個時間逾期的例外異常,其 Message 屬性將會得到 工作已取消。 的字串內容。
若您有參考這份 HttpClient 使用文章中的寫法,您將會發現到,我們有將 HttpClient 的所有存取動作,都使用 Try 包裹起來,也就是說,我們可以捕捉到任何 HttpClient 所發出的例外異常。
private static async Task<APIResult> JsonPutAsync(int sec = 4)
{
    APIResult fooAPIResult;
    using (HttpClientHandler handler = new HttpClientHandler())
    {
        using (HttpClient client = new HttpClient(handler))
        {
            try
            {
                client.Timeout = TimeSpan.FromSeconds(sec);
                #region 呼叫遠端 Web API
                string FooUrl = $"http://vulcanwebapi.azurewebsites.net/api/values";
                HttpResponseMessage response = null;

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

                // Accept 用於宣告客戶端要求服務端回應的文件型態 (底下兩種方法皆可任選其一來使用)
                //client.DefaultRequestHeaders.Accept.TryParseAdd("application/json");
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

                // Content-Type 用於宣告遞送給對方的文件型態
                //client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");

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

                #region 處理呼叫完成 Web API 之後的回報結果
                if (response != null)
                {
                    if (response.IsSuccessStatusCode == true)
                    {
                        // 取得呼叫完成 API 後的回報內容
                        String strResult = await response.Content.ReadAsStringAsync();
                        fooAPIResult = JsonConvert.DeserializeObject<APIResult>(strResult, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
                    }
                    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/api/values/LongTimeGet ,此時,將會觸發 Web API 伺服器上的 Values 控制器(Controller)的 public async Task<APIResult> LongTimeGet()動作(Action),其該動作的原始碼如下所示。
這個 Web API 動作,將會回傳一個 APIData 的 JSON 資料。
[HttpGet("LongTimeGet")]
public async Task<APIResult> LongTimeGet()
{
    APIResult foo;

    await Task.Delay(5000);
    foo = new APIResult()
    {
        Success = true,
        Message = "透過 Get 方法",
        Payload = new APIData()
        {
            Id = 777,
            Name = "Vulcan01"
        }
    };
    return foo;
}

進行測試

在程式進入點函式,我們先設定要使用 4 秒的逾期時間設定(伺服器端所指定的 Web API 動作,需要花費至少 5 秒的時間,才會回傳到用戶端上),因此,我們將會得到這樣失敗的結果;接著,我們將用戶端的逾期時間設定為 6 秒鐘,結果是可以順利得到遠端 Web API 的回傳結果。。
static void Main(string[] args)
{
    Console.WriteLine("遠端 Web API 需要花費 5 秒鐘,才會回傳結果內容");
    Console.WriteLine($"使用 Get 方法呼叫 Web API ,並且會逾期的結果 ( HttpClient 限時 4 秒內要完成 )");
    var foo = JsonPutAsync(4).Result;
    Console.WriteLine($"結果狀態 : {foo.Success}");
    Console.WriteLine($"結果訊息 : {foo.Message}");
    if (foo.Success == true)
    {
        var item = JsonConvert.DeserializeObject<APIData>(foo.Payload.ToString());
        Console.WriteLine($"Id : {item.Id}");
        Console.WriteLine($"Name : {item.Name}");
        Console.WriteLine($"Filename : {item.Filename}");
    }
    Console.WriteLine($"");

    Console.WriteLine($"Press any key to Continue...{Environment.NewLine}");
    Console.ReadKey();

    Console.WriteLine($"使用 Get 方法呼叫 Web API ,並且不會逾期的結果 ( HttpClient 限時 6 秒內要完成 )");
    foo = JsonPutAsync(6).Result;
    Console.WriteLine($"結果狀態 : {foo.Success}");
    Console.WriteLine($"結果訊息 : {foo.Message}");
    if (foo.Success == true)
    {
        var item = JsonConvert.DeserializeObject<APIData>(foo.Payload.ToString());
        Console.WriteLine($"Id : {item.Id}");
        Console.WriteLine($"Name : {item.Name}");
        Console.WriteLine($"Filename : {item.Filename}");
    }
    Console.WriteLine($"");

    Console.WriteLine($"Press any key to Exist...{Environment.NewLine}");
    Console.ReadKey();

}

執行結果

這個測試將會輸出底下內容
遠端 Web API 需要花費 5 秒鐘,才會回傳結果內容
使用 Get 方法呼叫 Web API ,並且會逾期的結果 ( HttpClient 限時 4 秒內要完成 )
結果狀態 : False
結果訊息 : 工作已取消。

Press any key to Continue...

使用 Get 方法呼叫 Web API ,並且不會逾期的結果 ( HttpClient 限時 6 秒內要完成 )
結果狀態 : True
結果訊息 : 透過 Get 方法
Id : 777
Name : Vulcan01
Filename :

Press any key to Exist...

HTTP 傳送與接收原始封包

讓我們來看看,這個 Web API 的呼叫動作中,在請求 (Request) 與 反應 (Response) 這兩個階段,會在網路上傳送了那些 HTTP 資料
  • 請求 (Request)
    首先,我們發出一個 GET 請求,底下是一般的 Http GET 請求的封包內容。
GET http://vulcanwebapi.azurewebsites.net/api/values/LongTimeGet HTTP/1.1
Accept: application/json
Host: vulcanwebapi.azurewebsites.net
Connection: Keep-Alive
  • 反應 (Response)
    底下是使用 Fiddler 側錄下 Http 封包的內容,雖然 C# 用戶端因為逾期時間為 4 秒,沒有在 5 秒後收到回傳結果,而在 C# 用戶端產生了例外異常,不過,伺服器還是會在 5 秒後將結果回傳回來(此時,C# 用戶端程式已經終止執行了),不過, Fiddler 還是有側錄下來。
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8
Server: Kestrel
X-Powered-By: ASP.NET
Set-Cookie: ARRAffinity=9d3635139ab6649f453417d1e9047b7ed7a79b7bef031b04afeb6a2c58b33d4e;Path=/;HttpOnly;Domain=vulcanwebapi.azurewebsites.net
Date: Mon, 23 Oct 2017 02:16:31 GMT

65
{"success":true,"message":"透過 Get 方法","payload":{"id":777,"name":"Vulcan01","filename":null}}
0
  • 請求 (Request)
    在這裡的第一行中,您將會看到了完整有查詢字串 (Query String) 的 URL,在問號之後的內容,將會傳送到後端 Web API 動作函式的參數內,此時的 Id值為 777。
GET http://vulcanwebapi.azurewebsites.net/api/values/LongTimeGet HTTP/1.1
Accept: application/json
Host: vulcanwebapi.azurewebsites.net
Connection: Keep-Alive
  • 反應 (Response)
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8
Server: Kestrel
X-Powered-By: ASP.NET
Set-Cookie: ARRAffinity=9d3635139ab6649f453417d1e9047b7ed7a79b7bef031b04afeb6a2c58b33d4e;Path=/;HttpOnly;Domain=vulcanwebapi.azurewebsites.net
Date: Mon, 23 Oct 2017 02:16:40 GMT

65
{"success":true,"message":"透過 Get 方法","payload":{"id":777,"name":"Vulcan01","filename":null}}
0

相關文章索引

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 課程


2017年10月26日 星期四

C# HttpClient WebAPI : 13. 上傳本機文字檔案與相關欄位值到遠端伺服器上

當我們要進行圖片等二進位類型檔案的上傳時候,通常也想要將一些該應用程式的資料或者狀態,上傳到後端 Web API上,若您有關看到這一系列的前面文章,您已經想到了,這可以使用查詢字串、Header、Cookie來做到。
不過,在這裡,我們將會展示另外一種做法,因為我們在用戶端使用了 multipart/form-data 資料編碼技術,所以,我們可以把我們要上傳的各種欄位,物件、狀態等資料,也透過 MultipartFormDataContent 類別物件,一並傳送過伺服器那哩,在上一篇文章中,我們使用到了 StreamContent 的物件,在更早之前的文章,我們有展示過如何使用 StringContent 的物件 (還有一種式 ByteArrayContent 類別,各位可以自行試用看看),我們將會在這篇文章中一起使用。

上傳本機文字檔案與相關欄位值到遠端伺服器上

為了簡化練習與說明,在這裡我們上傳的檔案類行為文字檔案。
當要從用戶端上傳檔案到後端 Web API 伺服器上,我們需要使用 multipart/form-data 編碼方式,所以,我們使用 var content = new MultipartFormDataContent() 敘述,建立一個 MultipartFormDataContent 類別物件。
接者,我們使用 File.Open 方法,得到要上傳文字檔案的 FileStream 物件,然後,我們建立一個 StreamContent 的物件(需要把剛剛取得的 FileStream 物件設定為建構函式的引數),設定這個物件的 Content-Type 與 Content-Disposition 描述定義,這樣,就完成了上傳圖片檔案的準備工作。
var streamContent = new StreamContent(fs);
streamContent.Headers.Add("Content-Type", "text/plain");
streamContent.Headers.Add("Content-Disposition", "form-data; name=\"files\"; filename=\"" + fooSt + "\"");
content.Add(streamContent, "file", filename);
response = await client.PostAsync(fooFullUrl, content);
另外,我們也建立了許多 StringContent 物件,把要傳送的不同欄位名稱與欄位數值都設定到 StringContent 物件內,最後加入到 MultipartFormDataContent 物件內。
最後,使用 PostAsync 方法,把剛剛產生的 MultipartFormDataContent 物件,設定為該方法的引數即可。
public static async Task<APIResult> UploadImageAsync(string filename, LoginInformation loginInformation)
{
    APIResult fooAPIResult;
    using (HttpClientHandler handler = new HttpClientHandler())
    {
        using (HttpClient client = new HttpClient(handler))
        {
            try
            {
                #region 呼叫遠端 Web API
                //string FooUrl = $"http://localhost:53495/api/Upload/FileAndData";
                string FooUrl = $"http://vulcanwebapi.azurewebsites.net/api/Upload/FileAndData";
                HttpResponseMessage response = null;


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

                // Accept 用於宣告客戶端要求服務端回應的文件型態 (底下兩種方法皆可任選其一來使用)
                //client.DefaultRequestHeaders.Accept.TryParseAdd("application/json");
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

                // Content-Type 用於宣告遞送給對方的文件型態
                //client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");

                #region 將剛剛拍照的檔案,上傳到網路伺服器上(使用 Multipart 的規範)
                // 規格說明請參考 https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
                using (var content = new MultipartFormDataContent())
                {

                    Dictionary<string, string> formDataDictionary = new Dictionary<string, string>()
                    {
                        { nameof(loginInformation.Account), loginInformation.Account },
                        { nameof(loginInformation.Password), loginInformation.Password },
                        { nameof(loginInformation.VerifyCode), loginInformation.VerifyCode }
                    };

                    foreach (var keyValuePair in formDataDictionary)
                    {
                        content.Add(new StringContent(keyValuePair.Value), keyValuePair.Key);
                    }

                    var rootPath = Directory.GetCurrentDirectory();
                    // 取得這個圖片檔案的完整路徑
                    var path = Path.Combine(rootPath, filename);

                    // 開啟這個圖片檔案,並且讀取其內容
                    using (var fs = File.Open(path, FileMode.Open))
                    {
                        var fooSt = $"My{filename}";
                        var streamContent = new StreamContent(fs);
                        streamContent.Headers.Add("Content-Type", "text/plain");
                        streamContent.Headers.Add("Content-Disposition", "form-data; name=\"files\"; filename=\"" + fooSt + "\"");
                        content.Add(streamContent, "file", filename);
                        // 上傳到遠端伺服器上
                        response = await client.PostAsync(fooFullUrl, content);
                    }
                }
                #endregion
                #endregion
                #endregion

                #region 處理呼叫完成 Web API 之後的回報結果
                if (response != null)
                {
                    if (response.IsSuccessStatusCode == true)
                    {
                        // 取得呼叫完成 API 後的回報內容
                        String strResult = await response.Content.ReadAsStringAsync();
                        fooAPIResult = JsonConvert.DeserializeObject<APIResult>(strResult, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
                    }
                    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/api/Upload/FileAndData ,此時,將會觸發 Web API 伺服器上的 Upload 控制器(Controller)的 public async Task<APIResult> FileAndData(List<IFormFile> files, LoginInformation loginInformation) 動作(Action),其該動作的原始碼如下所示。
這個 Web API 動作,將會回傳一個 APIData 的 JSON 資料。
[HttpPost("FileAndData")]
public async Task<APIResult> FileAndData(List<IFormFile> files, LoginInformation loginInformation)
{
    // https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads

    string webDatasRoot = Path.Combine(_HostingEnvironment.WebRootPath, "Datas");

    long size = files.Sum(f => f.Length);

    // full path to file in temp location
    if (files.Count > 0)
    {
        foreach (var formFile in files)
        {
            if (formFile.Length > 0)
            {
                using (var memoryStream = new MemoryStream())
                {
                    await formFile.CopyToAsync(memoryStream);
                    memoryStream.Seek(0, SeekOrigin.Begin);
                    var streamReader = new StreamReader(memoryStream);
                    var fooContent = streamReader.ReadToEnd();

                    fooAPIResult.Success = true;
                    fooAPIResult.Message = "檔案上傳成功";
                    fooAPIResult.Payload = new LoginInformation
                    {
                        Account = $">> {loginInformation.Account}",
                        Password = $">> {loginInformation.Account}",
                        VerifyCode = fooContent
                    };
                }
            }
        }
    }
    else
    {
        fooAPIResult.Success = false;
        fooAPIResult.Message = "沒有任何檔案上傳";
        fooAPIResult.Payload = null;
    }

    return fooAPIResult;
}

進行測試

在程式進入點函式,我們建立一個 LoginInformation 型別的物件,接著,設定該物件的相關屬性,這些屬性值,是我們要傳送到遠端伺服器端的資料,另外,也會指明要傳送到遠端伺服器上的文字檔案名稱。
static async Task Main(string[] args)
{
    var fooLoginInformation = new LoginInformation()
    {
        Account = "Vulcan",
        Password = "123",
        VerifyCode = "abc"
    };
    var foo = await UploadImageAsync("Readme.txt", fooLoginInformation);
    fooLoginInformation = JsonConvert.DeserializeObject<LoginInformation>(foo.Payload.ToString());
    Console.WriteLine($"使用 MultiPart/Form-Data 格式傳送文字檔案與資料、使用 Post 方法呼叫 Web API 的結果");
    Console.WriteLine($"結果狀態 : {foo.Success}");
    Console.WriteLine($"結果訊息 : {foo.Message}");
    Console.WriteLine($"Payload : {foo.Payload}");
    Console.WriteLine($"Account : {fooLoginInformation.Account}");
    Console.WriteLine($"Password : {fooLoginInformation.Password}");
    Console.WriteLine($"文字檔案內容 : {fooLoginInformation.VerifyCode}");
    Console.WriteLine($"Press any key to Exist...{Environment.NewLine}");
    Console.ReadKey();
}

執行結果

這個測試將會輸出底下內容
使用 MultiPart/Form-Data 格式傳送文字檔案與資料、使用 Post 方法呼叫 Web API 的結果
結果狀態 : True
結果訊息 : 檔案上傳成功
Payload : {
  "account": ">> Vulcan",
  "password": ">> Vulcan",
  "verifyCode": "這是一個文字類型檔案內容"
}
Account : >> Vulcan
Password : >> Vulcan
文字檔案內容 : 這是一個文字類型檔案內容
Press any key to Exist...

HTTP 傳送與接收原始封包

讓我們來看看,這個 Web API 的呼叫動作中,在請求 (Request) 與 反應 (Response) 這兩個階段,會在網路上傳送了那些 HTTP 資料
  • 請求 (Request)
    在這裡的第三行中,您將會看到 Http Header Content-Type 其設定值為 multipart/form-data; ,這表示了 這次的 POST 請求動作中,在 Http Body 封包內的資料,將會使用 multipart/form-data 編碼技術。
    從底下的 Http 封包中,我們可以看到了總共有四個欄位資料,使用了 multipart/form-data 編碼技術,而這四個欄位的定義內容,都會使用 Http Header 的 boundary 值,進行分割,這樣,後端伺服器才能夠清楚的分辨不同欄位的資料。
POST http://vulcanwebapi.azurewebsites.net/api/Upload/FileAndData HTTP/1.1
Accept: application/json
Content-Type: multipart/form-data; boundary="7e83396d-cea9-4d3f-9f03-02fb0dd30274"
Host: vulcanwebapi.azurewebsites.net
Content-Length: 631
Expect: 100-continue
Connection: Keep-Alive

--7e83396d-cea9-4d3f-9f03-02fb0dd30274
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=Account

Vulcan
--7e83396d-cea9-4d3f-9f03-02fb0dd30274
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=Password

123
--7e83396d-cea9-4d3f-9f03-02fb0dd30274
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=VerifyCode

abc
--7e83396d-cea9-4d3f-9f03-02fb0dd30274
Content-Type: text/plain
Content-Disposition: form-data; name="files"; filename="MyReadme.txt"

這是一個文字類型檔案內容
--7e83396d-cea9-4d3f-9f03-02fb0dd30274--
  • 反應 (Response)
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8
Server: Kestrel
X-Powered-By: ASP.NET
Set-Cookie: ARRAffinity=9d3635139ab6649f453417d1e9047b7ed7a79b7bef031b04afeb6a2c58b33d4e;Path=/;HttpOnly;Domain=vulcanwebapi.azurewebsites.net
Date: Mon, 23 Oct 2017 02:15:14 GMT

9c
{"success":true,"message":"檔案上傳成功","payload":{"account":">> Vulcan","password":">> Vulcan","verifyCode":"這是一個文字類型檔案內容"}}
0

相關文章索引

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 課程