rksoftware

Visual Studio とか C# とかが好きです

Xamarin Forms で SQLite を使う

Xamarin.Forms で SQLite を使うには SQLite-net を使うのが簡単そうです。
[ SQLite-net https://github.com/praeclarum/sqlite-net ]

■ NuGet パッケージのインストール

・Forms プロジェクトを .NET Standard で作成します。
・NuGet から「sqlite-net-pcl」を共通プロジェクトとプラットフォーム毎のプロジェクト全てにインストールします。
※Android、と iOS のみで確認しています。

!!注意!!
sqlite-net-pcl」で検索すると似た名前のパッケージがいくつか見つかります。注意して選択してください。
私の環境では一番目に「SQLite.Net-PCL-Silverlight」というパッケージが表示されるなどしました。
Silverlight... 2018 年にこの名前を目にするとは...

■ コード

まずは動かしてみたコードを

public class Tran
{
    [SQLite.PrimaryKey, SQLite.AutoIncrement]
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime TimeStamp { get; set; }
    public decimal Value { get; set; }
}

public partial class MainPage : ContentPage
{
    public static string DbPath { get; } = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "SQLiteDataBase.db");
    public MainPage()
    {
        InitializeComponent();
        var sl = (StackLayout)(this.Content = new StackLayout());
        var button1 = new Button() { Text = "Create Table" };
        var button2 = new Button() { Text = "Insert" };
        var button3 = new Button() { Text = "Select" };
        var button4 = new Button() { Text = "Query" };
        sl.Children.Add(button1);
        sl.Children.Add(button2);
        sl.Children.Add(button3);
        sl.Children.Add(button4);
        button1.Clicked += Button1_Clicked;
        button2.Clicked += Button2_Clicked;
        button3.Clicked += Button3_Clicked;
        button4.Clicked += Button4_Clicked;
    }

    // Query
    private void Button4_Clicked(object sender, EventArgs e)
    {   // データ取得
        using (var db = new SQLite.SQLiteConnection(DbPath))
        {   // Select
            foreach (var row in db.Query<Tran>("Select * From Tran Where Name like ? ", $"%3%"))
                Console.WriteLine($"Tran: {row.Id}, {row.Name}, {row.TimeStamp}, {row.Value}");
        }
    }

    // Select
    private void Button3_Clicked(object sender, EventArgs e)
    {   // データ取得
        using (var db = new SQLite.SQLiteConnection(DbPath))
        {   // Select
            foreach (var row in db.Table<Tran>().Where(r => r.Name.Contains("3")))
                Console.WriteLine($"Tran: {row.Id}, {row.Name}, {row.TimeStamp}, {row.Value}");
        }
    }

    // Insert
    private void Button2_Clicked(object sender, EventArgs e)
    {   // データ追加
        using (var db = new SQLite.SQLiteConnection(DbPath))
        {   // Insert
            db.Insert(new Tran() { Name = "Tran 12 Name", TimeStamp = DateTime.Now, Value = 1.1m });
            db.Insert(new Tran() { Name = "Tran 23 Name", TimeStamp = DateTime.Now, Value = 2.2m });
            db.Insert(new Tran() { Name = "Tran 34 Name", TimeStamp = DateTime.Now, Value = 3.3m });
            db.Insert(new Tran() { Name = "Tran 45 Name", TimeStamp = DateTime.Now, Value = 4.4m });
        }
    }

    // Create Table
    private void Button1_Clicked(object sender, EventArgs e)
    {   // データベース初期設定
        using (var db = new SQLite.SQLiteConnection(DbPath))
        {   // Create Table
            db.CreateTable<Tran>();
        }
    }
}

■ データベースの生成

データベースファイルを作成する処理はすべて吸収してくれています。
我々は、データベースファイルのパスを指定するだけでうまいことやってくれるようです。

■ テーブルの生成

まず Entity のクラスを作って

public class Tran
{
    [SQLite.PrimaryKey, SQLite.AutoIncrement]
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime TimeStamp { get; set; }
    public decimal Value { get; set; }
}

CreateTable メソッドを使えば良いだけです。

using (var db = new SQLite.SQLiteConnection(DbPath))
{   // Create Table
    db.CreateTable<Tran>();
}

とても簡単。

■ Insert

データの Insert も簡単。
Insert メソッドにデータオブジェクトのインスタンスを渡すだけ。

using (var db = new SQLite.SQLiteConnection(DbPath))
{   // Insert
    db.Insert(new Tran() { Name = "Tran 12 Name", TimeStamp = DateTime.Now, Value = 1.1m });
    db.Insert(new Tran() { Name = "Tran 23 Name", TimeStamp = DateTime.Now, Value = 2.2m });
    db.Insert(new Tran() { Name = "Tran 34 Name", TimeStamp = DateTime.Now, Value = 3.3m });
    db.Insert(new Tran() { Name = "Tran 45 Name", TimeStamp = DateTime.Now, Value = 4.4m });
}

■ Select パターン1

Select は大きく 2 通りの方法があります。
まず一つ目は、EntityFramework のように Where メソッドで対象を絞る条件を与えられるパターンです。

using (var db = new SQLite.SQLiteConnection(DbPath))
{   // Select
    foreach (var row in db.Table<Tran>().Where(r => r.Name.Contains("3")))
        Console.WriteLine($"Tran: {row.Id}, {row.Name}, {row.TimeStamp}, {row.Value}");
}

■ Select パターン2

直接クエリ文を渡せるパターンです。
複雑なデータを扱う場合や、RDB の得意な方と組み時にも安心ですね。

using (var db = new SQLite.SQLiteConnection(DbPath))
{   // Select
    foreach (var row in db.Query<Tran>("Select * From Tran Where Name like ? ", $"%3%"))
        Console.WriteLine($"Tran: {row.Id}, {row.Name}, {row.TimeStamp}, {row.Value}");
}

■ その他のメソッド

今回は試していませんが、Delete、Update、OrderBy、Join など必要な機能はそろっていそうです。

■ 非同期

今回は SQLiteConnection クラスを試しましたが、各種処理が非同期で行える SQLiteAsyncConnection クラスもあります。
アプリの起動時の処理やデータ更新などで時間がかかる場合に頼りになりそうです。

Xamarin Android で SQLite を使う

SQLite は Android の API をラップ下 API で素直に扱うことができます。
Android.Database.Sqlite.SQLiteOpenHelper クラスのサブクラスを作って素直に使えば OK です。

コード

・MySQLiteHelper / Android.Database.Sqlite.SQLiteOpenHelper のサブクラス

class MySQLiteHelper : Android.Database.Sqlite.SQLiteOpenHelper
{
    private static string DbName { get; } = "northwind";
    private static int Version { get; } = 1;

    public MySQLiteHelper(Context context):base(context, DbName, null, Version)
    {
        ;
    }

    public override void OnCreate(SQLiteDatabase db)
    {
        // データベースの初期化。テーブルの作成などする
        db.ExecSQL("Create Table Tran(Id integer primary key autoincrement, Name text, MId integer)");
        db.ExecSQL("Create Table Master(MId integer primary key autoincrement, MName text)");
    }

    public override void OnUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
    {
        // アプリケーションのアップグレード時の処理を書くなどする
        ;
    }
}

・MainActivity

public class MainActivity : Activity
{
    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
        // UI の構築。ボタンを一つ置いてイベントハンドラを設定
        SetContentView(Resource.Layout.Main);
        var button = new Button(this) { Text = "button" };
        ((Android.Views.ViewGroup)FindViewById<Android.Views.ViewGroup>(Android.Resource.Id.Content).GetChildAt(0)).AddView(button);
        button.Click += Button_Click;
    }

    // ボタンクリックのイベントハンドラ
    private void Button_Click(object sender, System.EventArgs e)
    {
        using (var db = new MySQLiteHelper(this.ApplicationContext).WritableDatabase)
        {
            {// 一つ目のテーブルに値を追加
                var values = new Android.Content.ContentValues();
                values.Put("Name", "N" + System.DateTime.Now.ToString());
                values.Put("MId", 1);
                db.Insert("Tran", null, values);
            }
            {// 二つ目のテーブルに値を追加
                var values = new Android.Content.ContentValues();
                values.Put("MName", "MN" + System.DateTime.Now.ToString());
                db.Insert("Master", null, values);
            }

            {// テーブルを Join して値を取得
                var text = new System.Text.StringBuilder();
                using (var cursor = db.RawQuery("Select * From Tran Inner Join Master On Tran.MId = Master.MId", null))
                {
                    while (cursor.MoveToNext())
                    {
                        foreach (var index in System.Linq.Enumerable.Range(0, cursor.ColumnCount))
                        {
                            text.AppendLine($"{cursor.GetColumnName(index)}:{cursor.GetString(index)}");
                        }
                        text.AppendLine();
                    }
                }
                // 取得結果を表示
                new AlertDialog.Builder(this).SetMessage(text.ToString()).Show();
            }
        }
    }
}

ポイント

MainActivity クラスでの Insert や Select はまあ見れば大体 OK でしょう。
Insert も Select も複数のメソッドがありますが、そこはまた後日。
今回は、MySQLiteHelper クラスの OnCreate メソッドに注目しておきます。
OnCreate メソッドはアプリがインストールして最初にデータベースを使う際に呼び出され、データベースの初期化をします。
上記では、テーブルを作成しています。初期データも設定する必要があればここでやっておくのが良いでしょう。

もう一つポイント

もう一つ、OnUpgrade メソッドもポイントです。
アプリのアップデートで、テーブルやカラム、マスターデータなどの追加をする場所です。
アプリのアップデートとは必ず毎回のアップデートを適用してもらえているものではありません。
OnUpgrade メソッドではアップデート前のバージョン番号と現在のバージョン番号が渡されるようになっています。

Xamarin Android で画面の一部のレイアウトを XML で定義する

Android では View を XML ファイルから構築できます。
この機能で構築した View を画面の要素に Add することで画面の一部だけを XML で定義できることになります。

何がうれしいの?

この機能を使うと、複数の画面で出てくる同じ要素の組み合わせの定義を共有したり、画面上の状況によって表示の変わる部分を切り出して定義できたりします。
ポップアップの表示にも使えたりします。
また、Android で View の要素一つづつを new してAddView して組み上げるコードは決して短くありません。
場合によっては、XML の文字列を構築する方がスマートなこともあるかもしれません(やったことはありませんが)。

LayoutInflater.Inflate メソッド

LayoutInflater の Inflate メソッドを使用します。

コード

次のようなコードになります。

レイアウト定義

・Main.amxl 通常の画面定義

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:id="@+id/ll"
    android:background="@android:color/background_light"
    android:padding="10dp"/>

・XMLFile1.xml 今回動的に追加する要素の定義

<?xml version="1.0" encoding="utf-8" ?>
<LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@android:color/background_dark">
  <TextView android:text="test"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
</LinearLayout>

Activity のコード

・MainActivity.cs

public class MainActivity : Activity
{
    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);

        SetContentView(Resource.Layout.Main);

        var v1 = LayoutInflater.Inflate(Resource.Layout.XMLFile1, null); // XMLFile1 の内容
        var root = FindViewById<ViewGroup>(Android.Resource.Id.Content); // Main.axml の内容
        var main = (ViewGroup)root.GetChildAt(0);   // id/ll の LinearLayout
        main.AddView(v1);
    }
}

実行例

Visual Studio でバイナリを編集する

Visual Studio にはバイナリーエディターという、ファイルのバイナリーを扱えるエディターが搭載されています。
最近はあまり出番がなくすっかりその存在を意識していませんでしたが、先日救われたのでメモを残しておきます。
画像は英語表示になっていますが、日本語環境の方も特に問題なく把握できるかと思います。

■ プロジェクト内のファイルを開く場合

・ソリューションエクスプローラーなどでバイナリエディターで開きたいファイルを右クリック。
・コンテキストメニューの [ Open With... ] を選択。
f:id:rksoftware:20171231195422j:plain
[ Binary Editor ] を選択して [ OK ]
f:id:rksoftware:20171231195438j:plain

■ プロジェクト外のファイルを開く場合

`メニュー > File > Open > File... を選択。
・開きたいファイルを選択して [ Open ] ボタンの右の [ ▼ ] をクリック。
[ Open With... ] を選択。
f:id:rksoftware:20171231195502j:plain
[ Binary Editor ] を選択して [ OK ]
f:id:rksoftware:20171231195438j:plain

■ バイナリエディター

このような画面が開きます。
f:id:rksoftware:20171231195527j:plain

■ バイナリーデータを編集する

(次の例のような変更はわざわざこんなことをしなくても、一般的なテキストエディターなどの機能で可能ですが、例として)
例えば、次のファイルは「EF BB BF」で始まり「0D 0A」で終わっています。
f:id:rksoftware:20171231195539j:plain
これは BOM 付きで改行が CRLF のファイルです。
これを編集すると BOM なしで改行が LF のファイルに変更できたりします。 f:id:rksoftware:20171231195604j:plain

Xamarin Android でデバッグ実行の対象としてエミュレーターが選択できない

Xamarin.Android でデバッグのデバイスとしてエミュレーターが選択できないパターンはいろいろあると思います。
これはその一例です。

Minimum Android version

Minimum Android version の APIレベル を満たしていないエミュレーターは通常の選択肢に出てきません。
出てきても実行できないわけですし。

Minimum Android version を変更する

プロジェクトのプロパティ > Android Manifest > Minimum Android version で変更できます。

テンプレートにより異なる

Minimum Android version の初期設定はプロジェクトにより異なります。
例えば現在の私の環境では Forms プロジェクトでは API レベル 15、Android プロジェクトでは 21 になりなります。
容量を減らすためにエミュレーターは1種類、某有名モンスターゲームを参考にレベル 19 のものしか用意していなかったので、2 秒迷いました。

LayoutInflater 使用時に You must supply a layout_width attribute

Xamarin.Android で LayoutInflater により View を構築し用とした際にBinary XML file line #1: You must supply a layout_width attribute. というエラーが発生し失敗することがありました。
結論から言うと原因は不明です。

状況

・Android のネイティブから Xaamrin.Android への移植時
・レイアウトの XML は Android ネイティブのプロジェクトからファイルコピーして取り込み
・1 階層以上の親子関係のある要素が定義されている
・XML ファイルを編集し、1 要素だけの階層のない定義にするとエラーにならない
・その他、どう書き換えても階層のある定義はエラー

解決?

・該当の XML にブラウザから 1 要素分の定義をコピペするとエラーにならなくなった

もうエラーが発生しない

・一度エラーが発生しなくなると今度は、どう書き換えてもエラーが発生しなくなった
・ファイルコピーでもともとエラーが発生していたファイルに戻してもエラーが再現しない

謎です。謎なのでまたいつ再発するかわからないので、楽しみに待つことにします。

Xamarin Android で音声認識をする

Xamarin.Android で音声認識の API を使ってみました。
早速ですがコードです。

using Android.App;
using Android.Widget;
using Android.OS;
using System.Linq;

namespace SpeechRecognizer
{
    [Activity(Label = "SpeechRecognizer", MainLauncher = true)]
    public class MainActivity : Activity
    {
        private static int _requestCode { get; } = 1;

        private TextView _text;

        protected override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);

            // ドダイ
            var parent = new LinearLayout(this) { Orientation = Orientation.Vertical, };
            SetContentView(parent);

            // ボタン
            var button1 = new Button(this) { Text = "風の気持ちで語りかける(Activityあり)", TextSize = 24, };
            parent.AddView(button1);
            var button2 = new Button(this) { Text = "風の気持ちで語りかける(Activityなし)", TextSize = 24, };
            parent.AddView(button2);

            // テキスト
            var text = _text = new TextView(this) { TextSize = 24, };
            parent.AddView(text);

            // ボタン
            button1.Click += (sender, e) => StartSpeechRecognizerActivity(this);
            button2.Click += (sender, e) => StartSpeechRecognizerListening(this);
        }

        // 音声認識の Activity を表示しないパターン (<uses-permission android:name="android.permission.RECORD_AUDIO" />が必要)
        protected void StartSpeechRecognizerListening(Activity con)
        {
            var intent = new Android.Content.Intent(Android.Speech.RecognizerIntent.ActionRecognizeSpeech);
            intent.PutExtra(Android.Speech.RecognizerIntent.ExtraLanguageModel, Android.Speech.RecognizerIntent.LanguageModelFreeForm);

            var recognizer = Android.Speech.SpeechRecognizer.CreateSpeechRecognizer(con);

            recognizer.Results += (sender, e) =>
            {
                var values = e.Results.GetStringArrayList(Android.Speech.SpeechRecognizer.ResultsRecognition);
                _text.Text = values == null ? "(null)" : string.Join(", ", values.ToArray());
            };

            recognizer.StartListening(intent);
        }

        // 音声認識の Activity を表示するパターン
        protected void StartSpeechRecognizerActivity(Activity con)
        {
            var intent = new Android.Content.Intent(Android.Speech.RecognizerIntent.ActionRecognizeSpeech);
            intent.PutExtra(Android.Speech.RecognizerIntent.ExtraLanguageModel, Android.Speech.RecognizerIntent.LanguageModelFreeForm);
            intent.PutExtra(Android.Speech.RecognizerIntent.ExtraPrompt, "風の気持ちで語りかけてください");

            con.StartActivityForResult(intent, _requestCode);
        }

        // 標準の音声認識の Activity の結果が帰ってくる
        protected override void OnActivityResult(int requestCode, Result resultCode, Android.Content.Intent data)
        {
            if (_requestCode != requestCode) return;
            if (Result.Ok != resultCode) return;

            var values = data.GetStringArrayListExtra(Android.Speech.RecognizerIntent.ExtraResults);
            _text.Text = values == null ? "(null)" : string.Join(", ", values.ToArray());

            base.OnActivityResult(requestCode, resultCode, data);
        }
    }
}

コードは基本的に、Android での情報を参考にしています。次の記事に非常にお世話になりました。

二つの方式

このコードを動かすと、次のように二つのボタンがあります。

音声認識には大きく二つの方式があり、それぞれのボタンが対応しています。

Android の音声認識用の Activity を表示する

音声認識中に、標準の UI が表示される方式です。
・ボタンを押すと音声認識中とわかる標準の UI が表示され...

・認識した文言を表示します。

私はこちらの方が好きですが、アプリの仕様を決める方にはもう一方の方式が人気ありそうです。

Android の音声認識用の Activity を表示しない

音声認識中に、標準の UI が表示されない方式です。
・ボタンを押すと UI 上は何も起きず...

・Android に語りかけると認識した文言を表示します。

音声認識中とわかる標準の UI は表示されませんが、逆に音声認識中に自由な UI を表示できます。
例えば、イルカや羊を表示するなどしたい場合はこちらの方式を使うことになります。私のおススメはイルカです。

permission

・Activity を表示しないパターンの場合は、RECORD_AUDIO の permission が必要です。
・表示するパターンでは permission は不要でした。

結果の文言はリストで複数返ってくる

音声の候補はすべてリストで返ってくる。これにより同音異義語や活舌が悪かった場合などにも対応できます。
上記の例でも「うまいうますぎる」と語り掛けたのですが、「甘い甘すぎる」かもしれないと認識して両方を候補として返してくれています。

「甘い甘すぎる」が何なのかはよくわかりませんが...。

文字の表記の違いも OK

同じ音で日本語の漢字等の表記の違いも候補としてリストで返してくれます。
次の例では「かすみがせきえき」で「霞ヶ関駅(埼玉県)」と「霞ケ関駅(東京都)」の両方を候補として返してくれています。

これにより例えば 東京国際大学(最寄り駅「霞ヶ関駅(埼玉県)」)へ行きたいユーザーに間違えて「霞ケ関駅(東京都)」へのルートを案内してしまうといった不幸な事故が防げますね。