‘Xamarin’のエントリ

Weather Typing 3.6を公開するにあたって、UIテストの自動化を研究していた。今、ウェザタイのリグレッションテストの項目が約2500件、WPFだけで500件くらいある。これだけあるとテストだけで数週間かかってしまい、何か修正してもテストがめんどうで公開をあとまわしにしてしまう。人を増やさずになんとかならないか考え続けた結果、UIテストがそろそろ実用的になっていそうなので、やってみた。

Webの世界ではSeleniumがよく使われているが、それをデスクトップアプリやモバイルアプリに使えるようにしたAppiumというフレームワークがある。バイナリだけでテストができるので、C#+Xamarin.Formsで作ったアプリでも、C#でテストコードを書いて、マルチプラットフォームでテストできる。

で、まあいろいろ苦労はしたが、最終的にWPFのテストは77%の自動化ができた。しかも面倒なテストはほとんど自動化できたので、手動のテストを含めても、早ければ1日でテストが終わる感触。

なお、今回はWPFなので、WindowsAppDriverを使用。Windowsストアアプリも同じものが使えるはずで、Android他はまた違うAppiumのドライバを使うが、テストコードは基本的に共有ができるはず。

Palm OSのアプリを作ってた時代は、グレムリンテスト(モンキーテストの自動化)くらいしかなくて、とてもちゃんとUIのテストはできなかった記憶があるけど、RPAも実用的になっている今の時代、以下のような項目くらいは自動化できるようになっていた。

  • ボタンなどをクリックして画面遷移した先のラベルなどが表示されていることを確認する
  • 入力ボックスの文字数制限やエラー表示が正しいことを確認する
  • 複数のインスタンスを起動して、互いに協調動作(具体的にはネット対戦)することを確認する

具体的には、プログラム上でUI部品にIDを与えておくと、実行時にそのIDからテキストを取得したり、クリックしたりできる。さらにテスト自体はプログラミング言語を使って書くので、ロジックは入れ放題。プラットフォーム依存になるけどファイル操作なんかも普通にできるので、かなり自由度が高い。

逆に、どうしても自動化できなかったのは例えば以下の項目。そりゃそうだろうね、っていう感じ。頑張ればできるかも知れないけど手動でやってもたいしたことはない。

  • 文字の色や背景画像を確認する
  • 音がなる、ならないことを確認する
  • 拡大縮小やスプリッターの位置を確認する
  • マウスカーソルの表示・非表示を確認する

UIテストをやる前は、結局目で見ないとおかしいことに気付かないから意味がないかな、と思ってたけど、Appiumの場合、実際にマウスでクリックしたりキーボードを入力したりするので、手動でテストするのとそんなに変わらない。テストスピードがあまり早くないのもあって、テストの様子を見ていればレイアウトが変になっていたりしても割と気付く。

手動ではどうしても確認にミスが出るので、むしろ自動の方が正確だし、手動の時は全組合せのテストは不可能である程度抜粋にせざるをえなかったが、自動なら何も考えずに全組合せテストしてもたいしたことはないので網羅的になる。

メモ。今回のUIテストでは以下のことを気を付けて作ってみた。

  • 汎用的になるようにする。エラーメッセージの確認なんかは、メッセージそのものを確認するとエラーメッセージの変更に堪えられないので、キーワードを含むかどうかだけ確認するようにしてみた。
  • いつでもテストを実行できるようにする。前提条件がないように、テスト項目開始時に設定ファイルやリプレイファイルなんかを全て削除して、必ず同じ条件で実行できるようにしてみた。

ついでに、WindowsAppDriverでハマったこと。

  • Xamarinからだと直接Accessibility IDを設定できない。Xamlから、Effectを使ってIDを設定し、各プラットフォーム固有コードでIDを設定するようにした。
  • 画面に表示されていない項目をクリックしたりはできないので、スクロールがめんどう。結果的にWindows用に最適化されている状態なので、Androidとかでどうしよう。
  • キーボードが101キーボードになっていて、打てない記号がある。特にバックスラッシュを打つ方法が見つからなかった。
  • Windows用のAppium用クラスとAndroid用のAppium用クラスが別物なので、統一的に扱えない。なので、一個ラップしたクラスを作って同じように使えるようにした。
  • Async/Awaitするとテストが終わってしまう。WaiterのUntilをうまく使って回避するくらいしかない。

ちょっと前からWeather TypingのWindowsデスクトップ版以外の更新をやっているのだが、テストが2300件くらいあって、さすがにテストが大変。てことでUIテストの自動化を本格的に調査。1週間くらいかかってようやく見えてきたのでメモ。ちなみにAndroid/UWP/WPFは実験成功して、Mac/iOSはまだ試せていない。

UIテストはいろいろな方法があると思うけど、マルチプラットフォーム対応だとAppiumがよさそう。Appiumは、WebのUIテスト自動化Seleniumと同様の方法でモバイルアプリのテストをするフレームワーク。サーバクライアントになっていて、クライアントでテストコードを書くと、サーバにJSON形式のコマンドを投げて、サーバからアプリのUIに向かってキーボードやマウスのイベントを投げてくれる。AppiumのサーバだとAndroidとiOSがテストできるが、Windows Application Driverを使えばWindowsのWPFやUWPもテストができる。クライアントについては、Pythonなどでやるのが普通かもしれないけど、Appium.WebDriverを使えばでC#で開発が可能。

まとめると、C#でUIをテストするコードを書いて、Appiumのサーバを起動すればAndroid/iOSのUIテスト、Windows Application Driverを起動すればWPF/UWPのテストが可能ということ。さらに一工夫いるが、Xamarin.Formsのアプリもテスト可能である。

Appiumのインストール
ここは普通にAppiumとAppium-doctorをインストール。Pythonとかnodeとかそのあたり。Appium-doctorを通すためにいろいろインストールする必要がある。だいたいはなんとかなるはずだが、Windowsで「bundletool.jar cannot be found」になるので環境変数「PATHEXT」に「.JAR」を追加する。これ1日はまったけどappium-doctorのソースまで見てようやく分かった。
Windows Application Driverのインストール
普通にインストール。特に困ることはない
Appium.WebDriverのインストール
とりあえずVisual StudioでUnitTestプロジェクトを作り、NuGetからインストールした。UnitTestなの? って思うけど・・・別にコンソールアプリでもいいのだが、テストケースの管理とかでUnitTestがいいかな、ってくらい
UI部品にIDを振る
テストの時にUI部品を特定するため、それぞれIDを振る。Appiumではいくつか特定の仕方があるが、これ実質Accessibility IDしか使えないよね。てことでXamarinのXAMLで、ButtonとかにAutomationId=”hoge”を入れる。だけでよければ話は簡単なんだけど、Android、UWPはうまくいったがWPFはダメだった。Xamarinのソースを見る限りWPFではViewRendererでSetAutomationIdをやっていないみたい。なので、Effectを使って特定の属性を設定すると、各プラットフォーム向けにAccessibility IDに対応する属性を設定するようにしてみた。これもEffectなの? って感じだけど、これ以外にうまい方法は思い浮かばない。

まずはEffectの属性。プラットフォーム共通。

    public static class TestEffect
    {
        public static readonly BindableProperty NameProperty =
          BindableProperty.CreateAttached("Name", typeof(string), typeof(TestEffect), "", propertyChanged: OnNameChanged);

        public static string GetName(BindableObject view)
        {
            return (string)view.GetValue(NameProperty);
        }

        public static void SetName(BindableObject view, string value)
        {
            view.SetValue(NameProperty, value);
        }

        private static void OnNameChanged(BindableObject bindable, object oldValue, object newValue)
        {
            var view = bindable as View;
            if (view == null)
            {
                return;
            }

            var name = (string)newValue;
            view.Effects.Add(new UITestEffect());
        }
    }

    class UITestEffect : RoutingEffect
    {
        public UITestEffect() : base("com.denasu.UITestEffect")
        {
        }
    }

次に各プラットフォーム用のEffect。

[assembly: ResolutionGroupName("company name")]
[assembly: ExportEffect(typeof(App1.WPF.UITestEffect), "UITestEffect")]

namespace App1.WPF
{
    public class UITestEffect : PlatformEffect
    {
        protected override void OnAttached()
        {
            var name = TestEffect.GetName(Element);
            Control.SetValue(System.Windows.FrameworkElement.NameProperty, name);
        }

        protected override void OnDetached()
        {
            Control.SetValue(System.Windows.FrameworkElement.NameProperty, "");
        }
    }
}
[assembly: ResolutionGroupName("company name")]
[assembly: ExportEffect(typeof(App1.UWP.UITestEffect), "UITestEffect")]

namespace App1.UWP
{
    public class UITestEffect : PlatformEffect
    {
        protected override void OnAttached()
        {
            var name = TestEffect.GetName(Element);
            Control.SetValue(Windows.UI.Xaml.Automation.AutomationProperties.AutomationIdProperty, name);
        }

        protected override void OnDetached()
        {
            Control.SetValue(Windows.UI.Xaml.Automation.AutomationProperties.AutomationIdProperty, "");
        }
    }
}
[assembly: ResolutionGroupName("company name")]
[assembly: ExportEffect(typeof(App1.Droid.UITestEffect), "UITestEffect")]

namespace App1.Droid
{
    public class UITestEffect : PlatformEffect
    {
        protected override void OnAttached()
        {
            var name = TestEffect.GetName(Element);
            Control.ContentDescription = name;
        }

        protected override void OnDetached()
        {
            Control.ContentDescription = "";
        }
    }
}

XAMLではEffectを付ける。

        <Entry local:TestEffect.Name="Text1" Text="{Binding TextA}" WidthRequest="100" />
        <Entry local:TestEffect.Name="Text2" Text="{Binding TextB}" WidthRequest="100"/>
        <Label local:TestEffect.Name="Result" Text="{Binding TextC}" WidthRequest="100"/>
        <Button local:TestEffect.Name="Execute" Text="Button" Clicked="Button_Click" WidthRequest="100"/>
テストケースの作成

C#ではこんな感じ。各プラットフォームのセッションを作って、共通のテストを実行するようにしてみた。

[TestClass]
public class UnitTest
{
    [TestMethod]
    public void TestMethod1()
    {
        var session = CreateSessionUWP();

        session.FindElementByAccessibilityId("Text1").Click();
        session.Keyboard.PressKey("111");
        session.FindElementByAccessibilityId("Text2").Click();
        session.Keyboard.PressKey("222");
        session.FindElementByAccessibilityId("Execute").Click();
        Assert.AreEqual(session.FindElementByAccessibilityId("Result").Text , "111222");
    }

    private WindowsDriver<WindowsElement> CreateSessionUWP()
    {
        DesiredCapabilities appCapabilities = new DesiredCapabilities();
        appCapabilities.SetCapability("app", "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXXXXXXXXXXXXXXXX!App");
        return new WindowsDriver<WindowsElement>(new Uri("http://127.0.0.1:4723"), appCapabilities);
    }

    private WindowsDriver<WindowsElement> CreateSessionWPF()
    {
        DesiredCapabilities appCapabilities = new DesiredCapabilities();
        appCapabilities.SetCapability("app", @"c:\xxx\App1.WPF.exe");
        return new WindowsDriver<WindowsElement>(new Uri("http://127.0.0.1:4723"), appCapabilities);
    }

    private AndroidDriver<AndroidElement> CreateSessionAndroid()
    {
        DesiredCapabilities appCapabilities = new DesiredCapabilities();
        appCapabilities.SetCapability(MobileCapabilityType.DeviceName, "emulator-5554");
        appCapabilities.SetCapability(MobileCapabilityType.PlatformVersion, "8.1");
        appCapabilities.SetCapability(AndroidMobileCapabilityType.AppPackage, "com.companyname.App1");
        appCapabilities.SetCapability(MobileCapabilityType.PlatformName, "Android");
        appCapabilities.SetCapability(AndroidMobileCapabilityType.AppActivity, "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.MainActivity");
        return new AndroidDriver<AndroidElement>(new Uri("http://127.0.0.1:4723/wd/hub"), appCapabilities);
    }

実行はこんな感じ。静止画じゃ分からないけど、勝手に文字が入力されてボタンが押される。

てことで、単純なUIのテストはこれで自動化できるかな。まあ、UIが崩れるとか、なんか変、っていうバグの方が多いんだけど、画面遷移とかはこれで自動化して、人が見ないと分からないようなテストにリソースを集中できれば成功かな。

そして、まだ試してないけど問題文章も自動で打てるのだろうか。そうすると不正ツールを作れてしまうかも。とりあえず打ち込み速度が遅いからよさそうだけど、設定ができたりすると厄介。

Xamarin.Forms 3.1のデグレードの続き。WPF版でXamarin.Formsを3.0から3.1にすると,UI部品を動的にサイズ変更できなくなるという問題が発生する。ソースの変更点を調べると,ViewRendererのUpdateHeight,UpdateWidthで,Element.HeightRequestとElement.WidthRequestを使っている箇所がElement.HeightとElement.Widthに変わっていた。元々Requestが-1のときは動いてなくて,Requestを取ってちゃんと動くようにしたっぽいんだけど,WPFのTextBlockのControl.Width/Heightを変えてしまうとうまくサイズ変更されない。なので,とりあえずLabelRendererを作ってUpdateHeight,UpdateWidthをオーバーライドして何もしないようにすればLabelはなんとかなる。だが,ListViewのHasUnevenRowsなんかは回避できなかったので,動的な行サイズの変更はやめた。

追記。Xamarin 3.2 Previewを動かしてみたらUWPではリストが途中で切れるようになるし,WPFではラベルが落ちるようになった。Xamarinのバージョンアップのバグに対応する前に次のバージョンのバグが出てくるからもう無理。過去のバグは1つも直らないのでバグ回避が増えていくだけ。

XamarinのListViewは鬼門というかおかしな動きが多い。2週間くらい前にXamarinを3.0から3.1にしたのだが,UWPでページ遷移をすると落ちるようになった。2週間ずっと調査していて,ようやく解決。

事象としては,NavigationPage内にListViewを含むページを入れて,ListView内の項目を全削除してから他のページにナビゲート(PopAsync/PushAsync)すると,UnhandledExceptionなNullReferenceExceptionが発生する。ノーヒント。

ListViewのBindingを外したりSleepを入れたりすると落ちなかったりするのだが,根本的には,Xamarin.Forms 3.1でUWP ListViewRendererのList.UpdateLayout()がDevice.BeginInvokeOnMainThreadで実行されるように変わったことが原因。今日神奈川に帰るときに新幹線内暇だったので,このレンダラーをプロジェクトに取り込んでデバッグしてみて,ようやく分かった。以前はListViewが変更されて,UpdateLayoutが行われて,ListViewがDisposeされるという順番だったのが,ListViewがDisposeされてからUIスレッドでUpdateLayoutが行われるので落ちる。

すっきりした回避方法もないんだけど,PopAsync/PushAsyncをDevice.BeginInvokeOnMainThreadで実行するようにして,Navigateが最後になるようにしてみてようやく落ちなくなった。

WPF版もXamarin.Forms 3.1でいろいろデグっているので回避しないと。いつになったら本当にやりたいことができるのか。

Weather Typing 3.3を公開。全プラットフォーム完成したけど,まずはWindowsデスクトップ版を公開して様子見。順次Windowsストア,Android,iOS,MacOS版を公開していく予定。GitHubも更新しないといけないけどもう少し待って下さい。

修正点はWTのページを参照として,ここでは技術的な話を少し書いておく。

今回,フレームワークをWPFからXamarin.Formsに変更した。前述の全プラットフォームに対して,C#を使った同一ソースとして書けるわけだが,まだまだフレームワークのバグが多くて大変。幸い?ネイティブのかなり深いところまでカスタマイズできるので,なんとか回避できてしまったりするわけだが,開発時間のほとんどをバグの回避に使った感じ。特に今日公開したWPF版は,最近Xamarin.Formsでサポートされたばかりなので不安定。Xamarin.Forms for WPFの本格的なアプリを公開してるって人,他にいるのかな。

感想として,Xamarin.Formsは,WPF/UWP/Android/iOS/MacOSのうち対応したいプラットフォームのネイティブアプリを作ったことがあって,各プラットフォームに精通した人でないとちょっとお勧めしない。逆に精通した人なら,各プラットフォームの性質をどこまでも引き出せるポテンシャルを持っているので,(フレームワークが安定すれば)いい開発環境になると思う。


さて,記録のために書いておくと,WT3.3はテスト項目が2240件。テスト中に見つかったバグが222件。だいたい10個テストすると1個バグが見つかっている。自分のロジックのバグならすぐ直せるけど,何割かはフレームワークがなんかおかしいというものなので調査に1日とか。これは進まない。自動テストもしたいことはしたいんだけど,画面がちらつくとかボタンのデザインが微妙におかしいとか,そういうのをテストできるような自動テストってあるかなあ。

各プラットフォームで公開した後はどうしようかな。本来ならラズパイを使ったウェザタイオフライン(仮)を作ろうとしてたんだけど,来月から関西に行くので,落ち着くまで電子工作的なことができないかも。WTEditorのオンライン化もやりたいんだけど,それをやりはじめるとまた半年くらいかかってしまうし。

Xamarin Forms 3.0が正式リリースになったのでアップデート。今更だが,Xamarin FormsはC#でWindowsストア,Android,iOS,Macの開発ができる共通フレームワークで,共通フレームワークで足りないところはネイティブの機能を使えるのが特徴。ウェザタイではこれを使ってマルチプラットフォーム版を作っている。

Microsoftはいつもバージョン3.0で一気によくなるジンクス通り,3.0でようやく実用的になった感じがある。詳しい変更点はMicrosoft Buildの動画参照だが,ウェザタイにとって嬉しい箇所を紹介。

パフォーマンス

すごく早くなったわけじゃないんだけど,リストビューとかボタンとか,表示の途中経過が見えていたのが目立たなくなった。途中経過が見えるとパフォーマンスが悪く見えるので,これは良い感じ。

Visual State Manager

WPFにあってXamarinになかったものとしてVisual State Managerがある。例えば横画面と縦画面でUIを動的に切り替えたりするときに使う。どうしても必要だったので自作していたのだが,デフォルトで用意された。

WPF

一番大きいのがこれ。これまでXamarinのWindows版はUWP,つまりWindows 10のストアのみに対応。なので,マルチプラットフォーム版ウェザタイはWindows 7では動かない,はずだった。でもWPFに対応したことでWindows 7と同じものが動くようになる。

とはいえリリースされたばかりなのでいろいろおかしなところがある。一番困ったのがList Viewのアイテムを選択したままアイテムを削除するとNotImplementedExceptionが発生する。どうしようもないので,アイテムを削除する前に,CustomRendererからWPFネイティブの「ListView.SelectedIndex = -1」を実行することに。まあこんな感じでやろうと思えばネイティブ機能を使えるところが良い点ではある。

Weather TypingマルチOS対応も大詰め。最後にMac対応をしているのだが、なかなか難しい。どうしてもダメだったのがListViewの列の高さを動的に変える方法。ListViewの項目のうち、下半分を普段は隠しておいて、選択したときだけ全部表示する、というのを実現したい。要はランキングのアップロードボタンを隠したいってことなんだけど。

で、WindowsとAndroidではボタン等の表示非表示を切り替えるだけで問題なく動くのだが、iOSとMacはうまくいかない。いろいろ調べて、ListViewItemがクリックされたときにViewCellのForceUpdateSizeを呼ぶとiOSではうまくいった。でも同じ事をMacでやってもうまくいかない。Xamarinのソースを見てみたが、ForceUpdateSizeの実装はTODOになっていて何もやってなかった。その後、Mac側のネイティブコントロールまでいって、NSTableView.NoteHeightOfRowsWithIndexesChangedを呼び出すとか、NSTableView.ReloadDataを呼び出すとかしてもダメ。で、どうもNSTableViewDelegateのGetRowHeightで値を返せばよさそうというところまではいったのだが、XamarinがDelegateを使っているようで、ごっそり置き換えるのは困難。

というところで、Mac版はボタンを表示しっぱなしにすることに。Xamarin側で実装してくれるといいな。

あと、Mac/iOSではListViewに大量のデータを入れると表示が遅い。Windows/Androidはちょっとずつ表示してくれるけど、Mac/iOSはやってくれないっぽい。これも実装してくれるといいな。

Xamarinを使ったウェザタイのマルチプラットフォーム化は順調に進んでいる。ただ、いろんなプラットフォームの仕様やらバグやらを回避するのが大変。ネット情報も少ないので、1個解決するのに1日かかるとかざら。オープンソースになってるのがせめてもの救い。Xamarin.Forms自体はかなりいいので、変な動作が多いのはもったいない。世の中に広めるためにも、今回分かったTipsをここに書いておくことにする。決して愚痴ではない。

Xamarin Forms for UWP

  • 凝ったUIをリストにしてで表示する場合、Xamarin.Forms的にはListViewのTemplateSelectorを使うのがお勧めのようだが、やってみるとテキストボックス(Entry)のカーソルが表示されなかったりPickerの選択肢が出なかったり(だったかな?)いろいろなことが起こる。結局ScrollView+StackLayoutに落ち着いた。
  • で、ScrollView+StackLayoutにすると、どこをクリックしても最初のテキストボックスがフォーカスされてしまう。ソースを見るとそういう仕様っぽいけど、困るので、ネット情報を参考に、ScrollViewの最初のアイテムとしてサイズ0のButtonを置くことで回避した。
  • XAMLで画像を表示するとき、Anrdoidなどでは画像をResources/Drawableに置けばルートからのパスで指定できるが、同じパスでUWPで使えるようにするには・・・ってことでUWPではルートに画像を置けば読み込めた。気持ち悪いけど。
  • ファイルの扱いも結構困る。Android等ではセキュリティ的に問題なければ自由にファイルパスを指定して扱えるが、UWPはFilePickerなどで指定したファイルか、プロファイル領域しか使えないし、Storageクラスを介さなければならないので自由度が低い。私はファイル操作をラップするクラスを作ってどのプラットフォームでも統一的に扱えるようにしている。

Xamarin Forms for Android

  • ScrollViewと他のUI部品を上下に配置すると、ScrollViewが他の部品をはみ出して画面一杯に表示されてしまう。ScrollViewのIsClippedToBoundsにTrueを設定すればOK。
  • デフォルトではスプラッシュ画面がなく、真っ白な画面が表示される。公式ドキュメントの通りにThemaを作って解決。

Xamarin Forms for Mac

  • ContentPageのBackgroundImageで背景画像を出していたのだが、Mac版だけはタイル表示になってしまう。しょうがないのでContentPageに画面全体のGridを用意してAspect FillにしたImageを配置した。
  • NavigationViewのPopAsyncを何回か実行して一気にタイトル画面に戻るとかしていると、Macだけは途中の画面が表示されてしまう。これはPopAsyncのAnimationパラメータをfalseにすればよい。
  • ListViewの背景がどうやっても白くなってしまうのに悩まされる。ListViewのBackgroundColorにTransparentを設定するだけだとダメで、さらに後ろに白い背景があるっぽい。ソースを見たらあえて白くしているようだったので、ListViewRendererを作って、NativeTableView.EnclosingScrollView.DrawsBackgroundをfalseにする。さらに、これだけだとScrollViewをドラッグしたときに後ろに白い線が見えることがあるのでNativeTableView.HeaderViewをnullにしてようやく完全に透明になった。

Xamarin Forms for iOS

  • ListViewに罫線が入ってしまう。ListViewのSeparatorVisibilityをNoneにすることで消せる。
  • DLLは使えず全部StaticLinkになるのでネイティブP/Invokeしている場合はスタティックリンクにする必要がある。また、他のプロジェクトのクラスの型を参照する場合、GetTypeにモジュール名は付けない。
  • 何故か分からないけどPCLの中にあるクラスに対してGetTypeはできないっぽい。入力方式プラグインはPCLじゃなくて普通のライブラリにした。
  • iOSネイティブのときは、UIInputTextを実装したUIViewでInputDelegateで受け取ったパラメータがUIKeyboardImplで、そのままUIInputTextDelegateにキャストできた。それを使ってTextWillChangeとかTextDidChangeを呼び出し、内部テキストを変更したことをシステムに通知できた。でも、XamarinだとWeakInputDelegateのパラメータをUIInputTextDelegateにキャストできない。ヘルプにはWeakだと実際の継承関係以外の使い方ができるように書いてある気がするけど、どうやるのが正解なんだろう。とりあえず今回はSimpleInputTextのサンプルのように[Adopts (“UITextInput”)]で実装した。ただ、それだとIUITextInputインターフェースを実装しないので、結局TextWillChangeとTextDidChangeを呼び出すパラメータに何を使えば良いのか分からなくなる。なので、わざわざIUITextInputを実装したラッパーを作って[Adopts (“UITextInput”)]を実装したクラスを呼び出すようにして、ようやくiOSネイティブ版と同じ動作になった。

しばらくXamarin.Forms使ってみて分かってきたこと。

1. F5を押してもビルドされない問題

F5を押すとすぐに実行されてしまうので、手動でビルド、デプロイ、実行していたが、Configuration ManagerでUWPプロジェクトのビルドとデプロイにチェックが入っていないためだった。デフォルトでオフになっているのかな。

2. Release版だと例外が発生する問題

Shared ProjectのMainPageを起動するときにSystem.PlatformNotSupportedException: ‘Arg_PlatformNotSupported’ 例外などが発生。Releaseで動かした場合だけなんだけど、そのまま例外をスルーすると動くっぽい。なんだろう。

3. Xamlのインテリセンスが死ぬ

一番困ってるのがこれ。たまに動くんだけど、何かしているといつのまにかインテリセンスが死んでいる。UWPプロジェクトを選択しているときはインテリセンスが正常に動いたことがない。”ContentPage was not found”みたいなのが出てインテリセンスの選択肢がほとんどない。Android/iOSプロジェクトを選択していると動くことも多いけど、いつの間にかインテリセンスのウィンドウが出なくなる。一応そんなときはAndroidやiOSのプロジェクトから共有プロジェクトの参照を消してビルド、もう一回共有プロジェクトの参照を追加してビルドすると出るようになったりする。かも?

とりあえずウェザタイをラズパイで動かすのが当面の目標だが、せっかくやるならモバイル対応も同時にできるとよい。ってことで前からやりたかったXamarin.Formsを試してみる。

Visual Studio 2017でクロスプラットフォームプロジェクトを作るとShared ProjectとUWP/Android/iOS用のプロジェクトができる。とりあえずWindows 10 IoTで動かすためにUWPを中心に。PCで実験してからラズパイに持って行けるのがすごく便利。

クロスプラットフォームのやり方はいろいろあって、まずはWPFでも使っていたPortable Class Library(PCL)はそのまま使える。で、WPF用の共通プロジェクトはShared Projectにして、ifdefでなんとかするかな。PCLでインターフェースを定義、プラットフォーム依存プロジェクトで実装を定義、でDependency Injectionでやるのが綺麗らしいけど、クラス数が多すぎてめんどい。

Shared Projectはそれ自体はDLLでもスタティックライブラリでもなくて、各プロジェクトに勝手にマージされるイメージっぽい。それはいいとして、ifdefのdefine値は各プラットフォームで定義されていると思ってたんだけどそういうわけではない、んですかね。とりあえず自分で各プロジェクトに_UWP__とか定義した。

XAMLの方のクロスプラットフォームはどうかというと、本を読んでいるとXamarin Formsで共通にするかプラットフォーム依存でそれぞれ作るかの2択で、やれることはそんなに変わらないように見えた。なので当然Xamarin Formsでやるんでしょ? って感じだったが、実際やってみると・・・これはプラットフォーム依存の方が楽かも、って思える。コーディング中に画面イメージが出ないのはまあ仕方ないとして、何かちょっとでも変なコードを書くとインテリセンスが動かなくなったり、とにかく不安定。まあ、何をするとおかしくなるのかを学習しながら作っていけばなんとかなる、のか? あと、コード変更した後いちいちBuild&Deployを手動でやらないと新しいバイナリでデバッグできないのは何か設定とかないのかな。

少し心配だったGPIO周り。XamarinのUWPで動くかな、と思っていたが普通にUWPのプロジェクトからGPIOのライブラリは参照できた。最終的に動くのはXamarinがエミュレーションしたものではなくてUWPそのものなのかな?

ひとまずUWP on Xamarin.Formsで作った最初の画面。

某所でXamarinを知った。.NETのマルチプラットフォーム実装のMonoというのがあるのは知っていたが,Monoをベースにした製品とのこと。WinRTのアプリを作るとAndroidとiOSでも動くようになるというのでAnalog Book ReaderのAndroid版とiOS版を作れるかとおもったけど,WinRTのコアの部分のみ対応で,それ以外はC#で各プラットフォームのUIを作り込むってことなのか。それだったらJava/Objecitve-Cで全部作った方がいいかな。