おでんはじめました。

required ちくわぶ and 巾着,optional はんぺん.

Xamarin.Forms で App Center のプッシュ通知を使ってみる

この記事は Xamarin Advent Calendar 2018 の 19 日目の記事です。

App Center の Push Notifications と Xamarin.Forms の Android を使用してプッシュ通知を送受信する方法を説明します。App Center の Push Notifications は Preview 版(2018.12.19 現在)ですのでご注意ください。

Xamarin.Forms でプッシュ通知を実装する方法

Xamarin.Forms でプッシュ通知を実装するには以下の方法があります。

プッシュ通知の仕組みは、プラットフォーム通知システム(以下 PNS: Platform Notification System )と呼ばれるプラットフォーム独自のインフラストラクチャを利用してプッシュ通知を配信します。 PNS は Android であれば Firebase Cloud Messaging (FCM)、iOS であれば Apple Push Notification Service (APNS) を使用することになります。

他にも Azure Notification Hubs を使用しないで直接各 PNS とプッシュ通知のやり取りをする方法や、プッシュ通知に似てるものとして Firebase In-App Messaging というのもあります。Firebase In-App Messaging はアプリに事前に URL スキームを埋め込んでおき、Firebase からその URL に対してメッセージを送信するとそのアプリがメッセージを受信するという仕組みです。

Azure Mobile Apps SDK を使用する方法は App Service の URL を指定するだけで各種の接続をまかなってくれて扱いやすいのですが、開発は止まってるぽいです。1

Azure Notification Hubs は ネイティブ側(iOS / Android)にコードを実装する必要があるのですが、App Center の Push Notifications は共通プロジェクトの App クラスにちょろちょろっと書くだけなのでずいぶん簡単になっています(それでも事前に設定することがいろいろありますが)。

Firebase Cloud Messaging の設定

今回は Android を使ってプッシュ通知をおこなうので、Firebase Cloud Messaging でのプロジェクトとアプリの登録が必要になります。

  1. Firebase にログインする
  2. 右上の「コンソールへ移動」をクリック
  3. プロジェクトを追加する
    f:id:masatoru:20181218183333p:plain
  4. 左上の設定をクリック
  5. 「プロジェクトの設定」をクリック
    f:id:masatoru:20181218183617p:plain
  6. Android アプリに Firebase を追加」をクリック f:id:masatoru:20181219122016p:plain
  7. 「アプリの登録」の「 Android パッケージ名」は Xamarin の Android プロジェクト→プロパティ→ Android マニフェスト→パッケージ名 をコピペします
    f:id:masatoru:20181219122502p:plain
  8. 「アプリを登録」をクリックすると google-services.json をダウンロードできるのでこれを保存しておきます
  9. JAVAに関するところは Xamarin には関係ないので「次へ」で無視

これでアプリ登録が完了しました。

サーバーキーの取得

設定→クラウドメッセージングの「サーバーキー」をコピペして保存しておきます。

App Center でアプリケーションを追加する

App Center にログインして All apps から右上の Add newAdd new app をクリックします。

「アプリケーション名」の入力と、OS は Android、Platform は Xamarin を指定します。

f:id:masatoru:20181219115000p:plain

作成したアプリケーションから Push → Notifications をクリックします。 ここに移行の実装手順が記載されています。

  1. Xamarin.Forms での実装手順
  2. (Firebase の設定と)Android プロジェクトへの記述
  3. Firebase のサーバーキーのコピペ

上記手順を以下進めていきます。

Xamarin.Forms にプッシュ通知を実装する

.NET Standard で Xamarin.Forms のプロジェクトを作成します。

パッケージマネージャーから Microsoft.AppCenter.Push をすべてのプロジェクトにインストールします。バージョンは 1.11.0(2018.12.15現在)です。

App.xaml.cs にコードを追加していきます。まずは using を追加します。

using Microsoft.AppCenter.Push

同様に App.xaml.csOnStart() メソッドに 以下を追加します。上記の Push → Notifications のページに記載されているのでそちらをコピペしてください。

protected override void OnStart()
{
  AppCenter.Start("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", typeof(Push));
  Push.PushNotificationReceived += this.Push_PushNotificationReceived;
}

プッシュ通知を受信したときに Push_PushNotificationReceived イベントが呼ばれるので、受信した内容を表示するようにします。

private async void Push_PushNotificationReceived(object sender, PushNotificationReceivedEventArgs e)
{
    await this.MainPage.DisplayAlert(e.Title, e.Message, "OK");
}

Android プロジェクトへ追記する

Android プロジェクトの PropertiesAndroidManifest.xml ファイルの <application>...</application> タグ内に以下を追加します。

<receiver android:name="com.google.firebase.iid.FirebaseInstanceIdInternalReceiver" android:exported="false" />
<receiver android:name="com.google.firebase.iid.FirebaseInstanceIdReceiver" android:exported="true" android:permission="com.google.android.c2dm.permission.SEND">
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<action android:name="com.google.android.c2dm.intent.REGISTRATION" />
<category android:name="${applicationId}" />
</intent-filter>
</receiver>

追加後は以下のようになります。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.company.PushAppCenterSample" android:installLocation="auto">
  <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="27" />
  <application android:label="PushAppCenterSample.Android">
    <receiver android:name="com.google.firebase.iid.FirebaseInstanceIdInternalReceiver" android:exported="false" />
    <receiver android:name="com.google.firebase.iid.FirebaseInstanceIdReceiver" android:exported="true" android:permission="com.google.android.c2dm.permission.SEND">
      <intent-filter>
        <action android:name="com.google.android.c2dm.intent.RECEIVE" />
        <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
        <category android:name="${applicationId}" />
      </intent-filter>
    </receiver>
  </application>
</manifest>

Fireabase でダウンロードした google-service.jsonAndroid プロジェクトにコピーして、「ビルドアクション」を GoogleServicesJson に設定します。

選択できない場合は一度プロジェクトを保存して Visual Studio を終わらせて、再度プロジェクトを開くと表示されます。

f:id:masatoru:20181219123640p:plain

Firebase のサーバーキーのコピペ

上記で Firebase の設定で取得したサーバーキーをここにコピペします。

以上で App Center の設定 と Xamarin.Forms の実装は完了です。

プッシュ通知の送信テストをする

送信テストは App Center のサイトの Notifications ページにある Send Notification ボタンからおこないます。

f:id:masatoru:20181218160145p:plain

Compose で送信するメッセージを指定します。Cmapaign Name, Message が必須で、Title, Custom Data はオプションになっています。Custom data は Key/Valueで値を送信することができます(後述)。

f:id:masatoru:20181218161842p:plain

Target でどのデバイスに送信するかを決定します。

  • All registerd devices - 登録されているすべてのデバイスに送信します。
  • Custom device list - 指定したデバイスに最大20個まで一度の送信することができます。
  • Audience - 国や言語、デバイスのバージョンなどを指定して送信することができます。

f:id:masatoru:20181218161802p:plain

最後に Review で送信する内容を確認して Send notification クリックします。

f:id:masatoru:20181218162629p:plain

うまくいけばデバイスにプッシュ通知が到達しメッセージが表示されます。これまでの経験ではいずれも1秒かからずに到達する感じです。

f:id:masatoru:20181219215210p:plain

Swagger を使ってプッシュ通知を送信する

REST API を使用してプッシュ通知を送信することもできます。使用できる API が OpenAPI(旧Swagger) で定義されています。

API を使用するためには API トークンが必要になります。API トークンは(アプリの設定ではなく) App Center の設定(右上の自分のアイコン)→ Account Settings → API Tokens → New API Token で発行できます。

f:id:masatoru:20181219173514p:plain

Swagger UI を使ってプッシュ通知のメッセージを送信してみます。プッシュ通知の API一覧 の右上にある Authorize をクリックして取得した API トークンを APIToken(apiKey) にコピペします。

f:id:masatoru:20181219180146p:plain

プッシュ通知の API一覧 から POST を使用します。

f:id:masatoru:20181219193918p:plain

[Try it out] をクリックすると値を編集できるので、ここに値を入力していきます。 すべてのデバイスに送信する場合は、"notification_target": null になります。

Example value

{
  "notification_content": {
    "name": "プッシュ通知のテスト",
    "title": "プッシュ通知のタイトル",
    "body": "プッシュ通知のメッセージ"
  },
    "notification_target" : null
}

The name of the owner にオーナー名、The name of the application にアプリ名を入力して、Execute をクリックします。成功すれば notification_id と値が JSON が返ります。

特定のデバイスにプッシュ通知をおこなう

特定のデバイスにプッシュ通知をおこなう場合は、notification_target に対象のデバイス ID を指定します。最大で同時に 20 台まで送信できます。

Example value

{
  "notification_content": {
    "name": "プッシュ通知のテスト",
    "title": "プッシュ通知のタイトル",
    "body": "プッシュ通知のメッセージ"
  },
  "notification_target":  {
    "type": "devices_target",
    "devices": ["xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"],
  }
}

バイス ID は、App Center の SDK を使用して AppCenter.GetInstallIdAsync(); で取得することができます。

Custom Data を使ってプッシュ通知をおこなう

送信する JSONcustom_data を付加すると Key/Value の値を送信することができます。

Example value

{
  "notification_content": {
    "name": "プッシュ通知のテスト",
    "title": "プッシュ通知のタイトル",
    "body": "プッシュ通知のメッセージ",
    "custom_data": {
      "dinner": "beer"
    },
  },
  "notification_target":  {
    "type": "devices_target",
    "devices": ["xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"],
  }
}

バイス側で受け取る方法は、Push_PushNotificationReceived イベントの引数の PushNotificationReceivedEventArgs クラスの CustomData プロパティに Key/Value 値が入っているので、これを使って分岐処理などをおこなうことができます。

private async void Push_PushNotificationReceived(object sender, PushNotificationReceivedEventArgs e)
{
    if (e.CustomData != null && e.CustomData.ContainsKey("dinner"))
    {
        var action = e.CustomData["dinner"];
        switch (action)
        {
            case "beer":
                await this.MainPage.DisplayAlert("Sample", "また飲むのか!", "OK");
                break;
            case "beef":
                await this.MainPage.DisplayAlert("Sample", "焼肉いいね!", "OK");
                break;
        }
    }
}

まとめ

プッシュ通知は環境の設定や実機が必要などめんどくさいイメージがありますが、App Center の Push Notifications は(これまでに比べて)実装が簡単で、かつ安定してプッシュ通知を受信してくれるので、それほどストレスなく使えると思います。ぜひ一度お試しください。

明日は @yu_ka1984 さんです。よろしくお願いします!

SkiaSharpがXamarin.MacでもWPFでも動くよという話

Xamarin その1 Advent Calendar 2017 - Qiitaの22日目です。

去年のアドベントカレンダーXamarin.Forms+SkiaSharpで縦組みの記事を書いたので、今回はこれを.NET Standard化してAndroidiOSとXamarin.MacWPFで動かしてみたよというお話です。

作成したサンプルはGitHubに置いたのでよければビルドしてみてください。

SkiaSharpとは

SkiaSharpとはC#と.Netで使用できるオープンソースの2Dグラフィックスのライブラリです。

github.com

現在はPCLと以下のプラットフォームに対応しています。

こちらに詳しく書かれています。 qiita.com

去年のプロジェクト構成

1年前はSkiaSharpがSharedプロジェクトのみに対応で、PCLプロジェクトに対応していなかったので途中で対応してたけど書き換える能力がなかったのでXAML用に専用のカスタムビュー(TategumiView)を作成し、カスタムレンダラーをiOSAndroidにそれぞれゴリゴリ書いて実装しました。

f:id:masatoru:20171220193746p:plain

今回のプロジェクト構成

現在のSkiaSharpはPCLプロジェクトにも対応しているので、専用のNativeViewを作る必要がなくなり(カスタムレンダラーが不要になり)XAMLでSkiaSharp標準のビューコントローラ(SKCanvasView)を使って描画まで実装が可能です。Xamarin.Mac / WPFも同様にSkiaSharp標準のビューコントローラが用意されています。

さらに.NET Standardに対応しているので描画ロジックなどを.Net Standard対応のライブラリ化をすることでXamarin.MacWPFからも共通のライブラリとして使用することができます。

f:id:masatoru:20171220194316p:plain

去年のSharedプロジェクトに比べてかなりシンプルな構成になっています。

.NET Standard2.0とは

.NET Standardの詳しい説明は賢者の方々のブログを見ていただくとして、僕の唯一の理解は

NET Standard2.0 は .NET Core 2.0 と .NET Framework 4.6.1 と Mono と Xamarin.iOS と Xamarin.Android と Xamarin.Mac と UWP を(ほぼ)サポートしている(ただし、OS やランタイム依存の処理がある場合を除く)

ということです。

.NET Framework4.5以降と.net standard1.xが共存できるということに関してはよくわかりません勉強中です。

要するに.NET Standardは複数のOSでソースを共有できる仕組みなので、 この仕組みのおかげで最近のXamarin.Formsや.NET Framework4.6.1以上のXamarin.MacWPFから.NET Standard2.0のライブラリを使用することができるというわけです。

「ほぼ」とあるのは.Net Standard2.0で作成したライブラリで例えば.NET Framework4.6.1のライブラリを参照すると完全な互換性がない可能性があると警告が表示されるのでそういうことです。

warning NU1701: パッケージ 'xxxxx 2.0.1' はプロジェクトのターゲット フレームワーク '.NETStandard,Version=v2.0' ではなく '.NETFramework,Version=v4.6.1' を使用して復元されました。このパッケージは、使用しているプロジェクトとの完全な互換性がない可能性があります。

僕の数少ない経験上では完全に互換性があります。

各プロジェクトのポイント

各プロジェクト作成にあたり一番特筆すべきは.NET Starndard2.0対応のために.csprojファイルを編集する必要がない!(2017.12.21時点)ということです。

参考までに現在のバージョンを記載しておきますが、少なくともこのバージョン以降であればIDEが提供するプロジェクト作成の手順だけで作成できます。

.NET Standard2.0ライブラリ

UIはXamarin.Forms / Xamarin.Mac / WPFそれぞれで作る必要がありますが、反対にそれ以外に関しては(上記条件を満たせば)共通の .NetStandard2.0ライブラリとしてまとめられるということになります。

今回のサンプルでは(フォントファイルなどリソースの取得に関する箇所を除く)描画ロジックをすべてをライブラリ化して、Xamarin.Forms / Xamarin.Mac / WPFから使用しています。

作成方法はとても簡単で、プロジェクト作成→.NET Standard→クラスライブラリ(.NET Standard)から作成できます。

f:id:masatoru:20171221111721p:plain

Xamarin.Formsプロジェクト

プロジェクト作成に関しては特筆すべきことはありません。プロジェクト作成から、Cross-Platform→Cross-Platform App(Xamarin Forms)→.NET Standardを選択で作成できます。

f:id:masatoru:20171221112156p:plain

nugetからSkiaSharpとSkiaSharp.Views.Formsを取得し、XAMLにSKCanvasViewを配置します。

<views:SKCanvasView x:Name="canvas" PaintSurface="OnPaintSample"/>

SKCanvasViewのPaintSurfaceのイベントハンドラに描画処理を記述します。

void OnPaintSample(object sender, SKPaintSurfaceEventArgs e)
{
  // SKCanvasを取得する
  var canvas = e.Surface.Canvas;
  // 描画処理
  DrawText(canvas);
}

Xamarin.Macプロジェクト

Visual Studio For Macで、Macアプリ→Cocoaアプリを選択します。

SkiaSharpを使用する場合でのはまりどころとしては、ターゲットフレームワーク(オプション→全般)をXamarin.Mac Modernを選択するということです。 .Net Framework4.6.1を選択すると、パッケージの取得でWindowsと認識してしまうのか(SkiaSharp.Mac.dllではなく)SkiaSharp.WPF.dllを持ってきてしまうので(仕組みがよくわからないので誰か教えてください)。

f:id:masatoru:20171221113820p:plain

nugetからSkiaSharpとSkiaSharp.Viewsを取得します。

Storyboardを使用してSKCanvasViewを配置してプロパティを関連付けます。

  1. Main.Storyboardをダブルクリック(XCodeが開く)
  2. ViewControllerにCustomViewを配置する
  3. CustomClassをSKCanvasViewにする
  4. 右上のAssitant editorを開く
  5. 作成したCustomViewをCtrlキーを押しながらViewController.hにドラッグ&ドロップする
  6. Nameをcanvasにする

f:id:masatoru:20171221121513p:plain

上記作業後にXCodeを保存するとコードビハインド(ViewController.designer.cs)にコントロールとプロパティが関連付けされていることがわかります。

[Register ("ViewController")]
partial class ViewController
{
    [Outlet]
    SkiaSharp.Views.Mac.SKCanvasView canvas { get; set; }
    
    void ReleaseDesignerOutlets ()
    {
        if (canvas != null) {
            canvas.Dispose ();
            canvas = null;
        }
    }
}

Xamarin.Forms同様に、ViewController.csにイベントハンドラを記述します。

// 画面が初期化される時に呼ばれる
public override void ViewWillAppear()
{
  base.ViewWillAppear();

  // SKCanvasViewにイベントハンドラを関連付ける
  canvas.PaintSurface += OnPaintCanvas;
}
private void OnPaintCanvas(object sender, SKPaintSurfaceEventArgs e)
{
  // SKCanvasを取得する
  var canvas = e.Surface.Canvas;
  // 描画処理
  DrawText(canvas);
}

Storyboardの操作方法などXamarin.Macのことが詳しく書かれているEssential Xamarinをぜひご一読ください。

WPFプロジェクト

Windowsクラッシックデスクトップ→WPFアプリ(.NET Framework)→.NET Framework4.6.1以上で作成します。

XAMLへの記述以降はほとんどXamarin.Formsと同じです(クラス名が少し違うので注意)。 nugetからSkiaSharpとSkiaSharp.Viewsを取得し、XAMLにSKElementを配置します。

<views:SKElement x:Name="canvas" PaintSurface="OnPaintSample"/>

SKCanvasViewのPaintSurfaceのイベントハンドラに描画処理を記述します。

void OnPaintSample(object sender, SKPaintSurfaceEventArgs e)
{
  // SKCanvasを取得する
  var canvas = e.Surface.Canvas;
  // 描画処理
  DrawText(canvas);
}

画面キャプチャ

最後に今回作成したサンプルの画面キャプチャをのせておきます。

f:id:masatoru:20171221103637p:plain

f:id:masatoru:20171221130955p:plain

f:id:masatoru:20171221130554p:plain

f:id:masatoru:20171221103656p:plain

最後に

.NET Standard2.0がより一般的になることでWinFormsなどのレガシーなC#の資産をXamarinやMacで再利用する流れがそろそろより一層広がればいいなと思います。

今後の課題として、(今回はすべてコードビハインドで書いていますが)WPFやXamarinでPrismやReactivePropertyなどのフレームワークを使用した場合に、MacCocoa Bindingなど別のフレームワークを利用することになるので、全体としてどういう構成(落としどころ)が望ましいのか引き続き勉強したいです。

以上です。 今宵もハッピーアドベント!(言わない?)

追伸、 あ!サンプルのボタンがどれも機能してないです(汗。随時実装していきますので。

明日は tafuji さんです。よろしくお願いします!

Xamarin.Macでアプリ起動時にコードでStoryboardを切り替える方法

Xamarin.Macの起動時に複数のストーリーボードからコードで選択する方法がわからなかったのでそのメモ。

デフォルトで起動時に表示されるインターフェイスはinfo.plistのMain Interfaceで指定されています(さらにInterface Builderの[is Initial Controller]にチェックが入っているWindow Controllerが表示対象)。

f:id:masatoru:20170828125045p:plain

この起動時に表示されるインターフェイスをコードで変更する方法です。

そもそもストーリーボードの使い方がよくわかってない*1のでストーリーボードの追加方法から。

  • ストーリーボードを追加する

    • ファイル→新しいファイル→ストーリーボードでSub.storyboardを作成する
    • 作成したSub.storyboardをInterfaceBuilderで開いてWindowControllerを追加する
    • InterfaceBuilderでWindowControllerを選択→IdentityInspectorを選択→StoryboardIDにMainWindowと入力する
    • 保存する

f:id:masatoru:20170828173725p:plain

  • AppDelegate.csでDidFinishLaunchingメソッドをオーバーライド*2して以下を追加する*3
public override void DidFinishLaunching(NSNotification notification)
{
    // 呼んじゃだめ!!
    //base.DidFinishLaunching(notification);

    // StoryBoardを呼び出し
    var storyboard = NSStoryboard.FromName("Sub", null);

    // StoryBoardからViewControllerを呼び出し
    var controller = storyboard.InstantiateControllerWithIdentifier("MainWindow") as NSWindowController;

    // 表示
    controller.ShowWindow(this);
}

以上っす。

*1:Ctrlキー押しながらdragとか無理っと指つりながら作業してます。

*2:インテリセンスで入力するとbase.DidFinishLaunching()も追加されるけどこれは呼んじゃだめ(「陰」のP24参照)

*3:コードは Working with Storyboards - XamarinのLoading from codeそのまま。

iOSのSimulatorからParallels越しのVisualStudioで起動したASP.NETを参照する方法

バックエンドのテストをしていて、Parallels越しのWindowsのVisualStudioで立ち上げたASP.NETIIS)を、XamarinStudioでビルドしたiOSアプリから参照する方法がわからなかったので調べてみました。

www.barelycompetent.co.za

VisualStudioでASP.NETを起動するとローカルの任意のポート(例:http://localhost:10897/)で実行されます。 このポートはWindowsの外側からは参照できないのでMac側のiOSのSimulator(やAndroidのEmulator)からは使用することができません。そこでSharpProxyというソフトを使ってポート番号を切り替える(すり替える?)ことで実現しています。以下その手順になります。

  • WIndows側でSharpProxyをクローンしてVisualStudioでビルド&起動します。
git clone https://github.com/jocull/SharpProxy.git
  • Windows側のVisualStudioでASP.NETを起動してポート番号を確認します(例:http://localhost:10897/)。

  • WindowsIPアドレスをipconfigで調べます(例:172.xx.xx.xx)。SharpProxyでも[Your IP Address]で表示されています。

  • SharpProxyの[InternalPort]に起動したASP.NETのポート番号(10897)を入力して[Start]をクリックします。

f:id:masatoru:20170404205444p:plain

これでiOS側からParallels越しのWindowsASP.NETを参照できます。

他にもっとスマートな方法があればぜひ教えて下さい。

追伸: Xamarinはいいぞ!!(今回はXamarinに限った話じゃないけど)

Xamarin.Formsで縦組み

Xamarin Advent Calendar 2016(その2)の21日目です。

はじめに

10月にJXUGC #17 お前の Xamarin アプリを見せてみろ!に登壇させていただいたのでそのまとめです。

goo.gl

なぜXamarin.Formsで縦組みか

最近のHTML+CSS縦組み機能が充実しており大抵のブラウザで表示可能です。ただしブラウザによってその実装はまちまちで、端末に依存することなく同じイメージで表示したいというのが始めたきっかけです。自分で実装することで例えば左にルビを表示する(そんな人はほとんどいないでしょうけど)など自由に機能追加できることが(当たり前ですが)魅力の一つです。

SkiaSharp

縦組みの描画は2Dの描画フレームワークSkiaSharpを使用しています。Xamarinが推奨してるわりに日本語情報は少ないのですが、最近になって記事も増えてきたので今後に期待しています。今回のサンプルではRendererを使ってAndroidiOSをそれぞれ実装していますが、最新バージョンではXamarin.Formsに対応しています。ということで早く書き直さないと(汗)。

日本語フォント

日本語フォントを表示するためにIPAIPAex明朝フォントを使用しています。サイトからダウンロードしたipaexm.ttfをリソースフォルダ(AndroidはAssetsフォルダ、iOSはResourcesフォルダ)へ追加しています。

縦組みエンジン(Hanako)

タグの読み込みと組版(Compose)はHanakoという縦組みエンジンで実装しています。この中で行末の折り返しやページごとに組版をおこなうといった処理をおこなっています。

縦組み描画コントロール(TategumiView)

組版されたページをTategumiViewコントロールで表示しています。SkiaSharpで用意されたCanvasにDrawTextで1文字ずつXYを指定して描画しています。

//文字の描画
static void drawHonbunChar(SKCanvas canvas, SKPaint paint, WaterTrans.TypeLoader.TypefaceInfo tfi, HKWaxBase ch)
{
  //文字をGlyphIdに変換
  var glyphs = stringToVerticalGlyphs(ch.Char, paint, tfi);
  //描画の文字サイズを設定
  paint.TextSize = ch.FontSize;
  //本文を描画
  drawText(canvas, glyphs, ch.DevX, ch.DevY, paint);
}
static unsafe void drawText(SKCanvas canvas, ushort[] glyphs, float x, float y, SKPaint paint)
{
  paint.TextEncoding = SKTextEncoding.GlyphId;
  fixed (ushort* p = glyphs)
  {
    canvas.DrawText((IntPtr)p, glyphs.Length * 2, x, y, paint);
  }
}

縦組み用の字形

上記の処理でstringやcharではなくGlyphIdで描画していますが、拗促音(ゃゅょっ)や括弧は縦組み用の字形に切り替える必要があり、これは@espresso3389さんが作った(というかこのために作ってもらった)WaterTrans.TypeLoaderを使用していて、ここでstringをGlyphIdに変換しています。

//縦組み用の文字に変換している
static ushort[] stringToVerticalGlyphs(string text, SKPaint paint
  , WaterTrans.TypeLoader.TypefaceInfo typefaceInfo)
{
  ushort[] glyphs;
  paint.Typeface.CharsToGlyphs(text, out glyphs);
  var conv = typefaceInfo.GetVerticalGlyphConverter();
  for (int i = 0; i < glyphs.Length; i++)
  {
    if (conv.CanConvert(glyphs[i]))
      glyphs[i] = conv.Convert(glyphs[i]);
  }
  return glyphs;
}

XAML

作成したTategumiViewをXAMLで配置してます。WidthやHeightをBindingしていてViewの向きが変わると再処理(Compose)→描画(View)処理がおこなわれます。

<local:TategumiView VerticalOptions="FillAndExpand"
 HorizontalOptions="FillAndExpand"
 Width="{Binding TateviewWidth.Value}"
 Height="{Binding TateviewHeight.Value}"
 CurrentPage="{Binding CurrentPage.Value}"
 PageIndex="{Binding Path=PageIndex.Value,Mode=TwoWay}">

データ形式

HTML形式で<p>タグと<ruby>タグを読み込むようにしています。タグのパースはHtmlAgilityPackを使用しています。

<p><ruby>私<rt>わたくし</rt></ruby>はその人を...</p>

サンプルデータ

青空文庫から直接スクレイピングしようとしたのですが、文字コードやタグの使い方が統一されていなかったり(HtmlAgilityPackのPCLではXPATHが使えなかったり)できれいに取得できなかったので、元のHTMLからルビと本文だけのHTMLに加工しました。作成したHTMLリソースフォルダ(AndroidのAssetフォルダ、iOSのResoucesそれぞれ)に置いて読み込む形になっています。

表示!!

目次から書名をタップするとこんな感じで表示されます。左右のタップでページ移動します。

f:id:masatoru:20161220184545p:plain

GitHub

というわけで(不十分なところが多々ありますが)今回の縦組みサンプルをGitHubに上げました(現在Android版のみ動作します。iOSでなぜか動かないので調査中です)。奇特な方々の優しいダメだしを頂ければ大変幸いです。

最後に

今年もXamarinおよびXamariの関係者の方々と楽しく時間を過ごせました。ありがとうございました。来年もより一層よろしくお願いします。よいお年を。

.Net Framework2.0から4.6.1に上げたけどSystem.BadImageFormatExceptionで動かない

昔のプログラムを.Net Framework2.0から4.6.1に上げたけどSystem.BadImageFormatExceptionとかで動作しない。

下記をチェックしたけどどれも該当しない。

例外のトラブルシューティング : System.BadImageFormatException

わかったことはメインのexeのapp.configに

<supportedRuntime version="v2.0.50727"/>>

とあったのでこれを下記に書き換えたら動いた。

<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5"/>

すっかりはまってしまった。

DbSetのAddをMockでTestする

書きなれないブログでタイトルからつまづいてますが。。。
EF6をServiceとMockを使っていい感じでテスト&モッキューできないかなと。
元ネタはなかじさんのブログ。

blog.nakajix.jp

ここの記事にあるEFのTestをMoqを使ってやる記事があります。

msdn.microsoft.com

この中の「Testing query scenarios」が、サンプルデータをServiceを通してGetAllBlogsで取得するとorder byされて返ってくるというTESTです。

[TestMethod] 
public void GetAllBlogs_orders_by_name() 
{ 
    var data = new List<Blog> 
    { 
        new Blog { Name = "BBB" }, 
        new Blog { Name = "ZZZ" }, 
        new Blog { Name = "AAA" }, 
    }.AsQueryable(); 

    var mockSet = new Mock<DbSet<Blog>>(); 
    mockSet.As<IQueryable<Blog>>().Setup(m => m.Provider).Returns(data.Provider); 
    mockSet.As<IQueryable<Blog>>().Setup(m => m.Expression).Returns(data.Expression); 
    mockSet.As<IQueryable<Blog>>().Setup(m => m.ElementType).Returns(data.ElementType); 
    mockSet.As<IQueryable<Blog>>().Setup(m => m.GetEnumerator()).Returns(0 => data.GetEnumerator()); 

    var mockContext = new Mock<BloggingContext>(); 
    mockContext.Setup(c => c.Blogs).Returns(mockSet.Object); 

    var service = new BlogService(mockContext.Object); 
    var blogs = service.GetAllBlogs(); 

    Assert.AreEqual(3, blogs.Count); 
    Assert.AreEqual("AAA", blogs[0].Name); 
    Assert.AreEqual("BBB", blogs[1].Name); 
    Assert.AreEqual("ZZZ", blogs[2].Name); 
} 

次にServiceでAddBlogすると追加されるTestをおこなうために下記を追加。
ここでなぜか(当然?)4件にならない、ということで本題に入ります。

service.AddBlog("CCC", "http://blogs.msdn.com/adonet");
var blogs = service.GetAllBlogs(); 
Assert.AreEqual(3, blogs.Count); //4件にならない

ServiceでAddBlogしたときに追加されるようにしたいわけですが、見つけたのが下記のAnswerの箇所。
要するに、DbSetでAddされる時のMockがないので追加されないとのこと。

stackoverflow.com

...Callbackがよくわからないのでちょっと勉強してから。

github.com

というわけで試行錯誤した結果一番最後の行ができたやつ。

//コピペのままではコンパイルされず
//mockSet.Setup(m => m.Add(It.IsAny<Blog>())).Callback(blog => data.Add(blog));

//IQueryrableにAddはない
//mockSet.Setup(m => m.Add(It.IsAny<Blog>())).Callback<Blog>(blog => data.Add(blog));   

//AddがないのでConcatで配列を作り直す→なぜかダメ
//mockSet.Setup(m => m.Add(It.IsAny<Blog>())).Callback((Blog blog) => data = data.Concat<Blog>(new[] { blog }))

//ListとIQueryableを上で使い分ける
mockSet.Setup(d => d.Add(It.IsAny<Blog>())).Callback<Blog>((s) => source.Add(s));

動いたソースを一応全部のっけておきます。

[Test]
public void GetAllBlogs_with_mock()
{
  var source = new List<Blog>
  {
    new Blog { Name = "BBB" },
    new Blog { Name = "ZZZ" },
    new Blog { Name = "AAA" },
  };
  var data = source.AsQueryable();
  
  var mockSet = new Mock<DbSet<Blog>>();
  mockSet.As<IQueryable<Blog>>().Setup(m => m.Provider).Returns(data.Provider);
  mockSet.As<IQueryable<Blog>>().Setup(m => m.Expression).Returns(data.Expression);
  mockSet.As<IQueryable<Blog>>().Setup(m => m.ElementType).Returns(data.ElementType);
  mockSet.As<IQueryable<Blog>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());

  //DbSetのAddをモックする
  //ListとIQueryableを上で使い分ける(これは仕方ない?ここをもうちょいきれいにしたい)
  mockSet.Setup(d => d.Add(It.IsAny<Blog>())).Callback<Blog>((s) => source.Add(s));
  
  var mockContext = new Mock<BloggingContext>();
  mockContext.Setup(c => c.Blogs).Returns(mockSet.Object);
  
  var service = new BlogService(mockContext.Object);
  var blogs = service.GetAllBlogs();
  
  Assert.AreEqual(3, blogs.Count,"追加前");
  Assert.AreEqual("AAA", blogs[0].Name);
  Assert.AreEqual("BBB", blogs[1].Name);
  Assert.AreEqual("ZZZ", blogs[2].Name);
  
  //もう一件追加する
  service.AddBlog("CCC", "http://blogs.msdn.com/adonet");
  blogs = service.GetAllBlogs();

  Assert.AreEqual(4, blogs.Count, "追加後");
  Assert.AreEqual("AAA", blogs[0].Name);
  Assert.AreEqual("BBB", blogs[1].Name);
  Assert.AreEqual("CCC", blogs[2].Name);
  Assert.AreEqual("ZZZ", blogs[3].Name);
}

で、下記に回答がありましたというオチです。

stackoverflow.com

追伸、 もっきゅーもっきゅーって居酒屋のメニューにしか聞こえないのは自分だけ?