shuhelohelo’s blog

Xamarin.Forms多めです.

Xamarin.FormsでASP.NET CoreなDI(Dependency Injection) (5) HttpClient

Asp.Net CoreでおなじみのAddHttpClientメソッドを使ってHttpClientを安全に正しく利用することができます.

HttpClientの使用上の注意と使い方については以下の公式のガイドに詳しく書いてあります.

docs.microsoft.com

ここで説明されているとおり,HttpClientはIHttpClientFactoryを介して使用することが推奨されています.

Microsoft.Extensions.Httpをインストール

NugetでMicrosoft.Extensions.Httpをインストールします.

f:id:shuhelohelo:20200609113559p:plain

クラスを登録する

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();
        }

コンストラクタ内にブレークポイントを設定して実行してみます.

f:id:shuhelohelo:20200609184215p:plain

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については以下の公式の説明が詳しいです.

docs.microsoft.com

HttpClientを使ってWebAPIからデータを取得する

せっかくHttpClientを取得できたので,WebAPIからデータを取得したいですね.

こちらに公開されているWebAPIがまとめられているので,好きなものを選びます.

matome.naver.jp

ここではCOVID-19 Japan Web APIを選びました.

documenter.getpostman.com

以下をブラウザのURL欄に入力すると,

https://covid19-japan-web-api.now.sh/api//v1/prefectures

このように県別の情報がJSON形式で取得できます.

f:id:shuhelohelo:20200609191032p:plain

ASP.NET CoreでのWebAPIの利用方法をそのまま参考にします.

以下の記事の「Web APIのりようについて」という項で説明しているので,それを参考にします.

shuhelohelo.hatenablog.com

まず,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内にブレークポイントを設置して実行してみましょう.

f:id:shuhelohelo:20200609194345p:plain

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の機能を使ってクラスを生成すると手間が省けて良いと思います.

qiita.com

www.c-sharpcorner.com

次に取得した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>

実行すると,以下のように取得したデータがリスト表示されます.

f:id:shuhelohelo:20200609210647p:plain

ここまでで,HttpClientを使ったデータの取得と,JSONシリアライズ,Viewへの表示ができました.

追記

WebAPIからJSON形式のレスポンスを得る場合,前述のようにJSON文字列を取得してからそれをSystem.Text.Jsonでデシリアライズするのではなく,System.Net.Http.Jsonを使ってデシリアライズしたほうが良かった.

shuhelohelo.hatenablog.com

System.Net.Http.Jsonをインストールする

Nugetからインストールします.

f:id:shuhelohelo:20200615130418p:plain

シリアライズされたデータを取得

例えば,以下のようにシンプルにできる.

                try
                {
                    prefecturesData = await this._httpClient.GetFromJsonAsync<List<Prefecture>>("prefectures");
                }
                catch (Exception ex)
                {

                }

おわりに

Asp.Net Coreの作法がそのまま使えるのはとても面白かったし,Asp.NetエンジニアがXamarin.Formsアプリ開発するときにやりやすいのでは,と思いました.

色々準備するのが手間なので,まるっとテンプレートを用意できたら便利かもしれない.

プロジェクト作ったらすぐにAddTransientしていけばいいというの.

ソースコード

github.com