ASP.NET Core での Razor ページの概要

作成者: Rick AndersonDave BrockKirk Larkin

Note

これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の ASP.NET Core 8.0 バージョンを参照してください。

Razor ページを利用することで、ページのコーディングに今まで以上に集中できます。また、コントローラーとビューを使用する場合より生産的になります。

モデル ビュー コントローラーのアプローチを使用するチュートリアルをお探しの場合は、「Get started with ASP.NET Core MVC」 (ASP.NET Core MVC の概要) を参照してください。

このドキュメントでは、Razor ページの概要について説明します。 手順を追って説明するチュートリアルではありません。 セクションの一部を理解できない場合は、「Razor ページの概要」を参照してください。 ASP.NET Core の概要については、「ASP.NET Core の概要」を参照してください。

前提条件

Razor ページ プロジェクトを作成する

Razor ページ プロジェクトを作成する詳細な手順については、「Razor ページの概要」を参照してください。

Razor Pages

Razor Pages は Program.cs で有効にします。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

上のコードでは以下の操作が行われます。

基本ページを検討します。

@page

<h1>Hello, world!</h1>
<h2>The time on the server is @DateTime.Now</h2>

上記のコードは、コントローラーとビューを含んだ ASP.NET Core アプリで使われる Razor ビュー ファイルによく似ています。 違いは、@page ディレクティブにあります。 @page はファイルを MVC アクションにします。つまり、コントローラーを経由せずに要求を直接処理します。 @page はページ上で最初の Razor ディレクティブである必要があります。 @page はその他の Razor コンストラクトの動作に影響します。 Razor ページのファイル名には ".cshtml" サフィックスが付きます。

PageModel クラスを使用している類似したページが、次の 2 つのファイルにあります。 Pages/Index2.cshtml ファイルは以下の通りです。

@page
@using RazorPagesIntro.Pages
@model Index2Model

<h2>Separate page model</h2>
<p>
    @Model.Message
</p>

Pages/Index2.cshtml.cs ページ モデル:

using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using System;

namespace RazorPagesIntro.Pages
{
    public class Index2Model : PageModel
    {
        public string Message { get; private set; } = "PageModel in C#";

        public void OnGet()
        {
            Message += $" Server time is { DateTime.Now }";
        }
    }
}

規則により、PageModel クラス ファイルは、Razor ページ ファイルと同じ名前に " .cs" が付加された名前になります。 たとえば、上の Razor ページは Pages/Index2.cshtml になります。 PageModel クラスを含むファイル名は、Pages/Index2.cshtml.cs になります。

URL パスのページへの関連付けは、ファイル システム内のページの場所によって決定されます。 次の表に、Razor ページ パスと一致 URL を示します。

ファイル名とパス 一致 URL
/Pages/Index.cshtml / または /Index
/Pages/Contact.cshtml /Contact
/Pages/Store/Contact.cshtml /Store/Contact
/Pages/Store/Index.cshtml /Store または /Store/Index

メモ:

  • 既定では、ランタイムが "Pages" フォルダー内で Razor ページ ファイルを検索します。
  • Index は、URL にページが含まれない場合の既定のページになります。

基本フォームを作成する

Razor ページは、アプリの構築時に Web ブラウザーで使用される一般的なパターンを実装しやすくするために設計されています。 モデル バインドタグ ヘルパー、HTML ヘルパーは、Razor Page クラスで定義されたプロパティで機能します。 Contact モデルの基本的な "お問い合わせ" フォームを実装するページを考察します。

このドキュメントのサンプルでは、Program.cs ファイルで DbContext が初期化されます。

インメモリ データベースには、Microsoft.EntityFrameworkCore.InMemory NuGet パッケージが必要です。

using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddDbContext<CustomerDbContext>(options =>
    options.UseInMemoryDatabase("name"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

データ モデル:

using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Models
{
    public class Customer
    {
        public int Id { get; set; }

        [Required, StringLength(10)]
        public string? Name { get; set; }
    }
}

db コンテキスト:

using Microsoft.EntityFrameworkCore;

namespace RazorPagesContacts.Data
{
    public class CustomerDbContext : DbContext
    {
        public CustomerDbContext (DbContextOptions<CustomerDbContext> options)
            : base(options)
        {
        }

        public DbSet<RazorPagesContacts.Models.Customer> Customer => Set<RazorPagesContacts.Models.Customer>();
    }
}

Pages/Customers/Create.cshtml ビュー ファイル:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer!.Name" />
    <input type="submit" />
</form>

Pages/Customers/Create.cshtml.cs ページ モデル:

public class CreateModel : PageModel
{
    private readonly Data.CustomerDbContext _context;

    public CreateModel(Data.CustomerDbContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [BindProperty]
    public Customer? Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        if (Customer != null) _context.Customer.Add(Customer);
        await _context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

規則により、PageModel クラスは <PageName>Model と呼ばれ、ページと同じ名前空間にあります。

PageModel クラスでは、ページの表示からロジックを分離できます。 これは、ページに送信される要求のページ ハンドラーと、ページのレンダリングに使用されるデータを定義します。 この分離により可能になること:

このページには、(ユーザーがフォームを投稿したときに) POST 要求で実行される OnPostAsyncハンドラー メソッドがあります。 任意の HTTP 動詞のハンドラー メソッドを追加できます。 最も一般的なハンドラーは次のとおりです。

  • ページに必要な状態を初期化するための OnGet。 前のコードでは、OnGet メソッドにより CreateModel.cshtmlRazor ページが表示されます。
  • フォームの送信を処理するための OnPost

Async 名前付けサフィックスは省略可能ですが、非同期関数の規則でよく使用されます。 上記のコードは、Razor ページでは一般的です。

コントローラーとビューを利用する ASP.NET アプリに慣れている場合:

  • 前の例の OnPostAsync コードは、一般的なコントローラー コードに似ています。
  • モデル バインド検証、アクションの結果など、MVC プリミティブのほとんどは Controllers ページや Razor ページと同じように動作します。

上記の OnPostAsync メソッド:

[BindProperty]
public Customer? Customer { get; set; }

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    if (Customer != null) _context.Customer.Add(Customer);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

OnPostAsync の基本的な流れは次のとおりです。

検証エラーを確認します。

  • エラーがない場合は、データを保存し、リダイレクトします。
  • エラーがある場合は、検証メッセージとともにページをもう一度表示します。 多くの場合、検証エラーはクライアントで検出され、サーバーには送信されません。

Pages/Customers/Create.cshtml ビュー ファイル:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer!.Name" />
    <input type="submit" />
</form>

Pages/Customers/Create.cshtml から表示される HTML タグ:

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input type="text" data-val="true"
           data-val-length="The field Name must be a string with a maximum length of 10."
           data-val-length-max="10" data-val-required="The Name field is required."
           id="Customer_Name" maxlength="10" name="Customer.Name" value="" />
    <input type="submit" />
    <input name="__RequestVerificationToken" type="hidden"
           value="<Antiforgery token here>" />
</form>

前のコードで投稿したフォーム:

  • 有効なデータ:

    • OnPostAsync ハンドラー メソッドにより RedirectToPage ヘルパー メソッドが呼び出されます。 RedirectToPageRedirectToPageResult のインスタンスを返します。 RedirectToPage:

      • はアクションの結果です。
      • は、(コントローラーやビューで使用される) RedirectToActionRedirectToRoute に似ています。
      • はページ用にカスタマイズされています。 上記のサンプルでは、ルート インデックス ページ (/Index) にリダイレクトします。 RedirectToPage については、「ページの URL の生成」セクションで詳しく説明されています。
  • サーバーに検証エラーが渡される:

    • OnPostAsync ハンドラー メソッドにより Page ヘルパー メソッドが呼び出されます。 PagePageResult のインスタンスを返します。 Page を返すのは、コントローラーのアクションが View を返す方法に似ています。 PageResult はハンドラー メソッドの既定の戻り値の型です。 void を返すハンドラー メソッドがページをレンダリングします。
    • 前の例では、値のないフォームが投稿された結果、ModelState.IsValid から false が返されます。 このサンプルでは、検証エラーはクライアントに表示されません。 検証エラーの処理については、このドキュメントの後半で説明します。
    [BindProperty]
    public Customer? Customer { get; set; }
    
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }
    
        if (Customer != null) _context.Customer.Add(Customer);
        await _context.SaveChangesAsync();
    
        return RedirectToPage("./Index");
    }
    
  • クライアント側の検証で検証エラーが検出される:

    • データはサーバーに投稿されていません
    • クライアント側の検証については、このドキュメントの後半で説明します。

Customer プロパティは [BindProperty] 属性を使用してモデル バインドにオプトインします。

[BindProperty]
public Customer? Customer { get; set; }

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    if (Customer != null) _context.Customer.Add(Customer);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

[BindProperty] は、クライアントが変更するべきではないプロパティを含むモデルで使用しないでください。 詳細については、「過剰ポスティング」をご覧ください。

既定では、Razor ページはプロパティを非 GET 動詞とのみバインドします。 プロパティにバインドすると、HTTP データをモデル型に変換する目的でコードを記述する必要がなくなります。 同じプロパティを使用してバインドすることでコードを減らし、フィールド (<input asp-for="Customer.Name">) からレンダリングして入力を受け入れます。

警告

セキュリティ上の理由から、ページ モデルのプロパティに対して GET 要求データのバインドをオプトインする必要があります。 プロパティにマップする前に、ユーザー入力を確認してください。 GET バインドをオプトインするのは、クエリ文字列やルート値に依存するシナリオに対処する場合に便利です。

GET 要求のプロパティをバインドするには、[BindProperty] 属性の SupportsGet プロパティを true に設定します。

[BindProperty(SupportsGet = true)]

詳細については、「ASP.NET Core コミュニティ スタンドアップ: GET ディスカッション (YouTube) でのバインド」を参照してください。

Pages/Customers/Create.cshtml ビュー ファイルのレビュー:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer!.Name" />
    <input type="submit" />
</form>
  • 前のコードでは、入力タグ ヘルパー<input asp-for="Customer.Name" /> によって HTML <input> 要素が Customer.Name モデル式にバインドされます。
  • @addTagHelper でタグ ヘルパーを使用可能にします。

ホーム ページ

Index.cshtml はホーム ページです:

@page
@model RazorPagesContacts.Pages.Customers.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<h1>Contacts home page</h1>
<form method="post">
    <table class="table">
        <thead>
            <tr>
                <th>ID</th>
                <th>Name</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
        @if (Model.Customers != null)
        {
            foreach (var contact in Model.Customers)
            {
                <tr>
                    <td> @contact.Id </td>
                    <td>@contact.Name</td>
                    <td>
                        <!-- <snippet_Edit> -->
                        <a asp-page="./Edit" asp-route-id="@contact.Id">Edit</a> |
                        <!-- </snippet_Edit> -->
                        <!-- <snippet_Delete> -->
                        <button type="submit" asp-page-handler="delete" asp-route-id="@contact.Id">delete</button>
                        <!-- </snippet_Delete> -->
                    </td>
                </tr>
            }
        }
        </tbody>
    </table>
    <a asp-page="Create">Create New</a>
</form>

関連付けられた PageModel クラス (Index.cshtml.cs):

public class IndexModel : PageModel
{
    private readonly Data.CustomerDbContext _context;
    public IndexModel(Data.CustomerDbContext context)
    {
        _context = context;
    }

    public IList<Customer>? Customers { get; set; }

    public async Task OnGetAsync()
    {
        Customers = await _context.Customer.ToListAsync();
    }

    public async Task<IActionResult> OnPostDeleteAsync(int id)
    {
        var contact = await _context.Customer.FindAsync(id);

        if (contact != null)
        {
            _context.Customer.Remove(contact);
            await _context.SaveChangesAsync();
        }

        return RedirectToPage();
    }
}

Index.cshtml ファイルには、次のマークアップが含まれています。

<a asp-page="./Edit" asp-route-id="@contact.Id">Edit</a> |

<a /a>アンカー タグ ヘルパーasp-route-{value} 属性を使用して編集ページへのリンクを生成しました。 リンクには、連絡先 ID とともにルート データが含まれています。 たとえば、https://localhost:5001/Edit/1 のようにします。 タグ ヘルパーを使うと、Razor ファイルでの HTML 要素の作成とレンダリングに、サーバー側コードを組み込むことができます。

Index.cshtml ファイルには、各顧客の連絡先の削除ボタンを作成するマークアップが含まれています。

<button type="submit" asp-page-handler="delete" asp-route-id="@contact.Id">delete</button>

レンダリングされた HTML:

<button type="submit" formaction="/Customers?id=1&amp;handler=delete">delete</button>

HTML で削除ボタンがレンダリングされる場合、その formaction には次のパラメーターが含まれています。

  • asp-route-id 属性によって指定された顧客の連絡先 ID。
  • asp-page-handler 属性によって指定された handler

ボタンが選択されると、フォームの POST 要求がサーバーに送信されます。 慣例により、ハンドラー メソッドの名前はスキーム OnPost[handler]Async に従った handler パラメーターの値に基づいて選択されます。

この例では handlerdelete であるため、OnPostDeleteAsync ハンドラー メソッドを使用して POST 要求が処理されます。 asp-page-handlerremove などの別の値に設定されている場合、名前が OnPostRemoveAsync のハンドラー メソッドが選択されます。

public async Task<IActionResult> OnPostDeleteAsync(int id)
{
    var contact = await _context.Customer.FindAsync(id);

    if (contact != null)
    {
        _context.Customer.Remove(contact);
        await _context.SaveChangesAsync();
    }

    return RedirectToPage();
}

OnPostDeleteAsync メソッド:

  • クエリ文字列から id を取得します。
  • FindAsync を使用してデータベースから顧客の連絡先を照会します。
  • 顧客の連絡先が見つからない場合、それは削除されており、データベースが更新されています。
  • ルート インデックス ページ (/Index) にリダイレクトされるように、RedirectToPage を呼び出します。

Edit.cshtml ファイル

@page "{id:int}"
@model RazorPagesContacts.Pages.Customers.EditModel

@{
    ViewData["Title"] = "Edit";
}

<h1>Edit</h1>

<h4>Customer</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Customer!.Id" />
            <div class="form-group">
                <label asp-for="Customer!.Name" class="control-label"></label>
                <input asp-for="Customer!.Name" class="form-control" />
                <span asp-validation-for="Customer!.Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="./Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

最初の行には @page "{id:int}" ディレクティブが含まれています。 ルーティングの制約 "{id:int}" は、int ルート データを含むページへの要求を受け入れるようにページに指示します。 ページへの要求に int に変換できるルート データが含まれていない場合は、ランタイムで HTTP 404 (見つかりません) エラーが返されます。 ID を省略するには、次のように ? をルート制約に追加します。

@page "{id:int?}"

Edit.cshtml.cs ファイルは以下の通りです。

public class EditModel : PageModel
{
    private readonly RazorPagesContacts.Data.CustomerDbContext _context;

    public EditModel(RazorPagesContacts.Data.CustomerDbContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Customer? Customer { get; set; }

    public async Task<IActionResult> OnGetAsync(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }

        Customer = await _context.Customer.FirstOrDefaultAsync(m => m.Id == id);
        
        if (Customer == null)
        {
            return NotFound();
        }
        return Page();
    }

    // To protect from overposting attacks, enable the specific properties you want to bind to.
    // For more details, see https://aka.ms/RazorPagesCRUD.
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        if (Customer != null)
        {
            _context.Attach(Customer).State = EntityState.Modified;

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!CustomerExists(Customer.Id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }
        }

        return RedirectToPage("./Index");
    }

    private bool CustomerExists(int id)
    {
        return _context.Customer.Any(e => e.Id == id);
    }
}

検証

検証規則:

  • はモデル クラスで指定 (宣言) されます。
  • はアプリ内のあらゆる場所で適用されます。

System.ComponentModel.DataAnnotations 名前空間には、クラスまたはプロパティに宣言的に適用される一連の組み込みの検証属性があります。 また、DataAnnotations には、書式設定を支援し、どの検証を行わない [DataType] のような書式設定属性もあります。

Customer モデルを考えてみましょう。

using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Models
{
    public class Customer
    {
        public int Id { get; set; }

        [Required, StringLength(10)]
        public string? Name { get; set; }
    }
}

次の Create.cshtml ビュー ファイルを使用:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Validation: customer name:</p>

<form method="post">
    <div asp-validation-summary="ModelOnly"></div>
    <span asp-validation-for="Customer!.Name"></span>
    Name:
    <input asp-for="Customer!.Name" />
    <input type="submit" />
</form>

<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>

上記のコードでは次の操作が行われます。

  • jQuery と jQuery 検証スクリプトが含まれます。

  • <div /><span />タグ ヘルパーを使用して次を有効にします。

    • クライアント側の検証。
    • 検証エラー レンダリング。
  • 次の HTML が生成されます。

    <p>Enter a customer name:</p>
    
    <form method="post">
        Name:
        <input type="text" data-val="true"
               data-val-length="The field Name must be a string with a maximum length of 10."
               data-val-length-max="10" data-val-required="The Name field is required."
               id="Customer_Name" maxlength="10" name="Customer.Name" value="" />
        <input type="submit" />
        <input name="__RequestVerificationToken" type="hidden"
               value="<Antiforgery token here>" />
    </form>
    
    <script src="/lib/jquery/dist/jquery.js"></script>
    <script src="/lib/jquery-validation/dist/jquery.validate.js"></script>
    <script src="/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
    

名前値なしで Create フォームを投稿すると、フォームに "The Name field is required." (名前フィールドは必須です。) というエラー メッセージが表示されます。 JavaScript がクライアントで有効になっている場合、サーバーに投稿されず、エラーがブラウザーに表示されます。

[StringLength(10)] 属性によって、レンダリングされた HTML で data-val-length-max="10" が生成されます。 data-val-length-max により、指定の最大長を超える入力がブラウザーで禁止されます。 Fiddler のようなツールを投稿の編集と返信に使用する場合:

  • 名前の長さ値が 10 を超えています。
  • "The field Name must be a string with a maximum length of 10." (名前フィールドは最大 10 文字の文字列である必要があります。) というエラー メッセージが返されます。

次の Movie モデルがあるとします。

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }

        [StringLength(60, MinimumLength = 3)]
        [Required]
        public string Title { get; set; }

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }

        [Range(1, 100)]
        [DataType(DataType.Currency)]
        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$")]
        [Required]
        [StringLength(30)]
        public string Genre { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
        [StringLength(5)]
        [Required]
        public string Rating { get; set; }
    }
}

検証属性では、適用対象のモデル プロパティに適用する動作が指定されます。

  • Required および MinimumLength 属性は、プロパティに値が必要であることを示します。ただし、この検証を満たすためにユーザーが空白を入力することは禁止されていません。

  • RegularExpression 属性は、入力できる文字を制限するために使用されます。 上のコード "Genre" では:

    • 文字のみを使用する必要があります。
    • 最初の文字は大文字にする必要があります。 空白、数字、特殊文字は使用できません。
  • RegularExpression "評価":

    • 最初の文字が大文字である必要があります。
    • 後続のスペースでは、特殊文字と数字が使用できます。 "PG-13" は評価に対して有効ですが、"Genre" に対しては失敗します。
  • Range 属性は、指定した範囲内に値を制限します。

  • StringLength 属性により、文字列プロパティの最大長が設定されます。任意で最小長も設定できます。

  • 値の型 (decimalintfloatDateTime など) は本質的に必須ではなく、[Required] 属性を必要としません。

Movie モデルの [作成] ページには、エラーと無効な値が表示されます。

Movie view form with multiple jQuery client-side validation errors

詳細については次を参照してください:

CSS の分離

CSS スタイルを個々のページ、ビュー、コンポーネントに分離して、次のものを減らすか回避します。

  • 維持が困難なグローバル スタイルへの依存関係。
  • 入れ子になったコンテンツでのスタイルの競合。

ページまたはビューの "スコープ付き CSS ファイル" を追加するには、.cshtml ファイルの名前に一致するコンパニオン .cshtml.css ファイルに CSS スタイルを配置します。 次の例では、Index.cshtml ページまたはビューにのみ適用される CSS スタイルが、Index.cshtml.css ファイルによって提供されます。

Pages/Index.cshtml.css (Razor Pages) または Views/Index.cshtml.css (MVC):

h1 {
    color: red;
}

CSS の分離は、ビルド時に発生します。 CSS セレクターは、アプリのページまたはビューによってレンダリングされたマックアップに一致するよう、フレームワークによって書き換えられます。 書き換えられた CSS スタイルは、静的なアセット {APP ASSEMBLY}.styles.css としてバンドルされ、生成されます。 プレースホルダー {APP ASSEMBLY} は、プロジェクトのアセンブリ名です。 バンドルされた CSS スタイルへのリンクは、アプリのレイアウトに配置されます。

アプリの Pages/Shared/_Layout.cshtml (Razor Pages) または Views/Shared/_Layout.cshtml (MVC) の <head> コンテンツでは、バンドルされた CSS スタイルへのリンクが存在することを確認します。

<link rel="stylesheet" href="~/{APP ASSEMBLY}.styles.css" />

次の例では、アプリのアセンブリ名は WebApp です。

<link rel="stylesheet" href="WebApp.styles.css" />

スコープ付き CSS ファイルで定義されているスタイルは、一致するファイルのレンダリング出力にのみ適用されます。 前の例では、アプリ内の他の場所で定義されている h1 CSS 宣言はすべて、Index の見出しスタイルと競合しません。 CSS スタイルのカスケードおよび継承の規則は、スコープ付き CSS ファイルに対して有効のままです。 たとえば、Index.cshtml ファイル内の <h1> 要素に直接適用されるスタイルは、Index.cshtml.css 内のスコープ付き CSS ファイルのスタイルをオーバーライドします。

Note

バンドルが発生したときに CSS スタイルの分離を保証するために、Razor コード ブロックでの CSS のインポートはサポートされていません。

CSS の分離は、HTML 要素にのみ適用されます。 CSS の分離は、タグ ヘルパーではサポートされていません。

バンドルされた CSS ファイル内では、各ページ、ビュー、または Razor コンポーネントが、b-{STRING} の形式のスコープ識別子に関連付けられています。ここで、{STRING} プレースホルダーは、フレームワークによって生成された 10 文字の文字列です。 次の例では、Razor Pages アプリの Index ページ内にある、前述の <h1> 要素のスタイルを示しています。

/* /Pages/Index.cshtml.rz.scp.css */
h1[b-3xxtam6d07] {
    color: red;
}

バンドルされているファイルから CSS スタイルが適用されている Index ページには、スコープ識別子が HTML 属性として追加されます。

<h1 b-3xxtam6d07>

識別子は、アプリに固有です。 ビルド時、規則 {STATIC WEB ASSETS BASE PATH}/Project.lib.scp.css でプロジェクト バンドルが作成されます。ここで、プレースホルダー {STATIC WEB ASSETS BASE PATH} は静的な Web アセットのベース パスです。

NuGet パッケージや Razor クラス ライブラリなどの他のプロジェクトを利用する場合、バンドル ファイルは次のようになります。

  • CSS インポートを使用してスタイルを参照する。
  • スタイルを使用するアプリの静的な Web アセットとして公開されない。

CSS プリプロセッサのサポート

CSS プリプロセッサは、変数、入れ子、モジュール、mixin、継承などの機能を利用することで CSS 開発を改善するのに役立ちます。 CSS の分離は、SASS や LESS などの CSS プリプロセッサをネイティブにサポートしませんが、ビルド プロセス中に、フレームワークにより CSS セレクターが書き換えられる前にプリプロセッサのコンパイルが行われる限り、CSS プリプロセッサの統合はシームレスです。 たとえば、Visual Studio を使用して、Visual Studio タスク ランナー エクスプローラーで既存のプリプロセッサ コンパイルをビルド前タスクとして構成します。

AspNetCore.SassCompiler などの多くのサードパーティ製 NuGet パッケージは、CSS の分離が発生する前に、ビルド プロセスの開始時に SASS または SCSS ファイルをコンパイルできます。追加の構成は必要ありません。

CSS の分離の構成

CSS 分離では、既存のツールやワークフローに依存関係がある場合など、いくつかの高度なシナリオでの構成が可能です。

スコープ識別子の形式をカスタマイズする

"このセクションの {Pages|Views} プレースホルダーは、Razor Pages アプリ用の Pages、または MVC アプリ用の Views のいずれかです。 "

既定では、スコープ識別子には、b-{STRING} の形式が使用されます。ここで、{STRING} プレースホルダーは、フレームワークによって生成される 10 文字の文字列です。 スコープ識別子の形式をカスタマイズするには、プロジェクト ファイルを目的のパターンに更新します。

<ItemGroup>
  <None Update="{Pages|Views}/Index.cshtml.css" CssScope="custom-scope-identifier" />
</ItemGroup>

前の例では、Index.cshtml.css 用に生成された CSS により、そのスコープ識別子は b-{STRING} から custom-scope-identifier に変更されます。

スコープ識別子を使用して、スコープ付き CSS ファイルでの継承を実現します。 次のプロジェクト ファイルの例では、BaseView.cshtml.css ファイルに、ビュー全体の共通スタイルが含まれています。 DerivedView.cshtml.css ファイルでは、これらのスタイルが継承されます。

<ItemGroup>
  <None Update="{Pages|Views}/BaseView.cshtml.css" CssScope="custom-scope-identifier" />
  <None Update="{Pages|Views}/DerivedView.cshtml.css" CssScope="custom-scope-identifier" />
</ItemGroup>

ワイルドカード (*) 演算子を使用して、複数のファイル間でスコープ識別子を共有します。

<ItemGroup>
  <None Update="{Pages|Views}/*.cshtml.css" CssScope="custom-scope-identifier" />
</ItemGroup>

静的な Web アセットのベース パスを変更する

スコープ付き CSS ファイルは、アプリのルートで生成されます。 プロジェクト ファイルでは、StaticWebAssetBasePath プロパティを使用して既定のパスを変更します。 次の例では、スコープ付き CSS ファイルとアプリの残りのアセットを _content パスに配置します。

<PropertyGroup>
  <StaticWebAssetBasePath>_content/$(PackageId)</StaticWebAssetBasePath>
</PropertyGroup>

自動バンドルを無効にする

フレームワークでスコープ付きファイルを公開し、それを実行時に読み込む方法をオプトアウトするには、DisableScopedCssBundling プロパティを使用します。 このプロパティを使用する場合、obj ディレクトリからの CSS ファイルの分離、それらの公開、および実行時の読み込みを、他のツールまたはプロセスが担当することを意味します。

<PropertyGroup>
  <DisableScopedCssBundling>true</DisableScopedCssBundling>
</PropertyGroup>

Razor クラス ライブラリ (RCL) のサポート

Razor クラス ライブラリ (RCL) により分離スタイルが提供される場合、<link> タグの href 属性は {STATIC WEB ASSET BASE PATH}/{PACKAGE ID}.bundle.scp.css を指します。ここで、プレースホルダーは次のとおりです。

  • {STATIC WEB ASSET BASE PATH}: 静的な Web 資産のベース パス。
  • {PACKAGE ID}: ライブラリのパッケージ識別子。 プロジェクト ファイルでパッケージ識別子が指定されていない場合、パッケージ識別子の既定値は、プロジェクトのアセンブリ名になります。

次に例を示します。

  • 静的な Web 資産のベース パスは _content/ClassLib です。
  • クラス ライブラリのアセンブリ名は ClassLib です。

Pages/Shared/_Layout.cshtml (Razor Pages) または Views/Shared/_Layout.cshtml (MVC):

<link href="_content/ClassLib/ClassLib.bundle.scp.css" rel="stylesheet">

RCL の詳細については、次の記事を参照してください。

Blazor の CSS の分離については、「ASP.NET Core Blazor の CSS の分離」をご覧ください。

OnGet ハンドラー フォールバックを使用した HEAD 要求の処理

HEAD 要求により、特定のリソースのヘッダーを取得できます。 GET 要求とは異なり、HEAD 要求から応答本文は返されません。

通常、HEAD 要求に対して OnHead ハンドラーが作成され、呼び出されます。

public void OnHead()
{
    HttpContext.Response.Headers.Add("Head Test", "Handled by OnHead!");
}

OnHead ハンドラーが定義されていない場合、Razor ページは OnGet ハンドラーの呼び出しにフォールバックします。

XSRF/CSRF および Razor ページ

Razor ページは、偽造防止検証によって保護されます。 FormTagHelper により HTML フォーム要素に偽造防止トークンが挿入されます。

Razor ページでのレイアウト、パーシャル、テンプレート、およびタグ ヘルパーの使用

ページは、Razor ビュー エンジンのすべての機能で動作します。 レイアウト、パーシャル、テンプレート、タグ ヘルパー、" _ViewStart.cshtml"、" _ViewImports.cshtml" は、従来の Razor ビューの場合と同じように動作します。

これらの機能の一部を利用してこのページをまとめてみましょう。

レイアウト ページPages/Shared/_Layout.cshtml に追加します。

<!DOCTYPE html>
<html>
<head>
    <title>RP Sample</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
</head>
<body>
    <a asp-page="/Index">Home</a>
    <a asp-page="/Customers/Create">Create</a>
    <a asp-page="/Customers/Index">Customers</a> <br />

    @RenderBody()
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
    <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
</body>
</html>

レイアウトは次のことを行います。

  • (ページでレイアウトを止めない限り) 各ページのレイアウトを制御します。
  • JavaScript やスタイルシートなどの HTML 構造をインポートします。
  • @RenderBody() が呼び出されるところで Razor ページの内容が表示されます。

詳細については、レイアウトに関するページを参照してください。

Layout プロパティは Pages/_ViewStart.cshtml で設定されています。

@{
    Layout = "_Layout";
}

レイアウトは、Pages/Shared フォルダーにあります。 ページは現在のページと同じフォルダーから開始して、階層的に他のビュー (レイアウト、テンプレート、パーシャル) を検索します。 "Pages/Shared" フォルダー内のレイアウトは、"Pages" フォルダー配下の任意の Razor ページから使用できます。

レイアウト ファイルは Pages/Shared フォルダーに入ります。

レイアウト ファイルを Views/Shared フォルダー内に配置しないことをお勧めします。 Views/Shared は MVC ビュー パターンです。 Razor ページは、パス規則ではなく、フォルダー階層に依存することを意図しています。

Razor ページからのビュー検索には、"Pages" フォルダーが含まれます。 MVC コントローラーで使用されているレイアウト、テンプレート、パーシャルと、従来の Razor ビューは "機能します"。

Pages/_ViewImports.cshtml ファイルを追加します:

@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

@namespace はこのチュートリアルで後ほど説明します。 @addTagHelper ディレクティブにより、組み込みタグ ヘルパーPages フォルダー内のすべてのページにもたらされます。

ページに設定される @namespace ディレクティブ:

@page
@namespace RazorPagesIntro.Pages.Customers

@model NameSpaceModel

<h2>Name space</h2>
<p>
    @Model.Message
</p>

@namespace ディレクティブは、ページの名前空間を設定します。 @model ディレクティブには、名前空間を含める必要はありません。

@namespace ディレクティブが _ViewImports.cshtml に含まれていると、指定した名前空間が @namespace ディレクティブをインポートするページで生成された名前空間のプレフィックスを提供します。 生成された名前空間の残りの部分 (サフィックスの部分) は、 _ViewImports.cshtml を含むフォルダーとページを含むフォルダー間のドットで区切られた相対パスです。

たとえば、PageModel クラス Pages/Customers/Edit.cshtml.cs は名前空間を明示的に設定します。

namespace RazorPagesContacts.Pages
{
    public class EditModel : PageModel
    {
        private readonly AppDbContext _db;

        public EditModel(AppDbContext db)
        {
            _db = db;
        }

        // Code removed for brevity.

Pages/_ViewImports.cshtml ファイルは次の名前空間を設定します。

@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

Pages/Customers/Edit.cshtmlRazor ページの生成された名前空間は、PageModel クラスと同じです。

@namespace は、 "従来の Razor ビューでも機能します。"

Pages/Customers/Create.cshtml ビュー ファイル を考えてみましょう。

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Validation: customer name:</p>

<form method="post">
    <div asp-validation-summary="ModelOnly"></div>
    <span asp-validation-for="Customer!.Name"></span>
    Name:
    <input asp-for="Customer!.Name" />
    <input type="submit" />
</form>

<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>

更新後の Pages/Customers/Create.cshtml ビュー ファイル、 _ViewImports.cshtml、前のレイアウト ファイル:

@page
@model CreateModel

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer!.Name" />
    <input type="submit" />
</form>

前のコードでは、 _ViewImports.cshtml によって名前空間とタグ ヘルパーがインポートされました。 レイアウト ファイルによって JavaScript ファイルがインポートされました。

Razor ページのスタート プロジェクトには、クライアント側の検証をフックする "Pages/_ValidationScriptsPartial.cshtml" が含まれています。

部分ビューの詳細については、「ASP.NET Core の部分ビュー」を参照してください。

ページの URL の生成

上に示した Create ページでは、RedirectToPage を使用します。

public class CreateModel : PageModel
{
    private readonly Data.CustomerDbContext _context;

    public CreateModel(Data.CustomerDbContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [BindProperty]
    public Customer? Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        if (Customer != null) _context.Customer.Add(Customer);
        await _context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

アプリには次のファイル/フォルダー構造があります。

  • /Pages

    • Index.cshtml

    • Privacy.cshtml

    • /Customers

      • Create.cshtml
      • Edit.cshtml
      • Index.cshtml

成功すると、Pages/Customers/Create.cshtml ページと Pages/Customers/Edit.cshtml ページが Pages/Customers/Index.cshtml にリダイレクトされます。 文字列 ./Index は、前のページにアクセスするために使用される相対ページ名です。 これは、Pages/Customers/Index.cshtml ページへの URI を生成するために使われます。 次に例を示します。

  • Url.Page("./Index", ...)
  • <a asp-page="./Index">Customers Index Page</a>
  • RedirectToPage("./Index")

絶対ページ名 /Index は、Pages/Index.cshtml ページへの URL を生成するために使われます。 次に例を示します。

  • Url.Page("/Index", ...)
  • <a asp-page="/Index">Home Index Page</a>
  • RedirectToPage("/Index")

ページ名は、先頭の / を含む、ルート /Pages フォルダーからページへのパスです (たとえば /Index)。 先述の URL 生成サンプルでは、URL のハードコーディングに関する拡張オプションと機能が提供されます。 URL の生成はルーティングを使用し、ターゲット パスで定義されたルート方法に従って、パラメーターの生成とエンコードができます。

ページの URL 生成は、相対名をサポートします。 次の表に、Pages/Customers/Create.cshtml の異なる RedirectToPage パラメーターで選択されたインデックス ページを示します。

RedirectToPage(x) ページ
RedirectToPage("/Index") Pages/Index
RedirectToPage("./Index"); Pages/Customers/Index
RedirectToPage("../Index") Pages/Index
RedirectToPage("Index") Pages/Customers/Index

RedirectToPage("Index")RedirectToPage("./Index")RedirectToPage("../Index")相対名です。 RedirectToPage パラメーターは現在のページのパスと組み合わされて、ターゲット ページの名前を計算します。

相対名のリンクは、複雑な構造を持つサイトを構築する際に役立ちます。 あるフォルダー内のページ間をリンクする目的で相対名を使用するとき:

  • フォルダー名を変更しても相対リンクは壊れません。
  • フォルダー名が含まれていないため、リンクは壊れません。

別の [区分] のページにリダイレクトするには、その区分を指定します。

RedirectToPage("/Index", new { area = "Services" });

詳しくは、「ASP.NET Core の区分」と「ASP.NET Core での Razor ページのルートとアプリの規則」をご覧ください。

ViewData 属性

データは ViewDataAttribute を含むページに渡すことができます。 [ViewData] 属性を含むプロパティにはその値が格納され、ViewDataDictionary から読み込まれます。

次の例では、AboutModel により、[ViewData] 属性が Title プロパティに適用されます。

public class AboutModel : PageModel
{
    [ViewData]
    public string Title { get; } = "About";

    public void OnGet()
    {
    }
}

[About] ページでは、モデル プロパティとして Title プロパティにアクセスします。

<h1>@Model.Title</h1>

レイアウトでは、タイトルは ViewData ディクショナリから読み込まれます。

<!DOCTYPE html>
<html lang="en">
<head>
    <title>@ViewData["Title"] - WebApplication</title>
    ...

TempData

ASP.NET Core により TempData が公開されます。 このプロパティは、読み取られるまでデータを格納します。 Keep メソッドと Peek メソッドは、削除せずにデータを確認するために使用できます。 TempData は、複数の要求に対してデータが必要な場合のリダイレクトに役立ちます。

次のコードは、TempData を使用して Message の値を設定します。

public class CreateDotModel : PageModel
{
    private readonly AppDbContext _db;

    public CreateDotModel(AppDbContext db)
    {
        _db = db;
    }

    [TempData]
    public string Message { get; set; }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _db.Customers.Add(Customer);
        await _db.SaveChangesAsync();
        Message = $"Customer {Customer.Name} added";
        return RedirectToPage("./Index");
    }
}

Pages/Customers/Index.cshtml ファイル内の次のマークアップは、TempData を使用して Message の値を表示します。

<h3>Msg: @Model.Message</h3>

Pages/Customers/Index.cshtml.cs ページは、[TempData] 属性を Message プロパティに適用します。

[TempData]
public string Message { get; set; }

詳細については、「TempData」を参照してください。

ページあたり複数のハンドラー

次のページでは、asp-page-handler タグ ヘルパーを使用して 2 つのハンドラーにマークアップが生成されます。

@page
@model CreateFATHModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <!-- <snippet_Handlers> -->
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
        <!-- </snippet_Handlers> -->
    </form>
</body>
</html>

前の例のフォームには、それぞれが FormActionTagHelper を使用して異なる URL に送信する 2 つの送信ボタンがあります。 asp-page-handler 属性は、asp-page のコンパニオンです。 asp-page-handler はページごとに定義されている各ハンドラー メソッドに送信する URL を生成します。 サンプルは現在のページにリンクしているため、asp-page は指定されません。

ページ モデル:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;

namespace RazorPagesContacts.Pages.Customers
{
    public class CreateFATHModel : PageModel
    {
        private readonly AppDbContext _db;

        public CreateFATHModel(AppDbContext db)
        {
            _db = db;
        }

        [BindProperty]
        public Customer Customer { get; set; }

        public async Task<IActionResult> OnPostJoinListAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _db.Customers.Add(Customer);
            await _db.SaveChangesAsync();
            return RedirectToPage("/Index");
        }

        public async Task<IActionResult> OnPostJoinListUCAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }
            Customer.Name = Customer.Name?.ToUpperInvariant();
            return await OnPostJoinListAsync();
        }
    }
}

上記のコードは、名前付きハンドラー メソッドを使用しています。 名前付きハンドラー メソッドは、名前の On<HTTP Verb> の後および Async の前 (ある場合) のテキストを取得して作成されます。 前の例では、ページ メソッドは OnPostJoinListAsync と OnPostJoinListUCAsync です。 OnPostAsync を削除すると、ハンドラー名は JoinListJoinListUC になります。

<input type="submit" asp-page-handler="JoinList" value="Join" />
<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />

上記のコードを使用すると、OnPostJoinListAsync に送信される URL パスは https://localhost:5001/Customers/CreateFATH?handler=JoinList になります。 OnPostJoinListUCAsync に送信される URL パスは https://localhost:5001/Customers/CreateFATH?handler=JoinListUC です。

カスタム ルート

@page ディレクティブを次に使用します:

  • カスタム ルートをページに指定します。 たとえば、[バージョン情報] ページへのルートを @page "/Some/Other/Path" を使用して /Some/Other/Path に設定することができます。
  • ページの既定のルートにセグメントを追加します。 たとえば、"item" セグメントを @page "item" を使用してページの既定のルートに追加することができます。
  • ページの既定のルートにパラメーターを追加します。 たとえば、@page "{id}" を含むページに ID パラメーター id を必須とすることができます。

パスの先頭のチルダ (~) によって指定されたルートの相対パスがサポートされます。 たとえば、@page "~/Some/Other/Path"@page "/Some/Other/Path" と同じです。

URL 内のクエリ文字列 ?handler=JoinList が気に入らない場合は、ルートを変更して URL のパス部分にハンドラー名を挿入することができます。 @page ディレクティブの後に二重引用符で囲んだルート テンプレートを追加して、ルートをカスタマイズすることができます。

@page "{handler?}"
@model CreateRouteModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
    </form>
</body>
</html>

上記のコードを使用すると、OnPostJoinListAsync に送信される URL パスは https://localhost:5001/Customers/CreateFATH/JoinList になります。 OnPostJoinListUCAsync に送信される URL パスは https://localhost:5001/Customers/CreateFATH/JoinListUC です。

handler の後の ? は、ルート パラメーターが省略可能なことを意味します。

JavaScript (JS) ファイルの併置

ページとビューの JavaScript (JS) ファイルの併置は、アプリでスクリプトを整理する便利な方法です。

JS ファイルを併置するには、次のファイル名拡張子の規則を使用します。

  • Razor Pages アプリのページと MVC アプリのビュー: .cshtml.js。 例:
    • Pages/Index.cshtml にある Razor Pages アプリの Index ページに対する Pages/Index.cshtml.js
    • Views/Home/Index.cshtml にある MVC アプリの Index ビューに対する Views/Home/Index.cshtml.js

併置された JS ファイルは、"プロジェクト内のファイルへのパス" を使用してパブリックにアドレス指定できます。

  • アプリ内の併置されたスクリプト ファイルのページとビュー:

    {PATH}/{PAGE, VIEW, OR COMPONENT}.{EXTENSION}.js

    • {PATH} プレースホルダーは、ページ、ビュー、またはコンポーネントへのパスです。
    • {PAGE, VIEW, OR COMPONENT} プレースホルダーは、ページ、ビュー、またはコンポーネントです。
    • {EXTENSION} プレースホルダーは、ページ、ビュー、またはコンポーネントの拡張子 (razor または cshtml) と一致します。

    Razor ページの例:

    Index ページの JS ファイルは、Index ページ (Pages/Index.cshtml) と共に Pages フォルダー (Pages/Index.cshtml.js) に配置されます。 Index ページでは、スクリプトは Pages フォルダー内のパスで参照されます。

    @section Scripts {
      <script src="~/Pages/Index.cshtml.js"></script>
    }
    

    アプリが公開されると、スクリプトはフレームワークによって Web ルートに自動的に移動されます。 前の例では、スクリプトは bin\Release\{TARGET FRAMEWORK MONIKER}\publish\wwwroot\Pages\Index.cshtml.js に移動されます。{TARGET FRAMEWORK MONIKER} プレースホルダーはターゲット フレームワーク モニカー (TFM) です。 Index ページ内のスクリプトの相対 URL を変更する必要はありません。

    アプリが公開されると、スクリプトはフレームワークによって Web ルートに自動的に移動されます。 前の例では、スクリプトは bin\Release\{TARGET FRAMEWORK MONIKER}\publish\wwwroot\Components\Pages\Index.razor.js に移動されます。{TARGET FRAMEWORK MONIKER} プレースホルダーはターゲット フレームワーク モニカー (TFM) です。 Index コンポーネント内のスクリプトの相対 URL を変更する必要はありません。

  • Razor クラス ライブラリ (RCL) によって提供されるスクリプトの場合:

    _content/{PACKAGE ID}/{PATH}/{PAGE, VIEW, OR COMPONENT}.{EXTENSION}.js

    • {PACKAGE ID} プレースホルダーは、RCL のパッケージ識別子 (または、アプリによって参照されるクラス ライブラリのライブラリ名) です。
    • {PATH} プレースホルダーは、ページ、ビュー、またはコンポーネントへのパスです。 Razor コンポーネントが RCL のルートにある場合、パス セグメントは含まれません。
    • {PAGE, VIEW, OR COMPONENT} プレースホルダーは、ページ、ビュー、またはコンポーネントです。
    • {EXTENSION} プレースホルダーは、ページ、ビュー、またはコンポーネントの拡張子 (razor または cshtml) と一致します。

詳細な構成と設定

次のセクションの構成と設定はほとんどのアプリで必要ありません。

高度なオプションを構成するには、RazorPagesOptions を構成する AddRazorPages オーバーロードを使用します。

using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages(options =>
{
    options.RootDirectory = "/MyPages";
    options.Conventions.AuthorizeFolder("/MyPages/Admin");
});

builder.Services.AddDbContext<CustomerDbContext>(options =>
    options.UseInMemoryDatabase("name"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

RazorPagesOptions を使用してページのルート ディレクトリを設定したり、ページのアプリケーション モデルの規則を追加したりできます。 規則の詳細については、「Razor ページの承認規則」を参照してください。

ビューをプリコンパイルするには、Razor ビューのコンパイルに関するページをご覧ください。

Razor Pages をコンテンツのルートに指定する

Razor Pages のルートは既定で /Pages ディレクトリです。 WithRazorPagesAtContentRoot を追加して、Razor ページをアプリのコンテンツ ルート (ContentRootPath) に置くように指定します。

using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages(options =>
{
    options.Conventions.AuthorizeFolder("/MyPages/Admin");
})
  .WithRazorPagesAtContentRoot();

builder.Services.AddDbContext<CustomerDbContext>(options =>
    options.UseInMemoryDatabase("name"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

Razor Pages をカスタム ルート ディレクトリに指定する

(相対パスを指定して) Razor ページをアプリのカスタム ルート ディレクトリに置くように指定するには、WithRazorPagesRoot を追加します。

using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages(options =>
{
    options.Conventions.AuthorizeFolder("/MyPages/Admin");
})
  .WithRazorPagesRoot("/path/to/razor/pages");

builder.Services.AddDbContext<CustomerDbContext>(options =>
    options.UseInMemoryDatabase("name"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

その他の技術情報

Razor ページ プロジェクトを作成する

Razor ページ プロジェクトを作成する詳細な手順については、「Razor ページの概要」を参照してください。

Razor Pages

Razor Pages は Startup.cs で有効にします。

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
        });
    }
}

基本ページを検討します。

@page

<h1>Hello, world!</h1>
<h2>The time on the server is @DateTime.Now</h2>

上記のコードは、コントローラーとビューを含んだ ASP.NET Core アプリで使われる Razor ビュー ファイルによく似ています。 違いは、@page ディレクティブにあります。 @page はファイルを MVC アクションにします。つまり、コントローラーを経由せずに要求を直接処理します。 @page はページ上で最初の Razor ディレクティブである必要があります。 @page はその他の Razor コンストラクトの動作に影響します。 Razor ページのファイル名には ".cshtml" サフィックスが付きます。

PageModel クラスを使用している類似したページが、次の 2 つのファイルにあります。 Pages/Index2.cshtml ファイルは以下の通りです。

@page
@using RazorPagesIntro.Pages
@model Index2Model

<h2>Separate page model</h2>
<p>
    @Model.Message
</p>

Pages/Index2.cshtml.cs ページ モデル:

using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using System;

namespace RazorPagesIntro.Pages
{
    public class Index2Model : PageModel
    {
        public string Message { get; private set; } = "PageModel in C#";

        public void OnGet()
        {
            Message += $" Server time is { DateTime.Now }";
        }
    }
}

規則により、PageModel クラス ファイルは、Razor ページ ファイルと同じ名前に " .cs" が付加された名前になります。 たとえば、上の Razor ページは Pages/Index2.cshtml になります。 PageModel クラスを含むファイル名は、Pages/Index2.cshtml.cs になります。

URL パスのページへの関連付けは、ファイル システム内のページの場所によって決定されます。 次の表に、Razor ページ パスと一致 URL を示します。

ファイル名とパス 一致 URL
/Pages/Index.cshtml / または /Index
/Pages/Contact.cshtml /Contact
/Pages/Store/Contact.cshtml /Store/Contact
/Pages/Store/Index.cshtml /Store または /Store/Index

メモ:

  • 既定では、ランタイムが "Pages" フォルダー内で Razor ページ ファイルを検索します。
  • Index は、URL にページが含まれない場合の既定のページになります。

基本フォームを作成する

Razor ページは、アプリの構築時に Web ブラウザーで使用される一般的なパターンを実装しやすくするために設計されています。 モデル バインドタグ ヘルパー、および HTML ヘルパーはすべて、Razor ページ クラスで定義されたプロパティで "機能します"。 Contact モデルの基本的な "お問い合わせ" フォームを実装するページを考察します。

このドキュメントのサンプルでは、Startup.cs ファイルで DbContext が初期化されます。

インメモリ データベースには、Microsoft.EntityFrameworkCore.InMemory NuGet パッケージが必要です。

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<CustomerDbContext>(options =>
                      options.UseInMemoryDatabase("name"));
    services.AddRazorPages();
}

データ モデル:

using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Models
{
    public class Customer
    {
        public int Id { get; set; }

        [Required, StringLength(10)]
        public string Name { get; set; }
    }
}

db コンテキスト:

using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Models;

namespace RazorPagesContacts.Data
{
    public class CustomerDbContext : DbContext
    {
        public CustomerDbContext(DbContextOptions options)
            : base(options)
        {
        }

        public DbSet<Customer> Customers { get; set; }
    }
}

Pages/Create.cshtml ビュー ファイル:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

Pages/Create.cshtml.cs ページ モデル:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;
using RazorPagesContacts.Models;
using System.Threading.Tasks;

namespace RazorPagesContacts.Pages.Customers
{
    public class CreateModel : PageModel
    {
        private readonly CustomerDbContext _context;

        public CreateModel(CustomerDbContext context)
        {
            _context = context;
        }

        public IActionResult OnGet()
        {
            return Page();
        }

        [BindProperty]
        public Customer Customer { get; set; }

        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _context.Customers.Add(Customer);
            await _context.SaveChangesAsync();

            return RedirectToPage("./Index");
        }
    }
}

規則により、PageModel クラスは <PageName>Model と呼ばれ、ページと同じ名前空間にあります。

PageModel クラスでは、ページの表示からロジックを分離できます。 これは、ページに送信される要求のページ ハンドラーと、ページのレンダリングに使用されるデータを定義します。 この分離により可能になること:

このページには、(ユーザーがフォームを投稿したときに) POST 要求で実行される OnPostAsyncハンドラー メソッドがあります。 任意の HTTP 動詞のハンドラー メソッドを追加できます。 最も一般的なハンドラーは次のとおりです。

  • ページに必要な状態を初期化するための OnGet。 前のコードでは、OnGet メソッドにより CreateModel.cshtmlRazor ページが表示されます。
  • フォームの送信を処理するための OnPost

Async 名前付けサフィックスは省略可能ですが、非同期関数の規則でよく使用されます。 上記のコードは、Razor ページでは一般的です。

コントローラーとビューを利用する ASP.NET アプリに慣れている場合:

  • 前の例の OnPostAsync コードは、一般的なコントローラー コードに似ています。
  • モデル バインド検証、アクションの結果など、MVC プリミティブのほとんどは Controllers ページや Razor ページと同じように動作します。

上記の OnPostAsync メソッド:

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Customers.Add(Customer);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

OnPostAsync の基本的な流れは次のとおりです。

検証エラーを確認します。

  • エラーがない場合は、データを保存し、リダイレクトします。
  • エラーがある場合は、検証メッセージとともにページをもう一度表示します。 多くの場合、検証エラーはクライアントで検出され、サーバーには送信されません。

Pages/Create.cshtml ビュー ファイル:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

Pages/Create.cshtml から表示される HTML タグ:

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input type="text" data-val="true"
           data-val-length="The field Name must be a string with a maximum length of 10."
           data-val-length-max="10" data-val-required="The Name field is required."
           id="Customer_Name" maxlength="10" name="Customer.Name" value="" />
    <input type="submit" />
    <input name="__RequestVerificationToken" type="hidden"
           value="<Antiforgery token here>" />
</form>

前のコードで投稿したフォーム:

  • 有効なデータ:

    • OnPostAsync ハンドラー メソッドにより RedirectToPage ヘルパー メソッドが呼び出されます。 RedirectToPageRedirectToPageResult のインスタンスを返します。 RedirectToPage:

      • はアクションの結果です。
      • は、(コントローラーやビューで使用される) RedirectToActionRedirectToRoute に似ています。
      • はページ用にカスタマイズされています。 上記のサンプルでは、ルート インデックス ページ (/Index) にリダイレクトします。 RedirectToPage については、「ページの URL の生成」セクションで詳しく説明されています。
  • サーバーに検証エラーが渡される:

    • OnPostAsync ハンドラー メソッドにより Page ヘルパー メソッドが呼び出されます。 PagePageResult のインスタンスを返します。 Page を返すのは、コントローラーのアクションが View を返す方法に似ています。 PageResult はハンドラー メソッドの既定の戻り値の型です。 void を返すハンドラー メソッドがページをレンダリングします。
    • 前の例では、値のないフォームが投稿された結果、ModelState.IsValid から false が返されます。 このサンプルでは、検証エラーはクライアントに表示されません。 検証エラーの処理については、このドキュメントの後半で説明します。
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }
    
        _context.Customers.Add(Customer);
        await _context.SaveChangesAsync();
    
        return RedirectToPage("./Index");
    }
    
  • クライアント側の検証で検証エラーが検出される:

    • データはサーバーに投稿されていません
    • クライアント側の検証については、このドキュメントの後半で説明します。

Customer プロパティは [BindProperty] 属性を使用してモデル バインドにオプトインします。

public class CreateModel : PageModel
{
    private readonly CustomerDbContext _context;

    public CreateModel(CustomerDbContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Customers.Add(Customer);
        await _context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

[BindProperty] は、クライアントが変更するべきではないプロパティを含むモデルで使用しないでください。 詳細については、「過剰ポスティング」をご覧ください。

既定では、Razor ページはプロパティを非 GET 動詞とのみバインドします。 プロパティにバインドすると、HTTP データをモデル型に変換する目的でコードを記述する必要がなくなります。 同じプロパティを使用してバインドすることでコードを減らし、フィールド (<input asp-for="Customer.Name">) からレンダリングして入力を受け入れます。

警告

セキュリティ上の理由から、ページ モデルのプロパティに対して GET 要求データのバインドをオプトインする必要があります。 プロパティにマップする前に、ユーザー入力を確認してください。 GET バインドをオプトインするのは、クエリ文字列やルート値に依存するシナリオに対処する場合に便利です。

GET 要求のプロパティをバインドするには、[BindProperty] 属性の SupportsGet プロパティを true に設定します。

[BindProperty(SupportsGet = true)]

詳細については、「ASP.NET Core コミュニティ スタンドアップ: GET ディスカッション (YouTube) でのバインド」を参照してください。

Pages/Create.cshtml ビュー ファイルのレビュー:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>
  • 前のコードでは、入力タグ ヘルパー<input asp-for="Customer.Name" /> によって HTML <input> 要素が Customer.Name モデル式にバインドされます。
  • @addTagHelper でタグ ヘルパーを使用可能にします。

ホーム ページ

Index.cshtml はホーム ページです:

@page
@model RazorPagesContacts.Pages.Customers.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<h1>Contacts home page</h1>
<form method="post">
    <table class="table">
        <thead>
            <tr>
                <th>ID</th>
                <th>Name</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            @foreach (var contact in Model.Customer)
            {
                <tr>
                    <td> @contact.Id  </td>
                    <td>@contact.Name</td>
                    <td>
                        <!-- <snippet_Edit> -->
                        <a asp-page="./Edit" asp-route-id="@contact.Id">Edit</a> |
                        <!-- </snippet_Edit> -->
                        <!-- <snippet_Delete> -->
                        <button type="submit" asp-page-handler="delete" asp-route-id="@contact.Id">delete</button>
                        <!-- </snippet_Delete> -->
                    </td>
                </tr>
            }
        </tbody>
    </table>
    <a asp-page="Create">Create New</a>
</form>

関連付けられた PageModel クラス (Index.cshtml.cs):

public class IndexModel : PageModel
{
    private readonly CustomerDbContext _context;

    public IndexModel(CustomerDbContext context)
    {
        _context = context;
    }

    public IList<Customer> Customer { get; set; }

    public async Task OnGetAsync()
    {
        Customer = await _context.Customers.ToListAsync();
    }

    public async Task<IActionResult> OnPostDeleteAsync(int id)
    {
        var contact = await _context.Customers.FindAsync(id);

        if (contact != null)
        {
            _context.Customers.Remove(contact);
            await _context.SaveChangesAsync();
        }

        return RedirectToPage();
    }
}

Index.cshtml ファイルには、次のマークアップが含まれています。

<a asp-page="./Edit" asp-route-id="@contact.Id">Edit</a> |

<a /a>アンカー タグ ヘルパーasp-route-{value} 属性を使用して編集ページへのリンクを生成しました。 リンクには、連絡先 ID とともにルート データが含まれています。 たとえば、https://localhost:5001/Edit/1 のようにします。 タグ ヘルパーを使うと、Razor ファイルでの HTML 要素の作成とレンダリングに、サーバー側コードを組み込むことができます。

Index.cshtml ファイルには、各顧客の連絡先の削除ボタンを作成するマークアップが含まれています。

<button type="submit" asp-page-handler="delete" asp-route-id="@contact.Id">delete</button>

レンダリングされた HTML:

<button type="submit" formaction="/Customers?id=1&amp;handler=delete">delete</button>

HTML で削除ボタンがレンダリングされる場合、その formaction には次のパラメーターが含まれています。

  • asp-route-id 属性によって指定された顧客の連絡先 ID。
  • asp-page-handler 属性によって指定された handler

ボタンが選択されると、フォームの POST 要求がサーバーに送信されます。 慣例により、ハンドラー メソッドの名前はスキーム OnPost[handler]Async に従った handler パラメーターの値に基づいて選択されます。

この例では handlerdelete であるため、OnPostDeleteAsync ハンドラー メソッドを使用して POST 要求が処理されます。 asp-page-handlerremove などの別の値に設定されている場合、名前が OnPostRemoveAsync のハンドラー メソッドが選択されます。

public async Task<IActionResult> OnPostDeleteAsync(int id)
{
    var contact = await _context.Customers.FindAsync(id);

    if (contact != null)
    {
        _context.Customers.Remove(contact);
        await _context.SaveChangesAsync();
    }

    return RedirectToPage();
}

OnPostDeleteAsync メソッド:

  • クエリ文字列から id を取得します。
  • FindAsync を使用してデータベースから顧客の連絡先を照会します。
  • 顧客の連絡先が見つからない場合、それは削除されており、データベースが更新されています。
  • ルート インデックス ページ (/Index) にリダイレクトされるように、RedirectToPage を呼び出します。

Edit.cshtml ファイル

@page "{id:int}"
@model RazorPagesContacts.Pages.Customers.EditModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers


<h1>Edit Customer - @Model.Customer.Id</h1>
<form method="post">
    <div asp-validation-summary="All"></div>
    <input asp-for="Customer.Id" type="hidden" />
    <div>
        <label asp-for="Customer.Name"></label>
        <div>
            <input asp-for="Customer.Name" />
            <span asp-validation-for="Customer.Name"></span>
        </div>
    </div>

    <div>
        <button type="submit">Save</button>
    </div>
</form>

最初の行には @page "{id:int}" ディレクティブが含まれています。 ルーティングの制約 "{id:int}" は、int ルート データを含むページへの要求を受け入れるようにページに指示します。 ページへの要求に int に変換できるルート データが含まれていない場合は、ランタイムで HTTP 404 (見つかりません) エラーが返されます。 ID を省略するには、次のように ? をルート制約に追加します。

@page "{id:int?}"

Edit.cshtml.cs ファイルは以下の通りです。

public class EditModel : PageModel
{
    private readonly CustomerDbContext _context;

    public EditModel(CustomerDbContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Customer = await _context.Customers.FindAsync(id);

        if (Customer == null)
        {
            return RedirectToPage("./Index");
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Attach(Customer).State = EntityState.Modified;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            throw new Exception($"Customer {Customer.Id} not found!");
        }

        return RedirectToPage("./Index");
    }

}

検証

検証規則:

  • はモデル クラスで指定 (宣言) されます。
  • はアプリ内のあらゆる場所で適用されます。

System.ComponentModel.DataAnnotations 名前空間には、クラスまたはプロパティに宣言的に適用される一連の組み込みの検証属性があります。 また、DataAnnotations には、書式設定を支援し、どの検証を行わない [DataType] のような書式設定属性もあります。

Customer モデルを考えてみましょう。

using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Models
{
    public class Customer
    {
        public int Id { get; set; }

        [Required, StringLength(10)]
        public string Name { get; set; }
    }
}

次の Create.cshtml ビュー ファイルを使用:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Validation: customer name:</p>

<form method="post">
    <div asp-validation-summary="ModelOnly"></div>
    <span asp-validation-for="Customer.Name"></span>
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>

上記のコードでは次の操作が行われます。

  • jQuery と jQuery 検証スクリプトが含まれます。

  • <div /><span />タグ ヘルパーを使用して次を有効にします。

    • クライアント側の検証。
    • 検証エラー レンダリング。
  • 次の HTML が生成されます。

    <p>Enter a customer name:</p>
    
    <form method="post">
        Name:
        <input type="text" data-val="true"
               data-val-length="The field Name must be a string with a maximum length of 10."
               data-val-length-max="10" data-val-required="The Name field is required."
               id="Customer_Name" maxlength="10" name="Customer.Name" value="" />
        <input type="submit" />
        <input name="__RequestVerificationToken" type="hidden"
               value="<Antiforgery token here>" />
    </form>
    
    <script src="/lib/jquery/dist/jquery.js"></script>
    <script src="/lib/jquery-validation/dist/jquery.validate.js"></script>
    <script src="/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
    

名前値なしで Create フォームを投稿すると、フォームに "The Name field is required." (名前フィールドは必須です。) というエラー メッセージが表示されます。 JavaScript がクライアントで有効になっている場合、サーバーに投稿されず、エラーがブラウザーに表示されます。

[StringLength(10)] 属性によって、レンダリングされた HTML で data-val-length-max="10" が生成されます。 data-val-length-max により、指定の最大長を超える入力がブラウザーで禁止されます。 Fiddler のようなツールを投稿の編集と返信に使用する場合:

  • 名前の長さ値が 10 を超えています。
  • "The field Name must be a string with a maximum length of 10." (名前フィールドは最大 10 文字の文字列である必要があります。) というエラー メッセージが返されます。

次の Movie モデルがあるとします。

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }

        [StringLength(60, MinimumLength = 3)]
        [Required]
        public string Title { get; set; }

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }

        [Range(1, 100)]
        [DataType(DataType.Currency)]
        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$")]
        [Required]
        [StringLength(30)]
        public string Genre { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
        [StringLength(5)]
        [Required]
        public string Rating { get; set; }
    }
}

検証属性では、適用対象のモデル プロパティに適用する動作が指定されます。

  • Required および MinimumLength 属性は、プロパティに値が必要であることを示します。ただし、この検証を満たすためにユーザーが空白を入力することは禁止されていません。

  • RegularExpression 属性は、入力できる文字を制限するために使用されます。 上のコード "Genre" では:

    • 文字のみを使用する必要があります。
    • 最初の文字は大文字にする必要があります。 空白、数字、特殊文字は使用できません。
  • RegularExpression "評価":

    • 最初の文字が大文字である必要があります。
    • 後続のスペースでは、特殊文字と数字が使用できます。 "PG-13" は評価に対して有効ですが、"Genre" に対しては失敗します。
  • Range 属性は、指定した範囲内に値を制限します。

  • StringLength 属性により、文字列プロパティの最大長が設定されます。任意で最小長も設定できます。

  • 値の型 (decimalintfloatDateTime など) は本質的に必須ではなく、[Required] 属性を必要としません。

Movie モデルの [作成] ページには、エラーと無効な値が表示されます。

Movie view form with multiple jQuery client-side validation errors

詳細については次を参照してください:

OnGet ハンドラー フォールバックを使用した HEAD 要求の処理

HEAD 要求により、特定のリソースのヘッダーを取得できます。 GET 要求とは異なり、HEAD 要求から応答本文は返されません。

通常、HEAD 要求に対して OnHead ハンドラーが作成され、呼び出されます。

public void OnHead()
{
    HttpContext.Response.Headers.Add("Head Test", "Handled by OnHead!");
}

OnHead ハンドラーが定義されていない場合、Razor ページは OnGet ハンドラーの呼び出しにフォールバックします。

XSRF/CSRF および Razor ページ

Razor ページは、偽造防止検証によって保護されます。 FormTagHelper により HTML フォーム要素に偽造防止トークンが挿入されます。

Razor ページでのレイアウト、パーシャル、テンプレート、およびタグ ヘルパーの使用

ページは、Razor ビュー エンジンのすべての機能で動作します。 レイアウト、パーシャル、テンプレート、タグ ヘルパー、" _ViewStart.cshtml"、" _ViewImports.cshtml" は、従来の Razor ビューの場合と同じように動作します。

これらの機能の一部を利用してこのページをまとめてみましょう。

レイアウト ページPages/Shared/_Layout.cshtml に追加します。

<!DOCTYPE html>
<html>
<head>
    <title>RP Sample</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
</head>
<body>
    <a asp-page="/Index">Home</a>
    <a asp-page="/Customers/Create">Create</a>
    <a asp-page="/Customers/Index">Customers</a> <br />

    @RenderBody()
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
    <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
</body>
</html>

レイアウトは次のことを行います。

  • (ページでレイアウトを止めない限り) 各ページのレイアウトを制御します。
  • JavaScript やスタイルシートなどの HTML 構造をインポートします。
  • @RenderBody() が呼び出されるところで Razor ページの内容が表示されます。

詳細については、レイアウトに関するページを参照してください。

Layout プロパティは Pages/_ViewStart.cshtml で設定されています。

@{
    Layout = "_Layout";
}

レイアウトは、Pages/Shared フォルダーにあります。 ページは現在のページと同じフォルダーから開始して、階層的に他のビュー (レイアウト、テンプレート、パーシャル) を検索します。 "Pages/Shared" フォルダー内のレイアウトは、"Pages" フォルダー配下の任意の Razor ページから使用できます。

レイアウト ファイルは Pages/Shared フォルダーに入ります。

レイアウト ファイルを Views/Shared フォルダー内に配置しないことをお勧めします。 Views/Shared は MVC ビュー パターンです。 Razor ページは、パス規則ではなく、フォルダー階層に依存することを意図しています。

Razor ページからのビュー検索には、"Pages" フォルダーが含まれます。 MVC コントローラーで使用されているレイアウト、テンプレート、パーシャルと、従来の Razor ビューは "機能します"。

Pages/_ViewImports.cshtml ファイルを追加します:

@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

@namespace はこのチュートリアルで後ほど説明します。 @addTagHelper ディレクティブにより、組み込みタグ ヘルパーPages フォルダー内のすべてのページにもたらされます。

ページに設定される @namespace ディレクティブ:

@page
@namespace RazorPagesIntro.Pages.Customers

@model NameSpaceModel

<h2>Name space</h2>
<p>
    @Model.Message
</p>

@namespace ディレクティブは、ページの名前空間を設定します。 @model ディレクティブには、名前空間を含める必要はありません。

@namespace ディレクティブが _ViewImports.cshtml に含まれていると、指定した名前空間が @namespace ディレクティブをインポートするページで生成された名前空間のプレフィックスを提供します。 生成された名前空間の残りの部分 (サフィックスの部分) は、 _ViewImports.cshtml を含むフォルダーとページを含むフォルダー間のドットで区切られた相対パスです。

たとえば、PageModel クラス Pages/Customers/Edit.cshtml.cs は名前空間を明示的に設定します。

namespace RazorPagesContacts.Pages
{
    public class EditModel : PageModel
    {
        private readonly AppDbContext _db;

        public EditModel(AppDbContext db)
        {
            _db = db;
        }

        // Code removed for brevity.

Pages/_ViewImports.cshtml ファイルは次の名前空間を設定します。

@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

Pages/Customers/Edit.cshtmlRazor ページの生成された名前空間は、PageModel クラスと同じです。

@namespace は、 "従来の Razor ビューでも機能します。"

Pages/Create.cshtml ビュー ファイル を考えてみましょう。

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Validation: customer name:</p>

<form method="post">
    <div asp-validation-summary="ModelOnly"></div>
    <span asp-validation-for="Customer.Name"></span>
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>

更新後の Pages/Create.cshtml ビュー ファイル、 _ViewImports.cshtml、前のレイアウト ファイル:

@page
@model CreateModel

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

前のコードでは、 _ViewImports.cshtml によって名前空間とタグ ヘルパーがインポートされました。 レイアウト ファイルによって JavaScript ファイルがインポートされました。

Razor ページのスタート プロジェクトには、クライアント側の検証をフックする "Pages/_ValidationScriptsPartial.cshtml" が含まれています。

部分ビューの詳細については、「ASP.NET Core の部分ビュー」を参照してください。

ページの URL の生成

上に示した Create ページでは、RedirectToPage を使用します。

public class CreateModel : PageModel
{
    private readonly CustomerDbContext _context;

    public CreateModel(CustomerDbContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Customers.Add(Customer);
        await _context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

アプリには次のファイル/フォルダー構造があります。

  • /Pages

    • Index.cshtml

    • Privacy.cshtml

    • /Customers

      • Create.cshtml
      • Edit.cshtml
      • Index.cshtml

成功すると、Pages/Customers/Create.cshtml ページと Pages/Customers/Edit.cshtml ページが Pages/Customers/Index.cshtml にリダイレクトされます。 文字列 ./Index は、前のページにアクセスするために使用される相対ページ名です。 これは、Pages/Customers/Index.cshtml ページへの URI を生成するために使われます。 次に例を示します。

  • Url.Page("./Index", ...)
  • <a asp-page="./Index">Customers Index Page</a>
  • RedirectToPage("./Index")

絶対ページ名 /Index は、Pages/Index.cshtml ページへの URL を生成するために使われます。 次に例を示します。

  • Url.Page("/Index", ...)
  • <a asp-page="/Index">Home Index Page</a>
  • RedirectToPage("/Index")

ページ名は、先頭の / を含む、ルート /Pages フォルダーからページへのパスです (たとえば /Index)。 先述の URL 生成サンプルでは、URL のハードコーディングに関する拡張オプションと機能が提供されます。 URL の生成はルーティングを使用し、ターゲット パスで定義されたルート方法に従って、パラメーターの生成とエンコードができます。

ページの URL 生成は、相対名をサポートします。 次の表に、Pages/Customers/Create.cshtml の異なる RedirectToPage パラメーターで選択されたインデックス ページを示します。

RedirectToPage(x) ページ
RedirectToPage("/Index") Pages/Index
RedirectToPage("./Index"); Pages/Customers/Index
RedirectToPage("../Index") Pages/Index
RedirectToPage("Index") Pages/Customers/Index

RedirectToPage("Index")RedirectToPage("./Index")RedirectToPage("../Index")相対名です。 RedirectToPage パラメーターは現在のページのパスと組み合わされて、ターゲット ページの名前を計算します。

相対名のリンクは、複雑な構造を持つサイトを構築する際に役立ちます。 あるフォルダー内のページ間をリンクする目的で相対名を使用するとき:

  • フォルダー名を変更しても相対リンクは壊れません。
  • フォルダー名が含まれていないため、リンクは壊れません。

別の [区分] のページにリダイレクトするには、その区分を指定します。

RedirectToPage("/Index", new { area = "Services" });

詳しくは、「ASP.NET Core の区分」と「ASP.NET Core での Razor ページのルートとアプリの規則」をご覧ください。

ViewData 属性

データは ViewDataAttribute を含むページに渡すことができます。 [ViewData] 属性を含むプロパティにはその値が格納され、ViewDataDictionary から読み込まれます。

次の例では、AboutModel により、[ViewData] 属性が Title プロパティに適用されます。

public class AboutModel : PageModel
{
    [ViewData]
    public string Title { get; } = "About";

    public void OnGet()
    {
    }
}

[About] ページでは、モデル プロパティとして Title プロパティにアクセスします。

<h1>@Model.Title</h1>

レイアウトでは、タイトルは ViewData ディクショナリから読み込まれます。

<!DOCTYPE html>
<html lang="en">
<head>
    <title>@ViewData["Title"] - WebApplication</title>
    ...

TempData

ASP.NET Core により TempData が公開されます。 このプロパティは、読み取られるまでデータを格納します。 Keep メソッドと Peek メソッドは、削除せずにデータを確認するために使用できます。 TempData は、複数の要求に対してデータが必要な場合のリダイレクトに役立ちます。

次のコードは、TempData を使用して Message の値を設定します。

public class CreateDotModel : PageModel
{
    private readonly AppDbContext _db;

    public CreateDotModel(AppDbContext db)
    {
        _db = db;
    }

    [TempData]
    public string Message { get; set; }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _db.Customers.Add(Customer);
        await _db.SaveChangesAsync();
        Message = $"Customer {Customer.Name} added";
        return RedirectToPage("./Index");
    }
}

Pages/Customers/Index.cshtml ファイル内の次のマークアップは、TempData を使用して Message の値を表示します。

<h3>Msg: @Model.Message</h3>

Pages/Customers/Index.cshtml.cs ページは、[TempData] 属性を Message プロパティに適用します。

[TempData]
public string Message { get; set; }

詳細については、「TempData」を参照してください。

ページあたり複数のハンドラー

次のページでは、asp-page-handler タグ ヘルパーを使用して 2 つのハンドラーにマークアップが生成されます。

@page
@model CreateFATHModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <!-- <snippet_Handlers> -->
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
        <!-- </snippet_Handlers> -->
    </form>
</body>
</html>

前の例のフォームには、それぞれが FormActionTagHelper を使用して異なる URL に送信する 2 つの送信ボタンがあります。 asp-page-handler 属性は、asp-page のコンパニオンです。 asp-page-handler はページごとに定義されている各ハンドラー メソッドに送信する URL を生成します。 サンプルは現在のページにリンクしているため、asp-page は指定されません。

ページ モデル:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;

namespace RazorPagesContacts.Pages.Customers
{
    public class CreateFATHModel : PageModel
    {
        private readonly AppDbContext _db;

        public CreateFATHModel(AppDbContext db)
        {
            _db = db;
        }

        [BindProperty]
        public Customer Customer { get; set; }

        public async Task<IActionResult> OnPostJoinListAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _db.Customers.Add(Customer);
            await _db.SaveChangesAsync();
            return RedirectToPage("/Index");
        }

        public async Task<IActionResult> OnPostJoinListUCAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }
            Customer.Name = Customer.Name?.ToUpperInvariant();
            return await OnPostJoinListAsync();
        }
    }
}

上記のコードは、名前付きハンドラー メソッドを使用しています。 名前付きハンドラー メソッドは、名前の On<HTTP Verb> の後および Async の前 (ある場合) のテキストを取得して作成されます。 前の例では、ページ メソッドは OnPostJoinListAsync と OnPostJoinListUCAsync です。 OnPostAsync を削除すると、ハンドラー名は JoinListJoinListUC になります。

<input type="submit" asp-page-handler="JoinList" value="Join" />
<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />

上記のコードを使用すると、OnPostJoinListAsync に送信される URL パスは https://localhost:5001/Customers/CreateFATH?handler=JoinList になります。 OnPostJoinListUCAsync に送信される URL パスは https://localhost:5001/Customers/CreateFATH?handler=JoinListUC です。

カスタム ルート

@page ディレクティブを次に使用します:

  • カスタム ルートをページに指定します。 たとえば、[バージョン情報] ページへのルートを @page "/Some/Other/Path" を使用して /Some/Other/Path に設定することができます。
  • ページの既定のルートにセグメントを追加します。 たとえば、"item" セグメントを @page "item" を使用してページの既定のルートに追加することができます。
  • ページの既定のルートにパラメーターを追加します。 たとえば、@page "{id}" を含むページに ID パラメーター id を必須とすることができます。

パスの先頭のチルダ (~) によって指定されたルートの相対パスがサポートされます。 たとえば、@page "~/Some/Other/Path"@page "/Some/Other/Path" と同じです。

URL 内のクエリ文字列 ?handler=JoinList が気に入らない場合は、ルートを変更して URL のパス部分にハンドラー名を挿入することができます。 @page ディレクティブの後に二重引用符で囲んだルート テンプレートを追加して、ルートをカスタマイズすることができます。

@page "{handler?}"
@model CreateRouteModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
    </form>
</body>
</html>

上記のコードを使用すると、OnPostJoinListAsync に送信される URL パスは https://localhost:5001/Customers/CreateFATH/JoinList になります。 OnPostJoinListUCAsync に送信される URL パスは https://localhost:5001/Customers/CreateFATH/JoinListUC です。

handler の後の ? は、ルート パラメーターが省略可能なことを意味します。

詳細な構成と設定

次のセクションの構成と設定はほとんどのアプリで必要ありません。

高度なオプションを構成するには、RazorPagesOptions を構成する AddRazorPages オーバーロードを使用します。

public void ConfigureServices(IServiceCollection services)
{            
    services.AddRazorPages(options =>
    {
        options.RootDirectory = "/MyPages";
        options.Conventions.AuthorizeFolder("/MyPages/Admin");
    });
}

RazorPagesOptions を使用してページのルート ディレクトリを設定したり、ページのアプリケーション モデルの規則を追加したりできます。 規則の詳細については、「Razor ページの承認規則」を参照してください。

ビューをプリコンパイルするには、Razor ビューのコンパイルに関するページをご覧ください。

Razor Pages をコンテンツのルートに指定する

Razor Pages のルートは既定で /Pages ディレクトリです。 WithRazorPagesAtContentRoot を追加して、Razor ページをアプリのコンテンツ ルート (ContentRootPath) に置くように指定します。

public void ConfigureServices(IServiceCollection services)
{            
    services.AddRazorPages(options =>
        {
            options.Conventions.AuthorizeFolder("/MyPages/Admin");
        })
        .WithRazorPagesAtContentRoot();
}

Razor Pages をカスタム ルート ディレクトリに指定する

(相対パスを指定して) Razor ページをアプリのカスタム ルート ディレクトリに置くように指定するには、WithRazorPagesRoot を追加します。

public void ConfigureServices(IServiceCollection services)
{            
    services.AddRazorPages(options =>
        {
            options.Conventions.AuthorizeFolder("/MyPages/Admin");
        })
        .WithRazorPagesRoot("/path/to/razor/pages");
}

その他の技術情報