2. ASP.NET MVCによるWebアプリケーション開発 - 基礎編

ASP.NET MVCの概要

MVC とは?

Model View Controller

MVC(Model View Controller モデル・ビュー・コントローラ)は、ユーザーインタフェースをもつアプリケーションソフトウェアを実装するためのデザインパターンである。 アプリケーションソフトウェアの内部データを、ユーザーが直接参照・編集する情報から分離する。そのためにアプリケーションソフトウェアを以下の3つの部分に分割する。

  1. model: アプリケーションデータ、ビジネスルール、ロジック、関数
  2. view: グラフや図などの任意の情報表現
  3. controller: 入力を受け取りmodelとviewへの命令に変換する

元々は Smalltalk における ウィンドウプログラム開発の設計指針として生まれたものです。

ASP.NET MVCは MVCのデザインパターンで ASP.NET Webアプリケーション を開発するにあたって 必須であったり、便利な機能を提供するフレームワークです。



環境について



ToDoアプリの開発

参考: Getting Started with Entity Framework 6 Code First using MVC 5


仕様概要



プロジェクトの作成

新しいプロジェクト

新規 ASP.NET プロジェクト

ASP.NET MVCプロジェクトのフォルダ構成


Entity Framework のインストール

NuGetを使用して、最新のEntity Frameworkをインストールします。



用語解説: Entity Framework

Entity Framework

Entity Framework (EF) は、.NET 開発者がドメイン固有のオブジェクトを使用してリレーショナル データを処理できるようにするオブジェクト リレーショナル マッパーです。 開発者が通常、記述する必要のあるデータ アクセス コードがほとんど不要になります。


用語解説: O/Rマッパー

Microsoft謹製のものが EntityFramework
軽量で、最も利用されていると思われるのが dapper


用語解説: NuGet

Visual Studio用のパッケージ管理システム。 rubyのgemやpythonのpipのようなツール。

コマンドラインやPowerShellからも利用可能ですが、今回は Visual Studio が用意している GUI で操作します。




手順

NuGetパッケージ マネージャーを開く

NuGetパッケージの管理

インストール完了




用語解説: Entity Framework の コード ファースト開発

データ構造を表現する POCO (Plain Old Clr Object: 特別なクラスやインターフェイスを継承していないクラス(のオブジェクト)) と POCOを管理する Contextクラスを定義することで、Entity Frameworkが必要なテーブルを生成します。 (Databaseの操作は必要ありません。)

コード ファースト開発は Entity Framework 4.1 から提供された機能です。

詳細については Entity Framework (EF) の概要 を参照してください。




Modelの作成

Todoクラス を作成していきます。

クラスの追加-1

クラスの追加-2

Todo.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel;

namespace WebApplication1.Models
{
    /// <summary>
    /// ToDoモデル
    /// </summary>
    public class Todo
    {
        public int id { get; set; }
        [DisplayName("概要")]
        public string summary { get; set; }
        [DisplayName("詳細")]
        public string detail { get; set; }
        [DisplayName("期限")]
        public DateTime limit { get; set; }
        [DisplayName("完了")]
        public bool done { get; set; }
    }
}

モデルの定義にあたって抑えるポイントは以下の3点です。




用語解説: 属性

[DisplayName("概要")]など、プロパティの上に記述されたカギカッコで括られた記述は 属性(Attributes あるいは Annotation)と呼ばれるものです。

Viewで項目が表示される時に、DisplayNameに設定した文言が項目名として使用されます。

DisplayName以外にも、 NOT NULL制約を表す Required や データの最大長を示す MaxLength などがあります。




TodoesContextクラスの作成

Contextクラスは先ほど作成した POCO とデータベースを繋げる役割を果たします。
データの取得、更新などの操作はすべてこのContextクラスを使用して行います。

TodoesContext.cs

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Web;

namespace WebApplication1.Models
{
    public class TodoesContext : DbContext
    {
        public DbSet<Todo> Todoes { get; set; }
    }
}

Contextクラスは DbContextクラスを継承します。

Todoのコレクションを管理するので、TodoesというDbSet<Todo>を定義します。
<Todo>DbSetに格納するクラスを表します。
Todoes にはデータベースから取得した Todo のコレクション (配列のようなもの) です。






Viewの作成: 共通レイアウトの作成

レイアウトページの追加

_LayoutPage1.cshtml

<!DOCTYPE html>

<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>@ViewBag.Title</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
</head>
<body>
    <div class="container">
        @RenderBody()
    </div>
</body>
</html>

Bootstrapを読み込むように指定します。 また、divclass="container"の指定を追加します。

@RenderBody() の位置に、後ほど作成する各Viewの内容がセットされます。




用語解説: Razor

Razor は HTMLにC#/VBのコードを埋め込むための仕組み (ビューエンジン) です。
@で始まる箇所がサーバーサイドで実行され、クライアントに生成したHTMLが返されます。


用語解説: Bootstrap

BootstrapはWebサイトやWebアプリケーションを作成するフリーソフトウェアツール集である。 タイポグラフィ、フォーム、ボタン、ナビゲーション、その他構成要素やJavaScript用拡張などがHTML及びCSSベースのデザインテンプレートとして用意されている。

Bootstrap




Controllerの作成

スキャフォールディング(Scaffolding、「骨組み」「足場」という意味)によって、 Create(作成)、Read(参照)、Update(更新)、Delete(削除)のような定型的なコードの骨組みを自動生成できます。

コントローラーの追加-1

コントローラーの追加-2

コントローラーの追加-3

以下のファイルが生成されます。

後ほど、生成されたファイルの中身について解説します。



デフォルトページの設定

App_Start/RouteConfig.cs を修正し、デフォルトページをToDoの一覧ページに変更します。

routes.MapRouteメソッドのdefaults引数にデフォルトの設定を定義します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace WebApplication1
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Todoes", action = "Index", id = UrlParameter.Optional }
            );
        }
    }
}




用語解説: ルーティング

ルーティングとは、リクエストURIに応じて処理を受け渡し先を決定することを言います。

ASP.NET MVCではクライアントからの要求を受け取ると、RouteConfig.csの内容を元に 呼び出すべきコントローラー/アクションを決定します。

routes.MapRouteメソッドのurl引数がルーティングの定義です。

例えば http://localhost/Todoes/Details/3 というリクエストが来た場合、 urlの定義にしたがってTodoesControllerDetailsメソッドにid=3を引数に与えて呼び出します。

また、defaultsでデフォルトのコントローラー、アクションを指定しているので、http://localhost/ という リクエストが来た場合は http://localhost/Todoes/Indexというリクエストとして処理されます。




デバッグ実行

F5キーを押して、デバッグ実行を行います。 ブラウザが起動し、一覧ページが表示されます。

Index

ToDoの追加、変更、削除が行えることを確認します。

Details






ソースコード解説

Controller

コードの細部について、順に解説します。

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Net;
using System.Web;
using System.Web.Mvc;
using WebApplication1.Models;

namespace WebApplication1.Controllers
{
    public class TodoesController : Controller
    {
        private TodoesContext db = new TodoesContext();

コントローラーのプライベート変数に Models に作成したTodoesContextを保持しています。

データベースへのアクセスはTodoesContextを介して行います。



        // GET: Todoes
        public ActionResult Index()
        {
            return View(db.Todoes.ToList());
        }

Indexメソッドの定義です。 View メソッドは アクションメソッドに対応した View を元に ViewResult ( ActionResult を継承した、Viewを表示するためのクラス)を返します。

ToList()Todo の全要素を取得し、リストとして返すメソッドです。

ここでは、views/Index.cshtml に すべてのTodoをListに格納したオブジェクトを渡して生成される結果を返しています。




用語解説: ActionResult

アクションメソッドは、戻り値となる ActionResult の派生オブジェクトを介してアクションの結果 (その後に行うべき挙動) を通知します。

代表的なActionResultの派生クラスは以下の通りです。

クラス名 ヘルパーメソッド 概要
ViewResult View アクションメソッドに対応したViewを出力
RedirectToRouteResult RedirectToAction 指定のアクションメソッドに処理を転送
ContentResult Content 指定されたテキストを出力
FileContentResult File 指定されたファイルを出力
JsonResult Json 指定されたオブジェクトをJSON形式で出力
HttpNotFoundResult HttpNotFound 404ページを出力
EmptyResult - なにも行わない



        // GET: Todoes/Details/5
        public ActionResult Details(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            Todo todo = db.Todoes.Find(id);
            if (todo == null)
            {
                return HttpNotFound();
            }
            return View(todo);
        }

Detailsメソッドの定義です。

int?intNullable型 です。
通常の intnull を設定できませんが、Nullable型は null が許容されます。

RouteConfig.csdefaults の定義により、idは省略可能です。
id が省略された場合、Detailsの引数 id には null が設定されます。

idnull の場合は要求されたURLが正しくないので、 BadRequestを返しています。

Todo todo = db.Todoes.Find(id); はデータベースから id が一致するデータをひとつ取り出します。

一致するデータが存在しない場合は Find から null が返ってくるので、 NotFoundを返しています。

一致するデータが存在すれば、それをViewにセットして返します。



        // GET: Todoes/Create
        public ActionResult Create()
        {
            return View();
        }

新規登録時は View を表示するだけです。



        // POST: Todoes/Create
        // 過多ポスティング攻撃を防止するには、バインド先とする特定のプロパティを有効にしてください。
        // 詳細については、http://go.microsoft.com/fwlink/?LinkId=317598 を参照してください。
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create([Bind(Include = "id,summary,detail,limit,done")] Todo todo)
        {
            if (ModelState.IsValid)
            {
                db.Todoes.Add(todo);
                db.SaveChanges();
                return RedirectToAction("Index");
            }

            return View(todo);
        }

[HttpPost]httpPOSTメソッドでリクエストがあった場合に呼び出されるアクションメソッドを表す属性です。

ValidateAntiForgeryTokenクロスサイト・リクエスト・フォージェリ攻撃 を防ぐための記述です。




用語解説: クロスサイトリクエストフォージェリ (CSRF)

クロスサイトリクエストフォージェリ(Cross site request forgeries、略記:CSRF、またはXSRF)は、WWW における攻撃手法のひとつである。 具体的な被害としては、掲示板に意図しない書き込みをさせられたり、オンラインショップで買い物をさせられたりするなどが挙げられる。 クロスサイトリクエストフォージェリ




Bind は POSTされたデータを Todoモデルに紐付けます。

ModelState.IsValid は入力チェックがOKかどうかを判定します。
例えば、DateTime型の limitに日付以外の値がセットされている場合は IsValidfalseとなるため 登録処理が行われません。

更新処理の中身はそのままの意味ですが、




モデルバインド と 過多ポスティング攻撃について

ASP.NET MVCでは、クライアントからの入力値を自動的にモデルに割り当てる モデルバインド という機能があります。
上記の Create は以下のように書くこともできます。

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Todo todo)

こうすることで、クライアントからPOSTされてきたデータのkey名を見てモデルに自動的に割り当ててくれるのですが、 上記の記述ではセキュリティホールの原因となる場合もあります。

例えば、以下の様な Accountクラスがあった場合を考えます。

public class Account
{
  public int id { get; set; }
  public string mail { get; set; }
  public string password { get; set; }
  public string role { get; set; }
}


ここで、 role(権限) は管理者のみが設定でき、エンドユーザーからは勝手に編集できないものとします。 (以下の様なViewのイメージ)

<html>
<body>
@using (Html.BeginForm())
{
  @Html.HiddenFor(model => model.id)
  @Html.EditorFor(model => model.mail)
  @Html.EditorFor(model => model.password)
}
</body>


Create.cshtml から POSTされた際に実行されるアクションメソッドを以下のように実装したとします。

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Account account)
{
    if (ModelState.IsValid)
    {
        db.Accounts.Add(account);
        db.SaveChanges();
        return RedirectToAction("Index");
    }

    return View(account);
}

Create.cshtmlからPOSTされた場合は id, mail, password のみがセットされてきます。
Addでは新しいレコードとしてDBに登録されるので、id は自動的に採番されますので、 プログラマーが期待している動作としては、 POSTされてきた mail と password がDBに登録される という動作だと思います。

しかし、悪意あるユーザーがPOSTデータを改ざんし、role値を含んだデータを送信すると、 Createメソッドはその値をモデルに割り当ててしまい、結果、意図せず roleに値が設定されてしまいます。

このような攻撃を 過多ポスティング攻撃 といいます。

自動生成された Create メソッドのように Bind を使用して モデルバインドするプロパティを明示することにより、過多ポスティング攻撃を防止します。

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "id,mail,password")] Account account)



        // GET: Todoes/Edit/5
        public ActionResult Edit(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            Todo todo = db.Todoes.Find(id);
            if (todo == null)
            {
                return HttpNotFound();
            }
            return View(todo);
        }

        // POST: Todoes/Edit/5
        // 過多ポスティング攻撃を防止するには、バインド先とする特定のプロパティを有効にしてください。
        // 詳細については、http://go.microsoft.com/fwlink/?LinkId=317598 を参照してください。
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Edit([Bind(Include = "id,summary,detail,limit,done")] Todo todo)
        {
            if (ModelState.IsValid)
            {
                db.Entry(todo).State = EntityState.Modified;
                db.SaveChanges();
                return RedirectToAction("Index");
            }
            return View(todo);
        }

Editメソッドは DetailsCreate の処理を組み合わせた内容です。



        // GET: Todoes/Delete/5
        public ActionResult Delete(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            Todo todo = db.Todoes.Find(id);
            if (todo == null)
            {
                return HttpNotFound();
            }
            return View(todo);
        }

        // POST: Todoes/Delete/5
        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public ActionResult DeleteConfirmed(int id)
        {
            Todo todo = db.Todoes.Find(id);
            db.Todoes.Remove(todo);
            db.SaveChanges();
            return RedirectToAction("Index");
        }

削除時の流れは以下のようになります。



        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                db.Dispose();
            }
            base.Dispose(disposing);
        }
    }
}

Dispose は終了処理です。 db.Dispose() で保持している Context を開放しています。






View

まず、Index.cshtml の内容について順に解説していきます。

@model IEnumerable<WebApplication1.Models.Todo>

コントローラーのアクションメソッドにて View(db.Todoes.ToList()) で引き渡されたデータを受け取るには、 @modelディレクティブ を使用します。
IEnumerable<WebApplication1.Models.Todo>Todoクラスのリストを表します。

@から始まる文はRazorの コードナゲット としてサーバーサイドで処理されます。

HTML内にRazorの式としてではなく、@をそのまま表示したい場合は @@ とすることでエスケープして表示されます。

また、コメントを記載する場合は @* ... *@ と記述します。
HTMLのコメントと異なり、サーバーサイドでコメントとして処理されるため、ブラウザでソースを表示しても @* ... *@ は出力されません。



@{
    ViewBag.Title = "Index";
    Layout = "~/Views/_LayoutPage1.cshtml";
}

@{ ... } の中にC#のコードを書くと、サーバーサイドで実行されます。
ここでは Title の設定とレイアウトの指定をしています。

ViewBagはコントローラとビューの間でデータをやりとりする際に使用します。



<h2>Index</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>

@Htmlから始まるメソッドは HTMLヘルパー と呼ばれ、HTMLの生成をカプセル化します。

@Html.ActionLink() はリンクを生成します。
上記の場合は TodoesControllerCreateアクションへのリンクが生成されます。



<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.summary)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.detail)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.limit)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.done)
        </th>
        <th></th>
    </tr>

@Html.DisplayNameFor() はモデルの定義に応じて、プロパティの表示名を表示します。

<label>タグとして出力したい場合は@Html.LabelFor()を使用すると、表示名をラベルに整形して出力します。



@foreach (var item in Model) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.summary)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.detail)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.limit)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.done)
        </td>
        <td>
            @Html.ActionLink("Edit", "Edit", new { id=item.id }) |
            @Html.ActionLink("Details", "Details", new { id=item.id }) |
            @Html.ActionLink("Delete", "Delete", new { id=item.id })
        </td>
    </tr>
}

</table>

制御構文内は自動的にコードブロックとして認識されます。 foreach だけでなく、ifswitch などC#の制御構文がそのまま使用できます。

@Html.DisplayFor() はモデル定義に応じて出力される内容が変わります。

DisplayForはプロパティの内容を表示するだけなので、チェックボックスはreadonlyとなります。






つづいて、編集画面の例として Edit.cshtml の解説をしていきます。

@model WebApplication1.Models.Todo

@{
    ViewBag.Title = "Edit";
    Layout = "~/Views/_LayoutPage1.cshtml";
}

<h2>Edit</h2>

ここまでは Index.cshtml と同様です。

コントローラでは 1つの項目を取得して Viewにセットされてきますので、 @model では単純に Todoモデルを受け取るように指定しています。



@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

@using (Html.BeginForm()) { ... } はブロック内を <form> で括ります。

@Html.AntiForgeryToken() は CSRF対策の <hidden> タグを埋め込みます。



    <div class="form-horizontal">
        <h4>Todo</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        @Html.HiddenFor(model => model.id)

        <div class="form-group">
            @Html.LabelFor(model => model.summary, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.summary, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.summary, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.detail, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.detail, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.detail, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.limit, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.limit, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.limit, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.done, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                <div class="checkbox">
                    @Html.EditorFor(model => model.done)
                    @Html.ValidationMessageFor(model => model.done, "", new { @class = "text-danger" })
                </div>
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
}

HTMLヘルパーで入力フォームを生成しています。



<div>
    @Html.ActionLink("Back to List", "Index")
</div>

<script src="~/Scripts/jquery-1.10.2.min.js"></script>
<script src="~/Scripts/jquery.validate.min.js"></script>
<script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>

最後に jQueryの読み込みを行っています。
これらのJavaScriptはvalidationで使用されています。






ASP.NET MVC 5Entity Framework 6 を活用して、非常にシンプルなWebアプリケーションを、簡単・簡潔に作成する手順について解説しました。

ほとんどコーディングを行わずに、基本的な CRUD を行うアプリケーションが作成できることに驚かれたと思います。

また、デバッグ実行したブラウザで F12 キーを押し、開発者ツールを起動すると分かりますが 生成されるHTMLは見通しがよく、JavaScriptCSSでの操作が容易です。

生成されるViewはBootstrapjQueryを使用することを想定した作りになっていますので 特別な対応を行わなくても、ある程度見栄えのするアプリケーションが作成できます。