部分ビューでの検証エラーの表示と jQuery UI のダイアログ表示(1)

部分ビューでの検証エラーの表示 の続きです。前回部分ビュー上での入力内容の検証エラー表示ができているので、今回は新規作成画面、詳細表示画面、修正画面、削除確認画面を jQuery UI の機能を使ってダイアログ表示してみます。使用するフレームワークは ASP.NET MVC 4 ですが、MVC 3 でも基本的に同じです(一部追加されたメソッドを使っていますが 😉 )。2011年4月に「ASP.NET MVC と jQuery で新規登録ダイアログと編集ダイアログ」で書いた内容のアップデートです 🙂

ダイアログ表示の基本的な方針として、ダイアログ表示を行う画面は jQuery Ajax API の Load メソッドで取得することとします。

画面は次のとおりです。
最初の一覧画面
最初の一覧画面
追加ダイアログの表示
追加ダイアログの表示
追加ダイアログでの検証エラーの表示
追加ダイアログでの検証エラーの表示
追加完了画面
追加完了画面
詳細ダイアログの表示
Ajax で詳細画面を取得して、ページ内容の動的変更を行なっているので、追加完了メッセージが残っています。消したい場合には Ajax で動的変更を行うのと同時に jQuery で DOM ツリーを操作することで消すことができます(以下同様)。
詳細ダイアログの表示
修正ダイアログの表示
修正ダイアログの表示
修正ダイアログでの検証エラーの表示
修正ダイアログでの検証エラーの表示
修正完了画面
修正完了画面
削除確認ダイアログの表示
削除確認ダイアログの表示
削除完了画面
削除完了画面

それではプログラムです。内容的に前回の続きになりますが、フレームワークを MVC 3 から MVC 4 に変更しているので、プロジェクトの作成から書いていきます。
まずは、新しいプロジェクトで ASP.NET MVC 4 Web アプリケーションを作成します(プロジェクト名は「TestJqueryUi005」としました)。 プロジェクト テンプレートは「基本」を選択します。
まずはモデルから。Models フォルダに Contact クラスを作ります。

using System.ComponentModel.DataAnnotations;

namespace TestJqueryUi005.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; }

        [Required]
        [Display(Name = "関係")]
        public int RelationshipId { get; set; }

        public virtual Relationship Relationship { get; set; }
    }
}

次に Relationship クラスを作ります。

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace TestJqueryUi005.Models
{
    public class Relationship
    {
        public int Id { get; set; }

        [Display(Name = "関係")]
        public string Name { get; set; }

        public virtual ICollection<Contact> Contacts { get; set; }
    }
}

次にビューの表示に利用する DataContainer を作ります。

using System.Linq;

namespace TestJqueryUi005.Models
{
    public class DataContainer
    {
        public IQueryable<Contact> Contacts { get; set; }
        
        public Contact TargetContact { get; set; }

        /// <summary>
        /// ビュー側で jQuery UI のダイアログ表示の振り分けに使用します
        /// </summary>
        public string Action { get; set; }
    }
}

次に DbContext です。プロジェクトに DAL フォルダを作り、作成した DAL フォルダに ContactContext クラスを作ります。

using System.Data.Entity;

using TestJqueryUi005.Models;

namespace TestJqueryUi005.DAL
{
    public class ContactsContext : DbContext
    {
        public ContactsContext() : base("ContactsDb") { }

        public DbSet<Contact> Contacts { get; set; }
        public DbSet<Relationship> Relationships { get; set; }
    }
}

次に DB のイニシャライザです。DAL フォルダに ContactDbInitializer クラスを作ります。

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

using TestJqueryUi005.Models;

namespace TestJqueryUi005.DAL
{
    public class ContactDbInitializer : DropCreateDatabaseAlways<ContactsContext>
    {
        protected override void Seed(ContactsContext 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="ContactsDb" connectionString="Data Source=|DataDirectory|\ContactsDb.sdf" providerName="System.Data.SqlServerCe.4.0" />
  </connectionStrings>
~省略~

DB は SQL Server Compact 4.0 の DB ファイルをプロジェクトの App_Data に作成するように設定しています。

次に DB のイニシャライザの設定です。Global.asax の Application_Start メソッドに設定します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Data.Entity;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;

using TestJqueryUi005.DAL;

namespace TestJqueryUi005
{
~省略~
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            Database.SetInitializer(new ContactDbInitializer());
        }
    }
}

以上で DB 関係の設定は終了です。

次にリポジトリです。
DAL フォルダに IContactsRepository インターフェイスを作ります。

using System;
using System.Linq;

using TestJqueryUi005.Models;

namespace TestJqueryUi005.DAL
{
    public interface IContactsRepository : IDisposable
    {
        IQueryable<Contact> FindContacts();
        Contact GetContact(int id);
        Contact AddContact(Contact contact);
        void UpdateContact(Contact contact);
        void DeleteContact(int id);

        IQueryable<Relationship> FindRelationships();
        Relationship GetRelationship(int id);

        void Save();
    }
}

次に ContactsRepository クラスを作ります。

using System;
using System.Linq;

using TestJqueryUi005.Models;

namespace TestJqueryUi005.DAL
{
    public class ContactsRepository : IContactsRepository
    {
        private ContactsContext _context;

        public ContactsRepository()
        {
            _context = new ContactsContext();
        }

        #region Contacts

        public IQueryable<Models.Contact> FindContacts()
        {
            return _context.Contacts.AsQueryable();
        }

        public Models.Contact GetContact(int id)
        {
            return _context.Contacts.Find(id);
        }

        public Models.Contact AddContact(Models.Contact contact)
        {
            return _context.Contacts.Add(contact);
        }

        public void UpdateContact(Models.Contact contact)
        {
            _context.Entry(contact).State = System.Data.EntityState.Modified;
        }

        public void DeleteContact(int id)
        {
            var contact = _context.Contacts.Find(id);
            if (contact == null)
            {
                return;
            }
            _context.Contacts.Remove(contact);
        }

        #endregion Contacts

        #region Relationships

        public IQueryable<Relationship> FindRelationships()
        {
            return _context.Relationships.AsQueryable();
        }

        public Relationship GetRelationship(int id)
        {
            return _context.Relationships.Find(id);
        }

        #endregion Relationships


        public void Save()
        {
            _context.SaveChanges();
        }

        #region IDisposable

        private bool _disposed = false;

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (_disposed) return;

            _disposed = true;

            if (disposing)
            {
                // マネージ リソースの解放処理
            }
            // アンマネージ リソースの解放処理
            _context.Dispose();
        }

        #endregion IDisposable
    }
}

次にヘルパーです。プロジェクトに Helpers フォルダを作り、Helpers フォルダに PropertyHelper クラスを作ります。

using System;
using System.Linq.Expressions;

namespace TestJqueryUi005.Helpers
{
    public class PropertyHelper
    {
        public static string GetPropertyName<T>(Expression<Func<T>> e)
        {
            return ((System.Linq.Expressions.MemberExpression)e.Body).Member.Name;
        }
    }
}

次にサービス層です。プロジェクトに Services フォルダを作ります。
最初にサービス層からコントローラーへ検証エラーを通知する ValidateException 例外です。Services フォルダに ValidateException クラスを作ります。

using System;

namespace TestJqueryUi005.Services
{
    enum ValidationError
    {
        DuplicateName = 1,
        NotFound,
    }

    [Serializable]
    class ValidateException : Exception
    {
        private string _targetName;

        public ValidateException() : base() { }
        /// <summary>
        /// 検証エラーとなったプロパティ名とエラーコードを例外で通知します。
        /// </summary>
        /// <param name="targetName"></param>
        /// <param name="errorCode"></param>
        public ValidateException(string targetName, ValidationError errorCode)
            : base()
        {
            _targetName = targetName;
            base.HResult = (int)errorCode;
        }
        public ValidateException(string message) : base(message) { }
        public ValidateException(string message, Exception inner) : base(message, inner) { }
        protected ValidateException(System.Runtime.Serialization.SerializationInfo info,
            System.Runtime.Serialization.StreamingContext context) { }

        /// <summary>
        /// 検証エラーのコードを取得します。
        /// </summary>
        public ValidationError ErrorCode
        {
            get { return (ValidationError)base.HResult; }
        }

        /// <summary>
        /// 検証エラーのメッセージを取得します。
        /// </summary>
        public override string Message
        {
            get
            {
                if (base.HResult != 0)
                {
                    return getMessage((ValidationError)base.HResult);
                }
                return base.Message;
            }
        }

        /// <summary>
        /// 検証エラーが発生したプロパティ名を取得します。
        /// </summary>
        public string TargetName
        {
            get { return _targetName; }
        }

        private string getMessage(ValidationError errorCode)
        {
            switch (errorCode)
            {
                case ValidationError.DuplicateName:
                    return "同じ名前が既に登録されています!";
                case ValidationError.NotFound:
                    return "対象データが見つかりません。要求は実行出来ませんでした!";
                default:
                    return "不明なエラーが発生しました!";
            }
        }
    }
}

次に ITestService インターフェイスを作ります(名前が単体テストと紛らわしいというツッコミはなしで 😉 )。

using System;
using System.Linq;

using TestJqueryUi005.Models;

namespace TestJqueryUi005.Services
{
    public interface ITestService : IDisposable
    {
        IQueryable<Contact> GetListContacts();
        Contact GetContact(int id);
        void CreateContact(Contact contact);
        void EditContact(Contact contact);
        void DeleteContact(int id);

        IQueryable<Relationship> GetListRelations();
        Relationship GetRelation(int id);
    }
}

次に TestService クラスを作ります。

using System;
using System.Data;
using System.Data.Entity.Infrastructure;
using System.Linq;

using TestJqueryUi005.DAL;
using TestJqueryUi005.Helpers;
using TestJqueryUi005.Models;

namespace TestJqueryUi005.Services
{
    public class TestService : ITestService
    {
        private IContactsRepository _repository;

        public TestService() : this(new ContactsRepository()) { }
        public TestService(IContactsRepository repository)
        {
            _repository = repository;
        }

        #region Contacts

        public IQueryable<Contact> GetListContacts()
        {
            return _repository.FindContacts();
        }

        public Models.Contact GetContact(int id)
        {
            return _repository.GetContact(id);
        }

        public void CreateContact(Contact contact)
        {
            // 関係の存在の確認(クライアント側で細工された場合に発生)
            checkRelationExistence(contact.RelationshipId,
                PropertyHelper.GetPropertyName(() => contact.RelationshipId));

            // 名前の重複の確認
            if (_repository.FindContacts().Where(w => w.Name == contact.Name).Count() != 0)
            {
                throw new ValidateException(PropertyHelper.GetPropertyName(() => contact.Name),
                    ValidationError.DuplicateName);
            }
            _repository.AddContact(contact);
            _repository.Save();
        }

        public void EditContact(Contact contact)
        {
            // 関係の存在の確認(クライアント側で細工された場合に発生)
            checkRelationExistence(contact.RelationshipId,
                PropertyHelper.GetPropertyName(() => contact.RelationshipId));

            // 名前の重複の確認
            if (_repository.FindContacts().Where(w => w.Name == contact.Name && w.Id != contact.Id).Count() != 0)
            {
                throw new ValidateException(PropertyHelper.GetPropertyName(() => contact.Name),
                    ValidationError.DuplicateName);
            }

            try
            {
                _repository.UpdateContact(contact);
                _repository.Save();
            }
            catch (DbUpdateConcurrencyException e)
            {
                if (e.InnerException.GetType() == typeof(OptimisticConcurrencyException))
                {   // 同時実行違反例外(この場合はエンティティが削除されている)
                    throw new ValidateException("", ValidationError.NotFound);
                }

                throw;
            }
        }

        public void DeleteContact(int id)
        {
            _repository.DeleteContact(id);
            _repository.Save();
        }

        #endregion Contacts

        #region Relations

        public IQueryable<Relationship> GetListRelations()
        {
            return _repository.FindRelationships();
        }

        public Relationship GetRelation(int id)
        {
            return _repository.GetRelationship(id);
        }

        #endregion Relations

        #region Validate

        private void checkRelationExistence(int id, string propertyName)
        {
            if (GetRelation(id) == null)
            {
                throw new ValidateException(propertyName, ValidationError.NotFound);
            }
        }

        #endregion Validate

        #region IDisposable

        private bool _disposed = false;

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (_disposed) return;

            _disposed = true;

            if (disposing)
            {
                // マネージ リソースの解放処理
            }
            // アンマネージ リソースの解放処理
            _repository.Dispose();
        }

        #endregion
    }
}

あとはコントローラーとビューですが、長くなったので次回に続きます。


部分ビューでの検証エラーの表示と jQuery UI のダイアログ表示(1)」への1件のフィードバック

コメントを残す

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