۱۳۹۱/۰۱/۲۲

ASP.NET MVC #13


اعتبار سنجي اطلاعات ورودي در فرم‌هاي ASP.NET MVC

زمانيكه شروع به دريافت اطلاعات از كاربران كرديم، نياز خواهد بود تا اعتبار اطلاعات ورودي را نيز ارزيابي كنيم. در ASP.NET MVC، به كمك يك سري متاديتا، نحوه‌ي اعتبار سنجي، تعريف شده و سپس فريم ورك بر اساس اين ويژگي‌ها، به صورت خودكار اعتبار اطلاعات انتساب داده شده به خواص يك مدل را در سمت كلاينت و همچنين در سمت سرور بررسي مي‌نمايد.
اين ويژگي‌ها در اسمبلي System.ComponentModel.DataAnnotations.dll قرار دارند كه به صورت پيش فرض در هر پروژه جديد ASP.NET MVC لحاظ مي‌شود.

يك مثال كاربردي

مدل زير را به پوشه مدل‌هاي يك پروژه جديد خالي ASP.NET MVC اضافه كنيد:

using System;
using System.ComponentModel.DataAnnotations;

namespace MvcApplication9.Models
{
    public class Customer
    {
        public int Id { set; get; }

        [Required(ErrorMessage = "Name is required.")]
        [StringLength(50)]
        public string Name { set; get; }

        [Display(Name = "Email address")]
        [Required(ErrorMessage = "Email address is required.")]
        [RegularExpression(@"\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*",
                           ErrorMessage = "Please enter a valid email address.")]
        public string Email { set; get; }

        [Range(0, 10)]
        [Required(ErrorMessage = "Rating is required.")]
        public double Rating { set; get; }

        [Display(Name = "Start date")]
        [Required(ErrorMessage = "Start date is required.")]
        public DateTime StartDate { set; get; }
    }
}

سپس كنترلر جديد زير را نيز به برنامه اضافه نمائيد:
using System.Web.Mvc;
using MvcApplication9.Models;

namespace MvcApplication9.Controllers
{
    public class CustomerController : Controller
    {
        [HttpGet]
        public ActionResult Create()
        {
            var customer = new Customer();
            return View(customer);
        }

        [HttpPost]
        public ActionResult Create(Customer customer)
        {
            if (this.ModelState.IsValid)
            {
                //todo: save data
                return Redirect("/");
            }
            return View(customer);
        }
    }
}

بر روي متد Create كليك راست كرده و گزينه Add view را انتخاب كنيد. در صفحه باز شده، گزينه Create a strongly typed view را انتخاب كرده و مدل را Customer انتخاب كنيد. همچنين قالب Scaffolding را نيز بر روي Create قرار دهيد.

توضيحات تكميلي

همانطور كه در مدل برنامه ملاحظه مي‌نمائيد، به كمك يك سري متاديتا يا اصطلاحا data annotations، تعاريف اعتبار سنجي، به همراه عبارات خطايي كه بايد به كاربر نمايش داده شوند، مشخص شده است. ويژگي Required مشخص مي‌كند كه كاربر مجبور است اين فيلد را تكميل كند. به كمك ويژگي StringLength، حداكثر تعداد حروف قابل قبول مشخص مي‌شود. با استفاده از ويژگي RegularExpression، مقدار وارد شده با الگوي عبارت باقاعده مشخص گرديده، مقايسه شده و در صورت عدم تطابق، پيغام خطايي به كاربر نمايش داده خواهد شد. به كمك ويژگي Range، بازه اطلاعات قابل قبول، مشخص مي‌گردد.
ويژگي ديگري نيز به نام System.Web.Mvc.Compare مهيا است كه براي مقايسه بين مقادير دو خاصيت كاربرد دارد. براي مثال در يك فرم ثبت نام، عموما از كاربر درخواست مي‌شود كه كلمه عبورش را دوبار وارد كند. ويژگي Compare در يك چنين مثالي كاربرد خواهد داشت.
در مورد جزئيات كنترلر تعريف شده در قسمت 11 مفصل توضيح داده شد. براي مثال خاصيت this.ModelState.IsValid مشخص مي‌كند كه آيا كارmodel binding موفق بوده يا خير و همچنين اعتبار سنجي‌هاي تعريف شده نيز در اينجا تاثير داده مي‌شوند. بنابراين بررسي آن پيش از ذخيره سازي اطلاعات ضروري است.
در حالت HttpGet صفحه ورود اطلاعات به كاربر نمايش داده خواهد شد و در حالت HttpPost، اطلاعات وارد شده دريافت مي‌گردد. اگر دست آخر، ModelState معتبر نبود، همان اطلاعات نادرست وارد شده به كاربر مجددا نمايش داده خواهد شد تا فرم پاك نشود و بتواند آن‌ها را اصلاح كند.
برنامه را اجرا كنيد. با مراجعه به مسير http://localhost/customer/create، صفحه ورود اطلاعات كاربر نمايش داده خواهد شد. در اينجا براي مثال در قسمت ورود اطلاعات آدرس ايميل، مقدار abc را وارد كنيد. بلافاصله خطاي اعتبار سنجي عدم اعتبار مقدار ورودي نمايش داده مي‌شود. يعني فريم ورك، اعتبار سنجي سمت كاربر را نيز به صورت خودكار مهيا كرده است.
اگر علاقمند باشيد كه صرفا جهت آزمايش، اعتبار سنجي سمت كاربر را غيرفعال كنيد، به فايل web.config برنامه مراجعه كرده و تنظيم زير را تغيير دهيد:

<appSettings>
    <add key="ClientValidationEnabled" value="true"/>

البته اين تنظيم تاثير سراسري دارد. اگر قصد داشته باشيم كه اين تنظيم را تنها به يك view خاص اعمال كنيم، مي‌توان از متد زير كمك گرفت:

@{ Html.EnableClientValidation(false); }

در اين حالت اگر مجددا برنامه را اجرا كرده و اطلاعات نادرستي را وارد كنيم، باز هم همان خطاهاي تعريف شده، به كاربر نمايش داده خواهد شد. اما اينبار يكبار رفت و برگشت اجباري به سرور صورت خواهد گرفت، زيرا اعتبار سنجي سمت كاربر (كه درون مرورگر و توسط كدهاي جاوا اسكريپتي اجرا مي‌شود)، غيرفعال شده است. البته امكان غيرفعال كردن جاوا اسكريپت توسط كاربر نيز وجود دارد. به همين جهت بررسي خودكار سمت سرور، امنيت سيستم را بهبود خواهد بخشيد.

نحوه تعريف عناصر مرتبط با اعتبار سنجي در Viewهاي برنامه نيز به شكل زير است:

<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>

@using (Html.BeginForm()) {
    @Html.ValidationSummary(true)
    <fieldset>
        <legend>Customer</legend>

        <div class="editor-label">
            @Html.LabelFor(model => model.Name)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Name)
            @Html.ValidationMessageFor(model => model.Name)
        </div>

همانطور كه ملاحظه مي‌كنيد به صورت پيش فرض از jQuery validator در سمت كلاينت استفاده شده است. فايل jquery.validate.unobtrusive متعلق به تيم ASP.NET MVC است و كار آن وفق دادن سيستم موجود، با jQuery validator مي‌باشد (validation adapter). در نگارش‌هاي قبلي، از كتابخانه‌هاي اعتبار سنجي مايكروسافت استفاده شده بود، اما از نگارش سه به بعد، jQuery به عنوان كتابخانه برگزيده مطرح است.
Unobtrusive همچنين در اينجا به معناي مجزا سازي كدهاي جاوا اسكريپتي، از سورس HTML صفحه و استفاده از ويژگي‌هاي data-* مرتبط با HTML5 براي معرفي اطلاعات مورد نياز اعتبار سنجي است:
<input data-val="true" data-val-required="The Birthday field is required." id="Birthday" name="Birthday" type="text" value="" />

اگر خواستيد اين مساله را بررسي كنيد، فايل web.config قرار گرفته در ريشه اصلي برنامه را باز كنيد. در آنجا مقدار UnobtrusiveJavaScriptEnabled را false كرده و بار ديگر برنامه را اجرا كنيد. در اين حالت كليه كدهاي اعتبار سنجي، به داخل سورس View رندر شده، تزريق مي‌شوند و مجزا از آن نخواهند بود.
نحوه‌ي تعريف اين اسكريپت‌ها نيز جالب توجه است. متد Url.Content، يك متد سمت سرور مي‌باشد كه در زمان اجراي برنامه، مسير نسبي وارد شده را بر اساس ساختار سايت اصلاح مي‌كند. حرف ~ بكارگرفته شده، در ASP.NET به معناي ريشه سايت است. بنابراين مسير نسبي تعريف شده از ريشه سايت شروع و تفسير مي‌شود.
اگر از اين متد استفاده نكنيم، مجبور خواهيم شد كه مسيرهاي نسبي را به شكل زير تعريف كنيم:

<script src="../../Scripts/customvaildation.js" type="text/javascript"></script>

در اين حالت بسته به محل قرارگيري صفحات و همچنين برنامه در سايت، ممكن است آدرس فوق صحيح باشد يا خير. اما استفاده از متد Url.Content، كار مسيريابي نهايي را خودكار مي‌كند.
البته اگر به فايل Views/Shared/_Layout.cshtml، مراجعه كنيد، تعريف و الحاق كتابخانه اصلي jQuery در آنجا انجام شده است. بنابراين مي‌توان اين دو تعريف ديگر مرتبط با اعتبار سنجي را به آن فايل هم منتقل كرد تا همه‌جا در دسترس باشند.
توسط متد Html.ValidationSummary، خطاهاي اعتبار سنجي مدل كه به صورت دستي اضافه شده باشند نمايش داده مي‌شود. اين مورد در قسمت 11 توضيح داده شد (چون پارامتر آن true وارد شده، فقط خطاهاي سطح مدل را نمايش مي‌دهد).
متد Html.ValidationMessageFor، با توجه به متاديتاي يك خاصيت و همچنين استثناهاي صادر شده حين model binding خطايي را به كاربر نمايش خواهد داد.



اعتبار سنجي سفارشي

ويژگي‌هاي اعتبار سنجي از پيش تعريف شده، پر كاربردترين‌ها هستند؛ اما كافي نيستند. براي مثال در مدل فوق، StartDate نبايد كمتر از سال 2000 وارد شود و همچنين در آينده هم نبايد باشد. اين موارد اعتبار سنجي سفارشي را چگونه بايد با فريم ورك، يكپارچه كرد؟
حداقل دو روش براي حل اين مساله وجود دارد:
الف) نوشتن يك ويژگي اعتبار سنجي سفارشي
ب) پياده سازي اينترفيس IValidatableObject


تعريف يك ويژگي اعتبار سنجي سفارشي

using System;
using System.ComponentModel.DataAnnotations;

namespace MvcApplication9.CustomValidators
{
    public class MyDateValidator : ValidationAttribute
    {
        public int MinYear { set; get; }

        public override bool IsValid(object value)
        {
            if (value == null) return false;

            var date = (DateTime)value;
            if (date > DateTime.Now || date < new DateTime(MinYear, 1, 1))
                return false;

            return true;
        }
    }
}

براي نوشتن يك ويژگي اعتبار سنجي سفارشي، با ارث بري از كلاس ValidationAttribute شروع مي‌كنيم. سپس بايد متد IsValid آن‌را تحريف كنيم. اگر اين متد false برگرداند به معناي شكست اعتبار سنجي مي‌باشد.
در ادامه براي بكارگيري آن خواهيم داشت:
[Display(Name = "Start date")]
[Required(ErrorMessage = "Start date is required.")]
[MyDateValidator(MinYear = 2000,
                         ErrorMessage = "Please enter a valid date.")]
public DateTime StartDate { set; get; }

اكنون مجددا برنامه را اجرا نمائيد. اگر تاريخ غيرمعتبري وارد شود، اعتبار سنجي سمت سرور رخ داده و سپس نتيجه به كاربر نمايش داده مي‌شود.


اعتبار سنجي سفارشي به كمك پياده سازي اينترفيس IValidatableObject

يك سؤال: اگر اعتبار سنجي ما پيچيده‌تر باشد چطور؟ مثلا نياز باشد مقادير دريافتي چندين خاصيت با هم مقايسه شده و سپس بر اين اساس تصميم گيري شود. براي حل اين مشكل مي‌توان از اينترفيس IValidatableObject كمك گرفت. در اين حالت مدل تعريف شده بايد اينترفيس ياد شده را پياده سازي نمايد. براي مثال:

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

namespace MvcApplication9.Models
{
    public class Customer : IValidatableObject
    {
        //... same as before

        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            var fields = new[] { "StartDate" };
            if (StartDate > DateTime.Now || StartDate < new DateTime(2000, 1, 1))
                yield return new ValidationResult("Please enter a valid date.", fields);

            if (Rating > 4 && StartDate < new DateTime(2003, 1, 1))
                yield return new ValidationResult("Accepted date should be greater than 2003", fields);
        }
    }
}

در اينجا در متد Validate، فرصت خواهيم داشت تا به مقادير كليه خواص تعريف شده در مدل دسترسي پيدا كرده و بر اين اساس اعتبار سنجي بهتري را انجام دهيم. اگر اطلاعات وارد شده مطابق منطق مورد نظر نباشند، كافي است توسط yield return new ValidationResult، يك پيغام را به همراه فيلدهايي كه بايد اين پيغام را نمايش دهند، بازگردانيم.
به اين نوع مدل‌ها، self validating models هم گفته مي‌شود.


يك نكته:

از MVC3 به بعد، حين كار با ValidationAttribute، امكان تحريف متد IsValid به همراه پارامتري از نوع ValidationContext نيز وجود دارد. به اين ترتيب مي‌توان به اطلاعات ساير خواص نيز دست يافت. البته در اين حالت نياز به استفاده از Reflection خواهد بود و پياده سازي IValidatableObject، طبيعي‌تر به نظر مي‌رسد:

protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
    var info = validationContext.ObjectType.GetProperty("Rating");
    //...
    return ValidationResult.Success;
}




فعال سازي سمت كلاينت اعتبار سنجي‌هاي سفارشي

اعتبار سنجي‌هاي سفارشي توليد شده تا به اينجا، تنها سمت سرور است كه فعال مي‌شوند. به عبارتي بايد يكبار اطلاعات به سرور ارسال شده و در بازگشت، نتيجه عمليات به كاربر نمايش داده خواهد شد. اما ويژگي‌هاي توكاري مانند Required و Range و امثال آن، علاوه بر سمت سرور، سمت كاربر هم فعال هستند و اگر جاوا اسكريپت در مرورگر كاربر غيرفعال نشده باشد، نيازي به ارسال اطلاعات يك فرم به سرور جهت اعتبار سنجي اوليه، نخواهد بود.
در اينجا بايد سه مرحله براي پياده سازي اعتبار سنجي سمت كلاينت طي شود:
الف) ويژگي سفارشي اعتبار سنجي تعريف شده بايد اينترفيس IClientValidatable را پياده سازي كند.
ب) سپس بايد متد jQuery validation متناظر را پياده سازي كرد.
ج) و همچنين مانند تيم ASP.NET MVC، بايد unobtrusive adapter خود را نيز پياده سازي كنيم. به اين ترتيب متاديتاي ASP.NET MVC به فرمتي كه افزونه jQuery validator آن‌را درك مي‌كند، وفق داده خواهد شد.

در ادامه، تكميل كلاس سفارشي MyDateValidator را ادامه خواهيم داد:
using System;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
using System.Collections.Generic;

namespace MvcApplication9.CustomValidators
{
    public class MyDateValidator : ValidationAttribute, IClientValidatable
    {
       // ... same as before

        public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
                    ModelMetadata metadata, 
                    ControllerContext context)
        {
            var rule = new ModelClientValidationRule
            {
                ValidationType = "mydatevalidator",
                ErrorMessage = FormatErrorMessage(metadata.GetDisplayName())
            };
            yield return rule;
        }
    }
}

در اينجا نحوه پياده سازي اينترفيس IClientValidatable را ملاحظه مي‌نمائيد. ValidationType، نام متدي خواهد بود كه در سمت كلاينت، كار بررسي اعتبار داده‌ها را به عهده خواهد گرفت.
سپس براي مثال يك فايل جديد به نام customvaildation.js به پوشه اسكريپت‌هاي برنامه با محتواي زير اضافه خواهيم كرد:

/// <reference path="jquery-1.5.1-vsdoc.js" />
/// <reference path="jquery.validate-vsdoc.js" />
/// <reference path="jquery.validate.unobtrusive.js" />

jQuery.validator.addMethod("mydatevalidator",
 function (value, element, param) {
     return Date.parse(value) < new Date();
});

jQuery.validator.unobtrusive.adapters.addBool("mydatevalidator");

توسط referenceهايي كه مشاهده مي‌كنيد، intellisense جي‌كوئري در VS.NET فعال مي‌شود.
سپس به كمك متد jQuery.validator.addMethod، همان مقدار ValidationType پيشين را معرفي و در ادامه بر اساس مقدار value دريافتي، تصميم گيري خواهيم كرد. اگر خروجي false باشد، به معناي شكست اعتبار سنجي است.
همچنين توسط متد jQuery.validator.unobtrusive.adapters.addBool، اين متد جديد را به مجموعه وفق دهنده‌ها اضافه مي‌كنيم.
و در آخر اين فايل جديد بايد به View مورد نظر يا فايل master page سيستم اضافه شود:

<script src="@Url.Content("~/Scripts/customvaildation.js")" type="text/javascript"></script>




تغيير رنگ و ظاهر پيغام‌هاي اعتبار سنجي

اگر از رنگ پيش فرض قرمز پيغام‌هاي اعتبار سنجي خرسند نيستيد، بايد اندكي CSS سايت را ويرايش كرد كه شامل اعمال تغييرات به موارد ذيل خواهد شد:

1. .field-validation-error
2. .field-validation-valid
3. .input-validation-error
4. .input-validation-valid
5. .validation-summary-errors
6. .validation-summary-valid




نحوه جدا سازي تعاريف متاديتا از كلاس‌هاي مدل برنامه

فرض كنيد مدل‌هاي برنامه شما به كمك يك code generator توليد مي‌شوند. در اين حالت هرگونه ويژگي اضافي تعريف شده در اين كلاس‌ها پس از توليد مجدد كدها از دست خواهند رفت. به همين منظور امكان تعريف مجزاي متاديتاها نيز پيش بيني شده است:

[MetadataType(typeof(CustomerMetadata))]
public partial class Customer
{
    class CustomerMetadata
     {

     }
}

public partial class Customer : IValidatableObject
{


حالت كلي روش انجام آن هم به شكلي است كه ملاحظه مي‌كنيد. كلاس اصلي، به صورت partial معرفي خواهد شد. سپس كلاس partial ديگري نيز به همين نام كه در برگيرنده يك كلاس داخلي ديگر براي تعاريف متاديتا است، به پروژه اضافه مي‌گردد. به كمك ويژگي MetadataType، كلاسي كه قرار است ويژگي‌هاي خواص از آن خوانده شود، معرفي مي‌گردد. موارد عنوان شده، شكل كلي اين پياده سازي است. براي نمونه اگر با WCF RIA Services كار كرده باشيد، از اين روش زياد استفاده مي‌شود. كلاس خصوصي تو در توي تعريف شده صرفا وظيفه ارائه متاديتاهاي تعريف شده را به فريم ورك خواهد داشت و هيچ كاربرد ديگري ندارد.
در ادامه كليه خواص كلاس Customer به همراه متاديتاي آن‌ها بايد به كلاس CustomerMetadata منتقل شوند. اكنون مي‌توان تمام متاديتاي كلاس اصلي Customer را حذف كرد.



اعتبار سنجي از راه دور (remote validation)

فرض كنيد شخصي مشغول به پر كردن فرم ثبت نام، در سايت شما است. پس از اينكه نام كاربري دلخواه خود را وارد كرد و مثلا به فيلد ورود كلمه عبور رسيد، در همين حال و بدون ارسال كل صفحه به سرور، به او پيغام دهيم كه نام كاربري وارد شده، هم اكنون توسط شخص ديگري در حال استفاده است. اين مكانيزم از ASP.NET MVC3 به بعد تحت عنوان Remote validation در دسترس است و يك درخواست Ajaxايي خودكار را به سرور ارسال خواهد كرد و نتيجه نهايي را به كاربر نمايش مي‌دهد؛ كارهايي كه به سادگي توسط كدهاي جاوا اسكريپتي قابل مديريت نيستند و نياز به تعامل با سرور، در اين بين وجود دارد. پياده سازي آن هم به نحو زير است:
براي مثال خاصيت Name را در مدل برنامه به نحو زير تغيير دهيد:

[Required(ErrorMessage = "Name is required.")]
[StringLength(50)]
[System.Web.Mvc.Remote(action: "CheckUserNameAndEmail",
                               controller: "Customer",
                               AdditionalFields = "Email",
                               HttpMethod = "POST",
                               ErrorMessage = "Username is not available.")]
public string Name { set; get; }

سپس متد زير را نيز به كنترلر Customer اضافه كنيد:

[HttpPost]
[OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
public ActionResult CheckUserNameAndEmail(string name, string email)
{
      if (name.ToLowerInvariant() == "vahid") return Json(false);
      if (email.ToLowerInvariant() == "name@site.com") return Json(false);
      //...
      return Json(true);
}


توضيحات:
توسط ويژگي System.Web.Mvc.Remote، نام كنترلر و متدي كه در آن قرار است به صورت خودكار توسط jQuery Ajax فراخواني شود، مشخص خواهند شد. همچنين اگر نياز بود فيلدهاي ديگري نيز به اين متد كنترلر ارسال شوند، مي‌توان آن‌ها را توسط خاصيت AdditionalFields، مشخص كرد.
سپس در كدهاي كنترلر مشخص شده، متدي با پارامترهاي خاصيت مورد نظر و فيلدهاي اضافي ديگر، تعريف مي‌شود. در اينجا فرصت خواهيم داشت تا براي مثال پس از بررسي بانك اطلاعاتي، خروجي Json ايي را بازگردانيم. return Json false به معناي شكست اعتبار سنجي است.
توسط ويژگي OutputCache، از كش شدن نتيجه درخواست‌هاي Ajaxايي جلوگيري كرده‌ايم. همچنين نوع درخواست هم جهت امنيت بيشتر، به HttpPost محدود شده است.
تمام كاري كه بايد انجام شود همين مقدار است و مابقي مسايل مرتبط با اعمال و پياده سازي آن خودكار است.


استفاده از مكانيزم اعتبار سنجي مبتني برمتاديتا در خارج از ASP.Net MVC

مباحثي را كه در اين قسمت ملاحظه نموديد، منحصر به ASP.NET MVC نيستند. براي نمونه توسط متد الحاقي زير نيز مي‌توان يك مدل را مثلا در يك برنامه كنسول هم اعتبار سنجي كرد. بديهي است در اين حالت نياز خواهد بود تا ارجاعي را به اسمبلي System.ComponentModel.DataAnnotations، به برنامه اضافه كنيم و تمام عمليات هم دستي است و فريم ورك ويژه‌اي هم وجود ندارد تا يك سري از كارها را به صورت خودكار انجام دهد.

using System.ComponentModel.DataAnnotations;

namespace MvcApplication9.Helper
{
    public static class ValidationHelper
    {
        public static bool TryValidateObject(this object instance)
        {
            return Validator.TryValidateObject(instance, new ValidationContext(instance, null, null), null);
        }
    }
}