مطالب
ارتباط بین کامپوننت‌ها در Vue.js - قسمت دوم استفاده از Event Bus
در قسمت قبلی، نحوه ارتباط بین کامپوننت‌های Parent و Child را مورد بررسی قرار دادیم و اینکه چگونه داده‌هایشان را به اشتراک میگذارند؛ اما چند موضوع در روش قبلی مورد بحث می‌باشد:

1)  مدیریت nested componentها برای استفاده از چنین روشی مشکل است.
2) اگر تعداد داده‌های اشتراکی زیاد باشد، مدیریت آنها با استفاده از props گیج کننده می‌باشد.

3) اگر دو کامپوننت مجزا (Sibling Component) قصد به اشتراک گذاری اطلاعاتی را داشتند تکلیف چیست؟ (هر چند با استفاده از یک کامپوننت Parent مشترک مقدور می‌باشد)

4) روش ساده‌تری برای ارتباط کامپوننت‌های Parent و Child  و همچنین Sibling Component وجود دارد.

 



استفاده از Event Bus:

با استفاده از EventBus، بسیاری از معایب مطرح شده در روش قبلی را نخواهیم داشت:



 تعریف Event Bus: یک Design Pattern ^ ,^  می باشد. در Vue.js یک نمونه از vue را بصورت سراسری (global) ایجاد میکنیم و درکامپوننت‌هایی که نیاز به ارتباط دارند، آن را فراخوانی (import) و با استفاده از متدهای emit$ و on$، ارتباط را ایجاد میکنیم.


یک فایل جاوا اسکریپتی را با نامی دلخواه (eventBus.js) در فولدر src ایجاد میکنیم و یک نمونه از Vue را در آن وهله سازی میکنیم:

import Vue from 'vue'
export default new Vue()
سپس در کامپوننت‌هایی که قصد ارتباط دارند، این فایل را import میکنیم ( مثال سبد خرید را در مقاله‌ی قبلی، به روش جاری تغییر میدهیم). 

در کامپوننت Shop-Button-Add، کد زیر را در قسمت script اضافه میکنیم:

import  EventBus  from "../eventBus";

کد تابع buttonClicked را بشکل زیر تغییر میدهیم:

 buttonClicked() {
      EventBus.$emit("shop-button-clicked", this.item);
    }

در کامپوننت App.vue  هم کد زیر را در قسمت script اضافه میکنیم:

import  EventBus  from "./eventBus";

و در تابع mounted که از توابع life cycle مربوط به Vue.js میباشد، کد زیر را اضافه میکنیم:

mounted() {
    EventBus.$on("shop-button-clicked", item => {
      this.updateCart(item);
    });
  }


مقایسه‌ی روش استفاده از EventBus با روش قبلی :

مراحل انجام کار در روش قبلی:

دو کامپوننت ارتباط داشتند Shop-Button-Add و App.vue: 

1) در کامپوننت Shop-Button-Add با زدن دکمه Add To Cart متد buttonClicked اجرا میشد.

2) متد buttonClicked  یک سیگنال به کامپوننت Parent خود (Shop-Item) ارسال می‌نمود.

  this.$emit('button-clicked')

3) در کامپوننت Shop-Item مشخص شده بود در صورت ارسال سیگنال از Shop-Button-Add، متد addToCart اجرا شود.

 <Shop-Button-Add    @button-clicked="addToCart(item)"     :item="item">
            <p>Add To Cart</p>
 </Shop-Button-Add>

4) اجرای متد addToCart در کامپوننت Shop-Item یک سیگنال را به کامپوننت App.vue به همراه دیتای مورد نظر ارسال می‌نمود.

addToCart(item) {
                this.$emit('update-cart', item)
            }

5) در کامپوننت App.vue مشخص شده بود با ارسال سیگنال از کامپوننت Shop-Item، متد updateCart اجرا شود.

<shop-item v-for="item in this.items" :item="item" :key="item.id" 
@update-cart="updateCart">
</shop-item>

6) در نهایت کار بروزرسانی سبد خرید با اجرای متد updateCart انجام میشد:

updateCart(e) {
      this.cart.push(e);
      this.total = this.shoppingCartTotal;
    }


نتیجه گیری:

مزایای استفاده از Event Bus :

1) کم شدن مراحل ارتباط بین کامپوننت‌ها

2) حل مشکل ارتباطی بین Sibling Component 

3) نوشتن کد کمتر


 کد سبد خرید به روش مقاله‌ی جاری-استفاده از EventBus  


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

npm install

همچنین نیاز هست تا پکیچ node-sass را نیز با دستور زیر برای این مثال نصب کنید.

npm install node-sass
مطالب
آشنایی با NuGet - قسمت اول

NuGet چیست؟

روش متداول استفاده از کتابخانه‌های موجود دات نتی در Visual studio‌ عموما به این صورت است: مراجعه به سایت مربوطه، دریافت بسته مورد نظر، باز کردن آن و سپس افزودن ارجاعی به اسمبلی‌های آن کتابخانه. در این حالت زمانیکه نسخه‌ی جدیدی از کتابخانه‌ی مورد استفاده ارائه ‌شود (و عموما تا مدت‌ها شاید از آن بی‌اطلاع باشیم) تمام این مراحل باید از ابتدا تکرار شوند و همینطور الی آخر.
برای رفع این نقیصه، تیم ASP.NET، افزونه‌ای سورس باز و رایگان را به نام NuGet جهت VS.Net 2010 طراحی کرده‌اند که کار مدیریت بسته‌های کتابخانه‌های مورد استفاده را بسیار ساده کرده است. امکانات این افزونه پس از نصب، در دو حالت استفاده از رابط گرافیکی کاربری آن و یا با استفاده از خط فرمان PowerShell ویندوز در دسترس خواهد بود. این افزونه در زمان بارگذاری، با مراجعه به فید سایت مرکزی خود، لیست بسته‌های مهیا را در اختیار علاقمندان قرار می‌دهد. درب این سایت مرکزی به روی تمام توسعه‌ دهنده‌ها جهت افزودن بسته‌های خود باز است.
و ... فراگیری کار با NuGet برای تمام برنامه نویسان دات نت لازم و ضروری است! از این جهت که پیغام "این بسته تنها برای NuGet عرضه شده است" کم کم در حال متداول شدن می‌باشد و دیگر سایت‌های مرتبط، لینک مستقیمی را جهت دریافت کتابخانه‌های خود ارائه نمی‌دهند. حتی خبر به روز شدن محصولات خود را هم شاید دیگر به صورت منظم ارائه ندهند؛ زیرا NuGet کار مدیریت آن‌ها را به عهده خواهد داشت.


دریافت و نصب NuGet

NuGet را حداقل به سه طریق می‌توان دریافت و نصب کرد:
الف) با مراجعه به سایت CodePlex : (+)
ب) دریافت آن از سایت گالری‌های آن : (+)


ج) با استفاده از امکانات VS.NET

هر سه روش فوق به دریافت و نصب فایل NuGet.Tools.vsix منتهی می‌شوند. برای مثال در روش (ج) باید به منوی Tools و گزینه‌ی Extension Manager مراجعه کنید. سپس برگه‌ی Online Gallery را گشوده و اندکی صبر کنید تا اطلاعات آن دریافت و نمایش داده شود. سپس NuGet را در Search box بالای صفحه نوشته و NuGet Package manager ظاهر شده را انتخاب و نصب کنید.



نحوه استفاده از NuGet

فرض کنید یک پروژه جدید ASP.NET را ایجاد کرده‌اید و نیاز است تا کتابخانه‌ی ELMAH به آن اضافه شود. روش انجام اینکار را به کمک NuGet در ادامه بررسی خواهیم کرد (کمتر از یک دقیقه زمان خواهد برد):

الف) با کمک امکانات رابط گرافیکی کاربر آن
ساده‌ترین روش استفاده از NuGet ، کلیک راست بر روی پوشه References در Solution explorer و سپس انتخاب گزینه‌ی Add Library Package Reference می‌باشد:



در صفحه‌ی باز شده، برگه‌ی Online را باز کنید و مدتی صبر نمائید تا اطلاعات لازم دریافت گردد (در زمان نگارش این مطلب، 1135 بسته در این مخزن موجود است):



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



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

روش دیگر ظاهر کردن این صفحه، مراجعه به منوی Tools و گزینه‌ی Library Package Manager می‌باشد:



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



نکته: NuGet در SharpDevelop 4.1 به بعد هم پشتیبانی می‌شود:




ب) با استفاده از امکانات خط فرمان PowerShell ویندوز
برای استفاده از امکانات پاورشل ویندوز نیاز است تا پاورشل نگارش 2 بر روی سیستم شما نصب باشد (نیاز به Windows XP with Service Pack 3 به بعد دارد). سپس به منوی Tools ، قسمت Library Package Manager ، گزینه‌ی Package Manager Console آن جهت فعال سازی کنسول پاور شل در VS.NET مراجعه نمائید:


نکته: در تصویر فوق پس از نوشتن el ، دکمه tab فشرده شده است. در این حالت منوی پکیج‌های مهیای شروع شده با el، از سایت مرکزی NuGet ظاهر گردیده است.

فرامین مهمی که در اینجا در دسترس هستند شامل: List-Package ، Uninstall-Package ، Update-Package و Get-Package می‌باشند. برای مثال اگر قصد جستجو در بین بسته‌های موجود را داشته باشید Get-Package بسیار مفید است:



برای مثال جهت یافتن بسته‌های مرتبط با wpf و silverlight به صورت زیر می‌توان عمل کرد:
PM> get-package -remote -filter wpf
PM> get-package -remote -filter silverlight

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


ج) استفاده از برنامه NuGet.exe
برنامه NuGet.exe از سایت CodePlex قابل دریافت است. این روش هم جهت علاقمندان به خط فرمان تدارک دیده شده است!
پس از دریافت آن فرض کنید می‌خواهیم تمام بسته‌های شروع شده با nhi را لیست کنیم. برای این منظور دستور خط فرمان ذیل را صادر کنید:
D:\Test>nuget list nhi
سپس برای دریافت مثلا NHibernate تنها کافی است دستور زیر اجرا شود:
D:\Test>nuget install NHibernate

به این صورت کتابخانه NHibernate ‌به همراه تمام وابستگی‌های آن دریافت خواهد شد.

به روز رسانی خودکار NuGet
برای به روز رسانی برنامه nuget.exe دستور زیر را می‌توان صادر نمود:
D:\Test>NuGet.exe u
و یا جهت فعال سازی به روز رسانی‌های خودکار افزونه‌ها در VS.NET به منوی زیر مراجعه کنید:
Tools | Options, then Environment | Extension Manager and click "Automatically check for updates to installed extensions."





ادامه دارد ...

مطالب
بررسی مقدار دهی اولیه متغیرها در T-SQL

یکی از موارد مشکل ساز حین استفاده از T-SQL ، مقدار دهی اولیه متغیرها به نال است و اگر اسکریپت تهیه شده کمی طولانی باشد، خطایابی مشکلات مرتبط با آن بسیار مشکل می‌شود. برای مثال:
Declare
@x int,
@y int

Set @x = 1
If (@x + @y = 1)
BEGIN
print 'yes!'
End

Set @y = (select sum(id) from Account)
If @x + @y = 1
BEGIN
print 'yes!'
End

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

بنابراین سؤال این است که چگونه این نوع مشکلات را در یک پروژه با تعداد زیادی رویه ذخیره شده، تابع و غیره می‌توان تشخیص داد؟
پاسخ:
در این مورد قبلا مطلبی در این سایت منتشر شده [+] (البته اگر از نگارش کامل VS 2010 استفاده می‌کنید نیازی به نصب چیزی نخواهید داشت) و نکته‌ی آن بررسی SR0007 است.



مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت سوم - غنی سازی کامپوننت‌ها
در قسمت قبل، مقدمه‌ای بر نحوه‌ی تعریف یک کامپوننت در AngularJS 2.0 عنوان شد و همچنین نحوه‌ی بوت استرپ و آغاز اینگونه برنامه‌ها بررسی گردید. در این قسمت می‌خواهیم امکانات پیشرفته‌تری از کامپوننت‌ها را بررسی کنیم.


روش‌های مختلف تعریف خاصیت template در یک کامپوننت

در قسمت قبل، روش تعریف inline یک template را مشاهده کردید:
template:`
          <div><h1>{{pageTitle}}</h1>
               <div>My First Component</div>
          </div>
 `
در اینجا رشته‌ی قالب نهایی این View، در همان تعاریف متادیتای Component قرار گرفته‌است (روش inline). اگر این رشته تک سطری باشد، از روش متداول ذکر "" برای تعریف رشته‌ها در جاوا اسکریپت استفاده می‌شود و اگر این رشته چند سطری باشد، از back tick مربوط به ES 6 مانند مثال فوق کمک گرفته خواهد شد. استفاده از back ticks و رشته‌های چند سطری، نحوه‌ی تعریف قالب‌های inline را خواناتر می‌کند.
هر چند این روش تعریف قالب‌ها، مزیت سادگی و امکان مشاهده‌ی View را به همراه کدهای مرتبط با آن، در یک فایل میسر می‌کند، اما به دلیل رشته‌ای بودن، مزیت کار کردن با ادیتورهای وب، مانند داشتن intellisense، فرمت خودکار کدها و بررسی syntax را از دست خواهیم داد و با بیشتر شدن حجم این رشته، این مشکلات بیشتر نمایان خواهند شد.
به همین جهت قابلیت دیگری به نام linked template نیز در اینجا درنظر گرفته شده‌است:
 templateUrl: 'product-list.component.html'
در این حالت، محتوای قالب، به یک فایل html مجزا منتقل شده و سپس لینک آن در خاصیت دیگری از متادیتای Component به نام templateUrl ذکر می‌شود.


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

در ادامه می‌خواهیم کامپوننتی را طراحی کنیم که آرایه‌ای از محصولات را نمایش می‌دهد. در اینجا مرسوم است هر ویژگی برنامه، در یک پوشه‌ی مجزا قرار گیرد. به همین جهت در ادامه‌ی مثال قسمت قبل که پوشه‌ی app را به ریشه‌ی پروژه اضافه کردیم و سپس main.ts راه انداز و کامپوننت ریشه‌ی سایت یا app.component.ts را در آن تعریف کردیم، در داخل همین پوشه‌ی app، پوشه‌ی جدیدی را به نام products اضافه می‌کنیم. سپس به این پوشه‌ی جدید محصولات، فایل جدیدی را به نام product-list.component.html اضافه کنید. از این فایل جهت تعریف قالب کامپوننت لیست محصولات استفاده خواهیم کرد. در اینجا نیز مرسوم است نام قالب یک Component را به صورت نام ویژگی ختم شده‌ی به کلمه‌ی Component، با پسوند html تعریف کنیم.


پس از اضافه شدن فایل product-list.component.html، محتوای آن‌را به نحو ذیل تغییر دهید:
<div class='panel panel-default'>
    <div class='panel-heading'>
        {{pageTitle}}
    </div>
    <div class='panel-body'>
        <div class='row'>
            <div class='col-md-2'>Filter by:</div>
            <div class='col-md-4'>
                <input type='text' />
            </div>
        </div>
        <div class='row'>
            <div class='col-md-6'>
                <h3>Filtered by: </h3>
            </div>
        </div>
        <div class='table-responsive'>
            <table class='table'>
                <thead>
                    <tr>
                        <th>
                            <button class='btn btn-primary'>
                                Show Image
                            </button>
                        </th>
                        <th>Product</th>
                        <th>Code</th>
                        <th>Available</th>
                        <th>Price</th>
                        <th>5 Star Rating</th>
                    </tr>
                </thead>
                <tbody>
 
                </tbody>
            </table>
        </div>
    </div>
</div>
در اینجا قصد داریم داخل پنل بوت استرپ 3، لیستی از محصولات را به صورت یک جدول نمایش دهیم. همچنین می‌خواهیم قابلیت جستجوی داخل این لیست را نیز فراهم کنیم. فعلا شکل کلی این قالب را به نحو فوق تهیه می‌کنیم. قسمت tbody جدول آن را که قرار است لیست محصولات را رندر کند، در ادامه‌ی بحث تکمیل خواهیم کرد.
تنها نکته‌ی AngularJS 2.0 قالب فوق، اتصال به pageTitle است که نمونه‌ای از آن‌را در قسمت قبل با معرفی اولین کامپوننت مشاهده کرده‌اید.

در ادامه نیاز است برای این قالب و view، یک کامپوننت را طراحی کنیم که متشکل است از یک کلاس TypeScript ایی مزین شده به Component. بنابراین فایل ts جدیدی را به نام product-list.component.ts به پوشه‌ی App\products اضافه کنید؛ با این محتوا:
import { Component } from 'angular2/core';
 
@Component({
    selector: 'pm-products',
    templateUrl: 'app/products/product-list.component.html'
})
export class ProductListComponent {
    pageTitle: string = 'Product List';
}


با جزئیات نحوه‌ی تعریف یک کامپوننت در قسمت قبل در حین معرفی کامپوننت‌ها آشنا شدیم. در اینجا کلاس ProductListComponent با واژه‌ی کلیدی export همراه است تا توسط module loader برنامه قابلیت بارگذاری را پیدا کند. همچنین خاصیت عمومی pageTitle نیز در آن تعریف شده‌است تا در قالب مرتبط مورد استفاده قرار گیرد.
سپس این کلاس، با decorator ویژه‌ای به نام Component مزین شده‌است تا AngularJS 2.0 بداند که هدف از تعریف آن، ایجاد یک کامپوننت جدید است. مقدار selector آن که تشکیل دهنده‌ی یک تگ HTML سفارشی متناظر با آن خواهد شد، به pm-products تنظیم شده‌است و اینبار بجای تعریف inline قالب آن به صورت یک رشته، از خاصیت templateUrl جهت معرفی مسیر فایل html قالبی که پیشتر آماده کردیم، کمک گرفته شده‌است.


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

خوب، تا اینجا یک کامپوننت جدید را به نام لیست محصولات، ایجاد کردیم؛ اما چگونه باید آن‌را نمایش دهیم؟
در قسمت قبل که کامپوننت ریشه‌ی برنامه یا AppComponent را تعریف کردیم، نام selector آن را pm-app درنظر گرفتیم و در نهایت این directive سفارشی را به نحو ذیل در body صفحه‌ی اصلی سایت نمایش دادیم:
    <div>
        @RenderBody()
        <pm-app>Loading App...</pm-app>
    </div>
اما این روش، تنها برای root component سایت مناسب است. برای سایر کامپوننت‌های غیر ریشه‌ای (یعنی تمام کامپوننت‌ها)، سه مرحله‌ی زیر باید طی شوند:
الف) تگ سفارشی این دایرکتیو جدید را به کامپوننت ریشه‌ی سایت یا همان AppComponent اضافه می‌کنیم. بنابراین فایل app.component.ts را گشوده و سپس selector کامپوننت لیست محصولات را به قالب آن اضافه کنید:
import { Component } from 'angular2/core';
 
@Component({
    selector: 'pm-app',
    template:`
    <div><h1>{{pageTitle}}</h1>
        <pm-products></pm-products>
    </div>
    `
})
export class AppComponent {
    pageTitle: string = "DNT AngularJS 2.0 APP";
}
همانطور که مشاهده می‌کنید، تگ جدید pm-products بر اساس نام selector کامپوننت لیست محصولات، به قالب کامپوننت ریشه‌ی سایت اضافه شده‌است.
ب) تا اینجا یک دایرکتیو جدید را به نام pm-products به یک کامپوننت دیگر اضافه کرده‌ایم. اما این کامپوننت نمی‌داند که اطلاعات آن‌را باید از کجا تامین کند. برای این منظور خاصیت جدیدی را به نام directives به لیست خاصیت‌های Component ریشه‌ی سایت اضافه می‌کنیم. این خاصیت، آرایه‌ای از دایرکتیوهای سفارشی را قبول می‌کند:
 directives: [ProductListComponent]
ج) بلافاصله که این تغییر را اعمال کنید، در ادیتور TypeScript ایی موجود، ذیل کلمه‌ی ProductListComponent خط قرمز کشیده خواهد شد. چون هنوز مشخص نکرده‌ایم که این شیء جدید باید از کدام ماژول تامین شود و ناشناخته‌است. بنابراین import مربوطه را به ابتدای فایل اضافه می‌کنیم:
import { Component } from 'angular2/core';
import { ProductListComponent } from './products/product-list.component';
 
@Component({
    selector: 'pm-app',
    template:`
    <div><h1>{{pageTitle}}</h1>
        <pm-products></pm-products>
    </div>
    `,
    directives: [ProductListComponent]
})
export class AppComponent {
    pageTitle: string = "DNT AngularJS 2.0 APP";
}
کدهای فوق، کد نهایی کامپوننت ریشه‌ی سایت هستند که به آن selector جدیدی به نام pm-products اضافه شده‌است. سپس directive متناظر آن به لیست دایرکتیوهای کامپوننت جاری اضافه شده و در نهایت این دایرکتیو، از ماژول مرتبط با آن import شده‌است.

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

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


یک نکته
اگر برنامه را اجرا کردید و خروجی را مشاهده نکردید، مطمئن شوید که فایل‌های ts شما کامپایل شده‌اند. فشردن دکمه‌ی ctrl+s مجدد در این فایل‌ها، سبب کامپایل مجدد آن‌ها می‌شوند و یا انتخاب گزینه‌ی Build و سپس ReBuild solution نیز همینکار را انجام می‌دهد.


غنی سازی کامپوننت‌های AngularJS 2.0 با data-binding

در AngularJS 2.0 عملیات binding، کار مدیریت ارتباطات بین یک کلاس کامپوننت و قالب آن‌را انجام می‌دهد. نمونه‌ای از آن‌را پیشتر با خاصیت pageTitle و سپس نمایش آن در قالب کامپوننت متناظر با آن کلاس، مشاهده کرده‌اید. همچنین در اینجا یک قالب می‌تواند متدهای داخل کلاس کامپوننت خود را توسط رخدادها نیز فراخوانی کند.
به نحوه‌ی نمایش {{pageTitle}} اصطلاحا interpolation می‌گویند. در اینجا خاصیت pageTitle اطلاعات خود را از کلاس کامپوننت دریافت می‌کند. به این نوع binding، انقیاد یک طرفه یا one-way binding نیز گفته می‌شوند؛ از خاصیت کلاس شروع شده و به قالب خاتمه می‌یابد.
ویژگی interpolation فراتر است از صرفا نمایش یک خاصیت و می‌تواند حاوی محاسبات نیز باشد:
{{'Title: ' + pageTitle}}
{{2*20+1}}
و یا حتی در آن می‌توان متدی از کلاس کامپوننت را نیز فراخوانی کرد. در مثال زیر فرض شده‌است که متد getTitle، در کلاس متناظر با کامپوننت این قالب، تعریف شده‌است:
{{'Title: ' + getTitle()}}
کار interpolation درج عبارت محاسبه شده‌ی نهایی بین المان‌های html است؛ مانند:
 <h1>{{pageTitle}}</h1>
و یا حتی می‌توان این مقدار نهایی را به خواص المان‌های html نیز نسبت داد:
 <h1 innerText={{pageTitle}}></h1>
در این مثال خاصیت innerText المان h1 توسط interpolation مقدار دهی شده‌است.

بنابراین به صورت خلاصه هر زمانیکه نیاز به نمایش اطلاعات فقط خواندنی (one-way binding) داریم، ابتدا خاصیتی را در کلاس کامپوننت تعریف کرده و سپس مقدار این خاصیت را توسط interpolation، در قالب کامپوننت درج می‌کنیم. حین استفاده از interpolation نیازی به ذکر "" نیست.
در مورد مباحث تکمیلی binding در قسمت‌های بعدی بیشتر بحث خواهیم کرد.


افزودن منطقی سفارشی به قالب یک کامپوننت

دایرکتیوها به صورت المان‌ها و یا ویژگی‌های سفارشی HTML، قابلیت توسعه‌ی امکانات پیش فرض آن‌را دارند. در اینجا می‌توان دایرکتیوهای سفارشی خود را تولید کرد (مانند pm-products فوق) و یا از دایرکتیوهای توکار AngularJS 2.0 استفاده کرد. برای مثال ngIf* و ngFor* جزو structural directives توکار AngularJS 2.0 هستند. ستاره‌ای که پیش از نام این دایرکتیوها قرار گرفته‌است، آن‌‌ها را در گروه structural directives قرار می‌دهد.
کار دایرکتیوهای ساختاری، تغییر ساختار یا همان view کامپوننت‌ها است؛ با افزودن، حذف و یا تغییر المان‌های HTML تعریف شده‌ی در صفحه.

بررسی ngIf*

فایل قالب product-list.component.html را گشوده و تعریف جدول آن‌را به نحو ذیل تغییر دهید:
 <table class='table' *ngIf='products && products.length'>
کار ngIf* نمایش یا عدم نمایش قسمتی از DOM یا document object model بر اساس برآورده شدن منطقی است که توسط آن بررسی می‌شود. اگر حاصل عبارتی که به ngIf* انتساب داده می‌شود به false تعبیر شود، آن المان و فرزندان آن از DOM حذف می‌شوند و اگر این عبارت به true تعبیر شود، آن المان و فرزندانش مجددا به DOM اضافه خواهند شد.
برای نمونه عبارت انتساب داده شده‌ی به ngIf* در مثال فوق به این معنا است که اگر خاصیت و آرایه‌ی products در کلاس کامپوننت این قالب تعریف شده بود و همچنین دارای اعضایی نیز بود، آنگاه این جدول را نمایش بده.
برای آزمایش آن، فایل product-list.component.ts را گشوده و خاصیت عمومی آرایه‌ی products را به نحو ذیل به آن اضافه کنید:
import { Component } from 'angular2/core';
 
@Component({
    selector: 'pm-products',
    templateUrl: 'app/products/product-list.component.html'
})
export class ProductListComponent {
    pageTitle: string = 'Product List';
    products: any[] = [
        {
            "productId": 2,
            "productName": "Garden Cart",
            "productCode": "GDN-0023",
            "releaseDate": "March 18, 2016",
            "description": "15 gallon capacity rolling garden cart",
            "price": 32.99,
            "starRating": 4.2,
            "imageUrl": "app/assets/images/garden_cart.png"
        },
        {
            "productId": 5,
            "productName": "Hammer",
            "productCode": "TBX-0048",
            "releaseDate": "May 21, 2016",
            "description": "Curved claw steel hammer",
            "price": 8.9,
            "starRating": 4.8,
            "imageUrl": "app/assets/images/rejon_Hammer.png"
        }
    ];
}
فعلا چون اینترفیسی را برای شیء محصول تعریف نکرده‌ایم، نوع این آرایه را any یا همان حالت پیش فرض جاوا اسکریپت تعریف می‌کنیم.
همچنین فعلا در اینجا اطلاعات را بجای دریافت از سرور، توسط آرایه‌ی مشخصی از اشیاء تعریف کرده‌ایم. این موارد را در قسمت‌های بعدی بهبود خواهیم بخشید.

اکنون که خاصیت عمومی products تعریف شده‌است، امکان استفاده‌ی از ngIf* ایی که پیشتر تعریف کردیم، میسر شده‌است. در این حالت اگر برنامه را اجرا کنید، قسمت table header تصویر قبلی نمایش سایت، هنوز نمایان است. یعنی ngIf* تعریف شده کار می‌کند؛ چون خاصیت products تعریف شده‌است و همچنین دارای اعضایی است.
برای آزمایش بیشتر، خاصیت products را کامنت کنید و یکبار نیز فایل ts آن‌را ذخیره کنید تا فایل js متناظر با آن کامپایل شود. سپس مجددا برنامه را اجرا کنید. در این حالت دیگر نباید هدر جدول نمایان باشد؛ چون products تعریف نشده‌است.


بررسی ngFor*

تا اینجا بر اساس داشتن لیستی از محصولات یا عدم آن، جدول متناظری را نمایش داده و یا مخفی کردیم. اما این جدول هنوز فاقد ردیف‌های نمایش اعضای آرایه‌ی products است.
برای این منظور مجددا فایل قالب product-list.component.html را گشوده و سپس بدنه‌ی جدول را به نحو ذیل تکمیل کنید:
<tbody>
    <tr *ngFor='#product of products'>
        <td></td>
        <td>{{ product.productName }}</td>
        <td>{{ product.productCode }}</td>
        <td>{{ product.releaseDate }}</td>
        <td>{{ product.price }}</td>
        <td>{{ product.starRating }}</td>
    </tr>
</tbody>
یکی دیگر از دایرکتیوهای ساختاری، ngFor* نام دارد. کار آن تکرار قسمتی از DOM، به ازای تک تک عناصر لیست انتساب داده شده‌ی به آن است.
بنابراین ابتدا قسمتی از عناصر HTML را طوری کنار هم قرار می‌دهیم که جمع آن‌ها یک تک آیتم را تشکیل دهند. سپس با استفاده از ngFor* به AngularJS 2.0 اعلام می‌کنیم که این قطعه را به ازای عناصر لیست دریافتی، تکرار و رندر کند.
برای نمونه در مثال فوق می‌خواهیم ردیف‌های جدول تکرار شوند. بنابراین هر ردیف را به عنوان یک قطعه‌ی تکرار شونده‌ی توسط ngFor* مشخص می‌کنیم. به این ترتیب این ردیف و عناصر فرزند آن، به ازای تک تک محصولات موجود در آرایه‌ی products، تکرار خواهند شد.
علامت # در اینجا (product#) یک متغیر محلی را تعریف می‌کند که تنها در قالب جاری قابل استفاده خواهد بود و همچنین فقط در فرزندان tr تعریف شده قابل دسترسی هستند.
به علاوه در اینجا بجای in از of استفاده شده‌است. این of از ES 6 گرفته شده‌است. زمانیکه از حلقه‌ی جدید for...of استفاده می‌شود، متغیر محلی product حاوی یک عنصر از لیست product خواهد بود؛ اما اگر از حلقه‌ی قدیمی for...in استفاده می‌شد، تنها ایندکس عددی این عناصر در دسترس قرار می‌گرفتند. به همین جهت است که در این حلقه، اکنون product.productName به نام محصول آن عنصر آرایه‌ی دریافتی اشاره می‌کند و قابل استفاده است.

تا اینجا اگر برنامه را اجرا کنید، چنین خروجی را مشاهده خواهید کرد:


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


خلاصه‌ی بحث

از inline templateها جهت معرفی قالب‌های کوتاه استفاده می‌شود. در اینجا از "" برای معرفی قالب یک سطری و یا از back tickهای ES 6، برای تعریف قالب‌های چندسطری استفاده خواهد شد. برای قالب‌های مفصل‌تر، بهتر است Linked templateها استفاده شود؛ با پشتیبانی کامل ادیتورهای موجود از لحاظ تکمیل و بررسی کدها.
برای استفاده از یک کامپوننت در کامپوننتی دیگر، نام selector آن‌را به صورت یک المان جدید HTML در قالب دیگری ذکر کرده و سپس با استفاده از خاصیت directives، نام کلاس متناظر با آن‌را نیز ذکر می‌کنیم. همچنین کار import ماژول آن نیز باید در ابتدای فایل صورت گیرد.
جهت غنی سازی قالب‌ها و کامپوننت‌ها و نمایش اطلاعات فقط خواندنی می‌توان از binding یک طرفه‌ی ویژه‌ای به نام interpolation استفاده کرد. کار آن اتصال یک خاصیت عمومی کلاس کامپوننت، به قالب آن است. interpolation توسط {{}} تعریف می‌شود و می‌تواند شامل محاسبات نیز باشد.
همچنین در ادامه‌ی بحث، نحوه‌ی کار با دو دایرکتیو توکار ساختاری AngularJS 2.0 را نیز بررسی کردیم. این دایرکتیوهای ساختاری نیاز است با ستاره شروع شوند و عبارت انتساب داده شده‌ی به آن‌ها باید داخل "" قرار گیرد (برخلاف interpolation که نیازی به اینکار ندارد). از ngIf* برای حذف یا افزودن یک المان و فرزندان آن از/به DOM استفاده می‌شود. اگر عبارت منتسب به آن به true ارزیابی شود، این المان از صفحه حذف خواهد شد. از ngFor* برای تکرار المانی مشخص به همراه فرزندان آن به تعداد اعضای لیستی که برای آن تعیین می‌گردد، استفاده می‌شود. متغیر محلی این پیمایشگر با # مشخص شده و حلقه‌ی آن با of بجای in تعریف می‌شود.
مطالب
MIME Sniffing و استاندارد OWASP

 یکی از نکات امنیتی که استاندارد  Owasp  بررسی مینماید هدر X-Content-Type-Options است که جهت جلوگیری از حملات از طریق فایل‌های نامرتبط میباشد. در این رخنه ممکن است فایلی که مرورگر دریافت میکند با آنچه که وب سایت ما آن را میشناسد متفاوت باشد. به عنوان مثال یک فایل اسکریپت که به عنوان یک فایل استایل معرفی میگردد ولی  قابلیت اجرای کدهای آن در مرورگر امکان پذیر است؛ به این نوع حملات MIME Sniffing میگویند. در یکی از سایت‌هایی که در حال حاضر در حال توسعه آن هستیم بخشی از گزارش‌ها با استفاده از ابزار FastReport  ایجاد شده بود و با توجه به اینکه در این ابزار از فایل‌های axd استفاده می‌گردید و مرورگر نوع دیتای برگشتی و MimeType معرفی شده را همخوان نمیدانست، از نمایش و بارگذاری آن ممانعت به عمل آورده و در نتیجه فایل گزارش دیده نمیشد. در این مقاله قصد داریم به معرفی این نوع حمله، روش جلوگیری از آن و همچنین رفع محدودیت پیش آمده را بررسی کرده تا در موارد مشابه از آن استفاده نماییم.


Mime Sniffing

زمانیکه مرورگر در هدر(پاسخ) Response، نوع محتوای ارسال شده، Content-Type را دریافت نکند، یا مرورگر متوجه مغایرتی در آن شود، این نوع رفتار را Mime Sniffing شناسایی میکند. نحوه این شناسایی‌ها در هر مرورگری میتواند متفاوت باشد؛ ولی عموما بر اساس type ارسالی و پسوند مورد نظر میباشد. در بعضی از موارد نیز خواندن بایت‌های ابتدایی یک فایل نیز میتواند نشان دهد که محتوای ارسالی واقعا چیست. به عنوان مثال برای فایل‌هایی با پسوند Gif، الگوی بایت‌های ابتدایی شامل  47 49 46 38 39 میباشد؛ ولی از آنجا که در همه فایل‌ها، بایت‌های ابتدایی الگوی یکسانی ندارند، پس نمی‌توان به این روش نیز بسنده کرد.


روش اینکه به مروگر بگوییم جلوی این نوع حملات را بگیرد و در صورت شناسایی Sniffing از اجرای آن سر باز بزند، استفاده از هدر X-Content-Type-Options میباشد که نحوه افزودن آن در فایل web.config  به شکل زیر است:

 <httpProtocol>
      <customHeaders>
  ...
        <remove name="X-Content-Type-Options"/>
        <add name="X-Content-Type-Options" value="nosniff" />
        ..
      </customHeaders>
    </httpProtocol>

در پروژه ما به دلیل اینکه بخشی از گزارش‌ها با استفاده از FastReport طراحی شده بود این مورد برای ما ایجاد مشکل می‌کرد و در گزارش نمایش داده نمی‌شد و در کنسول پیام‌هایی به شکل زیر دریافت میکردیم:

Refused to execute script from 'https://localhost:44377/WebResource.axd?d=xxx' because its MIME type ('text/js') is not executable, and strict MIME type checking is enabled.

با نگاهی به Response  دریافتی نیز میتوان بازگشت هدر امنیتی X-Content-Type-Options را نیز مشاهده نمود:

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

  <location path="WebResource.axd" >
    <system.webServer>
      <httpProtocol>
        <customHeaders>
          <remove name="X-Content-Type-Options" />
        </customHeaders>
      </httpProtocol>
    </system.webServer>
  </location>

 نتیجه آن را میتوانید در صفحه ذیل برای همان درخواست و پاسخ قبلی نیز مشاهده نمایید:

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


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

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

    this.products = [];

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



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

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


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

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

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

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

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


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

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

export class ProductGroupComponent implements OnInit {

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

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

    this.products = [];

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



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

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

export class AppErrorHandler implements ErrorHandler {

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

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

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

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



افزودن ToastyService به AppErrorHandler

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

export class AppErrorHandler implements ErrorHandler {

  constructor(private toastyService: ToastyService) {
  }

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

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

export class AppErrorHandler implements ErrorHandler {

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

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


مفهوم Zones در Angular

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

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

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

export class AppErrorHandler implements ErrorHandler {

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

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

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

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


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


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

این افزونه با استفاده از ابزار Visual Studio Tools for Office که به VSTO مشهور شده است، تهیه شد. در بسته به روز رسانی سیستم که در ذیل (معرفی افزونه) نیز معرفی شد نگارش sp1 vsto3.0 آن به صورت خودکار نصب خواهد شد.
برای ایجاد این پروژه در VS.Net 2008 ، تنها کافی است یک پروژه جدید Word add-in را آغاز نمائیم. (شکل زیر)





قبل از ادامه بحث، بهتر است در مورد بانک اطلاعاتی مورد استفاده نیز توضیح داده شود. در اینجا از SQLite استفاده شد. (بسیار سبک، کم حجم و سریع است و اساسا یک کاربر نهایی برای تنظیمات آن نیازی نیست اطلاعاتی داشته باشد). بسته به روز رسانی سیستم (در مطلب قبلی)، این مورد را نیز به صورت خودکار نصب خواهد کرد (در GAC باید نصب شود وگرنه افزونه قادر به یافتن آن نخواهد شد).
برای ایجاد این بانک اطلاعاتی، از افزونه SQLite manager برای فایرفاکس استفاده شد. (این افزونه رایگان شما را از هر ابزار جانبی برای مدیریت یک بانک اطلاعاتی SQLite بی‌نیاز می‌کند)
برای مثال فایل ErrorsBank.sqlite برنامه افزونه فارسی به پارسی را توسط افزونه SQLite manager فایرفاکس باز کنید (این فایل را در محل نصب افزونه می‌توانید پیدا کنید). در اینجا می‌توان جداول جدید را ایجاد کرد، کوئری‌های دلخواه را اجرا نمود و یا اطلاعات را مرور کرده، حذف یا ویرایش کرد (شکل زیر).




و خوشبختانه این بانک اطلاعاتی و محصور کننده‌های آن با اطلاعات یونیکد فارسی هیچ مشکلی ندارند و برای کارهایی با وسعت کم و تعداد رکورد پائین یکی از بهترین انتخاب‌ها به‌شمار می‌روند.
نحوه استفاده از SQLite نیز در دات نت بسیار ساده است. اگر با ADO.Net کار کرده باشید، پس از افزودن ارجاعی از اسمبلی System.Data.SQLite.DLL به پروژه و معرفی فضای نام آن به پروژه، تنها کافی است در کدهای قبلی خود برای مثال SqlConnection را به SQLiteConnectionتغییر دهید و امثال آن. یعنی دانش ADO.Net شما در اینجا نیز کاملا قابل استفاده خواهد بود و نیازی نیست مدتی را صرف آشنا شدن با کلاس‌ها و مفاهیم جدید نمائید (البته این تنها زمانی معنا خواهد داشت که به ویزاردها عادت نکرده باشید و کارهای خود را با کد نویسی انجام داده باشید).
تنها یک نکته را باید به‌خاطر داشت و آن هم مربوط است به ساز و کار درونی SQLite . هنگام انجام عملیات update یا insert حتما از transaction استفاده کنید تا سرعت کوئری‌های شما در SQLite به نحو شگفت انگیزی افزایش یابد. مثالی در این مورد را در فایل chm راهنمای SQLite.NET می‌توانید پیدا کنید.

مطلب دیگری که پیش از پرداختن به کد نویسی افزونه باید با آن آشنا شویم، مفهوم smart tags در مجموعه آفیس است که در این پروژه از آن استفاده گردید.
smart tags در مجموعه آفیس برچسب‌هایی هستند که به صورت خودکار توسط یکی از محصولات آفیس مثلا ورد یا اکسل و امثال آن، پس از تشخیص یک کلمه خاص ایجاد می‌شوند و می‌توان اعمالی را به این برچسب ایجاد شده انتساب داد. برای مثال در اینجا امکان جایگزین کردن کلمه فارسی با معادل پارسی در نظر گرفته شد.
ویدیویی در مورد نحوه ایجاد اسمارت تگ‌ها در VS.Net و یا مثالی پیشرفته‌تر در مورد تشخیص دمای فارنهایت در یک متن و ایجاد smart tag مخصوص به آن برای تبدیل به سلسیوس. (از regular expressions جهت یافتن یک الگو در متن استفاده شده است)

در این پروژه، حدود 3800 واژه فارسی به‌ یک smart tag انتساب داده می‌شود (در روال استاندارد ThisAddIn_Startup). سپس در هنگام نمایش آن، معادل پارسی کلمه نیز به منوی باز شده افزوده گشته و در روال رخداد کلیک آن، تعویض کلمه تشخیص داده شده با واژه پیدا شده صورت خواهد گرفت.

در ادامه فرض بر این است که یک پروژه جدید word add-in را در VS.Net ایجاد کرده‌اید و همچنین ارجاعی را به فایل System.Data.SQLite.DLL افزوده‌اید.

using System;
using System.Diagnostics;
using Microsoft.Office.Tools.Word;
using Action = Microsoft.Office.Tools.Word.Action;

private SmartTag _st;
private void init()
{
try
{
//Enable Smart Tags in Word
if (!Application.Options.LabelSmartTags)
{
//ممکن است اسمارت تگ‌ها در ورد غیرفعال باشند. به این صورت می‌شود آنها را فعال کرد
Application.Options.LabelSmartTags = true;
}

_st = new SmartTag(@"www.microsoft.com/Demo#FarsiSmartTag", @"فارسی به پارسی");

//دریافت واژه‌های فارسی از دیتابیس و افزودن خودکار آنها به اسمارت تگ‌ها
if (!DBhelper.AddSmartTagItems(_st, "select distinct farsi from tblFarsiToParsi")) return;

Action stActions = new Action("تبدیل");//تعریف یک اکشن جدید
stActions.Click += stActions_Click;//انتساب روال‌های رخداد گردان
stActions.BeforeCaptionShow += stActions_BeforeCaptionShow;
_st.Actions = new[] { stActions };
VstoSmartTags.Add(_st);//افزودن اسمارت تگ به مجموعه
}
catch (Exception ex)
{
EventLog.WriteEntry("FarsiToParsi", ex.ToString(), EventLogEntryType.Error, 7);
}
}

private void ThisAddIn_Startup(object sender, EventArgs e)
{
init();
}

دو روال رخداد گردان زیر نیز جهت تغییر عنوان پیش فرض به واژه یافته شده در لحظه نمایش منو و روال کلیک نیز ایجاد خواهد شد:

static void stActions_BeforeCaptionShow(object sender, ActionEventArgs e)
{
try
{
Action clickedAction = sender as Action;
if (clickedAction != null)
{
string parsi = DBhelper.FindParsi(e.Text);//معادل پارسی از دیتابیس دریافت می‌شود
clickedAction.Caption = (parsi == string.Empty ? e.Text : parsi);
}
}
catch (Exception ex)
{
EventLog.WriteEntry("FarsiToParsi", ex.ToString(), EventLogEntryType.Error, 7);
}
}

static void stActions_Click(object sender, ActionEventArgs e)
{
try
{
Action clickedAction = sender as Action;
if (clickedAction != null)
{
e.Range.Text = clickedAction.Caption;//جایگزینی متن موجود با عنوانی که پیشتر پارسی شده است
}
}
catch (Exception ex)
{
EventLog.WriteEntry("FarsiToParsi", ex.ToString(), EventLogEntryType.Error, 7);
}
}

نکته‌ای را که در اینجا باید حتما رعایت کرد بحث exception handling‌ است. خصوصا در روال استاندارد ThisAddIn_Startup . اگر در این روال خطایی مدیریت نشده رخ دهد، word افزودنی شما را به صورت غیرفعال به مجموعه اضافه خواهد کرد و فعال سازی بعدی آن پس از اصلاح کد واقعا مشکل خواهد بود. همانطور که ملاحظه می‌کنید تمامی خطاها در event log‌ ویندوز نوشته می‌شوند.
همچنین باید دقت داشت که اگر متغیری در سطح کلاس تعریف نشود به احتمال زیاد تا دقایقی بعد توسط garbage collector به دیار باقی خواهد شتافت (تعریف st_ در اینجا). اینجاست که شاید ساعت‌ها وقت صرف کنید که چرا روال‌های رخ‌داد گردان دیگر اجرا نمی‌شوند. چرا افزونه دیگر کار نمی‌کند.

همین! کل سورس این add-in منهای بحث دریافت اطلاعات از دیتابیس همین بود! وظیفه‌ی تشخیص کلمات معرفی شده به ms-word به‌عهده‌ی خود آن است و این‌کار را نیز به‌خوبی انجام می‌دهد. در گذشته‌های نچندان دور ایجاد یک افزونه برای word واقعا مشکل بود که با این روش بسیاری از موانع برطرف شده است.

کلاس DBHelper که کار دریافت اطلاعات واژه‌ها را از دیتابیس SQLite انجام می‌دهد به شرح زیر است:

using System;
using System.Data.SQLite;
using System.Diagnostics;
using System.Reflection;
using Microsoft.Office.Tools.Word;

namespace Farsi2Parsi
{
class DBhelper
{
#region Methods (2)

// Public Methods (2)

public static bool AddSmartTagItems(SmartTag st, string strSQL)
{
SQLiteDataReader myReader = null;
SQLiteCommand sqlCmd = null;
bool ret = false;
try
{
SQLiteConnection sqlCon = new SQLiteConnection
{
ConnectionString = "Data Source=" + ConStr.ConnectionString
};
sqlCon.Open();
sqlCmd = new SQLiteCommand(strSQL, sqlCon);
myReader = sqlCmd.ExecuteReader();

if (myReader != null)
while (myReader.Read())
{
if (myReader.GetValue(0) != DBNull.Value)
st.Terms.Add(myReader.GetValue(0).ToString());
}

ret = true;
}
catch (Exception ex)
{
EventLog.WriteEntry("FarsiToParsi", ex + "\n" + Environment.CurrentDirectory + "\n" +
Assembly.GetExecutingAssembly().Location, EventLogEntryType.Error, 7);
}
finally
{
if (myReader != null)
myReader.Close();

if (sqlCmd != null)
sqlCmd.Connection.Close();
}
return ret;
}

public static string FindParsi(string farsi)
{
SQLiteDataReader myReader = null;
SQLiteCommand sqlCmd = null;
string ret = string.Empty;
string strSQL = "select parsi from tblFarsiToParsi where farsi='" + farsi.Replace("'", "''") + "'";
try
{
SQLiteConnection sqlCon = new SQLiteConnection
{
ConnectionString = "Data Source=" + ConStr.ConnectionString
};
sqlCon.Open();
sqlCmd = new SQLiteCommand(strSQL, sqlCon);
myReader = sqlCmd.ExecuteReader();

if (myReader != null)
{
myReader.Read(); //اولین مورد کافی است
if (myReader.GetValue(0) != DBNull.Value)
ret = myReader.GetValue(0).ToString();
}
}
catch (Exception ex)
{
EventLog.WriteEntry("FarsiToParsi", ex + "\n" + Environment.CurrentDirectory + "\n" +
Assembly.GetExecutingAssembly().Location, EventLogEntryType.Error, 8);
}
finally
{
if (myReader != null)
myReader.Close();

if (sqlCmd != null)
sqlCmd.Connection.Close();
}
return ret;
}
#endregion Methods
}
}

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

هنگام نصب برنامه، مسیر پوشه نصب در رجیستری ویندوز توسط نصاب نوشته خواهد شد. از همین مورد برای ایجاد رشته اتصالی به دیتابیس استفاده گردید.

class ConStr
{
public static string ConnectionString
{
get
{
return Microsoft.Win32.Registry.LocalMachine.OpenSubKey("SOFTWARE\\FarsiToParsi").GetValue("folder") + "\\ErrorsBank.sqlite";
}
}
}

سورس کامل این افزونه را به صورت یک پروژه VS.Net 2008 SP1 از اینجا می‌توانید دریافت کنید.
نصاب برنامه با استفاده از NSIS ایجاد شده که در روزی دیگر درباره‌ی آن توضیح خواهم داد.
اگر قصد داشته باشید از روش‌های متداول استفاده کنید، مشاهده ویدیوی زیر توصیه می‌شود:
http://msdn.microsoft.com/en-us/office/bb851702.aspx

برای توزیع این نوع افزونه‌ها علاوه بر دات نت فریم ورک، به چهار به روز رسانی دیگر نیز نیاز خواهد بود:
به روز رسانی نصاب ویندوز (که احتمالا نصب هست)
WindowsInstaller-KB893803-v2-x86.exe
Microsoft Office System Update: Redistributable Primary Interop Assemblies :
o2007pia.msi
نصب vsto و همچنین sp1 آن
vstor30.exe
vstor30sp1-KB949258-x86.exe

این موارد را من در بسته به روز رسانی سیستم قرار داده‌ام که به صورت خودکار و یکی پس از دیگری اجرا و نصب خواهند شد.
پس از آن با کلیک بر روی فایلی با پسوند vsto که در پوشه build برنامه موجود است، می‌توان افزونه را نصب کرد (click once installation).




سایر اطلاعات در مورد پروژه‌های VSTO را می‌توان از طریق وبلاگ رسمی آنها دنبال کرد:
http://blogs.msdn.com/vsto/

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


مطالب
آموزش زمانبندی کارها با HangFire در Asp.Net Core

تسک‌های پس زمینه (Background Job) چیست؟

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


سؤال : HangFire چیست؟

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

مقایسه HangFire و Quartz 

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

دیتابیس :

تفاوتی که میتوان از آن نام برد، وجود قابلیت Redis Store در HangFire است که Quratz چنین قابلیتی را از سمت خودش ارائه نداده و برای استفاده از Redis در Quartz باید شخصا این دو را باهم کانفیگ کنید. دیتابیس Redis بخاطر ساختار دیتابیسی که دارد، سرعت و پرفرمنس بالاتری را ارائه میدهد که استفاده از این قابلیت، در پروژه‌هایی با تعداد تسک‌ها و رکورد‌های زیاد، کاملا مشهود است. البته این ویژگی در HangFire رایگان نیست و برای داشتن آن از سمت HangFire، لازم است هزینه‌ی آن را نیز پرداخت کنید؛ اما اگر هم نمیخواهید پولی بابتش بپردازید و همچنان از آن استفاده کنید، یک پکیج سورس باز برای آن نیز طراحی شده که از این لینک میتوانید آن‌را مشاهده کنید. 

ساختار :

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

داشبورد :

هردو پکیج از داشبورد، پشتیبانی میکنند که میتوانید در این داشبورد و ui اختصاصی که برای نمایش تسک‌ها طراحی شده، تسک‌های ایجاد شده را مدیریت کنید. داشبورد HangFire بصورت پیشفرض همراه با آن قرار دارد که بعد از نصب HangFire میتوانید براحتی داشبورد سوار بر آن را نیز مشاهده کنید. اما در Quartz ، داشبورد باید بصورت یک Extension، در پکیجی جدا به آن اضافه شود و مورد استفاده قرار گیرد. در لینک‌های 1 و 2، دوتا از بهترین داشبورد‌ها برای Quartz را مشاهده میکنید که در صورت نیاز میتوانید از آن‌ها استفاده کنید. 

استفاده از HangFire

1. نصب :

  • برای نصب HangFire در یک پروژه‌ی Asp.Net Core لازم است ابتدا پکیج‌های مورد نیاز آن را نصب کنید؛ که شامل موارد زیر است:
Install-Package Hangfire.Core 
Install-Package Hangfire.SqlServer
Install-Package Hangfire.AspNetCore
  • پس از نصب پکیج‌ها باید تنظیمات مورد نیاز برای پیاده سازی HangFire را در برنامه، اعمال کنیم. این تنظیمات شامل افزودن سرویس‌ها و اینترفیس‌های HangFire به برنامه است که اینکار را با افزودن HangFire به متد ConfigureService کلاس Startup انجام خواهیم داد: 
public void ConfigureServices(IServiceCollection services)
{
    // Add Hangfire services.
    services.AddHangfire(configuration => configuration
        .SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
        .UseSimpleAssemblyNameTypeSerializer()
        .UseRecommendedSerializerSettings()
        .UseSqlServerStorage(Configuration.GetConnectionString("HangfireConnection"), new SqlServerStorageOptions
        {
            CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
            SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
            QueuePollInterval = TimeSpan.Zero,
            UseRecommendedIsolationLevel = true,
            DisableGlobalLocks = true
        }));

    // Add the processing server as IHostedService
    services.AddHangfireServer();

    // Add framework services.
    services.AddMvc();
}
  • پکیج HangFire برای مدیریت کار و زمان ، Table هایی دارد که پس از نصب، بر روی دیتابیس برنامه‌ی شما قرار میگیرد. فقط باید دقت داشته باشید ConnectionString دیتابیس خود را در متد AddHangFire مقدار دهی کنید، تا از این طریق دیتابیس برنامه را شناخته و Table‌های مورد نظر را در Schema جدیدی با نام HangFire به آن اضافه کند. 

پ ن : HangFire بصورت پیش‌فرض با دیتابیس SqlServer ارتباط برقرار میکند.

  • این پکیج یک داشبورد اختصاصی دارد که در آن لیستی از انواع تسک‌های در صف انجام و گزارشی از انجام شده‌ها را در اختیار ما قرار میدهد. برای تنظیم این داشبورد باید Middleware مربوط به آن و endpoint جدیدی را برای شناسایی مسیر داشبورد HangFire در برنامه، در متد Configure کلاس Startup اضافه کنید : 
public void Configure(IApplicationBuilder app, IBackgroundJobClient backgroundJobs, IHostingEnvironment env)
{
    // HangFire Dashboard
    app.UseHangfireDashboard();

     app.UseEndpoints(endpoints =>
     {
         endpoints.MapControllers();
         
         // HangFire Dashboard endpoint
         endpoints.MapHangfireDashboard();
     });
}

برای اینکه به داشبورد HangFire دسترسی داشته باشید، کافیست پس از نصب و انجام تنظیمات مذکور، برنامه را اجرا کنید و در انتهای Url برنامه، کلمه‌ی "hangfire" را وارد کنید. سپس وارد پنل داشبورد آن خواهید شد. 
http://localhost:50255/hangfire
البته میتوانید آدرس داشبورد HangFire را در برنامه، از کلمه‌ی "hangfire" به هر چیزی که میخواهید شخصی سازی کنید. برای اینکار کافیست درون Middleware تعریف شده بصورت ورودی string، آدرس جدیدی را برای HangFire تعریف کنید. 
app.UseHangfireDashboard("/mydashboard");
و به طبع در Url : 
http://localhost:50255/mydashboard

2. داشبورد :

داشبورد HangFire شامل چندین بخش و تب مختلف است که به اختصار هر یک را بررسی خواهیم کرد. 



تب Job :
همه‌ی تسک‌های تعریف شده، شامل Enqueued, Succeeded, Processing, Failed و... در این تب نشان داده میشوند. 


تب Retries :
این تب مربوط به تسک‌هایی است که در روال زمانبندی و اجرا، به دلایل مختلفی مثل Stop شدن برنامه توسط iis یا Down شدن سرور و یا هر عامل خارجی دیگری، شکست خوردند و در زمانبندی مشخص شده، اجرا نشدند. همچنین قابلیت دوباره‌ی به جریان انداختن job مورد نظر را در اختیار ما قرار میدهد که از این طریق میتوان تسک‌های از دست رفته را مدیریت کرد و دوباره انجام داد. 


تب Recurring Jobs :
وقتی شما یک تسک را مانند گرفتن بکاپ از دیتابیس، بصورت ماهانه تعریف میکنید و قرار است در هر ماه، این اتفاق رخ دهد، این مورد یک تسک تکراری تلقی شده و این تب مسئول نشان دادن اینگونه از تسک‌ها میباشد. 


تب Servers :
این بخش، سرویس‌هایی را که HangFire برای محاسبه‌ی زمانبندی از آن‌ها استفاده میکند، نشان میدهد. وقتی متد services.AddHangfireServer را به متد ConfigureService کلاس Startup اضافه میکنید، سرویس‌های HangFire جهت محاسبه‌ی زمانبندی‌ها فعال میشوند. 


3. امنیت داشبورد :

همانطور که دانستید، داشبورد، اطلاعات کاملی از نوع کارها و زمان اجرای آن‌ها و نام متدها را در اختیار ما قرار میدهد و همچنین اجازه‌ی تغییراتی را مثل حذف یک تسک، یا دوباره به اجرا در آوردن تسک‌ها و یا اجرای سریع تسک‌های به موعد نرسیده را به کاربر میدهد. گاهی ممکن است این اطلاعات، شامل محتوایی امنیتی و غیر عمومی باشد که هرکسی در برنامه حق دسترسی به آن‌ها را ندارد. برای مدیریت کردن این امر، میتوانید مراحل زیر را طی کنید :
مرحله اول : یک کلاس را ایجاد میکنیم (مثلا با نام MyAuthorizationFilter) که این کلاس از اینترفیسی با نام IDashboardAuthorizationFilter ارث بری خواهد کرد. 
public class MyAuthorizationFilter : IDashboardAuthorizationFilter
{
    public bool Authorize(DashboardContext context)
    {
        var httpContext = context.GetHttpContext();

        // Allow all authenticated users to see the Dashboard (potentially dangerous).
        return httpContext.User.Identity.IsAuthenticated;
    }
}
درون این کلاس، متدی با نام Authorize از اینترفیس مربوطه، پیاده سازی میشود که شروط احراز هویت و صدور یا عدم صدور دسترسی را کنترل میکند. این متد، یک خروجی Boolean دارد که اگر هر یک از شروط احراز هویت شما تایید نشد، خروجی false را بر میگرداند. در این مثال، ما برای دسترسی، محدودیت Login بودن را اعمال کرده‌ایم که این را از HttpContext میگیریم.

مرحله دوم : در این مرحله کلاسی را که بعنوان فیلتر احراز هویت برای کاربران ساخته‌ایم، در option‌های middleware پکیج HangFire اضافه میکنیم. 
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
    Authorization = new [] { new MyAuthorizationFilter() }
});
یکی دیگر از option‌های این middleware که میتوان برای کنترل دسترسی در HangFire استفاده کرد، گزینه‌ی Read-only view نام دارد. 
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
    IsReadOnlyFunc = (DashboardContext context) => true
});
این گزینه اجازه‌ی هرگونه تغییری را در روند تسک‌ها، از طریق صفحه‌ی داشبورد، از هر کاربری سلب میکند و داشبورد را صرفا به جهت نمایش کار‌ها استفاده میکند نه چیز دیگر.



انواع تسک‌ها در HangFire :

1. تسک‌های Fire-And-Forget :

تسک‌های Fire-And-Forget زمانبندی خاصی ندارند و بلافاصله بعد از فراخوانی، اجرا میشوند. برای مثال شرایطی را در نظر بگیرید که میخواهید پس از ثبت نام هر کاربر در وب‌سایت، یک ایمیل خوش آمد گویی ارسال کنید. این عمل یک تسک پس زمینه تلقی میشود، اما زمانبندی خاصی را نیز نمیخواهید برایش در نظر بگیرید. در چنین شرایطی میتوانید از متد Enqueue استفاده کنید و یک تسک Fire-And-Forget را ایجاد کنید تا این تسک صرفا در تسک‌های پس زمینه‌تان نام برده شود و قابل مشاهده باشد. 
public class HomeController : Controller
    {
        private readonly IBackgroundJobClient _backgroundJobClient;
        public HomeController(IBackgroundJobClient backgroundJobClient)
        {
            _backgroundJobClient = backgroundJobClient;
        }
    
         [HttpPost]
         [Route("welcome")]
         public IActionResult Welcome(string userName)
         {
             var jobId = _backgroundJobClient.Enqueue(() => SendWelcomeMail(userName));
             return Ok($"Job Id {jobId} Completed. Welcome Mail Sent!");
         }

         public void SendWelcomeMail(string userName)
         {
             //Logic to Mail the user
             Console.WriteLine($"Welcome to our application, {userName}");
         }    
    }
همانطور که میبینید در مثال بالا ابتدا برای استفاده از تسک‌های Fire-and-Forget در HangFire باید اینترفیس IBackgroundJobClient را تزریق کنیم و با استفاده از متد Enqueue در این اینترفیس، یک تسک پس زمینه را ایجاد میکنیم که کار آن، فراخوانی متد SendWelcomeMail خواهد بود.

2. تسک‌های Delayed :

همانطور که از اسم آن پیداست، تسک‌های Delayed، تسک‌هایی هستند که با یک تاخیر در زمان، اجرا خواهند شد. بطور کلی زمانبندی این تسک‌ها به دو دسته تقسیم میشود :

  • دسته اول : اجرا پس از تاخیر در زمانی مشخص.
همان شرایط ارسال ایمیل را به کاربرانی که در وبسایتتان ثبت نام میکنند، در نظر بگیرید؛ اما اینبار میخواهید نه بلافاصله، بلکه 10 دقیقه بعد از ثبت نام کاربر، ایمیل خوش آمد گویی را ارسال کنید. در این نوع شما یک تاخیر 10 دقیقه‌ای میخواهید که Delayed Job‌ها اینکار را برای ما انجام میدهند. 
public class HomeController : Controller
    {
        private readonly IBackgroundJobClient _backgroundJobClient;
        public HomeController(IBackgroundJobClient backgroundJobClient)
        {
            _backgroundJobClient = backgroundJobClient;
        }
    
         [HttpPost]
         [Route("welcome")]
         public IActionResult Welcome(string userName)
         {
             var jobId = BackgroundJob.Schedule(() => SendWelcomeMail(userName),TimeSpan.FromMinutes(10));
             return Ok($"Job Id {jobId} Completed. Welcome Mail Sent!");
         }

         public void SendWelcomeMail(string userName)
         {
             //Logic to Mail the user
             Console.WriteLine($"Welcome to our application, {userName}");
         }    
    }
در این مثال با استفاده از متد Schedule در اینترفیس IBackgroundJobClient توانستیم متد SendWelcomeMail را صدا بزنیم و با ورودی TimeSpan یک تاخیر 10 دقیقه‌ای را در متد HangFire اعمال کنیم.
همچنین میتوانید از ورودی‌های دیگر نوع TimeSpan شامل TimeSpan.FromMilliseconds و TimeSpan.FromSecondsو TimeSpan.FromMinutes و TimeSpan.FromDays برای تنظیم تاخیر در تسک‌های خود استفاده کنید.

  • دسته دوم : اجرا در زمانی مشخص.
نوع دیگر استفاده از متد Schedule، تنظیم یک تاریخ و زمان مشخصی برای اجرا شدن تسک‌های در آن تاریخ و زمان واحد میباشد. برای مثال سناریویی را در نظر بگیرید که دستور اجرا و زمانبندی آن، در اختیار کاربر باشد و کاربر بخواهد یک Reminder، در تاریخ مشخصی برایش ارسال شود که در اینصوررت میتوانید با استفاده از instance دیگری از متد Schedule که ورودی ای از جنس DateTimeOffset را دریافت میکند، تاریخ مشخصی را برای آن اجرا، انتخاب کنید. 
public class HomeController : Controller
    {
        private readonly IBackgroundJobClient _backgroundJobClient;
        public HomeController(IBackgroundJobClient backgroundJobClient)
        {
            _backgroundJobClient = IBackgroundJobClient;
        }
    
         [HttpPost]
         [Route("welcome")]
         public IActionResult Welcome(string userName , DateTime dateAndTime)
         {
             var jobId = BackgroundJob.Schedule(() => SendWelcomeMail(userName),DateTimeOffset(dateAndTime));
             return Ok($"Job Id {jobId} Completed. Welcome Mail Sent!");
         }

         public void SendWelcomeMail(string userName)
         {
             //Logic to Mail the user
             Console.WriteLine($"Welcome to our application, {userName}");
         }    
    }
در این مثال، تاریخ مشخصی را برای اجرای تسک‌های خود، از کاربر، در ورودی اکشن متد دریافت کرده‌ایم و به متد Schedule، در غالب DateTimeOffset تعریف شده، پاس میدهیم.

3. تسک‌های Recurring :

تسک‌های Recurring به تسک‌هایی گفته میشود که باید در یک بازه‌ی گردشی از زمان اجرا شوند. در یک مثال، بیشتر با آن آشنا خواهیم شد. فرض کنید میخواهید هر هفته، برنامه از اطلاعات دیتابیس موجود بکاپ بگیرد. در اینجا تسکی را دارید که قرار است هر هفته و هربار به تکرر اجرا شود. 
public class HomeController : Controller
    {
        private readonly IRecurringJobManager _recurringJobManager;
        public HomeController(IRecurringJobManager recurringJobManager)
        {
            _recurringJobManager = recurringJobManager;
        }
    
         [HttpPost]
         [Route("BackUp")]
         public IActionResult BackUp(string userName)
         {
             _recurringJobManager.AddOrUpdate("test", () => BackUpDataBase(), Cron.Weekly);
             return Ok();
         }

         public void BackUpDataBase()
         {
            // ...
         }    
    }
برای تنظیم یک Recurring Job باید اینترفیس دیگری را بنام IRecurringJobManager، تزریق کرده و متد AddOrUpdate را استفاده کنید. در ورودی این متد، یک نوع تعریف شده در HangFire بنام Cron دریافت میشود که بازه‌ی گردش در زمان را دریافت میکند که در اینجا بصورت هفتگی است.

انواع دیگر Cron شامل :

  • هر دقیقه (Cron.Minutely) :
این Cron هر دقیقه یکبار اجرا خواهد شد. 
 _recurringJobManager.AddOrUpdate("test", () => job , Cron.Minutely);
  • هر ساعت (Cron.Hourly) :
این Cron هر یک ساعت یکبار و بصورت پیش‌فرض در دقیقه اول هر ساعت اجرا میشود. 
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Hourly);
اما میتوانید یک ورودی دقیقه به آن بدهید که در اینصورت در N اُمین دقیقه از هر ساعت اجرا شود. 
 _recurringJobManager.AddOrUpdate("test", () => Job, Cron.Hourly(10));
  • هر روز (Cron.Daily) :
این Cron بصورت روزانه و در حالت پیشفرض، در اولین ساعت و اولین دقیقه‌ی هر روز اجرا خواهد شد. 
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Daily);
در حالتی دیگر میتوانید ورودی ساعت و دقیقه را به آن بدهید تا در ساعت و دقیقه‌ای مشخص، در هر روز اجرا شود. 
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Daily(3,10));
  • هر هفته (Cron.Weekly) :
این Cron هفتگی است. بصورت پیشفرض هر هفته، شنبه در اولین ساعت و در اولین دقیقه، اجرا میشود. 
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Weekly);
در حالتی دیگر چندمین روز هفته و ساعت و دقیقه مشخصی را در ورودی میگیرد و حول آن میچرخد. 
_recurringJobManager.AddOrUpdate("test", () => Job,Cron.Weekly(DayOfWeek.Monday,3,10));
  • هر ماه (Cron.Monthly) :
این Cron بصورت ماهانه اولین روز ماه در اولین ساعت روز و در اولین دقیقه ساعت، زمانبندی خود را اعمال میکند. 
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Monthly);
و در صورت دادن ورودی میتوانید زمانبندی آن در چندمین روز ماه، در چه ساعت و دقیقه‌ای را نیز تنظیم کنید. 
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Monthly(10,3,10));
  • هر سال (Cron.Yearly) :
و در نهایت این Cron بصورت سالانه و در اولین ماه، روز، ساعت و دقیقه هر سال، وظیفه خود را انجام خواهد داد. 
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Yearly);
که این‌هم مانند بقیه، ورودی‌هایی دریافت میکند که به ترتیب شامل ماه، روز، ساعت و دقیقه است. 
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Yearly(2,4,3,10));
در نهایت با استفاده از این Cron‌ها میتوانید انواع مختلفی از Recurring Job‌ها را بسازید.

4. تسک‌های Continuations :

این نوع از تسک‌ها، وابسته به تسک‌های دیگری هستند و بطور کلی وقتی استفاده میشوند که ما میخواهیم تسکی را پس از تسک دیگری، با یک زمانبندی، به نسبت زمان اجرای تسک اول، اجرا کنیم. برای مثال میخواهیم 10 دقیقه بعد از ثبت نام کاربر، برای او ایمیل احراز هویت ارسال شود که شبیه اینکار را در تسک‌های Delayed انجام داده بودیم. اما همچنین قصد داریم 5 دقیقه بعد از ارسال ایمیل احراز هویت، لینک فرستاده شده را منسوخ کنیم. در این سناریو ما دو زمانبندی داریم؛ اول 10 دقیقه بعد از ثبت نام کاربر و دوم 5 دقیقه بعد از اجرای متد اول. 
var stepOne = _backgroundJobClient.Schedule(() => SendAuthorizationEmail(), TimeSpan.FromMinute(10));
_backgroundJobClient.ContinueJobWith(stepOne, () => _backgroundJobClient.Schedule(() => ExpireAuthorizationEmail(), TimeSpan.FromHours(5)));
برای ایجاد یک Continuations Job باید از متد ContinueJobWith در اینترفیس IBackgroundJobClient استفاده کنیم و در ورودی اول این متد، آیدی تسک ایجاد شده قبلی را پاس دهیم.

برخی از نکات و ترفند‌های HangFire

1. استفاده از Cron Expression در Recurring Job‌ها :

بطور کلی، Cron‌، ساختاری تعریف شده برای تعیین بازه‌های زمانی است. Cron اختصار یافته‌ی کلمات Command Run On میباشد که به اجرا شدن یک دستور، در زمان مشخصی اشاره دارد. برای استفاده از آن، ابتدا به تعریف این ساختار میپردازیم : 
* * * * * command to be executed
- - - - -
| | | | |
| | | | ----- Day of week (0 - 7) (Sunday=0 or 7)
| | | ------- Month (1 - 12)
| | --------- Day of month (1 - 31)
| ----------- Hour (0 - 23)
------------- Minute (0 - 59)
این ساختار را از پایین به بالا در زیر برایتان تشریح میکنیم : 
* * * * *
  • فیلد اول (Minute) : در این فیلد باید دقیقه‌ای مشخص از یک ساعت را وارد کنید؛ مانند دقیقه 10 (میتوانید محدوده‌ای را هم تعیین کنید)
  • فیلد دوم (Hour) : در این فیلد باید زمان معلومی را با فرمت ساعت وارد کنید؛ مانند ساعت 7 (میتوانید محدوده‌ای  را هم تعیین کنید، مانند ساعات 12-7)
  • فیلد سوم (Day of Month) : در این فیلد باید یک روز از ماه را وارد کنید؛ مانند روز 15 ام از ماه (میتوانید محدوده‌ای را هم تعیین کنید)
  • فیلد چهارم (Month) : در این فیلد باید یک ماه از سال را وارد کنید؛ مثلا ماه 4 ام(آوریل) (میتوانید محدوده‌ای را هم تعیین کنید)
  • فیلد پنجم (Day of Week) : در این فیلد باید روزی از روز‌های هفته یا محدوده‌ای از آن روز‌ها را تعیین کنید. مانند صفرم هفته که در کشور‌های اروپایی و آمریکایی معادل روز یکشنبه است.
همانطور که میبینید، Cron‌ها دسترسی بهتری از تعیین بازه‌های زمانی مختلف را ارائه میدهند که میتوانید از آن در Recurring متد‌ها بجای ورودی‌های Yearly - Monthly - Weekly - Daily - Hourly - Minutely استفاده کنید. در واقع خود این ورودی‌ها نیز متدی تعریف شده در کلاس Cron هستند که با فراخوانی آن، خروجی Cron Expression را میسازند و در درون ورودی متد Recurring قرار میگیرند.

در ادامه مثالی را خواهیم زد تا نیازمندی به Cron Expression‌ها را بیشتر درک کنید. فرض کنید میخواهید یک زمانبندی داشته باشید که "هر ماه بین بازه 10 ام تا 15 ام، بطور روزانه در ساعت 4:00" اجرا شود. اعمال این زمانبندی با متد‌های معمول در کلاس Cron امکان پذیر نیست؛ اما میتوانید با Cron Expression آن‌را اعمال کنید که به این شکل خواهد بود: 
0 4 10-15 * *
برای ساخت Cron Expression‌ها وبسایت هایی وجود دارند که کمک میکنند انواع Cron Expression‌های پیچیده‌ای را طراحی کنیم و با استفاده از آن، زمانبندی‌های دقیق‌تر و جزئی‌تری را بسازیم. یکی از بهترین وبسایت‌ها برای اینکار crontab.guru است.

پ. ن. برای استفاده از Cron Expression در متد‌های Recurring کافی است بجای ورودی‌های Yearly - Monthly - Weekly - Daily - Hourly - Minutely ، خود Cron Expression را درون ورودی متد تعریف کنیم : 
 _recurringJobManager.AddOrUpdate("test", () => job , "0 4 10-15 * *" );


2. متد Trigger :

متد Trigger یک متد برای اجرای آنی تسک‌های Recurring است که به کمک آن میتوانید این نوع از تسک‌ها را بدون در نظر گرفتن زمانبندی آن‌ها، در لحظه اجرا کنید و البته تاثیری در دفعات بعدی تکرار نداشته باشد. 
RecurringJob.Trigger("some-id");


3. تعیین تاریخ انقضاء برای Recurring Job‌ها :

گاهی ممکن است در تسک‌های Recurring شرایطی پیش آید که برفرض میخواهید کاری را هر ماه انجام دهید، اما این تکرار در پایان همان سال تمام میشود. در اینصورت باید یک Expire Time برای متد Recurring خود تنظیم کنیم تا بعد از 12 ماه تکرار، در تاریخ 140X/12/30 به پایان برسد. HangFire برای متد‌های Recurring ورودی با عنوان ExpireTime تعریف نکرده، اما میتوان از طریق ایجاد یک زمانبندی، تاریخ مشخصی را برای حذف کردن متد Recurring تعریف کرد که همانند یک ExpireTime عمل میکند. 
_recurringJobManager.AddOrUpdate("test", () => Console.WriteLine("Recurring Job"), Cron.Monthly);
_backgroundJobClient.Schedule(() => _recurringJobManager.RemoveIfExists("test"), DateTimeOffset(dateAndTime));
با اجرای این متد، اول کاری برای تکرار در زمانبندی ماهیانه ایجاد میشود و در متد دوم، زمانی برای حذف متد اول مشخص میکند.

در آخر امیدوارم این مقاله برایتان مفید واقع شده باشد. میتوانید فیدبکتان را در قالب کامنت یا یک قهوه برایم ارسال کنید.

مطالب
برنامه نویسی پیشرفته JavaScript - قسمت 2 - ماهیت انواع داده‌ای و حوزه دسترسی به متغیرها

مقادیر پایه (Primitive Values) و ارجاعی (Reference Values)

در جاوا اسکریپت، متغیرها شامل داده‌هایی از نوع پایه و یا ارجاعی می‌باشند. مقادیر String ، Number ، Boolean ، Null و Undefined به عنوان مقادیر پایه محسوب می‌گردند. در این نوع متغیرها تغییرات، مستقیما بر روی مقدار ذخیره شده در متغیر اعمال می‌شوند. اشیاء نیز که از مجموعه‌ای از مقادیر پایه ساخته شده‌اند، مقادیر ارجاعی نامیده می‌شوند. در این نوع مقادیر، شما به اشاره‌گری از شیء دسترسی دارید. بنابراین تغییرات مستقیما بر روی داده‌های ذخیره شده اعمال نمی‌شوند. به مثال‌های زیر توجه کنید:

var num1 = 10;
var num2 = num1;
alert("Num1=" + num1 + ", Num2=" + num2);

num2 = 20;
alert("Num1=" + num1 + ", Num2=" + num2);

num1 = 30;
alert("Num1=" + num1 + ", Num2=" + num2);

خروجی :

"Num1=10, Num2=10"

"Num1=10, Num2=20"

"Num1=30, Num2=20"


همانطور که از خروجی مثال فوق پیداست، در انتساب num1 به num2 ، مقدار num1 در num2 کپی شده‌است. بنابراین تغییراتی که بر روی num1 یا num2 صورت می‌گیرد، مستقیما بر روی مقدار ذخیره شده در هر یک از این متغیرها تاثیر می‌گذارد. رفتار مقادیر پایه همیشه به همین صورت می‌باشد.

var obj1 = new Object();
obj1.num = 10;
var obj2 = obj1;
alert("Obj1.Num=" + obj1.num + ", Obj2.Num=" + obj2.num);

obj2.num = 20;
alert("Obj1.Num=" + obj1.num + ", Obj2.Num=" + obj2.num);

obj1.num = 30;
alert("Obj1.Num=" + obj1.num + ", Obj2.Num=" + obj2.num);

خروجی :

"Obj1.Num=10, Obj2.Num=10"

"Obj1.Num=20, Obj2.Num=20"

"Obj1.Num=30, Obj2.Num=30"


با استفاده از نوع ارجاعی Object می‌توانیم اشیاء جدیدی را ایجاد کنیم و ویژگی‌هایی را به صورت پویا به آن‌ها اختصاص دهیم. همانطور که قبلا گفته شد، اشیاء از نوع ارجاعی می‌باشند و حاوی اشاره‌گری به مقادیر ذخیره شده می‌باشند. بنابراین انتساب obj1 به obj2 به معنای انتساب اشاره‌گر obj1 به obj2 می‌باشد. به عبارتی دیگر obj2 به همانجایی اشاره می‌کند که obj1 نیز اشاره می‌نماید. پس هر تغییری که بر روی ویژگی‌های obj1 رخ دهد، obj2 نیز تاثیرات آن را می‌بیند و بالعکس. همانطور که در خروجی مشاهده می‌نمایید، در مرحله‌ی اول obj1 به obj2 نسبت داده شد، پس مقدار ویژگی num برای هر دو آنها یکسان میباشد. در مرحله‌ی دوم، مقدار ویژگی num را در obj2 تغییر دادیم؛ ولی مقدار این ویژگی، در obj1 نیز تغییر نمود. در مرحله‌ی سوم نیز همان اتفاقات مرحله‌ی دوم تکرار شد.

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

توجه:

به انواع پایه، انواع داده‌ای مقداری یا اولیه نیز گفته می‌شود.


فراخوانی با مقدار (Call by Value)

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

function primitive(a, b) {
a += 100;
b += 200;
alert("a=" + a + ", b=" + b);
}

var x = 300, y = 400;
primitive(x, y);
alert("x=" + x + ", y=" + y);

خروجی :

"a=400, b=600"

"x=300, y=400"


x و y دو متغیر پایه می‌باشند، بنابراین تابع فوق به صورت مقداری فراخوانی شده‌است. یعنی مقدار آرگومان‌های x و y در آرگومان‌های a و b کپی می‌شوند. پس هر تغییری که بر روی a و b رخ دهد، هیچ تاثیری بر روی x و y ندارد. همچنین با توجه به توضیحی که در مورد مقادیر پایه داده شد، تغییرات مستقیما بر روی داده‌ی ذخیره شده در متغیر اعمال میشود. بنابراین تغییراتی که در تابع فوق بر روی a و b رخ داد، مستقیما مقادیر a و b را تغییر داده‌است وهیچ ارتباطی به x و y ندارد. البته توجه داشته باشید که حتی اگر نام آرگومان‌های تابع با آرگومان‌های ارسالی یکسان بود و یا حتی اگر تابع مقداری را به عنوان خروجی بر می‌گرداند، هیچ تفاوتی را در خروجی برنامه فوق مشاهده نمی‌کردید.


فراخوانی با ارجاع (Call by Reference)

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

function reference(obj) {
obj.a += 100;
obj.b += 200;
alert("obj.a=" + obj.a + ", obj.b=" + obj.b);
}

var calc = new Object();
calc.a = 300;
calc.b = 400;

reference(calc);
alert("calc.a=" + calc.a + ", calc.b=" + calc.b);

خروجی :

"obj.a=400, obj.b=600"

"calc.a=400, calc.b=600"


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


حوزه دسترسی به متغیرها (Variable Scope)

متغیر‌های محلی (Local Variables)

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

for (var i = 1; i <= 5; i++) {
var sqr = i * i;
alert(sqr);
}

alert(i);
alert(sqr);

خروجی :

1
4
9
16
25
6   // alert(i) out of for
25   // alert(sqr) out of for


متغیرهای i و sqr داخل حلقه‌ی for تعریف شده‌اند و منطقا نباید خارج از این حلقه قابل دسترسی باشند. ولی با توجه به خروجی فوق، مشاهده نمودید که متغیرهای i و sqr، نه تنها خارج از این حلقه قابل شناسایی می‌باشند، بلکه آخرین مقدار خود را نیز حفظ نموده‌اند. در جاوا اسکریپت، یک متغیر محلی زمانی مفهوم پیدا می‌کند که در داخل یک تابع تعریف شود. به مثال زیر توجه کنید:

function sqr(num) {
var sum = num * num;
return sum;
}

var n = 4;
alert(sqr(n));
alert(num); // Error: num is not defined
alert(sum); // Error: sum is not defined

خروجی :

16

Error: num is not defined

Error: sum is not defined

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


متغیرهای عمومی (Global Variables)

در جاوا اسکریپت، متغیرهایی که خارج از تابع تعریف می‌گردند، به شیء window نسبت داده می‌شوند و عمومی می‌باشند. به عبارتی دیگر این متغیرها به عنوان یک ویژگی از شیء window تعریف می‌شوند و در تمامی توابع و بلاک‌های کد قابل دسترسی می‌باشند. به مثال زیر توجه کنید:

var color = "Red";

function setColor() {
color = "Blue";
}

alert(color);
setColor();
alert(color);

خروجی :

"Red"

"Blue"


در مثال فوق، متغیر color به صورت عمومی تعریف شده‌است. بنابراین در کل برنامه قابل دسترسی می‌باشد. در alert اول مقدار فعلی متغیر color یعنی “Red” نمایش می‌یابد. سپس با فراخوانی تابع، مقدار این متغیر تغییر می‌کند. در alert دوم مقدار تغییر یافته‌ی متغیر color نمایش خواهد یافت. حال به مثال زیر توجه کنید:

var color = "Red";

function getColor() {
var color = "Blue";
return color;
}

alert(color);
alert(getColor());
alert(color);

خروجی :

"Red"

"Blue"

"Red"


در مثال فوق، ابتدا یک متغیر color به صورت عمومی یا Global تعریف شده است. در تابع getColor نیز یک متغیر color به صورت local یا محلی تعریف شده است. زمانی که در alert تابع getColor فراخوانی می‌شود، متغیر color مقداردهی می‌گردد. این مقداردهی برای متغیر محلی صورت گرفته است و هیچ ربطی به متغیر color که به صورت عمومی تعریف شده است ندارد.

جهت تعریف متغیر در جاوا اسکریپت، از کلمه‌ی کلیدی var استفاده می‌شود. اما تعریف متغیر در جاوا اسکریپت اجباری نمی‌باشد و می‌توان یک متغیر را مقداردهی نمود بدون آنکه تعریف شده باشد. در صورتی که متغیر با var اعلان نشود، آن متغیر به شیء window نسبت داده می‌شود و ماهیت عمومی پیدا می‌کند. به مثال زیر توجه کنید:

function sum(a, b) {
c = a + b;
}

sum(20, 30);
alert(c);

خروجی :

50


همانطور که مشاهده می‌کنید، متغیر c بدون تعریف شدن مورد استفاده قرار گرفته است. با اینکه به صورت محلی مقداردهی گردیده است، ولی چون توسط var اعلان نشده است، به شیء window نسبت داده شده و ماهیت عمومی پیدا کرده است. 

نظرات مطالب
شروع به کار با EF Core 1.0 - قسمت 1 - برپایی تنظیمات اولیه
این احتمال وجود دارد در صورتی که پروژه را از طریق cli و vscode اجرا میکنید محیط اجرایی بر روی حالت Production قرار گرفته باشد و باعث شود صفحه پیام خطا به شما نشان داده نشود. بعد از اجرای dotnet run در خطوط اولیه زیر در جلوی عبارت Hosting Environment این حالت قابل نمایش است:
> dotnet run
Project TestApp (.NETCoreApp,Version=v1.0) was previously compiled. Skipping compilation.

Hosting environment: Production  
Content root path: C:\Projects\TestApp  
Now listening on: http://localhost:5000  
Application started. Press Ctrl+C to shut down.
برای تغییر این حالت به حالت توسعه کدهای زیر را در CLI وارد نمایید:
>set ASPNETCORE_ENVIRONMENT "Development"

SUCCESS: Specified value was saved.
بعد از تغییر موفقیت آمیز مشکل همچنان ادامه دارد و دلیل آن اینست که این مقدار قبلا توسط Console خوانده شده است و مجددا باید کنسول جدید را اجرا نمایید تا مشکل حل شود.