SDD(Sleep-Driven Development)

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

SynchronizationContext.Currentでnullが返ってくる

躓きかけたのでメモ

C#で以下のようなメソッドを書いたとします。

//Task.Run(()=>Hoge())などで呼び出す
public void Hoge()
{
    //ネイティブUI操作
}

これを非同期(バッググラウンドスレッド)で触ろうとすると死にます。いわゆるUIスレッド以外でUI要素に触ってはいけない問題です。これを回避するにはUIスレッドで実行してやればいいだけです。

PCLの場合、呼び出し側が自分で制御できる場合は例えばTaskSchedulerクラスのFromCurrentSynchronizationContextを使えば問題ないわけですが

//var sheduler=TaskScheduler.FromCurrentSynchronizationContext();
//Task.Factory.StartNew(()=> { Hoge(); },CancellationToken.None,TaskCreationOptions.None, sheduler)で呼び出す
public void Hoge()
{
    //ネイティブUI操作
}

ライブラリ等で呼び出し側を自分で制御できない場合SynchronizationContext.Current.Postメソッドを使えばいいです。

しかしこれにも少し問題があって例えば以下のようなコードを書くと死にます。

//Task.Run(()=>Hoge())などで呼び出す
public void Hoge()
{
    SynchronizationContext.Current.Post((_=>
    {
        //ネイティブUI操作
    },null));
}

何故かと言うとSynchronizationContext.CurrentはUIスレッド以外で呼び出すとnullが返ってきてヌルポるからです。

これを回避するには必ずメインスレッド中で変数などに確保しておくのが手っ取り早いです。もしHoge()のクラスがバックグラウンドスレッド以外で初期化されるならフィールドに確保しておくのが一番簡単でしょう。

SynchronizationContext context=SynchronizationContext.Current;

//Task.Run(()=>Hoge())などで呼び出す
public void Hoge()
{
    context.Post((_=>
    {
        //ネイティブUI操作
    },null));
}

しかしクラスの初期化自体がバックグラウンドスレッドの場合はフィールドで初期化でもヌルポるかもしれません。

これを回避するには絶対にUIスレッドで実行されるところで初期化してstaticにもたせてあげればよさそうなのですが少し面倒ですね・・・

Xamarin.Forms macOSをとりあえず実行してみた

9/7、GithubのXamarin.FormsにmacOSブランチが生えました。

これでXamarin.Formsを使えばWindows(UWP)、AndroidiOSmacOSとほぼ全ての主要なプラットフォームを1ソースで書けるようになりました。やったねXamarin.Formsちゃん!対応するプラットフォームが増えたよ!!

さっそく動かしてみました。

git colne

最初にGithubからXamarin.FormsのmacOSブランチをクローンします。

Kazuki:Projects Yamamoto$ git clone -b macOS https://github.com/xamarin/Xamarin.Forms.git
Cloning into 'Xamarin.Forms'...
remote: Counting objects: 9996, done.
remote: Compressing objects: 100% (89/89), done.
remote: Total 9996 (delta 34), reused 0 (delta 0), pack-reused 9907
Receiving objects: 100% (9996/9996), 14.51 MiB | 1.73 MiB/s, done.
Resolving deltas: 100% (6314/6314), done.
Checking connectivity... done.

Xamarin Studioで実行

そしたらXamarin.Forms.slnをXamarin Studioで開きます。最初は各プロジェクトだけ開いて実行してましたがたまに起動できなくなったりして面倒くさくなったのでまとめて開くことにしました。

開いたらソリューションオプションを開き、スタートアッププロジェクトをXamarin.Forms.ContorlGallery.MacOSに変更します。

f:id:crocus7724:20160911051823p:plain

あとは実行ボタンを押せば起動できます!

f:id:crocus7724:20160911051848p:plain

うーんボタンに歴史を感じますね。あと適当にリストを選択するとクラッシュして死にます。

Demoページ

実行出来たのはいいですがデモはTwitter風でした。これはXamarin.Forms.Controls.MacTwitterDemoになります。なのでXamarin.Forms.Controls.App.csのMainPageを差し替えてあげます。

MainPage = new MacTwitterDemo();

そしてもう一度実行します。

f:id:crocus7724:20160911051934p:plain

ちゃんとデモみたいにTwitter風のページが表示されました。こっちはいい感じ。なおボタンとかは全部押せません。

ちなみにXamarin.FormsなのでiOSでもデモページを表示できます。

もう一度ソリューションオプションを開きスタートアッププロジェクトをXamarin.Forms.ControlGarally.iOSに変更して実行します。

f:id:crocus7724:20160911051952p:plain

画像が表示されてません。しかしそれ以外は表示できてるので問題無いですねハイ。

現在ブランチが生えただけですがそのうち正式にサポートされるでしょう。今は楽しみにやm・・・やりがいが増えるのを待ってます。

MacでNuGetパッケージを作成するときの注意

MacでNugetパッケージ作ってたときハマったのでメモ

nugetのバージョンは3.4.4.1321

Macでもnugetコマンドを使えばNugetパッケージを作れるのですが若干罠めいたものがあります。

結論から先に言うと.nuspecは.slnと同じ階層でパスは相対パス



では実際に問題のあるやり方をしていきましょう。

とりあえず例としてXamarin StudioでXamarin.Formsライブラリプロジェクトを作成してSampleと名づけました。

これをNugetパッケージにするときプロジェクトの真下にいろいろ広げてもいいのですが、たとえば複数プロジェクトを対象とするときは.slnと同じ階層にディレクトリを作ってそこに.nuspecなどを置く人もいると思います。

今回はtoolsというディレクトリを作り、そこでnuget specで.nuspecファイルを作成し以下のように編集しました。

<?xml version="1.0"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
  <metadata>
    <id>Sample</id>
    <version>1.0.0</version>
    <authors>Yamamoto</authors>
    <licenseUrl>http://LICENSE_URL_HERE_OR_DELETE_THIS_LINE</licenseUrl>
    <projectUrl>http://PROJECT_URL_HERE_OR_DELETE_THIS_LINE</projectUrl>
    <iconUrl>http://ICON_URL_HERE_OR_DELETE_THIS_LINE</iconUrl>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>Sample NuGet Package</description>
    <copyright>Copyright 2016</copyright>
    <tags>Sample</tags>
    <dependencies>
      <dependency id="Xamarin.Forms" version="2.3.1.114" />
    </dependencies>
  </metadata>
  <files>
    <file src="../Sample/bin/Debug/Sample.dll" target="lib"/>
  </files>
</package>

今回特にどこかで公開するようなパッケージではないのでlicenseUrlとかはデフォルトそのまま。

あとはnuget packしてあげればパッケージが作られます。

Kazuki:tools Yamamoto$ nuget pack Sample.nuspec -OutputDirectory ~/Nuget
'Sample.nuspec' からパッケージをビルドしています。
パッケージ '/Users/Yamamoto/Nuget/Sample.1.0.0.nupkg' が正常に作成されました。

私は~/Nugetに私物のNugetを置きXamarin Studioなどで参照させているので-OutputDirectoryオプションをつけています。

今回は特に問題なくパッケージが作成されました。

そして別プロジェクトでNugetパッケージを追加して早速使おうとするのですが・・・

f:id:crocus7724:20160910190400p:plain

表示されません。(本当なら名前空間にSampleがあるはず)

どういうことでしょうか?

とりあえずNugetパッケージはできているのに名前空間が出てこないということは.dllが参照されていない可能性があります。

相対パスがいけないのかということでsrcを絶対パスに。

<file src="/Users/Yamamoto/Projects/Sample/Sample/bin/Debug/Sample.dll" target="lib"/>

そしてまたnuget pack

Kazuki:tools Yamamoto$ nuget pack Sample.nuspec -OutputDirectory ~/Nuget
'Sample.nuspec' からパッケージをビルドしています。
Directory '/Users/Yamamoto/Projects/Sample/tools/Users/Yamamoto/Projects/Sample/Sample/bin/Debug' not found.

な に こ れ

なんと絶対パスで書いても相対パス扱いされてる。うーん意味がわからない。

なおNuspec Referenceのsrcの説明には

The location of the file or files to include. The path is relative to the NuSpec file unless an absolute path is specified.

とあります。私は英語苦手だけどGoogle先生の力を借りて読むと絶対パスを指定しない限り相対パスになるよとのこと。なら問題なく絶対パスが使えるはずなんだけど使えない辛い

なお解決方法は簡単です。諦めて.slnと同じ階層に.nuspecを置き

 <file src="Sample/bin/Debug/Sample.dll" target="lib"/>

にします。あとはnuget packしてあげてパッケージを新しいのにしてあげれば・・・

f:id:crocus7724:20160910190330p:plain

はい、無事に追加出来ました。

結論

MacでNugetパッケージを作るときは絶対パスも../も使わない

以上、Macでnuget怪奇現象でした。

Xamarin Studioでクリーンしてもクリーンされない問題

Xamarin Studioを使っているとたまに全てクリーンしてもクリーンされない時があります。 これのせいで不自然にファイルが残りそれが原因でバグることも・・・

この回避手段は簡単でobjフォルダとbinフォルダを消せば大丈夫です(少なくとも私が遭遇したケースでは)。

ただ手動で消すためだけにXamarin Studioから離れるのはアレですしそもそもXamarin Studioがちゃんと消してくれれば問題ありません。ということで...

Addin作りました

クリーンをしたとき、強制的にbinとobjを消す拡張機能を作りました。

使い方は簡単。Githubから.dllを落としてきて

/Applications/Xamarin Studio.app/Contents/Resources/lib/monodevelop/AddIns (macOSの場合)

に突っ込んでやります。あとは起動してソリューションを開き、適当にファイルを開いて上部のメニューバーのビルドを選べばこんな感じに出てきます。

f:id:crocus7724:20160908142956p:plain

Forced Cleanが現在選択しているプロジェクトのみでForced Clean Allがソリューション内の(Xamarin Studioが認識している)全プロジェクト向けになります。英語おかしいのは気にしないでください

内部処理はこんな感じ

using System.IO;
using MonoDevelop.Core;
using MonoDevelop.Core.Execution;
using MonoDevelop.Projects;

namespace ForcedCleanAddin
{
    public class ProjectCleanService
    {
        public static void Clean(Project project, OutputProgressMonitor monitor = null)
        {
            monitor?.Log.WriteLine($"{project.Name}をクリーン...");

            project.Clean(ProgressMonitorService.CleanProgressMonitor, project.DefaultConfiguration.Selector);

            var path = project.BaseDirectory.FullPath;

            var binPath = Path.Combine(path, "bin");

            if (Directory.Exists(binPath))
            {
                Directory.Delete(binPath, true);
                monitor?.Log.WriteLine($"{binPath}を削除しました!");
            }

            var objPath = Path.Combine(path, "obj");

            if (Directory.Exists(objPath))
            {
                Directory.Delete(objPath, true);
                monitor?.Log.WriteLine($"{objPath}を削除しました!");
            }

            monitor?.Log.WriteLine("完了");
            monitor?.Log.WriteLine("");
        }
    }
}


ようするに最初にXamarin Studio標準のクリーンをしたあと.csprojと同じ階層にbinobjがあったら強制的に消します。慈悲はありません。
もっと柔軟に(現在選択されているビルド設定(DebugとかReleaseとか)だけ消すとか)できますがこれが一番シンプルなのでこれのみです。シンプルイズベスト。

実際にこの問題にぶち当たる人がどれだけいるかわかりませんが(私は今のところめったにない)、参考までに。

追記

この拡張のVisual Studioバージョンをid:nuitsjpさんが作ってくださいました!!

www.nuits.jp

流石です! これでMac(Xamarin Studio)とWindows(Visual Studio)両方でこの問題に直面しても大丈夫ですね(なお私のプラグインがちゃんと動くとは言ってない)

Xamarin Studio Addinのつくりかた[Xamarin Studio]

Xamarin StudioではVisual Studioのように(?)、機能を拡張できます。しかしXamarin Studio Addinでググってもほとんど(というかほぼ)情報がありません。

せめてチュートリアルくらいはということで雑ですが書きました。 参考にしたのはXamarinのDevelopersページにあった"Extending Xamarin Studio with Add-Ins"です。

developer.xamarin.com

Edit->Insert Dateを押すと現在の日時が挿入される誰得機能です。

(最初はまじめに書こうと思ったのですが、ネタになりました)

環境

導入

導入はとても簡単です。 Xamarin Studioを立ち上げて上のツールバーのXamarin Studioをクリックしてメニューから"Add-ins..."を選択、Add-in ManagerからGalleryのAddin DevelopmentにあるAddin Makerをインストールすれば終了です。

f:id:crocus7724:20160613010312p:plain

とても簡単!!これならアドインの開発もとても簡単でしょう(((

ソリューション作成

最初にソリューション作成です。

いつもどおりファイル->新規->ソリューションを選択し、プロジェクト選択画面でその他のMiscellaneousを選ぶと真ん中いらへんにXamarin Studio Addinがあります。

f:id:crocus7724:20160613011030p:plain

これを選択し、ソリューション名を入れます。今回はサンプル通り'DateInserter'にしました。

Manifest.addin.xml編集

ソリューションが作成できたら、次にPropertiesフォルダの中にあるManifest.addin.xmlを編集します。 これで上のツールバーに表示される文字と押されたときに実行されるクラスを編集します。

<?xml version="1.0" encoding="UTF-8"?>
<ExtensionModel>
    <Extension
        path="/MonoDevelop/Ide/Commands/Edit">
        <Command
            id="DateInserter.DateInserterCommands.InsertDate"
            _label="Insert Date"
            defaultHandler="DateInserter.InsertDateHandler" />
    </Extension>
    <Extension
        path="/MonoDevelop/Ide/MainMenu/Edit">
        <CommandItem
            id="DateInserter.DateInserterCommands.InsertDate" />
    </Extension>
    <Runtime>
    </Runtime>
</ExtensionModel>

_labelが表示される文字列、defaultHandlerが押された時に実行されるクラス名ですかね?(自信ない

DateInserterCommands.cs

お次に新しい列挙型を追加します。
プロジェクトを右クリックし、追加->新しいファイルから空の列挙型を選択し、サンプル通り'DateInserterCommands'という名前にし、以下のように追加します。

namespace DateInserter
{
    public enum DateInserterCommands
    {
        InsertDate,
    }
}

これが上のManifest.addin.xmlのidと紐付いているのでしょう。

InsertDateHandler.cs

いよいよボタンが押されたときの処理です。クラスを追加し、以下のようにします。

using MonoDevelop.Components.Commands;
 using MonoDevelop.Ide;
 using MonoDevelop.Ide.Gui;   
 using Mono.TextEditor;
 using System;  

 namespace DateInserter
 {
     class InsertDateHandler : CommandHandler
     {
         protected override void Run ()
         {
              Document doc = IdeApp.Workbench.ActiveDocument;
              var textEditorData = doc.GetContent<ITextEditorDataProvider> ().GetTextEditorData ();  
              string date = DateTime.Now.ToString ();  
              textEditorData.InsertAtCaret (date); 
         }

         protected override void Update (CommandInfo info)
         {
               Document doc = IdeApp.Workbench.ActiveDocument;  
               info.Enabled = doc != null && doc.GetContent<ITextEditorDataProvider> () != null; 
         }   
     }
 }

Updateメソッドでボタンを押せるかどうかの処理を行い、Runメソッドで実際に押されたときの処理をしています。 これで完成です。それでは実行してみましょう!!

実行

f:id:crocus7724:20160613013642p:plain

_人人人人人人人人_
> ビルドエラー <
 ̄Y^Y^Y^Y^Y^Y^Y ̄

なんとMono.TextEditor名前空間が消失した模様。

サンプルが実行できない。終わった。先生の次回作にご期待ください。

がんばる

さすがにこれで終われないのでなんとかサンプルだけでも実行できるようにします。

幸いなことにXamarin StudioはIDEですから最終手段で片っ端から変数に値を入れてブレークポイントで実行時の中身をみて調べる事ができます。printfデバッグ!!printfデバッグ!!

とりあえずMono.TextEditorの代わりになるものを見つけなければいけません。 ということで適当にMonoDevelop名前空間を探していたら、MonoDevelop.Ide.Editor名前空間なるものを発見。その中にTextEditorというそれっぽいものがありました。

とりあえずこれのインスタンスの取得の仕方がわからなかったのでITextEditorDataProviderの代わりにTextEditorを突っ込んで実行してみました。

using MonoDevelop.Components.Commands;
using MonoDevelop.Ide;
using System;
using MonoDevelop.Ide.Editor;
using MonoDevelop.Ide.Gui;

namespace DateInserter
{
    class InsertDateHandler : CommandHandler
    {
        protected override void Run()
        {
            Document doc = IdeApp.Workbench.ActiveDocument;
            var textEditorData = doc.GetContent<TextEditor>();
            string date = DateTime.Now.ToString();
            textEditorData.InsertAtCaret(date);
        }

        protected override void Update(CommandInfo info)
        {
            Document doc = IdeApp.Workbench.ActiveDocument;
            info.Enabled = doc != null && doc.GetContent<TextEditor>() != null;
        }

    }
}

f:id:crocus7724:20160613015450p:plain

なんか実行できてる・・・

ひとまず適当にソリューションを開き、編集->InsertDateを押してみると・・・

f:id:crocus7724:20160613015648p:plain

無事に日付が挿入されました(((
(ほんとはgifにしたかったけど何故かはてなに弾かれてgifがアップロードできない・・・)

追記

普通にIdeApp.Workbench.ActiveDocument.Editorで'TextEditor'が取得できました。何故気づかなかったのか・・・

まとめ

Xamarin StudioのAdd-insは情報が少なく茨の道どころか道がない状態ですが、きっと誰かがそのうち便利なのを作ってくれるでしょう。

私には無理そうです。

参考

http://www.monodevelop.com/developers/articles/

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

Realm Xamarin メモ[Xamarin.Forms]

RealmのXamarin版が5/10に出ました。今回はそれを弄ってみたのでメモ等をつらつらと書いていきます。

Realmとは

詳しいことは

realm.io

を見てみてください。

シンプル!!早い!!クロスプラットフォーム!!先進的!!など魅力的な言葉が踊っています。これはぜひ試すしかないでしょう。

なおUWP(とMac)は今のところ非対応ですが将来的に対応したいとのことなので近いうちにサポートされるでしょう(願望)。(realm-dotnetのIssueには既にUWPサポートの声がたくさん)

Realm Browser

Realmのデータベースのデータがどうなってるか知りたいとき、MacユーザにはRealm Browserという便利なものが用意されています。
Realm Browser
ScreenShotの言語は英語なのに入力が日本語なのめっちゃ気になる
使い方は簡単でRealmファイルをダブルクリックして出てきたウインドウのAllowボタンを押すだけです。データが可視化されてしかも編集もできるので大変便利です。
(iOS SimulatorでRealmファイルの場所がわからない場合は

swift - Realm Browserの使い方 - スタック・オーバーフロー

をみれば大丈夫かも)

ただ、上のリンクのバージョンだと一度開くとRealm Xamarinで開けなくなります。(2016/5/17現在) なのでRealm Xamarinで使いたい人はGithubから直接ダウンロードしてきましょう。

インストール

さっそく使っていきましょう。 準備はとても簡単でVSの場合ソリューションを右クリック->Manage Nuget Packages for Solution...でRealmを検索してプロジェクト全てにチェックをした状態でインストールするだけ。とても簡単です。(Versionは0.74.1)

さあ、早速iOSでビルドしてみると・・・

f:id:crocus7724:20160516122302p:plain

失敗しましたorz

原因

どうやらRealmはiOS7以上が必要な模様。VSのXamarin PCLで作るとiOSのデフォルトターゲットは6なので7以上に上げる必要があるみたいです。 なのでiOSの場合はiOSのプロジェクトを右クリック->Properties->iOS ApplicationのDeployment Targetを適宜変更しましょう。

f:id:crocus7724:20160516123401p:plain

Androidも変える必要があるかもしれませんが残念ながらAndroidエミュレータは永眠中なので確かめられません。

もう一度ビルドしてみると成功しました!

使ってみる

それでは早速使っていきましょう。 Realmを使うのに各プロジェクトにそれぞれPath等を書く必要はありません。デフォルトでは'Environment.SpecialFolder.Personalに作られるみたいです(ファイル名はdefault.realm)。名前を変更したい場合はRealm.GetInstance(optionalPath)`のoptionalPathに好きな名前を入れましょう。

今回はお試しなので適当にテーブル作ってデータ突っ込んで取得して画面に表示させます。

public App()
{
            //Realmインスタンス取得
            var realm = Realm.GetInstance();

            //書き込み開始
            using (var transaction = realm.BeginWrite())
            {
                var sample = realm.CreateObject<Human>();
                sample.Age = 18;
                sample.Name = "Hoge hoge";
                
                transaction.Commit();
            }
            
            //テーブルの一番最初のデータ取得
            var human = realm.All<Human>().ToList().FirstOrDefault();

            var content = new ContentPage
            {
                Title = "RealmSample",
                Content = new StackLayout
                {
                    VerticalOptions = LayoutOptions.Center,
                    Children = {
                        new Label {
                            HorizontalTextAlignment = TextAlignment.Center,
                            Text = $"名前:{human.Name}\n年齢:{human.Age}才",
                        }
                    }
                }
            };

            MainPage = new NavigationPage(content);
}

public class Human : RealmObject
{
            public int Age { get; set; }
            public string Name { get; set; }
}

実行してみると・・・

f:id:crocus7724:20160516210907p:plain

無事に追加されています。やったね!

realm.All<Human>()で直接FirstOrDefault()しない理由はまだサポートされてないのでToListを挟まないと使えないからです。ただ、PullRequestも飛んでたし次のバージョンくらいにはサポートされるんじゃないかなーと期待してます。 なお現時点でもFirstメソッドは使えます。

注意点

実際に使ってみてハマったところや気になったところ

使えるデータ

Realmがサポートしているのは

  • bool
  • char
  • byte
  • short
  • int
  • long
  • float
  • double
  • string
  • DateTimeOffset

です。プリミティブ型はNullableもサポートしています。また、RealmObjectとRealmListも設定可能です。

Linqサポート

上にも書きましたが、まだLinqの全てのメソッドをサポートしたわけでは無いみたいです。私が知っているのはFirstOrDefaultとSingleOrDefault、Take、Where系の中でRealmObjectのプロパティ同士の比較(.Where(x=>x.Name==otherX.Name))とか。.Where(x=>x.Name=="Hoge")は大丈夫)です。(他にもありそう)

Transaction外でプロパティセット

上の例でもそうですが、using(var tansaction=realm.BeginWrite())の外でRealmオブジェクトを通して取得したRealmObjectオブジェクトのプロパティのセッターを呼ぶと死にます。 なのでMVVMパターンでうっかりViewModelのプロパティにRealmObjectを紐付けて双方向バインディングで更新をしようとしたら例外で即死します。

ObjectId

特筆すべきことではないっぽいのですが、主キーを設定したいときはObjectId属性をつけるとそうなります。 個人的にそういうのはPrimaryKeyって感じなので最初戸惑ったのですが他でもそう呼ばれているらしい?