مطالب
مسیریابی در Angular - قسمت دهم - Lazy loading
می‌خواهیم زمان نمایش اولین قالب برنامه را به حداقل برسانیم تا تاثیر روانی بهتری را بر روی کاربرانی که برنامه را اجرا می‌کنند، بگذاریم. برای این منظور در Angular، از Lazy loading استفاده می‌شود. همچنین این فریم ورک به همراه قابلیت پیش بارگذاری ماژول‌ها نیز هست تا سایر مسیرهای درخواستی را نیز با سرعت هرچه تمام‌تر نمایش دهد.
زمانیکه کاربری برنامه‌ی تک صفحه‌ای وب را در مرورگر باز می‌کند، ابتدا فایل index.html را در پاسخ دریافت خواهد کرد. این فایل تعاریف مداخل مورد نیاز برای رندر آن‌را مانند فایل‌های جاوا اسکریپت و CSS، به همراه دارد. سپس این فایل‌ها توسط مرورگر از سرور دریافت می‌شوند. در این حالت با پردازش این فایل‌ها، کامپوننت ریشه‌ی سایت بارگذاری می‌شود. پس از پایان آن، قالب این کامپوننت به کاربر نمایش داده خواهد شد. بنابر سرعت دریافت فایل‌ها توسط کاربر، این آغاز می‌تواند اندکی کند باشد. البته با رعایت نکات گفته‌ی شده‌ی در مطلب «Angular CLI - قسمت پنجم - ساخت و توزیع برنامه» می‌توان این حجم را توسط AoT و Tree-Shaking به میزان قابل ملاحظه‌ای کاهش داد. به علاوه با فعالسازی Lazy loading می‌توان قسمت‌های مختلف برنامه را تبدیل به یک سری Bundle کرد که در زمان درخواست، بارگذاری می‌شوند. به این ترتیب حجم فایل‌های ابتدایی که باید از سرور دریافت شوند بسیار کمتر شده و به علاوه با کاهش این حجم، مرورگر نیز باید میزان کمتری از کدها را در جهت نمایش اولین کامپوننت، پردازش و اجرا کند. در این حالت زمانیکه کاربری شروع به پیمایش مسیر یک ماژول خاص را می‌کند، آنگاه فایل‌های مرتبط با آن از سرور دریافت و در مرورگر پردازش می‌شوند. بنابراین اگر کاربری به قسمتی دسترسی ندارد، نیازی هم به دریافت فایل‌های آن نخواهد داشت؛ چون کار به فعالسازی مسیریابی آن ماژول نمی‌رسد.


آماده شدن جهت Lazy loading

پیش از Lazy loading یک قسمت از برنامه (که به آن async routing هم می‌گویند)، این قسمت باید دارای شرایطی باشد:
 - این قسمت از برنامه حتما باید در یک ماژول تعریف شده باشد. از این جهت که Lazy loading، لیست کامپوننت‌های قید شده‌ی در تعریف یک ماژول را بارگذاری می‌کند.
 - تمام مسیرهای این ماژول باید در ذیل یک مسیر والد، گروه بندی شده باشند. از این جهت که Lazy loading فقط بر روی مسیر ریشه‌ی والد تنظیم و بارگذاری می‌شود.
 - این ماژول نباید در هیچ ماژول دیگری import شده باشد. اگر این ماژول ارجاعی را در سایر ماژول‌ها داشته باشد، هیچ راهی بجز دریافت و کامپایل کامل آن توسط Angular وجود نخواهد داشت.


در مثال جاری این سری:
 - تمام ویژگی‌های قسمت مدیریت محصولات، داخل ماژول product.module.ts تعریف شده‌اند. بنابراین اولین شرط Lazy loading آن برقرار است.
 - در فایل product-routing.module.ts، کار گروه بندی مسیریابی‌ها ذیل یک والد مشخص انجام شده‌است (همان قسمت ششم این سری). بنابراین شرط دوم lazy loading این ماژول نیز پیشتر پیاده سازی شده‌است.
 - اما اگر به فایل src\app\app.module.ts مراجعه کنیم، ارجاعی به این ماژول در قسمت imports آن وجود دارد. بنابراین باید این ارجاع را حذف کنیم. در غیراینصورت کار دریافت کامل آن به همراه سایر ماژول‌های برنامه، در همان ابتدای کار صورت خواهد گرفت.
بنابراین در فایل src\app\app.module.ts، ابتدا import فایل آن‌را از ابتدای ماژول حذف و سپس ارجاع به نام کلاس کامپوننت ProductModule را نیز حذف می‌کنیم. در این حالت اگر از طریق منوی سایت سعی در دسترسی به این مسیرها کنیم، خطای 404 را دریافت خواهیم کرد؛ چون اکنون برنامه اطلاعاتی را در مورد نحوه‌ی مسیریابی قسمت محصولات برنامه، ندارد.

 
Lazy loading یک ماژول

برای بارگذاری غیرهمزمان یک ماژول و یا همان Lazy loading، می‌توان از خاصیت loadChildren تنظیمات مسیریابی، استفاده کرد:
{
   path: 'products',
   loadChildren:'app/product/product.module#ProductModule'
},
مقدار خاصیت loadChildren به صورت ذکر مسیر ماژول مرتبط به همراه یک # و سپس ذکر نام کلاس ماژول آن انجام می‌شود. مسیری هم که در اینجا ذکر می‌شود بر اساس محل قرارگیری فایل index.html، مقدار دهی شود.
با این تنظیم، زمانیکه مسیر ریشه‌ی produtcs درخواست شد، کار بارگذاری ماژول آن صورت گرفته و تنظیمات مسیریابی آن به سیستم اضافه می‌شود. به علاوه کار فعالسازی و نمایش کامپوننت آن را نیز انجام خواهد داد.

به همین منظور فایل src\app\app-routing.module.ts را گشوده و تنظیم فوق را به آن اضافه می‌کنیم:
const routes: Routes = [
  { path: 'home', component: WelcomeComponent },
  { path: 'welcome', redirectTo: 'home', pathMatch: 'full' },
  { path: 'products', loadChildren: 'app/product/product.module#ProductModule' },
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];
در این حالت اگر دستور ng serve -o را صادر کنید، خروجی آن اندکی متفاوت خواهد بود:
chunk    {0} polyfills.bundle.js, polyfills.bundle.js.map (polyfills) 165 kB {4} [initial]
chunk    {1} main.bundle.js, main.bundle.js.map (main) 32.7 kB {3} [initial] [rendered]
chunk    {2} styles.bundle.js, styles.bundle.js.map (styles) 129 kB {4} [initial]
chunk    {3} vendor.bundle.js, vendor.bundle.js.map (vendor) 2.72 MB [initial] [rendered]
chunk    {4} inline.bundle.js, inline.bundle.js.map (inline) 0 bytes [entry]
chunk    {5} 5.chunk.js, 5.chunk.js.map 51.1 kB {1} [rendered]
مورد {5} با فعالسازی lazy loading به لیست فایل‌های موجود اضافه شده‌است. این فایلی است که تنها درصورت درخواست مسیر نمایش لیست محصولات، توسط مرورگر دریافت خواهد شد و هیچ ارجاع مستقیمی به آن در فایل index.html تولیدی نهایی وجود ندارد.

به علاوه اگر در منوی سایت بر روی لینک نمایش لیست محصولات کلیک کنیم، هنوز خروجی نمایش داده نمی‌شود (هرچند خطای 404 را هم دریافت نمی‌کنیم). علت اینجا است که اگر به فایل src\app\product\product-routing.module.ts مراجعه کنیم، تعریف این مسیر ریشه، در این فایل نیز وجود دارد:
const routes: Routes = [
  {
    path: 'products',
    canActivate: [ AuthGuard ],
    children: [   ]
  }
];
بنابراین اکنون برای دسترسی به آن باید مسیر products/products را درخواست داد. به همین جهت، path و canActivate آن‌را حذف کرده و هر دو را به فایل src\app\app-routing.module.ts منتقل می‌کنیم:
import { AuthGuard } from './user/auth.guard';

const routes: Routes = [
  { path: 'home', component: WelcomeComponent },
  { path: 'welcome', redirectTo: 'home', pathMatch: 'full' },
  {
    path: 'products',
    loadChildren: 'app/product/product.module#ProductModule',
    canActivate: [AuthGuard]
  },
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];

یک نکته: اکنون تنظیمات مسیریابی فایل src\app\product\product-routing.module.ts چنین شکلی را پیدا کرده‌است:
const routes: Routes = [
  {
    path: '',
    component: ProductListComponent
  },
  {
    path: ':id',
    component: ProductDetailComponent,
    resolve: { product: ProductResolverService }
  },
  {
    path: ':id/edit',
    component: ProductEditComponent,
    resolve: { product: ProductResolverService },
    canDeactivate: [ProductEditGuard],
    children: [
      { path: '', redirectTo: 'info', pathMatch: 'full' },
      { path: 'info', component: ProductEditInfoComponent },
      { path: 'tags', component: ProductEditTagsComponent }
    ]
  }
];
تنظیمات مسیر والد به طور کامل حذف شده‌اند. به علاوه دیگر نیازی به ذکر خاصیت children آن نیست و تمام تنظیمات مسیریابی فرزندان، داخل [] اصلی قرار گرفته‌اند. همچنین دیگر نیازی به الحاق AuthGuard در ابتدای importهای این ماژول نیست؛ چون به فایل src\app\app-routing.module.ts منتقل شده‌است.

در این حالت اگر مسیر نمایش لیست محصولات را درخواست دهیم، مشاهده خواهیم کرد فایل 5.chunk.js که حاوی اطلاعات این ماژول است، به صورت مجزایی بارگذاری شده (lazy loading) و سپس با فعال شدن محافظ مسیر آن، صفحه‌ی لاگین نمایش داده می‌شود:


این بارگذاری با تاخیر و در صورت نیاز، به دو علت آغاز برنامه را سریعتر می‌کند:
الف) مرورگر اطلاعی از وجود فایل 5.chunk.js در ابتدای کار نداشته و آن‌را بارگذاری نمی‌کند (دریافت حجم کمتر، در آغاز نمایش برنامه).
ب) چون حجم کمتری از کدهای جاوا اسکریپت توسط مرورگر در آغاز کار دریافت می‌شود، کار پردازش و اجرای آن‌ها نیز بسیار سریعتر خواهد شد.


بررسی محافظ canLoad

تعدادی از محافظ‌های مسیرها را در قسمت قبل بررسی کردیم. هنگامیکه کامپوننت‌ها به صورت lazy loading فعالسازی شده و قالب آن‌ها نمایش داده می‌شوند، می‌توان از محافظ مسیر دیگری به نام canLoad نیز استفاده کرد و هدف از آن، بررسی منطقی، پیش از فعالسازی یک مسیر غیرهمزمان است. بنابراین اگر این محافظ false را برگرداند، حتی فایل‌های اسکریپت این ماژول، بارگذاری اولیه نیز نخواهد شد. به این ترتیب کسانیکه دسترسی به یک مسیر را نداشته باشند، فایل‌های اسکریپت متناظر با آن‌را نیز دریافت نخواهند کرد.

در مثال جاری، اگر به برگه‌ی network ابزار developer مرورگر دقت کنید، با درخواست نمایش مسیر لیست محصولات، ابتدا فایل js آن دریافت می‌شود که حاوی اطلاعات تمام کامپوننت‌ها و قالب‌های مرتبط با این مسیر است و سپس صفحه‌ی login نمایش داده خواهد شد. بنابراین اگر کاربر به این قسمت دسترسی نداشته باشد، فایل js آن بی‌جهت دریافت و بارگذاری شده‌است. برای بهبود این وضعیت می‌توان نمایش لاگین را پیش از بارگذاری فایل js این ماژول فعالسازی کرد و این مورد هدف اصلی محافظ canLoad است.

در ادامه برای تکمیل مثال جاری، می‌توان AuthGuard را طوری تنظیم کرد که علاوه بر پیاده سازی CanActivate، اینترفیس CanLoad را نیز پیاده سازی کند:
import { ActivatedRouteSnapshot, RouterStateSnapshot, CanActivate, Router, CanLoad, Route } from '@angular/router';

@Injectable()
export class AuthGuard implements CanActivate, CanLoad {

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    return this.checkLoggedIn(state.url);
  }

  canLoad(route: Route): boolean {
    return this.checkLoggedIn(route.path);
  }

 // … the same as before

}
همانطور که ملاحظه می‌کنید، متد canLoad بر خلاف متد canActivate دسترسی به سرویس‌های اطلاعات مسیریابی و وضعیت مسیریابی را ندارد؛ از این جهت که هنوز در این مرحله، ماژول درخواستی حاوی تنظیمات مسیریابی، بارگذاری و فعالسازی نشده‌است.

مرحله‌ی بعد، تغییر فایل src\app\app-routing.module.ts و جایگزین کردن تعریف فعلی canActivate با canLoad است:
  {
    path: 'products',
    loadChildren: 'app/product/product.module#ProductModule',
    canLoad: [AuthGuard]
  },
پس از این تغییر، برنامه را مجدا اجرا کرده و صفحه را refresh کنید. سپس برگه‌ی network ابزار developers را نیز باز نگه دارید. اکنون بر روی لینک نمایش لیست محصولات کلیک کنید. مشاهده خواهید کرد که در این حالت صفحه‌ی لاگین، بدون بارگذاری ماژول Js ایی نمایش داده می‌شود. در ادامه اگر لاگین کنیم، آنگاه فایل js این ماژول توسط مرورگر دریافت شده و بارگذاری می‌شود.


پیش بارگذاری ماژول‌ها

با فعالسازی lazy loading، ماژول‌های مورد نیاز کاربر دیگر به همراه فایل‌های js ابتدایی برنامه که در فایل index.html ارجاع مستقیمی به آن‌ها دارند، ارائه نمی‌شوند و تنها در صورت درخواست مشاهده‌ی مسیری، کار بارگذاری آن‌ها توسط برنامه صورت خواهد گرفت. همین مساله می‌تواند در بار اول نمایش این ماژول‌ها تاخیر کوتاهی را سبب شود. به همین جهت قابلیت پیش بارگذاری ماژول‌ها نیز در سیستم مسیریاب Angular پیش بینی شده‌است. به این قابلیت preloading و یا eager lazy loading نیز می‌گویند. در این حالت برنامه در پشت صحنه، کار پیش واکشی ماژول‌ها را انجام می‌دهد و زمانیکه کاربری مسیری را درخواست می‌دهد، آْن مسیر را بدون درنگ مشاهده خواهد کرد.
بدیهی است این قابلیت نباید برای ماژول‌هایی که قرار است توسط کاربرانی خاص مشاهده شوند فعال شود و هدف آن دسترسی سریع به ماژول‌های پرکاربرد برنامه‌است.

در اینجا سه استراتژی پیش بارگذاری ماژول‌ها میسر است:
 - No preloading که حالت پیش فرض است.
 - Preload all سبب پیش بارگذاری تمام قسمت‌های lazy load برنامه می‌شود.
 - Custom که اجازه‌ی تعریف یک استراتژی سفارشی را می‌دهد.

برای مثال برای فعالسازی حالت Preload all، باید به فایل src\app\app-routing.module.ts مراجعه کرده و تغییرات ذیل را اعمال کنیم:
import { Routes, RouterModule, PreloadAllModules } from '@angular/router';

@NgModule({
  imports: [RouterModule.forRoot(
    routes,
    { enableTracing: true, preloadingStrategy: PreloadAllModules  /*, useHash: true*/ }
  )],
در اینجا نحوه‌ی تنظیم preloadingStrategy را به PreloadAllModules مشاهده می‌کنید. در این حالت پس از آغاز ابتدایی برنامه، مسیریاب بلافاصله تمام مسیرهای lazy load را در پشت صحنه بارگذاری می‌کند.

یک نکته: وجود محافظ canLoad، هر نوع استراتژی prealoading را غیرفعال می‌کند. اما prealoading با سایر انواع محافظ‌ها کار می‌کند.
بنابراین برای آزمایش تنظیم  preloadingStrategy: PreloadAllModules، تعریف canLoad را به canActivate تغییر دهید.


تعریف استراتژی‌های سفارشی پیش بارگذاری ماژول‌ها

اگر نیاز به یک استراتژی پیش بارگذاری بهتر از هیچ یا همه باشد، می‌توان یک استراتژی سفارشی را نیز تدارک دید و ایجاد آن سه مرحله‌ی ایجاد سرویس مرتبط، ثبت آن سرویس در ماژول و در آخر تنظیم مسیریابی را به همراه دارد.
برای این منظور ابتدا دستور ذیل را صادر کنید تا قالب ابتدایی سرویس SelectiveStrategy ایجاد شود:
 >ng g s SelectiveStrategy -m app.module
که سبب تولید و به روز رسانی فایل‌های ذیل در پوشه‌ی src\app خواهد شد (چون مرتبط است به کل برنامه):
 installing service
  create src\app\selective-strategy.service.spec.ts
  create src\app\selective-strategy.service.ts
  update src\app\app.module.ts
در این حالت لیست providers فایل app.module.ts نیز به صورت خودکار تکمیل می‌گردد.

سپس کدهای SelectiveStrategyService را به نحو ذیل تغییر دهید:
import { Injectable } from '@angular/core';
import { Route, PreloadingStrategy } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';

@Injectable()
export class SelectiveStrategyService implements PreloadingStrategy {

  preload(route: Route, load: Function): Observable<any> {
    if (route.data && route.data['preload']) {
      return load();
    }
    return Observable.of(null);
  }
}
- این سرویس ویژه باید اینترفیس PreloadingStrategy را پیاده سازی کند. سپس باید متد اجباری preload آن‌را افزود و تکمیل نمود.
- پارامتر اول این متد، اطلاعاتی را در مورد مسیر جاری در اختیار ما قرار می‌دهد و دومین پارامتر آن متدی است که کار preloading را انجام می‌دهد.
- در اینجا است که تصمیم می‌گیریم ماژولی را preload کنیم یا خیر. برای نمونه در اینجا از خاصیت data مسیریابی استفاده شده‌است. این خاصیت نیز به یک مقدار ثابت اشاره می‌کند (قسمت «ارسال اطلاعات ثابت به مسیرهای مختلف برنامه» قسمت چهارم). برای مثال نام دلخواه آن‌را preload گذاشته‌ایم و اگر مقدار آن به true تنظیم شده بود، آنگاه این مسیر preload خواهد شد. فراخوانی متد load در اینجا به معنای preloading این مسیر است. در غیراینصورت null را بازگشت می‌دهیم.


در ادامه نیاز است در فایل src\app\app-routing.module.ts، بجای معرفی PreloadAllModules، این استراتژی سفارشی خود را معرفی کرد:
import { SelectiveStrategyService } from './selective-strategy.service';

@NgModule({
  imports: [RouterModule.forRoot(
    routes,
    { enableTracing: true, preloadingStrategy: SelectiveStrategyService
     /*, preloadingStrategy: PreloadAllModules*/  /*, useHash: true*/ }
  )],
و همچنین تعریف مسیریابی برنامه به این صورت تغییر می‌کند:
  {
    path: 'products',
    loadChildren: 'app/product/product.module#ProductModule',
    //canLoad: [AuthGuard] 
    canActivate: [AuthGuard],
    data: { preload: true }
  },
در اینجا نحوه‌ی مقدار دهی خاصیت data را به اطلاعات ثابت preload: true مشاهده می‌کنید. این اطلاعاتی است که در سرویس SelectiveStrategy سفارشی ما بررسی شده و بر اساس آن در مورد پیش بارگذاری این مسیر تصمیم‌گیری می‌شود.

برای آزمایش آن، برنامه را مجدا اجرا کرده و صفحه را refresh کنید. سپس برگه‌ی network ابزار developers را نیز باز نگه دارید. مشاهده خواهید کرد که علاوه بر فایل‌های js اصلی برنامه که در فایل index.html ارجاعی را دارند، فایل 5.chunk.js نیز پیش بارگذاری شده‌است.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-routing-lab-09.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng s -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت هشتم - دریافت اطلاعات از سرور
اغلب برنامه‌های AngularJS 2.0، اطلاعات خود را از طریق پروتکل HTTP، از سرور دریافت می‌کنند. برنامه یک درخواست Get را صادر کرده و سپس سرور پاسخ مناسبی را ارائه می‌دهد.


مقدمه‌ای بر RxJS

اگر به پیشنیازهای نصب AngularJS 2.0 در قسمت اول این سری دقت کرده باشید، یکی از موارد آن، RxJS است:
"dependencies": {
    "rxjs": "5.0.0-beta.2"
 },
یک Observable، آرایه‌ای است که اعضای آن به صورت غیر همزمان (asynchronously) در طول زمان دریافت می‌شوند. برای مثال پس از شروع یک عملیات async، ابتدا عنصر اول آرایه دریافت می‌شود، پس از مدتی عنصر دوم و در آخر عنصر سوم آن. به همین جهت از Observable‌ها برای مدیریت داده‌های async مانند دریافت اطلاعات از یک وب سرور، استفاده می‌شود.
قرار است Observableها به ES 2016 یا نگارش پس از ES 6 اضافه شوند و یکی از پیشنهادات آن هستند. اما هم اکنون AngularJS 2.0 از این امکان، توسط یک کتابخانه‌ی ثالث، به نام reactive extensions یا Rx، استفاده می‌کند. از RxJS در سرویس HTTP و همچنین مدیریت سیستم رخدادهای AngularJS 2.0 استفاده می‌شود. Observableها امکانی را فراهم می‌کنند تا به ازای دریافت هر اطلاعات async از سرور، بتوان توسط رخداد‌هایی از وقوع آن‌ها مطلع شد.

در نگارش قبلی AngularJS از Promises برای مدیریت اعمال غیرهمزمان استفاده می‌شد. Observableها تفاوت‌های قابل ملاحظه‌ای با Promises دارند:
- یک Promise تنها یک مقدار، یا خطا را بر می‌گرداند؛ اما یک Observable چندین مقدار را در طول یک بازه‌ی زمانی باز می‌گرداند.
- برخلاف Promises، می‌توان عملیات یک Observable را لغو کرد.
- Observableها از عملگرهایی مانند map، reduce، filter و غیره نیز پشتیبانی می‌کنند.

البته باید عنوان کرد که هنوز هم می‌توان از Promises در صورت تمایل در AngularJS 2.0 نیز استفاده کرد.


تنظیمات اولیه‌ی کار با RxJS در AngularJS 2.0

برای استفاده از RxJS در AngularJS 2.0، مراحلی مانند افزودن مدخل اسکریپت http.dev.js، ثبت پروایدر HTTP و importهای لازم، باید طی شوند که در ادامه آن‌ها را بررسی خواهیم کرد:
الف) سرویس HTTP جزئی از angular2/core نیست. به همین جهت مدخل اسکریپت متناظر با آن، باید به صفحه‌ی اصلی سایت اضافه شود که این مورد، در قسمت اول بررسی پیشنیازهای نصب AngularJS 2.0 صورت گرفته‌است:
 <!-- Required for http -->
<script src="~/node_modules/angular2/bundles/http.dev.js"></script>
این تعریف در فایل Views\Shared\_Layout.cshtml (و یا index.html) پروژه‌ی جاری موجود است. همچنین در این صفحه، مدخل Rx.js نیز ذکر شده‌است.

ب) اکنون فایل app.component.ts را گشوده و سرویس HTTP را به آن اضافه می‌کنیم. با نحوه‌ی ثبت سرویس‌ها در قسمت قبل آشنا شدیم:
import { Component } from 'angular2/core';
import { HTTP_PROVIDERS } from 'angular2/http';
import 'rxjs/Rx';   // Load all features
 
import { ProductListComponent } from './products/product-list.component';
import { ProductService } from './products/product.service';
 
@Component({
    selector: 'pm-app',
    template:`
    <div><h1>{{pageTitle}}</h1>
        <pm-products></pm-products>
    </div>
    `,
    directives: [ProductListComponent],
    providers: [
        ProductService,
        HTTP_PROVIDERS
    ]
})
export class AppComponent {
    pageTitle: string = "DNT AngularJS 2.0 APP";
}
از آنجائیکه می‌خواهیم سرویس HTTP، در تمام کامپوننت‌های برنامه در دسترس باشد، آن‌را در بالاترین سطح سلسه مراتب کامپوننت‌های موجود، یا همان کامپوننت ریشه‌ی سایت، ثبت و معرفی می‌کنیم. بنابراین سرویس توکار HTTP یا HTTP_PROVIDERS به لیست پروایدرها، اضافه شده‌است.

ج) پس از آن نیاز است importهای متناظر نیز به ابتدای ماژول فعلی، جهت شناسایی این سرویس و همچنین امکانات rx.js اضافه شوند.
تعریف 'import 'rxjs/Rx به این شکل، به module loader اعلام می‌کند که این کتابخانه را بارگذاری کن، اما چیزی را import نکن. هنگامیکه این کتابخانه بارگذاری می‌شود، کدهای جاوا اسکریپتی آن اجرا شده و سبب می‌شوند که عملگرهای ویژه‌ی Observable آن مانند map و filter نیز در دسترس برنامه قرار گیرند.


ساخت یک سرویس سمت سرور بازگشت لیست محصولات به صورت JSON

چون در ادامه می‌خواهیم لیست محصولات را از سرور دریافت کنیم، برنامه‌ی ASP.NET MVC فعلی را اندکی تغییر می‌دهیم تا این لیست را به صورت JSON بازگشت دهد.
بنابراین ابتدا کلاس مدل محصولات را به نحو ذیل به پوشه‌ی Models اضافه کرده:
namespace MVC5Angular2.Models
{
    public class Product
    {
        public int ProductId { set; get; }
        public string ProductName { set; get; }
        public string ProductCode { set; get; }
        public string ReleaseDate { set; get; }
        public decimal Price { set; get; }
        public string Description { set; get; }
        public double StarRating { set; get; }
        public string ImageUrl { set; get; }
    }
}
و سپس اکشن متد Products، لیست محصولات فرضی این سرویس را بازگشت می‌دهد:
using System.Collections.Generic;
using System.Text;
using System.Web.Mvc;
using MVC5Angular2.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
 
namespace MVC5Angular2.Controllers
{
    public class HomeController : Controller
    {
        // GET: Home
        public ActionResult Index()
        {
            return View();
        }
 
        public ActionResult Products()
        {
            var products = new List<Product>
            {
               new Product
               {
                    ProductId= 2,
                    ProductName= "Garden Cart",
                    ProductCode= "GDN-0023",
                    ReleaseDate= "March 18, 2016",
                    Description= "15 gallon capacity rolling garden cart",
                    Price= (decimal) 32.99,
                    StarRating= 4.2,
                    ImageUrl= "app/assets/images/garden_cart.png"
               },
               new Product
               {
                    ProductId= 5,
                    ProductName= "Hammer",
                    ProductCode= "TBX-0048",
                    ReleaseDate= "May 21, 2016",
                    Description= "Curved claw steel hammer",
                    Price= (decimal) 8.9,
                    StarRating= 4.8,
                    ImageUrl= "app/assets/images/rejon_Hammer.png"
               }
            };
 
            return new ContentResult
            {
                Content = JsonConvert.SerializeObject(products, new JsonSerializerSettings
                {
                    ContractResolver = new CamelCasePropertyNamesContractResolver()
                }),
                ContentType = "application/json",
                ContentEncoding = Encoding.UTF8
            };
        }
    }
}
در اینجا از JSON.NET جهت بازگشت camel case نام خواص مورد نیاز در سمت کاربر، استفاده شده‌است.
برای مطالعه‌ی بیشتر:
«استفاده از JSON.NET در ASP.NET MVC»
«تنظیمات و نکات کاربردی کتابخانه‌ی JSON.NET»

به این ترتیب، آدرس http://localhost:2222/home/products، خروجی JSON سرویس لیست محصولات را در مثال جاری، ارائه می‌دهد.


ارسال یک درخواست HTTP به سرور توسط AngularJS 2.0

اکنون پس از تنظیمات ثبت و معرفی سرویس HTTP و همچنین برپایی یک سرویس سمت سرور ارائه‌ی لیست محصولات، می‌خواهیم سرویس ProductService را که در قسمت قبل ایجاد کردیم (فایل product.service.ts)، جهت دریافت لیست محصولات از سمت سرور، تغییر دهیم:
import { Injectable } from 'angular2/core';
import { IProduct } from './product';
import { Http, Response } from 'angular2/http';
import { Observable } from 'rxjs/Observable';
 
@Injectable()
export class ProductService {
    private _productUrl = '/home/products';
 
    constructor(private _http: Http) { }
 
    getProducts(): Observable<IProduct[]> {
        return this._http.get(this._productUrl)
                         .map((response: Response) => <IProduct[]>response.json())
                         .do(data => console.log("All: " + JSON.stringify(data)))
                         .catch(this.handleError);
    }
 
    private handleError(error: Response) {
        console.error(error);
        return Observable.throw(error.json().error || 'Server error');
    }
}
از آنجائیکه این سرویس دارای یک وابستگی تزریق شده‌است، ذکر ()Injectable@، پیش از تعریف نام کلاس، ضروری است.
در سازنده‌ی کلاس ProductService، کار تزریق وابستگی سرویس Http انجام شده‌است. به این ترتیب با استفاده از متغیر خصوصی http_، می‌توان در کلاس جاری به امکانات این سرویس دسترسی یافت (همان «تزریق سرویس‌ها به کامپوننت‌ها» در قسمت قبل).
سپس متد get آن، یک درخواست HTTP از نوع GET را به آدرس مشخص شده‌ی در متغیر productUrl_ ارسال می‌کند (یا همان سرویس سمت سرور برنامه).
سرویس Http و همچنین شیء Response آن در ماژول‌های Http و Response قرار دارند که در ابتدای صفحه import شده‌اند.

متد http get یک Observable را بازگشت می‌دهد که در نهایت خروجی این متد نیز به همان <[]Observable<IProduct، تنظیم شده‌است. Observable یک شیء جنریک است و در اینجا نوع آن، آرایه‌ای از محصولات درنظر گرفته شده‌است.
اکنون که امضای این متد تغییر یافته است (پیش از این صرفا یک آرایه‌ی ساده از محصولات بود)، استفاده کننده (در کلاس ProductListComponent) باید به تغییرات آن از طریق متد subscribe گوش فرا دهد.
فعلا در کلاس جاری، پس از پایان کار دریافت اطلاعات از سرور، اطلاعات نهایی در متد map در دسترس قرار می‌گیرد (که یکی از عملگرهای RxJs است). کار متد map، اصطلاحا projection است. این متد، هر عضو دریافتی از خروجی سرور را به فرمتی جدید نگاشت می‌کند.
هر درخواست HTTP، در اصل یک عملیات async است. یعنی در اینجا توالی که در اختیار Observable ما قرار می‌گیرد، تنها یک المان دارد که همان شیء HTTP Response است.
بنابراین کار متد map فوق، تبدیل شیء خروجی از سرور، به آرایه‌ای از محصولات است.
در اینجا یک سری کدهای مدیریت استثناءها را نیز در صورت بروز مشکلی می‌توان تعریف کرد. برای مثال در اینجا متد catch، کار پردازش خطاهای رخ داده را انجام می‌دهد.
از متد do جهت لاگ کردن عملیات رخ داده و داده‌های دریافتی در کنسول developer tools مرورگرها استفاده شده‌است.

یک نکته:
اگر خروجی JSON از سرور، برای مثال داخل خاصیتی به نام data محصور شده بود، بجای ()response.json می‌بایستی از response.json().data استفاده می‌شد.


گوش فرا دادن به Observable دریافتی از سرور

تا اینجا یک درخواست HTTP GET را به سمت سرور ارسال کردیم و خروجی آن به صورت Observable در اختیار ما است. اکنون نیاز است کدهای ProductListComponent را جهت گوش فرا دادن به این Observable تغییر دهیم. برای این منظور فایل product-list.component.ts را گشوده و تغییرات ذیل را به آن اعمال کنید:
errorMessage: string;
ngOnInit(): void {
    //console.log('In OnInit');
    this._productService.getProducts()
                        .subscribe(
                              products => this.products = products,
                              error => this.errorMessage = <any>error);
}
در کلاس ProductListComponent، در متد ngOnInit که در آن کار آغاز و مقدار دهی وابستگی‌های کامپوننت انجام می‌شود، متد ()productService.getProducts_ فراخوانی شده‌است. این متد یک Observable را بر می‌گرداند. بنابراین برای پردازش نتیجه‌ی آن نیاز است متد subscribe را در ادامه‌ی آن، زنجیر وار ذکر کرد.
اولین پارامتر متد subscribe، کار دریافت نتایج حاصل را به عهده دارد. برای مثال اگر حاصل عملیات در طی سه مرحله صورت گیرد، سه بار نتیجه‌ی دریافتی را می‌توان در اینجا پردازش کرد. البته همانطور که عنوان شد، یک عملیات غیرهمزمان HTTP، تنها در طی یک مرحله، HTTP Response را دریافت می‌کند؛ بنابراین، پارامتر اول متد subscribe نیز تنها یکبار اجرا می‌شود. در اینجا فرصت خواهیم داشت تا آرایه‌ی دریافتی حاصل از متد map قسمت قبل را به خاصیت عمومی products کلاس جاری نسبت دهیم.
پارامتر دوم متد subscribe در صورت شکست عملیات فراخوانی می‌شود. در اینجا حاصل آن به خاصیت جدید errorMessage نسبت داده شده‌است.


اکنون برنامه را مجددا اجرا کنید، هنوز باید لیست محصولات، مانند قبل نمایش داده شود.


یک نکته
اگر برنامه را اجرا کردید و خروجی مشاهده نشد، به کنسول developer tools مرورگر مراجعه کنید؛ احتمالا خطای ذیل در آن درج شده‌است:
 EXCEPTION: No provider for Http!
به این معنا که پروایدر HTTP یا همان HTTP_PROVIDERS، جایی معرفی نشده‌است. البته مشکلی از این لحاظ در برنامه وجود ندارد و این پروایدر در بالاترین سطح ممکن و در فایل app.component.ts ثبت شده‌است. مشکل اینجا است که مرورگر، فایل قدیمی http://localhost:2222/app/app.component.js را کش کرده‌است (به همراه تمام اسکریپت‌های دیگر) و این فایل قدیمی، فاقد تعریف سرویس HTTP است. بنابراین با حذف کش مرورگر و دریافت فایل‌های js جدید، مشکل برطرف خواهد شد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: MVC5Angular2.part8.zip


خلاصه‌ی بحث

برای کار با سرور و ارسال درخواست‌های HTTP، ابتدا نیاز است مدخل تعریف http.dev.js به index.html اضافه شود و سپس HTTP_PROVIDERS را در بالاترین سطح کامپوننت‌های تعریف شده، ثبت و معرفی کرد. پس از آن نیاز است RxJs را نیز import کرد. در ادامه، سرویس دریافت لیست محصولات، وابستگی سرویس HTTP را توسط سازنده‌ی خود دریافت کرده و از آن برای صدور یک فرمان HTTP GET استفاده می‌کند. سپس با استفاده از متد map، کار نگاشت شیء Response دریافتی، به فرمت مناسب مدنظر، صورت می‌گیرد.
در ادامه هر کلاسی که نیاز دارد با این کلاس سرویس دریافت اطلاعات کار کند، متد subscribe را فراخوانی کرده و نتیجه‌ی عملیات را پردازش می‌کند.
مطالب
فرم‌های مبتنی بر قالب‌ها در Angular - قسمت چهارم - اعتبارسنجی ورودی‌ها
پس از برقراری ارتباط بین فرم و مدل آن در قسمت قبل، مرحله‌ی بعدی طراحی یک فرم خوب، اعتبارسنجی ورودی‌های کاربران است و واکنش نشان دادن به ورودی‌های نامطلوب.


کلاس‌های CSS اعتبارسنجی در Angular

زمانیکه Angular فرمی را تحت نظر قرار می‌دهد، کلاس‌های CSS خاصی را نیز بر اساس حالات عناصر مختلف آن، به آن‌ها متصل خواهد کرد. بر این اساس می‌توان ظاهر این المان‌ها را سفارشی سازی نمود. این کلاس‌ها به شرح زیر هستند:

  کلاس CSS اعتبارسنجی  توضیحات 
  ng-untouched   زمانیکه فرمی برای بار اول رندر می‌شود، تمام فیلدهای آن با کلاس CSS ایی به نام ng-untouched علامتگذاری می‌شوند. 
  ng-touched   همینقدر که کاربر با یک Tab از فیلدی عبور کند، با کلاس ng-touched مزین خواهد شد. بنابراین مهم نیست که حتما داده‌ای وارد شده باشد یا خیر. حتی عبور از یک فیلد نیز به معنای لمس آن نیز می‌باشد. 
 ng-pristine   مربوط به زمانی‌‌است که یک فیلد نه تغییر کرده‌است و نه لمس شده‌است. 
  ng-dirty   همینقدر که کاربر، تغییری را در فیلدی ایجاد کند، آن المان با کلاس ng-dirty مشخص خواهد شد. 
 ng-valid   برای حالت موفقیت آمیز بودن اعتبارسنجی، به آن المان انتساب داده می‌شود. 
  ng-invalid   برای حالت غیر موفقیت آمیز بودن اعتبارسنجی، به آن المان انتساب داده می‌شود. 

برای اینکه بتوانیم این موارد را در عمل مشاهده کنیم، به ابتدای فرم مثال این سری، تغییرات ذیل را اعمال خواهیم کرد:
<div class="form-group">
    <label>First Name</label>
    <input #firstName required type="text" class="form-control" name="firstName" [(ngModel)]="model.firstName">
</div>

<h3>Classes</h3>
<h4>{{ firstName.className }}</h4>
برای اینکه مشخص کنیم چه کلاسی به المان firstName متصل شده‌است، ابتدا نیاز است یک template reference variable را برای آن تعریف کنیم که اینکار را توسط معرفی firstName# انجام داده‌ایم. به این ترتیب است که می‌توان به خاصیت className آن در ادامه دسترسی یافت.


تصویر فوق کلاس‌هایی را نمایش می‌دهد که در اولین بار نمایش فرم، به المان firstName متصل شده‌اند. برای مثال در این حالت کلاس ng-pristine قابل مشاهده‌است و هنوز تغییری در آن حاصل نشده‌است.
در ادامه اگر حرفی را به آن اضافه کنیم:


هنوز هم ng-untouched آن برقرار است؛ اما ng-pristine آن به ng-dirty تبدیل شده‌است. در اینجا حتی اگر کل اطلاعات فیلد را نیز حذف کنیم و آن‌را خالی کنیم یا به حالت اول بازگردانیم نیز کلاس ng-dirty قابل مشاهده‌است. بنابراین اگر حالت فیلدی dirty شد، همواره به همین حالت باقی می‌ماند.
در این لحظه اگر با Tab به فیلد دیگری در فرم مراجعه کنیم:


در اینجا است که کلاس ng-untouched به ng-touched تبدیل می‌شود. بنابراین کلاس‌های مختلف لمس یک فیلد، ارتباطی به افزوده شدن یا حذف کاراکتری از یک فیلد ندارند و فقط به از دست رفتن focus و مراجعه‌ی به فیلد دیگری مرتبط می‌شوند.

اگر به المان تغییر یافته‌ی فوق دقت کنید، ویژگی required نیز به آن اضافه شده‌است (علاوه بر template reference variable ایی که تعریف کردیم). در این حالت کل فیلد را خالی کنید:


همانطور که مشاهده می‌کنید، اکنون کلاس ng-valid به کلاس ng-invalid تغییر یافته‌است.


ارتباط بین کلاس‌های CSS اعتبارسنجی و خواص ngModel

تمام کلاس‌های -ng ایی که در بالا معرفی شدند، معادل‌های خواص ngModel ایی نیز دارند. فقط کافی است -ng آن‌ها را حذف کنید، باقیمانده‌ی آن، نام خاصیت متناظری در ngModel خواهد بود. برای مثال کلاس ng-untouched به خاصیت untouched نگاشت می‌شود و به همین ترتیب برای مابقی.
template reference variable ایی را که تا به اینجا به المان اضافه کردیم (firstName#) به خواص همان المان دسترسی دارد (مانند className آن). اما در ادامه می‌خواهیم این متغیر به ngModel و خواص آن دسترسی داشته باشد و میدان دید آن تغییر کند. به همین جهت تنها کافی است تا ngModel را به این متغیر انتساب دهیم:
<div class="form-group">
    <label>First Name</label>
    <input #firstName="ngModel" required type="text" class="form-control" name="firstName" [(ngModel)]="model.firstName">
</div>

<h4>dirty: {{ firstName.dirty }}</h4>
پس از این تغییر اکنون template reference variable تعریف شده می‌تواند برای نمونه به خاصیت dirty شیء ngModel دسترسی پیدا کند.


یک نکته: در حالت خواص valid و invalid که مرتبط با اعتبارسنجی هستند، خاصیت سومی نیز به نام errors وجود دارد که حاوی اطلاعات بیشتری در مورد خطای اعتبارسنجی رخ داده‌است. بنابراین وجود این خاصیت و نال نبودن آن نیز دلالت بر وجود یک خطای اعتبارسنجی است. از خاصیت errors در ادامه‌ی بحث در قسمت «مدیریت چندین خطای همزمان اعتبارسنجی» استفاده خواهیم کرد.


نمایش بهتر خطاهای اعتبارسنجی با بررسی خواص ngModel

یکی از مزایای کار با خواص ngModel، امکان استفاده‌ی از آن‌ها در عبارات شرطی‌است که نسبت به کلاس‌های CSS معرفی شده‌ی در ابتدای بحث، انعطاف پذیری بیشتری را به همراه خواهند داشت.
<div class="form-group">
    <label>First Name</label>
    <input #firstName="ngModel" #firstNameElement required type="text" class="form-control" 
              name="firstName" [(ngModel)]="model.firstName">
    <div *ngIf="firstName.invalid && firstName.touched" class="alert alert-danger">
        First Name is required.
    </div>      
</div>

<h4>className: {{ firstNameElement.className }}</h4>
<h4>dirty: {{ firstName.dirty }}</h4>
<h4>invalid: {{ firstName.invalid }}</h4>
توسط ngIf می‌توان المانی را به DOM اضافه و یا کلا از آن حذف کرد. در اینجا یک عبارت boolean به آن نسبت داده شده‌است. ابتدا حالت firstName.invalid را بررسی کنید. مشاهده خواهید کرد که اگر فرم برای بار اول و با مقادیر خالی نمایش داده شود، div خطا نیز ظاهر می‌شود که آنچنان خوشایند نیست و بهتر است خطاها را پس از اینکه کاربر مشغول به کار با فرم شد، به او نمایش دهیم؛ تا اینکه از همان ابتدا این خطاها به صورت واضحی نمایش داده شوند. بنابراین && firstName.touched نیز در اینجا اضافه شده‌است. به این ترتیب div نمایش دهنده‌ی alert بوت استرپ، دیگر در اولین بار نمایش یک فرم خالی، ظاهر نخواهد شد. اما اگر کاربر با یک tab از فیلدی خالی رد شد، آنگاه این خطا نمایش داده می‌شود.



نمایش بهتر خطاهای اعتبارسنجی با مزین ساختن المان‌های ورودی

علاوه بر نمایش یک alert بوت استرپی متناظر با یک فیلد غیرمعتبر، می‌توان خود المان‌های ورودی را نیز با شیوه‌نامه‌هایی مزین ساخت.


این کار را در بوت استرپ با افزودن کلاس has-error در کنار form-group انجام می‌دهند. همچنین label نیز باید به کلاس control-label مزین شود تا hass-error بر روی آن نیز تاثیرگذار شود. برای پیاده سازی پویای آن در Angular به روش ذیل عمل می‌شود:
<div class="form-group" [class.has-error]="firstName.invalid && firstName.touched">
   <label class="control-label">First Name</label>
در اینجا روش افزودن شرطی کلاس ویژه‌ی has-error بوت استرپ را به مجموعه کلاس‌های div جاری ملاحظه می‌کنید. هر زمان که شرط ذکر شده برقرار باشد، در عبارت property binding مخصوص class.className، این className را به صورت خودکار به مجموعه کلاس‌های آن المان اضافه می‌کند و برعکس.
بنابراین اگر المان firstName خالی باشد و همچنین با یک Tab از روی آن عبور کرده باشیم، کلاس has-error در کنار کلاس form-group اضافه می‌شود.

روش دوم: همانطور که در ابتدای بحث نیز عنوان شد، Angular بر اساس حالات مختلف یک فیلد، کلاس‌های CSS خاصی را به آن‌ها انتساب می‌دهد. یک چنین کاری را با مقدار دهی این کلاس‌ها در فایل src\styles.css نیز می‌توان انجام داد که دقیقا معادل بررسی خواص invalid و touched با کدنویسی است:
.ng-touched.ng-invalid{
    border: 1px solid red;
}


سایر ویژگی‌های اعتبارسنجی HTML 5

تا اینجا ویژگی استاندارد required را به المان ورودی فرم ثبت اطلاعات کاربران، اضافه کردیم.
<input #firstName="ngModel" #firstNameElement 
required maxlength="3" minlength="2" pattern="^V.*"
type="text" class="form-control" name="firstName" [(ngModel)]="model.firstName">
در اینجا برای مثال اعمال ویژگی‌های maxlength، minlength و pattern را مشاهده می‌کنید. ویژگی pattern برای تعریف عبارات باقاعده بکار می‌رود (برای مثال، نام حتما باید با V شروع شود) و تقریبا در تمام مرورگرها (caniuse.com ) نیز پشتیبانی می‌شود.
برای نمونه minlength همه‌جا پشتیبانی نمی‌شود؛ اما آن‌را می‌توان برای مثال با الگویی مساوی "+..." جایگزین کرد.


مشکل! ذکر چند ویژگی اعتبارسنجی با هم، تداخل ایجاد می‌کنند!

در اینجا چون چهار ویژگی مختلف را به صورت یکجا به یک المان متصل کرده‌ایم، اکنون div ذیل به هر کدام از این ویژگی‌ها به صورت یکسانی واکنش نشان خواهد داد؛ زیرا خاصیت invalid را true می‌کنند:
    <div *ngIf="firstName.invalid && firstName.touched" class="alert alert-danger">
        First Name is required.
    </div>
روش مدیریت این حالت، به صورت ذیل است:
    <div class="form-group" [class.has-error]="firstName.invalid && firstName.touched">
      <label class="control-label">First Name</label>
      <input #firstName="ngModel" #firstNameElement 
             required maxlength="3" minlength="2" pattern="^V.*"
             type="text" class="form-control" name="firstName" [(ngModel)]="model.firstName">
      <div *ngIf="firstName.invalid && firstName.touched">
        <div class="alert alert-info">
           errors: {{ firstName.errors | json }}
        </div> 
        <div class="alert alert-danger" *ngIf="firstName.errors.required">
           Name is required.
        </div>
        <div class="alert alert-danger" *ngIf="firstName.errors.minlength">
           Name should be minimum {{firstName.errors.minlength.requiredLength}} characters.
        </div>
        <div class="alert alert-danger" *ngIf="firstName.errors.maxlength">
           Name should be max {{firstName.errors.maxlength.requiredLength}} characters.
        </div>
        <div class="alert alert-danger" *ngIf="firstName.errors.pattern">
           Name pattern: {{firstName.errors.pattern.requiredPattern}}
        </div>
      </div>      
    </div>
با یک چنین خروجی:


همانطور که در تصویر ملاحظه می‌کنید، محتوای خاصیت errors به صورت JSON، جهت دیباگ نیز درج شده‌است.
بنابراین وجود خاصیت firstName.errors.minlength و یا firstName.errors.pattern به این معنا است که این خطاهای خاص وجود دارند (خاصیت firstName.errors به همراه اضافاتی است) و برعکس نال بودن آن‌ها مؤید عدم وجود خطایی است. به همین جهت می‌توان به ازای هر کدام، یک div جداگانه را تشکیل داد.
مرحله‌ی بعد، استخراج اطلاعات بیشتری از همین شیء و محتوای خاصیت errors است. برای مثال زمانیکه در آن خاصیت minlength ظاهر شد، این خاصیت نیز دارای خاصیتی مانند requiredLength است که از آن می‌توان جهت درج عدد واقعی مورد نیاز این اعتبارسنج استفاده کرد.


بهبود اعتبارسنجی drop down

در حالت فعلی تعریف drop down مثال این سری، نیازی به اعتبارسنجی نیست؛ چون لیست مشخصی از طریق کامپوننت در اختیار این المان قرار می‌گیرد و همواره دقیقا یکی از این عناصر انتخاب خواهند شد. اما اگر گزینه‌ی دیگری را مانند:
<option value="default">Select a Language...</option>
به ابتدای این لیست اضافه کنیم، اینبار نیاز به اعتبارسنجی خواهد بود؛ زیرا اکنون کاربر می‌تواند گزینه‌ای را انتخاب نکند و راهی پیش فرض هم برای اعتبارسنجی این گزینه وجود ندارد.
    <div class="form-group" [class.has-error]="hasPrimaryLanguageError">
      <label class="control-label">Primary Language</label>
      <select class="form-control" name="primaryLanguage" 
                #primaryLanguage
                (blur)="validatePrimaryLanguage(primaryLanguage.value)"
                (change)="validatePrimaryLanguage(primaryLanguage.value)"
                [(ngModel)]="model.primaryLanguage">
          <option value="default">Select a Language...</option>
          <option *ngFor="let lang of languages">
            {{ lang }}
          </option>
      </select>
    </div>
در اینجا اتصال به class.has-error را همانند مثال‌های قبلی مشاهده می‌کنید. اما اینبار به یک خاصیت عمومی تعریف شده‌ی در سطح کامپوننت متصل شده‌است؛ زیرا Angular در مورد این المان خاص، کاری را برای ما انجام نخواهد داد. همچنین پیاده سازی حالت عبور از این کامپوننت با Tab نیز توسط اتصال به رخداد blur صورت گرفته‌است تا هر زمانیکه این فیلد focus را از دست داد، اجرا شود. دسترسی به مقدار جاری انتخابی این select نیز توسط یک template reference variable به نام primaryLanguage# صورت گرفته‌است. به این ترتیب می‌توان به خواص این المان مانند value آن دسترسی پیدا کرد. همچنین چون می‌خواهیم با انتخاب گزینه‌ی دیگری این علامت خطا رفع شود، این متد به رخداد change نیز علاوه بر blur، متصل شده‌است.
export class EmployeeRegisterComponent {
  hasPrimaryLanguageError = false;

  validatePrimaryLanguage(value) {
    if (value === 'default'){
      this.hasPrimaryLanguageError = true;
      }
    else{
      this.hasPrimaryLanguageError = false;
      }
  }
}
کار این متد تغییر مقدار hasPrimaryLanguageError است تا کلاس has-error در فرم نمایش داده شود. در اینجا اگر مقدار primaryLanguage انتخابی همان گزینه‌ی default ابتدایی باشد، خاصیت hasPrimaryLanguageError به true تنظیم می‌شود:



اعتبارسنجی در سطح کل فرم

تا اینجا بررسی‌هایی را که انجام دادیم، در سطح فیلدها بودند. اکنون اگر کاربر به طور کامل تمام این فیلدها را تغییر ندهد و بر روی دکمه‌ی ارسال کلیک کند چطور؟
<form #form="ngForm" novalidate>
زمانیکه فرم جاری را ایجاد کردیم، یک template reference variable را به نام form# نیز به آن نسبت دادیم و چون به ngForm متصل شده‌است، می‌توان به خواص این شیء نیز دسترسی یافت. برای نمونه پس از دکمه‌ی ثبت، عبارت ذیل را تعریف کنید:
<h3> form.valid: {{ form.valid }}</h3>
همانطور که ملاحظه می‌کنید یکی از خواص شیء ngForm که توسط متغیر form قابل دسترسی شده‌است، خاصیت valid می‌باشد که حاصل اعتبارسنجی تمام عناصر داخل فرم است. برای مثال می‌توان از این خاصیت جهت فعال یا غیرفعال کردن دکمه‌ی ثبت اطلاعات نیز استفاده کرد:
<button class="btn btn-primary" type="submit"
            [disabled]="form.invalid">Ok</button>
در اینجا مقدار form.invalid به خاصیت disabled المان متصل شده‌است و اگر اعتبارسنجی فرم با شکست مواجه شود، این دکمه در حالت غیرفعال ظاهر می‌شود.


در قسمت بعد نحوه‌ی ارسال این فرم را به سرور، بررسی می‌کنیم.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-template-driven-forms-lab-04.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
اشتراک‌ها
بازنشستگی AJAX Crawling Policy توسط گوگل و اثر آن بر روی SPA

به این معنا که ربات‌های گوگل به این حد از پختگی رسیده‌اند که سایت شما را همانند یک مرورگر کامل مرور کنند. به این ترتیب مشکل SEO برنامه‌های تک صفحه‌ای وب به صورت خودکار حل می‌شود.

بازنشستگی AJAX Crawling Policy توسط گوگل و اثر آن بر روی SPA
نظرات مطالب
بررسی Source Generators در #C - قسمت سوم - بهبود کارآیی برنامه با تبدیل عملیات Reflection به تولید کد خودکار
یک نکته‌ی تکمیلی: روش دیباگ پروژه‌های Source Generator در Visual Studio

برای اینکار اولین تغییر مورد نیاز، افزودن تنظیم زیر به فایل csproj پروژه‌ی Source Generator است:
<PropertyGroup>
   <TargetFramework>netstandard2.0</TargetFramework>
   <IsRoslynComponent>true</IsRoslynComponent>
سپس در Solution explorer به خواص پروژه‌ی source generator مراجعه کرده و در برگه‌ی Debug آن، دراپ‌داون Launch را انتخاب و گزینه‌ی IsRoslynComponent تنظیمی را انتخاب کنید.
در ادامه در هر قسمتی از کدهای Source generator که نیاز به دیباگ هست، برای مثال ابتدای متد Execute، باید چندسطر زیر را اضافه کرد:
#if DEBUG
if (!Debugger.IsAttached)
{
   Debugger.Launch();
}
#endif
این کدها فقط در حالت دیباگ اضافه شده و با کامپایل برنامه و یا فشردن دکمه‌ی F5، سبب نمایش پنجره‌ی جدید choose just-in-time debugger می‌شوند. در اینجا می‌توانید یک وهله‌ی جدید از ویژوال استودیو را برای دیباگ انتخاب کنید و یا می‌توان همان وهله‌ی جاری را نیز انتخاب کرد تا سبب اجرای مجدد ویژوال استودیو نشود. در این حالت debugger به پروژه متصل شده و امکان دیباگ آن میسر می‌شود. ابتدا ویژوال استودیو بر روی سطر Debugger.Launch، متوقف می‌شود. سپس break-pointهای مدنظر خود را قرار داده و با فشردن دکمه‌ی F5 می‌توان به آن‌ها رسید و آن‌ها را دیباگ کرد.
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت هشتم- تعریف سطوح دسترسی پیچیده
تعریف نقش‌ها، ساده‌ترین روش محدود کردن دسترسی به منابع است؛ برای نمونه مشخص می‌کنیم که کاربران دارای نقش PayingUser، امکان سفارش دادن نگارش قاب شده‌ی تصاویر را داشته باشند. اما می‌خواهیم منطق دسترسی به منابع مختلف را پیچیده‌تر کنیم. برای مثال می‌خواهیم بررسی کنیم اگر منبعی واقعا متعلق به کاربر جاری سیستم است، به آن دسترسی داشته باشد. برای مثال هرچند کاربر جاری دارای نقش PayingUser است، اما آیا باید اجازه‌ی ویرایش تصاویر تمام کاربران، برای او صادر شده باشد؟ برای پیاده سازی یک چنین موارد پیچیده‌ای که فراتر از مفهوم نقش‌ها هستند، ویژگی جدیدی به نام Authorization policies به ASP.NET Core اضافه شده‌است که آن‌را در این قسمت بر اساس امکانات IdentityServer 4 بررسی می‌کنیم.


مقایسه تعریف سطوح دسترسی «مبتنی بر نقش‌ها» با سطوح دسترسی «مبتنی بر سیاست‌های امنیتی»

- در سطوح دسترسی «مبتنی بر نقش‌ها»
یک‌سری نقش از پیش تعریف شده وجود دارند؛ مانند PayingUser و یا FreeUser که کاربر توسط هر نقش، به یکسری دسترسی‌های خاص نائل می‌شود. برای مثال PayingUser می‌تواند نگارش قاب شده‌ی تصاویر را سفارش دهد و یا تصویری را به سیستم اضافه کند.

- در سطوح دسترسی «مبتنی بر سیاست‌های امنیتی»
سطوح دسترسی بر اساس یک سری سیاست که بیانگر ترکیبی از منطق‌های دسترسی هستند، اعطاء می‌شوند. این منطق‌ها نیز از طریق ترکیب User Claims حاصل می‌شوند و می‌توانند منطق‌های پیچیده‌تری را به همراه داشته باشند. برای مثال اگر کاربری از کشور A است و نوع اشتراک او B است و اگر در بین یک بازه‌ی زمانی خاصی متولد شده باشد، می‌تواند به منبع خاصی دسترسی پیدا کند. به این ترتیب حتی می‌توان نیاز به ترکیب چندین نقش را با تعریف یک سیاست امنیتی جدید جایگزین کرد. به همین جهت نسبت به روش بکارگیری مستقیم کار با نقش‌ها ترجیح داده می‌شود.


جایگزین کردن بررسی سطوح دسترسی توسط نقش‌ها با روش بکارگیری سیاست‌های دسترسی

در ادامه می‌خواهیم بجای بکارگیری مستقیم نقش‌ها جهت محدود کردن دسترسی به قسمت‌های خاصی از برنامه‌ی کلاینت، تنها کاربرانی که از کشور خاصی وارد شده‌اند و نیز سطح اشتراک خاصی را دارند، بتوانند دسترسی‌های ویژه‌ای داشته باشند؛ چون برای مثال امکان ارسال مستقیم تصاویر قاب شده را به کشور دیگری نداریم.

تنظیم User Claims جدید در برنامه‌ی IDP
برای تنظیم این سیاست امنیتی جدید، ابتدا دو claim جدید subscriptionlevel و country را به خواص کاربران در کلاس src\IDP\DNT.IDP\Config.cs در سطح IDP اضافه می‌کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        public static List<TestUser> GetUsers()
        {
            return new List<TestUser>
            {
                new TestUser
                {
                    Username = "User 1",
                    // ...

                    Claims = new List<Claim>
                    {
    // ...
                        new Claim("subscriptionlevel", "PayingUser"),
                        new Claim("country", "ir")
                    }
                },
                new TestUser
                {
                    Username = "User 2",
// ...

                    Claims = new List<Claim>
                    {
    // ...
                        new Claim("subscriptionlevel", "FreeUser"),
                        new Claim("country", "be")
                    }
                }
            };
        }
سپس باید تعاریف این claims جدید را به متد GetIdentityResources افزود تا به صورت scopeهای جدید از طرف کلاینت‌ها قابل درخواست باشند و چون این claimها استاندارد نیستند، برای تعریف آن‌ها از IdentityResource استفاده می‌کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        // identity-related resources (scopes)
        public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new List<IdentityResource>
            {
   // ...     
                new IdentityResource(
                    name: "country",
                    displayName: "The country you're living in",
                    claimTypes: new List<string> { "country" }),
                new IdentityResource(
                    name: "subscriptionlevel",
                    displayName: "Your subscription level",
                    claimTypes: new List<string> { "subscriptionlevel" })
            };
        }
همچنین باید مطمئن شد که کلاینت مدنظر ما قادر است این scopeهای تعریف شده را درخواست کند و IDP مجاز است تا آن‌ها را بازگشت دهد. برای این منظور آن‌ها را به لیست AllowedScopes تعریف کلاینت، اضافه می‌کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
                    ClientName = "Image Gallery",
// ...
                    AllowedScopes =
                    {
    // ...
                        "country",
                        "subscriptionlevel"
                    }
// ...
                }
             };
        }
    }

استفاده‌ی از User Claims جدید در برنامه‌ی MVC Client
در ادامه به کلاس ImageGallery.MvcClient.WebApp\Startup.cs برنامه‌ی MVC Client مراجعه کرده و دو scope جدیدی را که در سمت IDP تعریف کردیم، در اینجا در تنظیمات متد AddOpenIdConnect، درخواست می‌دهیم:
options.Scope.Add("subscriptionlevel");
options.Scope.Add("country");
به این ترتیب برنامه‌ی کلاینت می‌تواند دسترسی به این دو claim جدید را از طریق IDP، پیدا کند.
البته همانطور که در قسمت‌های قبل نیز ذکر شد، اگر claim ای در لیست نگاشت‌های تنظیمات میان‌افزار OpenID Connect مایکروسافت نباشد، آن‌را در لیست this.User.Claims ظاهر نمی‌کند. به همین جهت همانند claim role که پیشتر MapUniqueJsonKey را برای آن تعریف کردیم، نیاز است برای این دو claim نیز نگاشت‌های لازم را به سیستم افزود:
options.ClaimActions.MapUniqueJsonKey(claimType: "role", jsonKey: "role");
options.ClaimActions.MapUniqueJsonKey(claimType: "subscriptionlevel", jsonKey: "subscriptionlevel");
options.ClaimActions.MapUniqueJsonKey(claimType: "country", jsonKey: "country");

ایجاد سیاست‌های دسترسی در برنامه‌ی MVC Client

برای تعریف یک سیاست دسترسی جدید در کلاس ImageGallery.MvcClient.WebApp\Startup.cs برنامه‌ی MVC Client، به متد ConfigureServices آن مراجعه کرده و آن‌را به صورت زیر تکمیل می‌کنیم:
namespace ImageGallery.MvcClient.WebApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthorization(options =>
            {
                options.AddPolicy(
                   name: "CanOrderFrame",
                   configurePolicy: policyBuilder =>
                    {
                        policyBuilder.RequireAuthenticatedUser();
                        policyBuilder.RequireClaim(claimType: "country", requiredValues: "ir");
                        policyBuilder.RequireClaim(claimType: "subscriptionlevel", requiredValues: "PayingUser");
                    });
            });
در اینجا نحوه‌ی تعریف یک Authorization Policy جدید را مشاهده می‌کنید. ابتدا یک نام برای آن تعریف می‌شود که در قسمت‌های دیگر برنامه جهت ارجاع به آن مورد استفاده قرار می‌گیرد. سپس تنظیمات این سیاست دسترسی جدید را مشاهده می‌کنید که در آن نیاز است کاربر مدنظر حتما اعتبارسنجی شده باشد. از کشور ir بوده و همچنین سطح اشتراک او PayingUser باشد. در اینجا پارامتر requiredValues، یک آرایه را می‌پذیرد. بنابراین اگر برای مثال کشورهای دیگری نیز مدنظر هستند، می‌توان لیست آن‌ها را در اینجا اضافه کرد.
به علاوه policyBuilder شامل متد RequireRole نیز هست. به همین جهت است که این روش تعریف سطوح دسترسی، روش قدیمی مبتنی بر نقش‌ها را جایگزین کرده و در برگیرنده‌ی آن نیز می‌شود؛ چون در این سیستم، role نیز تنها یک claim است، مانند country و یا subscriptionlevel فوق.


بررسی نحوه‌ی استفاده‌ی از Authorization Policy تعریف شده و جایگزین کردن آن با روش بررسی نقش‌ها

تا کنون از روش بررسی سطوح دسترسی‌ها بر اساس نقش‌های کاربران در دو قسمت استفاده کرده‌ایم:
الف) اصلاح Views\Shared\_Layout.cshtml برای استفاده‌ی از Authorization Policy
در فایل Layout با بررسی نقش PayingUser، منوهای مرتبط با این نقش را فعال می‌کنیم:
@if(User.IsInRole("PayingUser"))
{
  <li><a asp-area="" asp-controller="Gallery" asp-action="AddImage">Add an image</a></li>
  <li><a asp-area="" asp-controller="Gallery" asp-action="OrderFrame">Order a framed picture</a></li>
}
برای جایگزین کردن آن جهت استفاده‌ی از سیاست دسترسی جدید CanOrderFrame، ابتدا نیاز است در این View به سرویس IAuthorizationService دسترسی پیدا کنیم که روش تزریق آن‌را در ذیل مشاهده می‌کنید:
@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService
پس از آن، روش استفاده‌ی از این سرویس را در ذیل مشاهده می‌کنید:
@if (User.IsInRole("PayingUser"))
{
  <li><a asp-area="" asp-controller="Gallery" asp-action="AddImage">Add an image</a></li>
}
@if ((await AuthorizationService.AuthorizeAsync(User, "CanOrderFrame")).Succeeded)
{
  <li><a asp-area="" asp-controller="Gallery" asp-action="OrderFrame">Order a framed picture</a></li>
}
اکنون لینک منوی درخواست نگارش قاب شده‌ی یک تصویر، صرفا به کاربران تامین کننده‌ی سیاست دسترسی CanOrderFrame نمایش داده می‌شود.

ب) اصلاح کنترلر ImageGallery.MvcClient.WebApp\Controllers\GalleryController.cs برای استفاده‌ی از Authorization Policy
namespace ImageGallery.MvcClient.WebApp.Controllers
{
    [Authorize]
    public class GalleryController : Controller
    {
        [Authorize(Policy = "CanOrderFrame")]
        public async Task<IActionResult> OrderFrame()
        {
در اینجا فیلتر Authorize امکان پذیرش نام یک Policy را نیز به همراه دارد.

اکنون برای آزمایش برنامه یکبار از آن خارج شده و سپس توسط اکانت User 1 که از نوع PayingUser در کشور ir است، به آن وارد شوید.
ابتدا به قسمت IdentityInformation آن وارد شوید. در اینجا لیست claims جدید را می‌توانید مشاهده کنید. همچنین لینک سفارش تصویر قاب شده نیز نمایان است و می‌توان به آدرس آن نیز وارد شد.


استفاده از سیاست‌های دسترسی در سطح برنامه‌ی Web API

در سمت برنامه‌ی Web API، در حال حاضر کاربران می‌توانند به متدهای Get ،Put و Delete ای که رکوردهای آن‌ها الزاما متعلق به آن‌ها نیست دسترسی داشته باشند. بنابراین نیاز است از ورود کاربران به متدهای تغییرات رکوردهایی که OwnerID آن‌ها با هویت کاربری آن‌ها تطابقی ندارد، جلوگیری کرد. در این حالت Authorization Policy تعریف شده نیاز دارد تا با سرویس کاربران و بانک اطلاعاتی کار کند. همچنین نیاز به دسترسی به اطلاعات مسیریابی جاری را برای دریافت ImageId دارد. پیاده سازی یک چنین سیاست دسترسی پیچیده‌ای توسط متدهای RequireClaim و RequireRole میسر نیست. خوشبختانه امکان بسط سیستم Authorization Policy با پیاده سازی یک IAuthorizationRequirement سفارشی وجود دارد. RequireClaim و RequireRole، جزو Authorization Requirementهای پیش‌فرض و توکار هستند. اما می‌توان نمونه‌های سفارشی آن‌ها را نیز پیاده سازی کرد:
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;

namespace ImageGallery.WebApi.Services
{
    public class MustOwnImageRequirement : IAuthorizationRequirement
    {
    }

    public class MustOwnImageHandler : AuthorizationHandler<MustOwnImageRequirement>
    {
        private readonly IImagesService _imagesService;
        private readonly ILogger<MustOwnImageHandler> _logger;

        public MustOwnImageHandler(
            IImagesService imagesService,
            ILogger<MustOwnImageHandler> logger)
        {
            _imagesService = imagesService;
            _logger = logger;
        }

        protected override async Task HandleRequirementAsync(
            AuthorizationHandlerContext context, MustOwnImageRequirement requirement)
        {
            var filterContext = context.Resource as AuthorizationFilterContext;
            if (filterContext == null)
            {
                context.Fail();
                return;
            }

            var imageId = filterContext.RouteData.Values["id"].ToString();
            if (!Guid.TryParse(imageId, out Guid imageIdAsGuid))
            {
                _logger.LogError($"`{imageId}` is not a Guid.");
                context.Fail();
                return;
            }

            var subClaim = context.User.Claims.FirstOrDefault(c => c.Type == "sub");
            if (subClaim == null)
            {
                _logger.LogError($"User.Claims don't have the `sub` claim.");
                context.Fail();
                return;
            }

            var ownerId = subClaim.Value;
            if (!await _imagesService.IsImageOwnerAsync(imageIdAsGuid, ownerId))
            {
                _logger.LogError($"`{ownerId}` is not the owner of `{imageIdAsGuid}` image.");
                context.Fail();
                return;
            }

            // all checks out
            context.Succeed(requirement);
        }
    }
}
در پروژه‌ی ImageGallery.WebApi.Services ابتدا یک Authorization Requirement و سپس پیاده سازی کننده‌ی آن که Authorization Handler نام دارد را تعریف کرده‌ایم. این پروژه نیاز به وابستگی‌های ذیل را دارد تا کامپایل شود.
<Project Sdk="Microsoft.NET.Sdk">
  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="2.1.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.1.1.0" />
  </ItemGroup>
</Project>

پیاده سازی سیاست‌های پویای دسترسی شامل مراحل ذیل است:
1- تعریف یک نیازمندی دسترسی جدید
public class MustOwnImageRequirement : IAuthorizationRequirement
{
}
ابتدا نیاز است یک نیازمندی دسترسی جدید را با پیاده سازی اینترفیس IAuthorizationRequirement ارائه دهیم. این نیازمندی، خالی است و صرفا به عنوان نشانه‌ای جهت یافت AuthorizationHandler استفاده کننده‌ی از آن استفاده می‌شود. در اینجا در صورت نیاز می‌توان یک سری خاصیت اضافه را تعریف کرد تا آن‌ها را به صورت پارامترهایی ثابت به AuthorizationHandler ارسال کند.

2- پیاده سازی یک AuthorizationHandler استفاده کننده‌ی از نیازمندی دسترسی تعریف شده
که کدهای کامل آن‌را در کلاس MustOwnImageHandler مشاهده می‌کنید. کار آن با ارث بری از AuthorizationHandler شروع شده و آرگومان جنریک آن، همان نیازمندی است که پیشتر تعریف کردیم. از این آرگومان جنریک جهت یافتن خودکار AuthorizationHandler متناظر با آن توسط ASP.NET Core استفاده می‌شود. بنابراین در اینجا MustOwnImageRequirement تهیه شده صرفا کارکرد علامتگذاری را دارد.
در کلاس تهیه شده باید متد HandleRequirementAsync آن‌را بازنویسی کرد و اگر در این بین، منطق سفارشی ما context.Succeed را فراخوانی کند، به معنای برآورده شدن سیاست دسترسی بوده و کاربر جاری می‌تواند به منبع درخواستی بلافاصله دسترسی یابد و اگر context.Fail فراخوانی شود، در همینجا دسترسی کاربر قطع شده و HTTP status code مساوی 401 (عدم دسترسی) را دریافت می‌کند.
در این پیاده سازی از filterContext.RouteData برای یافتن Id تصویر مورد نظر استفاده شده‌است. همچنین Id شخص جاری نیز از sub claim موجود استخراج گردیده‌است. اکنون این اطلاعات را به سرویس تصاویر ارسال می‌کنیم تا توسط متد IsImageOwnerAsync آن مشخص شود که آیا کاربر جاری سیستم، همان کاربری است که تصویر را در بانک اطلاعاتی ثبت کرده‌است؟ اگر بله، با فراخوانی context.Succeed به سیستم Authorization اعلام خواهیم کرد که این سیاست دسترسی و نیازمندی مرتبط با آن با موفقیت پشت سر گذاشته شده‌است.

3- معرفی سیاست دسترسی پویای تهیه شده به سیستم
معرفی سیاست کاری پویا و سفارشی تهیه شده، شامل دو مرحله‌ی زیر است:
مراجعه‌ی به کلاس ImageGallery.WebApi.WebApp\Startup.cs و افزودن نیازمندی آن:
namespace ImageGallery.WebApi.WebApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthorization(authorizationOptions =>
            {
                authorizationOptions.AddPolicy(
                   name: "MustOwnImage",
                   configurePolicy: policyBuilder =>
                    {
                        policyBuilder.RequireAuthenticatedUser();
                        policyBuilder.AddRequirements(new MustOwnImageRequirement());
                    });
            });
            services.AddScoped<IAuthorizationHandler, MustOwnImageHandler>();
ابتدا باید MustOwnImageHandler تهیه شده را به سیستم تزریق وابستگی‌ها معرفی کنیم.
سپس یک Policy جدید را با نام دلخواه MustOwnImage تعریف کرده و نیازمندی علامتگذار خود را به عنوان یک policy.Requirements جدید، اضافه می‌کنیم. همانطور که ملاحظه می‌کنید یک وهله‌ی جدید از MustOwnImageRequirement در اینجا ثبت شده‌است. همین وهله به متد HandleRequirementAsync نیز ارسال می‌شود. بنابراین اگر نیاز به ارسال پارامترهای بیشتری به این متد وجود داشت، می‌توان خواص مرتبطی را به کلاس MustOwnImageRequirement نیز اضافه کرد.
همانطور که مشخص است، در اینجا یک نیازمندی را می‌توان ثبت کرد و نه Handler آن‌را. این Handler از سیستم تزریق وابستگی‌ها بر اساس آرگومان جنریک AuthorizationHandler پیاده سازی شده، به صورت خودکار یافت شده و اجرا می‌شود (بنابراین اگر Handler شما اجرا نشد، مطمئن شوید که حتما آن‌را به سیستم تزریق وابستگی‌ها معرفی کرده‌اید).

پس از آن هر کنترلر یا اکشن متدی که از این سیاست دسترسی پویای تهیه شده استفاده کند:
[Authorize(Policy ="MustOwnImage")]
به صورت خودکار توسط MustOwnImageHandler مدیریت می‌شود.


اعمال سیاست دسترسی پویای تعریف شده به Web API

پس از تعریف سیاست دسترسی MustOwnImage که پویا عمل می‌کند، اکنون نوبت به استفاده‌ی از آن در کنترلر ImageGallery.WebApi.WebApp\Controllers\ImagesController.cs است:
namespace ImageGallery.WebApi.WebApp.Controllers
{
    [Route("api/images")]
    [Authorize]
    public class ImagesController : Controller
    {
        [HttpGet("{id}", Name = "GetImage")]
        [Authorize("MustOwnImage")]
        public async Task<IActionResult> GetImage(Guid id)
        {
        }

        [HttpDelete("{id}")]
        [Authorize("MustOwnImage")]
        public async Task<IActionResult> DeleteImage(Guid id)
        {
        }

        [HttpPut("{id}")]
        [Authorize("MustOwnImage")]
        public async Task<IActionResult> UpdateImage(Guid id, [FromBody] ImageForUpdateModel imageForUpdate)
        {
        }
    }
}
در اینجا در سه قسمت GetImage ،DeleteImage و UpdateImage با اعمال سیاست دسترسی پویای MustOwnImage، اگر کاربر جاری همان Owner تصویر درخواستی نباشد، دسترسی او به اجرای کدهای داخل این اکشن متدها به صورت خودکار بسته خواهد شد.




کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشه‌ی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آن‌را اجرا کنید تا برنامه‌ی IDP راه اندازی شود.
- در آخر به پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحه‌ی login نام کاربری را User 1 و کلمه‌ی عبور آن‌را password وارد کنید.
مطالب
نحوه کاهش مصرف حافظه EF Code first حین گزارشگیری از اطلاعات
تمام ORMهای خوب، دارای سطح اول کش هستند. از این سطح جهت نگهداری اطلاعات تغییرات صورت گرفته روی اشیاء و سپس اعمال نهایی آن‌ها در پایان یک تراکنش استفاده می‌شود. بدیهی است جمع آوری این اطلاعات اندکی بر روی سرعت انجام کار و همچنین بر روی میزان مصرف حافظه برنامه تاثیرگذار است. به علاوه یک سری از اعمال مانند گزارشگیری نیازی به این سطح اول کش ندارند. اطلاعات مورد استفاده در آن‌ها مانند نمایش لیستی از اطلاعات در یک گرید، حالت فقط خواندنی دارد. در EF Code first برای یک چنین مواردی استفاده از متد الحاقی AsNoTracking تدارک دیده شده است که سبب خاموش شدن سطح اول کش می‌شود. در ادامه در طی یک مثال، اثر این متد را بر روی سرعت و میزان مصرف حافظه برنامه بررسی خواهیم کرد.

کدهای کامل این مثال را در ذیل ملاحظه می‌کنید:

using System;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Diagnostics;
using System.Linq;

namespace EFGeneral
{
    public class User
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public class MyContext : DbContext
    {
        public DbSet<User> Users { get; set; }
    }

    public class Configuration : DbMigrationsConfiguration<MyContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
        }

        protected override void Seed(MyContext context)
        {            
            for (int i = 0; i < 21000; i++)
            {
                context.Users.Add(new User { Name = "name " + i });
                if (i % 1000 == 0)
                    context.SaveChanges();
            }
            base.Seed(context);
        }
    }

    public class PerformanceHelper
    {
        public static string RunActionMeasurePerformance(Action action)
        {
            GC.Collect();
            long initMemUsage = Process.GetCurrentProcess().WorkingSet64;

            var stopwatch = new Stopwatch();
            stopwatch.Start();

            action();

            stopwatch.Stop();

            var currentMemUsage = Process.GetCurrentProcess().WorkingSet64;
            var memUsage = currentMemUsage - initMemUsage;
            if (memUsage < 0) memUsage = 0;

            return string.Format("Elapsed time: {0}, Memory Usage: {1:N2} KB", stopwatch.Elapsed, memUsage / 1024);
        }
    }

    public static class Test
    {
        public static void RunTests()
        {
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>());
            StartDb();            

            for (int i = 0; i < 3; i++)
            {
                Console.WriteLine("\nRun {0}", i + 1);

                var memUsage = PerformanceHelper.RunActionMeasurePerformance(() => LoadWithTracking());
                Console.WriteLine("LoadWithTracking:\n{0}", memUsage);

                memUsage = PerformanceHelper.RunActionMeasurePerformance(() => LoadWithoutTracking());
                Console.WriteLine("LoadWithoutTracking:\n{0}", memUsage);
            }
        }

        private static void StartDb()
        {
            using (var ctx = new MyContext())
            {
                var user = ctx.Users.Find(1);
                if (user != null)
                {
                    // keep the object in memory
                }
            }
        }

        private static void LoadWithTracking()
        {
            using (var ctx = new MyContext())
            {
                var list = ctx.Users.ToList();
                if (list.Any())
                {
                    // keep the list in memory
                }
            }
        }

        private static void LoadWithoutTracking()
        {
            using (var ctx = new MyContext())
            {
                var list = ctx.Users.AsNoTracking().ToList();
                if (list.Any())
                {
                    // keep the list in memory
                }
            }
        }
    }
}

توضیحات:
مدل برنامه یک کلاس ساده کاربر است به همراه id و نام او.
سپس این کلاس توسط Context برنامه در معرض دید EF Code first قرار می‌گیرد.
در کلاس Configuration تعدادی رکورد را در ابتدای کار برنامه در بانک اطلاعاتی ثبت خواهیم کرد. قصد داریم میزان مصرف حافظه بارگذاری این اطلاعات را بررسی کنیم.
کلاس PerformanceHelper معرفی شده، دو کار اندازه گیری میزان مصرف حافظه برنامه در طی اجرای یک فرمان خاص و همچنین مدت زمان سپری شدن آن‌را اندازه‌گیری می‌کند.
در کلاس Test فوق چندین متد به شرح زیر وجود دارند:
متد StartDb سبب می‌شود تا تنظیمات ابتدایی برنامه به بانک اطلاعاتی اعمال شوند. تا زمانیکه کوئری خاصی به بانک اطلاعاتی ارسال نگردد، EF Code first بانک اطلاعاتی را آغاز نخواهد کرد.
در متد LoadWithTracking اطلاعات تمام رکوردها به صورت متداولی بارگذاری شده است.
در متد LoadWithoutTracking نحوه استفاده از متد الحاقی AsNoTracking را مشاهده می‌کنید. در این متد سطح اول کش به این ترتیب خاموش می‌شود.
و متد RunTests، این متدها را در سه بار متوالی اجرا کرده و نتیجه عملیات را نمایش خواهد داد.

برای نمونه این نتیجه در اینجا حاصل شده است:


همانطور که ملاحظه کنید، بین این دو حالت، تفاوت بسیار قابل ملاحظه است؛ چه از لحاظ مصرف حافظه و چه از لحاظ سرعت.

نتیجه گیری:
اگر قصد ندارید بر روی اطلاعات دریافتی از بانک اطلاعاتی تغییرات خاصی را انجام دهید و فقط قرار است از آن‌ها به صورت فقط خواندنی گزارشگیری شود، بهتر است سطح اول کش را به کمک متد الحاقی AsNoTracking خاموش کنید.

 
مطالب
React 16x - قسمت 11 - طراحی یک گرید - بخش 1 - کامپوننت صفحه بندی
در طی چند قسمت قصد داریم مثال قسمت قبل را که کار نمایش لیستی از فیلم‌ها را انجام می‌دهد، تبدیل به یک کامپوننت گرید کنیم که دارای امکانات صفحه بندی، فیلتر کردن و مرتب سازی اطلاعات است.


بررسی ساختار کامپوننت Pagination

شبیه به کامپوننت Like که در قسمت قبل ایجاد کردیم، می‌خواهیم کامپوننت جدید Pagination نیز به طور کامل از اشیاء movie مستقل باشد؛ تا در آینده بتوان از آن در جاهای دیگری نیز استفاده کرد. به همین جهت فایل جدید src\components\common\pagination.jsx را ایجاد کرده و سپس با استفاده از میانبرهای imrc و cc در VSCode، ساختار ابتدایی این کامپوننت را ایجاد می‌کنیم. هرچند می‌توان این کامپوننت را به صورت «Stateless Functional Component» نیز طراحی کرد؛ چون state و متد دیگری بجز render نخواهد داشت و تمام اطلاعات خودش را از والد خود دریافت می‌کند.
سپس به کامپوننت movies مراجعه کرده و این کامپوننت خالی را import می‌کنیم:
import Pagination from "./common/pagination";
پس از آن به متد رندر کامپوننت movies مراجعه کرده و پس از بسته شدن tag المان جدول، قصد داریم این کامپوننت صفحه بندی را نمایش دهیم. به همین جهت المان آن‌را در این محل قرار می‌دهیم تا بتوانیم اینترفیس ابتدایی آن‌را پیش از پیاده سازی آن، طراحی کنیم:
<Pagination
  itemsCount={this.state.movies.length}
  pageSize={this.state.pageSize}
  onPageChange={this.handlePageChange}
/>
این کامپوننت نیاز به تعداد کل آیتم‌های فیلم‌ها را دارد؛ به علاوه‌ی اندازه‌ی صفحه، یا همان تعداد ردیفی که قرار است در هر صفحه نمایش داده شود. به این ترتیب کامپوننت صفحه بندی می‌تواند تعداد صفحات مورد نیاز را محاسبه کرده و سپس آن‌ها را رندر کند.
  state = {
    movies: getMovies(),
    pageSize: 4
  };

به همین جهت دو ویژگی itemsCount و pageSize را پیش از هرکاری به تعریف المان صفحه بندی اضافه کرده‌ایم تا داده‌های ورودی آن مشخص شوند. ویژگی itemsCount از تعداد اعضای آرایه‌ی movies حاصل می‌شود و pageSize را به عنوان یک خاصیت جدید شیء منتسب به state تعریف و با عدد 4 مقدار دهی کرده‌ایم.
همچنین در لیست صفحاتی که توسط این کامپوننت رندر می‌شود، باید با کلیک بر روی هر کدام، اطلاعات آن صفحه رندر شود. به همین جهت در اینجا ویژگی onPageChange تعریف شده و به متد جدید handlePageChange در کامپوننت movies، متصل گردیده تا عدد page را دریافت کرده و به آن واکنش نشان دهد:
  handlePageChange = page => {
    console.log("handlePageChange", page);
  };
تا اینجا اینترفیس کامپوننت صفحه بندی را پیش از پیاده سازی آن تعریف کردیم (تعیین ورودی‌ها و خروجی آن). در مرحله‌ی بعد، این کامپوننت را تکمیل می‌کنیم.


نمایش شماره صفحات گرید، در کامپوننت صفحه بندی

برای رندر کامپوننت صفحه بندی، از کلاس‌های مخصوص اینکار که در بوت استرپ تعریف شده‌اند، استفاده می‌کنیم که ساختار کلی آن به صورت زیر است و از یک المان nav که داخل آن ul ای با کلاس pagination و liهایی با کلاس page-item هستند، تشکیل می‌شود. هر li، به همراه یک anchor است؛ با کلاس page-link تا لینک به صفحه‌ای خاص را ارائه دهد که در اینجا بجای لینک، از کلیک بر روی آن‌ها استفاده خواهیم کرد:
import React, { Component } from "react";

class Pagination extends Component {
  render() {
    return (
      <nav>
        <ul className="pagination">
          <li className="page-item">
            <a className="page-link">1</a>
          </li>
        </ul>
      </nav>
    );
  }
}

export default Pagination;

تا اینجا اگر برنامه را ذخیره کرده و اجرا کنید، عدد 1 را در پایین جدول فیلم‌ها مشاهده خواهید کرد:


اکنون باید رندر این liها را بر اساس ورودی‌های این کامپوننت که پیشتر معرفی کردیم، یعنی pageSize و itemsCount، پویا کنیم. به همین جهت نیاز به آرایه‌ای داریم که بر اساس این ورودی‌ها، شماره‌ی صفحات مانند [1,2,3] را ارائه دهد تا بر روی آن متد Array.map را فراخوانی کرده و liهای مورد نیاز را به صورت پویا رندر کنیم:
class Pagination extends Component {
  // ...
  getPageNumbersArray() {
    const { itemsCount, pageSize } = this.props;
    const pagesCount = Math.ceil(itemsCount / pageSize);
    if (pagesCount === 1) {
      return null;
    }

    const pages = new Array();
    for (let i = 1; i <= pagesCount; i++) {
      pages.push(i);
    }
    return pages;
  }
}
در اینجا بر اساس ورودی‌ها، تعداد صفحات محاسبه شده و سپس بر اساس آن‌ها آرایه‌ای از این شماره صفحه‌ها تشکیل و بازگشت داده می‌شود. همچنین اگر تعداد صفحات 1 بود، می‌خواهیم این کامپوننت چیزی را رندر نکند. به همین جهت در اینجا null بازگشت داده شده‌است. دلیل استفاده‌ی از Math.ceil که کوچکترین عدد صحیح بزرگتر یا مساوی خروجی را بازگشت می‌دهد، نیز همین مورد است. توسط این متد، خروجی float دریافتی به integer تبدیل شده و سپس قابلیت مقایسه‌ی با 1 را پیدا می‌کند. برای مثال اگر تعداد ردیف‌های صفحه را به 10 تنظیم کنیم، خروجی این تقسیم در این مثال، 0.9 خواهد بود که شرط بررسی pagesCount === 1 را برآورده نمی‌کند. به همین جهت توسط متد Math.ceil، این خروجی به عدد 1 تقریب زده شده و سبب بازگشت نال از این متد خواهد شد.
سپس به کمک متد map، اعضای این آرایه را تبدیل به لیست liهای نمایش شماره صفحات می‌کنیم. در اینجا key هر li را نیز به شماره صفحه که منحصربفرد است، تنظیم کرده‌ایم:
class Pagination extends Component {
  render() {
    const pages = this.getPageNumbersArray();
    if (!pages) {
      return null;
    }

    return (
      <nav>
        <ul className="pagination">
          {pages.map(page => (
            <li key={page} className="page-item">
              <a className="page-link">{page}</a>
            </li>
          ))}
        </ul>
      </nav>
    );
  }

پس از ذخیره‌ی این کامپوننت و بارگذاری مجدد برنامه در مرورگر، شماره‌ی صفحات رندر شده، در پایین جدول مشخص هستند:


با داشتن 9 فیلم در آرایه‌ی movies و نمایش 4 فیلم به ازای هر صفحه، به 3 صفحه خواهیم رسید که به درستی در اینجا رندر شده‌است. یکبار هم برای آزمایش بیشتر، مقدار pageSize را در کامپوننت movies به 10 تنظیم کنید. در این حالت کامپوننت صفحه بندی نباید رندر شود.


مدیریت انتخاب شماره‌های صفحات

در این قسمت می‌خواهیم مدیریت onPageChange={this.handlePageChange} را که به تعریف المان صفحه بندی در کامپوننت movies اضافه کردیم، تکمیل کنیم. برای این منظور در کامپوننت صفحه بندی، قسمت anchor را به صورت زیر تغییر می‌دهیم تا با کلیک بر روی آن، رخداد onPageChange صادر شود:
<a
   onClick={() => this.props.onPageChange(page)}
   className="page-link"
   style={{ cursor: "pointer" }}
>
   {page}
</a>

تا اینجا اگر برنامه را آزمایش کنیم، با کلیک بر روی لینک‌های شماره صفحات، شماره صفحه‌ی انتخابی، در کنسول توسعه دهندگان مرورگر لاگ می‌شود.

اکنون می‌خواهیم اگر صفحه‌ای انتخاب شد، شماره‌ی آن صفحه با رنگی دیگر نمایش داده شود. در بوت استرپ برای اینکار تنها کافی است کلاس active را به className هر li اضافه کنیم و برعکس. یعنی اگر page ای مساوی صفحه‌ی جاری انتخاب شده بود (currentPage در اینجا)، آنگاه کلاس page-item active، به المان li اضافه شود. بنابراین در این کامپوننت نیاز است عدد currentPage را نیز دریافت کنیم. به همین جهت ویژگی currentPage را به تعریف المان Pagination در کامپوننت movies اضافه می‌کنیم:
<Pagination
  itemsCount={this.state.movies.length}
  pageSize={this.state.pageSize}
  onPageChange={this.handlePageChange}
  currentPage={this.state.currentPage}
/>
این ویژگی نیز مقدار خودش را از state به روز شده دریافت می‌کند:
class Movies extends Component {
  state = {
    movies: getMovies(),
    pageSize: 4,
    currentPage: 1
  };
به روز رسانی state نیز در متد handlePageChange که به ویژگی onPageChange متصل است، بر اساس page دریافتی از کامپوننت صفحه بندی، رخ می‌دهد:
  handlePageChange = page => {
    console.log("handlePageChange", page);
    this.setState({currentPage: page});
  };
بنابراین هرگاه که بر روی یک شماره صفحه در کامپوننت صفحه بندی کلیک می‌شود، رخ‌داد onPageChange متصل به تعریف المان Pagination درج شده‌ی در کامپوننت movies، روی داده و به همراه آن شماره صفحه‌ای، به متد رخ‌دادگران متصل به آن در کامپوننت movies که در اینجا handlePageChange نام دارد، ارسال می‌شود. در این متد state کامپوننت به روز شده و این امر سبب فراخوانی مجدد متد رندر می‌شود که در انتهای آن کامپوننت Pagination درج شده‌است. بنابراین به روز رسانی state، سبب رندر مجدد کامپوننت صفحه بندی با currentPage جدیدی که به آن رسیده‌است، خواهد شد.

پس از این تغییرات، به کامپوننت صفحه بندی مراجعه کرده و بر اساس currentPage دریافتی، کلاس active را به المان li اعمال می‌کنیم:
<li
  key={page}
  className={
    page === this.props.currentPage
    ? "page-item active"
    : "page-item"
  }
>
پس از اعمال این تغییرات، اکنون برنامه در مرورگر به صورت زیر به نظر می‌رسد:


در اولین بار نمایش برنامه، عدد 1 در حالت انتخاب شده قرار دارد؛ چون مقدار currentPage موجود در state، همان عدد 1 است. پس از آن با کلیک بر روی اعداد دیگر، با به روز رسانی state، مقدار currentPage تغییر کرده و کامپوننت صفحه بندی نسبت به آن واکنش نشان می‌دهد.


نمایش صفحه بندی شده‌ی اطلاعات

تا اینجا لیستی که نمایش داده می‌شود، حاوی تمام اطلاعات آرایه‌ی this.state.movies است و بر اساس شماره‌ی صفحه‌ی انتخابی، تغییر نمی‌کند. به همین جهت با استفاده از متد slice، تکه‌ای از آرایه‌ی movies را که بر اساس شماره صفحه‌ی انتخابی و تعداد ردیف‌ها در هر صفحه نیاز است نمایش داده شود، انتخاب کرده و بازگشت می‌دهیم:
  paginate() {
    const first = (this.state.currentPage - 1) * this.state.pageSize;
    const last = first + this.state.pageSize;
    return this.state.movies.slice(first, last);
  }
اکنون در متد رندر کامپوننت movies، بجای کار با کل آرایه‌ی this.state.movies، آرایه‌ی جدید slice شده را توسط متد paginate دریافت کرده:
  render() {
    const { length: count } = this.state.movies;

    if (count === 0) return <p>There are no movies in the database.</p>;

    const movies = this.paginate();
و سپس در همین متد رندر، فراخوانی this.state.movies.map را به movies.map تغییر می‌دهیم، تا تنها لیست مرتبط با هر صفحه‌ی انتخابی نمایش داده شود.

پس از ذخیره‌ی تغییرات و بارگذاری مجدد برنامه، اکنون می‌توان نمایش صفحه بندی شده‌ی اطلاعات را شاهد بود:



بررسی صحت نوع پارامترهای ارسالی به کامپوننت‌ها

تا اینجا فرض بر این است که مصرف کننده‌ی کامپوننت صفحه بندی، دقیقا همان ویژگی‌هایی را که ما طراحی کرده‌ایم، با همان نام‌ها و همان نوع‌ها را حتما به آن ارسال می‌کند. همچنین اگر افزونه‌ی eslint را هم در VSCode نصب کرده باشید، به همراه نصب وابستگی‌های زیر در خط فرمان:
> npm i -g typescript eslint tslint eslint-plugin-react-hooks jshint babel-eslint eslint-plugin-react eslint-plugin-mocha

 به ازای هر خاصیت props استفاده شده‌ی در کامپوننت صفحه بندی، اخطاری را مانند «'currentPage' is missing in props validation eslint(react/prop-types)» مشاهده خواهید کرد:



که عنوان می‌کند props validation این خاصیت استفاده شده، فراموش شده‌است.
در نگارش‌های قبلی React، امکانات بررسی نوع‌های ارسالی به کامپوننت‌ها، جزئی از بسته‌ی اصلی آن بود؛ اما از نگارش 15 به بعد، به بسته‌ی مستقلی منتقل شده‌است که باید به صورت جداگانه‌ای در ریشه‌ی پروژه نصب شود:
> npm i prop-types --save

البته اگر TypeScript را بر روی سیستم خود نصب کرده باشید، دیگر نیازی به نصب بسته‌ی npm فوق را ندارید و prop-types، جزئی از آن است که عموما در یک چنین مسیری قرار دارد و برای کار کردن با آن، تنها ذکر import مرتبط با PropType در ماژول‌های برنامه کافی بوده و برنامه در این حالت بدون مشکل کامپایل می‌شود:
 C:/Users/{username}/AppData/Local/Microsoft/TypeScript/3.6/node_modules/@types/prop-types/index

اکنون در ابتدای فایل کامپوننت صفحه بندی، تعریف زیر را اضافه می‌کنیم:
 import PropTypes from "prop-types";
سپس در انتهای این فایل، اعتبارسنجی props آن‌را تعریف خواهیم کرد:
Pagination.propTypes = {
  itemsCount: PropTypes.number.isRequired,
  pageSize: PropTypes.number.isRequired,
  currentPage: PropTypes.number.isRequired,
  onPageChange: PropTypes.func.isRequired
};

export default Pagination;
همانطور که مشاهده می‌کنید، در اینجا خاصیت جدید propTypes (دقیقا با همین نگارش؛ در غیراینصورت بررسی نوع‌ها کار نخواهد کرد)، به تعریف کلاس Pagination اضافه شده‌است (پس از تعریف کلاس کامپوننت به صورت مستقل اضافه می‌شود).
سپس مقدار این خاصیت جدید را به شیءای تنظیم می‌کنیم که نام خواص آن، دقیقا همان نام خواص و رویدادهای props استفاده شده‌ی در این کامپوننت است. در ادامه توسط PropTypes ای که در ابتدای ماژول import می‌شود، کار تعریف نوع این خواص و اجباری بودن آن‌ها را می‌توان مشخص کرد که برای مثال در اینجا سه خاصیت تعریف شده از نوع عددی و اجباری بوده و onPageChange، از نوع func است.
پس از اضافه کردن Pagination.propTypes و مقدار دهی آن، خطاهای eslint ای که در تصویر فوق مشاهده کردید، برطرف می‌شوند. همچنین اگر فراخوان کامپوننت Pagination مثلا بجای itemsCount یک رشته‌ی فرضی را وارد کند (برای آزمایش آن در کامپوننت movies، در تعریف المان Pagination، مقدار itemsCount را یک رشته وارد کنید)، چنین خطایی در مرورگر ظاهر خواهد شد که عنوان می‌کند itemsCount یک عدد را می‌پذیرد و نوع ورودی آن اشتباه است:

البته این خطا فقط در حالت development مشاهده می‌شود و در حالت توزیع برنامه، خیر.

بنابراین تعریف propTypes یک best practice ایجاد کامپوننت‌های React است که نه فقط بررسی نوع‌ها را فعال می‌کند، بلکه می‌تواند به عنوان مستندات آن نیز در جهت تعیین props مورد نیاز، همچنین نوع و اجباری بودن آن‌ها، اطلاعات کاملی را ارائه کند.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-11.zip