1
1

.NET MAUIとCleanArchitectureで簡単なアプリを作成してみる4~実装4~

Last updated at Posted at 2024-01-22

.NET MAUIとCleanArchitectureで簡単なアプリを作成してみる4~実装3~の続き

Viewを作成する

.NET MAUI環境をつくるで追加した「.NET MAUI アプリ」プロジェクトをそのまま使います。
フォルダ構成はフォルダ構成を決める(MauiApp1)の通り
以下をプロジェクト参照しておく
 MauiApp1.Domain
 MauiApp1.SqlInfrastructure
 MauiApp1.UseCase

MauiApp1直下のMainPage.xamlを「MauiApp1¥Views」へ移動させる
名前空間を「MauiApp1.Views」に変更する

MainPage.xaml
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
-             x:Class="MauiApp1.MainPage"
+             x:Class="MauiApp1.Views.MainPage"
MainPage.xaml.cs
- namespace MauiApp1
+ namespace MauiApp1.Views

以下の箇所も変更が必要

AppShell.xaml
<Shell
    x:Class="MauiApp1.AppShell"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
-    xmlns:local="clr-namespace:MauiApp1"
+    xmlns:local="clr-namespace:MauiApp1.Views"
    Shell.FlyoutBehavior="Disabled"
    Title="MauiApp1">

※AppShellはアプリのビジュアル階層を定義するクラスです。

「MauiApp1¥Views」配下にHealthEditPage.xamlを追加する
「.NET MAUI ContentPage (XAML)」を選択

HealthEditPage.PNG

最初はこんな感じ

HealthEditPage.xaml
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MauiApp1.Views.HealthEditPage"
             Title="HealthEditPage">
    <VerticalStackLayout>
        <Label 
            Text="Welcome to .NET MAUI!"
            VerticalOptions="Center" 
            HorizontalOptions="Center" />
    </VerticalStackLayout>
</ContentPage>

レイアウトは後で実装します。

Modelを作成する

体調管理Modelを作成する

ハート数の変化に合わせてハートマーク1~5の表示/非表示を切り替えたいので、ObservableObjectを継承する

Health.cs
using CommunityToolkit.Mvvm.ComponentModel;

    internal class Health : ObservableObject
    {
        /// <summary>
        /// コンストラクタ
        /// </summary>
        public Health()
        {
            RecordDate = DateTime.Now;
            HeartNumber = 3;
        }

        /// <summary>
        /// ID
        /// </summary>
        public int Id { get; set; }
        /// <summary>
        /// 記録日付
        /// </summary>
        public DateTime RecordDate { get; set; }

        private int _heartNumber;
        /// <summary>
        /// ハート数
        /// </summary>
        public int HeartNumber
        {
            get => this._heartNumber;
            set
            {
                if (this._heartNumber != value)
                {
                    this._heartNumber = value;

                    OnPropertyChanged();
                    OnPropertyChanged(nameof(IsVisibleHeart1));
                    OnPropertyChanged(nameof(IsVisibleHeart2));
                    OnPropertyChanged(nameof(IsVisibleHeart3));
                    OnPropertyChanged(nameof(IsVisibleHeart4));
                    OnPropertyChanged(nameof(IsVisibleHeart5));
                }
            }
        }

        /// <summary>
        /// ハート1表示/非表示
        /// </summary>
        public bool IsVisibleHeart1 { get => HeartNumber >= 1; }

        /// <summary>
        /// ハート2表示/非表示
        /// </summary>
        public bool IsVisibleHeart2 { get => HeartNumber >= 2; }

        /// <summary>
        /// ハート3表示/非表示
        /// </summary>
        public bool IsVisibleHeart3 { get => HeartNumber >= 3; }

        /// <summary>
        /// ハート4表示/非表示
        /// </summary>
        public bool IsVisibleHeart4 { get => HeartNumber >= 4; }

        /// <summary>
        /// ハート5表示/非表示
        /// </summary>
        public bool IsVisibleHeart5 { get => HeartNumber >= 5; }
    }

グラフ用のModelを作成する

LiveCharts2/samples/ViewModelsSamples/を参考に、グラフ用のデータを作成する

HealthGraph.cs
using CommunityToolkit.Mvvm.ComponentModel;
using LiveChartsCore;
using LiveChartsCore.Defaults;
using LiveChartsCore.SkiaSharpView;

    internal class HealthGraph : ObservableObject
    {
        /// <summary>
        /// X軸ラベル
        /// </summary>
        public Axis[] XAxes => new Axis[]
        {
            new() { Labeler = d => string.Format("{0:dd}", DateTime.FromOADate(d)) }
        };

        /// <summary>
        /// データ系列
        /// </summary>
        public ISeries[] Series { get; private set; } = Array.Empty<LineSeries<ObservablePoint>>();

        /// <summary>
        /// グラフデータを作成する
        /// </summary>
        /// <param name="startDate"></param>
        /// <param name="endDate"></param>
        /// <returns></returns>
        public async Task CreateGraph(DateTime startDate, DateTime endDate)
        {
            // 指定した期間の体調情報を取得する
            var interactor = ServiceProvider.GetService<IHealthGetListUseCase>();
            var request = new HealthGetListRequest(startDate, endDate);
            var response = await Task.Run(() => { return interactor.Handle(request); });

            if (response == null) return;

            var values = response.Healths.Select(h => new ObservablePoint(h.RecordDate.ToOADate(), h.HeartNumber)).ToArray();

            // データ系列を作成する
            Series = new LineSeries<ObservablePoint>[] 
            { 
                new() { Values = values  } 
            };

            OnPropertyChanged(nameof(Series));
        }
    }

ViewModelを作成する

メイン画面のViewModelを作成する

画面読み込み時に本日の体調情報の有無を確認したいので、Loadメソッドを作成する
→ コンストラクタで非同期処理を呼ぶのはどうかと思うので、Viewから呼ぶようにする
 (これはどうあるべきなのだろう?)

MainViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MauiApp1.UseCase.Healths.Create;
using MauiApp1.UseCase.Healths.GetList;
using System.Windows.Input;

    internal class MainViewModel : ObservableObject
    {
        private Models.Health _health;
        /// <summary>
        /// 本日の体調情報
        /// </summary>
        public Models.Health Health
        {
            get => this._health;
        }

        private DateTime _startDate;
        /// <summary>
        /// グラフの開始日
        /// </summary>
        public DateTime StartDate
        {
            get => this._startDate;
            set
            {
                if (this._startDate != value)
                {
                    this._startDate = value;
                    OnPropertyChanged();

                    _ = RefreshGraph();
                }
            }
        }

        private DateTime _endDate;
        /// <summary>
        /// グラフの終了日
        /// </summary>
        public DateTime EndDate
        {
            get => this._endDate;
            set
            {
                if (this._endDate != value)
                {
                    this._endDate = value;
                    OnPropertyChanged();

                    _ = RefreshGraph();
                }
            }
        }

        private Models.HealthGraph _healthGraph;
        /// <summary>
        /// 体調情報のグラフデータ
        /// </summary>
        public Models.HealthGraph HealthGraph
        {
            get => this._healthGraph;
        }

        private DateTime _selectedDate;
        /// <summary>
        /// 編集する日付
        /// </summary>
        public DateTime SelectedDate
        {
            get => this._selectedDate;
            set
            {
                if (this._selectedDate != value)
                {
                    this._selectedDate = value;
                    OnPropertyChanged();
                }
            }
        }

        /// <summary>
        /// 保存コマンド
        /// </summary>
        public ICommand SaveCommand { get; private set; }
        /// <summary>
        /// 編集コマンド
        /// </summary>
        public ICommand EditCommand { get; private set; }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public MainViewModel()
        {
            this._health = new Models.Health();
            this._healthGraph = new Models.HealthGraph();

            this._startDate = this._health.RecordDate.AddDays(-5);
            this._endDate = this._health.RecordDate;

            SelectedDate = this._health.RecordDate;

            SaveCommand = new AsyncRelayCommand(Save);
            EditCommand = new AsyncRelayCommand(Edit);
        }

        /// <summary>
        /// 体調情報を取得する
        /// </summary>
        /// <returns></returns>
        public async Task Load()
        {
            if (await Load(Health) is HealthDto dto)
            {
                Health.Id = dto.Id;
                Health.HeartNumber = dto.HeartNumber;
            }

            // グラフを更新する
            await RefreshGraph();
        }

        /// <summary>
        /// DBから体調情報を取得する
        /// </summary>
        /// <param name="health"></param>
        /// <returns></returns>
        private async Task<HealthDto> Load(Models.Health health)
        {
            var interactor = ServiceProvider.GetService<IHealthGetListUseCase>();

            var request = new HealthGetListRequest(health.RecordDate.Date, health.RecordDate);
            var response = await Task.Run(() => { return interactor.Handle(request); });

            return response?.Healths?.FirstOrDefault();
        }

        /// <summary>
        /// グラフを更新する
        /// </summary>
        /// <returns></returns>
        private async Task RefreshGraph()
        {
            await HealthGraph.CreateGraph(StartDate, EndDate);
        }

        /// <summary>
        /// 体調情報を保存する
        /// </summary>
        /// <returns></returns>
        private async Task Save()
        {
            var interactor = ServiceProvider.GetService<IHealthCreateUseCase>();

            var request = new HealthCreateRequest(Health.Id, Health.RecordDate, Health.HeartNumber);
            var response = await Task.Run(() => { return interactor.Handle(request); });

            Health.Id = response.Id;

            // グラフを更新する
            await RefreshGraph();
        }

        /// <summary>
        /// 編集画面に遷移する
        /// </summary>
        /// <returns></returns>
        private async Task Edit()
        {
            var health = new Models.Health() { RecordDate = SelectedDate };

            if (await Load(health) is HealthDto dto)
            {
                health.Id = dto.Id;
                health.HeartNumber = dto.HeartNumber;
            }

            // 編集画面に渡すパラメータ
            var navigationParameter = new Dictionary<string, object>
            {
                { "health", health },
            };

            // パラメータを渡して編集画面に遷移する
            await Shell.Current.GoToAsync("HealthEditPage", navigationParameter);
        }
    }

編集画面のViewModelを作成する

メイン画面からパラメータを受け取るためにIQueryAttributableを実装する
(ApplyQueryAttributesで受け取っている)

HealthEditViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MauiApp1.UseCase.Healths.Create;
using System.Windows.Input;

    internal class HealthEditViewModel : ObservableObject, IQueryAttributable
    {
        private Models.Health _health;
        /// <summary>
        /// 本日の体調情報
        /// </summary>
        public Models.Health Health
        {
            get => this._health;
        }

        private DateTime _selectedDate;
        /// <summary>
        /// 編集する日付
        /// </summary>
        public DateTime SelectedDate
        {
            get => this._selectedDate;
            set
            {
                if (this._selectedDate != value)
                {
                    this._selectedDate = value;
                    OnPropertyChanged();
                }
            }
        }

        /// <summary>
        /// 保存コマンド
        /// </summary>
        public ICommand SaveCommand { get; private set; }
        /// <summary>
        /// 戻るコマンド
        /// </summary>
        public ICommand ReturnCommand { get; private set; }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public HealthEditViewModel()
        {
            this._health = new Models.Health();

            SaveCommand = new AsyncRelayCommand(Save);
            ReturnCommand = new AsyncRelayCommand(Return);
        }

        /// <summary>
        /// メイン画面からのパラメータを受け取る
        /// </summary>
        /// <param name="query"></param>
        public void ApplyQueryAttributes(IDictionary<string, object> query)
        {
            if (query.ContainsKey("health") && query["health"] is Models.Health health)
            {
                this._health = health;
                SelectedDate = this._health.RecordDate;

                OnPropertyChanged(nameof(Health));
            }
        }

        /// <summary>
        /// 体調情報を保存する
        /// </summary>
        /// <returns></returns>
        private async Task Save()
        {
            var interactor = ServiceProvider.GetService<IHealthCreateUseCase>();

            var request = new HealthCreateRequest(Health.Id, Health.RecordDate, Health.HeartNumber);
            var response = await Task.Run(() => { return interactor.Handle(request); });
        }

        /// <summary>
        /// メイン画面に戻る
        /// </summary>
        /// <returns></returns>
        private async Task Return()
        {
            await Shell.Current.GoToAsync("///MainPage");
        }
    }

参考にしたURL

.NET MAUI チュートリアル
LiveCharts2を使ってグラフを表示する
LiveCharts2/samples/ViewModelsSamples/

まとめ

今回はModel、ViewModel、Viewを実装しました。
次はViewのレイアウトを実装します。
.NET MAUIとCleanArchitectureで簡単なアプリを作成してみる4~実装5~

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1