نظرات مطالب
پلاگین جستجو با jquery و twitter bootstrap
سلام ممنون از مطلب مفیدتون. چند تا سوال از خدمتتون داشتم: 1- آیا دیگه نیاز به قراردادن input نخواهیم داشت ؟ 2- فرض کنین از این سیستم سرچ در بخشی از یک فرم استفاده کردیم و پس از کلیک روی دکمه submit نیاز داریم که به آی دی گزینه ای که توی بخش سرچ انتخاب شده ، دست پیدا کنیم ... چجوری باید به آی دی گزینه ای که توی کنترل سرچ انتخاب شده دست پیدا کنم ؟ با تشکر
نظرات مطالب
سفارشی سازی ASP.NET Core Identity - قسمت ششم - فارسی سازی پیام‌ها
سلام و تشکر. اگر در این حالت بخواهیم سایت را به صورت چندزبانه طراحی کنیم چطور باید این خطاها را سفارشی سازی کنیم که برای زبانهای دیگر هم متن خطا به همان زبان نمایش داده شود؟ همینطور اگه بخواهیم متن این خطاها رو از بانک اطلاعاتی لود کنیم (به دلیل اینکه امکان اضافه و ویرایش کردن متن پیام‌ها به هر زبانی رو در پنل ادمین بخواهیم فراهم کنیم) چطور باید عمل کرد؟
مطالب
مسیریابی در Angular - قسمت دوم - مسیریابی ماژول‌ها
اغلب برنامه‌های بزرگ Angular، ویژگی‌های مختلف خود را به ماژول‌های مجزایی تبدیل می‌کنند. این ماژول‌ها شبیه به مفهوم Area در ASP.NET MVC هستند و هدف آن‌ها نظم بخشیدن به کامپوننت‌های ویژه‌ی یک قسمت خاص از برنامه، در ناحیه‌‌ای مختص به آن می‌باشد. به علاوه ایجاد ماژول‌ها، قابلیت lazy loading مسیریابی‌ها را نیز مسیر می‌کند. هر برنامه‌ی Angular حداقل به همراه یک ماژول است که بر اساس قراردادی، AppModule نام گرفته‌است و در فایل src\app\app.module.ts قرار دارد. با توسعه‌ی برنامه و بیشتر شدن قابلیت‌های آن، استفاده‌ی از این تک ماژول پیش فرض، مشکل تداخل مسئولیت‌ها را به همراه خواهد داشت. برای رفع این مشکل توصیه شده‌است که کامپوننت‌های مرتبط به یک ویژگی خاص را درون ماژولی مختص به خود قرار دهید؛ برای مثال مانند ماژول محصولات، برای نظم دهی به کامپوننت‌های لیست محصولات، جزئیات محصولات، ویرایش محصولات و غیره، ماژول کاربران برای تعریف کامپوننت‌های لاگین، تغییر کلمه‌ی عبور و امثال آن. در این قسمت قصد داریم نحوه‌ی تنظیمات مسیریابی و هدایت کاربران را به ماژول‌های مختلف برنامه، بررسی کنیم.


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

در اینجا نیازی به تنظیم base path نیست و این تنظیم تنها یکبار به ازای کل برنامه انجام می‌شود. همانطور که در قسمت قبل نیز عنوان شد، ماژول مسیریابی Angular و یا همان RouterModule، به همراه سرویسی برای دسترسی به امکانات آن، تنظیمات مسیریابی و یک سری دایرکتیو مانند routerLink، جهت تعامل با آن است. از آنجائیکه سرویس ماژول مسیریابی در فایل src\app\app-routing.module.ts تعریف و تنظیم شده‌است، باید اطمینان حاصل کرد که این سرویس تنها یکبار در طول عمر برنامه وهله سازی می‌شود و از آنجائیکه هر ماژول تنظیمات مجزای مسیریابی خود را خواهد داشت، دیگر نمی‌توان از متد RouterModule.forRoot سراسری استفاده کرد و در اینجا باید از متد forChild این ماژول، جهت تعریف تنظیمات مسیریابی‌های ماژول‌های مختلف کمک گرفت. متد forChild نیز شبیه به همان آرایه‌ی تنظیمات مسیریابی متد forRoot را دریافت می‌کند.

یک مثال: در ادامه‌ی مثالی که در قسمت قبل به کمک Angular CLI ایجاد کردیم، ماژول جدید محصولات را به همراه تنظیمات ابتدایی مسیریابی آن ایجاد می‌کنیم:
 >ng g m product --routing
پس از اجرای این دستور، ماژول جدید محصولات در فایل src\app\product\product.module.ts و تنظیمات ابتدایی آن در فایل src\app\product\product-routing.module.ts تشکیل می‌شوند:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class ProductRoutingModule { }
همانطور که مشاهده می‌کنید، در حین تشکیل تنظیمات ابتدایی مسیریابی این ماژول جدید، اینبار از متد forChild استفاده شده‌است و نه متد forRoot که مختص به ماژول اصلی و سراسری برنامه‌است.
سپس ProductRoutingModule به قسمت imports ماژول محصولات به صورت خودکار اضافه شده‌است:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { ProductRoutingModule } from './product-routing.module';

@NgModule({
  imports: [
    CommonModule,
    ProductRoutingModule
  ],
  declarations: []
})
export class ProductModule { }

در ادامه کامپوننت جدید لیست محصولات را به این ماژول اضافه می‌کنیم:
 >ng g c product/ProductList
به این ترتیب داخل پوشه‌ای به نام produc-list، کامپوننت product-list.component.ts تشکیل خواهد شد. در حقیقت این دستور اعمال ذیل را انجام می‌دهد:
 installing component
  create src\app\product\product-list\product-list.component.css
  create src\app\product\product-list\product-list.component.html
  create src\app\product\product-list\product-list.component.spec.ts
  create src\app\product\product-list\product-list.component.ts
  update src\app\product\product.module.ts
اگر به سطر آخر آن دقت کنید، کار به روز رسانی فایل ماژول محصولات، جهت درج این کامپوننت جدید، در قسمت declarations فایل product.module.ts نیز به صورت خودکار انجام شده‌است:
import { ProductListComponent } from './product-list/product-list.component';

@NgModule({
  imports: [
  ],
  declarations: [ProductListComponent]
})
export class ProductModule { }

اکنون که این ماژول جدید را به همراه یک کامپوننت نمونه در آن تعریف کردیم، برای افزودن مسیریابی به آن، به فایل src\app\product\product-routing.module.ts مراجعه کرده و آرایه‌ی  Routes آن‌را تکمیل می‌کنیم:
import { ProductListComponent } from './product-list/product-list.component';

const routes: Routes = [
  { path: 'products', component: ProductListComponent }
];
ابتدا کامپوننت لیست محصولات import شده و سپس آرایه‌ی Routes مسیری را به این کامپوننت تعریف کرده‌ایم.

در ادامه می‌خواهیم لینکی را به این مسیریابی جدید اضافه کنیم. در قسمت قبل منویی را به برنامه اضافه کردیم. به همین جهت به فایل src\app\app.component.html مراجعه کرده و routerLink جدیدی را به آن اضافه می‌کنیم:
<nav class="navbar navbar-default">
  <div class="container-fluid">
    <a class="navbar-brand">{{title}}</a>
    <ul class="nav navbar-nav">
      <li>
        <a [routerLink]="['/home']">Home</a>
      </li>
      <li>
        <a [routerLink]="['/products']">Product List</a>
      </li>
    </ul>
  </div>
</nav>
<div class="container">
  <router-outlet></router-outlet>
</div>
پیشتر لینکی را به کامپوننت welcome در قسمت قبل اضافه کرده بودیم. در اینجا لینک جدیدی را به کامپوننت لیست محصولات، در ذیل آن تعریف کرده‌ایم.
در اینجا نیز نحوه‌ی تعریف لینک‌ها مانند قبل است و آرایه‌ی تنظیمات پارامترهای لینک باید به مقدار خاصیت path تعریف شده اشاره کند.

اکنون دستور ng serve -o را صادر کنید تا برنامه در حافظه ساخته شده و در مرورگر نمایش داده شود. در ادامه اگر بر روی لینک لیست محصولات کلیک کنید، صفحه‌ی ذیل را مشاهده خواهید کرد:


به این معنا که برنامه اطلاعی از این مسیریابی جدید نداشته و صفحه‌ی یافت نشدن مسیریابی را که در قسمت قبل تنظیم کردیم، نمایش داده‌است. برای رفع این مشکل باید به فایل src\app\app.module.ts مراجعه کرده و این ماژول جدید را به آن معرفی کنیم:
import { ProductModule } from './product/product.module';

@NgModule({
  declarations: [
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,

    ProductModule,

    AppRoutingModule
  ],
در اینجا در ابتدا ماژول محصولات import شده و سپس به قسمت لیست imports ماژول App اضافه گردیده‌است. اکنون مسیریابی به این کامپوننت جدید واقع شده‌ی در قسمت ماژول محصولات، کار می‌کند:


نکته 1: علت اینکه ProductModule را پیش از AppRoutingModule تعریف کردیم این است که AppRoutingModule دارای تعریف مسیریابی ** یا catch all است که در قسمت قبل آن‌را جهت مدیریت مسیرهای یافت نشده به برنامه افزودیم. اگر ابتدا AppRoutingModule تعریف می‌شد و سپس ProductModule، هیچگاه فرصت به پردازش مسیریابی‌های ماژول محصولات نمی‌رسید؛ چون مسیر ** پیشتر برنده شده بود.
نکته 2: می‌توان در قسمت import متد RouterModule.forRoot را نیز مستقیما قرار داد (بجای AppRoutingModule). اگر این کار صورت گیرد، ابتدا مسیریابی‌های موجود در ماژول‌ها پردازش می‌شوند و در آخر مسیرهای موجود در RouterModule.forRoot صرفنظر از محل قرارگیری آن در این لیست بررسی خواهد شد (حتی اگر در ابتدای لیست قرار گیرد). هرچند جهت مدیریت بهتر برنامه، این متد به AppRoutingModule منتقل شده‌است. بنابراین اکنون «نکته‌ی 1» برقرار است.


انتخاب استراتژی مناسب نامگذاری مسیرها

هنگام کار کردن با تعدادی ویژگی مرتبط به هم قرار گرفته‌ی داخل یک ماژول، بهتر است روش نامگذاری مناسبی را برای تنظیمات مسیریابی آن درنظر گرفت تا مسیرهای تعیین شده علاوه بر زیبایی، وضوح بیشتری را نیز پیدا کنند. به علاوه این نامگذاری مناسب، گروه بندی مسیریابی‌ها و lazy loading آن‌ها را نیز ساده‌تر می‌کند.
استراتژی ابتدایی که به ذهن می‌رسد، نامگذاری هر مسیر بر اساس عملکرد آن‌ها است مانند products برای نمایش لیست محصولات، product/:id برای نمایش جزئیات محصولی خاص که در اینجا id پارامتر مسیریابی است و productEdit/:id برای ویرایش جزئیات یک محصول مشخص. همانطور که مشاهده می‌کنید، هرچند این مسیرها متعلق به یک ماژول هستند، اما مسیرهای تعیین شده‌ی برای آن‌ها اینگونه به نظر نمی‌رسد. بنابراین بهتر است تمام ویژگی‌های قرار گرفته‌ی درون یک ماژول را با مسیر ریشه‌ی یکسانی شروع کنیم. به این ترتیب نمایش لیست محصولات همان products باقی خواهد ماند اما برای نمایش جزئیات محصولی خاص از مسیر products/:id استفاده می‌کنیم (همان اسم جمع ریشه‌ی مسیر؛ بجای اسم مفرد). اینبار مسیر ویرایش جزئیات یک محصول به صورت products/:id/edit تنظیم خواهد شد:
products
products/:id
products/:id/edit
در اینجا نام ریشه‌ی یکسانی را برای عناصر مختلف قرارگرفته‌ی درون یک ماژول انتخاب کرده‌ایم؛ تا ارتباط بین آن‌ها بهتر مشخص شود و همچنین در آینده بتوان مباحث گروه بندی و lazy loading را نیز بر این اساس پیاده سازی کرد.


فعالسازی یک مسیر با کدنویسی

تا اینجا نحوه‌ی فعالسازی یک مسیر را با استفاده از دایرکتیو routerLink بررسی کردیم. اما گاهی از اوقات نیاز است تا بتوان با کدنویسی نیز کاربران را به مسیری خاص هدایت کرد. برای مثال پس از عملیات logout می‌خواهیم مجددا صفحه‌ی اول سایت نمایش داده شود. برای اینکار از سرویس Router مسیریاب Angular کمک گرفته می‌شود. ابتدا آن‌را در سازنده‌ی یک کامپوننت تزریق کرده و سپس می‌توان به قابلیت‌های آن مانند استفاده‌ی از متد navigate آن، در کدهای برنامه دسترسی یافت.
باید درنظر داشت که دایرکتیو routerLink نیز در پشت صحنه از همین متد navigate سرویس Router استفاده می‌کند. بنابراین تمام پارامترهای آن در متد navigate نیز قابل استفاده هستند. برای مثال زمانیکه تعداد پارامترهای routerLink یک مورد است، می‌توان آرایه‌ی آن‌را به یک رشته خلاصه کرد. یک چنین قابلیتی با متد navigate نیز میسر است.
متد navigate تنها قسمت‌هایی از URL جاری را تغییر می‌دهد. اگر نیاز باشد تا کل آدرس تعویض شود، می‌توان از متد دیگر سرویس Router به نام navigateByUrl استفاده کرد. این متد تمام URL segments موجود را با مسیر جدیدی جایگزین می‌کند. به علاوه برخلاف متد navigate، تنها یک رشته را به عنوان پارامتر می‌پذیرد.

در ادامه مثال جاری می‌خواهیم پیاده سازی ابتدایی login و logout را به برنامه اضافه کنیم. به همین منظور ابتدا ماژول جدید user را به همراه تنظیمات ابتدایی مسیریابی آن اضافه می‌کنیم:
 >ng g m user --routing
به این ترتیب دو فایل src\app\user\user-routing.module.ts و src\app\user\user.module.ts به برنامه اضافه می‌شوند.
همانند ماژول قبلی، نیاز است UserModule را به قسمت imports فایل src\app\app.module.ts نیز معرفی کنیم:
import { UserModule } from './user/user.module';

@NgModule({
  declarations: [
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,

    ProductModule,
    UserModule,

    AppRoutingModule
  ],

سپس کامپوننت جدید لاگین را به ماژول user برنامه اضافه می‌کنیم:
 >ng g c user/login
که اینکار سبب به روز رسانی فایل user.module.ts جهت تکمیل قسمت declarations آن با LoginComponent نیز می‌شود.

در ادامه به فایل src\app\user\user-routing.module.ts مراجعه کرده و مسیریابی جدیدی را به کامپوننت لاگین تعریف می‌کنیم:
import { LoginComponent } from './login/login.component';

const routes: Routes = [
  { path: 'login', component: LoginComponent}
];
ابتدا این کامپوننت import شده و سپس یک path جدید را به آن انتساب می‌دهیم.

مرحله‌ی بعد، فعالسازی این مسیریابی است، با تعریف لینکی به آن. به همین جهت به فایل src\app\app.component.html مراجعه کرده و منوی برنامه را تکمیل می‌کنیم:
<nav class="navbar navbar-default">
  <div class="container-fluid">
    <a class="navbar-brand">{{title}}</a>
    <ul class="nav navbar-nav">
      <li>
        <a [routerLink]="['/home']">Home</a>
      </li>
      <li>
        <a [routerLink]="['/products']">Product List</a>
      </li>
    </ul>
    <ul class="nav navbar-nav navbar-right">
      <li>
        <a [routerLink]="['/login']">Log In</a>
      </li>
    </ul>
  </div>
</nav>
<div class="container">
  <router-outlet></router-outlet>
</div>
اکنون دستور ng serve -o را صادر کنید تا برنامه در حافظه ساخته شده و در مرورگر نمایش داده شود و سپس بر روی لینک login کلیک کنید تا قالب ابتدایی آن نمایش داده شود:



تکمیل کامپوننت login و افزودن لینک logout

در ادامه می‌خواهیم یک فرم لاگین مقدماتی را پس از کلیک بر روی لینک لاگین نمایش دهیم و هدایت به صفحه‌ی لیست محصولات را پس از لاگین و مخفی کردن لینک لاگین و نمایش لینک خروج را در این حالت پیاده سازی کنیم. برای این منظور ابتدا اینترفیس خالی کاربر را ایجاد می‌کنیم:
 >ng g i user/user
که سبب ایجاد فایل src\app\user\user.ts خواهد شد. این اینترفیس را به صورت زیر تکمیل می‌کنیم:
export interface IUser {
    id: number;
    userName: string;
    isAdmin: boolean;
}

پس از آن یک سرویس ابتدایی اعتبارسنجی کاربران را نیز اضافه خواهیم کرد:
 >ng g s user/auth -m user/user.module
که سبب افزوده شدن سرویس auth.service.ts و همچنین به روز رسانی خودکار قسمت providers ماژول user.module.ts نیز می‌شود:
 installing service
  create src\app\user\auth.service.spec.ts
  create src\app\user\auth.service.ts
  update src\app\user\user.module.ts
اگر نام ماژول را ذکر نکنیم، سرویس مدنظر تولید خواهد شد، اما قسمت providers هیچ ماژولی به صورت خودکار تکمیل نمی‌شود.

پس از ایجاد قالب ابتدایی فایل auth.service.ts آن‌را به نحو ذیل تکمیل کنید:
import { IUser } from './user';
import { Injectable } from '@angular/core';

@Injectable()
export class AuthService {
  currentUser: IUser;

  constructor() { }

  isLoggedIn(): boolean {
    return !this.currentUser;
  }

  login(userName: string, password: string): boolean {
    if (!userName || !password) {
      return false;
    }

    if (userName === 'admin') {
      this.currentUser = {
        id: 1,
        userName: userName,
        isAdmin: true
      };
      return true;
    }

    this.currentUser = {
      id: 2,
      userName: userName,
      isAdmin: false
    };
    return true;
  }

  logout(): void {
    this.currentUser = null;
  }
}
در اینجا اگر کاربر هر نوع کلمه‌ی عبور و یا نام کاربری را وارد کند، به سیستم وارد خواهد شد. اگر نامش admin باشد، دسترسی admin پیدا می‌کند و همچنین متدهای logout با null کردن یوزر وارد شده‌ی به سیستم و isLoggedIn برای مشخص بودن نال نبودن شیء کاربر جاری، به این سرویس اضافه شده‌اند.

سپس کامپوننت لاگین واقع در فایل src\app\user\login\login.component.ts را به نحو ذیل تکمیل کنید:
import { Router } from '@angular/router';
import { AuthService } from './../auth.service';

import { Component, OnInit } from '@angular/core';
import { NgForm } from '@angular/forms';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {

  errorMessage: string;
  pageTitle = 'Log In';

  constructor(private authService: AuthService,
    private router: Router) { }

  ngOnInit() {
  }

  login(loginForm: NgForm) {
    if (loginForm && loginForm.valid) {
      let userName = loginForm.form.value.userName;
      let password = loginForm.form.value.password;
      if (this.authService.login(userName, password)) {
        this.router.navigate(['/products']);
      }
    } else {
      this.errorMessage = 'Please enter a user name and password.';
    };
  }
}
در این کامپوننت دو سرویس AuthService، که پیشتر ایجاد کردیم و سرویس Router، به سازنده‌ی کلاس تزریق شده‌اند.
از AuthService برای اعتبارسنجی کاربر و لاگین او به سیستم استفاده می‌کنیم و از سرویس مسیریاب Angular جهت فراخوانی متد navigate آن به صفحه‌ی مشاهده‌ی محصولات، پس از لاگین کاربر استفاده شده‌است.

اکنون می‌خواهیم قالب این کامپوننت را نیز تکمیل کنیم. پیش از آن به فایل src\app\user\user.module.ts مراجعه کرده و در قسمت imports آن FormsModule را نیز اضافه کنید:
import { FormsModule } from '@angular/forms';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    UserRoutingModule
  ],

سپس فایل src\app\user\login\login.component.html را به نحو ذیل تغییر دهید:
<div class="panel panel-default">
  <div class="panel-heading">
    {{pageTitle}}
  </div>

  <div class="panel-body">
    <form class="form-horizontal" novalidate (ngSubmit)="login(loginForm)" #loginForm="ngForm" autocomplete="off">
      <fieldset>
        <div class="form-group" [ngClass]="{'has-error': (userNameVar.touched || 
                                               userNameVar.dirty) && 
                                               !userNameVar.valid }">
          <label class="col-md-2 control-label" for="userNameId">User Name</label>
          <div class="col-md-8">
            <input class="form-control" id="userNameId" type="text" 
                   placeholder="User Name (required)" required 
                   (ngModel)="userName" name="userName" #userNameVar="ngModel" />
            <span class="help-block" *ngIf="(userNameVar.touched ||
                                             userNameVar.dirty) &&
                                             userNameVar.errors">
                            <span *ngIf="userNameVar.errors.required">
                                User name is required.
                            </span>
            </span>
          </div>
        </div>

        <div class="form-group" [ngClass]="{'has-error': (passwordVar.touched || 
                                               passwordVar.dirty) && 
                                               !passwordVar.valid }">
          <label class="col-md-2 control-label" for="passwordId">Password</label>

          <div class="col-md-8">
            <input class="form-control" id="passwordId" type="password" 
                   placeholder="Password (required)" required 
                  (ngModel)="password" name="password" #passwordVar="ngModel" />
            <span class="help-block" *ngIf="(passwordVar.touched ||
                                             passwordVar.dirty) &&
                                              passwordVar.errors">
                            <span *ngIf="passwordVar.errors.required">
                                Password is required.
                            </span>
            </span>
          </div>
        </div>

        <div class="form-group">
          <div class="col-md-4 col-md-offset-2">
            <span>
               <button class="btn btn-primary"
                       type="submit"
                       style="width:80px;margin-right:10px"
                       [disabled]="!loginForm.valid">
                               Log In
               </button>
            </span>
            <span>
                <a class="btn btn-default"
                   [routerLink]="['/welcome']">
                     Cancel
                </a>
            </span>
          </div>
        </div>
      </fieldset>
    </form>
    <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div>
  </div>
</div>
تا اینجا صفحه‌ی لاگین نمایش داده شده و کاربر می‌تواند به سیستم وارد شود. تا زمانیکه کلمه‌ی عبور و نام کاربری وارد نشده باشند، دکمه‌ی login این فرم غیرفعال باقی می‌ماند. پس از آن کاربر با هر نوع ترکیبی از کلمه‌ی عبور و نام کاربری می‌تواند به سیستم وارد شده و بلافاصله به صفحه‌ی لیست محصولات هدایت می‌شود.

اکنون می‌خواهیم پس از ورود او، نام او را نمایش داده و همچنین دکمه‌ی logout را بجای login در منوی بالای سایت نمایش دهیم. به همین جهت در قالب کامپوننت App که منوی برنامه در آن تنظیم شده‌است، نیاز است بتوانیم به سرویس Auth سفارشی دسترسی یافته و خروجی متد isLoggedIn آن‌را بررسی کنیم. به همین منظور به فایل src\app\app.component.ts مراجعه کرده و آن‌را به صورت ذیل تکمیل کنید:
import { Router } from '@angular/router';
import { AuthService } from './user/auth.service';
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  pageTitle: string = 'Routing Lab';

  constructor(private authService: AuthService,
    private router: Router) { }

  logOut(): void {
    this.authService.logout();
    this.router.navigateByUrl('/welcome');
  }
}
در اینجا دو سرویس Auth و Router به سازنده‌ی کامپوننت App تزریق شده‌اند. به این ترتیب می‌توان از شیء authService در قالب این کامپوننت برای دسترسی به متد isLoggedIn و سایر خواص این سرویس استفاده کرد. همچنین از سرویس مسیریاب Angular برای پیاده سازی logout و هدایت کاربر به صفحه‌ی welcome کمک گرفته‌ایم. در اینجا از متد navigateByUrl استفاده شده‌است؛ از این جهت که پس از Logout دیگر حفظ URL Segments موجود بی‌مفهوم است و تمام آن‌ها باید پاک شده و با آدرس جدید جایگزین شوند.
پس از این تغییرات، اکنون می‌توان قالب src\app\app.component.html را به نحو ذیل تکمیل کرد:
<nav class="navbar navbar-default">
  <div class="container-fluid">
    <a class="navbar-brand">{{title}}</a>
    <ul class="nav navbar-nav">
      <li>
        <a [routerLink]="['/home']">Home</a>
      </li>
      <li>
        <a [routerLink]="['/products']">Product List</a>
      </li>
    </ul>
    <ul class="nav navbar-nav navbar-right">
      <li *ngIf="authService.isLoggedIn()">
        <a>Welcome {{ authService.currentUser.userName }}</a>
      </li>
      <li *ngIf="!authService.isLoggedIn()">
        <a [routerLink]="['/login']">Log In</a>
      </li>
      <li *ngIf="authService.isLoggedIn()">
        <a (click)="logOut()">Log Out</a>
      </li>
    </ul>
  </div>
</nav>
<div class="container">
  <router-outlet></router-outlet>
</div>
همانطور که مشاهده می‌کنید، دوبار لاگین بودن کاربر جاری توسط متد authService.isLoggedIn بررسی شده‌است. اگر خروجی این متد true باشد، نام کاربری شخص به همراه لینک Logout نمایش داده می‌شود. اگر خروجی این متد false باشد، تنها لینک login نمایان شده و مابقی گزینه‌ها (لینک لاگین و نمایش نام شخص) از صفحه حذف می‌شوند.

اکنون اگر برنامه را توسط دستور ng serve -o اجرا کنید، صفحه‌ی لاگین و منوی بالای صفحه چنین شکلی را خواهد داشت:


پس از لاگین، لینک لاگین از منو حذف شده و سپس نام کاربری و لینک به logout نمایان می‌شوند.


اینبار اگر بر روی logout کلیک کنید، نام کاربری و لینک logout از صفحه حذف و مجددا لینک لاگین نمایش داده می‌شود.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-routing-lab-01.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
مطالب
ASP.NET MVC #9

مروری بر HTML Helpers استاندارد مهیا در ASP.NET MVC

یکی از اهداف وجودی Server controls در ASP.NET Web forms، رندر خودکار HTML است. برای مثال Menu control، TreeView control، GridView و امثال آن کار تولید تگ‌های table، tr و بسیاری موارد دیگر را در پشت صحنه برای ما انجام می‌دهند. اما در ASP.NET MVC، هدف رسیدن به یک markup ساده و تمیز است که 100 درصد بر روی اجزای آن کنترل داشته باشیم و این مورد به صورت ضمنی به این معنا است که در اینجا تمام این HTMLها را باید خودمان تولید کنیم. البته در عمل خیر. یک نمونه از آن‌را در قسمت قبل مشاهده کردیم که چطور می‌توان منطق تولید تگ‌های HTML را کپسوله سازی کرد و بارها مورد استفاده قرار داد. به علاوه فریم ورک ASP.NET MVC نیز به همراه تعدادی HTML helper توکار ارائه شده است مانند CheckBox، ActionLink، RenderPartial و غیره که کار تولید تگ‌های HTML ضروری و پایه را برای ما ساده می‌کنند.
یک مثال:
@Html.ActionLink("About us", "Index", "About")

در اینجا از متدی به نام ActionLink استفاده شده است. شیء Html هم وهله‌ای از کلاس HtmlHelper است که در تمام Viewها قابل دسترسی می‌باشد.
در این متد،‌ اولین پارامتر، متن نمایش داده شده به کاربر را مشخص می‌کند، پارامتر سوم، نام کنترلری است که مورد استفاده قرار می‌گیرد و پارامتر دوم، نام متد یا اکشنی در آن است که فراخوانی خواهد شد (البته هر کدام از این HtmlHelperها به همراه تعداد قابل توجهی overload هم هستند).
زمانیکه این صفحه را رندر کنیم، به خروجی زیر خواهیم رسید:
<a href="/About">About us</a>

در این لینک نهایی خبری از متد Index ایی که معرفی کردیم، نیست. چرا؟
متد ActionLink بر اساس تعاریف پیش فرض مسیریابی برنامه، سعی می‌کند بهترین خروجی را ارائه دهد. مطابق تعاریف پیش فرض برنامه، متد Index، اکشن پیش فرض کنترلرهای برنامه است. بنابراین ضرورتی به ذکر آن ندیده است.

مثالی دیگر:
همان کلاس‌های Product و Products قسمت هفتم را در نظر بگیرید (قسمت بررسی «ساختار پروژه مثال جاری» در آن مثال). همچنین به اطلاعات «نوشتن HTML Helpers ویژه، به کمک امکانات Razor» قسمت هشتم هم نیاز داریم.
اینبار می‌خواهیم بجای نمایش لیست ساده‌ای از محصولات،‌ ابتدا نام آن‌ها را به صورت لینک‌هایی در صفحه نمایش دهیم. در ادامه پس از کلیک کاربر روی یک نام، توضیحات بیشتری از محصول انتخابی را در صفحه‌ای دیگر ارائه نمائیم. کدهای View ما اینبار به شکل زیر تغییر می‌کنند:

@using MvcApplication5.Models
@model MvcApplication5.Models.Products
@{
ViewBag.Title = "Index";
}
@helper GetProductsList(List<Product> products)
{
<ul>
@foreach (var item in products)
{
<li>@Html.ActionLink(item.Name, "Details", new { id = item.ProductNumber })</li>
}
</ul>
}
<h2>Index</h2>

@GetProductsList(@Model)

توضیحات:
ابتدا یک helper method را تعریف کرده‌ایم و به کمک Html.ActionLink، از نام و شماره محصول، جهت تولید لینک‌های نمایش جزئیات هر یک از محصولات کمک گرفته‌ایم. بنابراین در کنترلر خود نیاز به متد جدیدی به نام Details خواهیم داشت که پارامتری از نوع ProductNumber را دریافت می‌کند. سپس جزئیات این محصول را یافته و در View متناظر با خودش ارائه خواهد داد. پارامتر سومی که در متد ActionLink بکارگرفته شده در اینجا مشاهده می‌کنید، یک anonymously typed object است و توسط آن خواصی را تعریف خواهیم کرد که توسط تعاریف مسیریابی تعریف شده در فایل Global.asax.cs،‌ قابل تفسیر و تبدیل به لینک‌های مرتبط و صحیحی باشد.
اکنون اگر این مثال را اجرا کنیم، اولین لینک تولیدی آن به این شکل خواهد بود:
http://localhost/Home/Details/D123

در اینجا به یک نکته مهم هم باید دقت داشت؛ نام کنترلر به صورت خودکار به این لینک اضافه شده است. بنابراین بهتر است از ایجاد دستی این نوع لینک‌ها خودداری کرده و کار را به متدهای استاندارد فریم ورک واگذار نمود تا بهترین خروجی را دریافت کنیم.
البته اگر الان بر روی این لینک کلیک نمائیم، با پیغام 404 مواجه خواهیم شد. برای تکمیل این مثال، متد Details را به کنترلر تعریف شده اضافه خواهیم کرد:

using System.Linq;
using System.Web.Mvc;
using MvcApplication5.Models;

namespace MvcApplication5.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
var products = new Products();
return View(products);
}

public ActionResult Details(string id)
{
var product = new Products().FirstOrDefault(x => x.ProductNumber == id);
if (product == null)
return View("Error");
return View(product);
}
}
}

در متد Details، ابتدا ProductNumber دریافت شده و سپس شیء محصول متناظر با آن، به View این متد، بازگشت داده می‌شود. اگر بر اساس ورودی دریافتی، محصولی یافت نشد، کاربر را به View ایی به نام Error که در پوشه Views/Shared قرار گرفته است، هدایت می‌کنیم.
برای اضافه کردن این View هم بر روی متد کلیک راست کرده و گزینه Add view را انتخاب کنید. چون یک شیء strongly typed از نوع Product را قرار است به View ارسال کنیم (مانند مثال قسمت پنجم)، می‌توان در صفحه باز شده تیک Create a strongly typed view را گذاشت و سپس Model class را از نوع Product انتخاب کرد و در قسمت Scaffold template هم Details را انتخاب نمود. به این ترتیب Code generator توکار VS.NET قسمتی از کار تولید View را برای ما انجام داده و بدیهی است اکنون سفارشی سازی این View تولیدی که قسمت عمده‌ای از آن تولید شده است، کار ساده‌ای می‌باشد:

@model MvcApplication5.Models.Product

@{
ViewBag.Title = "Details";
}

<h2>Details</h2>

<fieldset>
<legend>Product</legend>

<div class="display-label">ProductNumber</div>
<div class="display-field">@Model.ProductNumber</div>

<div class="display-label">Name</div>
<div class="display-field">@Model.Name</div>

<div class="display-label">Price</div>
<div class="display-field">@String.Format("{0:F}", Model.Price)</div>
</fieldset>
<p>
@Html.ActionLink("Edit", "Edit", new { /* id=Model.PrimaryKey */ }) |
@Html.ActionLink("Back to List", "Index")
</p>

در اینجا کدهای مرتبط با View نمایش جزئیات محصول را مشاهده می‌کنید که توسط VS.NET به صورت خودکار از روی مدل انتخابی تولید شده است.
اکنون یکبار دیگر برنامه را اجرا کرده و بر روی لینک نمایش جزئیات محصولات کلیک نمائید تا بتوان این اطلاعات را در صفحه‌ی بعدی مشاهده نمود.


یک نکته:
اگر سعی کنیم متد @helper GetProductsList فوق را در پوشه App_Code، همانند قسمت قبل قرار دهیم، به متد Html.ActionLink دسترسی نخواهیم داشت. چرا؟
پیغام خطایی که ارائه می‌شود این است:
'System.Web.WebPages.Html.HtmlHelper' does not contain a definition for 'ActionLink' 

به این معنا که در وهله‌ای از شیء System.Web.WebPages.Html.HtmlHelper، به دنبال متد ActionLink می‌گردد. در حالیکه ActionLink مورد نظر به کلاس System.Web.Mvc.HtmlHelper مرتبط می‌شود.
یک راه حل آن به صورت زیر است. به هر متد helper یک آرگومان WebViewPage page را اضافه می‌کنیم (به همراه دو فضای نامی که به ابتدای فایل اضافه می‌شوند)

@using System.Web.Mvc
@using System.Web.Mvc.Html

@using MvcApplication5.Models

@helper GetProductsList(WebViewPage page, List<Product> products)
{
<ul>
@foreach (var item in products)
{
<li> @page.Html.ActionLink(item.Name, "Details", new { id = item.ProductNumber })</li>
}
</ul>
}
سپس برای استفاده از آن در یک View خواهیم داشت:
@MyHelpers.GetProductsList(this, @Model)


متد ActionLink و عبارات فارسی

متد ActionLink آدرس‌های وبی را که تولید می‌کند، URL encoded هستند. برای نمونه اگر رشته‌ای که قرار است به عنوان پارامتر به اکشن متد ما ارسال شود، مساوی Hello World است، آن‌را به صورت Hello%20World در صفحه درج می‌کند. البته این مورد مشکلی را در سمت متدهای کنترلرها ایجاد نمی‌کند، چون کار URL decoding خودکار است. اما ... اگر مقداری که قرار است ارسال شود مثلا «مقدار یک» باشد، آدرس تولیدی این شکل را خواهد داشت:

http://localhost/Home/Details/%D9%85%D9%82%D8%AF%D8%A7%D8%B1%20%D9%8A%D9%83

و اگر این URL encoding انجام نشود، فقط اولین قسمت قبل از فاصله به متد ارسال می‌گردد.
مرورگرهایی مثل فایرفاکس و کروم، مشکلی با نمایش این لینک به شکل اصلی فارسی آن ندارند (حین نمایش، URL decoding را اعمال می‌کنند). اما اگر مرورگر مثلا IE8 باشد، کاربر دقیقا به همین شکل آدرس‌ها را در نوار آدرس مرورگر خود مشاهده خواهد کرد که آنچنان زیبا نیستند.
حل این مشکل، یک نکته کوچک را به همراه دارد. اگر href تولیدی به شکل زیر باشد:

<li><a href="/Home/Details/مقدار یک">Super Fast Bike</a></li>

IE حین نمایش نهایی آن، آن‌را فارسی نشان خواهد داد. حتی زمانیکه کاربر بر روی آن کلیک کند، به صورت خودکار کاراکترهایی را که لازم است encode نماید، به نحو صحیحی در URL نهایی قابل مشاهده در نوار آدرس‌ها ظاهر خواهد کرد. برای مثال %20 را به صورت خودکار اضافه می‌کند و نگرانی از این لحاظ وجود نخواهد داشت که الان بین دو کلمه فاصله‌ای وجود دارد یا خیر (مرورگرهای دیگر هم دقیقا همین رفتار را در مورد لینک‌های داخل صفحه دارند).
خلاصه این توضیحات متد کمکی زیر است:

@helper EmitCleanUnicodeUrl(MvcHtmlString data)
{
@Html.Raw(HttpUtility.UrlDecode(data.ToString()))
}

و برای نمونه نحوه استفاده از آن به شکل زیر خواهد بود:

@helper GetProductsList(List<Product> products)
{
<ul>
@foreach (var item in products)
{
<li>@EmitCleanUnicodeUrl(@Html.ActionLink(item.Name, "Details", new { id = item.ProductNumber }))</li>
}
</ul>
}

ضمن اینکه باید درنظر داشت کلا این نوع طراحی مشکل دارد! برای مثال فرض کنید که در این مثال، جزئیات، نمایش دهنده مطلب ارسالی در یک بلاگ است. یعنی یک سری عنوان و جزئیات متناظر با آن‌ها در دیتابیس وجود دارند. اگر آدرس مطالب به این شکل باشد http://site/blog/details/text، به این معنا است که این text مساوی است با primary key جدول بانک اطلاعاتی. یعنی وبلاگ نویس سایت شما فقط یکبار در طول عمر این برنامه می‌تواند بگوید «سال نو مبارک!». دفعه‌ی بعد به علت تکراری بودن، مجاز به ارسال پیام تبریک دیگری نخواهد بود! به همین جهت بهتر است طراحی را به این شکل تغییر دهید http://site/blog/details/id/text. در اینجا id همان primary key خواهد بود. Text هم عنوان مطلب. Id به جهت خوشایند بانک اطلاعاتی و Text هم برای خوشایند موتورهای جستجو در این URL قرار دارند. مطابق تعاریف مسیریابی برنامه، Text فقط حالت تزئینی داشته و پردازش نخواهد شد.
از این نوع ترفندها زیاد به کار برده می‌شوند. برای نمونه به URL مطالب انجمن‌های معروف اینترنتی دقت کنید. عموما یک عدد را به همراه text مشاهده می‌کنید. عدد در برنامه پردازش می‌شود، متن هم برای موتورهای جستجو درنظر گرفته شده است.



مطالب
استفاده از EF در اپلیکیشن های N-Tier : قسمت چهارم
در قسمت قبل تشخیص تغییرات توسط Web API را بررسی کردیم. در این قسمت نگاهی به پیاده سازی Change-tracking در سمت کلاینت خواهیم داشت.


ردیابی تغییرات در سمت کلاینت توسط Web API

فرض کنید می‌خواهیم از سرویس‌های REST-based برای انجام عملیات CRUD روی یک Object graph استفاده کنیم. همچنین می‌خواهیم رویکردی در سمت کلاینت برای بروز رسانی کلاس موجودیت‌ها پیاده سازی کنیم که قابل استفاده مجدد (reusable) باشد. علاوه بر این دسترسی داده‌ها توسط مدل Code-First انجام می‌شود.

در مثال جاری یک اپلیکیشن کلاینت (برنامه کنسول) خواهیم داشت که سرویس‌های ارائه شده توسط پروژه Web API را فراخوانی می‌کند. هر پروژه در یک Solution مجزا قرار دارد، با این کار یک محیط n-Tier را شبیه سازی می‌کنیم.

مدل زیر را در نظر بگیرید.

همانطور که می‌بینید مدل مثال جاری مشتریان و شماره تماس آنها را ارائه می‌کند. می‌خواهیم مدل‌ها و کد دسترسی به داده‌ها را در یک سرویس Web API پیاده سازی کنیم تا هر کلاینتی که به HTTP دسترسی دارد بتواند از آن استفاده کند. برای ساخت سرویس مذکور مراحل زیر را دنبال کنید.

  • در ویژوال استودیو پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب پروژه را Web API انتخاب کنید. نام پروژه را به Recipe4.Service تغییر دهید.
  • کنترلر جدیدی با نام CustomerController به پروژه اضافه کنید.
  • کلاسی با نام BaseEntity ایجاد کنید و کد آن را مطابق لیست زیر تغییر دهید. تمام موجودیت‌ها از این کلاس پایه مشتق خواهند شد که خاصیتی بنام TrackingState را به آنها اضافه می‌کند. کلاینت‌ها هنگام ویرایش آبجکت موجودیت‌ها باید این فیلد را مقدار دهی کنند. همانطور که می‌بینید این خاصیت از نوع TrackingState enum مشتق می‌شود. توجه داشته باشید که این خاصیت در دیتابیس ذخیره نخواهد شد. با پیاده سازی enum وضعیت ردیابی موجودیت‌ها بدین روش، وابستگی‌های EF را برای کلاینت از بین می‌بریم. اگر قرار بود وضعیت ردیابی را مستقیما از EF به کلاینت پاس دهیم وابستگی‌های بخصوصی معرفی می‌شدند. کلاس DbContext اپلیکیشن در متد OnModelCreating به EF دستور می‌دهد که خاصیت TrackingState را به جدول موجودیت نگاشت نکند.
public abstract class BaseEntity
{
    protected BaseEntity()
    {
        TrackingState = TrackingState.Nochange;
    }

    public TrackingState TrackingState { get; set; }
}

public enum TrackingState
{
    Nochange,
    Add,
    Update,
    Remove,
}
  • کلاس‌های موجودیت Customer و PhoneNumber را ایجاد کنید و کد آنها را مطابق لیست زیر تغییر دهید.
public class Customer : BaseEntity
{
    public int CustomerId { get; set; }
    public string Name { get; set; }
    public string Company { get; set; }
    public virtual ICollection<Phone> Phones { get; set; }
}

public class Phone : BaseEntity
{
    public int PhoneId { get; set; }
    public string Number { get; set; }
    public string PhoneType { get; set; }
    public int CustomerId { get; set; }
    public virtual Customer Customer { get; set; }
}
  • با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
  • کلاسی با نام Recipe4Context ایجاد کنید و کد آن را مطابق لیست زیر تغییر دهید. در این کلاس از یکی از قابلیت‌های جدید EF 6 بنام "Configuring Unmapped Base Types" استفاده کرده ایم. با استفاده از این قابلیت جدید هر موجودیت را طوری پیکربندی می‌کنیم که خاصیت TrackingState را نادیده بگیرند. برای اطلاعات بیشتر درباره این قابلیت EF 6 به این لینک مراجعه کنید.
public class Recipe4Context : DbContext
{
    public Recipe4Context() : base("Recipe4ConnectionString") { }
    public DbSet<Customer> Customers { get; set; }
    public DbSet<Phone> Phones { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        // Do not persist TrackingState property to data store
        // This property is used internally to track state of
        // disconnected entities across service boundaries.
        // Leverage the Custom Code First Conventions features from Entity Framework 6.
        // Define a convention that performs a configuration for every entity
        // that derives from a base entity class.
        modelBuilder.Types<BaseEntity>().Configure(x => x.Ignore(y => y.TrackingState));
        modelBuilder.Entity<Customer>().ToTable("Customers");
        modelBuilder.Entity<Phone>().ToTable("Phones");
}
}
  • فایل Web.config پروژه را باز کنید و رشته اتصال زیر را به قسمت ConnectionStrings اضافه نمایید.
<connectionStrings>
  <add name="Recipe4ConnectionString"
    connectionString="Data Source=.;
    Initial Catalog=EFRecipes;
    Integrated Security=True;
    MultipleActiveResultSets=True"
    providerName="System.Data.SqlClient" />
</connectionStrings>
  • فایل Global.asax را باز کنید و کد زیر را به متد Application_Start اضافه نمایید. این کد بررسی Entity Framework Model Compatibility را غیرفعال می‌کند و به JSON serializer دستور می‌دهد که self-referencing loop خواص پیمایشی را نادیده بگیرد. این حلقه بدلیل رابطه bidirectional بین موجودیت‌های Customer و PhoneNumber بوجود می‌آید.
protected void Application_Start()
{
    // Disable Entity Framework Model Compatibilty
    Database.SetInitializer<Recipe1Context>(null);
    // The bidirectional navigation properties between related entities
    // create a self-referencing loop that breaks Web API's effort to
    // serialize the objects as JSON. By default, Json.NET is configured
    // to error when a reference loop is detected. To resolve problem,
    // simply configure JSON serializer to ignore self-referencing loops.
    GlobalConfiguration.Configuration.Formatters.JsonFormatter
        .SerializerSettings.ReferenceLoopHandling =
            Newtonsoft.Json.ReferenceLoopHandling.Ignore;
    ...
}
  • کلاسی با نام EntityStateFactory بسازید و کد آن را مطابق لیست زیر تغییر دهید. این کلاس مقدار خاصیت TrackingState که به کلاینت‌ها ارائه می‌شود را به مقادیر متناظر کامپوننت‌های ردیابی EF تبدیل می‌کند.
public static EntityState Set(TrackingState trackingState)
{
    switch (trackingState)
    {
        case TrackingState.Add:
            return EntityState.Added;
        case TrackingState.Update:
            return EntityState.Modified;
        case TrackingState.Remove:
            return EntityState.Deleted;
        default:
            return EntityState.Unchanged;
    }
}
  • در آخر کد کنترلر CustomerController را مطابق لیست زیر بروز رسانی کنید.
public class CustomerController : ApiController
{
    // GET api/customer
    public IEnumerable<Customer> Get()
    {
        using (var context = new Recipe4Context())
        {
            return context.Customers.Include(x => x.Phones).ToList();
        }
    }

    // GET api/customer/5
    public Customer Get(int id)
    {
        using (var context = new Recipe4Context())
        {
            return context.Customers.Include(x => x.Phones).FirstOrDefault(x => x.CustomerId == id);
        }
    }

    [ActionName("Update")]
    public HttpResponseMessage UpdateCustomer(Customer customer)
    {
        using (var context = new Recipe4Context())
        {
            // Add object graph to context setting default state of 'Added'.
            // Adding parent to context automatically attaches entire graph
            // (parent and child entities) to context and sets state to 'Added'
            // for all entities.
            context.Customers.Add(customer);
            foreach (var entry in context.ChangeTracker.Entries<BaseEntity>())
            {
                entry.State = EntityStateFactory.Set(entry.Entity.TrackingState);
                if (entry.State == EntityState.Modified)
                {
                    // For entity updates, we fetch a current copy of the entity
                    // from the database and assign the values to the orginal values
                    // property from the Entry object. OriginalValues wrap a dictionary
                    // that represents the values of the entity before applying changes.
                    // The Entity Framework change tracker will detect
                    // differences between the current and original values and mark
                    // each property and the entity as modified. Start by setting
                    // the state for the entity as 'Unchanged'.
                    entry.State = EntityState.Unchanged;
                    var databaseValues = entry.GetDatabaseValues();
                    entry.OriginalValues.SetValues(databaseValues);
                }
            }

        context.SaveChanges();
    }

    return Request.CreateResponse(HttpStatusCode.OK, customer);
}

    [HttpDelete]
    [ActionName("Cleanup")]
    public HttpResponseMessage Cleanup()
    {
        using (var context = new Recipe4Context())
        {
            context.Database.ExecuteSqlCommand("delete from phones");
            context.Database.ExecuteSqlCommand("delete from customers");
            return Request.CreateResponse(HttpStatusCode.OK);
        }
    }
}
حال اپلیکیشن کلاینت (برنامه کنسول) را می‌سازیم که از این سرویس استفاده می‌کند.

  • در ویژوال استودیو پروژه جدیدی از نوع Console Application بسازید و نام آن را به Recipe4.Client تغییر دهید.
  • فایل program.cs را باز کنید و کد آن را مطابق لیست زیر تغییر دهید.
internal class Program
{
    private HttpClient _client;
    private Customer _bush, _obama;
    private Phone _whiteHousePhone, _bushMobilePhone, _obamaMobilePhone;
    private HttpResponseMessage _response;

    private static void Main()
    {
        Task t = Run();
        t.Wait();
        Console.WriteLine("\nPress <enter> to continue...");
        Console.ReadLine();
    }

    private static async Task Run()
    {
        var program = new Program();
        program.ServiceSetup();
        // do not proceed until clean-up completes
        await program.CleanupAsync();
        program.CreateFirstCustomer();
        // do not proceed until customer is added
        await program.AddCustomerAsync();
        program.CreateSecondCustomer();
        // do not proceed until customer is added
        await program.AddSecondCustomerAsync();
        // do not proceed until customer is removed
        await program.RemoveFirstCustomerAsync();
        // do not proceed until customers are fetched
        await program.FetchCustomersAsync();
    }

    private void ServiceSetup()
    {
        // set up infrastructure for Web API call
        _client = new HttpClient { BaseAddress = new Uri("http://localhost:62799/") };
        // add Accept Header to request Web API content negotiation to return resource in JSON format
        _client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue
        ("application/json"));
    }
    private async Task CleanupAsync()
    {
        // call the cleanup method from the service
        _response = await _client.DeleteAsync("api/customer/cleanup/");
    }

    private void CreateFirstCustomer()
    {
        // create customer #1 and two phone numbers
        _bush = new Customer
        {
            Name = "George Bush",
            Company = "Ex President",
            // set tracking state to 'Add' to generate a SQL Insert statement
            TrackingState = TrackingState.Add,
        };
        _whiteHousePhone = new Phone
        {
            Number = "212 222-2222",
            PhoneType = "White House Red Phone",
            // set tracking state to 'Add' to generate a SQL Insert statement
            TrackingState = TrackingState.Add,
        };
        _bushMobilePhone = new Phone
        {
            Number = "212 333-3333",
            PhoneType = "Bush Mobile Phone",
            // set tracking state to 'Add' to generate a SQL Insert statement
            TrackingState = TrackingState.Add,
        };
        _bush.Phones.Add(_whiteHousePhone);
        _bush.Phones.Add(_bushMobilePhone);
    }

    private async Task AddCustomerAsync()
    {
        // construct call to invoke UpdateCustomer action method in Web API service
        _response = await _client.PostAsync("api/customer/updatecustomer/", _bush, new JsonMediaTypeFormatter());
        if (_response.IsSuccessStatusCode)
        {
            // capture newly created customer entity from service, which will include
            // database-generated Ids for all entities
            _bush = await _response.Content.ReadAsAsync<Customer>();
            _whiteHousePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _bush.CustomerId);
            _bushMobilePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _bush.CustomerId);
            Console.WriteLine("Successfully created Customer {0} and {1} Phone Numbers(s)",
            _bush.Name, _bush.Phones.Count);
            foreach (var phoneType in _bush.Phones)
            {
                Console.WriteLine("Added Phone Type: {0}", phoneType.PhoneType);
            }
        }
        else
            Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase);
    }

    private void CreateSecondCustomer()
    {
        // create customer #2 and phone numbers
        _obama = new Customer
        {
            Name = "Barack Obama",
            Company = "President",
            // set tracking state to 'Add' to generate a SQL Insert statement
            TrackingState = TrackingState.Add,
        };
        _obamaMobilePhone = new Phone
        {
            Number = "212 444-4444",
            PhoneType = "Obama Mobile Phone",
            // set tracking state to 'Add' to generate a SQL Insert statement
            TrackingState = TrackingState.Add,
        };
        // set tracking state to 'Modifed' to generate a SQL Update statement
        _whiteHousePhone.TrackingState = TrackingState.Update;
        _obama.Phones.Add(_obamaMobilePhone);
        _obama.Phones.Add(_whiteHousePhone);
    }

    private async Task AddSecondCustomerAsync()
    {
        // construct call to invoke UpdateCustomer action method in Web API service
        _response = await _client.PostAsync("api/customer/updatecustomer/", _obama, new JsonMediaTypeFormatter());
        if (_response.IsSuccessStatusCode)
        {
            // capture newly created customer entity from service, which will include
            // database-generated Ids for all entities
            _obama = await _response.Content.ReadAsAsync<Customer>();
            _whiteHousePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _obama.CustomerId);
            _bushMobilePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _obama.CustomerId);
            Console.WriteLine("Successfully created Customer {0} and {1} Phone Numbers(s)",
            _obama.Name, _obama.Phones.Count);
            foreach (var phoneType in _obama.Phones)
            {
                Console.WriteLine("Added Phone Type: {0}", phoneType.PhoneType);
            }
        }
        else
            Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase);
    }

    private async Task RemoveFirstCustomerAsync()
    {
        // remove George Bush from underlying data store.
        // first, fetch George Bush entity, demonstrating a call to the
        // get action method on the service while passing a parameter
        var query = "api/customer/" + _bush.CustomerId;
        _response = _client.GetAsync(query).Result;

        if (_response.IsSuccessStatusCode)
        {
            _bush = await _response.Content.ReadAsAsync<Customer>();
            // set tracking state to 'Remove' to generate a SQL Delete statement
            _bush.TrackingState = TrackingState.Remove;
            // must also remove bush's mobile number -- must delete child before removing parent
            foreach (var phoneType in _bush.Phones)
            {
                // set tracking state to 'Remove' to generate a SQL Delete statement
                phoneType.TrackingState = TrackingState.Remove;
            }
            // construct call to remove Bush from underlying database table
            _response = await _client.PostAsync("api/customer/updatecustomer/", _bush, new JsonMediaTypeFormatter());
            if (_response.IsSuccessStatusCode)
            {
                Console.WriteLine("Removed {0} from database", _bush.Name);
                foreach (var phoneType in _bush.Phones)
                {
                    Console.WriteLine("Remove {0} from data store", phoneType.PhoneType);
                }
            }
            else
                Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase);
        }
        else
        {
            Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase);
        }
    }

    private async Task FetchCustomersAsync()
    {
        // finally, return remaining customers from underlying data store
        _response = await _client.GetAsync("api/customer/");
        if (_response.IsSuccessStatusCode)
        {
            var customers = await _response.Content.ReadAsAsync<IEnumerable<Customer>>();
            foreach (var customer in customers)
            {
                Console.WriteLine("Customer {0} has {1} Phone Numbers(s)",
                customer.Name, customer.Phones.Count());
                foreach (var phoneType in customer.Phones)
                {
                    Console.WriteLine("Phone Type: {0}", phoneType.PhoneType);
                }
            }
        }
        else
        {
            Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase);
        }
    }
}

  • در آخر کلاس‌های Customer, Phone و BaseEntity را به پروژه کلاینت اضافه کنید. چنین کدهایی بهتر است در لایه مجزایی قرار گیرند و بین لایه‌های مختلف اپلیکیشن به اشتراک گذاشته شوند.

اگر اپلیکیشن کلاینت را اجرا کنید با خروجی زیر مواجه خواهید شد.








شرح مثال جاری

با اجرای اپلیکیشن Web API شروع کنید. این اپلیکیشن یک MVC Web Controller دارد که پس از اجرا شما را به صفحه خانه هدایت می‌کند. در این مرحله سایت در حال اجرا است و سرویس‌ها قابل دسترسی هستند.

سپس اپلیکیشن کنسول را باز کنید و روی خط اول کد فایل program.cs یک breakpoint قرار داده و آن را اجرا کنید. ابتدا آدرس سرویس را نگاشت می‌کنیم و از سرویس درخواست می‌کنیم که اطلاعات را با فرمت JSON بازگرداند.

سپس توسط متد DeleteAsync که روی آبجکت HttpClient تعریف شده است اکشن متد Cleanup را روی سرویس فراخوانی می‌کنیم. این فراخوانی تمام داده‌های پیشین را حذف می‌کند.

در قدم بعدی یک مشتری بهمراه دو شماره تماس می‌سازیم. توجه کنید که برای هر موجودیت مشخصا خاصیت TrackingState را مقدار دهی می‌کنیم تا کامپوننت‌های Change-tracking در EF عملیات لازم SQL برای هر موجودیت را تولید کنند.

سپس توسط متد PostAsync که روی آبجکت HttpClient تعریف شده اکشن متد UpdateCustomer را روی سرویس فراخوانی می‌کنیم. اگر به این اکشن متد یک breakpoint اضافه کنید خواهید دید که موجودیت مشتری را بعنوان یک پارامتر دریافت می‌کند و آن را به context جاری اضافه می‌نماید. با اضافه کردن موجودیت به کانتکست جاری کل object graph اضافه می‌شود و EF شروع به ردیابی تغییرات آن می‌کند. دقت کنید که آبجکت موجودیت باید Add شود و نه Attach.

قدم بعدی جالب است، هنگامی که از خاصیت DbChangeTracker استفاده می‌کنیم. این خاصیت روی آبجکت context تعریف شده و یک <IEnumerable<DbEntityEntry را با نام Entries ارائه می‌کند. در اینجا بسادگی نوع پایه EntityType را تنظیم میکنیم. این کار به ما اجازه می‌دهد که در تمام موجودیت هایی که از نوع BaseEntity هستند پیمایش کنیم. اگر بیاد داشته باشید این کلاس، کلاس پایه تمام موجودیت‌ها است. در هر مرحله از پیمایش (iteration) با استفاده از کلاس EntityStateFactory مقدار خاصیت TrackingState را به مقدار متناظر در سیستم ردیابی EF تبدیل می‌کنیم. اگر کلاینت مقدار این فیلد را به Modified تنظیم کرده باشد پردازش بیشتری انجام می‌شود. ابتدا وضعیت موجودیت را از Modified به Unchanged تغییر می‌دهیم. سپس مقادیر اصلی را با فراخوانی متد GetDatabaseValues روی آبجکت Entry از دیتابیس دریافت می‌کنیم. فراخوانی این متد مقادیر موجود در دیتابیس را برای موجودیت جاری دریافت می‌کند. سپس مقادیر بدست آمده را به کلکسیون OriginalValues اختصاص می‌دهیم. پشت پرده، کامپوننت‌های EF Change-tracking بصورت خودکار تفاوت‌های مقادیر اصلی و مقادیر ارسالی را تشخیص می‌دهند و فیلدهای مربوطه را با وضعیت Modified علامت گذاری می‌کنند. فراخوانی‌های بعدی متد SaveChanges تنها فیلدهایی که در سمت کلاینت تغییر کرده اند را بروز رسانی خواهد کرد و نه تمام خواص موجودیت را.

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

متد UpdateCustomer در سرویس ما مقادیر TrackingState را به مقادیر متناظر EF تبدیل می‌کند و آبجکت‌ها را به موتور change-tracking ارسال می‌کند که نهایتا منجر به تولید دستورات لازم SQL می‌شود.

نکته: در اپلیکیشن‌های واقعی بهتر است کد دسترسی داده‌ها و مدل‌های دامنه را به لایه مجزایی منتقل کنید. همچنین پیاده سازی فعلی change-tracking در سمت کلاینت می‌تواند توسعه داده شود تا با انواع جنریک کار کند. در این صورت از نوشتن مقادیر زیادی کد تکراری جلوگیری خواهید کرد و از یک پیاده سازی می‌توانید برای تمام موجودیت‌ها استفاده کنید.

پاسخ به بازخورد‌های پروژه‌ها
تنظیم چندین OverallSummarySettings برای ستون های مختلف
خیلی ممنون، خیلی عالیه. فقط موردی هست که بر فرض در اینجا، در یک ستون هم Weight و هم Quantity  را نمایش می‌دهیم، پس انتظار می‌رود که در سطر جمع کل و جمع صفحه هم جمع Weight و هم جمع Quantity را زیر هم نمایش دهد که شما در مثالتان فقط مقدار جمع Weight را نمایش داده اید. 
راه حل خودم این هست که اگر بر فرض در هر ستون چهار فیلد قراره نمایش داده بشود، در کلاس CustomSum هم باید مقادیر جمع هر کدام به صورت مجزا ذخیره شود. کلاسی نیز با چهار مقدار تعریف شود که به هنگام برگرداندن مقادیر GroupValue و OverallValue، نمونه ای از کلاس که چهار مقدار متناظر چهار ستون را در خود دارد بر می‌گردانیم و با نوشتن  DisplayFormatFormula  و cast کردن object به کلاس متناظر چهار مقداره، مقدار جمع هر فیلد را به صورت مجزا و در زیر هم نمایش داد.
فقط مشکلی هست و این است که هنگام نمایش مقادیر جمع فیلد‌ها در زیر هم، DisplayFormatFormula  مقدار 'n\' را تاثیر نمی‌دهد و هر چه بعد از آن آمده باشد را نمایش نمی‌دهد.
ممنون میشم راهنمایی کنید که چگونه این مشکلات را حل کنم.
نظرات مطالب
مشکل اعتبار سنجی jQuery validator در Bootstrap tabs

خوب این همون حالت پیش فرضی هست که توضیح دادن: «هنگام validate کردن فرم متوجه می‌شوید که فقط کنترل‌های تب جاری اعتبارسنجی میشوند و بدون توجه به سایر کنترل هایی که در تب‌های دیگر هستند». این tab جاری منظور همون به ازای هر tab جداگانه هست.

برای نمایش خودکار تب بعدی هم باید کدنویسی کنید. فقط کافی هست که کلاس active رو به div تب مورد نظر اضافه کنید:

$("#home").removeClass("active");  // this deactivates the home tab
$("#profile").addClass("active");  // this activates the profile tab
مطالب
Gulp #3
در قسمت اول گالپ را معرفی کردیم و در مقاله قبلی به نوشتن اولین تسک با گالپ پرداختیم. در این قسمت می‌خواهیم با نصب bower، پروژه‌ی workflow بوت استرپ راستچین شده را انجام دهیم.

نصب bower



bower یک مدیریت پکیج سمت Front end است و از مزیای استفاده از آن می‌توان به موارد زیر اشاره کرد:
  • ساده کردن تعریف وابستگی‌های منابع پروژه با تعریف یک فایل bower.json
  • نیازی به commit کردن واستگی‌های پروژه نیست.
  • با ذکر ورژن مربوط به وابستگی یا محدوده‌ی قابل قبول برای آن، به روز رسانی منابع به سادگی با یک دستور انجام می‌شود.
  • وابستگی های وابسته به یک منبع  را نیز نصب می‌کند. برای مثال زمانیکه بوت استرپ را به عنوان وابستگی پروژه تعریف می‌کنیم، وابستگی آن یعنی jquery را چون در فایل bower.json بوت استرپ تعریف شده‌است، به صورت خودکار دانلود می‌کند.
  • در نهایت افراد هم تیمی یا توسعه دهندگان دیگر به راحتی با زدن دستور bower install تمام وابستگی‌های پروژه را می‌توانند نصب کنند.
برای نصب آن کافی است دستور زیر را بزنید و بعد از نصب نیز دستور خط دوم را در مسیر پروژه وارد کنید تا یک فایل bower.json را برایمان بسازد. برای اینکار به سوال‌هایی که می‌پرسد باید جواب دهیم. تنها نکته‌ای که قابل ذکر است، پاسخ به سوال ? what types of modules does this package expose است که باید گزینه‌ی Node را انتخاب کنید. 
sudo npm install -g bower
bower init
حال می‌خواهیم وابستگی‌های پروژه را نصب کنیم که عبارتند از bootstrap-sass,fontawesome,bootstrap-rtl :
bower install bootstrap-sass-official --save  
bower install fontawesome --save
bower install bootstrap-rtl --save
نکته : عبارت save-- وابستگی مربوطه را به bower.json اضافه می‌کند. اگر نصب با موفقیت صورت گرفته باشد، پکیج‌های مربوطه را می‌توانید در فولدر bower_components در root پروژه مشاهده کنید.

نصب پلاگین‌های مورد نیاز gulp

ما می‌خواهیم که بوت استرپ و نگارش sass آن‌را کامپایل کنیم و همچنین وابستگی‌های bower پروژه را از طریق گالپ نصب کنیم تا نیازی به زدن bower install نباشد و توسعه دهنده‌ی پروژه فقط با زدن npm install، تمام وابستگی‌های پروژه‌ی ما را نصب کند. می‌توان تمام پلاگین‌ها را پشت سر هم با یک دستور نصب کرد و یا به صورت جداگانه این کار را انجام داد.
sudo npm install gulp gulp-ruby-sass gulp-notify gulp-bower --save-dev
نکته۱:پلاگین gulp-notify به منظور نشان دادن خطاها در ترمینال است؛ تا در صورت وجود اشتباه در کامپایل فایل‌های Sass، کل روند گالپ متوقف نشود.
نکته ۲ : برای اینکه کامپایل sass انجام شود نیاز به Ruby دارید. برای ویندوز می‌توانید از روبی اینستالر استفاده کنید.

نوشتن تسک‌ها برای گالپ

به قسمت مهم و هیجان انگیز کار رسیدیم! همان طور در مقاله قبلی گفتیم، ابتدا باید ماژول‌هایی را که نصب کردیم، include کنیم به این صورت:
var gulp = require('gulp'),
    sass = require('gulp-ruby-sass'),
    notify = require('gulp-notify'),
    bower = require('gulp-bower');
برای اینکه دسترسی به مسیر‌های مهم پروژه آسان‌تر شود، آن‌ها را درون یک شیء نگه داری می‌کنیم.
var config = {
    sassPath = './resources/sass',
    bowerDir = './bower_components'
}
تسک باور را اضافه می‌کنیم تا کار bower install را خودکار کنیم. مزیت این کار این است اگر یک هم تیمی، پکیج جدیدی را در حین توسعه‌ی پروژه نصب کرد، بدون اینکه لازم باشد تا در جایی از پروژه، بقیه را از آن مطلع کنید، فقط با زدن gulp خیالتان راحت شود که تمام کارهایی که باید انجام دهید، گالپ برایتان انجام می‌دهد.
// create a task to do bower install
gulp.task('bower', function() {
    return bower()
      .pipe(gulp.dest(config.bowerDir))
});
در گام بعدی، تسک جاوا اسکریپت را اضافه می‌کنیم. یعنی جی کوری و فایل bootstrap.js را به مسیر public/js می‌آوریم. فولدر public برای جدا سازی فایل‌های نهایی از فایل‌های توسعه است و به همین صورت برای فونت آیکن‌های fontawesome.

// Copy js files to public folder
gulp.task('js', function() {
    return gulp.src([config.bowerDir + '/bootstrap-sass-official/assets/javascripts/bootstrap.min.js',
                    config.bowerDir + '/jquery/dist/jquery.min.js'
            ])
        .pipe(gulp.dest('./public/js'));

});

// Copy fontawesome icons to public/fonts folder
gulp.task('icons', function() {
    return gulp.src(config.bowerDir + '/fontawesome/fonts/**.*')
        .pipe(gulp.dest('./public/fonts'));
});

:برای سی اس اس هم تسک مربوطه‌اش را به صورت زیر می‌نویسیم

gulp.task('css', function() {
    return sass(config.sassPath + '/style.scss', { // Our coustom sass
      style: 'compressed',  // minify css
      loadPath: [ // load paths to easy use import in resources/sass
        './resources/sass',
        config.bowerDir + '/bootstrap-sass-official/assets/stylesheets', // bootstrap sass files
        config.bowerDir + '/fontawesome/scss' // awesome icons sass files
      ]
    })
});
 حال تعریف می‌کنیم که اگر خطایی در حین کامپایل رخ داد، آن را به ما نشان دهد و در نهایت فایل کامپایل و فشرده شده را در مسیر خروجی قرار می‌دهیم. کد‌ها را به صورت زیر به روز می‌کنیم
gulp.task('css', function() {
    return sass(config.sassPath + '/style.scss', { // Our coustom sass
      style: 'compressed',  // minify css
      loadPath: [ // load paths to easy use import in resources/sass
        './resources/sass',
        config.bowerDir + '/bootstrap-sass-official/assets/stylesheets', // bootstrap sass files
        config.bowerDir + '/fontawesome/scss' // awesome icons sass files
      ]
    })
        .on('error', notify.onError(function(error) {
            return 'Error: ' + error.message;
        }))
      .pipe(gulp.dest('./public/css'));
});
اینجا ما می‌خواهیم که کار‌ها را خودکار سازی کنیم تا با تغییر و ذخیره‌ی مجدد فایل‌های سس، تسک سی اس اس را انجام دهد. برای این کار کد‌های زیر را اضافه می‌کنیم
// Rerun the task when a file changes
gulp.task('watch', function() {

    gulp.watch(config.sassPath + '/**/*.scss', ['css']); 

});
در نهایت برای سادگی می‌توانیم مجموعه‌ای از وظایف را در یک تسک تعریف کنیم تا به راحتی و با زدن تنها یک دستور در ترمینال، کار‌ها خودکار سازی شوند
// Run this task with :  gulp
// OR gulp default
gulp.task('default', ['bower', 'icons', 'css','js']);

بسیار خوب؛ ما توانستیم پایه‌ی ورک فلو یمان را بسازیم. در مقاله‌ی بعدی از پلاگین‌های دیگری برای بهینه سازی کارهایمان کمک خواهیم گرفت

مخزن گیت هاب 
مطالب
تبدیل یک قالب HTML معمولی به قالب React

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

همانطور که میدانید React پوشه‌ای را به نام public، مهیا کرده‌است که برای استفاده‌ی عمومی از فایل‌های استاتیک ایجاد شده‌است. پس ابتدا فایل‌های js,css، تصاویر و دیگر فایل‌های استاتیک را به پوشه‌ی public  منتقل میکنیم. سپس فایل index قالب را باز کرده و به تگ header فایل مراجعه کنید. تگ‌های لینک‌های معرفی فایل‌های css و script ای را که در آن تعریف شده‌اند، کپی کرده به هدر فایل index.html که در پوشه‌ی public قرار دارد، منتقل کنید. همچنین از فایل‌های اسکرپیت دیگر که در پایین تگ Body قرار گرفته‌اند، غافل نگردید. 

در اینجا باید بخش‌های اساسی قالب، همانند navbar و sidebar را به صورت کامپوننت ایجاد کنیم.

پس ابتدا یک کامپوننت NavBar.jsx را ایجاد کرده  و کدهای همین قسمت را در متد render قرار میدهیم:

 import React, { Component } from "react";  
class NavBar extends Component {
  state = {};
  render() {
    return (
      <React.Fragment>
            <nav>
                <!-- Left navbar links -->
    <ul>
                    <li>
                        <a data-widget="pushmenu" href="#"><i></i></a>
                    </li>
                    <li>
                        <a href="index3.html">خانه</a>
                    </li>
                    <li>
                        <a href="#">تماس</a>
                    </li>
                </ul>

                <!-- SEARCH FORM -->
    <form>
                    <div>
                        <input type="search" placeholder="جستجو" aria-label="Search">
                            <div>
                                <button type="submit">
                                    <i></i>
                                </button>
                            </div>
        </div>
    </form>
                    ...
</nav>
      </React.Fragment>
    );
  }
}

export default NavBar;
در همین جا با خطاهای زیادی روبرو میشویم که  به شما نشان میدهد کدهای کپی شده، هیچ استانداردی از jsx را رعایت نمیکنند به همین جهت شروع به ویرایش کد کپی شده میکنیم: 
  • تمامی کامنت‌های موجود در فایل را حذف کنید.
  • تمام تگ‌ها که شامل خصوصیت class هستند  را با استفاده از ابزار جستجو، یافته و با عبارت className جایگزین کنید.
  • در صورتیکه روی تگ‌ها از خصوصیت style استفاده کرده‌اید، به شکل زیر ویرایش کرده و قالب jsx را روی آن پیاده کنید.
style="opacity:0.8;"
به
style={{ opacity: "0.8" }}

  • در صورتیکه از تگ‌های img و یا input استفاده میکنید، حتما باید انتها تگ‌ها به شکل زیر بسته شده باشند:
<input type="search" placeholder="جستجو" aria-label="Search">
به
 <input
                className="form-control form-control-navbar"
                type="search"
                placeholder="جستجو"
                aria-label="Search"
              />

در نهایت باید کامپوننت بدون هیچ خطایی به شکل زیر ایجاد گردد:
import React, { Component } from "react";

class NavBar extends Component {
  state = {};
  render() {
    return (
      <React.Fragment>
        <nav className="main-header navbar navbar-expand bg-white navbar-light border-bottom">
          <ul className="navbar-nav">
            <li className="nav-item">
              <a className="nav-link" data-widget="pushmenu" href="#">
                <i className="fa fa-bars"></i>
              </a>
            </li>
            <li className="nav-item d-none d-sm-inline-block">
              <a href="index3.html" className="nav-link">
                خانه
              </a>
            </li>
            <li className="nav-item d-none d-sm-inline-block">
              <a href="#" className="nav-link">
                تماس
              </a>
            </li>
          </ul>

          <form className="form-inline ml-3">
            <div className="input-group input-group-sm">
              <input
                className="form-control form-control-navbar"
                type="search"
                placeholder="جستجو"
                aria-label="Search"
              />
              <div className="input-group-append">
                <button className="btn btn-navbar" type="submit">
                  <i className="fa fa-search"></i>
                </button>
              </div>
            </div>
          </form>

     ...
        </nav>
      </React.Fragment>
    );
  }
}

export default NavBar;

کامپوننت‌های دیگر مانند sidebar و footer را به همین شکل ایجاد کرده و در نهایت در فایل App.jsx در متد رندر قرار دهید:
return (
    <React.Fragment>
      <NavBar />
      <SideBar />
      <div className="content-wrapper" style={{ marginTop: "20px" }}>
        <DomainList />
      </div>
      <Footer />
    </React.Fragment>
  )
 در کامپوننت‌های جاری همچون NavBar، نمونه‌هایی از اشیاء دیگر نیز به چشم میخورند که قابلیت تبدیل شدن به کامپوننت‌های مجزایی را دارند؛ همانند پیام‌های اخیر، اعلان‌های سیستم و ... که میتوانید به صورت جزء به جزء، ایجاد کامپوننت‌های آن‌ها را انجام دهید. 
نکته‌ی مهم: در فایل index.html، یک سری تگ را که به فایل‌های css و js ارجاع دارند، در اولین مرحله به این فایل کپی کرده‌اید. تعداد زیادی از این کتابخانه‌ها در مخزن npm موجود بوده و قابلیت import شدن آن‌ها توسط React فراهم است؛ همانند کتابخانه‌های بوت استرپ و یا FontAwesome  که در این مقاله  به همین شکل وارد شده‌اند. بهتر است این موراد اصلاح گردیده و انتقال این فایل‌ها به index.js فراهم گردد. از این رو که به روزرسانی این بسته‌ها از طریق npm ساده‌تر بوده و WebPack نیز مدیریت این بسته‌ها را به عهده می‌گیرد. 
مطالب
Blazor 5x - قسمت دهم - مبانی Blazor - بخش 7 - مسیریابی
تا اینجا به صورت بسیار مختصری با نحوه‌ی مسیریابی برنامه‌های مبتنی بر Blazor توسط دایرکتیو page@ آشنا شدیم. برای مثال با داشتن تعریف زیر در ابتدای یک کامپوننت:
@page "/LearnRouting"

<h3>Learn Routing</h3>
این کامپوننت جدید، صرفنظر از محل قرارگیری فایل آن که برای مثال در پوشه‌ی Pages\LearnBlazor\LearnRouting.razor است، در مسیر https://localhost:5001/LearnRouting قابل دسترسی خواهد شد و برای تعریف مدخل منوی جدید آن، به کامپوننت Shared\NavMenu.razor مراجعه کرده و NavLink جدیدی را برای آن تعریف می‌کنیم:
<li class="nav-item px-3">
    <NavLink class="nav-link" href="LearnRouting">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Learn Routing
    </NavLink>
</li>
در اینجا برچسب مدخل جدید تعریف شده، Learn Routing است و href لینک به آن، دقیقا به مسیریابی تعریف شده اشاره می‌کند.

یک نکته: مسیریابی‌های تعریف شده‌ی در Blazor، حساس به حروف کوچک و بزرگ نیستند.


امکان تعریف بیش از یک مسیریابی برای یک کامپوننت نیز وجود دارد

در کامپوننت‌های Blazor، محدودیتی از لحاظ تعداد بار تعریف دایرکتیو page@ وجود ندارد:
@page "/LearnRouting"
@page "/NewRouting"

<h3>Learn Routing</h3>
در این حالت می‌توان در حین تعریف یک مسیریابی جدید، مسیریابی قبلی را نیز حفظ کرد. در اینجا کامپوننت فوق، از طریق هر دو آدرس https://localhost:5001/LearnRouting و https://localhost:5001/NewRouting تعریف شده، قابل دسترسی‌است.


روش تعریف پارامترهای مسیریابی

تا اینجا اگر مسیر جدید https://localhost:5001/NewRouting/1/2 را درخواست کنیم چه اتفاقی رخ می‌دهد؟


در مورد نحوه‌ی تعریف قالب «یافت نشد» فوق، در قسمت دوم بیشتر بحث شد.
برای تعریف پارامترهای مسیریابی، می‌توان مسیریابی سومی را با پارامترهای مدنظر تعریف کرد که در مثال زیر، ذکر پارامتر دوم اختیاری است؛ چون سومین مسیریابی تعریف شده، امکان پردازش مسیرهایی با یک پارامتر را هم ممکن می‌کند:
@page "/LearnRouting"
@page "/NewRouting"
@page "/LearnRouting/{parameter1}"
@page "/LearnRouting/{parameter1}/{parameter2}"

<h3>Learn Routing</h3>

<p>Parameter1: @Parameter1</p>
<p>Parameter2: @Parameter2</p>

@code
{
    [Parameter]
    public string Parameter1 { set; get; }

    [Parameter]
    public string Parameter2 { set; get; }
}
سپس جهت دست‌یابی به مقادیر این پارامترها می‌توان در قسمت کدهای کامپوننت، از خواص عمومی مزین شده‌ی با ویژگی Parameter استفاده کرد. در اینجا هر خاصیت تعریف شده، باید هم نام پارامتر تعریف شده باشد (و این مورد نیز غیر حساس به حروف بزرگ و کوچک است).
پس از این تعاریف، مسیریابی مانند https://localhost:5001/LearnRouting/1 با یک پارامتر و یا https://localhost:5001/LearnRouting/1/2 که به همراه دو پارامتر است، قابل فراخوانی می‌شود.





روش تعریف لینک به سایر کامپوننت‌های Blazor

در ادامه کامپوننت جدید Pages\LearnBlazor\LearnAdvancedRouting.razor را اضافه می‌کنیم؛ با این محتوای آغازین:
@page "/LearnAdvancedRouting"

<h3>Learn Advanced Routing</h3>
در اینجا قصد نداریم لینک به این کامپوننت را به منوی اصلی برنامه اضافه کنیم؛ بلکه می‌خواهیم از طریق همان کامپوننت LearnRouting.razor ابتدای بحث، این مسیریابی را برقرار کنیم. برای اینکار یا می‌توان از یک anchor tag استاندارد استفاده کرد و یا همانند کامپوننت Shared\NavMenu.razor، از کامپوننت NavLink استفاده نمود. NavLink‌ها نیز همانند anchor tag‌های استاندارد HTML هستند، با این تفاوت که این کامپوننت، افزودن کلاس active مخصوص بوت استرپ را هم بر اساس فعال بودن مسیریابی مرتبط به آن، انجام می‌دهد ("class="nav-link active). به همین علت است که اگر گزینه‌ی منوی خاصی را انتخاب کنیم، این گزینه با رنگ متمایزی نشان داده می‌شود:


بنابراین یک روش تعریف لینک به کامپوننتی دیگر، استفاده از کامپوننت NavLink است که href آن به مسیریابی مقصد اشاره می‌کند:
<NavLink class="btn btn-secondary" href="LearnAdvancedRouting">
    <span class="oi oi-list-rich" aria-hidden="true"></span> Learn Advanced Routing
</NavLink>


ارسال کوئری استرینگ‌ها به کامپوننت‌های مختلف

پس از تعریف لینکی به کامپوننتی دیگر از درون یک کامپوننت، اکنون می‌خواهیم دو کوئری استرینگ param1 و param2 را نیز به آن ارسال کنیم:
<NavLink class="btn btn-secondary" href="LearnAdvancedRouting?param1=value1&param2=value2">
    <span class="oi oi-list-rich" aria-hidden="true"></span> Learn Advanced Routing
</NavLink>
در کامپوننت LearnAdvancedRouting برای دریافت این پارامترها، نیاز است آن‌ها را از URL جاری استخراج کرد:
@page "/LearnAdvancedRouting"
@inject NavigationManager NavigationManager

<h3>Learn Advanced Routing</h3>

<h4>Parameter 1 : @Param1</h4>
<h4>Parameter 2 : @Param2</h4>

@code
{
    string Param1;
    string Param2;

    protected override void OnInitialized()
    {
        base.OnInitialized();

        var absoluteUri = new Uri(NavigationManager.Uri);
        var queryParam = System.Web.HttpUtility.ParseQueryString(absoluteUri.Query);
        Param1 = queryParam["Param1"];
        Param2 = queryParam["Param2"];
    }
}
ابتدا سرویس توکار NavigationManager توسط دایرکتیو inject@ به کامپوننت جاری تزریق شده‌است که برای کار و دسترسی به آن، نیاز به تنظیمات ابتدایی خاصی نیست و پیشتر به مجموعه‌ی سرویس‌های برنامه افزوده شده‌است. برای نمونه توسط آن می‌توان به Uri در حال پردازش، دسترسی یافت. اکنون که این Uri را داریم، با استفاده از متد HttpUtility.ParseQueryString می‌توان به مجموعه‌ی کوئری استرینگ‌های ارسالی، به صورت key/value دسترسی یافت و برای مثال آن‌ها را در روال رویدادگردان OnInitialized، دریافت و با انتساب آن‌ها به دو فیلد تعریف شده، سبب نمایش مقادیر دریافتی شد:



هدایت به یک کامپوننت دیگر با کد نویسی

فرض کنید می‌خواهیم دکمه‌ای را اضافه کنیم که با کلیک بر روی آن، ما را به کامپوننت LearnRouting هدایت می‌کند:
@page "/LearnAdvancedRouting"
@inject NavigationManager NavigationManager

@*<NavLink href="/learnrouting" class="btn btn-secondary">Back to Routing</NavLink>*@
@*<a href="/learnrouting" class="btn btn-secondary">Back to Routing</a>*@
<button class="btn btn-secondary" @onclick="BackToRouting">Back to Routing</button>

@code
{
    private void BackToRouting()
    {
        NavigationManager.NavigateTo("learnrouting");
    }
}
در اینجا روش‌های مختلف تعریف لینک به کامپوننتی دیگر را مشاهده می‌کنید. یا می‌توان از کامپوننت NavLink استفاده کرد و یا از یک anchor tag استاندارد، که href هر دوی آن‌ها به مسیریابی مقصد اشاره می‌کنند و یا می‌توان با استفاده از سرویس NavigationManager و متد NavigateTo آن مانند کدهای فوق، سبب هدایت کاربر به صفحه‌ای دیگر شد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-10.zip