部分ビューでの検証エラーの表示

過去に書いた記事を見ていて、「新規登録と編集の入力画面の生成を部分ビューで行っていることから、サーバー側での検証でエラーを検出しても、入力画面の再表示をさせることができない」と書いていたのを見つけたので、訂正を兼ねて表示する方法を書くことにします。訂正ということで、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 内に共通部分があり、リファクタリング対象になりますが、そこまでやってません :mrgreen:

/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件のフィードバック

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です