مطالب
معرفی افزونه‌های مفید VSCode جهت کار با Angular
VSCode یکی از بهترین ادیتورهایی است که از آن می‌توان برای توسعه‌ی برنامه‌های Angular استفاده کرد و در این بین افزونه‌های ویژه‌ای جهت کار با Angular برای آن تدارک دیده شده‌اند که در ادامه تعدادی از مهم‌ترین‌های آن‌ها را بررسی می‌کنیم.


Angular Essentials

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


Angular Language Service

نگارش‌های اخیر Angular به همراه یک سرویس زبان نیز می‌باشند که به ادیتورهای مختلف این امکان را می‌دهد تا توسط این ویژگی بتوانند قابلیت‌های ویرایشی بهتری را جهت برنامه‌های Angular ارائه کنند. برای مثال ویرایش مطلوب قالب‌های کامپوننت‌های Angular و استفاده‌ی از Syntax خاص آن، موردی است که توسط هیچکدام از HTML ادیتورهای موجود پشتیبانی نمی‌شود. اکنون به کمک سرویس زبان Angular و افزونه‌ی ویژه‌ی آن برای VSCode که توسط تیم اصلی Angular توسعه یافته‌است، امکان ویرایش غنی قالب‌های HTML ایی آن فراهم شده‌است. این افزونه یک چنین قابلیت‌هایی را فراهم می‌کند:
الف) AOT Diagnostic messages
اگر قالب HTML ایی مورد استفاده (چه به صورت inline و چه در یک فایل html مجزا) به خاصیتی تعریف نشده اشاره کند، بلافاصله خطای مرتبطی ظاهر خواهد شد:


ب) Completions lists یا همان Intellisense
ج) امکان Go to definition با کلیک راست بر روی خواص و متدهای ذکر شده‌ی در قالب.
د) Quick info که با نزدیک کردن اشاره‌گر ماوس به خاصیت یا متدی در صفحه، اطلاعات بیشتری را در مورد آن نمایش می‌دهد.


angular2-inline

علاوه بر افزونه‌ی سرویس زبان‌های Angular، این افزونه نیز قابلیت درک قالب‌های inline کامپوننت‌ها را داشته و به همراه syntax highlighting و همچنین Intellisense است.


Auto Import

حین کار با TypeScript، هر ماژولی که در صفحه ارجاعی داشته باشد، باید در ابتدای فایل جاری import شود. افزونه‌ی Auto Import با بررسی ماژول‌های موجود و فراهم آوردن Intellisense ایی بر اساس آن‌ها، این‌کار را ساده‌تر می‌کند:


بنابراین این افزونه صرفا مختص به Angular نیست و برای کارهای متداول TypeScript نیز بسیار مفید است.


TSLint

این افزونه ابزار TSLint را با VSCode یکپارچه می‌کند. بنابراین نیاز است پیش از نصب این افزونه، وابستگی‌های ذیل را نیز به صورت سراسری نصب کرد:
 > npm install -g tslint typescript
کار TSLint انجام static code analysis است؛ چیزی شبیه به افزونه‌هایی مانند ری‌شارپر در ویژوال استودیو که راهنماهایی را در مورد بهتر کردن کیفیت کدهای نوشته شده ارائه می‌دهد.
تعدادی از امکانات آن‌را پس از نصب، با فشردن دکمه‌ی F1 می‌توان مشاهده کرد:


برای مثال تولید فایل tslint.json، امکان سفارشی سازی موارد بررسی شونده‌ی توسط این افزونه را فراهم می‌کند و اگر برنامه‌ی خود را توسط Angular CLI ایجاد کرده‌اید، این فایل هم اکنون در ریشه‌ی پروژه قرار دارد.
در مورد TSLint در مطلب «Angular CLI - قسمت دوم - ایجاد یک برنامه‌ی جدید» بیشتر توضیح داده شده‌است و اینبار به کمک این افزونه، خطاهای یاد شده را دقیقا درون محیط ادیتور و به صورت خودکار و یکپارچه‌ای مشاهده خواهید کرد.



Angular v4 TypeScript Snippets

سیستم کار VSCode مبتنی بر ایجاد فایل‌های خالی است و مفهوم قالب‌های از پیش آماده‌ی فایل‌ها در آن وجود ندارد. اما با کمک Code Snippets می‌توان این خلاء را پر کرد. افزونه‌ی Angular v4 TypeScript Snippets دقیقا به همین منظور طراحی شده‌است و زمانیکه حروف -a یا -rx را در صفحه تایپ می‌کنید، منویی ظاهر خواهد شد که توسط آن می‌توان قالب ابتدایی شروع به کار با انواع و اقسام جزئیات پروژه‌های Angular را تهیه کرد.



Path Intellisense

این افزونه مسیر فایل‌های موجود را به صورت یک Intellisense ارائه می‌کند و به این صورت به سادگی می‌توان مسیرهای اسکریپت‌ها و یا شیوه‌نامه‌ها را در ادیتور انتخاب و وارد کرد.

نظرات مطالب
خلاصه‌ای کوتاه در مورد WinRT
- بحث وب که سرجای خودش همانند سابق هست و هیچ فرقی نمی‌کند. برنامه‌های ASP.NET روی سرور اجرا می‌شوند و عموما روی سرور بجز یک سری سرویس‌های ویندوز NT‌ ، هیچ نرم افزار دیگری نصب نخواهد شد. مثلا IIS یا مثلا SQL Server و در همین حد. حتی عموما سرورها حتی مونیتور هم ندارند و با ریموت دسکتاپ یک سری کارهای مدیریتی آن‌ها را انجام می‌دهند و این کارها هم طوری نیست که هر روز تغییر کند. یکبار سرور تنظیم می‌شود که حداقل یکسال یا بیشتر کار کند. این مورد اصلا تغییری نخواهد داشت. بحث سمت سرور است. بنابراین سرمایه گذاری روی ASP.NET خوب است و شامل این بحث ویندوز 8 یا ویندوزهای بعدی نمی‌شود؛ چون این‌ها (WinRT) سمت کاربر محسوب می‌شوند.
- از این جهت که رابط‌های کاربری مبتنی بر WinRT ، یا بر پایه XAML است یا HTML/CSS ، یادگیری WPF و یا سیلورلایت (که قسمتی از WPF را به ارث برده) مفید خواهند بود؛ از این لحاظ که پایه رابط کاربری هر دوی این‌ها هم XAML است و اساسا طراحی XAML از اینجا به WinRT منتقل شده.

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

- یادگیری سی++ همیشه مفید است. حتی در کره مریخ هم تاجایی که اطلاع دارم (!) یک کامپایلر سی++ وجود دارد و می‌شود با آن برنامه‌ی Hello world را کامپایل کرد. اگر باور ندارید از این لینوکسی‌ها بپرسید!
مطالب
مسیریابی در Angular - قسمت دهم - Lazy loading
می‌خواهیم زمان نمایش اولین قالب برنامه را به حداقل برسانیم تا تاثیر روانی بهتری را بر روی کاربرانی که برنامه را اجرا می‌کنند، بگذاریم. برای این منظور در Angular، از Lazy loading استفاده می‌شود. همچنین این فریم ورک به همراه قابلیت پیش بارگذاری ماژول‌ها نیز هست تا سایر مسیرهای درخواستی را نیز با سرعت هرچه تمام‌تر نمایش دهد.
زمانیکه کاربری برنامه‌ی تک صفحه‌ای وب را در مرورگر باز می‌کند، ابتدا فایل index.html را در پاسخ دریافت خواهد کرد. این فایل تعاریف مداخل مورد نیاز برای رندر آن‌را مانند فایل‌های جاوا اسکریپت و CSS، به همراه دارد. سپس این فایل‌ها توسط مرورگر از سرور دریافت می‌شوند. در این حالت با پردازش این فایل‌ها، کامپوننت ریشه‌ی سایت بارگذاری می‌شود. پس از پایان آن، قالب این کامپوننت به کاربر نمایش داده خواهد شد. بنابر سرعت دریافت فایل‌ها توسط کاربر، این آغاز می‌تواند اندکی کند باشد. البته با رعایت نکات گفته‌ی شده‌ی در مطلب «Angular CLI - قسمت پنجم - ساخت و توزیع برنامه» می‌توان این حجم را توسط AoT و Tree-Shaking به میزان قابل ملاحظه‌ای کاهش داد. به علاوه با فعالسازی Lazy loading می‌توان قسمت‌های مختلف برنامه را تبدیل به یک سری Bundle کرد که در زمان درخواست، بارگذاری می‌شوند. به این ترتیب حجم فایل‌های ابتدایی که باید از سرور دریافت شوند بسیار کمتر شده و به علاوه با کاهش این حجم، مرورگر نیز باید میزان کمتری از کدها را در جهت نمایش اولین کامپوننت، پردازش و اجرا کند. در این حالت زمانیکه کاربری شروع به پیمایش مسیر یک ماژول خاص را می‌کند، آنگاه فایل‌های مرتبط با آن از سرور دریافت و در مرورگر پردازش می‌شوند. بنابراین اگر کاربری به قسمتی دسترسی ندارد، نیازی هم به دریافت فایل‌های آن نخواهد داشت؛ چون کار به فعالسازی مسیریابی آن ماژول نمی‌رسد.


آماده شدن جهت Lazy loading

پیش از Lazy loading یک قسمت از برنامه (که به آن async routing هم می‌گویند)، این قسمت باید دارای شرایطی باشد:
 - این قسمت از برنامه حتما باید در یک ماژول تعریف شده باشد. از این جهت که Lazy loading، لیست کامپوننت‌های قید شده‌ی در تعریف یک ماژول را بارگذاری می‌کند.
 - تمام مسیرهای این ماژول باید در ذیل یک مسیر والد، گروه بندی شده باشند. از این جهت که Lazy loading فقط بر روی مسیر ریشه‌ی والد تنظیم و بارگذاری می‌شود.
 - این ماژول نباید در هیچ ماژول دیگری import شده باشد. اگر این ماژول ارجاعی را در سایر ماژول‌ها داشته باشد، هیچ راهی بجز دریافت و کامپایل کامل آن توسط Angular وجود نخواهد داشت.


در مثال جاری این سری:
 - تمام ویژگی‌های قسمت مدیریت محصولات، داخل ماژول product.module.ts تعریف شده‌اند. بنابراین اولین شرط Lazy loading آن برقرار است.
 - در فایل product-routing.module.ts، کار گروه بندی مسیریابی‌ها ذیل یک والد مشخص انجام شده‌است (همان قسمت ششم این سری). بنابراین شرط دوم lazy loading این ماژول نیز پیشتر پیاده سازی شده‌است.
 - اما اگر به فایل src\app\app.module.ts مراجعه کنیم، ارجاعی به این ماژول در قسمت imports آن وجود دارد. بنابراین باید این ارجاع را حذف کنیم. در غیراینصورت کار دریافت کامل آن به همراه سایر ماژول‌های برنامه، در همان ابتدای کار صورت خواهد گرفت.
بنابراین در فایل src\app\app.module.ts، ابتدا import فایل آن‌را از ابتدای ماژول حذف و سپس ارجاع به نام کلاس کامپوننت ProductModule را نیز حذف می‌کنیم. در این حالت اگر از طریق منوی سایت سعی در دسترسی به این مسیرها کنیم، خطای 404 را دریافت خواهیم کرد؛ چون اکنون برنامه اطلاعاتی را در مورد نحوه‌ی مسیریابی قسمت محصولات برنامه، ندارد.

 
Lazy loading یک ماژول

برای بارگذاری غیرهمزمان یک ماژول و یا همان Lazy loading، می‌توان از خاصیت loadChildren تنظیمات مسیریابی، استفاده کرد:
{
   path: 'products',
   loadChildren:'app/product/product.module#ProductModule'
},
مقدار خاصیت loadChildren به صورت ذکر مسیر ماژول مرتبط به همراه یک # و سپس ذکر نام کلاس ماژول آن انجام می‌شود. مسیری هم که در اینجا ذکر می‌شود بر اساس محل قرارگیری فایل index.html، مقدار دهی شود.
با این تنظیم، زمانیکه مسیر ریشه‌ی produtcs درخواست شد، کار بارگذاری ماژول آن صورت گرفته و تنظیمات مسیریابی آن به سیستم اضافه می‌شود. به علاوه کار فعالسازی و نمایش کامپوننت آن را نیز انجام خواهد داد.

به همین منظور فایل src\app\app-routing.module.ts را گشوده و تنظیم فوق را به آن اضافه می‌کنیم:
const routes: Routes = [
  { path: 'home', component: WelcomeComponent },
  { path: 'welcome', redirectTo: 'home', pathMatch: 'full' },
  { path: 'products', loadChildren: 'app/product/product.module#ProductModule' },
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];
در این حالت اگر دستور ng serve -o را صادر کنید، خروجی آن اندکی متفاوت خواهد بود:
chunk    {0} polyfills.bundle.js, polyfills.bundle.js.map (polyfills) 165 kB {4} [initial]
chunk    {1} main.bundle.js, main.bundle.js.map (main) 32.7 kB {3} [initial] [rendered]
chunk    {2} styles.bundle.js, styles.bundle.js.map (styles) 129 kB {4} [initial]
chunk    {3} vendor.bundle.js, vendor.bundle.js.map (vendor) 2.72 MB [initial] [rendered]
chunk    {4} inline.bundle.js, inline.bundle.js.map (inline) 0 bytes [entry]
chunk    {5} 5.chunk.js, 5.chunk.js.map 51.1 kB {1} [rendered]
مورد {5} با فعالسازی lazy loading به لیست فایل‌های موجود اضافه شده‌است. این فایلی است که تنها درصورت درخواست مسیر نمایش لیست محصولات، توسط مرورگر دریافت خواهد شد و هیچ ارجاع مستقیمی به آن در فایل index.html تولیدی نهایی وجود ندارد.

به علاوه اگر در منوی سایت بر روی لینک نمایش لیست محصولات کلیک کنیم، هنوز خروجی نمایش داده نمی‌شود (هرچند خطای 404 را هم دریافت نمی‌کنیم). علت اینجا است که اگر به فایل src\app\product\product-routing.module.ts مراجعه کنیم، تعریف این مسیر ریشه، در این فایل نیز وجود دارد:
const routes: Routes = [
  {
    path: 'products',
    canActivate: [ AuthGuard ],
    children: [   ]
  }
];
بنابراین اکنون برای دسترسی به آن باید مسیر products/products را درخواست داد. به همین جهت، path و canActivate آن‌را حذف کرده و هر دو را به فایل src\app\app-routing.module.ts منتقل می‌کنیم:
import { AuthGuard } from './user/auth.guard';

const routes: Routes = [
  { path: 'home', component: WelcomeComponent },
  { path: 'welcome', redirectTo: 'home', pathMatch: 'full' },
  {
    path: 'products',
    loadChildren: 'app/product/product.module#ProductModule',
    canActivate: [AuthGuard]
  },
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];

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

در این حالت اگر مسیر نمایش لیست محصولات را درخواست دهیم، مشاهده خواهیم کرد فایل 5.chunk.js که حاوی اطلاعات این ماژول است، به صورت مجزایی بارگذاری شده (lazy loading) و سپس با فعال شدن محافظ مسیر آن، صفحه‌ی لاگین نمایش داده می‌شود:


این بارگذاری با تاخیر و در صورت نیاز، به دو علت آغاز برنامه را سریعتر می‌کند:
الف) مرورگر اطلاعی از وجود فایل 5.chunk.js در ابتدای کار نداشته و آن‌را بارگذاری نمی‌کند (دریافت حجم کمتر، در آغاز نمایش برنامه).
ب) چون حجم کمتری از کدهای جاوا اسکریپت توسط مرورگر در آغاز کار دریافت می‌شود، کار پردازش و اجرای آن‌ها نیز بسیار سریعتر خواهد شد.


بررسی محافظ canLoad

تعدادی از محافظ‌های مسیرها را در قسمت قبل بررسی کردیم. هنگامیکه کامپوننت‌ها به صورت lazy loading فعالسازی شده و قالب آن‌ها نمایش داده می‌شوند، می‌توان از محافظ مسیر دیگری به نام canLoad نیز استفاده کرد و هدف از آن، بررسی منطقی، پیش از فعالسازی یک مسیر غیرهمزمان است. بنابراین اگر این محافظ false را برگرداند، حتی فایل‌های اسکریپت این ماژول، بارگذاری اولیه نیز نخواهد شد. به این ترتیب کسانیکه دسترسی به یک مسیر را نداشته باشند، فایل‌های اسکریپت متناظر با آن‌را نیز دریافت نخواهند کرد.

در مثال جاری، اگر به برگه‌ی network ابزار developer مرورگر دقت کنید، با درخواست نمایش مسیر لیست محصولات، ابتدا فایل js آن دریافت می‌شود که حاوی اطلاعات تمام کامپوننت‌ها و قالب‌های مرتبط با این مسیر است و سپس صفحه‌ی login نمایش داده خواهد شد. بنابراین اگر کاربر به این قسمت دسترسی نداشته باشد، فایل js آن بی‌جهت دریافت و بارگذاری شده‌است. برای بهبود این وضعیت می‌توان نمایش لاگین را پیش از بارگذاری فایل js این ماژول فعالسازی کرد و این مورد هدف اصلی محافظ canLoad است.

در ادامه برای تکمیل مثال جاری، می‌توان AuthGuard را طوری تنظیم کرد که علاوه بر پیاده سازی CanActivate، اینترفیس CanLoad را نیز پیاده سازی کند:
import { ActivatedRouteSnapshot, RouterStateSnapshot, CanActivate, Router, CanLoad, Route } from '@angular/router';

@Injectable()
export class AuthGuard implements CanActivate, CanLoad {

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    return this.checkLoggedIn(state.url);
  }

  canLoad(route: Route): boolean {
    return this.checkLoggedIn(route.path);
  }

 // … the same as before

}
همانطور که ملاحظه می‌کنید، متد canLoad بر خلاف متد canActivate دسترسی به سرویس‌های اطلاعات مسیریابی و وضعیت مسیریابی را ندارد؛ از این جهت که هنوز در این مرحله، ماژول درخواستی حاوی تنظیمات مسیریابی، بارگذاری و فعالسازی نشده‌است.

مرحله‌ی بعد، تغییر فایل src\app\app-routing.module.ts و جایگزین کردن تعریف فعلی canActivate با canLoad است:
  {
    path: 'products',
    loadChildren: 'app/product/product.module#ProductModule',
    canLoad: [AuthGuard]
  },
پس از این تغییر، برنامه را مجدا اجرا کرده و صفحه را refresh کنید. سپس برگه‌ی network ابزار developers را نیز باز نگه دارید. اکنون بر روی لینک نمایش لیست محصولات کلیک کنید. مشاهده خواهید کرد که در این حالت صفحه‌ی لاگین، بدون بارگذاری ماژول Js ایی نمایش داده می‌شود. در ادامه اگر لاگین کنیم، آنگاه فایل js این ماژول توسط مرورگر دریافت شده و بارگذاری می‌شود.


پیش بارگذاری ماژول‌ها

با فعالسازی lazy loading، ماژول‌های مورد نیاز کاربر دیگر به همراه فایل‌های js ابتدایی برنامه که در فایل index.html ارجاع مستقیمی به آن‌ها دارند، ارائه نمی‌شوند و تنها در صورت درخواست مشاهده‌ی مسیری، کار بارگذاری آن‌ها توسط برنامه صورت خواهد گرفت. همین مساله می‌تواند در بار اول نمایش این ماژول‌ها تاخیر کوتاهی را سبب شود. به همین جهت قابلیت پیش بارگذاری ماژول‌ها نیز در سیستم مسیریاب Angular پیش بینی شده‌است. به این قابلیت preloading و یا eager lazy loading نیز می‌گویند. در این حالت برنامه در پشت صحنه، کار پیش واکشی ماژول‌ها را انجام می‌دهد و زمانیکه کاربری مسیری را درخواست می‌دهد، آْن مسیر را بدون درنگ مشاهده خواهد کرد.
بدیهی است این قابلیت نباید برای ماژول‌هایی که قرار است توسط کاربرانی خاص مشاهده شوند فعال شود و هدف آن دسترسی سریع به ماژول‌های پرکاربرد برنامه‌است.

در اینجا سه استراتژی پیش بارگذاری ماژول‌ها میسر است:
 - No preloading که حالت پیش فرض است.
 - Preload all سبب پیش بارگذاری تمام قسمت‌های lazy load برنامه می‌شود.
 - Custom که اجازه‌ی تعریف یک استراتژی سفارشی را می‌دهد.

برای مثال برای فعالسازی حالت Preload all، باید به فایل src\app\app-routing.module.ts مراجعه کرده و تغییرات ذیل را اعمال کنیم:
import { Routes, RouterModule, PreloadAllModules } from '@angular/router';

@NgModule({
  imports: [RouterModule.forRoot(
    routes,
    { enableTracing: true, preloadingStrategy: PreloadAllModules  /*, useHash: true*/ }
  )],
در اینجا نحوه‌ی تنظیم preloadingStrategy را به PreloadAllModules مشاهده می‌کنید. در این حالت پس از آغاز ابتدایی برنامه، مسیریاب بلافاصله تمام مسیرهای lazy load را در پشت صحنه بارگذاری می‌کند.

یک نکته: وجود محافظ canLoad، هر نوع استراتژی prealoading را غیرفعال می‌کند. اما prealoading با سایر انواع محافظ‌ها کار می‌کند.
بنابراین برای آزمایش تنظیم  preloadingStrategy: PreloadAllModules، تعریف canLoad را به canActivate تغییر دهید.


تعریف استراتژی‌های سفارشی پیش بارگذاری ماژول‌ها

اگر نیاز به یک استراتژی پیش بارگذاری بهتر از هیچ یا همه باشد، می‌توان یک استراتژی سفارشی را نیز تدارک دید و ایجاد آن سه مرحله‌ی ایجاد سرویس مرتبط، ثبت آن سرویس در ماژول و در آخر تنظیم مسیریابی را به همراه دارد.
برای این منظور ابتدا دستور ذیل را صادر کنید تا قالب ابتدایی سرویس SelectiveStrategy ایجاد شود:
 >ng g s SelectiveStrategy -m app.module
که سبب تولید و به روز رسانی فایل‌های ذیل در پوشه‌ی src\app خواهد شد (چون مرتبط است به کل برنامه):
 installing service
  create src\app\selective-strategy.service.spec.ts
  create src\app\selective-strategy.service.ts
  update src\app\app.module.ts
در این حالت لیست providers فایل app.module.ts نیز به صورت خودکار تکمیل می‌گردد.

سپس کدهای SelectiveStrategyService را به نحو ذیل تغییر دهید:
import { Injectable } from '@angular/core';
import { Route, PreloadingStrategy } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';

@Injectable()
export class SelectiveStrategyService implements PreloadingStrategy {

  preload(route: Route, load: Function): Observable<any> {
    if (route.data && route.data['preload']) {
      return load();
    }
    return Observable.of(null);
  }
}
- این سرویس ویژه باید اینترفیس PreloadingStrategy را پیاده سازی کند. سپس باید متد اجباری preload آن‌را افزود و تکمیل نمود.
- پارامتر اول این متد، اطلاعاتی را در مورد مسیر جاری در اختیار ما قرار می‌دهد و دومین پارامتر آن متدی است که کار preloading را انجام می‌دهد.
- در اینجا است که تصمیم می‌گیریم ماژولی را preload کنیم یا خیر. برای نمونه در اینجا از خاصیت data مسیریابی استفاده شده‌است. این خاصیت نیز به یک مقدار ثابت اشاره می‌کند (قسمت «ارسال اطلاعات ثابت به مسیرهای مختلف برنامه» قسمت چهارم). برای مثال نام دلخواه آن‌را preload گذاشته‌ایم و اگر مقدار آن به true تنظیم شده بود، آنگاه این مسیر preload خواهد شد. فراخوانی متد load در اینجا به معنای preloading این مسیر است. در غیراینصورت null را بازگشت می‌دهیم.


در ادامه نیاز است در فایل src\app\app-routing.module.ts، بجای معرفی PreloadAllModules، این استراتژی سفارشی خود را معرفی کرد:
import { SelectiveStrategyService } from './selective-strategy.service';

@NgModule({
  imports: [RouterModule.forRoot(
    routes,
    { enableTracing: true, preloadingStrategy: SelectiveStrategyService
     /*, preloadingStrategy: PreloadAllModules*/  /*, useHash: true*/ }
  )],
و همچنین تعریف مسیریابی برنامه به این صورت تغییر می‌کند:
  {
    path: 'products',
    loadChildren: 'app/product/product.module#ProductModule',
    //canLoad: [AuthGuard] 
    canActivate: [AuthGuard],
    data: { preload: true }
  },
در اینجا نحوه‌ی مقدار دهی خاصیت data را به اطلاعات ثابت preload: true مشاهده می‌کنید. این اطلاعاتی است که در سرویس SelectiveStrategy سفارشی ما بررسی شده و بر اساس آن در مورد پیش بارگذاری این مسیر تصمیم‌گیری می‌شود.

برای آزمایش آن، برنامه را مجدا اجرا کرده و صفحه را refresh کنید. سپس برگه‌ی network ابزار developers را نیز باز نگه دارید. مشاهده خواهید کرد که علاوه بر فایل‌های js اصلی برنامه که در فایل index.html ارجاعی را دارند، فایل 5.chunk.js نیز پیش بارگذاری شده‌است.


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

State machine مدلی است بیانگر نحوه واکنش سیستم به وقایع مختلف. یک ماشین حالت وضعیت جاری قسمتی از سیستم را نگهداری کرده و به ورودی‌های مختلف پاسخ می‌دهد. این ورودی‌ها در نهایت وضعیت سیستم را تغییر خواهند داد.
نحوه پاسخگویی یک ماشین حالت (State machine) را به رویدادی خاص، انتقال (Transition) می‌نامند. در یک انتقال مشخص می‌شود که ماشین حالت بر اساس وضعیت جاری خود، با دریافت یک رویداد، چه عکس العملی را باید بروز دهد. عموما (و نه همیشه) در حین پاسخگویی ماشین حالت به رویدادهای رسیده، وضعیت آن نیز تغییر خواهد کرد. در اینجا گاهی از اوقات پیش از انجام عملیاتی، نیاز است شرطی بررسی شده و سپس انتقالی رخ دهد. به این شرط، guard گفته می‌شود.
بنابراین به صورت خلاصه، یک ماشین حالت، مدلی است از رفتاری خاص، تشکیل شده از حالات، رویدادها، انتقالات، اعمال (actions) و شرط‌ها (Guards). در اینجا:
- یک حالت (State)، شرطی منحصربفرد در طول عمر ماشین حالت است. در هر زمان مشخصی، ماشین حالت در یکی از حالات از پیش تعریف شده خود قرار خواهد داشت.
- یک رویداد (Event)، اتفاقی است که به ماشین حالت اعمال می‌شود؛ یا همان ورودی‌های سیستم.
- یک انتقال (Transition)، بیانگر نحوه رفتار ماشین حالت جهت پاسخگویی به رویداد وارده بر اساس وضعیت جاری خود می‌باشد. در طی یک انتقال، سیستم از یک حالت به حالتی دیگر منتقل خواهد شد.
- برای انجام یک انتقال، نیاز است یک شرط (Guard/Conditional Logic) بررسی شده و در صورت true بودن آن، انتقال صورت گیرد.
- یک عمل (Action)، بیانگر نحوه پاسخگویی ماشین حالت در طول دوره انتقال است.


چگونه می‌توان الگوی ماشین حالت را تشخیص داد؟

اکثر برنامه‌های وب، متشکل از پیاده سازی چندین ماشین حالت می‌باشند؛ مانند ثبت نام در سایت، درخواست یک کتاب از کتابخانه، ارسال درخواست‌ها و پاسخگویی به آن‌ها و یا حتی ارسال یک مطلب در سایت، تائید و انتشار آن.
البته عموما در حین طراحی برنامه‌ها، کمتر به این نوع مسایل به شکل یک ماشین حالت نگاه می‌شود. به همین جهت بهتر است معیارهایی را برای شناخت زود هنگام آن‌ها مدنظر داشته باشیم:
- آیا در جدول بانک اطلاعاتی خود فیلدهایی مانند State (حالت) یا Status (وضعیت)دارید؟ اگر بله، به این معنا است که در حال کار با یک ماشین حالت هستید.
- عموما فیلدهای Bit و Boolean، بیانگر حضور ماشین‌های حالت هستند. مانند IsPublished ، IsPaid و یا حتی داشتن یک فیلد timeStamp که می‌تواند NULL بپذیرد نیز بیانگر استفاده از ماشین حالت است؛ مانند فیلدهای published_at، paid_at و یا confirmed_at.
- داشتن رکوردهایی که تنها در طول یک بازه زمانی خاص، معتبر هستند. برای مثال آبونه شدن در یک سایت در طول یک بازه زمانی مشخص.
- اعمال چند مرحله‌ای؛ مانند ثبت نام در سایت و دریافت ایمیل فعال سازی. سپس فعال سازی اکانت از طریق ایمیل.


مثالی ساده از یک ماشین حالت

یک کلید برق را در نظر بگیرید. این کلید دارای دو حالت (states) روشن و خاموش است. زمانی که خاموش است، با دریافت رخدادی (event)، به وضعیت (state/status) روشن، منتقل خواهد شد (Transition) و برعکس.


در اینجا حالات با مستطیل‌های گوشه گرد نمایش داده شده‌اند. انتقالات توسط فلش‌هایی انحناء دار که حالات را به یکدیگر متصل می‌کنند، مشخص گردیده‌اند. برچسب‌های هر فلش، مشخص کننده نام رویدادی است که سبب انتقال و تغییر حالت می‌گردد. با شروع یک ماشین حالت، این ماشین در یکی از وضعیت‌های از پیش تعیین شده‌اش قرار خواهد گرفت (initial state)؛ که در اینجا حالت خاموش است.
این نوع نمودارها می‌توانند شامل جزئیات بیشتری نیز باشند؛ مانند برچسب‌هایی که نمایانگر اعمال قابل انجام در طی یک انتقال هستند.


رسم ماشین‌های حالت در برنامه‌های وب، به کمک کتابخانه jsPlumb

کتابخانه‌های زیادی برای رسم فلوچارت، گردش‌های کاری، ماشین‌های حالت و امثال آن جهت برنامه‌های وب وجود دارند و یکی از معروف‌ترین‌های آن‌ها کتابخانه jsPlumb است. این کتابخانه به صورت یک افزونه jQuery طراحی شده است؛ اما به عنوان افزونه‌ای برای کتابخانه‌های MooTools و یا YUI3/Yahoo User Interface 3 نیز قابل استفاده می‌باشد. کتابخانه jsPlumb در مرورگرهای جدید از امکانات ترسیم SVG و یا HTML5 Canvas استفاده می‌کند. برای سازگاری با مرورگرهای قدیمی‌تر مانند IE8 به صورت خودکار به VML سوئیچ خواهد کرد. همچنین این کتابخانه امکانات ترسیم تعاملی قطعات به هم متصل شونده را نیز دارا است (شبیه به طراح یک گردش کاری). البته برای اضافه شدن امکاناتی مانند کشیدن و رها کردن در آن نیاز به jQuery-UI نیز خواهد داشت.
برای نمونه اگر بخواهیم مثال فوق را توسط jsPlumb ترسیم کنیم، روش کار به صورت زیر خواهد بود:
<!doctype html>
<html>
<head>
    <title>State Machine Demonstration</title>
    <style type="text/css">
        #opened
        {
            left: 10em;
            top: 5em;
        }
        
        #off
        {
            left: 12em;
            top: 15em;
        }
        
        #on
        {
            left: 28em;
            top: 15em;
        }
        
        .w
        {
            width: 5em;
            padding: 1em;
            position: absolute;
            border: 1px solid black;
            z-index: 4;
            border-radius: 1em;
            border: 1px solid #346789;
            box-shadow: 2px 2px 19px #e0e0e0;
            -o-box-shadow: 2px 2px 19px #e0e0e0;
            -webkit-box-shadow: 2px 2px 19px #e0e0e0;
            -moz-box-shadow: 2px 2px 19px #e0e0e0;
            -moz-border-radius: 0.5em;
            border-radius: 0.5em;
            opacity: 0.8;
            filter: alpha(opacity=80);
            cursor: move;
        }
        
        .ep
        {
            float: right;
            width: 1em;
            height: 1em;
            background-color: #994466;
            cursor: pointer;
        }
        
        .labelClass
        {
            font-size: 20pt;
        }
    </style>
    <script type="text/javascript" src="jquery.min.js"></script>
    <script type="text/javascript" src="jquery-ui.min.js"></script>
    <script type="text/javascript" src="jquery.jsPlumb-all-min.js"></script>
    <script type="text/javascript">
        $(document).ready(function () {

            jsPlumb.importDefaults({
                Endpoint: ["Dot", { radius: 5}],
                HoverPaintStyle: { strokeStyle: "blue", lineWidth: 2 },
                ConnectionOverlays: [
["Arrow", { location: 1, id: "arrow", length: 14, foldback: 0.8}]
]
            });

            jsPlumb.makeTarget($(".w"), {
                dropOptions: { hoverClass: "dragHover" },
                anchor: "Continuous"
            });

            $(".ep").each(function (i, e) {
                var p = $(e).parent();
                jsPlumb.makeSource($(e), {
                    parent: p,
                    anchor: "Continuous",
                    connector: ["StateMachine", { curviness: 20}],
                    connectorStyle: { strokeStyle: '#42a62c', lineWidth: 2 },
                    maxConnections: 2,
                    onMaxConnections: function (info, e) {
                        alert("Maximum connections (" + info.maxConnections + ") reached");
                    }
                });
            });

            jsPlumb.bind("connection", function (info) {
            });

            jsPlumb.draggable($(".w"));

            jsPlumb.connect({ source: "opened", target: "off" });
            jsPlumb.connect({ source: "off", target: "on", label: "Turn On" });
            jsPlumb.connect({ source: "on", target: "off", label: "Turn Off" });
        });
    </script>
</head>
<body>
    <div class="w" id="opened">
        Begin
        <div class="ep">
        </div>
    </div>
    <div class="w" id="off">
        Off
        <div class="ep">
        </div>
    </div>
    <div class="w" id="on">
        On
        <div class="ep">
        </div>
    </div>
</body>
</html>
مستندات کامل jsPlumb را در سایت آن می‌توان ملاحظه نمود.
در مثال فوق، ابتدا css و فایل‌های js مورد نیاز ذکر شده‌اند. توسط css، مکان قرارگیری اولیه المان‌های متناظر با حالات، مشخص می‌شوند.
سپس زمانیکه اشیاء صفحه در دسترس هستند، تنظیمات jsPlumb انجام خواهد شد. برای مثال در اینجا نوع نمایشی Endpoint‌ها به نقطه تنظیم شده است. موارد دیگری مانند مستطیل نیز قابل تنظیم است. سپس نیاز است منبع و مقصدها به کتابخانه jsPlumb معرفی شوند. به کمک متد jsPlumb.makeTarget، تمام المان‌های دارای کلاس w به عنوان منبع و با شمارش divهایی با class=ep، مقصدهای قابل اتصال تعیین شده‌اند (jsPlumb.makeSource). متد jsPlumb.bind یک callback function است و هربار که اتصالی برقرار می‌شود، فراخوانی خواهد شد. متد jsPlumb.draggable تمام عناصر دارای کلاس w را قابل کشیدن و رها کردن می‌کند و در آخر توسط متدهای jsPlumb.connect، مقصد و منبع‌های مشخصی را هم متصل خواهیم کرد. نمونه نهایی تهیه شده برای بررسی بیشتر.


برای مطالعه بیشتر
Finite-state machine
UML state machine
UML 2 State Machine Diagrams
مثال‌هایی در این مورد

مطالب
آپلود فایل‌های Excel در ASP.NET MVC توسط ExcelDataReader

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

Microsoft.Jet.OLEDB.4.0 provider --> Excel 97-2003 format (.xls)
Microsoft.ACE.OLEDB.12.0 provider --> Excel 2007+ format (.xlsx)
با خطاهای زیر روبرو می‌شویم:
The ‘Microsoft.Jet.OLEDB.4.0’ provider is not registered on the local machine
The ‘Microsoft.ACE.OLEDB.12.0’ provider is not registered on the local machine

البته راه حل، نصب  Office 2007 Data Connectivity Components یا Office 2010 Database Engine بر روی سرور می‌باشد. اما اگر هاست اشتراکی بوده و اجازه نصب نداشته باشیم؟

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

برای این کار:

1-  ابتدا یک پروژه خالی Asp.Net MVC  را ایجاد می‌کنیم.
2-  با استفاده از دستورات زیر در Package Manager Console بسته‌های ExcelDataReader و ExcelDataReader.DataSet را نصب می‌کنیم:

PM> Install-Package ExcelDataReader
PM> Install-Package ExcelDataReader.DataSet
توجه: دو روش برای خواندن از فایل‌های اکسل در این کتابخانه وجود دارد که نصب بسته دوم مربوط به روش دوم آن است.


3-  سپس کنترلر مورد نظر (در اینجا HomeController) را ایجاد نموده و اکشن Upload را بصورت زیر در آن قرار می‌دهیم:

public ActionResult Upload()
{
      return View();
}

4- برای آپلود فایل، اکشن دیگری را با نام Upload نیاز داریم که آن را بصورت زیر ایجاد می‌کنیم:

توجه: در قطعه کد زیر سعی شده از حداقل کانفیگ کتابخانه استفاده شود. کانفیگ بیشتر

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Upload(HttpPostedFileBase upload)
{  
           // اعتبار سنجی فایل آپلود شده
            if (upload != null && upload.ContentLength > 0 && (upload.FileName.EndsWith(".xls") || upload.FileName.EndsWith(".xlsx")))
            {
                // با خواندن فایل به صورت باینری، این کتابخانه نیازی به نصب پیش نیازهای آفیس ندارد
                Stream stream = upload.InputStream;

                //  نیازی به نگرانی در مورد پسوند فایل نیست
                // کتابخانه به صورت خودکار کلاس مورد نظر برای پسوند مربوطه را استفاده می‌کند
                // ExcelDataReader.ExcelBinaryReader یا ExcelDataReader.ExcelOpenXmlReader
                IExcelDataReader reader = ExcelReaderFactory.CreateReader(stream);

                // روش ذکر شده در قسمت دوم برای خواندن کل اطلاعات بصورت یکجا
                DataSet result = reader.AsDataSet(new ExcelDataSetConfiguration()
               {
                    ConfigureDataTable = (tableReader) => new ExcelDataTableConfiguration()
                    {
                        // true: ردیف اول از فایل را به عنوان هدر در نظر می‌گیرد
                        // مقدار پیش فرض: false
                        UseHeaderRow = true
                    }
                });

                reader.Close();
                return View(result.Tables[0]);
            }
      ModelState.AddModelError("File", "Please upload Excel file ...");
      return View();
 }  

  روش دیگر خواندن اطلاعات:
do {
        while (reader.Read()) {
       // reader.GetDouble(0);
        }
    } while (reader.NextResult());


5-  خب حالا از یک View (با نام Upload) هم برای ارسال فایل و همچنین نمایش محتویات آپلود شده بصورت زیر استفاده می‌کنیم:
@model System.Data.DataTable
@using System.Data;

<h2>Upload File</h2>

@using (Html.BeginForm("Upload", "Home", null, FormMethod.Post, new { enctype = "multipart/form-data" }))
{
    @Html.AntiForgeryToken()
    @Html.ValidationSummary()

    <div>
        <input type="file" id="dataFile" name="upload" />
    </div>

    <div>
        <input type="submit" value="Upload" />
    </div>

    if (Model != null)
    {
        <table>
            <thead>
                <tr>
                    @foreach (DataColumn col in Model.Columns)
                    {
                        <th>@col.ColumnName</th>
                    }
                </tr>
            </thead>
            <tbody>
                @foreach (DataRow row in Model.Rows)
                {
                    <tr>
                        @foreach (DataColumn col in Model.Columns)
                        {
                            <td>@row[col.ColumnName]</td>
                        }
                    </tr>
                }
            </tbody>
        </table>
    }
}
توجه داشته باشید که این مثال آموزشی است و در پروژه واقعی، قطعا روش‌های بهتری برای پردازش، پیمایش و نمایش محتوا وجود دارد.


6-  حالا پروژه را اجرا می‌کنیم تا خروجی را مشاهده کنیم.

ابتدا فایل مورد نظر را انتخاب و آپلود می‌کنیم:


انتخا فایل اکسل

و خروجی به صورت زیر خواهد بود:

خروجی

کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: UploadExcelFiles.rar

منابع: ^ و ^

مطالب
CoffeeScript #6

Classes

Inheritance & Super

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

class Animal
  constructor: (@name) ->

  alive: ->
    true

class Parrot extends Animal
  constructor: ->
    super("Parrot")

  dead: ->
    not @alive()
در مثال بالا، Parrot (طوطی) از کلاس Animal ارث بری شده، که تمام خصوصیات آن را مانند ()alive، ارث برده است.
همانطوری که در مثال بالا مشاهده می‌کنید، در کلاس Parrot در تابع constructor، تابع super فراخوانی شده است. با استفاده از کلمه‌ی کلیدی super می‌توان تابع سازنده‌ی کلاس پدر را فراخوانی کرد. نتیجه‌ی کامپایل super در مثال بالا به این صورت می‌شود:
Parrot.__super__.constructor.call(this, "Parrot");
تابع super در CoffeeScript دقیقا مانند Ruby و Python عمل می‌کند.
در صورتیکه تابع constructor را در کلاس فرزند ننوشته باشید، به طور پیش فرض CoffeeScript سازنده کلاس پدر را فراخوانی می‌کند.

CoffeeScript با استفاده از prototypal inheritance، به صورت خودکار تمامی خصوصیات کلاس پدر، به فرزندان انتقال پیدا می‌کند. این ویژگی سبب داشتن کلاس‌های پویا می‌شود. برای درک بهتر این موضوع، فرض کنید که خصوصیتی را به کلاس پدر بعد از ارث بری کلاس فرزند اضافه می‌کنید. خصوصیت اضافه شده به تمامی فرزندان کلاس پدر به صورت خودکار اضافه می‌شود.
class Animal
  constructor: (@name) ->

class Parrot extends Animal

Animal::rip = true

parrot = new Parrot("Macaw")
alert("This parrot is no more") if parrot.rip

Mixins

Mixins توسط CoffeeScript پشتیبانی نمی‌شود و برای همین نیاز است که این قابلیت را برای خودمان پیاده سازی کنیم، به مثال زیر توجه کنید.
extend = (obj, mixin) ->
  obj[name] = method for name, method of mixin        
  obj

include = (klass, mixin) ->
  extend klass.prototype, mixin

# Usage
include Parrot,
  isDeceased: true

alert (new Parrot).isDeceased
نتیجه کامپایل آن می‌شود:
var extend, include;
extend = function(obj, mixin) {
  var method, name;
  for (name in mixin) {
    method = mixin[name];
    obj[name] = method;
  }
  return obj;
};
include = function(klass, mixin) {
  return extend(klass.prototype, mixin);
};
include(Parrot, {
  isDeceased: true
});
alert((new Parrot).isDeceased);
Mixins یک الگوی عالی برای به اشتراک گذاشتن خصوصیت‌های مشترک، در بین کلاس‌هایی است که امکان ارث بری در آنها وجود ندارد. مهمترین مزیت استفاده از Mixins این است که می‌توان چندین خصوصیت را به یک کلاس اضافه کرد؛ در حالیکه برای ارث بری فقط از یک کلاس می‌توان ارث بری داشت.

Extending classes


Mixins خیلی مرتب و خوب است اما خیلی شیء گرا نیست؛ در عوض امکان ادغام را در کلاس‌های CoffeeScript ایجاد می‌کند. برای اینکه اصول شیء گرایی را بخواهیم رعایت کنیم و ویژگی ادغام را نیز داشته باشیم، کلاسی با نام Module را پیاده سازی می‌کنیم و تمامی کلاس‌هایی را که می‌خواهیم ویژگی ادغام را داشته باشند، از آن ارث بری می‌کنیم.
moduleKeywords = ['extended', 'included']

class Module
  @extend: (obj) ->
    for key, value of obj when key not in moduleKeywords
      @[key] = value

    obj.extended?.apply(@)
    this

  @include: (obj) ->
    for key, value of obj when key not in moduleKeywords
      # Assign properties to the prototype
      @::[key] = value

    obj.included?.apply(@)
    this
برای استفاده از کلاس Module به مثال زیر توجه کنید:
classProperties = 
  find: (id) ->
  create: (attrs) ->

instanceProperties =
  save: -> 

class User extends Module
  @extend classProperties
  @include instanceProperties

# Usage:
user = User.find(1)

user = new User
user.save()
همانطور که مشاهده می‌کنید دو خصوصیت ثابت (static property)، را به کلاس User اضافه کردیم (find, create) و خصوصیت save.
همچنین برای خلاصه نویسی بیشتر می‌توان از این الگو استفاده کرد (ساده و زیبا).
ORM = 
  find: (id) ->
  create: (attrs) ->
  extended: ->
    @include
      save: -> 

class User extends Module
  @extend ORM
مطالب
ایجاد قالب‌های سفارشی ستون‌ها و فرمت شرطی اطلاعات در PdfReport
صورت مساله:
- لیستی از حقوق کارکنان را داریم. در گزارش نهایی آن نیاز است عدد حقوق کارکنانی با مبلغ کمتر از 1000، با رنگی دیگر نمایش داده شوند.
همچنین در این گزارش هر ردیفی که در ماه 7 واقع شده نیز ظاهر عدد سلول مربوط به آن ماه، به رنگ قهوه‌ای و زمینه زرد تغییر یابد.
- در ستون مشخصات افراد این گزارش، نیاز است تصویر کارمند به همراه نام او در ذیل این تصویر (داخل یک سلول) نمایش داده شوند.

چیزی شبیه به این گزارش!


مورد اول در گزارشات، اصطلاحا به conditional formatting معروف است و مورد دوم مرتبط است به تهیه قالب‌های سفارشی، بجای استفاده از قالب‌های سلول‌های پیش فرض PdfReport؛ که در ادامه نحوه انجام این موارد را بررسی خواهیم کرد.

ابتدا سورس کامل این مثال را ملاحظه نمائید:
using System;
using iTextSharp.text;
using PdfRpt.Core.Contracts;
using PdfRpt.Core.Helper;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.CustomCellTemplate
{
    public class CustomCellTemplatePdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.RightToLeft);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
                doc.Compression(new CompressionSettings
                               {
                                   CompressionLevel = CompressionLevel.BestCompression,
                                   EnableCompression = true
                               });
            })
             .DefaultFonts(fonts =>
             {
                 fonts.Path(AppPath.ApplicationPath + "\\fonts\\irsans.ttf",
                            Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
             })
             .PagesFooter(footer =>
             {
                 footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy"));
             })
             .PagesHeader(header =>
             {
                 header.DefaultHeader(defaultHeader =>
                 {
                     defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                     defaultHeader.Message("گزارش جدید ما");
                 });
             })
             .MainTableTemplate(template =>
             {
                 template.BasicTemplate(BasicTemplate.SnowyPineTemplate);
             })
             .MainTablePreferences(table =>
             {
                 table.ColumnsWidthsType(TableColumnWidthType.Relative);
                 table.MultipleColumnsPerPage(new MultipleColumnsPerPage
                 {
                     ColumnsGap = 20,
                     ColumnsPerPage = 2,
                     ColumnsWidth = 250,
                     IsRightToLeft = true,
                     TopMargin = 7
                 });
             })
             .MainTableDataSource(dataSource =>
             {
                 var table = new System.Data.DataTable("لیست حقوق");
                 table.Columns.Add("شخص", typeof(string));
                 table.Columns.Add("ماه", typeof(int));
                 table.Columns.Add("مبلغ", typeof(decimal));

                 var rnd = new Random();
                 for (int i = 0; i < 200; i++)
                     table.Rows.Add("شخص " + i, rnd.Next(1, 12), rnd.Next(400, 2000));

                 dataSource.DataTable(table);
             })
             .MainTableEvents(events =>
             {
                 events.DataSourceIsEmpty(message: "There is no data available to display.");
                 events.CellCreated(args =>
                     {
                         //change the background color of the cell based on the value
                         if (args.RowType == RowType.DataTableRow && args.Cell.RowData.Value != null && args.Cell.RowData.Value is decimal)
                         {
                             if ((decimal)args.Cell.RowData.Value <= 1000)
                                 args.Cell.BasicProperties.BackgroundColor = BaseColor.CYAN;
                         }
                     });
             })
             .MainTableSummarySettings(summary =>
             {
                 summary.OverallSummarySettings("جمع کل");
                 summary.PageSummarySettings("جمع صفحه");
                 summary.PreviousPageSummarySettings("نقل از ستون قبل");
             })
             .MainTableColumns(columns =>
             {
                 columns.AddColumn(column =>
                 {
                     column.PropertyName("rowNo");
                     column.IsRowNumber(true);
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(0);
                     column.Width(1);
                     column.HeaderCell("ردیف");
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName("شخص");
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(1);
                     column.Width(3);
                     column.HeaderCell("شخص");
                     column.ColumnItemsTemplate(t => t.CustomTemplate(new MyCustomCellTemplate()));
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName("ماه");
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(2);
                     column.Width(2);
                     column.HeaderCell("ماه");
                     column.ColumnItemsTemplate(template =>
                     {
                         template.TextBlock();
                         template.ConditionalFormatFormula(list =>
                         {
                             var cellValue = int.Parse(list.GetSafeStringValueOf("ماه", nullValue: "0"));
                             if (cellValue == 7)
                             {
                                 return new CellBasicProperties
                                 {
                                     PdfFontStyle = DocumentFontStyle.Bold | DocumentFontStyle.Underline,
                                     FontColor = new BaseColor(System.Drawing.Color.Brown),
                                     BackgroundColor = new BaseColor(System.Drawing.Color.Yellow)
                                 };
                             }
                             return new CellBasicProperties { PdfFontStyle = DocumentFontStyle.Normal };
                         });
                     });
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName("مبلغ");
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(3);
                     column.Width(2);
                     column.HeaderCell("مبلغ");
                     column.ColumnItemsTemplate(template =>
                     {
                         template.TextBlock();
                         template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                     });
                     column.AggregateFunction(aggregateFunction =>
                     {
                         aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                         aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                     });
                 });
             })
             .Export(export =>
                 {
                     export.ToXml();
                     export.ToExcel();
                 })
             .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptDataTableSample.pdf"));
        }
    }
}
به همراه قالب سلول سفارشی آن:
using System;
using System.Collections.Generic;
using iTextSharp.text;
using iTextSharp.text.pdf;
using PdfRpt.Core.Contracts;
using PdfRpt.Core.Helper;

namespace PdfReportSamples.CustomCellTemplate
{
    public class MyCustomCellTemplate : IColumnItemsTemplate
    {
        Random _rnd = new Random();

        public void CellRendered(PdfPCell cell, Rectangle position, PdfContentByte[] canvases, CellAttributes attributes)
        {
        }

        public CellBasicProperties BasicProperties { set; get; }
        public Func<IList<CellData>, CellBasicProperties> ConditionalFormatFormula { set; get; }

        public PdfPCell RenderingCell(CellAttributes attributes)
        {
            var pdfCell = new PdfPCell();
            var table = new PdfPTable(1) { RunDirection = PdfWriter.RUN_DIRECTION_RTL };

            var filePath = AppPath.ApplicationPath + "\\Images\\" + _rnd.Next(1, 5).ToString("00") + ".png";
            var photo = PdfImageHelper.GetITextSharpImageFromImageFile(filePath);
            table.AddCell(new PdfPCell(photo, fit: false)
            {
                Border = 0,                
                VerticalAlignment = Element.ALIGN_BOTTOM,
                HorizontalAlignment = Element.ALIGN_CENTER
            });

            var name = attributes.RowData.TableRowData.GetSafeStringValueOf("شخص");
            table.AddCell(new PdfPCell(attributes.BasicProperties.PdfFont.FontSelector.Process(name))
            {
                Border = 0,
                HorizontalAlignment = Element.ALIGN_CENTER
            });

            pdfCell.AddElement(table);

            return pdfCell;
        }
    }
}

توضیحات:

- در این مثال از منبع داده‌ای از نوع DataTable استفاده شده است؛ که نحوه بکارگیری آن‌را در متد MainTableDataSource ملاحظه می‌کنید. ستون‌های تعریف شده در MainTableColumns نیز بر اساس ستون‌های DataTable مشخص شده‌اند.
- در متد DocumentPreferences، نحوه مشخص سازی فشرده سازی نهایی فایل PDF را ملاحظه می‌کنید. این مورد از مزایای استفاده از فایل‌های PDF است.

- برای اعمال فرمت شرطی اطلاعات در PdfReport دو روش وجود دارد.
الف) استفاده از متد MainTableEvents و کار کردن با رخ‌دادهای تعریف شده در آن مانند CellCreated. در اینجا می‌توان در نحوه رندر شدن یک سلول دخالت کرد:
events.CellCreated(args =>
    {
         //change the background color of the cell based on the value
         if (args.RowType == RowType.DataTableRow && args.Cell.RowData.Value != null && args.Cell.RowData.Value is decimal)
         {
              if ((decimal)args.Cell.RowData.Value <= 1000)
                   args.Cell.BasicProperties.BackgroundColor = BaseColor.CYAN;
          }
      });
برای مثال در تعاریف فوق، اگر نوع ردیف، از نوع ردیف‌های اطلاعاتی جدول باشد، مقدار آن دریافت شده و بر اساس شرطی مشخص، برای نمونه رنگ پس زمینه آن سلول تغییر داده می‌شود.
ب) همانطور که در قسمت تعریف ستون «ماه» ملاحظه می‌کنید، توسط متد template.ConditionalFormatFormula نیز، امکان فرمت شرطی اطلاعات فراهم شده است. در اینجا می‌توان به لیست اطلاعات سلول‌های ردیف جاری دسترسی یافت و سپس بر اساس آن تصمیم گیری کرد.

- جهت تعریف قالب‌های سفارشی سلول‌ها کافی است اینترفیس IColumnItemsTemplate را پیاده سازی کنیم؛ که نمونه‌ای از آن را در کدهای MyCustomCellTemplate فوق ملاحظه می‌کنید. در اینجا فرصت خواهید داشت هر شکل و طرح متنوعی را تهیه کرده و به صورت یک PdfPCell بازگشت دهید. برای نمونه در مثال فوق، یک جدول را در سلول تعریف شده قرار داده‌ایم. این جدول یک ستون دارد و هر سلولی که به آن اضافه خواهد شد، یک ردیف را تشکیل خواهد داد. در ردیف اول آن تصویر قرار گرفته و در ردیف دوم آن مقدار سلول جاری.
پاسخ به بازخورد‌های پروژه‌ها
درخواست مستندات
سلام آقای نصیری
بنده قصد ایجاد یک گزارش ساز رو دارم، که قراره یک محیط تحت وب باشه و خروجی این گزارش ساز یه فایل xml مثل زیر به ما میده.
<Report>
    <DocumentPreferences RunDirection="RTL" PageOrientation="Portrait" PageSize="A4">
        <Watermark RunDirection="" Text="پس زمینه نمونه" Font="bardia.ttf" FillOpacity="0.7" StrokeOpacity="0.5" />
    </DocumentPreferences>
    <DefaultFonts  PrimaryFont="irsans.ttf" SecondaryFont="farnaz.ttf" Size="12px" Color="#CCFFBB" /> 
    <DefaultFooter Message="پا صفحه نمونه" />
    <DefaultHeader Message="سرصفحه نمونه" MessageFontColor="#ََََAA0012" ImagePath="logo.png" />
    <MainTableTemplate TemplateName="AppleOrchardTemplate" />
    <MainTablePreferences ColumnsWidthsType="Relative" NumberOfDataRowsPerPage="0" >
        <GroupsPreferences GroupType="HideGroupingColumns" ShowOneGroupPerPage="true" /> 
    </MainTablePreferences>
    <MainTableSummarySettings OverallSummarySettings="جمع کل" PreviousPageSummarySettings="نقل از صفحه قبل" 
                              PageSummarySettings="جمع کل صفحه" AllGroupsSummarySettings="جمع کل گروه ها"   />
    <MainTableDataSource ProviderName="System.Data.SQLُServerCE" 
                         ConnectionString="Data Source=MyData.sdf;Persist Security Info=False;"
                         SqlStatement="SELECT [url], [name], [NumberOfPosts], [AddDate]
                               FROM [tblBlogs]
                               WHERE [NumberOfPosts]>=@p1” >
        <Parameters>
            <Param Type="int" Name="@p1"/>
        </Parameters>
    </MainTableDataSource>
    <MainTableColumns>
        <Columns>
            <Column CellHorizontalAlignment="Right" ColumnItemsTemplate="TextBlock" HeaderCell="عنوان ستون 1" PropertyName="Title" Order="0" Width="1" />
            <Column CellHorizontalAlignment="Right" Group="true" ColumnItemsTemplate="TextBlock" HeaderCell="عنوان ستون 2" PropertyName="Category" Order="1" Width="2" />
            <Column CellHorizontalAlignment="Right" ColumnItemsTemplate="Image" HeaderCell="عنوان ستون 3" PropertyName="Image" Order="2" Width="2" />
        </Columns>
    </MainTableColumns>
    <MainTableEvents> 
        <DataSourceIsEmpty Message="داده ای برای نمایش وجود ندارد" />
    </MainTableEvents>
    <Export ToExcel="True" ToCsv="False" ToXml="True" />
    <Generate Type="AsPdfFile" FileName="Report.pdf" />
</Report>
بعدش هر برنامه ای با هر زبانی بیاد و Provider و مفسر این فایل xml تولید شده رو واسه خودش بنویسه و فایل گزارش نهایی رو تولید کنه.
من برای برنامه‌های C# میخوام از pdfreport استفاده کنم،یعنی از روی این فایل xml کلاس متناظرش رو بسازم و runtime کامپایل کنم و خروجی رو به کاربر نشون بدم. آیا مستنداتی به غیر از sourceCode‌ها ومثال‌ها که البته اونا رو قبلا گرفتم برای پروژه pdfReport وجود داره در حال حاضر؟
با تشکر
مطالب
شروع به کار با Aurelia
در این مطلب قصد داریم نحوه‌ی راه اندازی و شروع به کار با Aurelia را توضیح دهیم. برای شروع ابتدا باید Git را نصب کنیم. زیرا می‌خواهیم از jspm برای نصب محتویات Aurelia استفاده کنیم و برای کار کردن با jspm باید Git بر روی سیستم نصب باشد. در صورتیکه Git نصب نباشد، زمان init کردن jspm با خطای زیر رو به رو می‌شویم :

پس از نصب Git اطمینان حاصل کنید که NodeJs ، npm و jspm نیز بر روی سیستم شما نصب باشند. در این قسمت گفته‌ایم که چگونه از این «اطمینان» آگاه شوید.

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

jspm init

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

حالا نوبت به نصب محتویات Aurelia می‌باشد. برای این کار دستورات زیر را اجرا کنید : 

jspm install aurelia-framework
jspm install aurelia-bootstrapper
با صادر کردن دستورات بالا، jspm تمامی فایل‌های مورد نیاز Aurelia را در پوشه‌ی jspm_packages قرار می‌دهد.

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

پس از این کارها، فایل Layout را باز کنید و کدهای آن را به صورت زیر تغییر دهید:
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Aurelia - www.dotnettips.info</title>
</head>
<body aurelia-app>
    <div>
        @RenderBody()
    </div>
    <script src="~/jspm_packages/system.js"></script>
    <script src="~/config.js"></script>
    <script>
        System.import("aurelia-bootstrapper");
    </script>
</body>
</html>
سپس در ریشه‌ی برنامه یک فایل جاوااسکریپت را با نام app ایجاد کنید و کدهای زیر را در آن قرار دهید:
export class App {
    
}
حالا به یک View، برای این کار نیاز داریم. در همان ریشه‌ی سایت یک فایل html را با نام app ایجاد کنید و کدهای زیر را در آن قرار دهید:
<template>
    <h3>www.dotnettips.info</h3>
</template>
پس از سپری کردن این 3 مرحله، برنامه‌ی خود را اجرا کنید. باید خروجی زیر را مشاهده کنید:

در بخش‌های بعدی در مورد کدهای فوق و همچنین به سایر مباحث دیگر Aurelia می‌پردازیم.

نکته‌ی تکمیلی

زمانیکه می‌خواهید از دستوارت jspm استفاده کنید، باید به ریشه‌ی برنامه مراجعه کنید. حال اگر پوشه‌های تودرتوی زیادی داشته باشید، این رفت و آمد‌ها زمانبر و خسته کننده خواهند شد. برای حل این مشکل کافیست روی پروژه، راست کلیک کنید و گزینه‌ی Open Folder in File Explorer را انتخاب کنید تا ریشه‌ی پروژه باز شود. حالا  کافیست در فضای خالی Windows Explorer کلید Shift را گرفته و راست کلیک کنید. حالا یک آیتم جدید با نام Open command window here ظاهر شده است. کافیست روی آن کلیک کنید. CMD در ریشه‌ی سایت باز خواهد شد.


دانلود پروژه جاری در مخزن گیت