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

‫طراحی شیء گرا: OO Design Heuristics - قسمت سوم

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

Class Coupling and Cohesion

تعدادی از قواعد شهودی هم، با Coupling و Cohesion به ترتیب مابین و درون کلاس‌ها، سروکار دارند. تلاش ما در راستای افزایش Cohesion درون کلاس‌ها و سست کردن و کاهش Coupling مابین کلاس‌ها می‌باشد. این قواعد شهودی همین اهداف را در پارادایم action-oriented، در ارتباط با توابع دارند. هدف از Tight Cohesion (انسجام و چسبندگی قوی) در توابع، انسجام بالا و ارتباط نزدیک مابین کدهای موجود در تابع، می‌باشد. هدفی که Loose Coupling (اتصال سست و ضعیف، وابستگی ضعیف) در بین توابع دنبال می‌کند، اشاره دارد به اینکه اگر تابعی قصد استفاده از تابع دیگری را داشته باشد، باید وارد شدن و خروج از آن، از یک نقطه صورت گیرد. این مباحث منجربه مطرح شدن قواعد شهودی از جمله: «یک تابع باید طوری سازماندهی شود که تنها یک دستور return  داشته باشد»،  در پارادایم action-oriented می‌شود.
ما در پارادایم شیء گرا، اهداف خود از Loose Coupling و Tight Cohesion را در سطح کلاس مطرح می‌کنیم. 5 شکل اصلی Coupling مابین کلاس‌ها به شرح زیر می‌باشد:

  •  Ni Coupling
  •  Export Coupling
  •  Overt Coupling
  •  Covert Coupling
  •  Surreptitious Coupling
Nil Coupling

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

Export Coupling

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

Overt Coupling

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

Cover Coupling

این نوع اتصال هم به مانند Overt می‌باشد؛ با این تفاوت که هیچ اجازه دسترسی به کلاس Y داده نشده است. اگر زبانی داشته باشیم که به کلاس Y اجازه دهد خود را به عنوان دوست کلاس X معرفی کند، در این صورت نوع اتصال بین دو کلاس X و Y از نوع Covert می‌باشد. به عنوان مثال واقعی، می‌توان به استفاده از Reflection در دات نت اشاره کرد.

Surreptitious Coupling (اتصال پنهان)

آخرین نوع اتصال که بدترین حالت هم محسوب می‌شود، مربوط است به زمانیکه کلاس X به هر طریقی که شده از جزئیات داخلی کلاس Y آگاه باشد و از اعضای عمومی داده‌ای (public data member) آن کلاس استفاده کند. منظور این است که با تغییر این داده‌های کلاس متوجه میشود که بر روی عملیات b از کلاس چه تأثیری می‌گذارد و با اتکاء به این دستاورد، جزئیات داخلی خود را پیاده سازی می‌کند و یک اتصال پنهان را با کلاس Y ایجاد کرده است. در این حالت یک وابستگی قوی به صورت پنهان مابین رفتاری از کلاس Y و پیاده سازی کلاس X ایجاد شده است.

قاعده شهودی 2.7
اتصال و پیوستگی مابین کلاس‌ها باید از نوع Nil یا Export باشد؛ به این معنی که یک کلاس فقط از واسط عمومی کلاس دیگر استفاده کند یا کاری با آن نداشته باشد. (Classes should only exhibit nil or export coupling with other classes, that is, a class should only use operations in the public interface of another class or have nothing to do with that class.)
بجز این دو نوع اتصال، بقیه شکل‌های اتصال به طریقی اجازه دسترسی به جزئیات پیاده سازی کلاس‌ها را اعطا می‌کنند. در نتیجه باعث ایجاد وابستگی مابین پیاده سازی دو کلاس می‌شوند. این وابستگی ما بین پیاده سازی‌ها به محض نیاز به تغییر پیاده سازی یکی از کلاس‌ها ، باعث به وجود آمدن مشکلات نگهداری خواهند شد.
Cohesion درون کلاس‌ها سعی بر این دارد که مطمئن شود تمام اجزای یک کلاس به شدت باهم مرتبط هستند. تعدادی از قواعد شهودی نیز در ادامه بر این خصوصیت دلالت دارند.

قاعده شهودی 2.8
یک کلاس باید یک و تنها یک Key Abstraction را تسخیر نماید. (A class should capture one and only one key abstraction)
یک Key Abstractionبه عنوان یک Entity در Domain Model تعریف می‌شود و اغلب در غالب اسم در اسناد و مشخصات نیازمندی‌ها ظاهر می‌شوند. هر کدام از آنها باید فقط به یک کلاس نگاشت پیدا کنند. اگر این نگاشت به بیش از یک کلاس انجام گیرد، در نتیجه احتمالا طراح هر تابع را به عنوان یک کلاس تسخیر کرده است. اگر بیش از یک Key Abstraction به یک کلاس نگاشت پیدا کرده باشد، پس احتمالا طراح یک سیستم متمرکز را طراحی کرده است. این کلاس‌ها Vague Classesنامیده می‌شوند و باید آنها در دو کلاس یا بیشتر، تسخیر شوند.

قاعده شهودی 2.9
داده و رفتار مرتبط را در یک جا (کلاس) نگه دارید. (Keep related data and behavior in one place)
در واقع هدفی که این قاعده به دنبال آن می‌باشد این است که هر دو جزء تشکیل دهنده یک Key Abstraction ، یعنی همان داده و رفتار، باید توسط فقط یک کلاس تسخیر شوند.  با نقض این قاعده، توسعه دهنده باید با قرار داد خاصی (Convention) برنامه نویسی کند. 
راه شناسایی
طراح باید کلاس‌هایی که مرتبا داده‌های مورد نیاز  خود را با متدهای get از سایر کلاس‌ها دریافت می‌کند، شناسایی کنند. زیرا این نوع کلاس‌ها این قاعده شهودی را نقض کرده‌اند.

مثال واقعی
استفاده ازالگوی Domain Model ارائه شده توسط اقای Martin Fowler که دقیقا اشاره به این قاعده دارد.
یا برعکس آن ضد الگوی Anemic Domain Model که ناقض این قاعده می‌باشد. 
در قسمت اول اشاره کردیم این قواعد را به راحتی می‌توان در صورت نیاز نقض کرد. بعضی از مواقع نیاز به طراحی فیزیکی است که باعث تغییر در طراحی منطقی شده و چه بسا می‌تواند باعث نقض هر کدام از این قواعد شهودی نیز شود. اگر به بخش پروژه‌های سایت رجوع کنید این نقض کاملا مشهود (DomainClasses و ServiceLayer موجود در طراحی فیزیکی آنها) می‌باشد (بیشتر از Anemic Domain Model استفاده شده است)؛ ولی نمی‌توان گفت که این کار اشتباه است.

قاعده شهودی 2.10
اطلاعات نامرتبط به هم را در کلاس‌های جدا از هم قرار دهید. ((Spin off nonrelated information into another class (i.e., noncommunicating behavior) 

هدف از این قاعده این است که اگر کلاسی داریم که یکسری از متدهایش با بخشی از داده و یکسری دیگر با بخش دیگر داده‌ها کار میکنند، در واقع شما دو Key Abstraction را به یک کلاس نگاشت کرده اید (Vague Class) و باید آنها را به کلاس‌های جدا نگاشت کنید.

شکل 2.6 A class with noncommunicating behavior

مثال واقعی

به کلاس Dictionary در تصویر زیر توجه کنید.
برای تعداد کمی داده، بهترین پیاده سازی با استفاده از List و در مقابل برای تعداد داده زیاد بهترین پیاده سازی، استفاده از HashTable می‌باشد. هر یک از این پیاده سازی‌ها، به متدهایی برای add و find کلمات نیاز دارند. طراحی سمت چپ تصویر نشان از نقض این قاعده شهودی دارد.

شکل 2.7 (Noncommunicating behavior (real-world example

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


‫Vue.js - بحث Event Handling - آشنایی و شیوۀ استفاده از رویدادها - قسمت پنجم

$
0
0
در این قسمت به بحث رویدادها می‌پردازیم و اینکه به چه صورتی می‌توانیم از آن‌ها درون پروژه استفاده کنیم. فریم‌ورک Vue.js در عین سادگی می‌تواند نیاز شما را برآورده کرده و به نحو مطلوبی خروجی مناسبی را بدون هیچ دردسری، به شما تحویل دهد.
رویدادها یا همان eventها به ما این اختیار را می‌دهند که بتوانیم عمل خاصی را بر روی یک صفحه یا قسمت‌های مختلف درون پروژه اعمال کنیم و کاملا شبیه رویدادهایی است که در زبان‌های دیگر با آن کار کرده و آشنایی دارید.
برای شروع کار ابتدا نیاز است تا همانند قسمت‌های قبلی، کدهای صفحه مورد نظر خود را بنویسم و صفحه مورد نظر را ذخیره کنیم. کدها برای شروع کار، بدین شکل است که به صورت زیر نوشته و آماده شده است.
<html><head><meta charset="UTF-8"><title>dotnet</title></head><body id="dotnettips"><a v-on:click="message" href="bank.html">click here!</a> <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.27/vue.min.js"></script><script type="text/javascript">
     new Vue({
        el: '#dotnettips',
        data:{
        },
        methods:{
            message: function () {
                alert("hi friends");
            }
        }
    });</script>      </body></html>
در کدهای بالا قسمت body طبق روال همیشگی یک id را به خود اختصاص داده است و لازم است که بدانیم مشخصه‌ی id درون بدنه‌ی vue و مشخصه‌ی elبه کدام بخش تعلق گرفته است.
دقت بفرمائید که ما نیاز داریم تا یک function بنویسم و رویداد مورد نظر را اجرا کنیم که جهت آزمایش به دو صفحه html نیاز است و در بخش زیر از کدهای فوق قابل مشاهده است.

vue مورد نظر و متد برای اجرای رویداد بدین شکل نوشته شده است.
  new Vue({
        el: '#dotnettips',
        data:{

        },
        methods:{
            message: function () {
                alert("hi friends");
            }
        }
    });
حال باید رویداد هدایت شود که از تگ a استفاده شده است. به صورت زیر:
<html><head><meta charset="UTF-8"><title>dotnet</title></head><body id="dotnettips"><a v-on:click="message" href="bank.html">click here!</a>
لازم به اشاره است که جهت معرفی رویدادی به صفحه دیگر باید از دستور v-on که vuejs در اختیار ما قرار داده است، استفاده کرد و ما نیز چنین کدی را نوشته‌ایم و صفحه مورد نظر را ارجاع داده‌ایم.
  • حتما باید نام متد به رویداد کلیک معرفی شود که در کد فوق قابل مشاهده است.
نام صفحه مورد نظر برای ارجاع : bank.html
دقت شود، درون vue که نوشته شده‌است، یک متد نیز فراخوانی شده است تا رویداد، اجرا شود و ما نام آن متد را message در نظر گرفته‌ایم. 
دو صفحه به دلخواه ساخته شده‌است که نام صفحه اول با نام cc.html و صفحه دوم با نام bank.html ایجاد شده‌اند. زمان اجرای رویداد، به صفحه دوم و دریافت پیغام hi friends،توسط تابعی که نوشته‌ایم و نمایش آن بر روی صفحه مواجه خواهیم شد. بدین معنی است که رویداد به درستی اجرا شده است و سپس به صفحه دوم هدایت می‌شویم.
تصویری از خروجی ضمیمه شده است.

‫Angular CLI - قسمت ششم - استفاده از کتابخانه‌های ثالث

$
0
0
در قسمت قبلبا نحوه‌ی ساخت و توزیع برنامه‌های Angular، توسط Angular CLI آشنا شدیم. یکی از فایل‌هایی که توسط سیستم build آن تولید می‌شود، فایل vendor.bundle.js است که شامل کدهای اصلی Angular و همچنین کتابخانه‌های ثالث مورد استفاده‌است و با توجه به اینکه در حالت پیش فرض کار با Angular CLI قرار نیست فایل تنظیمات webpack آن‌را استخراج و ویرایش کنیم، چگونه باید سایر کتابخانه‌های ثالث مورد نیاز را به این سیستم build معرفی کرد؟


استفاده از کتابخانه‌های جاوا اسکریپتی ثالث

برای استفاده از کتابخانه‌های جاوا اسکریپتی ثالث، نیاز است آن‌ها را به فایل angular-cli.json. معرفی کنیم:
  "apps": [
    {
      "assets": [
        "assets",
        "favicon.ico"
      ],
      "styles": [
        "styles.css"
      ],
      "scripts": [],
در اینجا امکان معرفی فایل‌های اسکریپت و همچنین شیوه‌نامه‌های اضافی بیشتری (علاوه بر فایل src\styles.css پیش فرض پروژه) جهت معرفی آن‌ها به سیستم build برنامه موجود است.
به علاوه تعریف پوشه‌ی src\assets را نیز در اینجا مشاهده می‌کنید؛ به همراه فایل‌های اضافی دیگری مانند src\favicon.ico که ذیل آن ذکر شده‌است.


یک مثال: معرفی کتابخانه‌ی ng2-bootstrap به Angular CLI

دریافت و نصب بسته‌های مورد نیاز
مرحله‌ی اول کار با یک کتابخانه‌ی ثالث نوشته شده‌ی برای Angular مانند ngx-bootstrap، دریافت و نصب بسته‌ی npm آن می‌باشد. به همین جهت به ریشه‌ی پروژه وارد شده و دستورات ذیل را صادر کنید تا بوت استرپ و همچنین کامپوننت‌های +Angular 2.0 آن نصب شوند:
> npm install bootstrap --save> npm install ngx-bootstrap --save

پرچم save در اینجا سبب به روز رسانی خودکار فایل package.json می‌شود:
"dependencies": {
   "bootstrap": "^3.3.7",
   "ngx-bootstrap": "^1.6.6",

معرفی بسته‌های نصب شده به تنظیمات Angular CLI
پس از آن، همانطور که عنوان شد نیاز است به فایل angular-cli.json. مراجعه کرده و شیوه‌نامه‌ی بوت استرپ را تعریف کنیم:
  "apps": [
    {
      "styles": [
    "../node_modules/bootstrap/dist/css/bootstrap.min.css",
        "styles.css"
      ],

چون از ngx-bootstrap استفاده می‌کنیم، نیازی به مقدار دهی مستقیم []:"scripts" فایل angular-cli.json. نیست. ولی اگر خواستید اینکار را انجام دهید، روش آن به صورت ذیل است (که البته نیاز به نصب بسته‌ی jQuery را نیز خواهد داشت):
"scripts": [
   "../node_modules/jquery/dist/jquery.js",
   "../node_modules/bootstrap/dist/js/bootstrap.js"
],

بنابراین تا اینجا بسته‌های بوت استرپ و همچنین ngx-bootstarp نصب شدند و شیوه‌نامه‌ی بوت استرپ به فایل angular-cli.json اضافه گردید (نیازی هم به تکمیل قسمت scripts نیست).


استفاده از ماژول‌های مختلف بسته‌ی نصب شده در برنامه
در ادامه نیاز است تا ماژولی را از ngx-bootstarp را به قسمت imports فایل src\app\app.component.ts اضافه کرد. هرکدام از کامپوننت‌های این بسته به صورت یک ماژول مجزا تعریف شده‌اند. بنابراین برای استفاده‌ی از آن‌ها نیاز است برنامه را از وجودشان مطلع کرد. برای مثال روش استفاده‌ی از AlertModule آن به صورت ذیل است:
import { AlertModule } from 'ngx-bootstrap';

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

    AlertModule.forRoot()
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
در اینجا ابتدا AlertModuleاز ngx-bootstrap دریافت شده و سپس به قسمت imports فایل src\app\app.component.ts اضافه گردیده‌است.

آزمایش برنامه و اجرای آن
برای آزمایش مراحل فوق، فایل src/app/app.component.html را گشوده و به صورت ذیل تغییر دهید:
<h1>
  {{title}}</h1><button class="btn btn-primary">Hello!</button><alert type="success">Alert success!</alert>
در اینجا یک دکمه‌ی جدید با شیوه‌نامه‌های بوت استرپ اضافه شده‌اند (جهت بررسی عملکرد بوت استرپ) و همچنین یک Alert نیز از مجموعه کامپوننت‌های ngx-bootstrap به صفحه اضافه شده‌است.
اکنون اگر دستور ng serve -o را اجرا کنیم، خروجی ذیل حاصل خواهد شد:



مستندات و مثال‌های بیشتری را از ماژول‌های ngx-bootstarp، در اینجامی‌توانید بررسی کنید.

‫Angular CLI - قسمت هفتم - اجرای آزمون‌ها

$
0
0
پروژه‌های Angular CLI در حالت پیش فرض آن‌ها به همراه دو نوع آزمون واحد و آزمون end to end ایجاد می‌شوند. Angular CLI از Karma برای اجرای آزمون‌های واحد استفاده می‌کند و از Protractor برای اجرای آزمون‌های end to end. برای شروع می‌توان از راهنمای آن کمک گرفت:
> ng test --help
زمانیکه دستور ng test را اجرا می‌کنیم، به صورت خودکار تمام فایل‌های spec.ts.* را یافته و آزمون‌های واحد موجود در آن‌ها را اجرا می‌کند. این نوع فایل‌های ویژه نیز به صورت خودکار، زمانیکه اجزای مختلف Angular را توسط Angular CLI ایجاد می‌کنیم، تولید می‌شوند. به علاوه دستور ng test تغییرات این فایل‌ها را تحت نظر قرار داده و در صورت نیاز، آزمون‌های واحد را مجددا و به صورت خودکار اجرا می‌کند.


یک مثال: بررسی اجرای دستور ng test

یکی از مثال‌های بررسی شده‌ی در این سری را انتخاب و یا حتی یک برنامه‌ی جدید را توسط Angular CLI ایجاد کرده و سپس دستور ng test را در ریشه‌ی این پروژه اجرا کنید. به این ترتیب برنامه به صورت خودکار کامپایل شده و سپس به صورت خودکار آزمون‌های واحد آن‌را که در فایل‌های spec.ts‌.* قرار دارند، اجرا می‌کند. در آخر نتیجه را در مرورگر گزارش می‌دهد:


همانطور که مشخص است، 3 specs, 3 failures داریم. در اینجا می‌توان بر روی لینک Spec List کلیک کرد و لیست آزمون‌های واحد موجود را مشاهده نمود:


هر کدام از عناوین ذکر شده نیز به جزئیات مشکلات آن‌ها، لینک شده‌اند. برای مثال اگر بر روی اولین مورد کلیک کنیم، خطایی مانند «'alert' is not a known element» قابل مشاهده‌است. به این معنا که برای نمونه در قسمت قبلکامپوننت alert را به صفحه اضافه کردیم:
<alert type="success">Alert success!</alert>
اما اجرا کننده‌ی آزمون‌های واحد اطلاعاتی در مورد آن ندارد؛ از این جهت که آزمون‌های واحد به صورت ایزوله فقط همان کامپوننت خاص برنامه را آزمایش می‌کنند و کاری به وابستگی‌های آن ندارد. به همین جهت فایل src\app\app.component.spec.ts را گشوده و تغییرات ذیل را به آن اعمال کنید:
import { NO_ERRORS_SCHEMA } from '@angular/core';

describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        AppComponent
      ],
  schemas: [NO_ERRORS_SCHEMA]
    }).compileComponents();
  }));
در اینجا ابتدا ماژول NO_ERRORS_SCHEMA معرفی شده و سپس به قسمت schemas معرفی گشته‌است.
پس از این تغییر، بلافاصله مجدد برنامه کامپایل شده و آزمون‌های واحد آن با موفقیت اجرا می‌شوند (با این فرض که هنوز پنجره‌ی اجرا کننده‌ی دستور ng test را باز نگه داشته‌اید):


تغییر افزودن schemas: [NO_ERRORS_SCHEMA] را باید در مورد تمام فایل‌های spec موجود تکرار کرد.


گزینه‌های مختلف دستور ng test

دستور ng test به همراه گزینه‌های متعددی است که شرح آن‌ها را در جدول ذیل مشاهده می‌کنید:

گزینه
 مخفف توضیح
 code-coverage-- cc-  تولید گزارش code coverage که به صورت پیش فرض خاموش است. 
 colors--   به صورت پیش فرض فعال است و سبب نمایش رنگ‌های قرمز و سبز، برای آزمون‌های شکست خورده و یا موفق می‌شود. 
 single-run-- sr-  اجرای یکباره‌ی آزمون‌های واحد، بدون فعال سازی گزینه‌ی مشاهده‌ی مداوم تغییرات که به صورت پیش فرض خاموش است. 
 progress--   نمایش جزئیات کامپایل و اجرای آزمون‌های واحد که به صورت پیش فرض فعال است. 
 sourcemaps-- sm-  تولید فایل‌های سورس‌مپ که به صورت پیش فرض فعال است. 
 watch--
 w-  بررسی مداوم تغییرات فایل‌ها و اجرای آزمون‌های واحد به صورت خودکار که به صورت پیش فرض فعال است. 

بنابراین اجرا دستور ng test بدون ذکر هیچ گزینه‌ای به معنای اجرای مداوم آزمون‌های واحد، در صورت مشاهده‌ی تغییراتی در آن‌ها، به کمک Karma است.
همچنین دو دستور ذیل نیز به یک معنا هستند و هر دو سبب یکبار اجرای آزمون‌های واحد می‌شوند:
> ng test -sr> ng test -w false


اجرای بررسی میزان پوشش آزمون‌های واحد

یکی از گزینه‌های ng test روشن کردن پرچم code-coverage است:
> ng test --code-coverage
برای آزمایش آن دستور ذیل را در ریشه‌ی پروژه اجرا کنید (که سبب اجرای یکبار برررسی میزان پوشش آزمون‌های واحد می‌شود):
> ng test -sr -cc
پس از اجرای این آزمون ویژه، پوشه‌ی جدیدی به نام coverage در ریشه‌ی پروژه‌ی جاری تشکیل می‌شود. فایل index.html آن‌را در مرورگر باز کنید تا بتوان گزارش تولید شده را مشاهده کرد:


کار این آزمون بررسی قسمت‌های مختلف برنامه و ارائه گزارشی است که مشخص می‌کند آیا آزمون‌های واحد نوشته شده تمام انشعابات برنامه را پوشش می‌دهند یا خیر؟ برای مثال اگر در متدی if/else دارید، آیا فقط قسمت if را پوشش داده‌اید و یا آیا قسمت else هم در آزمون‌های واحد، بررسی شده‌است.


اجرای آزمون‌های end to end

هدف از ساخت یک برنامه ... استفاده‌ی از آن توسط دیگران است؛ اینجا است که آزمون‌های end to end مفهوم پیدا می‌کنند. در آزمون‌های e2e رفتار برنامه همانند حالتی که یک کاربر از آن استفاده می‌کند، بررسی می‌شود (برای مثال باز کردن مرورگر، لاگین و مرور صفحات). برای این منظور، Angular CLI  در پشت صحنه از Protractor برای این نوع آزمون‌ها استفاده می‌کند.  
برای مشاهده‌ی راهنما و گزینه‌های مختلف مرتبط با آزمون‌های e2e، می‌توان دستور ذیل را صادر کرد:
>ng e2e --help
البته با توجه به اینکه این دستور کار توزیع برنامه را نیز انجام می‌دهد، تمام گزینه‌های ng serve نیز در اینجا صادق هستند، به علاوه‌ی موارد ذیل:

 گزینه مخففتوضیح
 config-- c-  به فایل کانفیگ آزمون‌های e2e اشاره می‌کند که به صورت پیش‌فرض همان protractor.conf.js واقع در ریشه‌ی پروژه‌است. 
 element-explorer-- ee-  بررسی و دیباگ protractor از طریق خط فرمان 
 serve-- s-  کامپایل و توزیع برنامه بر روی پورتی اتفاقی (حالت پیش فرض آن true است) 
 specs-- sp-  پیش فرض آن بررسی تمام specهای موجود در پروژ‌ه‌است. اگر نیاز به لغو آن باشد می‌توان از این گزینه استفاده کرد. 
 webdriver-update-- wu- به روز رسانی web driver که به صورت پیش فرض فعال است. 

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

از این جهت که این نوع آزمون‌ها، وابسته‌ی به جزئی خاص از برنامه نیستند، حالت عمومی داشته و فایل‌های spec آن‌ها را می‌توان در پوشه‌ی e2e واقع در ریشه‌ی پروژه، یافت. برای مثال در قسمتی از آن کار یافتن متن نمایش داده شده‌ی در صفحه‌ی اول سایت انجام می‌شود
getParagraphText() {
    return element(by.css('app-root h1')).getText();
}
و سپس در فایل spec آن بررسی می‌کند که آیا مساوی app works هست یا خیر؟
 expect(page.getParagraphText()).toEqual('app works!');

برای آزمایش آن دستور ng e2e را در ریشه‌ی پروژه صادر کنید. همچنین دقت داشته باشید که در این حالت نیاز است به اینترنت نیز متصل باشد؛ چون از chromedriver api گوگل نیز استفاده می‌کند. در غیراینصورت خطای ذیل را دریافت خواهید کرد:
 Error: getaddrinfo ENOTFOUND chromedriver.storage.googleapis.com chromedriver.storage.googleapis.com:443

‫مقدمه ای بر CQRS و Event Sourcing

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

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

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

ذات CQRS بر آن است که شما مدل‌های مختلفی را برای خواندن و نوشتن دیتا داشته باشید. الگوی آن چیزی شبیه به تصویر زیر است

چیزی که در این روش مشهود است این میباشد که برنامه نویسان باید قسمت‌های Command و Query را به صورت جداگانه طراحی نمایند.
CQRS این قابلیت را به شما میدهد که interface و Datastore و حتی بطور کامل Technology مجزایی در قسمت‌های CQ داشته باشید.

Event Sourcing
قسمت دوم و متمایز، معماری Event Sourcing یا ES میباشد که بصورت کوتاه، ES یک روش متفاوت برای Data storage میباشد.
اکثرا ما از Datastore‌هایی که مدلی از دیتا را انعکاس می‌دهند استفاده میکنیم. به مثال ساده‌ی زیر توجه کنید

ES از برنامه نویسان میخواهد که مدل سنتی CRUD را فراموش کرده و بجای آن تغییراتی را که روی دیتا صورت گرفته، نیز درج نمایند. اینکار به وسیله‌ی یک دیتابیس Append-only انجام میشود که به نام Event Store شناخته میشود.

در این معماری ما همه‌ی تغییرات روی دیتا را به صورت Serialize Event ذخیره میکنیم که میتواند دوباره در هر زمانی اجرا شده و current state هر objectی را در اختیار بگذارد.

این روش به ما کمک بزرگی میکند تا وضعیت یک object را در گذشته به راحتی پیدا کنیم و از آن میتوان به غیر از فوایدی که دارد، به عنوان یک Logger نیز استفاده نمود. به دلیل اینکه جزء به جزء تغییرات بر روی state سیستم، در آن ثبت شده است. از آنجاییکه دیتا بصورت serialize ذخیره میشود، بارگزاری آن نیز با سرعت بالایی انجام خواهد شد.

پس بصورت خلاصه در معماری Command Query Responsibility Segregation ابتدا باید به این موضوع توجه داشت که قسمت‌های Read و Write نرم افزار به صورت مجزایی طراحی میشوند و Event Sourcing شامل تغییراتی که روی data انجام شده است، میباشد و به‌صورت Serialize شده ذخیره میشود. ما تنها به یک دیتابیس و یک جدول برای نمایش event store نیازمندیم (بستگی به نیازتان میتوان تعداد آن را نیز بیشتر نمود و همچنین حتما لزومی ندارد که از دیتابیس‌های رابطه‌ای استفاده شود؛ بصورت مثال پیاده سازی این قسمت را میتوان با استفاده از Redisکه دیتابیسی غیر رابطه‌ای و باسرعت میباشد استفاده نمود).
برای شروع کار (نه پیاده سازی کامل) باید با قسمت‌های مختلف طراحی در این معماری آشنا شویم:
Domain Object
نکته: SimpleCqrsفریم ورکی برای پیاده سازی معماری CQRS , ES میباشد که برای ساده‌تر شدن کار، از آن استفاده شده است (شما حتی میتوانید پیاده سازی خود را داشته باشید)
مدل Movie از کلاسی به نام AggregateRoot ارث بری کرده‌است که توسط SimpleCQRS پیاده سازی شده‌است و یک guid key در آن تعبیه شده است (Aggregate root از مباحث Domain Driven برگرفته شده است و آشنایی با آن کمک شایانی به درک عمیق‌تر روی این مباحث مینماید).
public class Movie : AggregateRoot  
{
    public string Title { get; set; }
    public DateTime ReleaseDate { get; set; }
    public int RunningTimeMinutes { get; set; }

    public Movie() { }

    public Movie(Guid movieId, string title, DateTime releaseDate, int runningTimeMinutes)
    {
        //پیاده سازی خواهد شد
    }
}
توجه: SimpleCQRS فقط پیاده سازی guid برای کلید مربوط به هر مدل را پیاده سازی نموده است؛ بنابراین کلید مدل نمیتواند integer باشد.

Commands
command دستوراتی است که توسط end user فراخوانی میشود که باعث تغییرات خواهد شد. وقتی اپلیکیشن یک command را دریافت مینماید، command handler به پردازش آن برای فهمیدن خواسته کاربر میپردازد و پس از آن event مربوطه را برای اجرای آن وظیفه‌ی خاص صدا میزند.
همه‌ی command‌ها تغییراتی بر روی state جاری خواهند داشت. در نتیجه دیتا‌های ذخیره شده درون دیتابیس تغییرات خواهند کرد. هر commandی که تغییری بر روی State سیستم نداشته باشد، یک دستور غلط محسوب شده و باید در سمت query‌ها آن را پیاده سازی نمود.
در نتیجه Commnad‌ها دستوراتی هستند که از طرف کاربر برای تغییرات بر روی دیتا‌های ذخیره شده، ارسال میشوند.
فرض کنید Domain Objectی برای Movie تعریف کرده‌ایم و میخواهیم دستور اضافه کردن فیلم را پیاده سازی نماییم
public class CreateMovieCommand : ICommand  
{
    public string Title { get; set; }
    public DateTime ReleaseDate { get; set; }
    public int RunningTimeMinutes { get; set; }
    public CreateMovieCommand(string title, DateTime releaseDate, int runningTime)
    {
        Title = title;
        ReleaseDate = releaseDate;
        RunningTimeMinutes = runningTime;
    }
}
توجه: ICommand از طریق SimpleCQRS اضافه شده‌است.

Command Handler
بعد از اینکه Command مورد نیاز نوشته شد، حال احتیاج به پیاده سازی CommandHandler مربوطه که دستور متناظر را پردازش میکند، داریم.
public class CreateMovieCommandHandler : CommandHandler<CreateMovieCommand>  
{
    protected IDomainRepository _repository;

    public CreateMovieCommandHandler(IDomainRepository repository)
    {
        _repository = repository;
    }

    public override void Handle(CreateMovieCommand command)  
    {
        var movie = new Domain.Movie(Guid.NewGuid(), command.Title, 
    command.ReleaseDate, command.RunningTimeMinutes);

        _repository.Save(movie);
    }
}
Command Handler باید از کلاس جنریک <CommandHandler<T ارث بری نماید و T باید از نوع Command در نظر گرفته شود و همچنین IDomainRepository اینترفیسی است که توسط SimpleCQRS تعریف شده‌است و ما احتیاجی به پیاده سازی آن نداریم (در قسمت‌های بعدی پیکربندی آن را انجام میدهیم).
برای رسیدگی کردن به دستور مربوطه احتیاج به override کردن متد Handle میباشد.
کار اساسی توسط متد Save انجام میشود که همه‌ی event‌های pending شده توسط Domain Object را گرفته و آنها را به Event Store میفرستد.

Events
event‌ها تغییراتی هستند بر روی State جاری سیستم که توسط کاربر به وسیله‌ی Commandها فراخوانی میشوند.
رویداد‌ها serialize میشوند و درون Event Store ذخیره میشوند؛ بنابراین میتوان فراخوانی آنها را در هر لحظه انجام داد.
هر تعداد Event میتواند توسط یک دستور raise شود.
ساخت یک Event:
قبلا دستوری را برای ساخت یک movie نوشتیم و حال احتیاج به event مربوطه را داریم:
public class MovieCreatedEvent : DomainEvent  
{
    public Guid MovieId
    {
        get { return AggregateRootId; }
        set { AggregateRootId = value;}
    }

    public string Title { get; set; }
    public DateTime ReleaseDate { get; set; }
    public int RunningTimeMinutes { get; set; }

    public MovieCreatedEvent(Guid movieId, string title, DateTime releaseDate, int runningTime)
    {
        MovieId = movieId;
        Title = title;
        ReleaseDate = releaseDate;
        RunningTimeMinutes = runningTime;
    }
}
فراموش نکنید که این کلاس آبجکتی خواهد بود که Serialize شده و در دیتابیس ذخیره خواهد شد. باید همه‌ی پراپرتی‌های لازم که با استفاده از این Event ممکن است تغییر کنند را شامل شود (بدیهی است که این پراپرتی‌ها از Domain Object گرفته میشود).
public class Movie : AggregateRoot  
{
    public string Title { get; set; }
    public DateTime ReleaseDate { get; set; }
    public int RunningTimeMinutes { get; set; }

    public Movie(Guid movieId, string title, DateTime releaseDate, int runningTimeMinutes)
    {
        Apply(new MovieCreatedEvent(Guid.NewGuid(), title, releaseDate, runningTimeMinutes));
    }
}
به Aggregate فوق که در اوایل بحث صحبت شده‌است دقت کنید. حال متد Apply باعث میشود که event مربوطه درون بخش لوکال aggregate root ذخیره شود. بنابراین بعدا میتواند به صورت فیزیکی درون Event Store ذخیره شود.

Event Handler
هر Event Handler  میتواند تعداد زیادی از IHandleDomainEvents ‌ها را پیاده سازی نماید. حال متد Handle این اینترفیس را پیاده سازی نمودیم. 
public class MovieEventHandler : IHandleDomainEvents<MovieCreatedEvent>  
{
    public void Handle(MovieCreatedEvent createdEvent)
    {
        using (MoviesContext entities = new MoviesContext())
        {
            entities.Movies.Add(new Movie()
            {
                Id = createdEvent.AggregateRootId,
                Title = createdEvent.Title,
                ReleaseDate = createdEvent.ReleaseDate,
                RunningTimeMinutes = createdEvent.RunningTimeMinutes
            });

            entities.SaveChanges();
        }
    }
}
مثلا در این قسمت با استفاده از ORM، شیء مورد نظر به صورت فیزیکی درون دیتابیس ذخیره میشود.
در قسمت آخر نیازمندیم که تغییرات زیر را به Movie اضافه نماییم.
درون Doamin Objectی که قبلا تعریف کرده بودیم متدی را به صورت زیر پیاده سازی مینماییم
protected void OnMovieCreated(MovieCreatedEvent domainEvent)
        {
            Id = domainEvent.AggregateRootId;
            Title = domainEvent.Title;
            ReleaseDate = domainEvent.ReleaseDate;
            RunningTimeMinutes = domainEvent.RunningTimeMinutes;
        }
باعث میشود پس از فراخوانی شدن Event، تغییرات صورت گرفته‌ی بر state سیستم، بر روی Domain Object اعمال شود و آن را بروزرسانی نماید. این متد دقیقا بصورت اتوماتیک وقتی که event مربوطه raise میشود، فراخوانی میشود.
پس از ترکیب CQRS و ES معماری اولیه‌ی سیستم چیزی شبیه به دیاگرام زیر خواهد بود (بسته به سناریوهای خاص میتواند سفارشی سازی شود)

خلاصه:
کاربر دستوری را از طریق برنامه به سیستم ارسال مینماید.
command مربوطه دریافت میشود و به روی Command Bus قرار داده میشود.
Command Handler وظیفه‌ی تفسیر کردن Command مربوطه را به عهده میگیرد و به وسیله‌ی Domain object آن event مورد نظر فراخوانی خواهد شد و باعث میشود domain object بروزرسانی گردد.
Event همان objectی است که باید به صورت serialize شده درون append only database ذخیره شود.
Event handler رویداد مربوطه را گرفته و بصورت فیزیکی مقادیر مورد نظر را در دیتابیس ذخیره مینماید.

Query
از آنجاییکه قسمت Read، در سیستم به صورت CQRS طراحی میشود، به راحتی میتوان query‌ها را optimize کرده و به صورت مثال به جای استفاده از ORM‌های معمول بطور مستقیم Stored Procedure فراخوانی کرده، تا جای ممکن کیفیت query‌ها بهترین حالت ممکن باشند. در حالیکه در مدل CRUD بهینه کردن بخش read بسیار پیچیده و بعضا غیر ممکن میباشد.

مزایای استفاده از این مدل
Distributed Systems Capabilities
یکی از مهمترین مزیت‌های این مدل تسهیم گسترش پذیری سیستم بر روی ماشین‌های فیزیکی مختلف از طریق messaging pattern میباشد.
High Availability
از آنجایی که سیستم توزیع پذیر طراحی شده‌است، هر قسمت از آن میتواند بدون توجه به fail شدن قسمت‌های دیگر به کار خود ادامه دهد.
Reduce Complexity
در domain‌های پیچیده طراحی و پیاده سازی objectهایی که مسئول دو قسمت read و write هستند، میتواند کار را بیش از حد پیچیده کرده و در این صورت چون business logic و read logic در هم ترکیب میشوند، مدیریت کردن موارد multiple user, shared data, performance, transactions, consistency سخت و سخت‌تر میشود.
Facilitates Building Task-based UI
وقتی شما به پیاده سازی الگوی CQRS میپردازید، اصولا هر عملی که توسط End user از طریق ui ارسال میشود، معادل command مربوط به آن وجود دارد. به همین جهت میتوان عملیات لازم برای اجرای یک پروسه را بصورت واضحی درک کرد.
 Maintenance And Flexibility
هر چند پیاده سازی این مدل سخت خواهد بود، اما در ابعاد وسیع‌تر به دلیل اینکه هر قسمت به صورت مجزایی طراحی شده و اینکه دستورات و رویداد‌ها به صورت تفکیک شده پیاده سازی شده‌اند، همچنین وجود ES، قابلیت زیادی به debug سیستم می‌دهد.
نکته: ES مدل مورد قبولی برای اکثر معماری‌های نوین سیستم‌های نرم افزاری امروزی میباشد و فقط مختص به CQRS نمیباشد. بطور مثال در معماری Microservices به وفور از Event Sourcing استفاده میشود.

مشکلات استفاده از این مدل
  • ذاتا پیاده سازی این مدل سخت و دشوار است و از آنجاییکه سادگی در پیاده سازی سیستم‌های نرم افزاری، یک اصل مهم محسوب میشود، بنابراین استفاده از این مدل محدود میشود به سیستم‌های نرم افزاری که مزیت‌های گفته شده در قسمت فوق برایشان حیاتی محسوب شود.
  • برای پیاده سازی سیستمی با این مدل احتیاج به تیم توسعه‌ای است که با مفاهیم آن کاملا آشنا باشد.
  • هر چند امروزه فضای فیزیکی برای ذخیره سازی دیتا ارزان محسوب میشود، اما به هر حال استفاده از این مدل به همراه ES، حجم زیادی از Disk space را خواهد گرفت.
  • همانطور که دیدید برای پیاده سازی یک Insert ساده، حجم زیادی کد نوشته شده‌است. بنابراین تولید اینگونه نرم افزار‌ها به زمان بیشتری نیاز دارد.
بنابراین باید در انتخاب معماری سیستم بسیار دقت شود؛ هر چند که این مدل برای سیستم‌های بزرگ و پیچیده خیلی کارآمد محسوب میشود و باعث یک Domain object غنی ، History Tracking، شفافیت در مشکلات Concurrency و همچنین Scalability و غیره خواهد شد، اما پیدا کردن برنامه نویسانی با داشتن درک عمیق روی این مباحث کمی سخت به نظر میرسد.
در قسمت بعدی بصورت کامل به پیاده سازی این الگو در یک اپلیکشن دات نتی خواهیم پرداخت.

‫طراحی شیء گرا: OO Design Heuristics - قسمت چهارم

$
0
0

Dynamic Semantics

Objectها علاوه بر داده و رفتار به عنوان توصیفات ثابت، در زمان اجرا دارای یک Local State (‏‏a snapshot) از مقادیر داینامیک مربوط به اعضای داده‌ای خود، می‌باشند. مجموعه تمام حالاتی که وهله‌های یک کلاس می‌توانند بین آنها گذر (transition) داشته باشد، dynamic semantics مربوط به کلاس نامیده می‌شود و به وهله‌های کلاس این امکان را می‌دهند تا به یک پیغام مشابه رسیده و در زمان‌های مختلف از چرخه زندگی خود، به اشکال مختلف پاسخ دهند.

Method junk for the class X 
if (local state #1) then
do something
else if (local state #2) then
do something different
End Method

بخش اصلی هر طراحی شیء گرا، dynamic semantics وهله‌ها می‌باشد. dynamic semantics هر کلاسی باید در قالب یک دیاگرام state-transition مستند شود. شکل زیر dynamic semantics پروسه‌های موجود در یک سیستم عامل را در قابل یک دیاگرام حالت نمایش می‌دهد. این پروسه‌ها توانایی این را دارند که در هر کدام از حالات: runnable، current process، blocked، sleeping و یا در حالت exited، قرار داشته باشند. همچنین به عنوان مثال، یک پروسه زمانی می‌تواند در حالت current process قرار گیرد که حتما قبلا در حالت runnable قرار داشته باشد. این اطلاعات برای ایجاد تست برای کلاس‌ها و وهله‌های آنها می‌تواند مفید واقع شود.

شکل 2.8 State-transition diagram notation 

برخی از طراحان به طور تصادفی، dynamic semantics یک کلاس را به عنوان static semantics آن کلاس مدل می‌کنند. به عنوان مثال اگر color یکی از اعضای داده ای (data member) کلاس توپ باشد و بعد از وهله سازی از کلاس توپ، color آن بازهم قابل تغییر باشد، منظور اینکه توپ آبی به عنوان یک وهله از کلاس توپ در زمان حیات خود تغییر رنگ دهد، اصطلاحا می‌گویند: color جزء dynamic semantics کلاس توپ می‌باشد. با توجه به توضحیاتی که داده شد، حال اگر طراحی برای هر رنگ توپ یک کلاس جدا در نظر گرفته باشد، dynamic semantics را به عنوان static semantics مدل کرده و به احتمال زیاد ما را به سمت ایجاد مشکل Class Proliferation (ازدیاد کلاس ها) سوق خواهد داد.

Abstract Classes

به سوالات زیر توجه کنید:

  • آیا هرگز میوه خورده‌اید؟
  • آیا هرگز پیش غذا خورده‌اید؟ 
  • آیا هرگز دسر خورده‌اید؟ 
    اکثر مردم به این سوالات جواب «بله» را خواهند داد.
    حال با توجه به سوالات «مزه غذا چطور بود؟ دسری که خوردید، چه تعداد کالری داشت؟ هزینه پیش غذایی که خوردید چقدر بود» پاسخ چه خواهد بود؟
    من (نویسنده) ادعا میکنم که هیچ کسی تا به حال میوه نخورده است. بیشتر مردم، سیب، موز و پرتقال خورده‌اند؛ میوه‌ی قرمز رنگی به ارزش 3 پوند را نخورده‌اند.

    شبیه به این مسئله برای زمانی است که گارسون رستوران از شما سوال می‌کند: «برای شام چه چیزی میل دارید» و شما جواب می‌دهید: «یک پیش غذا، یک غذای اصلی و یک دسر». در این حالت چون شما دقیقا مشخص نکرده‌اید چه نوعی می‌خواهید، گارسون، مات و مبهوت خواهد ماند. همه می‌دانیم که چیزی تحت عنوان میوه، پیش غذا و یا وهله دسر در واقعیت وجود ندارد؛ بله این عبارات اطلاعات مفیدی را تسخیر می‌کنند. اگر من در دستم یک ساعت زنگی گرفته و از شما می‌پرسیدم: «نظرتان در مورد میوه من چیست؟»؛ بدون شک فکر می‌کردید من دیوانه شده‌ام. حال اگر در دستم سیبی گرفته و سوال قبلی را می‌پرسیدم، این بار از نظر شما من یک شخص عاقل بودم.
    با وجود اینکه نمی‌توان از میوه وهله سازی کرد، اما اطلاعات مفیدی را تسخیر می‌کند. در واقع میوه، یک کلاسی (concept) است که دانشی از نحوه وهله سازی وهله هایش به وسیله Type پیاده ساز خود، ندارد.

    کلاسی که دانشی از نحوه وهله سازی وهله‌های خود ندارد، abstract class (کلاس مجرد یا انتزاعی) نامیده می‌شود.
    کلاسی که دانش نحوه وهله سازی وهله‌های خود دارد، concrete classنامیده می‌شود.

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

    Roles Versus Classes

    قاعده شهودی 2.11
    مطمئن باشید انتزاع هایی را که مدل می‌کنید کلاس بوده و نه نقش‌هایی که وهله‌های آنها بازی می‌کنند. (Be sure the abstractions that you model are classes and not simply the roles objects play)
    آیا مادر و پدر به عنوان یک کلاس هستند یا نقش‌هایی هستند که وهله‌های کلاس شخص، بازی می‌کند؟ پاسخ این سوال وابسته به دامینی (domain) است که طراح در حال مدل سازی آن می‌باشد. اگر در دامین مورد نظر، مادر و پدر رفتارهای مختلفی دارند، احتمالا باید به عنوان کلاس‌های جدا مدل شوند. اگر رفتارهای یکسانی دارند، در نتیجه نقش‌های مختلفی هستند که وهله‌های کلاس شخص بازی می‌کنند. به عنوان مثال، می‌توان کلاس خانواده را متشکل از وهله‌ای از کلاس پدر، وهله‌ای از کلاس مادر و مجموعه‌ای از وهله‌های کلاس فرزند در نظر گرفت. در مقابل ممکن است کلاس خانواده را متشکل از وهله‌ای از کلاس شخص به عنوان پدر، وهله‌ای از کلاس شخص به عنوان مادر و آرایه‌ای از وهله‌های شخص به عنوان فرزندان، مدل کنید. قرار گرفتن در وضیعتی که هر نقش، بخشی از رفتاری‌های شخص را مورد استفاده قرار می‌دهد، کافی نیست و باید مطمئن شوید که رفتار‌ها واقعا متفاوت می‌باشند. همچنین باید به یاد داشته باشید که زمانیکه وهله‌ای از بخشی از رفتارهای کلاس خود استفاده می‌کند، نیز مشکلی وجود ندارد و لازم نیست کلاس‌های دیگری را به خاطر این موضوع در طراحی خود در نظر بگیرید.

    شکل 2.9 Two views of a family   

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

    قواعد شهودی فصل دوم

    قاعده شهودی 2.1 
    همه داده‌ها باید در داخل کلاس خود پنهان شده باشند. (All data should be hidden within its class) 
    قاعده شهودی 2.2
    استفاده کنندگان از کلاس باید به واسط عمومی آن وابسته باشند، اما یک کلاس نباید به استفاده کنندگان خود، وابسته باشد. (Users of a class must be dependent on its public interface, but a class should not be dependent on its users)
    قاعده شهودی 2.3
    تعداد پیغام‌های موجود در قرارداد یک کلاس را کمینه سازید. (Minimize the number of messages in the protocol of a class) 
    قاعده شهودی 2.4
    پیاده سازی یک واسط عمومی یکسان کمینه برای همه کلاس‌ها  (Implement a minimal public interface that all classes understand [e.g., operations such as copy (deep versus shallow), equality testing, pretty printing, parsing from an ASCII description, etc.].) 
    قاعده شهودی 2.5 
    جزئیات پیاده سازی، مانند توابع خصوصی common-code  ( توابعی که کد مشترک سایر متدهای کلاس را در بدنه خود دارند) را در واسط عمومی یک کلاس قرار ندهید.  (Do not put implementation details such as common-code private functions into the public interface of a class)
    قاعده شهودی 2.6 
    واسط عمومی کلاس را با اقلامی که یا استفاده کنندگان از کلاس توانایی استفاده از آن را نداشته و یا تمایلی به استفاده از آنها ندارند، آمیخته نکنید.  (Do not clutter the public interface of a class with items that users of that class are not able to use or are not interested in using )
    قاعده شهودی 2.7
    اتصال و پیوستگی مابین کلاس‌ها باید از نوع Nil یا Export باشد؛ به این معنی که یک کلاس فقط از واسط عمومی کلاس دیگر استفاده کند یا کاری با آن نداشته باشد. (Classes should only exhibit nil or export coupling with other classes, that is, a class should only use operations in the public interface of another class or have nothing to do with that class.)
    قاعده شهودی 2.8 
    یک کلاس باید یک و تنها یک Key Abstraction را تسخیر نماید. (A class should capture one and only one key abstraction) 
    قاعده شهودی 2.9 
    داده و رفتار مرتبط را در یک جا (کلاس) نگه دارید. (Keep related data and behavior in one place)
    قاعده شهودی 2.10 
    اطلاعات نامرتبط به هم را در کلاس‌های جدا از هم قرار دهید. ((Spin off nonrelated information into another class (i.e., noncommunicating behavior)
    قاعده شهودی 2.11
    مطمئن باشید انتزاع هایی را که مدل می‌کنید کلاس بوده و نه نقش‌هایی که وهله‌های آنها بازی می‌کنند. (Be sure the abstractions that you model are classes and not simply the roles objects play) 

    ‫ویژگی های کمتر استفاده شده در NET. - بخش پنجم

    $
    0
    0

    Nullable<T>.GetValueOrDefault Method

    با استفاده از متد GetValueOrDefault مقدار فعلی یک شیء Nullable و یا مقدار پیش فرض آن را می‌توان بدست آورد. این متد از عملگر ?? سریع‌تر است.
    float? yourSingle = -1.0f;
    Console.WriteLine( yourSingle.GetValueOrDefault() );
    
    yourSingle = null;
    Console.WriteLine( yourSingle.GetValueOrDefault() );
    
    // assign different default value
    Console.WriteLine( yourSingle.GetValueOrDefault( -2.4f ) );
    
    // returns the same result as the above statement
    Console.WriteLine( yourSingle ?? -2.4f );

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

    شما می‌توانید برای دیکشنری نیز یک متد Get امن ایجاد کنید (در صورت عدم وجود کلید، بجای پرتاب استثناء، مقدار پیش فرض بازگشت داده شود).

    public static class DictionaryExtensions
    {
        public static TValue GetValueOrDefault< TKey, TValue >( this Dictionary< TKey, TValue > dic,
                                                                TKey key )
        {
            TValue result;
            return dic.TryGetValue( key,
                                    out result )
                ? result
                : default(TValue);
        }
    }

    و روش استفاده

    var names = new Dictionary< int, string >
                {
                    { 0, "Vahid" }
                };
    Console.WriteLine( names.GetValueOrDefault( 1 ) );


    ZipFile in .NET

    با استفاده از کلاس ZipFile ( رفرنس به اسمبلی System.IO.Compression.FileSystem ) می‌توان عملیات بازکردن، ایجاد و استخراج فایل‌های Zip را انجام داد.
    var startPath = Path.Combine( Environment.GetFolderPath( Environment.SpecialFolder.Desktop ), "Start" );
    var resultPath = Path.Combine( Environment.GetFolderPath( Environment.SpecialFolder.Desktop ), "Result" );
    var extractPath = Path.Combine( Environment.GetFolderPath( Environment.SpecialFolder.Desktop ), "Extract" );
    Directory.CreateDirectory( startPath );
    Directory.CreateDirectory( resultPath );
    Directory.CreateDirectory( extractPath );
    
    var zipPath = Path.Combine( resultPath, Guid.NewGuid() + ".zip" );
    ZipFile.CreateFromDirectory( startPath, zipPath );
    ZipFile.ExtractToDirectory( zipPath, extractPath );

    C# Preprocessor Directives

    با استفاده از warning# می توان یک هشدار را در یک قسمت خاص از کد تولید کرد.
    #if DEBUG
    #warning DEBUG is defined
    #endif
    و خروجی آن

    با استفاده از error# می توان یک خطا را در یک جای خاصی از کد تولید کرد.
    #if DEBUG
    #error DEBUG is defined
    #endif
    در صورتی که کد بالا را اجرا کنید (در حال دیباگ) کامپایلر با نمایش DEBUG is definedدر پنجره Error List، جلوی اجرای برنامه را می‌گیرد. اما در حالت ریلیز، برنامه بدون هیچ مشکلی اجرا می‌شود.

    با استفاده از line# می توانید شماره خط کامپایلر و نام فایل خروجی (اختیاری) را برای خطاها و هشدارها تغییر دهید.

    در مثال زیر، در صورتیکه در خط اول break point قرار دهید و با کلید F10 برنامه را اجرا کنید، مشاهده می‌کنید که دیباگر، خطی را که بعد از دستور line hidden# نوشته شده است، در نظر نمی‌گیرد (برای دیباگ) اما اجرا می‌شود و دیباگر بر روی دستور بعد از line default# قرار می‌گیرد.

        Console.WriteLine("Normal line #1."); // Set break point here.
    #line hidden
        Console.WriteLine("Hidden line.");
    #line default
        Console.WriteLine("Normal line #2.");


    Stackalloc

    کلمه کلیدی stackallocبرای اختصاص یک بلاک از حافظه در stack، در زمینه کد غیرامن (unsafe code) استفاده می‌شود.
    مثال زیر 20 عدد اول دنباله فیبوناچی را تولید می‌کند. هر عدد از مجموع دو عدد قبلی به دست می‌آید. در این مثال، یک بلاک از حافظه به اندازه 20 عدد از نوع int را در stack (نه heap) اختصاص می‌دهد. (تفاوت stack با heap)
    static unsafe void Fibonacci()
    {
        const int arraySize = 20;
        int* fib = stackalloc int[arraySize];
        var p = fib;
        *p++ = *p++ = 1;
    
        for ( var i = 2; i < arraySize; ++i, ++p )
        {
            *p = p[-1] + p[-2];
        }
    
        for ( var i = 0; i < arraySize; ++i )
        {
            System.Console.WriteLine( fib[i] );
        }
    }
    آدرس بلاک حافظه در اشاره گر fib ذخیره می‌شود. این متغیر توسط GC جمع آوری نمی‌شود و طول عمر آن محدود به متدی است که در آن تعریف شده است و شما نمی‌توانید قبل از بازگشت متد، حافظه را آزاد کنید.
    تنها دلیل استفاده از stackalloc، عملکرد بهتر آن است (برای محاسبات و یا ردوبدل اطلاعات). با استفاده از stackalloc به جای اختصاص دادن آرایه (heap)، فشار کمتری را بر GC وارد می‌کنید (نیاز کمتری به اجرای GC وجود دارد). در نتیجه سرعت اجرای بالاتری خواهید داشت.
    توجه: برای اجرای مثال بالا باید پنجره خصوصیات پروژهرا باز کنید و در بخش Build، گزینه Allow unsafe codeرا تیک بزنید.

    ‫ویژگی های کمتر استفاده شده در NET. - بخش ششم

    $
    0
    0

    #Execute VB code via C

    می توان از طریق #C، ماکروهای Visual Basic مورد استفاده‌ی در Office را تولید کرد.
    static void AddChartButton( Workbook workBook,
                                Worksheet xlWorkSheetNew,
                                Range currentRange,  int macroId,
                                int startRow, int endRow,
                                int startCol, int endCol,
                                string buttonImagePath )
    {
        var cell = currentRange.Next;
        var width = cell.Width;
        var height = 24;
        var left = cell.Left;
        var top = System.Math.Max( cell.Top + cell.Height - height, 0 );
        var button = xlWorkSheetNew.Shapes.AddPicture( buttonImagePath,
                                                        MsoTriState.msoFalse,
                                                        MsoTriState.msoCTrue,
                                                        left, top, width, height );
        var module = workBook.VBProject.VBComponents.Add( vbext_ComponentType.vbext_ct_StdModule );
        module.CodeModule.AddFromString( GetMacro( macroId,
                                                    startRow, endRow,
                                                    startCol, endCol ) );
        button.OnAction = "Macro" + macroId;
    }
    
    static string GetMacro( int macroId,
                            int startRow,  int endRow,
                            int startCol, int endCol )
    {
        var sb = new StringBuilder();
        var range = "ActiveSheet.Range(Cells(" + startRow + "," + startCol + "), Cells(" + endRow + "," + endCol + ")).Select";
        sb.AppendLine( "Sub Macro" + macroId + "()" );
        sb.AppendLine( "On Error Resume Next" );
        sb.AppendLine( range );
        sb.AppendLine( "ActiveSheet.Shapes.AddChart.Select" );
        sb.AppendLine( "ActiveChart.ChartType = xlColumn" );
        sb.AppendLine( "ActiveChart.SetSourceData Source:=" + range );
        sb.AppendLine( "On Error GoTo 0" );
        sb.AppendLine( "End Sub" );
        return sb.ToString();
    }

    و برای استفاده از آن می‌توان مانند مثال زیر عمل کرد:

    var excelApp = new Microsoft.Office.Interop.Excel.Application();
    var fileName = @"C:\Users\Vahid\Desktop\VBA.xlsm";
    var workBook = excelApp.Workbooks.Open( fileName );
    var sheet = workBook.Sheets[1];
    
    AddChartButton( workBook,
                    sheet,
                    sheet.Range["B1"],
                    1231,
                    1,
                    10,
                    1,
                    2,
                    @"C:\Users\Vahid\Desktop\BarChart.png");
    
    excelApp.DisplayAlerts = false;
    workBook.Close( true,
                    fileName );

    خروجی مثال بالا


    نکته: در صورتیکه بعد از اجرای برنامه، خطای "programmatic access to visual basic project is not trusted" رخ داد از این طریق می‌توانید مشکل را حل کنید.

    File -> Options -> Trust Center -> Trust Center Settings -> Macro Settings -> Trust Access to the VBA Project object model


    volatile

    کلمه کلیدی volatile نشان می‌دهد که یک فیلد ممکن است توسط چندین thread به صورت همزمان تغییر کند. فیلدهایی که به عنوان volatile تعریف می‌شوند، شامل بهینه سازی کامپایلر برای دسترسی از طریق تنها یک thread قرار نمی‌گیرند و بروزرسانی مقدار فعلی این فیلد را در تمامی زمان‌ها، تضمین می‌کند.
    class Program
    {
        volatile bool _shouldPartyContinue = true;
    
        static void Main()
        {
            var firstDimension = new Program();
            var secondDimension = new Thread( firstDimension.StartPartyInAnotherDimension );
            secondDimension.Start( firstDimension );
            Thread.Sleep( 5000 );
            firstDimension._shouldPartyContinue = false;
            Console.WriteLine( "Party Finish" );
        }
    
        void StartPartyInAnotherDimension( object input )
        {
            var currentDimensionInput = (Program)input;
            Console.WriteLine( "let the party begin" );
            while ( currentDimensionInput._shouldPartyContinue ) {}
            Console.WriteLine( "Party ends: (" );
        }
    }
    نکته: اگر متغیر shouldPartyContinueبه وسیله volatileعلامت گذاری نشده بود، برنامه در حالت Release (که گزینه Optimize codeتیک داشته باشد) هیچگاه به پایان نمی‌رسید.

    ::global

    وقتی که یک عضو توسط موجودیت دیگری با همان نام مخفی شده باشد، با استفاده از کلمه کلیدی ::global (فضای نام سراسری) قابلیت دسترسی به آن امکان پذیر می‌شود.
    class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine( Number );  // Error
            global::System.Console.WriteLine( "Console: " + Console ); //OK
        }
    
        public class System { }
    
        // Define a constant called ‘Console’ to cause more problems.
        const int Console = 7;
        const int Number = 67;
    }
    همانطور که در مثال بالا مشاهده می‌کنید، به دلیل وجود ثابت Console و کلاس System امکان دسترسی به متد WriteLine وجود ندارد، برای در دسترس قرار گرفتن آن باید از ::global استفاده کرد.

    DebuggerDisplayAttribute

    با استفاده از DebuggerDisplayAttribute می‌توانید نحوه نمایش یک فیلد یا یک کلاس را در پنجره متغیر دیباگر مشخص کنید.
    [DebuggerDisplay( "{DebuggerDisplay}" )]
    public class DebuggerDisplayTest
    {
        public string FirstName { get; set; }
    
        public string LastName { get; set; }
    
        public int Age { get; set; }
    
        [DebuggerBrowsable( DebuggerBrowsableState.Never )]
        string DebuggerDisplay => $"{FirstName} {LastName} {Age} years old";
    }
    و بعد از استفاده‌ی از آن، خروجی زیر بدست می‌آید:

    همچنین شما می‌توانید عبارات مختلفی را به صورت مستقیم در این attribute استفاده کنید.
    [DebuggerDisplay( "Age {Age > 0 ? Age : 25}" )]
    public class DebuggerDisplayTest
    {
     //...
    }
    اگر مقدار پروپرتی Age بیشتر از 0 باشد، مقدار Age و در غیراینصورت 25 نشان داده می‌شود.

    ‫پیاده سازی Basic Authentication در ASP.NET MVC

    $
    0
    0
    در سیستم‌های اتصال از راه دور به خصوص اتصال تلفن‌های همراه به وب سرویس، یکی از مواردی که مرتبا قبل از هر درخواستی بررسی میگردد، صحت نام کاربری و کلمه عبور درخواستی است. در این روش کاربر، الزامات امنیتی (نام کاربری و کلمه عبور) را در بدنه درخواست قرار داده و هر api باید قبل از انجام عملیات، صحت آن را بررسی کند. این مورد باعث میشود که کدها از حالت بهینه خارج شده و در سمت سرور، Api مربوطه باید صحت آن را بررسی کند. در این مقاله قصد داریم با پیاده سازی Basic Authenticationاین مشکل را رفع کرده تا به کد یکدست‌تری برسیم. Basic Authentication طبق گفته ویکی پدیا در واقع یک روش در درخواست‌های ارسالی http میباشد که با فراهم سازی الزامات امنیتی، درخواست خود را به سرور ارائه میدهد. در این روش نام کاربری و کلمه عبور به جای ارسال در بدنه درخواست، در هدر درخواست قرار گرفته و الزامات امنیتی به صورت رمزگذاری شده (عموما Base64) به سمت سرور ارسال می‌شوند.
    این روش به دلیل عدم وجود ذخیره کوکی، ایجاد Session و دیگر مباحث امنیتی، جز ساده‌ترین روش‌های تشخیص هویت میباشد و همچنین مشخص است به دلیل رمزگذاری ساده‌ای چون Base64 و همچنین در صورت ارسال از طریق http نه https، حفاظت چندانی در ارسال الزامات امنیتی ندارند. ولی عموما برای گوشی‌های همراه تا حد زیادی قابل قبول است. همچنین در این روش مکانیزم logout فراهم نیست و تنها در طی یک درخواست پردازش اطلاعات انجام شده و بعد از آن هویت شما نابود خواهد شد.

    برای ارسال اطلاعات در هدر درخواستی، به شیوه زیر اطلاعات را ارسال میکنیم:
    Authorization: Basic  Username:Password
    از آنجا که گفتیم اطلاعات بالا بهتر هست در قالب خاصی رمزگذاری (encoding) شوند، خط بالا به صورت نهایی، به شکل زیر خواهد بود:
    Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
    از آنجا که این کد اعتبارسنجی باید مرتبا بررسی شود، باعث میشود که اصل تک مسئولیتی اکشن زیر سوال رفته و کدها در ابتدا مرتبا تکرار شوند. این وظیفه را بر عهده Attribute ‌ها میگذاریم. در Attribute‌های موجود در سیستم دات نت، می‌توان به AuthorizationFilterAttribute اشاره کرد که امکانات Authorization را در اختیار ما قرار میدهد. کد زیر را برای آن وارد میکنیم:
    public class BasicAuthetication:AuthorizationFilterAttribute
        {
            public override void OnAuthorization(HttpActionContext actionContext)
            {
                if (actionContext.Request.Headers.Authorization == null)
                {
                    SayYouAreUnAuthorize(actionContext);
                    return;
                }
    
                var param = actionContext.Request.Headers.Authorization.Parameter;
                var decodedCridential = Encoding.UTF8.GetString(Convert.FromBase64String(param));
                var splitedArray = decodedCridential.Split(':');
                if (splitedArray[0] == "username" && splitedArray[1] == "password")
                {
                    var userId = 1;
                    var genericIdentity=new GenericIdentity(userId.ToString());
                    var genericPrincipal=new GenericPrincipal(genericIdentity,null);
                    Thread.CurrentPrincipal = genericPrincipal;
                    return;
                }
                SayYouAreUnAuthorize(actionContext);
            }
    
            private void SayYouAreUnAuthorize(HttpActionContext actionContext)
            {
                actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);            
            }
        }

      در اولین خط از کد بررسی میشود که خاصیت Authorization در هدر درخواست وجود دارد یا خیر؛ در صورت که وجود نداشته باشد متد SayYouAreUnAuthorize  صدا زده میشود. این متد وظیفه دارد، در صورتیکه صحت نام کاربری و کلمه عبور تایید نشد، خطای مورد نظر را که با کد 401 شناخته میشود، بازگرداند که در متد اصلی دو بار مورد استفاده قرار گرفته است. با استفاده از این بازگردانی و دادن پاسخ کاربر، دیگر api مورد نظر به مرحله اجرا نمیرسد.

    در صورتیکه کد ادامه پیدا کند، مقدار تنظیم شده Authorization را دریافت کرده و در خط بعدی آن را از حالت انکود شده خارج میکنیم. از آنجاکه الگوی ارسالی ما به شکل username:password میباشد، با استفاده از متد الحاقی Split هر دو عبارت را جدا کرده و در خط بعد، صحت آن دو را مشخص میکنیم. از آنجا که این تکه کد تنها یک تست است، من آن را به شکل ساده و استاتیک نوشته‌ام. بدیهی است که در یک مثال واقعی، این تایید صحت توسط یک منبع داده انجام می‌شود. در صورتی که صحت آن تایید نشود مجددا متد SayYouAreUnAuthorize مورد استفاده قرار میگیرد. در غیر این صورت باید این تایید هویت را در ترد جاری تایید کرده و حفظ کنیم و حتی در صورت لزوم Id کاربر و نقش‌های آن را ذخیره کنیم تا اگر api مورد نظر بر اساس آن تصمیم میگیرد، آن‌ها را در اختیار داشته باشیم. به عنوان مثال ممکن است اطلاعات دریافتی پرداختی‌های یک کاربر باشد که به UserId نیاز است.

    از آنجا که در این روش نه ذخیره session و نه کوکی و نه مورد دیگری داریم و قصد داریم تنها در ترد یا درخواست جاری این اعتبار را نگه داریم، میتوانیم از thread.CurrentPrincipal استفاده کنیم. این خصوصیت نوع GenericPrincipal را نیاز دارد که دو پارامتر   GenereicIdentityو آرایه‌ای از نوع رشته را دریافت میکنید. این آرایه‌ها متعلق به زمانی است که شما قصد دارید نقش‌های یک کاربر را ارسال کنید و شیء GenericIdentity شیءایی است که شامل اطلاعات Authorization بوده و هیچ وابستگی به windows Domain ندارد. این شیء از شما تنها یک نام را به صورت رشته میگیرد که میتوانیم از طریق آن UserId را پاس کنیم. برای دریافت آن نیز داریم:
    var userId=int.Parse(Thread.CurrentPrincipal.Identity.Name);
    حال برای api به کد زیر نیاز داریم:
    [BasicAuthetication]
            public HttpResponseMessage GetPayments(int type)
            {
                var userId = int.Parse(Thread.CurrentPrincipal.Identity.Name);
                if (type == 1)
                {
                    return Request.CreateResponse(HttpStatusCode.OK, new
                    {
                        Id = 4,
                        Price = 20000,
                        userId=userId
                    });
                }
                    return Request.CreateResponse(HttpStatusCode.OK, new
                    {
                        Id = 3,
                        Price = 10000,
                        userId = userId
                    });
            }

    در api بالا attribute مدنظر بر روی آن اعمال شده و تنها مقادیر ورودی این api همان پارامترهای درخواست اصلی میباشد و ویژگی BasicAuthentication کدهای اعتبارسنجی را از کد اصلی دور نگه داشته است و api اصلی هیچگاه متوجه قضیه نمی‌شود و تنها کار اصلی خود را انجام می‌دهد. همچین در اینجا از طریق کلاس thread به UserId دسترسی داریم.

    حال تنها لازم است کلاینت، قسمت هدر را پر کرده و آن را به سمت سرور ارسال کند. در اینجا من از نرم افزار I'm only resting استفاده میکنیم تا درخواست رسیده را در WebApi تست نمایم.


    ‫ایجاد یک DbContext مشترک بین entityهای پروژه‌های متفاوت

    $
    0
    0
    فرضکنید پروژه بزرگی رادارید که هر قسمت را به یک برنامه نویس می‌سپارید تا آن قسمت را در پروژه مجزایی طراحی و برنامه نویسی کند.هر برنامه نویس Entity‌های خاص خود را در لایه‌های مربوط به پروژه خود تعریف می‌کند و از آنها استفاده می‌کند. حال یکی از برنامه نویس‌ها می‌خواهد ازEntityهای پروژه دیگر استفاده کند. در این صورت اگر از دو Context شیء‌ایی را بسازد و آنها را با یکدیگر Join  بزند، خطایی مربوط به تعلق داشتن دو Entity به دو Context متفاوت را می‌گیرد.

    در پروژه‌های کوچک، کل تیم بر روی ماژول‌های مختلف یک پروژه کار می‌کنند و یک DbContext مشترک دارند. اما راه حل این مشکل در پروژه‌های بزرگ چیست؟ 
    یکی از راه‌های پیشنهادی، استفاده از یک کلاس DbContextBase است که همه پروژه‌ها بایستی Context خود را از این کلاس به ارث ببرند که در این صورت باز هم مشکل ساخت چند DbContext وجود خواهد داشت که فقط می‌توان از Entity‌های موجود در DbContextBase و DbContext پروژه جاری استفاده کرد. اما در شرکت‌های بزرگ که پروژه‌هایی مانندERP دارند، روش دیگری استفاده می‌شود که در ادامه خواهیم دید.
    روش مورد استفاده به این صورت است که در زمان اجرا یک DbContext برای همه Entity‌های پروژه‌های مختلف ساخته می‌شود. اجازه بدهید همراه با مثال، این پروژه را پیش برویم. فرض کنید دو تیم برنامه نویسی داریم که هر کدام بر روی پروژه‌های مجزای SampleProject1 و SampleProject2 کار میکنند که Entity‌های هر کدام در لایه‌های Common قرار گرفته‌اند.

    در SampleProject1 مدل Product را داریم:

    public partial class Product : Entity
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public Nullable<byte> ProductTypeId { get; set; }
        }
    و در SampleProject2، مدل ProductType را داریم که هر دو Entity از کلاس Entity ارث بری می‌کند: 
     public partial class ProductType : Entity
        {
            public byte Id { get; set; }
            public string Name { get; set; }
        }
    همه پروژه‌ها را در پروژه‌ی SampleProject1.Console، به عنوان رفرنس اضافه می‌کنیم؛ بجز SampleProject2.Console و Output path همه پروژه‌ها را به یک پوشه مشترک هدایت می‌کنیم. در ادامه برای بدست آوردن Entity‌ها از کد زیر استفاده می‌کنیم:
                List<Assembly> allAssemblies = new List<Assembly>();
                string path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
    
                foreach (string dll in Directory.GetFiles(path, "*.Common.dll"))
                    allAssemblies.Add(Assembly.LoadFile(dll));
    
                var type = typeof(Entity);
          
                List<Type> types = allAssemblies
                 .SelectMany(s => s.GetTypes())
                 .Where(p => type.IsAssignableFrom(p)).ToList();
    
                List<string> entities = new List<string>();
                foreach (var item in types)
                {
                    entities.Add(item.Name);
                }
    
                types.Add(typeof(Entity));
    و سپس برای Generate کردن کلاس DbContext از کلاس زیر استفاده می‌کنیم:
    public class ContextGenerator
        {
            public void Generate(List<string> entities, params Type[] types)
            {
                StringBuilder code = new StringBuilder();
    
                code.AppendLine(@"
               using System.Data.Entity;
               using System.Data.Entity.Core.EntityClient;
               using SampleProject1.Common.Models;
               using SampleProject1.Common.Models.Mapping;
               using SampleProject2.Common.Models;
               using SampleProject2.Common.Models.Mapping;
    
               namespace DbContextGenerator
               {
                    public partial class TestContext : DbContext
                    {
                        static TestContext()
                        {
                            Database.SetInitializer<TestContext>(null);
                        }
    
                        public TestContext()
                            : base(""Data Source=.;Initial Catalog=Test;Integrated Security=True;MultipleActiveResultSets=True"")
                        {
                            }
                    ");
    
                var pluralizeHelper = new PluralizeHelper();
    
                foreach (var entity in entities)
                {
                    code.AppendLine($@"public DbSet<{entity}> {pluralizeHelper.Pluralize(entity)} {{ get; set; }}");
                }
    
                code.AppendLine(@"protected override void OnModelCreating(DbModelBuilder modelBuilder)");
                code.AppendLine(@"{");
    
                foreach (var entity in entities)
                {
                    code.AppendLine($@"modelBuilder.Configurations.Add(new {entity}Map());");
                }
                code.AppendLine(@"}");
                code.AppendLine(@"}");
                code.AppendLine(@"}");
               
                CSharpCodeProvider provider = new CSharpCodeProvider();
                CompilerParameters parameters = new CompilerParameters();
    
                parameters.ReferencedAssemblies.Add("System.Drawing.dll");
                parameters.ReferencedAssemblies.Add("System.Data.dll");
                parameters.ReferencedAssemblies.Add("System.Data.Entity.dll");
                parameters.ReferencedAssemblies.Add("System.ComponentModel.dll");
    
                foreach (var type in types)
                {
                    parameters.ReferencedAssemblies.Add(type.Assembly.Location);
                }
    
                parameters.ReferencedAssemblies.Add(typeof(DbSet).Assembly.Location);
                parameters.ReferencedAssemblies.Add(typeof(DbContext).Assembly.Location);
                parameters.ReferencedAssemblies.Add(typeof(IQueryable).Assembly.Location);
                parameters.ReferencedAssemblies.Add(typeof(IQueryable<>).Assembly.Location);
                parameters.ReferencedAssemblies.Add(typeof(System.ComponentModel.IListSource).Assembly.Location);
    
                parameters.GenerateExecutable = false;
                parameters.GenerateInMemory = false;
                parameters.OutputAssembly = "ProjectContext.dll";
    
                CompilerResults results = provider.CompileAssemblyFromSource(parameters, code.ToString());
    
                if (results.Errors.HasErrors)
                {
                    StringBuilder sb = new StringBuilder();
    
                    foreach (CompilerError error in results.Errors)
                    {
                        sb.AppendLine(String.Format("Error ({0}): {1}", error.ErrorNumber, error.ErrorText));
                    }
    
                    throw new InvalidOperationException(sb.ToString());
                }
            }
    
        }
    و نحوه فراخوانی آن:
     new ContextGenerator().Generate(entities, types.ToArray()); // generate dbContext
    همانطور که مشاهده می‌کنید، برای تولید کد، از کلاس CSharpCodeProvider استفاده میکنیم که نتیجه اجرای کد بالا، ساخت DLLی به نام ProjectContext.dll است. با مشاهده DLL ساخته شده توسط نرم افزار ILSpy، کد جنریت شده به صورت زیر خواهد بود: 

    حال برای استفاده از Context تولید شده، به صورت زیر شیءایی را ساخته:

     static DbContext _dbContext=null;
            public static DbContext GetDbContextInstance()
            {
                if (_dbContext == null)
                {
                    string path = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
                    var dllversionAssm = Assembly.LoadFile(path + "\\ProjectContext.dll");
                    Type type = dllversionAssm.GetType("DbContextGenerator.TestContext");
                    _dbContext = (DbContext)Activator.CreateInstance(type);
                }
                return _dbContext;
            }

    و سپس برای ساخت DbSet از هر Entity به کد زیر نیاز خواهیم داشت:

    public static System.Data.Entity.DbSet<T> Get<T>() where T : class
            {
                var set = GetDbContextInstance().Set<T>();
                return set;
            }

    هم اکنون می‌توان رکوردهای Entity‌ها را واکشی کرده و یا آن‌ها را با یکدیگر Join بزنیم:

                var products = Get<Product>().ToList();
    
                var productTypes = Get<ProductType>().ToList();
    
    
                var query = from p in Get<Product>()
                            join pt in Get<ProductType>() on p.ProductTypeId equals pt.Id
                            select new
                            {
                                Id = p.Id,
                                Name = p.Name,
                                ProductType = pt.Name
    
                            };
    
                var JoinResult = query.ToList();

    و نتیجه واکشی ها 


    کد کامل این پروژه  

    ‫اعتبارسنجی در Angular 2 توسط JWT

    $
    0
    0
    در مقالاتی که در سایت منتشر شده‌است، آشنایی و همچنین نحوه پیاده سازی Json Web Tokenرا فرا گرفتیم. در اینجا میخواهیم با استفاده از توکن تولید شده، برنامه‌های Angular2 یا هر نوع فریمورک spa را با آن ارتباط دهیم. در سایت جاری قبلا در مورد نحوه پیاده سازیآن صحبت شده‌است و میخواهیم از آن در یک پروژه Angular 2 صحبت کنیم.
    پروژه دات نت را از طریق این آدرس دریافت کرده  و آن را در حالت اجرا بگذارید.

    سپس یک سرویس جدید را در پروژه Angular خود اجرا کنید و متد زیر را به آن اضافه کنید:
    login():any{
        let urlSearchParams = new URLSearchParams();
        urlSearchParams.append('username', 'Vahid');
        urlSearchParams.append('password', '1234');
        urlSearchParams.append('grant_type', 'password');
        let body = urlSearchParams.toString();
    
        let headers = new Headers();
        headers.append('Content-Type', 'application/x-www-form-urlencoded');
        return this._http.post('http://localhost:9577/login', body, { headers: headers })
            .map(res => res.json());
    }
    در متد بالا ابتدا از کلاس  URLSearchParams  یک شیء میسازیم. این کلاس در ماژول Http قرار دارد و وظیفه آن تبدیل پارامترهای داده شده به شکل زیر میباشد:
    username=vahid&password=1234
    دلیل اینکه ما در اینجا همانند jquery از JSON.stringify استفاده نکردیم این است که در حالتیکه خصوصیت Content-Type هدر را بر روی application/x-wwww-form-urlencoded قرار میدهیم، شیء ایجاد شده از کلاس Http در اینجا، کل عبارت را تبدیل به کلید بدون مقدار میکند و باعث میشود که پارامترها به درستی به سمت Owin هدایت نشوند. بعد از آن هدری که ذکر شد را در درخواست قرار داده و آن را ارسال میکنیم.
    از آنجاکه پروژه انگیولار ساخته شده در آدرسی دیگر و جدا از پروژه‌ی دات نت قرار دارد و همینطور که می‌بینید آدرس کامل آن را به این دلیل قرار دادم، ممکن است خطای CORS را دریافت کنید که میتوانید آن را بانصب یک بسته نیوگتیحل کنید.
     
    همچنین برای تست و انجام یک عمل مرتبط با توکن، متد زیر را هم به آن اضافه می‌کنیم:
    ApiAdmin(token:any):any{
        let headers = new Headers();
        headers.append('Authorization', 'bearer ' + token);
        return this._http.get('http://localhost:9577/api/MyProtectedApi', { headers: headers })
            .map(res => res.json());
    }
    در اینجا با استفاده از روش Http Bearer که در اعتبارسنجی در سطح OAuth کاربرد زیادی دارد، توکن تولید شده را که در پارامتر ورودی متد دریافت کرده‌ایم، به هدر اضافه کرده و آن را ارسال میکنیم.

    کد کل سرویس،  الان به شکل زیر شده است:
    import { Http, Headers, URLSearchParams } from '@angular/http';
    import { Injectable } from '@angular/core';
    import { Observable } from "rxjs/Observable";
    import "rxjs/Rx";
    
    @Injectable()
    export class MemberShipService {
        constructor(private _http: Http) { }
    
        ApiAdmin(token: any): any {
            let headers = new Headers();
            headers.append('Authorization', 'bearer ' + token);
            return this._http.get('http://localhost:9577/api/MyProtectedApi', { headers: headers })
                .map(res => res.json());
        }
    
        login(): any {
            let urlSearchParams = new URLSearchParams();
            urlSearchParams.append('username', 'Vahid');
            urlSearchParams.append('password', '1234');
            urlSearchParams.append('grant_type', 'password');
            let body = urlSearchParams.toString();
    
            let headers = new Headers();
            headers.append('Content-Type', 'application/x-www-form-urlencoded');
            return this._http.post('http://localhost:9577/login', body, { headers: headers })
                .map(res => res.json());
        }
    }
    سپس سرویس ساخته شده را در ngModule معرفی میکنیم. در کامپوننت هدف دو دکمه را قرار میدهیم؛ یکی برای لاگین و دیگری برای دریافت اطلاعاتی که نیاز به اعتبار سنجی دارد. رویداد کلیک دکمه‌ها را به متدهای زیر متصل میکنیم:
    Login(){
        this._service.login()
            .subscribe(res => {
                localStorage.setItem('access_token', res.access_token);
                localStorage.setItem('refresh_token', res.refresh_token);
            }
            , error => console.log(error));
    }
    در اینجا ما اطلاعات لاگین را به سمت سرور فرستاده و در صورتیکه پاسخ صحیحی را دریافت کردیم، متد Subscribe اجرا خواهد شد و مقادیر دریافتی را باید به نحوی در سیستم و بین کامپوننت‌های مختلف، ذخیره و نگهداری کنیم. در اینجا من از روش Local Storageکه در سایت جاری قبلا به آن پرداخته شده‌است، استفاده میکنم. access_token و refresh_token جاری و اطلاعات دیگری را چون رول‌ها و ... هر یک را با یک کلید ذخیره میکنیم تا بعدا به آن دسترسی داشته باشیم.
    در متد بعدی که قرار است توسط آن صحت اعتبارسنجی مورد آزامایش قرار بگیرد، کدهای زیر را مینویسیم:
    CallApi()
    {
        let item = localStorage.getItem("access_token");
        if (item == null) {
            alert('please Login First.');
            return;
        }
        this._service.ApiAdmin(item)
            .subscribe(res => {
                alert(res);
            }
            , error => console.log(error));
    }
    در اینجا ابتدا توکن ذخیره شده را از Local Storage درخواست میکنیم. اگر نتیجه این درخواست نال باشد، به این معنی است که کاربر قبلا لاگین نکرده است؛ در غیر این صورت درخواست را با توکن دریافتی میفرستیم و پاسخ موفق آن را در یک alert می‌بینیم. در صورتیکه توکن ما اعتبار نداشته باشد، خطای بازگشتی در کنسول نمایش خواهد یافت.


    اعتبارسنجی در مسیریابی


    یکی از روش‌هایی که انگیولار باید بررسی کند این است که کاربر جاری با توجه به نقش‌هایی که ممکن است داشته باشد، یا اعتبار سنجی شده است یا خیر و میزان دسترسی او به کامپوننت‌ها، باید مشخص گردد. برای این مورد خصوصیتی به مسیریابی اضافه شده است به نام CanActivate که از شما یک کلاس را که در آن اینترفیس CanActivate پیاده سازی شده است، درخواست میکند. در اینجا ما یک Guard را با نام LoginGuard ایجاد میکنیم، تا تنها کاربرانی که لاگین کرده‌اند، به این صفحه دسترسی داشته باشند:
    import { CanActivate } from '@angular/router';
    import { Injectable } from '@angular/core';
    
    @Injectable()
    export class LoginGuard implements CanActivate {
        constructor() { }
    
        canActivate() {
            let item = localStorage.getItem('access_token');
            if (item == null)
                return false;
            return true;
        }
    }
    در متد Activate باید خروجی boolean بازگردد. در صورتیکه true بازگشت داده شود، عملیات هدایت به کامپوننت مقصد با موفقیت انجام خواهد شد. در صورتیکه خلاف این موضوع اتفاق بیفتد، کامپوننت هدف نمایش داده نمیشود. در اینجا بررسی کرده‌ایم که اگر توکن مورد نظر در localStorage  قرار داشت، به معنی این است که لاگین شده‌است. ولی این موضوع روشن است که در یک مثال واقعی باید صحت این توکن بررسی شود. این Guard در واقع یک سرویس است که باید در فایل ngModule معرفی شده و آن را در فایل مسیریابی به شکل زیر استفاده کنیم:
     { path: 'component-one/:id', component: Component1Component,canActivate:[LoginGuard]}
    در معرفی یک مسیر به فایل مسیریابی، خصوصیاتی چون مسیری که نوشته میشود و کامپوننت مربوط به آن مسیر ذکر می‌شود. خصوصیت دیگر، CanActivate است که به آن کلاس LoginGuard را معرفی مکنیم. در اینجا شما میتواند به هر تعداد گارد را که دوست دارید، معرفی کنید ولی به یاد داشته باشید که اگر یکی از آن‌ها در اعتبارسنجی ناموفق باشد، دیگر کامپوننت جاری در دسترس نخواهد بود. به معنای دیگر تمامی گاردها باید نتیجه‌ی true را بازگردانند تا دسترسی به کامپوننت هدف ممکن شود.
     { path: 'component-one/:id', component: Component1Component,canActivate:[LoginGuard,SecondGuard]}

    در یک گارد ممکن است کاربر لاگین نکرده باشد و شما نیاز دارید او را به صفحه لاگین هدایت کنید. در این صورت گارد لاگین را به شکل زیر بازنویسی میکنیم:
    import { Router } from '@angular/router';
    import { CanActivate } from '@angular/router';
    import { Injectable } from '@angular/core';
    
    @Injectable()
    export class LoginGuard implements CanActivate {
    
        constructor(public router: Router) { }
    
        canActivate() {
            let item = localStorage.getItem('access_token');
            if (item == null) {
                this.router.navigate(['/app']);
                return false;
            }
            return true;
        }
    }
    در اینجا ما از سازنده کلاس، شیءایی از نوع Router را ساختیم که امکانات و متدهای خوبی را در باب مسیریابی به همراه دارد و از آن برای انتقال به مسیری دیگر که میتواند صفحه لاگین باشد، استفاده کردیم.

    همچنین گارد میتواند اطلاعات مسیر درخواست شده را خوانده و بر اساس آن به شما پاسخ بدهد. به عنوان مثال پارامترهایی را که به سمت مسیر مورد نظر هدایت میشود، بخواند و بر اساس آن، تصمیم گیری کند. به عنوان نمونه کاربر به مسیر ذکرشده دسترسی دارد، ولی با Id که در مسیر ارسال کرده است، دسترسی ندارد:
    import { Router } from '@angular/router';
    import { CanActivate, ActivatedRouteSnapshot } from '@angular/router';
    import { Injectable } from '@angular/core';
    
    @Injectable()
    export class SecondGuard implements CanActivate {
    
        constructor(public router: Router) { }
    
        canActivate(route: ActivatedRouteSnapshot) {
            let id = route.params['id'];
            if (id == 1) {
                return false;
            }
            return true;
        }
    }

    متد CanActivate میتواند پارامترهای زیادی را به عنوان ورودی دریافت کند که یکی از آن‌ها ActivatedRouteSnapshot است که اطلاعات خوب و مفیدی را در مورد مسیر ارسال شده از طرف کاربر دارد و با استفاده از آن در اینجا میتوانیم پارامترهای ارسالی را دریافت کنیم. در اینجا ذکر کرده‌ایم که اگر پارامتر Id که بر مبنای مسیر زیر است، برابر 1 بود، مقدار برگشتی برابر false خواهد بود و دسترسی به کامپوننت در اینجا ممکن نخواهد بود.
     { path: 'component-one/:id', component: Component1Component,canActivate:[LoginGuard,SecondGuard] }

    ‫مسیریابی در Angular - قسمت اول - معرفی

    $
    0
    0
    مسیریابی در +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 سمت سرور ندارد. از آنجائیکه سرور هرچیزی را که پس # باشد ندید خواهد گرفت و سعی در یافتن آن‌، در سمت سرور نخواهد کرد.

    ‫مسیریابی در Angular - قسمت دوم - مسیریابی ماژول‌ها

    $
    0
    0
    اغلب برنامه‌های بزرگ 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 برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.

    ‫مسیریابی در Angular - قسمت سوم - پارامترهای مسیریابی

    $
    0
    0
    گاهی از اوقات نیاز است به همراه مسیریابی، اطلاعاتی را نیز به آن‌ها ارسال کنیم. برای مثال در حین نمایش لیست محصولات، برای هدایت به صفحه‌ی نمایش جزئیات هر محصول، نیاز است Id هر محصول نیز به همراه مسیریابی، به کامپوننت مقصد ارسال شود. اینکار توسط route parameters قابل مدیریت است.

    تنظیم مسیریابی‌ها جهت درج پارامترها

    پیش از ارسال اطلاعات مورد نیاز، به مسیری خاص، نیاز است محل قرارگیری این اطلاعات را در تعاریف مسیریابی‌ها مشخص کرد.
    در ادامه‌ی مثال این سری، دو کامپوننت جدید جزئیات و ویرایش محصولات را به ماژول محصولات اضافه می‌کنیم:
    >ng g c product/ProductDetail>ng g c product/ProductEdit
    این دستورات سبب به روز رسانی خودکار قسمت declarations فایل src\app\product\product.module.ts نیز خواهند شد.

    در ادامه با مراجعه به فایل src\app\product\product-routing.module.ts، تنظیمات مسیریابی آن‌را به شکل ذیل تکمیل خواهیم کرد:
    import { ProductEditComponent } from './product-edit/product-edit.component';
    import { ProductDetailComponent } from './product-detail/product-detail.component';
    import { ProductListComponent } from './product-list/product-list.component';
    
    const routes: Routes = [
      { path: 'products', component: ProductListComponent },
      { path: 'products/:id', component: ProductDetailComponent },
      { path: 'products/:id/edit', component: ProductEditComponent }
    ];
    اولین شیء مسیریابی تعریف شده را در قسمت‌های پیشینبررسی کردیم که امکان نمایش کامپوننت لیست محصولات را توسط یک routerLink در منوی سایت میسر می‌کند.
    دومین شیء مسیریابی، از مسیر ریشه‌ی یکسانی استفاده می‌کند (products) که علت آن‌را در قسمت قبل با «انتخاب استراتژی مناسب نامگذاری مسیرها» بررسی کردیم. در اینجا id: مکانی را مشخص می‌کند که قرار است اطلاعاتی را به آن مسیر خاص ارسال کند. در اینجا : به معنای تعریف مکان یک پارامتر اجباری مسیریابی است. به علاوه id یک نام دلخواه است و چون در مثال جاری می‌خواهیم id محصولات را انتقال دهیم، Id نام‌گرفته‌است؛ وگرنه هر نام دیگری نیز می‌تواند باشد.
    سومین شیء مسیریابی نیز از مسیر ریشه‌ی یکسانی استفاده می‌کند و تفاوت آن‌را با حالت نمایش جزئیات، با افزودن یک edit/ مشخص کرده‌ایم.

    در اینجا هر تعداد متغیر مورد نیاز، قابل تعریف است. برای مثال مسیری مانند orders/:id/items/:itemId با دو متغیر و یا بیشتر نیز قابل تعریف است. فقط باید دقت داشت که نام‌های پس از : در یک مسیریابی، باید منحصربفرد باشند.


    تکمیل کامپوننت نمایش لیست محصولات

    پیش از ادامه‌ی بحث نیاز است کامپوننت خالی لیست محصولات را که در قسمت‌های قبل ایجاد کردیم، تکمیل کنیم تا پس از آن بتوانیم لینک‌هایی را به صفحات نمایش جزئیات محصولات و همچنین ویرایش و افزودن محصولات نیز اضافه کنیم. به همین جهت ابتدا اینترفیس محصول را اضافه می‌کنیم:
    > ng g i product/IProduct
    و آن‌را به نحو ذیل تکمیل خواهیم کرد:
    export interface IProduct {
        id: number;
        productName: string;
        productCode: string;
    }


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

    یکی از بسته‌های Angular به نام angular-in-memory-web-api، قابلیت تشکیل یک REST Web API ساده را دارد که از آن جهت دریافت، ثبت و به روز رسانی لیست محصولات استفاده خواهیم کرد (بدون نیاز به نوشتن کد سمت سرور خاصی؛ صرفا در جهت ساده سازی ارائه‌ی مطلب).
    به همین جهت ابتدا بسته‌ی مرتبط با آن‌را نصب کنید:
    >npm install angular-in-memory-web-api --save
    ذکر پارامتر save در اینجا، سبب به روز رسانی فایل package.json نیز خواهد شد:
    "dependencies": {
       "angular-in-memory-web-api": "^0.3.1"
    },

    سپس کلاس ProductData را به ماژول محصولات اضافه می‌کنیم:
    > ng g cl product/ProductData
    این کلاس را در ادامه به صورت ذیل تکمیل خواهیم کرد:
    import { IProduct } from './iproduct';
    import { InMemoryDbService, InMemoryBackendConfig } from 'angular-in-memory-web-api';
    
    export class ProductData implements InMemoryDbService, InMemoryBackendConfig {
        createDb() {
            let products: IProduct[] = [
                {
                    'id': 1,
                    'productName': 'Product 1',
                    'productCode': '0011'
                },
                {
                    'id': 2,
                    'productName': 'Product 2',
                    'productCode': '0023'
                },
                {
                    'id': 5,
                    'productName': 'Product 5',
                    'productCode': '0048'
                },
                {
                    'id': 8,
                    'productName': 'Product 8',
                    'productCode': '0022'
                },
                {
                    'id': 10,
                    'productName': 'Product 10',
                    'productCode': '0042'
                }
            ];
            return { products };
        }
    }
    همانطور که ملاحظه می‌کنید، کلاسی که قرار است  به عنوان منبع داده‌ی بسته‌ی angular-in-memory-web-api بکار رود باید InMemoryDbService, InMemoryBackendConfig را نیز پیاده سازی کند که نمونه‌ای از آن‌را در اینجا برای بازگشت یک لیست درون حافظه‌ای محصولات، مشاهده می‌کنید.

    در آخر برای فعالسازی این REST Web API ساده، تنها کافی است به فایل src\app\app.module.ts مراجعه کرده و کلاس ProductData فوق را معرفی کنیم:
    import { ProductData } from './product/product-data';
    import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
    
    @NgModule({
      declarations: [
      ],
      imports: [
        BrowserModule,
        FormsModule,
        HttpModule,
        InMemoryWebApiModule.forRoot(ProductData, { delay: 1000 }),
    
        ProductModule,
        UserModule,
    
        AppRoutingModule
      ],
    - ابتدا مکان یافت شدن ماژول‌های مورد نیاز ProductData و InMemoryWebApiModule تعریف شده‌اند و سپس InMemoryWebApiModule.forRoot را جهت تشکیل یک Web API آزمایشی، برای ارائه‌ی اطلاعات کلاس ProductData، به لیست imports اضافه کرده‌ایم. باید دقت داشت که نیاز است تعریف InMemoryWebApiModule پس از تعریف HttpModule باشد تا بتواند تعدادی از پیش فرض‌های آن را بازنویسی کند.
    - در اینجا یک delay را هم در تنظیمات آن مشاهده می‌کنید. هدف از تعریف آن، شبیه سازی کند بودن دریافت اطلاعات از یک وب سرور واقعی است.
    - این وب سرویس در آدرس api/products قابل دسترسی است.


    تعریف سرویس HTTP کار با منبع اطلاعات درون حافظه‌ای

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

    پس از ایجاد قالب ابتدایی فایل product.service.ts، آن‌را به نحو ذیل تکمیل کنید:
    import { Injectable } from '@angular/core';
    import { Http, Response, Headers, RequestOptions } from '@angular/http';
    
    import { Observable } from 'rxjs/Observable';
    import 'rxjs/add/operator/do';
    import 'rxjs/add/operator/catch';
    import 'rxjs/add/observable/throw';
    import 'rxjs/add/operator/map';
    import 'rxjs/add/observable/of';
    
    import { IProduct } from './iproduct';
    
    @Injectable()
    export class ProductService {
      private baseUrl = 'api/products';
    
      constructor(private http: Http) { }
    
      getProducts(): Observable<IProduct[]> {
        return this.http.get(this.baseUrl)
          .map(this.extractData)
          .do(data => console.log('getProducts: ' + JSON.stringify(data)))
          .catch(this.handleError);
      }
    
      getProduct(id: number): Observable<IProduct> {
        if (id === 0) {
          return Observable.of(this.initializeProduct());
        };
        const url = `${this.baseUrl}/${id}`;
        return this.http.get(url)
          .map(this.extractData)
          .do(data => console.log('getProduct: ' + JSON.stringify(data)))
          .catch(this.handleError);
      }
    
      deleteProduct(id: number): Observable<Response> {
        let headers = new Headers({ 'Content-Type': 'application/json' });
        let options = new RequestOptions({ headers: headers });
    
        const url = `${this.baseUrl}/${id}`;
        return this.http.delete(url, options)
          .do(data => console.log('deleteProduct: ' + JSON.stringify(data)))
          .catch(this.handleError);
      }
    
      saveProduct(product: IProduct): Observable<IProduct> {
        let headers = new Headers({ 'Content-Type': 'application/json' });
        let options = new RequestOptions({ headers: headers });
    
        if (product.id === 0) {
          return this.createProduct(product, options);
        }
        return this.updateProduct(product, options);
      }
    
      private createProduct(product: IProduct, options: RequestOptions): Observable<IProduct> {
        product.id = undefined;
        return this.http.post(this.baseUrl, product, options)
          .map(this.extractData)
          .do(data => console.log('createProduct: ' + JSON.stringify(data)))
          .catch(this.handleError);
      }
    
      private updateProduct(product: IProduct, options: RequestOptions): Observable<IProduct> {
        const url = `${this.baseUrl}/${product.id}`;
        return this.http.put(url, product, options)
          .map(() => product)
          .do(data => console.log('updateProduct: ' + JSON.stringify(data)))
          .catch(this.handleError);
      }
    
      private extractData(response: Response) {
        let body = response.json();
        return body.data || {};
      }
    
      private handleError(error: Response): Observable<any> {
        console.error(error);
        return Observable.throw(error.json().error || 'Server error');
      }
    
      initializeProduct(): IProduct {
        // Return an initialized object
        return {
          id: 0,
          productName: null,
          productCode: null
        };
      }
    }
    این سرویس HTTP، به سرویس Web API آزمایشی واقع در آدرس  baseUrl، متصل خواهد شد:
       private baseUrl = 'api/products';
    از آن برای دریافت لیست محصولات (getProducts)، دریافت جزئیات یک محصول (getProduct)، حذف یک محصول (deleteProduct)، ثبت و یا به روز رسانی یک محصول (saveProduct) استفاده خواهیم کرد.


    نمایش لیست محصولات

    اکنون پس از این مقدمات می‌توانیم کامپوننت لیست محصولات را تکمیل کنیم. به همین جهت به قالب ابتدایی src\app\product\product-list\product-list.component.ts مراجعه کرده و آن‌را به نحو ذیل تکمیل کنید:
    import { ActivatedRoute } from '@angular/router';
    import { Component, OnInit } from '@angular/core';
    
    import { ProductService } from './../product.service';
    import { IProduct } from './../iproduct';
    
    @Component({
      selector: 'app-product-list',
      templateUrl: './product-list.component.html',
      styleUrls: ['./product-list.component.css']
    })
    export class ProductListComponent implements OnInit {
      pageTitle = 'Product List';
      errorMessage: string;
    
      products: IProduct[];
    
      constructor(private productService: ProductService,
        private route: ActivatedRoute) { }
    
      ngOnInit(): void {
        this.productService.getProducts()
          .subscribe(products => this.products = products,
          error => this.errorMessage = <any>error);
      }
    }
    در اینجا با استفاده از سرویس محصولاتی که پیشتر ایجاد کردیم، کار دریافت لیست محصولات انجام شده و سپس به خاصیت عمومی products نسبت داده می‌شود. این خاصیت را در قالب این کامپوننت نمایش خواهیم داد. به همین جهت فایل src\app\product\product-list\product-list.component.html را گشوده و آن‌را به نحو ذیل تکمیل کنید:
    <div class="panel panel-default"><div class="panel-heading">
        {{pageTitle}}</div><div class="panel-body"><div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div><div class="table-responsive"><table class="table" *ngIf="products && products.length"><thead><tr><th>Product</th><th>Code</th></tr></thead><tbody><tr *ngFor="let product of products"><td><a [routerLink]="['/products', product.id]">
                      {{product.productName}}</a></td><td>{{ product.productCode}}</td><td><a class="btn btn-primary" [routerLink]="['/products', product.id, 'edit']">
                    Edit</a></td></tr></tbody></table></div></div></div>
    اکنون اگر برنامه را توسط دستور ng serve -o ساخته و اجرا کنید، چنین صفحه‌ای قابل مشاهده خواهد بود:


    توضیحات:

    پس از تعریف مسیریابی‌های صفحات نمایش لیست محصولات و ویرایش محصولات، اکنون نوبت به اتصال آن‌ها به لینک‌هایی است تا توسط کاربران برنامه مورد استفاده قرار گیرند.
    در اینجا با تعریف لینکی، هر محصول را به صفحه‌ی مشاهده‌ی جزئیات آن متصل کرده‌ایم:
    <a [routerLink]="['/products', product.id]">
                      {{product.productName}}</a>
    برای مشاهده‌ی جزئیات هر محصول نیاز است Id آن محصول را به عنوان پارامتر مسیریابی ارسال کنیم. به همین جهت این Id را به عنوان پارامتری جدید، به routerLink انتساب داده‌ایم.
    و یا برای حالت edit نیز به همین صورت 'path: 'products/:id/edit را مقدار دهی کرده‌ایم.
    <a class="btn btn-primary" [routerLink]="['/products', product.id, 'edit']">
         Edit</a>
    در اینجا ابتدا root URL segment ذکر می‌شود. سپس پارامترهای متغیر مسیریابی و همچنین ثوابت آن مسیر خاص نیز باید ذکر شوند. اگر URL segment ثابت edit‌، در انتها ذکر نشود، این مسیر با تنظیم 'path: 'products/:id تطابق داده خواهد شد و نه با حالت 'path: 'products/:id/edit.

    به علاوه به فایل src\app\app.component.html نیز مراجعه کرده و لینکی را ذیل لینک لیست محصولات در منوی سایت، جهت افزودن یک محصول جدید اضافه می‌کنیم:
    <li><a [routerLink]="['/products', 0, 'edit']">Add Product</a></li>
    در اینجا عدد صفر را به عنوان پارامتر یا Id محصول جدید، به همان صفحه‌ی ویرایش اطلاعات یک محصول، ارسال کرده‌ایم. اگر به سرویس محصولات دقت کنید،
      saveProduct(product: IProduct): Observable<IProduct> {
        let headers = new Headers({ 'Content-Type': 'application/json' });
        let options = new RequestOptions({ headers: headers });
    
        if (product.id === 0) {
          return this.createProduct(product, options);
        }
        return this.updateProduct(product, options);
      }
    اگر id مساوی صفر باشد، یک محصول جدید ایجاد خواهد شد و اگر غیر صفر باشد، این محصول از پیش موجود، به روز رسانی می‌گردد.

    همچنین باید دقت داشت که تمام پارامترهای routerLink را با کدنویسی و در متد navigate نیز می‌توان بکار برد. برای مثال:
     this.router.navigate(['products', this.product.id]);


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

    تا اینجا لیست محصولات را نمایش دادیم و همچنین لینک‌هایی را به صفحات نمایش جزئیات، ویرایش و افزودن این محصولات، تعریف کردیم. مرحله‌ی بعد، پیاده سازی این کامپوننت‌ها است.
    مسیریاب Angular، پارامترهای هر مسیر را توسط سرویس ActivatedRoute استخراج کرده و در اختیار کامپوننت‌ها قرار می‌دهد. بنابراین برای دسترسی به آن‌ها تنها کافی است این سرویس را به سازنده‌ی کلاس کامپوننت خود تزریق کنیم. پس از آن دو روش برای خواندن اطلاعات مسیرجاری توسط این سرویس فراهم می‌شود:
    الف) استفاده از شیء this.route.snapshot که وضعیت آغازین مسیریابی را به همراه دارد. برای مثال جهت دسترسی به مقدار پارامتر id می‌توان به صورت ذیل عمل کرد:
     let id = +this.route.snapshot.params['id'];

    بنابراین ابتدا یک مسیریابی به همراه پارامتر یا پارامترهایی متغیر تعریف می‌شود:
     { path: 'products/:id', component: ProductDetailComponent }
    سپس این مسیریابی توسط لینک ذیل فعال می‌شود:
    <a [routerLink]="['/products', product.id]">{{product.productName}}</a>
    اکنون برای دریافت مقدار این پارامتر از URL جاری، می‌توان از this.route.snapshot.params['id'] استفاده کرد. این id دقیقا نام همان متغیری است که در تعریف مسیریابی ذکر شده‌است و حساس به کوچکی و بزرگی حروف می‌باشد.

    ب) این سرویس ویژه به همراه خاصیت this.route.params که یک Observable است نیز می‌باشد:
    this.route.params.subscribe(
             params => {
                let id = +params['id'];
                this.getProduct(id);
             }
          );
    هر زمان که پارامترهای مسیریابی تغییر کنند، این Observable به آن‌ها گوش فرا داده و برنامه را از این تغییرات مطلع می‌سازد.

    یک نکته:ذکر علامت + در اینجا (params['id']+) سبب تبدیل رشته‌ی دریافتی، به عدد می‌گردد.


    تکمیل کامپوننت نمایش جزئیات یک محصول

    در ادامه قالب ابتدایی مشاهده‌ی جزئیات یک محصول را که در فایل src\app\product\product-detail\product-detail.component.ts قرار دارد، گشوده و به نحو ذیل تکمیل کنید:
    import { Component, OnInit } from '@angular/core';
    import { ActivatedRoute } from '@angular/router';
    
    import { ProductService } from './../product.service';
    import { IProduct } from './../iproduct';
    
    @Component({
      selector: 'app-product-detail',
      templateUrl: './product-detail.component.html',
      styleUrls: ['./product-detail.component.css']
    })
    export class ProductDetailComponent implements OnInit {
      pageTitle = 'Product Detail';
      product: IProduct;
      errorMessage: string;
    
      constructor(private productService: ProductService,
        private route: ActivatedRoute) { }
    
      ngOnInit(): void {
        let id = +this.route.snapshot.params['id'];
        this.getProduct(id);
      }
    
      getProduct(id: number) {
        this.productService.getProduct(id).subscribe(
          product => this.product = product,
          error => this.errorMessage = <any>error);
      }
    }
    در این حالت اگر آدرس http://localhost:4200/products/1 توسط کاربر درخواست شود، نیاز است بتوان id=1 آن‌را از مسیرجاری استخراج کرد. به همین جهت سرویس ActivatedRoute به سازنده‌ی کلاس کامپوننت جزئیات محصول تزریق شده‌است. هرچند می‌توان از این سرویس در همان سازنده‌ی کلاس نیز استفاده کرد، اما انجام اعمال async آغازین یک کامپوننت بهتر است به ngOnInit منتقل شوند تا سبب تاخیری در آغاز و نمایش کامپوننت نگردند. این life cycle hook، پس از آغاز کامپوننت فراخوانی می‌گردد. به همین جهت ذکر implements OnInit را در قسمت تعریف کلاس مشاهده می‌کنید.
    در متد OnInit، مقدار id، از snapshot دریافت می‌گردد. سپس این id به متد getProduct ارسال می‌شود تا از RES Web API سرویس برنامه، جزئیات این محصول را واکشی کند و به خاصیت product نسبت دهد.


    برای تکمیل قالب این کامپوننت نیز فایل src\app\product\product-detail\product-detail.component.html را گشوده و به نحو ذیل تغییر دهید تا در آن بتوان از خاصیت عمومی product استفاده کرد:
    <div class="panel panel-primary" *ngIf="product"><div class="panel-heading" style="font-size:large">
        {{pageTitle + ": " + product.productName}}</div><div class="panel-body"><div>
          Name: {{product.productName}}</div><div>
          Code: {{product.productCode}}</div><div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div></div><div class="panel-footer"><a class="btn btn-default" [routerLink]="['/products']"><i class="glyphicon glyphicon-chevron-left"></i> Back</a><a class="btn btn-primary" style="width:80px;margin-left:10px" 
           [routerLink]="['/products', product.id, 'edit']">
           Edit</a></div></div>
    در اینجا علاوه بر استفاده از شیء product در جهت نمایش جزئیات محصول انتخابی، دو دکمه‌ی back و edit نیز اضافه شده‌اند که اولی صفحه‌ی لیست محصولات را مجددا نمایش می‌دهد و دومی کار هدایت به صفحه‌ی ویرایش جزئیات این محصول را میسر می‌کند.


    تکمیل کامپوننت ویرایش و افزودن جزئیات یک محصول

    از آنجائیکه قصد داریم به ماژول محصولات فرم جدیدی را اضافه کنیم، نیاز است به فایل src\app\product\product.module.ts مراجعه کرده و FormsModule را به قسمت imports آن اضافه کنیم:
    import { FormsModule } from '@angular/forms';
    
    @NgModule({
      imports: [
        CommonModule,
        FormsModule,
        ProductRoutingModule
      ]
    کد کامل کامپوننت ویرایش و افزودن جزئیات یک محصول به شرح ذیل است (فایل src\app\product\product-edit\product-edit.component.ts):
    import { Component, OnInit } from '@angular/core';
    import { ActivatedRoute, Router } from '@angular/router';
    
    import { ProductService } from './../product.service';
    import { IProduct } from './../iproduct';
    
    @Component({
      selector: 'app-product-edit',
      templateUrl: './product-edit.component.html',
      styleUrls: ['./product-edit.component.css']
    })
    export class ProductEditComponent implements OnInit {
      pageTitle = 'Product Edit';
      product: IProduct;
      errorMessage: string;
    
      constructor(private productService: ProductService,
        private route: ActivatedRoute,
        private router: Router) { }
    
      ngOnInit(): void {
        let id = +this.route.snapshot.params['id'];
        this.getProduct(id);
      }
    
      getProduct(id: number): void {
        this.productService.getProduct(id)
          .subscribe(
          (product: IProduct) => this.onProductRetrieved(product),
          (error: any) => this.errorMessage = <any>error
          );
      }
    
      onProductRetrieved(product: IProduct): void {
        this.product = product;
    
        if (this.product.id === 0) {
          this.pageTitle = 'Add Product';
        } else {
          this.pageTitle = `Edit Product: ${this.product.productName}`;
        }
      }
    
      deleteProduct(): void {
        if (this.product.id === 0) {
          // Don't delete, it was never saved.
          this.onSaveComplete();
        } else {
          if (confirm(`Really delete the product: ${this.product.productName}?`)) {
            this.productService.deleteProduct(this.product.id)
              .subscribe(
              () => this.onSaveComplete(`${this.product.productName} was deleted`),
              (error: any) => this.errorMessage = <any>error
              );
          }
        }
      }
    
      saveProduct(): void {
        if (true === true) {
          this.productService.saveProduct(this.product)
            .subscribe(
            () => this.onSaveComplete(`${this.product.productName} was saved`),
            (error: any) => this.errorMessage = <any>error
            );
        } else {
          this.errorMessage = 'Please correct the validation errors.';
        }
      }
    
      onSaveComplete(message?: string): void {
        if (message) {
          // TODO: show msg
        }
    
        // Navigate back to the product list
        this.router.navigate(['/products']);
      }
    }
    به همراه کد کامل قالب آن (فایل src\app\product\product-edit\product-edit.component.html):
    <div class="panel panel-primary"><div class="panel-heading">
        {{pageTitle}}</div><div class="panel-body" *ngIf="product"><form class="form-horizontal" novalidate (ngSubmit)="saveProduct()" #productForm="ngForm"><fieldset><div class="form-group" [ngClass]="{'has-error': (productNameVar.touched || 
                                                   productNameVar.dirty) && 
                                                   !productNameVar.valid }"><label class="col-md-2 control-label" for="productNameId">Product Name</label><div class="col-md-8"><input class="form-control" id="productNameId" type="text" placeholder="Name (required)" 
                       required minlength="3" [(ngModel)]=product.productName 
                       name="productName" #productNameVar="ngModel" /><span class="help-block" *ngIf="(productNameVar.touched ||
                                                             productNameVar.dirty) &&
                                                             productNameVar.errors"><span *ngIf="productNameVar.errors.required">
                        Product name is required.</span><span *ngIf="productNameVar.errors.minlength">
                        Product name must be at least three characters.</span></span></div></div><div class="form-group" [ngClass]="{'has-error': (productCodeVar.touched || 
                                                   productCodeVar.dirty) && 
                                                   !productCodeVar.valid }"><label class="col-md-2 control-label" for="productCodeId">Product Code</label><div class="col-md-8"><input class="form-control" id="productCodeId" type="text" placeholder="Code (required)" 
                       required [(ngModel)]=product.productCode
                       name="productCode" #productCodeVar="ngModel" /><span class="help-block" *ngIf="(productCodeVar.touched ||
                                                             productCodeVar.dirty) &&
                                                             productCodeVar.errors"><span *ngIf="productCodeVar.errors.required">
                         Product code 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]="!productForm.valid"
                             (click)="saveProduct()">
                             Save</button></span><span><a class="btn btn-default"
                        [routerLink]="['/products']">
                           Cancel</a></span><span><a class="btn btn-default"
                        (click)="deleteProduct()">
                         Delete</a></span></div></div></fieldset></form><div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div></div></div>

    توضیحات:

    نکته‌ی مهمی را که در این کدها می‌خواهیم بررسی کنیم، متد ngOnInit آن‌است:
    ngOnInit(): void {
        let id = +this.route.snapshot.params['id'];
        this.getProduct(id);
      }
    برنامه را یکبار توسط دستور ng server -o ساخته و اجرا کنید.
     - ابتدا لیست محصولات را مشاهده کنید.
     - سپس بر روی دکمه‌ی edit محصول شماره یک کلیک نمائید:


    تصویر فوق حاصل خواهد شد که صحیح است. اطلاعات مربوط به محصول یک از وب سرور آزمایشی برنامه واکشی شده و به شیء product نسبت داده شده‌است. همین انتساب سبب مقدار دهی فیلدهای فرم ویرایش اطلاعات گردیده‌است.
     - در ادامه بر روی لینک Add product در منوی بالای صفحه کلیک کنید:


    همانطور که مشاهده می‌کنید، هرچند URL صفحه تغییر کرده‌است، اما هنوز فرم ویرایش اطلاعات، به روز نشده و فیلدهای آن جهت درج یک محصول جدید خالی نشده‌اند. علت اینجا است که در متد ngOnInit، مقدار پارامتر id را از طریق شیء route.snapshot دریافت کرده‌ایم. اگر تنها پارامترهای URL تغییر کنند، کامپوننت مجددا آغاز نشده و متد ngOnInit فراخوانی نمی‌شود. در اینجا تغییر آدرس http://localhost:4200/products/1/edit به http://localhost:4200/products/0/edit به علت عدم تغییر  root URL segment آن یا همان products، سبب فراخوانی مجدد ngOnInit نمی‌شود. به همین جهت است که فیلدهای این فرم تغییر نکرده‌اند.
    برای حل این مشکل بجای دریافت پارامترهای مسیریابی از طریق شیء route.snapshot بهتر است به تغییرات آن‌ها گوش فرا دهیم. اینکار را از طریق متد route.params.subscribe می‌توان انجام داد:
      ngOnInit(): void {
        this.route.params.subscribe(
          params => {
            let id = +params['id'];
            this.getProduct(id);
          }
        );
      }
    در اینجا چون کامپوننت به علت نحوه‌ی تعریف مسیریابی آن مجددا آغاز نمی‌شود، شیء route.snapshot برای دسترسی به پارامترهای تغییر کرده‌ی مسیریابی، کارآیی لازم را نداشته و باید از روش دوم دسترسی به آن مقادیر که یک Observable است و به تغییرات پارامترها گوش فرا می‌دهد، استفاده کرد.

    یک نکته:هر زمانیکه از Observable‌ها استفاده می‌شود، نیاز است در پایان کار کامپوننت، unsubscribe آن نیز فراخوانی شود تا نشتی حافظه رخ ندهد. در اینجا یک سری استثناء هم وجود دارند، مانند this.route.params که به صورت خودکار توسط طول عمر سرویس ActivatedRoute مدیریت می‌شود.


    روش خواندن پارامترهای مسیریابی در +Angular 4

    روشی که تا به اینجا در مورد خواندن پارامترهای مسیریابی ذکر شد، با Angular 4 هم سازگار است. در Angular 4 روش دیگری را نیز اضافه کرده‌اند که توسط شیء paramMap مدیریت می‌شود:
        // For Angular 4+
        let id = +this.route.snapshot.paramMap.get('id');
        this.getProduct(id);
    
        // OR
        this.route.paramMap.subscribe(params => {
              let id = +params['id'];
              this.getProduct(id);
            });
    در اینجا دو روش دسترسی به پارامتر id را مشاهده می‌کنید. در حالت کار با snapshot متد paramMap.get اینبار یک رشته را قبول می‌کند و یا بجای params می‌توان از paramMap استفاده کرد.


    تعریف پارامترهای اختیاری مسیریابی

    فرض کنید یک صفحه‌ی جستجو را طراحی کرده‌اید که در آن می‌توان نام و کد محصول را جستجو کرد. سپس می‌خواهیم این پارامترها را به صفحه‌ی نمایش لیست محصولات هدایت کنیم. برای طراحی این نوع مسیریابی می‌توان از مطالبی که تاکنون گفته شد استفاده کرد و پارامترهایی اجباری را جهت مسیریابی تعریف نمود:
     { path: 'products/:name/:code', component: ProductListComponent }
    و سپس می‌توان یک چنین لینکی را جهت فعالسازی آن اضافه کرد:
     [routerLink]="['/products', productName, productCode]"
    این روش به همراه URLهایی ناخوانا خواهد بود که قسمت‌های مختلف آن مشخص نیستند و هر بار که قرار باشد گزینه‌ی دیگری را به جستجو اضافه کرد، نیاز است این پارامترها را نیز تغییر داد. همچنین در حین جستجو ممکن است تعدادی از فیلدها اختیاری باشند و نه اجباری. برای حل این مشکل امکان تعریف پارامترهای اختیاری مسیریابی نیز پیش بینی شده‌است. دراین حالت تعریف مسیریابی صفحه‌ی نمایش لیست محصولات به صورت ذیل خواهد بود (بدون ذکر هیچ پارامتری):
     { path: 'products', component: ProductListComponent },
    و سپس لینکی که به آن تعریف می‌شود، نحوه‌ی تعریف خاصی را خواهد داشت:
     [routerLink]="['/products', {name: productName, code: productCode}]"
    در اینجا پارامترهای اختیاری به صورت یک سری key/value در آرایه‌ی پارامترهای مسیریابی مشخص می‌شوند و هربار که نیاز به تغییر آن‌ها بود، نیازی نیست تا تعریف مسیریابی اصلی مرتبط را تغییر داد. باید دقت داشت که پارامترهای اختیاری باید همواره پس از پارامترهای اجباری در این آرایه، ذکر شوند.
    در این حالت لینک تولید شده چنین شکلی را خواهد داشت:
     http://localhost:4200/products;name=Controller;code=gmg
    نحوه‌ی خواندن این پارامترها، دقیقا همانند نحوه‌ی خواندن پارامترهای اجباری هستند و در اینجا از نام key‌ها برای اشاره‌ی به آن‌ها استفاده می‌شود:
    let name = this.route.snapshot.params['name'];
    let code = this.route.snapshot.params['code'];

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


    تعریف پارامترهای کوئری در مسیریابی

    فرض کنید لیست محصولات را بر اساس پارامتر یا پارامترهایی فیلتر کرده‌اید. اکنون اگر بر روی لینک مشاهده‌ی جزئیات یک محصول یافت شده کلیک کنید و سپس مجددا به لیست فیلتر شده بازگردید، تمام پارامترهای انتخابی پاک شده و لیست از ابتدا نمایش داده می‌شود. در یک چنین حالتی برای بهبود تجربه‌ی کاربری، بهتر است پارامترهای جستجو شده را در طی هدایت کاربر به قسمت‌های مختلف حفظ کرد:
     http://localhost:42000/products/5?filterBy=product1
    در این لینک جزئیات محصول 5 نمایش داده خواهد شد. پس از عدد 5، پارامترهای کوئری ذکر شده‌اند و برخلاف پارامترهای اختیاری مسیریابی، در بین مسیریابی و هدایت کاربران به صفحات مختلف، حفظ خواهند شد.
    در اینجا نیز برای تعریف یک چنین قابلیتی، مسیریابی ابتدایی تعریف شده، همانند قبل خواهد بود و در آن خبری از پارامترهای کوئری نیست:
     { path: 'products', component: ProductListComponent}
    اعمال پارامترهای کوئری مختلف، به لینک‌های تعریف شده، توسط دایرکتیو queryParams صورت می‌گیرد و در اینجا یک مجموعه‌ی از key/valueها ذکر خواهند شد:
    <a [routerLink] = "['/products', product.id]"
         [queryParams] = "{ filterBy: 'er', showImage: true }">
    {{product.productName}}</a>
    در این مثال یک ثابت دلخواه er مشخص شده‌است. بدیهی است می‌توان متغیری را نیز بجای این ثابت تعریف کرد (یک خاصیت عمومی تعریف شده‌ی در سطح کامپوننت که به تکست‌باکس جستجو متصل است).

    و یا با کدنویسی به صورت ذیل است:
    this.router.navigate(['/products'],
       {
           queryParams: { filterBy: 'er', showImage: true}
       }
    );
    باید دقت داشت که چون این پارامترهای کوئری در بین مسیریابی به صفحات مختلف حفظ می‌شوند، نباید کلیدهای انتخاب شده‌ی در اینجا با سایر کلیدهای موجود در صفحات دیگر تداخل پیدا کنند.

    یک مشکل:اگر در صفحه‌ی نمایش جزئیات یک محصول، دکمه‌ی Back وجود داشته باشد، با کلیک بر روی آن پارامترهای کوئری پاک خواهند شد و دیگر حفظ نمی‌شوند. مرحله‌ی آخر حفظ پارامترهای کوئری در بین صفحات مختلف تنظیم ذیل است:
     [preserveQueryParams] = "true"
    یعنی دکمه‌ی back به این شکل تغییر می‌کند:
    <a class="btn btn-default"
               [routerLink]="['/products']"
               [preserveQueryParams]="true"><i class="glyphicon glyphicon-chevron-left"></i> Back</a>
    و یا استفاده از { preserveQueryParams: true} در حین کدنویسی.
    که البته در Angular 4 مورد اول به "queryParamsHandling= "preserve و مورد دوم به { 'queryParamsHandling: 'preserve} تغییر یافته‌است و حالت قبلی منسوخ شده اعلام گردیده‌است.
    this.router.navigate(['/products'],
       { queryParamsHandling: 'preserve'}
    );

    پس از بازگشت به صفحه‌ی اصلی لیست محصولات، هرچند این پارامترهای کوئری حفظ شده‌اند، اما مجددا به صورت خودکار پردازش نخواهند شد. برای خواندن آن‌ها در متد ngOnInit باید به صورت ذیل عمل کرد:
    var filter = this.route.snapshot.queryParams['filterBy'] || '';
    var showImage = this.route.snapshot.queryParams['showImage'] === 'true';
    علت تعریف '' || این است که ممکن است filterBy تعریف نشده باشد (برای حالتی که صفحه برای بار اول نمایش داده می‌شود) و دلیل تعریف 'true' === این است که مقادیر دریافتی در اینجا رشته‌ای هستند و نه boolean. به همین جهت باید با رشته‌ی true مقایسه شوند.

    در مثال تکمیل شده‌ی جاری، امکان فیلتر کردن جدول با اضافه کردن یک pipe جدید به نام ProductFilter میسر شده‌است:
    >ng g p product/ProductFilter
    فایل src\app\product\product-filter.pipe.ts با این محتوا:
    import { PipeTransform, Pipe } from '@angular/core';
    import { IProduct } from './iproduct';
    
    @Pipe({
      name: 'productFilter'
    })
    export class ProductFilterPipe implements PipeTransform {
      transform(value: IProduct[], filterBy: string): IProduct[] {
        filterBy = filterBy ? filterBy.toLocaleLowerCase() : null;
        return filterBy ? value.filter((product: IProduct) =>
          product.productName.toLocaleLowerCase().indexOf(filterBy) !== -1) : value;
      }
    }
    و سپس تعریف تکست باکس فیلتر کردن در ابتدای قالب src\app\product\product-list\product-list.component.html :
    <div class="panel-body"><div class="row"><div class="col-md-2">Filter by:</div><div class="col-md-4"><input type="text" [(ngModel)]="listFilter" /></div></div><div class="row" *ngIf="listFilter"><div class="col-md-6"><h3>Filtered by: {{listFilter}} </h3></div></div>
    و اعمال این فیلتر به حلقه‌ی نمایش ردیف‌های جدول؛ به همراه تعریف پارامتر کوئری:
    <tr *ngFor="let product of products | productFilter:listFilter"><td><a [routerLink]="['/products', product.id]"
                       [queryParams]="{filterBy: listFilter}">
                      {{product.productName}}</a></td>


    در اینجا اگر به صفحه‌ی جزئیات محصول فیلتر شده مراجعه کنیم، این فیلتر حفظ خواهد شد:


    و در این حالت اگر بر روی دکمه‌ی back کلیک کنیم، مجددا فیلتر وارد شده بازیابی شده و نمایش داده می‌شود:


    که برای میسر شدن آن ابتدا خاصیت عمومی listFilter به کامپوننت لیست محصولات اضافه گردیده و سپس در ngOnInit، این پارامتر در صورت وجود، از سیستم مسیریابی دریافت می‌شود:
    @Component({
      selector: 'app-product-list',
      templateUrl: './product-list.component.html',
      styleUrls: ['./product-list.component.css']
    })
    export class ProductListComponent implements OnInit {
      listFilter: string;
    
      ngOnInit(): void {
        this.listFilter = this.route.snapshot.queryParams['filterBy'] || '';


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

    ‫مسیریابی در Angular - قسمت چهارم - پیش واکشی اطلاعات

    $
    0
    0
    اگر مثال قسمت قبلرا اجرا کرده باشید، حتما شاهد این تجربه‌ی ناخوشایند کاربری بوده‌اید:
    با کلیک بر روی لینک منوی نمایش لیست محصولات، ابتدا قاب خالی لیست محصولات نمایش داده می‌شود:


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


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

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


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

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

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


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

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

     فایل src\app\product\product-resolver.service.ts را به نحو ذیل تکمیل کنید:
    import { Injectable } from '@angular/core';
    import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
    
    import { Observable } from 'rxjs/Observable';
    import 'rxjs/add/operator/catch';
    import 'rxjs/add/observable/of';
    import 'rxjs/add/operator/map';
    
    import { ProductService } from './product.service';
    import { IProduct } from './iproduct';
    
    @Injectable()
    export class ProductResolverService implements Resolve<IProduct>  {
    
      constructor(private productService: ProductService,
        private router: Router) { }
    
      resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<IProduct> {
        let id = route.params['id'];
        if (isNaN(id)) {
          console.log(`Product id was not a number: ${id}`);
          this.router.navigate(['/products']);
          return Observable.of(null);
        }
    
        return this.productService.getProduct(+id)
          .map(product => {
            if (product) {
              return product;
            }
            console.log(`Product was not found: ${id}`);
            this.router.navigate(['/products']);
            return null;
          })
          .catch(error => {
            console.log(`Retrieval error: ${error}`);
            this.router.navigate(['/products']);
            return Observable.of(null);
          });
      }
    }
    توضیحات:
    مرحله‌ی اول تعریف یک سرویس Route Resolver، پیاده سازی اینترفیس جنریک Resolve است:
     export class ProductResolverService implements Resolve<IProduct>  {
    پارامتر جنریک Resolve، نوع اطلاعاتی را که دریافت می‌کند، مشخص خواهد کرد.
    این اینترفیس پیاده سازی متد resolve را با امضایی که مشاهده می‌کنید، درخواست می‌کند:
    resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<IProduct> {
    در اینجا ActivatedRouteSnapshot حاوی اطلاعاتی است از مسیریابی فعال شده. برای مثال اطلاعاتی مانند پارامترهای مسیریابی را می‌توان از آن دریافت کرد.
    RouterStateSnapshot وضعیت مسیریاب را در این لحظه در اختیار این سرویس قرار می‌دهد.
    خروجی این متد یک Observable است؛ از نوع اطلاعاتی که دریافت می‌کند. زمانیکه مسیریابی فعال می‌شود، متد resolve را فراخوانی کرده و منتظر پایان کار Observable آن می‌شود. پس از آن است که کامپوننت این مسیریابی را فعالسازی خواهد کرد.

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

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


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

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

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

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

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

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

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


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

    ‫مسیریابی در Angular - قسمت پنجم - تعریف Child Routes

    $
    0
    0
    در Angular امکان تعریف مسیریابی‌هایی، درون سایر مسیریابی‌ها نیز پیش بینی شده‌است. با استفاده از مفهوم Child Routes، امکان تعریف سلسله مراتب مسیریابی‌ها جهت ساماندهی و مدیریت مسیریابی درون برنامه، وجود دارد. همچنین lazy loading مسیریابی‌ها را نیز ساده‌تر کرده و کارآیی آغاز برنامه را بهبود می‌بخشند.


    علت نیاز به Child Routes
     
    در مثال این سری، منوی اصلی آن به صورت ذیل تعریف شده‌است:
    <ul class="nav navbar-nav"><li><a [routerLink]="['/home']">Home</a></li><li><a [routerLink]="['/products']">Product List</a></li><li><a [routerLink]="['/products', 0, 'edit']">Add Product</a></li>      </ul>
    سپس از دایرکتیو router-outlet جهت تعریف محل قرارگیری محتوای این مسیریابی‌ها استفاده شده‌است:
    <div class="container"><router-outlet></router-outlet></div>
    هربار که مسیری تغییر می‌کند، محتوای router-outlet با محتوای قالب آن کامپوننت جایگزین خواهد شد. اما اگر تعداد المان‌های صفحه‌ی ویرایش محصولات بیش از اندازه بودند و خواستیم فیلدهای آن‌را به دو برگه (tab) تقسیم کنیم چطور؟ برای اینکار نیاز است تا router-outlet ثانویه و مخصوص این قالب را تعریف کنیم. هربار که کاربری بر روی برگه‌ای کلیک می‌کند، به کمک Child routes، محتوای آن برگه را در این router-outlet ثانویه نمایش می‌دهیم. به این ترتیب به کمک Child routes می‌توان امکان نمایش محتوای مسیریابی دیگری را درون مسیریابی اصلی، میسر کرد.

    کاربردهای Child routes
     - امکان تقسیم فرم‌های طولانی به چند Tab
     - امکان طراحی طرحبندی‌های Master/Layout
     - قرار دادن قالب یک کامپوننت، درون قالب کامپوننتی دیگر
     - بهبود کپسوله سازی ماژول‌های برنامه
     - جزو الزامات Lazy loading هستند


    تنظیم کردن Child Routes

    مثال جاری این سری، تنها به همراه یک سری primary routes است؛ مانند صفحه‌ی خوش‌آمد گویی، نمایش لیست محصولات، افزودن و ویرایش محصولات. قالب‌های کامپوننت‌های این‌ها نیز در router-outlet اصلی برنامه نمایش داده می‌شوند. در ادامه می‌خواهیم کامپوننت ویرایش محصولات را تغییر داده و تعدادی برگه را به آن اضافه کنیم. برای اینکار، نیاز به تعریف Child routes است تا بتوان قالب‌های کامپوننت‌های هر برگه را در router-outlet کامپوننت والد که در درون router-outlet اصلی برنامه قرار دارد، نمایش داد.
    به همین جهت دو کامپوننت جدید ProductEditInfo و ProductEditTags را نیز به ماژول محصولات اضافه می‌کنیم:
    >ng g c product/ProductEditInfo>ng g c product/ProductEditTags
    این دستورات سبب به روز رسانی فایل src\app\product\product.module.ts، جهت تکمیل قسمت declarations آن نیز خواهند شد.

    به علاوه اینترفیس src\app\product\iproduct.ts را نیز جهت افزودن گروه محصولات و همچنین آرایه‌ی برچسب‌های یک محصول تکمیل می‌کنیم:
    export interface IProduct {
        id: number;
        productName: string;
        productCode: string;
        category: string;
        tags?: string[];
    }
    در این حالت می‌توانید فایل app\product\product-data.ts را نیز ویرایش کرده و به هر محصول، تعدادی گروه و برچسب را نیز انتساب دهید؛ که البته ذکر tags آن اختیاری است. در اینجا فایل src\app\product\product.service.ts نیز باید ویرایش شده و متد initializeProduct آن تعاریف []:category: null, tags را نیز پیدا کنند.

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


    تعیین محل نمایش Child Views

    برای نمایش قالب یک Child Route درون قالب والد آن، نیاز به تعریف یک دایرکتیو router-outlet جدید، درون قالب والد است و نحوه‌ی تعریف آن با primary outlet تعریف شده‌ی در فایل src\app\app.component.html تفاوتی ندارد.
    برای پیاده سازی این مفهوم، نیاز است از قالب ویرایش محصولات و یا فایل src\app\product\product-edit\product-edit.component.html که قالب والد این Child Routes است شروع و آن‌را به دو Child View تقسیم کنیم. این قالب، تاکنون حاوی فرمی جهت ویرایش و افزودن محصولات است. در ادامه می‌خواهیم بجای آن چند برگه را نمایش دهیم. به همین جهت این فرم را حذف کرده و با دو برگه‌ی جدید جایگزین می‌کنیم. در اینجا نحوه‌ی تعریف لینک‌های جدید، به Child Routes و همچنین محل قرارگیری router-outlet ثانویه را نیز مشاهده می‌کنید:
    <div class="panel panel-primary"><div class="panel-heading">
            {{pageTitle}}</div><div class="panel-body" *ngIf="product"><div class="wizard"><a [routerLink]="['info']">
                    Basic Information</a><a [routerLink]="['tags']">
                    Search Tags</a></div><router-outlet></router-outlet></div><div class="panel-footer"><div class="row"><div class="col-md-6 col-md-offset-2"><span><button class="btn btn-primary"
                                type="button"
                                style="width:80px;margin-right:10px"
                                [disabled]="!isValid()"
                                (click)="saveProduct()">
                            Save</button></span><span><a class="btn btn-default"
                            [routerLink]="['/products']">
                            Cancel</a></span><span><a class="btn btn-default"
                            (click)="deleteProduct()">
                            Delete</a></span></div></div></div><div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div></div>
    تا اینجا اگر برنامه را توسط دستور ng s -o اجرا کنید، صفحه‌ی ویرایش محصول اول، چنین شکلی را پیدا کرده‌است:




    فعالسازی Child Routes

    دو روش برای فعالسازی Child Routes وجود دارند:
    الف) با ذکر مسیر مطلق
    <a [routerLink]="['/products',product.id,'edit','info']">Info</a>
    در این حالت تمام URL segments این مسیر باید به عنوان پارامترهای لینک قید شوند.

    ب) با ذکر مسیر نسبی
    <a [routerLink]="['info']">Info</a>
    این مسیر از URL segment جاری شروع می‌شود و نباید در حین تعریف آن از / استفاده کرد. اگر از / استفاده شود، معنای ذکر مسیری مطلق را می‌دهد.
    در این حالت اگر تنظیمات والد این مسیریابی تغییر کنند، نیازی به تغییر مسیر نسبی تعریف شده نیست (برخلاف حالت مطلق که بر اساس قید کامل تمام اجزای مسیریابی والد آن کار می‌کند).

    دقیقا همین پارامترها، قابلیت استفاده‌ی در متد this.route.navigate را نیز دارند:
    الف) برای حالت ذکر مسیر مطلق:
     this.router.navigate(['/products', this.product.id,'edit','info']);
    ب) و برای حالت ذکر مسیر نسبی:
     this.router.navigate(['info', { relativeTo: this.route }]);
    در حالت ذکر مسیر نسبی، نیاز است پارامتر اضافه‌ی دیگری را جهت مشخص سازی مسیریابی والد نیز قید کرد.


    تکمیل Child Viewهای برنامه

    تا اینجا لینک‌هایی نسبی را به مسیریابی‌های info و tags اضافه کردیم. در ادامه قالب‌ها و کامپوننت‌های آن‌ها را تکمیل می‌کنیم:
    الف) تکمیل کامپوننت ProductEditInfoComponent در فایل src\app\product\product-edit\product-edit.component.ts
    import { ActivatedRoute } from '@angular/router';
    import { NgForm } from '@angular/forms';
    import { Component, OnInit, ViewChild } from '@angular/core';
    
    import { IProduct } from './../iproduct';
    
    @Component({
      //selector: 'app-product-edit-info',
      templateUrl: './product-edit-info.component.html',
      styleUrls: ['./product-edit-info.component.css']
    })
    export class ProductEditInfoComponent implements OnInit {
    
      @ViewChild(NgForm) productForm: NgForm;
    
      errorMessage: string;
      product: IProduct;
    
      constructor(private route: ActivatedRoute) { }
    
      ngOnInit(): void {
        this.route.parent.data.subscribe(data => {
          this.product = data['product'];
    
          if (this.productForm) {
            this.productForm.reset();
          }
        });
      }
    }
    با قالب src\app\product\product-edit\product-edit.component.html که در حقیقت همان فرمی است که از کامپوننت والد حذف کردیم و به اینجا منتقل شده‌است:
    <div class="panel-body"><form class="form-horizontal"
              novalidate
              #productForm="ngForm"><fieldset><legend>Basic Product Information</legend><div class="form-group" 
                        [ngClass]="{'has-error': (productNameVar.touched || 
                                                  productNameVar.dirty || product.id !== 0) && 
                                                  !productNameVar.valid }"><label class="col-md-2 control-label" 
                            for="productNameId">Product Name</label><div class="col-md-8"><input class="form-control" 
                                id="productNameId" 
                                type="text" 
                                placeholder="Name (required)"
                                required
                                minlength="3"
                                [(ngModel)] = product.productName
                                name="productName"
                                #productNameVar="ngModel" /><span class="help-block" *ngIf="(productNameVar.touched ||
                                                         productNameVar.dirty || product.id !== 0) &&
                                                         productNameVar.errors"><span *ngIf="productNameVar.errors.required">
                                Product name is required.</span><span *ngIf="productNameVar.errors.minlength">
                                Product name must be at least three characters.</span></span></div></div><div class="form-group" 
                        [ngClass]="{'has-error': (productCodeVar.touched || 
                                                  productCodeVar.dirty || product.id !== 0) && 
                                                  !productCodeVar.valid }"><label class="col-md-2 control-label" for="productCodeId">Product Code</label><div class="col-md-8"><input class="form-control" 
                                id="productCodeId" 
                                type="text" 
                                placeholder="Code (required)"
                                required
                                [(ngModel)] = product.productCode
                                name="productCode"
                                #productCodeVar="ngModel" /><span class="help-block" *ngIf="(productCodeVar.touched ||
                                                         productCodeVar.dirty || product.id !== 0) &&
                                                         productCodeVar.errors"><span *ngIf="productCodeVar.errors.required">
                                Product code is required.</span></span></div></div>           <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div></fieldset></form></div>


    ب) تکمیل کامپوننت ProductEditTagsComponent در فایل src\app\product\product-edit-tags\product-edit-tags.component.ts
    import { ActivatedRoute } from '@angular/router';
    import { IProduct } from './../iproduct';
    import { Component, OnInit } from '@angular/core';
    
    @Component({
      //selector: 'app-product-edit-tags',
      templateUrl: './product-edit-tags.component.html',
      styleUrls: ['./product-edit-tags.component.css']
    })
    export class ProductEditTagsComponent implements OnInit {
    
      errorMessage: string;
      newTags = '';
      product: IProduct;
    
      constructor(private route: ActivatedRoute) { }
    
      ngOnInit(): void {
        this.route.parent.data.subscribe(data => {
          this.product = data['product'];
        });
      }
    
      // Add the defined tags
      addTags(): void {
        let tagArray = this.newTags.split(',');
        this.product.tags = this.product.tags ? this.product.tags.concat(tagArray) : tagArray;
        this.newTags = '';
      }
    
      // Remove the tag from the array of tags.
      removeTag(idx: number): void {
        this.product.tags.splice(idx, 1);
      }
    }
    با قالب src\app\product\product-edit-tags\product-edit-tags.component.html
    <div class="panel-body"><form class="form-horizontal"
              novalidate><fieldset><legend>Product Search Tags</legend><div class="form-group" 
                        [ngClass]="{'has-error': (categoryVar.touched || 
                                                  categoryVar.dirty || product.id !== 0) && 
                                                  !categoryVar.valid }"><label class="col-md-2 control-label" for="categoryId">Category</label><div class="col-md-8"><input class="form-control" 
                               id="categoryId" 
                               type="text"
                               placeholder="Category (required)"
                               required
                               minlength="3"
                               [(ngModel)]="product.category"
                               name="category"
                               #categoryVar="ngModel" /><span class="help-block" *ngIf="(categoryVar.touched ||
                                                         categoryVar.dirty || product.id !== 0) &&
                                                         categoryVar.errors"><span *ngIf="categoryVar.errors.required">
                                A category must be entered.</span><span *ngIf="categoryVar.errors.minlength">
                                The category must be at least 3 characters in length.</span></span></div></div><div class="form-group" 
                        [ngClass]="{'has-error': (tagVar.touched || 
                                                  tagVar.dirty || product.id !== 0) && 
                                                  !tagVar.valid }"><label class="col-md-2 control-label" for="tagsId">Search Tags</label><div class="col-md-8"><input class="form-control" 
                               id="tagsId" 
                               type="text"
                               placeholder="Search keywords separated by commas"
                               minlength="3"
                               [(ngModel)]="newTags"
                               name="tags"
                               #tagVar="ngModel" /><span class="help-block" *ngIf="(tagVar.touched ||
                                                         tagVar.dirty || product.id !== 0) &&
                                                         tagVar.errors"><span *ngIf="tagVar.errors.minlength">
                                The search tag must be at least 3 characters in length.</span></span></div><div class="col-md-1"><button type="button"
                                class="btn btn-default"
                                (click)="addTags()">
                            Add</button></div></div><div class="row col-md-8 col-md-offset-2"><span *ngFor="let tag of product.tags; let i = index"><button class="btn btn-default"
                                style="font-size:smaller;margin-bottom:12px"
                                (click)="removeTag(i)">
                            {{tag}}<span class="glyphicon glyphicon-remove"></span></button></span></div><div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div></fieldset></form></div>



    دریافت اطلاعات جهت Child Routes

    روش‌های متعددی برای دریافت اطلاعات جهت Child Routes وجود دارند:
    الف) می‌توان از متد this.productService.getProduct جهت دریافت اطلاعات یک محصول استفاده کرد. اما همانطور که در قسمت قبلنیز بررسی کردیم، این روش سبب نمایش ابتدایی یک قالب خالی و پس از مدتی، نمایش اطلاعات آن می‌شود.
    ب) می‌توان توسط this.route.snapshot.data['product'] اطلاعات را از Route Resolver، پس از پیش واکشی آن‌ها از وب سرور، دریافت کرد.
    ج) اگر قسمت‌های مختلف Child Routes قرار است با اطلاعاتی یکسان کار کنند که قرار است بین برگه‌های مختلف آن به اشتراک گذاشته شوند، این اطلاعات را می‌توانند از Route Resolver والد خود به کمک this.route.snapshot.data['product'] دریافت کنند.
    در این مثال ما هرچند چندین برگه‌ی مختلف را طراحی کرده‌ایم، اما اطلاعات نمایش داده شده‌ی توسط آن‌ها متعلق به یک شیء محصول می‌باشند. بنابراین نیاز است بتوان این اطلاعات را بین کامپوننت‌های مختلف این Child Routes به اشتراک گذاشت و تنها با یک وهله‌ی آن کار کرد. به همین جهت با this.route.parentدر هر یک از Child Components تعریف شده کار می‌کنیم تا بتوان به یک وهله‌ی شیء محصول، دسترسی یافت.
    د) همچنین می‌توان از روش this.route.parent.data.subscribe نیز استفاده کرد. البته در اینجا چون صفحه‌ی افزودن محصولات با صفحه‌ی ویرایش محصولات، دارای root URL Segment یکسانی است، نیاز است از این روش استفاده کرد تا بتوان از تغییرات بعدی پارامتر id آن مطلع شد. این مورد روشی است که در کدهای ProductEditInfoComponent مشاهده می‌کنید.
    ngOnInit(): void {
        this.route.parent.data.subscribe(data => {
          this.product = data['product'];
    
          if (this.productForm) {
            this.productForm.reset();
          }
        });
      }
    در اینجا data['product'] به key/value تعریف شده‌ی resolve: { product: ProductResolverService } در تنظیمات مسیریابی اشاره می‌کند که آن‌را در قسمت قبلتکمیل کردیم.
    شبیه به همین روش را در ProductEditTagsComponent نیز بکار گرفته‌ایم و در آنجا نیز با شیء  this.route.parent و دسترسی به اطلاعات دریافتی از Route Resolver، کار می‌کنیم. به این ترتیب مطمئن خواهیم شد که  this.product این دو کامپوننت مختلف، هر دو به یک وهله از شیء product دریافتی از سرور، اشاره می‌کنند.
    به این ترتیب دکمه‌ی Save ذیل هر دو برگه، به درستی عمل کرده و می‌تواند اطلاعات نهایی یک شیء محصول را ذخیره کند.


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

    علت استفاده‌ی از ViewChild در ProductEditInfoComponent
     @ViewChild(NgForm) productForm: NgForm;
    که به فرم قالب آن اشاره می‌کند:
    <form class="form-horizontal" novalidate
    #productForm="ngForm">
    این است که بتوان متد this.productForm.reset آن‌را پس از هربار دریافت اطلاعات از سرور، فراخوانی کرد. این متد نه تنها اطلاعات آن‌را پاک می‌کند، بلکه خطاهای اعتبارسنجی آن‌را نیز به حالت نخست برمی‌گرداند. بنابراین در این حالت اگر سبب بروز یک خطای اعتبارسنجی، در فرم ویرایش اطلاعات شویم و در همان لحظه صفحه‌ی افزودن یک محصول جدید را درخواست کنیم، کاربر همان خطای اعتبارسنجی قبلی را مجددا مشاهده نکرده و یک فرم از ابتدا آغاز شده را مشاهده می‌کند.
    انجام اینکار برای برگه‌‌های دوم به بعد ضروری نیست. از این جهت که با اولین بار نمایش این صفحه، تمام آن‌ها از حافظه خارج می‌شوند و مجددا بازیابی خواهند شد.

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

    در اولین بار نمایش Child Routes، کامپوننت ویرایش اطلاعات در router-outlet آن نمایش داده می‌شود. در این حالت اگر کاربر بر روی لینک نمایش کامپوننت edit tags کلیک کند، قالب کامپوننت edit info به طور کامل از router-outlet حذف می‌شود و با قالب کامپوننت edit tags جایگزین می‌شود. این فرآیند به این معنا است که فرم edit info به همراه تمام اطلاعات اعتبارسنجی آن unload می‌شوند. به همین ترتیب زمانیکه کاربر درخواست نمایش برگه‌ی ویرایش اطلاعات را می‌کند، قالب edit tags و اطلاعات اعتبارسنجی آن unload می‌شوند. به این معنا که در یک router-outlet در هر زمان تنها یک فرم، به همراه اطلاعات اعتبارسنجی آن در دسترس هستند.
    راه حل‌‌های ممکن:
    الف) بدنه‌ی اصلی فرم را در کامپوننت والد قرار دهیم و سپس هر کدام از فرزندان، المان‌های فرم‌های مرتبط را ارائه دهند. این روش کار نمی‌کند چون Angular المان‌های فرم‌های قرار گرفته‌ی درون router-outlet را شناسایی نمیکند.
    ب) قرار دادن فرم‌ها، به صورت مجزا در هر کامپوننت فرزند (مانند روش فعلی) و سپس اعتبارسنجی دستی در کامپوننت والد.
    تغییرات مورد نیاز کامپوننت ProductEditComponent را جهت افزودن اعتبارسنجی فرم‌های فرزند آن‌را در اینجا ملاحظه می‌کنید:
    export class ProductEditComponent implements OnInit {
      private dataIsValid: { [key: string]: boolean } = {};
    
      isValid(path: string): boolean {
        this.validate();
        if (path) {
          return this.dataIsValid[path];
        }
        return (this.dataIsValid &&
          Object.keys(this.dataIsValid).every(d => this.dataIsValid[d] === true));
      }
    
      saveProduct(): void {
        if (this.isValid(null)) {
          this.productService.saveProduct(this.product)
            .subscribe(
            () => this.onSaveComplete(`${this.product.productName} was saved`),
            (error: any) => this.errorMessage = <any>error
            );
        } else {
          this.errorMessage = 'Please correct the validation errors.';
        }
      }
    
      validate(): void {
        // Clear the validation object
        this.dataIsValid = {};
    
        // 'info' tab
        if (this.product.productName &&
          this.product.productName.length >= 3 &&
          this.product.productCode) {
          this.dataIsValid['info'] = true;
        } else {
          this.dataIsValid['info'] = false;
        }
    
        // 'tags' tab
        if (this.product.category &&
          this.product.category.length >= 3) {
          this.dataIsValid['tags'] = true;
        } else {
          this.dataIsValid['tags'] = false;
        }
      }
    }
     - در اینجا dataIsValid، به صورت key/value تعریف شده‌است که در آن key، مسیر یک برگه و مقدار آن، معتبر بودن یا غیرمعتبر بودن وضعیت اعتبارسنجی آن است.
     - سپس متد validate اضافه شده‌است تا کار اعتبارسنجی را انجام دهد. در اینجا از خود شیء this.product که بین دو برگه به اشتراک گذاشته شده‌است برای انجام اعتبارسنجی استفاده می‌کنیم. از این جهت که برگه‌ها نیز با استفاده از  this.route.parent.data، دقیقا به همین وهله دسترسی دارند. بنابراین هرتغییری که در برگه‌ها بر روی این وهله اعمال شود، به کامپوننت والد نیز منعکس می‌شود.
     - متد isValid، مسیر هر برگه را دریافت می‌کند و سپس به متغیر dataIsValid مراجعه کرده و وضعیت آن برگه را باز می‌گرداند. اگر path در اینجا قید نشود، وضعیت تمام برگه‌ها بررسی می‌شوند؛ مانند if (this.isValid(null)) در متد ذخیره سازی اطلاعات.
     - در آخر در فایل product-edit.component.html، وضعیت فعال و غیرفعال دکمه‌ی ثبت را نیز به این متد متصل می‌کنیم:
    <button class="btn btn-primary"
                                type="button"
                                style="width:80px;margin-right:10px"
                                [disabled]="!isValid()"
                                (click)="saveProduct()">
          Save</button>


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

    ‫مسیریابی در Angular - قسمت ششم - گروه بندی مسیریابی‌ها

    $
    0
    0
    همانطور که در قسمت قبلمشاهده کردیم، از تعریف Child Routes برای میسر ساختن نمایش قالب‌های کامپوننت‌ها، در درون سایر قالب‌های کامپوننت‌ها، استفاده می‌شود. برای نمونه قالب‌های برگه‌های یک فرم ویرایش اطلاعات را با تعریف یک router-outlet دیگر، در درون قالب والد آن‌ها نمایش دادیم. اما شاید بخواهیم کار گروه بندی مسیریابی‌ها را بدون افزودن یک router-outlet دیگر انجام دهیم. برای این منظور می‌توان مسیریابی‌های کامپوننت‌های نمایش لیست محصولات، جزئیات یک محصول و ویرایش یک محصول را ذیل یک والد که هیچ کامپوننتی ندارد، گروه بندی کرد. به همین جهت به router-outlet اضافه‌تری نیاز ندارد و به آن component-less routes نیز گفته می‌شود.


    علت نیاز به گروه بندی مسیریابی‌ها در ذیل یک مسیریابی بدون کامپوننت

    علت وجود امکان گروه بندی مسیریابی‌ها، در ذیل یک مسیریابی بدون کامپوننت به شرح زیر هستند:
     - امکان مدیریت و ساماندهی ساده‌تر مسیریابی‌ها با افزایش حجم برنامه
     - امکان به اشتراک گذاری Route Resolvers و محافظت کننده‌های از مسیرها
     - ممکن ساختن امکان lazy loading آن گروه


    گروه بندی مسیریابی‌ها

    در حال حاضر، مسیریابی ماژول محصولات مثال این سری، یک چنین تعاریفی را پیدا کرده‌است:
    const routes: Routes = [
      { path: 'products', component: ProductListComponent },
      {
        path: 'products/:id', component: ProductDetailComponent,
        resolve: { product: ProductResolverService }
      },
      {
        path: 'products/:id/edit', component: ProductEditComponent,
        resolve: { product: ProductResolverService },
        children: [   ]
      }
    ];
    در اینجا می‌توان دو مسیریابی نمایش جزئیات یک محصول و ویرایش و افزودن یک محصول را تبدیل به فرزندان مسیریابی نمایش لیست محصولات کرد. از آنجائیکه Child Routes، سبب توسعه و بسط مسیریابی والد خود می‌شوند، نیاز است مسیرهای مطلق آن‌ها را تبدیل به مسیرهایی نسبی کنیم:
    const routes: Routes = [
      {
        path: 'products',
        children: [
          {
            path: '',
            component: ProductListComponent
          },
          {
            path: ':id',
            component: ProductDetailComponent,
            resolve: { product: ProductResolverService }
          },
          {
            path: ':id/edit',
            component: ProductEditComponent,
            resolve: { product: ProductResolverService },
            children: [
              { path: '', redirectTo: 'info', pathMatch: 'full' },
              { path: 'info', component: ProductEditInfoComponent },
              { path: 'tags', component: ProductEditTagsComponent }
            ]
          }
        ]
      }
    ];
    در اینجا کدهای کامل این تغییرات را جهت تعریف یک component-less route مشاهده می‌کنید.
     - ابتدا دو مسیریابی نمایش جزئیات و ویرایش یک محصول، تبدیل به یک گروه، به صورت فرزندان مسیریابی products با تعریف خاصیت children آن شده‌اند.
     - سپس pathهای آن‌ها ویرایش شده و با حذف /product از ابتدای آن‌ها، حالت نسبی را پیدا کرده‌اند.
     - مسیریابی products که والد این مسیریابی‌های فرزند است نیز بدون کامپوننت تعریف شده‌است.
     - کامپوننت مسیریابی products، به عنوان مدیریت کننده‌ی مسیر پیش فرض این فرزندان، تعریف شده‌است.
     
    Child routes در درون router-outlet تعریف شده‌ی درون قالب والد آن‌ها نمایش داده می‌شوند (مانند برگه‌های edit info و edit tags قسمت قبل). با توجه به اینکه اکنون دو مسیریابی دیگر، به صورت فرزندان مسیریابی صفحه‌ی نمایش لیست محصولات تعریف شده‌اند، به همین جهت باید یک router-outlet جدید را در درون قالب کامپوننت نمایش لیست محصولات، تعریف کرد. اما نمی‌خواهیم نمایش جزئیات یک محصول و یا صفحه‌ی ویرایش آن‌ها، در همان صفحه‌ی نمایش لیست محصولات ظاهر شوند. برای رفع این مشکل است که نیاز به تعریف یک مسیریابی بدون کامپوننت خواهیم داشت. به همین جهت در تعاریف فوق، تعریف component: ProductListComponent به یکی از مداخل آرایه‌ی children منتقل شده‌است (بجای تعریف آن همانند قبل ذیل مسیریابی products) که دارای path خالی است (یا همان مسیر پیش فرض که در اینجا به products اشاره می‌کند).
    در این حالت چون مسیریابی والد، به همراه یک کامپوننت تعریف نشده‌است، مسیریاب، سعی در نمایش فرزندان آن در router-outlet والد نمی‌کند. بجای آن، فرزندان، در یک router-outlet قرار گرفته‌ی در یک سطح بالاتر، نمایش داده می‌شوند که دقیقا همان router-outlet تعریف شده‌ی در فایل قالب src\app\app.component.html است.


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

    ‫مسیریابی در Angular - قسمت هفتم - بهبودهای بصری

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


    تزئین مسیر انتخاب شده در منوی سایت

    برای بهبود ظاهر برنامه نیاز است منوی سایت را به نحوی تغییر دهیم که مشخص کند، اکنون کاربر کدام گزینه را انتخاب کرده‌است. این مورد شامل سلسه مراتب مسیریابی‌ها نیز می‌شود؛ برای مثال فعالسازی حالت انتخاب شده‌ی منوی سایت، به همراه برگه‌ی انتخاب شده در یکی از Child Routes.
    برای پیاده سازی این قابلیت، دایرکتیو ویژه‌ای به نام routerLinkActive تدارک دیده شده‌است. این دایرکتیو را می‌توان به یک anchor tag و یا المان والد آن انتساب داد. مقدار آن‌را نیز می‌توان به یکی از کلاس‌های CSS برنامه مانند کلاس active تعریف شده‌ی در بوت استرپ تنظیم کرد. هر زمانیکه این مسیریابی فعال شود، مسیریاب به صورت خودکار این کلاس را با درج آن، به المان مرتبط اضافه می‌کند و برعکس.
    برای نمونه فایل src\app\product\product-edit\product-edit.component.html را گشوده و سپس تغییرات ذیل را اعمال کنید:
    <div class="wizard"><a [routerLink]="['info']" routerLinkActive="active">
                    Basic Information</a><a [routerLink]="['tags']" routerLinkActive="active">
                    Search Tags</a></div>
    در اینجا دایرکتیو‌های routerLinkActive، به هر کدام از لینک‌های تعریف شده اضافه گردیده‌اند. مقدار active در اینجا، به کلاس active بوت استرپ اشاره می‌کند. یا حتی می‌توان تعدادی کلاس جدا شده‌ی با کاما را نیز در اینجا ذکر کرد.

    یک نکته:از آنجائیکه در اینجا مقدار active یک string است و نه یک خاصیت یا عبارت متغیر، به همین جهت نیازی نیست تا این دایرکتیو را به صورت [routerLinkActive] تعریف کنیم.


    همانطور که مشاهده می‌کنید، همین دو تنظیم ساده سبب مشخص شدن برگه‌ی انتخابی شده‌اند.

    منوی بالای سایت نیز چنین تنظیماتی را نیاز دارد. برای این منظور به فایل src\app\app.component.html که دربرگیرنده‌ی منوی سایت است مراجعه کرده و تغییرات ذیل را اعمال می‌کنیم:
    <ul class="nav navbar-nav"><li routerLinkActive="active"><a [routerLink]="['/home']">Home</a></li><li routerLinkActive="active"><a [routerLink]="['/products']">Product List</a></li><li routerLinkActive="active"><a [routerLink]="['/products', 0, 'edit']">Add Product</a></li>      </ul>
    اینبار routerLinkActive به المان‌های li اعمال شده‌است؛ چون این المان‌های لیست، شیوه نامه‌ی المان‌های anchor را بازنویسی می‌کنند و اگر routerLinkActive را به لینک‌ها اعمال می‌کردیم، تغییری مشاهده نمی‌شد.


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

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


    اینبار هر دو گزینه با هم انتخاب شده‌اند. علت اینجا است که این دو مسیر دارای root URL segment یکسانی هستند؛ یا همان products/ در اینجا. به همین جهت routerLinkActive هر دو را به عنوان فعال انتخاب کرده‌است. برای مدیریت میدان دید آن می‌توان از دایرکتیو دیگری به نام routerLinkActiveOptions استفاده کرد:
    <li routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }"><a [routerLink]="['/products']">Product List</a></li>
    routerLinkActiveOptions را تنها به ریشه‌ی مسیر products اعمال کرده‌ایم؛ چون این مسیر است که می‌تواند با تمام مسیرهای مشتق شده‌ی از آن نیز تطابق داشته باشد. تنظیم exact: true آن سبب خواهد شد تا تطابق با مسیرهای مشتق شده‌ی از آن ندید گرفته شوند.


    اکنون کاربران بهتر می‌توانند درک کنند در کجای برنامه قرار دارند.


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

    در ادامه می‌خواهیم اگر برگه‌ای دارای مشکلات اعتبارسنجی بود، آیکن خطایی را در کنار برچسب آن برگه نمایش دهیم. به این ترتیب مدیریت چندین برگه برای کاربران ساده‌تر خواهد شد و به سادگی می‌توانند برگه‌های مشکل دار را پیدا کنند.
    در انتهای مطلب «مسیریابی در Angular - قسمت پنجم - تعریف Child Routes» متد isValid را تعریف کردیم. این متد مسیر یک tab را دریافت کرده و اگر اعتبارسنجی آن مشکلی نداشت، مقدار true را بر می‌گرداند. از این متد جهت نمایش آیکن خطای اعتبارسنجی برگه‌ها استفاده خواهیم کرد.
    <div class="wizard"><a [routerLink]="['info']" routerLinkActive="active">
                    Basic Information<span [ngClass]="{'glyphicon glyphicon-exclamation-sign': !isValid('info')}"></span></a><a [routerLink]="['tags']" routerLinkActive="active">
                    Search Tags<span [ngClass]="{'glyphicon glyphicon-exclamation-sign': !isValid('tags')}"></span></a></div>
    در اینجا دو span را تعریف کرده‌ایم که با کمک دایرکتیو ngClass سبب نمایش آیکن exclamation-sign در صورت وجود یک خطای اعتبارسنجی می‌شوند. به عبارتی اگر برگه‌ای معتبر نباشد، سبب درج کلاس آن در span جاری می‌شود:



    رخ‌دادهای مسیریابی

    هر زمانیکه کاربری مسیرهای مختلف برنامه را پیمایش می‌کند، مسیریاب تعدادی رخ‌داد را نیز تولید خواهد کرد. از این رخ‌دادها جهت تحت نظر قرار دادن، عیب‌یابی و یا اجرای منطقی می‌توان استفاده کرد. این رخ‌دادها شامل موارد ذیل هستند:
    - NavigationStart، با آغاز پیمایش یک مسیر رخ می‌دهد.
    - RoutesRecognized، با تشخیص و تطابق یک مسیر، با یکی از المان‌های تعریف شده‌ی در تنظیمات مسیریابی رخ می‌دهد.
    - NavigationEnd، با پایان پیمایش یک مسیر رخ می‌دهد.
    - NavigationCancel، در صورت لغو پیمایش یک مسیریابی توسط محافظ‌های مسیرها و یا هدایت به یک جهت دیگر رخ می‌دهد.
    - NavigationError، با شکست پیمایش یک مسیر رخ می‌دهد.

    این رخ‌دادها با فعالسازی تنظیم enableTracing تنظیمات مسیریابی به true فعال می‌شوند. برای این منظور فایل src\app\app-routing.module.ts را گشوده و به نحو ذیل تغییر دهید:
    @NgModule({
      imports: [RouterModule.forRoot(routes/*, { useHash: true }*/, { enableTracing: true })],
    پس از این تغییر، اگر به developer tools مرورگر دقت کنید، یک چنین خروجی را می‌توان مشاهده کرد:


    در اینجا ترتیب اجرای رخ‌دادهای متفاوت پیمایش مسیر نمایش لیست محصولات را مشاهده می‌کنید.
    - Router به هر مسیر، یک id خود افزایش یابنده را به صورت خودکار نسبت می‌دهد. برای نمونه، این مسیر خاص، id:2 را یافته‌است. از این id می‌توان برای دسترسی به مجموعه‌ای از رخ‌دادها استفاده کرد.
    - در این خروجی، url همان آدرس اصلی مسیر است و urlAfterRedirects به معنای مسیری است که پس از تنظیم redirect در تنظیمات مسیریابی (در صورت وجود) حاصل شده‌است.
    - یکی از روش‌هایی که برای دیباگ مسیریابی‌ها می‌توان استفاده کرد، همین فعالسازی enableTracing است.


    کار با رخ‌دادهای مسیریابی با کدنویسی

    به رخ‌دادهایی که در کنسول developer tools مرورگر مشاهده کردید، با کدنویسی نیز می‌توان دسترسی یافت. برای مثال می‌توان یک تصویر چرخنده یا لطفا منتظر بمانید را در آغاز پیمایش یک مسیریابی نمایش داد و سپس در پایان پیمایش این مسیریابی، آن‌را مخفی کرد. این events نیز از نوع Observable بوده و برای کار با آن‌ها باید مشترکشان شد:
    this.router.events.subscribe((routerEvent: Event) => {
        if (routerEvent instanceof NavigationStart) {
          //...
        }
    });
    شیء router به همراه خاصیت events است که با گوش فرادادن به رخ‌دادهای صادر شده‌ی توسط آن می‌توان دریافت چه نوع رخ‌دادی هم اکنون صادر شده‌است.

    در مثال جاری این سری، در «مسیریابی در Angular - قسمت چهارم - پیش واکشی اطلاعات»، سبب شدیم تا کل اطلاعات مورد نیاز یک مسیر، پیش از نمایش آن از سرور دریافت شوند تا به این صورت ابتدا یک قاب خالی نمایش داده نشده و پس از مدتی تکمیل شود. هرچند تجربه‌ی کاربری این روش بهتر از روش قبلی است، اما هنوز هم کاربر تاخیری را در ابتدا حس خواهد کرد (به اندازه‌ی زمان delay تنظیم شده)، بدون اینکه راهنمایی به او ارائه شود. در این حالت بهتر است در ابتدای کار، یک تصویر چرخنده نمایش داده شود تا کاربر متوجه شود، نیاز است اندکی منتظر بماند.
    در اینجا می‌خواهیم این تصویر چرخنده برای تمام مسیرهای برنامه فعال شود. به همین جهت گوش فرادادن به رخ‌دادها را در نقطه‌ی آغازین برنامه و یا همان src\app\app.component.ts انجام می‌دهیم:
    import { Router, Event, NavigationStart, NavigationEnd, NavigationError, NavigationCancel } 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';
    
      loading: boolean = true;
    
      constructor(private authService: AuthService,
        private router: Router) {
        router.events.subscribe((routerEvent: Event) => {
          this.checkRouterEvent(routerEvent);
        });
      }
    
      checkRouterEvent(routerEvent: Event): void {
        if (routerEvent instanceof NavigationStart) {
          this.loading = true;
        }
    
        if (routerEvent instanceof NavigationEnd ||
          routerEvent instanceof NavigationCancel ||
          routerEvent instanceof NavigationError) {
          this.loading = false;
        }
      }
    
      logOut(): void {
        this.authService.logout();
        this.router.navigateByUrl('/welcome');
      }
    }
    کدهای کامل AppComponent را جهت گوش فرادادن به رخ‌دادهای شروع و یا خاتمه/لغو/شکست پیمایش یک مسیریابی، در اینجا مشاهده می‌کنید.
    - ابتدا وابستگی‌های لازم آن import شده‌اند.
    - سپس می‌خواهیم خاصیت عمومی loading را در شروع به پیمایش یک مسیر، به true تنظیم کنیم و اگر این پیمایش به هر نحوی خاتمه یافت، آن‌را false خواهیم کرد.

    اکنون برای استفاده‌ی از این خاصیت عمومی و نمایش تصویر چرخنده، نیاز است قالب src\app\app.component.html را ویرایش کنیم:
    <span class="glyphicon glyphicon-refresh glyphicon-spin spinner" *ngIf="loading"></span>
    با افزودن span فوق به ابتدای فایل app.component.html به تغییرات خاصیت loading واکنش نشان خواهیم داد. کلاس‌های CSS ایی را که در اینجا اضافه شده‌اند، به فایل src\styles.css اضافه می‌کنیم:
    /* Spinner */
    .spinner {
      font-size:300%;
      position:absolute;
      top: 50%;
      left: 50%;
      z-index:10
    }
    
    .glyphicon-spin {
        -webkit-animation: spin 1000ms infinite linear;
        animation: spin 1000ms infinite linear;
    }
    @-webkit-keyframes spin {
        0% {
            -webkit-transform: rotate(0deg);
            transform: rotate(0deg);
        }
        100% {
            -webkit-transform: rotate(359deg);
            transform: rotate(359deg);
        }
    }
    @keyframes spin {
        0% {
            -webkit-transform: rotate(0deg);
            transform: rotate(0deg);
        }
        100% {
            -webkit-transform: rotate(359deg);
            transform: rotate(359deg);
        }
    }


    اکنون مسیرهایی که دارای route resolver هستند (مانند نمایش جزئیات/ویرایش یک محصول)، به همراه یک spinner نمایش داده خواهند شد و سایر مسیرها ابتدا نمایش داده خواهند شد و سپس اطلاعات آن‌ها از سرور دریافت می‌شود (مانند مسیر نمایش لیست محصولات که دارای route resolver نیست).
    البته می‌توان این true/false کردن loading را به ابتدا و انتهای کار یک Observable، مانند حالت نمایش لیست محصولات نیز منتقل کرد. اما در این حالت باید span مرتبط را نیز به قالب همان کامپوننت انتقال داد و دیگر سراسری نخواهد بود.


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

    ‫مسیریابی در Angular - قسمت هشتم - مسیرهای ثانویه

    $
    0
    0
    به چندین مسیر که در یک زمان و در یک سطح، نمایش داده می‌شوند، مسیرهای ثانویه (secondary routes) گفته می‌شوند و برای ساخت رابط‌های کاربری پیچیده مفید هستند. از آن‌ها می‌توان برای نمایش چندین پنل در یک صفحه استفاده کرد که هر کدام دارای محتوایی متفاوت، به همراه مسیریابی مستقل و خاص خودشان هستند؛ مانند ساخت یک صفحه‌ی مدیریتی. هرچند می‌توان این صفحه‌ی مدیریتی را با درج مستقیم کامپوننت‌های آن‌ها در یک صفحه نیز نمایش داد، اما اگر هر کدام نیاز به مسیریابی خاصی نیز جهت نمایش جزئیات آن‌ها داشته باشند، دیگر روش درج مستقیم کامپوننت‌ها توسط selector آ‌ن‌ها در صفحه پاسخگو نخواهد بود.


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

     به router-outlet ایی که در فایل قالب src\app\app.component.html قرار گرفته‌است، primary outlet می‌گویند. زمانیکه کاربر، برنامه را در مرورگر مشاهده می‌کند، با هربار کلیک بر روی یکی از لینک‌های منوی بالای سایت، قالب آن‌را در این primary outlet مشاهده می‌کند. اگر بخواهیم پنل دیگری را در همین صفحه و در همین سطح از نمایش، درج کنیم، نیاز به تعریف outlet دیگری است که به همراه مسیرهای ثانویه‌ای نیز خواهد بود.


    تعریف یک router-outlet نامدار

    با توجه به اینکه هر پنل به همراه مسیریابی ثانویه، نیاز به router-outlet خودش را خواهد داشت، مسیریاب برای اینکه بداند محتوای آن‌ها را در کجای صفحه درج کند، به نام‌های آن‌ها مراجعه می‌کند. به این ترتیب می‌توان چندین router-outlet را در یک سطح از نمایش تعریف کرد؛ اما هرکدام باید دارای نامی منحصربفرد باشند.
    در مثال این سری می‌خواهیم پنلی را در سمت راست صفحه‌ی اصلی درج کنیم. برای تعریف آن در همان سطحی که router-outlet اصلی قرار دارد، نیاز است فایل src\app\app.component.html را ویرایش کنیم:
    <div class="container"><div class="row"><div class="col-md-10"><router-outlet></router-outlet></div><div class="col-md-2"><router-outlet name="popup"></router-outlet></div></div></div>
    در اینجا با استفاده از امکانات بوت استرپ، دو ستون را در قالب اصلی برنامه تعریف کرده‌ایم. ستون اول حاوی router-outlet اصلی برنامه است و ستون دوم جهت درج پنل پیام‌های برنامه تعریف شده‌است. این router-outlet دوم، با نام popup مشخص گردیده‌است.


    افزودن ماژول جدید پیام‌های سیستم

    در ادامه ماژول جدید پیام‌های سیستم را به همراه تنظیمات ابتدایی مسیریابی آن اضافه خواهیم کرد که در آن ماژول، مدیریت نمایش پیام‌های مختلفی در router-outlet ثانویه popup صورت خواهد گرفت:
    >ng g m message --routing
    به این ترتیب دو فایل src\app\message\message-routing.module.ts و src\app\message\message.module.ts به برنامه اضافه می‌شوند.

    در ادامه نیاز است MessageModule را به قسمت imports فایل src\app\app.module.ts نیز معرفی کنیم (پیش از AppRoutingModule که حاوی مسیریابی catch all است):

    import { MessageModule } from './message/message.module';
    
    @NgModule({
      declarations: [
      ],
      imports: [
        BrowserModule,
        FormsModule,
        HttpModule,
        InMemoryWebApiModule.forRoot(ProductData, { delay: 1000 }),
    
        ProductModule,
        UserModule,
        MessageModule,
    
        AppRoutingModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }

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

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

    پس از ایجاد قالب ابتدایی فایل message.service.ts آن‌را به نحو ذیل تکمیل می‌کنیم:
    import { Injectable } from '@angular/core';
    
    @Injectable()
    export class MessageService {
      private messages: string[] = [];
      isDisplayed = false;
    
      addMessage(message: string): void {
        let currentDate = new Date();
        this.messages.unshift(message + ' at ' + currentDate.toLocaleString());
      }
    }
    هدف از این سرویس، به اشتراک گذاری اطلاعات بین کامپوننت‌های مختلف برنامه است. هر قسمت از برنامه (هر کامپوننتی) می‌تواند این سرویس را در سازنده‌ی خود تزریق کرده و پیامی را به مجموعه‌ی پیام‌های موجود اضافه کند.

    اکنون جهت تکمیل کامپوننت پیام‌ها، ابتدا فایل قالب message.component.html را به نحو ذیل تکمیل می‌کنیم:
    <div class="row"><h4 class="col-md-10">Message Log</h4><span class="col-md-2"><a class="btn btn-default"  (click)="close()">x</a></span></div><div *ngFor="let message of messageService.messages; let i=index"><div *ngIf="i<10" class="message-row">
        {{ message }}</div></div>
    به این ترتیب تنها 10 پیام از مجموعه پیام‌های سرویس پیام‌ها، توسط قالب این کامپوننت نمایش داده خواهد شد. یک دکمه‌ی بستن نیز در اینجا اضافه شده‌است.
    کدهای کامپوننت این قالب به صورت ذیل است:
    import { MessageService } from './../message.service';
    import { Router } from '@angular/router';
    import { Component, OnInit } from '@angular/core';
    
    @Component({
      //selector: 'app-message',
      templateUrl: './message.component.html',
      styleUrls: ['./message.component.css']
    })
    export class MessageComponent implements OnInit {
    
      constructor(private messageService: MessageService,
        private router: Router) { }
    
      ngOnInit() {
      }
    
      close(): void {
        // Close the popup.
        this.router.navigate([{ outlets: { popup: null } }]);
        this.messageService.isDisplayed = false;
      }
    }
    این کامپوننت سرویس پیام‌ها را در اختیار قالب خود قرار داده و همچنین یک دکمه‌ی بستن را نیز به همراه دارد که خاصیت isDisplayed  آن‌را false می‌کند.


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

    ابتدا به فایل src\app\product\product-edit\product-edit.component.ts مراجعه کرده و سرویس جدید پیام‌ها را به سازنده‌ی آن تزریق می‌کنیم:
    import { MessageService } from './../../message/message.service';
    
    @Component({
      selector: 'app-product-edit',
      templateUrl: './product-edit.component.html',
      styleUrls: ['./product-edit.component.css']
    })
    export class ProductEditComponent implements OnInit {
    
      constructor(private productService: ProductService,
        private messageService: MessageService,
        private route: ActivatedRoute,
        private router: Router) { }
    سپس ابتدای متد onSaveComplete آن‌را جهت درج پیام‌های این کامپوننت تغییر می‌دهیم.
      onSaveComplete(message?: string): void {
        if (message) {
          this.messageService.addMessage(message);
        }


    تنظیم مسیرهای ثانویه

    نحوه‌ی تعریف مسیریابی‌های مرتبط با router-outletهای غیراصلی برنامه، همانند سایر مسیریابی‌های برنامه‌است؛ با این تفاوت که در اینجا خاصیت outlet نیز به تنظیمات مسیر اضافه خواهد شد. به این ترتیب مشخص خواهیم کرد که محتوای این مسیر باید دقیقا در کدام router-outlet نامدار، درج شود.
    برای این منظور فایل src\app\message\message-routing.module.ts را گشوده و تنظیمات مسیریابی آن‌را که به صورت RouterModule.forChild تعریف می‌شوند (چون ماژول اصلی برنامه نیستند)، تکمیل خواهیم کرد:
    const routes: Routes = [
      { path: 'messages', component: MessageComponent, outlet: 'popup' }
    ];
    همانطور که مشاهده می‌کنید، تنها تفاوت آن‌ها با سایر تعاریف مسیریابی‌های برنامه، ذکر نام Outlet ایی است که باید قالب MessageComponent را نمایش دهد.


    فعالسازی یک مسیر ثانویه

    در اینجا نیز همانند سایر مسیریابی‌ها، از دایرکتیو routerLink برای فعالسازی مسیرهای ثانویه استفاده می‌کنیم؛ اما syntax آن کمی متفاوت است:
    <a [routerLink]="[{ outlets: { popup: ['messages'] } }]">Messages</a><a [routerLink]="['/products', product.id, 'edit', { outlets: { popup: ['summary', product.id] } }]">Messages</a>
    در اینجا می‌توان سبب فعال شدن چندین outlet به صورت همزمان شد. به همین جهت از نام جمع outlets استفاده شده‌است. سپس در ادامه key/valueهایی که بیانگر نام outlet و سپس path آن‌ها هستند، ذکر می‌شوند.
    در دومین لینک تعریف شده، ابتدا یک مسیر اصلی فعال شده و سپس یک مسیر ثانویه نمایش داده می‌شود.

    یک نکته:هرچند به primary outlet نامی انتساب داده نمی‌شود، اما نام آن دقیقا primary است و می‌توان قسمت outlets را به صورت ذیل نیز تعریف کرد:
    { outlets: { primary: ['/products', product.id,'edit'], popup: ['summary', product.id] }}


    در ادامه فایل src\app\app.component.html را ویرایش کرده و لینک Show Messages را به آن اضافه می‌کنیم:
    <ul class="nav navbar-nav navbar-right"><li *ngIf="authService.isLoggedIn()"><a>Welcome {{ authService.currentUser.userName }}</a></li><li><a [routerLink]="[{ outlets: { popup: ['messages'] } }]">Show Messages</a></li>
    که سبب نمایش لینک Show Messages در منوی بالای سایت می‌شود (تصویر فوق). در این حال اگر بر روی آن کلیک کنیم این پنل جدید به سمت راست صفحه اضافه می‌شود. برای آزمایش آن، محصولی را ویرایش کنید، تا پیام مرتبط با آن در این پنل نمایش داده شود.
    آدرس آن نیز چنین شکلی را پیدا می‌کند:
     http://localhost:4200/products(popup:messages)
    در اینجا مسیرثانویه داخل یک پرانتز نمایش داده شده‌است. در این حالت اگر به صفحات مختلف برنامه مراجعه کنیم، هنوز این قسمت داخل پرانتز حفظ می‌شود و نمایان خواهد بود.

    اکنون می‌خواهیم قابلیت مخفی سازی این پنل را نیز پیاده سازی کنیم. به همین جهت از خاصیت isDisplayed سرویس پیام‌ها که توسط دکمه‌ی بستن MessageComponent مدیریت می‌شود، استفاده خواهیم کرد. بنابراین لینک جدیدی را که در فایل src\app\app.component.html اضافه کردیم، به نحو ذیل تغییر خواهیم داد:
    <li *ngIf="!messageService.isDisplayed"><a (click)="displayMessages()">Show Messages</a></li><li *ngIf="messageService.isDisplayed"><a (click)="hideMessages()">Hide Messages</a></li>
    ngIfها بر اساس مقدار isDisplayed، سبب درج و یا حذف لینک‌های نمایش و مخفی کردن پیام‌ها می‌شوند و چون این قالب اکنون از سرویس پیام‌ها استفاده می‌کند، نیاز است این سرویس را به کامپوننت آن نیز تزریق کنیم:

    import { MessageService } from './message/message.service';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
    
      constructor(private authService: AuthService,
        private router: Router,
        private messageService: MessageService) {
      }
    
      displayMessages(): void {
        this.router.navigate([{ outlets: { popup: ['messages'] } }]);
        this.messageService.isDisplayed = true;
      }
    
      hideMessages(): void {
        this.router.navigate([{ outlets: { popup: null } }]);
        this.messageService.isDisplayed = false;
      }
    }
    در اینجا تزریق سرویس پیام‌ها را به سازنده‌ی کامپوننت App مشاهده می‌کنید. همچنین دو متد جدید نمایش و مخفی سازی پیام‌ها نیز تعریف شده‌اند که این متدها در قالب این کامپوننت، به لینک‌های مرتبطی متصل هستند.
    برای فعالسازی یک مسیرثانویه توسط متدهای برنامه، نیاز است از سرویس مسیریاب و متد navigate آن استفاده کرد که نمونه‌هایی از آن‌را در اینجا ملاحظه می‌کنید. پارامترهای ذکر شده‌ی در اینجا نیز همانند دایرکتیو routerLink هستند.

    یک نکته:اگر به متد hideMessages دقت کنید، مقدار value کلید popup به نال تنظیم شده‌است. این مورد سبب خواهد شد تا outlet آن خالی شود. به این ترتیب متد hideMessages علاوه بر مخفی کردن لینک نمایش پیام‌ها، پنل آن‌را نیز از صفحه حذف می‌کند. شبیه به همین نکته در متد close کامپوننت پیام‌ها که دکمه‌ی بستن آن‌را به همراه دارد، پیاده سازی شده‌است.


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

    ‫پیاده سازی پروژه‌ای مبتنی بر CQRS و ES

    $
    0
    0
    درقسمت قبلیبا معماری CQRS و Event Sourcing بصورت مختصر آشنا شدیم. برای درک بیشتر مطلب پیشین، احتیاج به پیاده سازی آن به صورت عملیاتی و نه فقط تئوری محض میباشد و در این مرحله قصد پیاده سازی این مدل را به ساده‌ترین صورت ممکن داریم.
    برای مطالعه‌ی ادامه‌ی این مقاله، نیاز به آشنایی با مباحث مطرح شده در قسمت قبل وجود دارد. پس از توضیحات اضافه بر روی قسمت‌های زیر گذشته و فرض بر آن است که آشنایی با این قسمت‌ها وجود دارد.
    از این مدل میتوان در زبان‌های مختلف برنامه نویسی و همچنین سیستم‌های مختلف اعم از وب اپلیکیشن و ... استفاده نمود. همچنین برای استفاده از این مدل نیاز قطعی به استفاده از فریم ورک خاصی نیست. در صورت نیاز میتوانید پیاده سازی سفارشی خاص خود را داشته باشید. اما برای ساده‌تر شدن و هرچه سریعتر شدن مراحل از فریمورک SimpleCqrs استفاده میکنیم. هر چند بر خلاف نامش امکانات فراوانی را در اختیار برنامه نویسان قرار میدهد و حتی در پروژه‌های واقعی نیز میتوان از آن استفاده نمود.
    برای سریعتر شدن کار میخواهیم پیاده سازی این مدل را در یک پروژه‌ی Console انجام دهیم و همچنین پس از ایجاد، پکیج‌های زیر را نصب مینماییم:
    Unity, SimpleCqrs, SimpleCqrs.Unity
    میخواهیم طبق مراحل گفته شده‌ی در قسمت قبل، به پیاده سازی این مدل بپردازیم و هدف، اضافه کردن یک Account به سیستم خواهد بود.
    ابتدا باید DomainObject مورد نظر نوشته شود:
    using System;
    using SimpleCqrs.Domain;
    
    namespace CqrsPattern.Cqrs.Command
    {
        public class Account : AggregateRoot
        {
            public Account(Guid id)
            {
                Apply(new AccountCreatedEvent { AggregateRootId = id });
            }
    
            public void SetName(string firstName, string lastName)
            {
                Apply(new AccountNameSetEvent { FirstName = firstName, LastName = lastName });
            }
    
            public void OnAccountCreated(AccountCreatedEvent evt)
            {
                Id = evt.AggregateRootId;
            }
        }
    }
    نکته: میخواهیم عملیات اضافه کردن یک Account، با استفاده از دو event مربوطه به نام AccountCreatedEvent و مقدار دهی آن با استفاده از AccountNameSetEvent انجام شود.
    eventهای فوق را در ادامه اضافه خواهیم داد (از توضیحات بیشتر صرفنظر شده و به مقاله‌ی قسمت قبل رجوع شود).
    حال احتیاج به پیاده سازی Command مربوطه برای انجام وظیفه‌ی خود داریم که هدف آن، اضافه کردن یک Account  به سیستم مورد نظر میباشد.
    فرض کنید برای اضافه شدن Account، پراپرتی‌های FirstName و LastName باید مقدار دهی شوند:
    using SimpleCqrs.Commanding;
    
    namespace CqrsPattern.Cqrs.Command
    {
        public class CreateAccountCommand : ICommand
        {
            public string FirstName { get; set; }
            public string LastName { get; set; }
        }
    }

    حال CommandHandler که وظیفه‌ی تفسیر کردن Command مربوطه را به عهده دارد، پیاده سازی خواهد شد:
    using System;
    using SimpleCqrs.Commanding;
    using SimpleCqrs.Domain;
    
    namespace CqrsPattern.Cqrs.Command
    {
        public class CreateAccountCommandHandler : CommandHandler<CreateAccountCommand>
        {
            private readonly IDomainRepository repository;
    
            public CreateAccountCommandHandler(IDomainRepository repository)
            {
                this.repository = repository;
            }
    
            public override void Handle(CreateAccountCommand command)
            {
                var account = new Account(Guid.NewGuid());
                account.SetName(command.FirstName, command.LastName);
    
                repository.Save(account);
            }
        }
    }
    نکته: از طریق account.SetName فراخوانی Event مربوطه انجام شده‌است و همچنین repository.Save به raise کردن EventHandler میپردازد.
    event مربوط به اضافه شدن Account را به صورت زیر پیاده سازی مینماییم:
    using SimpleCqrs.Eventing;
    
    namespace CqrsPattern.Cqrs.Command
    {
        public class AccountCreatedEvent : DomainEvent { }
    }
    و همچنین event مربوط به مقدار دهی پراپرتی‌ها نیز به صورت زیر خواهد بود:
    using SimpleCqrs.Eventing;
    
    namespace CqrsPattern.Cqrs.Command
    {
        public class AccountNameSetEvent : DomainEvent
        {
            public string FirstName { get; set; }
            public string LastName { get; set; }
        }
    }
    در این بخش، پیاده سازی EventHandler را خواهیم داشت. طبق مطلب پیشین هر Domain باید EventHnadler ی داشته باشد که از Event هایش ارث بری کرده و هر کدام از Event‌ها عملا در قسمت Handle مربوط به خودش پردازش خواهد شد.
    using System.Linq;
    using SimpleCqrs.Eventing;
    using CqrsPattern.Cqrs.Db;
    
    namespace CqrsPattern.Cqrs.Command
    {
        public class AccountEventHandler : IHandleDomainEvents<AccountCreatedEvent>,
                                                 IHandleDomainEvents<AccountNameSetEvent>
        {
            private readonly FakeAccountTable accountTable;
    
            public AccountEventHandler(FakeAccountTable accountTable)
            {
                this.accountTable = accountTable;
            }
    
            public void Handle(AccountCreatedEvent domainEvent)
            {
                accountTable.Add(new FakeAccountTableRow { Id = domainEvent.AggregateRootId });
            }
    
            public void Handle(AccountNameSetEvent domainEvent)
            {
                var account = accountTable.Single(x => x.Id == domainEvent.AggregateRootId);
                account.Name = domainEvent.FirstName + " " + domainEvent.LastName;
            }
        }
    }
    نکته: از آنجاییکه پیاده سازی ذخیره کردن Account با استفاده از دو event فوق انجام شده، بعد از Raise شدن EventHandler هر دو متد Handle، وظیفه‌ی Command مربوطه را به عهده دارند (بنابراین وظیفه‌ی هر Command میتواند با استفاده از event‌های مختلفی انجام شود).
    برای اینکه نخواهیم وارد فاز‌های مربوط به دیتابیس شویم، موقتا یک db به صورت fake شده را پیاده سازی مینماییم؛ به صورت زیر:
    using System.Collections.Generic;
    
    namespace CqrsPattern.Cqrs.Db
    {
        public class FakeAccountTable : List<FakeAccountTableRow>
        { }
    }
    using System;
    
    namespace CqrsPattern.Cqrs.Db
    {
        public class FakeAccountTableRow
        {
            public Guid Id { get; set; }
            public string Name { get; set; }
        }
    }

    و همچنین نیاز به ServiceLocator برای نمونه گرفتن از RunTime ی که از آن ارث بری کرده است داریم (برای سادگی کار از الگوی ServiceLocator استفاده میکنیم، ServiceLocator جز Anti-Pattern  ها محسوب میشود و معمولا در پروژه‌های واقعی از آن استفاده نمیشود)
    using SimpleCqrs;
    using SimpleCqrs.Unity;
    
    namespace CqrsPattern
    {
        public class SampleRunTime : SimpleCqrsRuntime<UnityServiceLocator> { }
    }
    حال احتیاج به پیاده سازی قسمت Queryداریم به همراه ReadModel و سرویسی برای فراخوانی آن
    using System;
    
    namespace CqrsPattern.Cqrs.Query
    {
        public class AccountReadModel
        {
            public string Name { get; set; }
            public Guid Id { get; set; }
        }
    }
    using CqrsPattern.Cqrs.Db;
    using System.Collections.Generic;
    using System.Linq;
    
    namespace CqrsPattern.Cqrs.Query
    {
        public class AccountReportReadService
        {
            private FakeAccountTable fakeAccountDb;
    
            public AccountReportReadService(FakeAccountTable fakeAccountDb)
            {
                this.fakeAccountDb = fakeAccountDb;
            }
    
            public IEnumerable<AccountReadModel> GetAccounts()
            {
                return from a in fakeAccountDb
                       select new AccountReadModel { Id = a.Id, Name = a.Name };
            }
        }
    }

    در قسمت Main نرم افزار نیاز به register کردن FakeTable خود داریم و همانطور که ملاحظه میکنید Command مورد نظر را نمونه سازی کرده و آن را روی CommandBus قرار میدهیم تا مراحل پیاده سازی شده در قسمت‌های فوق انجام شود و همچنین بعد از اتمام command ارسال شده از طریق Service مورد نظر اطلاعات ذخیره شده بازگردانی میشود
    using System;
    using SimpleCqrs.Commanding;
    using CqrsPattern.Cqrs.Query;
    using CqrsPattern.Cqrs.Command;
    
    namespace CqrsPattern
    {
        class Program
        {
            static void Main(string[] args)
            {
                var runtime = new SampleRunTime();
    
                runtime.Start();
    
                var fakeAccountTable = new FakeAccountTable();
                runtime.ServiceLocator.Register(fakeAccountTable);
                runtime.ServiceLocator.Register(new AccountReportReadService(fakeAccountTable));
                var commandBus = runtime.ServiceLocator.Resolve<ICommandBus>();
    
                var cmd = new CreateAccountCommand { FirstName = "Ali", LastName = "Kh" };
    
                commandBus.Send(cmd);
    
                var accountReportReadModel = runtime.ServiceLocator.Resolve<AccountReportReadService>();
    
                Console.WriteLine("Accounts in database");
                Console.WriteLine("####################");
                foreach (var account in accountReportReadModel.GetAccounts())
                {
                    Console.WriteLine(" Id: {0} Name: {1}", account.Id, account.Name);
                }
    
                runtime.Shutdown();
    
                Console.ReadLine();
            }
        }
    }
    اینگونه کل عملیات‌های لازم انجام خواهد شد.

    خلاصه:
    1) Command مربوطه را نمونه سازی کرده و روی CommandBus قرار میدهیم.
    2) CommandHandler فراخوانی شده و فانکشن Handle آن باعث نمونه سازی از AggregateRoot میشود.
    public override void Handle(CreateAccountCommand command)
            {
                var account = new Account(Guid.NewGuid()); //line 1
                account.SetName(command.FirstName, command.LastName); //line 2
                repository.Save(account); //line 3
            }
    در خط نخست Constructor کلاس Account باعث Apply شدن event مربوطه میشود.
    public Account(Guid id)
            {
                Apply(new AccountCreatedEvent { AggregateRootId = id });
            }
    و در خط دوم account.SetName  برای Apply شدن event مربوط به مقدار دهی property‌ها میباشد.
    public void SetName(string firstName, string lastName)
            {
                Apply(new AccountNameSetEvent { FirstName = firstName, LastName = lastName });
            }
    و همچنین در خط  سوم و پس از repository.Save باعث میشود event‌های pending شده Raise شده و توسط متد Handle مربوط به EventHandler پردازش شده و عملیات‌های زیر انجام شوند:
    public void Handle(AccountCreatedEvent domainEvent)
            {
                accountTable.Add(new FakeAccountTableRow { Id = domainEvent.AggregateRootId });
            }
    
            public void Handle(AccountNameSetEvent domainEvent)
            {
                var account = accountTable.Single(x => x.Id == domainEvent.AggregateRootId);
                account.Name = domainEvent.FirstName + " " + domainEvent.LastName;
            }
    رکورد مورد نظر ثبت شده و event بعدی، پراپرتی‌هایش را مقدار دهی مینماید  و بصورت InMemory درون FakeAccountTable ذخیره میشود (پر واضح است که در یک پروژه‌ی واقعی به جای ذخیره شدن در یک Collection باید درون دیتایس واقعی ذخیره سازی شود).
    و پس از اتمام عملیات انجام شده، بصورت زیر در Main برنامه اطلاعات ذخیره شده بازگردانده خواهد شد:
    var accountReportReadModel = runtime.ServiceLocator.Resolve<AccountReportReadService>();
    var accounts = accountReportReadModel.GetAccounts();

    در ادامه برای مطالعه بیشتر میتوان به Scale out کردن این سیستم و استفاده از فریمورک‌های  messaging چون Redis یا Kafka پرداخت و همچنین اعمال Load Balancing را در اینگونه سیستم‌ها انجام داد.
    نکته: Cqrs-Pattern را میتوانید از اینجا clone نمایید
    Viewing all 1980 articles
    Browse latest View live


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