اشتراکها
کتابخانه sir-trevor-js
مطالب دورهها
لغو اعمال غیرهمزمان
دات نت 4.5 روش عمومی را جهت لغو اعمال غیرهمزمان طولانی اضافه کردهاست. برای مثال اگر نیاز است تا چندین عمل با هم انجام شوند تا کار مشخصی صورت گیرد و یکی از آنها با شکست مواجه شود، ادامهی عملیات با سایر وظایف تعریف شده، بیحاصل است. لغو اعمال در برنامههای دارای رابط کاربری نیز حائز اهمیت است. برای مثال یک کاربر ممکن است تصمیم بگیرد تا عملیاتی طولانی را لغو کند.
مدل لغو اعمال
پایه لغو اعمال، توسط مکانیزمی به نام CancellationToken پیاده سازی شدهاست و آنرا به عنوان یکی از آرگومانهای متدهایی که لغو اعمال را پشتیبانی میکنند، مشاهده خواهید کرد. به این ترتیب یک عمل خاص میتواند دریابد چه زمانی لغو آن درخواست شدهاست. البته باید دقت داشت که این عملیات بر مبنای ایدهی همه یا هیچ است. به این معنا که یک درخواست لغو را بار دیگر نمیتوان لغو کرد.
یک مثال استفاده از CancellationToken
کدهای زیر، یک فایل حجیم را از مکانی به مکانی دیگر کپی میکنند. برای این منظور از متد CopyToAsync که در دات نت 4.5 اضافه شدهاست، استفاده کردهایم؛ زیرا از مکانیزم لغو عملیات پشتیبانی میکند.
کار با تعریف CancellationTokenSource شروع میشود. چون از نوع IDisposable است، نیاز است توسط عبارت using، جهت پاکسازی منابع آن، محصور گردد. سپس در اینجا اگر کاربر کلید c را فشار دهد، متد لغو توکن تعریف شده فراخوانی خواهد شد. این توکن نیز به عنوان آرگومان به متد CopyToAsync ارسال شدهاست.
علت استفاده از ContinueWith در اینجا این است که اگر یک task لغو شود، فراخوانی متد Wait بر روی آن سبب بروز استثناء میگردد. به همین جهت توسط ContinueWith یک Task خالی ایجاد شده و سپس بر روی آن Wait فراخوانی گردیدهاست.
همچنین باید دقت داشت که سازندهی CancellationTokenSource امکان دریافت زمان timeout عملیات را نیز دارد. به علاوه متد CancelAfter نیز برای آن طراحی شدهاست. نمونهی دیگری از تنظیم timeout را در قسمت قبل با معرفی متد Task.Delay و استفاده از آن با Task.WhenAny مشاهده کردید.
لغو ظاهری وظایفی که لغو پذیر نیستند
فرض کنید متدی به نام GetBitmapAsync با پارامتر cancellationToken طراحی نشدهاست. در این حالت کاربر قصد دارد با کلیک بر روی دکمهی لغو، عملیات را خاتمه دهد. یک روش حل این مساله، استفاده از متد ذیل است:
در اینجا از روش Task.WhenAny استفاده شدهاست که در آن دو task ترکیب شدهاند. Task اول همان وظیفهای اصلی است و task دوم، از یک TaskCompletionSource حاصل شدهاست. اگر کاربر دستور لغو را صادر کند، callback ثبت شده توسط این توکن، اجرا خواهد شد. بنابراین در اینجا TrySetResult به true تنظیم شده و یکی از دو Task معرفی شده در WhenAny خاتمه مییابد.
این مورد هر چند task اول را واقعا لغو نمیکند، اما سبب خواهد شد تا کدهای پس از await UntilCompletionOrCancellation اجرا شوند.
طراحی متدهای غیرهمزمان لغو پذیر
کلاس زیر را در نظر بگیرید:
در اینجا cancellationToken متد Task.Run تنظیم شدهاست. همچنین پس از فراخوانی آن، اگر کاربر کلیدی را فشار دهد، متد Cancel این توکن فراخوانی خواهد شد. اما .... خروجی برنامه به صورت زیر است:
بله. وظیفهی شروع شده، لغو شدهاست اما متد test آن هنوز مشغول به کار است.
روش اول حل این مشکل، معرفی پارامتر CancellationToken به متد test و سپس بررسی مداوم خاصیت IsCancellationRequested آن میباشد:
در اینجا اگر متد cts.Cancel فراخوانی شود، مقدار خاصیت ct.IsCancellationRequested مساوی true شده و حلقه خاتمه مییابد.
روش دوم لغو عملیات، استفاده از متد Register است. هر زمان که توکن لغو شود، callback آن فراخوانی خواهد شد:
این روش خصوصا برای حالتهایی مفید است که در آنها از متدهایی استفاده میشود که خودشان امکان لغو شدن را نیز دارند. به این ترتیب دیگر نیازی نیست مدام بررسی کرد که آیا مقدار IsCancellationRequested مساوی true شدهاست یا خیر. هر زمان که callback ثبت شده در متد Register فراخوانی شد، یعنی عملیات باید خاتمه یابد.
مدل لغو اعمال
پایه لغو اعمال، توسط مکانیزمی به نام CancellationToken پیاده سازی شدهاست و آنرا به عنوان یکی از آرگومانهای متدهایی که لغو اعمال را پشتیبانی میکنند، مشاهده خواهید کرد. به این ترتیب یک عمل خاص میتواند دریابد چه زمانی لغو آن درخواست شدهاست. البته باید دقت داشت که این عملیات بر مبنای ایدهی همه یا هیچ است. به این معنا که یک درخواست لغو را بار دیگر نمیتوان لغو کرد.
یک مثال استفاده از CancellationToken
کدهای زیر، یک فایل حجیم را از مکانی به مکانی دیگر کپی میکنند. برای این منظور از متد CopyToAsync که در دات نت 4.5 اضافه شدهاست، استفاده کردهایم؛ زیرا از مکانیزم لغو عملیات پشتیبانی میکند.
using System; using System.IO; using System.Threading; namespace Async08 { class Program { static void Main(string[] args) { var source = @"c:\dir\file.bin"; var target = @"d:\dir\file.bin"; using (var inStream = File.OpenRead(source)) { using (var outStream = File.OpenWrite(target)) { using (var cts = new CancellationTokenSource()) { var task = inStream.CopyToAsync(outStream, bufferSize: 4059, cancellationToken: cts.Token); Console.WriteLine("Press 'c' to cancel."); var key = Console.ReadKey().KeyChar; if (key == 'c') { Console.WriteLine("Cancelling"); cts.Cancel(); } Console.WriteLine("Wating..."); task.ContinueWith(t => { }).Wait(); Console.WriteLine("Status: {0}", task.Status); } } } } } }
علت استفاده از ContinueWith در اینجا این است که اگر یک task لغو شود، فراخوانی متد Wait بر روی آن سبب بروز استثناء میگردد. به همین جهت توسط ContinueWith یک Task خالی ایجاد شده و سپس بر روی آن Wait فراخوانی گردیدهاست.
همچنین باید دقت داشت که سازندهی CancellationTokenSource امکان دریافت زمان timeout عملیات را نیز دارد. به علاوه متد CancelAfter نیز برای آن طراحی شدهاست. نمونهی دیگری از تنظیم timeout را در قسمت قبل با معرفی متد Task.Delay و استفاده از آن با Task.WhenAny مشاهده کردید.
لغو ظاهری وظایفی که لغو پذیر نیستند
فرض کنید متدی به نام GetBitmapAsync با پارامتر cancellationToken طراحی نشدهاست. در این حالت کاربر قصد دارد با کلیک بر روی دکمهی لغو، عملیات را خاتمه دهد. یک روش حل این مساله، استفاده از متد ذیل است:
public static class CancellationTokenExtensions { public static async Task UntilCompletionOrCancellation(Task asyncOp, CancellationToken ct) { var tcs = new TaskCompletionSource<bool>(); using (ct.Register(() => tcs.TrySetResult(true))) { await Task.WhenAny(asyncOp, tcs.Task); } } }
این مورد هر چند task اول را واقعا لغو نمیکند، اما سبب خواهد شد تا کدهای پس از await UntilCompletionOrCancellation اجرا شوند.
طراحی متدهای غیرهمزمان لغو پذیر
کلاس زیر را در نظر بگیرید:
public class CancellationTokenTest { public static void Run() { var cts = new CancellationTokenSource(); Task.Run(async () => await test(), cts.Token); Console.ReadLine(); cts.Cancel(); Console.WriteLine("Cancel..."); Console.ReadLine(); } private static async Task test() { while (true) { await Task.Delay(1000); Console.WriteLine("Test..."); } } }
Test... Test... Test... Cancel... Test... Test... Test... Test...
روش اول حل این مشکل، معرفی پارامتر CancellationToken به متد test و سپس بررسی مداوم خاصیت IsCancellationRequested آن میباشد:
public class CancellationTokenTest { public static void Run() { var cts = new CancellationTokenSource(); Task.Run(async () => await test(cts.Token), cts.Token); Console.ReadLine(); cts.Cancel(); Console.WriteLine("Cancel..."); Console.ReadLine(); } private static async Task test(CancellationToken ct) { while (true) { await Task.Delay(1000, ct); Console.WriteLine("Test..."); if (ct.IsCancellationRequested) { break; } } Console.WriteLine("Test cancelled"); } }
روش دوم لغو عملیات، استفاده از متد Register است. هر زمان که توکن لغو شود، callback آن فراخوانی خواهد شد:
private static async Task test2(CancellationToken ct) { bool isRunning = true; ct.Register(() => { isRunning = false; Console.WriteLine("Query cancelled"); }); while (isRunning) { await Task.Delay(1000, ct); Console.WriteLine("Test..."); } Console.WriteLine("Test cancelled"); }
اشتراکها
ساده سازی Blazor Wasm PreRendering
پس از ایجاد AuthService در قسمت قبل، اکنون میخواهیم از آن برای تکمیل صفحهی ورود به سیستم و همچنین تغییر منوی بالای برنامه یا همان کامپوننت header استفاده کنیم.
ایجاد ماژول Dashboard و تعریف کامپوننت صفحهی محافظت شده
قصد داریم پس از لاگین موفق، کاربر را به یک صفحهی محافظت شده هدایت کنیم. به همین جهت ماژول جدید Dashboard را به همراه کامپوننت یاد شده، به برنامه اضافه میکنیم:
پس از اجرای این دستورات، ابتدا به فایل app.module.ts مراجعه کرده و تعریف این ماژول را که به صورت خودکار به قسمت imports اضافه شدهاست، به قبل از AppRoutingModule منتقل میکنیم تا مسیریابیهای آن توسط catch all ماژول AppRouting لغو نشوند:
همچنین به فایل dashboard-routing.module.ts ایجاد شده مراجعه کرده و مسیریابی کامپوننت جدید ProtectedPage را اضافه میکنیم:
ایجاد صفحهی ورود به سیستم
در قسمت اول این سری، کارهای «ایجاد ماژول Authentication و تعریف کامپوننت لاگین» انجام شدند. اکنون میخواهیم کامپوننت خالی لاگین را به نحو ذیل تکمیل کنیم:
مدل login را همان اینترفیس Credentials تعریف شدهی در قسمت قبل درنظر گرفتهایم. به همراه دو خاصیت error جهت نمایش خطاها در ذیل قسمت لاگین و همچنین ذخیره سازی returnUrl در صورت وجود:
AuthService را به سازندهی کامپوننت لاگین تزریق کردهایم تا بتوان از متدهای login و Logout آن استفاده کرد. همچنین از سرویس Router برای استفادهی از متد navigate آن استفاده میکنیم و از سرویس ActivatedRoute برای دریافت کوئری استرینگ returnUrl، در صورت وجود، کمک خواهیم گرفت.
اکنون متد ارسال این فرم چنین شکلی را پیدا میکند:
در اینجا توسط وهلهی تزریق شدهی this.authService، کار فراخوانی متد login و ارسال شیء Credentials به سمت سرور صورت میگیرد. خروجی متد login یک Observable از نوع boolean است. بنابراین در صورت true بودن این مقدار و یا موفقیت آمیز بودن عملیات لاگین، کاربر را به یکی از دو صفحهی protectedPage و یا this.returnUrl (در صورت وجود)، هدایت خواهیم کرد.
صفحهی خالی protectedPage را در ابتدای بحث، در ذیل ماژول Dashboard ایجاد کردیم.
در سمت سرور هم در صورت شکست اعتبارسنجی کاربر، یک return Unauthorized صورت میگیرد که معادل error.status === 401 کدهای فوق است و در اینجا در قسمت خطای عملیات بررسی شدهاست.
قالب این کامپوننت نیز به صورت ذیل به model از نوع Credentials آن متصل شدهاست:
در اینجا خاصیتهای نام کاربری، کلمهی عبور و همچنین rememberMe مدل، از کاربر دریافت میشوند؛ به همراه بررسی اعتبارسنجی سمت کلاینت آنها؛ با این شکل:
برای آزمایش برنامه، نام کاربری Vahid و کلمهی عبور 1234 را وارد کنید.
تکمیل کامپوننت Header برنامه
در ادامه، پس از لاگین موفق شخص، میخواهیم صفحهی protectedPage را نمایش دهیم:
در این صفحه، Login از منوی سایت حذف شدهاست و بجای آن Logout به همراه «نام نمایشی کاربر» ظاهر شدهاند. همچنین توکن decode شده به همراه تاریخ انقضای آن نمایش داده شدهاند.
برای پیاده سازی این موارد، ابتدا از کامپوننت Header شروع میکنیم:
این کامپوننت وضعیت گزارش شدهی ورود شخص را توسط خاصیت isLoggedIn در اختیار قالب خود قرار میدهد. برای این منظور به سرویس AuthService تزریق شدهی در سازندهی خود نیاز دارد.
اکنون در روال رخدادگردان ngOnInit، مشترک authStatus میشود که یک BehaviorSubject است و از آن جهت صدور رخدادهای authService به تمام کامپوننتهای مشترک به آن استفاده کردهایم:
Status بازگشت داده شدهی توسط آن از نوع boolean است و در صورت true بودن، خاصیت isLoggedIn را نیز true میکند که از آن در قالب این کامپوننت برای نمایش و یا مخفی کردن لینکهای login و logout استفاده خواهیم کرد.
همچنین اگر این وضعیت true باشد، مقدار DisplayName کاربر را نیز از سرویس authService دریافت کرده و توسط خاصیت this.displayName در اختیار قالب Header قرار میدهیم.
در آخر برای جلوگیری از نشتی حافظه، ضروری است اشتراک به authStatus، در روال رخدادگردان ngOnDestroy لغو شود:
همچنین در قالب Header، مدیریت دکمهی Logout را نیز انجام خواهیم داد:
با این مقدمات، قالب Header اکنون به صورت ذیل تغییر میکند:
در اینجا توسط ngIfها وضعیت خاصیت isLoggedIn این کامپوننت را بررسی میکنیم. اگر true باشد، Logoff به همراه نام نمایشی کاربر را در منوی راهبری سایت ظاهر خواهیم کرد و در غیراینصورت لینک به صفحهی Login را نمایش میدهیم.
تکمیل کامپوننت صفحهی محافظت شده
در تصویر قبل، نمایش توکن decode شده را نیز مشاهده کردید. این نمایش توسط کامپوننت صفحهی محافظت شده، مدیریت میشود:
در اینجا به کمک سرویس تزریقی AuthService، یکبار با استفاده از متد getDecodedAccessToken آن، مقدار اصلی توکن را و بار دیگر توسط متد getAccessTokenExpirationDate آن، تاریخ انقضای توکن دریافتی از سمت سرور را نمایش میدهیم؛ با این قالب:
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
ایجاد ماژول Dashboard و تعریف کامپوننت صفحهی محافظت شده
قصد داریم پس از لاگین موفق، کاربر را به یک صفحهی محافظت شده هدایت کنیم. به همین جهت ماژول جدید Dashboard را به همراه کامپوننت یاد شده، به برنامه اضافه میکنیم:
>ng g m Dashboard -m app.module --routing >ng g c Dashboard/ProtectedPage
import { DashboardModule } from "./dashboard/dashboard.module"; @NgModule({ imports: [ //... DashboardModule, AppRoutingModule ] }) export class AppModule { }
import { ProtectedPageComponent } from "./protected-page/protected-page.component"; const routes: Routes = [ { path: "protectedPage", component: ProtectedPageComponent } ];
ایجاد صفحهی ورود به سیستم
در قسمت اول این سری، کارهای «ایجاد ماژول Authentication و تعریف کامپوننت لاگین» انجام شدند. اکنون میخواهیم کامپوننت خالی لاگین را به نحو ذیل تکمیل کنیم:
export class LoginComponent implements OnInit { model: Credentials = { username: "", password: "", rememberMe: false }; error = ""; returnUrl: string;
constructor( private authService: AuthService, private router: Router, private route: ActivatedRoute) { } ngOnInit() { // reset the login status this.authService.logout(false); // get the return url from route parameters this.returnUrl = this.route.snapshot.queryParams["returnUrl"]; }
اکنون متد ارسال این فرم چنین شکلی را پیدا میکند:
submitForm(form: NgForm) { this.error = ""; this.authService.login(this.model) .subscribe(isLoggedIn => { if (isLoggedIn) { if (this.returnUrl) { this.router.navigate([this.returnUrl]); } else { this.router.navigate(["/protectedPage"]); } } }, (error: HttpErrorResponse) => { console.log("Login error", error); if (error.status === 401) { this.error = "Invalid User name or Password. Please try again."; } else { this.error = `${error.statusText}: ${error.message}`; } }); }
صفحهی خالی protectedPage را در ابتدای بحث، در ذیل ماژول Dashboard ایجاد کردیم.
در سمت سرور هم در صورت شکست اعتبارسنجی کاربر، یک return Unauthorized صورت میگیرد که معادل error.status === 401 کدهای فوق است و در اینجا در قسمت خطای عملیات بررسی شدهاست.
قالب این کامپوننت نیز به صورت ذیل به model از نوع Credentials آن متصل شدهاست:
<div class="panel panel-default"> <div class="panel-heading"> <h2 class="panel-title">Login</h2> </div> <div class="panel-body"> <form #form="ngForm" (submit)="submitForm(form)" novalidate> <div class="form-group" [class.has-error]="username.invalid && username.touched"> <label for="username">User name</label> <input id="username" type="text" required name="username" [(ngModel)]="model.username" #username="ngModel" class="form-control" placeholder="User name"> <div *ngIf="username.invalid && username.touched"> <div class="alert alert-danger" *ngIf="username.errors['required']"> Name is required. </div> </div> </div> <div class="form-group" [class.has-error]="password.invalid && password.touched"> <label for="password">Password</label> <input id="password" type="password" required name="password" [(ngModel)]="model.password" #password="ngModel" class="form-control" placeholder="Password"> <div *ngIf="password.invalid && password.touched"> <div class="alert alert-danger" *ngIf="password.errors['required']"> Password is required. </div> </div> </div> <div class="checkbox"> <label> <input type="checkbox" name="rememberMe" [(ngModel)]="model.rememberMe"> Remember me </label> </div> <div class="form-group"> <button type="submit" class="btn btn-primary" [disabled]="form.invalid ">Login</button> </div> <div *ngIf="error" class="alert alert-danger " role="alert "> {{error}} </div> </form> </div> </div>
برای آزمایش برنامه، نام کاربری Vahid و کلمهی عبور 1234 را وارد کنید.
تکمیل کامپوننت Header برنامه
در ادامه، پس از لاگین موفق شخص، میخواهیم صفحهی protectedPage را نمایش دهیم:
در این صفحه، Login از منوی سایت حذف شدهاست و بجای آن Logout به همراه «نام نمایشی کاربر» ظاهر شدهاند. همچنین توکن decode شده به همراه تاریخ انقضای آن نمایش داده شدهاند.
برای پیاده سازی این موارد، ابتدا از کامپوننت Header شروع میکنیم:
export class HeaderComponent implements OnInit, OnDestroy { title = "Angular.Jwt.Core"; isLoggedIn: boolean; subscription: Subscription; displayName: string; constructor(private authService: AuthService) { }
اکنون در روال رخدادگردان ngOnInit، مشترک authStatus میشود که یک BehaviorSubject است و از آن جهت صدور رخدادهای authService به تمام کامپوننتهای مشترک به آن استفاده کردهایم:
ngOnInit() { this.subscription = this.authService.authStatus$.subscribe(status => { this.isLoggedIn = status; if (status) { this.displayName = this.authService.getDisplayName(); } }); }
همچنین اگر این وضعیت true باشد، مقدار DisplayName کاربر را نیز از سرویس authService دریافت کرده و توسط خاصیت this.displayName در اختیار قالب Header قرار میدهیم.
در آخر برای جلوگیری از نشتی حافظه، ضروری است اشتراک به authStatus، در روال رخدادگردان ngOnDestroy لغو شود:
ngOnDestroy() { // prevent memory leak when component is destroyed this.subscription.unsubscribe(); }
همچنین در قالب Header، مدیریت دکمهی Logout را نیز انجام خواهیم داد:
logout() { this.authService.logout(true); }
با این مقدمات، قالب Header اکنون به صورت ذیل تغییر میکند:
<nav class="navbar navbar-default"> <div class="container-fluid"> <div class="navbar-header"> <a class="navbar-brand" [routerLink]="['/']">{{title}}</a> </div> <ul class="nav navbar-nav"> <li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }"> <a class="nav-link" [routerLink]="['/welcome']">Home</a> </li> <li *ngIf="!isLoggedIn" class="nav-item" routerLinkActive="active"> <a class="nav-link" queryParamsHandling="merge" [routerLink]="['/login']">Login</a> </li> <li *ngIf="isLoggedIn" class="nav-item" routerLinkActive="active"> <a class="nav-link" (click)="logout()">Logoff [{{displayName}}]</a> </li> <li *ngIf="isLoggedIn" class="nav-item" routerLinkActive="active"> <a class="nav-link" [routerLink]="['/protectedPage']">Protected Page</a> </li> </ul> </div> </nav>
تکمیل کامپوننت صفحهی محافظت شده
در تصویر قبل، نمایش توکن decode شده را نیز مشاهده کردید. این نمایش توسط کامپوننت صفحهی محافظت شده، مدیریت میشود:
import { Component, OnInit } from "@angular/core"; import { AuthService } from "../../core/services/auth.service"; @Component({ selector: "app-protected-page", templateUrl: "./protected-page.component.html", styleUrls: ["./protected-page.component.css"] }) export class ProtectedPageComponent implements OnInit { decodedAccessToken: any = {}; accessTokenExpirationDate: Date = null; constructor(private authService: AuthService) { } ngOnInit() { this.decodedAccessToken = this.authService.getDecodedAccessToken(); this.accessTokenExpirationDate = this.authService.getAccessTokenExpirationDate(); } }
<h1> Decoded Access Token </h1> <div class="alert alert-info"> <label> Access Token Expiration Date:</label> {{accessTokenExpirationDate}} </div> <div> <pre>{{decodedAccessToken | json}}</pre> </div>
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
These are the customer-reported issues addressed in 15.7.3:
- VS2017 compiler creates broken debug build using Qt framework and generates 'Invalid address specified to RtlValidateHeap' error.
- Incorrect code generation for matrix multiplication.
- VS 2017 Update 7: Git History Codelens only showing entries for the past 6 months.
- UWP projects reference multiple NetStandard 2.0 dlls after 15.7.1 upgrade.
- Building C++ code in VS 15.7 with /std:c++17 breaks binary compatibility for std::_Ptr_move_cat.
- Visual Studio 15.7 stuck when opening XAML files.
- CMake configuration fails and generates message "C++ IntelliSense information may be out of date, generate the CMake cache to refresh".
- Unable to start second process for debugging.
- After update to Visual Studio 15.7.1, some test programs fail in start-up due to reading access violation.
- Missing compiler required member 'Microsoft.CSharp.RuntimeBinder.Binder.Convert'.
- Build fails after 15.7.0 update on older project using .NET 3.5 .
- Coloring, typing, tooltips and IntelliSense slow in F# in VS2017 editor.
- F# editing experience takes up to a minute for tooltips and dropdowns to display.
- Certain class member variable value are incorrectly read as zero.
- Attempt to open XAML file for the first time causes VS to sit with the "Opening the file ..." message for about 10 minutes before XAML file opens.
- Visual Studio slows down and freezes, creating work loss.
- The target "GetBuiltProjectOutputRecursive" does not exist in the project.
- Internal error with lambda C++17 after 15.7.1 update.
- UWP App is slow to return a stopped state in IDE.
- MSVC auto-vectorization produces incorrect code or incorrect results.
- Visual Studio closed debug a new instance project1, when a new debug new instance project2 has started.
- Latest update breaks "start without debugging" on multiple projects.
- UWP XAML is very very slow on open.
- XAML viewer freezes on 15.7.2 and 15.8.0 preview 1.1.
- Xamarin UI Test App project template missing.
- Xamarin project creation problem.
- Visual Studio crashes when creating new Mobile APP Xamarin.
- Unable to create Xamarin.Forms-Projects.
- Não consigo criar novos projetos Xamarin Forms - Can't create new projects Xamarin Forms.
- Blank project crash after update.
نظرات مطالب
مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger - قسمت ششم - تکمیل مستندات محافظت از API
HandleChallengeAsync رو در حالت دیباگ تست کردم اصلا اجرا نمیشه!
private readonly SignInKeysOption _signInKeyOptions; private string failReason; public CredentialAuthenticationHandler( IOptionsMonitor < CustomAuthenticationOptions > options, IOptionsMonitor < SignInKeysOption > signInKeyOptions, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock): base(options, logger, encoder, clock) { _signInKeyOptions = signInKeyOptions.CurrentValue ?? throw new ArgumentNullException(nameof(signInKeyOptions)); } protected override Task HandleChallengeAsync(AuthenticationProperties properties) { Response.StatusCode = 401; if (failReason != null) { Response.HttpContext.Features.Get < IHttpResponseFeature > () !.ReasonPhrase = failReason; } return Task.CompletedTask; } protected async override Task < AuthenticateResult > HandleAuthenticateAsync() { string authorizationHeader = Request.Headers["Authorization"]; if (authorizationHeader == null) { Logger.LogWarning("Authorization key in header is null or empty"); //string result; //Context.Response.StatusCode = StatusCodes.Status401Unauthorized; //result = JsonConvert.SerializeObject(new { error = "Authorization key in header is null or empty" }); //Context.Response.ContentType = "application/json"; //await Context.Response.WriteAsync(result); failReason = "Authorization key in header is null or empty"; return AuthenticateResult.Fail("request doesn't contains header"); //return await Task.FromResult(AuthenticateResult.Fail("UnAuthenticate")); } }
در مطلب «فرمهای مبتنی بر قالبها در Angular - قسمت چهارم - اعتبارسنجی ورودیها» مشاهده کردیم که Angular در روش فرمهای مبتنی بر قالبها، تنها از 4 روش بومی اعتبارسنجی مرورگرها مانند ذکر ویژگی required برای فیلدهای اجباری، ویژگیهای minlength و maxlength برای تعیین حداقل و حداکثر تعداد حروف مجاز قابل ورود در یک فیلد و از pattern برای کار با عبارات با قاعده پشتیبانی میکند. برای بهبود این وضعیت در این مطلب قصد داریم روش تهیه اعتبارسنجهای سفارشی مخصوص حالت فرمهای مبتنی بر قالبها را بررسی کنیم.
تدارک مقدمات مثال این قسمت
این مثال، در ادامهی همین سری کار با فرمهای مبتنی بر قالبها است. به همین جهت ابتدا ماژول جدید CustomValidators را به آن اضافه میکنیم:
همچنین به فایل app.module.ts مراجعه کرده و CustomValidatorsModule را بجای CustomValidatorsRoutingModule در قسمت imports معرفی میکنیم. سپس به این ماژول جدید، کامپوننت فرم ثبت نام یک کاربر را اضافه خواهیم کرد:
که اینکار سبب به روز رسانی فایل custom-validators.module.ts و افزوده شدن UserRegisterComponent به قسمت declarations آن میشود.
در ادامه کلاس مدل معادل فرم ثبت نام کاربران را تعریف میکنیم:
با این محتوا:
در طراحی فرم HTML ایی آن نیاز است این موارد رعایت شوند:
- ورود نام کاربری اجباری بوده و باید بین 5 تا 8 حرف باشد.
- ورود ایمیل اجباری بوده و باید فرمت مناسبی نیز داشته باشد.
- ورود کلمهی عبور اجباری بوده و باید با confirmPassword تطابق داشته باشد.
- ورود «کلمهی عبور خود را مجددا وارد کنید» اجباری بوده و باید با password تطابق داشته باشد.
تعریف اعتبارسنج سفارشی ایمیلها
هرچند میتوان اعتبارسنجی ایمیلها را توسط ویژگی استاندارد pattern نیز مدیریت کرد، اما جهت بررسی نحوهی انتقال آن به یک اعتبارسنج سفارشی، کار را با ایجاد یک دایرکتیو مخصوص آن ادامه میدهیم:
این دستور علاوه بر ایجاد فایل جدید email-validator.directive.ts و تکمیل ساختار ابتدایی آن، کار به روز رسانی custom-validators.module.ts را نیز انجام میدهد. در این حالت به صورت خودکار قسمت declarations این ماژول با EmailValidatorDirective مقدار دهی میشود.
در ادامه کدهای کامل این اعتبارسنج سفارشی را مشاهده میکنید:
توضیحات تکمیلی:
- علت تعریف این اعتبارسنج به صورت یک دایرکتیو جدید این است که بتوان selector آنرا همانند ویژگیهای HTML، به فیلد ورودی اضافه کرد:
- روش تعریف selector آن اندکی متفاوت است:
در اینجا مطابق https://angular.io/guide/styleguide#style-02-08 توصیه شدهاست که:
الف) نام دایرکتیو باید با یک پیشوند شروع شود و این پیشوند در فایل angular-cli.json. به app تنظیم شدهاست:
این مساله در جهت مشخص کردن سفارشی بودن این دایرکتیو و همچنین کاهش احتمال تکرار نامها توصیه شدهاست.
ب) در اینجا formControlName، formControl و ngModel قید شدهی در کنار نام selector این دایرکتیو را نیز مشاهده میکنید. وجود آنها به این معنا است که کلاس این دایرکتیو، به المانهایی که به آنها ویژگی appEmailValidator اضافه شدهاست و همچنین آن المانها از یکی از سه نوع ذکر شده هستند، اعمال میشود و در سایر موارد بیاثر خواهد بود. البته ذکر این سه نوع، اختیاری است و صرفا میتوان نوشت:
- پس از آن قسمت providers را مشاهده میکنید:
کار قسمت multi آن این است که EmailValidatorDirective (یا همان کلاس جاری) را به لیست NG_VALIDATORS توکار (اعتبارسنجهای توکار مبتنی بر قالبها) اضافه میکند و سبب بازنویسی هیچ موردی نخواهد شد. بنابراین وجود این قسمت در جهت تکمیل تامین کنندههای توکار Angular ضروری است.
- سپس پیاده سازی اینترفیس توکار Validator را مشاهده میکنید:
این اینترفیس جزو مجموعهی فرمهای مبتنی بر قالبها است و از آن جهت نوشتن اعتبارسنجهای سفارشی میتوان استفاده کرد.
برای پیاده سازی این اینترفیس، نیاز است متد اجباری ذیل را نیز افزود و تکمیل کرد:
کار این متد این است که المانی را که appEmailValidator به آن اعمال شدهاست، به عنوان پارامتر متد validate در اختیار کلاس جاری قرار میدهد. به این ترتیب میتوان برای مثال به مقدار آن دسترسی یافت و سپس منطق سفارشی را پیاده سازی و یک خروجی key/value را بازگشت داد.
برای مثال در اینجا مقدار فیلد ایمیل element.value توسط عبارت باقاعدهی نوشته شده بررسی میشود. اگر با این الگو انطباق داشته باشد، نال بازگشت داده میشود (اعلام عدم وجود مشکلی در اعتبارسنجی) و اگر خیر، یک شیء key/value دلخواه را میتوان بازگشت داد.
- اکنون که این دایرکتیو جدید طراحی و ثبت شدهاست (در قسمت declarations فایل custom-validators.module.ts)، تنها کافی است selector آنرا به المان ورودی مدنظر اعمال کنیم تا کار اعتبارسنجی آنرا به صورت خودکار مدیریت کند:
نحوهی طراحی خروجی متد validate
هنگام پیاده سازی متد validate اینترفیس Validator، هیچ قالب خاصی برای خروجی آن درنظر گرفته نشدهاست و همینقدر که این خروجی یک شیء key/value باشد، کفایت میکند. برای مثال اگر اعتبارسنج استاندارد required با شکست مواجه شود، یک چنین شیءایی را بازگشت میدهد:
و یا اگر اعتبارسنج استاندارد minlength باشکست مواجه شود، اطلاعات بیشتری را در قسمت مقدار این کلید بازگشتی، ارائه میدهد:
در کل اینکه چه چیزی را بازگشت دهید، بستگی به طراحی مدنظر شما دارد؛ برای نمونه در اینجا appEmailValidator (یک کلید و نام دلخواه است و هیچ الزامی ندارد که با نام selector این دایرکتیو یکی باشد)، به true تنظیم شدهاست:
بنابراین شرط تامین نوع خروجی، برقرار است. علت true بودن آن نیز مورد ذیل است:
در اینجا اگر false را بازگشت دهیم، هرچند email.errors دارای کلید جدید appEmailValidator شدهاست، اما ngIf سبب رندر خطای اعتبارسنجی «ایمیل وارد شده معتبر نیست.» به علت false بودن نتیجهی نهایی، نمیشود. یا حتی میتوان بجای true یک رشته و یا یک شیء با توضیحات بیشتری را نیز تنظیم کرد؛ چون value این key/value به any تنظیم شدهاست و هر چیزی را میپذیرد.
از دیدگاه اعتبارسنج فرمهای مبتنی بر قالبها، همینقدر که آرایهی email.errors دارای عضو و کلید جدیدی شد، کار به پایان رسیدهاست و اعتبارسنجی المان را شکست خورده ارزیابی میکند. مابقی آن، اطلاعاتی است که برنامه نویس ارائه میدهد (بر اساس نیازهای نمایشی برنامه).
تهیه اعتبارسنج سفارشی مقایسهی کلمات عبور با یکدیگر
در طراحی کلاس User که معادل فیلدهای فرم ثبت نام کاربران است، دو خاصیت کلمهی عبور و تائید کلمهی عبور را مشاهده میکنید:
Angular به همراه اعتبارسنج توکاری برای بررسی یکی بودن این دو نیست. به همین جهت نمونهی سفارشی آنرا همانند EmailValidatorDirective فوق تهیه میکنیم. ابتدا یک دایرکتیو جدید را به نام EqualValidator به ماژول custom-validators اضافه میکنیم:
که سبب ایجاد فایل جدید equal-validator.directive.ts و به روز رسانی قسمت declarations فایل custom-validators.module.ts با EqualValidatorDirective نیز میشود.
در ادامه کدهای کامل آنرا در ذیل مشاهده میکنید:
توضیحات تکمیلی:
- قسمت آغازین این اعتبارسنج سفارشی، مانند توضیحات EmailValidatorDirective است که در ابتدای بحث عنوان شد. این کلاس به یک Directive مزین شدهاست تا بتوان selector آنرا به المانهای HTML ایی فرم افزود (برای مثال در اینجا به دو فیلد ورود کلمات عبور). قسمت providers آن نیز تنظیم شدهاست تا EqualValidatorDirective جاری به لیست توکار NG_VALIDATORS اضافه شود.
- در ابتدای کار، پیاده سازی اینترفیس Validator، همانند قبل انجام شدهاست؛ اما چون در اینجا میخواهیم نام فیلدی را که قرار است کار مقایسه را با آن انجام دهیم نیز دریافت کنیم، ابتدا یک Attribute و سپس یک پارامتر و خاصیت عمومی دریافت کنندهی مقدار آنرا نیز افزودهایم:
به این ترتیب زمانیکه قرار است فیلد کلمهی عبور را تعریف کنیم، ابتدا ویژگی appValidateEqual یا همان selector این اعتبارسنج به آن اضافه شدهاست تا کار فعال سازی ابتدایی صورت گیرد:
سپس Attribute یا ویژگی به نام compare-to نیز تعریف شدهاست. این compare-to همان نامی است که به Attribute@ نسبت داده شدهاست. سپس مقداری که به این ویژگی نسبت داده میشود، توسط خاصیت compareToControl دریافت خواهد شد.
در اینجا محدودیتی هم از لحاظ تعداد ویژگیها نیست و اگر قرار است این اعتبارسنج اطلاعات بیشتری را نیز دریافت کند میتوان ویژگیهای بیشتری را به سازندهی آن نسبت داد.
یک نکته: میتوان نام این ویژگی را با نام selector نیز یکی انتخاب کرد. به این ترتیب ذکر نام ویژگی آن، هم سبب فعال شدن اعتبارسنج و هم نسبت دادن مقداری به آن، سبب مقدار دهی خاصیت متناظر با آن، در سمت کلاس اعتبارسنج میگردد.
- در ابتدای این اعتبارسنج، نحوهی دسترسی به مقدار یک کنترل دیگر را نیز مشاهده میکنید:
در اینجا element.value مقدار المان یا کنترل HTML جاری است که appValidateEqual به آن اعمال شدهاست.
بر اساس مقدار خاصیت compareToControl که از ویژگی compare-to دریافت میشود، میتوان به کنترل دوم، توسط element.root.get دسترسی یافت.
- در ادامهی کار، مقایسهی سادهای را مشاهده میکنید:
اگر کنترل دوم یافت شد و همچنین مقدار آن با مقدار کنترل جاری یکی نبود، همان شیء key/value مورد انتظار متد validate، در جهت اعلام شکست اعتبارسنجی بازگشت داده میشود.
- در پایان کدهای متد validate، چنین تنظیمی نیز قرار گرفتهاست:
اعتبارسنج تعریف شده، فقط به کنترلی که هم اکنون در حال کار با آن هستیم اعمال میشود. اگر پیشتر کلمهی عبوری را وارد کرده باشیم و سپس به فیلد تائید آن مراجعه کنیم، وضعیت اعتبارسنجی فیلد کلمهی عبور قبلی به حالت غیرمعتبر تنظیم شدهاست. اما پس از تکمیل فیلد تائید کلمهی عبور، هرچند وضعیت فیلد جاری معتبر است، اما هنوز وضعیت فیلد قبلی غیرمعتبر میباشد. برای رفع این مشکل، ابتدا کلید دلخواه appValidateEqual را از آن حذف میکنیم (همان کلیدی است که پیشتر در صورت مساوی نبودن مقدار فیلدها بازگشت داده شدهاست). حذف این کلید سبب نال شدن آرایهی errors یک شیء نمیشود و همانطور که پیشتر عنوان شد، Angular تنها به همین مورد توجه میکند. بنابراین در ادامه کار، setErrors یا تنظیم آرایهی errors به نال هم انجام شدهاست. در اینجا است که Angular فیلد دوم را نیز معتبر ارزیابی خواهد کرد.
تکمیل کامپوننت فرم ثبت نام کاربران
اکنون user-register.component.ts را که در ابتدای بحث اضافه کردیم، چنین تعاریفی را پیدا میکند:
در اینجا تنها کار مهمی که انجام شدهاست، ارائهی خاصیت عمومی مدل، جهت استفادهی از آن در قالب HTML ایی این کامپوننت است. بنابراین به فایل user-register.component.html مراجعه کرده و آنرا نیز به صورت ذیل تکمیل میکنیم:
ابتدای فرم
در اینجا novalidate اضافه شدهاست تا اعتبارسنجی توکار مرورگرها با اعتبارسنجی سفارشی فرم جاری تداخل پیدا نکند. همچنین توسط یک template reference variable به وهلهای از فرم دسترسی یافته و آنرا به متد submitForm کامپوننت ارسال کردهایم.
تکمیل قسمت ورود نام کاربری
اعتبارسنجی فیلد نام کاربری شامل سه قسمت بررسی errors.required، errors.minlength و errors.maxlength است.
تکمیل قسمت ورود ایمیل
در اینجا نحوهی استفادهی از دایرکتیو جدید appEmailValidator را ملاحظه میکنید. این دایرکتیو ابتدا به المان فوق متصل و سپس نتیجهی آن در قسمت ngIf، برای نمایش خطای متناظری بررسی شدهاست.
تکمیل قسمتهای ورود کلمهی عبور و تائید آن
در اینجا نحوهی اعمال دایرکتیو جدید appValidateEqual و همچنین ویژگی compare-to آنرا به فیلدهای کلمهی عبور و تائید آن مشاهده میکنید.
همچنین خروجی آن نیز در قسمت ngIf آخر بررسی شدهاست و سبب نمایش خطای اعتبارسنجی متناسبی میشود.
تکمیل انتهای فرم
در اینجا بررسی میشود که آیا فرم معتبر است یا خیر. اگر خیر، دکمهی submit آن غیرفعال میشود و برعکس.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-template-driven-forms-lab-08.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس به ریشهی پروژه وارد شده و دو پنجرهی کنسول مجزا را باز کنید. در اولی دستورات
و در دومی دستورات ذیل را اجرا کنید:
اکنون میتوانید برنامه را در آدرس http://localhost:5000 مشاهده و اجرا کنید.
تدارک مقدمات مثال این قسمت
این مثال، در ادامهی همین سری کار با فرمهای مبتنی بر قالبها است. به همین جهت ابتدا ماژول جدید CustomValidators را به آن اضافه میکنیم:
>ng g m CustomValidators -m app.module --routing
>ng g c CustomValidators/user-register
در ادامه کلاس مدل معادل فرم ثبت نام کاربران را تعریف میکنیم:
>ng g cl CustomValidators/user
export class User { constructor( public username: string = "", public email: string = "", public password: string = "", public confirmPassword: string = "" ) {} }
- ورود نام کاربری اجباری بوده و باید بین 5 تا 8 حرف باشد.
- ورود ایمیل اجباری بوده و باید فرمت مناسبی نیز داشته باشد.
- ورود کلمهی عبور اجباری بوده و باید با confirmPassword تطابق داشته باشد.
- ورود «کلمهی عبور خود را مجددا وارد کنید» اجباری بوده و باید با password تطابق داشته باشد.
تعریف اعتبارسنج سفارشی ایمیلها
هرچند میتوان اعتبارسنجی ایمیلها را توسط ویژگی استاندارد pattern نیز مدیریت کرد، اما جهت بررسی نحوهی انتقال آن به یک اعتبارسنج سفارشی، کار را با ایجاد یک دایرکتیو مخصوص آن ادامه میدهیم:
>ng g d CustomValidators/EmailValidator -m custom-validators.module
در ادامه کدهای کامل این اعتبارسنج سفارشی را مشاهده میکنید:
import { Directive } from "@angular/core"; import { AbstractControl, NG_VALIDATORS, Validator } from "@angular/forms"; @Directive({ selector: "[appEmailValidator][formControlName],[appEmailValidator][formControl],[appEmailValidator][ngModel]", providers: [ { provide: NG_VALIDATORS, useExisting: EmailValidatorDirective, multi: true } ] }) export class EmailValidatorDirective implements Validator { validate(element: AbstractControl): { [key: string]: any } { const emailRegex = /\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/; const valid = emailRegex.test(element.value); return valid ? null : { appEmailValidator: true }; } }
- علت تعریف این اعتبارسنج به صورت یک دایرکتیو جدید این است که بتوان selector آنرا همانند ویژگیهای HTML، به فیلد ورودی اضافه کرد:
<input #email="ngModel" required appEmailValidator type="text" class="form-control" name="email" [(ngModel)]="model.email">
- روش تعریف selector آن اندکی متفاوت است:
selector: "[appEmailValidator][formControlName],[appEmailValidator][formControl],[appEmailValidator][ngModel]",
الف) نام دایرکتیو باید با یک پیشوند شروع شود و این پیشوند در فایل angular-cli.json. به app تنظیم شدهاست:
"apps": [ { // ... "prefix": "app",
ب) در اینجا formControlName، formControl و ngModel قید شدهی در کنار نام selector این دایرکتیو را نیز مشاهده میکنید. وجود آنها به این معنا است که کلاس این دایرکتیو، به المانهایی که به آنها ویژگی appEmailValidator اضافه شدهاست و همچنین آن المانها از یکی از سه نوع ذکر شده هستند، اعمال میشود و در سایر موارد بیاثر خواهد بود. البته ذکر این سه نوع، اختیاری است و صرفا میتوان نوشت:
selector: "[appEmailValidator]"
- پس از آن قسمت providers را مشاهده میکنید:
providers: [ { provide: NG_VALIDATORS, useExisting: EmailValidatorDirective, multi: true }
- سپس پیاده سازی اینترفیس توکار Validator را مشاهده میکنید:
export class EmailValidatorDirective implements Validator {
برای پیاده سازی این اینترفیس، نیاز است متد اجباری ذیل را نیز افزود و تکمیل کرد:
validate(element: AbstractControl): { [key: string]: any }
validate(element: AbstractControl): { [key: string]: any } { const emailRegex = /\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/; const valid = emailRegex.test(element.value); return valid ? null : { appEmailValidator: true }; }
- اکنون که این دایرکتیو جدید طراحی و ثبت شدهاست (در قسمت declarations فایل custom-validators.module.ts)، تنها کافی است selector آنرا به المان ورودی مدنظر اعمال کنیم تا کار اعتبارسنجی آنرا به صورت خودکار مدیریت کند:
<input #email="ngModel" required appEmailValidator type="text" class="form-control" name="email" [(ngModel)]="model.email">
نحوهی طراحی خروجی متد validate
هنگام پیاده سازی متد validate اینترفیس Validator، هیچ قالب خاصی برای خروجی آن درنظر گرفته نشدهاست و همینقدر که این خروجی یک شیء key/value باشد، کفایت میکند. برای مثال اگر اعتبارسنج استاندارد required با شکست مواجه شود، یک چنین شیءایی را بازگشت میدهد:
{ required:true }
{ minlength : { requiredLength : 3, actualLength : 1 } }
{ appEmailValidator: true }
<div class="alert alert-danger" *ngIf="email.errors.appEmailValidator"> The entered email is not valid. </div>
از دیدگاه اعتبارسنج فرمهای مبتنی بر قالبها، همینقدر که آرایهی email.errors دارای عضو و کلید جدیدی شد، کار به پایان رسیدهاست و اعتبارسنجی المان را شکست خورده ارزیابی میکند. مابقی آن، اطلاعاتی است که برنامه نویس ارائه میدهد (بر اساس نیازهای نمایشی برنامه).
تهیه اعتبارسنج سفارشی مقایسهی کلمات عبور با یکدیگر
در طراحی کلاس User که معادل فیلدهای فرم ثبت نام کاربران است، دو خاصیت کلمهی عبور و تائید کلمهی عبور را مشاهده میکنید:
public password: string = "", public confirmPassword: string = ""
>ng g d CustomValidators/EqualValidator -m custom-validators.module
در ادامه کدهای کامل آنرا در ذیل مشاهده میکنید:
import { Directive, Attribute } from "@angular/core"; import { Validator, AbstractControl, NG_VALIDATORS } from "@angular/forms"; @Directive({ selector: "[appValidateEqual][formControlName],[appValidateEqual][formControl],[appValidateEqual][ngModel]", providers: [ { provide: NG_VALIDATORS, useExisting: EqualValidatorDirective, multi: true } ] }) export class EqualValidatorDirective implements Validator { constructor(@Attribute("compare-to") public compareToControl: string) {} validate(element: AbstractControl): { [key: string]: any } { const selfValue = element.value; const otherControl = element.root.get(this.compareToControl); console.log("EqualValidatorDirective", { thisControlValue: selfValue, otherControlValue: otherControl ? otherControl.value : null }); if (otherControl && selfValue !== otherControl.value) { return { appValidateEqual: true // Or a string such as 'Password mismatch.' or an abject. }; } if ( otherControl && otherControl.errors && selfValue === otherControl.value ) { delete otherControl.errors["appValidateEqual"]; if (!Object.keys(otherControl.errors).length) { otherControl.setErrors(null); } } return null; } }
- قسمت آغازین این اعتبارسنج سفارشی، مانند توضیحات EmailValidatorDirective است که در ابتدای بحث عنوان شد. این کلاس به یک Directive مزین شدهاست تا بتوان selector آنرا به المانهای HTML ایی فرم افزود (برای مثال در اینجا به دو فیلد ورود کلمات عبور). قسمت providers آن نیز تنظیم شدهاست تا EqualValidatorDirective جاری به لیست توکار NG_VALIDATORS اضافه شود.
- در ابتدای کار، پیاده سازی اینترفیس Validator، همانند قبل انجام شدهاست؛ اما چون در اینجا میخواهیم نام فیلدی را که قرار است کار مقایسه را با آن انجام دهیم نیز دریافت کنیم، ابتدا یک Attribute و سپس یک پارامتر و خاصیت عمومی دریافت کنندهی مقدار آنرا نیز افزودهایم:
export class EqualValidatorDirective implements Validator { constructor(@Attribute("compare-to") public compareToControl: string) {}
<input #password="ngModel" required type="password" class="form-control" appValidateEqual compare-to="confirmPassword" name="password" [(ngModel)]="model.password">
در اینجا محدودیتی هم از لحاظ تعداد ویژگیها نیست و اگر قرار است این اعتبارسنج اطلاعات بیشتری را نیز دریافت کند میتوان ویژگیهای بیشتری را به سازندهی آن نسبت داد.
یک نکته: میتوان نام این ویژگی را با نام selector نیز یکی انتخاب کرد. به این ترتیب ذکر نام ویژگی آن، هم سبب فعال شدن اعتبارسنج و هم نسبت دادن مقداری به آن، سبب مقدار دهی خاصیت متناظر با آن، در سمت کلاس اعتبارسنج میگردد.
- در ابتدای این اعتبارسنج، نحوهی دسترسی به مقدار یک کنترل دیگر را نیز مشاهده میکنید:
export class EqualValidatorDirective implements Validator { constructor(@Attribute("compare-to") public compareToControl: string) {} validate(element: AbstractControl): { [key: string]: any } { const selfValue = element.value; const otherControl = element.root.get(this.compareToControl); console.log("EqualValidatorDirective", { thisControlValue: selfValue, otherControlValue: otherControl ? otherControl.value : null });
بر اساس مقدار خاصیت compareToControl که از ویژگی compare-to دریافت میشود، میتوان به کنترل دوم، توسط element.root.get دسترسی یافت.
- در ادامهی کار، مقایسهی سادهای را مشاهده میکنید:
if (otherControl && selfValue !== otherControl.value) { return { appValidateEqual: true // Or a string such as 'Password mismatch.' or an abject. }; }
- در پایان کدهای متد validate، چنین تنظیمی نیز قرار گرفتهاست:
if (otherControl && otherControl.errors && selfValue === otherControl.value) { delete otherControl.errors["appValidateEqual"]; if (!Object.keys(otherControl.errors).length) { otherControl.setErrors(null); } } return null;
تکمیل کامپوننت فرم ثبت نام کاربران
اکنون user-register.component.ts را که در ابتدای بحث اضافه کردیم، چنین تعاریفی را پیدا میکند:
import { NgForm } from "@angular/forms"; import { User } from "./../user"; import { Component, OnInit } from "@angular/core"; @Component({ selector: "app-user-register", templateUrl: "./user-register.component.html", styleUrls: ["./user-register.component.css"] }) export class UserRegisterComponent implements OnInit { model = new User(); constructor() {} ngOnInit() {} submitForm(form: NgForm) { console.log(this.model); console.log(form.value); } }
ابتدای فرم
<div class="container"> <h3>Registration Form</h3> <form #form="ngForm" (submit)="submitForm(form)" novalidate>
تکمیل قسمت ورود نام کاربری
<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" class="form-control" name="username" [(ngModel)]="model.username"> <div *ngIf="username.invalid && username.touched"> <div class="alert alert-info"> errors: {{ username.errors | json }} </div> <div class="alert alert-danger" *ngIf="username.errors.required"> username is required. </div> <div class="alert alert-danger" *ngIf="username.errors.minlength"> username should be minimum {{username.errors.minlength.requiredLength}} characters. </div> <div class="alert alert-danger" *ngIf="username.errors.maxlength"> username should be max {{username.errors.maxlength.requiredLength}} characters. </div> </div> </div>
تکمیل قسمت ورود ایمیل
<div class="form-group" [class.has-error]="email.invalid && email.touched"> <label class="control-label">Email</label> <input #email="ngModel" required appEmailValidator type="text" class="form-control" name="email" [(ngModel)]="model.email"> <div *ngIf="email.invalid && email.touched"> <div class="alert alert-info"> errors: {{ email.errors | json }} </div> <div class="alert alert-danger" *ngIf="email.errors.required"> email is required. </div> <div class="alert alert-danger" *ngIf="email.errors.appEmailValidator"> The entered email is not valid. </div> </div> </div>
تکمیل قسمتهای ورود کلمهی عبور و تائید آن
<div class="form-group" [class.has-error]="password.invalid && password.touched"> <label class="control-label">Password</label> <input #password="ngModel" required type="password" class="form-control" appValidateEqual compare-to="confirmPassword" name="password" [(ngModel)]="model.password"> <div *ngIf="password.invalid && password.touched"> <div class="alert alert-info"> errors: {{ password.errors | json }} </div> <div class="alert alert-danger" *ngIf="password.errors.required"> password is required. </div> <div class="alert alert-danger" *ngIf="password.errors.appValidateEqual"> Password mismatch. Please complete the confirmPassword . </div> </div> </div> <div class="form-group" [class.has-error]="confirmPassword.invalid && confirmPassword.touched"> <label class="control-label">Retype password</label> <input #confirmPassword="ngModel" required type="password" class="form-control" appValidateEqual compare-to="password" name="confirmPassword" [(ngModel)]="model.confirmPassword"> <div *ngIf="confirmPassword.invalid && confirmPassword.touched"> <div class="alert alert-info"> errors: {{ confirmPassword.errors | json }} </div> <div class="alert alert-danger" *ngIf="confirmPassword.errors.required"> confirmPassword is required. </div> <div class="alert alert-danger" *ngIf="confirmPassword.errors.appValidateEqual"> Password mismatch. </div> </div> </div>
همچنین خروجی آن نیز در قسمت ngIf آخر بررسی شدهاست و سبب نمایش خطای اعتبارسنجی متناسبی میشود.
تکمیل انتهای فرم
<button class="btn btn-primary" [disabled]="form.invalid" type="submit">Ok</button> </form> </div>
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-template-driven-forms-lab-08.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس به ریشهی پروژه وارد شده و دو پنجرهی کنسول مجزا را باز کنید. در اولی دستورات
>npm install >ng build --watch
>dotnet restore >dotnet watch run
در ادامه بررسی تصویر امنیتی سایت مواردی که باید پیاده سازی شود برای مورد اول میتوان کلاس زیر را در نظر گرفت که متدهایی برای تولید اعداد بصورت تصادفی در بین بازه معرفی شده را بازگشت میدهد:
// RandomGenerator.cs using System; using System.Security.Cryptography; namespace PersianCaptchaHandler { public class RandomGenerator { private static readonly byte[] Randb = new byte[4]; private static readonly RNGCryptoServiceProvider Rand = new RNGCryptoServiceProvider(); public static int Next() { Rand.GetBytes(Randb); var value = BitConverter.ToInt32(Randb, 0); if (value < 0) value = -value; return value; } public static int Next(int max) { Rand.GetBytes(Randb); var value = BitConverter.ToInt32(Randb, 0); value = value%(max + 1); if (value < 0) value = -value; return value; } public static int Next(int min, int max) { var value = Next(max - min) + min; return value; } } }
// NumberToString.cs namespace PersianCaptchaHandler { public class NumberToString { #region Fields private static readonly string[] Yakan = new[] { "صفر", "یک", "دو", "سه", "چهار", "پنج", "شش", "هفت", "هشت", "نه" }; private static readonly string[] Dahgan = new[] { "", "", "بیست", "سی", "چهل", "پنجاه", "شصت", "هفتاد", "هشتاد", "نود" }; private static readonly string[] Dahyek = new [] { "ده", "یازده", "دوازده", "سیزده", "چهارده", "پانزده", "شانزده", "هفده", "هجده", "نوزده" }; private static readonly string[] Sadgan = new [] { "", "یکصد", "دوصد", "سیصد", "چهارصد", "پانصد", "ششصد", "هفتصد", "هشتصد", "نهصد" }; private static readonly string[] Basex = new [] { "", "هزار", "میلیون", "میلیارد", "تریلیون" }; #endregion private static string Getnum3(int num3) { var s = ""; var d12 = num3 % 100; var d3 = num3 / 100; if (d3 != 0) s = Sadgan[d3] + " و "; if ((d12 >= 10) && (d12 <= 19)) { s = s + Dahyek[d12 - 10]; } else { var d2 = d12 / 10; if (d2 != 0) s = s + Dahgan[d2] + " و "; var d1 = d12 % 10; if (d1 != 0) s = s + Yakan[d1] + " و "; s = s.Substring(0, s.Length - 3); } return s; } public static string ConvertIntNumberToFarsiAlphabatic(string snum) { var stotal = ""; if (snum == "0") return Yakan[0]; snum = snum.PadLeft(((snum.Length - 1) / 3 + 1) * 3, '0'); var l = snum.Length / 3 - 1; for (var i = 0; i <= l; i++) { var b = int.Parse(snum.Substring(i * 3, 3)); if (b != 0) stotal = stotal + Getnum3(b) + " " + Basex[l - i] + " و "; } stotal = stotal.Substring(0, stotal.Length - 3); return stotal; } } }
using System; using System.IO; using System.Security.Cryptography; using System.Text; namespace PersianCaptchaHandler { public class Encryptor { #region constraints private static string Password { get { return "Mehdi"; } } private static string Salt { get { return "Payervand"; } } #endregion public static string Encrypt(string clearText) { // Turn text to bytes var clearBytes = Encoding.Unicode.GetBytes(clearText); var pdb = new PasswordDeriveBytes(Password, Encoding.Unicode.GetBytes(Salt)); var ms = new MemoryStream(); var alg = Rijndael.Create(); alg.Key = pdb.GetBytes(32); alg.IV = pdb.GetBytes(16); var cs = new CryptoStream(ms, alg.CreateEncryptor(), CryptoStreamMode.Write); cs.Write(clearBytes, 0, clearBytes.Length); cs.Close(); var encryptedData = ms.ToArray(); return Convert.ToBase64String(encryptedData); } public static string Decrypt(string cipherText) { // Convert text to byte var cipherBytes = Convert.FromBase64String(cipherText); var pdb = new PasswordDeriveBytes(Password, Encoding.Unicode.GetBytes(Salt)); var ms = new MemoryStream(); var alg = Rijndael.Create(); alg.Key = pdb.GetBytes(32); alg.IV = pdb.GetBytes(16); var cs = new CryptoStream(ms, alg.CreateDecryptor(), CryptoStreamMode.Write); cs.Write(cipherBytes, 0, cipherBytes.Length); cs.Close(); var decryptedData = ms.ToArray(); return Encoding.Unicode.GetString(decryptedData); } } }
و نیز برای اعتبار سنجی عدد دریافتی از کاربر میتوان از عبارت با قاعده زیر استفاده کرد:
// Utils.cs using System.Text.RegularExpressions; namespace PersianCaptchaHandler { public class Utils { private static readonly Regex NumberMatch = new Regex(@"^([0-9]*|\d*\.\d{1}?\d*)$", RegexOptions.Compiled); public static bool IsNumber(string number2Match) { return NumberMatch.IsMatch(number2Match); } } }
<add verb="GET" path="/captcha/" type="PersianCaptchaHandler.CaptchaHandler, PersianCaptchaHandler, Version=1.0.0.0, Culture=neutral" />
<!-- ASPX --> <dl> <dt>تصویر امنیتی</dt> <dd> <asp:Image ID="imgCaptchaText" runat="server" AlternateText="CaptchaImage" /> <asp:HiddenField ID="hfCaptchaText" runat="server" /> <asp:ImageButton ID="btnRefreshCaptcha" runat="server" ImageUrl="/img/refresh.png" OnClick="btnRefreshCaptcha_Click" /> <br /> <asp:TextBox ID="txtCaptcha" runat="server" AutoCompleteType="Disabled"></asp:TextBox> </dd> <dd> <asp:Button ID="btnSubmit" runat="server" Text="ثبت" OnClick="btnSubmit_Click" /> </dd> </dl> <asp:Label ID="lblMessage" runat="server"></asp:Label>
protected void Page_Load(object sender, EventArgs e) { if (!Page.IsPostBack) SetCaptcha(); } private void SetCaptcha() { lblMessage.Text = txtCaptcha.Text = string.Empty; var newNumber = RandomGenerator.Next(100, 999) ; var farsiAlphabatic = NumberToString.ConvertIntNumberToFarsiAlphabatic(newNumber.ToString()); hfCaptchaText.Value = HttpUtility .UrlEncode( Encryptor.Encrypt( farsiAlphabatic ) ); txtCaptcha.Text = string.Empty; imgCaptchaText.ImageUrl = "/captcha/?text=" + hfCaptchaText.Value; }
private string GetCaptcha() { var farsiAlphabatic = NumberToString.ConvertIntNumberToFarsiAlphabatic(txtCaptcha.Text); var encryptedString = HttpUtility .UrlEncode( Encryptor.Encrypt( farsiAlphabatic ) ); return encryptedString; } private bool ValidateUserInputForLogin() { if (!Utils.IsNumber(txtCaptcha.Text)) { lblMessage.Text = "تصویر امنیتی را بطور صحیح وارد نکرده اید"; return false; } var strGetCaptcha = GetCaptcha(); var strDecodedVAlue = hfCaptchaText.Value; if (strDecodedVAlue != strGetCaptcha) { lblMessage.Text = "کلمه امنیتی اشتباه است"; SetCaptcha(); return false; } return true; } protected void btnSubmit_Click(object sender, EventArgs e) { if (!ValidateUserInputForLogin()) return; lblMessage.Text = "کلمه امنیتی درست است"; } protected void btnRefreshCaptcha_Click(object sender, ImageClickEventArgs e) { SetCaptcha(); }