March 2018

Volume 33 Number 3

ASP.NET - Razor を使用して単一ページ アプリのテンプレート用 HTML を生成する

Nick Harrison

シングルページ アプリケーション (SPA) アプリの人気が非常に高まっています。それには相応の理由があります。ユーザーが Web アプリに期待するのは、高速で魅力があり、スマートフォンから超ワイド画面のデスクトップに至るまですべてのデバイス上で動作することです。それ以外にも、安全で、視覚的な魅力も備わり、何か役に立つことを実行できることも必要です。このように Web アプリには多くの要素が求められますが、これは実際のところ出発点に過ぎません。

ユーザーが Web アプリに期待することが増え始めるにつれ、Model-View-ViewModel (MVVM) パターンでのクライアント側実装を「支援」するクライアント側フレームワークが爆発的に増加しています。新しいフレームワークが毎週登場しているような印象さえ受けます。便利さを証明するものもあれば、それほどではないものもあります。しかし、それらはすべて設計パターンの実装が異なり、それぞれ固有の方法で新しい機能の追加や、繰り返し発生する問題の解決を図っています。各フレームワークでは設計パターンに異なるアプローチが取られ、テンプレート用や、ファクトリ、モジュール、オブザーバブルなどのカスタム概念を導入するために、固有の構文を用意しています。その結果、習得のペースが速まり、新しいフレームワークが登場しては廃れる中で最先端であり続けようとする開発者にとってはストレスになる可能性もあります。この習得ペースを緩めるためにできることは何であれすばらしいことです。

クライアント側のフレームワークでは MVVM パターンが実装され、サーバー上の ASP.NET では MVC パターンが実装されることがいくらか混乱を招いています。サーバー側の MVC とクライアント側の MVVM はどのように組み合わせればよいのでしょう。多くの開発者にとってその答えは単純です。組み合わせないことです。一般的なアプローチは、静的な HTML ページやフラグメントをビルドしてクライアントにサービスを提供し、サーバー側の処理を最小限に抑えるものです。このシナリオでは、以前なら MVC コントローラーで対応していた処理を、Web API コントローラーに置き換えるのが一般的です。

図 1 に示すように、Web サーバーと Web API 用の本格的なサーバーには最小要件があり、どちらのサーバーもファイアウォールの外側からアクセスできる必要があります。

代表的なアプリケーション レイアウト
図 1 代表的なアプリケーション レイアウト

この配置では懸念事項が適切に分離されるため、多数のアプリケーションがこのパターンに従って記述されています。しかし、開発者にとっては多くのことが手付かずになります。ASP.NET には他にも多数の機能が用意されており、クライアント側のフレームワークでさまざまな複雑な処理を行っている場合でさえ、MVC の実装では依然として有意義な多くの機能が提供されているのです。今回は、こうした機能の 1 つである Razor ビュー エンジンに注目します。

SPA での Razor

Razor ビュー エンジンは、HTML マークアップの生成を単純化することに関しては実に優れています。少し微調整を加えるだけで、生成されるマークアップが、クライアントで使用される任意のフレームワーク用のテンプレートの想定に合うように簡単にカスタマイズされます。Angular と Knockout を使用していくつかの例を紹介しますが、これらの手法は採用しているフレームワークとは無関係に機能します。サーバーにコールバックしてアプリケーションにテンプレートを提供する場合、Razor を使用することで、開発者は HTML を生成する複雑な処理を実行できるようになります。

ここには気に入る点が多数あります。EditorTemplates と DisplayTemplates は今もなお便利です。スキャフォールディング テンプレートも同様です。部分ビューも挿入でき、Razor 構文の滑らかなフローが長所である点も変わりません。ビュー以外に、MVC コントローラーも使用できます。これにより、処理能力を追加して作業速度を上げたり、セキュリティの層を増やしたりできます。こうしたことが不要でも、このコントローラーはビューへのシンプルなパススルーになる可能性があります。

これを利点として使用できることを実証するために、時間追跡アプリケーションの一部のデータ入力画面を作成しながら説明することで、Angular または Knockout での使用に適した HTML を Razor で効率良く作成できることを紹介します。

図 2 に示すように、代表的なレイアウトからそれほど変化はありません。MVC アプリケーションでこのデータベースを操作する可能性があることを示すオプションを追加しています。これはこの段階では不要ですが、処理を単純化する場合に利用できます。一般的に、フローは次のようになります。

  • MVC アプリケーションから完全なページを取得します。これにはすべてのスタイル シートおよび JavaScript リファレンスと最小限のコンテンツを含みます。
  • ページがブラウザーで完全に読み込まれてフレームワークが初期化されたら、フレームワークで MVC サーバーを呼び出してテンプレートを部分ビューとして要求できます。
  • このテンプレートは Razor で生成し、関連付けられたデータは通常含みません。
  • 同時に、フレームワークが API を呼び出してデータを取得します。そのデータは MVC サイトから受信したテンプレートにバインドされます。
  • ユーザーが必要な編集を行うと、フレームワークによって Web API サーバーが呼び出され、バックエンド データベースが更新されます。

Razor での単純なフローの追加
図 2 Razor での単純なフローの追加

新しいテンプレートが要求されたらこのワークフローを必要に応じて繰り返します。フレームワークでテンプレートの URL を指定できる場合は、MVC からそのテンプレートにサービスを提供できます。Angular と Knockout でのバインドに適したビューを生成する例を示しますが、これらが唯一のオプションにはなりません。

ソリューションのセットアップ

セットアップの点から見ると、少なくとも 3 つのプロジェクトが必要です。Web API 用のプロジェクト、MVC アプリケーション用のプロジェクト、そして最後に、これら 2 つのプロジェクトに共通するコードをホストする共通プロジェクトの 3 つです。今回の目的に合わせて、この共通プロジェクトでは Web と Web API の両方で使用できるように ViewModels をホストします。最初のプロジェクトは図 3 に示すような構造になります。

最初のプロジェクトの構造
図 3 最初のプロジェクトの構造

実際には、クライアント側のフレームワークを 1 つのみサポートすることになります。ここでは、2 つの MVC プロジェクトを用意し、説明するフレームワークごとに 1 つのプロジェクトを使用することで物事を単純化しています。Web アプリケーション、カスタマイズされたモバイル アプリケーション、SharePoint アプリケーション、デスクトップ アプリケーションのほか、一般的な UI プロジェクトからレンダリングすることが容易ではないシナリオをサポートする必要がある場合、似たような状況になる可能性があります。それはともかく、複数のフロント エンドをサポートする際には、UI プロジェクトに組み込むロジックのみを繰り返す必要があります。

データの提供

実際には、おそらく Entity Framework などのオブジェクト リレーショナル マッパー (ORM) を使用して、データをデータベースに格納することになるでしょう。ここではデータ永続化の問題は横に置いて、フロント エンドに注目します。Web API コントローラーを利用して、Get アクションでハードコーディングされている値を返すことにします。また、すべての API 呼び出しで成功が返される理想的な世界でこれらのサンプルを実行するものとします。適切なエラー処理の追加は、勇気ある読者の課題として残しておきましょう。

この例では、System.ComponentModel.DataAnnotations 名前空間の属性で修飾した単一のビュー モデルを使用します (図 4 参照)。

図 4 シンプルな TimeEntryViewModel

namespace TimeTracking.Common.ViewModels
{
  public class TimeEntryViewModel
  {
    [Key]
    public int Id { get; set; }
    [Required (ErrorMessage ="All time entries must always be entered")]
    [DateRangeValidator (ErrorMessage ="Date is outside of expected range",
      MinimalDateOffset =-5, MaximumDateOffset =0)]
    public DateTime StartTime { get; set; }
    [DateRangeValidator(ErrorMessage = "Date is outside of expected range",
      MinimalDateOffset = -5, MaximumDateOffset = 0)]
    public DateTime EndTime { get; set; }
    [StringLength (128, ErrorMessage =
      "Task name should be less than 128 characters")]
    [Required (ErrorMessage ="All time entries should be associated with a task")]
    public string Task { get; set; }
    public string Comment { get; set; }
  }
}

DateRangeValidator 属性は DataAnnotations 名前空間のものではありません。これは標準的な検証属性ではないものの、図 5 に、新しい検証コントロールを簡単に作成できることを示しています。適用すると、標準的な検証コントロールのように動作します。

図 5 カスタム検証コントロール

public class DateRangeValidator : ValidationAttribute
{
  public int MinimalDateOffset { get; set; }
  public int MaximumDateOffset { get; set; }
  protected override ValidationResult IsValid(object value,
    ValidationContext validationContext)
  {
    if (!(value is DateTime))
      return new ValidationResult("Inputted value is not a date");
    var date = (DateTime)value;
    if ((date >= DateTime.Today.AddDays(MinimalDateOffset)) &&
      date <=  DateTime.Today.AddDays(MaximumDateOffset))
      return ValidationResult.Success;
    return new ValidationResult(ErrorMessage);
  }
}

モデルが検証されるときは常に、カスタム検証コントロールを含むすべての検証コントロールが実行されます。Razor で作成したビューではこれらの検証を簡単にクライアント側に組み込むことができます。また、これらの検証コントロールはサーバー上でモデル バインダーによって自動的に評価されます。より安全なシステムを用意するうえで重要なのはユーザー入力を検証することです。

API コントローラー

ビュー モデルを用意したので、コントローラーを生成する準備ができました。組み込みスキャフォールディングを使用してコントローラーをスタブアウトします。これにより、標準動詞 (Get、Post、Put、Delete) に基づくアクション用のメソッドが作成されます。今回の目的のから、これらのアクションの詳細は考慮しません。クライアント側のフレームワークから呼び出されるエンドポイントがあることの確認のみに関心を向けます。

ビューの作成

次に、組み込みスキャフォールディングによりビュー モデルから生成されるビューに着目します。

TimeTracking.Web プロジェクトでは、新しいコントローラーを追加し、それに TimeEntryController という名前を付けて空のコントローラーとして作成します。このコントローラーでは、次のコードを追加することにより Edit アクションを作成します。

public ActionResult Edit()
{
  return PartialView(new TimeEntryViewModel());
}

このメソッドを内部で右クリックし、[ビューの追加] を選択します。 ポップアップで、Edit テンプレートが必要であることを指定し、TimeEntryViewModel テンプレートを選択します。

モデルの指定に加えて、必ず部分ビューの作成も指定します。生成されるビューには、生成されるビュー内で定義されたマークアップのみを含める必要があります。この HTML フラグメントはクライアントの既存のページに挿入されます。スキャフォールディングにより生成されるマークアップの一例を図 6 に示します。

図 6 Edit ビューの Razor マークアップ

@model TimeTracking.Common.ViewModels.TimeEntryViewModel
@using (Html.BeginForm())
{
  @Html.AntiForgeryToken()
  <div class="form-horizontal">
    <h4>TimeEntryViewModel</h4>
    <hr />
    @Html.ValidationSummary(true, "", new { @class = "text-danger" })
    @Html.HiddenFor(model => model.Id)
    <div class="form-group">
      @Html.LabelFor(model => model.StartTime, htmlAttributes:
        new { @class = "control-label col-md-2" })
      <div class="col-md-10">
        @Html.EditorFor(model => model.StartTime,
          new { htmlAttributes = new { @class = "form-control" } })
        @Html.ValidationMessageFor(model => model.StartTime, "",
          new { @class = "text-danger" })
      </div>
    </div>
    ...
  </div>
}
<div>
  @Html.ActionLink("Back to List", "Index")
</div>

この生成されたビューには注目すべき重要な点がいくつかあります。また、このビューでは、Bootstrap ベースのレスポンシブ UI が特別な構成なしで生成されます。重要な点は以下のとおりです。

  • ビュー モデルのプロパティごとにフォーム グループが繰り返されています。
  • マーク先を変更しないキーとして識別するキー属性を使用して Id プロパティがマークされているため、Id プロパティが非表示になっています。
  • 各入力フィールドに、関連付けられた検証メッセージ プレースホルダーがあります。これは、控えめな検証が有効にされている場合に使用できます。
  • すべてのラベルは LabelFor ヘルパーを使用して追加します。このヘルパーはプロパティのメタデータを解釈し、適切なラベルを特定します。DisplayAttribute を使用することで、ハンドルのローカライズに加え、より適切な名前を付けることができます。
  • 各入力コントロールは EditorFor ヘルパーを使用して追加します。このヘルパーはプロパティのメタデータを解釈し、適切なエディターを特定します。

メタデータは実行時に評価します。つまり、ビューの生成後に属性をモデルに追加でき、これらの属性が使用されることで必要に応じて検証コントロール、ラベル、およびエディターが特定されます。

スキャフォールディングは 1 回限りのコード生成であるため、生成されたマークアップを編集することは問題ありません。また、一般的にそうすることが想定されています。

Knockout へのバインド

生成されたマークアップを Knockout と連携させるには、その生成されたマークアップにいくつか属性を追加する必要があります。Knockout の内部動作を深く掘り下げることはしませんが、バインディングでは data-bind 属性を使用することに注意してください。バインディング宣言では、バインディングの種類を指定してから、使用するプロパティを指定します。data-bind 属性を入力コントロールに追加することが必要です。前述の生成されたマークアップを見直すと、class 属性が追加されているのがわかります。同じプロセスに従って、EditorFor 関数を次のコードのように変更できます。

@Html.EditorFor(model => model.StartTime,
  new { htmlAttributes = new { @class = "form-control",
         data_bind = "text: StartTime" } })

スキャフォールディングから特別な構成なしで生成されたマークアップを使用する場合、Knockout バインディングを追加するために必要な変更はこれだけです。

Angular へのバインド

Angular でのデータ バインディングも同様です。ng-model 属性か、data-ng-model 属性のいずれかを追加できます。data-ng-model 属性はマークアップを HTML5 準拠に維持しますが、一般的には相変わらず ng-bind を使用しています。どちらの場合も、属性の値は単純にバインドするプロパティの名前になります。Angular コントローラーへのバインディングをサポートするには、以下のコードを使用して EditorFor 関数を変更します。

@Html.EditorFor(model => model.StartTime,
  new { htmlAttributes = new { @class     = "form-control",
                                  data_ng-model = "StartTime" } })

アプリケーションとコントローラーの定義に影響するより細かい調整がいくつかあります。完全な動作例については、サンプル コードを参照して、コンテキスト内でこれらの変更を確認してください。

同様の手法を採用して、生成されたマークアップを、使用中の任意の MVVM フレームワークと連携させることができます。

スキャフォールディングの変更

スキャフォールディングでは T4 を使用して出力を生成するため、生成される対象を変更することで、生成されるすべてのビューを編集する必要がなくなります。使用されるテンプレートは Visual Studio のインストールの下に格納されます。Visual Studio 2017 では次の場所にあります。

C:\Program Files (x86)\Microsoft Visual Studio 14.0\
  Common7\IDE\Extensions\Microsoft\Web\Mvc\Scaffolding\Templates\MvcView

これらのテンプレートは直接編集できますが、これは同じコンピューターで作業しているすべてのプロジェクトに影響します。また、そのプロジェクトで作業中のほかのユーザーだれも、そのテンプレートに加えられた変更によるメリットを受けないでしょう。代わりに、T4 テンプレートをプロジェクトに追加し、ローカル コピーで標準の実装をオーバーライドします。

必要なテンプレートを、プロジェクトのルート フォルダーにある "Code Templates" というフォルダーに単純にコピーします。C# と Visual Basic の両方のテンプレートが見つかるでしょう。言語はそのファイル名に反映されています。必要なのは 1 つの言語のテンプレートのみです。Visual Studio からスキャフォールディングを呼び出すと、使用するテンプレートを求めて最初に CodeTemplates フォルダーが検索されます。そこにテンプレートが見つからなければ、次にスキャフォールディング エンジンは Visual Studio のインストールの下を確認します。

T4 は、コードだけでなく、テキスト全般を生成するための強力なツールです。T4 を学習することはそれ自体が大きなトピックです。ただし、T4 をまだよく知らなくても心配は不要です。テンプレートに対するこれら調整は非常に小さなものです。T4 の内部を深く理解してその動作のしくみを把握する必要はありません。ただし、Visual Studio で T4 テンプレートの編集をサポートするには、拡張機能をダウンロードする必要があります。Tangible T4 Editor (bit.ly/2Flqiqt、英語) と Devart T4 Editor (bit.ly/2qTO4GU、英語) はどちらも、T4 テンプレートの編集に対応したすばらしいコミュニティー バージョンの T4 エディターを提供しています。これらのエディターには構文強調表示が備わっており、テンプレートを使用して作成されたコードから、テンプレートを駆動するコードを分離しやすくなります。

Edit.cs.t4 ファイルを開くと、テンプレートを制御するコード ブロックと、マークアップのコード ブロックを確認できます。テンプレートを駆動するコードの多くは、部分ページのサポートや、外部キー、列挙型、ブール値のような特殊な型のプロパティに対する処理など、特殊なケースに対応するための条件処理です。図 7 に、適切な拡張機能を使用する Visual Studio におけるテンプレートのサンプルを示しています。テンプレートを駆動するコードは、折りたたまれたセクション内で非表示になります。それにより、出力の状態をより簡単に確認できます。

Visual Studio での T4 テンプレートの編集
図 7 Visual Studio での T4 テンプレートの編集

さいわい、これらの条件をトレースする必要はありません。代わりに、変更対象の生成済みマークアップを探します。今回の例では、6 つの異なるステートメントに注目します。参考のために、これらを図 8 に取り出しています。

図 8 エディターを生成するためのオリジナルのコード

@Html.DropDownList("<#= property.PropertyName #>", null,
  htmlAttributes: new { @class = "form-control" })
@Html.DropDownList("<#= property.PropertyName #>", String.Empty)
@Html.EditorFor(model => model.<#= property.PropertyName #>)
@Html.EnumDropDownListFor(model => model.<#= property.PropertyName #>,
  htmlAttributes: new { @class = "form-control" })
@Html.EditorFor(model => model.<#= property.PropertyName #>,
  new { htmlAttributes = new { @class = "form-control" } })
@Html.EditorFor(model => model.<#= property.PropertyName #>)

特定のクライアント側のフレームワークをサポートするように更新すると、テンプレートが生成するすべての新しいビューで自動的にフレームワークがサポートされます。

クライアント側の検証

生成後のビューを検証する際に、各プロパティ用に作成された ValidationMessageFor 関数呼び出しをスキップしました。これらの呼び出しで生成されるプレースホルダーには、クライアント側の検証が評価されるときに作成される検証メッセージが表示されます。これらの検証はモデルに追加される Validation 属性に基づきます。これらのクライアント側の検証を有効にするために必要なことは、jQuery 検証スクリプトへの参照を追加することのみです。

@Scripts.Render("~/bundles/jqueryval")

jqueryval バンドルは、App_Start フォルダーの BundleConfig クラスで定義しています。

データを入力せずにフォームを送信しようとすると、その送信を防ぐために必須フィールド検証コントロールがトリガーされます。

Bootstrap 形式の検証 (bit.ly/2CZmRqR、英語) など、別のクライアント側の検証方法の方が好ましい場合は、ValidationMessageFor 呼び出しが出力されないように T4 テンプレートを簡単に変更できます。また、ネイティブ検証アプローチを使用しないのであれば、jqueryval バンドルは必要なくなるため、このバンドルを参照する必要はありません。

エディター テンプレート

EditorFor HTML ヘルパーを呼び出すことで入力コントロールを指定しているため、入力コントロールにする対象を明示的に指定していません。代わりに、データ型や UIHint などの属性に基づいて、指定されたプロパティに最適な入力コントロールが使用されるように、その選択を MVC フレームワークに任せています。EditorTemplate を明示的に作成することで、エディターの選択に直接影響を与えることも可能です。これにより、特定の種類の入力に対する処理方法をグローバル レベルで制御できます。

DateTime プロパティの既定のエディターはテキスト ボックスです。最も理想的なエディターとはいえませんが、これは変更できます。

DateTime.cshtml という名前の部分ビューをフォルダー \Views\Shared\EditorTemplates に追加します。このファイルに追加したマークアップが、DateTime 型のプロパティ用のエディターとして使用されます。図 9 に示すマークアップを部分ビューに追加し、次のコードを Layout.cshtml の下部に追加します。

<script>
  $(document).ready(function () {
    $(".date").datetimepicker();
  });
</script>
Figure 9 Markup for a DateTime Editor
@model DateTime?
<div class="container">
  <div class="row">
    <div class='col-md-10'>
      <div class="form-group ">
        <div class='input-group date'>
         <span class="input-group-addon">
           <span class="glyphicon glyphicon-calendar"></span>
         </span>
         @Html.TextBox("", (Model.HasValue ?
           Model.Value.ToShortDateString() :
           string.Empty), new
           {
             @class = "form-control"
           })
        </div>
      </div>
    </div>
  </div>
</div>

これらのコード要素を追加したら、DateTime プロパティを編集しやすい、非常にすばらしい日付と時刻エディターの完成です。図 10 に、実行中のこの新しいエディターを示しています。

有効になった日付の選択
図 10 有効になった日付の選択

まとめ

ご覧のように、Razor を使うと、任意のクライアント側の MVVM フレームワークでビューの作成を簡略化および合理化するために、多くのことが可能になります。アプリケーションのスタイルや規則を自由にサポートでき、スキャフォールディングや EditorTemplates などの機能によってアプリケーション全体で一貫性を確保しやすくなります。また、ビュー モデルに追加される属性に基づく検証を組み込みでサポートできるため、アプリケーションがより安全になります。

Web アプリケーションの展望が変化し続けていても、ASP.NET MVC をもう一度見直せば、今でも有意義で便利な領域が数多く見つかることでしょう。


Nick Harrison は、妻 Tracy と娘と共にサウスカロライナ州コロンビアに住むソフトウェア コンサルタントです。2002 年以降、ビジネス ソリューションを構築するために、.NET を使用してフル スタックを開発しています。彼には Twitter (@Neh123us、英語) からアクセスできます。ブログ投稿、出版物、講演についても、Twitter で告知しています。

この記事のレビューに協力してくれた技術スタッフの Lide Winburn (Softdocks Inc.) に心より感謝いたします。
Lide Winburn は Softdocs, Inc のクラウド アーキテクトであり、学校のペーパーレス化を支援しています。ビール リーグ ホッケー選手でもあり、よく家族と一緒に映画も鑑賞しています。


この記事について MSDN マガジン フォーラムで議論する