読者です 読者をやめる 読者になる 読者になる

SDD(Sleep-Driven Development)

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

MacでC#ユーザーのRider入門〜インストール〜

RiderとはJetBrainsが開発中のCross-platform C# IDEです。 詳しくはこちらを御覧ください。

Project Rider – 新しい C# IDE #jetbrainsrider | JetBrains ブログ

ReSharpermacOSで使えます。

ReSharpermacOS で使えます!!

(あとIntelliJなどのプラグインも使えます)

これは使うしかありません。さっそくインストールしましょう!!

注意

まずRiderは2016/9/17現在Private EAPです。上のリンクにはリリースは8月を予定と書いてある通り、当初の予定では2月にPrivate EAP、6月ごろにPublic EAP、8月か9月ころに正式リリースだった記憶があるのですが、Private EAPもギリギリまで遅れ(日本時間で2/29の24時頃だったのでギリギリセーフ(?))それから未だにPrivate EAPです。なので

  • Xamarinのプロジェクトは作れません。Xamarin Studioをお使いください。
  • Xamarinのプロジェクトを実行できません。Xamarin Studioをお使いください。
  • プロジェクト操作がかなり怪しいです(ファイル削除してもファイルが残る)。Xamarin Studioでも怪しいです。

ただし、現在のPrivate EAP10ではだいぶ使い勝手もよくC#でコーディングする分にはほぼ問題ありません。むしろReSharperが素敵すぎます。

インストール手順

まずはPrivate EAPのサブスクライバーに登録します。

以下のページの下の方で申し込みが出来ます。

www.jetbrains.com

入力が終わったらSubscribeを押します。すると2-3分以内にメールがくるのでそのメールにあるOS Xのダウンロードリンクを押します。

するとdmgファイルが落っこちてくるのでインストールします。

f:id:crocus7724:20160917200425p:plain

インストールが終了したら早速起動してみましょう。

最初にこんな画面が出てきました。

f:id:crocus7724:20160917200505p:plain

以前のバージョンなんて無いので下を選択します。

次にどちらのUIを使うかの選択肢が出てきました。

f:id:crocus7724:20160917200535p:plain

LightかDarkですね。私は断然Dark派です。

次にどのエディターカラーを使うかを選択します。左がReSharper(違和感あるけど)、真ん中がVisualStudio(違和感あるけど)、右がDarcula(というかIntelliJ)ですね。

f:id:crocus7724:20160917200555p:plain

私は初期のRiderがReSharper一択で慣れてしまったのでReSharperです。

お次はKeymapです。

f:id:crocus7724:20160917201014p:plain

Visual StudioライクかReSharperライクかIntelliJIDEAライクかが選択できます。個人的にはあとで設定から変えられるReSharper(macOS)が好きですが初期設定でも結構キーが衝突しているのでmacOSユーザーならIntelliJのキーマップがいいのかな?

お次はターミナルからRiderを起動できるようにするかです。

f:id:crocus7724:20160917201123p:plain

私は結構ターミナルを使うことがあるのでとりあえずチェックを付けときました。

最後はチュートリアルやバグ報告する場所などのリンクです。

f:id:crocus7724:20160917201141p:plain

では早速Start using Riderを押しましょう!

f:id:crocus7724:20160917201230p:plain

IntelliJプラットフォームユーザーなら見慣れた画面が出てきました。

これで準備は完了です。お疲れ様でした!!

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