狐好きぷろぐらまー

狐好きプログラマーのブログです。

食材管理アプリを作ってみる その3

お久しぶりです。半年ぶりですね。

継続してアウトプットするのは難しいですね、自分でモチベーション上げて書いていきたいと思います。

それではその3書いていきます。

実装する予定の項目

優先度高の機能

  1. 食材データ管理(CRUD)
  2. カレンダー表示画面
  3. 食材データのエクスポート / インポート (Json形式)
  4. 食材データ管理画面
  5. 食材賞味期限表示
  6. ダッシュボード画面表示 (内容固定)

優先度中の機能

  1. 食材の栄養素をレーダーチャートで表示
  2. ダッシュボード画面表示(内容可変)

優先度低の機能

  1. 食材の調理候補検索機能

この記事の流れ

優先度高の1の食材データ管理(CRUD)から実装していこうと思います。

ViewModelのコレクションについても下の実装の中で、できるだけ説明したいと思います。

この記事のフローとしては

  1. 単体Modelの食材データ管理クラスの作成 <-- その2で実施
  2. 複数Modelの食材データ管理クラスの作成 <-- 今回実施
  3. 単体ViewModelの食材データ管理クラスの作成 <-- その2で実施 + 今回で確認
  4. ViewModelのコレクションクラスの作成 <-- 今回実施

という流れで書いていこうと思います。

既に1.の単体のModelと3.のViewModelは実装しましたが、

3.のViewModelはさらっと確認しただけでしたので、今回はパラメータを変更して確認してみたいと思います。

1. 単体Modelの食材データ管理クラスの作成

この項目は前回(その2)に記載した内容です。

f:id:pregum_fox:20190416233718p:plain
食材データのクラス図

FoodクラスはPrism.Mvvm名前空間に存在するBindableBase抽象クラスを実装しています。

これによりINotifyPropertyChangedインターフェースのPropertyChangedイベント(以降 通知イベントと呼びます)の発行の記述を簡略化しています。

後、Foodクラスのuint:idをGuid:Guidに変更しています。こちらの方が管理がしやすいので変更しました。

実際にBindableBase抽象クラスを使用している一部を記載します。

    /// <summary>
    /// 食材を表すクラス
    /// </summary>
    public class Food : BindableBase
    {
        /// <summary>
        /// Id
        /// </summary>
        private Guid _id;
        public Guid ID
        {
            get { return _id; }
            set { this.SetProperty(ref _id, value); }
            // 上記と同じ処理内容の記述(INotifyPropertyChanged実装時)
            // set {
            //       this._id = value;
            //       this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.Id)));
            //     }

         }

    // 省略...
     }

上記のIdプロパティのsetterでBindableBase抽象クラスのSetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null);を使用しています。これによって、プロパティを変更すれば自動で通知イベントが発行されます。

Modelについては今回は通知イベントを受け取ることがない(他のModelがあると他Modelの状態によって通知イベントを受け取ることもある)ので、通知イベントを送る機能のみを持たせています。

2. 複数Modelの食材データ管理クラスの作成

次に複数の食材をまとめて管理する為の食材コレクションクラスを作成します。 以下にクラス図を表示します。

f:id:pregum_fox:20190419070939p:plain
FoodModelコレクションクラスのクラス図

FoodCollectionが食材データを管理する為のクラスです。

今回はLoad・Saveの機能は実装していません。Load・Save機能については別の回で実装する予定です。

FoodCollectionはプライベートメンバ変数にFoodContainer : IFoodContainerを持っています。このメンバ変数がこのクラスの核となっています。

CRUD処理はこのFoodContainerメンバに対して行うことで実現しています。

本来はここでDBを使用するのが良いと思いますが、まだ学習途中の為ここはファイルベースでデータの管理を行います。

テストを書くことを考慮し、IFoodContainerインターフェースに依存しています。

こうすることで、FoodCollectionクラスがFileFoodContainerクラスに依存しないようにしています。

このインターフェースはFoodCollectionのコンストラクタ時の引数で受け取るようにしてCrud処理を引数から受け取ったクラスに受け渡しています。

        /// <summary>
        /// Crud処理を実装しているインターフェース
        /// </summary>
        private IFoodContainer _foodContainer;

        public FoodCollection(IFoodContainer foodContainer)
        {
            this._foodContainer = foodContainer;
            this._foodContainer.CollectionChanged += this._foodContainer_CollectionChanged;
        }

3. 単体ViewModelの食材データ管理クラスの作成

単体の食材データのViewModelの作成を行います。

こちらもその2で作成しているので大きな変更はありませんが、

idの型がGuidに変更されているのでそことコンストラクタでバインドしている行を変更します。

    // 変更前
    // public ReactiveProperty<uint> Id { get; }
    // 変更後
    public ReactiveProperty<Guid> ID { get; }


    // 省略...

    // 変更前
    //this.Id = food.ObserveProperty(x => x.ID)
    //              .ToReactiveProperty().AddTo(this.Disposable);
    // 変更後
    this.ID = food.ObserveProperty(x => x.ID)
                    .ToReactiveProperty().AddTo(this.Disposable);

上記の変更を確認する為に、デバッグ用の画面を作成します。

Viewフォルダ以下にViewModelTest.xamlを作成します。

f:id:pregum_fox:20190417211538p:plain
ViewModelTest.xamlの追加

View/ViewModelTest.xaml

<Window x:Class="Wpf_FoodManager.View.ViewModelTest"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:m="clr-namespace:Wpf_FoodManager.Model"
        xmlns:vm="clr-namespace:Wpf_FoodManager.ViewModel"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Wpf_FoodManager"
        mc:Ignorable="d"
        Title="ViewModelTest" Height="450" Width="800">

    <Grid>
        <StackPanel Margin="10">
            <StackPanel.Resources>
                <Style TargetType="{x:Type TextBlock}">
                    <Setter Property="FontSize" Value="14" />
                </Style>
                <Style TargetType="{x:Type TextBox}" >
                    <Setter Property="FontSize" Value="14" />
                </Style>
            </StackPanel.Resources>
            <TextBlock Text="Guid" />
            <TextBox Text="{Binding ID.Value}" />
            <TextBlock Text="名前" />
            <TextBox Text="{Binding Name.Value}" />
            <TextBlock Text="期限日" />
            <TextBox Text="{Binding LimitDate.Value}" />
            <TextBlock Text="購入日" />
            <TextBox Text="{Binding BoughtDate.Value}" />
            <TextBlock Text="イメージ" />
            <Image Source="{Binding Image.Value}" Height="50" />
            <Button Content="変更!" Click="Button_Click" Grid.Row="1" Margin="10" />
        </StackPanel>

    </Grid>
</Window>

対応するプロパティにValueをつけることを忘れないでください。私はここで一晩悩みました。。。

ViewModelTest.xaml.csにはボタンのClickイベントでFoodオブジェクトのNameプロパティを変更する処理を書きます。

using System;
using System.Windows;
using System.Windows.Media.Imaging;

namespace Wpf_FoodManager.View
{
    /// <summary>
    /// ViewModelTest.xaml の相互作用ロジック
    /// </summary>
    public partial class ViewModelTest : Window
    {
        /// <summary>
        /// Model
        /// </summary>
        private Model.Food _foodModel;

        /// <summary>
        /// ViewModel
        /// </summary>
        public ViewModel.FoodViewModel foodViewModel { get; set; }

        public ViewModelTest()
        {
            InitializeComponent();

            var ram = new Random();
            _foodModel = new Model.Food("秋刀魚", new DateTime(2019, 4, 20), new DateTime(2019, 4, 27), new BitmapImage());
            FoodViewModel = new ViewModel.FoodViewModel(_foodModel);
            this.DataContext = FoodViewModel;

        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            this._foodModel.Name = "変更しました!";
        }
    }
}

上記の処理を図にすると以下の通りです。

f:id:pregum_fox:20190417221450p:plain
バインディングのシーケンス図

そして最後にApp.xamlのStartupUri属性をViewModelTest.xamlに設定します。 こうすることで、ViewModelTest.xamlが最初に呼ばれるようになります。

App.xaml

<Application x:Class="Wpf_FoodManager.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:Wpf_FoodManager"
             StartupUri="View/ViewModelTest.xaml">
             <!-- 変更前 StartupUri="MainWindow.xaml">-->
    <Application.Resources>
         
    </Application.Resources>
</Application>

Starturi属性の最初にViewがついているのは名前空間のViewです。 ViewModelTest.xamlのみ記述していると、以下の画面のように非存在エラーが発生します。

f:id:pregum_fox:20190417220038p:plain
ViewModelTest.xaml非存在エラー

実行した結果が以下の通りです。

f:id:pregum_fox:20190417224122g:plain
実行結果

クリック後、名前の項目の"秋刀魚"が"変更しました!"に変更されていることが確認できました。

以上で単体ViewModelの作成・確認作業を終了します。

4. ViewModelのコレクションクラスの作成

それではViewModelのコレクションクラスを作成したいと思います。

クラス図は以下の通りです。

f:id:pregum_fox:20190419062003p:plain
ViewModelのコレクションクラスの関係図

作った私自身、わかりづらいなと思うほどなので初めて見た方はもっとわかりづらいものだと思います。

もう少しModelとViewModelのみに注目したクラス図を以下に記載します。

f:id:pregum_fox:20190419062429p:plain
ViewModelとModelのクラス図

クラス図を見てもらうとわかると思いますが、FoodViewModelもFoodModelも各コレクションクラスに関連づいています。(当たり前といえば当たり前ですが。)

このように単体Modelクラスの変更通知を購読する単体ViewModelクラスとModelコレクションクラスの変更通知を購読するViewModelコレクションクラスを用意することによって以下のようなメリットがあると思います。

  • 詳細ページに遷移したい時は、単体ViewModelをViewに渡せば情報の受け渡しができる。
  • 単体ViewModelは単体Modelのプロパティの変更通知の購読、ViewModelコレクションクラスはコレクションの変更通知の購読というように機能の分割ができる。

今回は単純なものなので、ModelとViewModelが似たような構成になりましたが、実際の物だと複数のModelに対してViewが作成されたり、1つのModelを複数のViewModelに分割したりすることがあると思いますので、その度にクラスの構成を考える必要があると思います。

長くなりましたが、画面を載せたいと思います。

f:id:pregum_fox:20190419064135g:plain
ViewModelコレクションの動作確認

上記は、ViewModelコレクションのテスト用に作成した画面です。

ご覧になるとわかるかと思いますが、変更ボタンをクリックしたら、項目が追加されています。 これはFoodViewModelCollectionに追加はしておらず、FoodModelCollectionに追加してそこから通知イベントを受け取りFoodViewModelCollectionに反映させています。

大分長くなってしまったのでここまでの変更を以下のgithubに更新しましたので、興味を持たれた方はのぞいてみてください。

起動時はApp.xamlのStartUri属性に設定されているxamlが最初に表示されます。

github.com

ここまで読んで下さりありがとうございました。

もし、間違ったことを書いていましたら教えて頂けると幸いです。

もしかすると自分の確認用にコレクションの記事を書くかもしれません。。。

書きました。

pregum-fox.hatenablog.jp