ユースケース「従業員を閲覧する」の実装
本ユースケースでは、従業員テーブルを利用します。関連テーブルを抜粋したER図を次に示します。
実際には図に表記していない多数の関連テーブルが存在します。しかし本稿ではこちらの図の中でもさらに、次の5つのテーブルのみを扱うものとします。
- BusinessEntity
- Person
- Employee
- Gender
- MaritalStatus
EmployeeテーブルにGenderとMaritalStatusというカラムがありますが、この中身にはコードが設定されています。
表示する値はそれぞれ、GenderテーブルとMaritalStatusテーブルに格納されているため、表示する際にはそちらの値に置き換える必要があります。
本ユースケースでは、BusinessEntity、Person、Employeeの三つのテーブルを次のように結合した、ManagedEmployeeというビューから値を取得します。
create view HumanResources.ManagedEmployee as select Employee.BusinessEntityID, Person.FirstName, Person.LastName, Person.EmailPromotion, Employee.NationalIDNumber, Employee.LoginID, Employee.JobTitle, Employee.BirthDate, Employee.MaritalStatus, Employee.Gender, Employee.HireDate, Employee.SalariedFlag, Employee.VacationHours, Employee.SickLeaveHours, Employee.CurrentFlag from HumanResources.Employee inner join Person.BusinessEntity on Employee.BusinessEntityID = BusinessEntity.BusinessEntityID inner join Person.Person on Employee.BusinessEntityID = Person.BusinessEntityID
データ取得処理時にテーブルを結合するのではなく、結合済みのビューに対してデータ取得処理行うことで、次のような利点を得られます。
- 複数個所で同様の結合を必要とした場合に、結合ルールの誤りによる不具合を予防できる
- 単一のビューにすることで、OR Mapperから扱いやすくなる
- インデックス付きビューなどを利用できる可能性がある
ManagedEmployeeにも、Employeeと同様にGenderとMaritalStatusのコードのみを持ちます。このため画面で表示する際にそれらのマスターテーブルから値を取得して表示することとします。
一覧画面で編集しない場合は、データベースのビューに表示名を入れると実装がより容易になります。しかし今回はグリッド上で編集も行います。その際、GenderやMaritalStatusはコンボボックスで取り扱うため、表示名はデータベースのビューには入れず、それぞれのマスターから取得して表示時に置き換えることとします。
さて次のシーケンス図が、「従業員を閲覧する」ユースケースのクライアントサイドの処理概要になります。
左から3レーン目のManagedEmployeeListViewModelからIHumanResourcesServiceを3回呼び出し、次の値を取得していることが見て取れます。
- Genderマスター
- MaritalStatusマスター
- 全従業員一覧
これらを取得したのちに、一覧画面を表示します。
先にも説明した通り、従業員を表すManagedEmployeeクラスでは、GenderとMaritalStatusはコードしか保持していないため、1.と2.で表示値を取得しています。その際に、編集機能でプルダウンで編集できるよう、選択可能な値をすべてここで取得しています。
注目していただきたいポイントの一つが、WCFサービスから取得されてきたManagedEmployeeをManagedEmployeeViewModelに詰め替えている箇所です。具体的なコードは次のとおりです。
private void LoadManagedEmployees() { var managedEmployees = _humanResourcesService.GetManagedEmployees(); foreach (var managedEmployee in managedEmployees) { ManagedEmployees.Add(new ManagedEmployeeViewModel(managedEmployee)); } }
ManagedEmployeeをコンストラクタの引数として渡して、ManagedEmployeeViewModelを生成していることが見て取れます。
ManagedEmployeeViewModelのコンストラクタの実装が次の通りです。
public ManagedEmployee ManagedEmployee { get; } public ManagedEmployeeViewModel(ManagedEmployee managedEmployee) { ManagedEmployee = managedEmployee; Mapper.Map(ManagedEmployee, this); ... }
ManagedEmployeeクラスとManagedEmployeeViewModelクラスはほとんど同じプロパティで構成されており、Mapperクラスを利用して、ViewModelへ値を詰め替えています。
わざわざ類似のクラスを用意して値を詰め替えているのには理由があります。本システムではグリッド上で従業員の追加・編集処理を行います。このため、グリッドにバインドする値には、新規・更新・未編集の何れかを表す状態プロパティが必要です。サービスから取得したクラスには当然、ユーザーインターフェースの実現のみに必要なプロパティは存在しないため、専用のViewModelを作成して値を詰め替えて表示します。
このような理由以外にも、次のようなケースでもViewModelへの詰め替えが必要になることがあります。
- 従業員の編集時にUndo機能を実装するためには、編集前の値を保持しておきたい
- ユーザーインターフェースの制御のための特殊な振る舞い(コマンドやメソッド)が必要
対して、GenderとMaritalStatusは、編集やユーザーインターフェース上の特殊な処理を想定しておらず、ViewModelへの詰め替え処理は行わないで、そのまま利用します。
さて必要な情報を取得してきたら、あとはSPREAD for WPFのGcSpreadGridコンポーネントのItemsSourceプロパティにManagedEmployeeViewModelのコレクションをバインドします。
次のコードが、実際のバインドしているコードの抜粋です。
<sg:GcSpreadGrid ItemsSource="{Binding ManagedEmployees}" AutoGenerateColumns="False" Locked="{Binding IsReadOnly}" Protected="{Binding IsReadOnly}" CanUserSortColumns="True" CanUserFilterColumns="True" ColumnDragMode="SelectThenDrag"> <i:Interaction.Behaviors> <behaviors:SetItemsSourceBehavior ComboBoxCell="{Binding ElementName=Gender}" ItemsSource="{Binding Genders}"/> ... </i:Interaction.Behaviors> <sg:GcSpreadGrid.Columns> <sg:Column Header="名" DataField="FirstName"> <sg:Column.CellType> <sg:TextCellType/> </sg:Column.CellType> </sg:Column> ... <sg:Column Header="性別" DataField="Gender"> <sg:Column.CellType> <sg:ComboBoxCellType x:Name="Gender" ContentPath="Name" SelectedValuePath="Code"/> </sg:Column.CellType> </sg:Column> ...
今回はSPREADの設定にデザイナーを使わず、XAMLで記述する方式を取りました。これは別にXAMLが特に優れているからという訳ではなく、リファレンスとしてお見せするのにXAMLのほうが都合が良いというのが理由です。実際にはXAMLかデザイナー(もしくは、お勧めしませんがコードビハインドでC#)の何れで実装するかは、各々のプロジェクトで判断する必要があるでしょう。
さて、前述のXAMLについて、四つほど見ていただきたいポイントがあります。
まず一つは、GcSpreadGridのプロパティのLockedとProtectedにViewModelのIsReadOnlyプロパティをバインドしている個所です。ViewModel側でデフォルトではtrueが設定されています。こうすることで、読み取り専用となるように制御しています。
続いてProtectedのすぐ下のCanUserSortColumnsとCanUserFilterColumns、ColumnDragModeの三つのプロパティです。これは全ての列でソートとフィルタリングを有効にし、選択した列をドラッグで移動可能にしています。実際の振る舞いは後程見ていただきましょう。
三つ目がGcSpreadGrid.Columns以降です。列は動的生成することも可能ですが、列の順序を制御したり、列ヘッダーの名称を日本語名で表示するためには、個別に列を定義する必要があります。実際には多数の列が定義されていますが、ここではFirstNameとGenderの列のみ記載しています。FirstNameはTextCellTypeを、GenderにはComboBoxCellTypeを指定しています。GenderのComboBoxCellTypeでは、表示値をGenderオブジェクトのNameプロパティを採用し、選択値にはCodeプロパティを採用するように定義しています。
そして四つ目がSetItemsSourceBehaviorです。これは本システムで作成したもので、ComboBoxCellTypeのItemsSourceへデータベースから取得したGenderのリストをバインディングしています。残念なことにSPREAD for WPFのComboBoxCellTypeはBindableObjectを継承していない関係で、ItemsSourceへ直接値をバインドすることができません。コードビハインドから設定することも可能ではありますが、XAMLからバインドするためにSetItemsSourceBehaviorを作成しました。良かったらコードを参考にしてみてください。
クライアントサイドについては以上です。続いてサーバーサイドの実装も見てみましょう。次のシーケンスがサーバーサイドの流れになります。
とくに見ていただきたいのが、AOPで「織り込まれる」InterceptorがTransactionInterceptorに加えて、AuthenticationInterceptorが増えていることです。
コードを見てみましょう。
public class AuthenticationInterceptor : IInterceptor { private readonly EmployeeDao _employeeDao; public AuthenticationInterceptor(EmployeeDao employeeDao) { _employeeDao = employeeDao; } public void Intercept(IInvocation invocation) { var name = ServiceSecurityContext.Current.WindowsIdentity.Name; if (_employeeDao.FindByLoginID(name) == null) { throw new SecurityAccessDeniedException(); } else { invocation.Proceed(); } } }
先に開設したAuthenticationServiceとほぼ同じロジックになっています。
AuthenticationServiceのAuthenticateメソッド以外は、実行時に利用者の認証処理を行います。これは不明なユーザーによってWCFサービスが実行されることを排除するためです。
このようにAOPを利用することによって、抜け漏れなく、かつ重複したコードもない、堅牢で生産性の高いサービスを構築することが可能となります。
DataAccess層の実装は、認証ユースケースと差がないため説明は割愛します。
では実際の動作を見てみましょう。
列クリックでのソートやフィルタリング、列を選択して列順の入れ替えが正しく動作しています。またGenderがコードではなく、名称が表示されていることが見て取れるでしょう。