こんばんは。このところXamarin.Formsのアプリ開発にはまっているジョニーです。
さて、アプリでQRコードを扱いたいと思って調べていて Zxing(Zebra Crossing:ゼブラ・クロッシングの略)をというライブラリが Xamarin.Forms に対応しているというのを見つけました。これを、Prism で扱えるようにしようとしてハマったものの、なんとか解決できたので誰かの参考になるかもしれないと思い記事を書いていおくことにします。実は数ヶ月後の自分のためかもしれない・・・
まずはサンプルの写経から
エクセルソフトの田淵さんという有名な方が記事を書かれていたので早速参考にさせていただきました。 qiita.com
素の Xamarin.Forms で扱う分には、上記の記事の通りやったらすぐに動くものができました。
やったこととしてはこのくらい。サンプルを動かすまでの所要時間は15分くらいでした。
- Nugetパッケージ「Zxing.Net.Mobile.Forms」の追加
- MainPage.xaml にボタンとイベント処理を追加
- MainPage.xaml.cs にイベントハンドラとして QRコードを読み取る処理を記述
- iOSプロジェクトの AppDelegate.cs に初期化コードを追加
- iOSプロジェクトの Info.plistにカメラを使うときの説明「NSCameraUsageDescription」を追加
Prism 対応の前に
ここでいう Prism 対応というのが何を指しているかをまず説明します。
まずは、もとのコード(前述のサンプル)です。
using ZXing.Net.Mobile.Forms; async void ScanButtonClicked(object sender, EventArgs s) { // スキャナページの設定 var scanPage = new ZXingScannerPage() { DefaultOverlayTopText = "バーコードを読み取ります", DefaultOverlayBottomText = "", }; // スキャナページを表示 await Navigation.PushAsync(scanPage); // データが取れると発火 scanPage.OnScanResult += (result) => { // スキャン停止 scanPage.IsScanning = false; // PopAsyncで元のページに戻り、結果をダイアログで表示 Device.BeginInvokeOnMainThread(async () => { await Navigation.PopAsync(); await DisplayAlert("スキャン完了", result.Text, "OK"); }); }; }
MainPage.xaml.cs に追加する ScanButtonClicked イベントハンドラの中で ZXingScannerPage というQRコードスキャナーのページを生成して、 OnScanResult というイベントのハンドラでスキャン完了時の処理を記述しています。そして、Device.BeginInvokeOnMainThread() 、await Navigation.PopAsync() 、DisplayAlert() というメソッドで画面遷移やアラートを表示しています。
Prism 対応
せっかく Prism を使うので、Prism らしく書けるようにしたいと思います。さて、どんな風に書くと Prism らしくなるのか、僕なりの解釈を説明したいと思います。
- 画面遷移の処理は、ページのコードビハインド(.xaml.cs)ではなく、ViewModel に書く
- Device 、Navigation は使わず、 IDeviceService 、INavigationService 、IPageDialogService を使う
- イベントは、EventToCommandBehavior を介して、ViewModel に定義した Command で処理する
このように書けるようにするために、必要になったことを説明していきます。
修正した内容
今回は、Prism のブランクプロジェクトに以下のファイルを追加しました。
- ScannerPage.xaml
- ScannerPage.xaml.cs
- ScannerPageViewModel.cs
- ScanFinishedEventArgs.cs
MainPage.xaml にはボタンを設置して、 ScannerPage へ遷移するだけとなっています。
MainPage.xaml
<Button Text="Scan" Command="{Binding NavigateCommand}" CommandParameter="ScannerPage"/>
MainPageViewModel.cs
public Command<string> NavigateCommand => new Command<string>(name => { _navigationService.NavigateAsync(name); });
App.xaml.cs
protected override void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.RegisterForNavigation<NavigationPage>(); containerRegistry.RegisterForNavigation<MainPage>(); containerRegistry.RegisterForNavigation<ScannerPage>(); }
これから説明する ScannerPage もコンテナレジストリに登録して利用します。
ScannerPage.xaml
<?xml version="1.0" encoding="UTF-8"?> <zxing:ZXingScannerPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:zxing="clr-namespace:ZXing.Net.Mobile.Forms;assembly=ZXing.Net.Mobile.Forms" xmlns:behaviors="clr-namespace:Prism.Behaviors;assembly=Prism.Forms" x:Class="PrismQRCodeSample.Views.ScannerPage"> <zxing:ZXingScannerPage.Behaviors> <behaviors:EventToCommandBehavior EventName="ScanFinished" Command="{Binding ScanResultCommand}" EventArgsParameterPath="Result"/> </zxing:ZXingScannerPage.Behaviors> </zxing:ZXingScannerPage>
ZXingScannerPage を XAML で定義するために、xmlns:で、zxing 名前空間を宣言します。これで、 zxing:ZXingScannerPage タグを使えるようになりました。余談ですが、名前空間の宣言が子要素だけでなく自身にも有効であることを知って驚きました。
次に、 Behaviors で EventToCommandBehavior を登録します。
ただ、ZXingScannerPage は、EventToCommandBehaviorに適合したイベントハンドラをもっていません。そこで、このビヘイビアの例によく使われている ListView のソースを参考に、イベントハンドラを作ってみようと思います。
ListView.cs https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Core/ListView.cs
ZXingScannerPage.cs github.com
ハンドラの部分を抜粋すると、このような違いがあります。
ListView.csの宣言
public event EventHandler<SelectedItemChangedEventArgs> ItemSelected;
ItemSelected は System.EventHandler デリゲート型であり、EventArgs サブクラスの SelectedItemChangedEventArgs を取ることがわかります。
SelectedItemChangedEventArgs.cs
using System; namespace Xamarin.Forms { public class SelectedItemChangedEventArgs : EventArgs { public SelectedItemChangedEventArgs(object selectedItem) { SelectedItem = selectedItem; } public object SelectedItem { get; private set; } } }
ListView.cs でハンドラが呼び出される部分では、以下のように使用されています。
static void OnSelectedItemChanged(BindableObject bindable, object oldValue, object newValue) => ((ListView)bindable).ItemSelected?.Invoke(bindable, new SelectedItemChangedEventArgs(newValue));
一方で、ZXingScannerPageはかなり違います。
ZXingScannerPage.cs
public delegate void ScanResultDelegate (ZXing.Result result); public event ScanResultDelegate OnScanResult;
呼び出しじはこんな感じ。
zxing.OnScanResult += (result) => {
this.OnScanResult?.Invoke( result );
};
そこで、 コードビハインドで元のイベントをラップして、EventHandler型のイベントを定義してみることにします。
ScannerPage.xaml.cs
public partial class ScannerPage : ZXingScannerPage { public ScannerPage() { InitializeComponent(); DefaultOverlayTopText = "バーコードを読み取ります"; DefaultOverlayBottomText = ""; this.OnScanResult += (result) => { this.ScanFinished?.Invoke(this, new ScanFinishedEventArgs(result)); this.IsScanning = false; }; } public event EventHandler<ScanFinishedEventArgs> ScanFinished; }
OnScanResult のハンドラで、新しく作った ScanFinished イベントを invoke しています。このとき使用する ScanFinishedEventArgs も作りました。
ScanFinishedEventArgs.cs
public class ScanFinishedEventArgs : EventArgs { public ScanFinishedEventArgs(ZXing.Result result) { Result = result; } public ZXing.Result Result { get; private set; } }
ScannerPage.xaml
<behaviors:EventToCommandBehavior EventName="ScanFinished" Command="{Binding ScanResultCommand}" EventArgsParameterPath="Result"/> </zxing:ZXingScannerPage.Behaviors>
EventArgsParameterPath には、EventHandler で指定した型のオブジェクト(今回は ScanFinishedEventArgs)のプロパティ名(今回は Result)を指定します。これで読み取り完了のイベントが、Commandに変換され、Resultを引数にとって呼び出せるようになりました。
では、最後に ScannerPageViewModel に定義した ScanResultCommand イベントハンドラを見てみましょう。
ScannerPageViewModel.cs
public Command<ZXing.Result> ScanResultCommand => new Command<ZXing.Result>(result => { _deviceService.BeginInvokeOnMainThread(() => { _navigationService.GoBackAsync(); _pageDialogService.DisplayAlertAsync("スキャン完了", result.Text, "OK"); }); });
ViewModelにはコンストラクタで IDeviceService、INavigationService、IPageDialogService がインジェクションされているので、このように書き換えることができました。やった〜!
というわけで、まずはここまでおつかれさまでした!!
ちなみにこちらが、読み込みテストに利用したQRコードですw
iOS の実機デバッグでハマったこと
これは、Zxing ではなく、Prism の話ですが、 iPhoneを繋いでアプリを配置しようとした時にエラーが出てハマったのでこれも書いておきます。
「iTunes Package Archive(IPA)をビルドする」というオプションをオンにしないと、iPhone 実機を選んでビルドするとエラーが出ます。シミュレータでやってるときはでなかったので少し焦りました・・・
エラーはこんな感じ、なにやらビルド途中でパスが入っているはずの変数が空で怒られているようです。
エラーの箇所に飛んでみると、IpaPackagePath という変数がからであることが問題のようです。Prism でない素の Xamarin.Forms ではこの問題はおきないことを確認しつつ、IPAを作るオプションを探したところ上記の「iTunes Package Archive(IPA)をビルドする」にたどり着きました。
このトラブルだけで、解決に1時間近くかかってしまった・・・まぁ、これも勉強ですね。
まとめ
Zxing を Prism っぽく使うためには、イベントハンドラを適合してあげる必要がある!
今回の事例を通して、イベント、ビヘイビア、XAMLとコードビハインドなど様々な点で Xamarin.Forms および Prism の理解を深めることができました。やっぱり、やりたいことがあって悪戦苦闘しながらできるようになっていく過程で学びや成長があるんだなと思いました。
取り掛かってから、動くようになるまでなんだかんだで4時間くらいかかってしまいました。
Xamarin.Forms については、詳しい方々が非常に有益な情報を書いてくださっているので、僕は自分が必要なものをいい感じで使おうとしてハマったこととか出てきたらそれを書いてみようかなと思います。
この記事が、Xamarin.Forms や Prism を使っている誰かの参考になれば幸いです。 最後までお読みいただき、ありがとうございました。
参考URL
Xamarin.Forms のAPIドキュメント docs.microsoft.com
Xamarin.Forms の GitHub リポジトリ(ソースコード) github.com
Prism の GitHub リポジトリ(ソースコード) github.com