C#でドメイン駆動開発 ViewModelの中に明細項目があるときの実装⑪

当サイトではアフィリエイト広告を利用しています。

C#でドメイン駆動開発

C#でドメイン駆動開発。今回はViewModelの中に

明細項目がる場合の実装を見ていきます。

画面の実装では一覧リストなどViewModelの中に

リスト項目を保持するケースはよくあると思いますので

そのあたりの解説をしていきます。

リスト表示

これまでは1行分のデータを扱ってきましたが,

今回は複数行のデータを扱うシナリオを考えてみましょう。

Testsプロジェクトに「MeasureListViewModelTest」を追加します。

    [TestClass]
    public class MeasureListViewModelTest
    {
        [TestMethod]
        public void 計測リスト_シナリオ()
        {
            var viewModel = new MeasureListViewModel();
        }
    }

ViewModelsフォルダにMeasureListViewModelクラスを新規で作成します。

MeasureListViewModelTestに戻り,

usingを追加してコンパイルエラーをなくします。

 

次にテストを書いていきます。

今回のシナリオは,

「計測リスト画面が表示したら,計測データが一覧に表示される」

というシナリオです。

テストでは2件表示されることにして次のように書きます。

 

[TestMethod]
public void 計測リスト_シナリオ()
{
    var viewModel = new MeasureListViewModel();
    viewModel.Measures.Count.Is(2);
}

コンパイルが通らないのでMeasureListViewModelに次のように書きます。

using System.ComponentModel;
namespace DDD.WinForm.ViewModels
{
    public class MeasureListViewModel : ViewModelBase
    {
        public BindingList<MeasureListViewModelMeasure> Measures { get; set; } = new BindingList<MeasureListViewModelMeasure>();
    }
}

BindingList<T>はWinFormでのバインドに使うコレクションです。

WPFであればObservableCollection<T> を使用します。

MeasureListViewModelMeasureという型はこれから作成する

一覧用のViewModelです。

MeasureListViewModelの中でMeasuresを表現するために

使用するViewModelなのでこう言った名前にしています。

MeasureListMeasureViewModelという名前にしてもよいとわ思いますが,

親となるViewModel「MeasureListViewModel」が

ソリューションエクスプローラー上一番上にくるほうがわかりやすいので

私はこのような命名にしています。

 

命名に関してはチームでコードレビューをして決定するのがよいでしょう。

ただマイクロソフトの定めた命名方法があり

.NETのクラスライブラリ設計 開発チーム直伝の設計原則,コーディング標準,パターン

に記載されていますので,ここにあるものはこれに倣ったほうがよいでしょう。

そのほうがマイクロソフトのフレームワークとの実装にも合いますし,

他のチームとの共存もしやすくなるでしょう。

 

それでは「MeasureListViewModelMeasure」を作成します。

場所は「ViewModes」フォルダに作成します。

namespace DDD.WinForm.ViewModels
{
    public class MeasureListViewModelMeasure
    {
    }
}

 

実装はまだ何もありません。

ここで一度テストしましょう。

MeasureListViewModelの「Measures」が0件のためテストが通りません。

画面起動時に計測データを取得するシナリオのため,次ように実装します。

using DDD.Domain.Repositories;
using System.ComponentModel;

namespace DDD.WinForm.ViewModels
{
    public class MeasureListViewModel : ViewModelBase
    {
        private IMeasureRepository _measureRepositor;
        public MeasureListViewModel(IMeasureRepository measureRepositor)
        {
            _measureRepositor = measureRepositor;
            foreach(var entity in _measureRepositor.GetData())
            {
                Measures.Add(new MeasureListViewModelMeasure());
            }
        }

        public BindingList<MeasureListViewModelMeasure> Measures { get; set; }
            = new BindingList<MeasureListViewModelMeasure>();
    }
}

 

IMeasureRepositoryに「GetData」を追加し,

MeasureEntityのリストで返却されるように定義します。

public interface IMeasureRepository
{
    MeasureEntity GetLatest();
    IReadOnlyList<MeasureEntity> GetData();
}

IMeasureRepositoryがコンパイルエラーになるためGetDataメソッドを追加します。

中身は未実装の例外「NotImplementedException」のままにしておきます。

internal sealed class MeasureFake : IMeasureRepository
{
    public IReadOnlyList<MeasureEntity> GetData()
    {
        throw new NotImplementedException();
    }
    
    public MeasureEntity GetLatest()
    {
        return new MeasureEntity("guidA", "2017/01/01 13:00:00".ToDate(), 1.23456f);
    }
}

テストコードがコンパイルエラーになるため

Mockを使用してコンストラクタの引数に設定します。

[TestMethod]
public void 計測リスト_シナリオ()
{
    var measureMock = new Mock<IMeasureRepository>();
    var measures = new List<MeasureEntity>
    {
        new MeasureEntity("guidA","2017/01/01 13:00:00".ToDate(),1.23456f),
        new MeasureEntity("guidB","2017/01/01 14:00:00".ToDate(),2.23456f),
    };
     measureMock.Setup(x => x.GetData()).Returns(measures);
    var viewModel = new MeasureListViewModel(measureMock.Object);
    viewModel.Measures.Count.Is(2);
}

これでテストをすると成功します。

ただこれではCountが2件あるというだけで,

計測リストに何が表示されているかがテストされていません。

 

そこで表示したい値をテストファーストで作成していきます。

計測リストに表示されるのは「計測日時」と「計測値」としましょう。

テストコードに次のコードを追加します。

viewModel.Measures[0].MeasureDate.Is("2017/01/01 13:00:00");
viewModel.Measures[0].MeasureValue.Is("1.23m/s");

「MeasureDate」と「MeasureVaule」はコンパイルエラーになるのでMeasureListViewModelMeasureに追加します。

 public class MeasureListViewModelMeasure
 {
     private MeasureEntity _entity;
     public MeasureListViewModelMeasure(MeasureEntity entity)
     {
         _entity = entity;
     }
     
     public string MeasureDate => _entity.MeasureDate.DisplayValue;
     public string MeasureValue => _entity.MeasureValue.DisplayValue;
 }  

 

「MeasureDate」と「MeasureVaule」の値は

引数で受け取ったMeasureEntityのVOのDisplayValueを表示するようにしています。

EntityにVOを乗せることで,

いろんな場所で統一した書式で表示することが可能になっています。

 

MeasureListViewModelではMeasureEntityを渡すように変更します。

public MeasureListViewModel(IMeasureRepository measureRepositor)
{
     _measureRepositor = measureRepositor;
     foreach(var entity in _measureRepositor.GetData())
     {
         Measures.Add(new MeasureListViewModelMeasure(entity));
     }
}

今回はMeasureEntityをカプセル化して_entityを保持していますが,

コンストラクタの引数で値を取得して次のようにしてもよいです。

 public MeasureListViewModelMeasure(string measureDate,string measureValue)
 {
     MeasureDate = measureDate;
     MeasureValue = measureValue;
 }
 
 public string MeasureDate { get; }
 public string MeasureValue { get; }

 

この場合は呼び出し元でVOのDisplayValueを投げます。

foreach (var entity in _measureRepositor.GetData())
 {
     Measures.Add(new MeasureListViewModelMeasure(
                         entity.MeasureDate.DisplayValue,
                         entity.MeasureValue.DisplayValue));
 }

このように本当に表示するだけであれば,

ドメインオブジェクト(MeasureEntity)を保持していなくてもよいでしょう。

DisplayValue以外にもドメイン要素が必要であれば

EntityまたはVOをカプセル化して,

プロパティでDisplayValueなどを選択するというコーディングになります。

 

また,Entityの項目が50項目くらいあるけど3項目しか使わない,

などという場合はEnityでカプセル化せず,

VOを3つカプセル化したほうが,メモリの節約になるでしょう。

 

これでテストを実行すると成功します。UIとインフラを作成しましょう。

まず,ViewsにMeasureListViewというフォームを追加します。

DataGridViewを張り付けDockをFillにしてNameを「MeasureDataGrid」にします。

画面のコードを表示し次のようにします。

public partial class MeasureListView : BaseForm
 {
     public MeasureListView()
     {
         InitializeComponent();
           var viewModel = new MeasureListViewModel();
         MeasureDataGrid.DataBindings.Add("DataSource", viewModel, nameof(viewModel.Measures));
     }
 }

ViewModelの生成でコンパイルエラーが出ているので

MeasureListViewModelに次のコンストラクタを追加します。

public MeasureListViewModel() : this(Factories.CreateMeasureRepository())
{
}

MenuViewにボタンを追加し「List」と表示します。クリックイベントで

「MeasureListView」が表示されるように記述します。

 

 private void ListButton_Click(object sender, EventArgs e)
 {
     using (var f = new MeasureListView())
     {
         f.ShowDialog();
     }
 }

実行してMenuViewのListボタンを押下してみましょう。

MeasureFakeのGetDataメソッドが未実装の例外を出しますね。

 

こうしておけば未実装部分を後回しにしていてもすぐに気づくことができるので,

この例を利用することをお勧めします。

 

ではMeasureFakeを実装しましょう。

private static List<MeasureEntity> _entities;
 static MeasureFake()
 {
     _entities = new List<MeasureEntity>
     {
         new MeasureEntity("guidA","2017/01/01 13:00:00".ToDate(),1.23456f),
         new MeasureEntity("guidB","2017/01/01 14:00:00".ToDate(),2.23456f),
     };
 }

public IReadOnlyList<MeasureEntity> GetData()
 {
     return _entities;
 }

public MeasureEntity GetLatest()
 {
     return new MeasureEntity("guidA", "2017/01/01 13:00:00".ToDate(), 1.23456f);
 }

_entitiesはstaticなリストでprivateでカプセル化しています。

アプリケーション全体で1つのリストを共有し,

MeasureFakeでしか変更できないようにしています。

 

こうすることで外部データをシミュレートしています。

staticなコンストラクタはMeasureFakeに何かがアクセスしたときに

一度だけ動作するため,最初に一回_entitiesが生成され,

初期値が2件作られます。GetDataではこの_entitiesを返却します。

 

ちなみに戻り値をIReadOnlyListとすることで外部から変更ができなくなり,

可読性,保守性が高まります。

 

List<T>はprivateで使用することを心掛け,

外部に公開するリストはReadOnlyにしてください。

 

それではF5で実行してみましょう。

Listボタンを押すと2件の計測データが表示されます。

 

VOを使用したことで「計測日時」「計測値」が,直近値画面や計測画面と同じ概念で動作しています。