مطالب
آموزش نصب مک بر روی Virtual Box
پیرو مطالب آموزشی وب سایت در رابطه با توسعه برنامه‌های Cross Platform توسط Xamarin  که میتوانید در این قسمت آن‌ها را ببینید، نیاز به نصب سیستم عامل مک برای توسعه  اپلیکیشن‌های مخصوص iDevice‌ها داریم. از آنجا که سخت افزار‌های اپل فوق العاده گران می‌باشند، تهیه آن برای یادگیری این پلتفرم مقرون به صرفه نیست. لذا سعی کردم آموزش کاملی از نصب این سیستم عامل را بر روی مجازی ساز‌ها و کامپیوتر واقعی تهیه کنم و در اختیار دوستان قرار دهم تا بتوانند نیاز خود برای دسترسی به این سیستم عامل را مرتفع کنند.
روش‌های مختلفی برای این کار وجود دارند:
  • استفاده از شبیه ساز‌ها مانند Virtual Box یا VMWare
  • استفاده از نسخه دستکاری شده که بتوانید بر روی سیستم سخت افزاری خود نصب کنید

مرحله اول : Virtual Box چیست؟

VirtualBox، نرم افزاری برای شبیه سازی ماشین‌های سخت افزاری است که می‌توانید بر روی آن سیستم عامل‌های مختلفی را نصب کنید که توسط سیستم عامل فعلی شما کنترل می‌شوند. ما از واژه‌ی سیستم عامل «میهمان» برای سیستم عامل مجازی و از سیستم عامل «میزبان» برای سیستم جاری خود استفاده خواهیم کرد. در اینجا امکان کنترل منابع سیستم عامل میهمان به راحتی وجود خواهد داشت.
برای نصب این نرم افزار، آن را از این لینک از سایت سازنده، دریافت کنید.

مرحله دوم : ایمیج سیستم عامل مک

ما برای نصب سیستم عامل بر روی ماشین مجازی چند گزینه خواهیم داشت.  برای یافتن آخرین نسخه سیستم عامل مک این لینک می تواند راهنمای خوبی باشد.
  • استفاده از فایل ISO همانند ویندوز
  • استفاده از فایل VMDK آماده
  • ایجاد ایمیج از روی فایل DMG

فایل ISO سیستم عامل
استفاده از فایل ISO در واقع همانند نصب سایر سیستم عامل‌ها می‌باشد. نیاز است تا فایل ISO مربوط به سیستم عامل را دانلود کرده و از آن به عنوان نصاب استفاده کنید. دقت داشته باشید که فایل ISO به تنهایی و با تبدیل فایل DMG به ISO قابل استفاده نخواهد بود. زیرا بوت لودر این سیستم عامل با سیستم عامل‌های مرسوم مانند لینوکس و ویندوز متفاوت می‌باشد. برای پیدا کردن فایل ISO آخرین نسخه سیستم عامل میتوانید عبارتی مشابه با "Mac OS  iso download" را در گوگل جستجو کنید و یا سایتی مانند این میتواند گزینه خوبی برای دریافت این فایل باشد.

فایل VMDK 
این فایل در واقع فایل هارد دیسک مربوط به مجازی ساز می‌باشد. نکته جالب این نوع فایل‌ها این است به صورت استاندارد میتوانند بین مجازی ساز‌های مختلف مانند VMWare Workstation نیز قابل اشتراک گذاری باشند.
برای سهولت در یافتن این فایل، من آخرین نسخه سیستم عامل مک را که در زمان نگارش این مطلب 10.14 با نام Mojave می‌باشد، بر روی اکانت گوگل درایو خودم آپلود کردم که میتوانید آن را در اینجا پیدا کنید:
فایل VMDK در واقع یک هارد دیسک شامل نسخه نصب شده و آماده استفاده سیستم عامل می‌باشد. در واقع راحت‌ترین و ساده‌ترین روش استفاده از این نوع فایل‌ها است که ما هم در این مقاله از آن استفاده خواهیم کرد.
ایجاد ایمیج از روی فایل DMG 
در این روش نیاز به یک سیستم عامل مک دارید که توسط آن بتوانید ایمیج مربوطه را بسازید. از آن جایی که احتمالا  خوانندگان این مطلب دسترسی به سیستم عامل مک را ندارند، در حال حاضر از توضیحات اضافی پرهیز میکنیم.
در آینده پیرو این مطالب سایر روش‌ها را نیز به صورت کامل توضیح خواهم داد. 
مرحله سوم: ایجاد ماشین مجازی

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

در مراحل مختلف، سوالات متعددی برای آماده سازی ماشین مجازی از شما پرسیده خواهد شد. نام آن را Mac قرار دهید. از کادر انتخابی Type، گزینه Mac OS را انتخاب کرده و نسخه 10.13 High Sierra را انتخاب کنید.

در صفحه بعدی شما میزان RAM ای را که به سیستم عامل میهمان اختصاص میدهید، باید مشخص کنید. حداقل 4 گیگابایت رم به سیستم عامل میهمان اختصاص دهید. دقت داشته باشید که میزان آن 50 الی 65 درصد از کل رم سیستم تان باشد.

در مرحله بعدی شما باید تنظیمات مربوط به هارد دیسک را انجام دهید. گزینه “use an existing virtual hard disk file”  را انتخاب کنید. سپس فایلی را که در مرحله قبلی با پسوند VMDK دانلود کرده‌اید، انتخاب کنید.

نهایتا بر روی Finish کلیک کنید تا ماشین میهمان ساخته شود.


مرحله چهارم: ویرایش تنظیمات مربوط به ماشین مجازی


ماشین مجازی را که در مرحله قبلی ایجاد کرده‌اید، باز کنید و بر روی دکمه‌ی Setting کلیک کنید. در دسته بندی System بر روی تب Motherboard  کلیک کنید. گزینه انتخابی Enable EFI را فعال کنید و Chipset را به IHC9 و یا PIIX3 تغییر دهید.


در تب Processor  گزینه Enable PAE/NX را فعال کرده و Core‌ها را به نصف Core‌های سیستم فعلی خود ارتقاء دهید.


در دسته Display، گزینه Video Memory را به 128 مگابایت ارتقا دهید.

شما میتوانید سایر گزینه‌ها را نیز بسته به نیاز خود تغییر دهید.


بر روی تب Storage کلیک کرده و گزینه Use Host I/O Cache را فعال کنید.



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

خط فرمان (CMD) را به عنوان Administrator باز کنید.

دستورات زیر را وارد کنید. دقت داشته باشید که بجای Your VM Name؛ نام ماشین مجازی خود را وارد کنید؛ در مثال ما Mac

cd "C:\Program Files\Oracle\VirtualBox\"
VBoxManage.exe modifyvm "Your VM Name" --cpuidset 00000001 000106e5 00100800 0098e3fd bfebfbff
VBoxManage setextradata "Your VM Name" "VBoxInternal/Devices/efi/0/Config/DmiSystemProduct" "iMac11,3"
VBoxManage setextradata "Your VM Name" "VBoxInternal/Devices/efi/0/Config/DmiSystemVersion" "1.0"
VBoxManage setextradata "Your VM Name" "VBoxInternal/Devices/efi/0/Config/DmiBoardProduct" "Iloveapple"
VBoxManage setextradata "Your VM Name" "VBoxInternal/Devices/smc/0/Config/DeviceKey" "ourhardworkbythesewordsguardedpleasedontsteal(c)AppleComputerInc"
VBoxManage setextradata "Your VM Name" "VBoxInternal/Devices/smc/0/Config/GetKeyFromRealSMC" 1

VirtualBox را قبل از اجزای این دستورات ببندید و سپس این دستورات را اجرا کنید.


مرحله ششم: اجرای سیستم عامل مک نسخه 10.14 بر روی Virtual Box


ماشین مجازی را که ایجاد کرده اید، باز کنید و بر روی start کلیک کنید:


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

تنظیمات اولیه و نام کاربری سیستم عامل را وارد کنید و تمام!


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

خوشبختانه برای کار با Xamarin.iOS شما مجبور به کد نویسی بر روی مک نخواهید بود و تنها پروسه بیلد پروژه بر روی آن انجام خواهد شد. لذا مشکلات کارآیی آن بر روی روند کار شما تاثیر چندانی نخواهد داشت. البته برای نصب این سیستم عامل به صورت مجازی توصیه می‌شود از هارد SSD استفاده کنید.



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


در صورتیکه در مراحل نصب و یا پس از نصب سیستم عامل، کیبرد و یا ماوس کار نمیکنند، میتوانید مراحل زیر را انجام دهید:

به سایت VirtualBox.org رفته و آخرین نسخه‌ی Extension Pack را دانلود کنید.

سپس VirtualBox را باز کنید و از منوی فایل، گزینه‌ی Preferences را انتخاب کنید: 

بر روی برگه‌ی Extensions کلیک کرده و گزینه‌ی Add را انتخاب کنید:

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

در صورتی که بعد از اعمال تغییرات فوق، مشکل همچنان باقی بود، می‌توانید در تنظیمات VM خود در تب  USB، گزینه‌ی USB 3.0 (xHCI) Controller.  را انتخاب کنید.


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

مطالب
طراحی یک گرید با Angular و ASP.NET Core - قسمت دوم - پیاده سازی سمت کلاینت
در قسمت قبل، کار پیاده سازی سمت سرور نمایش اطلاعات یک گرید، به پایان رسید. در این قسمت می‌خواهیم از سمت کلاینت، اطلاعات صفحه بندی و مرتب سازی را به سمت سرور ارسال کرده و همچنین نتیجه‌ی دریافتی از سرور را نمایش دهیم.



پیشنیازهای نمایش اطلاعات گرید به همراه صفحه بندی اطلاعات

در مطلب «Angular CLI - قسمت ششم - استفاده از کتابخانه‌های ثالث» نحوه‌ی نصب و معرفی کتابخانه‌ی ngx-bootstrap را بررسی کردیم. دقیقا همان مراحل، در اینجا نیز باید طی شوند و از این مجموعه تنها به کامپوننت Pagination آن نیاز داریم. همان قسمت ذیل گرید تصویر فوق که شماره صفحات را جهت انتخاب، نمایش داده‌است.
بنابراین ابتدا فرض بر این است که دو بسته‌ی بوت استرپ و ngx-bootstrap را نصب کرده‌اید:
> npm install bootstrap --save
> npm install ngx-bootstrap --save
در فایل angular-cli.json. شیوه‌نامه‌ی بوت استرپ را نیز افزوده‌اید:
  "apps": [
    {
      "styles": [
    "../node_modules/bootstrap/dist/css/bootstrap.min.css",
        "styles.css"
      ],
پس از آن باید به‌خاطر داشت که کامپوننت نمایش صفحه بندی این مجموعه PaginationModule نام دارد و باید در نزدیک‌ترین ماژول مورد نیاز، ثبت و معرفی شود:
import { PaginationModule } from "ngx-bootstrap";

@NgModule({
  imports: [
    PaginationModule.forRoot()
  ]
برای نمونه در این مثال، ماژولی به نام simple-grid.module.ts دربرگیرنده‌ی گرید مطلب جاری است و به صورت ذیل به برنامه اضافه شده‌است:
 >ng g m SimpleGrid -m app.module --routing
بنابراین تعریف PaginationModule باید به قسمت imports این ماژول اضافه شود و تعریف آن در app.module.ts تاثیری بر روی این قسمت نخواهد داشت.

کامپوننتی هم که مثال جاری را نمایش می‌دهد به صورت ذیل به ماژول SimpleGrid فوق اضافه شده‌است:
 >ng g c SimpleGrid/products-list


تهیه معادل‌های قراردادهای سمت سرور در سمت Angular

در قسمت قبل، تعدادی قرارداد مانند پارامترهای دریافتی از سمت کلاینت و ساختار اطلاعات ارسالی به سمت کلاینت را تعریف کردیم. اکنون جهت کار strongly typed با آن‌ها در سمت یک برنامه‌ی تایپ اسکریپتی Angular، کلاس‌های معادل آن‌ها را تهیه می‌کنیم.

ساختار شیء محصول دریافتی از سمت سرور
 >ng g cl SimpleGrid/app-product
با این محتوا
export class AppProduct {
  constructor(
    public productId: number,
    public productName: string,
    public price: number,
    public isAvailable: boolean
  ) {}
}
که در اینجا هر کدام از خواص ذکر شده، معادل camel case نمونه‌ی سمت سرور خود هستند (چون JSON.NET در ASP.NET Core، به صورت پیش فرض یک چنین خروجی را تولید می‌کند).

ساختار معادل پارامترهای صفحه بندی و مرتب سازی ارسالی به سمت سرور
 >ng g cl SimpleGrid/PagedQueryModel
با این محتوا
export class PagedQueryModel {
  constructor(
    public sortBy: string,
    public isAscending: boolean,
    public page: number,
    public pageSize: number
  ) {}
}
در اینجا همان ساختار IPagedQueryModel سمت سرور را مشاهده می‌کنید. از آن جهت مشخص سازی جزئیات صفحه بندی و نحوه‌ی مرتب سازی اطلاعات، استفاده می‌شود.

ساختار معادل اطلاعات صفحه بندی شده‌ی دریافتی از سمت سرور
 >ng g cl SimpleGrid/PagedQueryResult
با این محتوا
export class PagedQueryResult<T> {
  constructor(public totalItems: number, public items: T[]) {}
}
این ساختار جنریک نیز دقیقا معادل همان PagedQueryResult سمت سرور است و حاوی تعداد کل ردیف‌های یک کوئری و تنها قسمتی از اطلاعات صفحه بندی شده‌ی آن می‌باشد.

ساختار ستون‌های گرید نمایشی
 >ng g cl SimpleGrid/GridColumn
با این محتوا
export class GridColumn {
  constructor(
    public title: string,
    public propertyName: string,
    public isSortable: boolean
  ) {}
}
هر ستون نمایش داده شده، دارای یک برچسب، خاصیتی مشخص در سمت سرور و بیانگر قابلیت مرتب سازی آن می‌باشد. اگر isSortable به true تنظیم شود، با کلیک بر روی سرستون‌ها می‌توان اطلاعات را بر اساس آن ستون، مرتب سازی کرد.


تهیه سرویس ارسال اطلاعات صفحه بندی به سرور و دریافت اطلاعات از آن

پس از تدارک این مقدمات، اکنون کار تعریف سرویسی که این اطلاعات را به سمت سرور ارسال می‌کند و نتیجه را باز می‌گرداند، به صورت ذیل خواهد بود:
 >ng g s SimpleGrid/products-list -m simple-grid.module
این دستور سبب ایجاد کلاس ProductsListService شده و همچنین قسمت providers ماژول simple-grid را نیز بر این اساس به روز رسانی می‌کند.
پیش از تکمیل این سرویس، نیاز است متدی را جهت تبدیل یک شیء، به معادل کوئری استرینگ آن تهیه کنیم:
  toQueryString(obj: any): string {
    const parts = [];
    for (const key in obj) {
      if (obj.hasOwnProperty(key)) {
        const value = obj[key];
        if (value !== null && value !== undefined) {
          parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(value));
        }
      }
    }
    return parts.join("&");
  }
در قسمت قبل امضای متد GetPagedProducts دارای ویژگی HttpGet است. بنابراین، نیاز است اطلاعات را به صورت کوئری استرینگ از سمت کلاینت دریافت کند و متد toQueryString فوق به صورت خودکار بر روی تمام خواص یک شیء دلخواه حرکت کرده و آن‌ها را تبدیل به یک رشته‌ی حاوی کوئری استرینگ‌ها می‌کند.
[HttpGet("[action]")]
public PagedQueryResult<Product> GetPagedProducts(ProductQueryViewModel queryModel)
برای نمونه متد toQueryString فوق است که سبب ارسال یک چنین درخواستی به سمت سرور می‌شود:
 http://localhost:5000/api/Product/GetPagedProducts?sortBy=productId&isAscending=true&page=2&pageSize=7

پس از این تعریف، سرویس ProductsListService  به صورت ذیل تکمیل خواهد شد:
@Injectable()
export class ProductsListService {
  private baseUrl = "api/Product";

  constructor(private http: Http) {}

  getPagedProductsList(
    queryModel: PagedQueryModel
  ): Observable<PagedQueryResult<AppProduct>> {
    return this.http
      .get(`${this.baseUrl}/GetPagedProducts?${this.toQueryString(queryModel)}`)
      .map(res => {
        const result = res.json();
        return new PagedQueryResult<AppProduct>(
          result.totalItems,
          result.items
        );
      });
  }
در اینجا از متد toQueryString، جهت تکمیل متد get ارسالی به سمت سرور استفاده شده‌است تا پارامترها را به صورت کوئری استرینگ‌ها تبدیل کرده و ارسال کند.
سپس در متد map آن، res.json دقیقا همان ساختار PagedQueryResult سمت سرور را به همراه دارد. اینجا است که فرصت خواهیم داشت نمونه‌ی سمت کلاینت آن‌را که در ابتدای بحث تهیه کردیم، وهله سازی کرده و بازگشت دهیم (نگاشت فیلدهای دریافتی از سمت سرور به سمت کلاینت).


تکمیل کامپوننت نمایش گرید

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


  <table class="table table-striped table-hover table-bordered table-condensed">
    <thead>
      <tr>
        <th class="text-center" style="width:3%">#</th>
        <th *ngFor="let column of columns" class="text-center">
          <div *ngIf="column.isSortable" (click)="sortBy(column.propertyName)" style="cursor: pointer">
            {{ column.title }}
            <i *ngIf="queryModel.sortBy === column.propertyName" class="glyphicon"
              [class.glyphicon-sort-by-order]="queryModel.isAscending" [class.glyphicon-sort-by-order-alt]="!queryModel.isAscending"></i>
          </div>
          <div *ngIf="!column.isSortable" style="cursor: pointer">
            {{ column.title }}
          </div>
        </th>
      </tr>
    </thead>
در اینجا ابتدا بررسی می‌شود که آیا یک ستون قابلیت مرتب سازی را دارد، یا خیر؟ اگر اینطور است، در کنار آن یک گلیف آیکن مرتب سازی درج می‌شود. اگر خیر، صرفا متن عنوان آن نمایش داده خواهد شد. می‌شد تمام این موارد را به ازای هر ستون به صورت مجزایی ارائه داد، اما در این حالت به کدهای تکراری زیادی می‌رسیدیم. به همین جهت از یک حلقه بر روی تعریف ستون‌های این گرید استفاده شده‌است. آرایه‌ی این ستون‌ها نیز به صورت ذیل تعریف می‌شود:
export class ProductsListComponent implements OnInit {
  columns: GridColumn[] = [
    new GridColumn("Id", "productId", true),
    new GridColumn("Name", "productName", true),
    new GridColumn("Price", "price", true),
    new GridColumn("Available", "isAvailable", true)
  ];

همچنین در کدهای قالب این کامپوننت، مدیریت کلیک بر روی یک سر ستون را نیز مشاهده می‌کنید:
export class ProductsListComponent implements OnInit {
  itemsPerPage = 7;
  queryModel = new PagedQueryModel("productId", true, 1, this.itemsPerPage);

  sortBy(columnName) {
    if (this.queryModel.sortBy === columnName) {
      this.queryModel.isAscending = !this.queryModel.isAscending;
    } else {
      this.queryModel.sortBy = columnName;
      this.queryModel.isAscending = true;
    }
    this.getPagedProductsList();
  }
}
در این‌حالت اگر ستونی که بر روی آن کلیک شده، پیشتر مرتب سازی شده‌است، صرفا خاصیت صعودی بودن آن برعکس خواهد شد. در غیراینصورت، نام خاصیت درخواستی مرتب سازی و جهت آن نیز مشخص می‌شود. سپس مجددا این گرید توسط متد getPagedProductsList رندر خواهد شد.

کار رندر بدنه‌ی اصلی گرید توسط همین چند سطر در قالب آن مدیریت می‌شود:
    <tbody>
      <tr *ngFor="let item of queryResult.items; let i = index">
        <td class="text-center">{{ itemsPerPage * (currentPage - 1) + i + 1 }}</td>
        <td class="text-center">{{ item.productId }}</td>
        <td class="text-center">{{ item.productName }}</td>
        <td class="text-center">{{ item.price | number:'.0' }}</td>
        <td class="text-center">
          <input id="item-{{ item.productId }}" type="checkbox" [checked]="item.isAvailable"
            disabled="disabled" />
        </td>
      </tr>
    </tbody>
  </table>
اولین ستون آن، اندکی ابتکاری است. در اینجا شماره ردیف‌های خودکاری در هر صفحه درج خواهند شد. این شماره ردیف نیز جزو ستون‌های منبع داده‌ی فرضی برنامه نیست. به همین جهت برای درج آن، توسط let i = index در ngFor، به شماره ایندکس ردیف جاری دسترسی پیدا می‌کنیم. سپس توسط محاسباتی بر اساس تعداد ردیف‌های هر صفحه و شماره‌ی صفحه‌ی جاری، می‌توان شماره ردیف فعلی را محاسبه کرد.

در اینجا حلقه‌ای بر روی queryResult.items تشکیل شده‌است. این منبع داده به صورت ذیل در کامپوننت متناظر مقدار دهی می‌شود:
export class ProductsListComponent implements OnInit {
  itemsPerPage = 7;
  currentPage: number;
  numberOfPages: number;
  isLoading = false;
  queryModel = new PagedQueryModel("productId", true, 1, this.itemsPerPage);
  queryResult = new PagedQueryResult<AppProduct>(0, []);

  constructor(private productsListService: ProductsListService) {}

  ngOnInit() {
    this.getPagedProductsList();
  }

  private getPagedProductsList() {
    this.isLoading = true;
    this.productsListService
      .getPagedProductsList(this.queryModel)
      .subscribe(result => {
        this.queryResult = result;
        this.isLoading = false;
      });
  }
}
ابتدا سرویس ProductsListService را که در ابتدای بحث تکمیل شد، به سازنده‌ی این کامپوننت تزریق می‌کنیم. به کمک آن می‌توان در متد getPagedProductsList، ابتدا queryModel جاری را که شامل اطلاعات مرتب سازی و صفحه بندی است، به سرور ارسال کرده و سپس نتیجه‌ی نهایی را به queryResult انتساب دهیم. به این ترتیب تعداد کل رکوردها و همچنین آیتم‌های صفحه‌ی جاری دریافت می‌شوند. اکنون حلقه‌ی ngFor نمایش بدنه‌ی گرید، کار تکمیل صفحه‌ی جاری را انجام خواهد داد.

قسمت آخر کار، افزودن کامپوننت نمایش شماره صفحات است:


  <div align="center">
    <pagination [maxSize]="8" [boundaryLinks]="true" [totalItems]="queryResult.totalItems"
      [rotate]="false" previousText="&lsaquo;" nextText="&rsaquo;" firstText="&laquo;"
      lastText="&raquo;" (numPages)="numberOfPages = $event" [(ngModel)]="currentPage"
      (pageChanged)="onPageChange($event)"></pagination>
  </div>
  <pre class="card card-block card-header">Page: {{currentPage}} / {{numberOfPages}}</pre>
در اینجا از کامپوننت pagination مجموعه‌ی ngx-bootstarp استفاده شده‌است و یک سری از خواص مستند شده‌ی آن‌، مقدار دهی شده‌اند؛ مانند متن‌های صفحه‌ی بعد و قبل و امثال آن. مدیریت کلیک بر روی شماره‌های آن، در کامپوننت جاری به صورت ذیل است:
export class ProductsListComponent implements OnInit {
  itemsPerPage = 7;
  currentPage: number;
  numberOfPages: number;

  onPageChange(event: any) {
    this.queryModel.page = event.page;
    this.getPagedProductsList();
  }
}
علت تعریف دو خاصیت اضافه‌ی currentPage و numberOfPages، استفاده‌ی از آن‌ها در قسمت ذیل این شماره‌ها (خارج از کامپوننت نمایش شماره صفحات) جهت نمایش page 1/x است.
هر زمانیکه کاربر بر روی شما‌ره‌ای کلیک می‌کند، رخ‌داد onPageChange فراخوانی شده و در این‌حالت تنها کافی است شماره صفحه‌ی درخواستی queryModel جاری را به روزرسانی کرده و سپس آن‌را در اختیار متد getPagedProductsList جهت دریافت اطلاعات این صفحه‌ی درخواستی قرار دهیم.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
مطالب
اصول طراحی شی‌ء گرا: OO Design Principles - قسمت دوم

اصل چهارم: Starve for loosely coupled designs

"به دنبال طراحی با اتصال سست بین اجزا باش"

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

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

public class StrongCoupledConcreteA
    {
        public string GenerateString(string s) { return s + " from" + this.GetType().ToString(); }
    }

    public class StrongCoupledConcreteB
    {
        public void GenerateString(ref string s) { s += " from" + this.GetType().ToString(); }
    }

    public class Printer
    {
        bool condition;
        public Printer(bool cond)
        {
            condition = cond;
        }

        public void SetCondition(bool value) { condition = value; }

        public void Print()
        {
            string result;
            string input = " this message is";
            if (condition)
            {
                var stringGenerator = new StrongCoupledConcreteA();
                result = stringGenerator.GenerateString(input);
            }
            else
            {
                var stringGenerator = new StrongCoupledConcreteB();
                result = input;
                stringGenerator.GenerateString(ref result);
            }
            Console.WriteLine(result);
        }

    }
    public class Context
    {
        Printer printer;
        public void DoWork()
        {
            printer = new Printer(true);
            printer.Print();

            printer.SetCondition(false);
            printer.Print();
        }

    }

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

public interface IStringGenerator
        {
            string GenerateString(string s);
        }
        public class LooslyCoupledConcreteA : IStringGenerator
        {
            public string GenerateString(string s)
            {
                return s + " from " + this.GetType().ToString();
            }
        }
        public class LooslyCoupledConcreteB : IStringGenerator
        {
            public string GenerateString(string s)
            {
                return s + " from " + this.GetType().ToString();
            }
        }

           public class Printer
           {
               bool condition;
               public Printer(bool cond)
               {
                   condition = cond;
               }

               public void SetCondition(bool value) { condition = value; }

               public void Print()
               {
                   string result;
                   string input = " this message is";
                   IStringGenerator generator;
                   if (condition)
                   {
                       generator = new LooslyCoupledConcreteA();
                   }
                   else
                   {
                       generator = new LooslyCoupledConcreteB();
                   }
                   
                   result = generator.GenerateString(input);
                   Console.WriteLine(result);

               }

           }

با کمی دقت مشاهده میکنیم که در کلاس‌های strongly coupled با اینکه هدف هر دو کلاس تولید یک رشته است، ولی عدم وجود پروتکل باعث شده است نحوه‌ی گرفتن ورودی و برگرداندن خروجی متفاوت شود و در نتیجه نیازمند به اضافه کردن پیچیدگی در کلاس فراخوانی کننده‌ی آن‌ها می‌شویم. این در حالی است که در روش loosely coupled با ایجاد یک پروتکل (واسط IStringGenerator ) این پیچیدگی از بین رفته است. در اینجا نوع اتصال (وابستگی) از جنس اتصال (وابستگی) قوی به تعریف (prototype) و شاید به نوعی نحوه‌ی پیاده سازی متد می‌باشد.


SOLID Principles *

پنج اصل بعدی به اصول SOLID معروف هستند.

S: Single Responsibility

O: Open/Closed

L: Liskov’s Substitution

I: Interface Segregation

D: Dependency Injection


اصل پنجم: Single responsibility

"به دنبال ماژول‌های تک مسئولیتی باش"

در این قسمت مقصود از مسئولیت، «دلیلی است که کلاس باید تغییر کند» بدین معنا که اگر کلاسی با چند دلیل متفاوت مجبور به تغییر شود، آن کلاس چند مسئولیتی است. کلاس‌های چند مسئولیتی عموما کد حجیمی دارند؛ نام آنها تعریف دقیقی را از مسئولیتشان ارائه نمی‌دهد و با عنوانی بسیار کلی نامگذاری میشوند و اشکال زدایی آنها بسیار طاقت فرساست. از طرفی، چند مسئولیتی بودن یک کلاس، باعث از بین رفتن مزایای توارث می‌شود. مثلا فرض کنید دو مسئولیت A,B در واسطی بیان می‌شوند که به یکدیگر مرتبط نبوده و مستقلند. برای  مسئولیت A دو پیاده سازی و برای مسئولیت B،   سه پیاده سازی در نظر گرفته شده است و جمعا برای پشتیبانی از تمامی حالات باید شش کلاس پیاده ساز، در نظر گرفته شود که  توارث را سخت و بی معنی میکند زیرا قابلیت استفاده مجدد را از توارث سلب کرده است. با این وجود عملا رعایت همچین نکته‌ای در دنیای واقعی کار سختی است.

مثال زیر این مشکل را بیان می‌دارد: 

// single responsibility principle - bad example

    interface IEmail
    {
        void SetSender(string sender);
        void SetReceiver(string receiver);
        void SetContent(string content);
    }

    class Email : IEmail
    {
        public void SetSender(string sender)
        {
            throw new NotImplementedException();
        }
        public void SetReceiver(string receiver)
        {
            throw new NotImplementedException();
        }

        public void SetContent(string content)
        {
            throw new NotImplementedException();
        }
    }

در این مثال کلاس Email دارای دو مسئولیت (دلیل برای تغییر) است: الف- نحوه مقداردهی فرستنده و گیرنده براساس پروتکل‌های مختلف مانند IMAP, POP3 ، بدین معنا که با تغییر پروتکل نیاز به تغییر پیاده سازی خواهیم شد. ب- تعریف محتوای پیام، بدین معنا که برای پشتیبانی از محتوای html, xml   نیاز به تغییر کلاس Email داریم.

با تغییر طراحی خواهیم داشت: 

// single responsibility principle - good example
    public interface IMessage
    {
        void SetSender(string sender);
        void SetReceiver(string receiver);
        void SetContent(IContent content);
    }

    public interface IContent
    {
        string GetAsString(); // used for serialization
    }

    public class Email : IMessage
    {        
        public void SetSender(string sender)
        {
            throw new NotImplementedException();
        }

        public void SetReceiver(string receiver)
        {
            throw new NotImplementedException();
        }

        public void SetContent(IContent content)
        {
            throw new NotImplementedException();
        }
    }

در اینجا واسط IContent مسئولیت پشتیبانی از xml, html را خواهد داشت و نیازی به تغییر کلاس Email برای پشتیبانی از این فرمت‌های محتوای پیام را نخواهیم داشت.


اصل ششم: Open for extension, close for modification :  Open/Closed Principle

"پذیرای توسعه و بازدارنده از تغییر هر آنچه که هست، باش"

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

public class AreaCalculator
        {
            public double Area(object[] shapes)
            {
                double area = 0;

                foreach (var shape in shapes)
                {

                    if (shape is Square)
                    {
                        Square square = (Square)shape;
                        area += Math.Sqrt(square.Height);
                    }

                    if (shape is Triangle)
                    {
                        Triangle triangle = (Triangle)shape;
                        double TotalHalf = (triangle.FirstSide + triangle.SecondSide + triangle.ThirdSide) / 2;
                        area += Math.Sqrt(TotalHalf * (TotalHalf - triangle.FirstSide) *
                        (TotalHalf - triangle.SecondSide) * (TotalHalf - triangle.ThirdSide));
                    }

                    if (shape is Circle)
                    {
                        Circle circle = (Circle)shape;
                        area += circle.Radius * circle.Radius * Math.PI;
                    }

                }
                return area;
            }
        }
        public class Square
        {
            public double Height { get; set; }
        }
        public class Circle
        {
            public double Radius { get; set; }
        }
        public class Triangle
        {
            public double FirstSide { get; set; }
            public double SecondSide { get; set; }
            public double ThirdSide { get; set; }
        }

در اینجا کلاس AreaCalculator برای محاسبه مساحت تمام اشیاء ورودی، مساحت تک تک اشیاء را محاسبه میکند و نتیجه را برمی‌گرداند. در این مثال با اضافه شدن شکل هندسی جدید، باید کد این کلاس تغییر کند که با اصل Open/Closed مغایر است. برای بهبود این کد طراحی زیر پیشنهاد شده است:

public class AreaCalculator
{
    public double Area(Shape[] shapes)
    {
        double area = 0;

        foreach (var shape in shapes)
        {
            area += shape.Area();
        }

        return area;
    }
}
public abstract class Shape
{
    public abstract double Area();
}
public class Square : Shape
{
    public double Height { get { return _height; } }
    private double _height;

    public Square(double Height)
    {
        _height = Height;
    }

    public override double Area()
    {
        return Math.Sqrt(_height);
    }
}
public class Circle : Shape
{
    public double Radius { get { return _radius; } }

    private double _radius;

    public Circle(double Radius)
    {
        _radius = Radius;
    }

    public override double Area()
    {
        return _radius * _radius * Math.PI;
    }
}
public class Triangle : Shape
{
    public double FirstSide { get { return _firstSide; } }
    public double SecondSide { get { return _secondSide; } }
    public double ThirdSide { get { return _thirdSide; } }

    private double _firstSide;
    private double _secondSide;
    private double _thirdSide;

    public Triangle(double FirstSide, double SecondSide, double ThirdSide)
    {
        _firstSide = FirstSide;
        _secondSide = SecondSide;
        _thirdSide = ThirdSide;
    }

    public override double Area()
    {
        double TotalHalf = (_firstSide + _secondSide + _thirdSide) / 2;
        return Math.Sqrt(TotalHalf * (TotalHalf - _firstSide) * (TotalHalf - _secondSide) * (TotalHalf - _thirdSide));
    }
}

در این طراحی، پیچیدگی محاسبه مساحت هر شکل به کلاس آن شکل منتقل شده است و با اضافه شدن شکل جدید نیازی به تغییر کلاس AreaCalculator نداریم.

در مقاله‌ی بعدی به سه اصل دیگر اصول SOLID خواهم پرداخت.

مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 1 - NET Core. چیست؟
NET Core. چیست؟

برای اغلب توسعه دهنده‌های دات نت (برنامه‌های وب و دسکتاپ) تنها یک دات نت فریم ورک شناخته شده وجود دارد: The `Full` .NET Framework
که تنها بر روی ویندوز قابل اجرا است و آخرین نگارش پایدار آن در زمان نگارش این مطلب، 4.6.1 است. این فریم ورک بزرگ، از اجزایی تشکیل شده‌است که در تصویر ذیل قابل مشاهده‌اند:


مهم‌ترین قسمت‌های این فریم ورک «بزرگ» شامل مواردی مانند CLR که کار تبدیل کدهای IL را به کدهای ماشین انجام می‌دهد، BCL که کلاس‌های پایه‌ای را جهت کار با  IO، Text و غیره، فراهم می‌کنند، هستند؛ به علاوه کتابخانه‌هایی مانند Windows Forms، WPF و ASP.NET که برفراز BCL و CLR کار می‌کنند.
هرچند تعدادی از توسعه دهنده‌های دات نت تنها با Full framework کار می‌کنند، اما در طی سال‌های اخیر انشعابات بسیار دیگری از آن به وجود آمده‌اند؛ مانند دات نت‌های ویژه‌ی ویندوزهای 8 و Universal Windows Platform، دات نت مخصوص ویندوز فون 8 و ویندوز فون مبتنی بر پلتفرم سیلورلایت، به علاوه دات نت پلتفرم زامارین برای توسعه‌ی برنامه‌های iOS و Android نیز هم اکنون وجود دارند (البته در اینجا Mono، دات نت میکرو و غیره را هم باید ذکر کرد). این فریم ورک‌ها و انشعابات، به همراه پیاده سازی یک سری موارد مشترک و مواردی کاملا اختصاصی هستند که به سایر پلتفرم‌های دیگر قابل انتقال نیستند.

با زیاد شدن تعداد انشعابات دات نت «بزرگ»، نوشتن کدی که قابل اجرای بر روی تمام پلتفرم‌های یاد شده باشد، مشکل شد. اینجا بود که مفهومی را به نام PCL یا Portable class libraries معرفی کردند:


هدف از PCLها، ساده سازی کامپایل و به اشتراک گذاری کد بین پلتفرم‌های مختلف بود و پشتیبانی قابل توجهی هم از آن در VS.NET وجود دارد. هرچند این روش نسبتا موفق بود اما مشکلاتی را هم به همراه داشت. برای مثال با ارائه‌ی یک انشعاب و پلتفرم دیگری از دات نت «بزرگ»، کتابخانه‌ی PCL موجود، باید برای این انشعاب جدید مجددا کامپایل شود. به علاوه در اینجا تنها محدود به انتخاب امکانات مشترک بین پلتفرم‌های مختلف هستید.

برای رفع این مشکلات در پایان سال 2014، یک «دات نت فریم ورک جدید» به نام NET Core. معرفی شد که سورس باز است و همچنین چندسکویی (از ویندوز، لینوکس و OSX پشتیبانی می‌کند).


هرچند پیشتر Windows Store و ASP.NET Core app به صورت پلتفرم‌هایی مجزا ارائه شده بودند، اما اکنون از یک BCL مشترک به نام CoreFX استفاده می‌کنند و نحوه‌ی توزیع آن‌ها صرفا از طریق نیوگت است. به عبارتی اینبار بجای دریافت یک فریم ورک «بزرگ»، تنها اجزایی را دریافت می‌کنید که از طریق نیوگت سفارش داده‌اید.
به این ترتیب نه تنها کار توزیع برنامه‌های مبتنی بر NET Core. با سهولت بیشتری انجام خواهد شد، بلکه به روز رسانی اجزای یک برنامه، تاثیری بر روی سایر برنامه‌ها نخواهد داشت و مشکلات جانبی را به وجود نمی‌آورد. به علاوه دیگر نیازی نیست تا منتظر یک نگارش «بزرگ» دیگر باشید تا بتوانید آخرین به روز رسانی‌ها را دریافت کنید. اینبار به روز رسانی بسته‌های نیوگت برنامه معادل هستند با به روز رسانی کل فریم ورک در نگارش‌های قبلی «بزرگ» آن. در اینجا حتی CoreCLR و NET Native runtime. که مربوط به Windows runtime است هم از طریق نیوگت به روز رسانی می‌شود.

البته NET Core. انتهای مسیر نیست و هم اکنون NETStandard نیز جهت رفع مشکلات کامپایل مجدد PCLها در حال توسعه است و پس از ارائه‌ی آن، PCLها منسوخ شده درنظر گرفته می‌شوند. در این حالت با انتخاب target platform ایی به نام NETStandard (بجای مثلا انتخاب دات نت 4.5 و ویندوز فون 8)، اینبار دات نت 4.5 و ویندوز فون 8 و تمام پلتفرم‌های دیگر، به صورت یکجا انتخاب می‌شوند و اگر پلتفرم جدیدی برای مثال از NETStandard نگارش 1.1 پشتیبانی کند، به این معنا است که کتابخانه‌ی شما هم اکنون با آن سازگار است و دیگر نیازی به کامپایل مجدد آن نخواهد بود.
به علاوه هر برنامه‌ای که بر اساس NETStandard تهیه شود، قابلیت اجرای بر روی NET Core. را نیز خواهد داشت. به عبارتی برنامه‌های NETStandard همان برنامه‌های مبتنی بر NET Core. هستند.


ASP.NET Core چیست؟

در زمان نگارش این مطلب، دو گزینه‌ی برنامه‌های وب ASP.NET Core 1.0 و همچنین Windows Store apps (مبتنی بر NET Native Runtime.) قابلیت استفاده‌ی از این پلتفرم جدید NET Core. دارند.
ASP.NET Core 1.0، که پیشتر با نام ASP.NET 5 معرفی شده بود، بازنویسی کامل ASP.NET است که با ایده‌ی کاملا ماژولار بودن، تهیه شده‌است و از طریق آن، قابلیت به روز رسانی منظم و توزیع آسان از طریق نیوگت، میسر خواهد شد. به علاوه در آن، بسیاری از الگوهای برنامه نویسی شیء‌گرا مانند تزریق وابستگی‌ها، به صورت توکار و از ابتدا پشتیبانی می‌شوند.
ASP.NET Core 1.0 از WebForms ، VB ، WebPages و SignalR پشتیبانی نمی‌کند. البته در این بین عدم پشتیبانی از «وب فرم‌ها» قطعی است؛ اما افزودن سه مورد دیگر یاد شده، جزو لیست کارهای پس از ارائه‌ی نگارش 1 این فریم ورک قرار دارند و به زودی ارائه خواهند شد.


اکنون وضعیت  ASP.NET MVC 5 و ASP.NET Web API 2 چگونه است؟

ASP.NET Core 1.0 مدل برنامه نویسی ASP.NET MVC و Web API را به صورت یکپارچه ارائه می‌دهد و دیگر خبری از ارائه‌ی مجزای این‌ها نخواهد بود و دقیقا بر مبنای مفاهیم برنامه نویسی این دو بنا شده‌است. به صورت خلاصه MVC + Web API + Web Pages = Core MVC 1.0
پیشتر فضای نام System.Web.MVC مخصوص ASP.NET MVC بود و فضای نام مجزای دیگری به نام System.Web.Http مخصوص ASP.NET Web API. اما اکنون تنها یک فضای نام مشترک و یکپارچه به نام Microsoft.AspNet.Mvc هر دوی این‌ها را پوشش می‌دهد.

در این نگارش جدید وابستگی از system.web مبتنی بر IIS حذف شده‌است و با استفاده از هاست جدید چندسکویی به نام Kesterl، به سرعتی 5 برابر سرعت NodeJS دست یافته‌اند.


آخرین تاریخ به روز رسانی ASP.NET MVC 5.x دوشنبه، 20 بهمن 1393 است (با ارائه نگارش 5.2.3 که آخرین نگارش رسمی و پایدار آن است) و آخرین تاریخ به روز رسانی ASP.NET Web API 2.x نیز همان روز است.
هرچند مایکروسافت عادت به اعلام رسمی پایان پشتیبانی از بسیاری از محصولات خود را ندارد اما تمام فناوری‌های «قدیمی» خودش را بر روی CodePlex نگهداری می‌کند و تمام فناوری‌های «جدید» را به GitHub منتقل کرده‌است. بنابراین اگر در مورد فناوری خاصی به Codeplex رسیدید، یعنی «دیگر ادامه‌ی رسمی نخواهد یافت» و حداکثر در حد رفع یک سری باگ‌ها و مشکلات گزارش شده باقی می‌مانند.
مثال 1: هم اکنون نگارش دوم ASP.NET Identity را بر روی Codeplex می‌توانید مشاهده کنید. نگارش سوم آن به GitHub منتقل شد‌ه‌است که این نگارش صرفا با ASP.NET Core 1.0 سازگار است. در مورد ASP.NET MVC و Web API نیز چنین حالتی رخ داده‌است. نگارش‌های 5 و 2 آن‌ها بر روی Codeplex موجود هستند و نگارش ششم که به ASP.NET Core 1.0 تغییر نام یافت و ترکیبی است از MVC و Web API، در GitHub توسعه می‌یابد.
مثال 2: WCF به علت پیچیدگی بیش از حد و مدرن نبودن طراحی آن، رقابت را به ASP.NET Web API 2.x واگذار کرد و مدل برنامه نویسی ASP.NET Web API 2.x نیز هم اکنون جزئی از ASP.NET Core 1.0 است. بنابراین اگر قصد ایجاد پروژه‌ی جدیدی را بر این مبنا دارید، بهتر است با APS.NET Core 1.0 کار را شروع کنید.


اما هنوز تعداد زیادی از کتابخانه‌های Full framework به NET Core. انتقال پیدا نکرده‌اند

برای نمونه هنوز EF Core 1.0 که پیشتر نام EF 7.x به آن داده شده بود، به مرحله‌ی نهایی تکمیل قابلیت‌های آن نرسیده‌است. اما باید دانست که ASP.NET Core 1.0 صرفا بر فراز NET Core. قابل اجرا نیست؛ بلکه قابلیت اجرای بر فراز NET 4.6. و یا همان دات نت «بزرگ و کامل» را نیز دارد. بنابراین به سادگی قابلیت اجرای EF 6.x و یا NHibernate را نیز دارا است. تنها مزیتی را که در اینجا از دست خواهید، قابلیت چندسکویی بودن ASP.NET Core 1.0 است؛ زیرا EF 6.x با چنین دیدی طراحی نشده‌است.



همانطور که ملاحظه می‌کنید، ASP.NET Core 1.0 قابلیت اجرای بر روی هر دوی NET Core 1.0. و NET 4.6. را دارا است. اما یکی، چندسکویی است و دیگری صرفا مختص به ویندوز.


فناورهای منسوخ شده‌ی در NET Core.

یکسری از فناوری‌ها و کتابخانه‌ها احتمالا هیچگاه قابلیت انتقال به NET Core. را نخواهند یافت و یا حداقل باید تا چندنگارش بعدی آن صبر کنند. فناوری‌های خاتمه یافته‌ی با NET Core. به شرح زیر هستند:
- Reflection: همانطور که عنوان شد، NET Core. بر فراز CoreCLR و همچنین NET Native runtime. اجرا می‌شود و تولید برنامه‌های native و static linking آن‌ها مانند برنامه‌های ++C، نیاز به دانستن اجزایی دارد که به صورت پویا فراخوانی نمی‌شوند و بلافاصله و در زمان کامپایل، توسط کامپایلر قابل تشخیص هستند. همین محدودیت سبب شده‌است که استفاده‌ی از Reflection در NET Core. به حداقل ممکن آن برسد. برای مثال در System.Object متد GetType آن تنها نام نوع را باز می‌گرداند و نه اطلاعات بیشتری را مانند  GetMembers سابق.
- App Domains: هرچند CoreCLR از App Domains پشتیبانی می‌کند اما NET Native runtime. خیر. به همین جهت برای ایزوله سازی برنامه‌ها توصیه شده‌است که از containerهایی مانند docker استفاده شود.
- Remoting: پیش از WCF جهت برقراری ارتباط بین برنامه‌ها مطرح شده بود و هم اکنون در دات نت کامل هم آنچنان استفاده‌ای از آن نمی‌شود.
- binary serialization: البته کتابخانه‌هایی مانند JSON.NET و امثال آن، نگارش NET Core. هم دارند؛ اما چون binary serialization نیاز به اطلاعات reflection قابل توجهی دارد دیگر پشتیبانی نخواهد شد.


فناور‌هایی که به زودی به NET Core. منتقل می‌شوند

یکسری از فناوری‌ها مانند XAML هنوز معادل NET Core. ندارند و لیست زیر قرار است از طرف مایکروسافت سورس باز شده و همچنین به NET Core. منتقل شود:
System.Data
System.DirectoryServices
System.Drawing
System.Transactions
System.Xml.Xsl and System.Xml.Schema
System.Net.Mail
System.IO.Ports
System.Workflow
System.Xaml


مراحل نصب ASP.NET Core 1.0

پیش از نصب نگارش 1.0 RTM باید به این نکته دقت داشت که نصاب آن، نگارش‌های آزمایشی قبلی را حذف و یا بازنویسی نمی‌کند و همین مساله ممکن است سبب بروز تداخل‌هایی و یا حتی از کار افتادن VS.NET شما شود. بنابراین اگر نگارش‌های RC یا بتا را پیشتر نصب کرده‌اید، به Add remove programs ویندوز مراجعه کرده و سه مورد ذیل را حتما حذف کنید (خیلی مهم):
- Preview Tooling (all versions)
- NET Core Runtime SDK (all versions).
- NET Core Runtime (all Versions).
پس از حذف بسته‌های قدیمی، برای نصب نگارش 1.0 RTM، ابتدا نیاز است Visual Studio 2015 Update 3 را نصب کنید و پس از آن با استفاده از NET Core for Visual Studio Official MSI Installer. کار نصب اجزای مورد نیاز آن انجام خواهد شد.


بررسی شماره نگارش 1.0 RTM

پس از نصب اجزای عنوان شده، خط فرمان را گشوده و دستور ذیل را صادر کنید:
 C:\Users\Vahid>dotnet --version
1.0.0-preview2-003121
همانطور که مشاهده می‌کنید، نگارش ذکر شده هنوز در مرحله‌ی preview است و صرفا مرتبط است به tooling و یا ابزارهای مرتبط با آن.
اگر یک پروژه‌ی خالی ASP.NET Core Web Application را نیز شروع کنید (با طی مراحل زیر جهت ایجاد یک پروژه‌ی جدید):
 .NET Core -> ASP.NET Core Web Application (.NET Core) -> Select `Empty` Template


در اینجا فایل جدیدی را به نام global.json مشاهده می‌کنید که محتوایات آن شامل دقیقا همین شماره نگارش است؛ به همراه معرفی پوشه‌های اصلی پروژه:
{
  "projects": [ "src", "test" ],
  "sdk": {
    "version": "1.0.0-preview2-003121"
  }
}
مطالب
بررسی اینترفیس ICommand در WPF
مدتی هست که مشغول مطالعه و یادگیری WPF از طریق مطالب سایت هستم؛ به همین خاطر تصمیم گرفتم مطلبی را حول محور اینترفیس ICommnad  گردآوری کنم و در اختیار کاربران سایت قرار دهم.

سرفصل‌های این مطلب :
• Command چیست
• اینترفیس ICommand چیست 
• چرا اینترفیس ICommand
• ایجاد UI مورد نیاز 
• چگونگی استفاده از ICommand 
• استفاده از INotifyPropertyChanged

Command چیست ؟
در برنامه نویسی WPF به هر کلاسی که اینترفیس ICommand را پیاده سازی کند، اصطلاحا Commnad گوییم. تفاوت کوچکی بین یک Event و Command وجود دارد. رخداد‌ها برای کنترل‌های UI ساخته و تخصیص داده می‌شوند؛ اما Command‌ها انتزاعی‌تر هستند و تمرکز آنها بر روی نحوه‌ی انجام کارها می‌باشد.
برای تعاملات در برنامه‌ها از Commandها و Event‌ها استفاده می‌کنیم. ما در WPF دو روش برای تعامل با UI داریم:
1- استفاده از Event‌ها و نوشتن کد‌های مورد نیاز در بخش CodeBehind (رعایت نکردن الگوی MVVM).
WPF تعداد زیادی RoutedEvent پیش ساخته (Built In) دارد که از آنها می‌توان در این روش استفاده کرد. 
2- استفاده از Command و درگیر کردن کدهای اجرایی نوشته شده در ViewModel با استفاده از Command ها.
در زمان استفاده از الگوی MVVM مجاز به نوشتن کد در بخش CodeBehind نیستیم. بنابراین از سیستم رخداد‌های طراحی شده‌ی در WPF که RoutedEvent می‌باشد، نمی‌توان استفاده کرد. به این خاطر که رخداد‌ها در ViewModel قابل دسترسی نمی‌باشد.

اینترفیس ICommand:
این اینترفیس سه عضو دارد که آن‌ها را در جدول زیر مشاهده می‌کنید:

نام عضو

توضیحات

Bool CanExecute(object parameter)

این تابع پارامتری از نوع object را دریافت می‌کند و یک مقدار bool را به خروجی تابع می‌فرستد. اگر مقدار خروجی متد، true  باشدcommand  اجرا خواهد شد و در غیر اینصورت اتفاقی رخ نخواهد داد. اغلب ازDelegate  ها به عنوان پارامتر این تابع استفاده می‌شود؛Delegate های پیش ساخته‌ای همچون Action,Predicate,Func

Event EventHandler CanExecuteChanged

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

Void Execute(Object parameter)

این متد کار اصلی را در Command‌ها انجام می‌دهد. این متد تنها زمانی اجرا می‌شود که متدCanExecute  مقدار true را بازگرداند. این تابع پارامتری را از نوع object دریافت می‌کند، اما عموما ما یکDelegate  را به آن ارسال می‌کنیم. Delegate ارجاعی را به متدی، در خود نگاه می‌دارد که انتظار داریم در صورت اجرایcommand ، اجرا شود.


چرا اینترفیس ICommand :
هسته‌ی اصلی Command‌ها در WPF، اینترفیس ICommand می‌باشد. این اینترفیس به‌صورت گسترده‌ای در الگوی MVVM و Prism  استفاده شده است و استفاده‌ی از آن محدود به MVVM نمی‌باشد. تعداد زیادی Commnad بصورت پیش ساخته وجود دارند که این اینترفیس را پیاده سازی کرده‌اند .کلاس پایه RoutedCommand اینترفیس ICommand را پیاده سازی کرده است و WPF فرمان‌های زیر را فراهم کرده است:
• MediaCommnads
• ApplicationCommnads
• NavigationCommands
• ComponentCommnads
• EditingCommnads
که همگی از کلاس RoutedCommand استفاده کرده‌اند.
در این مطلب به دنبال ایجاد برنامه‌ای هستیم که حاصل جمع مفدار دو Textbox را پس از فشردن کلید جمع در یک textblock نمایش دهد.

ساخت UI مورد نیاز :
گام اول : 
با اجرای ویژوال استودیو، برنامه‌ای را با نام ICommnadSample ایجاد کنید. ساختار پروژه به شکل زیر است:
همانطور که می‌بینید View و ViewModel و در نهایت Command‌ها در پوشه‌های مجزایی ساماندهی شده‌اند.

گام دوم:
ابتدا قالب گرافیکی را مشخص می‌کنیم. در پوشه‌ی Views یک UserControl را به نام CalculatorView.xaml ایجاد کرده و کد زیر را در آن نوشتیم:
<Grid DataContext="{Binding Source={StaticResource calculatorVM}}" Background="#FFCCCC">
        <Grid.RowDefinitions>
            <RowDefinition Height="80"/>
            <RowDefinition/>
            <RowDefinition Height="80"/>
            <RowDefinition Height="44"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="4" FontSize="25"
               VerticalAlignment="Top" HorizontalAlignment="Center" Foreground="Blue" Content="ICommand Sample"/>
        <Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" 
               Margin="10,0,0,0" VerticalAlignment="Bottom" FontSize="20"  Content="First Input"/>
        <Label Grid.Row="0" Grid.Column="2" Grid.ColumnSpan="2" 
               Margin="10,0,0,0" VerticalAlignment="Bottom" FontSize="20"  Content="Second Input"/>

        <TextBox Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Margin="10,0,0,0" FontSize="20" 
                 HorizontalAlignment="Left" Height="30"  Width="150" TextAlignment="Center" Text="{Binding FirstValue, Mode=TwoWay}"/>
        <TextBox Grid.Row="1" Grid.Column="2" Grid.ColumnSpan="2" Margin="10,0,0,0" FontSize="20"
                 HorizontalAlignment="Left"  Height="30" Width="150" TextAlignment="Center" Text="{Binding SecondValue, Mode=TwoWay}"/>

        <Rectangle Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="4" Fill="LightBlue"/>
        <Button Grid.Row="2" Grid.Column="0" Content="+"  Margin="10,0,0,0" HorizontalAlignment="Left" Height="50" Width="50" FontSize="30" Command="{Binding AddCommand}"/>
        
        <Label Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2" FontSize="25" Margin="10,0,0,0" HorizontalAlignment="Left" Height="50"  Content="Result : "/>
        <TextBlock Grid.Row="3" Grid.Column="2" Grid.ColumnSpan="2" FontSize="20" Margin="10,0,0,0" Background="BlanchedAlmond"
                   TextAlignment="Center"  HorizontalAlignment="Left" Height="36" Width="150" Text="{Binding Output}"/>
    </Grid>
برای استفاده از این UserControl در پنجره‌ی اصلی برنامه (فایل MainWindows.Xaml) به شکل زیر عمل می‌کنیم:
ابتدا فضای نام View را به فایل MainWindows.xaml اضافه می‌کنیم :
   xmlns:myview="clr-namespace:ICommnadSample.Views"
ایجاد تگ برای استفاده از View تولید شده در  Grid اصلی برنامه :
<Grid>
        <myview:CalculatorView/>
</Grid>

گام سوم :
همانطور که مشاهده می‌کنید، کنترل‌هایی که در عملیات انقیاد داده‌ها (DataBinding) شرکت می‌کنند، از طریق خاصیت Binding و معرفی خصوصیت مورد نظر مشخص می‌شوند.
برای این منظور در پوشه‌ی ViewModels و به کلاس CalculatorViewModel سه خصوصیت به‌همراه فیلد خصوصی، به شکل زیر اضافه می‌کنیم:
private double firstValue; 
private double secondValue;
private double output;
public double FirstValue
        {
            get { return firstValue; }
            set
            {
                firstValue = value;
                OnPropertyChanged("FirstValue");
            }
        }
        public double SecondValue
        {
            get { return secondValue; }
            set
            {
                secondValue = value;
                OnPropertyChanged("SecondValue");
            }
        }
        public double Output
        {
            get { return output; }
            set
            {
                output = value;
                OnPropertyChanged("Output");
            }
        }

چگونگی استفاده‌ی از اینترفیس ICommand :
برای ایجاد ارتباط بین Command ‌ها و UI می‌بایست اینترفیس ICommand توسط کلاس مورد نظر ما پیاده سازی شود. در ابتدا کلاسی با عنوان PlusCommnad  ایجاد و از اینترفیس مورد نظر ارث بری می‌کنیم.
هدف ما شبیه سازی رویداد کلیک دکمه‌ی موجود در صفحه با استفاده از Command می‌باشد.

گام چهارم:
پس از پیاده سازی اینترفیس لازم است تا کمی تغییر در بدنه متد‌ها ایجاد کنیم:
public class PlusCommand : ICommand
{
    private CalculatorViewModel calculatorViewModel;
    public PlusCommand(CalculatorViewModel vm)
    {
        calculatorViewModel = vm;
    }
    public bool CanExecute(object parameter)
    {
        return true;
    }
    public void Execute(object parameter)
    {
        calculatorViewModel.Add();
    }
    public event EventHandler CanExecuteChanged;
}
همانطور که می‌بینید در ابتدا فیلدی از جنس کلاس CalculatorViewModel ایجاد و از طریق سازنده‌ی کلاس آن را مقدار دهی کردیم (قصد داریم نمونه‌ای از ViewModel مورد نظر را به این کلاس ارسال کنیم).
در ادامه بصورت دستی (Hard Code) مقدار بازگردانده شده را برای تابع CanExecute به مقدار true  تغییر دادیم و متد تابع Execute را به شکلی تغییر دادیم تا تابع Add را در CalculatorViewModel، اجرا کند.

گام پنجم:
از کلاس PlusCommand در CalculatorViewModel، یک شیء ساخته و در سازنده‌ی CalculatorViewModel آن را مقدار دهی می‌کنیم: 
        private PlusCommand plusCommand;
        public CalculatorViewModel()
        {
            plusCommand = new PlusCommand(this);
        }

گام ششم:
در فایل CalculatorView ارجاعی را به فضای نام کلاس CalculatorViewModel ایجاد می‌کنیم :
   xmlns:vm="clr-namespace:ICommnadSample.ViewModels"
پس از اضافه کردن فضای نام، از تگ UserControl.Resource برای رجیستر کردن CalculatorViewModel با کلید calculatorVM جهت مشخص کردن منبع داده استفاده کردیم.
 <UserControl.Resources>
        <vm:CalculatorViewModel x:Key="calculatorVM" />
    </UserControl.Resources>

گام هفتم:
اضافه کردن تابع Add در CalculatorViewModel برای عملیات جمع :
public void Add()
{
   Output = firstValue + secondValue;
}

گام هشتم:
تعریف یک Command  برای عملیات جمع به نام AddCommand. این همان خصوصیتی است که باید بجای رویداد کلیک دکمه از طریق خاصیت Command موجود در کنترل و ویژگی Binding به کنترل متصل شود.
  public ICommand AddCommand {
            get
            {
                return plusCommand;
            }
        }

نحوه‌ی استفاده:
   Command="{Binding AddCommand}"
 
گام نهم :
برای تکمیل عملیات انقیاد داده‌ها، کلاسی به نام ViewModelBase تعریف شده است. این کلاس از اینترفیس INotifyPropertyChange ارث بری کرده و اعضای این کلاس را پیاده سازی کرده است.
  public class ViewModelBase:INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
پیاده سازی این اینترفیس سبب می‌شود تا کلاس‌های ViewModel ایی که احتیاج به این اینترفیس برای عملیات انقیاد داده‌ها دارند، تنها با ارث بری، از این ظرفیت استفاده کنند و نیازی به پیاده سازی این اینترفیس در هر کلاس نباشد.

گام دهم:
ارث بری کلاس CalculatorViewModel از کلاس ViewModelBase:
   public class CalculatorViewModel : ViewModelBase
در این مرحله هر کنترلی را که قصد داریم با تغییر منبع داده بروز شود و یا با تغییر وضعیتش منبع داده تغییر کند، اعلام می‌کنیم:
   OnPropertyChanged("FirstValue");
برای هر سه خصوصیت ViewModel کد زیر را در بلاک Set تکرار می‌کنیم (توجه شود که پارامتر ارسالی باید نام پراپرتی مورد نظر باشد)
کد کامل کلاس CalculatorViewModel به شکل زیر است:
public class CalculatorViewModel : ViewModelBase
    {
        private double firstValue;
        private double secondValue;
        private double output;
        private PlusCommand plusCommand;
        public CalculatorViewModel()
        {
            plusCommand = new PlusCommand(this);
        }

        public double FirstValue
        {
            get { return firstValue; }

            set
            {
                firstValue = value;
                OnPropertyChanged("FirstValue");
            }
        }
        public double SecondValue
        {
            get { return secondValue; }
            set
            {
                secondValue = value;
                OnPropertyChanged("SecondValue");
            }
        }
        public double Output
        {

            get { return output; }

            set
            {
                output = value;
                OnPropertyChanged("Output");
            }
        }


        public ICommand AddCommand
        {
            get
            {
                return plusCommand;
            }
        }

        internal void Add()
        {
            Output = firstValue + secondValue;
        }
    }


حال می‌توانید برنامه را اجرا و تست کنید:



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

مطالب
رشته ها و پردازش متن در دات نت به زبان ساده
رشته، مجموعه‌ای از کاراکترهاست که پشت سرهم، در مکانی از حافظه قرار گرفته‌اند. هر کاراکتر حاوی یک شماره سریال در جدول یونیکد هست. به طور پیش فرض دات نت برای هر کاراکتر (نوع داده char) شانزده بیت در نظر گرفته است که برای 65536 کاراکتر کافی است.
برای نگهداری از رشته‌ها و انجام عملیات بر روی آنها در دات نت از نوع system.string استفاده می‌کنیم:
string greeting = "Hello, C#";

که در این حالت مجموعه‌ای از کاراکترها را ایجاد خواهد کرد:

اتفاقاتی که در داخل کلاس string رخ می‌دهد بسیار ساده است و ما را از تعریف []char بی‌نیاز می‌کند تا مجبور نشویم خانه‌های  آرایه را به ترتیب پر کنیم. از معایب استفاده از آرایه char میتوان موارد زیر را برشمارد:
  1. خانه‌های آن یک ضرب پر نمیشوند بلکه به ترتیب، خانه به خانه پر می‌شوند.
  2. قبل از انتساب متن باید باید از طول متن مطمئن شویم تا بتوانیم تعداد خانه‌ها را بر اساس آن ایجاد کنیم.
  3. همه عملیات آرایه‌ها از پر کردن ابتدای کار گرفته تا هر عملی، نیاز است به صورت دستی صورت بگیرد و تعداد خطوط کد برای هر کاری هم بالا می‌رود.
البته استفاده از string هم راه حل نهایی برای کار با متون نیست. در انتهای این مطلب مورد دیگری را نیز بررسی خواهیم کرد. از ویژگی دیگر رشته‌ها این است که آن‌ها شباهت زیادی به آرایه‌ای از کاراکتر‌ها دارند؛ ولی اصلا شبیه آن‌ها نیستند و نمی‌توانید به صورت یک آرایه آن‌ها را مقداردهی کنید. البته کلاس string امکاناتی را با استفاده از indexer [] مهیا کرده است که میتوانید بر اساس اندیس‌ها به کاراکترها به صورت جداگانه دسترسی داشته باشید ولی نمی‌توانید آن‌ها را مقدار دهی کنید. این اندیس‌ها از 0 تا طول آن length-1 ادامه دارند.
string str = "abcde";
char ch = str[1]; // ch == 'b'
str[1] = 'a'; // Compilation error!
ch = str[50]; // IndexOutOfRangeException
همانطور که میدانیم برای مقداردهی رشته‌ها از علامت‌های نقل قول "" استفاده میکنیم که باعث میشود اگر بخواهیم علامت " را در رشته‌ها داشته باشیم نتوانیم. برای حل این مشکل از علامت \ استفاده میکنیم که البته باعث استفاده از بعضی کاراکترهای خاص دیگر هم می‌شود:
string a="Hello \"C#\"";
string b="Hello \r\n C#"; //مساوی با اینتر
string c="C:\\a.jpg"; //چاپ خود علامت  \ -مسیردهی
البته اگر از علامت @ در قبل از رشته استفاده شود علامت \ بی اثر خواهد شد.
string c=@"C:\a.jpg";// == "C:\\a.jpg"

مقداردهی رشته‌ها و پایدار (تغییر ناپذیر) بودن آنها Immutable
رشته‌ها ساختاری پایدار هستند؛ به این معنی که به صورت reference مقداردهی می‌شوند. موقعی که شما مقداری را به یک رشته انتساب می‌دهید، مقدار متغیر در  String pool یا لینک در Heap ذخیره می‌شوند و اگر همین متغیر را به یک متغیر دیگر انتساب دهیم، متغیر جدید مقدار آن را دیگر در حافظه پویا (داینامیک) Heap به عنوان مقدار جدید ذخیره نخواهد کرد؛ بلکه تنها یک pointer خواهد بود که به آدرس حافظه متغیر اولی اشاره می‌کند. به مثال زیر دقت کنید. متغیر source مقدار some source را ذخیره می‌کند و بعد همین متغیر، به متغیر assigned انتساب داده میشود؛ ولی مقداری جابجا نمی‌شود. بلکه متغیر assign به آدرسی در حافظه اشاره می‌کند که متغیر source اشاره می‌کند. هرگاه که در یکی از متغیرها، تغییری رخ دهد، همان متغیری که تغییر کرده است، به آدرس جدید با محتوای تغییر داده شده اشاره می‌کند.
string source = "Some source";
string assigned = source;

این ویژگی نوع reference فقط برای ساختارهای Immutable به معنی پایدار رخ می‌دهد و نه برای ساختار‌های ناپایدار (تغییر پذیر)  mutable؛ به این خاطر که آن‌ها مقادیرشان را مستقیما تغییر میدهند و اشاره‌ای در حافظه صورت نمی‌گیرد. 
string hel = "Hel";
string hello = "Hello";
string copy = hel + "lo";

string hello = "Hello";
string same = "Hello";

برای اطلاعات بیشتر در این زمینه این لینک را مطالعه نمایید.


مقایسه رشته‌ها
برای مقایسه دو رشته میتوان از علامت == یا از متد Equals استفاده نماییم. در این حالت به خاطر اینکه کد حروف کوچک و بزرگ متفاوت است، مقایسه حروف هم متفاوت خواهد بود. برای اینکه حروف کوچک و بزرگ تاثیری بر مقایسه ما نگذارند و #c را با #C برابر بدانند باید از متد Equals به شکل زیر استفاده کنیم:
Console.WriteLine(word1.Equals(word2,
    StringComparison.CurrentCultureIgnoreCase));
برای اینکه بزرگی و کوچکی اعداد را مشخص کنیم از علامت‌های < و > استفاده میکنیم ولی برای رشته‌ها از متد CompareTo بهره می‌بریم که چینش قرارگیری آن‌ها را بر اساس حروف الفبا مقایسه می‌کند و سه عدد، می‌تواند خروجی آن باشند. اگر 0 باشد یعنی برابر هستند، اگر -1 باشد رشته اولی قبل از رشته دومی است و اگر 1 باشد رشته دومی قبل از رشته اولی است.
string score = "sCore";
string scary = "scary";
 
Console.WriteLine(score.CompareTo(scary));
Console.WriteLine(scary.CompareTo(score));
Console.WriteLine(scary.CompareTo(scary));
 
// Console output:
// 1
// -1
// 0
 اینبار هم برای اینکه حروف کوچک و بزرگ، دخالتی در کار نداشته باشند، میتوانید از داده شمارشی StringComparison در متد ایستای (string.Compare(s1,s2,StringComparison استفاده نمایید؛ یا از نوع داده‌ای boolean برای تعیین نوع مقایسه استفاده کنید.
string alpha = "alpha";
string score1 = "sCorE";
string score2 = "score";
 
Console.WriteLine(string.Compare(alpha, score1, false));
Console.WriteLine(string.Compare(score1, score2, false));
Console.WriteLine(string.Compare(score1, score2, true));
Console.WriteLine(string.Compare(score1, score2,
    StringComparison.CurrentCultureIgnoreCase));
// Console output:
// -1
// 1
// 0
// 0
نکته : برای مقایسه برابری  دو رشته از متد Equals یا == استفاده کنید و فقط برای تعیین کوچک یا بزرگ بودن از compare‌ها استفاده نمایید. دلیل آن هم این است که برای مقایسه از فرهنگ culture فعلی سیستم استفاده میشود و نظم جدول یونیکد را رعایت نمی‌کنند و ممکن است بعضی رشته‌های نابرابر با یکدیگر برابر باشند. برای مثال در زبان آلمانی دو رشته "SS" و "ß " با یکدیگر برابر هستند.

عبارات با قاعده Regular Expression
این عبارات الگوهایی هستند که قرار است عبارات مشابه الگویی را در رشته‌ها پیدا کنند. برای مثال الگوی +[A-Z0-9] مشخص می‌کند که رشته مورد نظر نباید خالی باشد و حداقل با یکی از حروف بزرگ یا اعداد پرشده باشد. این الگوها میتوانند برای واکشی داده‌ها یا قالب‌های خاص در رشته‌ها به کار بروند. برای مثال شماره تماس‌ها ، پست الکترونیکی و ...
در اینجا میتواند نحوه‌ی الگوسازی را بیاموزید. کد زیر بر اساس یک الگو، شماره تماس‌های مورد نظر را یافته و البته با فیلتر گذاری آن‌ها را نمایش می‌دهد:
string doc = "Smith's number: 0898880022\nFranky can be " +
    "found at 0888445566.\nSteven's mobile number: 0887654321";
string replacedDoc = Regex.Replace(
    doc, "(08)[0-9]{8}", "$1********");
Console.WriteLine(replacedDoc);
// Console output:
// Smith's number: 08********
// Franky can be found at 08********.
// Steven' mobile number: 08********
سه شماره تماس در رشته‌ی بالا با الگوی ما همخوانی دارند که بعد با استفاده از متد replace در شی Regex عبارات دلخواه خودمان را جایگزین شماره تماس‌ها خواهیم کرد. الگوی بالا شماره تماس‌هایی را میابد که با 08 آغاز شده‌اند و بعد از آن 8 عدد دیگر از 0 تا 9 قرار گرفته‌اند. بعد از اینکه متن مطابق الگو یافت شد، ما آن را با الگوی ********1$ جایگزین می‌کنیم که علامت $ یک placeholder برای یک گروه است. هر عبارت () در عبارات با قاعده یک گروه حساب میشود و اولین پرانتر 1$ و دومین پرانتز یا گروه میشود 2$ که در عبارت بالا (08) میشود 1$ و به جای مابقی الگو، 8 علامت ستاره نمایش داده میشود.

اتصال رشته‌ها در Loop
برای اتصال رشته‌ها ما از علامت + یا متد ایستای string.concat استفاده می‌کنیم ولی استفاده‌ی از آن در داخل یک حلقه باعث کاهش کارآیی برنامه خواهد شد. برای همین بیایید ببینم در حین اتتقال رشته‌ها در حافظه چه اتفاقی رخ میدهد. ما در اینجا دو رشته str1 و str2 داریم که عبارات "super" و "star" را نگه داری می‌کنند و در واقع دو متغیر هستند که به حافظه‌ی پویای Heap اشاره می‌کنند. اگر این دو را با هم جمع کنیم و نتیجه را در متغیر result قرار دهیم، سه متغیر میشوند که هر کدام به حافظه‌ای جداگانه در heap اشاره می‌کنند. در واقع برای این اتصال، قسمت جدیدی از حافظه تخصصیص داده شده و مقدار جدید در آن نشسته‌است. در این حالت یک متغیر جدید ساخته شد که به آدرس آن اشاره می‌کند. کل این فرآیند یک فرآیند کاملا زمانبر است که با تکرار این عمل موجب از دست دادن کارآیی برنامه می‌شود؛ به خصوص اگر در یک حلقه این کار صورت بگیرد.
سیستم دات نت همانطور که میدانید شامل GC یا سیستم خودکار پاکسازی حافظه است که برنامه نویس را از dispose کردن بسیاری از اشیاء بی نیاز می‌کند. موقعی‌که متغیری به قسمتی از حافظه اشاره می‌کند که دیگر بلا استفاده است، سیستم GC به صورت خودکار آنها را پاکسازی می‌کند که این عمل زمان بر هم خودش موجب کاهش کارآیی می‌شود. همچنین انتقال رشته‌ها از یک مکان حافظه به مکانی دیگر، باز خودش یک فرآیند زمانبر است؛ به خصوص اگر رشته مورد نظر طولانی هم باشد.
مثال عملی: در تکه کد زیر قصد داریم اعداد 1 تا 20000 را در یک رشته الحاق کنیم:
 DateTime dt = DateTime.Now;
            string s = "";
        for (int index = 1; index <= 20000; index++)
        {
            s += index.ToString();
        }
            Console.WriteLine(s);
            Console.WriteLine(dt);
            Console.WriteLine(DateTime.Now);
            Console.ReadKey();
کد بالا تاز زمان نمایش کامل، بسته به قدرت سیستم ممکن است یکی دو ثانیه طول بکشد. حالا عدد را به 200000 تغییر دهید (یک صفر اضافه تر). برنامه را اجرا کنید و مجددا تست بزنید. در این حالت چند دقیقه ای بسته به قدرت سیستم زمان خواهد برد؛ مثلا دو دقیقه یا سه دقیقه یا کمتر و بیشتر.
عملیاتی که در حافظه صورت میگیرد این چند گام را طی میکند:
  • قسمتی از حافظه به طور موقت برای این دور جدید حلقه، گرفته میشود که به آن بافر میگوییم.
  • رشته قبلی به بافر انتقال میابد که بسته به مقدار آن زمان بر و کند است؛ 5 کیلو یا 5 مگابایت یا 50 مگابایت و ...
  • شماره تولید شده جدید به بافر چسبانده میشود.
  • بافر به یک رشته تبدیل میشود وجایی برای خود در حافظه Heap میگیرد.
  • حافظه رشته قدیمی و بافر دیگر بلا استفاده شده‌اند و توسط GC پاکسازی میشوند که ممکن است عملیاتی زمان بر باشد.

String Builder
این کلاس ناپایدار و تغییر پذیر است. به کد و شکل زیر دقت کنید:
string declared = "Intern pool";
string built = new StringBuilder("Intern pool").ToString();

این کلاس دیگر مشکل الحاق رشته‌ها یا دیگر عملیات پردازشی را ندارد. بیایید مثال قبل را برای این کلاس هم بررسی نماییم:
 StringBuilder sb = new StringBuilder();
      sb.Append("Numbers: ");

            DateTime dt = DateTime.Now;
        for (int index = 1; index <= 200000; index++)
        {
            sb.Append(index);
        }
            Console.WriteLine(sb.ToString());
            Console.WriteLine(dt);
            Console.WriteLine(DateTime.Now);
            Console.ReadKey();
اکنون همین عملیات چند دقیقه‌ای قبل، در زمانی کمتر، مثلا دو ثانیه انجام میشود.
حال این سوال پیش می‌آید مگر کلاس stringbuilder چه میکند که زمان پردازش آن قدر کوتاه است؟
همانطور که گفتیم این کلاس mutable یا تغییر پذیر است و برای انجام عملیات‌های ویرایشی نیازی به ایجاد شیء جدید در حافظه ندارد؛ در نتیجه باعث کاهش انتقال غیرضروری داده‌ها برای عملیات پایه‌ای چون الحاق رشته‌ها میگردد.
stringbuilder شامل یک بافر با ظرفیتی مشخص است (به طور پیش فرض 16 کاراکتر). این کلاس آرایه‌هایی از کاراکترها را پیاده سازی میکند که برای عملیات و پردازش‌هایش  از یک رابط کاربرپسند برای برنامه نویسان استفاده می‌کند. اگر تعداد کاراکترها کمتر از 16 باشد مثلا 5 ، فقط 5 خانه آرایه استفاده میشود و مابقی خانه‌ها خالی میماند و با اضافه شدن یک کاراکتر جدید، دیگر شیء جدیدی در حافظه درست نمی‌شود؛ بلکه در خانه ششم قرار می‌گیرد و اگر تعداد کاراکترهایی که اضافه می‌شوند باعث شود از 16 کاراکتر رد شود، مقدار خانه‌ها دو برابر میشوند؛ هر چند این عملیات دو برابر شدن resizing عملیاتی کند است ولی این اتفاق به ندرت رخ می‌دهد.
کد زیر یک آرایه 15 کاراکتری ایجاد می‌کند و عبارت #Hello C را در آن قرار می‌دهد.
StringBuilder sb = new StringBuilder(15);
sb.Append("Hello, C#!");

در شکل بالا خانه هایی خالی مانده است Unused و  جا برای کاراکترهای جدید به اندازه خانه‌های unused هست و اگر بیشتر شود همانطور که گفتیم تعداد خانه‌ها 2 برابر می‌شوند که در اینجا میشود 30.

استفاده از متد ایستای string.Format
از این متد برای نوشتن یک متن به صورت قالب و سپس جایگزینی مقادیر استفاده می‌شود:
DateTime date = DateTime.Now;
string name = "David Scott";
string task = "Introduction to C# book";
string location = "his office";
 
string formattedText = String.Format(
    "Today is {0:MM/dd/yyyy} and {1} is working on {2} in {3}.",
    date, name, task, location);
Console.WriteLine(formattedText);
در کد بالا ابتدا ساختار قرار گرفتن تاریخ را بر اساس الگو بین {} مشخص می‌کنیم و متغیر date در آن قرار می‌گیرد و سپس برای {1},{2},{3} به ترتیب قرار گیری آن‌ها متغیرهای name,last,location قرار میگیرند.
از ()ToString. هم می‌توان برای فرمت بندی خروجی استفاده کرد؛ مثل همین عبارت MM/dd/yyyy در خروجی نوع داده تاریخ و زمان.
مطالب
شروع کار با Dart - قسمت 3
لطفا قسمت دوم را در اینجا مطالعه بفرمایید

خدمت دوستان عزیز مطلبی را عرض کنم که البته باید در ابتدای این سری مقالات متذکر می‌شدم. این سری مقالات Dart مرجع کاملی برای یادگیری Dart نمی‌باشد. فقط یک Quick Start یا Get Started محسوب می‌شود برای آشنایی مقدماتی با ساختار Dart. از عنوان مقاله هم این موضوع قابل درک و تشخیص می‌باشد. همچنین فرض شده است که دوستان آشنایی مقدماتی با جاوااسکریپت و مباحث شی گرایی را نیز دارند. البته اگر مشغله کاری به بنده این اجازه را بدهد، مطالب جامع‌تری را در این زمینه آماده و منتشر می‌کنم.

گام پنجم: ذخیره سازی اطلاعات در فضای محلی یا Local
در این گام، تغییرات badge را در فضای ذخیره سازی سیستم Local نگهداری می‌نماییم؛ بطوری که اگر دوباره برنامه را راه اندازی نمودید، badge با داده‌های ذخیره شده در سیستم Local مقداردهی اولیه می‌گردد.
کتابخانه dart:convert را به منظور استفاده از کلاس مبدل JSON به فایل piratebadge.dart اضافه نمایید.
import 'dart:html';
import 'dart:math' show Random;

import 'dart:convert' show JSON;
همچنین یک Named Constructor یا سازنده‌ی با نام را به کلاس PirateName بصورت زیر اضافه کنید.
class PirateName {
  ...
  PirateName.fromJSON(String jsonString) {
    Map storedName = JSON.decode(jsonString);
    _firstName = storedName['f'];
    _appellation = storedName['a'];
  }
}
توضیحات
- جهت کسب اطلاعات بیشتر در مورد Json به این لینک مراجعه نمایید
- کلاس JSON جهت کار با داده هایی به فرمت Json استفاده می‌شود که امکاناتی را جهت دسترسی سریعتر و راحت‌تر به این داده‌ها فراهم می‌کند.
- سازنده‌ی PirateName.fromJSON، از یک رشته حاوی داده‌ی Json، یک نمونه از کلاس PirateName ایجاد می‌کند.
- سازنده‌ی PirateName.fromJSON، یک Named Constructor می‌باشد. این نوع سازنده‌ها دارای نامی متفاوت از نام سازنده‌های معمول هستند و بصورت خودکار نمونه ای از کلاس مورد نظر را ایجاد نموده و به عنوان خروجی بر می‌گردانند.
- تابع JSON.decode یک رشته‌ی حاوی داده‌ی Json را تفسیر نموده و اشیاء Dart را از آن ایجاد می‌کند.
یک Getter به کلاس PirateName اضافه کنید که مقادیر ویژگی‌های آن را به یک رشته Json تبدیل می‌کند
class PirateName {
  ...
  String get jsonString => JSON.encode({"f": _firstName, "a": _appellation});
}
جهت ذخیره سازی آخرین تغییرات کلاس PirateName در فضای ذخیره سازی Local، از یک کلید استفاده می‌کنیم که مقدار آن محتوای PirateName می‌باشد. در واقع فضای ذخیره سازی Local داده‌ها را به صورت جفت کلید-مقدار یا Key-Value Pairs نگهداری می‌نماید. جهت تعریف کلید، یک متغیر رشته ای را بصورت top-level و به شکل زیر تعریف کنید.
final String TREASURE_KEY = 'pirateName';

void main() {
  ...
}
زمانیکه تغییری در badge name صورت گرفت، این تغییرات را در فضای ذخیره سازی Local، توسط ویژگی window.localStorage ذخیره می‌نماییم. تغییرات زیر را در تابع setBadgeName اعمال نمایید
void setBadgeName(PirateName newName) {
  if (newName == null) {
    return;
  }
  querySelector('#badgeName').text = newName.pirateName;
  window.localStorage[TREASURE_KEY] = newName.jsonString;
}
تابع getBadgeNameFromStorage را بصورت top-level تعریف نمایید. این تابع داده‌های ذخیره شده را از Local Storage بازیابی نموده و یک شی از نوع کلاس PirateName ایجاد می‌نماید.
void setBadgeName(PirateName newName) {
  ...
}

PirateName getBadgeNameFromStorage() {
  String storedName = window.localStorage[TREASURE_KEY];
  if (storedName != null) {
    return new PirateName.fromJSON(storedName);
  } else {
    return null;
  }
}
در پایان نیز تابع setBadgeName را به منظور مقدار دهی اولیه به badge name، در تابع main، فراخوانی می‌نماییم.
void main() {
  ...
  setBadgeName(getBadgeNameFromStorage());
}
حال به مانند گامهای قبل برنامه را اجرا و بررسی نمایید.

گام ششم: خواندن نام‌ها از فایل‌های ذخیره شده به فرمت Json
در این گام کلاس PirateName را به گونه‌ای تغییر می‌دهیم که نام‌ها را از فایل Json بخواند. این عمل موجب می‌شود تا به راحتی اسامی مورد نظر را به فایل اضافه نمایید تا توسط کلاس خوانده شوند، بدون آنکه نیاز باشد کد کلاس را مجددا دستکاری کنید.
به منوی File > New File... مراجعه نموده و فایل piratenames.json را با محتوای زیر ایجاد نمایید. این فایل را در پوشه 1-blankbadge و در کنار فایلهای HTML و Dart ایجاد کنید.
{ "names": [ "Anne", "Bette", "Cate", "Dawn",
        "Elise", "Faye", "Ginger", "Harriot",
        "Izzy", "Jane", "Kaye", "Liz",
        "Maria", "Nell", "Olive", "Pat",
        "Queenie", "Rae", "Sal", "Tam",
        "Uma", "Violet", "Wilma", "Xana",
        "Yvonne", "Zelda",
        "Abe", "Billy", "Caleb", "Davie",
        "Eb", "Frank", "Gabe", "House",
        "Icarus", "Jack", "Kurt", "Larry",
        "Mike", "Nolan", "Oliver", "Pat",
        "Quib", "Roy", "Sal", "Tom",
        "Ube", "Val", "Walt", "Xavier",
        "Yvan", "Zeb"],
  "appellations": [ "Awesome", "Captain",
        "Even", "Fighter", "Great", "Hearty",
        "Jackal", "King", "Lord",
        "Mighty", "Noble", "Old", "Powerful",
        "Quick", "Red", "Stalwart", "Tank",
        "Ultimate", "Vicious", "Wily", "aXe", "Young",
        "Brave", "Eager",
        "Kind", "Sandy",
        "Xeric", "Yellow", "Zesty"]}
این فایل شامل یک شی Json با دو لیست رشته ای می‌باشد.
به فایل piratebadge.html مراجعه نمایید و فیلد input و المنت button را غیر فعال نمایید.
...
  <div>
    <input type="text" id="inputName" maxlength="15" disabled>
  </div>
  <div>
    <button id="generateButton" disabled>Aye! Gimme a name!</button>
  </div>
...
این دو المنت پس از اینکه تمامی نام‌ها از فایل Json با موفقیت خوانده شدند فعال می‌گردند.
کتابخانه dart:async را در ابتدای فایل دارت import نمایید
import 'dart:html';
import 'dart:math' show Random;
import 'dart:convert' show JSON;

import 'dart:async' show Future;
توضیحات
- کتابخانه dart:async برنامه نویسی غیر همزمان یا asynchronous را فراهم می‌کند
- کلاس Future روشی را ارئه می‌کند که در آن مقادیر مورد نیاز در آینده ای نزدیک و به صورت غیر همزمان واکشی خواهند شد.
در مرحله بعد لیست‌های names و appellations را با کد زیر بصورت یک لیست خالی جایگزین نمایید.
class PirateName {
  ...
  static List<String> names = [];
  static List<String> appellations = [];
  ...
}
توضیحات
- مطمئن شوید که کلمه کلیدی final را از تعاریف فوق حذف نموده اید
- [] معادل new List() می‌باشد
- کلاس List یک نوع Generic می‌باشد که می‌تواند شامل هر نوع شی ای باشد. اگر می‌خواهید که لیست شما فقط شامل داده هایی از نوع String باشد، آن را بصورت List<String> تعریف نمایید.
دو تابع static را بصورت زیر به کلاس PirateName اضافه نمایید
class PirateName {
  ...

  static Future readyThePirates() {
    var path = 'piratenames.json';
    return HttpRequest.getString(path)
        .then(_parsePirateNamesFromJSON);
  }
  
  static _parsePirateNamesFromJSON(String jsonString) {
    Map pirateNames = JSON.decode(jsonString);
    names = pirateNames['names'];
    appellations = pirateNames['appellations'];
  }
}
توضیحات
- کلاس HttpRequest یک Utility می‌باشد که داده‌ها را از یک آدرس یا URL خاص واکشی می‌نماید.
- تابع getString یک درخواست را به صورت GET ارسال می‌نماید و رشته ای را بر می‌گرداند
- در کد فوق از کلاس Future استفاده شده است که موجب می‌شود درخواست GET بصورت غیر همزمان ارسال گردد.
- زمانیکه Future با موفقیت خاتمه یافت، تابع then فراخوانی می‌شود. پارامتر ورودی این تابع، یک تابع می‌باشد که پس از خاتمه درخواست GET اجرا خواهد شد. به این نوع توابع که پس از انجام یک عملیات خاص بصورت خودکار اجرا می‌شوند توابع CallBack می‌گویند.
- زمانیکه Future با موفقیت خاتمه یافت، اسامی از فایل Json خوانده خواهند شد
- تابع readyThePirates دارای نوع خروجی Future می‌باشد بطوری که برنامه اصلی در زمانی که فایلها در حال خوانده شدن هستند، به کار خود ادامه میدهد و متوقف نخواهد شد
یک متغیر top-level از نوع SpanElement در کلاس PirateName ایجاد کنید.
SpanElement badgeNameElement;

void main() {
  ...
}
تغییرات زیر را در تابع main ایجاد کنید.
void main() {
  InputElement inputField = querySelector('#inputName');
  inputField.onInput.listen(updateBadge);
  genButton = querySelector('#generateButton');
  genButton.onClick.listen(generateBadge);
  
  badgeNameElement = querySelector('#badgeName');
  ...
}
کد زیر را نیز به منظور خواندن نام‌ها از فایل Json اضافه کنید. در این کد اجرای موفقیت آمیز درخواست و عدم اجرای درخواست، هر دو به شکلی مناسب مدیریت شده اند. 
void main() {
  ...
  
  PirateName.readyThePirates()
    .then((_) {
      //on success
      inputField.disabled = false; //enable
      genButton.disabled = false;  //enable
      setBadgeName(getBadgeNameFromStorage());
    })
    .catchError((arrr) {
      print('Error initializing pirate names: $arrr');
      badgeNameElement.text = 'Arrr! No names.';
    });
}
توضیحات
- تابع readyThePirates فراخوانی شده است که یک Future بر می‌گرداند.
- زمانی که Future با موفقیت خاتمه یافت تابع CallBack موجود در تابع then فراخوانی می‌شود.
- (_) به عنوان پارامتر ورودی تابع then ارسال شده است، به این معنا که از پارامتر ورودی صرف نظر شود.
- تابع then المنت‌های صفحه را فعال می‌کند و داده‌های ذخیره شده را بازیابی می‌نماید
- اگر Future با خطا مواجه شود، توسط تابع catchError که یک تابع CallBack می‌باشد، پیغام خطایی را نمایش می‌دهیم.
برنامه را به مانند گامهای قبل اجرا نموده و نتیجه را مشاهده نمایید
مطالب
افزودن یک DataType جدید برای نگه‌داری تاریخ خورشیدی - 1

ثبت و نگه‌داری تاریخ خورشیدی در SQL Server از دیرباز یکی از نگرانی‌های برنامه‌نویسان و طراحان پایگاه داده‌ها بوده است. در این نوشتار، راه‌کار تعریف یک DataType در SQL Server 2012 به روش CLR آموزش داده خواهد شد.

در ویژوال استودیو یک پروژه‌ی جدید از نوع SQL Server Database Project به شکل زیر ایجاد کنید: 

نام پروژه را به یاد تقویم خیام، prgJalaliDate می‌گذارم. در Solution Explorer روی نام پروژه راست‌کلیک کرده، سپس روی Add New Item کلیک کنید. در پنجره‌ی بازشده مطابق شکل SQL CLR C# User Defined Type را برگزینید؛ سپس نام JalaliDateType را برای آن انتخاب کنید.
 

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

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

using System;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;

[Serializable()]
[SqlUserDefinedType(Format.Native)]
public struct JalaliDate : INullable
{
    private Int16 m_Year;
    private byte m_Month;
    private byte m_Day;
    private byte m_Hour;
    private byte m_Minute;
    private byte m_Second;
    private bool is_Null;


    public Int16 Year
    {
        get
        {
            return (this.m_Year);
        }
        set
        {
            m_Year = value;
        }
    }

    public byte Month
    {
        get
        {
            return (this.m_Month);
        }
        set
        {
            m_Month = value;
        }
    }

    public byte Day
    {
        get
        {
            return (this.m_Day);
        }
        set
        {
            m_Day = value;
        }
    }

    public byte Hour
    {
        get
        {
            return (this.m_Hour);
        }
        set
        {
            m_Hour = value;
        }
    }

    public byte Minute
    {
        get
        {
            return (this.m_Minute);
        }
        set
        {
            m_Minute = value;
        }
    }

    public byte Second
    {
        get
        {
            return (this.m_Second);
        }
        set
        {
            m_Second = value;
        }
    }

    public bool IsNull
    {
        get
        {
            return is_Null;
        }
    }

    public static JalaliDate Null
    {
        get
        {
            JalaliDate jl = new JalaliDate();
            jl.is_Null = true;
            return (jl);
        }
    }


    public override string ToString()
    {
        if (this.IsNull)
        {
            return "NULL";
        }
        else
        {
            return this.m_Year.ToString("D4") + "/" + this.m_Month.ToString("D2") + "/" + this.m_Day.ToString("D2") + " " + this.Hour.ToString("D2") + ":" + this.Minute.ToString("D2") + ":" + this.Second.ToString("D2");
        }
    }


    public static JalaliDate Parse(SqlString s)
    {
        if (s.IsNull)
        {
            return Null;
        }

        System.Globalization.PersianCalendar pers = new System.Globalization.PersianCalendar();
        string str = Convert.ToString(s);
        string[] JDate = str.Split(' ')[0].Split('/');

        JalaliDate jl = new JalaliDate();

        jl.Year = Convert.ToInt16(JDate[0]);
        byte MonthsInYear = (byte)pers.GetMonthsInYear(jl.Year);
        jl.Month = (byte.Parse(JDate[1]) <= MonthsInYear ? (byte.Parse(JDate[1]) > 0 ? byte.Parse(JDate[1]) : (byte)1) : MonthsInYear);
        byte DaysInMonth = (byte)pers.GetDaysInMonth(jl.Year, jl.Month); ;
        jl.Day = (byte.Parse(JDate[2]) <= DaysInMonth ? (byte.Parse(JDate[2]) > 0 ? byte.Parse(JDate[2]) : (byte)1) : DaysInMonth);
        if (str.Split(' ').Length > 1)
        {
            string[] JTime = str.Split(' ')[1].Split(':');
            jl.Hour = (JTime.Length >= 1 ? (byte.Parse(JTime[0]) < 23 && byte.Parse(JTime[0]) >= (byte)0 ? byte.Parse(JTime[0]) : (byte)0) : (byte)0);
            jl.Minute = (JTime.Length >= 2 ? (byte.Parse(JTime[1]) < 59 && byte.Parse(JTime[1]) >= (byte)0 ? byte.Parse(JTime[1]) : (byte)0) : (byte)0);
            jl.Second = (JTime.Length >= 3 ? (byte.Parse(JTime[2]) < 59 && byte.Parse(JTime[2]) >= (byte)0 ? byte.Parse(JTime[2]) : (byte)0) : (byte)0);
        }
        else { jl.Hour = 0; jl.Minute = 0; jl.Second = 0; }

        return (jl);
    }

    public SqlString GetDate()
    {
        return this.m_Year.ToString("D4") + "/" + this.m_Month.ToString("D2") + "/" + this.m_Day.ToString("D2");
    }

    public SqlString GetTime()
    {
        return this.Hour.ToString("D2") + ":" + this.Minute.ToString("D2") + ":" + this.Second.ToString("D2");
    }

    public SqlDateTime ToGregorianTime()
    {
        System.Globalization.PersianCalendar pers = new System.Globalization.PersianCalendar();
        return SqlDateTime.Parse(pers.ToDateTime(this.Year, this.Month, this.Day, this.Hour, this.Minute, this.Second, 0).ToString());
    }

    public SqlString JalaliDateAdd(SqlString interval, int increment)
    {
        System.Globalization.PersianCalendar pers = new System.Globalization.PersianCalendar();
        DateTime dt = pers.ToDateTime(this.Year, this.Month, this.Day, this.Hour, this.Minute, this.Second, 0);
        string CInterval = interval.ToString();
        bool isConvert = true;
        switch (CInterval)
        {
            case "Year":
                dt = pers.AddYears(dt, increment);
                break;
            case "Month":
                dt = pers.AddMonths(dt, increment);
                break;
            case "Day":
                dt = pers.AddDays(dt, increment);
                break;
            case "Hour":
                dt = pers.AddHours(dt, increment);
                break;
            case "Minute":
                dt = pers.AddMinutes(dt, increment);
                break;
            case "Second":
                dt = pers.AddSeconds(dt, increment);
                break;
            default:
                isConvert = false;
                break;
        }

        if (isConvert == true)
        {
            this.Year = (Int16)pers.GetYear(dt);
            this.Month = (byte)pers.GetMonth(dt);
            this.Day = (byte)pers.GetDayOfMonth(dt);
            this.Hour = (byte)pers.GetHour(dt);
            this.Minute = (byte)pers.GetMinute(dt);
            this.Second = (byte)pers.GetSecond(dt);
        }


        return this.m_Year.ToString("D4") + "/" + this.m_Month.ToString("D2") + "/" + this.m_Day.ToString("D2") + " " + this.Hour.ToString("D2") + ":" + this.Minute.ToString("D2") + ":" + this.Second.ToString("D2");
    }
}

از منوهای بالا روی منوی Bulild و سپس گزینه‌ی Publish prgJalaliDate کلیک کتید:

در پنجره‌ی بازشده روی دکمه‌ی Edit کلیک کنید سپس تنظیمات مربوط به اتصال به پایگاه داده را انجام دهید.

روی دکمه‌ی OK کلیک کنید و سپس در پنجره‌ی اولیه، روی دکمه‌ی Publish کلیک کتید:

به همین سادگی، DataType مربوطه در SQL Server 2012 ساخته می‌شود. خبر خوش این‌که شما می‌توانید با راست‌کلیک روی نام پروژه و انتخاب گزینه‌ی Properties در قسمت Project Setting تنظیمات مربوط به نگارش SQL Server را انجام دهید. (از نگارش 2005 به بعد در VS 2012 پشتیبانی می‌شود.)


اکنون زمان آن رسیده است که DataType ایجادشده را در SQL Server 2012 بیازماییم. SQL Server را باز کنید و دستور زیر را در آن اجرا کتید.

USE Northwind

GO

CREATE TABLE dbo.TestTable
(
Id int NOT NULL IDENTITY (1, 1),
TestDate dbo.JalaliDate NULL
)  ON [PRIMARY]
GO
همین‌طور که مشاهده می‌کنید؛ امکان به‌کارگیری DataType تعریف‌شده وجود دارد. 
اکنون چند رکورد درون این جدول درج می‌کنیم:
Insert into TestTable (TestDate) Values ('1392/02/09'),('1392/02/09 22:40'),('1392/12/30 22:40')
پس از اجرای این دستور خطای زیر در پایین صفحه‌ی SQL Server نمایان می‌شود:

این خطا به این خاطر است که CLR را در SQL Server  فعال نکرده ایم. جهت فعال‌کردن CLR دستور زیر را اجرا کنید:
sp_configure 'clr enabled', 1
Reconfigure
بار دیگر دستور درج را اجرا می‌کنیم:
Insert into TestTable (TestDate) Values ('1392/02/09'),('1392/02/09 22:40'),('1392/12/30 22:40')
ملاحظه می‌کنید که داده‌ها در جدول مربوطه ذخیره شده است. در رکورد نخست چون ساعت، دقیقه و ثانیه تعریف نشده است؛ به طور هوشمند صفر درج شده است. در رکورد دوم، ساعت و دقیقه مقدار دارد ولی ثانیه صفر ثبت شده است. و در رکورد سوم چون سال 1392 کبیسه نیست؛ به صورت هوشمند آخرین روز ماه به جای روز ثبت شده است. هرچند می‌توان با دست‌کاری در توابع سی‌شارپ، این قوانین را عوض کرد.

اکنون زمان آن رسیده است که توسط یک پرس‌وجو، همه‌ی توابعی که در سی‌شارپ برای این نوع داده نوشتیم، بیازماییم. پرس‌وجوی زیر را اجرا کنید:
Select TestDate.ToString() as JalaliDateTime,
          TestDate.GetDate() as JalaliDate, TestDate.GetTime() as JalaliTime,
          TestDate.ToGregorianTime() as GregorianTime,
          TestDate.JalaliDateAdd('Day',1) JalaliTomorrow,
          TestDate.Month as JalaliMonth from TestTable
خروجی این پرس‌وجو به شکل زیر خواهد بود:

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

نیازی به گفتن نیست که می‌توانید به سادگی از توابع مربوط به DateTime در SQL Server بهره ببرید. برای مثال برای به دست آوردن فاصله‌ی میان دو روز از پرس‌وجوی زیر استفاده کنید:
Declare @a JalaliDate  = '1392/02/07 00:00:00'
Declare @b JalaliDate = '1392/02/05 00:00:00'

SELECT DATEDIFF("DAY",@b.ToGregorianTime(),@a.ToGregorianTime()) AS DiffDate

شاد و پیروز باشید.
مطالب
کار با Docker بر روی ویندوز - قسمت هفتم - مدیریت اجرای چندین کانتینر به هم وابسته
تا اینجا نحوه‌ی اجرای برنامه‌ها، وب سرورها و حتی بانک‌های اطلاعاتی را توسط داکر بررسی کردیم. در این قسمت می‌خواهیم یک برنامه و بانک اطلاعاتی مخصوص آن‌را داخل یک کانتینر اجرا کنیم و برای این منظور از ابزار ساده کننده‌ی docker-compose استفاده خواهیم کرد.


docker-compose چیست؟

فرض کنید برنامه‌ی ما، از یک قسمت منطق خود برنامه و قسمت دیگر بانک اطلاعاتی آن تشکیل شده‌است. در این حالت برای توزیع آن توسط کانتینرها، نیاز به دو کانتینر مجزا خواهد بود؛ یکی برای برنامه و دیگری برای بانک اطلاعاتی:
dockerrun --name db`
-d `
-p 3306:3306 `
-e MYSQL_ROOT_PASSWORD=my-secret-pw `
-v db:/var/lib/mysql`
mysql

dockerinspect db # extract ipaddress

dockerrun --name web `
-d `
-p 8080:80 `
-e MY_DB_PORT=3306 `
-e MY_DB_HOST=? `
-v /my/php/app:/usr/share/nginx/html `
nginx
تمام این دستورات را به همراه یک ` نیز مشاهده می‌کنید. این روشی است که از آن برای چندسطری کردن دستورات در PowerShell استفاده می‌شود.
- دستور اول مطابق توضیحات قسمت قبل، یک بانک اطلاعاتی MySQL را در پس زمینه، با نام db که در آن، پورت 3306 میزبان به پورت 3306 کانتینر نگاشت شده‌است و همچنین بانک اطلاعاتی آن در یک volume نامدار به نام db با مسیر نگاشت شده‌ی به /var/lib/mysql/ داخل کانتینر ایجاد می‌شود، اجرا می‌کند.
- دستور دوم کار استخراج اطلاعات این کانتینر را انجام می‌دهد که شامل آدرس IP آن نیز می‌باشد. از این IP در برنامه‌ی وب استفاده خواهیم کرد.
- دستور سوم مطابق توضیحات قسمت پنجم، یک وب سرور nginx را برای هاست یک برنامه‌ی PHP که در آن پورت 8080 میزبان به پورت 80 کانتینر نگاشت شده‌است و همچنین فایل‌های آن از مسیر /my/php/app/ میزبان به مسیر /usr/share/nginx/html/ داخل کانتینر نگاشت و تامین می‌شوند، اجرا می‌کند. در اینجا از پارامتر e برای تعریف یک سری متغیر محیطی مانند شماره‌ی پورت و IP کانتینر اجرا کننده‌ی mysql، استفاده شده‌است.

در این مثال دو کانتینر به هم وابسته را اجرا کرده‌ایم و برای اجرای کانتینر دوم، نیاز است حداقل IP کانتینر اول را دانست و در قسمت MY_DB_HOST مقدار دهی کرد. روش دیگری نیز برای مدیریت ساده‌تر اجرای چندین کانتینر به هم وابسته توسط ابزاری به نام docker-compose وجود دارد. اگر از Dockerfile (که آن‌را در قسمت پنجم معرفی کردیم) جهت ایجاد Imageهای سفارشی بکار می‌رود، فایل docker-compose.yml، کار خودکار سازی ایجاد و اجرای چندین کانتینر را انجام می‌دهد که با قالب YAML تعریف می‌شود:
version: '2'
services:
    db:
         image: mysql
         ports:
             -3306:3306
         environment:
             -MYSQL_ROOT_PASSWORD=my-secret-pw
         volumes:
             -db:/var/lib/mysql

    web:
         image: nginx
         ports:
             -8080:80
         environment:
             -MY_DB_PORT=3306
             -MY_DB_HOST=db
         volumes:
             -/my/php/app:/usr/share/nginx/html
همانطور که مشاهده می‌کنید، هرچند قالب آن اندکی متفاوت شده‌است، اما در اصل با دستورات docker ای که در ابتدا معرفی کردیم، تفاوتی ندارد. محتوای فوق را در یک فایل متنی ویژه به نام docker-compose.yml ذخیره و آن‌را به ابزار خط فرمان دیگری به نام docker-compose معرفی می‌کنیم.
در ابتدای این فایل، شماره نگارش قالب YAML مورد استفاده، مشخص شده‌است. در این نگارش، به کانتینرها، services گفته می‌شود که در اینجا دو سرویس db و web را مشاهده می‌کنید. در فایل‌های yml، فضاهای خالی و indentations مهم هستند و بر این اساس است که کانتینرها و سپس مشخصات این کانتینرها، تمیز داده می‌شوند.


راه اندازی TeamCity به کمک فایل docker-compose.yml آن

در اینجا محتویات فایل docker-compose.yml مخصوص راه اندازی TeamCity را مشاهده می‌کنید که از سه کانتینر تشکیل شده‌است و از بانک اطلاعاتی postgres استفاده می‌کند:
version: '2'
services:
    teamcity:
        image: sjoerdmulder/teamcity
        ports:
            - 8111:8111
    teamcity-agent:
        image: sjoerdmulder/teamcity-agent
        environment:
            - TEAMCITY_SERVER=http://teamcity:8111
    postgres:
        image: postgres
        environment:
            - POSTGRES_DB=teamcity
در این تنظیمات، پورت 8111 میزبان به پورت 8111 کانتینر teamcity نگاشت شده‌است. در ادامه teamcity-agent نیاز به IP این کانتینر را دارد (یک build-server است). زمانیکه چندین کانتینر را توسط فایل docker-compose.yml راه اندازی می‌کنیم، داکر یک شبکه‌ی ایزوله (private network) را نیز برای اینکار مهیا می‌کند. داخل این شبکه‌ی ایزوله، یک DNS سرور توکار نیز وجود دارد که امکان دسترسی به کانتینرهای مختلف را از طریق نام کانتینرهای آن‌ها میسر می‌کند. به همین جهت است که مقدار TEAMCITY_SERVER، به http://teamcity:8111 تنظیم شده‌است و دقیقا از نام کانتینر teamcity برای یافتن IP آن استفاده می‌کند. همچنین باید دقت داشت در این آدرس، عدد 8111 به پورت داخل کانتینر teamcity اشاره می‌کند و نه به پورت میزبان. کانتینرها از طریق آدرس IP و پورت خودشان با هم در تماس هستند. پورت میزبان 8111، صرفا جهت فراخوانی teamcity از طریق سیستم میزبان مشخص شده‌است.


در ادامه برای کار با آن، ابتدا این محتویات را به صورت یک فایل متنی docker-compose.yml ذخیره کنید. سپس از طریق خط فرمان به پوشه‌ی آن وارد شده و دستور docker-compose up را صادر کنید. docker-compose یکی دیگر از ابزارهای خط فرمان نصب شده‌ی به همراه داکر است و پارامتر up آن کار راه اندازی و اجرای کانتینرهای ذکر شده‌ی در فایل yml موجود را انجام می‌دهد. نام پوشه‌ای که این فایل در آن قرار دارد، به عنوان نام پروژه‌ی مشترک بین این کانتینرها در گزارشات آن مورد استفاده قرار می‌گیرد.
پس از صدور این فرمان، ابتدا تمام imageهای ذکر شده‌ی در فایل yml دریافت می‌شوند (سه image در اینجا) و هر سه کانتینر راه اندازی می‌شوند. اکنون می‌توان در سیستم میزبان به آدرس http://localhost:8111 مراجعه کرد و از برنامه‌ی teamcity استفاده نمود. البته صفحه‌ی ابتدایی آن کار تنظیمات بانک اطلاعاتی آن‌را انجام می‌دهد و جائیکه در مورد database type سؤال می‌پرسد می‌توان postgres را انتخاب کرد و سپس در ذیل آن مقدار database host را نیز postgres وارد می‌کنیم. علت آن‌را نیز پیشتر توضیح دادیم. postgres در اینجا نام کانتینر نیز هست و ذکر نام آن، با ذکر IP مرتبط با آن کانتینر، یکی است. نام بانک اطلاعاتی را teamcity وارد کنید (مطابق تنظیمات فایل yml فوق) و نام کاربری آن نیز postgres است؛ بدون کلمه‌ی عبور. البته می‌شد در فایل yml فوق، متغیر محیطی POSTGRES_PASSWORD=xyz را نیز تنظیم کرد و سپس از آن در اینجا استفاده نمود.


docker-compose و ایجاد شبکه‌های ایزوله

توسط دستور docker network ls می‌توان لیست شبکه‌های مجازی ایجاد شده‌ی توسط docker را مشاهده کرد (و همچنین سایر network adapters موجود). اگر این دستور را اجرا کنید، کارت شبکه‌ی مجازی متناظر با شبکه‌ی خصوصی teamcity_default را که پیشتر در مورد آن توضیح داده شد، می‌توانید مشاهده کنید. این teamcity در اینجا همان نام پروژه و یا در اصل نام پوشه‌ای است که فایل docker-compose را از آنجا اجرا کردیم.
برای دریافت اطلاعات بیشتری در مورد این کارت شبکه‌ی به خصوص، می‌توان دستور docker network inspect teamcity_default را صادر کرد. یکی از قسمت‌های خروجی این گزارش، لیست کانتینرهایی است که هم اکنون به این شبکه متصل هستند؛ که در اینجا teamcity و بانک اطلاعاتی آن است.
مزیت ایجاد یک شبکه‌ی خصوصی مخصوص کانتینرهای به هم پیوسته، علاوه بر سادگی تشکیل فایل docker-compose آن‌ها با اشاره‌ی به نام کانتینرها، بجای ذکر مستقیم آدرس IP هر کدام، ایزوله ساختن این شبکه، از شبکه‌ی پیش‌فرض docker و بالا بردن میزان امنیت سایر کانتینرهایی است که هم اکنون از آن شبکه استفاده می‌کنند.


docker-compose و ایجاد DNS Server توکار

همانطور که عنوان شد، در این شبکه‌ی خصوصی ویژه‌ی کانتینرهای به هم پیوسته که توسط docker-compse اجرا و مدیریت شده‌اند، می‌توان از نام containerها بجای آدرس IP آن‌ها استفاده کرد و این مورد با وجود یک DNS Server توکار در این شبکه میسر شده‌است. برای آزمایش بیشتر این قابلیت، ابتدا دستور docker ps را صادر می‌کنیم تا نام کانتینرهای در حال اجرا را بدست بیاوریم. سپس سعی می‌کنیم پروسه‌ی bash shell داخل کانتینر بانک اطلاعاتی را اجرا کنیم:
docker ps
docker exec -it teamcity_postgres_1 bash
#exit
استفاده از docker exec یک روش است برای دسترسی به shell این کانتینر در حال اجرا؛ روش دیگر، استفاده از خود docker-compse می‌باشد که در این حالت، کار با آن ساده‌تر است:
 docker-compose exec postgres bash
در اینجا نیازی به ذکر سوئیچ it نیست؛ چون مقدار پیش‌فرض آن است و همچنین دیگر نیازی به اجرای docker ps برای یافتن نام کانتینری هم نیست و مستقیما می‌توان از نام‌های سرویس‌هایی که در فایل docker-compose.yml تعریف شده‌اند، استفاده کرد.
پس از دسترسی به شل، دستور زیر را اجرا کنید:
#ping teamcity
#exit
مشاهده خواهید کرد که این کانتینر می‌تواند کانتینر دیگری را صرفا با ذکر نام آن، ping کند.

یک نکته: اگر بخواهیم از وضعیت بانک‌های اطلاعاتی postgres توسط برنامه‌ی psql آن گزارش بگیریم نیز روش اجرای آن به همین صورت است:
docker-compose exec postgres psql -U postgres
postgres=#\l
postgres=#\q


اتصال یک کانتینر خارج از شبکه‌ی مجازی ایجاد شده‌ی توسط docker-compose به آن

فرض کنید می‌خواهید کانتینر کم حجم لینوکس alpine را اجرا کنید و توسط آن به شبکه‌ی مجازی ایجاد شده‌ی توسط docker-compose متصل شوید. روش آن به صورت زیر است:
 docker run --name apline -it --rm --net teamcity_default alpine sh
در اینجا توسط سوئیچ net، می‌توان نام شبکه‌ی مجازی را که نیاز است به آن متصل شویم، ذکر کرد. نام teamcity_default نیز از طریق اجرای دستور docker netwrok ls بدست آمده‌است.
این دستور، کانتینر لینوکس alpine را در حالت interactive جهت اجرای shell آن، راه اندازی می‌کند. سپس به شبکه‌ی مجازی teamcity_default متصل می‌شود. برای آزمایش این اتصال، در این shell راه اندازی شده، دستور ping teamcity را می‌توان صادر کرد. همچنین از داخل کانتینر teamcity نیز می‌توان این کانتینر را با نام آن ping کرد.


راه اندازی مجدد کانتینرها توسط docker-compose

اگر دستور docker-compose ps را دقیقا در پوشه‌ای که فایل yml آن قرار دارد اجرا کنیم، می‌توان گزارشی را صرفا از وضعیت کانتینرهای مرتبط با این فایل yml بدست آورد. دستور docker ps، لیست وضعیت تمام کانتینرهای در حال اجرای موجود را بر می‌گرداند. اکنون فرض کنید یکی از کانتینرهای اجرای شده‌ی توسط docker-compose، دیگر در حال اجرا نیست. برای راه اندازی مجدد آن می‌توان از دستور docker-compose start teamcity-agent استفاده کرد. همچنین دستور docker-compose logs teamcity-agent، لیست آخرین لاگ‌های مرتبط با یک کانتینر را بر می‌گرداند که می‌تواند برای رفع اشکال بسیار مفید باشد.


حذف کانتینرهای به هم پیوسته‌ی ایجاد شده‌ی توسط docker-compose

در ذیل ابتدا یک سری دستور را جهت مشاهده‌ی وضعیت سیستم مشاهده می‌کنید. سپس دستور docker-compose stop، کار متوقف کردن کانتینرهای مرتبط با فایل yml آن‌را انجام می‌دهد. دستور docker-compose rm -v، علاوه بر حذف این کانتینرها، volumeهای بانک‌های اطلاعاتی مرتبط را نیز حذف می‌کند. در آخر دستور docker-compose down، شبکه‌ی مجازی مرتبط را نیز حذف خواهد کرد. سپس مجددا از وضعیت سیستم گزارش گیری شده‌است.
 docker ps
docker-compose ps
docker volume ls
docker network ls

docker-compose stop
docker-compose rm -v
docker-compose down

docker ps -a
docker volume ls
docker network ls


اجرای پروژه‌ی ASP.NET Core Music Store توسط docker-compose

پروژه‌ی معروف Music Store مایکروسافت را به همراه فایل docker-compose.windows.yml آن، در اینجا می‌توانید مشاهده کنید. محتوای این فایل نیز به صورت زیر است:
version: '3'
services:
  db:
    image: microsoft/mssql-server-windows-developer
    environment:
      sa_password: "Password1"
      ACCEPT_EULA: "Y"
    ports:
      - "1433:1433" # REMARK: This is currently required, needs investigation
    healthcheck:
      test: [ "CMD", "sqlcmd", "-U", "sa", "-P", "Password1", "-Q", "select 1" ]
      interval: 1s
      retries: 30
web:
    build:
      context: .
      dockerfile: Dockerfile.windows
    environment:
      - "Data:DefaultConnection:ConnectionString=Server=db,1433;Database=MusicStore;User Id=sa;Password=Password1;MultipleActiveResultSets=True"
    depends_on:
      - db
    ports:
      - "5000:5000"
در اینجا تعریف دو کانتینر را مشاهده می‌کنید:
- کانتینر db که بر اساس image مخصوص mssql-server-windows-developer راه اندازی می‌شود. تنظیمات آن نیز بسیار شبیه به مطلب «کار با Docker بر روی ویندوز - قسمت ششم - کار با بانک‌های اطلاعاتی درون Containerها» است که پیشتر در مورد آن بحث کردیم.
- کانتینر web آن که از فایل Dockerfile.windows برای build سپس publish و در آخر run خودکار این برنامه‌ی ASP.NET Core، کمک می‌گیرد. در اینجا context به پوشه‌ی جاری اشاره می‌کند. در قسمت تنظیمات بانک اطلاعاتی آن، استفاده‌ی از نام کانتینر db را در قسمت رشته‌ی اتصالی مشاهده می‌کنید. قسمت depends_on آن ترتیب اجرای این کانتینرها را مشخص می‌کند. یعنی ابتدا باید کانتینر db اجرا شود و سپس web.
محتوای فایل Dockerfile.windows آن نیز به صورت زیر است که بر اساس دستورات NET Core CLI. تهیه شده‌است:
FROM microsoft/dotnet-nightly:2.0-sdk-nanoserver
SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]
ENV NUGET_XMLDOC_MODE skip
ENV DOTNET_SKIP_FIRST_TIME_EXPERIENCE 1
RUN New-Item -Path \MusicStore\samples\MusicStore -Type Directory
WORKDIR app
ADD samples/MusicStore/MusicStore.csproj samples/MusicStore/MusicStore.csproj
ADD build/dependencies.props build/dependencies.props
ADD NuGet.config .
RUN dotnet restore --runtime win10-x64 .\samples\MusicStore
ADD samples samples
RUN dotnet publish --output /out --configuration Release --framework netcoreapp2.0 --runtime win10-x64 .\samples\MusicStore
FROM microsoft/dotnet-nightly:2.0-runtime-nanoserver
WORKDIR /app
COPY --from=0 /out .
EXPOSE 5000
ENV ASPNETCORE_URLS http://0.0.0.0:5000
CMD dotnet musicstore.dll
روش اجرای آن‌را نیز به صورت زیر بیان کرده‌است:
docker-compose -f .\docker-compose.windows.yml build
docker-compose -f .\docker-compose.windows.yml up
البته باید دقت داشت که اجرای فرمان up، به صورت خودکار کار build را هم انجام می‌دهد. پس از اجرای آن، برنامه را بر روی پورت 5000 می‌توانید مشاهده کنید.
مطالب
احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular - قسمت اول - معرفی و ایجاد ساختار برنامه
قصد داریم در طی یک سری مطلب، یک کلاینت Angular 5.x را برای مطلب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» تهیه کنیم. البته این سری، مستقل از قسمت سمت سرور آن تهیه خواهد شد و صرفا در حد دریافت توکن از سرور و یا ارسال مشخصات کاربر جهت لاگین، نیاز بیشتری به قسمت سمت سرور آن ندارد و تاکید آن بر روی مباحث سمت کلاینت Angular است. بنابراین اینکه چگونه این توکن را تولید می‌کنید، در اینجا اهمیتی ندارد و کلیات آن با تمام روش‌های پیاده سازی سمت سرور (حتی مطلب «پیاده سازی JSON Web Token با ASP.NET Web API 2.x») سازگار است.
این سری شامل بررسی موارد ذیل خواهد بود:
  1. قسمت اول - معرفی و ایجاد ساختار برنامه
  2. قسمت دوم - سرویس اعتبارسنجی
  3. قسمت سوم - ورود به سیستم
  4. قسمت چهارم - به روز رسانی خودکار توکن‌ها
  5. قسمت پنجم - محافظت از مسیرها
  6. قسمت ششم - کار با منابع محافظت شده‌ی سمت سرور 


پیشنیازها
- آشنایی با Angular CLI
- آشنایی با مسیریابی‌ها در Angular
- آشنایی با فرم‌های مبتنی بر قالب‌ها


همچنین اگر پیشتر Angular CLI را نصب کرده‌اید، قسمت «به روز رسانی Angular CLI» ذکر شده‌ی در مطلب «Angular CLI - قسمت اول - نصب و راه اندازی» را نیز اعمال کنید. در این سری از angular/cli: 1.6.0@ استفاده شده‌است.


ایجاد ساختار اولیه و مسیریابی‌های آغازین مثال این سری

در ادامه، یک پروژه‌ی جدید مبتنی بر Angular CLI را به نام ASPNETCore2JwtAuthentication.AngularClient به همراه تنظیمات ابتدایی مسیریابی آن ایجاد می‌کنیم:
> ng new ASPNETCore2JwtAuthentication.AngularClient --routing

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

در ادامه برای تکمیل مثال جاری، دو کامپوننت جدید خوش‌آمدگویی و همچنین یافتن نشدن مسیرها را به برنامه اضافه می‌کنیم:
>ng g c welcome
>ng g c PageNotFound
که سبب ایجاد کامپوننت‌های src\app\welcome\welcome.component.ts و src\app\page-not-found\page-not-found.component.ts خواهند شد؛ به همراه به روز رسانی خودکار فایل src\app\app.module.ts جهت تکمیل قسمت declarations آن:
@NgModule({
  declarations: [
    AppComponent,
    WelcomeComponent,
    PageNotFoundComponent
  ],
سپس فایل src\app\app-routing.module.ts را به نحو ذیل تکمیل نمائید:
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { WelcomeComponent } from './welcome/welcome.component';
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
  { path: 'welcome', component: WelcomeComponent },
  { path: '', redirectTo: 'welcome', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];
@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
در اینجا زمانیکه کاربر ریشه‌ی سایت را درخواست می‌کند، به کامپوننت welcome هدایت خواهد شد.
همچنین مدیریت مسیریابی آدرس‌های ناموجود در سایت نیز با تعریف ** صورت گرفته‌است.


ایجاد ماژول Authentication و تعریف کامپوننت لاگین

کامپوننت‌های احراز هویت و اعتبارسنجی کاربران را در ماژولی به نام Authentication قرار خواهیم داد. بنابراین ماژول جدید آن‌را به همراه تنظیمات ابتدایی مسیریابی آن ایجاد می‌کنیم:
>ng g m Authentication -m app.module --routing
با این خروجی
  create src/app/authentication/authentication-routing.module.ts (257 bytes)
  create src/app/authentication/authentication.module.ts (311 bytes)
  update src/app/app.module.ts (696 bytes)
اگر به سطر آخر آن دقت کنید، فایل app.module.ts را نیز به صورت خودکار به روز رسانی کرده‌است:
import { EmployeeRoutingModule } from './employee/employee-routing.module';
@NgModule({
  imports: [
    BrowserModule,
    AppRoutingModule,
    AuthenticationModule
  ]
در اینجا AuthenticationModule را به انتهای لیست imports افزوده‌است که نیاز به اندکی تغییر دارد و باید آن‌را پیش از AppRoutingModule تعریف کرد. علت این است که AppRoutingModule، دارای تعریف مسیریابی ** یا catch all است که آن‌را جهت مدیریت مسیرهای یافت نشده به برنامه افزوده‌ایم. بنابراین اگر ابتدا AppRoutingModule تعریف شود و سپس AuthenticationModule، هیچگاه فرصت به پردازش مسیریابی‌های ماژول اعتبارسنجی نمی‌رسد؛ چون مسیر ** پیشتر برنده شده‌است.
بنابراین فایل app.module.ts چنین تعاریفی را پیدا می‌کند:
import { EmployeeModule } from './employee/employee.module';
@NgModule({
  imports: [
    BrowserModule,
    AuthenticationModule,
    AppRoutingModule 
  ]

در ادامه کامپوننت جدید لاگین را به این ماژول اضافه می‌کنیم:
>ng g c Authentication/Login
با این خروجی
  create src/app/Authentication/login/login.component.html (24 bytes)
  create src/app/Authentication/login/login.component.ts (265 bytes)
  create src/app/Authentication/login/login.component.css (0 bytes)
  update src/app/Authentication/authentication.module.ts (383 bytes)
اگر به سطر آخر آن دقت کنید، کار به روز رسانی فایل ماژول authentication، جهت درج این کامپوننت جدید، در قسمت declarations فایل authentication.module.ts نیز به صورت خودکار انجام شده‌است:
import { LoginComponent } from "./login/login.component";

@NgModule({
  declarations: [LoginComponent]
})

در ادامه می‌خواهیم قالب این کامپوننت را در منوی اصلی سایت قابل دسترسی کنیم. به همین جهت به فایل src/app/authentication/authentication-routing.module.ts مراجعه کرده و مسیریابی این کامپوننت را تعریف می‌کنیم:
import { LoginComponent } from "./login/login.component";

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


ایجاد ماژول‌های Core و Shared

در مطلب «سازماندهی برنامه‌های Angular توسط ماژول‌ها» در مورد اهمیت ایجاد ماژول‌های Core و Shared بحث شد. در اینجا نیز این دو ماژول را ایجاد خواهیم کرد.
فایل src\app\core\core.module.ts، جهت به اشتراک گذاری سرویس‌های singleton سراسری برنامه، یک چنین ساختاری را پیدا می‌کند:
import { NgModule, SkipSelf, Optional, } from "@angular/core";
import { CommonModule } from "@angular/common";
import { RouterModule } from "@angular/router";

// import RxJs needed operators only once
import "./services/rxjs-operators";

import { BrowserStorageService } from "./browser-storage.service";

@NgModule({
  imports: [CommonModule, RouterModule],
  exports: [
    // components that are used in app.component.ts will be listed here.
  ],
  declarations: [
    // components that are used in app.component.ts will be listed here.
  ],
  providers: [
    // global singleton services of the whole app will be listed here.
    BrowserStorageService
  ]
})
export class CoreModule {
  constructor( @Optional() @SkipSelf() core: CoreModule) {
    if (core) {
      throw new Error("CoreModule should be imported ONLY in AppModule.");
    }
  }
}
در اینجا از BrowserStorageService مطلب «ذخیره سازی اطلاعات در مرورگر توسط برنامه‌های Angular» استفاده شده‌است تا در سراسر برنامه در دسترس باشد. از آن در جهت ذخیره سازی توکن دریافتی از سرور در مرورگر کاربر، استفاده خواهیم کرد.
همچنین سطر "import "./services/rxjs-operators نیز از مطلب «روش‌هایی برای مدیریت بهتر عملگرهای RxJS در برنامه‌های Angular» کمک می‌گیرد تا مدام نیاز به import عملگرهای rxjs نباشد.

و ساختار فایل src\app\shared\shared.module.ts جهت به اشتراک گذاری کامپوننت‌های مشترک بین تمام ماژول‌ها، به صورت ذیل است:
import { NgModule, ModuleWithProviders } from "@angular/core";
import { CommonModule } from "@angular/common";

@NgModule({
  imports: [
    CommonModule
  ],
  entryComponents: [
    // All components about to be loaded "dynamically" need to be declared in the entryComponents section.
  ],
  declarations: [
    // common and shared components/directives/pipes between more than one module and components will be listed here.
  ],
  exports: [
    // common and shared components/directives/pipes between more than one module and components will be listed here.
    CommonModule
  ]
  /* No providers here! Since they’ll be already provided in AppModule. */
})
export class SharedModule {
  static forRoot(): ModuleWithProviders {
    // Forcing the whole app to use the returned providers from the AppModule only.
    return {
      ngModule: SharedModule,
      providers: [ /* All of your services here. It will hold the services needed by `itself`. */]
    };
  }
}

و در آخر تعاریف این دو ماژول جدید به فایل src\app\app.module.ts اضافه خواهند شد:
import { FormsModule } from "@angular/forms";
import { HttpClientModule } from "@angular/common/http";

import { CoreModule } from "./core/core.module";
import { SharedModule } from "./shared/shared.module";

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    HttpClientModule,
    CoreModule,
    SharedModule.forRoot(),
    AuthenticationModule,
    AppRoutingModule
  ]
})
export class AppModule { }
در اینجا «FormsModule» و «HttpClientModule جدید» اضافه شده از Angular 4.3 را نیز import کرده‌ایم.


افزودن کامپوننت Header

در ادامه می‌خواهیم لینکی را به این مسیریابی جدید در نوار راهبری بالای سایت اضافه کنیم. همچنین قصد نداریم فایل app.component.html را با تعاریف آن شلوغ کنیم. به همین جهت یک کامپوننت هدر جدید را برای این منظور اضافه می‌کنیم:
> ng g c Header
با این خروجی:
  create src/app/header/header.component.html (25 bytes)
  create src/app/header/header.component.ts (269 bytes)
  create src/app/header/header.component.css (0 bytes)
  update src/app/app.module.ts (1069 bytes)
سپس به فایل src\app\header\header.component.html مراجعه کرده و آن‌را به صورت ذیل تغییر می‌دهیم:
<nav>
  <div>
    <div>
      <a [routerLink]="['/']">{{title}}</a>
    </div>
    <ul>
      <li role="menuitem" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">
        <a [routerLink]="['/welcome']">Home</a>
      </li>
      <li role="menuitem" routerLinkActive="active">
        <a queryParamsHandling="merge" [routerLink]="['/login']">Login</a>
      </li>
    </ul>
  </div>
</nav>
که title آن نیز به صورت ذیل تامین می‌شود:
export class HeaderComponent implements OnInit {
    title = "Angular.Jwt.Core";

در آخر به فایل app.component.html مراجعه کرده و selector این کامپوننت را در آن درج می‌کنیم:
<app-header></app-header>
<div>
  <router-outlet></router-outlet>
</div>
زمانیکه یک کامپوننت فعالسازی می‌شود، قالب آن در router-outlet نمایش داده خواهد شد. app-header نیز کار نمایش nav-bar را انجام می‌دهد.

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



انتقال کامپوننت‌هایی که در app.component.ts استفاده می‌شوند به CoreModule

 با توجه به مطلب «سازماندهی برنامه‌های Angular توسط ماژول‌ها»، کامپوننت‌هایی که در app.component.ts مورد استفاده قرار می‌گیرند، باید به Core Module منتقل شوند و قسمت declarations فایل app.module.ts از آن‌ها خالی گردد. به همین جهت پوشه‌ی جدید src\app\core\component را ایجاد کرده و سپس پوشه‌ی src\app\header را به آنجا منتقل می‌کنیم (با تمام فایل‌های درون آن).
پس از آن، تعریف HeaderComponent را از قسمت declarations مربوط به AppModule حذف کرده و آن‌را به دو قسمت exports و declarations مربوط به CoreModule منتقل می‌کنیم:
import { HeaderComponent } from "./component/header/header.component";

@NgModule({
  exports: [
    // components that are used in app.component.ts will be listed here.
    HeaderComponent
  ],
  declarations: [
    // components that are used in app.component.ts will be listed here.
    HeaderComponent
  ]
})
export class CoreModule {


کدهای کامل این سری را از اینجا می‌توانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه‌ی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشه‌ی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.