SDD(Sleep-Driven Development)

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

Xamarin.Forms上でHello World

これは[学生さん・初心者さん大歓迎!]Xamarin Advent Calendar 2016 - Qiitaの23日目の記事です。

前日は

普段Swift書いてますがXamarinに入門してみました | Developers.IO

でした。

今回、本当はXamarin Studio Addinについて書こうとしたのですが、Xamarin Studioにブチ切れた諸々の諸事情により書く時間が足りなかったので思いっきりネタに走りました。本当に誰得レベル。

最近Xamarin.Forms書いてないし(専らNativeのほう)、そういえば私も1本もアプリ作れてないので初心者だと思います。なので、入門に向けてXamarin.Forms上でHello Worldをやってみました。
目指すのは以下のような感じ。

f:id:crocus7724:20161223225732p:plain

大事なことなのでもう一度いいます。ネタです。

環境

  • macOS Sierra
  • Xamarin Studio 6.1.2
  • Xamarin Forms 2.3.1.114

Hello World!!

とりあえずXamarin StudioでちゃちゃっとXamarin FormsのPortableでプロジェクトを作成します。(今考えるとSharedのほうが良かった)

作成したらぱぱっとHello Worldを実行したいのですが、PortableライブラリではNuGetを追加できませんでした。なので実行するためのインターフェースとして以下をPortableライブラリに追加します。

using System.Threading.Tasks;

namespace HelloForms
{
    public interface IScripting
    {
        Task<T> RunAsync<T>(string code);
    }
}

またNative側のNuGetパッケージにMicrosoft.CodeAnalytics.CSharp.Scriptingを追加します。 そして先程のインターフェースを実装したクラスを作成します。

using System.Threading.Tasks;
using HelloForms.iOS;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Xamarin.Forms;

[assembly: Dependency(typeof(Scripting))]

namespace HelloForms.iOS
{
    public class Scripting : IScripting
    {
        public async Task<T> RunAsync<T>(string code)
        {
            var result = await CSharpScript.RunAsync<T>(code);

            return result.ReturnValue;
        }
    }
}

上の[assembly:Dependency(typeof(Scripting))]はPortableライブラリ側でこのクラスを使うために必要です。 Tで戻り値の型を指定できるのですが今回全く使ってませんでした。(なくてもよかった)

これで準備は整いました。あとはPortableライブラリ側でViewとなんちゃってViewModelを作り・・・

  • HelloFormsPage.xaml
<?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:local="clr-namespace:HelloForms"
             x:Class="HelloForms.HelloFormsPage">
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness"
                    iOS="0,20,0,0"
                    Android="0"
                    WinPhone="0" />
    </ContentPage.Padding>
    <ContentPage.BindingContext>
        <local:HelloFormsPageViewModel />
    </ContentPage.BindingContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="5*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="5*" />
        </Grid.RowDefinitions>

        <Editor Text="{Binding Code,Mode=OneWayToSource}" />
        <Button Grid.Row="1" Text="Run Script"
                Command="{Binding ExecuteCommand}" />
        <Editor Grid.Row="2" Text="{Binding Result}" />
    </Grid>
</ContentPage>
  • HelloFormsPageViewModel
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Xamarin.Forms;

namespace HelloForms
{
    public class HelloFormsPageViewModel : INotifyPropertyChanged
    {
        private string _code;
        private string _result;

        public string Code
        {
            get { return _code; }
            set
            {
                _code = value;
                OnPropertyChanged();
            }
        }

        public Command ExecuteCommand { get; }

        public string Result
        {
            get { return _result; }
            set
            {
                _result = value;
                OnPropertyChanged();
            }
        }

        public HelloFormsPageViewModel()
        {
            ExecuteCommand = new Command(async () =>
            {
                try
                {
                    var result = await DependencyService.Get<IScripting>().RunAsync<object>(Code);
                    Result = result?.ToString() ?? "";
                }
                catch (Exception e)
                {
                    Result = e.Message;
                }
            });
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

実行すれば・・・

f:id:crocus7724:20161220000511g:plain

Hello Worldができました!!さらにIDE側のApplication OutputにもHello Worldできます。
これで無事私も入門できました!

なにか違う

思えば確かにHello Worldと出力されたけどあれは真のHello Worldでは無い気がします。私が、いや私達が目指す真のHello Worldはまっさらな画面の真ん中にシステム標準の大きさで表示されているHello Worldなはずです。

というわけで改造。各プラットフォーム毎のScripting.RunAsync<T>の中身を以下のように変更

public async Task<T> RunAsync<T>(string code)
{
    var result = await CSharpScript.RunAsync<T>(code,ScriptOptions.Default
        .AddReferences(Assembly.Load("Xamarin.Forms.Core"),
            Assembly.Load("Xamarin.Forms.Platform"),
            Assembly.Load("Xamarin.Forms.Platform.iOS"),
            Assembly.Load("Xamarin.Forms.Xaml"))
        .AddImports("Xamarin.Forms"));
    return result.ReturnValue;
}

Xamarin.Forms関連のアセンブリを片っ端から読み込ませます。ここいらへんヤケクソ これで実行時にXamarin.Forms関連のアセンブリが参照と、using Xamarin.Formsの省略ができます。

これで完成。

いちいち入力するの怠いので以下のコードをクリップボードにコピーしといて・・・

Application.Current.MainPage.Navigation.PushAsync(new ContentPage
{
    Content = new StackLayout
    {
        VerticalOptions = LayoutOptions.Center,
        HorizontalOptions = LayoutOptions.Center,
        Children = { new Label
        {
            Text = "Hello World!"
        }}
    }
})

実行時貼り付けてやれば・・・

f:id:crocus7724:20161221222342g:plain

やりました!!真のHello Worldができました!!

違う

よくよく考えなくてもXamarin.FormsのViewはXAMLで組み立てるのが可読性的な意味でベストです。
なので正真正銘のHello WorldXAMLを使って表示させるべきなのではないでしょうか??

というわけでまたまた改造していきます。
まず、XAMLで書かれた文字列をインスタンスに変えなければならないのですが、自力で実装するのは骨が折れます。
幸いなことに、文字列からインスタンスに変換してくれるメソッドはXamarin.Formsにはあります。

https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Xaml/ViewExtensions.cs

このExtensionsクラスのLoadFromXamlメソッドの第2引数がstringのものがまさに求めていたものなのですが、非常に残念なことにアクセシビリティinternalです。

なので通常の方法で呼び出すことができません。。。非常に残念なので

using System;
using System.Linq.Expressions;

namespace HelloForms
{
    public static class Extensions
    {
        private static readonly Func<object, string, object> loadFromXamlDelegate;

        static Extensions()
        {
            // object view;
            var view = Expression.Parameter(typeof(object), "view");
            // string xaml;
            var xaml = Expression.Parameter(typeof(string), "xaml");

            // (view, xaml) => Xamarin.Forms.Xaml.Extensions.LoadFromXaml<view.Type>(view, xaml)
            var lambda = Expression.Lambda<Func<object, string, object>>(
                Expression.Call(typeof(Xamarin.Forms.Xaml.Extensions),
                    nameof(Xamarin.Forms.Xaml.Extensions.LoadFromXaml),
                    new[] {view.Type},view, xaml),
                view, xaml);

            loadFromXamlDelegate = lambda.Compile();
        }

        public static TXaml LoadFromXaml<TXaml>(this TXaml view, string xaml)
            => (TXaml) loadXamlDelegate.Invoke(view, xaml);
    }
}

リフレクションで呼び出すことにしました。
毎回リフレクション叩くのは芸がないなーということと通常のリフレクションだとうまくいかなかったので式木を使ってデリゲートを生成しています。 すごい便利ですね。式木。なおこれ式木でやる意味がない気がしますが、気がするだけで気のせいです。

あとはこいつを呼び出してあげればいいわけです。

RunAsyncHelloFormsの参照を追加してあげて

public async Task<T> RunAsync<T>(string code)
{
    var result = await CSharpScript.RunAsync<T>(code, ScriptOptions.Default
            .AddReferences(Assembly.Load("Xamarin.Forms.Core"),
                Assembly.Load("Xamarin.Forms.Platform"),
                Assembly.Load("Xamarin.Forms.Platform.iOS"),
                Assembly.Load("Xamarin.Forms.Xaml"),
                Assembly.Load("HelloForms"))
            .AddImports("Xamarin.Forms","HelloForms"));
    return result.ReturnValue;
}

実行時にいちいち書くのは面倒なので以下のコードをクリップボードにあらかじめコピーしといて

var page = new ContentPage().LoadFromXaml(@"<?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:local=""clr-namespace:HelloForms""
             x:Class=""HelloForms.HelloFormsPage"">
    <StackLayout VerticalOptions=""Center"" HorizontalOptions=""Center"">
        <Label Text=""Hello World!!"" />
    </StackLayout>
</ContentPage>");

実行してあげれば

f:id:crocus7724:20161223191825g:plain

やりました!XAMLでHelloWorldに成功しました!!

今回は時間がなかったのでnew ContentPage().LoadFromXaml("{XAML}")インスタンス化しましたが、ちゃんとXAML用のエディタを用意してあげればいつもと同じような感じでアプリケーション上でXamarin.Formsを使ってMVVMが試せますね。凄い。補完がないので地獄ですが。

ようするに

Roslynです。 詳しくは以下が参考になります。

C#スクリプト実行 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C

Roslyn for Scriptingで、あなたのアプリケーションにもC#スクリプトを!! – kekyoの丼

アプリケーションのマクロ的な言語にC#が使えます。
これでどこでもC#が書ける!ということで詳しく説明する記事を書こうと思ったのですが...断念。

理由の1つ目がNuGet。追加する前はほとんどない(実際Xamarin.Formsだけ)NuGetパッケージが

f:id:crocus7724:20161220003553p:plain

Microsoft.CodeAnalytics.CSharp.Scripting追加後・・・

f:id:crocus7724:20161220003626p:plain

こうなりました。すごい(小並感)

また、ScriptingはiOSでは使えません。シミュレータ動いてんじゃんと思うかもしれませんが、

f:id:crocus7724:20161221065702p:plain

ハイ。JITコンパイルじゃないと使えないらしいです(シミュレータはJITコンパイル、実機はAOTコンパイル)。なのでJITコンパイルが使えないiOSと、最近はAndroidもAOTですかね??まあモバイル勢はいろいろ制約がキツそうです。無念。

じゃあXamarin.Macは?と思うかもしれませんが、Target FrameworkがMobileだとSystem.Runtime.LoaderSystem.NotImplementedExceptionが出てScriptが実行できず、.NET4.5だとそもそもインストールできませんでした。

そもそもSystem.Runtime.Loaderが謎い。iPhoneシミュレータだとSystem.Runtime.Loaderがなくても実行できる(むしろあるとNotImplemented)のですが、iPhone実機で実行しようとするとコンパイルエラー。うーん、コンパイルの仕方で必要なアセンブリが変わるんですかね。。ここいらへんの仕組みがよくわかってないです。

というわけでXamarinだと今のところUWP?とAndroid(JIT)?とiPhoneシミュレータ上だけです。おしい、惜しい。
ただ前までは式木もできなかったらしいので今後Scriptingもできるようになることを期待してます。

以上、ネタでした。

明日の担当はnobukuma - Qiitaさんになります。よろしくお願いします!