.NET MAUIで前回のオイル交換日を記録するアプリ2

C# コンピュータ
C#
過去に行ったオイル交換などの作業日付を記録するだけのアプリです。
前回からの変更点として、前回は登録出来る作業が3件固定でしたが、今回は追加することができるようにしました。

パッケージをインストール

NuGetで必要なパッケージをインストール
Visual Studio 2022のメニュー→「プロジェクト」→「NuGetパッケージの管理」
ReactiveProperty
System.Reactive
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.Sqlite.Core
SQLitePCLRaw.bundle_green
sqlite-net-pcl
検索しインストール

ソースコード

ファイル名:MainPage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="LastOilChange2.MainPage">

    <ScrollView>
        <VerticalStackLayout>
            <CollectionView
                ItemsSource="{Binding Items}"
                SelectedItem="{Binding SelectedItem.Value}"
                SelectionChanged="OnCollectionViewSelectionChanged"
                SelectionMode="Single">
                <CollectionView.ItemTemplate>
                    <DataTemplate>
                        <Grid Padding="10">
                            <Grid.RowDefinitions>
                                <RowDefinition Height="40" />
                                <RowDefinition Height="40" />
                            </Grid.RowDefinitions>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="240" />
                                <ColumnDefinition Width="*" />
                            </Grid.ColumnDefinitions>
                            <Label Grid.Column="0"
                                    Text="{Binding Memo}"
                                    FontSize="18" />
                            <Label Grid.Column="1"
                                    Text="{Binding Date}"
                                    FontSize="18" />
                        </Grid>
                    </DataTemplate>
                </CollectionView.ItemTemplate>
            </CollectionView>
            <Label
                Text="{Binding SelectedName.Value}"
                FontSize="18"/>
            <Button
                Text="Add"
                Margin="8"
                Command="{Binding OnAddCliked}" />
        </VerticalStackLayout>
    </ScrollView>
    
</ContentPage>

ファイル名:MainPage.xaml.cs

namespace LastOilChange2;

public partial class MainPage : ContentPage
{
    ViewModel _vm;

    public MainPage()
    {
        InitializeComponent();
        this.Loaded += MainPage_Loaded;
    }

    private void MainPage_Loaded(object sender, EventArgs e)
    {
        _vm = new ViewModel();
        this.BindingContext = _vm;
    }
    private async void OnCollectionViewSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        var item = (e.CurrentSelection.FirstOrDefault() as LastUpdate);
        if (item != null)
        {
            var page = new DetailPage(item);
            await Navigation.PushModalAsync(page);
        }
    }
    protected override void OnNavigatedTo(NavigatedToEventArgs args)
    {
        _vm?.UpdateListView();
    }

}

ファイル名:DetailPage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="LastOilChange2.DetailPage"
             Title="DetailPage">
    <ScrollView>
        <VerticalStackLayout
            Spacing="25"
            Padding="30,0"
            VerticalOptions="Center">
            <Entry Text="{Binding Memo.Value}" 
                   FontSize="18" />
            <Entry Text="{Binding Date.Value}" 
                   FontSize="18" />

            <HorizontalStackLayout>
                <Button
                    Text="日付更新"
                    Margin="8"
                    Command ="{Binding OnUpdateDate}"
                    HorizontalOptions="Center" />

                <Button
                    Text="削除"
                    Margin="8"
                    Clicked="OnRemoveBtnClicked"
                    HorizontalOptions="Center" />
            
                <Button
                    Text="戻る"
                    Margin="8"
                    Clicked="OnCloseBtnClicked"
                    HorizontalOptions="Center" />
            </HorizontalStackLayout>
        </VerticalStackLayout>
    </ScrollView>
</ContentPage>

ファイル名:DetailPage.xaml.cs

namespace LastOilChange2;

public partial class DetailPage : ContentPage
{
    DetailViewModel _vm;
    public DetailPage(LastUpdate item)
    {
        InitializeComponent();
        _vm = new DetailViewModel(item);
        Loaded += (s, e) =>
        {
            BindingContext = _vm;
        };
    }
    // 戻る
    private async void OnCloseBtnClicked(object sender, EventArgs e)
    {
        await Navigation.PopModalAsync();
    }
    // 削除
    private async void OnRemoveBtnClicked(object sender, EventArgs e)
    {
        var db = new MyDbContext();
        db.Remove(_vm.Item);
        db.SaveChanges();
        await Navigation.PopModalAsync();
    }
}

ファイル名:DetailViewModel.cs


using Microsoft.EntityFrameworkCore;
using Reactive.Bindings;
using SQLitePCL;

namespace LastOilChange2;

public class DetailViewModel
{
    public ReactiveProperty<string> Memo { get; set; } = new ReactiveProperty<string>("");
    public ReactiveProperty<string> Date { get; set; } = new ReactiveProperty<string>("");

    public ReactiveCommand OnUpdateDate { get; set; } = new ReactiveCommand();

    public LastUpdate Item;

    public DetailViewModel(LastUpdate item)
    {
        var db = new MyDbContext();
        this.Item = item;
        Memo.Value = item.Memo;
        Date.Value = item.Date;

        Memo.Subscribe(_ =>
        {
            item.Memo = Memo.Value;
            db.Update(item);
            db.SaveChanges();
        });
        Date.Subscribe(_ =>
        {
            item.Date = Date.Value;
            db.Update(item);
            db.SaveChanges();
        });
        OnUpdateDate.Subscribe(_ =>
        {
            Date.Value = DateTime.Now.ToString("yyyy/MM/dd");
        });
    }
}

ファイル名:LastUpdate.cs

using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace LastOilChange2;

public class LastUpdate
{
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    [Key]
    public long Id { get; set; }
    public string Memo { get; set; } = "";
    public string Date { get; set; } = "";
}

ファイル名:MyDbContext.cs

using Microsoft.EntityFrameworkCore;

namespace LastOilChange2;

public class MyDbContext : DbContext
{
    public DbSet<LastUpdate> LastUpdate => Set<LastUpdate>();
    public string DbPath { get; set; } = "";

    public MyDbContext()
    {
        DbPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) + "/sample.db";
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite($"Data Source={DbPath}");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<LastUpdate>().ToTable("LastUpdate");
    }
}

ファイル名:ViewModel.cs

using Microsoft.EntityFrameworkCore;
using Reactive.Bindings;
using SQLitePCL;

namespace LastOilChange2;

public class ViewModel
{
    public ReactiveCollection<LastUpdate> Items { get; set; } = new ReactiveCollection<LastUpdate>();
    public ReactiveProperty<LastUpdate> SelectedItem { get; } = new ReactiveProperty<LastUpdate>();
    public ReactiveProperty<string> SelectedName { get; } = new ReactiveProperty<string>("");
    public ReactiveCommand OnAddCliked { get; set; } = new ReactiveCommand();


    public void UpdateListView()
    {
        var db = new MyDbContext();
        // ListViewのクリア
        Items.Clear();
        // レコードの取得
        foreach (var r in db.LastUpdate)
        {
            // ListViewに追加
            Items.Add(r);
        }

    }
    public ViewModel()
    {
        var db = new MyDbContext();

        // テーブル作成
        db.Database.EnsureCreated();


        // レコード件数が0の場合
        if (!db.LastUpdate.Any())
        {
            // 追加
            string date = DateTime.Now.ToString("yyyy/MM/dd");
            db.Add(new LastUpdate { Memo = "オイル交換", Date = date, });
            db.Add(new LastUpdate { Memo = "メモ2", Date = date, });
            db.Add(new LastUpdate { Memo = "メモ3", Date = date, });
            db.SaveChanges();
        }
        UpdateListView();


        SelectedItem.Subscribe((o) =>
        {
            // SelectedName.Value = o?.Memo;
            
        });

        // 追加
        OnAddCliked.Subscribe((o) =>
        {
            string memo = String.Format("メモ{0}", (Items.Count+1));
            string date = DateTime.Now.ToString("yyyy/MM/dd");
            var card = new LastUpdate { Memo = memo, Date = date, };
            // テーブルに追加
            db.Add(card);
            db.SaveChanges();

            UpdateListView();
        });
    }
}

実行

「Add」ボタンを押す
「メモ4」が追加される。次に「メモ4」の行をクリックする。
詳細に切り替わる。
各項目は編集可能で編集を終了する場合「戻る」ボタンをクリック。

「日付更新」ボタンで当日日付に置き換わる。

「削除」ボタンを押すと「メモ4」が削除されます。

感想

普段使っているAndroidスマートフォンにインストールして動作することを確認しました。機能的にはViewで入力した内容に合わせてDBを追加、更新、削除するだけです。とりあえず動くようにはしましたがViewとViewModel、ViewModelとModelの関係がこのような実装で良いのかと不安があり、もっと学習の必要性があると感じています。

MAUIを使ってアプリケーションを作ってみて思ったのですが、1つのページ?をルーティングで使いまわす作りが、Webアプリケーションと似たような感じがします。あと、今回はデータをSQLiteに保存するスタンドアローンなアプリケーションを作成してみましたが、スマートフォン1つで完結する要件は余り多くなく、ネットクライアントとしての要件が多いと思います。そうなるとサーバーが欲しいところですが、個人でサーバーを用意するのはハードルが高いので、次の学習ステップとして何をするか検討中です。

コメント