dnackのブログ

コード書く仕事の間にコード書いてる知命目前のおっさんです。

Raspberry Pi:デフォルトユーザの名前を変える&VNCサーバの設定

最近お仕事のほうのデバッグやら調査やらで力尽きて全然触れてないんだよな。。 まとまった時間が取れなくてもできそうなところで、 このくらいはやっておこうということで。

デフォルトユーザの名前を変える

前回sshからログインしたときのメッセージ

SSH is enabled and the default password for the 'pi' user has not been changed.
This is a security risk - please login as the 'pi' user and type 'passwd' to set a new password.

で、デフォルトのユーザ名とパスワード変えてないので、パスワード変えろと言われているのだが、どう考えてもユーザ名のpiもかえるべきで、とりあえずユーザ名・パスワードを変えるのを今回の目標にする。

まあそんなのググったら出てくるよね

jyn.jp

・ちょっと引っかかった点

テンポラリで作ったユーザから sudo usermod -l newpi pi しようとしたらプロセスで使ってるといわれて消せなかった。 →sudo raspi-configでbootの方法をCUIのオートログインなしに変更(GUIの起動でpiユーザが使われてそう)するとOK(もちろん再起動は必要)。

というわけでこのエントリはおしまい。

あまりにあっさりだったのでVNCの接続にもトライしてみる。

VNC接続

まず、Windows側にVNCクライアントのVNC Viewerをインストール

www.realvnc.com

ラズパイ側は、

$sudo raspi-config 

で、InterfaceOptionからVNCを有効に。 (バージョンによってメニューの構成は変わるかもしれない。)

これで設定は終わったと思うので、VNC Viewerから接続する。 まず、file→new connectionから新しい接続を作成

f:id:dnack:20210912003831p:plain

(VNC Serverにアドレス化ホスト名、NameにVNCViewerに表示する名前を入れて後はそのままでOK)

f:id:dnack:20210912004130p:plain

これで、上のようにVNC Viewerのホーム画面にサーバのアイコンができるので、 ダブルクリックしてユーザ名・パスワードを入れると

f:id:dnack:20210912004547p:plain

ふええ なんで?

ryoichi0102.hatenablog.com

まあ、たいていのことはだれかが経験しているんだよな。 少しのことにも先達はあらまほしきことなり。

nanoってなんじゃろと思って調べたらrapsberry pi osに入ってるテキストエディタなのね。

マウスが効かないだけでnotepadのような普通のエディタのように使える。

この辺もなんというかちゃんと門戸広げてあるんだよな。

「エディタ?はぁ?Linuxなんだからviはいってるだろ?勝手に使えよ」だと初心者はついてこれないもんな。

じゃあとりあえずviインストールしとこうかな(なんでだよ)。

脱線はともかく、先達の教えに従って/boot/config.txtのhdmi_force_hotplug=1を有効にして、再起動。 そしてVNC Serverを立ち上げる。 f:id:dnack:20210912010019p:plain

VNC Clientの接続の設定で、アドレスを、IPアドレス:5901の変えて接続すると (今作ったVNCServerのディスプレイ番号が1なので、ポート番号は5900+1で5901) f:id:dnack:20210912010421p:plain

お、つながったようだ。 f:id:dnack:20210912010901p:plain

めでたしめでたし。 なんかIPアドレス:1の設定でもつながったな。IPアドレスだけだとはじかれた。

解像度設定を変えたい

触ってるうちに画面が狭いなと思ってVNCの画面からScreen Configration を変えようとするが、メニューが出てこない。 まあこういうときは悩む前にググるのがいいんだ。俺は学んだんだ。

www.indoorcorgielec.com

少しのことにもいやそれはさっき言ったな。

これに従ってResolutionの設定をDMT 82 1920x1080に変えてみるがうまくいかない。 ブログではブートがデスクトップモードになっているようなので、それでやってみるか。

f:id:dnack:20210912013609p:plain

解像度の変更に成功。 いやさ、フルHDXGAの間ないの?フルHDだとディスプレイ占有するし、XGAは狭すぎるんだよな。。

いずれにせよ、ブートモードはCLIのほうがいいので戻すか。 そうすると上の方法での解像度の変更はできないので、vncの起動時に解像度変えて起動するしかない。 それならコマンドパラメータなり設定ファイルでできるんじゃね? もろもろしらべると、とりあえず、これで行けそう。

dnack@raspberrypi:~ $ vncserver -geometry 1440x900 

起動時にサイズ指定することはできたので、とりあえずこれでいい。 起動中にサイズを変えようとするのはちょっとややこしそうだし、当面GUI使う予定ないからな(それを言ってはおしまいなのでは…)。

f:id:dnack:20210912022456p:plain

というわけでVNC接続記念に弊社サイトにアクセスしといた。 重いなうちのサイト。。

今日は本当にここまで

Prism/XamarinでAndroidアプリの作成(6):Permissionが欲しい

Permission

前回のBLEのデバイススキャンを試したが、位置情報のパーミションをアプリ設定からつけてあげる必要があった。 これはとても面倒なので、BLE使う前に権限を確認して、なければユーザに権限を付けてもらうようにしてみた。 ここは今回の主題ではないので、あっさりと行きたい。というわけで出来上がったソースコードはこちら。 (※3分間クッキングみたいになってきたな…)

他は全然変わらないので、ButtonAClickedAsync( )の中だけですが

        private async void ButtonAClickedAsync( )
        {
            TextMessage.Value = "XXXX";
            var ble = CrossBluetoothLE.Current;
            var adapter = CrossBluetoothLE.Current.Adapter;
            var deviceList = new List<IDevice>();
            
            //State
            Debug.WriteLine("★★★★BLE Stete:"+ble.State);

            //Permission
            // 権限の確認
            var permissionStatus = await Permissions.CheckStatusAsync<Permissions.LocationWhenInUse>();
            Debug.WriteLine("★★★★PermissionStatus of location service " + permissionStatus);

            // 権限がなければ権限ダイアログを出してユーザに付与してもらう。
            if (permissionStatus !=  PermissionStatus.Granted )
            {
                permissionStatus = await Permissions.RequestAsync<Permissions.LocationWhenInUse>();
            }

            // 権限がもらえなかったら何もしないで終わる。
            if( permissionStatus != PermissionStatus.Granted)
            {
                return;
            }

            //scan device 
            adapter.DeviceDiscovered += (s, a) =>
            {
                deviceList.Add(a.Device);
                Debug.WriteLine("★★★★Found:"+a.Device.Id.ToString());
            };

            adapter.ScanTimeout = 10000;
            
            await adapter.StartScanningForDevicesAsync();

            foreach(var device in deviceList)
            {
                Debug.WriteLine("★★★★:ID" + device.Id.ToString() +":Name:" + device.Name +":RSSI:" + device.Rssi.ToString() +":State:" + device.State.ToString());
            }
        }

このメソッド以外ではPermissions クラスを使うためにusing Xamarin.Essentials;のusing句を追加している。 //Permissionから//scan deviceの間が追加したコードになる。 権限があるか確認に行って(CheckStatusAsync())、権限がなかったらユーザに権限ダイアログを出して(RequestAsync()) ユーザに権限付けてもらえなかったら処理を終わって(return)、 そうでなければ前回書いたデバイススキャンの処理へ行くというコードになっている。 一度アプリ設定から権限を却下して

f:id:dnack:20210908212136p:plain:w120

起動して、ボタンを押してみると、こんなダイアログが出る。

f:id:dnack:20210908212210p:plain:w120

よかったよかった。

これで終われば話は平和なんだが、いや終わってもいいのだが、気になる話がある。

Android11での仕様変更

developer.android.com

曰く、「Android 11 以降では、デバイスにインストールされたアプリの全期間に、同じ権限に対してユーザーが何度も [許可しない] をタップした場合、アプリがその権限を再度リクエストしても、ユーザーにシステム権限ダイアログが表示されることはありません。」 何事かというと、上のキャプチャ画面のような権限ダイアログで「許可しない」を何度も選ぶと今度から権限ダイアログが表示されなくなるらしい。 もちろん、その場合でもアプリ設定から権限付ければ問題なく使えるわけだが、ユーザにそれを促すような仕組みが求められる。 特にBLEの場合は、使いたいのは通信機能なのにいきなり位置情報の権限を求められてよくわからないユーザは「いやダメだろ」って却下しちゃいそうだしなぁ。。

というわけで、上のリンクで「権限の拒否を処理する」もご覧あれと書いていたので読んでみる。

developer.android.com

権限がなかった場合、アクセス許可が必要な理由を説明する必要があるかを確認して、 (これが要するにそのまま進めると権限ダイアログが出ないか出るかということなのだろうか) その必要があれば何か表示して教えてあげなさいっていうことのようですね。 Xamarinのドキュメントの該当箇所はここか。

docs.microsoft.com

これ、”一般的な使用法”のところにほぼそのまま使えるコード書いてるからこれを拝借してくるか。 Xamarinのドキュメントにしてもdeveloper.androidのドキュメントにしても、 ちゃんと読めばちゃんと書いてあるんだよなぁ。ちゃんと読めば()

というわけでこれをパックて書いたコードがこちら。 変更したのはButtonAClickedAsync( ) と追加したCheckAndRequestLocationPermission()だけなのでその部分だけ。

        private async void ButtonAClickedAsync( )
        {
            TextMessage.Value = "XXXX";
            var ble = CrossBluetoothLE.Current;
            var adapter = CrossBluetoothLE.Current.Adapter;
            var deviceList = new List<IDevice>();
            
            //State
            Debug.WriteLine("★★★★BLE Stete:"+ble.State);

            //Permission
            PermissionStatus permission= await CheckAndRequestLocationPermission();
            if ( permission != PermissionStatus.Granted)
            {
                return;
            }

            //scan device 
            adapter.DeviceDiscovered += (s, a) =>
            {
                deviceList.Add(a.Device);
                Debug.WriteLine("★★★★Found:"+a.Device.Id.ToString());
            };

            adapter.ScanTimeout = 10000;
            
            await adapter.StartScanningForDevicesAsync();

            foreach(var device in deviceList)
            {
                Debug.WriteLine("★★★★:ID" + device.Id.ToString() +":Name:" + device.Name +":RSSI:" + device.Rssi.ToString() +":State:" + device.State.ToString());
            }
        }

        private async Task<PermissionStatus> CheckAndRequestLocationPermission()
        {
            var status = await Permissions.CheckStatusAsync<Permissions.LocationWhenInUse>();

            if (status == PermissionStatus.Granted)
                return status;

            if (status == PermissionStatus.Denied && DeviceInfo.Platform == DevicePlatform.iOS)
            {
                // Prompt the user to turn on in settings
                // On iOS once a permission has been denied it may not be requested again from the application
                return status;
            }

            if (Permissions.ShouldShowRationale<Permissions.LocationWhenInUse>())
            {
                // Prompt the user with additional information as to why the permission is needed
                await _dialogService.DisplayAlertAsync("Alert",
                    "For BLE communication, permission to location is required.", "OK");
            }

            status = await Permissions.RequestAsync<Permissions.LocationWhenInUse>();

            return status;
        }

正直なところどういう条件で勝手にアプリのパーミションを消すのかとかどういう条件でShouldShowRationaleがtrueになるのかとかがわかっていないので、ピントは来ないが、どうやらそれなりにちゃんと動いてそう。 デバイススキャンまでということではこんなものかな。 開始前にBLE.StateとかViewModelに書いてないでModelにもってけとか結果をデバッグじゃなくてUIに出せとかいろいろあるけど、今回はまずはこんなところで。

Prism/XamarinでAndroidアプリの作成(5):BLE Pluginを使ってみる。

前回まではPrismをつかったMVVM的なコードの書き方を試してきたんだけど飽きてきたので、今回は趣向を変えて、BLEを動かしてみたい。 Prismのアーキテクチャ回りも、処理の部分をModelで書いたりとかまだやることはたくさんあるんだけどそっちはとりあえずお休み。 今回のBLEの処理も、本当はModelに独立したクラスを作るべきなのだろうけど、まずはごちゃごちゃ考えずに動かしてみるということでその辺りは気にせずViewModelのところにべたべたと書いてみる。(のでその辺りのお作法上いまいちなコードが出てくるけれど気にしない。)

Plugin.BLEのインストールと準備

NugetからPlugin.BLEを探して入れるだけ。 GithubにあるREADME.mdがわりと親切なのが助かる。

github.com

MvvmCross用にはPluginが用意されているが、残念ながらPrism用にはない。 日本語のTipsはPrismが圧倒的に多いんだけど、海外ではMvvmCross優勢とも聞くからなぁ。 そういえばVisualStudio 2019で新しく入ったmvvm toolkitが入ったという話は聞いたがそっちの話はあまり聞かないな。

さて、とりあえずUsageに従ってコード書いていく。 まずは、以下の4つのパーミションを加えろとのこと。 ACCESS_COARSE_LOCATION ACCESS_FINE_LOCATION BLUETOOTH BLUETOOTH_ADMIN プロパティのAndroid マニフェストのタブからこの4つのパーミションを追加。 f:id:dnack:20210906224431p:plain 多分直接AndroidManifest.xmlに直接足しても大丈夫なはず。

今回は手っ取り早く動かしてみるということで、 処理は前回のアプリのViewモデルのボタンのハンドラにコードべた書きし、 結果の表示も画面に出さずに、デバッグ出力というやっつけ仕事。

using Plugin.BLE;
using Plugin.BLE.Abstractions.Contracts;
using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using Reactive.Bindings;
using SamplePrismApp.Views;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;

namespace SamplePrismApp.ViewModels
{
    public class MainPageViewModel : ViewModelBase
    {
        public ReactiveProperty<string> TextMessage { get; set; }

        public DelegateCommand ButtonACommand { get; set; }
        public DelegateCommand ButtonNextPageCommand { get; set; }

        private INavigationService _navigateionService;

        public MainPageViewModel(INavigationService navigationService)
            : base(navigationService)
        {
            Title = "Main Page";
            TextMessage = new ReactiveProperty<string>( "0000" );
            ButtonACommand = new DelegateCommand( ButtonAClickedAsync );
            ButtonNextPageCommand = new DelegateCommand( NavigateSecondPageAsync );
            _navigateionService = navigationService;
        }

        private async void ButtonAClickedAsync()
        {
            TextMessage.Value = "XXXX";
            var ble = CrossBluetoothLE.Current;
            var adapter = CrossBluetoothLE.Current.Adapter;
            var deviceList = new List<IDevice>();
            
            //State
            Debug.WriteLine("★★★★BLE Stete:"+ble.State);

            //scan device 
            adapter.DeviceDiscovered += (s, a) =>
            {
                deviceList.Add(a.Device);
                Debug.WriteLine("★★★★Found:"+a.Device.Id.ToString());
            };

            adapter.ScanTimeout = 10000;
            
            await adapter.StartScanningForDevicesAsync();

            foreach(var device in deviceList)
            {
                Debug.WriteLine("★★★★:ID" + device.Id.ToString() +":Name:" + device.Name +":RSSI:" + device.Rssi.ToString() +":State:" + device.State.ToString());
            }
        }

        private async void NavigateSecondPageAsync()
        {
            _ = await _navigateionService.NavigateAsync(nameof(SecondPage));
        }
    }
}

前回のコードと比べてもらえば一目瞭然だと思うが、 ButtonAClickedにデバイススキャンして結果をデバッグ出力するコードを足している。 メソッドが非同期になっているのは、前回の画面遷移の時とおなじで、 前回は画面遷移のメソッドが非同期だったが、 今回はデバイススキャンのメソッドが非同期なのでこれを待つ必要がある。 そして、待つ必要があるからasyncにする必要がある、という理由でこうなっている。 using句は、 CrossBluetoothLEのためにPlugin.BLE、IDeviceのためにPlugin.BLE.Abstractions.Contracts、 Debug.WriteLineのために System.Diagnosticsのをそれぞれ追加。

実行すると、 f:id:dnack:20210907132612p:plain

おや? 特にエラーは出てないけど、DeviceDiscovered が一度も呼ばれていないようなので、 BLEのデバイスはここらへんにはないらしいな。 いやいやいや、、そんなことはありえないので何かがおかしい。 (この間約30分) あ。もしかしてあれなのでは?

f:id:dnack:20210907133127p:plain

ほらねーー。 プロジェクトで位置情報の権限は付けたけど(ACCESS_COARSE_LOCATION,ACCESS_FINE_LOCATION)、 端末のほうでアプリに位置情報の権限を渡す設定も入れないといけないんだよな。 それをしてないので、BLEにアクセスできなかったわけ。 このケース何のエラーコードも出ないので本当にタチが悪いんだよな。 アプリのBLUETOOTHの権限がなかったりすると普通にエラーになるんだけどね。

f:id:dnack:20210907133317p:plain

ということで、こうやってアプリに位置情報の権限を与えればOK。なはず。 気を取り直して実行すると。

f:id:dnack:20210907133404p:plain

おー、なんかいっぱい見つかった。

しかしこれ、実行時に権限なかったら取りに行くコード入れたほうがいいね。 それは宿題ということで今回は気持ちよく動いたところでここまで。

Raspberry Pi:Raspberry Piが街にやってくる

買っちゃいました。

f:id:dnack:20210905140845j:plain

というわけで我が家にRaspberry PIがやってきました。 まだほぼつないで起動しただけですが、現時点での買ったものと感想。

本体

jp.rs-online.com 大人なので8GBモデルです。 OSインストールするところとかで変に躓きたくないのでキット買いました。昔ならその辺で躓くのも楽しんでた気もするが。。これが老いか。。

ディスプレイ

https://www.amazon.co.jp/gp/product/B08V54V4NNwww.amazon.co.jp EVICIV のタッチパネル付き7inch 1024x600

適当に買ったんだけど、自分個人としての今回のケースの選択としては失敗感はある。 スペック通り動いているという意味で製品そのものについては何の問題もないことは念のため断っておく。

まず解像度。老眼なので狭い画面に高い解像度はいらないと思ってこの解像度で妥協したけど、やっぱりちょっとだけ狭い。 1024の数字でXGAのイメージがあって、それならありだなと思っちゃったと思うんだけど、768と600の差は大きい。画面の解像度は縦のほうが大事なんだよな。。

というわけで、ディスプレイはどうせ買うなら、ほかの用途にもつぶしのきく13-15インチくらいのモバイルディスプレイでも買ったほうがよかったかもしれない。linux使うのにタッチパネル機能も別に必須じゃない。なんならVCN設定するまでのつなぎと割り切るなら、メインのディスプレイにつなぐ運用でディスプレイ新しく買わないという選択肢もあった。

うちの環境ではディスプレイのディスプレイのHDMIポートがもうPCとプレステで埋まってるので差し替えが面倒だけど。(ン年起動してないプレステを抜けばいいだけだという話もある。)

使い続けることを考えるなら、左記に書いたようにほかの用途にも使えるそこそこ大きくてそこそこの解像度のディスプレイ化、せめてこの辺の解像度のものでも縦にも使えるほうがよかったかな。(1024x600よりは600x1024のほうが使いやすそう)。

というわけではやくSSHVNCを設定してこの狭い画面からおさらばしよう。

マウスとキーボード

これもつなぎなら適当に余っているものを使ってもよかったんだけど、フルサイズのキーボードが机に2セットあるのはいかにもうるさそうだし、安くて小さいのを1セットくらい持ってても困らないかなと思って買いました。 キーボードは普段メカニカルに甘やかされてるので(Logicool K840はいいぞ)違和感があるし、マウスも小さすぎてちょっと辛いけどどちらも慣れの問題だろうし、長時間連続で使うものでもないからいいだろう。

キーボード:エレコム ELECOM TK-FCM089SBK/ヨドバシ.com

マウス:Digio デジオ MUS-UKT166R/ヨドバシ.com

つなげちゃいました。

f:id:dnack:20210905144843j:plain

ディスプレイとの接続は付属のフラットケーブルでつないでいる。microHDMI-HDMIのケーブルも同梱されていたが長くて邪魔なのでこっちでつないだ。 ディスプレイは裏にラズパイをマウントするように作ってあるんだが、ディスプレイ裏にマウントして電源を共有するとラズパイGPIOの5V電源ピンをディスプレイが占有するのでファンがつけられないという問題があるな。 そんな些末(そうか?)な問題は横におくとしても、そもそも基板というのは堂々と机にあるのがよいのであって、ディスプレイ裏に隠しては味もそっけもないではないか(個人の感想です)。 上部左は付属のACアダプタに接続。MicroUSBなのは勘弁してほしい。上部右はタッチセンサ用の接続でこちらもMicroUSB。まあMicro-CのUSBケーブルは一本付属していたのでもう一本探してきて無事接続。

キーボードとマウスはラズパイのUSBポートにつなげるだけ。 以上で有線でのプリフェラルとの接続は終了。

ネットワークはとりあえずWi-Fiで接続。有線接続するつもりでケーブルも買ったのだけど、地味にルータから生えてるケーブルが邪魔になりそうで、必要になるまでは無線で行くつもり。 OSのインストールはキットに付属のmicroSDを本体に挿して電源入れて何度かキーたたけば簡単に完了。キットで買ってよかったよー。

つなげちゃいました。その2

小さい画面とメンブレンキーボードで作業しつづけるのも疲れるので、とっととSSHの設定しちゃいましょう。

最初にラズパイのIPアドレスは固定しておくほうがよさそう。 正統派っぽいやり方はラズパイのアドレスをDHCPサーバの払い出しアドレスの外の固定アドレス持っちゃう方法だとおもうけど、面倒なのでルータのDHCPサーバから固定アドレスを払い出す設定で対応。うちのルータの設定画面だとこんな感じ。久しぶりにルータの設定なんか触ったので自信はないけどつながってるからOK。 f:id:dnack:20210905160239p:plain

一応、ラズパイのローカルコンソールからifconfigコマンド、ちゃんと設定した192.168.0.201が払い出されていることを確認した。 さていよいよSSH接続。 f:id:dnack:20210905160746p:plain

よし、ちゃんとつながったな。ってかおっちゃんパスワード間違いすぎやからな。。。

つながったんだからipアドレスがちゃんと変わってるのは当たり前だしローカルでも確認したけど、一応確認しとこうね。 f:id:dnack:20210905160846p:plain

やってることが電子工作というより完全にサーバの世話なんですが、まあ何事も準備が必要なので。。

もうしばらく準備は続くが、SSH疎通したのでリモートから入ってやりたい放題できるので、もうローカルコンソール用のキーボードもマウスもディスプレイも必要ない。

というわけで、こうだ。 f:id:dnack:20210905162322j:plain

おーすっきり。

ちゃんと起動して、sshでログインできることを確認。(今度はパスワード間違いませんでした。) f:id:dnack:20210905162412p:plain

普段使い用のユーザの追加とsambaとVNCの設定とくらいはやっておきたいけど、とりあえず今日のところはここまで。

え?Prism?そっちもやるからちょっと待っててください。。

ところで

そういえば、ラズパイの電源って、3A以上必要なので、みんなのうちのも転がってるかもしれない、もちろんうちにも一山いくらで売れそうなくらい転がっている普通のUSBの電源アダプタ(多分だいたい2.4A)って使えないんだよなぁ。 うちではこんなのを導入済みなんだけど(だからUSBアダプタが売るほど余ってるわけだけど)、これも2.4Aしか吐かないので足りない。 f:id:dnack:20210905184504j:plain

というわけで同梱されていたACアダプタを使うしかないんだけどこれがまたタップで横のアダプタとけんかしそうな形状をしている。 f:id:dnack:20210905184701j:plain

なのでこんなこともあろうかと用意していたこいつが役に立ちましたとさ。 f:id:dnack:20210905184745j:plain

どうでもいいけど、何かの役に立つだろうと買って以来ずっと埃かぶってたものが役に立って地味にうれしかったので報告。

Prism/XamarinでAndroidアプリの作成(4):画面の遷移は絶えずしてしかも元の画面にあらず

今回は画面遷移をやってみよう。

ページの追加

ということで、遷移するからには遷移先の画面を追加しないといけない。これはウィザードで便利にできる。
まず、ソリューションエクスプローラからViewフォルダを選んで右クリックメニューの追加→新しい項目からPrism ContentPageを選ぶ。名前入力のテキストボックスがえらく下のほうに出てきて見落としそうになるが、わすれないで適当な名前を付ける。
(名前を付け忘れても後で変更はできるけど変更箇所が多くて面倒くさい。)
f:id:dnack:20210903003151p:plain

ちゃんとViewのフォルダを選んで追加すると一緒にViewModelのファイルもできて、
f:id:dnack:20210903003440p:plain
App.xaml.csのコンテナの登録も追加してくれる。
f:id:dnack:20210903003545p:plain

なにこれむっちゃ便利じゃん。(逆に言うと、ウィザード使わなかったり、名前入れ間違えたりするとこれ全部手でやんなきゃいけないわけで、さっき言った面倒くさいっていったのはこれね。)

画面遷移の実装

画面作っただけでは何も変わらないので、画面遷移の実装を書く。もともとのMainPageに遷移のためのボタンを追加。遷移先のページは、Titleだけ書いておけば遷移したことがわかるだろう。
ボタンの追加は、前回やったのと同じ。

MainPage.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"
             x:Class="SamplePrismApp.Views.MainPage"
             Title="{Binding Title}">

    <StackLayout HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand">
        <Label Text="Welcome to Xamarin Forms and Prism!" />
        <Label Text="{Binding TextMessage.Value} "/>
        <Button Text="ButtonA" Command="{Binding ButtonACommand}"/>
        <Button Text="ButtonNextPage" Command="{Binding ButtonNextPageCommand}"/>
    </StackLayout>

</ContentPage>

ButtonNextPageを追加している。ButtonNextPageのコマンドは例によってViewModelに追加する。

MainPageViewModel.cs

using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using Reactive.Bindings;
using SamplePrismApp.Views;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace SamplePrismApp.ViewModels
{
    public class MainPageViewModel : ViewModelBase
    {
        public ReactiveProperty<string> TextMessage { get; set; }

        public DelegateCommand ButtonACommand { get; set; }
        public DelegateCommand ButtonNextPageCommand { get; set; }

        private INavigationService _navigateionService;

        public MainPageViewModel(INavigationService navigationService)
            : base(navigationService)
        {
            Title = "Main Page";
            TextMessage = new ReactiveProperty<string>( "0000" );
            ButtonACommand = new DelegateCommand( ButtonAClicked );
            ButtonNextPageCommand = new DelegateCommand( NavigateSecondPageAsync );
            _navigateionService = navigationService;
        }

        private void ButtonAClicked()
        {
            TextMessage.Value = "XXXX";
        }

        private async void NavigateSecondPageAsync()
        {
            _ = await _navigateionService.NavigateAsync(nameof(SecondPage));
        }
    }
}

量的には大した変更はない。
まず、ButtonNextP上げCommandを追加して、コンストラクタでNavigateSecondPageAsync()を登録している。
そのNavigateSecondPageAsync() は中身が次のページに遷移するための処理たった一行だが、それが今回のキモ。

NavigateAsyncというのが画面遷移のメソッド。引数はいくつかのオーバーロードがあるが、一番簡単なのはここに挙げた移動先ページの名前だけを書くもの。これで新たに追加したSecondPageへの遷移ができる。
以上終了!
としたいところだがもうちょっとだけ御託を並べさせてほしい。

NavigateAsyncの引数は”SecondPage"とリテラルで書く流儀も見かけるが、リテラルの書き間違いはコンパイルを通ってしまって見つけづらいバグになるので(それで泣いたことが何度かあるので)、ここではそれを避けてnameofを使っている。ただ、この記法のためにusing句でSamplePrismApp.Viewsを追加しなければいけないのはViewとViewModelの分離の観点からどうなのかという点はとても悩ましいところ。私はメリットのほうが大きいとみるのでこの書き方を採用している。

awaitは処理が終わるまでロックせずに待つための演算子。このawaitを含むためこのメソッドが非同期(async)になり、またこのメソッド名の末尾のAsyncは非同期のメソッド名の末尾にはこれをつけましょうという慣例によってついているもの。要するに非同期処理の仕組みなのだが、非同期処理は今回の処理の本筋ではないし、話し出すと深いし、その気になればブログやなんかで丁寧にわかりやすく説明しているところはいっぱい見つかるので、例によってここでは説明しない。

さて説明を最後にしてしまったがこの一行の本当のキモがここから。
_navigateionServiceとは何ぞや。たどってみると、コンストラクタの引数として渡されたものをprivateフィールドに保存したもので、出所はコンストラクタの引数、つまりオブジェクト生成時に渡されたものである。
このオブジェクト(MainPageViewModel)で使うであろうオブジェクト(navigationService)をオブジェクト生成時に外部から渡すというのは、Dependency Injection(依存性の注入/DI)といわれている構造によるもの。
MVVMはView/ViewModel/Modelの間を疎結合にするのが大事という話を前にしたが(してないかもしれない)、これも、モジュールの間を疎結合にするための仕組みの一つである。

以下蛇足

ちなみにDIもこれまた突き詰めればそれなりにややこしい概念ではあるのだが、理解をことさら難解にしているのは「依存性の注入」という日本語訳だと思う。
英語のWikipediaを見ると冒頭の説明に”dependency injection is a technique in which an object receives other objects that it depends on, called dependencies. "の記載。

en.wikipedia.org

dependencyというのは、”そのオブジェクトが依存する別のオブジェクト”の意味で、依存性と訳してしまったのはよくある技術用語の誤訳っぽい。依存性を注入するのではなく(そのオブジェクトが依存する、すなわちそのオブジェクトで使う)オブジェクトを注入するわけです。
オブジェクト作成時に、作ろうとしているオブジェクトが使うオブジェクトを渡しますよ という話なら、それ自体はそんなに難しい話ではないだろう。(それを実現するための仕組みだとか何のためにだとかメリットデメリットと言い出すとややこしいのだけれど)
ところが、日本語の”依存性”から”オブジェクト”という意味を想像するのは難しく、それが理解の妨げになっているのだろう。名前って大事だよね。
そういうこともあって、私は”依存性の注入”というタームは極力使わず、DIまたはDependency Injectionを使うようにしている。まあ、そういうタームが出てくる小難しい話はそもそもあんまりしないのだが(台無し)。

蛇足終了

話はそれたが、そういうことで、ViewModelに渡されたnavigationServiceにある、NavigateAsyncというメソッドで画面遷移を実現しているわけである。

とまあ、この1行でこれだけ話せてしまう程度には内容の濃い一行だったわけです。
使う分にはそんな小難しいこと全部無視してこの行コピペして遷移先のページだけ望みのページに書き換えればOKなのだけど(台無し)。

さて、お次に遷移先ページのxamlファイルですが、こっちは遷移したことが見た目で分かりやすいようにタイトルだけ書いています。最初のスケルトンアプリでは、MainPageのTitleが変数になってたけど、まあ、普通はここって変数にしないよね。

SecondPage.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:prism="http://prismlibrary.com"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="SamplePrismApp.Views.SecondPage"
             Title="SecondPage">

</ContentPage>

ウィザードが書いてくれたコードに Title="SecondPage" だけを追加。
ViewModelは変更なし。

これで画面遷移の実装は完了。

実行結果は次の通り。

ちゃんとボタンが出てて、BUTTONNEXTPAGEをタップすると

ちゃんと表示されたタイトルがSecondPageになっている。

めでたしめでたし。

Prism/XamarinでAndroidアプリの作成(3):たかがボタン、されどボタン

ボタンを作成

UIにボタンとテキストを一つずつ追加して、ボタンを押すとテキストが書き換わるというチャチな仕組みを、Prismの仕組みを利用して書いてみることにする。

まずは、Viewの変更。

MainPage.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"
             x:Class="SamplePrismApp.Views.MainPage"
             Title="{Binding Title}">

    <StackLayout HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand">
        <Label Text="Welcome to Xamarin Forms and Prism!" />
        <Label Text="{Binding TextMessage.Value} "/>
        <Button Text="ButtonA" Command="{Binding ButtonACommand}"/>
    </StackLayout>

</ContentPage>

LabelのテキストとButtonのコマンドにBinding がついているが、これはTitleと同じように実体をViewModelに送り込むためのおまじない(とでも思っておけばいい)。

Labelのテキストの実体であるTextMessage.ValueValueが気になるが、これはTextMessageをstring型ではなく、ReactiveProperty<string>型で定義するから、ReactiveProperty型のTextMessageではなく、そのメンバのValueを実態に指定する必要があるためだ。これもまあ、当面はReactiveProperty使うためのおまじないと思っていればいいかもしれない。

Viewの変更は以上。MainPage.xaml.csの変更は不要。

 

続いてViewModel。

MainPageViewModelはこんな感じ

using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using Reactive.Bindings;


namespace SamplePrismApp.ViewModels
{
    public class MainPageViewModel : ViewModelBase
    {
        public ReactiveProperty<string> TextMessage { get; set; }
        public DelegateCommand ButtonACommand { get; set; }

        public MainPageViewModel(INavigationService navigationService)
            : base(navigationService)
        {
            Title = "Main Page";
            TextMessage = new ReactiveProperty<string>("0000");
            ButtonACommand = new DelegateCommand(ButtonAClicked);
        }

        private void ButtonAClicked()
        {
            TextMessage.Value = "XXXX";
        }
    }
}

データバインディングにReactivePropertyを入れるので、using句using Reactive.Bindings;を追加。

Viewに追加したTextMessage、ButtonACommandの実体をそれぞれ定義してコンストラクタの中で初期化している。

ButtonAタップ時の動作は、ButtonAClickedというメソッドの中で定義して、初期化時にデリゲートに登録している。コマンドの中身は単にテキストを切り替えるだけのもの。

起動時が右、ボタンをタップしたら左画面になる。

f:id:dnack:20210902165620p:plain
f:id:dnack:20210902165623p:plain

 

本当にちょっとした例だけど、PrismでUI要素足すときの作業としてはほとんどこれの応用になる。

さあこれでがんがんPrismのプログラム書けるよ。

 

というわけで次回に続く。多分。

 

Prism/XamarinでAndroidアプリの作成(2):お前がBlank Appの中身をみるとき、Black Appもまた以下略

ViewとViewModel

ソリューションエクスプローラーで、作ったテンプレートで作ったBlankAppの中を見るとこんな感じ。なんだかViews,ViewModelsというフォルダができている。ViewsにはView、ViewModelsにはViewModelが置かれる。

うん、さっぱり説明になってないな。

f:id:dnack:20210902121314p:plain

PrismはMVVMというアーキテクチャを採用している。

MVVMについてはググればいろんなところにちゃんとした説明が書いてあるし、ここで私が胡乱な理解で怪しい説明をしても害にならないだろうから省く。大雑把に言えば、見た目と処理を分離して、Viewに見た目、ViewModelに処理を置くソフトウェアアーキテクチャだ。そのViewとViewModelはたがいに依存せずに疎結合にすることによって、見た目と処理を構造的に分離しているのがMVVMの大事なところ。

例えばある機能をUIで提供するときに、その機能をボタンで提供するのか、トグルスイッチで提供するのか、はたまたほかの方法なのか、そのボタンは画面上のどこの位置にあってどういう色で表示するのかみたいなことは全部Viewでやって、そのボタンが押された時の処理はViewModelでやりますよということになる。

ちなみにMVVMの2文字目のVが”View”で、3-4文字目ののVMが”ViewModel”の意味。じゃあ最初のMって何ってことに当然なるんだが、これは”Model”の意味。Viewが画面、ViewModelが画面裏で動く処理、Modelはさらにその裏側にあるビジネスロジック本体という構成になる。

ViewModelとModelはどうちがうのか。これは実践的にはかなり難しい問題のような気がするが、私はソフト本来の処理というのはModelにいて、ViewModelはModelのフロントエンドみたいな役割をするような作りがいいのかなあと勝手に思っている。(”UIは本来の動作じゃないのかよ”という意見が当然聞こえてくるが、そんなものはもちろんソフト本来の動作じゃない。なんで計算機様が人間に気を使わないといけないのだ!)

※筆者は携帯電話のドライバを開発していた時代にUIソフト開発担当にすごくいじめられた覚えがあるので、UIを憎んでいます。坊主が憎いと今朝まで憎いんです。それが人間だから。

繰り返しになるが、これは私の胡乱でかつ自分の設計思想をMVVMに乗せたいがためのあるる意味恣意的な理解なので、MVVMについてちゃんと知りたければ調べてほしい。ちょっとググればまともな解説は山ほど出てくる。

大雑把な理解としては、 これまでに書いた通り、UIのレイアウトをViewに書いて、動作をViewModelに書くというイメージでとりあえずはいい。

 

さて、アプリの中身に戻ろう。

MainPageというページに対して、そのViewModelがMainPageViewModelで、ViewがMainPageViewとなっている。

たとえばボタンを追加するなら、ボタンの配置をViewsの下にあるMainPage.xamlに書いて、ボタンをタップした時の動作をVieModelsの下にあるMainPageViewModel.csに書くというようなイメージ。

ViewModelBaseとはなにかというと、これはViewModelの基底クラスで、これを継承して各画面のViewModelのクラスを作っていくようになっているが、コードを書いていくにあたっては、今はとりあえずあまり気にしなくていい。

ブランクアプリの中身

テンプレートウィザードが吐くスケルトンアプリの解説なんて探せばどこでもやってると思うけど、一応ここでも中身を見てみる。

MainPage.xmal

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="SamplePrismApp.Views.MainPage"
             Title="{Binding Title}">

    <StackLayout HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand">
        <Label Text="Welcome to Xamarin Forms and Prism!" />
    </StackLayout>

</ContentPage>

 

細かい文法の解説は割愛するが、ここで説明したいのは赤字になっている以下のところ。

Title="{Binding Title}"

これを

Title={Title}

と書くと、このページのタイトルはTitleという変数ですよという意味になる。この場合、Titleは、(x:Classとして指定されている)MainPage.xaml.csにあるMainPageクラスのメンバとして定義されることになる。

しかし、MVVMの考え方では、Titleという変数の定義はViewであるMainPageクラスではなく、ViewModelであるMainPageViewModelクラスのほうで記載したい。

それを可能にするのが、このBindingという句である。これはデータバインディングという仕組みを示しているが、まあ名前は今はいい。コードを書く上では、Binding句を付けることによって、変数がViewのメンバではなくViewModelのメンバに定義できるんだと思っておけばいいだろう。(正直私も当面のところそれ以上の理解は放棄している…)

コードビハインドはこんな感じ。あっさりしたもんである。

MainPage.xaml.cs

namespace SamplePrismApp.Views
{
    public partial class MainPage
    {
        public MainPage()
        {
            InitializeComponent();
        }
    }
}

TitleのTの字も出てこない。残念それはViewModelのほうにいる。

特殊なことをしようとおもわない限り、データの処理はViewModelde行い、UIの配置はxamlファイルで完結するので、このクラスには手を加える必要はない。

MainPageViewModel.cs

namespace SamplePrismApp.ViewModels
{
    public class MainPageViewModel : ViewModelBase
    {
        public MainPageViewModel(INavigationService navigationService)
            : base(navigationService)
        {
            Title = "Main Page";
        }
    }
}

using句は省略。

これもスケルトンなのでほぼ空っぽ。コンストラクタの中に1行処理が書かれているだけ。だがしかし、この1行がさっきのBinding句とペアになっている大事な処理だ。

Binding句を付けることによって、TitleがViewのメンバではなくViewModelのメンバだと宣言されたわけだから、ここに初期化が来るのは道理である。

Titleの初期化があるだけなので、これをViewでやったら何が悪いのかというのはぴんと来ないかもしれないが、Titleのテキストを何かの条件で書き換えるというロジックがあるとすると、TitleがViewのメンバにいた場合、Viewに処理を書くか、または、ViewのメンバをViewModelから書き換えるためにViewModelがViewのメンバを知る(Viewに依存する)必要が出てきてしまう。TitleをViewModelのメンバにすることにより、このいずれも回避し、ViewModelがViewに依存することなく疎結合を維持したまま、ViewModelだけでTitleの中身を変更する処理を書くことができるようになるわけだ。

 

これで、デザインだけ変えたい人はViewだけいじればいいし、データ構造作りたい人はデザインで悩みたい人に引っ張られることなく中身をガンガン作ることになる。

中身を一生懸命作ってる人がUIデザインやってる人の思い付きの「ちょっとここのボタンの色変えたいのでコード変えたから、マージしておいてね」みたいなことで煩わされる必要がなくなるのだ(※筆者はUIやってる人に含むところはありません。ないったらないです。/もちろん逆もまた真です。)

私はなんせUIのデザインに興味がない人間なので、処理を書くのにわざわざ画面の仕様変更に振り回されたくないのである。

まあ、これはあくまで僕の理解であり僕がMVVMを使う理由なので、みんなはみんなの理由でMVVMやPrismを好きになればいいと思う。

(私がPrismを好き?まさか…(ツンデレ))

若干蛇足気味の御託を並べてしまったが、ようやく次からコードを触っていこう。