DependencyServiceを属性を使って注入する[Xamarin.Forms]
タイトル通りのはずです。意味がわからないですけどできたので備忘録代わりに。
なぜしようと思ったのか
の記事を見てこういう機能は面白そうだなーと思いました。(Get
そして以前JavaのWebフレームワークである「Spring Boot」というのを(ほんの軽く)触って、属性(アノテーション?)を使ってXMLとか特に書かずにルーティングなどができるのは素晴らしい!と思いました。(特に大学の講義でServletをやって、web.xmlを手書きしていたため)
そのSpring Bootの機能の1つにAutoWiredというのがあります。これをクラスのフィールドにつけると勝手に依存性を注入してくれます。
この2つの発想を合わせてみました。
ソースコード
試したのはXamarin.Forms2.0(VSのプロジェクトテンプレートそのまま)です。 最初は属性クラスです。
using System; namespace XamarinSample { [AttributeUsage(AttributeTargets.Field|AttributeTargets.Property|AttributeTargets.Parameter)] public class AutoWiredAttribute:Attribute { } }
ひとまず目印になってほしいだけなので中身空っぽです。あと名前はSpringBootのパクリです。
次にこの属性を見つけてDependencyServiceを注入するクラスです。
using System.Linq; using System.Reflection; using Xamarin.Forms; namespace XamarinSample { public class DependencyServiceInjection { public static void Inject(object source) { //DependencyService.Get<T>()メソッドの情報クラス var dependencyService = typeof(DependencyService).GetRuntimeMethod("Get", new[] {typeof(DependencyFetchTarget)}); //sourceオブジェクトのフィールドにAutoWired属性がついてるFieldInfoコレクション var field = source.GetType().GetRuntimeFields().Where(x => x.GetCustomAttribute<AutoWiredAttribute>() != null); //sourceオブジェクトのプロパティに(ry var prop = source.GetType().GetRuntimeProperties().Where(x => x.GetCustomAttribute<AutoWiredAttribute>() != null); foreach (var fieldInfo in field) { //DependensyService.Getに型引数をつけてInvoke var instance = dependencyService.MakeGenericMethod(fieldInfo.FieldType) .Invoke(typeof(DependencyService), new object[] {DependencyFetchTarget.GlobalInstance}); //フィールドに代入 fieldInfo.SetValue(source, instance); } foreach (var propertyInfo in prop) { //Fieldのプロパティバージョン var instance = dependencyService.MakeGenericMethod(propertyInfo.PropertyType) .Invoke(typeof(DependencyService), new object[] {DependencyFetchTarget.GlobalInstance}); propertyInfo.SetValue(source, instance); } } } }
見て分かる通りリフレクション使いまくりです。
あとはこれをコンストラクタなどで呼び出せば勝手に注入してくれます。
では実際に使ってみたいと思います。
namespace XamarinSample { public interface ISample { string Get(string text); } }
namespace XamarinSample { public interface ISample2 { string GetPath(); } }
めっちゃ適当です。。。
今回はXamarin.Forms側に2つのInterfaceを用意してみました。
これらの各プラットフォーム毎の実装が以下になります。
iOS
using Xamarin.Forms; using XamarinSample.iOS; [assembly:Dependency(typeof(Sample))] namespace XamarinSample.iOS { public class Sample:ISample { public string Get(string text) { return text + "Platform on iOS."; } } }
using System; using Xamarin.Forms; using XamarinSample.iOS; [assembly:Dependency(typeof(Sample2))] namespace XamarinSample.iOS { public class Sample2:ISample2 { public string GetPath() { return Environment.CurrentDirectory; } } }
Android
iOSとほぼおなじ。
UWP
using Xamarin.Forms; using XamarinSample.UWP; [assembly:Dependency(typeof(Sample))] namespace XamarinSample.UWP { public class Sample:ISample { public string Get(string text) { return text+"Platform on UWP."; } } }
using System.IO; using Xamarin.Forms; using XamarinSample.UWP; [assembly:Dependency(typeof(Sample2))] namespace XamarinSample.UWP { public class Sample2:ISample2 { public string GetPath() { return Directory.GetCurrentDirectory(); } } }
雑ですがこれでDependencyService部分ができたのでXamarin.Formsに戻ってViewとViewModelを作っていきます。
namespace XamarinSample { public class SampleViewModel { [AutoWired] private ISample sample; public string Text { get; private set; } [AutoWired] public ISample2 Sample2 { get; set; } public string Path { get; set; } public SampleViewModel() { DependencyServiceInjection.Inject(this); Text = sample.Get("Hello Xamarin!"); Path = Sample2.GetPath(); } } }
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:xamarinSample="clr-namespace:XamarinSample;assembly=XamarinSample" x:Class="XamarinSample.SampleView"> <ContentPage.BindingContext> <xamarinSample:SampleViewModel /> </ContentPage.BindingContext> <StackLayout Orientation="Vertical"> <Label Text="{Binding Text}" VerticalOptions="Center" HorizontalOptions="Center" /> <Label Text="{Binding Path}" VerticalOptions="Center" HorizontalOptions="Center" /> </StackLayout> </ContentPage>
これで完成です。
実行
iOS
Android
エミュレータ永眠
UWP
無事に注入されています。これでいちいちDependencyService.Get
注意点
リフレクションはめちゃくちゃ遅くなります。それを大量に使用しています。
あとは察してあげてください・・・
さらに深みへ
これでフィールドとプロパティにDependencyServiceを注入することができました。しかしPrismではコンストラクタのパラメータで注入しています。
もしかするとDependencyServiceを使うのは一度だけでよくインスタンスを確保しておく必要はない場合もあるかもしれません。
しかし、属性を使う都合上呼び出し側でゴニョゴニョする必要がありますが、XAMLでやる場合コンストラクタは引数なしを強要されます(リフレクションでやってるから?)。
ではどうするかというと、要するに描画に間に合うように注入できればいいのだからBehaviorを使ってViewModel側のメソッドを呼び出し、注入させます。
ソースコード
using System; using System.Linq; using System.Reflection; using Xamarin.Forms; namespace XamarinSample { public class DependencyInjectionBehavior : Behavior<Page> { public static readonly BindableProperty TargetProperty = BindableProperty.Create(nameof(Target), typeof(object), typeof(DependencyInjectionBehavior), default(object)); /// <summary> /// メソッドを呼び出すViewModel /// </summary> public object Target { get { return (object) GetValue(TargetProperty); } set { SetValue(TargetProperty, value); } } public static readonly BindableProperty MethodNameProperty = BindableProperty.Create(nameof(MethodName), typeof(string), typeof(DependencyInjectionBehavior), null); /// <summary> /// 呼び出すメソッドの名前 /// </summary> public string MethodName { get { return (string) GetValue(MethodNameProperty); } set { SetValue(MethodNameProperty, value); } } protected override void OnAttachedTo(Page bindable) { base.OnAttachedTo(bindable); BindingContext = bindable.BindingContext; bindable.BindingContextChanged += OnBindingContextChanged; Initialize(); } private void Initialize() { var dependency = typeof(DependencyService).GetRuntimeMethod("Get", new Type[] {typeof(DependencyFetchTarget)}); //MethodNameと同じ名前のメソッドをTargetから取得 var methodinfo = Target.GetType().GetRuntimeMethods() .FirstOrDefault(x => x.Name == this.MethodName); //パラメータ情報取得 var paraminfo = methodinfo.GetParameters(); //パラメータ情報からTypeを取得し、それでDependencyService.Get<T>()を実行 var parameters = paraminfo .Where(x => x.GetCustomAttribute<AutoWiredAttribute>()!=null) .Select(x => dependency.MakeGenericMethod(x.ParameterType) .Invoke(typeof(DependencyService),new object[] {DependencyFetchTarget.GlobalInstance})).ToArray(); //メソッド起爆 methodinfo.Invoke(Target, parameters); } private void OnBindingContextChanged(object sender, System.EventArgs e) { BindingContext = ((BindableObject) sender).BindingContext; Initialize(); } } }
要するにバインドしたTargetオブジェクトからリフレクションでMethodNameと同じメソッドを見つけ、そのパラメータをリフレクションで取得し、取得したパラメータタイプからリフレクションでDependencyService.Get
問題点としてこの仕組だと呼び出しメソッドをオーバーロードした場合意図した動作をしない可能性があることと、AutoWiredを付け忘れると即死することですがまあ些細な問題でしょう(棒)
namespace XamarinSample { public class SampleViewModel { public string Text { get; private set; } public string Path { get; set; } public SampleViewModel() { } public void Initialize([AutoWired]ISample sample=null,[AutoWired]ISample2 sample2=null) { Text = sample.Get("Hello!!\n"); Path = sample2.GetPath(); } } }
<ContentPage.Behaviors> <xamarinSample:DependencyInjectionBehavior Target="{Binding}" MethodName="Initialize"/> </ContentPage.Behaviors>
ViewModelはInitializeメソッドの中に初期化処理を入れただけ、Viewは上の行を追加しただけです。
実行
iOS
UWP
無事に表示されました。
感想
こういった機能は書いてるうちはめっちゃ楽しいです、ハイ。
まあ何かの参考程度に・・・