اعتبارسنجی در فرم‌های ASP.NET MVC با Remote Validation
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: سه دقیقه

بعد از آمدن نسخه‌ی سوم ASP.NET MVC مکانیسمی به نام Remote Validation به آن اضافه شد که کارش اعتبارسنجی از راه دور بود. فرض کنید نیاز است در یک فرم، قبل از اینکه کل فرم به سمت سرور ارسال شود، مقداری بررسی شده و اعتبارسنجی آن انجام گیرد و این اعتبارسنجی چیزی نیست که بتوان سمت کاربر و بدون فرستاده شدن مقداری به سمت سرور صورت گیرد. نمونه بارز این مسئله صفحه عضویت اکثر سایت‌هایی هست که روزانه داریم با آن‌ها کار می‌کنیم. فیلد نام کاربری توسط شما پر شده و بعد از بیرون آمدن از آن فیلد، سریعا مشخص می‌شود که آیا این نام کاربری قابل استفاده برای شما هست یا خیر. به‌صورت معمول برای انجام این کار باید با جاوا اسکریپت، مدیریتی روی فیلد مربوطه انجام دهیم. مثلا با بیرون آمدن فوکوس از روی فیلد، با Ajax نام کاربری وارد شده را به سمت سرور بفرستیم، چک کنیم و بعد از اینکه جواب برگشت بررسی کنیم که الان آیا این نام کاربری قبلا گرفته شده یا نه.
انجام این کار به‌راحتی با مزین‌کردن خصوصیت (Property) مربوطه موجود در مدل برنامه به Attribute یا ویژگی Remote و داشتن یک Action در Controller مربوطه که کارش بررسی وجود یوزرنیم هست امکان پذیر است. ادامه بحث را با مثال همراه می‌کنم.
به عنوان مثال در سیستمی که قرار هست محصولات ما را ثبت کند، باید بیایم و قبل از اینکه محصول جدید به ثبت برسد این عملیات چک‌کردن را انجام دهیم تا کالای تکراری وارد سیستم نشود. شناسه اصلی که برای هر محصول وجود دارد بارکد هست و ما آن را میخواهیم مورد بررسی قرار دهیم.


مدل برنامه

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

        [Display(Name = "نام کالا")]
        [Required(ErrorMessage = "{0} یک فیلد اجباری است و باید آن را وارد کنید.")]
        [StringLength(50, ErrorMessage = "طول {0} باید کمتر از {1} کاراکتر باشد.")]
        public string Name { get; set; }

        [Display(Name = "قیمت")]
        [Required(ErrorMessage = "{0} یک فیلد اجباری است و باید آن را وارد کنید.")]
        [DataType(DataType.Currency)]
        public double Price { get; set; }

        [Display(Name = "بارکد")]
        [Required(ErrorMessage = "{0} یک فیلد اجباری است و باید آن را وارد کنید.")]
        [StringLength(50, ErrorMessage = "طول {0} باید کمتر از {1} کاراکتر باشد.")]
        [Remote("IsProductExist", "Product", HttpMethod = "POST", ErrorMessage = "این بارکد از قبل در سیستم وجود دارد.")]
        public string Barcode { get; set; }
    }

همونطور که می‌بینید خصوصیت Barcode را مزین کردیم به ویژگی Remote. این ویژگی دارای ورودی‌های خاص خودش هست. وارد کردن نام اکشن و کنترلر مربوطه برای انجام این چک‌کردن از مهم‌ترین قسمت‌های اصلی هست. چیزهایی دیگه‌ای هم هست که می‌توانیم آن‌ها را مقداردهی کنیم. مثل HttpMethod، ErrorMessage و یا AdditionFields. HttpMethod که همان طریقه‌ی ارسال درخواست به سرور هست. ErrorMessage هم همان خطایی هست که در زمان رخ‌داد قرار است نشان داده شود. AdditionFields هم خصوصیتی را مشخص می‌کند که ما می‌خوایم به‌همراه فیلد مربوطه به سمت سرور بفرستیم. مثلا می‌تونیم به‌همراه بارکد، نام کالا را هم برای بررسی‌های مورد نیازمان بفرستیم.


کنترلر برنامه

        [HttpPost]
        [OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
        public ActionResult IsProductExist(string barcode)
        {
            if (barcode == "123456789") return Json(false); // اگر محصول وجود داشت
            return Json(true);
        }

در اینجا به نمایش قسمتی از کنترلر برنامه می‌پردازیم. اکشنی که مربوط می‌شود به چک‌کردن مقدارهای لازم و در پایان آن یک خروجی Json را برمی‌گردانیم که مقدار true یا false دارد. در حقیقت مقدار را به این صورت برمی‌گردانیم که اگر مقدار ورودی در پایگاه داده وجود دارد، false را برمی‌گرداند و اگر وجود نداشت true. همین‌طور آمدیم از کش شدن درخواست‌هایی که با Ajax آمده با ویژگی OutputCache جلوگیری کردیم.

  • #
    ‫۱۰ سال و ۲ ماه قبل، یکشنبه ۱۲ مرداد ۱۳۹۳، ساعت ۱۷:۳۳
    مشکل این روش این است که بعد از یک بار اعتبار سنجی دفعه بعد با زدن هر کلید داخل تکست باکس می‌خواد بره به بانک و اطلاعات را چک کنه.
    • #
      ‫۱۰ سال و ۲ ماه قبل، سه‌شنبه ۱۴ مرداد ۱۳۹۳، ساعت ۱۳:۰۴
      بله. البته. ولی به نظر شما چه نیازی هستش وقتی که فیلد مربوطه پر شد دوباره به فیلد برگرده. این فقط در حالتی هستش که کاربر بخواد مقدار رو تغییر بده. پس اولویت داره استفاده از این کار در برابر استفاده نکردن
  • #
    ‫۱۰ سال و ۱ ماه قبل، شنبه ۱ شهریور ۱۳۹۳، ساعت ۲۰:۱۸
    روش بسیار جالبی بود.اما یک مشکل که در این روش با آن روبرو میشویم این است که هنگام ویرایش یک رکورد موجود در بانک ، اگر قرار نباشد فیلد مورد نظر بروز رسانی شود ، اکشن متد ذکر شده دست ما را می‌بندد و اجازه آپدیت را نمی‌دهد.
    شاید یک راه ایجاد یک viewModel جداگانه برای آپدیت باشد که در آن از remote attribute صرف نظر شود ، اما این راه حل زیاد جالب به نظر نمی‌رسد!
    آیا راه حل مناسب‌تری وجود دارد؟ 
    • #
      ‫۱۰ سال و ۱ ماه قبل، یکشنبه ۲ شهریور ۱۳۹۳، ساعت ۲۳:۳۶
      برای حالت ویرایش AdditionFields آن کاربرد داره. مثلا فیلد Id رو اینجا میشه ارسال کرد تا مشخص باشه حالت ویرایش هست. در حالت ثبت معمولی، خوب هنوز Id رکورد مشخص نیست و نال هست.
      • #
        ‫۹ سال قبل، یکشنبه ۲۹ شهریور ۱۳۹۴، ساعت ۱۵:۲۷
        سلام دوست عزیز...لطفا برای قسمت ویرایش و Additional Fields  بیشتر توضیح بدین.
        • #
          ‫۹ سال قبل، یکشنبه ۲۹ شهریور ۱۳۹۴، ساعت ۱۵:۵۴

          لینک‌های قسمت مطالب مرتبط ذیل مطلب رو پیگیری کنید. مثلا اولین مورد آن یعنی ASP.NET MVC #13 به این موضوع پرداخته.

  • #
    ‫۹ سال و ۱۱ ماه قبل، سه‌شنبه ۲۲ مهر ۱۳۹۳، ساعت ۱۶:۲۷
    با سلام و تشکر؛ در صورتی که قصد داشته باشیم هنگام remote یک loader هم نشون بدیم باید چکار کرد.
    • #
      ‫۹ سال و ۱۱ ماه قبل، سه‌شنبه ۲۲ مهر ۱۳۹۳، ساعت ۱۷:۴۲
      از روال‌های رخدادگردان عمومی ajaxStart و ajaxComplete استفاده کنید.
      • #
        ‫۹ سال و ۱۱ ماه قبل، چهارشنبه ۲۳ مهر ۱۳۹۳، ساعت ۱۲:۳۷
        با تشکر 
        در واقع من سه input دارم که می‌خام remote بشند برای هر کدام هم یه loader در کنارش قرار دارم ، چه طوری میشه فهمید الان کدوم input در صفحه ajax  رو start کرده تا loader اون نمایش داده بشه 
        • #
          ‫۹ سال و ۱۱ ماه قبل، چهارشنبه ۲۳ مهر ۱۳۹۳، ساعت ۱۴:۱۷
          اگر به فایل jquery.validate.js مراجعه کنید، در قسمت remote آن، متد startRequest پیش از شروع عملیات Ajax و متد stopRequest پس از پایان کار فراخوانی می‌شوند.
          prototype: {
          startRequest: function( element ) {
            //...
          },
          stopRequest: function( element, valid ) {
            //...
          },
           این دو متد را باید برای نمایش loading بازنویسی کرد. برای مثال:
             
          var originalStartRequest = $.validator.prototype.startRequest;
          $.validator.prototype.startRequest = function (element) {
              // یافتن عنصر در حال بررسی
              var container = $('form').find("[data-valmsg-for='" + element.name + "']");
          // افزودن کلاس نمایش منتظر بمانید 
              container.addClass('loading');      
              
          // فراخوانی متد اصلی برای انجام کارهای درونی افزونه
              originalStartRequest.apply(this, arguments);
          };
          
          var originalStopRequest = $.validator.prototype.stopRequest;
          $.validator.prototype.stopRequest = function (element) {
              // یافتن عنصر در حال بررسی
              var container = $('form').find("[data-valmsg-for='" + element.name + "']");
          // حذف  کلاس نمایش منتظر بمانید 
              container.removeClass('loading');
          
          // فراخوانی متد اصلی برای انجام کارهای درونی افزونه
              originalStopRequest.apply(this, arguments);
          };
          در اینجا loading به span مخفی data-valmsg-for اضافه می‌شود.
          <span class="field-validation-valid" data-valmsg-replace="true" data-valmsg-for="Url"></span>

          نمونه‌ی این بازنویسی در مطلب « اعتبار سنجی سمت کاربر wysiwyg-editor‌ها در ASP.NET MVC  » هم انجام شده‌است.
  • #
    ‫۹ سال و ۱۰ ماه قبل، جمعه ۳۰ آبان ۱۳۹۳، ساعت ۲۳:۱۱
    با سلام
    من از این روش استفاده کردم خیلی جالبه، فقط  یک مشکل ، توی سیستم خودم خوب جواب میده ولی وقتی سایت رو پابلیش میکنم و روی هاست میزارم کار نمیکنه. نمیدونم مشکل از کجاست.
    • #
      ‫۳ سال و ۸ ماه قبل، پنجشنبه ۱۱ دی ۱۳۹۹، ساعت ۱۲:۵۶
      برا منم همچین اتفاقی می‌افتاد. بدلیل اینکه فایل‌های js مربوط به jquery.validation رو نمیشناسه. که من با گذاشتن کد  زیر در کلاس BundleConfig در داخل ساب RegisterBundles ، این مشکل رفع شد. تو سرور ببینید فایلهای jquery.validation  رو لود میکنه یا نه.
      BundleTable.EnableOptimizations = False;

  • #
    ‫۹ سال و ۳ ماه قبل، پنجشنبه ۴ تیر ۱۳۹۴، ساعت ۱۶:۴۹
    سلام وقت بخیر ; در صورتی که مدل در پوشه Model برنامه باشد   این روش اوکی است .. در صورتی که مدل برنامه در یک Calss Library  دیگر باشد این روش عمل نمی‌کند . من در این حالت با JQuery  چک می‌کنم .. آیا با Remote Validation   این امکان وجود دارد که اگر مدل در یک کلاس Library  دیگر باشد ارتباط برقرار کند ؟ 
     با تشکر و سپاس فراوان از شما 
    • #
      ‫۹ سال و ۳ ماه قبل، جمعه ۵ تیر ۱۳۹۴، ساعت ۲۱:۱۶
      با بررسی فیلد مورد نظر در خروجی html تولید شده، می‌توانید صحت عملکرد برنامه را بررسی کنید.
      مثال زیر در این زمینه می‌باشد که مدل آن در یک class library دیگر است (البته در اینجا به جای استفاده از نام اکشن و نام کنترلر از نام روت استفاده شده است)
      حالت اول: مدل برنامه در حالتی که فقط فیلد مورد نظر باید بررسی شود (ایجاد کاربر):
      namespace Project.Models
      {
          public class EmployeeCreateModel
          {
              [Required]
              [Display(Name = "آدرس ایمیل")]
              [EmailAddress(ErrorMessage = "لطفاً {0} معتبر وارد کنید.")]
              [Remote("UserExistByEmailValidation",
                  HttpMethod = "POST",
                  ErrorMessage = "ایمیل وارد شده هم اکنون توسط یکی از کاربران مورد استفاده است.‏")]
              public string Email { get; set; }
          
           ...
      
           }
      }
      - حالت دوم: مدل برنامه در حالتی که به جز فیلد مورد نظر باید یک فیلد دیگر نیز مورد بررسی قرار گیرد (ویرایش کاربر):
      namespace Project.Models
      {
          public class EmployeeEditModel
          {
              public int Id { get; set; }
      
              [Required]
              [Display(Name = "آدرس ایمیل")]
              [EmailAddress(ErrorMessage = "لطفاً {0} معتبر وارد کنید.")]
              [Remote("EmailExistForOtherUserValidation",
                  AdditionalFields = "Id",
                  HttpMethod = "POST",
                  ErrorMessage = "ایمیل وارد شده هم اکنون توسط یکی از کاربران مورد استفاده است.‏")]
              public string Email { get; set; }
      
              ....
      
          }
      }

      کنترلر چک کننده (partial بودن کلاس و virtual بودن اکشن‌ها به دلیل استفاده از T4MVC است):
      namespace Project.Web.Controllers
      {
          [RoutePrefix("UserValidation")]
          [Route("{Action}")]
          [OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
          public partial class UserValidationController : Controller
          {
              readonly IUserService<User> _userService;
              readonly IUnitOfWork _uow;
      
              public UserValidationController(IUnitOfWork uow, IUserService<User> userService)
              {
                  _userService = userService;
                  _uow = uow;
              }
      
              [HttpPost]
              [Route("~/CheckEmail", Name = "UserExistByEmailValidation")]
              public virtual JsonResult CheckEmail(string email)
              {
                  return Json(!_userService.UserExistsByEmail(email));
              }
      
              [HttpPost]
              [Route("~/CheckEmailForOtherUser", Name = "EmailExistForOtherUserValidation")]
              public virtual JsonResult CheckEmailForOtherUser(string email, int id)
              {
                  return Json(!_userService.EmailExistForOtherUser(email, id));
              }
          }
      }
       فیلد مورد نظر در خروجی Html  تولید شده، باید به صورت زیر باشد:
      - حالت اول:

      remote validation

      - حالت دوم (فیلد Id هم ارسال می‌گردد):

      remote validation + additional fields

       در صورتی که خروجی درست بود، باید script‌ها را مورد بررسی قرار دهید که یکی از متدوال‌ترین آن‌ها
      @section Scripts {
          @Scripts.Render("~/bundles/jqueryval")
      }
      می‌باشد.
  • #
    ‫۶ سال و ۵ ماه قبل، سه‌شنبه ۱۴ فروردین ۱۳۹۷، ساعت ۱۸:۱۰
    برخی مواقع تغییر مقدار تکست باکس  و برخی مواقع برداشته شدن focus از تکست باکس، موجب فعال شدن (trigger، fire)
    اعتبار سنجی می‌شود (که هنگام استفاده remote validation مشکل دوچندان می‌شود)، که این شرایط می‌تواند برای کاربر گیج کننده یا گمراه کننده باشد؛
    برای مثال در سایت جاری دو حالت زیر را در نظر بگیرید که از remote validation نیز استفاده شده است:

    1) به تنظیمات کاربری رفته و روی تکست باکس نام مستعار کلیک کرده و یک مقدار تکراری وارد کنید (مثلا سیاوش). اکنون روی تکست باکس نام کاربری کلیک کنید: پیغام تکرای بودن نام مستعار نمایش داده می‌شود. حالا روی تکست باکس نام مستعار کلیک کنید، هر مقداری را که وارد نمایید پیغام تکرای بودن نام مستعار تغییر نمی‌کند (در کل هیچ نوع اعتبار سنجی انجام نمی‌گیرد!).
    2) به تنظیمات کاربری رفته (صفحه را رفرش کنید) و روی تکست باکس نام مستعار کلیک کرده و یک مقدار تکراری وارد کنید (مثلا سیاوش).  اکنون روی تکست باکس کلمه عبور کلیک کنید (به جای تکست باکس نام کاربری در حالت قبلی): پیغام تکرای بودن نام مستعار نمایش داده می‌شود. حالا روی تکست باکس نام مستعار کلیک کنید، این بار با وارد کردن هر کارکتر اعتبار سنجی انجام می‌گیرید (در حالت قبل هیج اعتبار سنجی انجام نمی‌گرفت). 
    این مشکل (حالت 1) را چطور می‌توان برطرف کرد؟
    به نظر بنده، اگر موقع کلیک کردن (focus) روی نام مستعار، پیغام تکرای بودن .... پاک می‌شد، مشکل حل می‌شد!

    • #
      ‫۶ سال و ۵ ماه قبل، سه‌شنبه ۱۴ فروردین ۱۳۹۷، ساعت ۱۸:۳۹
      حالت پیش‌فرض اعتبارسنجی آن OnBlur هست. یکبار هم که انجام شد، تا زمانیکه cache آن تغییری نکند، تکرار نمی‌شود. اگر می‌خواهید این وضعیت را تغییر دهید، می‌توان آن‌را دستی هم فعال کرد:
      $("#id1").change(function () {
         // trigger RemoteValidation
         $('#id1').removeData('previousValue'); //clear cache
         $('form').validate().element('#id1'); //retrigger remote call
         // $('#id1').blur();
      });
      • #
        ‫۶ سال و ۵ ماه قبل، چهارشنبه ۱۵ فروردین ۱۳۹۷، ساعت ۰۰:۵۱
        البته این کد مشابه ()blur عمل می‌کند و هنگام برداشته شدن focus از تکست باکس، اگر مقدار تکست‌باکس تغییر کرده باشد اجرا می‌شود.
        $("#id1").change(function () {
           // trigger RemoteValidation
           $('#id1').removeData('previousValue'); //clear cache
           $('form').validate().element('#id1'); //retrigger remote call
           // $('#id1').blur();
        });

        اما کد زیر با هر بار تغییر اجرا می‌شود:
               $('#id1').on('change input', function () {
                        //alert('salam');
                        $(this).removeData('previousValue'); //clear cache
                        $('#registerForm').validate().element(this); //retrigger remote call
                    }
                });

        البته کلیک بهتر است و درخواست‌های اضافی به سرور ارسال نمی‌شود:
                $('#id1').click(function () {
                   if ($(this).val().length > 0 || $(this).hasClass('input-validation-error')) {
                        //alert('salam');
                        $(this).removeData('previousValue'); //clear cache
                        $('#registerForm').validate().element(this); //retrigger remote call
                    }
                });

  • #
    ‫۳ سال و ۸ ماه قبل، شنبه ۶ دی ۱۳۹۹، ساعت ۱۳:۲۰
    باتشکر، حالتی رو در نظر بگیرید که یک مودال بهمراه یک input  در صفحه وجود دارد ، زمانی که input فوکوس خود رو از دست میدهد مثلا با فشردن کلید Tab اعتبارسنجی به درستی انجام می‌شود و فرم ارسال نمی‌شود ، ولی زمانی که مستقیما دکمه Enter فشرده می‌شود و یا دکمه ثبت با کلیک ماوس فشرده می‌شود اعتبار سنجی اتفاق می‌افتد ولی تاثیری در عدم ارسال فرم ندارد ، لطفا راهنمائی کنید 
    • #
      ‫۳ سال و ۸ ماه قبل، دوشنبه ۸ دی ۱۳۹۹، ساعت ۱۳:۴۷
      روش عدم ارسال فرم در صورت شکست اعتبارسنجی:
      <script type="text/javascript">        
              $(document).ready(function () { 
                  $("form").submit(function () {
                      $(this).validate();
                      if (!$(this).valid()) {
                          console.log("validation error");
                          //note: here return false will stop the submit
                          return false;  
                      }                             
                  });
              });
      </script>