今更ですが、ASP.NET MVCでのCRUDのやり方についてまとめてみました。
前提
知識
とりあえずASP.NET MVCでHelloWorldは出せて、SQL ServerでDBやTableを作成できる程度を想定してます。
ASP.NETのバージョン
個人的にはASP.NET Coreを利用したいところですが、業務ではまだまだASP.NET 4.x系になるので、ここではWindows環境での実行を前提としたASP.NET 4.6(MVC 5)ベースで話を進めます。
各種ツール
VisualStudio(VS)
VisualStudio 2015 Community Update3を使います。
Database
手元の環境がSQL Server 2012だったのでそれ使います。2014,2016やExpressでも問題ありません。
実装方針
- データベースファースト(CodeFirstではなく)で進めます。
- Viewの記述ではHtmlHelperはメリットが感じられる最小限度しか使いません。
- ViewはBootstrapを利用しScaffoldで生成されるマークアップを参考にします。
Viewの表示の仕組みやカスタマイズしたい方はこちらをご覧ください。
下準備
###データベースの作成
開発を開始する前にローカルのSQL ServerにDBとテーブルを作成しておきましょう。
testdbというデータベースにMembersというテーブルを作成し、サンプルデータを流し込んでおきます。
下記テーブル情報を参考にテーブルを作成してください。下記クエリをそのまま実行しても問題ありません。
if object_id('Members') is not null
drop table Members;
create table Members
(
id int not null primary key identity(1,1),
name nvarchar(50),
email nvarchar(50)
);
insert into members(name,email) values('hoge','hoge@hoge.com');
insert into members(name,email) values('foo','foo@foo.com');
実装
プロジェクトの作成
VisualStudioの新規作成からC# -> Web -> Web Applicaton(.NET Framework) -> MVCと選択し、プロジェクトを作成します。
テストとかAPIとかのチェックは外しておいてもらって問題ないです。
クラウドにホストするとかもいらないです。
初期では上記のようなディレクトリ構造です。
Web.config(データベース接続文字列)の設定
標準ではlocaldbを利用するようになっているので、SQL Serverを利用するように接続文字列を変更します。
プロジェクト直下のWeb.configには既にDefaultConnectionが設定されているので、それを変更するか、新規に追加します。
ここではMyContextという接続文字列を追加しています。
<connectionStrings>
<add name="MyContext" connectionString="Data Source=localhost;Initial Catalog=testdb;User ID=sa;Password=P@ssw0rd!" providerName="System.Data.SqlClient" />
</connectionStrings>
なお、この接続名は次のステップのContextクラス名と一致させることで明示的に指定しなくても接続文字列として利用されます。なお、ASP.NET Identityによる認証を利用する場合は、IdentityModels.cs内の接続文字列も合わせて変更して下さい。
Idntityについて詳しいことはこちらをご覧ください。
モデル
ASP.NET MVCに限らずMVCモデルの開発の最初はモデル(クラス)の定義です。
VisualStudioのウィザードを利用してクラスを生成することもできますが、ここでは手書きで書いてます。
ModelクラスやContextクラスはクラス毎にファイルを分けても構いませんが、ここではModelsフォルダ以下にMyModels.csという1つのファイルに書いていきます(Java出身のしとは気持ち悪いかもしれません)。
モデルクラスを作成したら忘れずビルドしておきます(でないと、コード中で既存クラスとして認識されません)。
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Linq;
using System.Web;
namespace crud.Models
{
//member model
public class Member
{
public int id { get; set; }
public string name { get; set; }
public string email { get; set; }
}
//context class
public class MyContext : DbContext
{
public DbSet<Member> Members { get; set; }
}
}
Memberテーブルと対を成すMemberクラスを定義し、MyContextにて、物理テーブル(Members)にマップしています。
型の定義は、こちらなどを参考にしながら行います。
一覧(Index)
では、Membersテーブル中のデータ一覧を表示してみます。
ここでは標準で存在するHomeController.csを改変することにします。
Controller
まず、HomeController.csを下記のように書き換えます。ポイントとしては、
- HomeController全体で利用するContextをdbとして定義。
- Index()を定義し、dbの全検索結果を取得し、Viewに送っています。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using crud.Models;
using System.Data.Entity;
namespace crud.Controllers
{
public class HomeController : Controller
{
//context as db
private MyContext db = new MyContext();
//index
public ActionResult Index()
{
var members = db.Members.ToList();
return View(members);
}
}
}
View
つづいてViewです。View/HomeにあるIndex.chtmlを編集します。テンプレートを使用しているので@RenderBody()による表示部のみ記述しています。
ポイントとしては、冒頭で
@model IEnumerable<crud.Models.Member>
と、View内で利用するModelを指定していることです。これにり、View中でControllerから送られたモデル(members)をModel(もしくはmodel)で参照可能となります。ここでは、複数の値が渡されるためIEnumerableが指定されています。
なお、私はHtmlHelperがあまり好きではありません。その為、その利用は明確なメリットが得られる最小範囲にとどめています。
@model IEnumerable<crud.Models.Member>
@{
ViewBag.Title = "Home Page";
}
<h3>一覧</h3>
<p><a href="/Home/Create">新規作成</a></p>
<table class="table">
<thead>
<tr>
<th>id</th>
<th>name</th>
<th>email</th>
<th>operation</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>@item.id</td>
<td>@item.name</td>
<td>@item.email</td>
<td>
<a href="/Home/Details/@item.id">詳細</a> |
<a href="/Home/Edit/@item.id">編集</a> |
<a href="/Home/Delete/@item.id">削除</a> |
</td>
</tr>
}
</tbody>
</table>
記述を終えたら[F5]を押してアプリケーションを実行します。
こんな風に表示されたら問題ありません。各ボタンのリンク等も正しく出力されているか確認してください。
詳細表示(Details)
つづいて詳細ページを作成します。
Controller
HomeController.csに下記のアクションを追加します(以下、アクション部のみ抜粋)。
idを受け取り、そのidで検索した結果をViewに送ります。
// details
public ActionResult Details(int? id)
{
Member member = db.Members.Find(id);
return View(member);
}
なお、idがnullの場合や検索結果が0(null)の場合のエラー処理などを行うべきですが、サンプルのシンプルさを重視し、ここでは割愛します(詳細はscaffoldにて生成されるコードを参考にしてみてください)。
View
対応するView(View/Details.chtml)を作成します。
@model crud.Models.Member
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h3>詳細</h3>
<p><a href="/Home/Index">一覧へ</a></p>
<table class="table">
<tr>
<th>id</th>
<td>@Model.id</td>
</tr>
<tr>
<th>name</th>
<td>@Model.name</td>
</tr>
<tr>
<th>email</th>
<td>@Model.email</td>
</tr>
</table>
記述を終えたら動作を確認してください。ここではid1をクリックして詳細を見てみます。
なお、起動ページとしてDetailsを指定するとidが空のためエラーが発生します。その場合はIndexを起動し、そこの「詳細」リンクから表示するようにして下さい。
新規作成(Create)
つづいて新規作成です。
Controller
新規作成では、
- [HttpGet]:単に新規作成画面を表示した場合
- [HttpPost]:値を入力しsubmitボタンが押された場合
の2つのアクションを実装します。
//create
public ActionResult Create()
{
//単に入力ページを表示
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Member member)
{
//POSTされた時
//値を受けっとってdbに保存します
if (ModelState.IsValid)
{
db.Members.Add(member);
db.SaveChanges();
//登録に成功したら一覧を表示
return RedirectToAction("Index");
}
//バリデーションに問題があったら元のページに返す
return View(member);
}
なお、[HttpGet]はデフォルトで省略可能です。[ValidateAntiForgeryToken]はCSRF防止用のtokenのチェックを行っています(Viewで送っているから)。
View
Viewは入力画面を表示します。
@model crud.Models.Member
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h3>新規作成</h3>
<form action="/Home/Create" method="post">
<!-- csrf対策用のtokenを送付 -->
@Html.AntiForgeryToken()
<div class="form-horizontal">
<!-- name -->
<div class="form-group">
<label class="control-label col-md-2">name</label>
<div class="col-md-10">
<input type="text" name="name" class="form-control" />
@Html.ValidationMessageFor(model => model.name, "", new { @class = "text-danger" })
</div>
</div>
<!-- email -->
<div class="form-group">
<label class="control-label col-md-2">email</label>
<div class="col-md-10">
<input type="text" name="email" class="form-control" />
@Html.ValidationMessageFor(model => model.email, "", new { @class = "text-danger" })
</div>
</div>
<!-- submit -->
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
</form>
Indexページの新規作成をクリックして動作確認をして下さい。
編集(Edit)
つづいて編集です。編集は詳細表示と新規作成を組み合わせたような感じです。
Controller
編集の対象となるデータを取得・表示するアクションと編集内容を受け取り、データベースを後進する2つのアクションを実装します。
// edit
public ActionResult Edit(int? id)
{
//指定したidの値を取得
Member member = db.Members.Find(id);
return View(member);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(Member member)
{
if (ModelState.IsValid)
{
//更新であることを明示
db.Entry(member).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(member);
}
View
新規作成とよく似ていますが、idをhiddenで送付しているところなどが異なります。
@model crud.Models.Member
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h3>編集</h3>
<form action="/Home/Edit" method="post">
@Html.AntiForgeryToken()
<div class="form-horizontal">
<!-- id -->
<input type="hidden" name="id" value="@Model.id"/>
<!-- name -->
<div class="form-group">
<label class="control-label col-md-2">name</label>
<div class="col-md-10">
<input type="text" name="name" class="form-control" value="@Model.name"/>
@Html.ValidationMessageFor(model => model.name, "", new { @class = "text-danger" })
</div>
</div>
<!-- email -->
<div class="form-group">
<label class="control-label col-md-2">email</label>
<div class="col-md-10">
<input type="text" name="email" class="form-control" value="@Model.email"/>
@Html.ValidationMessageFor(model => model.email, "", new { @class = "text-danger" })
</div>
</div>
<!-- submit -->
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="更新" class="btn btn-default" />
</div>
</div>
</div>
</form>
編集ページも新規作成とほぼ動作は同じです。
削除(Delete)
最後で削除機能の実装です。不用意な削除を避けるため、削除機能はPOSTで実装するのが慣例となっています。
一覧のクリックのみで削除してもいいのですが、scaffoldの仕様に合わせて削除ページを踏む仕様です。
Controller
// delete
public ActionResult Delete(int? id)
{
//削除対象を検索
Member member = db.Members.Find(id);
return View(member);
}
//同じ名前、同じ引数が定義できない(実際は?がついているからできるけど)
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id)
{
//削除対象を取得・削除・保存
Member member = db.Members.Find(id);
db.Members.Remove(member);
db.SaveChanges();
return RedirectToAction("Index");
}
なお、[HttpGet]、[HttpPost]とアノテーションが違っても同名、同引数のアクションを作成することができないため、Deleteは別名機能を利用しています。
View
削除対象を表示・確認し、submitボタンを押すことで削除します。
@model crud.Models.Member
@{
ViewBag.Title = "Delete";
}
<h3>削除しますか?</h3>
<dl class="dl-horizontal">
<dt>id</dt>
<dd>@Model.id</dd>
<dt>name</dt>
<dd>@Model.name</dd>
<dt>email</dt>
<dd>@Model.email</dd>
</dl>
<form action="/Home/Delete" method="post">
@Html.AntiForgeryToken()
<div class="form-actions no-color">
<input type="hidden" name="id" value="@Model.id" />
<input type="submit" value="Delete" class="btn btn-default" />
</div>
</form>
先程追加したデータを消してみましょう。
基本的なCRUD機能の実装は以上です。
補足
バリデーション
ここでは、Controller内でif (ModelState.IsValid)と書いてますが、実際にはバリデーションは利用していません。利用した場合はModelにアノテーションを記述します。例えば、
public class Member
{
public int id { get; set; }
[Required(ErrorMessage ="名前は必須です。")]
public string name { get; set; }
public string email { get; set; }
}
という感じです。アノテーションについてはこちらの記事が参考になります。
場所によりバリデーションルールを変えたい
同じモデルでも新規作成時と更新時ではバリデーションルールを変えたいということもあります。
その場合は専用の別のモデル(ViewModel)を作成して対応するのが王道のようです。
例えば、
//新規用
public class Member
{
public int id { get; set; }
[Required(ErrorMessage ="名前は必須です。")]
public string name { get; set; }
public string email { get; set; }
}
//更新用
public class UpdateMember
{
public int id { get; set; }
[Required(ErrorMessage = "名前は必須です。")]
public string name { get; set; }
[Required(ErrorMessage = "E-Mailは必須です。")]
public string email { get; set; }
}
として、Controllerにて、専用のモデルを利用します。
public ActionResult Update(UpdateMember umb)
{
if(ModelState.IsValid)
{
//何かする
}
return View();
}
保存の際はUpdateMemberをMemberにマップし直す必要がありますが。
##参考文献
下記内容が大変参考になります。