Programming & Development Note

C# とか JavaScript が好きなプログラマー1年生です。

【ASP.NET Core 2.0】スキャフォールディング機能で作成したコードを編集・確認する

sanonosa-dev.hateblo.jp

前回スキャフォールディングで自動生成したコードの確認と編集を行います。

Razor PagesASP.NET MVCModel-View-Contollor とは異なり、 Model-View-ViewModel デザインパターンがとられています。
Xxxxx.cshtml がビュー、Xxxxx.cshtml.cs がビューモデルです。

f:id:sanonosa:20180217155105p:plain


スキャフォールディングしたコードは以下のように編集しました。

一覧ページ

\Pages\Movies\Index.cshtml

@page
@model RazorPagesMovie.Pages.Movies.IndexModel

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

<h2>Index</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Movies)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.ReleaseDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Genre)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Price)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

Razor Pages のビュースクリプトでは先頭に @page ディレクティブを宣言します。
@model ディレクティブは、ビューが参照するモデルオブジェクトを宣言します。
ASP.NET MVC と同様の @ を使ったコードナゲットやコードブロックに加え、asp-* タグヘルパーが使用できます。

\Pages\Movies\Index.cshtml.cs

using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;

namespace RazorPagesMovie.Pages.Movies
{
    public class IndexModel : PageModel
    {
        private readonly MovieContext _context;

        public IndexModel(MovieContext context) => 
            this._context = context;

        public IList<Movie> Movies { get; set; }

        public async Task OnGetAsync() => 
            this.Movies = await this._context.Movie.ToListAsync();
    }
}

ページモデルクラスは Microsoft.AspNetCore.Mvc.RazorPages.PageModel を継承します。
ASP.NET CoreDI コンテナーで自動でコンストラクターの引数にコンテキストクラスが渡されます。
Movies プロパティは ビュースクリプト側で foreach で一覧表示されます。
OnGetAsyncでデータベースからレコードを全て取り出します。

作成ページ

\Pages\Movies\Create.cshtml

@page
@model RazorPagesMovie.Pages.Movies.CreateModel

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

<h2>Create</h2>

<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Movie.Title" class="control-label"></label>
                <input asp-for="Movie.Title" class="form-control" />
                <span asp-validation-for="Movie.Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Movie.ReleaseDate" class="control-label"></label>
                <input asp-for="Movie.ReleaseDate" class="form-control" />
                <span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Movie.Genre" class="control-label"></label>
                <input asp-for="Movie.Genre" class="form-control" />
                <span asp-validation-for="Movie.Genre" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Movie.Price" class="control-label"></label>
                <input asp-for="Movie.Price" class="form-control" />
                <span asp-validation-for="Movie.Price" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>

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

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

RenderPartialAsync() メソッドでは _ValidationScriptsPartial.cshtml 部分ビューを描画します。
部分ビューでは以下のようにjQuery検証を読み込んでいます。

\Pages\_ValidationScriptsPartial.cshtml

<environment include="Development">
    <script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
    <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
</environment>
<environment exclude="Development">
    <script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.14.0/jquery.validate.min.js"
            asp-fallback-src="~/lib/jquery-validation/dist/jquery.validate.min.js"
            asp-fallback-test="window.jQuery && window.jQuery.validator"
            crossorigin="anonymous"
            integrity="sha384-Fnqn3nxp3506LP/7Y3j/25BlWeA3PXTyT1l78LjECcPaKCV12TsZP7yyMxOe/G/k">
    </script>
    <script src="https://ajax.aspnetcdn.com/ajax/jquery.validation.unobtrusive/3.2.6/jquery.validate.unobtrusive.min.js"
            asp-fallback-src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"
            asp-fallback-test="window.jQuery && window.jQuery.validator && window.jQuery.validator.unobtrusive"
            crossorigin="anonymous"
            integrity="sha384-JrXK+k53HACyavUKOsL+NkmSesD2P+73eDMrbTtTk0h4RmOF8hF8apPlkp26JlyH">
    </script>
</environment>

\Pages\Movies\Create.cshtml.cs

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesMovie.Models;

namespace RazorPagesMovie.Pages.Movies
{
    public class CreateModel : PageModel
    {
        private readonly MovieContext _context;

        public CreateModel(MovieContext context) => 
            this._context = context;

        public IActionResult OnGet() => Page();

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

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

            this._context.Movie.Add(this.Movie);
            await this._context.SaveChangesAsync();

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

Movie プロパティは BindProperty 属性が付与され、ビュースクリプト側でユーザーが入力した値がバインドされます。
OnPostAsync() メソッドでは ユーザーが入力したユーザーが作成したデータを登録します。
検証に失敗した場合、登録は行われません。

更新ページ

\Pages\Movies\Edit.cshtml

@page
@model RazorPagesMovie.Pages.Movies.EditModel

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

<h2>Edit</h2>

<h4>Movie</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="Movie.ID" />
            <div class="form-group">
                <label asp-for="Movie.Title" class="control-label"></label>
                <input asp-for="Movie.Title" class="form-control" />
                <span asp-validation-for="Movie.Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Movie.ReleaseDate" class="control-label"></label>
                <input asp-for="Movie.ReleaseDate" class="form-control" />
                <span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Movie.Genre" class="control-label"></label>
                <input asp-for="Movie.Genre" class="form-control" />
                <span asp-validation-for="Movie.Genre" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Movie.Price" class="control-label"></label>
                <input asp-for="Movie.Price" class="form-control" />
                <span asp-validation-for="Movie.Price" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>

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

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

\Pages\Movies\Edit.cshtml.cs

using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;

namespace RazorPagesMovie.Pages.Movies
{
    public class EditModel : PageModel
    {
        private readonly MovieContext _context;

        public EditModel(MovieContext context) => this._context = context;

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

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

            this.Movie = await this._context.Movie.SingleOrDefaultAsync(m => m.ID == id);

            if (this.Movie == null)
            {
                return NotFound();
            }
            return Page();
        }

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

            this._context.Attach(this.Movie).State = EntityState.Modified;

            try
            {
                await this._context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!MovieExists(this.Movie.ID))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return RedirectToPage("./Index");
        }

        private bool MovieExists(int id) => this._context.Movie.Any(e => e.ID == id);
    }
}

OnGetAsync() メソッドでは {id} がない場合や {id} の 映画がデータベースに存在しない場合、404NotFoundを返します。
ユーザーの入力値で映画情報を更新する OnPostAsync() では DbUpdateConcurrencyException をキャッチするため楽観的同時実行制御がされています。

詳細画面

\Pages\Movies\Details.cshtml

@page
@model RazorPagesMovie.Pages.Movies.DetailsModel

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

<h2>Details</h2>

<div>
    <h4>Movie</h4>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            @Html.DisplayNameFor(model => model.Movie.Title)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Movie.Title)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Movie.ReleaseDate)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Movie.ReleaseDate)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Movie.Genre)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Movie.Genre)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Movie.Price)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Movie.Price)
        </dd>
    </dl>
</div>
<div>
    <a asp-page="./Edit" asp-route-id="@Model.Movie.ID">Edit</a> |
    <a asp-page="./Index">Back to List</a>
</div>

\Pages\Movies\Details.cshtml.cs

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;

namespace RazorPagesMovie.Pages.Movies
{
    public class DetailsModel : PageModel
    {
        private readonly MovieContext _context;

        public DetailsModel(MovieContext context) => this._context = context;

        public Movie Movie { get; set; }

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

            this.Movie = await this._context.Movie.SingleOrDefaultAsync(m => m.ID == id);

            if (this.Movie == null)
            {
                return NotFound();
            }
            return Page();
        }
    }
}

削除ページ

\Pages\Movies\Delete.cshtml

@page
@model RazorPagesMovie.Pages.Movies.DeleteModel

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

<h2>Delete</h2>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Movie</h4>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            @Html.DisplayNameFor(model => model.Movie.Title)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Movie.Title)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Movie.ReleaseDate)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Movie.ReleaseDate)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Movie.Genre)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Movie.Genre)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Movie.Price)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Movie.Price)
        </dd>
    </dl>
    
    <form method="post">
        <input type="hidden" asp-for="Movie.ID" />
        <input type="submit" value="Delete" class="btn btn-default" /> |
        <a asp-page="./Index">Back to List</a>
    </form>
</div>

\Pages\Movies\Delete.cshtml.cs

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;

namespace RazorPagesMovie.Pages.Movies
{
    public class DeleteModel : PageModel
    {
        private readonly MovieContext _context;

        public DeleteModel(MovieContext context) => this._context = context;

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

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

            this.Movie = await this._context.Movie.SingleOrDefaultAsync(m => m.ID == id);

            if (this.Movie == null)
            {
                return NotFound();
            }
            return Page();
        }

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

            this.Movie = await this._context.Movie.FindAsync(id);

            if (this.Movie != null)
            {
                this._context.Movie.Remove(this.Movie);
                await this._context.SaveChangesAsync();
            }

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