Quantcast
Channel: ‫فید مطالب .NET Tips
Viewing all 1980 articles
Browse latest View live

‫Pipeها در Angular 2 – قسمت سوم – Pipeهای Pure و Impure

$
0
0
 در قسمت قبلبیان شد که Angular برای اعمال Pipe بر روی Template expressions باید تمامی رخدادهای برنامه را تحت نظر قرار داده و با مشاهده‌ی هر تغییری بر روی عبارت ورودی Pipe، فراخوانی Pipe را آغاز کند. از جمله این رخدادها می‌توان به رخدادهای mouse move، timer tick، server response و فشرده شدن کلیدهای ماوس و یا کیبورد اشاره کرد. واضح است که بررسی تغییرات عبارت در این همه رخداد می‌تواند مخرب باشد و بر روی کارآئی (Performance) تاثیر منفی خواهد گذاشت. اما Angular برای حل این مشکل و همچنین هنگام مشاهده سریع تغییرات هنگام استفاده از Pipeها، الگوریتم‌های سریع و ساده‌ای در نظر گرفته است که آن‌ها را در این بخش مورد برسی قرار خواهیم داد.


Pipeهای Pure و Impure

Pipeها کلا در دو دسته‌ی Pure و Impure قرار می‌گیرند. هنگام ساخت Pipe سفارشی در صورتیکه نوع Pipe مشخص نشود، به صورت پیش فرض از نوع Pure خواهد بود. برای تعریف Pipeهایی از نوع Impure کافی است در متادیتای Pipe@، پرچم Pure را به مقدار false تنظیم کنید.
@Pipe({ name: 'impurePipe', pure: false })
تفاوت این Pipeها در زمان فراخوانی دوباره آنها است.


Pure Pipe

این نوع Pipeها تنها زمانی فراخوانی مجدد می‌شوند که یک تغییر محض (Pure Change) بر روی عبارت ورودی آنها رخ دهد. هر نوع تغییری بر روی عبارات ورودی از جنس string ، number ، Boolean ، Symbol و عبارات اولیه، یا هرنوع تغییری در ارجاع یک شیء مانند  Date ، Array ، Function و Object نیز تغییر محض محسوب می‌شود. به عنوان مثال هیچکدام از تغییرات زیر یک تغییر محض محسوب نمی‌شوند:
numbers.push(10);
obj.name = ‘javad’;
زیرا با اضافه شدن عنصری به یک آرایه یا تغییر خصوصیتی از یک شیء، باعث تغییری در ارجاع آنها نمی‌شود و همانطور که اشاره شد، در عبارات از نوع آرایه و Object، فقط تغییر در ارجاع آن‌ها یک تغییر محض محسوب می‌شود.
حالا می‌توان به این نتیجه رسید که اضافه شدن مقداری به آرایه یا به‌روزرسانی یک property از object، باعث فراخوانی مجدد Pure Pipe نخواهد نشد. شاید این نوع از Pipeها محدود کننده باشند، اما بسیار سریع هستند (برسی تغییر در ارجاع یک شیء بسیار سریعتر از بررسی کامل یک شیء، صورت می‌گیرد).


Impure Pipe

این نوع Pipeها در اغلب رخدادهای کامپوننت از جمله فشره شدن کلید یا حرکت ماوس و رخدادهای دیگر فراخوانی مجدد می‌شوند. با در نظر گرفتن این نگرانی، هنگام پیاده سازی این نوع Pipeها باید مراقب بود؛ زیرا این نوع Pipeها با اجرای طولانی خود می‌توانند رابط کاربری شما را نابود کنند. برای درک کامل تفاوت این دو نوع از Pipeها مثالی را دنبال می‌کنیم.

مثال: قصد داریم Pipe سفارشی را پیاده سازی کنیم تا آرایه‌ای از اعداد را دریافت و فقط اعداد زوج را فیلتر کرده و نمایش دهد.
برای این منظور یک فایل جدید را با نام even-numbers.pipe.ts با محتویات زیر ایجاد می‌کنیم: 
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'evenNumbers'
})
export class EvenNumbersPipe implements PipeTransform {
  transform(numbers: Array<number>): Array<number> {
    var x=numbers.filter(r => r % 2 == 0);
    return x;
  }
}
همانطور که مشخص است این Pipe در متد transform، آرایه‌ای از اعداد را دریافت کرده و فقط اعداد زوج را بازگشت می‌دهد. حالا باید Pipe تعریف شده خود را در AppModule در قسمت declares تعریف کنیم.
// . . .
import { EvenNumbersPipe } from './pipes/even-numbers.pipe'
@NgModule({
  declarations: [
    . . .
    EvenNumbersPipe
  ],
 . . .
})
export class AppModule { }

سپس در کامپوننت مورد نظر خود متغیری را به نام numbers از نوع آرایه، با مقدار اولیه‌ی اعداد از یک تا ده، تعریف می‌کنیم:
numbers: Array<number> = [1,2,3,4,5,6,7,8,9,10];
برای نمایش این اعداد در رابط کاربری تگ‌های زیر را به قالب کامپوننت خود اضافه می‌کنیم:
<h1>All numbers</h1><span *ngFor="let number of numbers">
  {{number}}</span>
همچنین با استفاده از تگ‌های زیر یک input برای اضافه کردن مقدار جدید به آرایه درنظر می‌گیریم:
<p><input type="text" #number /><input type="button" (click)="numbers.push(number.value)" value="Add number"/></p>

تگ‌های زیر را نیز برای اعمال Pipe نمایش اعداد زوج، به قالب کامپوننت اضافه می‌کنیم:
<h1>even numbers</h1><span *ngFor="let number of numbers | evenNumbers">
  {{number}}</span>
بعد از اجرای برنامه، یک عدد جدید زوج را به آرایه اضافه کنید. متوجه خواهید شد با اینکه لیست اعداد در قسمت All numbers به‌روز می‌شوند، ولی Pipe، متوجه تغییری بر روی آرایه نشده‌است و همچنان اعداد قبلی را نمایش می‌دهد. دلیل این امر همانطور که قبلا نیز اشاره شد، بخاطر Pure بودن Pipe و عدم فراخوانی مجدد این نوع Pipe‌ها در زمان اضافه شدن مقداری به آرایه یا تغییری در خصوصیت یک شیء است.

برای حل این مشکل، هنگام اضافه شدن عدد به آرایه، اگر ارجاع آرایه را تغییر دهیم، Pure Pipe متوجه تغییرات خواهد شد و لیست اعداد را به‌روز رسانی می‌کند (تغییر در ارجاع یک شیء، از نوع تغییرات محض است):
<p><input type="text" #number /><input type="button" (click)="numbers = numbers.concat(number.value)" value="Add number"/></p>
با تغییر نحوه اضافه شدن عنصر به آرایه به شکل بالا خواهیم دید که با افزودن اعداد جدید، لیست اعداد زوج نیز در لحظه اعمال خواهند شد. این راه‌حل همیشه کارآمد نخواهد بود. همیشه تشخیص محل اضافه شدن عنصر به آرایه در برنامه کار ساده‌ای نیست تا در آنجا ارجاع آرایه را نیز تغییر دهیم. راه‌حل، استفاده از Impure Pipe است. کافی است متادیتای Pipe@ را هنگام تعریف به شکل زیر تغییر دهید:
@Pipe({
  name: 'evenNumbers',
  pure: false
})
export class EvenNumbersPipe implements PipeTransform {
   //…
}

کسانیکه با Angular 1.x آشنایی دارند، شاید اکنون متوجه این شده‌اند که چرا در Angular به مشابه Angular 1.x دیگر خبری ازfilter و orderBy نیست. با توجه به اینکه این دو فیلتر فقط با عبارات از نوع object سروکار داشتند، پیاده‌سازی آنها فقط با Impure Pipeها امکان پذیر بود و با توجه به اینکه Impure Pipeها در هر بار چرخه تغییرات کامپوننت اجرا خواهند شد، باعث کندی در صفحات خواهند شد. 

‫الگوریتم‌های داده کاوی در SQL Server Data Tools یا SSDT - قسمت پنجم - الگوریتم‌ Association Rules

$
0
0

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


مقدمه

خودتان را جای مدیر یک سوپرمارکت بگذارید. یکی از وظایف شما فروش بالاتر نسبت به بقیه مدیران یک سوپرمارکت زنجیره ای است. برای نیل به این هدف، درک الگوی خرید مشتریان بسیار حایز اهمیت است. فرض کنید متوجه شده‌اید که مشتریان شما در 75 درصد موارد سس، هات داگ و ترشی را با هم خریده‌اند. بنابراین چیدن قفسه به طوری که این اقلام کنار یکدیگر باشند، بهتر است. همچنین می‌توانید پکیجی را شامل این اقلام ایجاد کرده و با درصد تخفیف مناسبی به‌فروش برسانید؛ برای مثال یک ترشی را که تازه به بازار آمده و هنوز اقبال عمومی در رابطه با آن وجود ندارد، اما سود خوبی در فروش آن نصیب شما می‌شود، در این پکیج و در کنار هات داگ و سس معروفی قرار داده و بفروش برسانید.


نحوه عملکرد الگوریتم

این الگوریتم، براساس شمارش ترکیبات تکرارشونده‌ی حالات گوناگون ویژگی‌های یک مدل، کار می‌کند. این الگوریتم شبیه به الگوریتم Naïve Bayes می‌باشد؛ با این تفاوت که دارای رویکرد کمیاست (براساس عدد خامی از وقوع ترکیبات حالات یک ویژگی) و رویکرد کیفی ندارد (محاسبه تمامی احتمالات شرطی، آنچه که در الگوریتم Naïve Bayes اتفاق می‌افتاد). همچنین در اینجا ماتریس ضرایبی محاسبه نمی‌شود، بلکه تنها ضرایب قابل توجه، نگهداری می‌شوند.


تفسیر مدل

این الگوریتم، پس از پردازش، سه تب دارد.


تب Itemsets تعداد تکرار مجموعه اقلام کشف شده را نشان می‌دهد. مقدار پارامتر Minimum_Support اگر خیلی پایین در نظر گرفته شود، آنگاه لیست طولانی را ایجاد می‌کند. با استفاده از Filter Item Set می‌توان Item Set‌های موردنظر را فیلتر نمود. برای مثال می‌توان چنین Item Set ای را در نظر گرفت Gender=Male.

تب Rules نشان دهنده قوانین وابستگی کاربردی و ارزشمندی می‌باشد که به همراه احتمال و درجه اهمیتشان در یک جدول آورده شده‌اند. درجه اهمیت (Importance) نشان دهنده میزان سودمندی یک قانون است و هرچه بیشتر باشد، قانون متناظر آن درجه کیفی بالاتری دارد. به عبارت دیگر بیشتر می‌توان روی آن قانون حساب کرد. توسط پارامترهای Minimum_Probability و Minimum_Importance به ترتیب می‌توان لیست مزبور را براساس مینیمم احتمال و مینیمم درجه اهمیت فیلتر کرد.

تب Network Dependency، هر آیتم و قانون، وابستگی بین آن‌ها را نشان می‌دهد.


نکته آخر:در یک مدل وابستگی، اگر ستونی به عنوان ورودی در نظر گرفته شود، مقادیرش فقط می‌توانند در itemset‌های تکرار شونده و درسمت چپ قوانین وابستگی قرار بگیرند. اگر ستونی به عنوان خروجی درنظر گرفته شود، حالات مختلف آن ستون می‌توانند در itemset‌های تکرار شونده و در سمت راست قوانین وابستگی قرار بگیرند. اگر ستونی به عنوان ورودی-خروجی در نظر گرفته شود، آنگاه حالات مختلف آن ستون می‌توانند در itemset‌های تکرار شونده و در سمت چپ و هم راست قوانین وابستگی قرار بگیرند.  

‫مدیریت سراسری خطاها در یک برنامه‌ی Angular

$
0
0
در این مطلب قصد داریم پیام‌ها و اخطارهای برنامه را توسط کامپوننت Angular2 Toastyنمایش داده و همچنین برای کاهش میزان تکرار قسمت‌های نمایش خطا در برنامه، کار مدیریت متمرکز و سراسری آن‌ها را نیز انجام دهیم.


نمایش پیام‌ها و اخطارهای یک برنامه‌ی Angular توسط ng2-toasty

در مطلب «ایجاد Drop Down List‌های آبشاری در Angular» در قسمت دریافت اطلاعات drop down دوم از سرور، اگر کاربر مجددا گروه را بر روی حالت «لطفا گروهی را انتخاب کنید ...» قرار دهد، مقدار categoryId به undefined تغییر می‌کند:
  fetchProducts(categoryId?: number) {
    console.log(categoryId);

    this.products = [];

    if (categoryId === undefined || categoryId.toString() === "undefined") {
      return;
    }
در اینجا می‌خواهیم توسط کامپوننت Angular2 Toasty، پیام متناسبی را نمایش دهیم:



پیشنیازهای کار با کامپوننت Angular2 Toasty توسط یک برنامه‌ی Angular CLI

برای کار با کامپوننت Angular2 Toasty، ابتدا از طریق خط فرمان به پوشه‌ی ریشه‌ی برنامه وارد شده و سپس دستور ذیل را صادر می‌کنیم:
> npm install ng2-toasty --save
اینکار سبب خواهد شد تا این کامپوننت در پوشه‌ی node_modules\ng2-toasty نصب شده و همچنین فایل package.json نیز جهت درج مدخل آن به روز رسانی شود:


یک نکته:اگر در حین اجرای این دستور به خطای ذیل برخوردید:
 npm ERR! Error: EPERM: operation not permitted, rename
چون VSCode پوشه‌ی node_modules را تحت نظر قرار می‌دهد، ممکن است یک سری اعمال npm مجوز اجرا را پیدا نکنند. بنابراین ابتدا VSCode را بسته و مجددا دستور npm را اجرا کنید.

پس از آن نیاز است یکی از شیوه‌نامه‌هایی را که در تصویر فوق ملاحظه می‌کنید، در فایل angular-cli.json. مشخص کنیم:
"styles": [
    "../node_modules/bootstrap/dist/css/bootstrap.min.css",
    "../node_modules/ng2-toasty/bundles/style-bootstrap.css",
    "styles.css"
],
که برای نمونه در اینجا، شیوه‌نامه‌ی بوت استرپ آن انتخاب شده‌است.

سپس باید به فایل src\app\app.module.ts مراجعه کرد و ماژول این کامپوننت را معرفی نمود:
import { ToastyModule } from "ng2-toasty";

@NgModule({
  imports: [
    BrowserModule,
    ToastyModule.forRoot(),

همچنین در همین قسمت، به فایل قالب src\app\app.component.html مراجعه کرده و selector tag این کامپوننت را در ابتدای آن تعریف می‌کنیم:
<ng2-toasty [position]="'top-right'"></ng2-toasty>
در اینجا با استفاده از property binding و تعیین مقدار رشته‌ای top-right، محل نمایش اعلانات برنامه را مشخص می‌کنیم. مقدارهای ممکن آن شامل bottom-right، bottom-left، top-right، top-left، top-center، bottom-center، center-center هستند. برای مثال اگر می‌خواهید آن‌را در میانه‌ی صفحه نمایش دهید، مقدار center-center را انتخاب کنید. همچنین باید دقت داشت که این مقدار باید درون '' قرار گیرد تا مشخص شود که رشته‌ای به خاصیت position انتساب داده شده‌است و این مقدار یک خاصیت عمومی تعریف شده‌ی در کامپوننت متناظر با قالب، نیست.


نمایش یک پیام خطا توسط ToastyService

اکنون که کار برپایی کامپوننت Angular2 Toasty به پایان رسید، کار کردن با آن به سادگی تزریق سرویس آن به سازنده‌ی یک کامپوننت و فراخوانی متدهای info، success ، wait ، error و warning آن است:
import { ToastyService, ToastOptions } from "ng2-toasty";

export class ProductGroupComponent implements OnInit {

  constructor(
    private productItemsService: ProductItemsService,
    private toastyService: ToastyService) { }

  fetchProducts(categoryId?: number) {
    console.log(categoryId);

    this.products = [];

    if (categoryId === undefined || categoryId.toString() === "undefined") {
      this.toastyService.error(<ToastOptions>{
        title: "Error!",
        msg: "Please select a category.",
        theme: "bootstrap",
        showClose: true,
        timeout: 5000
      });
      return;
    }
- در اینجا در ابتدا ماژول‌های مورد نیاز import شده‌اند.
- سپس ToastyService به سازنده‌ی کلاس کامپوننت مدنظر تزریق شده‌است تا بتوان از امکانات آن استفاده کرد.
- در ادامه، فراخوانی متد this.toastyService.error سبب نمایش اخطار قرمز رنگی می‌شود که تصویر آن‌را در ابتدای مطلب جاری مشاهده کردید.
- علت ذکر <ToastOptions> در اینجا این است که وجود آن سبب خواهد شد تا intellisense در VSCode فعال شود و پس از آن بتوان تمام گزینه‌های این متد و تنظیمات را بدون مراجعه‌ی به مستندات آن از طریق intellisense یافت و درج کرد:



مدیریت سراسری خطاهای مدیریت نشده، در یک برنامه‌ی Angular

در برنامه‌های Angular از این دست کدها بسیار مشاهده می‌شوند:
    this.productItemsService.getCategories().subscribe(
      data => {
        this.categories = data;
      },
      err => console.log("get error: ", err)
    );
تا اینجا قسمت err یا بروز خطا را با console.log مدیریت کرده‌ایم. در این حالت کاربر ممکن است 10 بار بر روی دکمه‌ای کلیک کند یا صفحه‌ای را بارگذاری کند و دست آخر متوجه نشود که مشکل کار چیست. به همین جهت می‌توان خطاها را نیز توسط ToastyService نمایش داد تا کاربران دقیقا متوجه بروز مشکل رخ داده شوند. اما ... به این ترتیب تکرار کد زیادی را خواهیم داشت و باید به ازای تمام این موارد، یکبار this.toastyService.error را فراخوانی کنیم. برای مدیریت بهتر یک چنین سناریویی در Angular، کلاس و سرویس توکاری به نام ErrorHandler وجود دارد. در هر قسمتی از برنامه‌ی Angular که استثنایی مدیریت نشده رخ دهد، ابتدا از این کلاس رد شده و سپس به برنامه انتشار پیدا می‌کند. بنابراین می‌توان یک ErrorHandler سفارشی را با ارث بری از آن تهیه کرد و سپس بجای سرویس توکار اصلی، به برنامه معرفی و از آن استفاده نمود. به این ترتیب می‌توان یک Global Error Interceptor را طراحی نمود.
به همین منظور کلاس جدیدی را به صورت ذیل در پوشه‌ی src\app اضافه می‌کنیم:
> ng g cl app.error-handler
با این خروجی
 installing class
  create src\app\app.error-handler.ts
سپس این کلاس را به نحو ذیل تکمیل خواهیم کرد:
import { ErrorHandler } from "@angular/core";

export class AppErrorHandler implements ErrorHandler {

  handleError(error: any): void {
    console.log("Error:", error);
  }
}
کلاس جدید AppErrorHandler از کلاس پایه ErrorHandler ارث بری می‌کند. بنابراین import آن‌را در ابتدای کار مشاهده می‌کنید. سپس باید متد handleError آن‌را با امضایی که مشاهده می‌کنید، پیاده سازی کنیم. فعلا با استفاده از console.log این خطا را در کنسول developer tools نمایش می‌دهیم.

اکنون نیاز است این ErrorHandler سفارشی را بجای نمونه‌ی اصلی به برنامه معرفی کنیم. برای این منظور به فایل src\app\app.module.ts مراجعه کرده و تغییرات ذیل را اعمال می‌کنیم:
import { NgModule, ErrorHandler } from "@angular/core";
import { AppErrorHandler } from "./app.error-handler";

@NgModule({
  providers: [
    { provide: ErrorHandler, useClass: AppErrorHandler }
  ]
ابتدا ErrorHandler به لیست imports اضافه شده‌است و همچنین محل تامین AppErrorHandler نیز مشخص گردیده‌است. سپس در قسمت providers ماژول جاری، از تعریف خاصی که ملاحظه می‌کنید، استفاده خواهد شد. به این ترتیب به Angular اعلام می‌کنیم، هرگاه نیازی به وهله‌ای از کلاس توکار ErrorHandler بود، وهله‌ای از کلاس سفارشی AppErrorHandler را مورد استفاده قرار بده.

اکنون برای آزمایش آن، در کدهای سمت سرور مطلب «ایجاد Drop Down List‌های آبشاری در Angular»، یک استثنای عمدی را قرار می‌دهیم:
[HttpGet("[action]/{categoryId:int}")]
public async Task<IActionResult> GetProducts(int categoryId)
{
   throw new Exception();
به این ترتیب هر زمانیکه گروهی انتخاب شد، دریافت محصولات آن گروه با خطا مواجه می‌شود.
برای اینکه AppErrorHandler، مورد استفاده قرار گیرد، قسمت err دریافت لیست محصولات را نیز حذف می‌کنیم (تا تبدیل به یک استثنای مدیریت نشده شود):
    this.productItemsService.getProducts(categoryId).subscribe(
      data => {
        this.products = data;
        this.isLoadingProducts = false;
      }// ,
      // err => {
      //   console.log("get error: ", err);
      //   this.isLoadingProducts = false;
      // }
    );
اکنون اگر برنامه را اجرا کنیم، چنین پیامی، در کنسول developer tools ظاهر می‌شود و مشخص است از فایل AppErrorHandler صادر شده‌است:



افزودن ToastyService به AppErrorHandler

در ادامه می‌خواهیم بجای console.log از ToastyService برای نمایش خطاهای مدیریت نشده‌ی برنامه در کلاس AppErrorHandler استفاده کنیم:
import { ToastyService, ToastOptions } from "ng2-toasty";
import { ErrorHandler } from "@angular/core";

export class AppErrorHandler implements ErrorHandler {

  constructor(private toastyService: ToastyService) {
  }

  handleError(error: any): void {
    // console.log("Error:", error);
    this.toastyService.error(<ToastOptions>{
      title: "Error!",
      msg: "Fatal error!",
      theme: "bootstrap",
      showClose: true,
      timeout: 5000
    });
  }
}
به همین منظور سرویس آن‌را به سازنده‌ی کلاس AppErrorHandler تزریق کرده و سپس از آن به نحو متداولی در متد handleError استفاده می‌کنیم. به این ترتیب بجای ده‌ها و یا صدها قسمت مدیریت err=>this.toastyService.error در برنامه، تنها یک مورد مدیریت مرکزی را خواهیم داشت.

مشکل اول!اکنون اگر برنامه را اجرا کنیم، در کنسول developer tools چنین خطایی ظاهر می‌شود:
 Uncaught Error: Can't resolve all parameters for AppErrorHandler: (?).
به این معنا که Angular قادر نیست وهله‌ای از AppErrorHandler را ایجاد کند؛ چون نمی‌داند که چگونه باید پارامتر سازنده‌ی ToastyService را وهله سازی و تزریق نماید. علت اینجا است که کار آغاز کلاس ویژه‌ی ErrorHandler سراسری، پیش از کار بارگذاری ماژول مرتبط با ToastyService انجام می‌شود. به همین جهت، این مورد جزو معدود مواردی است که باید به صورت دستی تزریق شود:
import { ErrorHandler, Inject } from "@angular/core";

export class AppErrorHandler implements ErrorHandler {

  constructor(
    @Inject(ToastyService) private toastyService: ToastyService
  ) {
  }
در اینجا توسط Inject decorator، کار تزریق دستی ToastyService انجام خواهد شد. اکنون اگر برنامه را مجدد اجرا کنیم، خطای قبلی برطرف شده‌؛ یعنی کلاس AppErrorHandler با موفقیت وهله سازی شده‌است.

مشکل دوم!اینبار برنامه را اجرا کنید. سپس گروهی را انتخاب نمائید. مشاهده می‌کنید که خطایی نمایش داده نشد؛ هرچند در کنسول developer tools می‌توان اثری از آن را مشاهده کرد. مجددا گروه دیگری را انتخاب کنید، در این بار دوم است که خطای ارائه شده‌ی توسط this.toastyService.error ظاهر می‌شود. توضیح آن نیاز به بررسی مفهومی به نام Zones در Angular دارد.


مفهوم Zones در Angular

زمانیکه متد this.toastyService.error در یک کامپوننت برنامه مورد استفاده قرار گرفت، به خوبی کار می‌کرد و در همان بار اول فراخوانی، پیام را نمایش می‌داد. اما با انتقال آن به کلاسAppErrorHandler ، این قابلیت از کار افتاد. علت اینجا است که زمینه‌ی اجرایی این قطعه کد، اکنون خارج از Zone یا ناحیه‌ی Angular است و به همین دلیل متوجه تغییرات آن نمی‌شود. Zone زمینه‌ی اجرایی اعمال async است و اگر به فایل package.json یک برنامه‌ی Angular دقت کنید، بسته‌ی zone.js، یکی از وابستگی‌های همراه آن است.
تغییرات حالت برنامه، توسط یکی از اعمال ذیل رخ می‌دهند:
الف) بروز رخ‌دادهایی مانند کلیک، ورود اطلاعات و یا ارسال فرم
ب) اعمال Ajax ایی
ج) استفاده از Timers مانند استفاده از setTimeout و  setInterval

هر سه مورد یاد شده از نوع async بوده و زمانیکه رخ می‌دهند، حالت برنامه را تغییر خواهند داد. Angular نیز تنها به این موارد علاقمند بوده و به آ‌ن‌ها در جهت به روز رسانی رابط کاربری برنامه واکنش نشان می‌دهد.
برای مثال this.toastyService.error دارای خاصیتی است به نام timeout: 5000 که در آن، مورد «ج» فوق رخ می‌دهد؛ یعنی یک Timer پس از 5 ثانیه سبب بسته شدن آن خواهد شد. به همین جهت است که اگر پیش از پایان این 5 ثانیه مجددا درخواست واکشی لیست محصولات یک گروه را بدهیم، خطای مربوطه مشاهده می‌شود. چون Angular زمینه‌ی اجرایی لازم را فراهم کرده (یا همان Zone در اینجا) و مجبور به واکنش به عملیات async از نوع Timer است.

برای دسترسی به امکانات کتابخانه‌ی zone.js، می‌توان از طریق تزریق سرویس آن به نام NgZone به سازنده‌ی کلاس شروع کرد:
import { ToastyService, ToastOptions } from "ng2-toasty";
import { ErrorHandler, Inject, NgZone } from "@angular/core";
import { LocationStrategy, PathLocationStrategy } from "@angular/common";

export class AppErrorHandler implements ErrorHandler {

  constructor(
    @Inject(NgZone) private ngZone: NgZone,
    @Inject(ToastyService) private toastyService: ToastyService,
    @Inject(LocationStrategy) private locationProvider: LocationStrategy
  ) {
  }

  handleError(error: any): void {
    // console.log("Error:", error);

    const url = this.locationProvider instanceof PathLocationStrategy ? this.locationProvider.path() : "";
    const message = error.message ? error.message : error.toString();
    this.ngZone.run(() => {
      this.toastyService.error(<ToastOptions>{
        title: "Error!",
        msg: `URL:${url} \n ERROR:${message}`,
        theme: "bootstrap",
        showClose: true,
        timeout: 5000
      });
    });

    // IMPORTANT: Rethrow the error otherwise it gets swallowed
    // throw error;
  }
}
در اینجا فراخوانی this.ngZone.runسبب می‌شود تا درخواست نمایش خطای رخ‌داده وارد Angular Zone شده و بلافاصله سبب نمایش آن گردد:
 


چند نکته
1- اگر می‌خواهید علاوه بر رخ‌دادگردانی سراسری خطاها، این خطاها را به محل اصلی آن‌ها نیز انتشار دهید، نیاز است سطر throw error را در انتهای متد handleError نیز ذکر کنید. در غیر اینصورت، کار در همینجا به پایان خواهد رسید و این خطاها دیگر منتشر نمی‌شوند.
2- روش دریافت URL جاری صفحه را نیز در اینجا مشاهده می‌کنید. این اطلاعات می‌توانند جهت ارسال به سرور برای ثبت و بررسی‌های بعدی مفید باشند.
3- مقدار new Error().stack معادل stack trace جاری است و تقریبا در تمام مرورگرهای جدید پشتیبانی می‌شود.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-template-driven-forms-lab-07.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس به ریشه‌ی پروژه وارد شده و دو پنجره‌ی کنسول مجزا را باز کنید. در اولی دستورات
>npm install>ng build --watch
و در دومی دستورات ذیل را اجرا کنید:
>dotnet restore>dotnet watch run
اکنون می‌توانید برنامه را در آدرس http://localhost:5000 مشاهده و اجرا کنید.

‫بررسی Bad code smell ها: کلاس بزرگ

$
0
0
این نوع کد بد بو در دسته بندی «کدهای متورم» قرار می‌گیرد. یکی از نتایج متورم شدن کدها، سخت شدن نگهداری آنهاست. بدیهی به نظر می‌رسد که نگهداری و اعمال تغییرات بر روی یک کلاس بزرگ، دشوار و زمان گیر خواهد بود. علارغم سادگی مفهوم این نوع کد بد بو، این مورد یکی از موارد پر تکرار درمحصول‌ها است.  
کلاس بزرگ کلاسی است که تعداد اعضای آن (فیلد، خصوصیت، متد) زیاد باشند و تعداد خطوط کد زیادی نیز داشته باشد. 

چرا چنین بویی به راه می‌افتد 

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

نشانه‌های این کد بد بو 

نشانه‌هایی که به تشخیص یک کلاس بزرگ کمک می‌کنند به صورت زیر هستند: 
  • تعداد خطوط زیاد: این معیار نسبت به فناوری و زبان برنامه نویسی مورد استفاده درمحصول متفاوت است؛ ولی در حالت کلی زمانیکه یک کلاس تعداد خطوط کدی بیشتر از 100 داشت، مشکلی بوجود آمده است. 
  • تعداد وضعیت‌های داخلی (در تعریف شیء گرایی) زیاد در یک کلاس، نشان دهنده بزرگی یک کلاس هستند.  
  • تعداد پارامترهای زیاد سازنده کلاس نشان دهنده متورم شدن کلاس هستند. معمولا مدیریت کردن تعداد وضعیت‌های داخلی زیاد منجر به دریافت تعداد زیاد پارامتر ورودی در سازنده می‌شوند. اگر قانون مربوط به تعداد پارامترهای یک متد را در نظر داشته باشیم و با فرض اینکه سازنده نیز یک متد است، حداکثر پارامترهای مناسب برای یک سازنده 4 خواهد بود. 
  • متغیرهایی وجود دارند که به صورت دسته‌ای پیشوند یا پسوند خاصی دارند. این پیشوندها یا پسوندها نشان دهنده مواردی هستند که احتمالا می‌توانند به کلاس مخصوص به خود انتقال داده شوند. زیرا از نظر منطقی ارتباطی بین آنها وجود دارد و مربوط به کلاس فعلی نمی‌شوند (زیرا اگر اینگونه بود نیازی به پیشوند یا پسوند نبود).


    مشکل این کد بد بو چیست؟ 

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


    چگونه این بو را رفع کنیم؟  

    دیدگاه کلی برای رفع چنین بویی تقسیم مسئولیت‌های موجود در یک کلاس بزرگ است. این تقسیم می‌تواند به صورت‌های زیر انجام شود:
    • ایجاد کلاسی مستقل برای هریک از مسئولیت‌های موجود در کلاس بزرگ 
    • ایجاد کلاسی پایه (Base class) برای انجام برخی از امور مشترک در کلاس 


    جمع بندی 

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

    ‫نوشتن اعتبارسنج‌های سفارشی برای فرم‌های مبتنی بر قالب‌ها در Angular

    $
    0
    0
    در مطلب «فرم‌های مبتنی بر قالب‌ها در Angular - قسمت چهارم - اعتبارسنجی ورودی‌ها» مشاهده کردیم که Angular در روش فرم‌های مبتنی بر قالب‌ها، تنها از 4 روش بومی اعتبارسنجی مرورگرها مانند ذکر ویژگی required برای فیلدهای اجباری، ویژگی‌های minlength و maxlength برای تعیین حداقل و حداکثر تعداد حروف مجاز قابل ورود در یک فیلد و از pattern برای کار با عبارات با قاعده پیشتیبانی می‌کند. برای بهبود این وضعیت در این مطلب قصد داریم روش تهیه اعتبارسنج‌های سفارشی مخصوص حالت فرم‌های مبتنی بر قالب‌ها را بررسی کنیم.


    تدارک مقدمات مثال این قسمت

    این مثال، در ادامه‌ی همین سری کار با فرم‌های مبتنی بر قالب‌ها است. به همین جهت ابتدا ماژول جدید CustomValidators را به آن اضافه می‌کنیم:
    >ng g m CustomValidators -m app.module --routing
    همچنین به فایل app.module.ts مراجعه کرده و CustomValidatorsModule را بجای CustomValidatorsRoutingModule در قسمت imports معرفی می‌کنیم. سپس به این ماژول جدید، کامپوننت فرم ثبت نام یک کاربر را اضافه خواهیم کرد:
    >ng g c CustomValidators/user-register
    که اینکار سبب به روز رسانی فایل custom-validators.module.ts و افزوده شدن UserRegisterComponent به قسمت declarations آن می‌شود.
    در ادامه کلاس مدل معادل فرم ثبت نام کاربران را تعریف می‌کنیم:
    >ng g cl CustomValidators/user
    با این محتوا:
    export class User {
      constructor(
        public username: string = "",
        public email: string = "", 
        public password: string = "", 
        public confirmPassword: string = "" 
      ) {}
    }
    در طراحی فرم HTML ایی آن نیاز است این موارد رعایت شوند:
    - ورود نام کاربری اجباری بوده و باید بین 5 تا 8 حرف باشد.
    - ورود ایمیل اجباری بوده و باید فرمت مناسبی نیز داشته باشد.
    - ورود کلمه‌ی عبور اجباری بوده و باید با confirmPassword تطابق داشته باشد.
    - ورود «کلمه‌ی عبور خود را مجددا وارد کنید» اجباری بوده و باید با password تطابق داشته باشد.



    تعریف اعتبارسنج سفارشی ایمیل‌ها

    هرچند می‌توان اعتبارسنجی ایمیل‌ها را توسط ویژگی استاندارد pattern نیز مدیریت کرد، اما جهت بررسی نحوه‌ی انتقال آن به یک اعتبارسنج سفارشی، کار را با ایجاد یک دایرکتیو مخصوص آن ادامه می‌دهیم:
    >ng g d CustomValidators/EmailValidator -m custom-validators.module
    این دستور علاوه بر ایجاد فایل جدید email-validator.directive.ts و تکمیل ساختار ابتدایی آن، کار به روز رسانی custom-validators.module.ts را نیز انجام می‌دهد. در این حالت به صورت خودکار قسمت declarations این ماژول با EmailValidatorDirective مقدار دهی می‌شود.
    در ادامه کدهای کامل این اعتبارسنج سفارشی را مشاهده می‌کنید:
    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]",
    در اینجا مطابق https://angular.io/guide/styleguide#style-02-08توصیه شده‌است که:
    الف) نام دایرکتیو باید با یک پیشوند شروع شود و این پیشوند در فایل angular-cli.json. به app تنظیم شده‌است:
    "apps": [
    {
       // ...
       "prefix": "app",
    این مساله در جهت مشخص کردن سفارشی بودن این دایرکتیو و همچنین کاهش احتمال تکرار نام‌ها توصیه شده‌است.
    ب) در اینجا formControlName، formControl و ngModel قید شده‌ی در کنار نام selector این دایرکتیو را نیز مشاهده می‌کنید. وجود آن‌ها به این معنا است که کلاس این دایرکتیو، به المان‌هایی که به آن‌ها ویژگی appEmailValidator اضافه شده‌است و همچنین آن المان‌ها از یکی از سه نوع ذکر شده هستند، اعمال می‌شود و در سایر موارد بی‌اثر خواهد بود. البته ذکر این سه نوع، اختیاری است و صرفا می‌توان نوشت:
     selector: "[appEmailValidator]"

    - پس از آن قسمت providers را مشاهده می‌کنید:
      providers: [
        {
          provide: NG_VALIDATORS,
          useExisting: EmailValidatorDirective,
          multi: true
        }
    کار قسمت multi آن این است که EmailValidatorDirective (یا همان کلاس جاری) را به لیست NG_VALIDATORS توکار (اعتبارسنج‌های توکار مبتنی بر قالب‌ها) اضافه می‌کند و سبب بازنویسی هیچ موردی نخواهد شد. بنابراین وجود این قسمت در جهت تکمیل تامین کننده‌های توکار Angular ضروری است.

    - سپس پیاده سازی اینترفیس توکار Validator را مشاهده می‌کنید:
     export class EmailValidatorDirective implements Validator {
    این اینترفیس جزو مجموعه‌ی فرم‌های مبتنی بر قالب‌ها است و از آن جهت نوشتن اعتبارسنج‌های سفارشی می‌توان استفاده کرد.
    برای پیاده سازی این اینترفیس، نیاز است متد اجباری ذیل را نیز افزود و تکمیل کرد:
     validate(element: AbstractControl): { [key: string]: any }
    کار این متد این است که المانی را که appEmailValidator به آن اعمال شده‌است، به عنوان پارامتر متد validate در اختیار کلاس جاری قرار می‌دهد. به این ترتیب می‌توان برای مثال به مقدار آن دسترسی یافت و سپس منطق سفارشی را پیاده سازی و یک خروجی key/value را بازگشت داد.
    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 };
    }
    برای مثال در اینجا مقدار فیلد ایمیل element.value توسط عبارت باقاعده‌ی نوشته شده بررسی می‌شود. اگر با این الگو انطباق داشته باشد، نال بازگشت داده می‌شود (اعلام عدم وجود مشکلی در اعتبارسنجی) و اگر خیر، یک شیء key/value دلخواه را می‌توان بازگشت داد.

    - اکنون که این دایرکتیو جدید طراحی و ثبت شده‌است (در قسمت 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 باشکست مواجه شود، اطلاعات بیشتری را در قسمت مقدار این کلید بازگشتی، ارائه می‌دهد:
    { minlength : {
         requiredLength : 3,
         actualLength : 1
       }
    }
    در کل اینکه چه چیزی را بازگشت دهید، بستگی به طراحی مدنظر شما دارد؛ برای نمونه در اینجا appEmailValidator (یک کلید و نام دلخواه است و هیچ الزامی ندارد که با نام selector این دایرکتیو یکی باشد)، به true تنظیم شده‌است:
     { appEmailValidator: true }
    بنابراین شرط تامین نوع خروجی، برقرار است. علت true بودن آن نیز مورد ذیل است:
    <div class="alert alert-danger"  *ngIf="email.errors.appEmailValidator">
    The entered email is not valid.</div>
    در اینجا اگر false را بازگشت دهیم، هرچند email.errors دارای کلید جدید appEmailValidator شده‌است، اما ngIf سبب رندر خطای اعتبارسنجی «ایمیل وارد شده معتبر نیست.» به علت false بودن نتیجه‌ی نهایی، نمی‌شود. یا حتی می‌توان بجای true یک رشته و یا یک شیء با توضیحات بیشتری را نیز تنظیم کرد؛ چون value این key/value به any تنظیم شده‌است و هر چیزی را می‌پذیرد.
    از دیدگاه اعتبارسنج فرم‌های مبتنی بر قالب‌ها، همینقدر که آرایه‌ی email.errors دارای عضو و کلید جدیدی شد، کار به پایان رسیده‌است و اعتبارسنجی المان را شکست خورده ارزیابی می‌کند. مابقی آن، اطلاعاتی است که برنامه نویس ارائه می‌دهد (بر اساس نیازهای نمایشی برنامه).


    تهیه اعتبارسنج سفارشی مقایسه‌ی کلمات عبور با یکدیگر

    در طراحی کلاس User که معادل فیلدهای فرم ثبت نام کاربران است، دو خاصیت کلمه‌ی عبور و تائید کلمه‌ی عبور را مشاهده می‌کنید:
    public password: string = "",
    public confirmPassword: string = ""
    Angular به همراه اعتبارسنج توکاری برای بررسی یکی بودن این دو نیست. به همین جهت نمونه‌ی سفارشی آن‌را همانند EmailValidatorDirective فوق تهیه می‌کنیم. ابتدا یک دایرکتیو جدید را به نام EqualValidator به ماژول custom-validators اضافه می‌کنیم:
    >ng g d CustomValidators/EqualValidator -m custom-validators.module
    که سبب ایجاد فایل جدید equal-validator.directive.ts و به روز رسانی قسمت declarations فایل custom-validators.module.ts با EqualValidatorDirective نیز می‌شود.

    در ادامه کدهای کامل آن‌را در ذیل مشاهده می‌کنید:
    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) {}
    به این ترتیب زمانیکه قرار است فیلد کلمه‌ی عبور را تعریف کنیم، ابتدا ویژگی appValidateEqual یا همان selector این اعتبارسنج به آن اضافه شده‌است تا کار فعال سازی ابتدایی صورت گیرد:
    <input #password="ngModel" required type="password" class="form-control"
    appValidateEqual compare-to="confirmPassword" name="password" [(ngModel)]="model.password">
    سپس Attribute یا ویژگی به نام compare-to نیز تعریف شده‌است. این compare-to همان نامی است که به Attribute@ نسبت داده شده‌است. سپس مقداری که به این ویژگی نسبت داده می‌شود، توسط خاصیت compareToControl دریافت خواهد شد.
    در اینجا محدودیتی هم از لحاظ تعداد ویژگی‌ها نیست و اگر قرار است این اعتبارسنج اطلاعات بیشتری را نیز دریافت کند می‌توان ویژگی‌های بیشتری را به سازنده‌ی آن نسبت داد.

    یک نکته:می‌توان نام این ویژگی را با نام 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
        });
    در اینجا element.value مقدار المان یا کنترل HTML جاری است که appValidateEqual به آن اعمال شده‌است.
    بر اساس مقدار خاصیت compareToControl که از ویژگی compare-to دریافت می‌شود، می‌توان به کنترل دوم، توسط element.root.get دسترسی یافت.

    - در ادامه‌ی کار، مقایسه‌ی ساده‌ای را مشاهده می‌کنید:
        if (otherControl && selfValue !== otherControl.value) {
          return {
            appValidateEqual: true // Or a string such as 'Password mismatch.' or an abject.
          };
        }
    اگر کنترل دوم یافت شد و همچنین مقدار آن با مقدار کنترل جاری یکی نبود، همان شیء key/value مورد انتظار متد validate، در جهت اعلام شکست اعتبارسنجی بازگشت داده می‌شود.

    - در پایان کدهای متد validate، چنین تنظیمی نیز قرار گرفته‌است:
        if (otherControl && otherControl.errors && selfValue === otherControl.value) {
          delete otherControl.errors["appValidateEqual"];
          if (!Object.keys(otherControl.errors).length) {
            otherControl.setErrors(null);
          }
        }
    
        return null;
    اعتبارسنج تعریف شده، فقط به کنترلی که هم اکنون در حال کار با آن هستیم اعمال می‌شود. اگر پیشتر کلمه‌ی عبوری را وارد کرده باشیم و سپس به فیلد تائید آن مراجعه کنیم، وضعیت اعتبارسنجی فیلد کلمه‌ی عبور قبلی به حالت غیرمعتبر تنظیم شده‌است. اما پس از تکمیل فیلد تائید کلمه‌ی عبور، هرچند وضعیت فیلد جاری معتبر است، اما هنوز وضعیت فیلد قبلی غیرمعتبر می‌باشد. برای رفع این مشکل، ابتدا کلید دلخواه appValidateEqual را از آن حذف می‌کنیم (همان کلیدی است که پیشتر در صورت مساوی نبودن مقدار فیلدها بازگشت داده شده‌است). حذف این کلید سبب نال شدن آرایه‌ی errors یک شیء نمی‌شود و همانطور که پیشتر عنوان شد، Angular تنها به همین مورد توجه می‌کند. بنابراین در ادامه کار، setErrors یا تنظیم آرایه‌ی errors به نال هم انجام شده‌است. در اینجا است که Angular فیلد دوم را نیز معتبر ارزیابی خواهد کرد.


    تکمیل کامپوننت فرم ثبت نام کاربران

    اکنون 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);
      }
    }
    در اینجا تنها کار مهمی که انجام شده‌است، ارائه‌ی خاصیت عمومی مدل، جهت استفاده‌ی از آن در قالب HTML ایی این کامپوننت است. بنابراین به فایل user-register.component.html مراجعه کرده و آن‌را نیز به صورت ذیل تکمیل می‌کنیم:

    ابتدای فرم
    <div class="container"><h3>Registration Form</h3><form #form="ngForm" (submit)="submitForm(form)" novalidate>
    در اینجا novalidate اضافه شده‌است تا اعتبارسنجی توکار مرورگرها با اعتبارسنجی سفارشی فرم جاری تداخل پیدا نکند. همچنین توسط یک template reference variable به وهله‌ی از فرم دسترسی یافته و آن‌را به متد submitForm کامپوننت ارسال کرده‌ایم.

    تکمیل قسمت ورود نام کاربری

    <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>
    اعتبارسنجی فیلد نام کاربری شامل سه قسمت بررسی errors.required، errors.minlength و errors.maxlength است.


    تکمیل قسمت ورود ایمیل

    <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>
    در اینجا نحوه‌ی استفاده‌ی از دایرکتیو جدید appEmailValidator را ملاحظه می‌کنید. این دایرکتیو ابتدا به المان فوق متصل و سپس نتیجه‌ی آن در قسمت ngIf، برای نمایش خطای متناظری بررسی شده‌است.


    تکمیل قسمت‌های ورود کلمه‌ی عبور و تائید آن

    <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>
    در اینجا نحوه‌ی اعمال دایرکتیو جدید appValidateEqual و همچنین ویژگی compare-to آن‌را به فیلدهای کلمه‌ی عبور و تائید آن مشاهده می‌کنید.
    همچنین خروجی آن نیز در قسمت ngIf آخر بررسی شده‌است و سبب نمایش خطای اعتبارسنجی متناسبی می‌شود.


    تکمیل انتهای فرم

    <button class="btn btn-primary" [disabled]="form.invalid" type="submit">Ok</button></form></div>
    در اینجا بررسی می‌شود که آیا فرم معتبر است یا خیر. اگر خیر، دکمه‌ی submit آن غیرفعال می‌شود و برعکس.



    کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-template-driven-forms-lab-08.zip
    برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس به ریشه‌ی پروژه وارد شده و دو پنجره‌ی کنسول مجزا را باز کنید. در اولی دستورات
    >npm install>ng build --watch
    و در دومی دستورات ذیل را اجرا کنید:
    >dotnet restore>dotnet watch run
    اکنون می‌توانید برنامه را در آدرس http://localhost:5000 مشاهده و اجرا کنید.

    ‫افزودن و اعتبارسنجی خودکار Anti-Forgery Tokens در برنامه‌های Angular مبتنی بر ASP.NET Core

    $
    0
    0
    Anti-forgery tokens یک مکانیزم امنیتی، جهت مقابله با حملات CSRF هستند. در برنامه‌های ASP.NET Core، فرم‌های دارای Tag Helper مانند asp-controller و asp-action به صورت خودکار دارای یک فیلد مخفی حاوی این token، به همراه تولید یک کوکی مخصوص جهت تعیین اعتبار آن خواهند بود. البته در برنامه‌های ASP.NET Core 2.0 تمام فرم‌ها، چه حاوی Tag Helpers باشند یا خیر، به همراه درج این توکن تولید می‌شوند.
    برای مثال در برنامه‌های ASP.NET Core، یک چنین فرمی:
    <form asp-controller="Manage" asp-action="ChangePassword" method="post">   <!-- Form details --> </form>
    به صورت ذیل رندر می‌شود که حاوی قسمتی از Anti-forgery token است و قسمت دیگر آن در کوکی مرتبط درج می‌شود:
    <form method="post" action="/Manage/ChangePassword">   <!-- Form details --> <input name="__RequestVerificationToken" type="hidden" value="CfDJ8NrAkSldwD9CpLR...LongValueHere!" /> </form>
    در این مطلب چگونگی شبیه سازی این عملیات را در برنامه‌های Angular که تمام تبادلات آن‌ها Ajax ایی است، بررسی خواهیم کرد.


    تولید خودکار کوکی‌های Anti-forgery tokens برای برنامه‌های Angular

    در سمت Angular، مطابق مستندات رسمی آن (^و ^)، اگر کوکی تولید شده‌ی توسط برنامه، دارای نام مشخص «XSRF-TOKEN» باشد، کتابخانه‌ی HTTP آن به صورت خودکار مقدار آن‌را استخراج کرده و به درخواست بعدی ارسالی آن اضافه می‌کند. بنابراین در سمت ASP.NET Core تنها کافی است کوکی مخصوص فوق را تولید کرده و به Response اضافه کنیم. مابقی آن توسط Angular به صورت خودکار مدیریت می‌شود.
    می‌توان اینکار را مستقیما داخل متد Configure کلاس آغازین برنامه انجام داد و یا بهتر است جهت حجیم نشدن این فایل و مدیریت مجزای این مسئولیت، یک میان‌افزار مخصوص آن‌را تهیه کرد:
    using System;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Antiforgery;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Http;
    
    namespace AngularTemplateDrivenFormsLab.Utils
    {
        public class AntiforgeryTokenMiddleware
        {
            private readonly RequestDelegate _next;
            private readonly IAntiforgery _antiforgery;
    
            public AntiforgeryTokenMiddleware(RequestDelegate next, IAntiforgery antiforgery)
            {
                _next = next;
                _antiforgery = antiforgery;
            }
    
            public Task Invoke(HttpContext context)
            {
                var path = context.Request.Path.Value;
                if (path != null && !path.StartsWith("/api/", StringComparison.OrdinalIgnoreCase))
                {
                    var tokens = _antiforgery.GetAndStoreTokens(context);
                    context.Response.Cookies.Append(
                          key: "XSRF-TOKEN",
                          value: tokens.RequestToken,
                          options: new CookieOptions
                          {
                              HttpOnly = false // Now JavaScript is able to read the cookie
                          });
                }
                return _next(context);
            }
        }
    
        public static class AntiforgeryTokenMiddlewareExtensions
        {
            public static IApplicationBuilder UseAntiforgeryToken(this IApplicationBuilder builder)
            {
                return builder.UseMiddleware<AntiforgeryTokenMiddleware>();
            }
        }
    }
    توضیحات تکمیلی:
    - در اینجا ابتدا سرویس IAntiforgery به سازنده‌ی کلاس میان افزار تزریق شده‌است. به این ترتیب می‌توان به سرویس توکار تولید توکن‌های Antiforgery دسترسی یافت. سپس از این سرویس جهت دسترسی به متد GetAndStoreTokens آن برای دریافت محتوای رشته‌ای نهایی این توکن استفاده می‌شود.
    - اکنون که به این توکن دسترسی پیدا کرده‌ایم، تنها کافی است آن‌را با کلید مخصوص XSRF-TOKEN که توسط Angular شناسایی می‌شود، به مجموعه‌ی کوکی‌های Response اضافه کنیم.
    - علت تنظیم مقدار خاصیت HttpOnly به false، این است که کدهای جاوا اسکریپتی Angular بتوانند به مقدار این کوکی دسترسی پیدا کنند.

    پس از تدارک این مقدمات، کافی است متد الحاقی کمکی UseAntiforgeryToken فوق را به نحو ذیل به متد Configure کلاس آغازین برنامه اضافه کنیم؛ تا کار نصب میان افزار AntiforgeryTokenMiddleware، تکمیل شود:
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
       app.UseAntiforgeryToken();


    پردازش خودکار درخواست‌های ارسالی از طرف Angular

    تا اینجا برنامه‌ی سمت سرور ما کوکی‌های مخصوص Angular را با کلیدی که توسط آن شناسایی می‌شود، تولید کرده‌است. در پاسخ، Angular این کوکی را در هدر مخصوصی به نام «X-XSRF-TOKEN» به سمت سرور ارسال می‌کند (ابتدای آن یک X اضافه‌تر دارد).
    به همین جهت به متد ConfigureServices کلاس آغازین برنامه مراجعه کرده و این هدر مخصوص را معرفی می‌کنیم تا دقیقا مشخص گردد، این توکن از چه قسمتی باید جهت پردازش استخراج شود:
    public void ConfigureServices(IServiceCollection services)
    {
          services.AddAntiforgery(x => x.HeaderName = "X-XSRF-TOKEN");
          services.AddMvc();
    }

    یک نکته:اگر می‌خواهید این کلیدهای هدر پیش فرض Angular را تغییر دهید، باید یک CookieXSRFStrategyسفارشی را برای آن تهیه کنید.


    اعتبارسنجی خودکار Anti-forgery tokens در برنامه‌های ASP.NET Core

    ارسال کوکی اطلاعات Anti-forgery tokens و سپس دریافت آن توسط برنامه، تنها یک قسمت از کار است. قسمت بعدی، بررسی معتبر بودن آن‌ها در سمت سرور است. روش متداول انجام اینکار‌، افزودن ویژگی [ValidateAntiForgeryToken]  به هر اکشن متد مزین به [HttpPost] است:
      [HttpPost] 
      [ValidateAntiForgeryToken] 
      public IActionResult ChangePassword() 
      { 
        // ... 
        return Json(…); 
      }
    هرچند این روش کار می‌کند، اما در ASP.NET Core، فیلتر توکار دیگری به نام AutoValidateAntiForgeryToken نیز وجود دارد. کار آن دقیقا همانند فیلتر ValidateAntiForgeryToken است؛ با این تفاوت که از حالت‌های امنی مانند GET و HEAD صرفنظر می‌کند. بنابراین تنها کاری را که باید انجام داد، معرفی این فیلتر توکار به صورت یک فیلتر سراسری است، تا به تمام اکشن متدهای HttpPost برنامه به صورت خودکار اعمال شود:
    public void ConfigureServices(IServiceCollection services)
    {
           services.AddAntiforgery(x => x.HeaderName = "X-XSRF-TOKEN");
           services.AddMvc(options =>
           {
               options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
           });
    }
    به این ترتیب دیگر نیازی نیست تا ویژگی ValidateAntiForgeryToken را به تک تک اکشن متدهای از نوع HttpPost برنامه به صورت دستی اعمال کرد.

    یک نکته:در این حالت بررسی سراسری، اگر در موارد خاصی نیاز به این اعتبارسنجی خودکار نبود، می‌توان از ویژگی [IgnoreAntiforgeryToken] استفاده کرد.


    آزمایش برنامه

    برای آزمایش مواردی را که تا کنون بررسی کردیم، همان مثال «فرم‌های مبتنی بر قالب‌ها در Angular - قسمت پنجم - ارسال اطلاعات به سرور» را بر اساس نکات متدهای ConfigureServices و Configure مطلب جاری تکمیل می‌کنیم. سپس برنامه را اجرا می‌کنیم:


    همانطور که ملاحظه می‌کنید، در اولین بار درخواست برنامه، کوکی مخصوص Angular تولید شده‌است.
    در ادامه اگر فرم را تکمیل کرده و ارسال کنیم، وجود هدر ارسالی از طرف Angular مشخص است و همچنین خروجی هم با موفقیت دریافت شده‌است:



    کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-template-driven-forms-lab-09.zip
    برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس به ریشه‌ی پروژه وارد شده و دو پنجره‌ی کنسول مجزا را باز کنید. در اولی دستورات
    >npm install>ng build --watch
    و در دومی دستورات ذیل را اجرا کنید:
    >dotnet restore>dotnet watch run
    اکنون می‌توانید برنامه را در آدرس http://localhost:5000 مشاهده و اجرا کنید.

    ‫راه‌اندازی Http Interceptor در Angular

    $
    0
    0
    ماژول Http در Angular، برای برقراری ارتباط بین کلاینت و سمت سرور، مورد استفاده قرار می‌گیرد. معمولا هنگام ساخت درخواست‌های Http، یکسری کدهای تکراری برای تنظیم هدر (برای اعتبارسنجی و همچنین تنظیمات دیگر) نوشته می‌شوند که در هر درخواست یکسان هستند. همچنین بعد از آمدن جواب (Response) از سمت سرور نیز یکسری کدهای تکراری جهت برسی کد response و یا تغییر فرمت اطلاعات رسیده، به ساختار مورد توافق نوشته خواهند شد.

    برای مثال در صورتیکه در برنامه خود از اعتبار سنجی مبتنی بر توکن (Token Base Authentication) استفاده می‌کنید، قبل از ارسال هر درخواست (request)، کدهایی مشابه کد زیر باید نوشته شوند:
    let headers = new Headers();
    let token = localStorage.getItem('token');
    headers.append('Authorization', 'bearer ' + token);
    this.http.get('/api/controller/action', { headers: headers })

    همچنین فرض کنید بعد از رسیدن جواب هر درخواست، می‌خواهید response code را چک کنید و خطاهای احتمالی را مدیریت کنید. مثلا درصورت دریافت کد 401، کاربر را به صفحه «ورود» و با دریافت کد 404 آنرا را به صفحه «یافت نشد» هدایت کنید و یا با دریافت کد 403 یا 500 پیغام مناسبی را نمایش دهید. بدیهی است در این صورت بعد از هر آمدن پاسخ از سمت سرور (response)، این کدها بایستی تکرار شوند.

    جهت پرهیز از این کدهای تکراری، می‌توان برای ماژول Http، یک interceptor واحد درنظر گرفت که تمامی کدهای تکراری را تنها یکبار داخل آن پیاده سازی کرد. مزیت این روش، مدیریت راحت کد، کاهش پیچیدگی‌ها و همچنین حذف کدهای تکراری و یکسان سازی آنها است.
    هرچند در Angular دیگر به مانند Angular 1.x مفهوم intercept بر روی Http را به صورت توکار نداریم، ولی به دلایل زیر ما نیاز به پیاده سازی interceptor برای ماژول Http را داریم:
    - تنظیم هدرهای سفارشی و اصلاح آدرس، قبل از ارسال درخواست به سمت سرور
    - تنظیم token در هدر درخواست، جهت اعتبار سنجی
    - مدیریت سراسری خطاهای Http
    - انجام هرگونه عملیات crosscutting

    حالا که Angular مفهموم intercept را برای ماژول Http خود به صورت توکار درنظر نگرفته است، راه‌حل چیست؟ بهترین راه‌حل برای پیاده سازی موارد مطرح شده در بالا، ارث بری و یا گسترش (extend) مستقیم ماژول Http است:
    import { Injectable } from "@angular/core";
    import { ConnectionBackend, RequestOptions, Request, RequestOptionsArgs, Response, Http, Headers } from "@angular/http";
    import { Observable } from "rxjs/Rx";
    import 'rxjs/add/operator/catch';
    import 'rxjs/add/observable/throw';
    
    @Injectable()
    export class InterceptedHttp extends Http {
        constructor(backend: ConnectionBackend, defaultOptions: RequestOptions) {
            super(backend, defaultOptions);
        }
    
        request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
            // اصلاح url
            if (typeof (url) === 'string')
                url = this.updateUrl((url as string));
            else
                url.url = this.updateUrl((url as Request).url);
    
            return super.request(url, this.getRequestOptionArgs(options)).catch((err: Response) => {
                // Exception handling
                switch (err.status) {
                    case 400:
                        console.log('interceptor: 400');
                        console.log(err);
                        break;
                    case 404:
                        console.log('interceptor: 404');
                        console.log(err);
                        break;
                    case 500:
                        console.log('interceptor: 500');
                        console.log(err);
                        break;
                    case 401:
                        console.log('interceptor: 401');
                        console.log(err);
                        break;
                    case 403:
                        console.log('interceptor: 403');
                        console.log(err);
                        break;
                    default:
                        console.log('interceptor: ' + err.status);
                        console.log(err);
                }
                return Observable.throw(err);
    
            });
        }
    
        private updateUrl(req: string) {
            return `http://localhost:61366/api/${req}`
        }
    
        private getRequestOptionArgs(options?: RequestOptionsArgs): RequestOptionsArgs {
            if (options == null) {
                options = new RequestOptions();
            }
            if (options.headers == null) {
                options.headers = new Headers();
            }
            // هدر درخواست تنظیم
            let token = localStorage.getItem('token');
            options.headers.append('Authorization', 'bearer ' + token);
    
    
            return options;
        }
    }
    تمامی افعال Http، از جمله get ،post ،put ،delete ،patch ،head و options در نهایت از متد request موجود در ماژول http برای ارسال درخواست خود استفاده می‌کنند. به همین جهت تمامی عملیات crosscutting را در این متد پیاده سازی کرده‌ایم. به علاوه قبل از ارسال درخواست، توسط متد updateUrl آدرس url خود را اصلاح کرده‌ایم. همچنین توسط متد getRequestOptionArgs هدر درخواست را جهت اعتبار سنجی مقداردهی کرده‌ایم. در اینجا بعد از ارسال درخواست و آمدن response از سمت سرور، توسط catch خطاهای احتمالی را برسی کرده‌ایم.

    نکته: به عنوان مثال، در صورتیکه قصد دارید، برای درخواست‌هایی از جنس get، هدر متفاوتی نسبت به دیگر درخواست‌ها داشته باشید، آنگاه پیاده سازی عملیات اصلاح هدر در متد request جواب کار را نخواهد داد. برای حل این موضوع می‌توانید به جای اصلاح header در متد request، تمامی متدهای get ،post، put ،delete ،patch ،head و options را باز نویسی کرده و در هرکدام از این متدها اینکار را انجام دهید.

    حالا با تغییر قسمت providers در ماژول اصلی برنامه به شکل زیر، Angular را مجبور می‌کنیم بجای استفاده از ماژول Http توکار خود، از ماژول جدید InterceptedHttp استفاده کند:
    //…
    providers: [{
            provide: Http,
            useFactory: (backend: XHRBackend, options: RequestOptions) => {
                return new InterceptedHttp(backend, options);
            },
            deps: [XHRBackend, RequestOptions],
        }],
    //…

    همه چیز آماده است. اکنون کافی است ماژول Http را در سرویس یا کامپوننت‌های خود تزریق کرده و درخواست‌های Http را بدون هیچگونه نوشتن کد اضافی برای تنظیم هدر و غیره (با فرض اینکه تمامی آنها در متد request از ماژول http نوشته شده‌اند)، به مانند قبل صادر کنید. برای نمونه کد زیر را ببینید.
    import { Http, URLSearchParams } from '@angular/http';
    
    //…
    constructor(private _http: Http) { }
    
    ngOnInit() {
        let urlSearchParams: URLSearchParams = new URLSearchParams();
        urlSearchParams.append('page', page.toString());
        urlSearchParams.append('count', count.toString());
        let params = urlSearchParams.toString();
        this._http.get(`/cars`, { params: params })
            .subscribe(result => {
                console.log('service: Succ');
                this.cars = result.json();
            }, err => {
                console.log('service: error');
            });
    }
    //…
    با اینکه Angular از interceptor پشتیبانی نمی‌کند، ولی کتابخانه‌هایی برای ایجاد قابلیت مشابه interceptor به وجود آمده‌اند که برخی از آنها عبارتند از:  angular2-cool-http، ng2-http-interceptor، ng2-interceptors . به جای extend مستقیم ماژول Http توسط خودتان، اینکار را می‌توانید به این کتابخانه‌ها بسپارید.

    ‫بررسی روش آپلود فایل‌ها از طریق یک برنامه‌ی Angular به یک برنامه‌ی ASP.NET Core

    $
    0
    0
    پیشنیازها
    «بررسی روش آپلود فایل‌ها در ASP.NET Core»
    «ارسال فایل و تصویر به همراه داده‌های دیگر از طریق jQuery Ajax»
    - در مطلب اول، روش دریافت فایل‌ها از کلاینت، در سمت سرور و ذخیره سازی آن‌ها در یک برنامه‌ی ASP.NET Core بررسی شده‌است که کلیات آن در اینجا نیز صادق است.
    - در مطلب دوم، روش کار با FormDataاستاندارد بررسی شده‌است. هرچند در مطلب جاری از jQuery استفاده نمی‌شود، اما نکات نحوه‌ی کار با شیء FormData استاندارد، در اینجا نیز یکی است.


    تدارک مقدمات مثال این قسمت

    این مثال در ادامه‌ی همین سری کار با فرم‌های مبتنی بر قالب‌ها است. به همین جهت ابتدا ماژول جدید UploadFile را به آن اضافه می‌کنیم:
    >ng g m UploadFile -m app.module --routing
    همچنین به فایل app.module.ts مراجعه کرده و UploadFileModule را بجای UploadFileRoutingModule در قسمت imports معرفی می‌کنیم. سپس به این ماژول جدید، کامپوننت فرم ثبت یک درخواست پشتیبانی را اضافه خواهیم کرد:
    >ng g c UploadFile/UploadFileSimple
    که اینکار سبب به روز رسانی فایل upload-file.module.ts و افزوده شدن UploadFileSimpleComponent به قسمت declarations آن می‌شود.
    در ادامه کلاس مدل معادل فرم ثبت نام یک درخواست پشتیبانی را تعریف می‌کنیم:
    >ng g cl UploadFile/Ticket
    با این محتوا:
    export class Ticket {
      constructor(public description: string = "") {}
    }
    در اینجا Ticket تعریف شده دارای یک خاصیت توضیحات است و این فرم به همراه فیلد ارسال چندین فایل نیز می‌باشد که نیازی به درج آن‌ها در کلاس فوق نیست:



    ایجاد مقدمات کامپوننت UploadFileSimple و قالب آن

    پس از ایجاد ساختار کلاس Ticket، یک وهله از آن‌را به نام model ایجاد کرده و در اختیار قالب آن قرار می‌دهیم:
    import { Ticket } from "./../ticket";
    
    export class UploadFileSimpleComponent implements OnInit {
      model = new Ticket();
    سپس قالب این کامپوننت و یا همان فایل upload-file-simple.component.html را به صورت ذیل تکمیل می‌کنیم:
    <div class="container"><h3>Support Form</h3><form #form="ngForm" (submit)="submitForm(form)" novalidate><div class="form-group" [class.has-error]="description.invalid && description.touched"><label class="control-label">Description</label><input #description="ngModel" required type="text" class="form-control"
            name="description" [(ngModel)]="model.description"><div *ngIf="description.invalid && description.touched"><div class="alert alert-danger"  *ngIf="description.errors.required">
              description is required.</div></div></div><div class="form-group"><label class="control-label">Screenshot(s)</label><input #screenshotInput required type="file" multiple (change)="fileChange($event)"
            class="form-control" name="screenshot"></div><button class="btn btn-primary" [disabled]="form.invalid" type="submit">Ok</button></form></div>
    در اینجا ابتدا فیلد توضیحات درخواست جدید، ارائه و به خاصیت model.description متصل شده‌است. همچنین این فیلد با ویژگی required مزین، و اجباری بودن آن بررسی گردیده‌است.
    سپس در انتها، فیلد آپلود را مشاهده می‌کنید؛ با این ویژگی‌ها:
    الف) ngModel ایی به آن متصل نشده‌است؛ چون روش کار با آن متفاوت است.
    ب) یک template reference variable به نام screenshotInput# در آن تعریف شده‌است. از این متغیر، در کامپوننت قالب استفاده خواهیم کرد.
    ج) به رخ‌داد change این کنترل، متد fileChange متصل شده‌است که رخ‌داد جاری را نیز دریافت می‌کند.
    د) ذکر ویژگی استاندارد multiple را نیز در اینجا مشاهده می‌کنید. وجود آن سبب خواهد شد تا کاربر بتواند چندین فایل را با هم انتخاب کند. اگر نیازی به ارسال چندین فایل نیست، این ویژگی را حذف کنید.


    دسترسی به المان ارسال فایل در کامپوننت متناظر

    تا اینجا یک المان ارسال فایل را به فرم، اضافه کرده‌ایم. اما چگونه باید به فایل‌های آن برای ارسال به سرور دسترسی پیدا کنیم؟
    برای این منظور در ادامه دو روش را بررسی خواهیم کرد:

    1) دسترسی به المان ارسال فایل از طریق رخ‌داد change
    در تعریف فیلد ارسال فایل، اتصال به رخ‌داد change تعریف شده‌است:
     (change)="fileChange($event)"
    معادل آن در سمت کامپوننت متناظر، به صورت ذیل است:
    fileChange(event) {
        const filesList: FileList = event.target.files;
        console.log("fileChange() -> filesList", filesList);
    }
    همانطور که مشاهده می‌کنید، event.target، امکان دسترسی مستقیم به المان متناظری را در قالب کامپوننت میسر می‌کند. سپس می‌توان به خاصیت files آن دسترسی یافت.


    در اینجا ساختار شیء استاندارد FileList و اجزای آن‌را مشاهده می‌کنید. برای مثال چون دو فایل انتخاب شده‌است، این لیست به همراه یک خاصیت طول و دو شیء File است.

    تعاریف این اشیاء استاندارد، در فایل ذیل قرار دارند و به همین جهت است که VSCode، بدون نیاز به تنظیمات دیگری، آن‌ها را شناسایی و intellisense متناظری را مهیا می‌کند:
     C:\Program Files (x86)\Microsoft VS Code\resources\app\extensions\node_modules\typescript\lib\lib.dom.d.ts
    همچنین اگر به فایل tsconfig.json پروژه نیز مراجعه کنید، یک چنین تعاریفی در آن قرار دارند:
    {
        "lib": [
          "es2016",
          "dom"
        ]
      }
    }
    وجود و تعریف کتابخانه‌ی dom است که سبب کامپایل شدن کدهای فوق، بدون بروز هیچگونه خطایی می‌شود.


    2) دسترسی به المان آپلود فایل از طریق یک template reference variable
    در حین تعریف المان فایل در فرم برنامه، متغیر screenshotInput# نیز ذکر شده‌است. می‌توان به یک چنین متغیرهایی در کامپوننت متناظر به روش ذیل دسترسی یافت:
    import { Component, OnInit, ViewChild, ElementRef } from "@angular/core";
    
    export class UploadFileSimpleComponent implements OnInit {
      @ViewChild("screenshotInput") screenshotInput: ElementRef;
    
      submitForm(form: NgForm) {
        const fileInput: HTMLInputElement = this.screenshotInput.nativeElement;
        console.log("fileInput.files", fileInput.files);
      }
    ابتدا یک خاصیت جدید را به نام screenshotInput از نوع ElementRef که در angular/core@ تعریف شده‌است، اضافه می‌کنیم. سپس برای اتصال آن به template reference variable ایی به نام screenshotInput، از ویژگی به نام ViewChild، با پارامتری مساوی نام همین متغیر، استفاده خواهیم کرد.
    اکنون خاصیت screenshotInput کامپوننت، به متغیری به همین نام در قالب متناظر با آن متصل شده‌است. بنابراین با استفاده از خاصیت nativeElement آن همانند کدهایی که در متد submitForm فوق ملاحظه می‌کنید، می‌توان به خاصیت files این کنترل ارسال فایل‌ها دسترسی یافت.
    نوع جدید و استاندارد HTMLInputElement نیز در فایل lib.dom.d.ts که پیشتر معرفی شد، ثبت شده‌است.


    ارسال فرم درخواست پشتیبانی به سرور

    تا اینجا فرمی را تشکیل داده و همچنین به فیلد file آن دسترسی پیدا کردیم. اکنون می‌خواهیم این اطلاعات را به سمت سرور ارسال کنیم. برای این منظور، سرویس جدیدی را ایجاد خواهیم کرد:
    >ng g s UploadFile/UploadFileSimple -m upload-file.module
    که سبب به روز رسانی خودکار قسمت providers فایل upload-file.module.ts نیز می‌شود.
    در ادامه کدهای کامل این سرویس را مشاهده می‌کنید:
    import { Http, RequestOptions, Response, Headers } from "@angular/http";
    import { Injectable } from "@angular/core";
    import { Observable } from "rxjs/Observable";
    import "rxjs/add/operator/do";
    import "rxjs/add/operator/catch";
    import "rxjs/add/observable/throw";
    import "rxjs/add/operator/map";
    import "rxjs/add/observable/of";
    
    import { Ticket } from "./ticket";
    
    @Injectable()
    export class UploadFileSimpleService {
      private baseUrl = "api/SimpleUpload";
    
      constructor(private http: Http) {}
    
      private extractData(res: Response) {
        const body = res.json();
        return body || {};
      }
    
      private handleError(error: Response): Observable<any> {
        console.error("observable error: ", error);
        return Observable.throw(error.statusText);
      }
    
      postTicket(ticket: Ticket, filesList: FileList): Observable<any> {
        if (!filesList || filesList.length === 0) {
          return Observable.throw("Please select a file.");
        }
    
        const formData: FormData = new FormData();
    
        for (const key in ticket) {
          if (ticket.hasOwnProperty(key)) {
            formData.append(key, ticket[key]);
          }
        }
    
        for (let i = 0; i < filesList.length; i++) {
          formData.append(filesList[i].name, filesList[i]);
        }
    
        const headers = new Headers();
        headers.append("Accept", "application/json");
        const options = new RequestOptions({ headers: headers });
    
        return this.http
          .post(`${this.baseUrl}/SaveTicket`, formData, options)
          .map(this.extractData)
          .catch(this.handleError);
      }
    }
    توضیحات تکمیلی:
    روش کار با فرم‌هایی که فیلدهای ارسال فایل را به همراه دارند، متفاوت است با روش کار با فرم‌های معمولی. در فرم‌های معمولی، اصل شیء Ticket را به متد this.http.post واگذار می‌کنیم. مابقی آن خودکار است. در اینجا باید شیء استاندارد FormData را تشکیل داده و سپس اطلاعات را از طریق آن ارسال کنیم:
    الف) افزودن مقادیر خواص شیء Ticket به FormData
      postTicket(ticket: Ticket, filesList: FileList): Observable<any> {
        const formData: FormData = new FormData();
    
        for (const key in ticket) {
          if (ticket.hasOwnProperty(key)) {
            formData.append(key, ticket[key]);
          }
        }
    با استفاده از حلقه‌ی for می‌توان بر روی خواص یک شیء جاوا اسکریپتی حرکت کرد. به این ترتیب می‌توان نام و مقدار آن‌ها را یافت و سپس به formData به صورت key/value افزود.

    ب) افزودن فایل‌ها به شیء FormData
    پس از افزودن اطلاعات ticket به FormData، اکنون نوبت به افزودن فایل‌های فرم است:
        for (let i = 0; i < filesList.length; i++) {
          formData.append(filesList[i].name, filesList[i]);
        }
    این مورد نیز به سادگی تشکیل یک حلقه، بر روی خاصیت files المان آپلود فایل است. به همین جهت بود که به دو روش سعی کردیم، به این خاصیت دسترسی پیدا کنیم.

    یک نکته:چون در اینجا کلید اضافه شده، نام فایل است، دیگر نمی‌توان در سمت سرور از روش model binding استفاده کرد. چون این نام دیگر ثابت نیست و هربار می‌تواند متغیر باشد (در حالت model binding دقیقا مشخص است که کلید مشخصی قرار است به سرور ارسال شود و بر همین اساس، نام خاصیت یا پارامتر سمت سرور تعیین می‌گردد). به همین جهت در سمت سرور برای دسترسی به این مجموعه، از روش Request.Form.Files استفاده می‌کنیم.

    ج) ارسال اطلاعات نهایی به سرور
    اکنون که formData را بر اساس اطلاعات اضافی ticket و فایل‌های متصل به آن تشکیل دادیم، روش ارسال آن به سرور همانند قبل است:
        const headers = new Headers();
        headers.append("Accept", "application/json");
        const options = new RequestOptions({ headers: headers });
    
        return this.http
          .post(`${this.baseUrl}/SaveTicket`, formData, options)
          .map(this.extractData)
          .catch(this.handleError);

    یک نکته:در اینجا در روش استفاده از formData نباید Content-Type را به multipart/form-data  تنظیم کرد. در غیراینصورت خطای Missing content-type boundary error را دریافت می‌کنید.


    تکمیل کامپوننت ارسال درخواست پشتیبانی

    پس از تکمیل سرویس ارسال اطلاعات به سمت سرور، اکنون نوبت به استفاده‌ی از آن در کامپوننت ارسال فرم درخواست پشتیبانی است. بنابراین ابتدا این سرویس جدید را به سازنده‌ی UploadFileSimpleComponent تزریق می‌کنیم:
    import { UploadFileSimpleService } from "./../upload-file-simple.service";
    
    export class UploadFileSimpleComponent implements OnInit {
      constructor(private uploadService: UploadFileSimpleService  ) {}
    و سپس متد submitForm چنین شکلی را پیدا می‌کند:
      submitForm(form: NgForm) {
        const fileInput: HTMLInputElement = this.screenshotInput.nativeElement;
        console.log("fileInput.files", fileInput.files);
    
        this.uploadService
          .postTicket(this.model, fileInput.files)
          .subscribe(data => {
            console.log("success: ", data);
          });
      }
    در اینجا this.model حاوی اطلاعات شیء ticket است (برای مثال اطلاعات توضیحات آن) و fileInput.files امکان دسترسی به اطلاعات فایل‌های انتخابی توسط کاربر را می‌دهد. پس از آن فراخوانی متدهای this.uploadService.postTicket و subscribe، سبب ارسال این اطلاعات به سمت سرور می‌شوند.


    دریافت فرم درخواست پشتیبانی در سمت سرور و ذخیره‌ی فایل‌های آن‌

    کدهای کامل SimpleUpload که در سرویس فوق مشخص شده‌است، به صورت ذیل هستند. ابتدا مدل Ticket مشخص شده‌است:
    namespace AngularTemplateDrivenFormsLab.Models
    {
        public class Ticket
        {
            public int Id { set; get; }
            public string Description { set; get; }
        }
    }
    و سپس کنترلر ذخیره سازی اطلاعات Ticket را مشاهده می‌کنید:
    using System.IO;
    using System.Threading.Tasks;
    using AngularTemplateDrivenFormsLab.Models;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Mvc;
    
    namespace AngularTemplateDrivenFormsLab.Controllers
    {
        [Route("api/[controller]")]
        public class SimpleUploadController : Controller
        {
            private readonly IHostingEnvironment _environment;
            public SimpleUploadController(IHostingEnvironment environment)
            {
                _environment = environment;
            }
    
            [HttpPost("[action]")]
            public async Task<IActionResult> SaveTicket(Ticket ticket)
            {
                //TODO: save the ticket ... get id
                ticket.Id = 1001;
    
                var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads");
                if (!Directory.Exists(uploadsRootFolder))
                {
                    Directory.CreateDirectory(uploadsRootFolder);
                }
    
                var files = Request.Form.Files;
                foreach (var file in files)
                {
                    //TODO: do security checks ...!
    
                    if (file == null || file.Length == 0)
                    {
                        continue;
                    }
    
                    var filePath = Path.Combine(uploadsRootFolder, file.FileName);
                    using (var fileStream = new FileStream(filePath, FileMode.Create))
                    {
                        await file.CopyToAsync(fileStream).ConfigureAwait(false);
                    }
                }
    
                return Created("", ticket);
            }
        }
    }
    توضیحات تکمیلی
    - تزریق IHostingEnvironment در سازنده‌ی کلاس کنترلر، سبب می‌شود تا از طریق خاصیت WebRootPath آن، به مسیر wwwroot سایت دسترسی پیدا کنیم و فایل‌های نهایی را در آنجا ذخیره سازی کنیم.
    - همانطور که ملاحظه می‌کنید، هنوز هم model binding کار کرده و می‌توان شیء Ticket را به نحو متداولی دریافت کرد:
     SaveTicket(Ticket ticket)
    اما همانطور که عنوان شد، چون در حلقه‌ی افزودن فایل‌ها در سمت کلاینت، کلید نام این فایل‌ها هربار متفاوت است:
     formData.append(filesList[i].name, filesList[i]);
    مجبور هستیم در سمت سرور بر روی Request.Form.Files یک حلقه را تشکیل داده و تمام فایل‌های رسیده را پردازش کنیم:
    var files = Request.Form.Files;
    foreach (var file in files)



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

    ‫اعتبارسنجی از راه دور در فرم‌های مبتنی بر قالب‌های Angular

    $
    0
    0
    در پروژه angular2-validations، یک نمونه پیاده سازی اعتبارسنجی از راه دور یا RemoteValidationرا می‌توانید مشاهده کنید. این پیاده سازی مبتنی بر Promiseها است. در مطلب جاری پیاده سازی دیگری را بر اساس Observableها مشاهده خواهید کرد و همچنین ساختار آن شبیه به ساختار remote validation در ASP.NET MVC و jQuery Validator طراحی شده‌است.


    نگاهی به ساختار طراحی اعتبارسنجی از راه دور در ASP.NET MVC و jQuery Validator

    در نگارش‌های مختلف ASP.NET MVC و ASP.NET Core، ویژگی Remote سمت سرور، سبب درج یک چنین ویژگی‌هایی در سمت کلاینت می‌شود:
    data-val-remote="کلمه عبور وارد شده را راحت می&zwnj;توان حدس زد!" 
    data-val-remote-additionalfields="*.Password1" 
    data-val-remote-type="POST" 
    data-val-remote-url="/register/checkpassword"
    که شامل موارد ذیل است:
    - متن نمایشی خطای اعتبارسنجی.
    - تعدادی فیلد اضافی که در صورت نیز از فرم استخراج می‌شوند و به سمت سرور ارسال خواهند شد.
    - نوع روش ارسال اطلاعات به سمت سرور.
    - یک URL که مشخص می‌کند، این اطلاعات باید به کدام اکشن متد در سمت سرور ارسال شوند.

    سمت سرور هم می‌تواند یک true یا false را بازگشت دهد و مشخص کند که آیا اطلاعات مدنظر معتبر نیستند یا هستند.
    شبیه به یک چنین ساختاری را در ادامه با ایجاد یک دایرکتیو سفارشی اعتبارسنجی برنامه‌های Angular تدارک خواهیم دید.


    ساختار اعتبارسنج‌های سفارشی async در Angular

    در مطلب «نوشتن اعتبارسنج‌های سفارشی برای فرم‌های مبتنی بر قالب‌ها در Angular» جزئیات نوشتن اعتبارسنج‌های متداول فرم‌های Angular را بررسی کردیم. این نوع اعتبارسنج‌ها چون اطلاعاتی را به صورت Ajax ایی به سمت سرور ارسال نمی‌کنند، با پیاده سازی اینترفیس Validator تهیه خواهند شد:
     export class EmailValidatorDirective implements Validator {
    اما زمانیکه نیاز است اطلاعاتی مانند نام کاربری یا ایمیل او را به سرور ارسال کنیم و در سمت سرور، پس از جستجوی در بانک اطلاعاتی، منحصربفرد بودن آن‌ها مشخص شود یا خیر، دیگر این روش همزمان پاسخگو نخواهد بود. به همین جهت اینبار اینترفیس دیگری به نام AsyncValidator برای انجام اعمال async و Ajax ایی در Angular تدارک دیده شده‌است:
     export class RemoteValidatorDirective implements AsyncValidator {
    در این حالت امضای متد validate این اینترفیس به صورت ذیل است:
    validate(c: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null>;
    یعنی در اینجا هم می‌توان یک Promise را بازگشت داد (مانند پیاده سازی که در ابتدای بحث عنوان شد) و یا می‌توان یک Observable را بازگشت داد که در ادامه نمونه‌ای از پیاده سازی این روش دوم را بررسی می‌کنیم؛ چون امکانات بیشتری را نسبت به Promiseها به همراه دارد. برای مثال در اینجا می‌توان اندکی صبر کرد تا کاربر تعدادی حرف را وارد کند و سپس این اطلاعات را به سرور ارسال کرد. به این ترتیب ترافیک ارسالی به سمت سرور کاهش پیدا می‌کند.


    پیاده سازی یک اعتبارسنج از راه دور مبتنی بر Observableها در Angular

    ابتدا یک دایرکتیو جدید را به نام RemoteValidator به ماژول custom-validators اضافه کرده‌ایم:
    >ng g d CustomValidators/RemoteValidator -m custom-validators.module
    در ادامه کدهای کامل این اعتبارسنج را مشاهده می‌کنید:
    import { Directive, Input } from "@angular/core";
    import {
      AsyncValidator,
      AbstractControl,
      NG_ASYNC_VALIDATORS
    } from "@angular/forms";
    import { Http, RequestOptions, Response, Headers } from "@angular/http";
    import "rxjs/add/operator/map";
    import "rxjs/add/operator/distinctUntilChanged";
    import "rxjs/add/operator/takeUntil";
    import "rxjs/add/operator/take";
    import { Observable } from "rxjs/Observable";
    import { Subject } from "rxjs/Subject";
    
    @Directive({
      selector:
        "[appRemoteValidator][formControlName],[appRemoteValidator][formControl],[appRemoteValidator][ngModel]",
      providers: [
        {
          provide: NG_ASYNC_VALIDATORS,
          useExisting: RemoteValidatorDirective,
          multi: true
        }
      ]
    })
    export class RemoteValidatorDirective implements AsyncValidator {
      @Input("remote-url") remoteUrl: string;
      @Input("remote-field") remoteField: string;
      @Input("remote-additional-fields") remoteAdditionalFields: string;
    
      constructor(private http: Http) {}
    
      validate(control: AbstractControl): Observable<{ [key: string]: any }> {
        if (!this.remoteUrl || this.remoteUrl === undefined) {
          return Observable.throw("`remoteUrl` is undefined.");
        }
    
        if (!this.remoteField || this.remoteField === undefined) {
          return Observable.throw("`remoteField` is undefined.");
        }
    
        const dataObject = {};
        if (
          this.remoteAdditionalFields &&
          this.remoteAdditionalFields !== undefined
        ) {
          const otherFields = this.remoteAdditionalFields.split(",");
          otherFields.forEach(field => {
            const name = field.trim();
            const otherControl = control.root.get(name);
            if (otherControl) {
              dataObject[name] = otherControl.value;
            }
          });
        }
    
        // This is used to signal the streams to terminate.
        const changed$ = new Subject<any>();
        changed$.next(); // This will signal the previous stream (if any) to terminate.
    
        const debounceTime = 400;
    
        return new Observable((obs: any) => {
          control.valueChanges
            .takeUntil(changed$)
            .take(1)
            .debounceTime(debounceTime)
            .distinctUntilChanged()
            .flatMap(term => {
              dataObject[this.remoteField] = term;
              return this.doRemoteValidation(dataObject);
            })
            .subscribe(
              (result: IRemoteValidationResult) => {
                if (result.result) {
                  obs.next(null);
                } else {
                  obs.next({
                    remoteValidation: {
                      remoteValidationMessage: result.message
                    }
                  });
                }
    
                obs.complete();
              },
              error => {
                obs.next(null);
                obs.complete();
              }
            );
        });
      }
    
      private doRemoteValidation(data: any): Observable<IRemoteValidationResult> {
        const headers = new Headers({ "Content-Type": "application/json" }); // for ASP.NET MVC
        const options = new RequestOptions({ headers: headers });
    
        return this.http
          .post(this.remoteUrl, JSON.stringify(data), options)
          .map(this.extractData)
          .do(result => console.log("remoteValidation result: ", result))
          .catch(this.handleError);
      }
    
      private extractData(res: Response): IRemoteValidationResult {
        const body = <IRemoteValidationResult>res.json();
        return body || (<IRemoteValidationResult>{});
      }
    
      private handleError(error: Response): Observable<any> {
        console.error("observable error: ", error);
        return Observable.throw(error.statusText);
      }
    }
    
    export interface IRemoteValidationResult {
      result: boolean;
      message: string;
    }
    توضیحات تکمیلی

    ساختار Directive تهیه شده مانند همان مطلب «نوشتن اعتبارسنج‌های سفارشی برای فرم‌های مبتنی بر قالب‌ها در Angular» است، تنها با یک تفاوت:
    @Directive({
      selector:
        "[appRemoteValidator][formControlName],[appRemoteValidator][formControl],[appRemoteValidator][ngModel]",
      providers: [
        {
          provide: NG_ASYNC_VALIDATORS,
          useExisting: RemoteValidatorDirective,
          multi: true
        }
      ]
    })
    در اینجا بجای NG_VALIDATORS، از NG_ASYNC_VALIDATORS استفاده شده‌است.

    سپس ورودی‌های این دایرکتیو را مشاهده می‌کنید:
    export class RemoteValidatorDirective implements AsyncValidator {
      @Input("remote-url") remoteUrl: string;
      @Input("remote-field") remoteField: string;
      @Input("remote-additional-fields") remoteAdditionalFields: string;
    به این ترتیب زمانیکه appRemoteValidator به المانی اضافه می‌شود (نام selector این دایرکتیو)، سبب فعالسازی این اعتبارسنج می‌گردد.
    <input #username="ngModel" required maxlength="8" minlength="4" type="text"
            appRemoteValidator [remote-url]="remoteUsernameValidationUrl" remote-field="FirstName"
            remote-additional-fields="email,password" class="form-control" name="username"
            [(ngModel)]="model.username">
    - در اینجا توسط ویژگی remote-url، آدرس اکشن متد سمت سرور دریافت می‌شود.
    - ویژگی remote-field مشخص می‌کند که اطلاعات المان جاری با چه کلیدی به سمت سرور ارسال شود.
    - ویژگی remote-additional-fields مشخص می‌کند که علاوه بر اطلاعات کنترل جاری، اطلاعات کدامیک از کنترل‌های دیگر را نیز می‌توان به سمت سرور ارسال کرد.

    یک نکته:
    ذکر "remote-field="FirstName به معنای انتساب مقدار رشته‌ای FirstName به خاصیت متناظر با ویژگی remote-field است.
    انتساب ویژه‌ی "remoteUsernameValidationUrl" به [remote-url]، به معنای انتساب مقدار متغیر remoteUsernameValidationUrl که در کامپوننت متناظر این قالب مقدار دهی می‌شود، به خاصیت متصل به ویژگی remote-url است.
    export class UserRegisterComponent implements OnInit {
       remoteUsernameValidationUrl = "api/Employee/CheckUser";
    بنابراین اگر remote-field را نیز می‌خواستیم به همین نحو تعریف کنیم، ذکر '' جهت مشخص سازی انتساب یک رشته، ضروری می‌بود؛ یعنی درج آن به صورت:
     [remote-field]="'FirstName'"


    ساختار مورد انتظار بازگشتی از سمت سرور

    در کدهای فوق، یک چنین ساختاری باید از سمت سرور بازگشت داده شود:
    export interface IRemoteValidationResult {
       result: boolean;
       message: string;
    }
    برای نمونه این ساختار را می‌توان توسط یک anonymous object ایجاد کرد و بازگشت داد:
    namespace AngularTemplateDrivenFormsLab.Controllers
    {
        [Route("api/[controller]")]
        public class EmployeeController : Controller
        {
            [HttpPost("[action]")]
            [ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
            public IActionResult CheckUser([FromBody] Employee model)
            {
                var remoteValidationResult = new { result = true, message = $"{model.FirstName} is fine!" };
                if (model.FirstName?.Equals("Vahid", StringComparison.OrdinalIgnoreCase) ?? false)
                {
                    remoteValidationResult = new { result = false, message = "username:`Vahid` is already taken." };
                }
    
                return Json(remoteValidationResult);
            }
        }
    }
    در اینجا برای مثال بررسی می‌شود که آیا FirstName ارسالی از سمت کاربر، معادل Vahid است یا خیر؟ اگر بله، result به false تنظیم شده و همچنین پیام خطایی نیز بازگشت داده می‌شود.
    همچنین اعتبارسنج سفارشی از راه دور فوق، پیام‌ها را تنها از طریق HttpPost ارسال می‌کند. علت اینجا است که در حالت POST، برخلاف حالت GET می‌توان اطلاعات بیشتری را بدون نگرانی از طول URL، ارسال کرد و همچنین کل درخواست، به علت وجود کاراکترهای غیرمجاز در URL (حالت GET، به درخواست یک URL از سرور تفسیر می‌شود)، برگشت نمی‌خورد.


    تکمیل کامپوننت فرم ثبت نام کاربران

    در ادامه تکمیل قالب user-register.component.html را مشاهده می‌کنید:
    <div class="form-group" [class.has-error]="username.invalid && username.touched"><label class="control-label">User Name</label><input #username="ngModel" required maxlength="8" minlength="4" type="text"
            appRemoteValidator [remote-url]="remoteUsernameValidationUrl" remote-field="FirstName"
            remote-additional-fields="email,password" class="form-control" name="username"
            [(ngModel)]="model.username"><div *ngIf="username.pending" class="alert alert-warning">
            Checking server, Please wait ...</div><div *ngIf="username.invalid && username.touched"><div class="alert alert-danger"  *ngIf="username.errors.remoteValidation">
              {{username.errors.remoteValidation.remoteValidationMessage}}</div></div></div>
    در مورد ویژگی‌های appRemoteValidator پیشتر بحث شد. در اینجا تنها یک نکته‌ی جدید وجود دارد:
    زمانیکه یک async validator مشغول به کار است و هنوز پاسخی را دریافت نکرده‌است، خاصیت pending را به true تنظیم می‌کند. به این ترتیب می‌توان پیام اتصال به سرور را نمایش داد:


    همچنین چون در اینجا نحوه‌ی طراحی شکست اعتبارسنجی به صورت ذیل است:
    obs.next({
                    remoteValidation: {
                      remoteValidationMessage: result.message
                    }
                  });
    وجود کلید remoteValidation در مجموعه‌ی username.errors، بیانگر وجود خطای اعتبارسنجی از راه دور است و به این ترتیب می‌توان پیام دریافتی از سمت سرور را نمایش داد:



    مزایای استفاده از Observableها در حین طراحی async validators

    در کدهای فوق چنین مواردی را هم مشاهده می‌کنید:
        // This is used to signal the streams to terminate.
        const changed$ = new Subject<any>();
        changed$.next(); // This will signal the previous stream (if any) to terminate.
    
        const debounceTime = 400;
    
        return new Observable((obs: any) => {
          control.valueChanges
            .takeUntil(changed$)
            .take(1)
            .debounceTime(debounceTime)
            .distinctUntilChanged()
    در اینجا بجای کار مستقیم با control.value (روش متداول دسترسی به مقدار کنترل دریافتی در یک اعتبارسنج)، به رخ‌داد valueChanges آن متصل شده و سپس پس از 400 میلی‌ثانیه، جمع نهایی ورودی کاربر، در اختیار متد http.post برای ارسال به سمت سرور قرار می‌گیرد. به این ترتیب می‌توان تعداد رفت و برگشت‌های به سمت سرور را کاهش داد و به ازای هر یکبار فشرده شدن دکمه‌ای توسط کاربر، سبب بروز یکبار رفت و برگشت به سرور نشد.
    همچنین وجود و تعریف new Subject، دراینجا ضروری است و از نشتی حافظه و همچنین رفت و برگشت‌های اضافه‌ی دیگری به سمت سرور، جلوگیری می‌کند. این subject سبب می‌شود تا کلیه اعمال ناتمام پیشین، لغو شده (takeUntil) و تنها آخرین درخواست جدید رسیده‌ی پس از 400 میلی‌ثانیه، به سمت سرور ارسال شود.

    بنابراین همانطور که مشاهده می‌کنید، Observableها فراتر هستند از صرفا ارسال اطلاعات به سرور و بازگشت آن‌ها به سمت کلاینت (استفاده‌ی متداولی که از آن‌ها در برنامه‌های Angular وجود دارد).


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

    ‫آپلود فایل‌ها در یک برنامه‌ی Angular به کمک کامپوننت ng2-file-upload

    $
    0
    0
    در مطلب «بررسی روش آپلود فایل‌ها از طریق یک برنامه‌ی Angular به یک برنامه‌ی ASP.NET Core» روش عمومی آپلود فایل‌ها را بررسی کردیم. آن مطلب وابستگی به کامپوننت خاصی ندارد و عمومی است. در مطلب جاری می‌خواهیم روش دیگری را مبتنی بر کامپوننت ng2-file-uploadبررسی کنیم که به همراه نمایش درصد پیشرفت ارسال فایل‌ها، امکان انتخاب بهتر نوع فایل‌های آپلودی و همچنین امکان مشاهده‌ی لیست کامل فایل‌های انتخاب شده و امکان حذف مواردی از آن، پیش از ارسال نهایی است.



    پیشنیازهای کار با کامپوننت ng2-file-upload

    برای شروع به کار با کامپوننت ng2-file-upload، ابتدا نیاز است بسته‌ی npm آن‌را نصب کرد:
    >npm install ng2-file-upload --save

    همچنین یک کامپوننت آزمایشی را هم به برنامه (دقیقا همان مثال مطلب قبلی) جهت اعمال آن اضافه می‌کنیم:
    >ng g c UploadFile/ng2-file-upload-test

    پس از آن نیاز است به ماژولی که این کامپوننت جدید در آن قرار دارد، مدخل FileUploadModule کامپوننت ng2-file-upload را افزود:
    import { FileUploadModule } from "ng2-file-upload";
    
    @NgModule({
      imports: [
        FileUploadModule
      ]
    در غیراینصورت خطای شناخته نشدن خاصیت uploader را در حین اعمال این کامپوننت مشاهده خواهید کرد.


    تکمیل Ng2FileUploadTestComponent جهت اعمال ng2-file-upload

    اکنون به کلاس کامپوننت جدیدی که ایجاد کردیم، مراجعه کرده و تغییرات ذیل را اعمال می‌کنیم:
    import { FileUploader, FileUploaderOptions } from "ng2-file-upload";
    import { Ticket } from "./../ticket";
    
    export class Ng2FileUploadTestComponent implements OnInit {
      fileUploader: FileUploader;
      model = new Ticket();
    در اینجا یک خاصیت عمومی از نوع FileUploader تعریف شده‌است که در اختیار قالب این کامپوننت قرار خواهد گرفت. همچنین شیء مدل فرم نیز همانند مطلب «بررسی روش آپلود فایل‌ها از طریق یک برنامه‌ی Angular به یک برنامه‌ی ASP.NET Core» تهیه شده‌است. هدف این است که بررسی کنیم علاوه بر ارسال فایل‌ها، چگونه می‌توان اطلاعات یک فرم را نیز به سمت سرور ارسال کرد.


    وهله سازی از کامپوننت ng2-file-upload و انجام تنظیمات اولیه‌ی آن

    پس از تعریف خاصیت عمومی fileUploader، اکنون نوبت به وهله سازی آن است:
        this.fileUploader = new FileUploader(<FileUploaderOptions>{
            url: "api/SimpleUpload/SaveTicket",
            headers: [
              { name: "X-XSRF-TOKEN", value: this.getCookie("XSRF-TOKEN") },
              { name: "Accept", value: "application/json" }
            ],
            isHTML5: true,
            // allowedMimeType: ["image/jpeg", "image/png", "application/pdf", "application/msword", "application/zip"]
            allowedFileType: [
              "application",
              "image",
              "video",
              "audio",
              "pdf",
              "compress",
              "doc",
              "xls",
              "ppt"
            ],
            removeAfterUpload: true,
            autoUpload: false,
            maxFileSize: 10 * 1024 * 1024
          }
        );
    - در اینجا url، مسیر اکشن متدی را در سمت سرور مشخص می‌کند که قرار است فایل‌های ارسالی را دریافت و ذخیره کند.
    - اگر برنامه از نکات anti-forgery tokenاستفاده می‌کند، این کامپوننت برخلاف روش مطرح شده‌ی در مطلب مشابه قبلی، هیچ هدری را به سمت سرور ارسال نمی‌کند. بنابراین نیاز است کوکی مرتبط را خودمان یافته و سپس به لیست هدرها اضافه کنیم. در اینجا روش استخراج یک کوکی را توسط کدهای جاوا اسکریپتی مشاهده می‌کنید:
      getCookie(name: string): string {
        const value = "; " + document.cookie;
        const parts = value.split("; " + name + "=");
        if (parts.length === 2) {
          return decodeURIComponent(parts.pop().split(";").shift());
        }
      }

    - برای محدود سازی فایل‌های ارسالی توسط این کامپوننت، دو روش وجود دارد:
    الف) مشخص سازی مقدار خاصیت allowedMimeType
    همانطور که مشاهده می‌کنید، در اینجا باید mime type فایل‌های مجاز را مشخص کرد.
    ب) مشخص سازی مقدار خاصیت allowedFileType
    برخلاف تصور، در اینجا از پسوند فایل‌ها استفاده نمی‌کند و از یک لیست از پیش مشخص که نمونه‌ای از آن‌را در اینجا مشاهده می‌کنید، کمک گرفته می‌شود. بنابراین اگر برای مثال تنها نیاز به ارسال تصاویر بود، مقدار image را نگه داشته و مابقی را از لیست حذف کنید.

    - removeAfterUpload به این معنا است که آیا لیست نهایی که نمایش داده می‌شود، پس از آپلود باقی بماند یا خیر؟
    - توسط خاصیت maxFileSize می‌توان حداکثر اندازه‌ی قابل قبول فایل‌های ارسالی را مشخص کرد.


    مدیریت رخ‌دادهای کامپوننت ng2-file-upload

    اکنون که وهله‌ای از این کامپوننت ساخته شده‌است، می‌توان رخ‌دادهای آن‌را نیز مدیریت کرد. برای مثال:
    الف) نحوه‌ی ارسال اطلاعات اضافی به همراه یک فایل به سمت سرور
        this.fileUploader.onBuildItemForm = (fileItem, form) => {
          for (const key in this.model) {
            if (this.model.hasOwnProperty(key)) {
              form.append(key, this.model[key]);
            }
          }
        };
    در اینجا شبیه به مطلب مشابه قبلی، مقادیر خواص شیء مدل، به صورت خودکار استخراج شده و به خاصیت form این کامپوننت که درحقیقت همان FormData ارسالی به سمت سرور است، اضافه می‌شوند.

    ب) اطلاع یافتن از رخ‌داد خاتمه‌ی کار
    رخ‌داد onCompleteAll پس از ارسال تمام فایل‌ها به سمت سرور فراخوانی می‌شود:
        this.fileUploader.onCompleteAll = () => {
          // clear the form
          // this.model = new Ticket();
        };

    ج) در حین وهله سازی fileUploader، تعدادی محدودیت نیز قابل اعمال هستند. این محدودیت‌ها سبب نمایش هیچگونه پیام خطایی نمی‌شوند. فقط زمانیکه کاربر فایلی را انتخاب می‌کند، این فایل در لیست ظاهر نمی‌شود. اگر علاقمند به مدیریت این وضعیت باشید، می‌توان از رخ‌داد onWhenAddingFileFailed استفاده کرد:
        this.fileUploader.onWhenAddingFileFailed = (item, filter, options) => {
          // msg: `You can't select ${item.name} file because of the ${filter.name} filter.`
        };

    د) اگر ارسال فایلی به سمت سرور با شکست مواجه شود، در ر‌خ‌دادگردان onErrorItem می‌توان به نام این فایل و اطلاعات بیشتری که از سمت سرور دریافت شده‌است، دسترسی یافت:
        this.fileUploader.onErrorItem = (fileItem, response, status, headers) => {
           //
        };

    ه) اگر از سمت سرور اطلاعات JSON مانندی یا هر اطلاعات دیگری به سمت کلاینت پس از آپلود ارسال می‌شود، این اطلاعات را می‌توان در رخ‌دادگردان onSuccessItem دریافت کرد:
        this.fileUploader.onSuccessItem = (item, response, status, headers) => {
          if (response) {
            const ticket = JSON.parse(response);
            console.log(`ticket:`, ticket);
          }
        };


    ارسال نهایی فرم و  فایل‌ها به سمت سرور

    در پایان، با فراخوانی متد uploadAll شیء fileUploader جاری، می‌توان اطلاعات فرم و تمام فایل‌های آن‌را به سمت سرور ارسال کرد:
      submitForm(form: NgForm) {
        this.fileUploader.uploadAll();
    
        // NOTE: Upload multiple files in one request -> https://github.com/valor-software/ng2-file-upload/issues/671
      }
    فقط باید دقت داشت که این کامپوننت هر فایل را جداگانه به سمت سرور ارسال می‌کند و برخلاف روش مطلب قبلی، همه را یکجا و در طی یک درخواست به سمت سرور ارسال نمی‌کند. اما کدهای سمت سرور آن با مطلب مشابه قبلی دقیقا یکی است و تفاوتی نمی‌کند (همان نکات قسمت «دریافت فرم درخواست پشتیبانی در سمت سرور و ذخیره‌ی فایل‌های آن‌» مطلب قبلی نیز در اینجا صادق است).


    کدهای کامل کامپوننت ng2-file-upload-test.component.ts را در اینجامی‌توانید مشاهده کنید.


    تکمیل قالب کامپوننت Ng2FileUploadTestComponent

    اکنون که کار تکمیل کامپوننت آزمایشی ارسال فایل‌ها به سمت سرور به پایان رسید، نوبت به تکمیل قالب آن است.

    افزودن فیلد اضافی توضیحات به فرم

    <div class="container"><h3>Support Form(ng2-file-upload)</h3><form #form="ngForm" (submit)="submitForm(form)" novalidate><div class="form-group" [class.has-error]="description.invalid && description.touched"><label class="control-label">Description</label><input #description="ngModel" required type="text" class="form-control"
            name="description" [(ngModel)]="model.description"><div *ngIf="description.invalid && description.touched"><div class="alert alert-danger"  *ngIf="description.errors.required">
              description is required.</div></div></div>
    هدف از این فیلد این است که شیء Ticket را وهله سازی و مقدار دهی کند. از مقدار آن در رخ‌دادگردان onBuildItemForm که پیشتر توضیح داده شد، استفاده می‌شود.

    تعریف ویژه‌ی فیلد ارسال فایل‌ها به سمت سرور

    <div class="form-group"><label class="control-label">Screenshot(s)</label><input required type="file" multiple ng2FileSelect [uploader]="fileUploader"
            class="form-control" name="screenshot"></div>
    در اینجا ابتدا دایرکتیو ng2FileSelect ذکر می‌شود تا کامپوننت مرتبط فعالسازی شود. سپس خاصیت uploader این دایرکتیو توسط خاصیت fileUploader که پیشتر در کامپوننت، وهله سازی و تنظیم شد، مقدار دهی می‌شود.
    ذکر ویژگی استاندارد multiple را نیز در اینجا مشاهده می‌کنید. وجود آن سبب خواهد شد تا کاربر بتواند چندین فایل را با هم انتخاب کند. اگر نیازی به ارسال چندین فایل نیست، این ویژگی را حذف کنید.

    نمایش لیست فایل‌ها و نمایش درصد پیشرفت آپلود آن‌ها

    جدولی را که در تصویر ابتدای بحث مشاهده کردید، به صورت ذیل شکل می‌گیرد (کدهای آن در همان صفحه‌ی توضیحات کامپوننتنیز موجود هستند):
    <div style="margin-bottom: 10px" *ngIf="fileUploader.queue.length"><h3>Upload queue</h3><p>Queue length: {{ fileUploader?.queue?.length }}</p><table class="table"><thead><tr><th width="50%">Name</th><th>Size</th><th>Progress</th><th>Status</th><th>Actions</th></tr></thead><tbody><tr *ngFor="let item of fileUploader.queue"><td><strong>{{ item?.file?.name }}</strong></td><td nowrap>{{ item?.file?.size/1024/1024 | number:'.2' }} MB</td><td><div class="progress" style="margin-bottom: 0;"><div class="progress-bar" role="progressbar" [ngStyle]="{ 'width': item.progress + '%' }"></div></div></td><td class="text-center"><span *ngIf="item.isError"><i class="glyphicon glyphicon-remove"></i></span></td><td nowrap><button type="button" class="btn btn-danger btn-xs" (click)="item.remove()"><span class="glyphicon glyphicon-trash"></span> Remove</button></td></tr></tbody></table><div><div>
              Queue progress:<div class="progress"><div class="progress-bar" role="progressbar" [ngStyle]="{ 'width': fileUploader.progress + '%' }"></div></div></div><button type="button" class="btn btn-danger btn-s" (click)="fileUploader.clearQueue()"
              [disabled]="!fileUploader.queue.length"><span class="glyphicon glyphicon-trash"></span> Remove all</button></div></div>
    شیء fileUploader وهله سازی شده‌ی در کامپوننت این قالب، دارای خاصیت queue است. در این خاصیت، لیست فایل‌های انتخابی توسط کاربر درج می‌شوند. برای مثال مقدار fileUploader?.queue?.length مساوی تعداد فایل‌های انتخابی توسط کاربر است. بنابراین می‌توان حلقه‌ای را بر روی آن تشکیل داد و مشخصات این فایل‌ها را در صفحه نمایش داد. همچنین هر آیتم آن دارای متد remove نیز هست. کار این متد، حذف این آیتم از لیست queue است و یا اگر متد fileUploader.clearQueue فراخوانی شود، تمام آیتم‌های این لیست را حذف می‌کند.
    در اینجا از progress-bar بوت استرپ برای نمایش درصد آپلود فایل‌ها استفاده شده‌است:
    <div class="progress" style="margin-bottom: 0;"><div class="progress-bar" role="progressbar" [ngStyle]="{ 'width': item.progress + '%' }"></div></div>
    این کامپوننت ارسال فایل، خاصیت item.progress هر فایل موجود در queue را مدام به روز رسانی می‌کند. به همین جهت می‌توان از آن جهت تغییر عرض پیشرفت progress-bar بوت استرپ استفاده کرد.

    غیرفعال کردن دکمه‌ی ارسال، در صورت عدم انتخاب یک فایل

    <button class="btn btn-primary" [disabled]="form.invalid || !fileUploader.queue.length"
          type="submit">Submit</button></form>
    اگر بخواهیم انتخاب حداقل یک فایل را توسط کاربر اجباری کنیم، می‌توان خاصیت disabled دکمه‌ی ارسال را به طول صف یا fileUploader.queue.length نیز متصل کرد.


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

    ‫تولید هدرهای Content Security Policy توسط ASP.NET Core برای برنامه‌های Angular

    $
    0
    0
    پیشتر مطلب «افزودن هدرهای Content Security Policy به برنامه‌های ASP.NET» را در این سایت مطالعه کرده‌اید. در اینجا قصد داریم معادل آن‌را برای ASP.NET Core تهیه کرده و همچنین نکات مرتبط با برنامه‌های Angular را نیز در آن لحاظ کنیم.


    تهیه میان افزار افزودن هدرهای Content Security Policy

    کدهای کامل این میان افزار را در ادامه مشاهده می‌کنید:
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Http;
    
    namespace AngularTemplateDrivenFormsLab.Utils
    {
        public class ContentSecurityPolicyMiddleware
        {
            private readonly RequestDelegate _next;
    
            public ContentSecurityPolicyMiddleware(RequestDelegate next)
            {
                _next = next;
            }
    
            public Task Invoke(HttpContext context)
            {
                context.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN");
                context.Response.Headers.Add("X-Xss-Protection", "1; mode=block");
                context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
    
                string[] csp =
                {
                  "default-src 'self'",
                  "style-src 'self' 'unsafe-inline'",
                  "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
                  "font-src 'self'",
                  "img-src 'self' data:",
                  "connect-src 'self'",
                  "media-src 'self'",
                  "object-src 'self'",
                  "report-uri /api/CspReport/Log" //TODO: Add api/CspReport/Log
                };
                context.Response.Headers.Add("Content-Security-Policy", string.Join("; ", csp));
                return _next(context);
            }
        }
    
        public static class ContentSecurityPolicyMiddlewareExtensions
        {
            /// <summary>
            /// Make sure you add this code BEFORE app.UseStaticFiles();,
            /// otherwise the headers will not be applied to your static files.
            /// </summary>
            public static IApplicationBuilder UseContentSecurityPolicy(this IApplicationBuilder builder)
            {
                return builder.UseMiddleware<ContentSecurityPolicyMiddleware>();
            }
        }
    }
    که نحوه‌ی استفاده از آن در کلاس آغازین برنامه به صورت ذیل خواهد بود:
    public void Configure(IApplicationBuilder app)
    {
       app.UseContentSecurityPolicy();

    توضیحات تکمیلی

    افزودن X-Frame-Options
     context.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN");
    از هدر X-FRAME-OPTIONS، جهت منع نمایش و رندر سایت جاری، در iframeهای سایت‌های دیگر استفاده می‌شود. ذکر مقدار SAMEORIGIN آن، به معنای مجاز تلقی کردن دومین جاری برنامه است.


    افزودن X-Xss-Protection
     context.Response.Headers.Add("X-Xss-Protection", "1; mode=block");
    تقریبا تمام مرورگرهای امروزی قابلیت تشخیص حملات XSS را توسط static analysis توکار خود دارند. این هدر، آنالیز اجباری XSS را فعال کرده و همچنین تنظیم حالت آن به block، نمایش و رندر قسمت مشکل‌دار را به طور کامل غیرفعال می‌کند.


    افزودن X-Content-Type-Options
     context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
    وجود این هدر سبب می‌شود تا مرورگر، حدس‌زدن نوع فایل‌ها، درخواست‌ها و محتوا را کنار گذاشته و صرفا به content-type ارسالی توسط سرور اکتفا کند. به این ترتیب برای مثال امکان لینک کردن یک فایل غیرجاوا اسکریپتی و اجرای آن به صورت کدهای جاوا اسکریپت، چون توسط تگ script ذکر شده‌است، غیرفعال می‌شود. در غیراینصورت مرورگر هرچیزی را که توسط تگ script به صفحه لینک شده باشد، صرف نظر از content-type واقعی آن، اجرا خواهد کرد.


    افزودن Content-Security-Policy
    string[] csp =
                {
                  "default-src 'self'",
                  "style-src 'self' 'unsafe-inline'",
                  "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
                  "font-src 'self'",
                  "img-src 'self' data:",
                  "connect-src 'self'",
                  "media-src 'self'",
                  "object-src 'self'",
                  "report-uri /api/CspReport/Log" //TODO: Add api/CspReport/Log
                };
    context.Response.Headers.Add("Content-Security-Policy", string.Join("; ", csp));
    وجود این هدر، تزریق کدها و منابع را از دومین‌های دیگر غیرممکن می‌کند. برای مثال ذکر self در اینجا به معنای مجاز بودن الصاق و اجرای اسکریپت‌ها، شیوه‌نامه‌ها، تصاویر و اشیاء، صرفا از طریق دومین جاری برنامه است و هرگونه منبعی که از دومین‌های دیگر به برنامه تزریق شود، قابلیت اجرایی و یا نمایشی نخواهد داشت.

    در اینجا ذکر unsafe-inline و unsafe-eval را مشاهده می‌کنید. برنامه‌های Angular به همراه شیوه‌نامه‌های inline و یا بکارگیری متد eval در مواردی خاص هستند. اگر این دو گزینه ذکر و فعال نشوند، در کنسول developer مرورگر، خطای بلاک شدن آن‌ها را مشاهده کرده و همچنین برنامه از کار خواهد افتاد.

    یک نکته:با فعالسازی گزینه‌ی aot--در حین ساخت برنامه، می‌توان unsafe-eval را نیز حذف کرد.


    استفاده از فایل web.config برای تعریف SameSite Cookies

    یکی از پیشنهادهای اخیرارائه شده‌ی جهت مقابله‌ی با حملات CSRF و XSRF، قابلیتی است به نام  Same-Site Cookies. به این ترتیب مرورگر، کوکی سایت جاری را به همراه یک درخواست ارسال آن به سایت دیگر، پیوست نمی‌کند (کاری که هم اکنون با درخواست‌های Cross-Site صورت می‌گیرد). برای رفع این مشکل، با این پیشنهاد امنیتی جدید، تنها کافی است SameSite، به انتهای کوکی اضافه شود:
     Set-Cookie: sess=abc123; path=/; SameSite

    نگارش‌های بعدی ASP.NET Core، ویژگی SameSite را نیز به عنوان CookieOptions لحاظ کرده‌اند. همچنین یک سری از کوکی‌های خودکار تولیدی توسط آن مانند کوکی‌های anti-forgery به صورت خودکار با این ویژگی تولید می‌شوند.
    اما مدیریت این مورد برای اعمال سراسری آن، با کدنویسی میسر نیست (مگر اینکه مانند نگارش‌های بعدی ASP.NET Core پشتیبانی توکاری از آن صورت گیرد). به همین جهت می‌توان از ماژول URL rewrite مربوط به IIS برای افزودن ویژگی SameSite به تمام کوکی‌های تولید شده‌ی توسط سایت، کمک گرفت. برای این منظور تنها کافی است فایل web.config را ویرایش کرده و موارد ذیل را به آن اضافه کنید:
    <?xml version="1.0" encoding="utf-8"?><configuration><system.webServer><rewrite><outboundRules><clear /><!-- https://scotthelme.co.uk/csrf-is-dead/ --><rule name="Add SameSite" preCondition="No SameSite"><match serverVariable="RESPONSE_Set_Cookie" pattern=".*" negate="false" /><action type="Rewrite" value="{R:0}; SameSite=lax" /><conditions></conditions></rule><preConditions><preCondition name="No SameSite"><add input="{RESPONSE_Set_Cookie}" pattern="." /><add input="{RESPONSE_Set_Cookie}" pattern="; SameSite=lax" negate="true" /></preCondition></preConditions></outboundRules></rewrite></system.webServer></configuration>


    لاگ کردن منابع بلاک شده‌ی توسط مرورگر در سمت سرور

    اگر به هدر Content-Security-Policy دقت کنید، گزینه‌ی آخر آن، ذکر اکشن متدی در سمت سرور است:
       "report-uri /api/CspReport/Log" //TODO: Add api/CspReport/Log
    با تنظیم این مورد، می‌توان موارد بلاک شده را در سمت سرور لاگ کرد. اما این اطلاعات ارسالی به سمت سرور، فرمت خاصی را دارند:
    {
      "csp-report": {
        "document-uri": "http://localhost:5000/untypedSha",
        "referrer": "",
        "violated-directive": "script-src",
        "effective-directive": "script-src",
        "original-policy": "default-src 'self'; style-src 'self'; script-src 'self'; font-src 'self'; img-src 'self' data:; connect-src 'self'; media-src 'self'; object-src 'self'; report-uri /api/Home/CspReport",
        "disposition": "enforce",
        "blocked-uri": "eval",
        "line-number": 21,
        "column-number": 8,
        "source-file": "http://localhost:5000/scripts.bundle.js",
        "status-code": 200,
        "script-sample": ""
      }
    }
    به همین جهت ابتدا نیاز است توسط JsonProperty کتابخانه‌ی JSON.NET، معادل این خواص را تولید کرد:
        class CspPost
        {
            [JsonProperty("csp-report")]
            public CspReport CspReport { get; set; }
        }
    
        class CspReport
        {
            [JsonProperty("document-uri")]
            public string DocumentUri { get; set; }
    
            [JsonProperty("referrer")]
            public string Referrer { get; set; }
    
            [JsonProperty("violated-directive")]
            public string ViolatedDirective { get; set; }
    
            [JsonProperty("effective-directive")]
            public string EffectiveDirective { get; set; }
    
            [JsonProperty("original-policy")]
            public string OriginalPolicy { get; set; }
    
            [JsonProperty("disposition")]
            public string Disposition { get; set; }
    
            [JsonProperty("blocked-uri")]
            public string BlockedUri { get; set; }
    
            [JsonProperty("line-number")]
            public int LineNumber { get; set; }
    
            [JsonProperty("column-number")]
            public int ColumnNumber { get; set; }
    
            [JsonProperty("source-file")]
            public string SourceFile { get; set; }
    
            [JsonProperty("status-code")]
            public string StatusCode { get; set; }
    
            [JsonProperty("script-sample")]
            public string ScriptSample { get; set; }
        }
    اکنون می‌توان بدنه‌ی درخواست را استخراج و سپس به این شیء ویژه نگاشت کرد:
    namespace AngularTemplateDrivenFormsLab.Controllers
    {
        [Route("api/[controller]")]
        public class CspReportController : Controller
        {
            [HttpPost("[action]")]
            [IgnoreAntiforgeryToken]
            public async Task<IActionResult> Log()
            {
                CspPost cspPost;
                using (var bodyReader = new StreamReader(this.HttpContext.Request.Body))
                {
                    var body = await bodyReader.ReadToEndAsync().ConfigureAwait(false);
                    this.HttpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body));
                    cspPost = JsonConvert.DeserializeObject<CspPost>(body);
                }
    
                //TODO: log cspPost
    
                return Ok();
            }
        }
    }
    در اینجا نحوه‌ی استخراج Request.Body را به صورت خام را مشاهده می‌کنید. سپس توسط متد DeserializeObject کتابخانه‌ی JSON.NET، این رشته به شیء CspPost نگاشت شده‌است.


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

    ‫کنترل دسترسی‌ها در Angular با استفاده از Ng2Permission

    $
    0
    0
    سناریویی را در نظر بگیرید که در آن بعد از احراز هویت کاربر، لیست دسترسی‌هایی را که کاربر به بخش‌های مختلف خواهد داشت، از سرور دریافت می‌کند. به عنوان مثال کل دسترسی‌های موجود در سیستم به شرح زیر است:
    1. ViewUsers 
    2. CreateUser 
    3. EditUser 
    4. DeleteUser 
    حالا فرض کنید، کاربر X بعد از احراز هویت، از لیست دسترسی‌های موجود، تنها دسترسی ViewUsers و EditUser را دریافت می‌کند. یعنی تنها مجاز به مشاهده‌ی لیست کاربران و ویرایش کردن آنها می‌باشد.
    در اینجا جهت جلوگیری از دسترسی به ویرایش کاربر، با استفاده از یک Router guard سفارشی می‌توان مسیر users/edit را برای کاربر غیر قابل استفاده کرد؛ به نحوی که اگر کاربر وارد شده مجوز EditUser را نداشت، این مسیر غیر قابل دسترسی باشد. 
    از طرفی صفحه‌ی ViewUsers، برای کاربری با تمامی دسترسی‌ها، به شکل زیر خواهد بود: 


    همانطور که مشاهده می‌کنید، المنت‌هایی در صفحه وجود دارند که کاربر X نباید آنها را مشاهده کند. از جمله دکمه حذف کاربر و دکمه ایجاد کاربر. برای مخفی کردن آنها چه راه‌حلی را می‌توان ارائه داد؟ شاید بخواهید برای اینکار از ngIf* استفاده کنید. برای اینکار کافی است دست بکار شوید تا مشکلاتی را که در این روش به آنها بر می‌خورید، متوجه شوید. از جمله این مشکلات می‌توان به پیچیدگی بسیار زیاد و وجود کدهای تکراری در هر کامپوننت اشاره کرد (در بهترین حالت هر کامپوننت باید سرویس حاوی دسترسی‌ها را در خود تزریق کرده و با استفاده از توابعی، وجود یا عدم وجود دسترسی را بررسی کند). 

    راه‌حل بهتر، استفاده از یک Directive سفارشی است. همچنین ماژول Ng2Permission علاوه بر فراهم کردن Directive جهت مدیریت المنت‌های روی صفحه، امکاناتی را جهت نگهداری و تعریف دسترسی‌های جدید و همچنین محافظت از Routeها فراهم کرده است. این ماژول الهام گرفته از ماژول angular-permission می‌باشد.

    کافی است با استفاده از دستور زیر این ماژول را نصب کنید: 

    npm install angular2-permission --save

    بعد از نصب، ماژول Ng2Permission را در قسمت imports در ماژول اصلی برنامه، اضافه کنید. 

    import { Ng2Permission } from 'angular2-permission';
    @NgModule({
      imports: [
        Ng2Permission
      ]
    })


    مدیریت دسترسی‌ها

     برای مدیریت دسترسی‌های کاربر، از سرویس PermissionService، در هرجایی از برنامه می‌توانید استفاده کنید. این سرویس دارای متدهای زیر است.:
    توضیحات امضاء 
     تعریف دسترسی‌های جدید   define(permissions: Array<string>): void
     افزودن دسترسی جدید   add(permission:string):void 
     حذف دسترسی مشخص شده   remove(permission:string):void 
     برسی اینکه دسترسی قبلا تعریف شده است؟   hasDefined(permission:string):boolean 
     برسی اینکه حداقل یکی از دسترسی‌های ورودی قبلا تعریف شده است؟   hasOneDefined(permissions:Array<string>):boolean 
     حذف تمامی دسترسی‌ها   clearStore():void 
     دریافت تمامی دسترسی‌های تعریف شده   get store():Array<string>
     Emitter جهت تغییر در دسترسی‌ها  get permissionStoreChangeEmitter(): EventEmitter<any>

    برای مثال جهت تعریف دسترسی‌های جدید، کافی است سرویس PermissionService را تزریق کرده و با استفاده از متدهای define اقدام به اینکار کنید: 

    import { PermissionService } from 'angular2-permission';
    @Component({
        […]
    })
    export class LoginComponent implements OnInit {
        constructor(private _permissionService: PermissionService) { 
            this._permissionService.define(['ViewUsers', 'CreateUser', 'EditUser', 'DeleteUser']);
        }
    }

    آنچه که واضح است، این است که لیست دسترسی‌ها می‌توانند از سمت سرور تامین شوند.

     

    محافظت از مسیرهای تعریف شده

     بعد از تعریف دسترسی‌ها، برای محافظت از مسیر‌های غیر مجاز برای کاربر می‌توانید از PermissionGuard به شکل زیر استفاده کنید: 
    import { PermissionGuard, IPermissionGuardModel } from 'angular2-permission';
    […]
    const routes: Routes = [
        {
            path: 'login',
            component: LoginComponent,
            children: []
        },
        {
            path: 'users',
            component: UserListComponent,
            canActivate: [PermissionGuard],
            data: {
                Permission: {
                    Only: ['ViewUsers'],
                    RedirectTo: '403'
                } as IPermissionGuardModel
            },
            children: []
        },
        {
            path: 'users/create',
            component: UserCreateComponent,
            canActivate: [PermissionGuard],
            data: {
                Permission: {
                    Only: ['CreateUser'],
                    RedirectTo: '403'
                } as IPermissionGuardModel
            },
            children: []
        },
        {
            path: '403',
            component: AccessDeniedComponent,
            children: []
        }
    ];
    
    @NgModule({
        imports: [RouterModule.forRoot(routes)],
        exports: [RouterModule]
    })
    export class AppRoutingModule { }
    همانطور که مشاهده می‌کنید کافی است canActivate در هر Route را به PermissionGuard تنظیم کنید. همچنین در data از طریق کلید Permission، تنظیمات این Guard را مشخص کنید. کلید Permission در data یک شیء از نوع IPermissionGuardModel را دریافت خواهد کرد که حاوی خصوصیات زیر است:
     
    توضیحات    خصوصیت 
     فقط دسترسی‌های تعریف شده در این قسمت، امکان پیمایش به این مسیر را خواهند داشت.   Only 
     تمامی دسترسی‌های تعریف شده، به جز دسترسی‌های تعریف شده در این قسمت، امکان پیمایش به این مسیر را خواهند داشت.   Except 
     در صورتیکه تقاضای غیر مجازی جهت پیمایش به این مسیر صادر شد، کاربر را به این مسیر هدایت می‌کند.   RedirectTo 

    نکته ۱:مقدار دهی همزمان Only و Except مجاز نیست.
    نکته ۲:ذکر چند دسترسی در هر یک از خصوصیت‌های Only و Except معنی «یا منطقی» دارد. مثلا در قطعه کد زیر، اگر دسترسی Admin باشد «یا» دسترسی CreateUser باشد امکان مشاهده پیمایش صفحه وجود خواهد داشت. 
    {
        path: 'users/create',
        component: UserCreateComponent,
        canActivate: [PermissionGuard],
        data: {
            Permission: {
                Only: ['Admin', 'CreateUser'],
                RedirectTo: '403'
            } as IPermissionGuardModel
        },
        children: []
    }

    محافظت از المنت‌های صفحه

     جهت محافظت از المنت‌های موجود در صفحه، ماژول Ng2Permission دایرکتیوهایی را برای این منظور تدارک دیده است: 
     توضیحات  Input type    Directive 
     فقط دسترسی‌های تعریف شده در این دایرکتیو امکان مشاهده (به صورت پیش فرض) المنت را دارند.   Arrayy<string>   hasPermission 
     تمامی دسترسی‌های موجود، به جز دسترسی‌های تعریف شده در این دایرکتیو امکان مشاهده (به صورت پیش فرض) المنت را دارند.   Array<string>   exceptPermission
     استراتژی برخورد با المنت، هنگامیکه کاربر دسترسی به المنت دارد.   string | Function   onAuthorizedPermission 
     استراتژی برخورد با المنت، هنگامیکه کاربر دسترسی به المنت را ندارد.  string | Function 
     onUnauthorizedPermission 

    در مثال زیر فقط کاربرانی که دسترسی DeleteUser را داشته باشند، امکان مشاهده دکمه حذف را خواهند داشت.
    <button type="button" [hasPermission]="['DeleteUser']"><span aria-hidden="true"></span>
      Delete</button>

    در صورتیکه بیش از یک دسترسی مد نظر باشد، با کاما از هم جدا خواهند شد.

    <button type="button" [hasPermission]="[ 'Admin', 'DeleteUser']"><span aria-hidden="true"></span>
      Delete</button>
    در مثال بالا درصورتیکه کاربر دسترسی Admin « یا» دسترسی DeleteUser را داشته باشد، امکان مشاهده (به صورت پیش فرض) المنت را خواهد داشت. مثال زیر یعنی تمامی کاربران با هر دسترسی امکان مشاهده المنت را خواهند داشت؛ بجز دسترسی GeustUser. 
    <button type="button" [exceptPermission]="['GeustUser']"><span aria-hidden="true"></span>
      Delete</button>
    به صورت پیش فرض هنگامیکه کاربری دسترسی به المنتی را نداشته باشد، دایرکتیو، این المنت را مخفی خواهد کرد (با تنظیم استایل display به none). شاید شما بخواهید بجای مخفی/نمایش المنت، مثلا از فعال/غیرفعال کردن المنت استفاده کنید. برای این کار از خصوصیت onAuthorizedPermission و onUnauthorizedPermission به شکل زیر استفاده کنید:
    <button type="button" 
      [hasPermission]="['GeustUser']"
      onAuthorizedPermission="enable"
      onUnauthorizedPermission="disable"><span aria-hidden="true"></span>
      Delete</button>
    تمامی استراتژی‌های از قبل تعریف شده و قابل استفاده برای خصوصیت onAuthorizedPermission و onUnauthorizedPermission به شرح زیر است:
     رفتار  مقدار 
     حذف خصوصیت disabled از المنت   enable 
    افزودن خصوصیت disabled به المنت     disable 
     تنظیم استایل display به inherit   show 
     تنظیم استایل display به none   hide 

    در صورتیکه هیچ‌کدام از این استراتژی‌ها، پاسخگوی نیاز شما جهت برخورد با المنت نبود، می‌توانید بجای ارسال یک رشته ثابت به خصوصیت onAuthorizedPermission و onUnauthorizedPermission، یک تابع را ارسال کنید تا عملی که می‌خواهید، بر روی المنت انجام دهد. به عنوان مثال می‌خواهیم در صورتیکه کاربر مجاز به استفاده از المنتی نبود، المنت را از طریق تنظیم استایل visibility به hidden، مخفی کنیم و در صورتیکه کاربر مجاز به استفاده از المنت بود، استایل visibility به inherit تنظیم شود. 
    با این فرض، کدهای کامپوننت به شکل زیر:
    @Component({
        selector: 'app-user-list',
        templateUrl: './user-list.component.html',
        styleUrls: ['./user-list.component.css']
    })
    export class UserListComponent {
    
      constructor() { }
    
      OnAuthorizedPermission(element: ElementRef) {
        element.nativeElement.style.visibility ="inherit";
      }
    
      OnUnauthorizedPermission(element: ElementRef) {
        element.nativeElement.style.visibility = "hidden";    
      }
    }
    و تگهای Html زیر: 
    <button 
      [hasPermission]="['CreateUser']"
      [onAuthorizedPermission]="OnAuthorizedPermission"
      [onUnauthorizedPermission]="OnUnauthorizedPermission"><span aria-hidden="true"></span>
      Add New User</button>
    نکته: هر تغییر در لیست دسترسی‌ها در لحظه سبب اجرای دوباره دایرکتیوها خواهد شد و تغییرات، در همان لحظه در لایه نمایش قابل مشاهده خواهند بود. 

    ‫بررسی Bad code smell ها: گذاره‌های Switch

    $
    0
    0
    این کد بد بو در دسته «بد استفاده کنندگان از شیء گرایی» قرار می‌گیرد. زمانیکه گذاره‌های switch و یا دنباله‌ای از گذاره‌های if در کد وجود داشته باشد، معمولا با چنین الگویی روبرو هستیم. تشخیص این کد بد بو نیز بسیار آسان است. 
    در شرایط نادری استفاده از switch می‌تواند یک طراحی شیء گرای مناسب باشد. در طراحی شیء گرا معمولا یک گذاره switch نشان دهنده یک رابطه چند ریختی (Polymorphism) نادیده گرفته شده است. 
    معمولا زمانیکه بجای استفاده از اصول طراحی شیء گرا، از گذاره‌های switch استفاده می‌شود، این گذاره‌ها در بخش‌های مختلف کد پراکنده خواهند شد. در نتیجه، برای اضافه یا حذف نمودن یک شرط از شرایط switch، باید تمامی گذاره‌ها را در کد، تغییر داد.  
    از اولین قدم‌ها برای رفع چنین بوی بدی، می‌تواند متمرکز کردن گذاره switch پراکنده در کلاسی مناسب باشد. با این کار می‌توان کنترل نسبتا بیشتری بر روی گذاره و شرط‌های آن داشت.  
    یکی از دلایل ایجاد چنین بوی بدی استفاده از کد نوع (Type code) بجای پیاده سازی روابط ارث بریاست. به طور مثال فرض کنید در حال تولید سیستمی هستید که دو نوع مشتری حقیقی و حقوقی در آن وجود دارد. ممکن است به این نتیجه برسید که یک فیلد به نام Type را در کلاس مربوط به مشتری اضافه کنید. مانند کد زیر:  
    public enum CustomerType 
    { 
         Person = 0, 
         Company = 1 
    } 
    public class Customer 
    { 
        public CustomerType Type { get; set; } 
    }

    با وجود چنین کلاسی از مشتری و نیاز به انجام فعالیت‌های مختلفی بر روی آن، احتمالا نیاز خواهد بود که در بخش‌های مختلف کد، گذاره‌ی switch ای مانند زیر را اضافه کنید:  

    switch (customer.Type) 
    { 
          case CustomerType.Person: 
             // calculate discount, or send message or edit customer or anything else 
              break; 
          case CustomerType.Company: 
              // calculate discount, or send message or edit customer or anything else 
              break; 
          default: 
              throw new ArgumentOutOfRangeException(); 
     }

    برای انجام فعالیت‌های مختلفی مانند محاسبه تخفیف، ارسال پیام و یا ویرایش مشتری، نیاز خواهد بود این گذاره تکرار شود که خود این موضوع بوی بد duplicate code است و به الگوی shotgun surgery نیز ختم خواهد شد. 

    حال فرض کنید نیاز است مشتریان حقوقی، خود به دو نوع مشتری حقوقی بخش خصوصی و مشتری حقوقی بخش دولتی تقسیم شوند. در پیاده سازی ذکر شده باید به CustomerType یک آیتم افزوده شود و در تمامی switch‌ها نیز در صورت نیاز شرط مربوط به آن اضافه شود.  

    برای حل این نوع از کد بد بو، معمولا یک کلاس پدر را به نام مشتری ایجاد کرده و کلاس‌های مختص هر یک از انواع مشتری را از آن به ارث می‌برند (Replace type code with subclass):

    یا می‌توان طراحی را کمی متفاوت‌تر و به صورت زیر انجام داد:

    دلیل مشابه دیگر ایجاد این الگوی بد کد استفاده از type code به عنوان وضعیتیک تایپ است. که در این صورت می‌توان بجای type code از state object استفاده کرد (Replace type code with strategy). به این مورد در مباحث مربوط به refactoring به طور مفصل پرداخته شده است.


    جمع بندی 

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

    ‫طراحی یک گرید با Angular و ASP.NET Core - قسمت اول - پیاده سازی سمت سرور

    $
    0
    0
    یکی از نیازهای مهم هر برنامه‌ای، امکانات گزارشگیری و نمایش لیستی از اطلاعات است. به همین جهت در طی چند قسمت، قصد داریم یک گرید ساده را به همراه امکانات نمایش، صفحه بندی و مرتب سازی اطلاعات، تنها به کمک امکانات توکار Angular و ASP.NET Core تهیه کنیم.




    تهیه مقدمات سمت سرور

    مدلی که در تصویر فوق نمایش داده شده‌است، در سمت سرور چنین ساختاری را دارد:
    namespace AngularTemplateDrivenFormsLab.Models
    {
        public class Product
        {
            public int ProductId { set; get; }
            public string ProductName { set; get; }
            public decimal Price { set; get; }
            public bool IsAvailable { set; get; }
        }
    }

    همچنین یک منبع ساده درون حافظه‌ای را نیز جهت بازگشت 1500 محصول تهیه کرده‌ایم. علت اینجا است که ساختار نهایی اطلاعات آن شبیه به ساختار اطلاعات حاصل از ORMها باشد و همچنین به سادگی قابلیت اجرا و بررسی را داشته باشد:
    using System.Collections.Generic;
    
    namespace AngularTemplateDrivenFormsLab.Models
    {
        public static class ProductDataSource
        {
            private static readonly IList<Product> _cachedItems;
            static ProductDataSource()
            {
                _cachedItems = createProductsDataSource();
            }
    
            public static IList<Product> LatestProducts
            {
                get { return _cachedItems; }
            }
    
            private static IList<Product> createProductsDataSource()
            {
                var list = new List<Product>();
                for (var i = 0; i < 1500; i++)
                {
                    list.Add(new Product
                    {
                        ProductId = i + 1,
                        ProductName = "نام " + (i + 1),
                        IsAvailable = (i % 2 == 0),
                        Price = 1000 + i
                    });
                }
                return list;
            }
        }
    }


    مشخص کردن قرارداد اطلاعات دریافتی از سمت کلاینت

    زمانیکه کلاینت Angular برنامه، اطلاعاتی را به سمت سرور ارسال می‌کند، یک چنین ساختاری را دریافت خواهیم کرد:
     http://localhost:5000/api/Product/GetPagedProducts?sortBy=productId&isAscending=true&page=2&pageSize=7
    درخواست، به یک اکشن متد مشخص ارسال شده‌است و حاوی یک سری کوئری استرینگ مشخص کننده‌ی نام خاصیت یا فیلدی که قرار است مرتب سازی بر اساس آن صورت گیرد، صعودی و نزولی بودن این مرتب سازی، شماره صفحه‌ی درخواستی و تعداد آیتم‌های در هر صفحه، می‌باشد.
    بنابراین اینترفیسی را دقیقا بر اساس نام کلیدهای همین کوئری استرینگ‌ها تهیه می‌کنیم:
        public interface IPagedQueryModel
        {
            string SortBy { get; set; }
            bool IsAscending { get; set; }
            int Page { get; set; }
            int PageSize { get; set; }
        }


    کاهش کدهای تکراری صفحه بندی اطلاعات در سمت سرور

    با تعریف این اینترفیس چند هدف را دنبال خواهیم کرد:
    الف) استاندارد سازی نام خواصی که مدنظر هستند و اعمال یک دست آن‌ها به ViewModelهایی که قرار است از سمت کلاینت دریافت شوند:
        public class ProductQueryViewModel : IPagedQueryModel
        {
            // ... other properties ...
    
            public string SortBy { get; set; }
            public bool IsAscending { get; set; }
            public int Page { get; set; }
            public int PageSize { get; set; }
        }
    برای مثال در اینجا یک ViewModel مخصوص Product را ایجاد کرده‌ایم که می‌تواند شامل یک سری فیلد دیگر نیز باشد. اما یک سری خواص مرتب سازی و صفحه بندی آن، یک دست و مشخص هستند.

    ب) امکان استفاده‌ی از این قرارداد در متدهای کمکی که نوشته خواهند شد:
        public static class IQueryableExtensions
        {
            public static IQueryable<T> ApplyPaging<T>(
              this IQueryable<T> query, IPagedQueryModel model)
            {
                if (model.Page <= 0)
                {
                    model.Page = 1;
                }
    
                if (model.PageSize <= 0)
                {
                    model.PageSize = 10;
                }
    
                return query.Skip((model.Page - 1) * model.PageSize).Take(model.PageSize);
            }
        }
    در حین ارائه‌ی اطلاعات نهایی صفحه بندی شده به کلاینت، همیشه یک قسمت Skip و Take وجود خواهند داشت. این متدها نیز باید بر اساس یک سری خاصیت و مقدار مشخص، مانند صفحه شماره صفحه‌ی جاری و تعداد ردیف‌های در هر صفحه کار کنند. اکنون که قرارداد IPagedQueryModel را تهیه کرده‌ایم و ViewModel ما نیز آن‌را پیاده سازی می‌کند، مطمئن خواهیم بود که می‌توان به سادگی به این خواص دسترسی یافت و همچنین این کد تکراری صفحه بندی را توانسته‌ایم به یک متد الحاقی کمکی منتقل و حجم کدهای نهایی را کاهش دهیم.
    همچنین دراینجا بجای صدور استثناء در حین دریافت مقادیر غیرمعتبر شماره صفحه یا تعداد ردیف‌های هر صفحه، از حالت «بخشنده» بجای حالت «تدافعی» استفاده شده‌است. برای مثال در حالت «بخشنده» اگر شماره صفحه منفی بود، همان صفحه‌ی اول اطلاعات نمایش داده می‌شود؛ بجای صدور یک استثناء (یا حالت «تدافعی و defensive programming»).


    کاهش کدهای تکراری مرتب سازی اطلاعات در سمت سرور

    همانطور که عنوان شد، از سمت کلاینت، چنین لینکی را دریافت خواهیم کرد:
     http://localhost:5000/api/Product/GetPagedProducts?sortBy=productId&isAscending=true&page=2&pageSize=7
    در اینجا، هربار sortBy و isAscending می‌توانند متفاوت باشند و در نهایت به یک چنین کدهایی خواهیم رسید:
    if(model.SortBy == "f1")
    {
       query = !model.IsAscending ? query.OrderByDescending(x => x.F1) : query.OrderBy(x => x.F1);
    }
    امکان نوشتن این نوع کوئری‌ها توسط قابلیت تعریف زنجیره‌وار کوئری‌های LINQ میسر است و در نهایت زمانیکه ToList نهایی فراخوانی می‌شود، آنگاه است که کوئری SQL معادل این‌ها تولید خواهد شد.
    اما در این حالت نیاز است به ازای تک تک فیلدها، یکبار if/else یافتن فیلد و سپس بررسی صعودی و نزولی بودن آن‌ها صورت گیرد که در نهایت ظاهر خوشایندی را نخواهند داشت.

    یک نمونه از مزیت‌های تهیه‌ی قرارداد IPagedQueryModel را در حین نوشتن متد ApplyPaging مشاهده کردید. نمونه‌ی دیگر آن کاهش کدهای تکراری مرتب سازی اطلاعات است:
    namespace AngularTemplateDrivenFormsLab.Utils
    {
        public static class IQueryableExtensions
        {
            public static IQueryable<T> ApplyOrdering<T>(
              this IQueryable<T> query,
              IPagedQueryModel model,
              IDictionary<string, Expression<Func<T, object>>> columnsMap)
            {
                if (string.IsNullOrWhiteSpace(model.SortBy) || !columnsMap.ContainsKey(model.SortBy))
                {
                    return query;
                }
    
                if (model.IsAscending)
                {
                    return query.OrderBy(columnsMap[model.SortBy]);
                }
                else
                {
                    return query.OrderByDescending(columnsMap[model.SortBy]);
                }
            }
        }
    }
    در اینجا متد الحاقی ApplyOrdering، کار دریافت و بررسی خواص مدنظر را از طریق یک دیکشنری انجام می‌دهد و مابقی کدهای تکراری نوشته شده، حذف خواهند شد. برای نمونه، مثالی از نحوه‌ی استفاده‌ی از این متد الحاقی را در ذیل مشاهده می‌کنید:
    var columnsMap = new Dictionary<string, Expression<Func<Product, object>>>()
                {
                    ["productId"] = p => p.ProductId,
                    ["productName"] = p => p.ProductName,
                    ["isAvailable"] = p => p.IsAvailable,
                    ["price"] = p => p.Price
                };
    query = query.ApplyOrdering(queryModel, columnsMap);
    ابتدا نگاشتی بین خواص رشته‌ای دریافتی از سمت کلاینت، با خواص شیء Product برقرار شده‌است. سپس این نگاشت به متد ApplyOrdering ارسال شده‌است. به این ترتیب از نوشتن تعداد زیادی if/else یا switch بر اساس خاصیت SortBy بی‌نیاز شده‌ایم، حجم کدهای نهایی تولیدی کاهش پیدا می‌کنند و برنامه نیز خواناتر می‌شود.


    تهیه قرارداد ساختار اطلاعات بازگشتی از سمت سرور به سمت کلاینت

    تا اینجا قرارداد اطلاعات دریافتی از سمت کلاینت را مشخص کردیم. همچنین از آن برای ساده سازی عملیات مرتب سازی و صفحه بندی اطلاعات کمک گرفتیم. در ادامه نیاز است مشخص کنیم چگونه می‌خواهیم این اطلاعات را به سمت کلاینت ارسال کنیم:
    using System.Collections.Generic;
    
    namespace AngularTemplateDrivenFormsLab.Models
    {
        public class PagedQueryResult<T>
        {
            public int TotalItems { get; set; }
            public IEnumerable<T> Items { get; set; }
        }
    }
    عموما ساختار اطلاعات صفحه بندی شده، شامل تعداد کل آیتم‌های تمام صفحات (خاصیت TotalItems) و تنها اطلاعات ردیف‌های صفحه‌ی جاری درخواستی (خاصیت Items) است و چون در اینجا این Items از هر نوعی می‌تواند باشد، بهتر است آن‌را جنریک تعریف کنیم.


    پایان کار بازگشت اطلاعات سمت سرور با تهیه اکشن متد GetPagedProducts

    در اینجا اکشن متدی را مشاهده می‌کنید که اطلاعات نهایی مرتب سازی شده و صفحه بندی شده را بازگشت می‌دهد:
        [Route("api/[controller]")]
        public class ProductController : Controller
        {
            [HttpGet("[action]")]
            public PagedQueryResult<Product> GetPagedProducts(ProductQueryViewModel queryModel)
            {
                var pagedResult = new PagedQueryResult<Product>();
    
                var query = ProductDataSource.LatestProducts
                                             .AsQueryable();
    
                //TODO: Apply Filtering ... .where(p => p....) ...
    
                var columnsMap = new Dictionary<string, Expression<Func<Product, object>>>()
                {
                    ["productId"] = p => p.ProductId,
                    ["productName"] = p => p.ProductName,
                    ["isAvailable"] = p => p.IsAvailable,
                    ["price"] = p => p.Price
                };
                query = query.ApplyOrdering(queryModel, columnsMap);
    
                pagedResult.TotalItems = query.Count();
                query = query.ApplyPaging(queryModel);
                pagedResult.Items = query.ToList();
                return pagedResult;
            }
        }
    توضیحات تکمیلی

    امضای این اکشن متد، شامل دو مورد مهم است:
     public PagedQueryResult<Product> GetPagedProducts(ProductQueryViewModel queryModel)
    الف) ViewModel ایی که پیاده سازی کننده‌ی IPagedQueryModel است. به این ترتیب می‌توان به ساختار استانداردی از مقادیر مورد نیاز برای صفحه بندی و مرتب سازی رسید و همچنین این ViewModel می‌تواند حاوی خواص اضافی ویژه‌ی خود نیز باشد.
    ب) خروجی آن از نوع PagedQueryResult است که در مورد آن توضیح داده شد. بنابراین باید به همراه تعداد کل رکوردهای جدول محصولات و همچنین تنها آیتم‌های صفحه‌ی جاری درخواستی باشد.

    در ابتدای کار، دسترسی به منبع داده‌ی درون حافظه‌ای ابتدای برنامه را مشاهده می‌کنید. برای اینکه کارکرد آن‌را شبیه به کوئری‌های ORMها کنیم، یک AsQueryable نیز به انتهای آن اضافه شده‌است.
     var query = ProductDataSource.LatestProducts
      .AsQueryable();
    
    //TODO: Apply Filtering ... .where(p => p....) ...
    اینجا دقیقا جائی است که در صورت نیاز می‌توان کار فیلتر اطلاعات و اعمال متد where را انجام داد.

    پس از مشخص شدن منبع داده و فیلتر آن در صورت نیاز، اکنون نوبت به مرتب سازی اطلاعات است:
    var columnsMap = new Dictionary<string, Expression<Func<Product, object>>>()
                {
                    ["productId"] = p => p.ProductId,
                    ["productName"] = p => p.ProductName,
                    ["isAvailable"] = p => p.IsAvailable,
                    ["price"] = p => p.Price
                };
    query = query.ApplyOrdering(queryModel, columnsMap);
    توضیحات این مورد را پیشتر مطالعه کردید و هدف از آن، تهیه یک نگاشت ساده‌ی بین خواص رشته‌ای رسیده‌ی از سمت کلاینت به خواص مدل متناظر با آن است و سپس ارسال آن‌ها به همراه queryModel دریافتی از کاربر، برای اعمال مرتب سازی نهایی.

    در آخر مطابق ساختار PagedQueryResult بازگشتی، ابتدا تعداد کل آیتم‌های منبع داده محاسبه شده‌است و سپس صفحه بندی به آن اعمال گردیده‌است. این ترتیب نیز مهم است و گرنه TotalItems دقیقا به همان تعداد ردیف‌های صفحه‌ی جاری محاسبه می‌شود:
    var pagedResult = new PagedQueryResult<Product>();
    pagedResult.TotalItems = query.Count();
    query = query.ApplyPaging(queryModel);
    pagedResult.Items = query.ToList();
    return pagedResult;


    در قسمت بعد، نحوه‌ی نمایش این اطلاعات را در سمت Angular بررسی خواهیم کرد.


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

    ‫طراحی یک گرید با Angular و ASP.NET Core - قسمت دوم - پیاده سازی سمت کلاینت

    $
    0
    0
    در قسمت قبل، کار پیاده سازی سمت سرور نمایش اطلاعات یک گرید، به پایان رسید. در این قسمت می‌خواهیم از سمت کلاینت، اطلاعات صفحه بندی و مرتب سازی را به سمت سرور ارسال کرده و همچنین نتیجه‌ی دریافتی از سرور را نمایش دهیم.



    پیشنیازهای نمایش اطلاعات گرید به همراه صفحه بندی اطلاعات

    در مطلب «Angular CLI - قسمت ششم - استفاده از کتابخانه‌های ثالث» نحوه‌ی نصب و معرفی کتابخانه‌ی ngx-bootstrap را بررسی کردیم. دقیقا همان مراحل، در اینجا نیز باید طی شوند و از این مجموعه تنها به کامپوننت Pagination آننیاز داریم. همان قسمت ذیل گرید تصویر فوق که شماره صفحات را جهت انتخاب، نمایش داده‌است.
    بنابراین ابتدا فرض بر این است که دو بسته‌ی بوت استرپ و ngx-bootstrap را نصب کرده‌اید:
    > npm install bootstrap --save> npm install ngx-bootstrap --save
    در فایل angular-cli.json. شیوه‌نامه‌ی بوت استرپ را نیز افزوده‌اید:
      "apps": [
        {
          "styles": [
        "../node_modules/bootstrap/dist/css/bootstrap.min.css",
            "styles.css"
          ],
    پس از آن باید به‌خاطر داشت که کامپوننت نمایش صفحه بندی این مجموعه PaginationModule نام دارد و باید در نزدیک‌ترین ماژول مورد نیاز، ثبت و معرفی شود:
    import { PaginationModule } from "ngx-bootstrap";
    
    @NgModule({
      imports: [
        PaginationModule.forRoot()
      ]
    برای نمونه در این مثال، ماژولی به نام simple-grid.module.ts دربرگیرنده‌ی گرید مطلب جاری است و به صورت ذیل به برنامه اضافه شده‌است:
    >ng g m SimpleGrid -m app.module --routing
    بنابراین تعریف PaginationModule باید به قسمت imports این ماژول اضافه شود و تعریف آن در app.module.ts تاثیری بر روی این قسمت نخواهد داشت.

    کامپوننتی هم که مثال جاری را نمایش می‌دهد به صورت ذیل به ماژول SimpleGrid فوق اضافه شده‌است:
    >ng g c SimpleGrid/products-list


    تهیه معادل‌های قراردادهای سمت سرور در سمت Angular

    در قسمت قبل، تعدادی قرارداد مانند پارامترهای دریافتی از سمت کلاینت و ساختار اطلاعات ارسالی به سمت کلاینت را تعریف کردیم. اکنون جهت کار strongly typed با آن‌ها در سمت یک برنامه‌ی تایپ اسکریپتی Angular، کلاس‌های معادل آن‌ها را تهیه می‌کنیم.

    ساختار شیء محصول دریافتی از سمت سرور
    >ng g cl SimpleGrid/app-product
    با این محتوا
    export class AppProduct {
      constructor(
        public productId: number,
        public productName: string,
        public price: number,
        public isAvailable: boolean
      ) {}
    }
    که در اینجا هر کدام از خواص ذکر شده، معادل camel case نمونه‌ی سمت سرور خود هستند (چون JSON.NET در ASP.NET Core، به صورت پیش فرض یک چنین خروجی را تولید می‌کند).

    ساختار معادل پارامترهای صفحه بندی و مرتب سازی ارسالی به سمت سرور
    >ng g cl SimpleGrid/PagedQueryModel
    با این محتوا
    export class PagedQueryModel {
      constructor(
        public sortBy: string,
        public isAscending: boolean,
        public page: number,
        public pageSize: number
      ) {}
    }
    در اینجا همان ساختار IPagedQueryModel سمت سرور را مشاهده می‌کنید. از آن جهت مشخص سازی جزئیات صفحه بندی و نحوه‌ی مرتب سازی اطلاعات، استفاده می‌شود.

    ساختار معادل اطلاعات صفحه بندی شده‌ی دریافتی از سمت سرور
    >ng g cl SimpleGrid/PagedQueryResult
    با این محتوا
    export class PagedQueryResult<T> {
      constructor(public totalItems: number, public items: T[]) {}
    }
    این ساختار جنریک نیز دقیقا معادل همان PagedQueryResult سمت سرور است و حاوی تعداد کل ردیف‌های یک کوئری و تنها قسمتی از اطلاعات صفحه بندی شده‌ی آن می‌باشد.

    ساختار ستون‌های گرید نمایشی
    >ng g cl SimpleGrid/GridColumn
    با این محتوا
    export class GridColumn {
      constructor(
        public title: string,
        public propertyName: string,
        public isSortable: boolean
      ) {}
    }
    هر ستون نمایش داده شده، دارای یک برچسب، خاصیتی مشخص در سمت سرور و بیانگر قابلیت مرتب سازی آن می‌باشد. اگر isSortable به true تنظیم شود، با کلیک بر روی سرستون‌ها می‌توان اطلاعات را بر اساس آن ستون، مرتب سازی کرد.


    تهیه سرویس ارسال اطلاعات صفحه بندی به سرور و دریافت اطلاعات از آن

    پس از تدارک این مقدمات، اکنون کار تعریف سرویسی که این اطلاعات را به سمت سرور ارسال می‌کند و نتیجه را باز می‌گرداند، به صورت ذیل خواهد بود:
    >ng g s SimpleGrid/products-list -m simple-grid.module
    این دستور سبب ایجاد کلاس ProductsListService شده و همچنین قسمت providers ماژول simple-grid را نیز بر این اساس به روز رسانی می‌کند.
    پیش از تکمیل این سرویس، نیاز است متدی را جهت تبدیل یک شیء، به معادل کوئری استرینگ آن تهیه کنیم:
      toQueryString(obj: any): string {
        const parts = [];
        for (const key in obj) {
          if (obj.hasOwnProperty(key)) {
            const value = obj[key];
            if (value !== null && value !== undefined) {
              parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(value));
            }
          }
        }
        return parts.join("&");
      }
    در قسمت قبلامضای متد GetPagedProducts دارای ویژگی HttpGet است. بنابراین، نیاز است اطلاعات را به صورت کوئری استرینگ از سمت کلاینت دریافت کند و متد toQueryString فوق به صورت خودکار بر روی تمام خواص یک شیء دلخواه حرکت کرده و آن‌ها را تبدیل به یک رشته‌ی حاوی کوئری استرینگ‌ها می‌کند.
    [HttpGet("[action]")]
    public PagedQueryResult<Product> GetPagedProducts(ProductQueryViewModel queryModel)
    برای نمونه متد toQueryString فوق است که سبب ارسال یک چنین درخواستی به سمت سرور می‌شود:
     http://localhost:5000/api/Product/GetPagedProducts?sortBy=productId&isAscending=true&page=2&pageSize=7

    پس از این تعریف، سرویس ProductsListService  به صورت ذیل تکمیل خواهد شد:
    @Injectable()
    export class ProductsListService {
      private baseUrl = "api/Product";
    
      constructor(private http: Http) {}
    
      getPagedProductsList(
        queryModel: PagedQueryModel
      ): Observable<PagedQueryResult<AppProduct>> {
        return this.http
          .get(`${this.baseUrl}/GetPagedProducts?${this.toQueryString(queryModel)}`)
          .map(res => {
            const result = res.json();
            return new PagedQueryResult<AppProduct>(
              result.totalItems,
              result.items
            );
          });
      }
    در اینجا از متد toQueryString، جهت تکمیل متد get ارسالی به سمت سرور استفاده شده‌است تا پارامترها را به صورت کوئری استرینگ‌ها تبدیل کرده و ارسال کند.
    سپس در متد map آن، res.json دقیقا همان ساختار PagedQueryResult سمت سرور را به همراه دارد. اینجا است که فرصت خواهیم داشت نمونه‌ی سمت کلاینت آن‌را که در ابتدای بحث تهیه کردیم، وهله سازی کرده و بازگشت دهیم (نگاشت فیلدهای دریافتی از سمت سرور به سمت کلاینت).


    تکمیل کامپوننت نمایش گرید

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


    <table class="table table-striped table-hover table-bordered table-condensed"><thead><tr><th class="text-center" style="width:3%">#</th><th *ngFor="let column of columns" class="text-center"><div *ngIf="column.isSortable" (click)="sortBy(column.propertyName)" style="cursor: pointer">
                {{ column.title }}<i *ngIf="queryModel.sortBy === column.propertyName" class="glyphicon"
                  [class.glyphicon-sort-by-order]="queryModel.isAscending" [class.glyphicon-sort-by-order-alt]="!queryModel.isAscending"></i></div><div *ngIf="!column.isSortable" style="cursor: pointer">
                {{ column.title }}</div></th></tr></thead>
    در اینجا ابتدا بررسی می‌شود که آیا یک ستون قابلیت مرتب سازی را دارد، یا خیر؟ اگر اینطور است، در کنار آن یک گلیف آیکن مرتب سازی درج می‌شود. اگر خیر، صرفا متن عنوان آن نمایش داده خواهد شد. می‌شد تمام این موارد را به ازای هر ستون به صورت مجزایی ارائه داد، اما در این حالت به کدهای تکراری زیادی می‌رسیدیم. به همین جهت از یک حلقه بر روی تعریف ستون‌های این گرید استفاده شده‌است. آرایه‌ی این ستون‌ها نیز به صورت ذیل تعریف می‌شود:
    export class ProductsListComponent implements OnInit {
      columns: GridColumn[] = [
        new GridColumn("Id", "productId", true),
        new GridColumn("Name", "productName", true),
        new GridColumn("Price", "price", true),
        new GridColumn("Available", "isAvailable", true)
      ];

    همچنین در کدهای قالب این کامپوننت، مدیریت کلیک بر روی یک سر ستون را نیز مشاهده می‌کنید:
    export class ProductsListComponent implements OnInit {
      itemsPerPage = 7;
      queryModel = new PagedQueryModel("productId", true, 1, this.itemsPerPage);
    
      sortBy(columnName) {
        if (this.queryModel.sortBy === columnName) {
          this.queryModel.isAscending = !this.queryModel.isAscending;
        } else {
          this.queryModel.sortBy = columnName;
          this.queryModel.isAscending = true;
        }
        this.getPagedProductsList();
      }
    }
    در این‌حالت اگر ستونی که بر روی آن کلیک شده، پیشتر مرتب سازی شده‌است، صرفا خاصیت صعودی بودن آن برعکس خواهد شد. در غیراینصورت، نام خاصیت درخواستی مرتب سازی و جهت آن نیز مشخص می‌شود. سپس مجددا این گرید توسط متد getPagedProductsList رندر خواهد شد.

    کار رندر بدنه‌ی اصلی گرید توسط همین چند سطر در قالب آن مدیریت می‌شود:
    <tbody><tr *ngFor="let item of queryResult.items; let i = index"><td class="text-center">{{ itemsPerPage * (currentPage - 1) + i + 1 }}</td><td class="text-center">{{ item.productId }}</td><td class="text-center">{{ item.productName }}</td><td class="text-center">{{ item.price | number:'.0' }}</td><td class="text-center"><input id="item-{{ item.productId }}" type="checkbox" [checked]="item.isAvailable"
                disabled="disabled" /></td></tr></tbody></table>
    اولین ستون آن، اندکی ابتکاری است. در اینجا شماره ردیف‌های خودکاری در هر صفحه درج خواهند شد. این شماره ردیف نیز جزو ستون‌های منبع داده‌ی فرضی برنامه نیست. به همین جهت برای درج آن، توسط let i = index در ngFor، به شماره ایندکس ردیف جاری دسترسی پیدا می‌کنیم. سپس توسط محاسباتی بر اساس تعداد ردیف‌های هر صفحه و شماره‌ی صفحه‌ی جاری، می‌توان شماره ردیف فعلی را محاسبه کرد.

    در اینجا حلقه‌ای بر روی queryResult.items تشکیل شده‌است. این منبع داده به صورت ذیل در کامپوننت متناظر مقدار دهی می‌شود:
    export class ProductsListComponent implements OnInit {
      itemsPerPage = 7;
      currentPage: number;
      numberOfPages: number;
      isLoading = false;
      queryModel = new PagedQueryModel("productId", true, 1, this.itemsPerPage);
      queryResult = new PagedQueryResult<AppProduct>(0, []);
    
      constructor(private productsListService: ProductsListService) {}
    
      ngOnInit() {
        this.getPagedProductsList();
      }
    
      private getPagedProductsList() {
        this.isLoading = true;
        this.productsListService
          .getPagedProductsList(this.queryModel)
          .subscribe(result => {
            this.queryResult = result;
            this.isLoading = false;
          });
      }
    }
    ابتدا سرویس ProductsListService را که در ابتدای بحث تکمیل شد، به سازنده‌ی این کامپوننت تزریق می‌کنیم. به کمک آن می‌توان در متد getPagedProductsList، ابتدا queryModel جاری را که شامل اطلاعات مرتب سازی و صفحه بندی است، به سرور ارسال کرده و سپس نتیجه‌ی نهایی را به queryResult انتساب دهیم. به این ترتیب تعداد کل رکوردها و همچنین آیتم‌های صفحه‌ی جاری دریافت می‌شوند. اکنون حلقه‌ی ngFor نمایش بدنه‌ی گرید، کار تکمیل صفحه‌ی جاری را انجام خواهد داد.

    قسمت آخر کار، افزودن کامپوننت نمایش شماره صفحات است:


    <div align="center"><pagination [maxSize]="8" [boundaryLinks]="true" [totalItems]="queryResult.totalItems"
          [rotate]="false" previousText="&lsaquo;" nextText="&rsaquo;" firstText="&laquo;"
          lastText="&raquo;" (numPages)="numberOfPages = $event" [(ngModel)]="currentPage"
          (pageChanged)="onPageChange($event)"></pagination></div><pre class="card card-block card-header">Page: {{currentPage}} / {{numberOfPages}}</pre>
    در اینجا از کامپوننت pagination مجموعه‌ی ngx-bootstarp استفاده شده‌است و یک سری از خواص مستند شده‌ی آن‌، مقدار دهی شده‌اند؛ مانند متن‌های صفحه‌ی بعد و قبل و امثال آن. مدیریت کلیک بر روی شماره‌های آن، در کامپوننت جاری به صورت ذیل است:
    export class ProductsListComponent implements OnInit {
      itemsPerPage = 7;
      currentPage: number;
      numberOfPages: number;
    
      onPageChange(event: any) {
        this.queryModel.page = event.page;
        this.getPagedProductsList();
      }
    }
    علت تعریف دو خاصیت اضافه‌ی currentPage و numberOfPages، استفاده‌ی از آن‌ها در قسمت ذیل این شماره‌ها (خارج از کامپوننت نمایش شماره صفحات) جهت نمایش page 1/x است.
    هر زمانیکه کاربر بر روی شما‌ره‌ای کلیک می‌کند، رخ‌داد onPageChange فراخوانی شده و در این‌حالت تنها کافی است شماره صفحه‌ی درخواستی queryModel جاری را به روزرسانی کرده و سپس آن‌را در اختیار متد getPagedProductsList جهت دریافت اطلاعات این صفحه‌ی درخواستی قرار دهیم.


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

    ‫استفاده از کتابخانه‌ی moment-jalaali در برنامه‌های Angular

    $
    0
    0
    چندی قبل مطلب «نمایش تاریخ شمسی توسط JavaScript در AngularJS» را در این سایت مطالعه کردید. در اینجا قصد داریم معادل Angular آن‌را تهیه کنیم (واژه‌ی AngularJS به نگارش‌های 1x اشاره می‌کند و Angular به تمام نگارش‌های پس از 2).


    نصب پیشنیازهای کار با moment-jalaali

    ابتدا نیاز است بسته‌ی npm این کتابخانهرا نصب کنیم که به همراه فایل‌های js مرتبط با آن می‌باشد:
     npm install moment-jalaali --save

    سپس جهت بهبود تجربه‌ی کاربری با آن در IDEهای امروزی، خصوصا VSCode، بهتر است typingsآن‌را نیز نصب کنیم؛ تا علاوه بر داشتن Intellisense، بتوان به صورت strongly typed با آن کار کرد:
     npm install @types/moment-jalaali --save-dev


    VSCode به صورت خودکار پوشه‌ی مخصوص node_modules\@types را تحت نظر قرار می‌دهد و نصب بسته‌های typings در آن، سبب بارگذاری آنی آن‌ها خواهد شد.
    به علاوه اگر به فایل tsconfig.json واقع در ریشه‌ی پروژه نیز دقت کنید، وجود تعریف ذیل، امکان خوانده شدن این تعاریف را توسط کامپایلر TypeScript میسر می‌کند:
    {
        "typeRoots": [
          "node_modules/@types"
        ]
    }

     
    نحوه‌ی کار Strongly Typed با کتابخانه‌ی moment-jalaali در برنامه‌های مبتنی بر TypeScript

    پس از نصب پیشنیازهای یاد شده، ابتدا برای دسترسی به امکانات این کتابخانه، ماژول آن‌را import می‌کنیم:
    import * as momentJalaali from "moment-jalaali";
    
    export class MomentJalaaliTestComponent implements OnInit {
      now: string;
      nowLongDateFormat: string;
      nowExtraLongDateFormat: string;
    
      ngOnInit() {
        this.persianDateTests();
      }
    
      persianDateTests() {
        // https://github.com/jalaali/moment-jalaali
        momentJalaali.loadPersian(/*{ usePersianDigits: true }*/); // نمایش فارسى نام ماه‌ها، روزها و امثال آن
    
        this.now = momentJalaali().format("jYYYY/jMM/jDD HH:mm");
        this.nowLongDateFormat = momentJalaali().format("jD jMMMM jYYYY [ساعت] LT");
        this.nowExtraLongDateFormat = momentJalaali().format(
          "dddd، jD jMMMM jYYYY [ساعت] LT"
        );
      }
    }
    - پس از import ماژولی به نام moment-jalaali، اکنون نحوه‌ی استفاده‌ی از آن‌را در متد persianDateTests مشاهده می‌کنید.
    - متد momentJalaali.loadPersian باید تنها یکبار فراخوانی شود. کار آن تبدیل نام‌های روزها و ماه‌های میلادی، به شمسی است.
    - پس از آن از طریق متد format آن، می‌توان انواع و اقسام حالات مختلف را بررسی کرد که در اینجا سه نمونه را مشاهده می‌کنید.



    نوشتن یک Pipe سفارشی برای تبدیل تاریخ‌های میلادی دریافتی از سرور به قالب شمسی

    پس آشنا شدن با نحوه‌ی استفاده‌ی از این کتابخانه در یک برنامه‌ی تایپ‌اسکریپتی، تبدیل کردن آن به یک Pipe سفارشی بسیار ساده‌است. برای این منظور ابتدا یک Pipe جدید را به ماژول فرضی custom-pipe.module اضافه می‌کنیم:
     ng g p CustomPipe/moment-jalaali -m custom-pipe.module
    با این محتوا:
    import { Pipe, PipeTransform } from "@angular/core";
    
    import * as momentJalaali from "moment-jalaali";
    
    @Pipe({
      name: "momentJalaali"
    })
    export class MomentJalaaliPipe implements PipeTransform {
      transform(value: any, args?: any): any {
        return momentJalaali(value).format(args);
      }
    }
    در اینجا نیز ابتدا ماژول moment-jalaali تعریف شده‌است و سپس توسط آن، value به عنوان پارامتر متد momentJalaali و args به عنوان پارامتر متد format ارسال شده‌اند. در حین استفاده‌ی از Pipe، مقدار value همان تاریخ دریافتی است و args به فرمت خاصی که توسط استفاده کننده مشخص می‌شود، تنظیم خواهد شد.
    به این ترتیب می‌توان یک چنین تبدیلات سمت کاربری را انجام داد که نمونه‌ای از خروجی آن‌را در تصویر فوق نیز ملاحظه می‌کنید:
    <h2>Server side dates:</h2><div *ngFor="let date of dates"><span dir="ltr">{{date | momentJalaali:'jYYYY/jMM/jDD hh:mm' }}</span>,<span dir="rtl">{{date | momentJalaali:'jD jMMMM jYYYY [ساعت] LT'}}</span></div>


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

    ‫بررسی Bad code smell ها: فیلدهای موقتی

    $
    0
    0
    فیلد موقتی یا Temporary field در دسته بندی الگوهای «بد استفاده کنندگان از شیء گرایی» قرار می‌گیرد. در این الگوی بد، فیلدها یا خصوصیات یک کلاس، در شرایط خاصی مقدار گرفته و مورد استفاده قرار می‌گیرند و در بقیه شرایط خالی هستند. 
    زمانیکه در یک کلاس، متدی برای انجام فعالیت خود، تعدادی پارامتر ورودی زیادی نیاز داشته باشد، ممکن است برنامه نویس برای مواجه نشدن با تعداد پارامترهای زیاد ورودی، فیلدها یا خصوصیاتی را در کلاس مربوط به آن متد ایجاد کند. این فیلدها عملا فقط زمان صدا زدن آن متد مقدار گرفته و در بقیه شرایط خالی هستند.
    خواندن و استفاده از این نوع کدها معمولا مشکل و چالش برانگیز است. زیرا خواننده شاهد فیلدهایی است که در اکثر مواقع خالی هستند. همچنین زمان استفاده از این کلاس نمی‌توان از وجود مقادیر فیلدها یا خصوصیات آن‌ها اطمینان لازم را داشت.

    روش‌های اصلاح این کد بد بو 

    برای اصلاح چنین بوی بدی به طور معمول دو راه وجود دارد. 
    اول:با در نظر گرفتن اینکه تمامی کد موجود در متد و فیلدهای مرتبط به آن قابلیت انتقال به کلاس خاص خودشان را دارند، می‌تواند کلاس مجزایی را برای آن متد و فیلدهای مربوطه ایجاد کرد. 
    دوم:برای فیلدها و خصوصیاتی که در خیلی مواقع خالی هستند، می‌توان با روش Null object برای وضعیت خالی بودن آن، یک شیء خالی بی اثر را ایجاد کرد.  
    به مثال زیر توجه کنید: 
    فرض کنید در حال تولید سیستمی هستید که در روال خاصی، نیاز به محاسبه پورسانت فروشنده‌ها دارید. برای محاسبه پورسانت به موارد زیر نیاز است:
    • لیست محصولات فروخته شده
    • درصد کمیسیون خام
    • تاریخ فروش
    • شعبه فروش
    • نوع پرداخت 
    به طور نمونه اگر فروشنده فروش نقدی ای انجام دهد، کمیسیون بیشتری نسبت به کمیسیون پیشفرض، به او تعلق خواهد گرفت و … 
    ممکن است طراحی اولیه برای چنین متدی به صورت زیر باشد: 
    public class Salesman 
    { 
        public void Method1() 
        { 
           return; 
        } 
        public void Method2() 
        { 
            return; 
        } 
        public void Method3() 
        { 
           return; 
        }
        public decimal CalculateCommission(dynamic products, dynamic commissionRate, dynamic saleDate, dynamic branch, dynamic paymentType) 
        { 
           return decimal.MaxValue; 
        } 
    }
    با مشاهده پارامتر‌های زیاد متد، برنامه نویس می‌تواند از روش‌های اصلاح بوی بد «تعداد زیاد پارامترهای ورودی» استفاده کند. یا اینکه برنامه نویس برای خلاصی از این کد بد بو، بجای ارسال پارامتر، فیلدهایی را در کلاس Salesman ایجاد کند؛ مانند کد زیر:  
    public class SalesmanV2 
    { 
        public IEnumerable<dynamic> Products { get; set; } 
        public dynamic CommisionRate { get; set; } 
        public dynamic SaleDate { get; set; } 
        public dynamic Branch { get; set; } 
        public dynamic PaymentType { get; set; } 
        public void Method1() 
        { 
            return; 
        } 
        public void Method2() 
        { 
            return; 
        } 
        public void Method3() 
        { 
            return; 
        } 
        public decimal CalculateCommission() 
        { 
           return decimal.MaxValue; 
        } 
    }
    با این تغییر، پارامترهای متد CalculateCommision به خصوصیاتی در کلاس Salesman تبدیل خواهند شد. دقت کنید این کلاس متدهای دیگری برای فعالیت‌های مختلف دارد.  
    در روش‌های اصلاح این کد بد بو، اشاره به انتقال منطق متد مذکور به کلاس مجزا و مخصوص به خود شده بود. در واقع کد بد بوی «فیلد موقتی» ناشی از عدم رعایت اصل single responsibility است و محاسبه پورسانت را از نظر ذاتی می‌توان وظیفه‌ی اضافه‌ای در این کلاس دانست. با توجه به اینکه می‌توان محاسبه پورسانت را به صورت جداگانه پیاده سازی کرد. به چنین پیاده سازی ای خواهیم رسید.  
    public class SalesmanV3 
    { 
        public void Method1() 
        { 
            return; 
        } 
        public void Method2() 
        { 
            return; 
        } 
        public void Method3() 
        { 
            return; 
        } 
    } 

    public class CommissionCalculator 
    { 
        private IEnumerable<dynamic> _products; 
        private dynamic _commisionRate; 
        private dynamic _saleDate; 
        private dynamic _branch; 
        private dynamic _paymentType; 
        public CommissionCalculator(IEnumerable<dynamic> products, dynamic commisionRate, 
                dynamic saleDate, dynamic branch, dynamic paymentType) 
        { 
            _products = products; 
            _commisionRate = commisionRate; 
            _saleDate = saleDate; 
            _branch = branch; 
            _paymentType = paymentType; 
        } 
    }
    در شرایط نادری، کد بد بوی «فیلد موقتی» ناشی از عدم رعایت اصل single responsibility نیست. در چنین شرایطی می‌توان از null object برای رفع این بوی بد استفاده کرد.

    جمع بندی 

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

    ‫مایکرو سرویس‌ها - قسمت 2 - بررسی ویژگی‌ها

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

    ویژگی‌های یک مایکرو سرویس چیست؟

    بعد از آشنایی با معماری مایکرو سرویس‌ها می‌خواهیم با ویژگی‌های آن آشنا شویم. البته باید به این نکته توجه داشت که همه‌ی معماری‌های مایکروسرویس‌ها این ویژگی‌ها را ندارند؛ ولی میتوان انتظار داشت اکثر آن‌ها این ویژگی‌ها را از خود به نمایش بگذارند.

    Componentization via Services 

    تعریف ما از component، واحدی از نرم افزار می‌باشد که به تنهایی قابل جایگزینی و به‌روز رسانی می‌باشد.
    همانطور که گفته شد، مایکرو سرویس‌ها یک نرم افزار را به سرویس‌های (business component) کوچکتری تقسیم میکنند که به تنهایی قابل توسعه و استقرار می‌باشند. ما کتابخانه‌ها را به عنوان component در نظر میگیریم که به نرم افزار ما پیوند خورده‌اند و به وسیله فراخوانی توابع در پروسه نرم افزار قابل استفاده می‌باشند. در حالیکه سرویس‌ها componentهایی هستند که خارج از پروسه نرم افزار ما می‌باشند و به وسیله مکانیزم‌های ارتباطی مانند Web Service Request و Remote Procedure Call قابل دسترسی هستند.
    در سیستم های یکپارچه که از چندین  component تشکیل شده‌اند و این componentها در جریان یک پروسه با هم در ارتباط هستند، اگر بخواهیم در یک component تغییر ایجاد کنیم، این تغییر نیازمند استقرار مجدد کل سیستم می‌باشد. در حالیکه در معماری مایکرسرویس، کافی است شما  component (سرویس) ای را که تغییر کرده‌است، دوباره مستقر کنید و سایر بخشهای سیستم از این تغییر تاثیر نمی‌پذیرند.

    Technology Heterogeneity 

    یک سیستم را در نظر بگیرید که از همکاری  چندین سرویس تشکیل شده‌است. ما میتوانیم تصمیم بگیریم که برای هر کدام از سرویسها از تکنولوژی متفاوتی استفاده کنیم. این امر به ما اجازه می‌دهد برای حل هر مشکل، از بهترین ابزار و تکنولوژی استفاده کنیم. این امر بر خلاف سیستم‌هایی که تنها باید از تکنولوژی بکار رفته در سیستم استفاده کنند، انعطاف پذیری سیستم را بسیار بالا می‌برد.
    برای روشن شدن موضوع فرض کنید می‌خواهیم در بخشی از سیستم، performance را افزایش دهیم. برای این امر میتوانیم از هر تکنولوژی که به ما کمک میکند، استفاده کنیم. برای نمونه در یک سیستم شبکه اجتماعی، می‌توانیم اطلاعات مربوط به کاربران و ارتباطات آن‌ها را در یک دیتابیس مبتنی بر گراف و اطلاعات مربوط به پست‌ها و نظرات کاربران را در یک دیتابیس سندگرا ذخیره کنیم. به این ترتیب مشاهده میکنیم که وابسته‌ی به تکنولوژی خاصی نمی‌باشیم و می‌توانیم بر اساس نیاز خود، از تکنولوژی مورد نظر بهره ببریم.


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

    Resilience 

    چنانچه یکی ازسرویسها دچار اشکال شود و این مسئله بصورت زنجیروار برای تمام سرویس‌ها رخ ندهد، با جدا شدن آن سرویس، درروند کار سیستم خللی بوجود نمی‌آید و سیستم بدون کمترین مشکلی به ادامه کار خود می‌پردازد. یعنی محدوده یک سرویس، دیوار حائلی می‌شود تا سایر بخشها تاثیر نپذیرند. در سیستم‌های یکپارچه چنانچه یک سرویس دچار اشکال شود، سایر بخش‌های سیستم نیز غیر قابل استفاده می‌شوند. البته می‌توان با نصب نسخه‌های متفاوتی بر روی ماشین‌های متفاوت (Scale out)، تاثیر اینگونه اختلالات را تا حدودی کاهش داد. اما بوسیله مایکرو سرویسها می‌توانیم سیستم‌هایی را بسازیم که قادرند با خطاهای بوجود آمده در سرویس‌ها به‌درستی رفتار کنند؛ تا خللی در کار سیستم ایجاد نشود.

    Scaling

    فرض کنید در بخشی ازیک سیستم، دغدغه Performance بوجود می‌آید. چنانچه ما یک معماری یکپارچه را داشته باشیم، مجبوریم کل آن را scale کنیم. درحالیکه این مشکل تنها در بخش کوچکی از نرم افزار ایجاد شده‌است. اما اگرمعماری ما مایکرو سرویس باشد، فقط همان سرویس مربوطه را که بر روی آن دغدغه داریم، scale می‌کنیم و سایر بخش‌های نرم افزار بدون نیاز به تغییر و بزرگ شدن، به کار خود ادامه می‌دهند.


    Gilt  یک خرده فروش آنلاین در صنعت مد می‌باشد که  بخاطر مسائل Performance به معماری مایکروسرویس‌ها روی آورد. آنها در سال 2007 با یک نرم افزار یکپارچه شروع به کار کردند. اما بعد از دوسال سیستم آنها قادر به مقابله با ترافیک سایت نبود. آنها با تقسیم بندی قسمت‌های اصلی سیستم خود به مایکرو سرویسها، توانستند خیلی بهتر و موثرتر با ترافیک رسیده مقابله کنند و قابلیت دسترسی پذیری سیستم را افزایش دهند. در حال حاضر سیستم  آنها از حدود 450 مایکرو سرویس تشکیل شده که هر کدام روی چندین ماشین مختلف در حال اجرا می‌باشند.


    Ease of Deployment 

    تغییر حتی یک خط کد، در برنامه‌ای که از چندین میلیون خط تشکیل شده است، ما را ملزم به استقرار مجدد کل سیستم  و انتشار نسخه جدید می‌کند. این استقرار می‌تواند تاثیر و ریسک بالایی را داشته باشد و ما را مجبور به تغییرات بیشتر و انتشار نسخه‌های دیگرکند که این حجم زیاد تغییرات ممکن است به محصول ما ضربه بزند.
    اما در مایکرو سرویس‌ها یک تغییر کوچک، تمام سیستم را تحت تاثیر قرار نمی‌دهد. بلکه تنها تغییرات مربوط به سرویس مورد نظر می‌باشد که به صورت مستقل قابلیت استقرار را دارد و چنانچه سیستم دچار مشکل شود، منشاء تغییرات کاملا مشخصی می‌باشد و می‌توان سریعتر سیستم را به وضعیت قابل اطمینان بازگرداند. این ویژگی یکی از مهمترین دلایلی می‌باشد که شرکت‌هایی مانند Netflix و Amazon  به پیاده سازی این معماری روی آورده‌اند.

    Organizational Alignment 

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

    Composability

    یکی از ویژگی‌های کلیدی که در سیستم‌های توزیع شده و سرویس گرا به آن وعده داده می‌شود، قابلیت استفاده مجدد از functionality‌های سیستم می‌باشد. مایکرو سرویس‌ها این امکان را برای ما فراهم می‌کنند تا functionality ‌های سیستم، به روش‌های مختلف و با اهداف گوناگونی مورد استفاده قرار گیرند. اینکه استفاده کنندگان نرم‌افزار ما چگونه از آن استفاده می‌کنند، برای ما مهم است. در حال حاضر ما باید درباره راه‌های مختلفی که می‌توانند قابلیت‌های سیستم را باهم ترکیب کنند تا در برنامه‌های وب، موبایل و یا ابزار‌های پوشیدنی مورد استفاده قرار گیرند، فکر کنیم. در مایکرو سرویس تفکر ما این است که بخش‌های مختلف سیستم خود را از هم جدا کنیم و این بخشها توسط استفاده کننده‌های بیرونی قابل دسترسی باشند و با تغییر شرایط بتوانیم  بخش‌های گوناگونی را بسازیم.


    Optimizing for Replaceability

    اگر شما در یک سازمان متوسط و یا بزرگ، مشغول به کار باشید، ممکن است با یکسیستم بزرگ و قدیمی مواجه شده باشید که در گوشه‌ای از سازمان در حال استفاده می‌باشد و هیچ کسی رغبت نزدیک شدن به آن و دست بردن در آن را ندارد و اگر کسی از شما بپرسد «چرا نمی‌توان آن را تغییر داد؟» خواهید گفت این کار، بسیار پرریسک و پردردسر است.
    با استفاده از معماری مایکروسرویس، هزینه این تغییرات به مراتب کمتر و تغییرات با ضریب اطمینان بالاتری انجام می‌شوند.

    ‫الگوریتم‌های داده کاوی در SQL Server Data Tools یا SSDT - قسمت ششم (آخرین قسمت) - الگوریتم‌ Neural Network و Logistic Regression

    $
    0
    0

    در قسمت قبلبا الگوریتم Association Rules که بیشتر برای تحلیل سبد خرید استفاده می‌شد، آشنا شدیم. در این قسمت که قسمت آخر از سری مقالات الگوریتم‌های داده کاوی در SSDT می‌باشد، با الگوریتم‌های Neural Network و Logistic Regression آشنا می‌شویم.


    Neural Network (هوش مصنوعی)

    مقدمه

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



    توصیف الگوریتم

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

    پیچیدگی تحلیل انجام شده توسط این الگوریتم به دو عامل بر می‌گردد:

    1. ممکن است یک یا تمام ورودی‌ها به طریقی با یک یا همه‌ی خروجی‌ها مرتبط باشند و الگوریتم باید این موضوع را در آموزش مدل درنظر بگیرد.
    2. ممکن است ترکیبات مختلفی از ورودی‌ها به طریقی با خروجی‌ها در ارتباط باشند.

    دسته بندی اسنادیکی از موضوعاتی است که شبکه‌های عصبی بهتر از الگوریتم‌های دیگر آن را حل می‌کنند. البته اگر سرعت برای ما مهم باشد، می‌توان از الگوریتم Naïve Bayesاستفاده کرد. اما درصورتیکه دقت مهم‌تر باشد، آنگاه باید از الگوریتم شبکه‌های عصبی استفاده نمود.


    تفسیر مدل

     نتیجه‌ی حاصله از این الگوریتم نسبت به الگوریتم‌های قبلی کاملا متفاوت است. در اینجا دیگر خبری از طرح محتوای مدل و نمودار گرافیکی لایه آموزش نیست. هدف اصلی در اینجا نمایش تاثیر صفت-مقدار، بر ویژگی قابل پیش بینی است. برای مثال جدول زیر در رابطه با تمایل به خرید یا اجاره خانه در رابطه با صفات مختلف می‌باشد. همانطور که مشخص است، دو ستون اول نشان دهنده‌ی جفت صفت-مقدار و دو ستون دوم، صفت مدنظر جهت پیش بینی را نشان می‌دهند. براساس این جدول می‌توان نتیجه گرفت که مهمترین فاکتور در تمایل به خریداری خانه، سن افراد می‌باشد. افرادی که سنی بین 38 تا 54 سال را دارند، بیشترین تمایل را در خرید یک خانه دارند. فاکتورهایی مانند متاهل بودن، سطح تحصیلات فوق دکترا، بازه سنی 33 تا 38  و خانم بودن نیز دارای اهمیت می‌باشند که به ترتیب از درجه اهمیت آن‌ها کم می‌شود. از طرفی بازه سنی 20 تا 28 سال بیشترین تمایل برای اجاره خانه را دارند. همچنین می‌توان گفت که افرادی که مجرد هستند، طلاق گرفته‌اند و یا سطح تحصیلاتشان دبیرستان است، بیشتر تمایل به اجاره خانه دارند تا به خرید آن.



    Logistic Regression

    همانند الگوریتم شبکه‌های عصبی است؛ با این تفاوت که لایه مخفی‌ای برای تولید ترکیبی از ورودی‌ها ندارد. یعنی سعی در برقراری ارتباط بین ترکیبی از ورودی‌ها و خروجی‌ها نمی‌کند (در واقع همان الگوریتم شبکه‌های عصبی است که پارامتر Hidden Node Ratio آن روی صفر تنظیم شده است). بنابراین سرعت پردازش و آموزش مدل در آن، بالاتر می‌باشد. البته صرف اینکه این الگوریتم دارای پیچیدگی کمتری است نمی‌توان گفت که همیشه ضعیف‌تر از الگوریتم شبکه‌های عصبی است. بلکه حتی در بعضی از مدل‌ها بهتر از الگوریتم شبکه‌های عصبی عمل می‌کند و مانع از باز آموزشی مدل می‌گردد.


    به پایان آمد این دفتر، حکایت همچنان باقی است!

    باسپاس فراوان از تمامی دوستانی که در این مدت سری مقالات الگوریتم‌های داده کاوی را دنبال نمودند. از آنجاکه هر یک از الگوریتم‌ها، دارای ریزه کاری‌های به خصوصی است، بنابراین انتخاب الگوریتم مناسب در رابطه با داده کاوی بسیار حائز اهمیت می‌باشد و به دلیل فرّار بودن این ریزه کاری‌ها، در گذشته بنده هر زمانیکه نیاز به داده کاوی داشتم مجبور بودم مطالب مربوط به الگوریتم‌ها را مطالعه کنم تا بتوانم بهترین الگوریتم (ها) را در رابطه با داده کاوی مدنظر انتخاب نمایم. در نتیجه برآن شدم تا چکیده‌ای نسبتا کارا را از این الگوریتم‌ها که در این شش قسمت آورده شد، تهیه و در اختیار عموم قرار دهم. به امید موفقیت و پیشرفت روز افزون تمامی برنامه نویسان و توسعه دهندگان ایرانی.

    ‫سفارشی سازی صفحه‌ی اول برنامه‌های Angular CLI توسط ASP.NET Core

    $
    0
    0
    در مطلب «Angular CLI - قسمت پنجم - ساخت و توزیع برنامه» با نحوه‌ی ساخت و توزیع برنامه‌های Angular، در دو حالت محیط توسعه و محیط ارائه‌ی نهایی آشنا شدیم. همچنین در مطلب «یکپارچه سازی Angular CLI و ASP.NET Core در VS 2017» نحوه‌ی ترکیب یک برنامه‌ی ASP.NET Core و Angular را بررسی کردیم. در اینجا می‌خواهیم فایل index.html ایی را که Angular CLI تولید می‌کند، با فایل Layout برنامه‌های ASP.NET Core جایگزین کنیم؛ تا بتوانیم در صورت نیاز، سفارشی سازی‌های بیشتری را به صفحه‌ی اول سایت اعمال نمائیم.


    استفاده از Tag Helpers ویژه‌ی ASP.NET Core برای مدیریت محیط‌های توسعه و تولید

    فایل‌های برنامه‌ی تک صفحه‌ای تولید شده‌ی توسط Angular CLI، در نهایت یک چنین شکلی را خواهند داشت:


    این فایل‌ها نیز در حالت توسعه تهیه شده‌اند. در یک برنامه‌ی واقعی، صفحه‌ی ساده‌ی index.html تولیدی آن، تنها می‌تواند یک قالب شروع به کار باشد و نه فایل نهایی که قرار است ارائه شود. نیاز است به این فایل تگ‌های بیشتری را اضافه کرد و سفارشی سازی‌های خاصی را به آن اعمال نمود. در این حالت با توجه به بازنویسی و تولید مجدد این فایل در هر بار ساخت برنامه، می‌توان از فایل Layout پروژه‌ی ASP.NET Core جاری استفاده کرد. به این ترتیب از مزایای Razor و تمام زیرساختی که در اختیار داریم نیز محروم نخواهیم شد.
    بنابراین تنها کاری را که باید انجام دهیم، کپی ساختار فایل index.html تولیدی به فایل Layout برنامه است.

    مشکل!در حالت توسعه، نام فایل‌های تولید شده به همین سادگی است که ملاحظه می‌کنید. اما در حالت ارائه‌ی نهایی، این فایل‌ها به همراه یک هش نیز تولید می‌شوند (پیاده سازی مفهوم cache busting و اجبار به به‌روز رسانی کش مرورگر، باتوجه به تغییر آدرس فایل‌ها)؛ مانند vendor.ea3f8329096dbf5632af.bundle.js

    راه حل اول: تولید فایل‌های نهایی بدون هش
     ng build -prod --output-hashing=none
    در حین ساخت و تولید یک برنامه‌ی Angular CLI در حالت ارائه‌ی نهایی، تنها کافی است سوئیچ output-hashing، به none تنظیم شود، تا این هش‌ها به نام فایل‌های تولیدی اضافه نشوند.
    درکل بهتر است از این روش استفاده نشود، چون با وجود پروکسی‌های کش کردن اطلاعات در بین راه، احتمال اینکه کاربران نگارش‌های قدیمی برنامه را مشاهده کنند، بسیار زیاد است.

    راه حل دوم: تگ Script در ASP.NET Core اجازه‌ی ذکر تمام فایل‌های اسکریپت یک پوشه را نیز می‌دهد
    <script type="text/javascript" asp-src-include="*.js"></script>
    هرچند این قابلیت جالب است و سبب الحاق یکجای تمام فایل‌های js موجود در پوشه‌ی wwwroot خواهد شد، اما پاسخگوی کار ما نخواهد بود؛ چون ترتیب قرارگیری این فایل‌ها مهم است.

    راه حل واقعی
    در اینجا کدهای کامل فایل Views\Shared\_Layout.cshtml را که می‌تواند جایگزین فایل index.html تولیدی توسط Angular CLI باشد، ملاحظه می‌کنید:
    <!DOCTYPE html><html><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><link rel="icon" type="image/x-icon" href="favicon.ico"><title>ng2-lab</title><base href="/"><environment names="Development"></environment><environment names="Staging,Production"><link rel="stylesheet" asp-href-include="~/styles*.css" /></environment></head><body>
        @RenderBody()<app-root></app-root><environment names="Development"><script type="text/javascript" src="/inline.bundle.js"></script><script type="text/javascript" src="/polyfills.bundle.js"></script><script type="text/javascript" src="/scripts.bundle.js"></script><script type="text/javascript" src="/styles.bundle.js"></script><script type="text/javascript" src="/vendor.bundle.js"></script><script type="text/javascript" src="/main.bundle.js"></script></environment><environment names="Production,Staging"><script type="text/javascript" asp-src-include="~/inline*.js"></script><script type="text/javascript" asp-src-include="~/polyfills*.js"></script><script type="text/javascript" asp-src-include="~/scripts*.js"></script><script type="text/javascript" asp-src-include="~/vendor*.js"></script><script type="text/javascript" asp-src-include="~/main*.js"></script></environment></body></html>
    در اینجا دو حالت توسعه و همچنین ارائه‌ی نهایی مدنظر قرار گرفته‌اند. در حالت توسعه، دقیقا از همان نام‌های ساده‌ی تولیدی استفاده شده‌است و در حالت ارائه‌ی نهایی چون نام فایل‌ها به صورت
    [name].[hash].bundle.js
    تولید می‌شوند، می‌توان قسمت هش را با * جایگزین کرد؛ مانند "asp-src-include="~/vendor*.js
    همچنین باید دقت داشت که در حالت توسعه، تمام شیوه نامه‌های برنامه در فایل styles.bundle.js قرار می‌گیرند. اما در حالت ارائه‌ی نهایی، این فایل وجود نداشته و با نام کلی styles*.css تولید می‌شود که باید در head صفحه قرار گیرد (مانند تنظیمات حالت تولید در Layout فوق).


    اصلاح قسمت URL Rewrite برنامه

    در حالت کار با برنامه‌های تک صفحه‌ای وب، در اولین درخواست رسیده‌ی به برنامه ممکن است آدرسی درخواست شود که معادل کنترلر و اکشن متدی را در برنامه‌ی سمت سرور نداشته باشد. در این حالت کاربر را به همان صفحه‌ی index.html هدایت می‌کنیم تا سیستم مسیریابی سمت کلاینت، کار نمایش آن صفحه را انجام دهد:
    app.Use(async (context, next) =>
    {
        await next();
        var path = context.Request.Path.Value;
        if (path != null &&
            context.Response.StatusCode == 404 &&
            !Path.HasExtension(path) &&
            !path.StartsWith("/api/", StringComparison.OrdinalIgnoreCase))
            {
                context.Request.Path = "/index.html";
                await next();
            }
    });
    از آنجائیکه پس از اصلاحات فوق دیگر از این فایل استفاده نمی‌شود، باید تغییر ذیل را نیز اعمال کرد:
     //context.Request.Path = "/index.html";
    context.Request.Path = "/"; // since we are using views/shared/_layout.cshtml now.


    کدهای کامل این قسمت را از اینجامی‌توانید دریافت کنید.
    Viewing all 1980 articles
    Browse latest View live


    <script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>