はじめに
前回、.NET MAUIのデータストアについて調査しました。
ローカルDBとしてsqlite-net-pclを利用することができましたが、EntityFramework.Coreについては未調査だったので、試してみました。
また、サービスのDI(Dependency Injection = 依存性注入)が使えることが分かったので、それについても記載しておきます。
開発環境
- Win11
- VS2022 Preview Version 17.3.0 Preview 2.0
環境構築&プロジェクト作成の流れはこちらの記事でどうぞ。
サンプルソース
以前作った、データ検証(Validation)のソースの疑似ログイン処理を拡張していきます。
一式GitHubに公開してます。
NuGet
Microsoft.EntityFrameworkCore.Sqlite
EF Core のSQLite実装をNuGetします。
Table
ユーザーテーブルを作成します。
using MauiAppDemo.Pages;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MauiAppDemo.Models
{
[Table(nameof(User))]
public class User : ValidationPropertyModel
{
private int _No;
private string _Name;
private string _Password;
[Display(Order = 1, Name = nameof(Messages.User_No), ResourceType = typeof(Messages))]
[Key]
[Required(ErrorMessageResourceName = nameof(Messages.Error_Required), ErrorMessageResourceType = typeof(Messages))]
[Range(1, 9999, ErrorMessageResourceName = nameof(Messages.Error_Range), ErrorMessageResourceType = typeof(Messages))]
public int No
{
get => _No;
set => SetProperty(ref _No, value);
}
[Column(nameof(Name))]
[Display(Order = 2, Name = nameof(Messages.User_Name), ResourceType = typeof(Messages))]
[Required(ErrorMessageResourceName = nameof(Messages.Error_Required), ErrorMessageResourceType = typeof(Messages))]
[MinLength(1, ErrorMessageResourceName = nameof(Messages.Error_Required), ErrorMessageResourceType = typeof(Messages))]
public string Name
{
get => _Name;
set => SetProperty(ref _Name, value);
}
[Column(nameof(Password))]
[Display(Order = 3, Name = nameof(Messages.User_Password), ResourceType = typeof(Messages))]
[Required(ErrorMessageResourceName = nameof(Messages.Error_Required), ErrorMessageResourceType = typeof(Messages))]
[MinLength(1, ErrorMessageResourceName = nameof(Messages.Error_Required), ErrorMessageResourceType = typeof(Messages))]
public string Password
{
get => _Password;
set => SetProperty(ref _Password, value);
}
}
}
Database
User テーブルのみのデータベースを作成します。
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MauiAppDemo.Models
{
public class MauiAppDemoDbContext : DbContext
{
public DbSet<User> Users { get; set; }
private string connStr;
public MauiAppDemoDbContext()
{
connStr = Path.Combine(FileSystem.AppDataDirectory, "MauiAppDemo.sq3");
}
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseSqlite($"Data Source={connStr}");
}
}
DI(Dependency Injection) in .NET MAUI
アプリケーション起動時にDbContextを作ったら、いろんなページから利用したいところです。
そこで、.NET MAUIのサービスのDIの仕組みを利用します。
サービスを作成
DbContext取得サービスのインターフェース
using MauiAppDemo.Models;
namespace MauiAppDemo.Services;
public interface IMauiAppDemoService
{
public MauiAppDemoDbContext DBContext { get; }
}
DbContext取得サービスの実装
※DBを作成、テーブルがなければ作成、初期ユーザーがいなければ追加
using MauiAppDemo.Models;
using MauiAppDemo.Utils;
namespace MauiAppDemo.Services;
public class MauiAppDemoService : IMauiAppDemoService
{
private MauiAppDemoDbContext ctx;
public MauiAppDemoDbContext DBContext
{
get
{
if (ctx == null)
{
ctx = new MauiAppDemoDbContext();
ctx.Database.EnsureCreated();
using (var t = ctx.Database.BeginTransaction())
{
var count = ctx.Users.Count();
if (count == 0)
{
ctx.Users.Add(new User() { No = 1234, Name = "Kashin777", Password = Hashs.Sha256("1234", "1234") });
ctx.Users.Add(new User() { No = 9999, Name = "User9999", Password = Hashs.Sha256("9999", "Password") });
}
t.Commit();
}
}
return ctx;
}
}
}
DIの設定
DbContext取得サービス、それを利用したいクラス(ViewModel、ContentPage)をサービスに追加します。
※ソースの★の部分
using Microsoft.Maui.Platform;
using MauiAppDemo.Pages;
using MauiAppDemo.Services;
namespace MauiAppDemo;
public static partial class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
// ★ Depedency Injection
builder.Services
.AddTransient<LoginPage>()
.AddTransient<LoginPageViewModel>()
.AddTransient<MainPage>()
.AddTransient<MainPageViewModel>()
.AddSingleton<IMauiAppDemoService, MauiAppDemoService>();
// プラットフォームに応じたカスタマイズ
Microsoft.Maui.Handlers.EntryHandler.Mapper.ModifyMapping(nameof(IEntry.Background), (handler, entry, action) =>
{
#if ANDROID
handler.PlatformView.BackgroundTintList = Android.Content.Res.ColorStateList.ValueOf(Colors.Gray.ToPlatform());
#endif
});
return builder.Build();
}
}
Add~によって、DIされるインスタンスのライフサイクルが変わります。
// 都度生成
builder.Services.AddTransient()
// スコープ間で1回生成
builder.Services.AddScoped()
// 常に1個
builder.Services.AddSingleton()
DI適用後のログイン処理
ContentPage -> ViewModel -> DbContextサービスの流れでDIします。
コントラクタ引数を追加することで、勝手にインスタンスが割り当てられます。
※ソースの★部分
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Windows.Input;
using MauiAppDemo.Models;
using MauiAppDemo.Utils;
using MauiAppDemo.Services;
namespace MauiAppDemo.Pages;
/// <summary>
/// ログインページ。
/// </summary>
public partial class LoginPage : ContentPage
{
//★ LoginPageにViewModelをDI
public LoginPage(LoginPageViewModel viewModel)
{
// 言語を変更
var culture = new CultureInfo(Preferences.Get("Language", "ja"));
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
InitializeComponent();
//★ DIされたViewModelをBindingContextに設定
BindingContext = viewModel;
}
}
/// <summary>
/// ログインページのビューモデル。
/// </summary>
public class LoginPageViewModel : ValidationPropertyViewModel
{
private StringKeyValuPair _Language = new StringKeyValuPair() { Key = Preferences.Get("Language", "ja") };
private int? _No;
private string _Password;
/// <summary>
/// 社員番号
/// </summary>
[Display(Order = 1, Name = nameof(Messages.Login_No), ResourceType = typeof(Messages))]
[Required(ErrorMessageResourceName = nameof(Messages.Error_Required), ErrorMessageResourceType = typeof(Messages))]
[Range(1, 9999, ErrorMessageResourceName = nameof(Messages.Error_Range), ErrorMessageResourceType = typeof(Messages))]
public int? No {
set
{
SetProperty(ref _No, value);
ChangeCanExecute(LoginCommand);
}
get => _No;
}
/// <summary>
/// パスワード
/// </summary>
[Display(Order = 2, Name = nameof(Messages.Login_Password), ResourceType = typeof(Messages))]
[Required(ErrorMessageResourceName = nameof(Messages.Error_Required), ErrorMessageResourceType = typeof(Messages))]
[MinLength(1, ErrorMessageResourceName = nameof(Messages.Error_Required), ErrorMessageResourceType = typeof(Messages))]
public string Password
{
set
{
SetProperty(ref _Password, value);
ChangeCanExecute(LoginCommand);
}
get => _Password;
}
/// <summary>
/// 言語
/// </summary>
public StringKeyValuPair Language {
set {
SetProperty(ref _Language, value);
ChangeCanExecute(LanguageCommand);
}
get => _Language;
}
/// <summary>
/// 言語変更コマンド。
/// </summary>
public ICommand LanguageCommand { protected set; get; }
/// <summary>
/// ログインコマンド。
/// </summary>
public ICommand LoginCommand { protected set; get; }
//★ ViewModelにDbContext取得サービスをDI
public LoginPageViewModel(IMauiAppDemoService service)
{
// 言語変更コマンドの実装
LanguageCommand = new Command(() =>
{
// 言語を保存
Preferences.Set("Language", Language.Key);
// 画面を初期化
App.Current.MainPage = new AppShell();
},
() => {
return !Language.Key.Equals(CultureInfo.CurrentUICulture.TwoLetterISOLanguageName);
});
// ログインコマンドの実装
LoginCommand = new Command(async() =>
{
// データ検証
if (Validate())
{
//★ DbContextを取得
var ctx = service.DBContext;
// 社員番号とパスワードが一致するか?
var user = await ctx.FindAsync<User>(No);
if (user != null && user.Password.Equals(Hashs.Sha256(user.No.ToString(), Password)))
{
// メインページへ移動
await Shell.Current.GoToAsync($"///MainPage");
// ログインユーザを通知
MessagingCenter.Send<User>(user, "Login");
// 初期化
No = null;
Password = null;
ClearErrors();
}
// 一致しない場合
else
{
AddModelError(Messages.Error_Comman_Login);
}
}
ChangeCanExecute(LoginCommand);
},
() =>
{
// 画面にエラーが出ている場合は無効
return !HasErrors;
});
// コマンドを登録
AddCommands(LanguageCommand, LoginCommand);
}
}
ご参考