過去に書いた記事を見ていて、「新規登録と編集の入力画面の生成を部分ビューで行っていることから、サーバー側での検証でエラーを検出しても、入力画面の再表示をさせることができない」と書いていたのを見つけたので、訂正を兼ねて表示する方法を書くことにします。訂正ということで、MVC 3 を使ったコードを書きます。
過去に書いた記事はダイアログ化する記事でしたが、今回はダイアログ化まではやっていません。ダイアログ化は次回に行います。
プログラムは「連絡先表示」のプログラムです。
追加画面での検証エラー表示(メールアドレスの検証エラー表示)
修正画面での検証エラー表示(関係、メールアドレス、電話番号を修正して、メールアドレスが重複)
部分ビューは追加画面と修正画面の一覧部分、追加入力部分、修正入力部分になります。
それではコードを。
新しいプロジェクトで MVC 3 Web アプリケーションを選び、プロジェクト名は PartialViewDisplayValidationErr として、プロジェクト テンプレートは「空」を選んでいます。Entity Framework を使うので、ソリューション エクスプローラーの「参照設定」に「System.Data.Entity」が無い場合には、ソリューション エクスプローラーのプロジェクト名を右クリックして「NuGet パッケージの管理」から Entity Framework をプロジェクトに導入してください。
まずはモデルから。Models フォルダに Contact クラスを作ります。
using System.ComponentModel.DataAnnotations;
namespace PartialViewDisplayValidationErr.Models
{
public class Contact
{
public int Id { get; set; }
[Required]
[Display(Name = "名前")]
public string Name { get; set; }
[Required]
[DataType(DataType.EmailAddress)]
[Display(Name = "メールアドレス")]
public string Email { get; set; }
[Required]
[DataType(DataType.PhoneNumber)]
[Display(Name = "電話番号")]
public string Phone { get; set; }
[Display(Name="関係")]
public int RelationshipId { get; set; }
public virtual Relationship Relationship { get; set; }
}
}
次に Relationship クラスを作ります。
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace PartialViewDisplayValidationErr.Models
{
public class Relationship
{
public int Id { get; set; }
[Display(Name = "関係")]
public string Name { get; set; }
public virtual ICollection<Contact> Contacts { get; set; }
}
}
Relationship クラスは連絡先として保持する人との関係種別(友人、同僚など)を保持するクラスで、Contact クラスが連絡先を保持するクラスです。したがって 1 対 n の関係になります。
次に ManageModel クラスを作ります。
using System.Linq;
namespace PartialViewDisplayValidationErr.Models
{
public class ManageModel
{
public EditMode Mode { get; set; }
public IQueryable<Contact> Contacts { get; set; }
public Contact Target { get; set; }
}
public enum EditMode
{
Create,
Edit,
}
}
このクラスは、部分ビューの表示で利用します。
次に DbContext です。プロジェクトに DAL フォルダを作り、作成した DAL フォルダに ContactContext クラスを作ります。
using System.Data.Entity;
using PartialViewDisplayValidationErr.Models;
namespace PartialViewDisplayValidationErr.DAL
{
public class ContactContext : DbContext
{
public ContactContext() : base("ContactDb") { }
public DbSet<Relationship> Relationships { get; set; }
public DbSet<Contact> Contacts { get; set; }
}
}
次に DB のイニシャライザです。DAL フォルダに ContactDbInitializer クラスを作ります。
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using PartialViewDisplayValidationErr.Models;
namespace PartialViewDisplayValidationErr.DAL
{
public class ContactDbInitializer : DropCreateDatabaseAlways<ContactContext>
{
protected override void Seed(ContactContext context)
{
new List<Relationship> {
new Relationship { Name = "友人" },
new Relationship { Name = "同僚" },
new Relationship { Name = "顧客" },
}.ForEach(m => context.Relationships.Add(m));
context.SaveChanges();
new List<Contact> {
new Contact {
Name = "テストユーザー1", Email = "test1@example.com", Phone = "1234-11-1234",
Relationship = context.Relationships.Where(w => w.Name == "友人").First()
},
new Contact {
Name = "テストユーザー2", Email = "test2@example.com", Phone = "1234-22-1234",
Relationship = context.Relationships.Where(w => w.Name == "同僚").First()
},
}.ForEach(m => context.Contacts.Add(m));
context.SaveChanges();
}
}
}
動作テスト用なので、起動毎に DB を初期化させています。
次に、プロジェクト トップの Web.Config に接続文字列を設定します。
<?xml version="1.0" encoding="utf-8"?>
~省略~
<connectionStrings>
<add name="ContactDb" connectionString="Data Source=|DataDirectory|\ContactDb.sdf" providerName="System.Data.SqlServerCe.4.0" />
</connectionStrings>
DB は SQL Server Compact 4.0 の DB ファイルをプロジェクトの App_Data に作成するように設定しています。プロジェクトに App_Data フォルダが無い場合には、App_Data フォルダを作っておいてください。
次に DB のイニシャライザの設定です。Global.asax の Application_Start メソッドに設定します。
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using PartialViewDisplayValidationErr.DAL;
namespace PartialViewDisplayValidationErr
{
~省略~
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
Database.SetInitializer(new ContactDbInitializer());
}
}
}
以上で DB 関係の設定は終了です。
次にコントローラーを作成します。
Controllers フォルダに HomeController クラスを作ります。
using System.Data;
using System.Linq;
using System.Web.Mvc;
using PartialViewDisplayValidationErr.Models;
using PartialViewDisplayValidationErr.DAL;
namespace PartialViewDisplayValidationErr.Controllers
{
public class HomeController : Controller
{
private ContactContext db = new ContactContext();
//
// GET: /Home/
public ViewResult Index()
{
return View(new ManageModel { Contacts = db.Contacts.AsQueryable() });
}
//
// GET: /Home/Details/5
public ViewResult Details(int id)
{
Contact contact = db.Contacts.Find(id);
return View(contact);
}
//
// GET: /Home/Manage/5
public ViewResult Manage(int? id)
{
var model = new ManageModel { Mode = EditMode.Create, Contacts = db.Contacts.AsQueryable() };
if (id != null)
{
model.Mode = EditMode.Edit;
model.Target = db.Contacts.Find(id);
}
// ドロップダウンリスト用のデータ
var relationships = db.Relationships;
if (model.Mode == EditMode.Create)
{
ViewData["Target.RelationshipId"] = new SelectList(relationships, "Id", "Name");
}
else
{
ViewData["Target.RelationshipId"] = new SelectList(relationships, "Id", "Name", model.Target.Relationship.Id);
}
return View(model);
}
//
// POST: /Home/Create
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(ManageModel model)
{
if (db.Relationships.Find(model.Target.RelationshipId) == null)
{
ModelState.AddModelError("Target.RelationshipId", "選択された値が不正です。");
}
if (ModelState.IsValid)
{
if (db.Contacts.Where(w => w.Email == model.Target.Email).Count() == 0)
{
db.Contacts.Add(model.Target);
db.SaveChanges();
TempData["StatusMessage"] = "連絡先を追加しました。";
return RedirectToAction("Index");
}
ModelState.AddModelError("Target.Email", "同じメールアドレスが登録されています。");
}
model.Mode = EditMode.Create;
model.Contacts = db.Contacts.AsQueryable();
// ドロップダウンリスト用のデータ
var relationships = db.Relationships;
if (model.Target.Relationship == null)
{
ViewData["Target.RelationshipId"] = new SelectList(relationships, "Id", "Name");
}
else
{
ViewData["Target.RelationshipId"] = new SelectList(relationships, "Id", "Name", model.Target.RelationshipId);
}
return View("Manage", model);
}
//
// POST: /Home/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(ManageModel model)
{
if (db.Relationships.Find(model.Target.RelationshipId) == null)
{
ModelState.AddModelError("Target.RelationshipId", "選択された値が不正です。");
}
if (ModelState.IsValid)
{
if (db.Contacts.Where(w => w.Email == model.Target.Email && w.Id != model.Target.Id).Count() == 0)
{
db.Entry(model.Target).State = EntityState.Modified;
db.SaveChanges();
TempData["StatusMessage"] = "連絡先を変更しました。";
return RedirectToAction("Index");
}
ModelState.AddModelError("Target.Email", "同じメールアドレスが登録されています。");
}
model.Mode = EditMode.Edit;
model.Contacts = db.Contacts.AsQueryable();
// ドロップダウンリスト用のデータ
var relationships = db.Relationships;
if (model.Target.Relationship == null)
{
ViewData["Target.RelationshipId"] = new SelectList(relationships, "Id", "Name");
}
else
{
ViewData["Target.RelationshipId"] = new SelectList(relationships, "Id", "Name", model.Target.RelationshipId);
}
return View("Manage", model);
}
//
// GET: /Home/Delete/5
public ActionResult Delete(int id)
{
Contact contact = db.Contacts.Find(id);
return View(contact);
}
//
// POST: /Home/Delete/5
[HttpPost, ActionName("Delete")]
public ActionResult DeleteConfirmed(int id)
{
Contact contact = db.Contacts.Find(id);
db.Contacts.Remove(contact);
db.SaveChanges();
return RedirectToAction("Index");
}
protected override void Dispose(bool disposing)
{
db.Dispose();
base.Dispose(disposing);
}
}
}
連絡先の追加や修正などの際の GET要求は Manage アクションが受け取り、Manage ビューを返します。
連絡先の追加や修正などの際の POST 要求が行われたときに検証エラーとなった場合、return View("Manage", model); として、Manage ビューへ model を渡して検証エラーメッセージを表示します。このとき、クライアント側では URL 表示が /Home/Manage から /Home/Create などへ変化します。
ViewData["Target.RelationshipId"] には、ドロップダウンリストにセットする SelectList クラスのインスタンスをセットしています。
メールアドレスの重複チェックは DB 側の UNIQUE 制約で行うのが筋ですが、テスト用のプログラムなのでアプリ側で行なっています。
POST 要求を受け取る Edit アクションで model.Target.RelationshipId の存在確認を行なっているのは、クライアント側での細工対策です。
Relationships テーブルの変更操作は実装していません。
次にビューです。
Details と Delete は部分ビューにしていません。
_ContactCreate.cshtml と _ContactEdit.cshtml の Form 内に共通部分があり、リファクタリング対象になりますが、そこまでやってません
/Home/Index.cshtml
@model PartialViewDisplayValidationErr.Models.ManageModel
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
<p class="message-success">@TempData["StatusMessage"]</p>
<p>
@Html.ActionLink("新規作成", "Manage")
</p>
<h2>連絡先一覧</h2>
<table>
<tr>
<th>
名前
</th>
<th>
関係
</th>
<th>
メールアドレス
</th>
<th>
電話番号
</th>
<th></th>
</tr>
@foreach (var item in Model.Contacts) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Relationship.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Email)
</td>
<td>
@Html.DisplayFor(modelItem => item.Phone)
</td>
<td>
@Html.ActionLink("修正", "Manage", new { id=item.Id }) |
@Html.ActionLink("詳細", "Details", new { id=item.Id }) |
@Html.ActionLink("Delete", "Delete", new { id=item.Id })
</td>
</tr>
}
</table>
/Home/_ContactList.cshtml
@model PartialViewDisplayValidationErr.Models.ManageModel
<h2>連絡先一覧</h2>
<table>
<tr>
<th>
名前
</th>
<th>
関係
</th>
<th>
メールアドレス
</th>
<th>
電話番号
</th>
</tr>
@foreach (var item in Model.Contacts) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Relationship.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Email)
</td>
<td>
@Html.DisplayFor(modelItem => item.Phone)
</td>
</tr>
}
</table>
/Home/_ContactCreate.cshtml
@model PartialViewDisplayValidationErr.Models.ManageModel
@using (Html.BeginForm("Create", "Home")) {
@Html.AntiForgeryToken()
<fieldset>
<legend>Contact</legend>
<div class="editor-label">
@Html.LabelFor(model => model.Target.Name)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Target.Name)
@Html.ValidationMessageFor(model => model.Target.Name)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Target.RelationshipId)
</div>
<div class="editor-field">
@Html.DropDownListFor(model => model.Target.RelationshipId, null)
@Html.ValidationMessageFor(model => model.Target.RelationshipId)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Target.Email)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Target.Email)
@Html.ValidationMessageFor(model => model.Target.Email)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Target.Phone)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Target.Phone)
@Html.ValidationMessageFor(model => model.Target.Phone)
</div>
<p>
<input type="submit" value="作成" />
</p>
</fieldset>
}
/Home/_ContactEdit.cshtml
@model PartialViewDisplayValidationErr.Models.ManageModel
@using (Html.BeginForm("Edit", "Home")) {
@Html.AntiForgeryToken()
<fieldset>
<legend>Contact</legend>
@Html.HiddenFor(model => model.Target.Id)
<div class="editor-label">
@Html.LabelFor(model => model.Target.Name)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Target.Name)
@Html.ValidationMessageFor(model => model.Target.Name)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Target.RelationshipId)
</div>
<div class="editor-field">
@Html.DropDownListFor(model => model.Target.RelationshipId, null)
@Html.ValidationMessageFor(model => model.Target.RelationshipId)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Target.Email)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Target.Email)
@Html.ValidationMessageFor(model => model.Target.Email)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Target.Phone)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Target.Phone)
@Html.ValidationMessageFor(model => model.Target.Phone)
</div>
<p>
<input type="submit" value="修正" />
</p>
</fieldset>
}
/Home/Manage.cshtml
@model PartialViewDisplayValidationErr.Models.ManageModel
@if (Model.Mode == PartialViewDisplayValidationErr.Models.EditMode.Create)
{
ViewBag.Title = "新規追加";
<h2>新規追加</h2>
}
else
{
ViewBag.Title = "修正";
<h2>修正</h2>
}
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
@Html.Partial("_ContactList")
@Html.ValidationSummary(true)
@if (Model.Mode == PartialViewDisplayValidationErr.Models.EditMode.Create)
{
@Html.Partial("_ContactCreate")
}
else
{
@Html.Partial("_ContactEdit")
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
/Home/Details.cshtml
@model PartialViewDisplayValidationErr.Models.Contact
@{
ViewBag.Title = "Details";
}
<h2>Details</h2>
<fieldset>
<legend>Contact</legend>
<div class="display-label">名前</div>
<div class="display-field">
@Html.DisplayFor(model => model.Name)
</div>
<div class="display-label">メールアドレス</div>
<div class="display-field">
@Html.DisplayFor(model => model.Email)
</div>
<div class="display-label">電話番号</div>
<div class="display-field">
@Html.DisplayFor(model => model.Phone)
</div>
<div class="display-label">関係</div>
<div class="display-field">
@Html.DisplayFor(model => model.Relationship.Name)
</div>
</fieldset>
<p>
@Html.ActionLink("Edit", "Edit", new { id=Model.Id }) |
@Html.ActionLink("Back to List", "Index")
</p>
/Home/Delete.cshtml
@model PartialViewDisplayValidationErr.Models.Contact
@{
ViewBag.Title = "Delete";
}
<h2>Delete</h2>
<h3>Are you sure you want to delete this?</h3>
<fieldset>
<legend>Contact</legend>
<div class="display-label">名前</div>
<div class="display-field">
@Html.DisplayFor(model => model.Name)
</div>
<div class="display-label">関係</div>
<div class="display-field">
@Html.DisplayFor(model => model.Relationship.Name)
</div>
<div class="display-label">メールアドレス</div>
<div class="display-field">
@Html.DisplayFor(model => model.Email)
</div>
<div class="display-label">電話番号</div>
<div class="display-field">
@Html.DisplayFor(model => model.Phone)
</div>
</fieldset>
@using (Html.BeginForm()) {
<p>
<input type="submit" value="Delete" /> |
@Html.ActionLink("Back to List", "Index")
</p>
}
これで部分ビューでの入力に対する検証エラーの表示が動くようになります 😉
次回は入力部分を jQuery UI のダイアログ化します。
「部分ビューでの検証エラーの表示」への1件のフィードバック