すもぎのめも

いろいろあったことをメモしています

Xamarin.Forms DataPages のデータソースをユーザー定義型にする

はじめに

公式ドキュメントによると、DataPages は 3種類のデータソースに対応しています。

  • JsonDataSource
  • AzureDataSource
  • AzureEasyTableDataSource

使い方はシンプルで、データソースの URL を指定するだけです。

<p:ListDataPage.DataSource>
    <p:JsonDataSource Source="http://demo3143189.mockable.io/sessions" />
</p:ListDataPage.DataSource>

簡単ですね。この XAML を眺めていると、ユーザー定義型の DataSource を使用できるように見えてきます。今回はユーザー定義型 DataSource を作成して DataPages に表示させることを目標とします。

<p:ListDataPage.DataSource>
    <l:CsvDataSource Path="https://raw.githubusercontent.com/smallgeek/DataPagesSample/master/sample.csv" />
</p:ListDataPage.DataSource>

こんな感じ。誰もが一度はお世話になるであろう CSV を使用します。

コード

CsvDataSource

まずは BaseDataSource から派生した CsvDataSource を作成します。

// https://github.com/smallgeek/DataPagesSample/blob/master/src/DataPagesSample/CsvDataSource.cs
public class CsvDataSource : BaseDataSource
{
    protected override async Task<IList<IDataItem>> GetRawData()
    {
        if (initialized == false)
        {
            IsLoading = true;

            var csv = await LoadCsv();

            csv.Rows
               .Select(row => new CsvDataRow(row, csv))
               .Select((data, i) => new DataItem(i.ToString(), data))
               .ForEach(item => dataItems.Add(item));

            IsLoading = false;

            initialized = true;
        }

        return dataItems;
    }

    protected override object GetValue(string key) { ... }
    
    protected override bool SetValue(string key, object value) { ... }
}

自分でパーサーを書いてもいいのですが、今回は FSharp.DataCSV パーサー & リーダーを使用しました。また、C# から F# の非同期処理を呼び出すときに便利なライブラリ FSharp.Control.FusionTasks も使用しています。これがあると F# の非同期処理を async / await で呼び出し可能になります (C# の async と F# の async は別物)。

CsvDataRow

次に、CSV 1行を表す CsvDataRow 型を作成します。

public class CsvDataRow : BaseDataSource
{
    private CsvRow raw;
    private CsvFile parent;

    public CsvDataRow(CsvRow raw, CsvFile parent)
    {
        this.raw = raw;
        this.parent = parent;
    }

    protected override Task<IList<IDataItem>> GetRawData()
    {
        // 型をうまく推論してくれない……
        IList<IDataItem> data = raw
                .Columns.Select((c, i) => new DataItem(parent.Headers.Value[i], c))
                .Cast<IDataItem>()
                .ToList();

        return Task.FromResult(data);
    }

    protected override object GetValue(string key) { ... }
    
    protected override bool SetValue(string key, object value) { ... }
}

ここで重要なのは、行を表すデータでも BaseDataSource から派生する必要があることです。単純なデータ型を使用した場合、DataPages の一覧表示はされますが、詳細表示したときに真っ白な画面になってしまいます。

実行

事前に CSV と画像を GitHub にあげておき、実行します。

f:id:smallgeek:20161127100407p:plain f:id:smallgeek:20161127100411p:plain

表示できましたね。データソースの title・presenter・image がタイトル・詳細・画像に対応しています。

DataTemplate

この一覧表示だと詳細ページの意味があまりないのと、詳細ページの画像の比率が気になるので、DataPages の DataTemplate を変更します。

<p:ListDataPage.DefaultItemTemplate>
    <DataTemplate>
        <ViewCell>
            <p:ListItemControl
                Title="{p:DataSourceBinding title}"
                Detail="{p:DataSourceBinding presenter}"
                ImageSource="{p:DataSourceBinding image}"
                DataSource="{Binding Value}"
                HeightRequest="90" />
        </ViewCell>
    </DataTemplate>
</p:ListDataPage.DefaultItemTemplate>

<p:ListDataPage.DetailTemplate>
    <DataTemplate>
        <l:DetailPage
            Title="Step Details"
            DataSource="{Binding Value}"
            DefaultItemTemplate="{StaticResource DetailCell}">
            <p:HeroImage 
                Aspect="AspectFill"
                Text="{p:DataSourceBinding title}" 
                Detail="{p:DataSourceBinding presenter}" 
                ImageSource="{p:DataSourceBinding image}" 
                DataSource="{Binding Value}" />
        </l:DetailPage>
    </DataTemplate>
</p:ListDataPage.DetailTemplate>

f:id:smallgeek:20161127101105p:plain f:id:smallgeek:20161127101109p:plain

こんな感じで拡張できます。DataPages のコントロールについては公式ドキュメントに詳しく描かれています。

サンプルで使用されている画像は本人が描いたもので、キャラクターデザインはありまさんです。念のため。本当は福祉に関するマークの意味を調べられるアプリを考案していたのですが、使用するには面倒な手続きが必要みたいです。

まとめ

はまりどころもありましたが、DataPages を拡張するのは難しくありません。DataPages 自体は公式がやる気あるのか定かではないところですが、いろいろなデータソースに対応させてパッケージを公開するのも楽しそうです。

ソースコードはこちら

github.com

Xamarin.Forms 用の fsproj をビルドしたときに遭遇したバグの話

はじめに

この記事で扱われるバグは Alpha preview で修正されています。記事を書いている間に修正されたことを記事を公開する直前に気づいて枕を濡らしました。

概要

F# で Xamarin.Forms 向けのプロジェクトを作成しようとしたらビルドエラーになったので調査した話。コードはほとんど出てきませんが無駄に長いです。

前書き

現在は F# の Xamarin.Android や Xamarin.iOS 向けのプロジェクトテンプレートが提供されていて、昔に比べると開発が容易になったと感じます。そんなプロジェクトテンプレートですが、残念なことに F# の Xamarin.Forms 向けは提供されていません。GitHub で見かける F# + Xamarin.Forms のサンプルは画面回りが C# で、共通部分が F# になっているものばかりです。F#er なら全部 F# にしたい。そこで今回は、Xamarin Forms のプロジェクトをすべて F# で作ってからアプリケーションを開発することに挑戦します。

手順

  1. 前述のプロジェクトテンプレートを使用して、それぞれのプロジェクトを作成します。次に PCL 側を追加し、Android / iOS プロジェクトから参照します。
  2. paket を使用して作成したプロジェクトに Xamarin.Forms パッケージを参照させます。 paket の使い方についてはこちらの素晴らしい記事 Paketコトハジメ #FsAdvent を参考にしてください。
  3. 各プラットフォームで Xamarin.Forms.Forms.Init を呼び出します。
// Android
[<Activity (Label = "Sample.Droid", MainLauncher = true)>]
type MainActivity () =
    inherit Xamarin.Forms.Platform.Android.FormsApplicationActivity()

    override this.OnCreate (bundle) =

        base.OnCreate (bundle)

        Xamarin.Forms.Forms.Init(this, bundle)
        this.LoadApplication(new App());
[<Register ("AppDelegate")>]
type AppDelegate () =
    inherit Xamarin.Forms.Platform.iOS.FormsApplicationDelegate()

    // This method is invoked when the application is ready to run.
    override this.FinishedLaunching (app, options) =

        Xamarin.Forms.Forms.Init()

        this.LoadApplication(App())

        base.FinishedLaunching(app, options)

あとは Visual Studio または Xamarin Studio を使用してビルドします。

Droid

いざビルドしてみても失敗してしまいます。ログを確認してみます。

エラー FS0010: 予期しない キーワード 'end' です メンバー定義内
エラー FS0010: この場所またはその前にある構造化コンストラクトが不完全です 実装ファイル内。このポイントまたはその前にある構造化コンストラクトが不完全です または他のトークンを指定してください。
警告 FS0046: 識別子 'fixed' は F# で将来利用するために予約されています

Resource.designer.fs でエラーが発生していました。

// aapt resource value: 0x7f07001c
static member end = 2131165212
            
// aapt resource value: 0x7f070098
static member end_padder = 2131165336

F# の予約語である end がエスケープされていないためエラーになっています。このファイルは Xamarin.Android が自動生成するため、自分のプログラミングミスではありません。そもそもまだコードを書いていません。どうやら Xamarin.Forms が依存するパッケージに含まれるリソースを Resource.designer.fs に出力する処理に問題があるようです。

Google it!

困ったのでぐぐると issue が見つかりました。しかしこのバグは修正済みだと Don Syme 氏が仰っていました。Bug: The end keyword is not escaped in generated codedom #10

RELEASE_NOTES によると、0.9.4.0 で修正されているとのことです。

バグの報告元では榎本さんが参照を更新したと仰っていました。24709 – F# reserved keyword 'end' being generated in member for resource.designer.fs

しかし私の環境ではエスケープされていません。再インストールしても、alpha チャンネルにしても同じ現象が発生しています。

DLL を確認する

FSharp.Compiler.CodeDom.dll を確認します。Windows だと %ProgramFiles(x86)%MSBuild\Xamarin\Android に、OS X だと /Library/Frameworks/Xamarin.Android.framework/Versions/{バージョン}/lib/xbuild/Xamarin/Android に格納されています。

$ monodis --assembly /Library/Frameworks/Xamarin.Android.framework/Versions/7.0.2-37/lib/xbuild/Xamarin/Android/FSharp.Compiler.CodeDom.dll 
Assembly Table
Name:          FSharp.Compiler.CodeDom
Hash Algoritm: 0x00008004
Version:       0.9.2.0
Flags:         0x00000000
PublicKey:     BlobPtr (0x00000000)
    Zero sized public key
Culture:  

エスケープが Fix されたバージョンよりも古い DLL が格納されていました。

コードを確認する

Xamarin.Androidオープンソースソフトウェアですので、コードを参照できます。ビルド関連のコードを探すと、ResourceDesigner を生成するコードが見つかりました。FSharp.Compiler.CodeDom.FSharpCodeProvider も使用されています。

実験

FSharp.Compiler.CodeDom.dll を入れ替えてみましょう。Xamarin.Android.Build.Tasks.dll が参照している FSharp.Compiler.CodeDom.dllFSharp.Compiler.CodeDom, Version=0.9.2.0, Culture=neutral, PublicKeyToken=null であるため、バージョンと PublicKeyToken を合わせる必要があります。

fsprojects/FSharp.Compiler.CodeDom から clone してビルドします。dll ができあがったら、先ほどのフォルダにある dll と入れ替えます。その後 Droid プロジェクトをビルドすると、なんと成功しました。再生成された Resource.designer.fs 内のキーワードもエスケープされています。ということは、この FSharp.Compiler.CodeDom.dll をどうにかしてやればビルドが通るようになるはずです。

オープンソース版の xamarin-android をビルドする

であれば、オープンソース版を自分でビルドして参照してやればよさそうです。製品版の dll をいじるのはいろいろと問題がありますし。手始めに xamarin/xamarin-android からリポジトリを clone します。あとは git submodule update --init --recursive make prepare make とコマンドを実行すればよいはずですが、ubuntu だとうまくいかなかったので今回は OS X でビルドしました。

targets の import 先を変更する

出来上がった生成物の中に Xamarin.Android.FSharp.targets が含まれていますので、Droid プロジェクトの Import 先を変更します。

<Import Project="..\..\..\path\to\Xamarin.Android.FSharp.targets" />

Build

ここまで来たらビルド可能になっているはずなので、コマンドを実行してビルドを走らせます。

xbuild Sample.Droid.fsproj /p:AndroidSupportedAbis=armeabi-v7a,x86 /t:SignAndroidPackage
...
Build succeeded

ビルドに成功しました。apk ファイルも生成されています。

Debug

出来上がったアプリを実行してもすぐに落ちるので IDE を使用してデバッグ実行してみたのですが、エラーが発生します。

f:id:smallgeek:20161201235944p:plain

Xamarin Studio だとこれといったエラーログは出力されないのですが、Visual Studio だと Xamarin.Android.Build.Debugging.Tasks.dll が見つからなかったと出力されます。xamarin-android の生成物に Xamarin.Android.Build.Debugging.Tasks.dll は含まれていないので、製品版の DLL を使用してみましたがうまくいかず。Stable に修正が反映されるのを待ちましょう。

iOS

iOS 向けプロジェクトは何もしなくてもビルドできてシミュレーターでも動作しています。テンプレートそのままなので当然なのですが何もエラーがなくて逆に怖いですね。

f:id:smallgeek:20161201232613p:plain

余談

xamarin-androidリポジトリを観察しているとこういうのも見つかりました。これは F# の TypeProvider という機能を使用してリソースを生成するライブラリのようです。 github.com

TypeProvider については恐らく F# level-9 ninja の方が記事を書いてくれると思います。

まとめ

Xamarin はオープンソースソフトウェアですので、情報を収集しやすく、自分で調査・実験することもできます。自分の使用するソフトウェアをビルドしてみるとわかることがたくさんあります。Xamarin は更新が早いのでアップデート内容は早めに確認しましょう。

参考

grimoire of Android

Tomas Petricek 氏と邂逅した

図らずも松竹スクエアのエレベーターで Tomas Petricek 氏、Evelina Gabasova 氏と 3人になってしまったときの記憶。

Tomas : Hello.
Evelina : Hello.
Me : Hello, I'm honor to meet you! (準備しておいた完璧な挨拶)
Tomas : Do you use F#?
Me : Sometimes. I useally use C#. 
Me : (間が持たないので一方的に喋る) F#, It's a game changer for me.
Tomas : Wao
Me : So I repeated learning F# over and over again, but I'm still level-0 Ninja.
Homas : Haha
Me : Real-World Functional Programming book is very interesting.
Will you write 2nd edition?

Tomas : Ah~ (ここからちゃんと聞き取れず。多分いろいろやってるみたいなことを言っていた)
Tomas : (こいつ聞き取れてないな、みたいな顔) Maybe.
Me : Maybe.
Me : (None だな)