Razor Pagesと階層分割アプローチ
Razor PagesはASP.NET Core 2.0の新機能で、「Model-View-ViewModel」という階層分割アプローチを取り入れたアーキテクチャーパターンに沿ったフレームワークです。
はじめに、Model-View-ViewModelのアーキテクチャーパターンや階層分割アプローチについて、少し触れておきます。
ソフトウェアの規模
アプリケーションを作成時に開発者が遭遇する問題のひとつに、「ソフトウェアの規模」の問題があります。
ソフトウェアの規模が小さいアプリケーションは、比較的見通しもよく、可読性が高いことから取り扱いもしやすいため、プログラミングの際に大きな問題となることは多くないでしょう。
しかし、実際にソフトウェアで取り扱う問題は単純なものばかりではありません。 加えて「複雑な問題を機械的に間違いなく実施することができる」ことは、ソフトウェアを作成する動機のひとつでもあります。
ソフトウェアで複雑な問題を取り扱うと、その複雑さに比例してソフトウェアの規模も大きくなります。
規模が大きいソフトウェアを取り扱う場合、何も考慮せずソフトウェアを作成すると、元々の問題の複雑さに加えて、ソフトウェアの複雑さが加わる場合があります。
その結果、ソフトウェアの可読性・保守性が低下し、ひいては品質の低下につながることになります。
つまり、複雑なソフトウェアを作成する場合には、それに対応するための対処方法が必要になります。
関心事の分離
このような複雑な問題に対するひとつのアプローチとして、「関心事の分離(Separation of Concerns、略してSoCと呼びます)」があります。
SoCでは、ソフトウェアが解決したい問題を個々の問題(関心)毎に分離し、構成する手法です。つまり、「大きな問題」を「小さな複数の問題」に分割して扱うことで、一つひとつの問題の複雑さを軽減し、取り扱いやすくするという設計原則です。
ただし、やみくもに分割すると、分割の粒度があいまいになることで、問題の粒度が不揃いになり、問題の内容がかえって分かりづらくなったりします。その結果、ソフトウェアの全体像が見えづらくなったり、複雑さが増してしまう場合があります。
分離へのアプローチ
そこで、これらの分割の手法としてよく用いられるのが「階層化アプローチ(Layered approach)」です。
「階層化アプローチ」 は、責任の分割を主な目的としています。ソフトウェアが扱う問題を特定の役割を持つ階層に分割します。分割した各階層は、他の階層への「インターフェース」、つまり窓口だけを取り決めして、その結果を保証します。
各階層とやりとりを行う「インターフェース」と、「インターフェース」でどのような「データ」を受け渡すかを取り決めます。
それ以外の各階層内で行われる処理方法や、階層の内部で使用されるデータについては、他の階層が一切関知しないものとします。
それにより、各階層内部は処理やデータの影響範囲を限定されるため、ソフトウェア保守時に掌握すべき項目が減るため、可読性・保守性の向上につながります。
階層化アプローチの基準
実際にどのような階層に分割すればよいかの基準のひとつとして、マーチンファウラーが提唱した「プレゼンテーションとドメインの分離(Presentation Domain Separation、略してPDSと呼びます)」があります。
PDSでは、ソフトウェアの「ユーザーインターフェース」部分と「その他の機能」部分を分割することで、以下のメリットを享受できるとしています。(引用:Martin Fowler’s Bliki (ja)日本語訳)
- プレゼンテーションロジックとドメインロジックが分かれていると、理解しやすい
- 同じ基本プログラムを、重複コードなしに、複数のプレゼンテーションに対応させることができる
- ユーザーインターフェースはテストがしにくいため、それを分離することにより、テスト可能なロジック部分に集中できる
- スクリプト用のAPIやサービスとして外部化するためのAPIを楽に追加できる(選択可能なプレゼンテーション部分で見かける)
- プレゼンテーション部分のコードは、ドメイン部分のコードと違ったスキルと知識が必要
ここにあるような分割のアプローチを具現化したアーキテクチャーパターンが、ASP.NETでも利用されているModel-View-Controller(MVC)であったり、これから利用するModel-View-ViewModel(MVVM)などになります。
Model-View-ViewModel
Razor Pagesは、階層化アプローチであるModel-View-ViewModelを用いたフレームワークです。ここで、Model-View-ViewModelについても、少し触れておきたいと思います。
Model-View-ViewModelでは、プレゼンテーション部分をView-ViewModel、ドメイン部分をModelに分割して構成します。
そして、ViewとViewModelは「データバインディング」のみで関連付けを行うことで、ユーザーインターフェースとデータの分離を実現するアーキテクチャーパターンです。
MVVMは元々、Windows Presentation Foundation(WPF)やUniversal Windows Platform(UWP)などで利用されていたパターンです。
WPFやUWPは、ユーザーインターフェースの記述を「XAML」と呼ばれるXMLベースのマークアップ言語で行います。XAMLは、データとユーザーインターフェースの要素を関連付けして、値の同期を実現する、データバインディング機能を持ちます。このデータバインディング機能を使用することで、ViewとViewModelの分離を実現していました。
このとき、ViewModelではViewに表示するために必要なデータを「プロパティ」で表現し、Viewはそのプロパティを参照することでユーザーインターフェースの表現に関連付けました。
以下の例は、実際にデータバインディングを行っているViewのXAMLとViewModelのC#コードです。ここでは、ViewとViewModelが分離されていることを示す例なので、コードの内容は理解できなくてもかまいません。
<Grid> <Grid.DataContext> <local:SampleViewModel/> <!-- ViewModelクラスのインスタンスを設定 --> </Grid.DataContext> <TextBox Text="{Binding CustomerName}"/> <!-- ViewModelクラスのプロパティにBinding --> </Grid>
public class SampleViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private string customerName; // Bindingするためのプロパティ public string CustomerName { get => customerName; set { customerName = value; RaisePropertyChanged(); } } protected virtual void RaisePropertyChanged([CallerMemberName] string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); }
この分離によって、Viewに表示されるべきデータが正しく表示できない原因は「正しくデータバインディングが設定されていない」または「ViewModel以降の階層が正しく実装されていない」の2点に限定することができました。
さらにViewModelクラスは、上記のように単独のC#クラスであるため、その他のModelクラスと同様に単体テストの作成も行いやすくなります。
つまり、View以外のいずれの階層でも単体テストを行うことができるようになったことで、回帰テストが実施しやすくなり、品質の向上に貢献します。
Razor PagesとModel-View-ViewModel
繰り返しになりますが、Razor Pagesは、このModel-View-ViewModelのアーキテクチャーモデルに倣ってWebアプリの開発が行えるフレームワークです。
Model-View-ViewModelに沿って、Razor Pagesではcshtml(Razor構文によるView)-cshtml.cs(PageModelクラスを継承した単一のクラス)-Modelクラスで構成します。
ここで、RazorPagesのテンプレートプロジェクトを見てみましょう。Razor Pagesのプロジェクトテンプレートの作成は、Visual Studioの新規プロジェクトの作成で[ASP.NET Core Web アプリケーション]を選択するか、dotnetコマンドラインで行えます。
dotnetコマンドライン CLIで行う場合は、任意のフォルダー内で以下のコマンドを実行することで作成されます。
dotnet new razor
作成された実際のプロジェクトは下図の通りです。
Razor Pagesのプロジェクトテンプレートは、ASP.NET Core MVCのプロジェクトに比べて、フォルダーが「Pages」のみとシンプルです。
「Pages」フォルダーの中には、いくつかのcshtmlファイルが入っていますが、「About.cshtml」「Contact.cshtml」「Error.cshtml」「Index.cshtml」の4ファイルには、関連する「*.cshtml.cs」ファイルが紐づけされています。
この「*.cshtml.cs」ファイルが、ViewModelの役割を担う「PageModel」ファイルです。
Razor PagesによるWebページの作成
Razor PagesによるWebページの作成方法をまとめると、以下の通りになります。
- Viewである「*.cshtml」は、Razor構文で記述する
-
Razor Pagesのcshtmlファイルの先頭行にはRazor Pagesである宣言として
@page
を記述する(先頭にないと404で表示できなくなる) -
ViewModelは、cshtmlに対応する
PageModel
クラスを継承する - PageModelクラスには、cshtmlにデータバインドするためのpublicプロパティを追加する
-
GET、POSTなどでデータ送信が必要なる項目のプロパティは、
BindProperty
属性を付与する
実際にASP.NET Coreによるテンプレートプロジェクトを少し加工し、「Index.cshtml」と「Index.cshtml.cs」を修正したソースが以下です。
@page @model IndexModel @{ ViewData["Title"] = "Home page"; } <!-- Index.cshtml --> <form method="post"> <div class="row"> <div class="col-md-12"> <h1>@Model.Message</h1> </div> </div> <div class="row"> <div class="col-md-3">Your Name</div> <div class="col-md-9"> <input type="text" asp-for="UserName" /> </div> </div> <div class="row"> <div class="col-md-12"> <input type="submit" /> </div> </div> </form>
// Index.cshtml.cs public class IndexModel : PageModel { // 表示だけならば通常プロパティでOK public string Message { get; set; } // 送信時にデータを格納したい場合はBindProperty属性を付与 [BindProperty] public string UserName { get; set; } // GET時に行う処理 public void OnGet() { Message = "Hello, world"; UserName = string.Empty; } // POST時に行う処理 public void OnPost() { Message = $"Hello, {UserName}"; } }