WPF」を活用して業務アプリケーション開発する際の実践的なアーキテクチャ例を紹介します。今回は、コードを交えながら具体的な実装方法を解説します。実装ビューさて、それではいよいよコードを交えて、実装方法について触れていきます。本節では、ユースケースビューで取り上げた代表的なユースケースと、アーキテクチャ上重要な要素について、具体的な実装方法を解説します。具体的に取り上げる内容は次のとおりです。本システムではアプリケーション起動時に、統合Windows認証を利用してシングルサインオンでログインします。本節ではアプリケーションを起動しユーザーを認証しする流れを解説し、個別の箇所をコードを見ながら説明したいと思います。大まかな流れは次の通りです。ユースケース「ログインする」あくまで全体の流れを把握していただくための図ですので、省略していたり不正確な箇所がありますがご了承ください。画面左側の赤く囲った部分がクライアント端末上で動作するWPFアプリケーションの領域、右側の赤く囲った部分がアプリケーションサーバー上で動作するWCFサービスの領域です。WPFアプリケーションの初期画面のロード時に、ViewModelのAuthenticateメソッドを経由してIAuthenticationServiceのAuthenticateを呼び出します。そこからネットワークを経由して、アプリケーションサーバー上のAuthenticationServiceのAuthenticateメソッドを呼び、EmployeeDaoでリモート(WPFアプリケーション)のドメインアカウントから従業員情報を取得し、取得できたら認証OKとします。認証結果がViewModelまで帰ってきて、認証OKであったらRequestNavigateメソッドを呼び出してメニュー画面を表示します。認証NGだった場合は、エラー画面へ遷移します。具体的なコードを見ていきましょう。先の図ではOnLoadedメソッドからMainWindowViewModelのAuthorityメソッドを呼び出していました。実際の実装は異なります。MainWindowからMainWindowViewModelを呼び出している箇所を見てみましょう。実際にViewModelを呼び出している個所はMainWindow.xamlにあります。...WindowでLoadedイベントが発生したタイミングでMainWindowViewModelのAuthenticateCommandを実行しています。MainWindowViewModelのAuthenticateCommandは、次のように実装されています。...DelegateCommandはPrismで提供されているICommandクラスの実装クラスで、コンストラクタで渡されたActionをコマンド実行時に呼び出すように作られています。WindowでLoadedイベントが発生すると、ViewModelのAuthenticateCommandが実行され、AuthenticateCommandはAuthenticateメソッドを呼び出す。いう流れになっています。MVVMパターンを採用した実装では一般的なコードではありますが、それでもコードビハインドにイベントハンドラを定義して記述した方が簡単じゃないか?と思う人も多いのではないでしょうか。MVVMパターンの実装で、イベントハンドラとコードビハインドをあまり利用せず、XAMLへICommandをバインドして実装することには明確な理由があります。それはコードビハインドに記述したコードはテストが困難で再利用性も低いからです。コードビハインドに記述した場合、そこに書かれたコードはViewをインスタンス化しないと実行できません。Testコードからインスタンス化しにくいという問題があります。そのため、コードビハインドに記述せず、TriggerやBehaviorといった技術要素を利用してXAMLに記述します。TriggerやBehavior自体はViewと直接依存関係はありませんので、テスト容易性が確保できる、ということになります。コードビハインドに書いたコードは、類似のロジックを別のViewで使いまわそうとした場合、コードをコピーすることになります。TriggerやBehaviorなどで実装されていれば、すでに品質が確保されているコードを容易に再利用できます。XAMLを採用したMVVMパターンで、イベントハンドラとコードビハインドあまり利用しないのは、まとめると次の二つの理由によると私は考えています。このケースでは殆どないと思います。ある箇所ではコードビハインドに、ある箇所ではXAMLにと、記述が分散すると可読性が低下してしまいます。これらを担保するために「無理のない範囲で」XAMLに記述することを優先したら良いと考えています。続いてMainWindowViewModelでRequestNavigateを呼び出している個所を見てみましょう。RequestNavigate<ErrorViewModel>(_regionManager);}RequestNavigateの型パラメーターで遷移先のViewに対応するViewModelの型を渡して遷移を指示しています。RequestNavigate<TViewModel>の実装も見てみましょう。RequestNavigate<TViewModel>メソッドはMainWindowViewModelの親クラスである、ViewModelBaseに定義されています。viewName);}ViewModelのクラス名から遷移先のView名を決定して画面遷移を行なっています。Regionの名称はデフォルトはRegionNamesクラスの定数を利用しています。ちなみにMainWindow.xamlでRegionを定義している箇所も次のように同じ定数を参照しています。/>...このようなコードを用意せず、ベタに書くと次のようなコードになります。"Menu");"Menu"に関しては、ViewModelから類推するのではなく、nameof(Menu)でも良いかも知れません。nameof(Menu));ViewとViewModelが微妙に双方向依存になってしまいますが妥協できる範囲でしょう。Regionの名称に関しては何れかで定数定義する必要があります。業務アプリケーションの場合は画面遷移が単純で、ほとんど同じRegionしか利用しないということもよくあります。そのため本システムではこの共通メソッドを用意しています。画面遷移で、前の画面に戻る遷移ケースで、ViewやViewModelのインスタンスを破棄したいような場合にも、ベースクラスで共通メソッドを用意しておくと実装が楽になります。そのため戻る処理と粒度を合わせる意味で、RequestNavigate<TViewModel>もViewModelの基底クラスに共通メソッドを用意しています。画面遷移の戻る処理時に、View・ViewModelのインスタンスを破棄する件の詳細は以下の記事をご覧ください。WPFで非表示となったViewのインスタンスを破棄する設定さて、クライアント側の実装は以上です。続いてサーバーサイドの実装も見てみましょう。ログイン処理に利用するWCFサービスは、IAuthenticationServiceクラスのAuthenticateメソッドです。Authenticate();}呼び出し元のユーザーに本システムの利用権限の有無を返却します。認証には統合Windows認証を用いてユーザーを識別し、Employeeテーブルに対象のアカウントが登録されているかどうか判定します。先に記載した図では、トランザクションの管理が隠蔽されてしまっていますので、もう少し詳しい図を次に用意しました。Programing(AOP)を利用します。赤で括った部分がAOPで実装されている個所です。実際にはWCFホストがAntiDeadlockInterceptorを呼び出すわけではありません。WCFホストはAuthenticateServiceのAuthenticateメソッドを呼び出します。その呼び出しをCastle.CoreのDynamicProxyの仕組みを利用してインターセプトして、デッドロック対応処理やトランザクション処理を「織り込んで」います。順番にコードを見ていきましょう。AntiDeadlockInterceptorのコードです。}}AntiDeadlockInterceptorは、Catsle.CoreのIInterceptorを実装しています。WCFサービスのAPI呼び出しを、IInterceptorのInterceptメソッドでフックします。AntiDeadlockInterceptorではデッドロック時のリトライ処理が実装されています。どれだけ丁寧に気を配ってデータベース処理を実装しても、一般的な使用方法の範疇であってもデッドロックを完璧に回避することはできません。そのため後続処理でデッドロック例外がスローされてきた場合、ここでリトライ処理を実施しています。AntiDeadlockInterceptorはIInvocationのProceedメソッドを呼び出すことで、後続のTransactionInterceptorを呼び出します。続いてTransactionInterceptorを見てみましょう。}}TransactionInterceptorではトランザクションを開始し、InterceptのProceedメソッドを呼び出すことで後続、つまりWCFサービスの実装を呼び出します。WCFサービスの実装が正常に実行完了したら、Completeメソッドを呼び出しトランザクションをコミットします。WCFサービスの実装が例外を発生した場合は、Completeメソッドが呼び出されずトランザクションはロールバックされます。ITransactionContextはTransactionInterceptorで開始されたデータベーストランザクションを、WCFサービスのリクエスト~レスポンスの間、保持するクラスです。詳細な実装については、あまり重要なポイントはないため説明は割愛します。興味のある方はコードをご覧ください。ITransactionContext.csTransactionContext.cs続いてAuthenticationServiceの実装を見てみましょう。}}コンストラクタでEmployeeDaoがインジェクションされ、Authenticateメソッドの中で利用しています。Authenticateメソッドの中では、ServiceSecurityContextから統合Windows認証で認証されたユーザー名を取得しています。「ドメイン名/アカウント名」のような形で取得されます。取得したユーザー名からEmployeeテーブルを検索し、Employeeが存在した場合は認証OKとしてtrueを返します。WCFで統合Windows認証を利用するための詳細な設定は、以下の記事をご覧ください。その場合、次のような実装に変更する必要があるでしょう。}}WFCサービスのメソッドすべてで、同じ実装が重複するかと思うと、眩暈がしてきます。AOPを利用しない場合と比較すると、次のような問題があることが分かります。WCFサービスのすべてのメソッドに同じようなトランザクション制御コードを記述する必要があるトランザクション制御を個別に実装しているので、WCFの全メソッドでトランザクションの成功時と失敗時のテストを記述する必要があるデータベースをコミットしてしまうため、テスト前後でデータベースの事前・事後処理が必要になりテスト工数が膨らむ同様の理由から、テストの平行実行が難しいこれらの理由からトランザクション制御とビジネスロジックの実装は分離することが好ましく、そのための手法としてAOPが最も適切であると私は考えています。AOPの実現手段は色々ありますが、今回は先にも記載したようにCatsle.CoreのDynamicProxyを、SimpleInjectorがインスタンス生成する際に適用するようにしています。実際にその指定をしている個所を見てみましょう。実際のコードはWCFサービス実装のBootstrapperクラスにあります。Bootstrapper.csBootstrapperクラスのBuildContainerメソッドを見てみましょう。...}container.Interceptと呼び出している個所が該当のコードになります。IAuthenticationServiceに対して、AntiDeadlockInterceptorとTransactionInterceptorを適用するように指示しています。Interceptは拡張メソッドとして定義されており、私がNuGetに公開しているSimpleInjector.Extras.DynamicProxyパッケージに実装されています。興味のある方は次のリンク先をご覧ください。SimpleInjector.Extras.DynamicProxyInterceptorExtensions#Interceptさて、最後にEmployeeDaoを見てみます。}}コンストラクタでITransactionContextが注入されています。この時注入されるインスタンスは、TransactionInterceptorに注入されていたインスタンスと同じものが取得されます。FindByLoginIDメソッドでITransactionContextのConnectionプロパティから、データベースの接続オブジェクトを取得していますが、これはITransactionInterceptorのInterceptメソッド内でOpenした時に開かれたものと同じものとなり、ITransactionInterceptorで管理されているトランザクション内でデータベース操作が行えることとなります。本ユースケースでは、従業員テーブルを利用します。関連テーブルを抜粋したER図を次に示します。実際には図に表記していない多数の関連テーブルが存在します。本稿ではこちらの図の中でもさらに、次の5つのテーブルのみを扱うものとします。BusinessEntityPersonEmployeeGenderMaritalStatusEmployeeテーブルにGenderとMaritalStatusというカラムがありますが、この中身にはコードが設定されています。TableEmployee表示する値はそれぞれ、GenderテーブルとMaritalStatusテーブルに格納されているため、表示する際にはそちらの値に置き換える必要があります。TableMasters本ユースケースでは、BusinessEntity、Person、Employeeの三つのテーブルを次のように結合した、ManagedEmployeeというビューから値を取得します。Person.BusinessEntityIDデータ取得処理時にテーブルを結合するのではなく、結合済みのビューに対してデータ取得処理行うことで、次のような利点を得られます。Mapperから扱いやすくなるインデックス付きビューなどを利用できる可能性があるManagedEmployeeにも、Employeeと同様にGenderとMaritalStatusのコードのみを持ちます。このため画面で表示する際にそれらのマスターテーブルから値を取得して表示することとします。一覧画面で編集しない場合は、データベースのビューに表示名を入れると実装がより容易になります。今回はグリッド上で編集も行います。その際、GenderやMaritalStatusはコンボボックスで取り扱うため、表示名はデータベースのビューには入れず、それぞれのマスターから取得して表示時に置き換えることとします。次のシーケンス図が、「従業員を閲覧する」ユースケースのクライアントサイドの処理概要になります。ユースケース「従業員を閲覧する」:クライアントサイド左から3レーン目のManagedEmployeeListViewModelからIHumanResourcesServiceを3回呼び出し、次の値を取得していることが見て取れます。全従業員一覧これらを取得したのちに、一覧画面を表示します。先にも説明した通り、従業員を表すManagedEmployeeクラスでは、GenderとMaritalStatusはコードしか保持していないため、1.と2.で表示値を取得しています。その際に、編集機能でプルダウンで編集できるよう、選択可能な値をすべてここで取得しています。注目していただきたいポイントの一つが、WCFサービスから取得されてきたManagedEmployeeをManagedEmployeeViewModelに詰め替えている箇所です。具体的なコードは次のとおりです。}}ManagedEmployeeをコンストラクタの引数として渡して、ManagedEmployeeViewModelを生成していることが見て取れます。ManagedEmployeeViewModelのコンストラクタの実装が次の通りです。...}ManagedEmployeeクラスとManagedEmployeeViewModelクラスはほとんど同じプロパティで構成されており、Mapperクラスを利用して、ViewModelへ値を詰め替えています。類似のクラスを用意して値を詰め替えているのには理由があります。本システムではグリッド上で従業員の追加・編集処理を行います。何れかを表す状態プロパティが必要です。サービスから取得したクラスには当然、ユーザーインターフェースの実現のみに必要なプロパティは存在しないため、専用のViewModelを作成して値を詰め替えて表示します。このような理由以外にも、次のようなケースでもViewModelへの詰め替えが必要になることがあります。対して、GenderとMaritalStatusは、編集やユーザーインターフェース上の特殊な処理を想定しておらず、ViewModelへの詰め替え処理は行わないで、そのまま利用します。WPFのGcSpreadGridコンポーネントのItemsSourceプロパティにManagedEmployeeViewModelのコレクションをバインドします。次のコードが、実際のバインドしているコードの抜粋です。...今回は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のリストをバインディングしています。WPFのComboBoxCellTypeはBindableObjectを継承していない関係で、ItemsSourceへ直接値をバインドすることができません。コードビハインドから設定することも可能ではありますが、XAMLからバインドするためにSetItemsSourceBehaviorを作成しました。良かったらコードを参考にしてみてください。SetItemsSourceBehavior.csクライアントサイドについては以上です。続いてサーバーサイドの実装も見てみましょう。次のシーケンスがサーバーサイドの流れになります。ユースケース「従業員を閲覧する」:サーバーサイドとくに見ていただきたいのが、AOPで「織り込まれる」InterceptorがTransactionInterceptorに加えて、AuthenticationInterceptorが増えていることです。コードを見てみましょう。}}先に開設したAuthenticationServiceとほぼ同じロジックになっています。AuthenticationServiceのAuthenticateメソッド以外は、実行時に利用者の認証処理を行います。これは不明なユーザーによってWCFサービスが実行されることを排除するためです。このようにAOPを利用することによって、抜け漏れなく、かつ重複したコードもない、堅牢で生産性の高いサービスを構築することが可能となります。DataAccess層の実装は、認証ユースケースと差がないため説明は割愛します。実際の動作を見てみましょう。列クリックでのソートやフィルタリング、列を選択して列順の入れ替えが正しく動作しています。Genderがコードではなく、名称が表示されていることが見て取れるでしょう。先にも記載した通り、本ユースケースのUIからデータベースまでの相互作用は、基本的に「従業員を閲覧する」ユースケースと違いがありません。本ユースケースをアーキテクチャ設計として記載する目的は、ユーザーインターフェースの詳細な実装アーキテクチャを決定・解説することにあります。そのため、実際のユーザーインターフェースの操作を追いながら、詳細を記述していきたいと思います。プロパティの変更通知の実装さて本ユースケースは、「従業員を閲覧する」ユースケースを拡張して実現します。具体的には、「従業員を閲覧する」ユースケースを実行後、次の画面の「編集」ボタンを押下することでユースケースを開始します。WPFは編集に対応しているところを、「従業員を閲覧する」ユースケースで読み取り専用で表示しているため、編集ボタンを押下することで編集可能に状態を変更することで、ユースケースを開始します。「従業員を閲覧する」ユースケースの解説でもお見せしましたが、XAML上で編集可否を制御している箇所をもう一度見てみましょう。IsReadOnly}"初期はLockedとProtectedの二つのプロパティにtrueが設定されているため、編集ボタンを押下してIsReadOnlyをfalseに変更します。このとき変更を通知するため、通常はINotifyPropertyChangedインターフェースを実装した、次のようなコードを記述する必要があります。}}正直、通知が必要なプロパティに全てこの記述を行うのは苦痛です。苦痛な上に、不具合の温床でもあります。そこで本システムではPropertyChanged.Fodyを利用することとします。PropertyChanged.Fodyを利用することで、先のコードと同等のコードが次のように記述するのみで実現できます。true;}PropertyChanged.FodyはFodyという静的コード生成フレームワークを利用しています。前述のコードではクラスの属性としてAddINotifyPropertyChangedInterfaceが宣言されています。PropertyChanged.Fodyでは、AddINotifyPropertyChangedInterface属性が宣言されたクラスに対して、コンパイル時にコンパイルされたバイトコードを修正して、INotifyPropertyChangedを利用した変更通知に必要な処理を自動的に組み込んでくれます。詳細は英語になりますが、次のサイトが参考になります。編集ボタンと保存ボタンがあります。これらのボタンはそれぞれ、閲覧中は編集ボタンのみが押下でき、編集中は保存ボタンのみが押下できるように、状態を管理する必要があります。ボタン状態ボタンには次のように、ViewModelのEditCommandとSaveCommandをバインドします。SaveCommand}"/>ボタンはバインドされているCommandのCanExecuteがtrueの場合に押下可能となります。ICommandの詳細は次の記事も参考にしてください。WPFやXamarinのICommandを改めて整理するここでは先にも記載した、IsReadOnlyプロパティの状態によって制御すればよいのですが、具体的な実装方法は多数あります。本システムではいずれかのプロパティの状態の変更を受けて、別のプロパティの状態を変更したり、何らかの処理をする場合、ReactivePropertyを利用することとします。ReactivePropertyはMVVMアーキテクチャでリアクティブプログラミングを容易に利用するためのライブラリです。詳細は次の記事をお勧めします。実際のコードを見てみましょう。...EditCommandもSaveCommandも、いずれもReactiveCommandとして宣言しており、インスタンスはコンストラクタで生成しています。EditCommandはIsReadOnlyプロパティがtrueの時のみ押下でき、押下するとIsReadOnlyをfalseに変更するよう実装しています。SaveCommandは逆にIsReadOnlyがfalseの時のみ押下でき、押下するとOnSaveメソッドを呼び出すように実装しています。リアクティブプログラミングのメリットは、さまざまな解説がされていますが、私は上記コードのように条件と振る舞いを宣言的に記述できることに大きなメリットを感じています。仕様をコードでそのまま表現できる感覚が気に入っています。Frameworkから始まって、現在は多種多様なプログラミング言語に輸出されています。取っ掛かりに困難を感じることもあるかもしれませんが、その困難を乗り越える努力を払う価値が十分にあるでしょう。ReactivePropertyはリアクティブプログラミングへの入門としても非常に優れたライブラリです。ReactivePropertyのはじめの使い方グリッドの各行の編集状態の管理グリッドの各行は次の三つの状態を取ります。UnModifiedModifiedNewこの状態を1行を表すManagedEmployeeViewModelクラスで管理します。実際のコードを見てみましょう。...}先に記載した、「従業員を閲覧する」ユースケースのコードに大きく二つの修正を加えています。一つはEditStatusプロパティを追加したこと。先に記載した三つの状態の何れかを表します。一つは、自分自身のPropertyChangedイベントを監視し、EditStatusがUnModifiedで更新されたプロパティがEditStatus以外だった場合に、EditStatusをModifiedに変更する処理を追加しています。することでグリッド上で既存の行を更新されたときに、ViewModel側で変更された行を検知できるようにしています。新たに行が追加された場合には、ManagedEmployeeViewModelのデフォルトコンストラクタが呼び出されます。WPFでは、バリデーションの実現方法を、データソースに実装する検証やイベントによる継承、DataAnnotationによる検証など、複数用意しています。本システムでは、DataAnnotationを利用しています。DataAnnotationによるバリデーションは、ViewModelのプロパティに属性を指定することで実現します。次のコードは、姓が必須であることを表しています。チェックでエラーになった場合、次のように表示されます。バリデーションエラーのある項目にマーカー(ここでは赤い三角)が表示され、エラーのある項目にフォーカスが当たると任意のエラーメッセージが表示されます。Frameworkが提供している仕組みで、次のページで標準でサポートされているDataAnnotationを確認することが可能です。名前空間グリッドの編集状態をサービスへ通知し、データベースを更新する更新のトリガー処理は既に先に記載済みです。XAML上でButtonのCommandにViewModelのSaveCommandがバインドされており、SaveCommandが実行されると、ViewModelのOnSaveメソッドが呼び出されます。OnSaveメソッドの実装は次の通りです。LoadManagedEmployees();}次の手順で更新処理を実施しています。新規追加行のViewModelを取得し、Commitを呼び出した後、ManagedEmployeeのListの作成IHumanResourcesServiceを呼び出して、データベースへの追加・更新を実行グリッドを一旦クリア従業員情報を再ロードCommitの実装は次のようになっています。_managedEmployee;}ViewModelの編集状態をManagedEmployeeの値に上書き更新し、更新後のManagedEmployeeを返却しています。従業員情報を再ロードしているのは、再ロードすることでEmployeeテーブルなどのModifiedDateを取得し直し、更新の衝突処理を実装できるようにする意図があります。今回のサンプルではそこまで実装されていません。実装ビューの解説は全てとなります。さいごにさて、ここまで特定のビジネス・システム化の背景を想定して、求められるシステムを構築するためのアーキテクチャを検討・記載してきました。ビジネス要件、システム要件、非機能要件が明確になりきっているとは言えないため、アーキテクチャも不鮮明な部分が残ってはいます。それでもアプリケーションを構築する際のヒントには十分になりえるのではないかと思います。本稿が、アプリケーション構築にお悩みの方の一助になれれば幸いです。金融ソリューション開発部所属。圧縮・解凍UNIX基本講座@CodeZine『CodeZine(コードジン)』の公式アカウントです。【NEW】デブスト2019デブサミ2019福岡・関西デブサミ2019夏デブスト関西デブサミ2019×ログインEメールパスワード※旧SEメンバーシップ会員の方は、同じ登録情報(Eメール&パスワード)でログインいただけます。イラストの無断転載を禁じます。記載されているロゴ、システム名、製品名は各社及び商標権者の登録商標あるいは商標です。