مطالب
مسیریابی در Angular - قسمت اول - معرفی
مسیریابی در +Angular 2 به همراه قابلیت‌های فراوان و ویژه‌ای است که تعدادی از آن‌ها را تابحال در این سایت بررسی کرده‌ایم. مورد مقدماتی اول، نیاز به بازنویسی کامل دارد، مورد دوم جهت آشنایی با ساختار Angular CLI مفید است و مورد سوم یکی از مباحث تکمیلی آن است. به همین جهت در طی یک سری، ویژگی‌های متعدد سیستم مسیریابی +Angular 2 را از ابتدا بررسی خواهیم کرد.


مسیریابی در +Angular 2

عموما از مسیریابی جهت حرکت بین Viewهای مختلف برنامه استفاده می‌شود، اما کارهای بیشتری را نیز می‌توان با آن انجام داد؛ مانند ارسال اطلاعات، به مسیریابی‌ها، پیش بارگذاری اطلاعات، جهت نمایش در Viewها، گروه بندی و محافظت از مسیریابی‌ها، پویانمایی و انیمیشن و همچنین بهبود کارآیی، با بارگذاری async مسیرهای مختلف.
کار سیستم مسیریاب +Angular 2 زمانی شروع می‌شود که تغییری را در آدرس درخواستی از برنامه مشاهده می‌کند؛ یا از طریق درخواست آدرسی توسط مرورگر و یا هدایت به قسمتی خاص، از طریق کدنویسی. سپس مسیریاب به آرایه‌ی تنظیم شده‌ی مسیرهای سیستم مراجعه می‌کند تا بتواند تطابقی را بین آدرس درخواستی و یکی از کلیدهای تنظیم شده‌ی در آن پیدا کند. در این حالت اگر تطابقی یافت نشود، کارمسیریابی خاتمه خواهد یافت. در غیراینصورت کار ادامه یافته و سپس مسیریاب، محافظ‌های مسیر درخواستی را بررسی می‌کند تا مشخص شود که آیا کاربر مجاز به هدایت به این قسمت خاص از برنامه هست یا خیر؟ در صورت مثبت بودن پاسخ، مرحله‌ی بعد، پیش بارگذاری اطلاعات درخواستی جهت نمایش View مرتبط است. در ادامه کامپوننت متناظر با مسیریابی فعالسازی می‌شود. سپس قالب این کامپوننت را در قسمتی که توسط router-outlet مشخص می‌گردد، جایگذاری کرده و نمایش می‌دهد.


تعریف مسیر پایه یا Base path

اولین مرحله‌ی کار با سیستم مسیریابی +Angular 2، تعریف یک base path است. مسیرپایه، به زیرپوشه‌ای اشاره می‌کند که برنامه‌ی ما در آن قرار گرفته‌است:
 www.mysite.com/myapp
در این مثال، مسیرپایه برنامه، /myapp/ است.
مسیریاب از این مسیرپایه جهت ساخت آدرس‌های مسیریابی استفاده می‌کند. مقدار آن نیز به صورت ذیل در فایل index.html برنامه، درست پس از تگ head تعیین می‌گردد:
<!DOCTYPE html>
<html>
<head>
    <base href="/">
در اینجا با ذکر / ، به عنوان مسیرپایه، به ریشه‌ی سایت اشاره شده‌است. اگر برنامه‌ی شما قرار است در یک زیرپوشه قرار بگیرد، در این حالت نحوه‌ی تعیین این زیر پوشه به صورت ذیل خواهد بود:
<base href="/myapp/">


تعیین مسیرپایه جهت ارائه‌ی نهایی

استفاده از مسیر پایه / برای حالت توسعه و همچنین زمانیکه برنامه‌ی نهایی شما در ریشه‌ی سایت توزیع می‌شود، بسیار مناسب است. اما اگر برای حالت توسعه از مقدار / و برای حالت توزیع از مقدار /myapp/ بخواهید استفاده کنید، مدام نیاز خواهید داشت تا فایل index.html نهایی سایت را ویرایش کنید. برای این منظور Angular CLI دارای پرچمی است به نام base-href:
 > ng build --base-href /myapp/
به این ترتیب در زمان ساخت و توزیع نهایی برنامه توسط Angular CLI، کار تنظیم خودکار مسیرپایه نیز صورت خواهد گرفت.
حالت پیش فرض تولید برنامه‌های Angular توسط Angular CLI، تنظیم مسیرپایه در فایل src\index.html به صورت خودکار به / می‌باشد.


تعریف مسیریاب Angular

مسیریاب Angular در ماژولی به نام RouterModule قرار گرفته‌است و باید در ابتدای کار import شود. این ماژول شامل سرویسی است جهت هدایت کاربران به صفحات دیگر و مدیریت URLها، تنظیماتی برای تعریف جزئیات مسیریابی‌ها و تعدادی دایرکتیو که برای فعالسازی و نمایش مسیرها از آن‌ها استفاده می‌شود. برای مثال دایرکتیو RouterLink آن یک المان قابل کلیک HTML را به مسیر و کامپوننتی خاص در برنامه متصل می‌کند. RouterLinkActive، شیوه‌نامه‌ها را به لینک فعال انتساب می‌دهد و RouterOutlet محل نمایش قالب کامپوننت فعال شده را مشخص می‌کند.

یک مثال: در ادامه، یک پروژه‌ی جدید مبتنی بر Angular CLI را به نام angular-routing-lab به همراه تنظیمات ابتدایی مسیریابی آن ایجاد می‌کنیم:
 > ng new angular-routing-lab --routing
به علاوه قصد استفاده‌ی از بوت استرپ را نیز داریم. به همین جهت ابتدا به ریشه‌ی پروژه وارد شده و سپس دستور ذیل را صادر کنید تا بوت استرپ نیز نصب شود و پرچم save آن سبب به روز رسانی فایل package.json نیز گردد:
 > npm install bootstrap --save
پس از آن نیاز است به فایل angular-cli.json. مراجعه کرده و شیوه‌نامه‌ی بوت استرپ را تعریف کنیم:
  "apps": [
    {
      "styles": [
    "../node_modules/bootstrap/dist/css/bootstrap.min.css",
        "styles.css"
      ],
به این ترتیب به صورت خودکار این شیوه نامه به همراه توزیع برنامه حضور خواهد داشت و نیازی به تعریف مستقیم آن در فایل index.html نیست.

در ادامه اگر به فایل src\app\app-routing.module.ts مراجعه کنید، یک چنین محتوایی را خواهید یافت:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  {
    path: '',
    children: []
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
در اینجا برای شروع به کار با سیستم مسیریابی، RouterModule معرفی شده‌است. ابتدا import آن‌را مشاهده می‌کنید و سپس این ماژول به قسمت imports مربوط به NgModule اضافه شده‌است.
می‌توان این قسمت را خلاصه کرد و فایل app-routing.module.ts را نیز حذف کرد و سپس import لازم و تعریف ماژول آن‌را به ماژول آغازین برنامه یا همان src\app\app.module.ts نیز منتقل کرد. اما پس از مدتی تنظیمات مسیریابی آن، فایل ماژول اصلی برنامه را بیش از اندازه شلوغ خواهند کرد. بنابراین Angular-CLI تصمیم به ایجاد یک ماژول مستقل را برای تعریف تنظیمات مسیریابی برنامه گرفته‌است. سپس تعریف آن را به فایل src\app\app.module.ts به صورت خودکار اضافه می‌کند:
import { AppRoutingModule } from './app-routing.module';

@NgModule({
  imports: [
    AppRoutingModule
  ],

اگر به قسمت import مربوط به NgModule فایل src\app\app-routing.module.ts دقت کنید، این ماژول به همراه متد forRoot معرفی شده‌است.
@NgModule({
  imports: [RouterModule.forRoot(routes)],
ماژول مسیریابی به همراه دو متد ذیل است:
الف) forRoot
- کار آن تعریف دایرکتیوهای مسیریابی، مدیریت تنظیمات مسیریابی و ثبت سرویس مسیریابی است.
- نکته‌ی مهم اینجا است که متد forRoot تنها یکبار باید در طول عمر یک برنامه تعریف شود.
- این متد آرایه‌ای از تنظیمات مسیریابی‌های تعریف شده را دریافت می‌کند.
ب) forChild
- کار آن تعریف دایرکتیوهای مسیریابی و مدیریت تنظیمات مسیریابی است؛ اما سرویس مسیریابی را مجددا ثبت نمی‌کند.
- از این متد در جهت تعریف مسیریابی‌های ماژول‌های ویژگی‌های مختلف برنامه و نظم بخشیدن به آن‌ها استفاده می‌شود.

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

اکنون تمام کامپوننت‌های قید شده‌ی در قسمت declaration، امکان دسترسی به دایرکتیوهای مسیریابی را پیدا می‌کنند. همچنین از آنجائیکه AppRoutingModule به همراه متد forRoot است، سرویس مسیریابی نیز در کل برنامه قابل دسترسی است.


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

آرایه‌ی const routes: Routes فایل src\app\app-routing.module.ts در ابتدای کار خالی است. در اینجا کار تعریف URL segments و سپس اتصال آن‌ها به کامپوننت‌های متناظری جهت فعالسازی و نمایش قالب آن‌ها صورت می‌گیرد. این نمایش نیز در محل router-outlet تعریف شده‌ی در فایل src\app\app.component.html انجام می‌شود:
<h1>
  {{title}}
</h1>
<router-outlet></router-outlet>
به ازای هر router-outlet، تنها یک مسیر را در هر زمان می‌توان نمایش داد. بنابراین با فعال شدن هر کامپوننت، قالب آن، قالب کامپوننت پیشین را در router-outlet جایگزین خواهد کرد. امکان تعریف router-outletهای دیگری نیز وجود دارند که به آن‌ها secondary routes گفته می‌شود.

در ادامه برای تکمیل مثال جاری، دو کامپوننت جدید خوش‌آمد گویی و همچنین یافتن نشدن مسیرها را به برنامه اضافه می‌کنیم:
>ng g c welcome
>ng g c PageNotFound
که سبب ایجاد کامپوننت‌های src\app\welcome\welcome.component.ts و src\app\page-not-found\page-not-found.component.ts خواهند شد، به همراه به روز رسانی خودکار فایل src\app\app.module.ts جهت تکمیل قسمت declarations آن:
@NgModule({
  declarations: [
    AppComponent,
    WelcomeComponent,
    PageNotFoundComponent
  ],

سپس فایل src\app\app-routing.module.ts را به نحو ذیل تکمیل نمائید:
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { WelcomeComponent } from './welcome/welcome.component';
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  { path: 'welcome', component: WelcomeComponent },
  { path: '', redirectTo: 'welcome', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
در اینجا در ابتدا دو کامپوننت جدید تعریف شده، import شده‌اند.

یک نکته: افزونه‌ی auto import، کار تعریف کامپوننت‌ها را در VSCode بسیار ساده می‌کند و امکان تشکیل خودکار قسمت import را با ارائه‌ی یک intellisense به همراه دارد.

سپس کار تکمیل آرایه‌ی Routes انجام شده‌است. همانطور که مشاهده می‌کنید، این آرایه متشکل است از اشیایی که به همراه خاصیت path و سایر پارامترهای مورد نیاز هستند.
کار خاصیت path، تعیین URL segment متناظری است که این مسیریابی را فعال می‌کند. برای مثال اولین شیء تعریف شده با آدرس‌هایی مانند www.mysite.com/welcome متناظر است.
 { path: 'welcome', component: WelcomeComponent },
در اغلب حالات به همراه path، خاصیت کامپوننت متناظر با آن مسیر نیز ذکر می‌شود؛ مانندWelcomeComponent ایی که در اینجا مقدار دهی شده‌است. هنگامیکه این مسیریابی فعال می‌شود، قالب این کامپوننت‌است که در router-outlet نمایش داده خواهد شد (فایل src\app\welcome\welcome.component.html).

چند نکته:
- در حین تعریف مقدار خاصیت path، هیچ / آغاز کننده‌ای تعریف نشده‌است.
- مقدار خاصیت path، حساس به کوچکی و بزرگی حروف است.
- WelcomeComponent تعریف شده، یک رشته نیست و ارجاعی را به کامپوننت مرتبط دارد. به همین جهت نیاز به import statement ابتدایی را دارد و وجود آن توسط کامپایلر بررسی می‌شود.


تعیین مسیریابی پیش فرض سایت

اما زمانیکه برنامه برای بار اول بارگذاری می‌شود، چطور؟ در این حالت هیچ URL segment ایی وجود ندارد. بنابراین برای تنظیم مسیرپیش فرض سایت، خاصیت path، به یک رشته‌ی خالی همانند دومین شیء تنظیمات مسیریابی، تنظیم می‌شود:
   { path: '', redirectTo: 'welcome', pathMatch: 'full' },
در اینجا زمانیکه کاربر، ریشه‌ی سایت را درخواست می‌کند، به کامپوننت welcome هدایت خواهد شد. در این تنظیم خاصیت pathMatch نحوه‌ی تطابق با خاصیت path را مشخص می‌کند. مقدار full نیز به این معنا است که اگر تمام URL segment دریافتی خالی بود، از این تنظیم استفاده کن.


مدیریت مسیریابی آدرس‌های ناموجود در سایت

تنظیم سومی را نیز در اینجا مشاهده می‌کنید:
 { path: '**', component: PageNotFoundComponent },
** به این معنا است که اگر مسیریاب موفق به تطابق URL segment دریافتی، با هیچکدام از pathهای تعریف شده نبود، از این تنظیم استفاده کند (catch all). برای مثال اگر کاربر آدرس www.mysite.com/someAddr را درخواست کند، چون URL segment آن در آرایه‌ی تنظیمات، تعریف نشده‌است، می‌خواهیم یک صفحه‌ی یافت نشد را نمایش دهیم.

یک نکته: ترتیب مسیریابی‌ها در آرایه‌ی تعریف آن‌ها اهمیت دارد. در اینجا از استراتژی «اولین تطابق یافته‌، برنده خواهد بود» استفاده می‌شود. بنابراین تنظیم ** باید در انتهای لیست ذکر شود؛ در غیراینصورت هیچکدام از مسیریابی‌های تعریف شده‌ی پس از آن پردازش نخواهند شد.


مدیریت تغییرات آدرس‌های برنامه

در طول عمر برنامه ممکن است نیاز به تغییر آدرس‌های برنامه باشد. برای مثال بجای مسیر welcome مسیر home نمایش داده شود:
const routes: Routes = [
  { path: 'home', component: WelcomeComponent },
  { path: 'welcome', redirectTo: 'home', pathMatch: 'full' },
  { path: '', redirectTo: 'welcome', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];
در این تنظیمات جدید برنامه، مسیر home به WelcomeComponent اشاره می‌کند. اما جهت هدایت آدرس‌های قدیمی welcome، به این آدرس جدید home می‌توان از redirectTo، مانند دومین شیء مسیریابی اضافه شده‌ی به آرایه‌ی تنظیمات استفاده کرد. به این ترتیب هم تغییرات آرایه‌ی Routes به حداقل می‌رسد و هم آدرس‌های ذخیره شده‌ی توسط کاربران هنوز هم معتبر خواهند بود.

نکته: redirectToها قابلیت تعریف زنجیره‌ای را ندارند. به این معنا که اگر ریشه‌ی سایت درخواست شود، ابتدا به مسیر welcome هدایت خواهیم شد. مسیر welcome هم یک redirectTo دیگر به مسیر home را دارد. اما در اینجا کار به این redirectTo دوم نخواهد رسید و این پردازش، زنجیره‌ای نیست. بنابراین مسیریابی پیش‌فرض را نیز باید ویرایش کرد و به home تغییر داد:
const routes: Routes = [
  { path: 'home', component: WelcomeComponent },
  { path: 'welcome', redirectTo: 'home', pathMatch: 'full' },
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];
به این ترتیب دیگر درخواست زنجیره‌وار redirectToها رخ نخواهد داد.

نکته: redirectToها می‌توانند local و یا absolute باشند. تعریف محلی آن‌ها مانند ذکر home و welcome در اینجا است و تنها سبب تغییر یک URL segment می‌شود. اما اگر در ابتدای مقادیر redirectToها یک / قرار دهیم، به معنای تعریف یک مسیر مطلق است و کل URL را جایگزین می‌کند.


تعیین محل نمایش قالب‌های کامپوننت‌ها

زمانیکه یک کامپوننت فعالسازی می‌شود، قالب آن در router-outlet نمایش داده خواهد شد. برای این منظور فایل 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>
    </ul>
  </div>
</nav>
<div class="container">
  <router-outlet></router-outlet>
</div>
در اینجا دایرکتیو router-outlet محلی است که قالب کامپوننت فعالسازی شده، نمایش داده می‌شود. برای مثال اگر کاربر بر روی لینک Home کلیک کند، کامپوننت متناظر با آن از طریق تنظیمات مسیریابی یافت شده و فعالسازی می‌شود و سپس قالب آن در محل router-outlet رندر خواهد شد.

یک نکته: چون کامپوننت welcome از طریق مسیریابی نمایش داده می‌شود و دیگر به صورت مستقیم با درج تگ selector آن در صفحه فعالسازی نخواهد شد، می‌توان به تعریف کامپوننت آن مراجعه کرده و selector آن‌را حذف کرد.
 @Component({
//selector: 'app-welcome',
templateUrl: './welcome.component.html',
styleUrls: ['./welcome.component.css']
})

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


اگر به آدرس تنظیم شده‌ی در مرورگر دقت کنید، http://localhost:4200/home آدرسی است که در ابتدای نمایش سایت نمایان خواهد شد. علت آن نیز به تنظیم مسیریابی پیش فرض سایت برمی‌گردد.
و اگر یک مسیر غیرموجود را درخواست دهیم، قالب کامپوننت PageNotFound ظاهر می‌شود:



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

کاربران را می‌توان به روش‌های مختلفی به قسمت‌های گوناگون برنامه هدایت کرد؛ برای مثال با کلیک بر روی المان‌های قابل کلیک HTML و سپس اتصال آن‌ها به کامپوننت‌های برنامه. استفاده‌ی کاربر از bookmark مرورگر و یا ورود مستقیم و دستی آدرس قسمتی از برنامه و یا کلیک بر روی دکمه‌های forward و back مرورگر. تنها مورد اول است که نیاز به تنظیم دارد و سایر قسمت‌ها به صورت خودکار مدیریت خواهند شد. نمونه‌ی آن‌را نیز با تعریف لینک Home پیشتر مشاهده کردید:
  <a [routerLink]="['/home']">Home</a>
- در اینجا با استفاده از یک دایرکتیو ویژه به نام routerLink، المان‌های قابل کلیک HTML را به خاصیت‌های path تعریف شده‌ی در تنظیمات مسیریابی برنامه متصل می‌کنیم. routerLink یک attribute directive است؛ بنابراین می‌توان آن‌را به یک المان قابل کلیک anchor tag، مانند مثال فوق نسبت داد.
- زمانیکه کاربر بر روی این لینک کلیک می‌کند، اولین path متناظر با routerLink یافت شده و فعالسازی خواهد شد.
- علت تعریف مقدار routerLink به صورت [] این است که آرایه‌ی پارامترهای لینک را مشخص می‌کند. بنابراین چون آرایه‌است، نیاز به [] دارد. اولین پارامتر این آرایه مفهوم root URL segment را دارد. در اینجا حتما نیاز است URL segment را با یک / شروع کرد. به علاوه باید دقت داشت که خاصیت path تنظیمات مسیریابی، حساس به حروف کوچک و بزرگ است. بنابراین این مورد را باید در اینجا نیز مدنظر داشت.
- پارامترهای دیگر routerLink می‌توانند مفهوم پارامترهای این segment و یا حتی segments دیگری باشند.

یک نکته: چون در مثال فوق، آرایه‌ی تعریف شده تنها دارای یک عضو است، آن‌را می‌توان به صورت ذیل نیز خلاصه نویسی کرد (one-time binding):
 <a routerLink="/home">Home</a>


تفاوت بین آدرس‌های HTML 5 و Hash-based

زمانیکه مسیریاب Angular کار پردازش آدرس‌های رسیده را انجام می‌دهد، اینکار در سمت کلاینت صورت می‌گیرد و تنها URL segment مدنظر را تغییر داده و این درخواست را به سمت سرور ارسال نمی‌کند. به همین جهت سبب reload صفحه نمی‌شود. دو روش در اینجا جهت مدیریت سمت کلاینت آدرس‌ها قابل استفاده است:
الف) HTML 5 Style
- آدرسی مانند http://localhost:4200/home، یک آدرس به شیوه‌ی HTML 5 است. در اینجا مسیریاب Angular با استفاده از HTML 5 history pushState سبب به روز رسانی History مرورگر شده و آدرس‌ها را بدون ارسال درخواستی به سمت سرور، در همان سمت کلاینت تغییر می‌دهد.
- این روش حالت پیش فرض Angular است و نحوه‌ی نمایش آن بسیار طبیعی به نظر می‌رسد.
- در اینجا URL rewriting سمت سرور نیز جهت هدایت آدرس‌ها، به برنامه‌ی Angular ضروری است. برای مثال زمانیکه کاربری آدرس http://localhost:4200/home را مستقیما در مرورگر وارد می‌کند، این درخواست ابتدا به سمت سرور ارسال خواهد شد و چون چنین صفحه‌ای در سمت سرور وجود ندارد، پیغام خطای 404 را دریافت می‌کند. اینجا است که URL rewriting سمت سرور به فایل index.html برنامه، جهت مدیریت یک چنین حالت‌هایی ضروری است.
برای نمونه اگر از وب سرور IIS استفاده می‌کنید، تنظیم ذیل را به فایل web.config در قسمت system.webServer اضافه کنید (کار کرد آن هم وابسته‌است به نصب و فعالسازی ماژول URL Rewrite بر روی IIS):
<rewrite>
    <rules>
        <rule name="Angular 2+ pushState routing" stopProcessing="true">
            <match url=".*" />
                <conditions logicalGrouping="MatchAll">
                    <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
                    <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
                    <add input="{REQUEST_FILENAME}" pattern=".*\.[\d\w]+$" negate="true" />
                    <add input="{REQUEST_URI}" pattern="^/(api)" negate="true" />
                </conditions>
                <action type="Rewrite" url="/index.html" />
        </rule>
    </rules>
</rewrite>

ب) Hash-based
- آدرسی مانند http://localhost:4200/#/home یک آدرس به شیوه‌ی Hash-based بوده و مخصوص مرورگرهایی است بسیار قدیمی که از HTML 5 پشتیبانی نمی‌کنند. اینبار قطعات قرار گرفته‌ی پس از علامت # دارای نام URL fragments بوده و قابلیت پردازش در سمت کلاینت را دارا می‌باشند.
- اگر علاقمند به استفاده‌ی از این روش هستید، نیاز است خاصیت useHash را به true تنظیم کنید:
@NgModule({
   imports: [RouterModule.forRoot(routes, { useHash: true })],
-  این روش نیازی به URL rewriting سمت سرور ندارد. از آنجائیکه سرور هرچیزی را که پس # باشد ندید خواهد گرفت و سعی در یافتن آن‌، در سمت سرور نخواهد کرد.
نظرات مطالب
استفاده از GitHub Actions برای Build و توزیع خودکار پروژه‌های NET Core.
یک نکته تکمیلی :
کد زیر یک کد نسبتا کامل برای Workflow کتابخانه‌های NET Core. است
  • از اجرای workflow اضافی به هنگام تغییر فایل readme.md جلوگیری میکند (می توانید فایل یا پوشه‌های دیگری را هم اضافه کنید)
  • مراحل Build و Test پروژه را در حالت Release انجام میدهد
  • فایل .nupkg مورد نیاز برای پکیج Nuget را در حالت Release ایجاد میکند (عبارت src نام پوشه اصلی پروژه است; در صورت نیاز تغییر دهید)
  • به هنگام Push شدن ریپازیتوری به همراه تگ "release " به صورت خودکار پکیج را به سایت nuget آپلود میکند (عبارت secrets.NUGET_TOKEN شامل مقدار API_KEY شما در سایت Nuget است که باید در قسمت Setting ریپازیتوری، قسمت Secrects ذخیره شده باشد)
name: .NET Core

on:
  push:
    paths-ignore:
      - 'readme.md'
  pull_request:
    paths-ignore:
      - 'readme.md'

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v2

    - name: Setup .NET Core
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: 3.1.101

    - name: Build (Release)
      run: dotnet build --configuration Release

    - name: Test (Release)
      run: dotnet test --configuration Release

    - name: Pack (Release)
      run: dotnet pack src --configuration Release

    - name: Publish (Nuget)
      if: github.event_name == 'push'
      run: |      
          if ( "${{github.ref}}" -match "^refs/tags/[0-9]+\.[0-9]+\.[0-9]+$" ) {
              dotnet nuget push src\bin\Release\*.nupkg -s nuget.org -k ${{secrets.NUGET_TOKEN}}
          } else {
              echo "Publish is only enabled by tagging with a release tag."
          }

مطالب
آزمایش Web APIs توسط Postman - قسمت پنجم - انواع متغیرهای قابل تعریف در Postman
در قسمت دوم، از متغیرهای سراسری، برای ایجاد یک گردش کاری بین دو درخواست ارسالی، استفاده کردیم. در این قسمت می‌خواهیم با انواع متغیرهای قابل تعریف در Postman آشنا شویم.


متغیرهای سراسری در Postman

برای تعریف متغیرهای سراسری که در تمام برگه‌های Postman قابل دسترسی باشند، می‌توان از متد pm.globals.set در قسمت Tests هر درخواست که پس از پایان درخواست جاری اجرا می‌شود، استفاده کرد. دراینجا فرصت خواهیم داشت تا مقدار دریافتی از سرور را در یک متغیر ذخیره کنیم. سپس می‌توان از این متغیر، در حین ارسال درخواستی دیگر، استفاده کرد که نمونه‌ای از آن‌را در قسمت دوم، با تبدیل شیء response به یک شیء جاوا اسکریپتی و استخراج خاصیت uuid آن، مشاهده کردید:
let jsonResponse = pm.response.json();
pm.globals.set("uuid", jsonResponse.uuid);
روش دیگر تعریف متغیرهای سراسری، تعریف دستی آن‌ها است. در اینجا با می‌توان با کلیک بر روی دکمه‌‌ای با آیکن چشم، در بالای سمت راست صفحه، گزینه‌ی Edit مخصوص Global variables را انتخاب کرد:


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


برای کار با متغیرهای سراسری، 4 متد زیر در Postman قابل استفاده هستند:

عملکرد 
 متد
  تعریف و مقدار دهی یک متغیر سراسری 
 pm.globals.set("varName", "VALUE");
 دریافت مقدار یک متغیر سراسری 
pm.globals.get("varName");
  پاک کردن یک متغیر سراسری مشخص 
pm.globals.unset("varName");
 حذف تمام متغیرهای سراسری 
pm.globals.clear();

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


روش استفاده‌ی از متغیرهای تعریف شده

پس از تعریف این متغیرها، برای دسترسی به آن‌ها می‌توان از روش {{variableName}} در قسمت‌های مختلف postman استفاده کرد:
Request URL: http://{{domain}}/users/{{userId}}
Headers (key:value): X-{{myHeaderName}}:foo
Request body: {"id": "{{userId}}", "name": "John Doe"}
در اینجا سه مثال را در مورد امکان استفاده‌ی از متغیرها، در حین ساخت URLها، اجزای هدرها و یا بدنه‌ی درخواست ارسالی به سمت سرور، مشاهده می‌کنید.


متغیرهای محیطی در Postman

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

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


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


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

پس از تعریف متغیرهای جدید محیطی و مقادیر آن‌ها، نحوه‌ی استفاده‌ی از این متغیرها دقیقا همانند روشی است که از متغیرهای سراسری استفاده کردیم و توسط روش {{variableName}} قابل دسترسی هستند.

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



API کار با متغیرهای محیطی از طریق کد نویسی

API دسترسی به متغیرهای محیطی، بسیار شبیه به متغیرهای سراسری است:

  عملکرد   متد 
 تعریف و مقدار دهی یک متغیر محیطی 
pm.environment.set("varName", "VALUE");
 دریافت مقدار یک متغیر محیطی 
pm.environment.get("varName");
  پاک کردن یک متغیر محیطی مشخص 
pm.environment.unset("varName");
 حذف تمام متغیرهای محیطی 
pm.environment.clear();


 تفاوت میدان دید متغیرهای محیطی و متغیرهای سراسری

باید دقت داشت که هر دوی متغیرهای سراسری و محیطی، در تمام برگه‌های تعریف شده قابل دسترسی می‌باشند و از این لحاظ تفاوتی بین آن‌ها نیست. اما فرض کنید یک متغیر سراسری را با نام port1 تعریف کرده‌اید و از آن برای ساخت آدرسی مانند https://localhost:{{port1}} استفاده کرده‌اید. همچنین دقیقا همین متغیر port1 را در محیط جدیدی به نام Env1 نیز تعریف کرده‌اید. اگر محیطی انتخاب نشده باشد، port1 به همان متغیر سراسری تعریف شده اشاره می‌کند.


اما اگر محیط انتخابی را به Env1 تغییر دهیم، اینبار port1، از طریق اطلاعات Env1 تامین شده و مقدار متغیر سراسری تعریف شده را بازنویسی (یا مخفی) می‌کند. بنابراین در حین کارکردن با محیطی مشخص، متغیرهای محیطی، بر متغیرهای سراسری مقدم هستند.


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


عدم انتشار مقادیر اولیه‌ی حساس، در حین گرفتن خروجی‌ها

اگر به تصاویر فوق دقت کنید، حین تنظیم مقادیر متغیرها، ستون اول، initial value نام دارد و ستون دوم، current value. هنگام گرفتن خروجی از یک مجموعه‌ی Postman، تنها این مقدار اولیه در خروجی وجود خواهد داشت و با دیگران به اشتراک گذاشته می‌شود. مقدار جاری همانی است که در حین ارسال درخواست‌ها مورد استفاده قرار می‌گیرد. بنابراین تنها کاربرد initial value، در تهیه‌ی خروجی‌ها است که در انتهای قسمت سوم آن‌را بررسی کردیم.
مشکل اینجا است که اگر از متدهای به روز رسانی مقادیر متغیرها استفاده کنیم، هر دو مقدار را تغییر می‌دهند که ممکن است علاقمند نباشید آن‌ها را به اشتراک بگذارید. برای رفع این مشکل می‌توان به منوی  File->Settings آن مراجعه و گزینه‌ی Automatically persist variable values را خاموش کرد:


با اینکار تغییر current value توسط متدهای API، سبب تغییر initial value که در exports ظاهر می‌شوند، نخواهد شد.
اشتراک‌ها
ساده ترین روش برای اضافه کردن Dark-Mode به هر وبسایتی با استفاده از DarkReader
گوگل کروم افزونه بسیار خوبی برای طرفداران dark-mode دارد که میتوانید با استفاده از آن حدودا بدون مشکل در ۹۵ درصد از وبسایت هایی که این حالت را پشتیبانی نمیکنند استفاده نمایید. با کمی بررسی نحوه کار آن متوجه شدم که اسکریپتی توسط تیم توسعه دهنده آن تولید شده برای افزودن این  قابلیت به وبسایت خودتان,  پس از تست و اضافه کردن آن به چند  وبسایت مختلف که درحال توسعه آن بودیم نتیجه بسیار خوبی داشت. این شد گفتم این مطلب را با دوستان به اشتراک بگذارم شاید به سایت .NetTips  هم اضافه بشه که نیازی به داشتن افزونه کروم در این سایت هم نداشته باشیم. برای نصب این پکیج میتوانید از npm  استفاده نمایید‌:
npm install darkreader
پس از دریافت این پکیج و افزودن آن به صفحه html خودتان میتوانید به این صورت از آن استفاده نمایید‌:
// فعال کردن
DarkReader.enable({
    brightness: 100,
    contrast: 90,
    sepia: 10
});
// غیر فعال کردن 
DarkReader.disable();
و یا اگر از فریم ورک‌های جاوا اسکریپتی استفاده میکنید به این روش ابتدا دو متد فعال و غیر فعال کردن آن را import کرده و از آن استفاده نمایید. 
import {
    enable as enableDarkMode,
    disable as disableDarkMode,
} from 'darkreader';
// فعال کردن
enableDarkMode({
    brightness: 100,
    contrast: 90,
    sepia: 10,
});
// غیر فعال کردن
disableDarkMode();
متد enable در این پکیج تنظیمات زیر را دریافت میکند‌:
var defaultTheme = {
        mode: 1,  // (0 1) for enabling or disabling dark mode
        brightness: 100,
        contrast: 100,
        grayscale: 0,
        sepia: 0,
        useFont: false,
        fontFamily: '', // add custom font
        textStroke: 0,
        engine: ThemeEngines.dynamicTheme,
        stylesheet: '',  // add custom css styles
    };
اگر افزونه کروم را نصب کرده باشید این تنظیمات برای شما مفهوم‌تر خواهد بود که دقیقا هر کدام برای چه کاری هستند چرا که در این افزونه این موارد قابل تنظیم است. در آخر تنها کاری که باید انجام دهید اضافه کردن یک button به صفحه خود برای فعال و غیر فعال کردن darkmode و ذخیره حالت مورد نظر کاربر در یک کوکی یا local-Storage مرورگر میباشد تا در مراجعه بعدی بتوانید حالت مورد نظر را برای کاربر فعال کنید. چندین اسکریپت و پکیج مشابه هم وجود دارد مانند darkmodejs که با آنها نسبت به darkreader به مراتب به مشکلات زیادی در css‌های خود مواجه خواهید شد. و نیاز دارند که  حدودا ۵۰ درصد از css‌های خود را برای داشتن darkmode بازنویسی نمایید. برای کسب اطلاعات بیشتر در مورد darkreader میتوانید به این لینک مراجعه نمایید.
ساده ترین روش برای اضافه کردن Dark-Mode به هر وبسایتی با استفاده از DarkReader
مطالب
چگونه نرم افزارهای تحت وب سریعتری داشته باشیم؟ قسمت اول
در این سلسله مقالات قصد دارم چندین مطلب راجع به افزایش سرعت نرم افزارهای تحت وب مطرح نمایم. این مطالب هرچند بسیار مختصر می‌باشند ولی در کارایی و سرعت برنامه‌های شما در آینده تاثیر خواهند داشت.
1.کش کردن همیشه آخرین حربه می‌باشد
این مهم است که بخش‌های مختلف سایت شما در سطوح مختلف کش شوند (ASP.NET, Kernel, Server, Proxy Server, Browser ,...) ولی این موضوع باید همیشه آخرین حربه و نکته ای باشد که آن را در مورد سایت خود اعمال می‌کنید.
یعنی همیشه مطمئن شوید ابتدا تمامی نکات مربوط به افزایش کارایی در برنامه خود را رعایت کرده اید، سپس اقدام به کش داده‌ها در سطوح مختلف نمایید. توجه کنید کش کردن داده‌ها و صفحات می‌تواند مشکلات را برای شما به عنوان یک برنامه نویس یا تست کننده برنامه پنهان کند و به شما اطمینان دهد که همه چیز خوب کار می‌کند در حالی که این چنین نیست!
البته ذکر این نکته هم بی فایده نیست که کش کردن همه چیز بعضی مواقع دشمن برنامه شما محسوب می‌شود! هیچ وقت یادم نمی‌رود، در پورتال داخلی یک شرکت که در وقت استراحت به کارکنان اجازه مطالعه روزنامه‌های روز را می‌داد (به صورت آفلاین)، این نکته در بالای صفحه آورده شده بود: «لطفا برای به روز رساندن صفحات روزنامه‌ها از کلید Ctrl+F5 استفاده نمایید». این موضوع یعنی بحث کشینگ در برنامه آن پرتال در سطح فاجعه می‌باشد! حالا فرض کنید این مشکل در فرم ورود و یا مرور اطلاعات یک برنامه به وجود آید...
2.حذف View Engine‌های غیر ضروری
به عنوان یک برنامه نویس ASP.NET MVC، یابد اطلاع داشته باشید که CLR به صورت خودکار View Engine‌های Razor و Web Forms را لود می‌کند. این موضوع به این دلیل است که اطلاعی از نحوه برنامه نویسی شما ندارد. اگر شما فقط از یکی از این دو View Engine استفاده می‌کنید،لطفا دیگری را غیر فعال کنید! فعال بودن هر دوی آنها یعنی اتلاف وقت گرانبهای CPU سرور شما برای رندر کردن تمامی صفحات شما توسط دو انجین! ایتدا view‌های شما با Web Forms Engine رندر شده سپس نتیجه به  Razor Engine منتقل شده و مجدد توسط این انجین رندر می‌شود. این موضوع در سایت‌های با تعداد کاربر بالا یعنی فاجعه!
برای حل این مشکل کافی است خطوط زیر را در فایل Global.asax و در رویداد بخش Application_Start وارد نمایید:
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new RazorViewEngine());
این دو خط یعنی خداحافظ Web Forms Engine...
قبل از استفاده از این کد، اطمینان حاصل کنید کل برنامه شما توسط Razor تهیه شده است وگرنه بنده هیچ مسئولیتی در رابطه با فریادهای کارفرمای شما متقبل نمی‌شوم!
صد البته برای حذف Razor Engine و استفاده از Web Form Engine می‌توان از کد زیر در همان موقعیت فوق استفاده کرد:
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new WebFormViewEngine());
البته همانطور که حتما دوستان مطلع هستند امکان گسترش Engine‌های فوق توسط ارث بری از کلاس BuildManagerViewEngine جهت ایجاد Engine‌های دیگر همیشه محیا است. در این صورت می‌توانید تنها انجین سفارشی مورد نظر خود را لود کرده و از لود دیگر انجین‌ها پرهیز کنید. 
3. استفاده از فایل‌های PDB مایکروسافت برای دیباگ و یا پروفایل کردن DLL‌های دیگران  
دیباگ یا پروفایل کردن برنامه ها، DLL ها، اسمبلی‌ها و منابعی از برنامه که شما آن را خود ننوشته اید (سورس آنها در دسترس شما نمی‌باشد) همیشه یکی از سخت‌ترین مراحل کار می‌باشد. جهت کمک به دیباگر‌ها یا پروفایلرها، نیاز است فایل‌های PDB مرتبط با DLL‌ها را در اختیار آنها قرار دهید تا به بهترین نتیجه دسترسی پیدا کنید. این فایل‌ها محتوی نام توابع، شماره خطوط برنامه و metadata‌های دیگر برنامه اصلی قبل از optimize شدن توسط کامپایلر یا JIT می‌باشد.  خوب حالا اگر نیاز شد این کار را در رابطه با DLL‌ها و کلاس‌های پایه Microsoft.NET انجام دهیم چه کار کنیم؟
خیلی ساده! خود Microsoft سروری جهت این موضوع تدارک دیده که فایل‌های PDB را جهت دیباگ کردن در اختیار تیم‌های برنامه نویسی قرار می‌دهد.کافی است از منوی Tools گزینه Options را انتخاب، سپس به بخش Debugging و به بخش Symbols بروید و گزینه Microsoft Symbol Servers as your source for Symbols را انتخاب کنید. برای اطمینان از اینکه هر مرتبه که برنامه را دیباگ می‌کنید مجبور به دانلود این فایل‌ها نشوید، فراموش نکنید پوشه ای را جهت کش این فایل‌ها ایجاد و آدرس آن را در بخش Cache symbols in this directory همین صفحه وارد نمایید.
این امکان در Visual Studio 2010, 2012 در دسترس می‌باشد.
نظرات مطالب
افزونه فارسی به پارسی برای word 2007
چند مورد رو می‌تونید بررسی کنید
- البته نیازی نیست آن 200 و خرده‌ای مگ را دریافت کنید. علت بالا بودن حجم آن این است که 32 بیتی و 64 بیتی و غیره، همه را با هم دارد. به آدرس زیر مراجعه کنید
http://msdn.microsoft.com/en-us/netframework/aa569263.aspx
روی دکمه نصب کلیک کنید تا یک برنامه 2 مگی در اختیار شما قرار دهد. این برنامه قسمت‌های مورد نیاز سیستم شما رو از سایت مایکروسافت دریافت و نصب خواهد کرد. بنابراین حجم کمتری دارد.
- آیا این افزونه به لیست add-in ها در word‌ اضافه شده؟ (شکل سوم در این صفحه)
- event viewer ویندوز را باز کنید. در قسمت run ویندوز تایپ کنید eventvwr.msc و سپس enter (و یا به قسمت administrative tools ویندوز مراجعه کنید این برنامه را مشاهده خواهید کرد). در صفحه باز شده به قسمت applications مراجعه کنید. آیا موردی با source مساوی FarsiToParsi از نوع خطا با آیکون قرمز مشاهده می‌شود؟ اگر بله، لطفا دوبار روی آن کلیک کنید و توضیحات صفحه باز شده را اینجا ارسال کنید. از روی این خطا می‌شود مشکل را بهتر بررسی کرد.

با تشکر
مطالب
اجرای برنامه‌های ASP.NET توسط Mono در Ubuntu
در ادامه مباحث بررسی اجرای برنامه‌های دات نت بر روی لینوکس، قصد داریم برنامه‌های ASP.NET را به کمک Mono 3.0 و یک وب سرور لینوکسی، بر روی Ubuntu اجرا کنیم.


پیشنیازها
دو پروژه خالی ASP.NET Web forms و ASP.NET MVC را در VS.NET تحت ویندوز ایجاد نمائید. آن‌ها را یکبار کامپایل کرده و اجرا کنید. سپس فایل‌‌های آن‌ها را به ubuntu منتقل کنید (پوشه‌های bin پروژه‌ها فراموش نشوند؛ خصوصا نگارش MVC که به همراه یک سری کتابخانه جانبی است).
برای انتقال فایل‌ها به لینوکس، اگر از VMWare workstation برای اجرا و آزمایش Ubuntu استفاده می‌کنید، کپی و paste مستقیم فایل‌ها از ویندوز به درون ماشین مجازی لینوکس پشتیبانی می‌شود.


نصب وب سرور آزمایشی مونو یا XSP
اگر نیاز به یک وب سرور آزمایشی، چیزی شبیه به وب سرور توکار VS.NET داشتید، پروژه XSP جهت این نوع آزمایشات ایجاد شده است.
پس از نصب آن (که به همراه همان بسته PPA قسمت قبل، هم اکنون بر روی سیستم شما نصب است)، در ترمینال لینوکس، با استفاده از دستور cd به ریشه وب سایت خود وارد شوید، سپس دستور xsp4 را اجرا کنید تا وب سرور xsp4 مشغول هاست سایت شما شود (برای اجرا در مسیر  /opt/bin/xsp4 نصب شده است).


اجرای برنامه ASP.NET Web forms 4 توسط XSP
بدون هیچ مشکل خاصی در همان ابتدای کار اجرا شد (البته باید دقت داشت که لینوکس به کوچکی و بزرگی حروف حساس است. یعنی حتما باید Default.aspx وارد شود و نه default.aspx):



اجرای برنامه ASP.NET MVC 4 توسط XSP
اجرا نشد! پیام می‌دهد که
 "Missing method System.Web.Security.FormsAuthentication::get_IsEnabled() in assembly System.Web.dll
و یا
Compiler Error Message: CS1703: An assembly with the same identity `mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' 
has already been imported. Consider removing one of the references
علت اینجا است که XSP4 همراه با نسخه PPA، قدیمی است. بنابراین باید نسخه اصلی را از مخزن کد آن دریافت و کامپایل کنیم. پیشنیازهای اینکار مانند Git و Mono، در قسمت قبل دریافت شدند. سپس فرامین ذیل را در خط فرمان لینوکس اجرا کنید:
 git clone git://github.com/mono/xsp.git
cd xsp
./autogen.sh --prefix=/opt
make
sudo make install
پس از کامپایل، اگر این نگارش جدید را اجرا کنید، به خطای ذیل خواهید رسید:
 System.IO.FileNotFoundException: Could not load file or assembly XSP, Version=3.0.0.0
برای رفع این مشکل باید اینبار وب سرور جدید را با فرمان sudo یا دسترسی مدیریتی اجرا کنید تا مشکل برطرف شود.
البته من سورس دریافت شده را در خود monodevelop کامپایل کردم (فایل sln آن‌را در monodevelop باز کرده و پروژه را build کنید). در این حالت دو فایل Mono.WebServer.dll و Mono.WebServer.XSP.exe در پوشه  xsp/src/Mono.WebServer.XSP/bin/Debug ظاهر می‌شوند.
یکی دیگر از دلایل ظاهر شدن خطای فوق، نیاز به نصب این دو فایل در GAC است که به نحو زیر قابل انجام می‌باشد:
 cd xsp/src/Mono.WebServer.XSP/bin/Debug
sudo gacutil -i Mono.WebServer.XSP.exe
sudo gacutil -i Mono.WebServer.dll
بعد این دو فایل dll و exe را در پوشه برنامه MVC خود کپی کنید و سپس دستور ذیل را اجرا نمائید:
 cd myMvcAppPath
sudo mono Mono.WebServer.XSP.exe
اینبار وب سرور جدید، روی پورت 9000 شروع به کار می‌کند. اکنون اگر در فایرفاکس آدرس http://localhost:9000 را باز کنید، برنامه اجرا شده اما به خطای ذیل خواهید رسید:
 CS0234: The type or namespace name `Helpers' does not exist in the namespace `System.Web'.
Are you missing an assembly reference?
برای رفع این مشکل باید اندکی فایل web.config برنامه را ویرایش کرد:
  <system.web> 
    <compilation debug="true" targetFramework="4.0">
      <assemblies>
        <add assembly="System.Web.Helpers, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />        
        <add assembly="System.Web.Mvc, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />        
      </assemblies>
    </compilation>
سعی بعدی ... اجرا نشد! با هر بار refresh صفحه یک خطای جدید نمایش می‌داد که ... Type خاصی را نمی‌تواند بارگذاری کند (به همراه نام اسمبلی مربوطه). برای رفع این مشکل dllهای ذیل را از پوشه bin پروژه MVC خود که از ویندوز به لینوکس کپی کرده‌اید، حذف کنید:
Microsoft.Web.Infrastructure.dll
System.Net.Http.dll
System.Net.Http.Formatting.dll
System.Web.Http.dll
System.Web.Http.WebHost.dll
این فایل‌ها توسط تیم Mono به صورت مستقل پیاده سازی شده‌اند و نیازی نیست تا از ویندوز به لینوکس کپی شوند.
بعد از حذف این فایل‌های اضافی، برنامه ASP.NET MVC نیز اجرا شد:



چند نکته تکمیلی
- نحوه تشخیص موجود بودن یک DLL خاص، در نگارش جاری Mono نصب شده:
 $ gacutil -l Microsoft.Web.Infrastructure
The following assemblies are installed into the GAC:
Microsoft.Web.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
Number of items = 1
- اگر می‌خواهید مطمئن شوید که تمام اسمبلی‌های موجود در GAC درست نصب شده‌اند یا خیر، فرمان ذیل را اجرا کنید:
cd /opt/lib/mono/gac # assuming this is your main gac
sudo find . */*/*.dll -exec gacutil -i '{}' \;
- در نسخه لینوکسی System.Web ممکن است یک سری از فضاهای نام هنوز موجود نباشند. لیست آن‌ها را در این آدرس می‌توانید مشاهده کنید:
مطالب
بررسی تغییرات Blazor 8x - قسمت هشتم - مدیریت انتقال اطلاعات Pre-Rendering سمت سرور، به جزایر تعاملی
این Prerendering است که امکان رندر یک کامپوننت تعاملی را در سمت سرور میسر می‌کند تا کاربر بتواند پیش از فعال شدن قابلیت‌های پیشرفته‌ی یک کامپوننت، یک حداقل خروجی را از آن مشاهده کند و همچنین وجود آن برای موتورهای جستجو و بهبود SEO بسیار مفید است. اما ... در این بین مشکلی رخ می‌دهد که نمونه‌ی آن‌را در قسمت قبل مشاهده کردیم: آغاز آن دوبار صورت می‌گیرد؛ یکبار در سمت سرور برای تهیه‌ی یک خروجی SSR و یکبار هم پس از فعال شدن قابلیت‌های تعاملی آن در سمت کلاینت. این آغاز دوباره، برای هر دو حالت کامپوننت‌های تعاملی Blazor Server و Blazor WASM برقرار است. راه‌حل‌هایی از نحوه‌ی مواجه شدن با یک چنین مشکلی را در قسمت قبل بررسی کردیم. راه‌حل دیگری که در این بین ارائه شده و توسط خود مایکروسافت هم در مثال‌های آن مورد استفاده قرار می‌گیرد، استفاده از سرویس PersistentComponentState است که جزئیات آن‌را در این قسمت بررسی خواهیم کرد.


بررسی نحوه‌ی عملکرد سرویس PersistentComponentState

سرویس PersistentComponentState، در دات‌نت 6، به Blazor اضافه شد و امکان جدیدی نیست. قسمتی از این مباحث جدید SSR که به‌نظر مختص به Blazor 8x هستند، پیشتر هم وجود داشتند؛ تحت عنوان pre-rendering. برای مثال فقط کافی بودن تا در برنامه‌های Blazor Server قبلی، فایل Host.cshtml_ را به صورت زیر ویرایش کرد تا pre-rendering فعال شود:
<component type="typeof(App)" render-mode="ServerPrerendered" />
مشکلی که در این حالت بروز می‌کرد این بود که متد OnInitializedAsync یک کامپوننت، دوبار فراخوانی می‌شد؛ یکبار در زمان pre-rendering در سمت سرور، تا HTML استاتیکی برای ارائه‌ی به مرورگر کاربر تولید شود و بار دیگر در زمان فعال شدن اتصال SignalR کامپوننت و ارائه‌ی نهایی تعاملی آن. به همین جهت، کار سنگین آغازین یک کامپوننت، دوبار انجام می‌شد که تجربه‌ی کاربری ناخوشایندی را هم به همراه داشت. برای رفع این مشکل، tag helper ویژه‌ای را به نام persist-component-state تهیه کردند که به صورت زیر به فایل host.cshtml_ اضافه می‌شد:
<body>
    <component type="typeof(App)" render-mode="WebAssemblyPrerendered" /> 
    <persist-component-state />
</body>
این tag-helper فوق است که کار درج رمزنگاری شده‌ی اطلاعات کش شده‌ی pre-rendering سمت سرور را در انتهای فایل HTML صفحه انجام می‌دهد و برای نمونه، نحوه‌ی استفاده‌ی از آن به صورت زیر است:
@page "/"

@implements IDisposable
@inject PersistentComponentState applicationState
@inject IUserRepository userRepository

@foreach(var user in users)
{
    <ShowUserInformation user="@item"></ShowUserInformation>
}

@code {
    private const string cachingKey = "something_unique";
    private List<User> users = new();
    private PersistingComponentStateSubscription persistingSubscription;
    
    protected override async Task OnInitializedAsync()
    {
        persistingSubscription = applicationState.RegisterOnPersisting(PersistData);

        if (applicationState.TryTakeFromJson<List<User>>(cachingKey, out var restored))
        {
            users = restored;
        }
        else 
        {
            users = await userRepository.GetAllUsers();
        }
    }

    public void Dispose()
    {
        persistingSubscription.Dispose();
    }

    private Task PersistData()
    {
        applicationState.PersistAsJson(cachingKey, users);
        return Task.CompletedTask;
    }

}
توضیحات:
- ابتدا تزریق سرویس PersistentComponentState را مشاهده می‌کنید. این همان کامپوننتی است که کار کش کردن اطلاعات را مدیریت می‌کند.
- سپس فراخوانی متد RegisterOnPersisting انجام شده‌است. متدی که توسط آن ثبت می‌شود، در حین عملیات pre-rendering فراخوانی می‌شود تا اطلاعاتی را کش کند. نحوه‌ی این کش شدن را در ادامه‌ی مطلب بررسی می‌کنیم.
- سپس فراخوانی متد TryTakeFromJson رخ‌داده‌است. اگر این متد اطلاعاتی را برگرداند، یعنی pre-rendering سمت سرور پیشتر انجام شده و اطلاعاتی کش شده، برای دریافت در سمت کلاینت، وجود داشته و نیازی به مراجعه‌ی مجدد و دوباره، به بانک اطلاعاتی نیست.
- دراینجا یک قسمت Dispose را هم مشاهده می‌کنید تا اگر کاربر به صفحه‌ی دیگری مراجعه کرد، متد ثبت شده‌ی در اینجا، از لیست مواردی که باید در حین pre-rendering سریالایز شوند، حذف گردد.

اگر از این روش استفاده نشود، به علت دوبار فراخوانی شدن متد OnInitializedAsync یک کامپوننت به همراه pre-rendering، اطلاعاتی که در حین pre-rendering کامپوننت از بانک اطلاعاتی، برای تولید محتوای استاتیک صفحه در سمت سرور دریافت شده، با فعالسازی آتی تعاملی سمت کلاینت آن کامپوننت، از دست خواهد رفت و در این حالت کلاینت باید مجددا این اطلاعات را از بانک اطلاعاتی درخواست کند. بنابراین هدف از persist-component-state، حفظ داده‌ها در بین دو رندر است؛ رندر اولی که در سمت سرور انجام می‌شود و رندر دومی که متعاقبا در سمت کلاینت رخ می‌دهد.

یک نکته: به یک چنین قابلیتی در فریم‌ورک‌های دیگر «hydration» هم گفته می‌شود که در اصل یک شیء دیکشنری است که مقدار شیء حالت را در سمت سرور دریافت کرده و آن‌را در زمان فعال شدن سمت کلاینت کامپوننت، برای استفاده‌ی مجدد مهیا می‌کند.


سؤال: اطلاعات سرویس PersistentComponentState کجا ذخیره می‌شوند؟

همانطور که در مثال فوق هم مشاهده کردید، سرویس PersistentComponentState، این state را به صورت JSON ذخیره می‌کند. اما ... این اطلاعات دقیقا کجا ذخیره می‌شوند؟
برای مشاهده‌ی آن‌ها، فقط کافی است به source صفحه مراجعه کنید تا با دو مقدار مخفی رمزنگاری شده‌ی زیر در انتهای HTML صفحه مواجه شوید!
<!--Blazor-Server-Component-State:CfDJXyz->
<!--Blazor-WebAssembly-Component-State:eyJVc2Xyz->
فرمت این اطلاعات، JsonSerializer.SerializeToUtf8Bytes رمزنگاری شده‌ی توسط IDataProtectionProvider است. این هم یک روش نگهداری اطلاعات، بجای استفاده از کش مرورگر است؛ بدون نیاز به استفاده از جاوااسکریپت و کار با local storage و امثال آن.

مایکروسافت از این نوع کارها پیشتر در ASP.NET Web forms توسط ViewStateها انجام داده‌است! البته ViewStateها جهت نگهداری اطلاعات وضعیت کلاینت، به سمت سرور ارسال می‌شوند؛ اما این Component-Stateهای مخفی، فقط یکبار توسط قسمت کلاینت خوانده می‌شوند و هدف آن‌ها ارسال اطلاعاتی به سمت سرور نیست.

یک نکته: همانطور که عنوان شد، در نگارش‌های قبلی Blazor، از تگ‌هلپری به نام persist-component-state برای درج این اطلاعات در انتهای صفحه استفاده می‌کردند. استفاده از این تگ‌هلپر در Blazor 8x منسوخ شده و به صورت خودکار داده‌های تمام سرویس‌های PersistentComponentState فعالی که توسط PersistAsJson اطلاعاتی را ذخیره می‌کنند، جمع آوری و به صورت یکجا در انتهای صفحه به صورت رمزنگاری شده، درج می‌کنند.


روش خاموش کردن Pre-rendering

برای خاموش کردن pre-rendering نیاز است پارامتر هم‌نامی را به نحو زیر با false مقدار دهی کرد:
@rendermode renderMode

@code 
{
    private static IComponentRenderMode renderMode = new InteractiveWebAssemblyRenderMode(prerender: false);
}


بازنویسی مثال قسمت قبل با استفاده از سرویس PersistentComponentState

در قسمت قبل هرچند روش مواجه شدن با مشکل دوبار فراخوانی شدن متد آغازین یک کامپوننت را در سمت سرور و کلاینت بررسی کردیم، اما ... در نهایت دوبار مراجعه به بانک اطلاعاتی انجام می‌شود؛ یکبار در زمان pre-rendering و با مراجعه‌ی مستقیم به یک سرویس سمت سرور و یکبار دیگر هم در زمان فراخوانی httpClient.GetFromJsonAsync در سمت کلاینت برای دریافت اطلاعات مورد نیاز از یک Web API Endpoint. اگر بخواهیم اطلاعات لیست محصولات دریافتی سمت سرور را به سمت کلاینت منتقل کنیم و مجددا آن‌را از بانک اطلاعاتی دریافت نکنیم، راه‌حل سوم آن، استفاده از سرویس PersistentComponentState است.

در کدهای زیر، بازنویسی کامپوننت محصولات مشابه را مشاهده می‌کنید که کمی پیچیده‌تر شده‌است. اینبار لیست محصولات مشابه، در همان بار اول رندر کامپوننت نمایش داده می‌شوند و نه پس از کلیک بر روی دکمه‌ای. به همین جهت باید کار مدیریت دوبار فراخوانی متد رویدادگردان OnInitializedAsync را به درستی انجام داد. متد OnInitializedAsync یکبار در حین پیش‌رندر سمت سرور اجرا می‌شود و بار دیگر زمانیکه وب‌اسمبلی جاری در مرورگر به صورت کامل دریافت شده و فعال می‌شود؛ یعنی برای بار دوم، کدهای اجرایی آن سمت کلاینت هستند.
در این مثال با استفاده از سرویس PersistentComponentState، اطلاعات دریافت شده‌ی در حین pre-rendering ابتدایی رخ‌داده‌ی در سمت سرور، در متد OnPersistingAsync، کش شده و در حین فعال شدن وب‌اسمبلی مرتبط در سمت کلاینت، با استفاده از متد PersistentState.TryTakeFromJson، از کش خوانده می‌شوند و دیگر دوبار رفت و برگشت به بانک اطلاعاتی را شاهد نخواهیم بود.

@implements IDisposable

@inject IProductStore ProductStore
@inject PersistentComponentState PersistentState

<h3>Related products</h3>

@if (_relatedProducts == null)
{
    <p>Loading...</p>
}
else
{
    <div class="mt-3">
        @foreach (var item in _relatedProducts)
        {
            <a href="/ProductDetails/@item.Id">
                <div class="col-sm">
                    <h5 class="mt-0">@item.Title (@item.Price)</h5>
                </div>
            </a>
        }
    </div>
}

@code{

    private const string StateCachingKey = "products";
    private IList<Product>? _relatedProducts;
    private PersistingComponentStateSubscription _persistingSubscription;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnInitializedAsync()
    {
        _persistingSubscription = PersistentState.RegisterOnPersisting(OnPersistingAsync, RenderMode.InteractiveWebAssembly);

        if (PersistentState.TryTakeFromJson<IList<Product>>(StateCachingKey, out var restored))
        {
            _relatedProducts = restored;
        }
        else
        {
            await Task.Delay(500); // Simulates asynchronous loading
            _relatedProducts = await ProductStore.GetRelatedProducts(ProductId);
        }
    }

    private Task OnPersistingAsync()
    {
        PersistentState.PersistAsJson(StateCachingKey, _relatedProducts);
        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _persistingSubscription.Dispose();
    }

}
در این پیاده سازی هنوز هم از سرویس IProductStore استفاده می‌شود که دو نگارش سمت سرور و سمت کلاینت آن‌را در قسمت قبل تهیه کردیم. برای مثال باتوجه به اینکه کدهای فوق در حین pre-rendering در سمت سرور اجرا می‌شوند، به صورت خودکار از نگارش سمت سرور IProductStore استفاده خواهد شد.

نکته‌ی مهم: فعلا کدهای فوق فقط برای حالت بارگذاری اولیه‌ی کامپوننت درست کار می‌کنند. یعنی اگر نگارش وب‌اسمبلی کامپوننت پس از وقوع پیش‌رندر سمت سرور، در مرورگر دریافت و بارگذاری کامل شود؛ اما برای دفعات بعدی مراجعه‌ی به این صفحه با استفاده از enhanced navigation و راهبری بهبود یافته که در قسمت ششم این سری بررسی کردیم ... دیگر کار نمی‌کنند و در این حالت کش شدنی رخ نمی‌دهد که در نتیجه، شاهد دوبار رفت و برگشت به بانک اطلاعاتی خواهیم بود و اساسا استفاده‌ی از PersistentComponentState را زیر سؤال می‌برد؛ چون در حین راهبری بهبود یافته، از آن استفاده نمی‌شود (قسمت PersistentState.TryTakeFromJson آن، هیچگاه اطلاعاتی را از کش نمی‌خواند). اطلاعات بیشتر 
مطالب
شروع کار با Angular Material ۲
Angular Material ۲، کامپوننت‌های طراحی متریال (Material Design) را برای برنامه‌های انگیولار ۲ فراهم می‌آورد. هدف Angular Material ۲ ارائه مجموعه‌ای از کامپوننت‌های واسط کاربری با طراحی متریال (Material Design)، برای ساخت برنامه‌هایی توسط انگیولار ۲ و تایپ اسکریپت است. در این مقاله مراحل پیاده سازی یک پروژه انگیولار ۲ را که واسط کاربری آن از طراحی متریال بهره می‌برد، دنبال خواهیم کرد. 

نکته: پروژه انگیولار متریال ۲ در زمان نوشتن این مقاله به تازگی نسخه بتا ۵ را ارائه داده و همچنان در حال توسعه است. این بدان معنی است که ممکن است همه چیز به سرعت تغییر یابد. 


مقدمه

انگیولار متریال ۲ همانند انگیولار متریال یک، تمامی المانهای مورد نیاز برای طراحی یک برنامه تک صفحه‌ای را به راحتی فراهم می‌کند (هرچند تمامی المانهای آن در نسخه بتا پیاده سازی نشده‌اند). خبر خوب اینکه، اکثر کامپوننتهای ارائه شده در انگیولار متریال ۲ از قالب راست به چپ پشتیبانی می‌کنند و اعمال این قالب به سادگی اضافه کردن خصوصیت dir یک المان به rtl است.

در صفحه گیت‌هاب انگیولار متریال ۲ آمده‌است که انگیولار متریال ۲، واسط‌های کاربری با کیفیت بالا را ارائه می‌دهد و در ادامه منظورش را از «کیفیت بالا»، اینگونه بیان می‌کند:

  1. بین‌المللی و قابل دسترس برای همه به نحوی که تمامی کاربران می‌توانند از آنها استفاده کنند (عدم مشکل در چند زبانه بودن و پشتیبانی از قالب راست به چپ و چپ به راست) .
  2. دارای APIهای ساده برای توسعه دهندگان. 
  3. رفتار مورد انتظار و بدون خطا در تمامی موردهای کاری
  4. تست تمامی رفتارها توسط تست یکپارچگی (unit test ) و تست واحد ( integration test

  5. قابلیت سفارشی سازی در چارچوب طراحی متریال 

  6. بهره‌وری بالا 

  7. کد تمیز و مستندات خوب 


شروع کار با انگیولار متریال ۲ 

قدم اول: نصب angular-material و hammerjs

برای شروع بایستی Angular Material و angular animations و hammer.js را توسط npm به صورت زیر نصب کنید.

npm install --save @angular/material @angular/animations
npm install --save hammerjs

angular/material@: بسته مربوط به انگیولار متریال دو را نصب خواهد کرد.

angular/animations@: این بسته امکاناتی جهت ساخت افکت‌های ویژه هنگام تغییر صفحات، یا بارگذاری المنت‌ها را از طریق کدهای css نوشته شده، به راحتی امکان‌پذیر می‌کند.

Hammerjs: برخی از کامپوننتهای موجود در انگیولار متریال ۲ وابسته به کتابخانه Hammerjs هستند. (از جمله md-slide-toggle و md-slider, mdTooltip)


  قدم دوم: معرفی کتابخانه‌های خارجی به  angular-cli.json 
اگر تصمیم به استفاده از کتابخانه Hammerjs گرفتید بایستی آدرس فایل hammer.js را به قسمت script در فایل angular-cli.json اضافه کنید. 
"scripts": [
  "../node_modules/hammerjs/hammer.min.js"
],

قدم سوم: افزودن Angular Material به ماوژل اصلی برنامه انگیولار
حالا نوبت اضافه کردن ماژول متریال به ماژول اصلی برنامه است. پس از معرفی ماژول MaterialModule و BrowserAnimationsModule در فایل app.module.ts این دو ماژول را به قسمت imports نیز اضافه می‌کنیم. فایل app.module.ts ما تقریبا به شکل زیر خواهد بود. 
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { MaterialModule } from '@angular/material';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import { AppComponent } from './app.component';

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

    MaterialModule,
    BrowserAnimationsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }


قدم چهارم: افزودن تم و آیکون  

همراه با نصب Angular Material تعدادی تم از قبل ساخته شده نیز نصب خواهند شد که شامل یکسری استایل با رنگهای مشخصی هستند. از جمله این تم‌ها عبارتند از:

  • indigo-pink
  • deeppurple-amber
  • purple-green
  • pink-bluegrey 

همچنین با استفاده از Material Design icons نیز با استفاده از تگ <md-icon> به آیکونهای متریال نیز می‌توان دسترسی داشت.

برای افزودن آیکونهای متریال و همچنین انتخاب یک تم از قبل ساخته شده دو خط زیر را به فایل style.css اصلی برنامه اضافه کنید. 

@import '~https://fonts.googleapis.com/icon?family=Material+Icons';
@import '~@angular/material/prebuilt-themes/deeppurple-amber.css';

نکته‌ای که در تگ <md-icon> وجود دارد این است که این تگ انواع فونت‌ها و آیکونهای svg را نیز پشتیبانی می‌کند. استفاده از آیکونهای متریال یکی از قابلیت‌های این تگ محسوب می‌شود.

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


قدم آخر: انگیولار متریال آماده است! 

با انجام مراحل بالا اکنون می‌توانید به راحتی از کامپوننت‌های متریال استفاده کنید. کافی است کدهای زیر را به فایل app.component.html اضافه کنید و یک قالب ساده برای برنامه خود بسازید. 

<md-sidenav-container>
  
  <md-sidenav #end align="end" opened="true" mode="side">
    
    <md-toolbar color="accent">
      <div>
        <md-toolbar-row>
          <img src="https://material.angular.io/favicon.ico" style="height:50px;margin-top: 2px; margin-bottom: 2px;">
          <span>
            برنامه من
          </span>
        </md-toolbar-row>
      </div>
    </md-toolbar>
    
    <md-nav-list>
      <md-list-item [routerLink]="['/']">
        <div>
          <div></div>
          <md-icon role="img" aria-label="home">home</md-icon>
          <span>خانه</span>
        </div>
      </md-list-item>
    </md-nav-list>

    <md-nav-list>
      <md-list-item [routerLink]="['/registries']">
        <div>
          <div></div>
          <md-icon role="img" aria-label="forms">content_paste</md-icon>
          <span>فرم</span>
        </div>
      </md-list-item>
    </md-nav-list>

    <md-nav-list>
      <md-list-item href="/charts">
        <div>
          <div></div>
          <md-icon role="img" aria-label="charts">show_chart</md-icon>
          <span>نمودارها</span>
        </div>
      </md-list-item>
    </md-nav-list>
  </md-sidenav>

  <header>
    <md-toolbar color="primary">
      <button md-icon-button (click)="end.toggle()">
        <md-icon>menu</md-icon>
      </button>
      <span>داشبورد</span>
      
      <button md-icon-button [md-menu-trigger-for]="menu">
        <md-icon>person</md-icon>
      </button>

    </md-toolbar>

    <md-menu x-position="before" #menu="mdMenu">
      <button md-menu-item>تنظیمات</button>
      <button md-menu-item>خروج</button>
    </md-menu>

  </header>

  <main>
    <router-outlet></router-outlet>
  </main>

</md-sidenav-container>

<span>
  <button md-fab>
    <md-icon>check circle</md-icon>
  </button>
</span>

همچنین کدهای css زیر را به فایل اصلی style.css اضافه کنید.

html, body, material-app, md-sidenav-container, .my-content {
  margin: 0;
  direction: rtl;
  width: 100%;
  height: 100%;
}

.mat-button-toggle,
.mat-button-base,
.mat-button,
.mat-raised-button,
.mat-fab,
.mat-icon-button,
.mat-mini-fab,
.mat-card,
.mat-checkbox,
.mat-input-container,
.mat-list,
.mat-menu-item,
.mat-radio-button,
.mat-select,
.mat-list .mat-list-item .mat-list-item-content,
.mat-nav-list .mat-list-item .mat-list-item-content,
.mat-simple-snackbar,
.mat-tab-label,
.mat-slide-toggle-content,
.mat-toolbar,
.mat-tooltip { font-family: 'Iranian Sans', Tahoma !important; 
}

md-sidenav {
  width: 225px;
  max-width: 70%;
}

  md-sidenav md-nav-list {
    display: block;
  }

    md-sidenav md-nav-list :hover {
      background-color: rgb(250, 250, 250);
    }



    md-sidenav md-nav-list .md-list-item {
      cursor: pointer;
    }

.side-navigation {
  padding-top: 0;
}

md-nav-list.side-navigation a[md-list-item] > .md-list-item > span.title {
  margin-right: 10px;
}

md-nav-list.side-navigation a[md-list-item] > .md-list-item {
  -webkit-font-smoothing: antialiased;
  letter-spacing: .14px;
}

md-list a[md-list-item] .md-list-item, md-list md-list-item .md-list-item, md-nav-list a[md-list-item] .md-list-item, md-nav-list md-list-item .md-list-item {
  display: flex;
  flex-direction: row;
  align-items: center;
  box-sizing: border-box;
  height: 48px;
  padding: 0 16px;
}

button.my-fab {
  position: absolute;
  right: 20px;
  bottom: 10px;
}

md-card {
  margin: 1em;
}

md-toolbar-row {
  justify-content: space-between;
}

.done {
  position: fixed;
  bottom: 20px;
  left: 20px;
  color: white;
}

md-nav-list.side-navigation a[md-list-item] {
  position: relative;
}

md-list a[md-list-item], md-list md-list-item, md-nav-list a[md-list-item], md-nav-list md-list-item {
  display: block;
}

md-list a[md-list-item], md-list md-list-item, md-nav-list a[md-list-item], md-nav-list md-list-item {
  color: #000;
}

md-nav-list a {
  text-decoration: none;
  color: inherit;
}

a {
  color: #039be5;
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
}

.no-padding {
  padding: 0 !important;
}

به همین راحتی برنامه نمونه با طراحی متریال آماده است. 

مطالب
استفاده از LocalDb در IIS، قسمت دوم: مالکیت وهله ها
در قسمت قبلی این مقاله گفتیم که دو خاصیت از LocalDb هنگام استفاده از Full IIS باعث بروز خطا می‌شوند:

  • LocalDb نیاز دارد که پروفایل کاربر بارگذاری شده باشد
  • بصورت پیش فرض وهله LocalDb متعلق به یک کاربر بوده، و خصوصی است

در قسمت قبل دیدیم چگونه باید پروفایل کاربر را بدرستی بارگذاری کنیم. در این مقاله به مالکیت وهله‌ها (instance ownership) می‌پردازیم.


مشکل وهله خصوصی

در پایان قسمت قبلی، اپلیکیشن وب را در این حالت رها کردیم:

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

System.Data.SqlClient.SqlException: Cannot open database "OldFashionedDB" requested by the login. The login failed.
Login failed for user 'IIS APPPOOL\ASP.NET v4.0'. 

این بار پیغام خطا واضح و روشن است. LocalDb با موفقیت اجرا شده و اپلیکیشن وب هم توانسته به آن وصل شود، اما این کانکشن سپس قطع شده چرا که دسترسی به وهله جاری وجود نداشته است. اکانت ApplicationPoolIdentity (در اینجا IIS APPPOOL\ASP.NET v4.0) نتوانسته به دیتابیس LocalDb وارد شود، چرا که دیتابیس مورد نظر در رشته اتصال اپلیکیشن (OldFashionedDB) وجود ندارد. عجیب است، چرا که وصل شدن به همین دیتابیس با رشته اتصال جاری در ویژوال استودیو با موفقیت انجام می‌شود.

همانطور که در تصویر بالا مشاهده می‌کنید از ابزار SQL Server Object Explorer استفاده شده است. این ابزار توسط SQL Server Data Tools معرفی شد و در نسخه‌های بعدی ویژوال استودیو هم وجود دارد و توسعه یافته است. چطور ممکن است ویژوال استودیو براحتی بتواند به دیتابیس وصل شود، اما اپلیکیشن وب ما با همان رشته اتصال نمی‌تواند دیتابیس را باز کند؟ در هر دو صورت رشته اتصال ما بدین شکل است:

Data Source=(localdb)\v11.0;Initial Catalog=OldFashionedDB;Integrated Security=True

پاسخ این است که در اینجا، دو وهله از LocalDb وجود دارد. بر خلاف وهله‌های SQL Server Express که بعنوان سرویس‌های ویندوزی اجرا می‌شوند، وهله‌های LocalDb بصورت پروسس‌های کاربری (user processes) اجرا می‌شوند. هنگامی که کاربران مختلفی سعی می‌کنند به LocalDb متصل شوند، برای هر کدام از آنها پروسس‌های مجزایی اجرا خواهد شد. هنگامی که در ویژوال استودیو به localdb)\v11.0) وصل می‌شویم، وهله ای از LocalDb ساخته شده و در حساب کاربری ویندوز جاری اجرا می‌شود. اما هنگامی که اپلیکیشن وب ما در IIS می‌خواهد به همین دیتابیس وصل شود، وهله دیگری ساخته شده و در ApplicationPoolIdentity اجرا می‌شود. گرچه ویژوال استودیو و اپلیکیشن ما هر دو از یک رشته اتصال استفاده می‌کنند، اما در عمل هر کدام به وهله‌های متفاوتی از LocalDb دسترسی پیدا خواهند کرد. پس مسلما دیتابیسی که توسط وهله ای در ویژوال استودیو ساخته شده است، برای اپلیکیشن وب ما در IIS در دسترس نخواهد بود.

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

به همین دلیل است که اپلیکیشن وب ما می‌تواند بدون هیچ مشکلی روی IIS Express اجرا شود و دیتابیس را باز کند. چرا که IIS Express درست مانند LocalDb یک پروسس کاربری است. IIS Express توسط ویژوال استودیو راه اندازی می‌شود و روی حساب کاربری جاری اجرا می‌گردد، پس پروسس آن با پروسس خود ویژوال استودیو یکسان خواهد بود و هر دو زیر یک اکانت کاربری اجرا خواهند شد.


راه حل ها

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


رویکرد 1: اجرای IIS روی کاربر جاری ویندوز

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

قسمت Advanced Settings را باز کنید:

روی دکمه سه نقطه کنار خاصیت Identity کلیک کنید تا پنجره Application Pool Identity باز شود:

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

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


رویکرد 2: استفاده از وهله مشترک

یک راه حال دیگر استفاده از قابلیت instance sharing است. این قابلیت به ما این امکان را می‌دهد تا یک وهله LocalDb را بین کاربران یک سیستم به اشتراک بگذاریم. وهله به اشتراک گذاشته شده، توسط یک نام عمومی (public name) قابل دسترسی خواهد بود.

ساده‌ترین راه برای به اشتراک گذاشتن وهله‌های LocalDb استفاده از ابزار SqlLocalDB.exe است. بدین منظور Command Prompt را بعنوان مدیر سیستم باز کنید و فرمان زیر را اجرا نمایید:

sqllocaldb share v11.0 IIS_DB
این فرمان وهله خصوصی LocalDb را با نام عمومی IIS_DB به اشتراک می‌گذارد. حال تمام کاربران سیستم می‌توانند با آدرس localdb)\.\IIS_DB) به این وهله وصل شوند. این فرمت آدرس دهی سرور دیتابیس، مشخص می‌کند که از یک وهله shared استفاده می‌کنیم. رشته اتصال جدید مانند لیست زیر خواهد بود:

Data Source=(localdb)\.\IIS_DB;Initial Catalog=OldFashionedDB;Integrated Security=True

پیش از آنکه اپلیکیشن وب ما بتواند به این وهله متصل شود، باید لاگین‌های مورد نیاز برای ApplicationPoolIdentity را ایجاد کنیم. راه اندازی وهله ساده است، کافی است دیتابیس را در SQL Server Object Explorer باز کنید. این کار اتصالی به دیتابیس برقرار می‌کند و آن را زنده نگاه می‌دارد. برای ایجاد لاگین مورد نظر، می‌توانیم در SQL Server Object Explorer یک کوئری اجرا کنیم:

create login [IIS APPPOOL\ASP.NET v4.0] from windows;
exec sp_addsrvrolemember N'IIS APPPOOL\ASP.NET v4.0', sysadmin

اسکریپت بالا به اکانت ApplicationPoolIdentity سطح دسترسی کامل می‌دهد. در صورت امکان بهتر است از سطوح دسترسی محدود‌تری استفاده کنید، مثلا دسترسی به دیتابیس یا جداولی مشخص. حالا می‌توانید اپلیکیشن را مجددا اجرا کنید و همه چیز بدون خطا باید کار کند.

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


رویکرد 3: استفاده از SQL Server Express

از آنجا که نسخه کامل SQL Server Express بعنوان یک سرویس ویندوزی اجرا می‌شود، شاید بهترین راه استفاده از همین روش باشد. کافی است یک نسخه از SQL Server Express را نصب کنیم، دیتابیس مورد نظر را در آن بسازیم و سپس به آن متصل شویم. برای این کار حتی می‌توانید از ابزار جدید SQL Server Data Tools استفاده کنید، چرا که با تمام نسخه‌های SQL Server سازگار است. در صورت استفاده از نسخه‌های کامل تر، رشته اتصال ما بدین شکل تغییر خواهد کرد:

Data Source=.\SQLEXPRESS;Initial Catalog=OldFashionedDB;Integrated Security=True
مسلما در این صورت نیز، لازم است اطمینان حاصل کنیم که ApplicationPoolIdentity به وهله SQL Server Express دسترسی کافی دارد. برای این کار می‌توانیم از اسکریپت قبلی استفاده کنیم:

create login [IIS APPPOOL\ASP.NET v4.0] from windows;
exec sp_addsrvrolemember N'IIS APPPOOL\ASP.NET v4.0', sysadmin

حال اجرای مجدد اپلیکیشن باید با موفقیت انجام شود. استفاده از این روش مسلما امکان استفاده از LocalDb را از ما می‌گیرد. ناگفته نماند که وهله‌های SQL Server Express همیشه در حال اجرا خواهند بود چرا که بصورت سرویس‌های ویندوزی اجرا می‌شوند. همچنین استفاده از این روش ممکن است شما را با مشکلاتی هم مواجه کند. مثلا خرابی رجیستری ویندوز می‌تواند SQL Server Express را از کار بیاندازد و مواردی از این دست. راهکار‌های دیگری هم وجود دارند که در این مقاله به آنها نپرداختیم. مثلا می‌توانید از AttachDbFilename استفاده کنید یا از اسکریپت‌های T-SQL برای استفاده از وهله خصوصی ASP.NET کمک بگیرید. اما این روش‌ها دردسر‌های زیادی دارند، بهمین دلیل از آنها صرفنظر کردیم.


مطالعه بیشتر درباره LocalDb