SDD(Sleep-Driven Development)

睡眠の重要性!!睡眠の重要性!!

DependencyServiceを属性を使って注入する[Xamarin.Forms]

タイトル通りのはずです。意味がわからないですけどできたので備忘録代わりに。

なぜしようと思ったのか

blog.okazuki.jp

の記事を見てこういう機能は面白そうだなーと思いました。(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

f:id:crocus7724:20160526175349p:plain

Android

エミュレータ永眠

UWP

f:id:crocus7724:20160526175412p:plain

無事に注入されています。これでいちいち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

f:id:crocus7724:20160527042442p:plain

UWP

f:id:crocus7724:20160527042500p:plain

無事に表示されました。

感想

こういった機能は書いてるうちはめっちゃ楽しいです、ハイ。

まあ何かの参考程度に・・・



参考

実行時型情報 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C