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

[.NET MAUIとCleanArchitectureで簡単なアプリを作成してみる4～実装3～](https://qiita.com/GOROGOROMOGUMOGU/items/097e0c26f167f27b35c4)の続き
# Viewを作成する
[.NET MAUI環境をつくる](https://qiita.com/GOROGOROMOGUMOGU/items/a7a6263b0fdab8af8420#net-maui%E7%92%B0%E5%A2%83%E3%82%92%E3%81%A4%E3%81%8F%E3%82%8B)で追加した「.NET MAUI アプリ」プロジェクトをそのまま使います。
フォルダ構成は[フォルダ構成を決める(MauiApp1)](https://qiita.com/GOROGOROMOGUMOGU/items/3803ca4a09c0800b1c86#%E3%83%95%E3%82%A9%E3%83%AB%E3%83%80%E6%A7%8B%E6%88%90%E3%82%92%E6%B1%BA%E3%82%81%E3%82%8B)の通り
以下をプロジェクト参照しておく
　MauiApp1.Domain
　MauiApp1.SqlInfrastructure
　MauiApp1.UseCase

MauiApp1直下のMainPage.xamlを「MauiApp1¥Views」へ移動させる
名前空間を「MauiApp1.Views」に変更する
```diff_xml: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"
```
```diff_c_sharp:MainPage.xaml.cs
- namespace MauiApp1
+ namespace MauiApp1.Views
```
以下の箇所も変更が必要
```diff_xml: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3674221/f5fd043d-211e-3f85-b268-b74b090858af.png)

最初はこんな感じ
```xml: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を継承する
```c#: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/](https://github.com/beto-rodriguez/LiveCharts2/tree/master/samples/ViewModelsSamples)を参考に、グラフ用のデータを作成する
```c#: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から呼ぶようにする
　(これはどうあるべきなのだろう？)
```c#: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で受け取っている)
```c#: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 チュートリアル](https://learn.microsoft.com/ja-jp/dotnet/maui/tutorials/notes-app/?view=net-maui-8.0)
[LiveCharts2を使ってグラフを表示する](https://marunaka-blog.com/net-maui-livecharts2/8173/)
[LiveCharts2/samples/ViewModelsSamples/](https://github.com/beto-rodriguez/LiveCharts2/tree/master/samples/ViewModelsSamples)

# まとめ
今回はModel、ViewModel、Viewを実装しました。
次はViewのレイアウトを実装します。
[.NET MAUIとCleanArchitectureで簡単なアプリを作成してみる4～実装5～](https://qiita.com/GOROGOROMOGUMOGU/items/27d9390bea2c6f37b678)