Xamarin.FormsでASP.NET CoreなDI(Dependency Injection) (5) HttpClient
Asp.Net CoreでおなじみのAddHttpClient
メソッドを使ってHttpClientを安全に正しく利用することができます.
HttpClientの使用上の注意と使い方については以下の公式のガイドに詳しく書いてあります.
ここで説明されているとおり,HttpClientはIHttpClientFactoryを介して使用することが推奨されています.
Microsoft.Extensions.Httpをインストール
NugetでMicrosoft.Extensions.Http
をインストールします.
クラスを登録する
Startup.cs内のConfigureServicesメソッドで以下のようにします.
static void ConfigureServices(HostBuilderContext ctx, IServiceCollection services) { //開発と本番で登録するクラスを切り替える if (ctx.HostingEnvironment.IsDevelopment()) { services.AddSingleton<IDataService, MockDataService>(); } else { services.AddSingleton<IDataService, DataService>(); } services.AddHttpClient();//これ services.AddTransient<MainPageViewModel>(); services.AddTransient<MainPage>(); services.AddTransient<SecondPageViewModel>(); services.AddTransient<SecondPage>(); services.AddSingleton<App>(); }
これによってDIされるのはDefaultHttpClientFactory
のインスタンスです.
DI先のコンストラクタの引数にどちらを指定するか選びます.
DIしてみる
SecondPageViewModelにコンストラクタインジェクションしてみます.
private readonly HttpClient _httpClient; public SecondPageViewModel(IHttpClientFactory httpClientFactory) { this.Message = "This is Second page!"; this._httpClient = httpClientFactory.CreateClient(); }
コンストラクタ内にブレークポイントを設定して実行してみます.
DefaultHttpClientFactory
のインスタンスが渡され,そこからHttpClientをCreateできていることが確認できます.
ちなみに,HttpClientFactoryではなくHttpClientをダイレクトにDIする場合は,
Startup.csで,
services.AddSingleton<HttpClient>();
して,DI先のコンストラクタで,
public SecondPageViewModel(HttpClient httpClient) { this.Message = "This is Second page!"; this._httpClient = httpClient; }
とすることもできます.
AddHttpClientとIHttpClientFactoryについては以下の公式の説明が詳しいです.
HttpClientを使ってWebAPIからデータを取得する
せっかくHttpClientを取得できたので,WebAPIからデータを取得したいですね.
こちらに公開されているWebAPIがまとめられているので,好きなものを選びます.
ここではCOVID-19 Japan Web API
を選びました.
以下をブラウザのURL欄に入力すると,
https://covid19-japan-web-api.now.sh/api//v1/prefectures
このように県別の情報がJSON形式で取得できます.
ASP.NET CoreでのWebAPIの利用方法をそのまま参考にします.
以下の記事の「Web APIのりようについて」という項で説明しているので,それを参考にします.
まず,AddHttpClientメソッドで,BaseAddressの設定などをしておきます.
https://covid19-japan-web-api.now.sh/api//v1/
までがBaseAddressになるので,AddHttpClientメソッドを以下のように名前つきで記述します.
services.AddHttpClient("covid19_japan", c => { c.BaseAddress = new Uri("https://covid19-japan-web-api.now.sh/api//v1/"); });
ここではcovid19_japan
という名前をつけました.名前をつけておくことで,異なるWeb APIようにAddHttpClientで登録しておいて,CreateClientメソッドで目的のWebAPI用のHttpClientを取得することができ,とても便利です.
それではSecondPageViewModelで,先程のBaseAddressが設定されたHttpClientを取得します.
public SecondPageViewModel(IHttpClientFactory httpClientFactory) { this.Message = "This is Second page!"; this._httpClient = httpClientFactory.CreateClient("covid19_japan"); }
これで準備は完了です.
それでは,SecondPageにButtonを設置して,押されたときに上記のWeb APIからデータを取得するように以下のようにCommandを用意します.
SecondPage.xaml:
<?xml version="1.0" encoding="utf-8" ?> <ContentPage x:Class="XFUseAspNetCoreDI.Views.SecondPage" xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:d="http://xamarin.com/schemas/2014/forms/design" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <ContentPage.Content> <StackLayout> <Label FontSize="Large" Text="{Binding Message}" TextColor="Green" /> <Button Command="{Binding GetPrefecturesDataCommand}" Text="Get Prefectures Data" /> </StackLayout> </ContentPage.Content> </ContentPage>
SecondPageViewModel.cs:
public ICommand GetPrefecturesDataCommand { get; } public SecondPageViewModel(IHttpClientFactory httpClientFactory) { this.Message = "This is Second page!"; //this._httpClient = httpClientFactory.CreateClient(); this._httpClient = httpClientFactory.CreateClient("covid19_japan"); GetPrefecturesDataCommand = new Command(async (_) => { //リクエストを投げて,結果を取得する //BaseAddressを予め設定してあるので,BaseAddress以降をパラメータとして与えるだけでよい var response = await this._httpClient.GetAsync("prefectures"); //レスポンスからJSON文字列として取得 var prefecturesJsonString = await response.Content.ReadAsStringAsync(); }); }
このCommand内にブレークポイントを設置して実行してみましょう.
Text Visualizerで文字列を表示すると,以下のように都道府県ごとのデータが取得できていることが確認できます.
Viewに表示する
データが取得できたので, これをViewに表示します.
県ごとのデータを表すPrefecture
クラスを作成します.(System.Text.Jsonを使っているので,JsonPropertyName
属性です)
using System; using System.Collections.Generic; using System.Text; using System.Text.Json.Serialization; namespace XFUseAspNetCoreDI.Models.Covid19 { public class Prefecture { [JsonPropertyName("id")] public int Id { get; set; } [JsonPropertyName("name_ja")] public string Name_Ja { get; set; } [JsonPropertyName("name_en")] public string Name_En { get; set; } [JsonPropertyName("lat")] public float Lat { get; set; } [JsonPropertyName("lng")] public float Lng { get; set; } [JsonPropertyName("population")] public int Population { get; set; } [JsonPropertyName("last_updated")] public Last_Updated Last_Updated { get; set; } [JsonPropertyName("cases")] public int Cases { get; set; } [JsonPropertyName("deaths")] public int Deaths { get; set; } [JsonPropertyName("pcr")] public int Pcr { get; set; } } public class Last_Updated { [JsonPropertyName("cases_date")] public int Cases_Date { get; set; } [JsonPropertyName("deaths_date")] public int Deaths_Date { get; set; } [JsonPropertyName("pcr_date")] public int Pcr_Date { get; set; } } }
JSON文字列からクラスを生成すると,今回の件に限らず便利ですので,以下のようにかずきさんの拡張機能を使うか,Visual Studioの機能を使ってクラスを生成すると手間が省けて良いと思います.
次に取得したJSON文字列からオブジェクト生成します.これにはSystem.Text.Json.Deserialize
メソッドを使います.
//JSON文字列をデシリアライズしてList<Prefecture>型のデータに変換
var prefecturesData = JsonSerializer.Deserialize<List<Prefecture>>(prefecturesJsonString);
そしてこれをView側でバインディングする予定のプロパティに代入して,最終的にSecondPageViewModelは以下のようになります.
using MvvmHelpers.Commands; using System; using System.Collections.Generic; using System.Net.Http; using System.Runtime.CompilerServices; using System.Text; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.RegularExpressions; using System.Text.Unicode; using System.Windows.Input; using XFUseAspNetCoreDI.Models.Covid19; namespace XFUseAspNetCoreDI.ViewModels { public class SecondPageViewModel : BaseViewModel { private string _message; public string Message { get => _message; set => SetProperty(ref _message, value); } private readonly HttpClient _httpClient; //public SecondPageViewModel(HttpClient httpClient) //{ // this.Message = "This is Second page!"; // this._httpClient = httpClient; //} private List<Prefecture> _prefecturesData; public List<Prefecture> PrefecturesData { get => _prefecturesData; set => SetProperty(ref _prefecturesData, value); } public ICommand GetPrefecturesDataCommand { get; } public SecondPageViewModel(IHttpClientFactory httpClientFactory) { this.Message = "This is Second page!"; //this._httpClient = httpClientFactory.CreateClient(); this._httpClient = httpClientFactory.CreateClient("covid19_japan"); GetPrefecturesDataCommand = new Command(async (_) => { //リクエストを投げて,結果を取得する //BaseAddressを予め設定してあるので,BaseAddress以降をパラメータとして与えるだけでよい var response = await this._httpClient.GetAsync("prefectures"); //response.EnsureSuccessStatusCode(); //レスポンスからJSON文字列を取得 var prefecturesJsonString = await response.Content.ReadAsStringAsync(); //JSON文字列をデシリアライズしてList<Prefecture>型のデータに変換 var prefecturesData = JsonSerializer.Deserialize<List<Prefecture>>(prefecturesJsonString); //プロパティに入れる this.PrefecturesData = prefecturesData; }); } } }
最後にViewの方にCollectionViewを追加して,リスト形式で表示します.
<?xml version="1.0" encoding="utf-8" ?> <ContentPage x:Class="XFUseAspNetCoreDI.Views.SecondPage" xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:d="http://xamarin.com/schemas/2014/forms/design" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <ContentPage.Content> <StackLayout> <Label FontSize="Large" Text="{Binding Message}" TextColor="Green" /> <Button Command="{Binding GetPrefecturesDataCommand}" Text="Get Prefectures Data" /> <CollectionView ItemsSource="{Binding PrefecturesData}"> <CollectionView.ItemsLayout> <LinearItemsLayout ItemSpacing="10" Orientation="Vertical" /> </CollectionView.ItemsLayout> <CollectionView.ItemTemplate> <DataTemplate> <Grid> <Frame Margin="10,5"> <StackLayout> <Label BackgroundColor="LightPink" FontSize="Medium" Text="{Binding Name_Ja}" /> <Label Text="{Binding Population, StringFormat='人口 {0}人'}" /> <Label Text="{Binding Cases, StringFormat='感染者数 {0}人'}" /> <Label Text="{Binding Deaths, StringFormat='死亡者数 {0}人'}" /> <Label Text="{Binding Pcr, StringFormat='PCR検査? {0}人'}" /> </StackLayout> </Frame> </Grid> </DataTemplate> </CollectionView.ItemTemplate> </CollectionView> </StackLayout> </ContentPage.Content> </ContentPage>
実行すると,以下のように取得したデータがリスト表示されます.
ここまでで,HttpClientを使ったデータの取得と,JSONデシリアライズ,Viewへの表示ができました.
追記
WebAPIからJSON形式のレスポンスを得る場合,前述のようにJSON文字列を取得してからそれをSystem.Text.Json
でデシリアライズするのではなく,System.Net.Http.Json
を使ってデシリアライズしたほうが良かった.
System.Net.Http.Jsonをインストールする
Nugetからインストールします.
デシリアライズされたデータを取得
例えば,以下のようにシンプルにできる.
try { prefecturesData = await this._httpClient.GetFromJsonAsync<List<Prefecture>>("prefectures"); } catch (Exception ex) { }
おわりに
Asp.Net Coreの作法がそのまま使えるのはとても面白かったし,Asp.NetエンジニアがXamarin.Formsアプリ開発するときにやりやすいのでは,と思いました.
色々準備するのが手間なので,まるっとテンプレートを用意できたら便利かもしれない.
プロジェクト作ったらすぐにAddTransientしていけばいいというの.