نظرات مطالب
ASP.NET MVC #17

سلام؛ ممنون بابت پاسختون. این طوری به forgeryToken نگاه نکرده بودم.

در صفحه برای Delete و ثبت اطلاعات و یا حتی فیلترکردن اطلاعات و هرجا که کاربر اطلاعاتی وارد می‌کند از AntiForgeryToken استفاده کردم و حذف و ورود اطلاعات و فیلتر کلا در یک صفحه اما هرکدام در تگ Form مجزایی ارسال و دریافت می‌شود و در هر تگ Form یک ForgeryToken گذاشتم.

برای اونها چه کار کنم؟ اصلا کار بنده درست بوده؟ من هرکجا کاربر اطلاعاتی وارد کرده از Token استفاده کردم ولی مشکل اینجاست که با توجه به طراحی کل اونها نیازه که در یک صفحه باشه.

می تونم یک AntiForgeryToken کلی داشته باشم و با جاوا اسکریپت اون و بخونم و به Form.Serilaise() اضافه کنم. اما می‌خواستم راه بهینه اون و بپرسم.

مطالب
کار با modal dialogs مجموعه Bootstrap در برنامه‌های Angular
در مطلب «Angular CLI - قسمت ششم - استفاده از کتابخانه‌های ثالث» با نحوه‌ی دریافت، نصب و راه اندازی کتابخانه‌ی ngx-bootstrap آشنا شدیم. در اینجا می‌خواهیم نحوه‌ی کار با کامپوننت Modal آن را بررسی کنیم.


سازماندهی بهتر کامپوننت‌های ngx-bootstrap

پس از نصب بسته‌ی npm کتابخانه‌ی ngx-bootstrap و تنظیم فایل angular-cli.json. که در مطلب «Angular CLI - قسمت ششم - استفاده از کتابخانه‌های ثالث» بررسی شدند، برای کار با کامپوننت‌های این کتابخانه باید متدهای BsDropdownModule.forRoot، TooltipModule.forRoot، ModalModule.forRoot و ... را به قسمت imports فایل app.module.ts اضافه کرد. با انجام این‌کار پس از مدتی به یک فایل بسیار شلوغ app.module.ts خواهیم رسید. برای مدیریت بهتر آن می‌توان شبیه به مطلب «سازماندهی برنامه‌های Angular توسط ماژول‌ها» در پوشه‌ی Shared برنامه، ماژول ذیل را تدارک دید. برای اینکار ابتدا فایل جدید src\app\shared\shared.bootstrap.module.ts را ایجاد نمائید. سپس کامپوننت‌های این کتابخانه را به صورت ذیل در این تک ماژول اختصاصی قرار دهید:
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { BsDropdownModule } from "ngx-bootstrap/dropdown";
import { TooltipModule } from "ngx-bootstrap/tooltip";
import { ModalModule } from "ngx-bootstrap/modal";

@NgModule({
  imports: [
    CommonModule,
    BsDropdownModule.forRoot(),
    TooltipModule.forRoot(),
    ModalModule.forRoot()
  ],
  exports: [
    BsDropdownModule,
    TooltipModule,
    ModalModule
  ]
})
export class SharedBootstrapModule { }
متدهای forRoot در قسمت imports قرار می‌گیرند (فلسفه‌ی وجودی این متد و الگوی ویژه را در مطلب «سازماندهی برنامه‌های Angular توسط ماژول‌ها» پیشتر بررسی کرده‌ایم). سپس برای اینکه این کامپوننت‌ها در سایر ماژول‌های برنامه قابل استفاده باشند، باید نام ماژول مرتبط با هر کدام را در قسمت exports نیز ذکر کرد.
اکنون برای استفاده‌ی از SharedBootstrapModule اختصاصی فوق، می‌توان دو روش را بکار برد:
الف) import مستقیم آن در فایل app.module.ts
import { SharedBootstrapModule } from './shared/shared.bootstrap.module';
@NgModule({
  imports: [BrowserModule, SharedBootstrapModule],
  // ...
})
export class AppModule {}
ب) import آن در SharedModule
و یا اگر فایل src\app\shared\shared.module.ts را مطابق مطلب «سازماندهی برنامه‌های Angular توسط ماژول‌ها» ایجاد کرده‌اید، این ماژول به صورت ذیل، در دو قسمت imports و exports آن اضافه خواهد شد:
import { SharedBootstrapModule } from "./shared.bootstrap.module";

@NgModule({
  imports: [
    CommonModule,
    SharedBootstrapModule
  ],
  exports: [
    CommonModule,
    SharedBootstrapModule
  ]
})


نمایش یک modal dialog توسط کامپوننت Modal

پس از تعریف ModalModule.forRoot، اکنون می‌توان به کامپوننت Modal این ماژول دسترسی یافت. برای این منظور کامپوننتی که قرار است یک Modal را نمایش دهد، چنین ساختاری را پیدا می‌کند:
import { Component, OnInit, TemplateRef } from "@angular/core";
import { BsModalRef, BsModalService } from "ngx-bootstrap";

@Component({
  selector: "app-modal-dialog-test",
  templateUrl: "./modal-dialog-test.component.html",
  styleUrls: ["./modal-dialog-test.component.css"]
})
export class ModalDialogTestComponent implements OnInit {

  modalRef: BsModalRef;

  constructor(private modalService: BsModalService) { }

  openModal(template: TemplateRef<any>) {
    this.modalRef = this.modalService.show(template,
      { animated: true, keyboard: true, backdrop: true, ignoreBackdropClick: false });
  }

  closeModal() {
    this.modalRef.hide();
  }
}
توسط سرویس BsModalService که به سازنده‌ی کلاس کامپوننت تزریق شده‌است، می‌توان به متد show آن دسترسی یافت. این متد یک ng-template را قبول می‌کند. بنابراین در قالب این کامپوننت باید قسمتی را که قرار است به صورت modal نمایش داده شود، توسط یک ng-template تعریف کرد.
سپس با فراخوانی متد this.modalService.show می‌توان این قالب را نمایش داد. خروجی این متد ارجاعی را به این modal بازگشت می‌دهد. از این ارجاع می‌توان در جهت بستن آن استفاده کرد (مانند متد closeModal).

بنابراین در ادامه، قالب کامپوننت مثال این قسمت، یک چنین شکلی را پیدا می‌کند:
<h1>Displaying modal bootstrap dialogs</h1>

<button type="button" class="btn btn-info" (click)="openModal(template1)">Create template modal</button>

<ng-template #template1>
  <div class="modal-header">
    <h4 class="modal-title pull-left">Modal</h4>
    <button type="button" class="close pull-right" aria-label="Close" (click)="closeModal()">
      <span aria-hidden="true">&times;</span>
    </button>
  </div>
  <div class="modal-body">
    This is a modal.
  </div>
</ng-template>
در اینجا محتوای modal داخل یک ng-template قرار گرفته‌است و این قالب توسط یک template reference variable به نام template1 مشخص شده‌است. این نام را در متد openModal(template1) استفاده خواهیم کرد تا به متد show سرویس نمایش modal منتقل شود.



طراحی یک کامپوننت عمومی مودال جهت دریافت تائید انجام عملیات

در ادامه می‌خواهیم توسط یک modal dialog، کار دریافت تائید و یا لغو انجام یک عملیات را انجام دهیم. چون این کامپوننت عمومی قرار است در بیش از یک ماژول استفاده شود، بنابراین نیاز است آن‌را در Shared Module ثبت کرد. به همین جهت این کامپوننت را به نحو ذیل در پوشه‌ی Shared ایجاد می‌کنیم:
 ng g c Shared/ConfirmModal --skip-import
پرچم skip-import نیز ذکر شده‌است، چون قصد نداریم به صورت مستقیم از طریق درج selector آن در صفحه، با آن کار کنیم. سرویس سفارشی مودالی که برای این منظور تدارک خواهیم دید، کار نمایش آن‌را انجام می‌دهد.
import { ConfirmModalComponent } from "./confirm-modal/confirm-modal.component";

@NgModule({
  imports: [
  ],
  entryComponents: [
    ConfirmModalComponent
  ],
  declarations: [
    ConfirmModalComponent
  ]
})
export class SharedModule {}
نحوه‌ی درج و تعریف این کامپوننت اندکی متفاوت است. چون این کامپوننت قرار است «به صورت پویا» توسط متد show سرویس BsModalService نمایش داده شود (پارامتر اول آن می‌تواند یک قالب و یا یک کامپوننت کامل باشد)، باید در قسمت entryComponents و declarations مربوط به SharedModule درج شود. آن‌را در قسمت exports ذکر نمی‌کنیم، چون قرار نیست با درج مستقیم selector آن در صفحه، آن‌را نمایش دهیم.

این کامپوننت دریافت تائید کاربر به صورت ذیل تعریف می‌شود:
import { Component } from "@angular/core";

@Component({
  selector: "app-confirm-modal",
  templateUrl: "./confirm-modal.component.html",
  styleUrls: ["./confirm-modal.component.css"]
})
export class ConfirmModalComponent {

  args: {
    title: string;
    message: string;
  };

  close: (val?: any) => void;

}
در اینجا args آن توسط سرویسی که در ادامه طراحی می‌کنیم، مقدار دهی خواهد شد (طراحی args در اینجا کاملا دلخواه است و در کامپوننت‌های مشابه دیگر می‌تواند متفاوت باشد). متد close آن نیز کار گزارش دهی به فراخوان را انجام می‌دهد.
قالب این کامپوننت نیز بدون استفاده از ng-template تعریف می‌شود:
<div class="modal-header">
  <h4 class="modal-title pull-left">{{ args?.title }}</h4>
  <button type="button" class="close pull-right" aria-label="Close" (click)="close()">
    <span aria-hidden="true">&times;</span>
  </button>
</div>
<div class="modal-body">
  <p>{{ args?.message }}</p>
</div>
<div class="modal-footer">
  <button class="btn btn-danger" (click)="close(true)">Yes</button>
  <button class="btn btn-primary" (click)="close()">Cancel</button>
</div>
چون این کامپوننت قرار است به صورت پویا توسط متد show بارگذاری شود، نیازی نیست محتوای قالب آن‌را توسط ng-template مخفی کرد و سپس نمایش داد. زمانیکه این کامپوننت بارگذاری شد، یعنی قصد داریم یک modal کامل را نمایش دهیم.

تا اینجا یک کامپوننت نمایش دریافت تائید انجام عملیات را تهیه کردیم. در ادامه نیاز است یک سرویس را جهت بارگذاری پویای اینگونه کامپوننت‌های مودال طراحی کنیم. این سرویس عمومی در پوشه‌ی Core و CoreModule ثبت خواهد شد:
 >ng g s Core/Modal
با این محتوا
import { Injectable } from "@angular/core";
import { BsModalService } from "ngx-bootstrap";

@Injectable()
export class ModalService {

  constructor(private bsModalService: BsModalService) { }

  show(component: any, args?: any, options?: any): Promise<any> {
    return new Promise(resolve => {
      options = options || {};
      const modal = this.bsModalService.show(component, options);
      let result: any;
      const sub = this.bsModalService.onHidden.subscribe(() => {
        sub.unsubscribe();
        resolve(result);
      });
      modal.content.args = args;
      modal.content.close = (val?: any) => {
        result = val;
        modal.hide();
      };
    });
  }
}
کار این سرویس، نمایش یک کامپوننت مودال مانند ConfirmModalComponent به صورت پویا است؛ از این جهت که متد this.bsModalService.show هم امکان نمایش یک ng-template را دارد و هم یک کامپوننت کامل را به صورت پویا.

یک مودال در سه حالت ممکن است بسته شود:
الف) کلیک بر روی دکمه‌ی close و یا cancel
ب) کلیک بر روی علامت ضربدر درج شده‌ی در یک سمت عنوان آن
ج) کلیک بر روی قسمتی از صفحه، خارج از مودال
در حالات ب و ج، رخ‌داد this.bsModalService.onHidden فراخوانی می‌شود. در حالت الف، همان متد close درج شده‌ی در کامپوننت فراخوانی می‌شود.
برای اینکه بتوان نتیجه‌ی عملیات را از طرف یک سرویس به کامپوننت فراخوان آن گزارش دهیم، یکی از روش‌ها، استفاده از Promiseها است که مشاهده می‌کنید. با فراخوانی resolve(result)، کار ارسال نتیجه‌ی فراخوانی متدهای close(true) و ()close صورت می‌گیرد (یا true و یا undefined).

خاصیت modal.content امکان دسترسی به خواص عمومی کامپوننت در حال استفاده را میسر می‌کند (content به کامپوننت بارگذاری شده اشاره می‌کند). اینجا است که می‌توان برای مثال به خاصیت args یک کامپوننت، مقادیری را نسبت داد و یا به متد close آن دسترسی یافت.

پس از افزودن این سرویس، محل تعریف آن در قسمت providers مربوط به CoreModule است تا در تمام برنامه قابل دسترسی شود:
import { ModalService } from "./modal.service";

@NgModule({
  providers: [
    ModalService
  ]
})
export class CoreModule {}
در پایان برای آزمایش این سرویس جدید، یک دکمه و یک برچسب را به قالب کامپوننت ModalDialogTestComponent ابتدای بحث اضافه می‌کنیم:
<button type="button" class="btn btn-danger" (click)="deleteRecord()">Delete record</button>
<div *ngIf="confirmResult" class="alert alert-info">{{confirmResult}}</div>
با این کدها:
import { ModalService } from "./../../core/modal.service";
import { ConfirmModalComponent } from "./../../shared/confirm-modal/confirm-modal.component";

export class ModalDialogTestComponent implements OnInit {

  confirmResult: string;

  constructor(private modalService: ModalService) { }

  deleteRecord() {
    this.confirmResult = "";
    this.modalService.show(
      ConfirmModalComponent,
      {
        title: "Confirm", message: "Do you want to delete this record?"
      },
      {
        animated: true, keyboard: true, backdrop: true, ignoreBackdropClick: false
      }).then(confirmed => {
        if (confirmed) {
          this.confirmResult = "Deleted!";
        } else {
          this.confirmResult = "Canceled!";
        }
      });
  }
}
در اینجا نحوه‌ی استفاده‌ی از این ModalService سفارشی را ملاحظه می‌کنید. ابتدا به سازنده‌ی کلاس تزریق شده‌است و سپس در متد deleteRecord توسط متد show آن، کامپوننت ConfirmModalComponent به صورت پویا بارگذاری شده‌است. همچنین خاصیت args آن نیز با خواص title و message سفارشی، مقدار دهی شده‌است. چون این متد یک Promise را باز می‌گرداند، می‌توان مشترک آن شد و نتیجه‌ی نهایی را از آن دریافت کرد و بر اساس آن تصمیم گرفت که آیا باید عملیاتی رخ‌دهد، یا خیر.


توسط this.modalService.show می‌توان انواع و اقسام کامپوننت‌های مودال را به صورت پویا بارگذاری کرد و نمایش داد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
نظرات مطالب
React 16x - قسمت 11 - طراحی یک گرید - بخش 1 - کامپوننت صفحه بندی
- زمانیکه بر روی یک شماره صفحه کلیک می‌شود، روال handlePageChange فراخوانی خواهد شد. کار آن فقط setState است (و نه شکل دهی به اطلاعات). در مطلب « React 16x - قسمت 9 - ترکیب کامپوننت‌ها - بخش 3 - Lifecycle Hooks » قسمت توضیح « مرحله‌ی Update » داریم: «... پس از آن (به روز رسانی state) فراخوانی خودکار متد رندر در صف قرار می‌گیرد ...». یعنی نیازی نیست کار شکل دهی به اطلاعات را جای دیگری انجام دهیم. همینقدر که setState را داریم، یعنی حتما چند لحظه بعد متد render فراخوانی می‌شود و در اینجا می‌توان هماهنگی کاملی را بین اجزای مختلف صفحه داشت. برای مثال در قسمت‌های بعدی با کلیک بر روی سر ستون‌ها، sort کردن را خواهیم داشت و یا با انتخاب گروهی از صفحه، این اطلاعات باید فیلتر شوند (هم باید اطلاعات صفحه‌ی انتخابی درست باشد، هم مرتب شده باشد و هم فیلتر شده باشد). در این موارد هم تنها کاری که انجام می‌شود به روز رسانی state است و بعد منتظر شدن برای وقوع render تا اطلاعات یکدستی را نمایش دهیم.
- به علاوه قرار دادن const movies ای که دست آخر باید رندر شود (خلاصه‌ی تمام اعمال)، در state یا هر جای دیگری نه فقط کار محاسباتی React را زیاد می‌کند، بلکه خواندن و درک کدها را هم مشکل می‌کند؛ خصوصا اینکه نیاز داریم ترتیب دقیق فیلتر کردن، مرتب سازی و سپس صفحه بندی را هم بر روی لیست نهایی movies اعمال کنیم.
مطالب
شبیه سازی ارسال ایمیل در ASP.Net

فرض کنید مشغول به کار بر روی کامپیوتری هستید که دسترسی به هیچ شبکه‌ای ندارد و همچنین نیاز است تا قسمت اطلاع رسانی برنامه ASP.Net خود را که از طریق ایمیل کار می‌کند، تست کنید. برای مثال حداقل یکبار شکل و شمایل و محتوای ایمیل واقعی ارسالی آنرا در آوت لوک مشاهده کنید. برای حل این مساله چه باید کرد؟
برای تحقق این منظور باید کمی فایل web.config سایت را ویرایش کرد و سطرهای زیر را به آن افزود (پس از بسته شدن تگ system.web):

<system.net>
<mailSettings>
<smtp deliveryMethod="SpecifiedPickupDirectory">
<specifiedPickupDirectory pickupDirectoryLocation="c:\mail"/>
</smtp>
</mailSettings>
</system.net>

و همچنین در اینجا باید دقت داشت که هنگام کد نویسی دیگر نیازی به ذکر smtp server نخواهد بود و new SmtpClient().Send تنظیمات خودش را از فایل کانفیگ خواهد خواند.
اکنون با هر بار ارسال ایمیل، نتیجه حاصل (مطابق تصاویر زیر) در مسیر c:\mail ذخیره خواهد شد و فرمت حاصل با استفاده از outlook قابل مشاهده است.





شایان ذکر است که این روش با برنامه‌های غیر ASP.Net نیز کار می‌کند و تنها کافی است یک فایل app.config‌ به برنامه اضافه کرده و تنظیمات فوق را به آن اعمال نمائید.

مطالب
React 16x - قسمت 14 - طراحی یک گرید - بخش 4 - پویاسازی تعاریف ستون‌ها
در گریدی که تا به اینجا طراحی کردیم، اگر قرار باشد بجای جدول فیلم‌ها، جدول مشتری‌ها نمایش داده شود، چکار باید کرد؟ با پیاده سازی فعلی، باید کل تعاریف MoviesTable را در کامپوننت دیگری مانند CustomersTable تکرار کنیم. به همین جهت برای پویاسازی تعاریف ستون‌ها نیاز است این قسمت را از جدول اصلی جدا کرده و به کامپوننت مستقلی مانند tableHeader منتقل کنیم.


ایجاد کامپوننت جدید tableHeader

برای پویاسازی تعاریف ستون‌ها و همچنین کم کردن مسئولیت‌های کامپوننت MoviesTable، فایل جدید src\components\common\tableHeader.jsx را ایجاد می‌کنیم تا در برگیرنده‌ی کامپوننت جدید TableHeader شود. پس از ایجاد این فایل، با استفاده از میانبرهای imrc و cc، ساختار ابتدایی کامپوننت TableHeader را تشکیل می‌دهیم. سپس به کامپوننت MoviesTable بازگشته و متد raiseSort آن‌را cut و به اینجا منتقل می‌کنیم. همچنین نیاز است کل thead جدول فیلم‌ها را نیز به اینجا منتقل کنیم. اما چون می‌خواهیم این تعاریف پویا باشند، باید امکان تعریف پویای ستون‌ها را نیز به آن اضافه کنیم. بنابراین اینترفیس این کامپوننت به صورت زیر است:
- ورودی‌های آن: آرایه‌ی ستون‌های جدول و همچنین شیء sortColumn و رخ‌داد onSort که در متد raiseSort استفاده می‌شوند.

با این توضیحات، کامپوننت TableHeader چنین شکلی را پیدا می‌کند:
import React, { Component } from "react";

class TableHeader extends Component {
  raiseSort = path => {
    console.log("raiseSort", path);
    const sortColumn = { ...this.props.sortColumn };
    if (sortColumn.path === path) {
      sortColumn.order = sortColumn.order === "asc" ? "desc" : "asc";
    } else {
      sortColumn.path = path;
      sortColumn.order = "asc";
    }
    this.props.onSort(sortColumn);
  };

  render() {
    return (
      <thead>
        <tr>
          {this.props.columns.map(column => (
            <th onClick={() => this.raiseSort(column.path)}>{column.label}</th>
          ))}
        </tr>
      </thead>
    );
  }
}

export default TableHeader;
در ابتدای آن، متد raiseSort را از کامپوننت MoviesTable به اینجا منتقل کرده‌ایم.
سپس در متد رندر آن، بر اساس آرایه‌ی columns که از props این کامپوننت دریافت خواهد شد، لیست thهای هدر را به صورت پویا رندر می‌کنیم. در اینجا ساختار مورد نیاز شیء column را نیز مشاهده می‌کنید. نیاز است یک برچسب نمایش داده شود و همچنین برای اینکه this.raiseSort نیز بتواند مجددا کار کند، نیاز است نام خاصیتی که قرار است مرتب سازی بر اساس آن انجام شود نیز مشخص باشد. بنابراین تا اینجا شیء column باید دارای دو خاصیت label و path باشد.

پس از تعریف ابتدایی کامپوننت TableHeader، به کامپوننت MoviesTable بازگشته و شروع به استفاده‌ی از آن می‌کنیم:
import TableHeader from "./common/tableHeader";

در ادامه باید آرایه‌ی columns را که به صورت props به کامپوننت TableHeader ارسال می‌شود، تعریف و مقدار دهی کنیم که تشکیل شده‌است از اشیایی با خواص path و label:
  columns = [
    { path: "title", label: "Title" },
    { path: "genre.name", label: "Genre" },
    { path: "numberInStock", label: "Stock" },
    { path: "dailyRentalRate", label: "Rate" },
    {},
    {}
  ];
در اینجا دو شیء خالی را نیز در انتهای لیست مشاهده می‌کنید که به thهای خالی مانند نمایش Like و دکمه‌ی Delete اشاره می‌کنند.
اکنون می‌توان کل تعریف thead موجود در این کامپوننت را به طور کامل با کامپوننت TableHeader ای که import کردیم، جایگزین کنیم:
  render() {
    const { movies, onDelete, onLike, onSort, sortColumn } = this.props;

    return (
      <table className="table">
        <TableHeader
          columns={this.columns}
          sortColumn={sortColumn}
          onSort={onSort}
        />
        <tbody>
در اینجا ویژگی‌های مورد نیاز جهت تامین props کامپوننت TableHeader نیز ذکر شده‌اند. this.columns را که در همین کامپوننت تعریف کردیم، sortColumn و onSort هم جزو props ارسالی به کامپوننت جاری هستند.

در این حالت اگر برنامه را اجرا کنید، بدون مشکل خروجی نهایی را رندر می‌کند؛ اما در کنسول توسعه دهندگان مرورگر یک چنین خطایی را نیز لاگ خواهد کرد:
index.js:1375 Warning: Each child in a list should have a unique "key" prop.
Check the render method of `TableHeader`. See https://fb.me/react-warning-keys for more information.
در حین تعریف رندر لیست thها در کامپوننت TableHeader، ذکر ویژگی key را فراموش کرده‌ایم. البته در اینجا می‌توان از column.path به‌عنوان key استفاده کرد، اما چون در آرایه‌ی ستون‌ها دو شیء خالی را نیز در انتهای لیست داریم، بهتر است برای این‌ها یک id را نیز تعریف کردیم تا بتوان آن‌ها را به صورت منحصربفردی شناسایی کرد:
class MoviesTable extends Component {
  columns = [
    { path: "title", label: "Title" },
    { path: "genre.name", label: "Genre" },
    { path: "numberInStock", label: "Stock" },
    { path: "dailyRentalRate", label: "Rate" },
    { key: "like" },
    { key: "delete" }
  ];
سپس متد رندر کامپوننت TableHeader را جهت درج key به روز رسانی می‌کنیم:
  render() {
    return (
      <thead>
        <tr>
          {this.props.columns.map(column => (
            <th
              key={column.path || column.key}
              style={{ cursor: "pointer" }}
              onClick={() => this.raiseSort(column.path)}
            >
              {column.label}
            </th>
          ))}
        </tr>
      </thead>
    );
دراینجا اگر column.path مقدار دهی شده بود، از آن استفاده می‌شود، در غیراینصورت از مقدار column.key، به عنوان مقدار ویژگی خاصیت key هر المان th، استفاده خواهد شد.


استخراج TableBody از جدول کامپوننت MoviesTable

اکنون با استخراج TableHeader از کامپوننت MoviesTable، به همان مشکل مخلوط بودن درجه‌ی abstractions رسیده‌ایم. از یک طرف با یک abstraction سطح بالا مانند TableHeader در این کامپوننت سر و کار داریم و از طرف دیگر، نمایش تمام جزئیات درونی رندر جدول نیز پیش روی ما است. همچنین رندر ستون‌های آن نیز پویا نیست و هنوز بر اساس خاصیت this.columns تعریف شده، واکنش نشان نمی‌دهد. به همین جهت tbody این جدول را نیز به یک کامپوننت مستقل تبدیل می‌کنیم. برای این منظور فایل جدید src\components\common\tableBody.jsx را اضافه می‌کنیم. سپس با استفاده از میانبرهای imrc و cc، ساختار ابتدایی کامپوننت TableBody را تشکیل می‌دهیم.
این کامپوننت قرار است آرایه‌ای از اشیاء را دریافت و ردیف‌هایی را بر اساس آن‌ها رندر کند. به همین جهت این آرایه را از props و با نام data دریافت می‌کنیم. نام data به عمد انتخاب شده‌است، تا بیانگر عمومی بودن آن باشد؛ بجای استفاده از نام ویژه‌ی آرایه‌ی movies، در این مثال خاص.
import React, { Component } from "react";

class TableBody extends Component {
  render() {
    const { data, columns } = this.props;

    return (
      <tbody>
        {data.map(item => (
          <tr>
            {columns.map(column => (
              <td></td>
            ))}
          </tr>
        ))}
      </tbody>
    );
  }
}

export default TableBody;
تا اینجا ساختار ابتدایی کامپوننت TableBody را مشاهده می‌کنید که هدف آن، رندر پویای قسمت tbody جدول است. این کامپوننت ابتدا نیاز دارد تا data را از props دریافت کند و بر اساس آن، لیست tr‌ها را رندر کند. سپس هر tr نیز از چندین td تشکیل می‌شود. به همین جهت به لیست دومی به نام columns، برای رندر پویای tdها نیاز است.


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

در این مرحله می‌خواهیم محتویات tdها را رندر کنیم و حالت فعلی آن‌ها یک چنین شکلی را داشته و در آن ارجاع مستقیمی به شیء movie و خواص آن وجود دارد:
{movies.map(movie => (
  <tr key={movie._id}>
    <td>{movie.title}</td>
به علاوه این tdها به رندر دکمه‌ی Like و Delete که المان‌های سفارشی نیز محسوب می‌شوند، ختم شده‌اند.
برای رندر خواص اشیاء آرایه‌ی ارسالی به کامپوننت TableBody، می‌توان از روش [] برای دسترسی به مقادیر خواص استفاده کرد که سبب رندر پویای این مقادیر می‌شود:
<td>{item[column.path]}</td>
مشکل! روش item[column.path] با خاصیتی مانند "genre.name" که یک خاصیت تو در تو است، کار نمی‌کند. به همین جهت نیاز به متد زیر، برای انجام اینکار است:
  getPropValue(obj, path) {
    if (!path) {
      return obj;
    }

    const properties = path.split(".");
    return this.getPropValue(obj[properties.shift()], properties.join("."));
  }
بنابراین تا اینجا روش رندر مقدار هر خاصیت به صورت زیر تغییر می‌کند:
<td>{getPropValue(item, column.path)}</td>
 این تغییر می‌تواند 4 ستون اول را بدون مشکل رندر کند. اما برای مثال در ستون پنجم، کامپوننت Like قرار گرفته‌است. برای نمایش آن باید چکار کرد؟
همانطور که در ابتدای این سری نیز بررسی کردیم، عبارات JSX در نهایت به اشیاء خالص جاوا اسکریپتی ترجمه می‌شوند. این ویژگی در حین تعریف المان‌های سفارشی مانند کامپوننت Like نیز صادق است. به همین جهت در آرایه‌ی columns که تعاریف ستون‌های جدول را به همراه دارد، می‌توان یک خاصیت جدید را تعریف و به آن عبارات JSX را انتساب داد. بنابراین تعاریف tdهای Like و Delete را به طور کامل cut کرده و به خاصیت جدید content این دو شیء خالی انتهای لیست آرایه‌ی columns انتساب می‌دهیم:
class MoviesTable extends Component {
  columns = [
    { path: "title", label: "Title" },
    { path: "genre.name", label: "Genre" },
    { path: "numberInStock", label: "Stock" },
    { path: "dailyRentalRate", label: "Rate" },
    {
      key: "like",
      content: movie => (
        <Like liked={movie.liked} onClick={() => this.props.onLike(movie)} />
      )
    },
    {
      key: "delete",
      content: movie => (
        <button
          onClick={() => this.props.onDelete(movie)}
          className="btn btn-danger btn-sm"
        >
          Delete
        </button>
      )
    }
  ];
البته در اینجا جهت مقدار دهی اشیایی مانند movie، بجای استفاده‌ی مستقیم از یک React element، از یک arrow function استفاده کرده‌ایم تا movie را دریافت کند و یک المان React را بازگشت دهد. همچنین پیشتر از متغیرهای onLike و onDelete در کدهای tdها استفاده کرده بودیم که در ابتدای متد رندر تعریف شده بودند؛ اما زمانیکه این قطعات کد را به خاصیت content منتقل می‌کنیم، دیگر شناسایی نمی‌شوند. بنابراین در اینجا برای دسترسی به آن‌ها، مستقیما از props استفاده می‌شود.

مرحله‌ی بعد، مراجعه به کامپوننت tableBody و استفاده از خاصیت جدید content، جهت رندر محتوای آن است. در اینجا در متد renderCell بررسی می‌کنیم اگر ستونی دارای خاصیت content باشد، آن content را رندر می‌کنیم. در غیراینصورت از همان getPropValue متداول استفاده خواهد شد:
  renderCell = (item, column) => {
    if (column.content) {
      return column.content(item);
    }

    return this.getPropValue(item, column.path);
  };

  createKey = (item, column) => {
    return item._id + (column.path || column.key);
  };

  render() {
    const { data, columns } = this.props;

    return (
      <tbody>
        {data.map(item => (
          <tr key={item._id}>
            {columns.map(column => (
              <td key={this.createKey(item, column)}>
                {this.renderCell(item, column)}
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    );
  }
- در متد renderCell، فراخوانی column.content(item) با توجه به function بودن content تعریف شده‌ی در آرایه‌ی columns، در حقیقیت یک عبارت JSX را بازگشت می‌دهد که در خروجی‌های متدهای React مجاز است و در نهایت تبدیل به المان‌های خالص جاوا اسکریپتی در DOM مجازی React و در نهایت DOM اصلی مرورگر می‌شوند.
- همچنین در اینجا یک createKey را نیز مشاهده می‌کنید. المان‌های هر Array.map نوشته شده، نیاز به یک ویژگی key مقدار دهی شده دارند که در دو قسمت trها و همچنین tdها تعریف شده‌است. در فرمول آن جائیکه از || استفاده شده، اگر ستونی دارای path بود، مقدار آن درج می‌شود، اما اگر مانند دو ستون آخر صرفا key تعریف شده بود، وجود || سبب می‌شود تا column.key درنظر گرفته شود و مشکلی رخ ندهد.
- علت تعریف دو متد مجزای renderCell و createKey هم کم شدن بار if/elseها، در بین کدهای درج شده‌ی در ردیف‌های جدول است.

اکنون به کامپوننت MoviesTable مراجعه کرده و کل tbody آن‌را حذف و با المان کامپوننت TableBody، جایگزین می‌کنیم:
//...
import TableBody from "./common/tableBody";
//...

class MoviesTable extends Component {
  // ...

  render() {
    const { movies, onSort, sortColumn } = this.props;

    return (
      <table className="table">
        <TableHeader
          columns={this.columns}
          sortColumn={sortColumn}
          onSort={onSort}
        />
        <TableBody columns={this.columns} data={movies} />
      </table>
    );
  }
}
تا اینجا اگر این تغییرات را ذخیره کرده و برنامه را مجددا در مرورگر بارگذاری کنیم، باید به همان خروجی قبلی برسیم؛ که اینبار تعاریف ستون‌های آن پویا شده‌است.


اضافه کردن آیکن مرتب سازی اطلاعات به سر ستون‌های جدول

در کامپوننت tableHeader، کار رندر thها انجام می‌شود. در اینجا پس از نام سرستون، می‌خواهیم آیکن نمایش صعودی و یا نزولی بودن روش مرتب سازی جاری را نمایش دهیم. برای این منظور، ابتدا متد renderSortIcon را به این کامپوننت اضافه می‌کنیم:
  renderSortIcon = column => {
    const { sortColumn } = this.props;

    if (column.path !== sortColumn.path) {
      return null;
    }

    if (sortColumn.order === "asc") {
      return <i className="fa fa-sort-asc" />;
    }

    return <i className="fa fa-sort-desc" />;
  };
این متد، شیء column در حال رندر را دریافت کرده و بر اساس sortColumn دریافتی از props و همچنین صعودی و یا نزولی بودن روش مرتب سازی، یکی از آیکن‌های font-awesome را به صورت یک المان جدید رندر می‌کند. اگر این column در حال رندر، با sortColumn تعیین شده یکی نبود، آیکنی رندر نمی‌شود (با بازگشت نال، هیچ چیزی رندر نخواهد شد).
و سپس در متد رندر کامپوننت tableHeader، این متد را در کنار label آن ستون درج خواهیم کرد:
{column.label} {this.renderSortIcon(column)}
پس از ذخیره سازی تغییرات و بارگذاری مجدد برنامه در مرورگر، خروجی آن‌را برای نمونه به صورت یک آیکن مثلثی شکل، در کنار عنوان Title می‌توان مشاهده کرد:



استخراج کل Table از جدول کامپوننت MoviesTable

در حال حاضر اگر به پیاده سازی کامپوننت MoviesTable دقت کنیم، یک تگ table به همراه دو کامپوننت TableHeader و TableBody در آن درج شده‌اند. با این طراحی، اگر قصد استفاده‌ی از این امکانات را در جای دیگری داشته باشیم، باید دقیقا همین قطعه کد را تکرار کنیم. به همین جهت کل تگ table این کامپوننت را استخراج کرده و به کامپوننت جدیدی منتقل می‌کنیم. به همین جهت فایل جدید src\components\common\table.jsx را ایجاد کرده و با استفاده از میانبرهای imrc و cc، ساختار ابتدایی کامپوننت Table را تشکیل می‌دهیم. سپس کل تگ table کامپوننت MoviesTable را cut کرده و به متد رندر کامپوننت جدید Table منتقل می‌کنیم. سپس اولین قدم برای سازگار کردن این محتوا با یک کامپوننت جدید، افزودن importهای زیر است:
import TableBody from "./tableBody";
import TableHeader from "./tableHeader";
سپس باید تمام ویژگی‌های استفاده شده‌ی در این المان منتقل شده را از طریق props دریافت کرد که انجام اینکار را در سطر اول متد رندر مشاهده می‌کنید:
import TableBody from "./tableBody";
import TableHeader from "./tableHeader";

class Table extends Component {
  render() {
    const { columns, sortColumn, onSort, data } = this.props;
    return (
      <table className="table">
        <TableHeader
          columns={columns}
          sortColumn={sortColumn}
          onSort={onSort}
        />
        <TableBody columns={columns} data={data} />
      </table>
    );
  }
}

export default Table;
با این تغییرات به یک کامپوننت ساده‌ی با قابلیت استفاده‌ی مجدد رسیده‌ایم. اکنون المان آن‌را در کامپوننت MoviesTable، در جای تگ قبلی table قرار می‌دهیم:
//...

import Table from "./common/table";

class MoviesTable extends Component {
  //... 

  render() {
    const { movies, onSort, sortColumn } = this.props;

    return (
      <Table
        columns={this.columns}
        sortColumn={sortColumn}
        onSort={onSort}
        data={movies}
      />
    );
  }
}


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:  sample-14.zip
مطالب
مسیریابی در Angular - قسمت چهارم - پیش واکشی اطلاعات
اگر مثال قسمت قبل را اجرا کرده باشید، حتما شاهد این تجربه‌ی ناخوشایند کاربری بوده‌اید:
با کلیک بر روی لینک منوی نمایش لیست محصولات، ابتدا قاب خالی لیست محصولات نمایش داده می‌شود:


سپس بعد از یک ثانیه، شاهد بارگذاری اطلاعات جدول لیست محصولات خواهید بود. این یک ثانیه تاخیر را نیز به عمد توسط منبع داده درون حافظه‌ای برنامه ایجاد کردیم، تا بتوان شرایط دنیای واقعی را شبیه سازی کرد:
 InMemoryWebApiModule.forRoot(ProductData, { delay: 1000 }),
برای مدیریت یک چنین حالتی، در سیستم مسیریابی Angular، امکان پیش بارگذاری اطلاعات مسیری خاص، پیش از نمایش قالب آن درنظر گرفته شده‌است.


ارسال اطلاعات ثابت به مسیرهای مختلف برنامه

روش‌های متعددی برای ارسال اطلاعات به مسیرهای مختلف برنامه وجود دارند که تعدادی از آن‌ها را مانند پارامترهای اختیاری، پارامترهای اجباری و پارامترهای کوئری، در قسمت قبل بررسی کردیم. روش دیگری را که در اینجا می‌توان بکار برد، استفاده از خاصیت data تعاریف مسیریابی برنامه است:
 { path: 'products', component: ProductListComponent, data: { pageTitle: 'Product List'} },
خاصیت data، برای تعریف اطلاعات ثابتی که در طول عمر برنامه تغییر نمی‌کنند (static data) مفید است و به صورت مجموعه‌ای از key/valueهای دلخواه، قابل تعریف است.
برای خواندن این اطلاعات ثابت می‌توان از شیء route.snapshot سرویس ActivatedRoute استفاده کرد:
 this.pageTitle = this.route.snapshot.data['pageTitle'];
باید درنظر داشت که چون این اطلاعات ثابت است، در اینجا استفاده‌ی از this.route.params که یک Observable است، غیرضروری می‌باشد.


پیش بارگذاری اطلاعات پویای مسیرهای مختلف برنامه

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

پیاده سازی یک Route Resolver شامل سه مرحله‌است:
الف) ایجاد و ثبت سرویس Route Resolver
ب) معرفی Route Resolver به تنظیمات مسیریابی
ج) خواندن اطلاعات دریافتی توسط Route Resolver به کمک سرویس ActivatedRoute


ایجاد سرویس Route Resolver

یک Route Resolver به صورت یک سرویس جدید ایجاد می‌شود:
> ng g s product/ProductResolver -m product/product.module
installing service
  create src\app\product\product-resolver.service.spec.ts
  create src\app\product\product-resolver.service.ts
  update src\app\product\product.module.ts
پس از ایجاد قالب خالی این سرویس و به روز رسانی خودکار ماژول مرتبط، جهت تکمیل قسمت providers آن (سطر آخر فوق):
 providers: [ProductService, ProductResolverService]

 فایل src\app\product\product-resolver.service.ts را به نحو ذیل تکمیل کنید:
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/map';

import { ProductService } from './product.service';
import { IProduct } from './iproduct';

@Injectable()
export class ProductResolverService implements Resolve<IProduct>  {

  constructor(private productService: ProductService,
    private router: Router) { }

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<IProduct> {
    let id = route.params['id'];
    if (isNaN(id)) {
      console.log(`Product id was not a number: ${id}`);
      this.router.navigate(['/products']);
      return Observable.of(null);
    }

    return this.productService.getProduct(+id)
      .map(product => {
        if (product) {
          return product;
        }
        console.log(`Product was not found: ${id}`);
        this.router.navigate(['/products']);
        return null;
      })
      .catch(error => {
        console.log(`Retrieval error: ${error}`);
        this.router.navigate(['/products']);
        return Observable.of(null);
      });
  }
}
توضیحات:
مرحله‌ی اول تعریف یک سرویس Route Resolver، پیاده سازی اینترفیس جنریک Resolve است:
 export class ProductResolverService implements Resolve<IProduct>  {
پارامتر جنریک Resolve، نوع اطلاعاتی را که دریافت می‌کند، مشخص خواهد کرد.
این اینترفیس پیاده سازی متد resolve را با امضایی که مشاهده می‌کنید، درخواست می‌کند:
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<IProduct> {
در اینجا ActivatedRouteSnapshot حاوی اطلاعاتی است از مسیریابی فعال شده. برای مثال اطلاعاتی مانند پارامترهای مسیریابی را می‌توان از آن دریافت کرد.
RouterStateSnapshot وضعیت مسیریاب را در این لحظه در اختیار این سرویس قرار می‌دهد.
خروجی این متد یک Observable است؛ از نوع اطلاعاتی که دریافت می‌کند. زمانیکه مسیریابی فعال می‌شود، متد resolve را فراخوانی کرده و منتظر پایان کار Observable آن می‌شود. پس از آن است که کامپوننت این مسیریابی را فعالسازی خواهد کرد.

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

پس از آن نیاز به دریافت اطلاعات محصول درخواست شده، از REST Web API برنامه است. به همین جهت سرویس ProductService را که در قسمت قبل معرفی کردیم، به سازنده‌ی کلاس تزریق کرده‌ایم تا از طریق متد getProduct آن، کار دریافت اطلاعات یک محصول را انجام دهیم.
در اینجا متد getProduct(+id) به همراه عملگر + است تا id دریافتی را به عدد تبدیل کند. سپس بر روی این متد، عملگر map فراخوانی شده‌است. به این ترتیب می‌توان به اطلاعات دریافتی از سرور، پیش از بازگشت آن به فراخوان متد resolve، دسترسی یافت. به این ترتیب در اینجا نیز می‌توان یک سری اعتبارسنجی را انجام داد. برای مثال آیا id دریافتی، متناظر با محصولی در سمت سرور است یا خیر؟
map operator خروجی را به صورت یک observable بازگشت می‌دهد. به همین جهت در اینجا نیازی به ذکر return Observable.of نیست.


معرفی Route Resolver به تنظیمات مسیریابی

بعد از پیاده سازی سرویس Route Resolver، نیاز است آن‌را به تنظیمات مسیریابی برنامه اضافه کنیم. به همین جهت فایل src\app\product\product-routing.module.ts را گشوده و تنظیمات آن‌را به شکل زیر تغییر دهید:
import { ProductResolverService } from './product-resolver.service';

const routes: Routes = [
  { path: 'products', component: ProductListComponent },
  {
    path: 'products/:id', component: ProductDetailComponent,
    resolve: { product: ProductResolverService }
  },
  {
    path: 'products/:id/edit', component: ProductEditComponent,
    resolve: { product: ProductResolverService }
  }
];
در اینجا با استفاده از خاصیت resolve تنظیمات مسیریابی، می‌توان لیستی از Route Resolverها را به صورت key/valueها معرفی کرد. در اینجا key، یک نام دلخواه است و value، ارجاعی را به سرویس Route Resolver تعریف شده دارد.
در اینجا هر تعداد Route Resolver مورد نیاز را می‌توان تعریف کرد. برای مثال اگر مسیریابی خاصی، اطلاعات دیگری را نیز از سرویس خاصی دریافت می‌کند، می‌توان یک جفت کلید/مقدار دیگر را نیز برای آن تعریف کرد. فقط باید دقت داشت که keyها باید منحصربفرد باشند.
به این ترتیب اطمینان حاصل خوهیم کرد که اطلاعات مورد نیاز این مسیریابی‌ها، پیش از فعالسازی کامپوننت آن‌ها، از REST Web API برنامه دریافت می‌شوند.

 
خواندن اطلاعات دریافتی توسط Route Resolver به کمک سرویس ActivatedRoute

پس از تعریف سرویس Route Resolver سفارشی خود و معرفی آن به تنظیمات مسیریابی برنامه، قسمت نهایی این عملیات، خواندن این اطلاعات پیش واکشی شده‌است. به همین جهت فایل src\app\product\product-detail\product-detail.component.ts را گشوده و محتوای آن‌را به نحو ذیل اصلاح کنید:
  constructor(private route: ActivatedRoute) { }

  ngOnInit(): void {
    this.product = this.route.snapshot.data['product'];
  }
- اگر قرار نیست Route Resolver، اطلاعات مدنظر را «مجددا» واکشی کند، می‌توان از شیء route.snapshot برای خواندن اطلاعات Resolver متناظر با این مسیریابی استفاده کرد. در اینجا خاصیت data‌، به کلید خاصیت resolve تعریف شده‌ی در تنظیمات مسیریابی برنامه اشاره می‌کند که همان product است.
- همانطور که مشاهده می‌کنید، دیگر در این کامپوننت نیازی به تزریق سرویس ProductService نبوده و قسمت دریافت اطلاعات آن از طریق این سرویس، حذف شده‌است.

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

یک نکته: اگر یک سرویس Route Resolver، در دو کامپوننت مختلف استفاده شود، اطلاعات آن، بین این دو کامپوننت به اشتراک گذاشته خواهد شد.

مرحله‌ی بعد، ویرایش فایل src\app\product\product-edit\product-edit.component.ts است تا کامپوننت ویرایش جزئیات اطلاعات نیز بتواند از قابلیت پیش واکشی اطلاعات استفاده کند. در اینجا هنوز نیاز به سرویس ProductService است تا بتوان اطلاعات را ذخیره و یا حذف کرد. تنها قسمتی که باید تغییر کند، حذف متد getProduct و تغییر متد ngOnInit است:
ngOnInit(): void {
        this.route.data.subscribe(data => {
            this.onProductRetrieved(data['product']);
        });
    }
در اینجا نیز همانند قسمت قبل، نباید از خاصیت route.snapshot.data استفاده کرد؛ زیرا در حالت مشاهده‌ی جزئیات یک محصول و سپس بر روی لینک افزودن یک محصول جدید، چون root URL Segment تغییر نمی‌کند (یا همان قسمت /products/ در URL جاری)، سبب فراخوانی مجدد متد ngOnInit نخواهد شد. به همین جهت به یک Observable برای گوش فرادادن به تغییرات مسیریابی نیاز است و در اینجا route.data نیز یک Observable است.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-routing-lab-03.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
مطالب
استخراج تمام XPathهای یک محتوای HTMLایی به کمک کتابخانه HtmlAgilityPack
اولین قدم کار کردن با کتابخانه قدرتمند HtmlAgilityPack، داشتن XPath معتبر و متناظر با یک گره خاص می‌باشد. هرچند به ظاهر تعدادی از مرورگرها با کمک افزونه‌های خود امکان استخراج این XPathها را فراهم کرده‌اند اما ... عموما این مقادیر ارائه شده، نادرست هستند و بر روی محتوای HTML اصلی یک سایت قابل اجرا نیستند؛ علت هم به نرمال سازی‌های انجام شده بر روی محتوای یک سایت، توسط موتور مرورگر بر می‌گردد.
خود کتابخانه HtmlAgilityPack به ازای هر HtmlNode ایی که ارائه می‌دهد، خاصیت XPath معتبری را نیز به همراه دارد. در ادامه قصد داریم از این امکان توکار استفاده کرده و کلیه XPath‌های یک محتوای HTML ایی را استخراج کنیم.


پردازش تگ‌های تو در توی یک HTML به کمک کتابخانه HtmlAgilityPack

using System;
using System.Linq;
using System.Net;
using System.Text;
using HtmlAgilityPack;

namespace HapTests
{
    public class HtmlReader
    {
        public Action<string> ParseError { set; get; }

        public Func<HtmlNode, bool> ParserHtmlNode { set; get; }

        public void StartParsingHtml(Uri url)
        {
            using (var client = new WebClient { Encoding = Encoding.UTF8 })
            {
                client.Headers.Add("user-agent", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)");
                StartParsingHtml(client.DownloadString(url));
            }
        }

        public void StartParsingHtml(string htmlContent)
        {
            if (string.IsNullOrWhiteSpace(htmlContent))
                throw new ArgumentNullException("content");

            var doc = new HtmlDocument
            {
                OptionCheckSyntax = true,
                OptionFixNestedTags = true,
                OptionAutoCloseOnEnd = true,
                OptionDefaultStreamEncoding = Encoding.UTF8
            };
            doc.LoadHtml(htmlContent);

            if (doc.ParseErrors != null && doc.ParseErrors.Any())
            {
                foreach (var error in doc.ParseErrors)
                {
                    if (ParseError != null)
                        ParseError(error.Code + " - " + error.Reason);
                }
            }

            if (!doc.DocumentNode.HasChildNodes)
                return;

            handleChildren(doc.DocumentNode.ChildNodes);
        }

        private void handleChildren(HtmlNodeCollection nodes)
        {
            foreach (var itm in nodes)
            {
                if (itm.Name.ToLower().Equals("html"))
                {
                    if (itm.Element("body") != null)
                        handleChildren(itm.Element("body").ChildNodes);
                }
                else
                    handleHtmlNode(itm);
            }
        }

        private void parserChildNodes(HtmlNode content)
        {
            foreach (var item in content.ChildNodes)
            {
                handleHtmlNode(item);
            }
        }

        private void handleHtmlNode(HtmlNode htmNode)
        {
            switch (htmNode.Name.ToLower())
            {
                case "html":
                case "body":
                    handleChildren(htmNode.ChildNodes);
                    break;

                default:
                    if (ParserHtmlNode == null)
                        throw new ArgumentNullException("ParserHtmlNode");

                    if (ParserHtmlNode(htmNode))
                        parserChildNodes(htmNode);

                    break;
            }
        }
    }
}
در اینجا کدهایی را ملاحظه می‌کنید که علاوه بر ارائه تنظیمات اولیه HtmlAgilityPack (خصوصا با درنظر گرفتن مباحث ورودی یونیکد)، به صورت بازگشتی (با توجه به اینکه الزاما مسیر یا Node خاصی مدنظر نیست)، کلیه گره‌های یک HTML را بررسی و ارائه می‌دهند.
این کد برای نوشتن مبدل‌های HTML به XYZ بسیار مناسب است. برای مثال اگر بخواهید یک مبدل HTML به PDF را تهیه کنید، کدهای ابتدایی آن همین موارد است:
new HtmlReader
{
    ParseError = error => Console.WriteLine(error),
    ParserHtmlNode = htmlNode =>
    {
        //switch(htmlNode.Name) { }
        return true; //it's a nested node.
    }
}.StartParsingHtml(html);
نمونه‌ای از نحوه استفاده از کدهای کلاس HtmlReader را ملاحظه می‌کنید.
در اینجا html، محتوای HTMLایی در حال بررسی است. ParserHtmlNode یک callback است. هر زمانیکه به یک گره HTML برخورد، آن‌را در اختیار شما قرار می‌دهد. در ادامه فرصت خواهید داشت تا برای نمونه یک swicth را تهیه کرده و مثلا به ازای تگ hr یک خط رسم کنید، به ازای تگ br یک سطر جدید را درنظر بگیرید و الی آخر. اگر خروجی این Func را true درنظر بگیرید، فرض بر این خواهد بود که گره جاری تو در تو است (حالت دنیای واقعی)؛ در غیراینصورت، یک سطح این گره، بیشتر بررسی نخواهد شد.
در این کلاس، ParseError نیز یک callback است و اگر کتابخانه HtmlAgilityPack، در حین آنالیز کدهای HTML دریافتی به خطایی برخورد، آن‌را گزارش خواهد داد.
در کلاس فوق، دو حالت برای متد StartParsingHtml در نظر گرفته شده است. در حالت اول، یک Uri یا آدرس اینترنتی دریافت و سپس آنالیز می‌گردد. در حالت دوم، فرض بر این است که محتوای کدهای HTML مدنظر به هر نحوی پیشتر تهیه شده و به صورت string موجود است.


استخراج کلیه XPathها از یک فایل HTML به کمک کتابخانه HtmlAgilityPack

اکنون که یک HTML Parser عمومی را تهیه کرده‌ایم، استخراج XPathها توسط آن کار ساده‌ای خواهد بود. یک مثال کامل را در این زمینه در ادامه ملاحظه می‌کنید:
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using HtmlAgilityPack;

namespace HapTests
{
    class Program
    {
        static void Main(string[] args)
        {
            var html = 
                @"<table width='750' border='0' style='font-size: 10pt; width: 736px' class='boxcar2 gerd'>
            <tbody><tr>
            <td height='70' colspan='4' class='boxcart1 gerd'>
            <iframe width='718' scrolling='no'>
            </iframe></td>
            </tr>
            <tr>
            <td height='70' colspan='4' class='boxcart1 gerd'>
    </td>
            </tr>
            <tr>
            <td width='193' height='36' class='boxcart2 gerd'>
            <a target='_self' href='Curr.cbi.2.php'>نرخ ارز مبادله ای بانک مرکزی</a></td>
            <td width='181' height='36' class='boxcart2 gerd'>
            <a target='_self' href='Curr.cbi.php'>نرخ ارز مرجع بانک مرکزی</a></td>
            <td width='149' height='36' class='boxcart2 gerd'>
            <a target='_self' href='curv.htm'>نمودار قیمت طلا</a></td>
            <td width='199' height='36' class='boxcart2 gerd'>
            <a target='_self' href='index.php'>قیمت طلا و سکه در بازار ایران</a></td>
            </tr>
            <tr>
            <td height='48' colspan='4' class='boxcart1 gerd'>
            <p dir='rtl'><span style='font-size: 13pt;'>تابلو آنلاین قیمت جهانی طلا و نقره ( دلار 
            )</span></p></td>
            </tr>
            <tr>
            <td height='57' colspan='2' class='boxcart1 gerd'>قیمت لحظه ای هر انس 
            نقره در بازارهای جهانی<br>
            <span style='font-size: 9pt;'>
            </span></td>
            <td height='57' colspan='2' class='boxcart1 gerd'>قیمت لحظه ای هر انس 
            طلا در بازارهای جهانی<br>
            <span style='font-size: 9pt;'>
            </span></td>
            </tr>
            <tr>
            <td height='48' colspan='4' class='boxcart1 gerd'>
            <p dir='rtl'><span style='font-size: 13pt'>تابلو آنلاین قیمت طلا ، سکه 
            و نقره در بازار ایران ( ریال )</span></p>
            </td>
            </tr>
            <tr>
            <td style='direction: rtl; font-size: 8pt' colspan='4'><div align='center'>
                            <table id='gold_tbl'><tbody><tr><th>قیمت طلا</th><th>قیمت زنده</th><th>تغییر</th>
                            <th>کمترین</th><th>بیشترین</th><th>زمان</th></tr><tr><td>انس طلا <sup>دلار</sup></td>
                            <td class='s0_1'>1,375.90</td><td class='c0_1 neg'>(-0.34%) -4.70</td>
                            <td class='l0_1'>1,374.90</td><td class='h0_1'>1,380.80</td><td class='z0_1 fa'>17:53</td>
                            </tr><tr><td>مثقال طلا</td><td class='s3_2'>5,290,000</td>
                            <td class='c3_2 pos'>(1.63%) 85,000</td><td class='l3_2'>5,200,000</td><td class='h3_2'>5,320,000</td><td class='z3_2 fa'>17:50</td></tr><tr><td>گرم طلای 18</td>
                            <td class='s3_3'>1,221,200</td><td class='c3_3 pos'>(1.63%) 19,600</td><td class='l3_3'>1,200,400</td><td class='h3_3'>1,228,100</td><td class='z3_3 fa'>17:50</td>
                            </tr><tr><td>انس نقره <sup>دلار</sup></td><td class='s0_5'>21.83</td><td class='c0_5'>(0.00%) 0.00</td><td class='l0_5'>21.67</td><td class='h0_5'>21.96</td>
                            <td class='z0_5 fa'>17:53</td></tr></tbody></table><br><table id='coin_tbl'><tbody><tr><th>سکه</th><th>قیمت زنده</th><th>تغییر</th><th>کمترین</th>
                            <th>بیشترین</th><th>ارزش طلا</th><th>زمان</th></tr><tr><td>بهار آزادی</td><td class='s3_10'>12,650,000</td><td class='c3_10 pos'>(2.68%) 330,000</td>
                            <td class='l3_10'>12,320,000</td><td class='h3_10'>12,650,000</td><td class='z4_10'>11,918,400</td><td class='z3_10 fa'>16:07</td></tr><tr><td>امامی</td>
                            <td class='s3_11'>12,960,000</td><td class='c3_11 pos'>(2.61%) 330,000</td><td class='l3_11'>12,630,000</td><td class='h3_11'>13,050,000</td><td class='z4_11'>11,918,400</td>
                            <td class='z3_11 fa'>17:43</td></tr><tr><td>نیم</td><td class='s3_12'>6,880,000</td><td class='c3_12 pos'>(2.69%) 180,000</td><td class='l3_12'>6,700,000</td>
                            <td class='h3_12'>6,900,000</td><td class='z4_12'>5,959,200</td><td class='z3_12 fa'>16:08</td></tr><tr><td>ربع</td><td class='s3_13'>4,250,000</td><td class='c3_13 pos'>(2.41%) 100,000</td>
                            <td class='l3_13'>4,150,000</td><td class='h3_13'>4,300,000</td><td class='z4_13'>2,978,100</td><td class='z3_13 fa'>17:42</td></tr><tr><td>گرمی</td><td class='s3_14'>2,940,000</td>   
                            <td class='c3_14 pos'>(3.16%) 90,000</td><td class='l3_14'>2,850,000</td><td class='h3_14'>2,940,000</td><td class='z4_14'>1,465,400</td><td class='z3_14 fa'>17:40</td></tr></tbody></table></div></td>
            </tr>
            </tbody></table>
                ";

            extractXPath(html);
            test(html);
        }

        /// <summary>
        /// Converts /#comment[1] to /comment()[1] 
        /// or /#text[1] to /text()[1]
        /// </summary>
        private static string GetValidXPath(string xpath)
        {
            var index = xpath.LastIndexOf("/");
            var lastPath = xpath.Substring(index);

            if (lastPath.Contains("#"))
            {
                xpath = xpath.Substring(0, index);
                lastPath = lastPath.Replace("#", "");
                lastPath = lastPath.Replace("[", "()[");
                xpath = xpath + lastPath;
            }

            return xpath;
        }

        private static void extractXPath(string html)
        {
            var sb = new StringBuilder();
            new HtmlReader
            {
                ParseError = error => Console.WriteLine(error),
                ParserHtmlNode = htmlNode =>
                {
                    if (htmlNode is HtmlTextNode)
                    {
                        sb.AppendLine("Text NodeName: " + htmlNode.Name.Trim());
                        sb.AppendLine("InnerText: " + htmlNode.InnerText.Trim());
                    }
                    else
                    {
                        sb.AppendLine("NodeName: " + htmlNode.Name.Trim());
                        var nodeText = new StringBuilder();
                        for (int i = 0; (i < htmlNode.OuterHtml.Length && htmlNode.OuterHtml[i] != '>'); i++)
                            nodeText.Append(htmlNode.OuterHtml[i]);

                        nodeText.Append(">");

                        sb.AppendLine("Node Start: " + nodeText.ToString());
                    }

                    sb.AppendLine("XPath: " + GetValidXPath(htmlNode.XPath.Trim()));
                    sb.AppendLine(Environment.NewLine);

                    return true; //it's a nested node.
                }
            }.StartParsingHtml(html);

            File.WriteAllText("xpath.txt", sb.ToString());
            Process.Start("xpath.txt");
        }

        private static void test(string html)
        {
            var doc = new HtmlDocument
            {
                OptionCheckSyntax = true,
                OptionFixNestedTags = true,
                OptionAutoCloseOnEnd = true,
                OptionDefaultStreamEncoding = Encoding.UTF8
            };
            doc.LoadHtml(html);
            var node = doc.DocumentNode.SelectSingleNode("/table[1]/tbody[1]/tr[7]/td[1]/div[1]/table[2]/tbody[1]/tr[6]/td[7]/text()[1]");
            Console.WriteLine(node.InnerText);
        }
    }
}
در این مثال html مقداری است که از یک سایت عمومی دریافت شده است.
سپس نمونه‌ای دیگر از نحوه استفاده از کلاس HtmlReader قسمت قبل را در ادامه، در متد extractXPath ملاحظه می‌کنید. در اینجا کلاس HtmlReader در یک عملیات بازگشتی، کلیه گره‌های تو در توی HTML مورد نظر را آنالیز کرده و توسط callback ایی به نام ParserHtmlNode در اختیار ما قرار می‌دهد. اکنون که این htmlNode را داریم، خاصیت XPath آن دقیقا مقداری است که به دنبالش هستیم.
در اینجا چند نکته حائز اهمیت هستند:
- با بررسی HtmlTextNode، به نودهایی خواهیم رسید که دارای مقدار متنی هستند. در غیراینصورت این گره، خود ابتدای یک سری گره تو در توی دیگر است.
- XPath بازگشتی توسط کتابخانه HtmlAgilityPack نیاز به کمی تمیز سازی دارد. اینکار در متد GetValidXPath انجام شده است.
- در متد test انتهایی، نمونه‌ای از نحوه استفاده از XPathهای استخراجی را ملاحظه می‌کنید.
Text NodeName: #text
InnerText: 17:40
XPath: /table[1]/tbody[1]/tr[7]/td[1]/div[1]/table[2]/tbody[1]/tr[6]/td[7]/text()[1]
برای نمونه سه سطر فوق، یکی از مداخل فایل نهایی تولیدی مثال جاری است. اکنون که XPath را داریم، استفاده از آن جهت استخراج مقدار InnerText مدنظر، ساده خواهد بود.
مطالب
آزمایش Web APIs توسط Postman - قسمت هفتم - استفاده از خروجی OpenAPI Swagger در Postman
در سری «OpenAPI Swagger» با نحوه‌ی مستندسازی یک Web API و همچنین آزمایش دستی اجزای آن به کمک Swagger-UI که رابط کاربری ایجاد شده‌ای بر اساس خروجی Open API است، آشنا شدیم. بنابراین اگر می‌توان رابط کاربری خودکاری را بر اساس OpenAPI Spec ایجاد کرد، به این معنا است که تمام اطلاعات لازم جهت انجام اینکار، هم اکنون در آن قرار دارد. در ادامه قصد داریم تعامل دستی با Swagger-UI را جهت آزمایش Web API، به Postman منتقل کرده تا اجرای مجموعه‌ای از آن‌ها را توسط Collection Runner، خودکار کنیم.


ساخت و ایجاد درخواست‌های Postman به کمک خروجی OpenAPI

در اینجا از همان برنامه‌ای که در سری «مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger» بررسی کردیم، استفاده خواهیم کرد. بنابراین، این برنامه از پیش تنظیم شده‌است و هم اکنون به همراه یک تولید کننده‌ی OpenAPI Specification نیز می‌باشد. آن‌را اجرا کنید تا بتوان به OpenAPI Specification تولیدی آن در آدرس زیر دسترسی یافت:
https://localhost:5001/swagger/LibraryOpenAPISpecification/swagger.json
سپس برنامه‌ی Postman را گشوده و از منوی File، گزینه‌ی Import آن‌را انتخاب کنید:


در برگه‌ی Import from link آن، همان URL فوق را که به خروجی OpenAPI Spec اشاره می‌کند، وارد کنید. اکنون با کلیک بر روی دکمه‌ی Import، یک مجموعه‌ی جدید، به نام Library API، به لیست مجموعه‌های Postman، اضافه می‌شود:


Postman تمام این اطلاعات را به صورت خودکار از OpenAPI Spec استخراج کرده‌است. تمام نام‌ها نیز بر اساس توضیحاتی که برای متدها نوشته‌ایم، انتخاب شده‌اند.


ارسال اولین درخواست به Web API

در اینجا برای نمونه اگر درخواست «Get list of authors» را انتخاب کنیم، یک چنین خروجی ظاهر می‌شود:


همانطور که مشاهده می‌کنید، متغیر {{baseUrl}} را جهت تنظیم آدرس پایه‌ی Web API انتخاب کرده‌است. این نکته در مطلب «قسمت پنجم - انواع متغیرهای قابل تعریف در Postman» بیشتر بحث شده‌است. هدف از تعریف متغیر {{baseUrl}} به این شکل در اینجا، امکان تعریف آن به صورت یک متغیر محیطی است تا بتوان آن‌را به سادگی بر اساس محیط‌های مختلفی که تعریف و انتخاب می‌کنیم، تغییر داد؛ بدون اینکه نیازی باشد اصل درخواست‌های تعریف شده، تغییری کنند. بنابراین در ادامه نیاز است یک محیط جدید را تعریف کنیم.
برای تعریف یک محیط جدید می‌توان بر روی دکمه‌‌ای با آیکن چشم، در بالای سمت راست صفحه و کلیک بر روی گزینه‌ی Add آن، یک محیط جدید را ایجاد کرد:


در صفحه‌ی باز شده ابتدا باید نامی را برای این محیط جدید انتخاب کرد و سپس می‌توان key/valueهایی را مخصوص این محیط، تعریف نمود:


ابتدا یک نام دلخواه وارد شده‌است و سپس متغیر محیطی baseUrl را با مقدار اولیه‌ی https://localhost:5001 تنظیم کرده‌ایم. پس از آن با کلیک بر روی Add پایین این صفحه، کار تعریف این محیط جدید به پایان می‌رسد.

مرحله‌ی بعد، انتخاب این محیط تعریف شده، به عنوان محیط کاری جاری است:


پس از این انتخاب، اگر اشاره‌گر ماوس را به متغیر baseUrl نزدیک کنیم، می‌توان مقدار تنظیم شده‌ی آن‌را مشاهده کرد:


اکنون اگر بر روی دکمه‌ی send این درخواست کلیک کنیم، چنین خروجی ظاهر می‌شود:


علت آن‌را می‌توان در برگه‌ی Authorization درخواست جاری مشاهده کرد:


همانطور که در مطلب «قسمت ششم - یک مثال تکمیلی: تبدیل رابط کاربری مثال JWT به یک مجموعه‌ی Postman» نیز مشاهده کردیم، برای تعریف هدرهای Authorization یا می‌توان به برگه‌ی هدرهای درخواست جاری مراجعه کرد و این هدرها را دستی تولید کرد و یا می‌توان با استفاده از برگه‌ی Authorization آن، کار تعریف این هدرها را ساده نمود. برای مثال در اینجا Postman بر اساس خروجی OpenAPI، دقیقا تشخیص داده‌است که این Web API از Basic authentication استفاده می‌کند. به همین جهت فیلدهای ورود نام کاربری و کلمه‌ی عبور را علاوه بر نوع اعتبارسنجی از پیش انتخاب شده، تدارک دیده‌است.
برای اینکه این مقادیر را نیز تبدیل به متغیرهای محیطی کنیم، برای ویرایش اطلاعات منتسب به محیط جاری، ابتدا باید آن‌را از dropdown محیط‌های بالای صفحه انتخاب کرد. اکنون با کلیک بر روی دکمه‌‌ای با آیکن چشم، در بالای سمت راست صفحه، لینک ویرایش این محیط انتخاب شده ظاهر می‌شود. با کلیک بر روی آن، می‌توان دو متغیر محیطی جدید را تعریف کرد:


پس از تعریف متغیرهای محیطی {{username}} و {{password}}، آن‌ها را در قسمت Authorization درخواست جاری استفاده می‌کنیم:


اینبار اگر مجددا بر روی دکمه‌ی Send کلیک کنیم، خروجی ذیل حاصل خواهد شد:


 
مطالب
شروع به کار با Ember.js
Ember.js کتابخانه‌ای است جهت ساده سازی تولید برنامه‌های تک صفحه‌ای وب. برنامه‌هایی که شبیه به برنامه‌های دسکتاپ در مرورگر کاربر عمل می‌کنند. دو برنامه نویس اصلی آن Yehuda Katz که عضو اصلی تیم‌های jQuery و Ruby on Rails است و Tom Dale که ابتدا SproutCore را به وجود آورد و بعدها به Ember.js تغییر نام یافت، هستند.

منابع اصلی Ember.js

پیش از شروع به بحث نیاز است با تعدادی از سایت‌های اصلی مرتبط با Ember.js آشنا شد:
سایت اصلی: http://emberjs.com
مخزن کدهای آن: https://github.com/emberjs
انجمن اختصاصی پرسش و پاسخ: http://discuss.emberjs.com
موتور قالب‌های آن: http://handlebarsjs.com
لیست منابع مطالعاتی مرتبط مانند ویدیوهای آموزشی و لیست مقالات موجود: http://emberwatch.com
و بسته‌ی نیوگت آن: https://www.nuget.org/packages/EmberJS



مفاهیم پایه‌ای Ember.js

شیء Application
 App = Ember.Application.create();
یک برنامه‌ی Ember.js با تعریف وهله‌ای از شیء Application آن آغاز می‌شود. با اینکار به صورت خودکار رویدادگردان‌هایی به صفحه اضافه می‌شوند. کامپوننت‌های پیش فرض آن ایجاد شده و همچنین قالب اصلی برنامه رندر می‌شود.

مسیر یابی
با مرور قسمت‌های مختلف برنامه توسط کاربر، نیاز است حالات برنامه را مدیریت کرد؛ اینجا است که کار قسمت مسیریابی شروع می‌شود. مسیریابی، منابع مورد نیاز جهت آدرس‌های مشخصی را تامین می‌کند.
App.Router.map(function() {
    this.resource('accounts'); // takes us to /accounts
    this.resource('gallery'); // takes us to /gallery
});
در اینجا نحوه‌ی تعریف آغازین مسیریابی Ember.js را مشاهده می‌کنید که توسط متد resource آن مسیرهای قابل ارائه توسط برنامه مشخص می‌شوند.
به این ترتیب مسیرهای accounts/ و gallery/ قابل پردازش خواهند شد.

این مسیرها، تو در تو نیز می‌توانند باشند. برای مثال:
App.Router.map(function() {
    this.resource('news', function() {
        this.resource('images', function () { // takes us to /news/images
            this.route('add');// takes us to /news/images/add
        });
    });
});
به این ترتیب نحوه‌ی تعریف مسیریابی آدرس news/images/add را مشاهده می‌کنید. همچنین در این مثال از دو متد resource و route استفاده شده‌است. از متد resource برای حالت تعریف اسامی استفاده کنید و از متد route برای تعریف افعال و تغییر دهنده‌ها. برای نمونه در اینجا فعل افزودن تصاویر با متد route مشخص شده‌است.


مدل‌ها
مدل‌ها همان اشیایی هستند که برنامه مورد استفاده قرار می‌دهد و می‌توانند یک آرایه‌ی ساده و یا اشیاء JSON دریافتی از وب سرور باشند.
حداقل به دو روش می‌توان مدل‌ها را تعریف کرد:
الف) با استفاده از افزونه‌ی Ember Data
ب) با کمک شیء Ember.Object
App.SiteLink = Ember.Object.extend({});
App.SiteLink.reopenClass({
    findAll: function() {
        var links = [];
        //… $.getJSON …

        return links;
    }
});
ابتدا یک زیرکلاس از Ember.Object به کمک متد extend ایجاد خواهد شد. سپس از متد توکار reopenClass برای توسعه‌ی API کمک خواهیم گرفت.
در ادامه متد دلخواهی را ایجاد کرده و برای مثال آرایه‌ای از اشیاء دلخواه جاوا اسکریپتی را بازگشت خواهیم داد.
پس از تعریف مدل، نیاز است آن‌را به سیستم مسیریابی معرفی کرد:
App.GalleryRoute = Ember.Route.extend({
    model: function() {
        return App.SiteLink.findAll();
    }
});
به این ترتیب زمانیکه کاربر به آدرس gallery/ مراجعه می‌کند، دسترسی به model وجود خواهد داشت. در اینجا model یک واژه‌ی کلیدی است.


کنترلرها
کنترلرها جهت ارائه‌ی اطلاعات مدل‌ها به View و قالب برنامه تعریف می‌شوند. در اینجا همیشه باید بخاطر داشت که model تامین کننده‌ی اطلاعات است. کنترلر جهت در معرض دید قرار دادن این اطلاعات، به View برنامه کاربرد دارد و مدل‌ها هیچ اطلاعی از وجود کنترلرها ندارند.
کنترلرها علاوه بر اطلاعات model، می‌توانند حاوی یک سری خواص و اشیاء صرفا نمایشی که قرار نیست در بانک اطلاعاتی ذخیره شوند نیز باشند.
در Ember.js قالب‌ها (templates) اطلاعات خود را از کنترلر دریافت می‌کنند. کنترلرها اطلاعات مدل را به همراه سایر خواص نمایشی مورد نیاز در اختیار View و قالب‌های برنامه قرار می‌دهند.


برای تعریف یک کنترلر می‌توان درون شیء مسیریابی، با تعریف متد setupController شروع کرد:
App.GalleryRoute = Ember.Route.extend({
    setupController: function(controller) {
        controller.set('content', ['red', 'yellow', 'blue']);
    }
});
در این مثال یک خاصیت دلخواه به نام content تعریف و سپس آرایه‌ای به آن انتساب داده شده‌است.

روش دوم تعریف کنترلرها با ایجاد یک زیر کلاس از شیء Ember.Controller انجام می‌شود:
App.GalleryController = Ember.Controller.extend({
    search: '',
    content: ['red', 'yellow', 'blue'],
    query: function() {
        var data = this.get('search');
        this.transitionToRoute('search', { query: data });
    }
});


قالب‌ها یا templates
قالب‌ها قسمت‌های اصلی رابط کاربری را تشکیل خواهند داد. در اینجا از کتابخانه‌ای به نام handlebars برای تهیه قالب‌های سمت کاربر کمک گرفته می‌شود.
<script type="text/x-handlebars" data-template-name="sayhello">
    Hello,
    <strong>{{firstName}} {{lastName}}</strong>!
</script>
این قالب‌ها توسط تگ اسکریپت تعریف شده و نوع آن‌ها text/x-handlebars مشخص می‌شود. به این ترتیب Ember.js، این قسمت از صفحه را یافته و عبارات داخل {{}} را با مقادیر دریافتی از کنترلر جایگزین می‌کند.
<script type="text/x-handlebars" data-template-name="sayhello">
    Hello,
    <strong>{{firstName}} {{lastName}}</strong>!
 
    {{#if person}}
    Welcome back,
    <strong>{{person.firstName}} {{person.lastName}}</strong>!
    {{/if}}
 
    <ul>
        {{#each friend in friends}}
        <li>
            {{friend.name}}
        </li>
        {{/each}}
    </ul>
 
    <img {{bindAttr src="link.url" }} />
    {{#linkTo ''about}}About{{/linkTo}}
</script>
در این مثال نحوه‌ی تعریف عبارات شرطی و یا یک حلقه را نیز مشاهده می‌کنید. همچنین امکان اتصال به ویژگی‌هایی مانند src یک تصویر و یا ایجاد لینک‌ها را نیز دارا است.
بهترین مرجع آشنایی با ریز جزئیات کتابخانه‌ی handlebars، مراجعه به سایت اصلی آن است.


قواعد پیش فرض نامگذاری در Ember.js
اگر به مثال‌های فوق دقت کرده باشید، خواصی مانند GalleryController و یا GalleryRoute به شیء App اضافه شده‌اند. این نوع نامگذاری‌ها در ember.js بر اساس روش convention over configuration کار می‌کنند. برای نمونه اگر مسیریابی خاصی را به نحو ذیل تعریف کردید:
 this.resource('employees');
شیء مسیریابی آن App.EmployeesRoute
کنترلر آن App.EmployeesController
مدل آن App.Employee
View آن App.EmployeesView
و قالب آن employees
بهتر است تعریف شوند. به عبارتی اگر اینگونه تعریف شوند، به صورت خودکار توسط Ember.js یافت شده و هر کدام با مسئولیت‌های خاص مرتبط با آن‌ها پردازش می‌شوند و همچنین ارتباطات بین آن‌ها به صورت خودکار برقرار خواهد شد. به این ترتیب برنامه نظم بهتری خواهد یافت. با یک نگاه می‌توان قسمت‌های مختلف را تشخیص داد و همچنین کدنویسی پردازش و اتصال قسمت‌های مختلف برنامه نیز به شدت کاهش می‌یابد.


تهیه‌ی اولین برنامه‌ی Ember.js
تا اینجا نگاهی مقدماتی داشتیم به اجزای تشکیل دهنده‌ی هسته‌ی Ember.js. در ادامه مثال ساده‌ای را جهت نمایش ساختار ابتدایی یک برنامه‌ی Ember.js، بررسی خواهیم کرد.
بسته‌ی Ember.js را همانطور که در قسمت منابع اصلی آن در ابتدای بحث عنوان شد، می‌توانید از سایت و یا مخزن کد آن دریافت کنید و یا اگر از VS.NET استفاده می‌کنید، تنها کافی است دستور ذیل را صادر نمائید:
 PM> Install-Package EmberJS
پس از اضافه شدن فایل‌های js آن به پوشه‌ی Scripts برنامه، در همان پوشه‌، فایل جدید Scripts\app.js را نیز اضافه کنید. از آن برای افزودن تعاریف کدهای Ember.js استفاده خواهیم کرد.
در این حالت ترتیب تعریف اسکریپت‌های مورد نیاز صفحه به صورت ذیل خواهند بود:
<script src="Scripts/jquery-2.1.1.js" type="text/javascript"></script>
<script src="Scripts/handlebars.js" type="text/javascript"></script>
<script src="Scripts/ember.js" type="text/javascript"></script>
<script src="Scripts/app.js" type="text/javascript"></script>
کدهای ابتدایی فایل app.js جهت وهله سازی شیء Application و سپس تعریف مسیریابی صفحه‌ی index بر اساس روش convention over configuration به همراه تعریف یک کنترلر و افزودن متغیری به نام content به آن که با یک آرایه مقدار دهی شده‌است:
App = Ember.Application.create();
App.IndexRoute = Ember.Route.extend({
    setupController:function(controller) {
        controller.set('content', ['red', 'yellow', 'blue']);
    }
});
باید دقت داشت که تعریف مقدماتی Ember.Application.create به همراه یک سری تنظیمات پیش فرض نیز هست. برای مثال مسیریابی index به صورت خودکار به نحو ذیل توسط آن تعریف خواهد شد و نیازی به تعریف مجدد آن نیست:
App.Router.map(function() {
    this.resource('application'); 
    this.resource('index');
});
سپس برای اتصال این کنترلر به یک template خواهیم داشت:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <script src="Scripts/jquery-2.1.1.js" type="text/javascript"></script>
    <script src="Scripts/handlebars.js" type="text/javascript"></script>
    <script src="Scripts/ember.js" type="text/javascript"></script>
    <script src="Scripts/app.js" type="text/javascript"></script>
</head>
<body>
    <script type="text/x-handlebars" data-template-name="index">
        Hello,
        <strong>Welcome to Ember.js</strong>!
        <ul>
            {{#each item in content}}
            <li>
                {{item}}
            </li>
            {{/each}}
        </ul>
    </script>
</body>
</html>
توسط اسکریپتی از نوع text/x-handlebars، اطلاعات آرایه content دریافت و در طی یک حلقه در صفحه نمایش داده خواهد شد.
مقدار data-template-name در اینجا مهم است. اگر آن‌را به هر نام دیگری بجز index تنظیم کنید، منبع دریافت اطلاعات آن مشخص نخواهد بود. نام index در اینجا به معنای اتصال این قالب به اطلاعات ارائه شده توسط کنترلر index است.

تا همینجا اگر برنامه را اجرا کنید، به خوبی کار خواهد کرد. نکته‌ی دیگری که در مورد قالب‌های Ember.js قابل توجه هستند، قالب پیش فرض application است. با تعریف Ember.Application.create یک چنین قالبی نیز به ابتدای هر صفحه به صورت خودکار اضافه خواهد شد:
<body>
    <script type="text/x-handlebars" data-template-name="application">
        <h1>Header</h1>
        {{outlet}}
    </script>
outlet واژه‌‌ای است کلیدی که سبب رندر سایر قالب‌های تعریف شده در صفحه می‌گردد. مقدار data-template-name آن نیز به application تنظیم شده‌است (اگر این مقدار ذکر نگردد نیز به صورت خودکار از application استفاده می‌شود). برای مثال اگر بخواهید به تمام قالب‌های رندر شده در صفحات مختلف، مقدار ثابتی را اضافه کنید (مانند هدر یا منو)، می‌توان قالب application را به صورت دستی به نحو فوق اضافه کرد و آن‌را سفارشی سازی نمود.



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