アドレス帳アプリケーションの作成
前回、とりあえず ASP.NET Core を Linux上で動かせたので、 ASP.NET Core MVC と EntityFramework Core を使って簡単な Web+DBアプリケーションを作成してみます。 あまり Linux 固有の話はありません。
アドレス帳プロジェクトを作る
generator-aspnet
には、 ASP.NET Core Mvc 設定済のプロジェクトを生成するジェネレータも用意されていますが、学習のため Empty Web Application から初めていきます。
$ yo aspnet
? What type of application do you want to create? Empty Web Application
? What's the name of your ASP.NET application? Addressbook
$ cd Addressbook
$ dotnet restore
プロジェクトを生成したら、そのディレクトリに移動して dotnet restore
まで実行しておきます。
MVCを使う準備
ASP.NET Core MVC を使うには、まず project.json
にライブラリを追加します。
dependencies: {
...
"Microsoft.AspNetCore.Mvc": "1.0.0"
}
追加したら、 dotnet restore
でライブラリをインストールします。
$ dotnet restore
VS Code を利用している場合は、 dotnet: Restore Packages
を実行すればよいでしょう。
WebアプリケーションでMVCを有効にする
ライブラリがインストールできたら、アプリケーションの Startup
クラス内で ASP.NET Core MVC を有効にします。
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
ConfigureServices
メソッド内で、 services
の AddMvc
メソッドを実行します。
AddMvc
メソッドは IserviceCollection
への拡張メソッドで、 Microsoft.AspNetCore.Mvc
をインストールすると使えるようになります。
さらにルーティングの設定も行います。 今回はとりあえずデフォルトのルーティングを利用します。
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvcWithDefaultRoute();
}
Emptty Web Application
では Run
メソッドになっていた部分を UseMvcWithDefaultRoute
の呼び出しに変更します。
これは {controller}/{action}/{id?}
というルーティングを設定するメソッドです。
Controllerを追加する
ルーティングができたのでその先で実際に処理を行うコントローラーを追加します。
コントローラークラスの追加も generator-aspnet
のサブジェネレータが用意されてるので、それを利用することにします。
$ mkdir Controllers
$ mkdir Views
$ yo aspnet:MvcController HomeController
$ yo aspnet:MvcView Home/Index.cs
コントローラーを追加すると、空の Index
メソッドだけ用意されてるので、対応するビューのファイルも生成しています。
ビューの中身はなにもないので、単にエラーにならないだけの真っ白なページを表示できます。
Modelを定義する
アドレス帳の中身を作成していきます。
アドレス帳にのせる人物を Person
クラスで実装しましょう。
Models
ディレクトリ以下に aspnet:Class
ジェネレータを使って単純なクラスを生成します。
$ mkdir Models
$ cd Models
$ yo aspnet:Class Person
Person
クラスにいくつかのプロパティを実装します。
namespace Addressbook.Models
{
public class Person
{
public int Id
{
get;
set;
}
public string FirstName
{
get;
set;
}
public string LastName
{
get;
set;
}
public string FullName
{
get {return FirstName + " " + LastName;}
}
public string Email
{
get;
set;
}
}
}
FullName
プロパティは FirstName
, LastName
から算出するプロパティです。
またこのあとデータベースに保存するので、 Id
プロパティも実装しておきます。
Modelにアトリビュートをつける
モデルクラスにアトリビュートをつけてプロパティの扱いを指定します。
FullName
プロパティは算出されるものなので、データベースに保存しないように NotMappedAttribute
を指定します。
Email
プロパティは EmailAddressAttribute
をつけて単なる文字列ではなくメールアドレスを持つプロパティであることを明示しておきます。
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
...
[Required]
public string FirstName
{
get;
set;
}
[Required]
public string LastName
{
get;
set;
}
[NotMapped]
public string FullName
{
get {return FirstName + " " + LastName;}
}
[Required,EmailAddress]
public string Email
{
get;
set;
}
...
これらのアトリビュートは、 System.ComponentModel.Annotations
ライブラリの System.ComponentModel.DataAnnotations
名前空間や System.ComponentModel.DataAnnotations.Schema
名前空間に含まれています。
View
モデルオブジェクトを作成してビューに表示してみます。 とりあえずデータベースのことは忘れて、アクションメソッドの中でインスタンスを生成してビューに渡すようにします。
HomeController
の Index
メソッドの中で Person
の配列を作成してみます。
public IActionResult Index()
{
var people = Enumerable.Range(0, 10)
.Select(i => new Models.Person() {
FirstName = string.Format("Person{0}", i),
LastName = "Last",
Email = string.Format("person{0}@example.com", i)
})
.ToArray();
return View(people);
}
Linq を使って10個の Person
要素を持つ配列 people
を作成しています。
この people
をそのまま View
メソッドの引数に渡すと、ビューのテンプレート内で Model
として参照できます。
配列を渡しているので Home/Index.cshtml
の中ではループして、それぞれの要素を表示します。
<table>
<tr>
<th>FullName</th>
<th>Email</th>
</tr>
@foreach (var person in Model)
{
<tr>
<td>@person.FullName</td>
<td>@person.Email</td>
</tr>
}
+</table>
@foreach
を使って配列をループ処理します。
また、変数の参照は @
の後に変数名を書くとその値を評価した結果が表示されます。
モデルとフォーム
新規作成や編集フォームのビューも作ってみましょう。
それぞれ、 New
, Edit
アクションとして実装してみます。
HomeController
に New
メソッドを追加します。
public IActionResult New()
{
return View();
}
対応するビューを Home/New.cshtml
に作成します。
$ yo aspnet:MvcView Home/New.cshtml
New.cshtml
にフォームを作成します。
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@model Addressbook.Models.Person
@{
// ViewBag.Title = "New Page";
}
<form asp-action="create" method="post">
<div>
<label asp-for="FirstName"></label>
<input asp-for="FirstName"/>
</div>
<div>
<label asp-for="LastName"></label>
<input asp-for="LastName"/>
</div>
<div>
<label asp-for="Email"></label>
<input asp-for="Email"/>
</div>
</form>
フォームのフィールドで asp-for
という属性を指定しています。
これはタグヘルパーという機能です。
使いたいタグヘルパーを @AddTagHelper
で指定します。
また、モデルのクラスを @model
で指定します。
フォームの中の input
や label
には asp-for
タグヘルパーでプロパティを指定します。
同様に編集用のフォームを Edit
アクションに作成します。
public IActionResult Edit(int id)
{
var person = new Models.Person() {
FirstName = "Edit First Name",
LastName = "Edit Last Name",
Id = id,
Email = "edit@example.com",
};
return View(person);
}
Edit
メソッドでは、編集対象のオブジェクトをビューにモデルとして渡すようにしています。
ここでは、仮の内容で Person
クラスのインスタンスを作成しています。
TagHelperを使ってリンクを作る
それぞれのフォームへのリンクを追加します。
<a asp-action="new">New</a>
<table>
<tr>
<th>FullName</th>
<th>Email</th>
</tr>
@foreach (var person in Model)
{
<tr>
<td>
<a asp-action="edit" asp-route-id="@person.Id">@person.FullName</a>
</td>
<td>@person.Email</td>
</tr>
}
</table>
フォームへのリンクも TagHelpers
を利用します。
asp-action
でアクション名を指定し、 asp-route-id
でルートURL内の id
パラメータに値を渡します。
現在のコントローラーと別のコントローラーへのリンクを作成する場合は asp-controller
でコントローラー名を指定しますが、今回は同じ HomeController
のアクションを利用するため指定しません。
デフォルトのルート設定は {controller}/{action}/{id?}
となっているため、 id
が 1
の場合では home/edit/1
というURLへのリンクが生成されます。
EntityFramework Core とツールをインストールする
モデルを永続化するために EntityFramework Core
を利用します。
EntityFramework Core
を依存ライブラリに追加します。
dependencies: {
...
"Microsoft.EntityFrameworkCore": "1.0.0",
"Microsoft.EntityFrameworkCore.Sqlite": "1.0.0"
}
今回はデータベースに Sqlite を使うことにします。
また、スキーママイグレーションをするために EntityFramework Core Tools
をインストールします。
EntityFramework Core Tools
は project.json
の tools
セクションに追加します。
"tools": {
...
"Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final"
},
EntityFramework Core Tools
に必要な EntityFramework Core Design
を依存ライブラリに追加します。
dependencies: {
...
"Microsoft.EntityFrameworkCore.Design": "1.0.0-preview2-final"
}
project.json
にライブラリを追加したら、 dotnet restore
でライブラリをインストールします。
EntityFramework Core でモデルを永続化する
DbContext
を継承して ApplicationDbContext
を作成します。
using Microsoft.EntityFrameworkCore;
namespace Addressbook.Data
{
public class ApplicationDbContext: DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
:base(options)
{
}
}
}
ApplicationDbContext
のコンストラクタは DbContextOptions
を受け取るようにします。
この値はスーパークラスの DbContext
のコンストラクタにそのまま渡すようにします。
ApplicationDbContext
クラスに、永続化したいクラスの DbSet
をプロパティで定義します。
public DbSet<Models.Person> People
{
get;
set;
}
データベース接続設定
appsettings.json
を作成して、データベース接続を設定します。
{
"ConnectionStrings": {
"DefaultConnection": "DataSource = addressbook.sqlite"
}
}
Startup
クラスのコンストラクタで、 appsettings.json
などから設定を読み込む処理を追加します。
using Microsoft.Extensions.Configuration;
using System.IO;
...
public class Startup
{
IConfigurationRoot Configuration
{
get;
set;
}
public Startup()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables();
Configuration = builder.Build();
}
...
ApplicationDbContext
を ConfigureServices
中で services
に追加します。
services.AddDbContext<Data.ApplicationDbContext>(options => {
options.UseSqlite(Configuration.GetConnectionString("DefaultConnection"));
});
}
ここで設定した options
が ApplicationDbContext
クラスのコンストラクタに渡されます。
Migrationする
ApplicationDbContext
に Person
用の DbSet
プロパティを追加したので、 EntityFramework Core Tools
を使ってテーブルを作成します。
EntityFramework Core Tools
の機能は dotnet
コマンドの ef
サブコマンドから利用できます。
試しに、 ApplicationDbContext
がアプリケーション内のDbContextとして認識されているのか確認してみましょう。
$ dotnet ef dbcontext list
Project Addressbook (.NETCoreApp,Version=v1.0) was previously compiled. Skipping compilation.
Addressbook.Data.ApplicationDbContext
それではデータベースにスキーママイグレーションを実行してテーブルを作成してみます。
$ dotnet ef migrations add FirstModel
$ dotnet ef database update
実行すると、 bin/Debug/netcoreapp1.0
に addressbook.sqlite
ファイルが作成されます。
sqlite3コマンドでデータベースファイルの中身を確認してみましょう。
$ sqlite3 bin/Debug/netcoreapp1.0/addressbook.sqlite
sqlite> .schema
CREATE TABLE "__EFMigrationsHistory" (
"MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY,
"ProductVersion" TEXT NOT NULL
);
CREATE TABLE "People" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_People" PRIMARY KEY AUTOINCREMENT,
"Email" TEXT NOT NULL,
"FirstName" TEXT NOT NULL,
"LastName" TEXT NOT NULL
);
Person
クラスのテーブルとして People
テーブルが作成されています。
また、マイグレーションのバージョンを管理するためのテーブル __EFMigrationsHistory
テーブルも作成されます。
ControllerにDbContextをインジェクションする
ApplicationDbContext
をコントローラーから扱うには、DependencyInjectionに登録されたオブジェクトをコンストラクタで受け取るようにします。
単にコントローラークラスのメソッド内で ApplicationDbContext
クラスのインスタンスを作成してもデータベース接続が設定されていない状態のものになってしまいます。
フォーム処理
コントローラーでフォームの値を受け取って処理するようにしていきます。
New
アクションのフォームから値を受け取り、アドレス帳に新規追加する Create
アクションを HomeController
のメソッドとして実装してみます。
フォームの内容はメソッドの引数として受け取ります。
引数に Bind
アトリビュートを追加してどの値を受け取るのか指定します。
例えば、 New
アクションから受け取るフォームの内容は以下のようにして Person
クラスのインスタンスに割り当てます。
[HttpPost]
public IActionResult Create([Bind("FirstName,LastName,Email")]Models.Person person)
{
...
}
POST
メソッド以外でフォームの値を受け取らないように HttpPost
アトリビュートをメソッド自体に設定しています。
引数の person
は Person
クラスのインスタンスです。
Bind
アトリビュートを使って FirstName
, LastName
, Email
の値を person
の各プロパティに割り当てています。
また、 Person
クラスのプロパティにはそれぞれ Required
アトリビュートや EmailAddress
アトリビュートを追加しています。
これらのプロパティにしたがって、フォームの入力値が正しい形式になっているのか確認するには ModelState
の IsValid
プロパティを利用します。
[HttpPost]
public IActionResult Create([Bind("FirstName,LastName,Email")]Models.Person person)
{
if (!ModelState.IsValid) {
return View("New", person);
}
}
IsValid
が false
となる場合は、再びフォームを表示するため New
ビューを呼び出して処理を中断するようにします。
モデルを永続化する処理
受け取った person
オブジェクトをデータベースに保存するには DbContext
の Add
メソッドで追加した後に SaveChanges
メソッドでデータベースに反映します。
[HttpPost]
public IActionResult Create([Bind("FirstName,LastName,Email")]Models.Person person)
{
if (!ModelState.IsValid) {
return View("New", person);
}
DbContext.Add(person);
DbContext.SaveChanges();
return RedirectToAction("Index");
}
IsValid
の確認後に DbContext.Add
と DbContext.SaveChanges
を呼び出します。
保存処理が終わったら、 RedirectToAction
メソッドで Index
アクションにリダイレクトして一覧を表示させます。
残りのCRUDアクションを実装する
新規追加のアクションができあがったので残りのアクションも EntityFramework Core
を使った処理で実装していきます。
一覧表示をする Index
は DbContext
の People
プロパティを使って、保存されている Person
オブジェクトの配列を取得するように変更します。
using System.Linq;
...
[HttpGet]
public IActionResult Index()
{
var people = DbContext.People.ToArray();
return View(people);
}
Person
は IEnumerable
を実装しているため、 System.Linq
名前空間を追加しておくと ToArray
メソッドで配列を取得できるようになります。
Update
アクションは Create
と同様に Person
クラスのオブジェクトでフォームの値を受け取るようにします。
Create
アクションとは異なり、 URLパターンの中に更新対象を示す id
パラメータがあります。
この値も person
のプロパティとして受け取るために Bind
アトリビュートで Id
を追加します。
[HttpPost]
public IActionResult Update(int id, [Bind("Id,FirstName,LastName,Email")]Models.Person data)
{
if (!ModelState.IsValid)
{
return View("Edit", data);
}
var person = DbContext.People.FirstOrDefault(p => p.Id == id);
if (person == null)
{
return NotFound();
}
person.FirstName = data.FirstName;
person.LastName = data.LastName;
person.Email = data.Email;
DbContext.SaveChanges();
return RedirectToAction("Index");
}
Update
内では更新対象を FirstOrDefault
メソッドで取得します。
存在しない場合は NotFound
メソッドでステータスコードが 404
のレスポンスを返して処理を終了します。
取得したオブジェクトのプロパティに、フォームの値を代入したら、 DbContext
の SaveChanges
メソッドでデータベースに反映します。
DbContext
から取得したオブジェクトは DbContext
が状態を追跡しているため、 Add
メソッドを呼び出す必要はありません。
処理が完了したら Create
アクションと同様に Index
アクションにリダイレクトして一覧を表示します。
最後に Delete
アクションです。
[HttpPost]
public IActionResult Delete(int id)
{
var person = DbContext.People.FirstOrDefault(p => p.Id == id);
if (person == null)
{
return NotFound();
}
DbContext.Remove(person);
DbContext.SaveChanges();
return RedirectToAction("Index");
}
モデルをデータベースから削除するには DbContext
の Remove
メソッドを利用します。
まずは id
で対象のモデルを取得します。
取得したモデルを引数に Remove
メソッドを呼び出します。
SaveChanges
メソッドを呼ぶと、実際にデータベースから削除されます。
処理が完了したら Create
アクションと同様に Index
アクションにリダイレクトして一覧を表示します。
これで、 HomeController
に Create, Retivie, Update Delete の CRUDに対応したアクションがすべてそろいました。
Viewをまとめる
これまで View
には動作に必要な最低限の内容だけを書いてあります。
また、TagHelperの利用など、アプリケーション内で共通な設定がそれぞれのViewに記述されています。
すべてのViewで共通するレイアウトを作成してみましょう。
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css">
</head>
<body>
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a href="/" class="navbar-brand">Addressbook</a>
</div>
</div>
</nav>
<div class="container-fluid">
@RenderBody()
</div>
</body>
</html>
twitter-bootstrap
のスタイルを適用するレイアウトです。
レイアウトの中では RenderBody()
メソッドを呼び出して、実際のViewの内容を表示します。
レイアウトは _Layout.cshtml
ファイルに作成します。
コントローラーやアクション専用のViewは Views/[コントローラー]/[アクション].cshtml
に配置されています。
_Layout.cshtml
は Views/Shared
以下の汎用のディレクトリに配置します。
実際にレイアウトを適用するには、 それぞれの View
の中で Layout
変数に利用するレイアウトのビュー名を設定します。
@{
Layout = "_Layout"
}
この処理をすべてのViewの中で書かずとも _ViewStart.cshtml
に書いておくと、自動ですべてのViewで実行されるようになります。
上記のレイアウト指定の内容で Views/_ViewStart.cshtml
を作成しましょう。
また、タグヘルパーなど複数の View
で利用するライブラリがあります。
これらの import
は _ViewStart.cshtml
に書いても効果はありません。
_ViewImports.cshtml
に共通に利用するライブラリなどの設定をまとめましょう。
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
_ViewImports.chtml
を作成したら、それぞれの View
に書いていた addTagHelper
の設定を削除しておきましょう。
まとめ
- ASP.NET Core MVC には ViewとControllerの仕組みが用意されている
- Model 部分には EntityFramework Core を利用する
- フォームの内容は
FromForm
アトリビュートで指定したモデルオブジェクトのプロパティで受け取れる - フォームバリデーションは
ModelState
のIsValid
とTagHelpers
を組み合わせて利用する - モデルに
System.ComponentModel.DataAnnotations
などで制約などを設定できる