نگاهی به ساختار طراحی اعتبارسنجی از راه دور در ASP.NET MVC و jQuery Validator
در نگارشهای مختلف ASP.NET MVC و ASP.NET Core، ویژگی Remote سمت سرور، سبب درج یک چنین ویژگیهایی در سمت کلاینت میشود:
data-val-remote="کلمه عبور وارد شده را راحت می‌توان حدس زد!" data-val-remote-additionalfields="*.Password1" data-val-remote-type="POST" data-val-remote-url="/register/checkpassword"
- متن نمایشی خطای اعتبارسنجی.
- تعدادی فیلد اضافی که در صورت نیز از فرم استخراج میشوند و به سمت سرور ارسال خواهند شد.
- نوع روش ارسال اطلاعات به سمت سرور.
- یک URL که مشخص میکند، این اطلاعات باید به کدام اکشن متد در سمت سرور ارسال شوند.
سمت سرور هم میتواند یک true یا false را بازگشت دهد و مشخص کند که آیا اطلاعات مدنظر معتبر نیستند یا هستند.
شبیه به یک چنین ساختاری را در ادامه با ایجاد یک دایرکتیو سفارشی اعتبارسنجی برنامههای Angular تدارک خواهیم دید.
ساختار اعتبارسنجهای سفارشی async در Angular
در مطلب «نوشتن اعتبارسنجهای سفارشی برای فرمهای مبتنی بر قالبها در Angular» جزئیات نوشتن اعتبارسنجهای متداول فرمهای Angular را بررسی کردیم. این نوع اعتبارسنجها چون اطلاعاتی را به صورت Ajax ایی به سمت سرور ارسال نمیکنند، با پیاده سازی اینترفیس Validator تهیه خواهند شد:
export class EmailValidatorDirective implements Validator {
export class RemoteValidatorDirective implements AsyncValidator {
validate(c: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null>;
پیاده سازی یک اعتبارسنج از راه دور مبتنی بر Observableها در Angular
ابتدا یک دایرکتیو جدید را به نام RemoteValidator به ماژول custom-validators اضافه کردهایم:
>ng g d CustomValidators/RemoteValidator -m custom-validators.module
import { Directive, Input } from "@angular/core"; import { AsyncValidator, AbstractControl, NG_ASYNC_VALIDATORS } from "@angular/forms"; import { Http, RequestOptions, Response, Headers } from "@angular/http"; import "rxjs/add/operator/map"; import "rxjs/add/operator/distinctUntilChanged"; import "rxjs/add/operator/takeUntil"; import "rxjs/add/operator/take"; import { Observable } from "rxjs/Observable"; import { Subject } from "rxjs/Subject"; @Directive({ selector: "[appRemoteValidator][formControlName],[appRemoteValidator][formControl],[appRemoteValidator][ngModel]", providers: [ { provide: NG_ASYNC_VALIDATORS, useExisting: RemoteValidatorDirective, multi: true } ] }) export class RemoteValidatorDirective implements AsyncValidator { @Input("remote-url") remoteUrl: string; @Input("remote-field") remoteField: string; @Input("remote-additional-fields") remoteAdditionalFields: string; constructor(private http: Http) {} validate(control: AbstractControl): Observable<{ [key: string]: any }> { if (!this.remoteUrl || this.remoteUrl === undefined) { return Observable.throw("`remoteUrl` is undefined."); } if (!this.remoteField || this.remoteField === undefined) { return Observable.throw("`remoteField` is undefined."); } const dataObject = {}; if ( this.remoteAdditionalFields && this.remoteAdditionalFields !== undefined ) { const otherFields = this.remoteAdditionalFields.split(","); otherFields.forEach(field => { const name = field.trim(); const otherControl = control.root.get(name); if (otherControl) { dataObject[name] = otherControl.value; } }); } // This is used to signal the streams to terminate. const changed$ = new Subject<any>(); changed$.next(); // This will signal the previous stream (if any) to terminate. const debounceTime = 400; return new Observable((obs: any) => { control.valueChanges .takeUntil(changed$) .take(1) .debounceTime(debounceTime) .distinctUntilChanged() .flatMap(term => { dataObject[this.remoteField] = term; return this.doRemoteValidation(dataObject); }) .subscribe( (result: IRemoteValidationResult) => { if (result.result) { obs.next(null); } else { obs.next({ remoteValidation: { remoteValidationMessage: result.message } }); } obs.complete(); }, error => { obs.next(null); obs.complete(); } ); }); } private doRemoteValidation(data: any): Observable<IRemoteValidationResult> { const headers = new Headers({ "Content-Type": "application/json" }); // for ASP.NET MVC const options = new RequestOptions({ headers: headers }); return this.http .post(this.remoteUrl, JSON.stringify(data), options) .map(this.extractData) .do(result => console.log("remoteValidation result: ", result)) .catch(this.handleError); } private extractData(res: Response): IRemoteValidationResult { const body = <IRemoteValidationResult>res.json(); return body || (<IRemoteValidationResult>{}); } private handleError(error: Response): Observable<any> { console.error("observable error: ", error); return Observable.throw(error.statusText); } } export interface IRemoteValidationResult { result: boolean; message: string; }
ساختار Directive تهیه شده مانند همان مطلب «نوشتن اعتبارسنجهای سفارشی برای فرمهای مبتنی بر قالبها در Angular» است، تنها با یک تفاوت:
@Directive({ selector: "[appRemoteValidator][formControlName],[appRemoteValidator][formControl],[appRemoteValidator][ngModel]", providers: [ { provide: NG_ASYNC_VALIDATORS, useExisting: RemoteValidatorDirective, multi: true } ] })
سپس ورودیهای این دایرکتیو را مشاهده میکنید:
export class RemoteValidatorDirective implements AsyncValidator { @Input("remote-url") remoteUrl: string; @Input("remote-field") remoteField: string; @Input("remote-additional-fields") remoteAdditionalFields: string;
<input #username="ngModel" required maxlength="8" minlength="4" type="text" appRemoteValidator [remote-url]="remoteUsernameValidationUrl" remote-field="FirstName" remote-additional-fields="email,password" class="form-control" name="username" [(ngModel)]="model.username">
- ویژگی remote-field مشخص میکند که اطلاعات المان جاری با چه کلیدی به سمت سرور ارسال شود.
- ویژگی remote-additional-fields مشخص میکند که علاوه بر اطلاعات کنترل جاری، اطلاعات کدامیک از کنترلهای دیگر را نیز میتوان به سمت سرور ارسال کرد.
یک نکته:
ذکر "remote-field="FirstName به معنای انتساب مقدار رشتهای FirstName به خاصیت متناظر با ویژگی remote-field است.
انتساب ویژهی "remoteUsernameValidationUrl" به [remote-url]، به معنای انتساب مقدار متغیر remoteUsernameValidationUrl که در کامپوننت متناظر این قالب مقدار دهی میشود، به خاصیت متصل به ویژگی remote-url است.
export class UserRegisterComponent implements OnInit { remoteUsernameValidationUrl = "api/Employee/CheckUser";
[remote-field]="'FirstName'"
ساختار مورد انتظار بازگشتی از سمت سرور
در کدهای فوق، یک چنین ساختاری باید از سمت سرور بازگشت داده شود:
export interface IRemoteValidationResult { result: boolean; message: string; }
namespace AngularTemplateDrivenFormsLab.Controllers { [Route("api/[controller]")] public class EmployeeController : Controller { [HttpPost("[action]")] [ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)] public IActionResult CheckUser([FromBody] Employee model) { var remoteValidationResult = new { result = true, message = $"{model.FirstName} is fine!" }; if (model.FirstName?.Equals("Vahid", StringComparison.OrdinalIgnoreCase) ?? false) { remoteValidationResult = new { result = false, message = "username:`Vahid` is already taken." }; } return Json(remoteValidationResult); } } }
همچنین اعتبارسنج سفارشی از راه دور فوق، پیامها را تنها از طریق HttpPost ارسال میکند. علت اینجا است که در حالت POST، برخلاف حالت GET میتوان اطلاعات بیشتری را بدون نگرانی از طول URL، ارسال کرد و همچنین کل درخواست، به علت وجود کاراکترهای غیرمجاز در URL (حالت GET، به درخواست یک URL از سرور تفسیر میشود)، برگشت نمیخورد.
تکمیل کامپوننت فرم ثبت نام کاربران
در ادامه تکمیل قالب user-register.component.html را مشاهده میکنید:
<div class="form-group" [class.has-error]="username.invalid && username.touched"> <label class="control-label">User Name</label> <input #username="ngModel" required maxlength="8" minlength="4" type="text" appRemoteValidator [remote-url]="remoteUsernameValidationUrl" remote-field="FirstName" remote-additional-fields="email,password" class="form-control" name="username" [(ngModel)]="model.username"> <div *ngIf="username.pending" class="alert alert-warning"> Checking server, Please wait ... </div> <div *ngIf="username.invalid && username.touched"> <div class="alert alert-danger" *ngIf="username.errors.remoteValidation"> {{username.errors.remoteValidation.remoteValidationMessage}} </div> </div> </div>
زمانیکه یک async validator مشغول به کار است و هنوز پاسخی را دریافت نکردهاست، خاصیت pending را به true تنظیم میکند. به این ترتیب میتوان پیام اتصال به سرور را نمایش داد:
همچنین چون در اینجا نحوهی طراحی شکست اعتبارسنجی به صورت ذیل است:
obs.next({ remoteValidation: { remoteValidationMessage: result.message } });
مزایای استفاده از Observableها در حین طراحی async validators
در کدهای فوق چنین مواردی را هم مشاهده میکنید:
// This is used to signal the streams to terminate. const changed$ = new Subject<any>(); changed$.next(); // This will signal the previous stream (if any) to terminate. const debounceTime = 400; return new Observable((obs: any) => { control.valueChanges .takeUntil(changed$) .take(1) .debounceTime(debounceTime) .distinctUntilChanged()
همچنین وجود و تعریف new Subject، دراینجا ضروری است و از نشتی حافظه و همچنین رفت و برگشتهای اضافهی دیگری به سمت سرور، جلوگیری میکند. این subject سبب میشود تا کلیه اعمال ناتمام پیشین، لغو شده (takeUntil) و تنها آخرین درخواست جدید رسیدهی پس از 400 میلیثانیه، به سمت سرور ارسال شود.
بنابراین همانطور که مشاهده میکنید، Observableها فراتر هستند از صرفا ارسال اطلاعات به سرور و بازگشت آنها به سمت کلاینت (استفادهی متداولی که از آنها در برنامههای Angular وجود دارد).
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.