ユースケース「従業員を編集する」の実装
先にも記載した通り、本ユースケースのUIからデータベースまでの相互作用は、基本的に「従業員を閲覧する」ユースケースと違いがありません。本ユースケースをアーキテクチャ設計として記載する目的は、ユーザーインターフェースの詳細な実装アーキテクチャを決定・解説することにあります。そのため、実際のユーザーインターフェースの操作を追いながら、詳細を記述していきたいと思います。
プロパティの変更通知の実装
さて本ユースケースは、「従業員を閲覧する」ユースケースを拡張して実現します。具体的には、「従業員を閲覧する」ユースケースを実行後、次の画面の「編集」ボタンを押下することでユースケースを開始します。
元々、SPREAD for WPFは編集に対応しているところを、「従業員を閲覧する」ユースケースで読み取り専用で表示しているため、編集ボタンを押下することで編集可能に状態を変更することで、ユースケースを開始します。
「従業員を閲覧する」ユースケースの解説でもお見せしましたが、XAML上で編集可否を制御している箇所をもう一度見てみましょう。
<sg:GcSpreadGrid Grid.Row="1" ItemsSource="{Binding ManagedEmployees}" AutoGenerateColumns="False" Locked="{Binding IsReadOnly}" Protected="{Binding IsReadOnly}"
初期はLockedとProtectedの二つのプロパティにtrueが設定されているため、編集ボタンを押下してIsReadOnlyをfalseに変更します。
このとき変更を通知するため、通常はINotifyPropertyChangedインターフェースを実装した、次のようなコードを記述する必要があります。
public class ManagedEmployeeListViewModel : INotifyPropertyChanged { private bool _isReadOnly = true; public bool IsReadOnly { get => _isReadOnly; set => SetProperty(value, ref _isReadOnly); } public event PropertyChangedEventHandler PropertyChanged; protected bool SetProperty<T>( T value, ref T field, [CallerMemberName] string propertyName = null) { if (Equals(field, value)) return false; field = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); return true; } }
正直、通知が必要なプロパティに全てこの記述を行うのは苦痛です。苦痛な上に、不具合の温床でもあります。
そこで本システムではPropertyChanged.Fodyを利用することとします。PropertyChanged.Fodyを利用することで、先のコードと同等のコードが次のように記述するのみで実現できます。
[AddINotifyPropertyChangedInterface] public class ManagedEmployeeListViewModel : ViewModelBase { public bool IsReadOnly { get; set; } = true; }
PropertyChanged.FodyはFodyという静的コード生成フレームワークを利用しています。
前述のコードではクラスの属性としてAddINotifyPropertyChangedInterfaceが宣言されています。PropertyChanged.Fodyでは、AddINotifyPropertyChangedInterface属性が宣言されたクラスに対して、コンパイル時にコンパイルされたバイトコードを修正して、INotifyPropertyChangedを利用した変更通知に必要な処理を自動的に組み込んでくれます。
詳細は英語になりますが、次のサイトが参考になります。
編集状態の変更に伴うボタンの状態管理
従業員一覧画面には、編集ボタンと保存ボタンがあります。これらのボタンはそれぞれ、閲覧中は編集ボタンのみが押下でき、編集中は保存ボタンのみが押下できるように、状態を管理する必要があります。
ボタンには次のように、ViewModelのEditCommandとSaveCommandをバインドします。
<Button Style="{DynamicResource AccentedSquareButtonStyle}" Width="100" Margin="10, 0" Content="編集" Command="{Binding EditCommand}"/> <Button Style="{DynamicResource AccentedSquareButtonStyle}" Width="100" Margin="10, 0" Content="保存" Command="{Binding SaveCommand}"/>
ボタンはバインドされているCommandのCanExecuteがtrueの場合に押下可能となります。
ICommandの詳細は次の記事も参考にしてください。
ここでは先にも記載した、IsReadOnlyプロパティの状態によって制御すればよいのですが、具体的な実装方法は多数あります。本システムではいずれかのプロパティの状態の変更を受けて、別のプロパティの状態を変更したり、何らかの処理をする場合、ReactivePropertyを利用することとします。
ReactivePropertyはMVVMアーキテクチャでリアクティブプログラミングを容易に利用するためのライブラリです。詳細は次の記事をお勧めします。
実際のコードを見てみましょう。
[AddINotifyPropertyChangedInterface] public class ManagedEmployeeListViewModel : ViewModelBase { public bool IsReadOnly { get; set; } = true; public ReactiveCommand EditCommand { get; } public ReactiveCommand SaveCommand { get; } public ManagedEmployeeListViewModel( IHumanResourcesService humanResourcesService, IRegionManager regionManager) { ... EditCommand = this.ObserveProperty(x => x.IsReadOnly).ToReactiveCommand(); EditCommand.Subscribe(() => IsReadOnly = false); SaveCommand = this.ObserveProperty(x => x.IsReadOnly).Select(x => !x).ToReactiveCommand(); SaveCommand.Subscribe(OnSave); } ...
EditCommandもSaveCommandも、いずれもReactiveCommandとして宣言しており、インスタンスはコンストラクタで生成しています。
EditCommandはIsReadOnlyプロパティがtrueの時のみ押下でき、押下するとIsReadOnlyをfalseに変更するよう実装しています。
SaveCommandは逆にIsReadOnlyがfalseの時のみ押下でき、押下するとOnSaveメソッドを呼び出すように実装しています。
リアクティブプログラミングのメリットは、さまざまな解説がされていますが、私は上記コードのように条件と振る舞いを宣言的に記述できることに大きなメリットを感じています。仕様をコードでそのまま表現できる感覚が気に入っています。
リアクティブプログラミングは.NET Frameworkから始まって、現在は多種多様なプログラミング言語に輸出されています。取っ掛かりに困難を感じることもあるかもしれませんが、その困難を乗り越える努力を払う価値が十分にあるでしょう。ReactivePropertyはリアクティブプログラミングへの入門としても非常に優れたライブラリです。まだ経験のない方は、ぜひ次の記事も読んでみてはいかがでしょうか?
グリッドの各行の編集状態の管理
グリッドの各行は次の三つの状態を取ります。
- UnModified
- Modified
- New
この状態を1行を表すManagedEmployeeViewModelクラスで管理します。実際のコードを見てみましょう。
[AddINotifyPropertyChangedInterface] public class ManagedEmployeeViewModel : INotifyPropertyChanged { private readonly ManagedEmployee _managedEmployee; public EditStatus EditStatus { get; private set; } public ManagedEmployeeViewModel(ManagedEmployee managedEmployee) { _managedEmployee = managedEmployee; EditStatus = EditStatus.UnUpdated; Mapper.Map(managedEmployee, this); PropertyChanged += ChangedProperty; } private void ChangedProperty(object sender, PropertyChangedEventArgs e) { if (EditStatus == EditStatus.UnModified && e.PropertyName != nameof(EditStatus)) { EditStatus = EditStatus.Modified; } } public event PropertyChangedEventHandler PropertyChanged; ... }
先に記載した、「従業員を閲覧する」ユースケースのコードに大きく二つの修正を加えています。
一つはEditStatusプロパティを追加したこと。先に記載した三つの状態の何れかを表します。
もう一つは、自分自身のPropertyChangedイベントを監視し、EditStatusがUnModifiedで更新されたプロパティがEditStatus以外だった場合に、EditStatusをModifiedに変更する処理を追加しています。
こうすることでグリッド上で既存の行を更新されたときに、ViewModel側で変更された行を検知できるようにしています。
新たに行が追加された場合には、ManagedEmployeeViewModelのデフォルトコンストラクタが呼び出されます。
public ManagedEmployeeViewModel() { _managedEmployee = new ManagedEmployee(); EditStatus = EditStatus.New; }
入力値のバリデーション
SPREAD for WPFでは、バリデーションの実現方法を、データソースに実装する検証やイベントによる継承、DataAnnotationによる検証など、複数用意しています。
本システムでは、DataAnnotationを利用しています。DataAnnotationによるバリデーションは、ViewModelのプロパティに属性を指定することで実現します。
例えば次のコードは、姓が必須であることを表しています。
[Required] public virtual string LastName { get; set; }
実際にバリデーション チェックでエラーになった場合、次のように表示されます。
エラーのある項目にマーカー(ここでは赤い三角)が表示され、エラーのある項目にフォーカスが当たると任意のエラーメッセージが表示されます。
DataAnnotation自体は.NET Frameworkが提供している仕組みで、次のページで標準でサポートされているDataAnnotationを確認することが可能です。
グリッドの編集状態をサービスへ通知し、データベースを更新する
更新のトリガー処理は既に先に記載済みです。
XAML上でButtonのCommandにViewModelのSaveCommandがバインドされており、SaveCommandが実行されると、ViewModelのOnSaveメソッドが呼び出されます。
OnSaveメソッドの実装は次の通りです。
private void OnSave() { IsReadOnly = true; var updatedEmployees = ManagedEmployees .Where(x => x.EditStatus == EditStatus.Modified) .Select(x => x.Commit()) .ToList(); var newEmployees = ManagedEmployees .Where(x => x.EditStatus == EditStatus.New) .Select(x => x.Commit()) .ToList(); _humanResourcesService.ModifyManagedEmployees(updatedEmployees, newEmployees); ManagedEmployees.Clear(); LoadManagedEmployees(); }
次の手順で更新処理を実施しています。
- グリッドを読み取り専用に変更
- 更新行のViewModelを取得し、Commitを呼び出した後、ManagedEmployeeのListの作成
- 新規追加行のViewModelを取得し、Commitを呼び出した後、ManagedEmployeeのListの作成
- IHumanResourcesServiceを呼び出して、データベースへの追加・更新を実行
- グリッドを一旦クリア
- 従業員情報を再ロード
Commitの実装は次のようになっています。
public ManagedEmployee Commit() { Mapper.Map(this, _managedEmployee); return _managedEmployee; }
ViewModelの編集状態をManagedEmployeeの値に上書き更新し、更新後のManagedEmployeeを返却しています。
従業員情報を再ロードしているのは、再ロードすることでEmployeeテーブルなどのModifiedDateを取得し直し、更新の衝突処理を実装できるようにする意図があります。ただし今回のサンプルではそこまで実装されていません。
さあ、それでは最後に実際に実装したものの動作の様子を見てみましょう。
以上で実装ビューの解説は全てとなります。
さいごに
さて、ここまで特定のビジネス・システム化の背景を想定して、求められるシステムを構築するためのアーキテクチャを検討・記載してきました。
ビジネス要件、システム要件、非機能要件が明確になりきっているとは言えないため、アーキテクチャも不鮮明な部分が残ってはいます。それでもアプリケーションを構築する際のヒントには十分になりえるのではないかと思います。
本稿が、アプリケーション構築にお悩みの方の一助になれれば幸いです。