SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

ComponentZine(ComponentOne)(AD)

ASP.NET CoreとComponentOneを使って、マルチプラットフォームで動作するWebアプリを作成しよう!【Mac/Linux編】

  • このエントリーをはてなブックマークに追加

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は「データバインディング」のみで関連付けを行うことで、ユーザーインターフェースとデータの分離を実現するアーキテクチャーパターンです。

Model-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とMVVM
RazorPagesとMVVM

 ここで、RazorPagesのテンプレートプロジェクトを見てみましょう。Razor Pagesのプロジェクトテンプレートの作成は、Visual Studioの新規プロジェクトの作成で[ASP.NET Core Web アプリケーション]を選択するか、dotnetコマンドラインで行えます。

 dotnetコマンドライン CLIで行う場合は、任意のフォルダー内で以下のコマンドを実行することで作成されます。

dotnet new razor

 作成された実際のプロジェクトは下図の通りです。

Razor Pages プロジェクト
Razor Pages プロジェクト

 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}";
        }
    }
実行結果
実行結果

次のページ
使用するWebアプリについて

この記事は参考になりましたか?

  • このエントリーをはてなブックマークに追加
ComponentZine(ComponentOne)連載記事一覧

もっと読む

この記事の著者

森 博之(AZPower株式会社)(モリ ヒロユキ)

 Microsoft Azure、.NETテクノロジーを使用したWebサービスのプロダクトアーキテクト。他にも技術記事の執筆やトレーニング、セミナースピーカーを行っている。 Microsoft MVPをDeveloper Technologies、Visual Studio and Development Technologies、Windows Development、Client App Dev、.NET、Silverlight、Visual C#などのコン...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

【AD】本記事の内容は記事掲載開始時点のものです 企画・制作 株式会社翔泳社

この記事は参考になりましたか?

この記事をシェア

  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/10782 2018/04/25 14:00

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング