إضافة التحقق Validation إلى MVC Model وتطبيق مبدأ DRY

في هذا الدرس سنقوم بإضافة التحقق إلى  book model وهذا سيضمن لنا أن قواعد التحقق مطبقة في أي وقت يحاول فيه المستخدم إضافة أو تعديل كتاب باستخدام التطبيق.

أحد المبادئ الأساسية في ASP.NET MVC هو مبدأ  DRY وهي اختصار للكلمات Don’t Repeat Yourself.

هذه المقالة جزء من سلسلة لتعلم أساسيات ASP.NET MVC للمبتدئين:

إن ASP.NET MVC  تنصح بتعريف تابع لمرة واحدة ومن ثم استخدامه في أي مكان نحتاجة في التطبيق وهذا يؤدي إلى تقليل الكود الذي نكتبه ويجعل الأخطاء في الكود أقل و أسهل للتعديل.

إن دعم التحقق الذي تقدمه ASP.NET MVC و  Entity Framework Code First هو أفضل مثال عن مبدأ DRY

حيث يمكن تعريف قواعد التحقق في مكان واحد (في  model class)  ويتم تطبيق هذه القواعد في كامل التطبيق.

إضافة قواعد التحقق إلى Book Model

سنبدأ بإضافة بعض قواعد التحقق إلى Book class

قم بفتح الملف Book.cs فنلاحظ أن ال  System.ComponentModel.DataAnnotations namespace لايحتوي System.Web

إن DataAnnotations  تقوم بتوفير مجموعة من خصائص التحقق التي يمكن أن تطبق على أي class  أو خاصية

سنقوم الآن بتعديل  Book class  للاستفادة من خصائص التحقق. قم باستبدال كود Book class بالكود التالي:

public class Book
    {
        public int Id { set; get; }

        [StringLength(60, MinimumLength = 3)]
        public string Title { set; get; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z''-'\s]*$")]
        [Required]
        [StringLength(30)]
        public string Description { set; get; }

        [Display(Name = "Publish Date")]
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime PublishDate { set; get; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z''-'\s]*$")]
        [Required]
        [StringLength(30)]
        public string Author { set; get; }
        public int NumberOfPages { set; get; }
       
        public double GoodreadsRate { set; get; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z''-'\s]*$")]
        [StringLength(5)]
        public string Rating { set; get; }
        public int TypeId { set; get; }
        public virtual Type Type { set; get; }
    }

إن الخاصية StringLength تحدد الطول الأعظمي للسلسلة النصية وتعكس هذا الطول على الخاصية  المقابلة في قاعدة البيانات و هذا سيؤدي إلى تغير بنية قاعدة البيانات

قم بالنقر بزر الفأرة اليميني على جدول Books في نافذة Server explorer وقم بالنقر على

Open Table Definition

في الصورة السلبقة نلاحظ أن الحقول النصية موضوعة على أنها  (NVARCHAR (MAX

سنستخدم migrations  لتحديث قاعدة البيانات. قم بعمل Build للتطبيق ثم قم بفتح نافذة Package Manager Console وقم بإدخال الأوامر التالية:

add-migration DataAnnotations
update-database

عند انتهاء تنفيذ التعليمات فإن Visual Studio يقوم بفتح ملف class يحتوي على تعريف DataAnnotations class الذي يرث من  DbMigration class

نلاحظ كود التابع Up الذي يقوم بتحديث البنية للجدول Books

public override void Up()
 {
 AlterColumn("dbo.Books", "Title", c => c.String(maxLength: 60));
 AlterColumn("dbo.Books", "Description", c => c.String(nullable: false, maxLength: 30));
 AlterColumn("dbo.Books", "Author", c => c.String(nullable: false, maxLength: 30));
 AlterColumn("dbo.Books", "Rating", c => c.String(maxLength: 5));
 }

 إن الحقل Author لم يعد nullable  أي يجب أن يتم إدخال قيمة في هذا الحقل و كذلك الأمر بالنسبة للحقل Description

الحقل Rating له طول أعظمي 5 محارف و الحقل Title له طول أعظمي 60 محرف و طول أصغري 3 محارف

الصورة التالية تعرض بنية الجدول Books

نلاحظ أن الحقول النصية أصبح لها طول محدد و أن الحقول Author و Description لم تعد nullable

إن خصائص التحقق تحدد السلوك الذي نريد تطبيقه على خصائص model

إن الخاصية Required تحدد أن الخاصية يجب أن تملك قيمة (هذا لايمنع المستخدم من أن يدخل فراغ لتحقيق الشرط)

والخاصية   MinimumLength تحدد الطول الأصغري للسلسلة النصية

إن الخاصية RegularExpression تستخدم لتحديد ماهي المحارف الممكن إدخالها

في الكود السابق فإن الخصائص Description و Author  و Rating يجب أن تقبل فقط الأحرف(أي أن الفراغات و الأرقام والمحارف الخاصة غير مسموحة)

إن الخاصية StringLength تحدد الطول الأعظمي للخاصية النصية ويمكن  أيضاً تحديد الطول الأصغري

إن الحقول ذات القيم مثل(decimal, int, float, DateTime) بشكل افتراضي هي مطلوبة  وبالتالي لا تحتاج إلى الخاصية Required

إن Code First تضمن أن قواعد التحقق التي قمنا بوضعها في model class يتم تطبيقها قبل أن يقوم التطبيق بحفظ التغييرات في قاعدة البيانات

على سبيل المثال الكود التالي سيعطي exception  من النوع DbEntityValidationException عند استدعاء التابع

SaveChanges لأن العديد من الخصائص المطلوبة لم يتم إعطاء قيم لها

BookDBContextdb = new BookDBContext();
Book book= new Book();
book.Title = "Clean Code";
db.Books.Add(book);
db.SaveChanges();

الكود السابق يعطي exception التالي

Validation failed for one or more entities. See ‘EntityValidationErrors’ property for more details

يساعد وجود قواعد التحقق التي يتم تطبيقها  تلقائيا بواسطة  NET Framework. على جعل التطبيق أكثر قوة. كما أنه يضمن أنه لا يمكن أن ننسى التحقق من صحة شيء والسماح عن غير قصد للبيانات الخاطئة  في قاعدة البيانات.

قم بتشغيل التطبيق وانتقل إلى الرابط Books/

قم بالنقر على الرابط Create New لإضافة كتاب جديد

قم بملئ  الحقول بقيم غير صالحة

فنلاحظ أن jQuery client side validation قامت بتحديد الخطأ وعرض رسالة خطأ

نلاحظ أن النموذج وبشكل أوتوماتيكي قام بوضع إطار أحمر حول text boxes التي تحتوي على بيانات غير صالحة

وعرض رسالة خطأ تحت كل منها

يتم التحقق من الأخطاء في كل من  client-side باستخدام JavaScript و jQuery

و في طرف server-side في حال كانت JavaScript معطلة في متصفح الزبون

إن الفائدة من القيام بالتحقق كما فعلنا هي أننا لانحتاج لتعديل أي سطر كود في  BooksController class أو في ملف Create.cshtml  لتفعيل التحقق

حيث يقوم المتحكم و ال views  التي قمنا بإضافتها و بشكل أوتوماتيكي بأخذ قواعد التحقق التي قمنا بتحديدها باستخدام خصائص التحقق على خصائص Book model class

قم باختبار التحقق باستخدام التابع Edit فنلاحظ أن نفس قواعد التحقق مطبقة

إن بيانات النموذج لا ترسل إلى المخدم حتى يتم التحقق من عدم وجود أخطاء في طرف الزبون

يمكن التحقق من ذلك من خلال وضع break point في تابع HTTP Post أو باستخدام  fiddler tool أو باستخدام  IE F12 developer tools

كيفية عمل التحقق في Create view وتابع Create

ربما نتساءل كيف يتم توليد واجهة التحقق بدون أي تعديلات على الكود في المتحكم

الكود التالي هو لتابع Create في المتحكم BookController حيث أنه لم نقم بأي تعديل على الكود

 public ActionResult Create()
        {
            ViewBag.TypeId = new SelectList(db.Types, "Id", "Title");
            return View();
        }

        // POST: Books/Create
        // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
        // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create([Bind(Include = "Id,Title,Description,PublishDate,Author,NumberOfPages,GoodreadsRate,TypeId,Rating")] Book book)
        {
            if (ModelState.IsValid)
            {
                db.Books.Add(book);
                db.SaveChanges();
                return RedirectToAction("Index");
            }

            ViewBag.TypeId = new SelectList(db.Types, "Id", "Title", book.TypeId);
            return View(book);
        }

إن التابع HTTP GET) Create) يعرض نموذج إضافة الكتاب

إن التابع  HTTP Post) Create)  يعالج البيانات القادمة من المتصفح حيث أن هذا التابع يستخدم الخاصية ModelState.IsValid لفحص فيما إذا كان الكتاب يملك أي أخطاء في التحقق

وباستخدام هذه الخاصية يتم تقييم كل خصائص التحقق التي المطبقة على object فإذا كان object يحتوي على أخطاء تحقق فإن التابع Create يقوم بإعادة عرض النموذج وإذ لم يكن هناك أخطاء فإن التابع يقوم بحفظ الكتاب الجديد في قاعدة البيانات

في مثالنا فإن النموذج(form) لن يرسل إلى المخدم إذا كان هناك أخطاء تحقق في طرف الزبون وهذا يعني أن تابع

HTTP Post) Create) لن يتم استدعاؤه

في حال قمنا بتعطيل JavaScript  في المتصفح فإن التحقق من الصحة في طرف الزبون سيتم تعطيله وبالتالي فإن التابع HTTP POST Create سيستدعي الخاصية ModelState.IsValid لفحص فيما إذا كان book object فيه أخطاء تحقق

يمكن وضع break point عند التابع HttpPost Create والتأكد من أن التابع لن يتم استدعاؤه

حيث أن التحقق من طرف الزبون لن يرسل بيانات  النموذج إلى المخدم عندما يكون هناك أخطاء تحقق

وفي حال قمنا بتعطيل JavaScript  في المتصفح فإن النموذج سيرسل إلى المخدم مع أخطاء وعندها سيتم استدعاء التابع

HTTP POST Create

يمكن تعطيل JavaScript في متصفح Chrome باتباع الخطوات التالية

قم بالنقر على قائمة Chrome  والتي لها الشكل  More في الزاوية العليا اليمنى للمتصفح

قم باختيار Settings 

بعد ذلك قم بالنقر على Show advanced settings 

في قسم Privacy and security قم بالنقر على زر Content settings 

في قسم Javascript قم بتعطيل ال Javascript

الصورة التالية تعرض كيف يتم تعطيل JavaScript في متصفح Chrome

الصور التالية تعرض كيف يتم تعطيل JavaScript في متصفح Internet Exploler

قم بوضع break point عند التابع HttpPost Create  وقم بتشغيل التطبيق و الانتقال إلى الرابط Books/

قم بالنقر على الرابط Create New لإضافة كتاب جديد

قم بإدخال بيانات غير صالحة كما في الصورة التالية

بعد إدخال البيانات قم بالنقر على زر Create لإضافة الكتاب

نلاحظ انه سيتم استدعاء التابع HttpPost Create للتحقق من صحة القيم المدخلة وعندها تتفعل  break point  التي قمنا بوضعها عند التابع HttpPost Create

قم بالنقر على زر Continue في Visual Stdio لاستكمال التنفيذ و قم بالعودة للمتصفح فنلاحظ عرض رسائل الخطأ كما في الصورة التالية

الكود التالي هو لملف Create.cshtml  حيث أنه يستخدم من قبل تابع HTTP GET Create وتابع

HTTP POST Create لعرض النموذج الفارغ ولإعادة عرضه في حال وجود أخطاء

 
@model ArabicArchive.Models.Book

@{
    ViewBag.Title = "Create";
} 
@using (Html.BeginForm()) 
{
    @Html.AntiForgeryToken()

@Html.ValidationSummary(true, “”, new { @class = “text-danger” })

@Html.LabelFor(model => model.Title, htmlAttributes: new { @class = “control-label col-md-2” })

@Html.EditorFor(model => model.Title, new { htmlAttributes = new { @class = “form-control” } }) @Html.ValidationMessageFor(model => model.Title, “”, new { @class = “text-danger” })

 

 

@Html.LabelFor(model => model.Description, htmlAttributes: new { @class = “control-label col-md-2” })

@Html.EditorFor(model => model.Description, new { htmlAttributes = new { @class = “form-control” } }) @Html.ValidationMessageFor(model => model.Description, “”, new { @class = “text-danger” })

 

 

@Html.LabelFor(model => model.PublishDate, htmlAttributes: new { @class = “control-label col-md-2” })

@Html.EditorFor(model => model.PublishDate, new { htmlAttributes = new { @class = “form-control” } }) @Html.ValidationMessageFor(model => model.PublishDate, “”, new { @class = “text-danger” })

 

 

@Html.LabelFor(model => model.Author, htmlAttributes: new { @class = “control-label col-md-2” })

@Html.EditorFor(model => model.Author, new { htmlAttributes = new { @class = “form-control” } }) @Html.ValidationMessageFor(model => model.Author, “”, new { @class = “text-danger” })

 

 

@Html.LabelFor(model => model.NumberOfPages, htmlAttributes: new { @class = “control-label col-md-2” })

@Html.EditorFor(model => model.NumberOfPages, new { htmlAttributes = new { @class = “form-control” } }) @Html.ValidationMessageFor(model => model.NumberOfPages, “”, new { @class = “text-danger” })

 

 

@Html.LabelFor(model => model.GoodreadsRate, htmlAttributes: new { @class = “control-label col-md-2” })

@Html.EditorFor(model => model.GoodreadsRate, new { htmlAttributes = new { @class = “form-control” } }) @Html.ValidationMessageFor(model => model.GoodreadsRate, “”, new { @class = “text-danger” })

 

 

@Html.LabelFor(model => model.Rating, htmlAttributes: new { @class = “control-label col-md-2” })

@Html.EditorFor(model => model.Rating, new { htmlAttributes = new { @class = “form-control” } }) @Html.ValidationMessageFor(model => model.Rating, “”, new { @class = “text-danger” })

 

 

@Html.LabelFor(model => model.TypeId, “Type”, htmlAttributes: new { @class = “control-label col-md-2” })

@Html.DropDownList(“TypeId”, null, htmlAttributes: new { @class = “form-control” }) @Html.ValidationMessageFor(model => model.TypeId, “”, new { @class = “text-danger” })

 

 

 

 

 

}

@Html.ActionLink(“Back to List”, “Index”)

@section Scripts { @Scripts.Render(“~/bundles/jqueryval”) }

نلاحظ أنه في الكود يتم استخدام التابع Html.EditorFor لإظهار الوسم <input> من أجل كل خاصية من خصائص Book class

السطر التالي هو استدعاء للتابع Html.ValidationMessageFor

هذان التابعان يعملان مع model object المرر من المتحكم إلى view وبشكل أتوماتيكي تبحث عن خصائص التحقق المحددة في ال model   وتعرض رسائل الخطأ المناسبة

مايميز هذه الطريقة هو أن المتحكم وال Create view لايعرفان أي شيء عن قواعد التحقق الفعلية المطبقة او حتى عن رسائل الخطأ المعروضة

إن قواعد التحقق و نصوص الأخطاء تحدد فقط في Book class

إن نفس قواعد التحقق مطبقة بشكل أتوماتيكي على Edit view وأي views templates أخرى التي تضيف بيانات أو تعدل بيانات في ال model

إذا أردنا تعديل التحقق لاحقا يمكن القيام بذلك من خلال القيام بإضافة خصائص التحقق إلى model (في مثالنا Book class)

حيث أن التحقق يعرف في مكان واحد ويستخدم في كل مكان في التطبيق وهذا يجعل الكود واضح وأسهل في التعديل والتطوير

وهذا يحقق مبدأ DRY

استخدام خصائص DataType

قم بفتح الملف Book.cs

إن  System.ComponentModel.DataAnnotations namespace يوفر خصائص التنسيق بالإضافة إلى مجموعة من خصائص التحقق

قمنا  سابقاً بتطبيق الخاصية  DataType على حقل PublishDate

الكود التالي يعرض الخاصية PublishDate مع خاصية DataType

 [DataType(DataType.Date)]
public DateTime PublishDate { set; get; }

إن خصائص DataType  تعطي التوجيهات لمحرك العرض لتنسيق البيانات

يمكن استخدام الخاصية RegularExpression للتحقق من تنسيق البيانات

حيث تستخدم الخاصية DataType لتحديد نوع البيانات بشكل أكثر تحديداً من النوع الأساسي في فاعدة البيانات فهي ليست خصاص تحقق

في مثالنا نريد فقط بيانات التاريخ وليس التاريخ والوقت

إن DataType Enumeration يوفر العديد من أنواع البيانات مثل  Date و Time و PhoneNumber و Currency و EmailAddress وغيرها

يمكن للخاصية   DataType تمكين  التطبيق من توفير ميزات خاصة بالنوع تلقائيا على سبيل المثال الرابط mailto يمكن أن يتم إنشاؤه باستخدام  DataType.EmailAddress

و حقل اختيار التاريخ يمكن إنشاؤه باستخدام  DataType.Date في المتصفحات التي تدعم HTML5

إن خصائص DataType لا توفر أي تحقق كما ذكرنا سابقا

إن DataType.Date لاتؤمن تنسيق التاريخ الذي يتم عرضه وبشكل افتراضي فإن حقل البيانات يتم عرضه وفق التنسيق الافتراضي

إن الخاصية DisplayFormat تستخدم لتنسيق التاريخ

 [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime PublishDate { set; get; }

إن ApplyFormatInEditMode تحدد بأن التنسيق المحدد يجب أن يطبق عندما تعرض القيمة في مربع النص للتعديل

يمكن استخدام الخاصية DisplayFormat لوحدها ولكن من الأفضل استخدام الخاصية DataType معها أيضاً

إن الخاصية DataType  تهتم بمعاني البيانات بدلاً من كيفية عرضها على الشاشة وهي توفر المزايا التالية التي لاتوفرها الخاصية  DisplayFormat

يمكن للمتصفح تمكين ميزات HTML5(على سبيل المثال عرض عنصر calendar)

وبشكل افتراضي فإن المتصفح سيعرض البيانات بالتنسيق الصحيح بالاعتماد على locale

إن الخاصية DataType تمكن  MVC لاختيار الحقل المناسب لعرض البيانات

في حال استخدام الخاصية DataType مع حقل تاريخ فيجب أن استخدام الخاصية DisplayFormat أيضاً لضمان عرض الحقل بشكل صحيح  في متصفح  Chrome

ملاحظة: إن jQuery validation لايعمل مع خاصية Range و DateTime على سبيل المثال الكود التالي سيعرض دائماً خطأ من طرف الزبون حتى إذا كان التاريخ المدخل ضمن المجال المحدد

[Range(typeof(DateTime), "1/1/1966", "1/1/2020")]

يجب تعطيل jQuery date validation لاستخدام الخاصية Range مع DateTim وهذا غير جيد

إن استخدام الخاصية Range مع DateTim لاينصح به

الكود التالي يعرض الدمج بين الخصائص على سطر واحد

public class Book
    {
        public int Id { set; get; }

        [StringLength(60, MinimumLength = 3)]
        public string Title { set; get; }

        [Required,StringLength(30),RegularExpression(@"^[A-Z]+[a-zA-Z''-'\s]*$")]
        public string Description { set; get; }

        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true), DataType(DataType.Date), Display(Name = "Publish Date")]
        public DateTime PublishDate { set; get; }

        [Required, StringLength(30), RegularExpression(@"^[A-Z]+[a-zA-Z''-'\s]*$")]
        public string Author { set; get; }
        public int NumberOfPages { set; get; }
       
        public double GoodreadsRate { set; get; }

        [StringLength(5), RegularExpression(@"^[A-Z]+[a-zA-Z''-'\s]*$")]
        public string Rating { set; get; }
        public int TypeId { set; get; }
        public virtual Type Type { set; get; }
    }

في الدرس القادم سنقوم بعمل بعض التحسينات على توابع Details و Delete.

تغذية راجعة

اتمنى عدم التردد في الاستفسار عن أي مفهوم تم ذكره في هذا التدوينة، وأرجو تجربة ماورد في التدوينة بشكل فعلي وعدم الاكتفاء بالقراءة لتحقيق أكبر فائدة ممكنة، وكالعادة سأكون سعيد إن شاركتموني تجربتكم وتصويباتكم في حال وجود أخطاء كتابية.

 

هذه المقالة مستندة إلى سلسلة دروس مايكروسوفت الرسمية للـ ASP.NET MVC، وذلك لترتيب الدروس المناسب واعتقادي بسلاستها وأهمية نقلها إلى العربية بأسلوب مناسب وتجربة تتوافق مع الأدوات المتاحة لنا والمتوفرة في منطقتنا.

مدير تقني وشريك مؤسس لـ فسيلة تِك، مبرمج متعدد المهارات، مهتم في إنجاز أمور استثنائية في مجال التكنولوجيا وأعمل جاهداً لترك أثر إيجابي في الحياة