從 Xamarin.Forms 存取 RESTful API

昨天我們學會了使用 ListView 元件在頁面導覽切換到新頁面之後,讀取資料顯示在頁面上,當時為了方面解說(想控制在 30 分鐘內講完) ,讀取資料的方法是直接寫死在程式碼當中,這不符合在真實的商業應用程式的使用情境(多人使用的系統,資料是有可能隨時變動的,有必要隨時從伺服器端讀取資料)。

因此,今天就來學習如何讀取先前發行到 Azure App Service 的那一支 ASP.NET Core Web API 程式。

安裝 Nuget 套件

要讓 Xamarin.Forms 能夠存取 RESTful API 必需安裝 Microsoft.AspNet.WebApi.Client 套件,請用 Nuget 封裝管理員安裝相關的套件:

相關的套件也會隨 Microsoft.AspNet.WebApi.Client 一起被安裝進來:

自訂例外處理(Exception)

 還記得先前曾經說明過的,我們目前所學的商業應用程式是分為《前端》與《後端》兩個程式,《前端》透過 HTTP 的 GET、POST、PUT、DELETE 的方法存取《後端》所提供的 REST API 執行 CRUD 等動作。雖然我們期望程式應該會照著我們所想的進行,但是事與願違,不是所有狀況都是應用程式開發人員所能掌握的,比較實際一點,在山區訊號不良,或是 ISP 所提供的 DNS 伺服器掛了,這些都有可能造成應用程式無法執行,就算是連線成功,送出請求,而接手的 REST API 程式內部怎麼運行也不是《前端》應用程式設計人員所能控制的,也只能由 REST API 所回傳的 Status Code 來判斷程式是否有正常執行。

由上述理由,我們有必要再為原有的 Exception 再增加記錄是否連線成功,以及回傳 Status Code 的屬性。

請在 Demae.App 專案中新增一個 Exceptions 資料夾,並在資料夾內新增一個命名為 DemaeApiException.cs 類別(名稱可自訂,重點是要能由名稱中分辨是什麼功能用的)。

接著繼承 Exception 並追加兩個用來記錄 HttpStatusCode 和是否連線成功的屬性,並改寫原 Exception 的建構函式:

using System;
using System.Net;

namespace Demae.App.Exceptions
{
    public class DemaeApiException : Exception
    {
        public HttpStatusCode StatusCode { get; set; }
        public bool Connection { get; set; }

        public DemaeApiException(string message, HttpStatusCode statusCode)
            : base(message)
        {
            StatusCode = statusCode;
            Connection = true;
        }

        public DemaeApiException(string message, bool connection, Exception inner)
            : base(message, inner)
        {
            Connection = connection;
            StatusCode = HttpStatusCode.ServiceUnavailable;
        }

    }

    public class DemaeApiError
    {
        public string Message { get; set; }
    }
}

以下表格為原始 Exception 的建構函式的說明連結,請參考:

建構函式

  名稱 描述
System_CAPS_pubmethod Exception()

初始化 Exception 類別的新執行個體。

System_CAPS_protmethod Exception(SerializationInfo, StreamingContext)

使用序列化資料,初始化 Exception 類別的新執行個體。

System_CAPS_pubmethod Exception(String)

使用指定的錯誤訊息,初始化 Exception 類別的新執行個體。

System_CAPS_pubmethod Exception(String, Exception)

使用指定的錯誤訊息以及造成此例外狀況的內部例外狀況的參考,初始化 Exception類別的新執行個體。

實作服務(Service)

接著如下圖所示,在 Demae.App 專案底下新增一個 Services 資料夾,並在資料夾底下新增一個介面和兩個類別:

接著在命名為 BaseProvider.cs 內加入如下的程式碼:

using Demae.App.Exceptions;
using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace Demae.App.Services
{
    public class BaseProvider
    {
        protected string _baseUrl;

        protected HttpClient GetClient()
        {
            return GetClient(_baseUrl);
        }

        protected virtual HttpClient GetClient(string baseUrl)
        {
            HttpClient client = new HttpClient();
            client.BaseAddress = new Uri(baseUrl);

            return client;
        }     

        protected async Task<T> Get<T>(string url)
        {
            using (HttpClient client = GetClient())
            {
                try
                {
                    var response = await client.GetAsync(url);
                    if (!response.IsSuccessStatusCode)
                    {
                        var error = await response.Content.ReadAsAsync<DemaeApiError>();
                        var message = error != null ? error.Message : "";
                        throw new DemaeApiException(message, response.StatusCode);
                    }
                    return await response.Content.ReadAsAsync<T>();
                }
                catch (HttpRequestException ex)
                {
                    throw new DemaeApiException("", false, ex);
                }
                catch (UnsupportedMediaTypeException ex)
                {
                    throw new DemaeApiException("", false, ex);
                }
            }
        }
    }
}

誠如類別名稱 BaseProvider 的命名,這是一個基礎的實作,請回憶一下,在先前的《RESTful Web Service 初探》時所提到的『名詷』也就是網址,所以用 _baseUrl 記錄基礎網址(可以想像是發行網站空間的 DNS)而 Task Get(string url) 為泛用的方法。

介面

using Demae.App.Models;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Demae.App.Services
{
    public interface IAddressService
    {
        Task<List<AddressModel>> GetAddresses();
    }
}

實作 GET 服務

在服務的建構函式中設定服務的基礎網址,並實作取得(GET)方法接收的型別(本例例是 List<AddressModel>),和控制器方法的路由(本案例是 Address):

using Demae.App.Models;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Demae.App.Services
{
    public class AddressService : BaseProvider, IAddressService
    {
        public AddressService()
        {
            _baseUrl = "http://demaeapi.azurewebsites.net/api/";
        }

        public async Task<List<AddressModel>> GetAddresses()
        {
            return await Get<List<AddressModel>>("Addresses");
        }
    }
}

註冊服務

讀取(GET)的功能實作完後,接著必需先註冊才能使用,所以請在 Demae.App 的 App.xaml.cs 的 RegisterTypes() 方法中,註冊剛剛所實作的服務: 

protected override void RegisterTypes()
{
    .......
    .......
    .......
    Container.RegisterType<IAddressService, AddressService>();
}

讀取資料

最後將昨天寫死在程式中的讀取地址資料的方法改成今天所實作的方法:

private IAddressService _apiService;
public MyFirstPageViewModel(IAddressService apiService)
{
    _apiService = apiService;
}
......
......
......       

public async void OnNavigatedTo(NavigationParameters parameters)
{
    try
    {
        var result = await _apiService.GetAddresses();
        Addresses = new ObservableCollection<AddressModel>(result);
    }
    catch (System.Exception)
    {
        throw;
    }    
}        
註:
理論上,如果有例外發生應該要出現訊息通知使用者,或是寫進 Log 之類的處理,但是還是為了能控制在 30 分鐘內講完,該 catch 之後的處理就留待日後有機會再討論。

終於到了最後驗收成果了,執行後,果然跟使用 Postman 讀取 Azure Web App 的資料相同,證實行動裝置是由 Azure Web App 讀取資料。讀者也可以試著以 Postman 新增一筆資料(或修改一筆資料)看看是否會反應到行動裝置的應用程式的頁面上。

好吧!今天就先學習到這裡,明天就來學習,按下 ListView 中的某個項目,然後將該項目的資料帶到下一頁,以後在下一頁中以該帶來的資料顯示該地址的 Google 地圖好了。