shuhelohelo’s blog

Xamarin.Forms多めです.

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

montemagno.com

こちらの記事を試してみた2回目.

1回目はこちら. shuhelohelo.hatenablog.com

今回はプラットフォーム固有の機能をDIする場合についてです.

プラットフォーム固有の機能(例えば通知機能)を使う場合,共有プロジェクト側でインターフェースを用意し,それをプラットフォーム側で実装し,インターフェース経由でプラットフォーム固有の機能を呼び出す,という形をとります(?)

通知機能の場合だと,以下の記事(メモ程度の内容ですが)のようにXamarin.FormsのDependencyServiceを使ってオブジェクトの登録やインスタンスの取得などを行います.

shuhelohelo.hatenablog.com

これも自動でDIされるようにします.

まずはこの通知の仕組を実装します.省略します.

using System;
using System.Collections.Generic;
using System.Text;

namespace XFUseAspNetCoreDI.Services
{
    public interface INotificationService
    {
        event EventHandler NotificationReceived;

        void Initialize();

        int ScheduleNotification(string title, string message);
        void ReceiveNotification(string title, string message);
    }

    public class NotificationEventArg : EventArgs
    {
        public string Title { get; set; }
        public string Message { get; set; }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Android.App;
using Android.Content;
using Android.Graphics;
using Android.OS;
using Android.Runtime;
using Android.Support.V4.App;
using Android.Views;
using Android.Widget;
using XFUseAspNetCoreDI.Services;
using AndroidApp=Android.App.Application;

[assembly:Xamarin.Forms.Dependency(typeof(XFUseAspNetCoreDI.Droid.Services.AndroidNotificationService))]
namespace XFUseAspNetCoreDI.Droid.Services
{
    public class AndroidNotificationService : INotificationService
    {
        readonly string _channelId = "default";
        readonly string _channelName = "Default";
        readonly string _channelDescription = "The default channel for notifications.";
        readonly int _pendingIntentId = 0;

        public const string TitleKey = "title";
        public const string MessageKey = "message";

        bool _channelInitialized = false;
        int _messageId = -1;
        NotificationManager _manager;

        public event EventHandler NotificationReceived;

        public void Initialize()
        {
            CreateNotificationChannel();
        }

        public void ReceiveNotification(string title, string message)
        {
            var args = new NotificationEventArg()
            {
                Title = title,
                Message = message,
            };
            NotificationReceived?.Invoke(null, args);
        }

        public int ScheduleNotification(string title, string message)
        {
            if (!_channelInitialized)
            {
                CreateNotificationChannel();
            }

            _messageId++;

            Intent intent = new Intent(AndroidApp.Context, typeof(MainActivity));
            intent.PutExtra(TitleKey, title);
            intent.PutExtra(MessageKey, message);

            PendingIntent pendingIntent = PendingIntent.GetActivity(AndroidApp.Context, _pendingIntentId, intent, PendingIntentFlags.OneShot);

            NotificationCompat.Builder builder = new NotificationCompat.Builder(AndroidApp.Context, _channelId)
                .SetContentIntent(pendingIntent)
                .SetContentTitle(title)
                .SetContentText(message)
                .SetLargeIcon(BitmapFactory.DecodeResource(AndroidApp.Context.Resources, Resource.Mipmap.icon))
                .SetSmallIcon(Resource.Mipmap.icon)
                .SetAutoCancel(true)
                .SetPriority(NotificationCompat.PriorityHigh)//ヘッドアップ通知にする場合はHighにする.Android 7.1以下
                .SetDefaults((int)NotificationDefaults.Sound | (int)NotificationDefaults.Vibrate);

            var notification = builder.Build();
            _manager.Notify(_messageId, notification);

            return _messageId;
        }

        private void CreateNotificationChannel()
        {
            _manager = (NotificationManager)AndroidApp.Context.GetSystemService(AndroidApp.NotificationService);
            //Channelの作成はAndroid 8.0以上で必要
            if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
            {
                var channelNameJava = new Java.Lang.String(_channelName);
                var channel = new NotificationChannel(_channelId, channelNameJava, NotificationImportance.High)
                {
                    Description = _channelDescription
                };
                _manager.CreateNotificationChannel(channel);
            }

            _channelInitialized = true;
        }
    }
}

Viewにボタンを配置して,押したら通知が表示されるようにします.

        <Button Command="{Binding ShowNotificationCommand}" Text="Notify!" />
        public ICommand ShowNotificationCommand { get; }//追加

        public MainPageViewModel(IDataService dataService)
        {
            this.Message = "Hello, Dependency Injection!";
            this._dataService = dataService;

            GetPeopleCommand = new Command(_ =>
            {
                this.People = _dataService.FindAll();
            });

            //通知部分追加
            _notifiCationService = DependencyService.Get<INotificationService>();//DependencyServiceからインスタンスを取得する
            ShowNotificationCommand = new Command(_ => {
                _notifiCationService.ScheduleNotification("My Notification", "This is Test");//通知!
            });
        }

ボタンを押すと,このように通知が表示される.

f:id:shuhelohelo:20200608112932p:plain

ASP.NET CoreのDIの仕組みを使う

これをASP.NET CoreのDIの仕組みを使ってDIしたい.

プラットフォーム固有の機能は各プラットフォームのプロジェクト内で定義されるため,共有プロジェクトからは参照できない.ということはAddSingletonメソッドなどでの登録は各プラットフォーム側で記述する必要がある.

ネイティブ側からは共有プロジェクト側が参照できるので,ネイティブ側でのAddSingletonなどのメソッド実行をコールバックとしてStartup.Initに渡してやることで,ネイティブ機能のオブジェクト登録を共有プロジェクトStartup.Initで行うようにする.

    public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
    {
        protected override void OnCreate(Bundle savedInstanceState)
        {
            TabLayoutResource = Resource.Layout.Tabbar;
            ToolbarResource = Resource.Layout.Toolbar;

            base.OnCreate(savedInstanceState);

            Xamarin.Essentials.Platform.Init(this, savedInstanceState);
            global::Xamarin.Forms.Forms.Init(this, savedInstanceState);

            //セットアップ
            Startup.Init(ConfigureServices);//コールバックとしてConfigureServicesメソッドを渡す

            LoadApplication(new App());
        }

...

        /// <summary>
        /// プラットフォーム固有の機能のDIはこちら
        /// </summary>
        /// <param name="ctx"></param>
        /// <param name="services"></param>
        private void ConfigureServices(HostBuilderContext ctx,IServiceCollection services)
        {
            //ここにDIしたいクラスを追加する
            //今回は通知機能
            services.AddSingleton<INotificationService, AndroidNotificationService>();
        }
    }

次にこれに合わせてStartup.Initメソッドを以下のように変更する.

        public static void Init(Action<HostBuilderContext, IServiceCollection> nativeConfigureServices)
        {
            var a = Assembly.GetExecutingAssembly();
            using var stream = a.GetManifestResourceStream("XFUseAspNetCoreDI.appsettings.json");

            var host = new HostBuilder()
                .ConfigureHostConfiguration(c =>
                {
                    //これは?
                    c.AddCommandLine(new string[] { $"ContentRoot={FileSystem.AppDataDirectory}" });

                    //設定ファイルを読む
                    c.AddJsonStream(stream);
                })
                .ConfigureServices((c, x) =>
                {
                    //プラットフォーム固有機能のDIを行うためのコールバックを実行
                    nativeConfigureServices(c, x);
                    ConfigureServices(c, x);
                })
                .Build();

            ServiceProvider = host.Services;
        }

一箇所だけ異なるのは,ネイティブ側から渡されたConfigureServicesメソッドを実行する以下の箇所.

                    //プラットフォーム固有機能のDIを行うためのコールバックを実行
                    nativeConfigureServices(c, x);

ここでネイティブ側のオブジェクトの登録が行われる.

その後にプラットフォームに依存しない共有プロジェクトのオブジェクトの登録を行うConfigureServicesメソッドを実行している.

先程はApp.xaml.csのコンストラクタ内でStartup.Init();としていたが,ネイティブ側でStartup.Init(ConfigureServices);とするようになったので,App.xaml.csからはStartup.Init();を削除して以下のようになる.

        public App()
        {
            InitializeComponent();

            //ServiceProviderからMainPageのインスタンスを取得
            MainPage = Startup.ServiceProvider.GetService<MainPage>();//Xamarin.Forms.Xamlに定義されてる拡張メソッドGetService<T>
        }

ここまででオブジェクトの登録は済んだので,後はこれが通知を利用する先でインジェクト!されればOkです.

今回,通知機能はMainPageViewModelから実行されるので,ここにインジェクトされるようにします.先程はDependencyServiceから取得していましたがMainPageViewModelのコンストラクタを以下のように変更します.

        private readonly INotificationService _notificationService;
        public MainPageViewModel(IDataService dataService, INotificationService notificationService)
        {
            ・・・・省略
            this._notificationService = notificationService;

            ShowNotificationCommand = new Command(_ =>
            {
                _notificationService.ScheduleNotification("My Notification", "This is Test");
            });
        }

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

以下のようにネイティブ側の通知用クラスのインスタンスが渡されていることが確認できます.

f:id:shuhelohelo:20200608125609p:plain

ソースコード

github.com