放浪軍師のXamarin.Formsアプリ開発局

Xamarin.Forms+Prism+ReactivePropertyで素人がAndroidやUWPのアプリを右往左往しながら開発している様を発信していきます。性質上間違いも多いのでご注意ください。

SQLiteを学ぶ

放浪軍師のXamarin.Formsによるアプリ開発
今回は SQLite を学んでみました。
まず、初めて訪問された方は以下をお読みください。
www.gunshi.info

状態を復元したい

ユーザーが色々な操作をした後にアプリケーションを終了し、再度起動した際に終了前の情報を復元したいという場面は多々あります。その場合、当然データをどこかしらに保持しておく必要がありますが、その手段の一つとしてデータベース形式でアプリケーションに組み込むことができる SQLite という物があるらしいとの事でやってみました。

sqlite-net-pcl

Xamarin.Forms で SQLite を扱う場合は sqlite-net-pcl という Nuget パッケージを用いるのがどうやら一般的なようです。詳しくは以下rksoftwareさんの記事を参照してください。いつもお世話になっております!
rksoftware.hatenablog.com

ただ、このまま実装してみたのですが、Android は動いたのですが UWP では例外が発生して動きませんでした。どうやら UWP の場合 SQLite 用のファイルがうまく生成できない様子。そこで更に調べてみるとi以下のような記事を発見。Atsushi Nakamuraさんの記事ですね。いつもお世話になっております!
www.nuits.jp

こちらでは PCLStorage という Nuget パッケージを用いて SQLite のファイルを生成しているコードが公開されていて、動かしてみたところ無事 UWP でも動作する事が確認できました。

PCLStorage

通常、ファイルを保存する場合は DependencyService を使って各プラットフォーム別にアクセス用コードを書かなければいけないのですが、PCLStorage を使えば PCL だけでアクセスが可能になります。物すんごく便利ですね!詳しくは以下の記事をご覧ください。田淵さんの記事です。いつもお世話になっております!
www.buildinsider.net

余談ですが、Xamarin.Forms って初期の頃は各プラットフォーム別の記述が山ほどあって大変だったんじゃないかと思うんですよ。でも段々とこうやって各プラットフォーム別に記述しなければいけなかった事が PCL だけで出来るようになりつつあるようで、後発組である私なんかには非常にありがたい事ですね。そのうち私も Xamarin.Forms に貢献できるような Nuget パッケージを作成配布できるようになりたいですねぇ…。

では実装してみよう

記事を読んで理解したと思ってもやっぱ作ってみないと成長できないのでこんなのを作ってみました。
f:id:roamschemer:20181114020713g:plain:w200
機能は以下の通りです。

  1. 左側で Age、Name、Gender を入力後、Insert をタップすると右側のリストに追加される。ID は自動で付く。
  2. 同じく Age、Name、Gender を入力後、リストを選択して UpDate をタップすると、そのリストが入力値に置き換わる。ID はそのまま。
  3. リストを選択して Delete をタップすると、そのリストが削除される。削除されたリストの ID は二度と使われることはない。
  4. 上記動作時には SQLite を更新する。再起動時には SQLite を読み込むので状態は保持される。

ちょっと色々と粗い感じになっていますが、まぁ動作の確認程度であればいいかな?

Model

Person クラスをコレクション化している Commanders を SQLite でデータベース化。 一番下の CreateDb メソッドで PCLStorage を使用しているので UWP でも動くようになっています。尚、毎回 DbLoad メソッドを呼び出しているのは自動発行された ID を取得する必要があるからですが、リストが増えると重くなるので、本来ならそのレコードだけ再取得した方が良いと思います。

using Prism.Mvvm;
using SQLite;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using PCLStorage;

namespace SQLiteSample.Models
{
    public class Person : BindableBase
    {
        //主キー
        [PrimaryKey, AutoIncrement]
        public int Id
        {
            get => id;
            set => SetProperty(ref id, value);
        }
        private int id = 0;

        public int Age
        {
            get => age;
            set => SetProperty(ref age, value);
        }
        private int age = 0;

        public string Name
        {
            get => name;
            set => SetProperty(ref name, value);
        }
        private string name = "";

        public string Gender
        {
            get => gender;
            set => SetProperty(ref gender, value);
        }
        private string gender = "";
    }

    public class Commander : BindableBase
    {
        private readonly string dbPath;
        public ObservableCollection<Person> Commanders { get; private set; } = new ObservableCollection<Person>();

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="dbPath">SQLite保存名称</param>
        public Commander(string dbPath)
        {
            this.dbPath = dbPath;
            DbLoad();
        }

        /// <summary>
        /// リストに追加
        /// </summary>
        public void Insert(int age, string name, string gender)
        {
            var person = new Person { Age = age, Name = name, Gender = gender };
            DbInsert(person);
            DbLoad();
        }

        /// <summary>
        /// リストの更新
        /// </summary>
        public void UpDate(Person selectedPerson, int age, string name, string gender)
        {
            if (selectedPerson == null) return;
            var person = new Person { Id = selectedPerson.Id, Age = age, Name = name, Gender = gender };
            DbUpDate(person);
            DbLoad();
        }

        /// <summary>
        /// リストから消す
        /// </summary>
        /// <param name="person">人情報</param>
        public void Delete(Person selectedPerson)
        {
            if (selectedPerson == null) return;
            var person = new Person { Id = selectedPerson.Id };
            DbDelete(person);
            DbLoad();
        }

        /// <summary>
        /// データベースへ追加
        /// </summary>
        /// <param name="person">人情報</param>
        private async void DbInsert(Person person)
        {
            using (var db = await CreateDb())
            {
                db.Insert(person);
            }
        }

        /// <summary>
        /// データベースの更新
        /// </summary>
        /// <param name="person">人情報</param>
        private async void DbUpDate(Person person)
        {
            using (var db = await CreateDb())
            {
                db.Update(person);
            }
        }

        /// <summary>
        /// データベースから削除
        /// </summary>
        /// <param name="person">人情報</param>
        private async void DbDelete(Person person)
        {
            using (var db = await CreateDb())
            {
                db.Delete<Person>(person.Id);
            }
        }

        /// <summary>
        /// データベースの呼出
        /// </summary>
        private async void DbLoad()
        {
            Commanders.Clear();
            using (var db = await CreateDb())
            {
                foreach (var x in db.Table<Person>())
                {
                    Commanders.Add(x);
                }
            }
        }

        /// <summary>
        /// データベースの生成と取得(以下のようにPCLStorageを使わないとUWPで例外が発生した)
        /// </summary>
        /// <returns></returns>
        private async Task<SQLiteConnection> CreateDb()
        {
            IFolder rootFolder = FileSystem.Current.LocalStorage;
            var result = await rootFolder.CheckExistsAsync(dbPath);
            if (result == ExistenceCheckResult.NotFound)
            {
                IFile file = await rootFolder.CreateFileAsync(dbPath, CreationCollisionOption.ReplaceExisting);
                var db = new SQLiteConnection(file.Path);
                db.CreateTable<Person>();
                return db;
            }
            else
            {
                IFile file = await rootFolder.CreateFileAsync(dbPath, CreationCollisionOption.OpenIfExists);
                return new SQLiteConnection(file.Path);
            }
        }

    }

}
ViewModel

ToReadOnlyReactiveCollection を使って ListView と Commanders を ViewModel ← Model 単方向同期させています。ちなみに ObservableCollection を双方向同期させる方法は無いみたいですね。

using Prism.Navigation;
using Reactive.Bindings;
using System;
using System.Linq;
using System.Collections.ObjectModel;
using SQLiteSample.Models;

namespace SQLiteSample.ViewModels
{
    public class MainPageViewModel : ViewModelBase
    {

        public ReadOnlyReactiveCollection<Person> ListView { get; private set; }
        public ReactiveProperty<Person> SelectedItem { get; private set; } = new ReactiveProperty<Person>();
        public ObservableCollection<int> AgeList { get; private set; } = new ObservableCollection<int>();
        public ObservableCollection<string> GenderList { get; private set; } = new ObservableCollection<string>();

        public ReactiveProperty<int> Age { get; } = new ReactiveProperty<int>();
        public ReactiveProperty<string> Gender { get; } = new ReactiveProperty<string>();
        public ReactiveProperty<string> Name { get; } = new ReactiveProperty<string>();

        public ReactiveCommand InsertTapped { get; private set; } = new ReactiveCommand();
        public ReactiveCommand UpDateTapped { get; private set; } = new ReactiveCommand();
        public ReactiveCommand DeleteTapped { get; private set; } = new ReactiveCommand();

        private Commander commander = new Commander("commander.db3");

        public MainPageViewModel(INavigationService navigationService)
            : base(navigationService)
        {
            //PickerList
            AgeList = new ObservableCollection<int>(Enumerable.Range(1, 100).ToList());
            GenderList = new ObservableCollection<string>() { "男", "女", "不明" };

            //ViewModel←Model
            ListView = commander.Commanders.ToReadOnlyReactiveCollection();

            //Button
            InsertTapped.Subscribe(_ => commander.Insert(Age.Value, Name.Value, Gender.Value));
            UpDateTapped.Subscribe(_ => commander.UpDate(SelectedItem.Value, Age.Value, Name.Value, Gender.Value));
            DeleteTapped.Subscribe(_ => commander.Delete(SelectedItem.Value));
        }

    }
}
View

まぁここはどうにでもなるかと。一応載せておきます。

<?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="SQLiteSample.Views.MainPage"
             Title="SQLiteSample">

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="3*" />
        </Grid.ColumnDefinitions>
        <StackLayout Grid.Column="0">
            <Label  Text="DbInput" />
            <Picker Title="Age" ItemsSource="{Binding AgeList}" SelectedItem="{Binding Age.Value}"/>
            <Entry  Placeholder="Name" Text ="{Binding Name.Value}" Keyboard="Text"/>
            <Picker Title="Gender" ItemsSource="{Binding GenderList}" SelectedItem="{Binding Gender.Value}" />
            <Button Text="Insert" Command="{Binding InsertTapped}"/>
            <Button Text="UpDate" Command="{Binding UpDateTapped}"/>
            <Button Text="Delete" Command="{Binding DeleteTapped}"/>
        </StackLayout>
        <ListView Grid.Column="1" ItemsSource="{Binding ListView}" SelectedItem="{Binding SelectedItem.Value}">
            <ListView.Header>
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*" />
                        <ColumnDefinition Width="*" />
                        <ColumnDefinition Width="3*" />
                        <ColumnDefinition Width="*" />
                    </Grid.ColumnDefinitions>
                    <Label Grid.Column="0" Text="ID" />
                    <Label Grid.Column="1" Text="Age" />
                    <Label Grid.Column="2" Text="Name" />
                    <Label Grid.Column="3" Text="Gender" />
                </Grid>
            </ListView.Header>
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*" />
                                <ColumnDefinition Width="*" />
                                <ColumnDefinition Width="3*" />
                                <ColumnDefinition Width="*" />
                            </Grid.ColumnDefinitions>
                            <Label Grid.Column="0" Text="{Binding Id}" />
                            <Label Grid.Column="1" Text="{Binding Age}" />
                            <Label Grid.Column="2" Text="{Binding Name}" />
                            <Label Grid.Column="3" Text="{Binding Gender}" />
                        </Grid>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</ContentPage>
GitHub

今回も上げておきました。何かのお役に立てれば幸い。
github.com

まとめ

思ったよりずっと簡単に扱う事が出来てビックリしています。これを使えば簡単に状態を復元できますね!早速乱ちゃんにも組み込まなきゃ!