眠いしお腹すいたし(´・ω・`)

C#関連を主に書きます。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

Azure Functions の SingletonAttributeとModeプロパティ

今日職場でSingletonAttributeのModeプロパティに関する話が出てたので検証してみました。

ソースコードはこちら

github.com

gist.github.com

TriggerTest1はSingleton(Mode=Function)

TriggerTest2はSingleton(Mode=Listner)

TriggerTest3はSingletonなし

動作環境はAppServiceプラン(S1 インスタンス数を10に固定)です。

まずはTest3のSingletonなしの結果から

f:id:tamafuyou:20171029151501p:plain

同じ時間に並列実行されています。またPartitionKeyもバラバラです。PartitionKeyにはstatic Lazyで作成されたGuidを設定していますのでGuidの違いは実行インスタンスの違いを表しています。要するにSingletonを付けないキュートリガー関数は複数のインスタンスでそれぞれ並列処理が行われる感じになります。今回の場合は10インスタンスなので10インスタンス * デフォルトサイズ16 なので、最大同時実行数は160になります。

続いてTest1の結果はこんな感じ

f:id:tamafuyou:20171029151547p:plain

Timeを見てみるとおおよそ秒毎に処理が実行されていますので、まさにシングル動作です。またPartitionKeyをみると結構バラバラです。

SingletonをFunctionモードで使用すると複数インスタンスであっても同時実行数1が保証される感じになります。

次にTest2の結果

f:id:tamafuyou:20171029151646p:plain

ingletonを使っているのに同じ時間帯にFunctionが実行されています。しかしPartitionKeyに注目してください。全て一緒です。

SingletonをListnerモードで使用すると単一のインスタンスでの実行が保証されます。今回の場合にはバッチサイズはデフォルトなので最大同時実行数は16になります。

ではListnerモードはどのように有効活用すると良いか? という事ですが、私自身、これだ!!っていう決定的な活用法は思いついていないのですが

qiita.com

こちらで書いたような同時実行の細かな制御を行うのに良いのかなぁとか思っています。

Azure Functionsのカスタム出力バインディングの作り方を調べた

Azure Functionsのバインディングの自作方法を調べてみました。

結論から言うと

github.com

このページに全て書いてあります。

ですので、ここからは私の勉強メモということで

github.com

こちらに出力バインディングツイッターでツイートするサンプルを作ってみました。

出力バインディングを指定するパラメータAttributeの作り方

Microsoft.Azure.WebJobs.Description.BindingAttribute が付いたAttributeを作成すればOK。

作成するカスタムAttributeのプロパティにAppSettingAttributeを付けるとプロパティの文字列でConfigurationManager.AppSettingsのキー名で検索を行って値を設定してくれる。接続文字列、ユーザ・パスワードなどのプロパティに設定しておくとよいです。

出力バインディングするためのクラスの作り方

出力バインディングするためのクラスはIAsyncCollectorを実装したクラスを作成するかStreamを実装したクラスを作成します。

今回は簡単なIAsyncCollectorを実装しました。

SampleProjects/TweetAsyncCollector.cs at master · yuka1984/SampleProjects · GitHub

IAsyncCollectorはAddAsyncとFlushAsyncを宣言していますので、この二つの関数を実装する必要があります。

FunctionsにてICollector.Add /IAsyncCollector.AddAsyncが呼ばれた際にAddAsyncが呼び出されます。FunctionsにてICollectorを引数としている場合にはSyncAsyncCollectorAdapterというICollector実装クラスにIAsyncCollector実装がラップされて実行されます。Addが呼び出された時にGetAwaiter.GetResultしてる感じです。

out string みたいな引数のケースでは後程説明しますが関数終了直後にAddAsyncが呼び出されます。 そこまで処理がすべて完了したらFlushAsyncが呼び出される感じです。

この仕組みで重要な点はIAsyncCollectorの実装では必ずしもAddAsync時に出力が行われているわけではなくFlushAsyncが終了した段階でAddAsyncされたデータが出力されていると考える必要があるということです。

ようするにAddAsyncで出力が行われるかは出力バインディングの実装次第だよ、っていうことです。

バインドを行う為の設定クラスの作り方

Attributeを作成したら次はバインド設定を行う為のクラスを作ります。

Microsoft.Azure.WebJobs.Host.Config.IExtensionConfigProviderの実装クラスを作成してFunctionsプロジェクトで読み込むことでIExtentionConfigProvider.initializeメソッドが呼び出されるようになります。

このメソッドでコンバータの登録とバインディングルールの登録を行います。 バインディングルールは出力バインディングの場合には、このAttributeが設定されている時には、このIAsyncCollector実装を呼び出すよ という設定を行います。

context.AddBindingRule<TweetAttribute>().BindToCollector<TweetMessage>(attr => new TweetAsyncCollector(attr));

Converterは様々な引数タイプに対応するために、IAsyncCollector実装のgenericタイプに変換するための処理を登録しておくイメージになります。

context.AddConverter<string, TweetMessage>(input => new TweetMessage() {Message = input});

以上な感じで実装してあげることでオリジナルな出力バインディングを作成することができます。

新しい出力バインディングを作っておいて使いまわしたい。

既存の出力バインディングの挙動が気に入らないので作りたい、なんて時に良いかもしれません。

Azureでリクエストを1件ずつ処理してみる

ごあいさつ

風邪ひきました( д)、;‘.・

はじめに

最近、お仕事でバックエンドの設計とか実装とかしていてAzureを使っているわけですが

こういう時どうしよう、ああいう事したい時にはどうしたら?

みたいな事が沢山でてきてます。

ほとんどは同僚に聞けば解決なのですが

ちょっとは自分で考えましょう、ということで勉強のために実験してみた事を記事にしてみました。

どんなことしてみたの?

パラレルに発生するリクエストに対して順序通りに1件ずつ処理して同期的に返答するにはどうすれば良いか?

という事を考えてみました。

そもそも「そんな設計自体が間違っている」とか「アンチパターンだ」とか思われる方もいらっしゃると思います

もちろん・・・正解です。 

成果物

github.com

設計

こんなイメージ

f:id:tamafuyou:20170819005114j:plain

リクエスト・レスポンスメッセージングパターンに対して不特定な送信者に対応するためにレスポンスメッセージをトッピクによるPub/Subにした感じ。

解説

使用した材料

  • Function App (従量課金プラン) ・・・ 1個 (必須ではない)
  • Function App (AppServiceプランB1)・・・1個   
  • Service Bus (Standard) ・・・ 1個
  • Azureストレージ ・・・1個 (Function Appを作るのに必要)

下準備

ServiceBusにてキューを1個とトピックを1個作っておきます。

HttpTrigger Function

Httpリクエストを受け付けるためにHttp TriggerのFunctionsを使用します。

        [FunctionName("GetRequestTrigger")]
        public static async Task<HttpResponseMessage> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequestMessage req
            , ILogger log)
        {
            var request = new MessageModel();
            var namevalue = req.RequestUri.ParseQueryString();
            foreach (var key in namevalue.AllKeys) {
                if (key.Equals("a", StringComparison.CurrentCultureIgnoreCase))
                    if (int.TryParse(namevalue[key], out int a))
                        request.A = a;
                if (key.Equals("b", StringComparison.CurrentCultureIgnoreCase))
                    if (int.TryParse(namevalue[key], out int b))
                        request.B = b;
            }

            var namespaceManager =
                NamespaceManager.CreateFromConnectionString(
                    ConfigurationManager.AppSettings["servicebusInQueueConnections"]);
            var subscriptionName = Guid.NewGuid().ToString("N");

            await namespaceManager.CreateSubscriptionAsync(new SubscriptionDescription("out", subscriptionName));

            var sbclient =
                SubscriptionClient.CreateFromConnectionString(
                    ConfigurationManager.AppSettings["servicebusInQueueConnections"], "out", subscriptionName);

            var queueclient =
                QueueClient.CreateFromConnectionString(ConfigurationManager.AppSettings["servicebusInQueueConnections"],
                    "in");

            var resultJson = JsonConvert.SerializeObject(request);
            var binary = Encoding.UTF8.GetBytes(resultJson);


            var outMessage = new BrokeredMessage(new MemoryStream(binary));
            var id = Guid.NewGuid().ToString();
            outMessage.To = id;

            await queueclient.SendAsync(outMessage);

            while (true) {
                var message = await sbclient.ReceiveAsync();
                await message.CompleteAsync();
                if (message.To == id) {
                    var stream = message.GetBody<Stream>();
                    var json = new StreamReader(stream).ReadToEnd();

                    await namespaceManager.DeleteSubscriptionAsync("out", subscriptionName);
                    return req.CreateResponse(HttpStatusCode.OK, json);
                }
            }
        }

リクエストを受け付けたら、まずNamespaceManagerを使用してトピックにサブスクリプションを作成します。

そして、作成したサブスクリプションへ接続するクライアントを準備します。

次にキューへの接続クライントを作成します。

BrokeredMessageを作成して送信メッセージを作成してToプロパティにGUIDを設定し、キューへ送信します。

その後、トピックからの受信を待ちます。

トピックからメッセージを受信しToプロパティが送信時にせっていたGUID出会った場合には処理が戻ってきたという事でHttpRequestMessageを戻して終了です。

このファンクションは従量課金プランのFunction Appへデプロイします。

Service Bus Queue Trigger Function

[FunctionName("SumSBTrigger")]
        public static void SumSBTrigger(
            [ServiceBusTrigger("in", AccessRights.Listen, Connection = "servicebusInQueueConnections")] BrokeredMessage
                myQueueItem
            ,
            [ServiceBus("out", Connection = "servicebusInQueueConnections", EntityType = EntityType.Topic)] out
                BrokeredMessage outMessage, ILogger log)
        {
            myQueueItem.RenewLock();
            var stream = myQueueItem.GetBody<Stream>();
            var json = new StreamReader(stream).ReadToEnd();
            var request = JsonConvert.DeserializeObject<MessageModel>(json);


            Thread.Sleep(5000);
            request.Result = request.A + request.B;
            request.ExecuteDateTime = DateTime.Now;

            var resultJson = JsonConvert.SerializeObject(request);
            var binary = Encoding.UTF8.GetBytes(resultJson);

            outMessage = new BrokeredMessage(new MemoryStream(binary));
            outMessage.To = myQueueItem.To;
            myQueueItem.Complete();
        }

特に特別な事はなく

受信したら五秒待機してトピックにアウトプットしています。

トピックにアウトプットするBrokerdMessage.Toに受信したBrokerdMessage.Toを設定しておきます。

このファンクションがもっとも大事な点はAppServiceプランのFunction Appへデプロイするという点とhost.jsonにて以下の設定を行います。

{
  "serviceBus": {
    "maxConcurrentCalls": 1
  }
}

この設定を行う事で1つのインスタンス場でのFunctionの同時実行数が1つとなりシングルスレッド的に処理を行う事ができます。

なぜAppServiceプランのFunction App?

Function Appはスケールコントローラによって自動的にスケールアウト・スケールダウンが行われます。

従量課金プランの場合やAppServiceプランの自動スケールアウトを有効にしていた場合にはインスタンス数の固定化が行えないため負荷によってインスタンス数が増えてしまいます。

AppServiceプランで手動スケールアウトにして1インスタンスに設定し、maxConcurrentCalls = 1に設定する事で完全に1つのプロセスでキューの処理を行う事ができます。

おわりに

こんな事しなくて良い設計を行う事が大事だと思います。

(ノ゚Д゚)八(゚Д゚ )ノイエーイ

NugetPackage License Downloader

プロジェクトで使用しているnugetパッケージのライセンスをテキストファイルに変換してくれるサービスを作ってみました。

Downloader - NugetPackage License Downloader

1つのnugetパッケージに対して1つのテキストファイルが作成されて、zipファイルでまとまめてダウンロードすることができます。

使ってみて不具合等、要望等ありましたらIssue投げてください。

github.com

I tried to create a service that converts license of nuget package used in project into text file.

Downloader - NugetPackage License Downloader

One text file is created for one nuget package and can be downloaded in zip file at once.

If you have problems such as using, if there are demands etc. Please Issue.

TwoWayViewをnugetへ公開しました

github.com

の Xamarin Androidクローンをnugetへ公開しました。

www.nuget.org

プロジェクトサイトはこちらになります。

github.com

サンプルもありますので機会がありましたら使ってみてください。

DroidKaigi2017アプリをベースにXamarin Android開発を考えてたら2か月以上の月日が経過してました(´・ω・`)

はじめに

ジメジメした時期に色々と(´Д`)ハァ…な事が多い日本のXamarin界隈ですが皆さんいかがお過ごしでしょうか?

今回はXamarin Android開発に関して勉強してみたり考えてみた事をジメジメ書き綴ってみようかと思います。

主に頭の中の整理用の記事ですので大多数の方には( ゚Д゚)ハァ?みたいな内容です。

DroidKaigi2017

DroidKaigi2017 凄く良いイベントでした。

本当にキッチリ技術にフォーカスしていて1プログラマとして色々なセッションを聞けてとても面白かったです。

ただ私自身がAndroidアプリを作った経験が少なく分かる事が限られてしまっていたのがとても残念でした。

そこでDroidKaigi2017アプリをXamarinで作成してAndroid開発とXamarin Android開発を両方勉強してv( ̄Д ̄)v イエイしてみようと思い立って約3か月・・・・

ようやく一区切りできたので今回の記事を書いている状況です。

前提条件

今回の取り組みに関する知識・経験の前提を記載します。

  • Xamarin Androidアプリは作った事があるけどFragmentとかViewとかサポートライブラリすら使ったことがなかった。
  • Androidアプリは修正とかした事があるけど,FragmentとかViewとかサポートライブラリすら使っていないアプリだった。
  • Xamarin.Formsを使用したAndroid/iOSクロスプラットフォーム開発はチョットワカル
  • Xamarin.Formsで利用している範囲のAndroidはフワッとわかる。
  • Rxはちょっとわかるけど他ののプラットフォームのRxとかUniRxとかは使ったことない。
  • Javaの言語仕様よく知らない。C#に似てるらしい。
  • Kotlinの言語仕様はよく知らない。なんか新しいらしい。
  • MVVMはくぁwせdrftgyふじこlp

今回目標にした事を記載します。

  • DroidKaigi2017アプリのコードを読んでFragmentとかViewとか使い方と最近のAndroidアプリの作りを理解しよう。
  • DroidKaigi2017アプリをXamarin Androidに移植してXamarin Android開発をしてみよう。
  • モデル層を共通化してDroidKaigi2017 for iOSを作成してみよう(今回未達)

今回の成果

DroidKaigi2017アプリのコードを読んでみた

github.com

メモ取りながら作業をしていなかったので細かい点まで覚えていませんでした。

ですので印象的だったことをピックアップしていきたいと思います。

DI

google.github.io

Dagger2というライブラリを用いてDIを行っていた。

このDagger2というライブラリのパターンが私が普段よく使っている.NETのDIライブラリと使い方がかなり違っていて理解するのに一番時間がかかった。

Dagger2はアノテーションプロセッシングを活用してDIを実現してた。

クラスやメソッドにアノテーション(C#でいうAttribute)をつけることでインジェクションや依存関係を定義する。

私が普段書くプログラムではクラスにてDI設定を定義するはなくて、外部から(コード or Config)定義して使用する書き方をしていたし、それが普通だと思っていたのでビックリした。

どこでコンテナへのインジェクション設定を書いてるんだろうってずっと探してたけど、まさかアノテーション書くだけで実現していたとは・・・

Yukiの枝折: Android: Dagger2

Yukiの枝折: Android: Dagger2 - Subcomponent vs. dependencies

この辺を読んで勉強した。

DaggerはAndroidのライフサイクルに対応するための、特化したDIライブラリという感じだった。

でも結局Daggerの書き方には違和感があってなじまなかった。

私はクラスにDIに必要な要素を書きたくない(´・ω・`)

Rx

RxJava2とRxAndroidというライブラリが使われているみたい。

主に通信を非同期で扱うために使われていた。SingleというObservableの実装があってSingleをSubscribeして結果を受け取った後の処理を書く、みたいな感じだった。

C#でいうところのObservable.FromAsyncでTaskをObservableにしたものって感じ。Nextの次にCompleteが来るやつ。

この使い方ならasync/awaitで書けばよいのに?って思ったらJavaには無いっぽい。だからRxが発達してるんだなぁって理解できた。

通信系はRx使っておくとリトライとか書きやすいのでRxでも良いなぁって思った。

あとはDB操作系でもSingleな戻り値のパターンがあったかな。

retrofit2

これすごかった。

インターフェース書いてアノテーションでURL指定すると、それだけでHttpRequestの実装が終わってる( ゚Д゚)

.NETにも移植したものがあるらしい。

アーキテクチャ

いわゆるクリーンアーキテクチャリポジトリーまでがあってUseCase以降はViewModelとなるパターン。

なんでUseCaseつくらないのかなぁって思ったけど、そもそも単一プラットフォームに対する実装であればUseCase部はViewModelにしちゃえばよいのかなぁって思った。

結構ViewModelでContextを使ってた。ViewModelのライフサイクルがFragmentのライフサイクルと一致していれば特に問題ないんだなぁって感じた。

でもちょっと違和感。それはFragmentとかViewでするべきでは?的な感じがした。

Android Data Binding

xmlのAttributeにViewModelとのバインディングを指定できるすごいやつ。Xamlバインディングとは仕組み的には結構違う。C#はその時にBindingするための処理をするけどAndroidではコンパイル前にバインディングクラスが作られてる。 感覚的にはxamlコンパイルクラスにバインディングの為の関数類も生成されてますって感じ。

なので、単純にバインディングだけじゃなくてコードビハインド的なこともできる(FindViewByIdとか書かなくてよい。)

Converterとかない感じ? 見当たらなかった。そのせいなのかViewModelでViewのプロパティが生えている事が多かった。Visibilityとかリソースとか。

思ったこと

Activity Fragment Viewの使い方は大体わかった。

全体的にアノテーションプロセッシングとかJavaのツールを利用したライブラリが多くて、これをXamarinにそのまま移植するのはハードルが非常に高いと感じた。

Xamarin Androidは、確かにネイティブのAPIは殆どカバーしているけれどJavaの言語仕様や周辺ツール群をカバーしているわけじゃない。

VisualStudioでC#でXamarin Android開発を行う事は単純にJavaの代わりにC#を使うという事ではなくて全く別の開発プラットフォームであると認識したうえで.NETでよく使われるライブラリ群を利用してどのようにAndroid開発にフィットさせるかが大切なのではないか? というところまで考えて、移植作業ではその辺の部分を意識した開発を行ってみた。

DroidKaigi2017アプリをXamarinで再現してみた。

全ての機能を移植していません。セッション周りのみとなります。

他の画面はiOS移植を行う際にForms Embeddingの練習に使おうかと思います。

https://sleepyandhungry1984.tumblr.com/post/162514943270/xamarin-andorid-application
sleepyandhungry1984.tumblr.com

動作はこんな感じです。横画面をキャプチャし忘れましたが問題ないです。

方針

どういう事を実現してみたかったかを書いてみます。

  • Xamarin.Formsアプリでよく使っているパターンで実装してみたかった。
  • できる限りAndroid産のインフラライブラリを使用しないで馴染みの深いライブラリを採用してXamarin Androidにフィットさせてみる。
  • DroidKaigiアプリはconfigChangesを指定していたがArchitecture Componentsの考え方を使えばConfigChangesを指定せずに回転を乗り越えられそうだったので乗り越えてみた。

Architecture Components

コード書き始めた頃に発表になりました。

当然DroidKaigiアプリでは採用されていないんですが、気になったので調べてみて一部をC#に移植してみて適応してみました。

ちゃんとしたArchitecture ComponentsはXamarinチームの凄い人がXamarin環境でも利用できるようにしてくれると思います。

Nyanto

今回作成したインフラ周りはNyantoというプロジェクトにまとめました。

機会があったら独立させてパッケージ化しようかなぁと思いますが使っても私しか幸せになれないのでArchitecture Componentsを使ってください。

このフレームワーク

  • AutofacとArchitectureComponentsのViewModel周りの実装を活用して良い感じにDIを行う
  • ReactivePropertyの利用を行いやすくする。

ということを目的にしています。

ACのViewModelはHolderFragmentというRetainInstance = trueにしたFragmentにViewModelのインスタンスを持たせることによって、画面が回転してActivity/Fragmentのインスタンスが破棄されてもViewModelは維持される、というようなことをしています。

このコードを見た時に、真っ先に思ったことが、なんでViewModelの部分はGenericになっていないのだろう?っていううことでした。

なのでこのViewModel部分をGeneric化してC#に移植し、NyantoではAutofacのILifetimeScopeをあてはめて使用しています。

これにより SingletonはApplicationクラスのライフサイクルと同じで InstancePerLifetimeScopeActivityの存在期間と同じというスコープを実現しました。

またACのLiveDataにあたる部分はReadOnlySwitchReactiveProperty

DroidKaigi2017forXamarin/ReadOnlySwitchReactiveProperty.cs at master · yuka1984/DroidKaigi2017forXamarin · GitHub

というIReacOnlyReactiveProprty<T>の実装を作成することで対応しました。

ReadOnlySwitchReactivePropertyは作成時に通常のIObservable<T>に加えてIObservable<bool>を引数に取ります。

そしてbool IsActiveというプロパティを持ちます。

要するにIsActivetrueの間はいつものReadOnlyReactiveProperty<T>と同じように動作するけどfalseになった時には内部的にValue値は更新されるけどP```ropertyChangedは発生せず値が流れていかない、という動作をします。

またIsActiveがfalseからtrueに変化したときに最新の値を流します。

そしてViewModelBaseには

DroidKaigi2017forXamarin/ViewModelBase.cs at master · yuka1984/DroidKaigi2017forXamarin · GitHub

IObservable<bool>IsActiveObservableというプロパティを持ちます。

IsActiveObservableはNyantoの実装によってViewModelを使用するFragmentが乗っかるActivityのライフサイクルがstart or resumeの時にtrueを流しそれ以外に変化したときにはfalseを流します。

この二つを組み合わせることで画面を更新してはいけない時には更新しない、という仕組みを実現しています。

その他、NyantoにはIObservableとViewのプロパティのバインドを助けるためのExtentionsだったりが含まれています。

Nyanto.ViewSupportTool

FindViewByIdを書くのがつらかったので作りました。

AndroidのLayoutのxmlファイルからViewへのアクセスクラスを生成してみる - Qiita

DroidKaigi2017forXamarin/SessionDetailFragment.cs at master · yuka1984/DroidKaigi2017forXamarin · GitHub

少しは手間を減らせたと思います。

nugetパッケージ化してビルド前に走らせるとか出来るんですけど今回はそこまでしてません。めんどくさいです。

TwoWayView

セッション画面を実現するために使われています。RecyclerViewの実装なのですがGridなLayoutの拡張って感じです。

ここはすっごい苦労しました。 は 一応Bindingしてくれてるgituhubリポジトリがあったんですが、このライブラリはサポートツールのかなり古いバージョンを使用していてサポートライブラリバージョンの辻褄をどうしても合わせられなかったのでフルでC#移植を行いました。

その際にバージョン依存している部分は本家TwoWayViewのIssueに対応方法っぽいものが載っていた採用して移植をしています。最新のサポートライブラリで動作します。

こちらも機会があったらパッケージ化しようかなぁって思ったり思わなかったりしています。めんどくさい。

アーキテクチャ

私の好みのアーキテクチャで実装してます。

私の好きなやつです。以上です。

クリーンアーキテクチャに近い感じでリポジトリをSingleInstanceにしてObserverパターンで全体的に伝搬させるやつですね。

DIを活用して実装しているので結構気軽にインスタンスのライフサイクルの変更にも対応できます。

できる限りTwoWayにならないように設計することを心掛けるようにしています。

バックエンド

本家はGithubjsonを読んだりgoogleフォームにフィードバックを送信したりしてるんですけどフィードバック送っちゃうのはだめだと思うのでバックエンドをAzure MobileAppsのEasyTableに置き換えました。オフライン同期とか使えるんですけど、ちょっと試してみてはまりそうだったのでオフライン同期は使用せずにそれっぽいことをしています。

開発としては最初から置き換えて実装したわけではなくて、最初はGithubからデータを取得するモッククラスやFeedbackの部分はオンメモリで登録したかのように動作するモッククラスを作成してView/ViewModelを作成していって、最後にAzureEasyTableを利用した実装で置き換える。

みたいな感じでいつもやっている開発手法がXamarin Androidでもちゃんとできて、当たり前ではあるけど良い経験ができました。

その他

本家ではViewModelでもContextを扱うような実装だったのですが、個人的にはあまり納得できなかったのでContextを扱う処理はFragmentの方で行うようにしました。

終わりに

細かい点を挙げれば、こうしたい、ああしたい、は沢山あるのですけど、ひとまずの目標を達成したので一区切りで次はiOSネイティブとXamarin iOSの学習に入ろうかと思います。

Xamarin.FormsでLink表現してみた( ̄ー+ ̄)

今回はXamarin.FormsでLink表現をおこなってみました。

LinkerLabelって名前にしています。

完成動画はこち

https://sleepyandhungry1984.tumblr.com/post/162190761683/links-in-label-for-xamarinforms
sleepyandhungry1984.tumblr.com

コードはこち

github.com

スマホアプリでリンク表現が正しいのか・・・

..(/^^)/ ソレハコッチニオイトイテ

表現手段が増えることは良い事です。

利用方法

まずは、このLinkerLabelをどのように使うかのコードを示します。

          <shared:LinkerLabel Text="{Binding BaseText}" 
                              FontSize="15"
                              VerticalOptions="Center" HorizontalTextAlignment="Start"
                              HorizontalOptions="Center"  ItemsSource="{Binding LinkWords}" Command="{Binding LinkCommand}"/>

LinkerLabelはLabelを継承していますので基本的にはLabelです。

Labelとの違いはItemsSourceとCommandのBindablePropertyを持っている事です。

ItemsSourceにIEnumerableを渡してあげると、Text文字列中で、その配列に含まれる文字と一致する部分をLink表現にします。 そしてCommandを設定しておくとLink部分をタップした際にコマンドが実行されコマンドプロパティとしてタップした文字が渡されます。

ItemsSourceは当然INotifyCollectionChangedに対応していますのでItemsSourceを変更すればLink表現も追従します。

今回のサンプルのViewModelはLinkerLabel/MainPageViewModel.cs at master · yuka1984/LinkerLabel · GitHubですが画面から入力した文字列をLinkWordsコレクションに追加する事でLink表現が変化しています。

では実装方法を見ていきましょう。

共通実装部分

LinkerLabel/LinkerLabel.cs at master · yuka1984/LinkerLabel · GitHub

Labelを継承してItemsSourceやCommand、LinkColorのプロパティを増やしてそれに対する実装をおこなっています。

ItemsSourceが変更された際には

        private void UpdateMatchWords()
        {
            var txt = Text;
            var buffer = new List<MatchWord>();
            var sources = ItemsSource.Cast<string>().ToList();
            foreach (var source in sources)
            {
                var matches = Regex.Matches(txt, source);
                if (matches.Count > 0)
                    foreach (Match match in matches)
                        if (!buffer.Any(x => x.StartPosition <= match.Index && x.EndPositon >= match.Index))
                            buffer.Add(new MatchWord
                            {
                                Word = source,
                                StartPosition = match.Index
                            });
            }
            _matchWords = buffer;
            OnPropertyChanged(nameof(MatchWords));
        }

こんな感じでTextからLink箇所を抽出して配列化してinternalなプロパティMatchWordsを設定しています。

Rendererの方でMatchWordsの変更を受け取ってプラットフォーム毎にゴニョゴニョする感じになります。

Androidの実装

コードはこち

LinkerLabel/LinkerLabelRenderer.cs at master · yuka1984/LinkerLabel · GitHub

qiita.com

ほぼ記事そのままの実装なので詳細はこちらの記事を見てください。

要約するとSpannableStringというクラスを使うことでリンク表現できますよ~ってことです。

iOSの実装

コードはこち

LinkerLabel/LinkerLabelRenderer.cs at master · yuka1984/LinkerLabel · GitHub

qiita.com

ほぼ記事そのままの実装なので詳細はこちらの記事を見てください。

要約すると

NSMutableAttributedStringというクラスを使うことでリンク表現できますよ~

Tap検出はUITextView.GetClosestPositionToPointできますよ~

でもLabelのiOSでのプラットフォームコントロールはUILabelなのでできませんよ~

なので仮想のUITextViewを作ってtap検出しましたよ~

っていう感じです。

Xamarin iOSでNSLocationInRange関数が見つからなくて代替手段を考えるのに少し時間がかかりました。

おわりに

今回はしませんでしたがURL検出などを行うようにすればAndroidのLinkifyみたいなこともXamarin.Formsでも可能かもしれませんね。

最近はXamarin Androidでの開発の勉強をしていてDroidKaigi2017アプリをXamarin Androidで書くとどうなるか?みたいな研究をしているのですが、これが本当に難しくて今まで私がXamarin Nativeに思っていた事と現実にはかなりの差があって、考えを改めながら色々と作っている最中です。

ずっとローカルで作業していたのですが最近GitHub上に公開しました。

GitHub - yuka1984/DroidKaigi2017forXamarin

まだまだなのですが、少しずつXamarin Androidでの開発を習得していきたいと思います。

それではまた~(^^)/