نظرات مطالب
کامپوننت‌های متداول طرحبندی صفحات در بوت استرپ 4
یک نکته‌ی تکمیلی: در حالت استفاده‌ی از table-responsive، جدول، عرض صفحه را پر نمی‌کند.
علت اینجا است که این کلاس باید به یک div بیرونی اعمال شود و نه به خود جدول:
<div class="table-responsive">
  <table class="table">
  ...
  </table>
</div>
نظرات مطالب
نمایش اخطارها و پیام‌های بوت استرپ به کمک TempData در ASP.NET MVC
- در اینجا هدف نمایش پیام‌ها در صفحه ایی دیگر است.
- پبام‌ها زمانی نمایش داده می‌شوند که TempData حاوی اطلاعات ارسالی باشد. فراخوانی partial نیز یکبار درون Layout انجام می‌شود.
مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت دهم - کار با فرم‌ها - قسمت اول
هر برنامه‌ی وبی، نیاز به کار با فرم‌های وب را دارد و به همین جهت، AngularJS 2.0 به همراه دو نوع از فرم‌ها است: فرم‌های مبتنی بر قالب‌ها و فرم‌های مبتنی بر مدل‌ها.
کار با فرم‌های مبتنی بر قالب‌ها ساده‌تر است؛ اما کنترل کمتری را بر روی مباحث اعتبارسنجی داده‌های ورودی توسط کاربر، در اختیار ما قرار می‌دهند. اما فرم‌های مبتنی بر مدل‌ها هر چند به همراه اندکی کدنویسی بیشتر هستند، اما کنترل کاملی را جهت اعتبارسنجی ورودی‌های کاربران، ارائه می‌دهند. در این قسمت فرم‌های مبتنی بر قالب‌ها (Template-driven forms) را بررسی می‌کنیم.


ساخت فرم مبتنی بر قالب‌های ثبت یک محصول جدید

در ادامه‌ی مثال این سری، می‌خواهیم به کاربران، امکان ثبت اطلاعات یک محصول جدید را نیز بدهیم. به همین جهت فایل‌های جدید product-form.component.ts و product-form.component.html را به پوشه‌ی App\products برنامه اضافه می‌کنیم (جهت تعریف کامپوننت فرم جدید به همراه قالب HTML آن).
الف) محتوای کامل product-form.component.html
<form #f="ngForm" (ngSubmit)="onSubmit(f.form)">
    <div class="panel panel-default">
        <div class="panel-heading">
            <h3 class="panel-title">
                Add Product
            </h3>
        </div>
        <div class="panel-body form-horizontal">
            <div class="form-group">
                <label for="productName" class="col col-md-2 control-label">Name</label>
                <div class="controls col col-md-10">
                    <input ngControl="productName" id="productName" required
                           #productName="ngForm"
                           (change)="log(productName)"
                           minlength="3"
                           type="text" class="form-control"
                           [(ngModel)]="productModel.productName"/>
                    <div *ngIf="productName.touched && productName.errors">
                        <label class="text-danger" *ngIf="productName.errors.required">
                            Name is required.
                        </label>
                        <label class="text-danger" *ngIf="productName.errors.minlength">
                            Name should be minimum {{ productName.errors.minlength.requiredLength }} characters.
                        </label>
                    </div>
                </div>
            </div>
            <div class="form-group">
                <label for="productCode" class="col col-md-2 control-label">Code</label>
                <div class="controls col col-md-10">
                    <input ngControl="productCode" id="productCode" required
                           #productCode="ngForm"
                           type="text" class="form-control"
                           [(ngModel)]="productModel.productCode"/>
                    <label class="text-danger" *ngIf="productCode.touched && !productCode.valid">
                        Code is required.
                    </label>
                </div>
            </div>
            <div class="form-group">
                <label for="releaseDate" class="col col-md-2 control-label">Release Date</label>
                <div class="controls col col-md-10">
                    <input ngControl="releaseDate" id="releaseDate" required
                           #releaseDate="ngForm"
                           type="text" class="form-control"
                           [(ngModel)]="productModel.releaseDate"/>
                    <label class="text-danger" *ngIf="releaseDate.touched && !releaseDate.valid">
                        Release Date is required.
                    </label>
                </div>
            </div>
            <div class="form-group">
                <label for="price" class="col col-md-2 control-label">Price</label>
                <div class="controls col col-md-10">
                    <input ngControl="price" id="price" required
                           #price="ngForm"
                           type="text" class="form-control"
                           [(ngModel)]="productModel.price"/>
                    <label class="text-danger" *ngIf="price.touched && !price.valid">
                        Price is required.
                    </label>
                </div>
            </div>
            <div class="form-group">
                <label for="description" class="col col-md-2 control-label">Description</label>
                <div class="controls col col-md-10">
                    <textarea ngControl="description" id="description" required
                              #description="ngForm"
                              rows="10" type="text" class="form-control"
                              [(ngModel)]="productModel.description"></textarea>
                    <label class="text-danger" *ngIf="description.touched && !description.valid">
                        Description is required.
                    </label>
                </div>
            </div>
            <div class="form-group">
                <label for="imageUrl" class="col col-md-2 control-label">Image</label>
                <div class="controls col col-md-10">
                    <input ngControl="imageUrl" id="imageUrl" required
                           #imageUrl="ngForm"
                           type="text" class="form-control"
                           [(ngModel)]="productModel.imageUrl"/>
                    <label class="text-danger" *ngIf="imageUrl.touched && !imageUrl.valid">
                        Image is required.
                    </label>
                </div>
            </div>
        </div>
        <footer class="panel-footer">
            <button [disabled]="!f.valid"
                    type="submit" class="btn btn-primary">
                Submit
            </button>
        </footer>
    </div>
</form>

ب) محتوای کامل product-form.component.ts
import { Component } from 'angular2/core';
import { Router } from 'angular2/router';
import { IProduct } from './product';
import { ProductService } from './product.service';
 
@Component({
    //selector: 'product-form',
    templateUrl: 'app/products/product-form.component.html'
})
export class ProductFormComponent {
 
    productModel = <IProduct>{}; // creates an empty object of an interface
 
    constructor(private _productService: ProductService, private _router: Router) { }
 
    log(productName): void {
        console.log(productName);
    }
 
    onSubmit(form): void {
        console.log(form);
        console.log(this.productModel);
 
        this._productService.addProduct(this.productModel)
            .subscribe((product: IProduct) => {
                console.log(`ID: ${product.productId}`);
                this._router.navigate(['Products']);
            });
    }
}

اکنون ریز جزئیات و تغییرات این دو فایل را قدم به قدم بررسی خواهیم کرد.

تا اینجا در فایل product-form.component.html یک فرم ساده‌ی HTML ایی مبتنی بر بوت استرپ 3 را تهیه کرده‌ایم. نکات ابتدایی آن، دقیقا مطابق است با مستندات بوت استرپ 3؛ از لحاظ تعریف form-horizontal و سپس ایجاد یک div با کلاس form-group و قرار دادن المان‌هایی با کلاس‌های form-control در آن. همچنین برچسب‌های تعریف شده‌ی با ویژگی for، در این المان‌ها، جهت بالارفتن دسترسی پذیری به عناصر فرم، اضافه شده‌اند. این مراحل در مورد تمام فرم‌های استاندارد وب صادق هستند و نکته‌ی جدیدی ندارند.

در ادامه تعاریف AngularJS 2.0 را به این فرم اضافه کرد‌ه‌ایم. در اینجا هر کدام از المان‌های ورودی، تبدیل به Controlهای AngularJS 2.0 شده‌اند. کلاس Control، خواص ویژه‌ای را در اختیار ما قرار می‌دهد. برای مثال value یا مقدار این المان چیست؟ وضعیت touched و untouched آن چیست؟ (آیا کاربر فوکوس را به آن منتقل کرده‌است یا خیر؟) آیا dirty است؟ (مقدار آن تغییر کرده‌است؟) و یا شاید هم pristine است؟ (مقدار آن تغییری نکرده‌است). علاوه بر این‌ها دارای خاصیت valid نیز می‌باشد (آیا اعتبارسنجی آن موفقیت آمیز است؟)؛ به همراه خاصیت errors که مشکلات اعتبارسنجی موجود را باز می‌گرداند.
<div class="form-group">
    <label for="description" class="col col-md-2 control-label">Description</label>
    <div class="controls col col-md-10">
        <textarea ngControl="description" id="description" required
                  #description="ngForm"
                  rows="10" type="text" class="form-control"
                  [(ngModel)]="productModel.description"></textarea>
        <label class="text-danger" *ngIf="description.touched && !description.valid">
            Description is required.
        </label>
    </div>
</div>
در اینجا کلاس مفید دیگری به نام ControlGroup نیز درنظر گرفته شده‌است. برای مثال هر فرم، یک ControlGroup است (گروهی متشکل از کنترل‌ها، در صفحه). البته می‌توان یک فرم بزرگ را به چندین ControlGroup نیز تقسیم کرد. تمام خواصی که برای کلاس Control ذکر شدند، در مورد کلاس ControlGroup نیز صادق هستند. با این تفاوت که این‌بار اگر به خاصیت valid آن مراجعه کردیم، یعنی تمام کنترل‌های قرار گرفته‌ی در آن گروه معتبر هستند و نه صرفا یک تک کنترل خاص. به همین ترتیب خاصیت errors نیز تمام خطاهای اعتبارسنجی یک گروه را باز می‌گرداند.
هر دو کلاس Control و ControlGroup از کلاس پایه‌ای به نام AbstractControl مشتق شده‌اند و این کلاس پایه است که خواص مشترک یاد شده را به همراه دارد.

بنابراین برای کار ساده‌تر با یک فرم AngularJS 2.0، کل فرم را تبدیل به یک ControlGroup کرده و سپس هر کدام از المان‌های ورودی را تبدیل به یک Control مجزا می‌کنیم. کار برقراری این ارتباط، با استفاده از دایرکتیو ویژه‌ای به نام ngControl انجام می‌شود. بنابراین دایرکتیو ngControl، با نامی دلخواه و معین، به تمام المان‌های ورودی، انتساب داده شده‌است.
هرچند در این مثال نام ngControl‌ها با مقدار id هر کنترل یکسان درنظر گرفته شده‌است، اما ارتباطی بین این دو نیست. مقدار id جهت استفاده‌ی در DOM کاربرد دارد و مقدار ngControl توسط AngularJS 2.0 استفاده می‌شود. جهت رسیدن به کدهایی یکدست، بهتر است این نام‌ها را یکسان درنظر گرفت؛ اما هیچ الزامی هم ندارد.

برای بررسی جزئیات این اشیاء کنترل، در المان productName، یک متغیر محلی را به نام productName# تعریف کرده‌ایم و آن‌را به دایرکتیو ngControl انتساب داده‌ایم. این انتساب توسط ngForm انجام شده‌است. زمانیکه AngularJS 2.0 یک متغیر محلی تنظیم شده‌ی به ngForm را مشاهده می‌کند، آن‌را به صورت خودکار به ngControl همان المان ورودی متصل می‌کند. سپس این متغیر محلی را به متد log ارسال کرده‌ایم. این متد در کلاس کامپوننت جاری تعریف شده‌است و کار آن نمایش شیء Control جاری در کنسول developer tools مرورگر است.
<input ngControl="productName" id="productName" required
       #productName="ngForm"
       (change)="log(productName)"
       minlength="3"
       type="text" class="form-control"
       [(ngModel)]="productModel.productName"/>


همانطور که در تصویر مشاهده می‌کنید، عناصر یک شیء Control، در کنسول نمایش داده شده‌اند و در اینجا بهتر می‌توان خواصی مانند valid و امثال آن‌را که به همراه این کنترل وجود دارند، مشاهده کرد. برای مثال خاصیت dirty آن true است چون مقدار آن المان ورودی، تغییر کرده‌است.

بنابراین تا اینجا با استفاده از دایرکتیو ngControl، یک المان ورودی را به یک شیء Control متصل کردیم. همچنین نحوه‌ی تعریف یک متغیر محلی را در المانی و سپس ارسال آن را به کلاس متناظر با کامپوننت فرم، نیز بررسی کردیم.


افزودن اعتبارسنجی به فرم ثبت محصولات

به کنترل‌هایی که به صورت فوق توسط ngControl ایجاد می‌شوند، اصطلاحا implicitly created controls می‌گویند؛ یا به عبارتی ایجاد آن‌ها به صورت «ضمنی» توسط AngularJS 2.0 انجام می‌شود که نمونه‌ای از آن‌را در تصویر فوق نیز مشاهده کردید. این نوع کنترل‌های ضمنی، امکانات اعتبارسنجی محدودی را در اختیار دارند؛ که تنها سه مورد هستند:
الف) required
ب) minlength
ج) maxlength

این‌ها ویژگی‌های استاندارد اعتبارسنجی HTML 5 نیز هستند. نمونه‌ای از اعمال این موارد را با افزودن ویژگی required، به المان‌های فرم ثبت محصولات فوق، مشاهده می‌کنید.
سپس نیاز داریم تا خطاهای اعتبارسنجی را در مقابل هر المان ورودی نمایش دهیم.
<textarea ngControl="description" id="description" required
          #description="ngForm"
          rows="10" type="text" class="form-control"></textarea>
<div class="alert alert-danger" *ngIf="description.touched && !description.valid">
    Description is required.
</div>
پس از افزودن ویژگی required به یک المان، افزودن و نمایش خطاهای اعتبارسنجی، شامل سه مرحله‌ی زیر است:
الف) ایجاد یک div ساده جهت نمایش پیام خطای اعتبار سنجی
ب) افزودن یک متغیر محلی با # و تنظیم شده‌ی به ngForm، جهت دسترسی به شیء کنترل ایجاد شده
ج) استفاده از این متغیر محلی در دایرکتیو ساختاری ngIf* جهت دسترسی به خاصیت valid آن کنترل. بر مبنای مقدار این خاصیت است که تصمیم گرفته می‌شود، پیام اجباری بودن پر کردن فیلد نمایش داده شود یا خیر.
در اینجا یک سری کلاس بوت استرپ 3 هم جهت نمایش بهتر این پیام خطای اعتبارسنجی، اضافه شده‌اند.

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



بهبود شیوه نامه‌ی پیش فرض المان‌های ورودی اطلاعات در AngularJS 2.0

می‌خواهیم اگر اعتبارسنجی یک المان ورودی با شکست مواجه شد، یک حاشیه‌ی قرمز، در اطراف آن نمایش داده شود. این مورد را با توجه به اینکه AngularJS 2.0، شیوه نامه‌های ویژه‌ای را به صورت خودکار به المان‌ها اضافه می‌کند، می‌توان به صورت سراسری به تمام فرم‌ها اضافه کرد. برای این منظور فایل app.component.css واقع در ریشه‌ی پوشه‌ی app را گشوده و تنظیمات ذیل را به آن اضافه کنید:
.ng-touched.ng-invalid{
    border: 1px solid red;
}

ویژگی‌های اضافه شده‌ی در حالت شکست اعتبارسنجی؛ مانند ng-invalid


ویژگی‌های اضافه شده‌ی در حالت موفقیت اعتبارسنجی؛ مانند ng-valid



مدیریت چندین ویژگی اعتبارسنجی یک المان با هم

گاهی از اوقات نیاز است برای یک المان ورودی، چندین نوع اعتبارسنجی مختلف را تعریف کرد. برای مثال فرض کنید که ویژگی‌های required و همچنین minlength، برای نام محصول تنظیم شده‌اند. در این حالت ذکر productName.valid خیلی عمومی است و هر دو حالت اجباری بودن فیلد و حداقل طول آن‌را با هم به همراه دارد:
<div class="alert alert-danger" *ngIf="productName.touched && !productName.valid">
   Name is required.
</div>
بنابراین در این حالت از روش ذیل استفاده می‌شود:
<div *ngIf="productName.touched && productName.errors">
    <div class="alert alert-danger" *ngIf="productName.errors.required">
        Name is required.
    </div>
    <div class="alert alert-danger" *ngIf="productName.errors.minlength">
        Name should be minimum 3 characters.
    </div>
</div>
خاصیت errors نیز یکی دیگر از خواص شیء کنترل است. اگر نال بود، یعنی خطایی وجود ندارد و در غیراینصورت، به ازای هر نوع اعتبارسنجی تعریف شده، خواصی به آن اضافه می‌شوند. بنابراین ذکر productName.errors.required به این معنا است که آیا خاصیت errors، دارای کلیدی به نام required است؟ اگر بله، یعنی این فیلد هنوز پر نشده‌است.
همچنین چون در این حالت productName.touched نیاز است چندین بار تکرار شود، می‌توان آن‌را در یک div محصور کننده‌ی دو div مورد نیاز جهت نمایش خطاهای اعتبارسنجی قرار داد. به علاوه بررسی نال نبودن productName.errors نیز در div محصور کننده صورت گرفته‌است و دیگر نیازی نیست این بررسی را به ngIfهای داخلی اضافه کرد.

نکته 1
اگر علاقمند بودید تا جزئیات خاصیت errors را مشاهده کنید، آن‌را می‌توان توسط pipe توکاری به نام json به صورت موقت نمایش داد و بعد آن‌را حذف کرد:
 <div *ngIf="productName.touched && productName.errors">
  {{ productName.errors | json }}

نکته 2
بجای ذکر مستقیم عدد سه در «minimum 3 characters»، می‌توان این عدد را مستقیما از تعریف ویژگی minlength نیز استخراج کرد:
 Name should be minimum {{ productName.errors.minlength.requiredLength }} characters.


بررسی ngForm

شبیه به ngControl که یک المان ورودی را به یک کنترل AngularJS 2.0 متصل می‌کند، دایرکتیو دیگری نیز به نام ngForm وجود دارد که کل فرم را به شیء ControlGroup بایند می‌کند و برخلاف ngControl، نیازی به ذکر صریح آن وجود ندارد. هر زمانیکه AngularJS 2.0، المان استاندارد فرمی را در صفحه مشاهده می‌کند، این اتصالات را به صورت خودکار برقرار خواهد کرد.
ngForm دارای خاصیتی است به نام ngSumbit که از نوع EventEmitter است (نمونه‌ای از آن را در مبحث کامپوننت‌های تو در تو پیشتر ملاحظه کرده‌اید). بنابراین از آن می‌توان جهت اتصال رخداد submit فرم، به متدی در کلاس کامپوننت خود، استفاده کرد. متد متصل به این رخداد، زمانی فراخوانی می‌شود که کاربر بر روی دکمه‌ی submit کلیک کند:
 <form #f="ngForm" (ngSubmit)="onSubmit(f.form)">
همچنین در اینجا متغیر محلی f جهت دسترسی به شیء ControlGroup و ارسال آن به متد onSubmit تعریف شده‌است (شبیه به متغیرهای محلی دسترسی به ngControl که پیشتر جهت نمایش خطاهای اعتبارسنجی، اضافه کردیم).

پس از تعریف این رخداد و اتصال آن در قالب کامپوننت، اکنون می‌توان متد onSubmit را در کلاس آن نیز اضافه کرد.
onSubmit(form): void {
   console.log(form);
}
فعلا هدف از این متد، نمایش جزئیات شیء form دریافتی، در کنسول developer tools است.



غیرفعال کردن دکمه‌ی submit در صورت وجود خطاهای اعتبارسنجی

در قسمت بررسی ngForm، یک متغیر محلی را به نام f ایجاد کردیم که به شیء ControlGroup فرم جاری اشاره می‌کند. از این متغیر و خاصیت valid آن می‌توان با کمک property binding به خاصیت disabled یک دکمه، آن‌را به صورت خودکار فعال یا غیرفعال کرد:
<button [disabled]="!f.valid"
        type="submit" class="btn btn-primary">
    Submit
</button>
هر زمانیکه کل فرم از لحاظ اعتبارسنجی مشکلی نداشته باشد، دکمه‌ی submit فعال می‌شود و برعکس.



نمایش فرم افزودن محصولات توسط سیستم Routing

با نحوه‌ی تعریف مسیریابی‌ها در قسمت قبل آشنا شدیم. برای نمایش فرم افزودن محصولات، می‌توان تغییرات ذیل را به فایل app.component.ts اعمال کرد:
//same as before...
import { ProductFormComponent }  from './products/product-form.component';
 
@Component({
    //same as before…
    template: `
                //same as before…
                    <li><a [routerLink]="['AddProduct']">Add Product</a></li>
               //same as before…
    `,
    //same as before…
})
@RouteConfig([
    //same as before…
    { path: '/addproduct', name: 'AddProduct', component: ProductFormComponent }
])
//same as before...
ابتدا به RouteConfig، مسیریابی کامپوننت فرم افزودن محصولات اضافه شده‌است. سپس ماژول این کلاس در ابتدای فایل import شده و در آخر routerLink آن به قالب سایت و منوی بالای سایت اضافه شده‌است.



اتصال المان‌های فرم به مدلی جهت ارسال به سرور

برای اتصال المان‌های فرم به یک مدل، این مدل را به صورت یک خاصیت عمومی، در سطح کلاس کامپوننت فرم، تعریف می‌کنیم:
 productModel = <IProduct>{}; // creates an empty object of an interface
اگر از اینترفیسی مانند IProduct که در قسمت‌های قبل این سری تعریف شد، نیاز است شیء جدیدی ساخته شود، الزاما نیازی نیست تا یک کلاس جدید را از آن مشتق کرد و بعد متغیر new ClassName را تهیه کرد. در TypeScript می‌توان به صورت خلاصه از syntax فوق نیز استفاده کرد.
پس از تعریف خاصیت productModel، اکنون کافی است با استفاده از two-way data binding، آن‌را به المان‌های فرم نسبت دهیم. برای مثال:
<textarea ngControl="description" id="description" required
          #description="ngForm"
          rows="10" type="text" class="form-control"
          [(ngModel)]="productModel.description"></textarea>
در اینجا با استفاده از ngModel و انقیاد دو طرفه، کار اتصال به خاصیت توضیحات شیء محصول انجام شده‌است. اکنون بلافاصله تغییرات اعمالی به فرم، به مدل متناظر منعکس می‌شود و برعکس. این ngModel را به تمام المان‌های ورودی فرم متصل خواهیم کرد.
پس از تعریف یک چنین اتصالی، دیگر نیازی به مقدار دهی پارامتر onSubmit(f.form) نیست. زیرا شیء productModel، در متد onSumbit در دسترس است و این شیء همواره حاوی آخرین تغییرات اعمالی به المان‌های فرم است.

پس از اینکه فرم را به مدل آن متصل کردیم، فایل product.service.ts را گشوده و متد جدید addProduct را به آن اضافه کنید:
addProduct(product: IProduct): Observable<IProduct> {
    let headers = new Headers({ 'Content-Type': 'application/json' }); // for ASP.NET MVC
        let options = new RequestOptions({ headers: headers });
 
    return this._http.post(this._addProductUrl, JSON.stringify(product), options)
        .map((response: Response) => <IProduct>response.json())
        .do(data => console.log("Product: " + JSON.stringify(data)))
        .catch(this.handleError);
}
کار این متد، ارسال شیء محصول به یک اکشن متد برنامه‌ی ASP.NET MVC جاری است. با جزئیات کار با obsevables درمطلب «دریافت اطلاعات از سرور» پیشتر آشنا شده‌ایم.
نکته‌ی مهم اینجا است که content type پیش فرض ارسالی متد post آن، plain text است و در این حالت ASP.NET MVC شیء JSON دریافتی از کلاینت را پردازش نخواهد کرد. بنابراین نیاز است تا هدر content type را به صورت صریحی در اینجا ذکر نمود؛ در غیراینصورت در سمت سرور، شاهد نال بودن مقادیر دریافتی از کاربران خواهیم بود.
امضای سمت سرور متد دریافت اطلاعات از کاربر، چنین شکلی را دارد (تعریف شده در فایل Controllers\HomeController.cs):
 [HttpPost]
public ActionResult AddProduct(Product product)
{

اشیاء هدرها و تنظیمات درخواست، در متد addProduct سرویس ProductService، در ماژول‌های ذیل تعریف شده‌اند که باید به ابتدای فایل product.service.ts اضافه شوند:
 import { Headers, RequestOptions } from 'angular2/http';

پس از تعریف متد addProduct در سرویس ProductService، اکنون با استفاده از ترزیق این سرویس به سازنده‌ی کلاس فرم ثبت یک محصول جدید، می‌توان متد this._productService.addProduct را جهت ارسال productModel به سمت سرور، در متد onSubmit فراخوانی کرد:
//Same as before…
import { IProduct } from './product';
import { ProductService } from './product.service';
 
@Component({
//Same as before…
})
export class ProductFormComponent {
 
    productModel = <IProduct>{}; // creates an empty object of an interface
 
    constructor(private _productService: ProductService, private _router: Router) { }
 
    //Same as before… 

    onSubmit(form): void {
        console.log(form);
        console.log(this.productModel);
 
        this._productService.addProduct(this.productModel)
            .subscribe((product: IProduct) => {
                console.log(`ID: ${product.productId}`);
                this._router.navigate(['Products']);
            });
    }
}
همانطور که ذکر شد، از آنجائیکه شیء productModel حاوی آخرین تغییرات اعمالی توسط کاربر است، اکنون می‌توان پارامتر form متد onSubmit را حذف کرد.
در اینجا پس از فراخوانی متد addProduct، متد subscribe، در انتهای زنجیره، فراخوانی شده‌است. کار آن هدایت کاربر به صفحه‌ی نمایش لیست محصولات است. در اینجا this._router از طریق تزریق وابستگی‌های سرویس مسیریاب به سازنده‌ی کلاس، تامین شده‌است. نمونه‌ی آن‌را در قسمت «افزودن دکمه‌ی back با کدنویسی» مربوطه به مطلب آشنایی با مسیریابی، پیشتر مطالعه کرده‌اید.



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


خلاصه‌ی بحث

فرم‌های template driven در AngularJS 2.0 به این نحو طراحی می‌شوند:
 1) ابتدا فرم HTML را به حالت معمولی آن طراحی می‌کنیم؛ با تمام المان‌های آن.
 2) به تمام المان‌های فرم، دیراکتیو ngControl را متصل می‌کنیم، تا AngularJS 2.0 آن‌را تبدیل به یک کنترل خاص خودش کند. کنترلی که دارای خواصی مانند valid و touched است.
 3) سپس برای دسترسی به این کنترل ایجاد شده‌ی به صورت ضمنی، یک متغیر محلی آغاز شده‌ی با # را به تمام المان‌ها اضافه می‌کنیم.
 4) اعتبارسنجی‌هایی را مانند required  به المان‌های فرم اضافه می‌کنیم.
 5) از متغیر محلی تعریف شده و ngIf* برای بررسی خواصی مانند valid و touched برای نمایش خطاهای اعتبارسنجی کمک گرفته می‌شود.
 6) پس از تعریف فرم، تعریف ngControlها، تعریف متغیر محلی شروع شده‌ی با # و افزودن خطاهای اعتبارسنجی، اکنون نوبت به ارسال این اطلاعات به سرور است. بنابراین رخداد ngSubmit را باید به متدی در کلاس کامپوننت جاری متصل کرد.
 7) اکنون که با کلیک بر روی دکمه‌ی submit فرم، متد onSubmit متصل به ngSubmit فراخوانی می‌شود، نیاز است بین المان‌های فرم HTML و کلاس کامپوننت، ارتباط برقرار کرد. این‌کار را توسط two-way data binding و تعریف ngModel بر روی تمام المان‌های فرم، انجام می‌دهیم. این ngModelها، به یک خاصیت عمومی که متناظر است با وهله‌ای از شیء مدل فرم، متصل هستند. بنابراین این مدل، در هر لحظه، بیانگر آخرین تغییرات کاربر است و از آن می‌توان برای ارسال اطلاعات به سرور استفاده کرد.
 8) پس از اتصال فرم به کلاس متناظر با آن، اکنون سرویس محصولات را تکمیل کرده و به آن متد HTTP Post را جهت ارسال اطلاعات سمت کاربر، به سرور، اضافه می‌کنیم. در اینجا نکته‌ی مهم، تنظیم content type ارسالی به سمت سرور است. در غیراینصورت فریم ورک سمت سرور قادر به تشخیص JSON بودن این اطلاعات نخواهد شد.
مطالب
فعال سازی سطح دوم کش در Fluent NHibernate

سطح اول کش در NHibernate در یک تراکنش معنا پیدا می‌کند (+)؛ اما نتایج حاصل از اعمال سطح دوم (+) آن، در اختیار تمام تراکنش‌های جاری برنامه خواهند بود. در ادامه قصد داریم نحوه فعال سازی سطح دوم کش NHibernate را توسط Fluent NHibernate بررسی کنیم.

الف) دریافت کش پروایدر
برای این منظور به صفحه اصلی آن در سایت سورس فورج مراجعه نمائید(+). اگر به علت تحریم‌ها امکان دریافت فایل‌های مرتبط را نداشتید از این برنامه استفاده کنید(+). پس از دریافت، می‌خواهیم نحوه فعال سازی NHibernate.Caches.SysCache.dll را بررسی کنیم (این اسمبلی، در برنامه‌های وب و دسکتاپ بدون مشکل کار می‌کند).

ب) اعمال به قسمت تعاریف اولیه
پس از دریافت اسمبلی NHibernate.Caches.SysCache.dll و افزودن ارجاعی به آن، اکنون نوبت به معرفی آن به تنظیمات Fluent NHibernate‌ می‌باشد. این‌کار هم بسیار ساده است:
...
.ConnectionString(x => x.FromConnectionStringWithKey(...))
.Cache(x => x.UseQueryCache()
.UseMinimalPuts()
.ProviderClass<NHibernate.Caches.SysCache.SysCacheProvider>())
...

ج) تعریف نوع کش در هنگام ایجاد نگاشت‌ها
اگر از ClassMap‌ها برای تعریف نگاشت‌ها استفاده می‌کنید، در انتهای تعاریف یک سطر Cache.ReadWrite را اضافه کنید.
اگر از AutoMapping استفاده می‌کنید، نیاز است تا با استفاده از IAutoMappingOverride (+) سطر یاد شده اضافه گردد؛ برای مثال:
using FluentNHibernate.Automapping.Alterations;

namespace NH3Test.MappingDefinitions.Domain
{
public class AccountOverrides : IAutoMappingOverride<Account>
{
public void Override(FluentNHibernate.Automapping.AutoMapping<Account> mapping)
{
mapping.Cache.ReadWrite();
}
}
}
تعریف یک سطر فوق هم مهم است؛ زیرا در غیراینصورت فقط primary key حاصل از بار اول فراخوانی کوئری‌های مرتبط کش می‌شوند؛ نه نتیجه عملیات. هرچند این مورد هم یک قدم مثبت به شمار می‌رود از این لحاظ که برای مثال تهیه نتایج کوئری بر روی فیلدی که ایندکس بر روی آن تعریف نشده است همیشه از حالت تهیه کوئری بر روی فیلد دارای ایندکس کندتر است. اما هدف ما در اینجا این است که پس از بار اول فراخوانی کوئری، بار‌های دوم و بعدی دیگر کوئری خاصی را به بانک اطلاعاتی ارسال نکرده و نتایج از کش خوانده شوند (جهت استفاده عموم کاربران در کلیه تراکنش‌های جاری برنامه).

د) اعمال متد Cacheable به کوئر‌ی‌ها
سه مرحله قبل نحوه برپایی مقدماتی سطح دوم کش را بیان می‌کنند و تنها یکبار نیاز است انجام شوند. در ادامه هر جایی که نیاز داشتیم نتایج کوئری مورد نظر کش شوند (و باید دقت داشت که این کش شدن سطح دوم به معنی در دسترس بودن نتایج آن جهت تمام کاربران برنامه در تمام تراکنش‌های جاری برنامه هستند؛ برای مثال نتایج آمار سایت که دسترسی عمومی دارد) تنها کافی است متد Cacheable را به کوئری مورد نظر اضافه کرد؛ برای مثال:
var data = session.QueryOver<Account>()
.Where(s => s.Name == "name")
.Cacheable()
.List();

ه) چگونه صحت اعمال سطح دوم کش را بررسی کنیم؟
برای بررسی این امر باید به خروجی SQL نهایی مراجعه کرد (+). سه تراکنش مجزا را تعریف کنید. در تراکنش اول یک insert ساده، در تراکنش دوم کوئری از اطلاعات قبل (به همراه اعمال متد Cacheable) و در تراکنش سوم مجددا همان کوئری تراکنش دوم را (به همراه اعمال متد Cacheable) تکرار کنید. حاصل کار تنها باید دو عبارت SQL باشند. یک مورد جهت insert و یک مورد هم select . در تراکنش سوم، از نتایج کش شده تراکنش دوم استفاده خواهد شد؛ به همین جهت دیگری کوئری سومی به بانک اطلاعاتی ارسال نخواهد شد.
اگر اعمال مورد (ج) فوق را فراموش کنید، سه کوئری را مشاهده خواهید کرد، اما کوئری سوم با کوئری دوم اندکی متفاوت خواهد بود و بهینه‌تر؛ چون به صورت هوشمند بر اساس جستجوی بر روی primary key تغییر کرده است (صرفنظر از اینکه قسمت where کوئری شما چیست).

مطالب
تبدیل pdf به تصویر توسط GhostScript

در  مطلبی که در همین سایت اشاره شد با استفاده از Adobe Acrobat می‌توان فایل‌های pdf  را به تصویر تبدیل کرد اما چون نیاز بود تا در وب از آن استفاده کنیم و گاهی اوقات امکان نصب Adobe Acrobat Sdk در سرور وجود ندارد می‌توان از روش زیر نیز استفاده کرد.

ابتدا فایل gsdll32.dll را در پوشه bin  پروژه کپی کنید ( این فایل به همراه مثال ارائه شده وجود دارد).

سپس برای متدهای موردنیاز موجود در Api که بصورت Unmanaged می‌باشند یکسری wrapper ایجاد میکنیم. این متدها شامل :

  1. gsapi_new_instance که برای ایجاد یک نمونه جدید از api بکار می‌رود.
  2.  gsapi_init_with_args که برای مقداردهی نمونه ایجاد شده بوسیله آرگومان‌ها بکار می‌رود .
  3. gsapi_delete_instance  و  gsapi_exit برای آزادسازی منابع ایجاد شده. 

در زیر چند آرگومان مهم که باید به api ارسال شوند نیز آمده است:

 -sDEVICE  فرمت تصویر خروجی 
 dFirstPage-  صفحه آغازین برای تبدیل 
dLastPage -  صفحه پایانی برای تبدیل 
dDEVICEWIDTHPOINTS-  اندازه width فایل pdf 
dDEVICEHEIGHTPOINTS- اندازه height فایل pdf
 dDEVICEXRESOLUTION-  resolutionX
-dDEVICEYRESOLUTION  resolutionY
sOutputFile- مسیر فایل(های) خروجی 
  مسیر فایل ورودی 
نکته اول : برای حالتی که قصد دارید بیش از یک صفحه از فایل pdf را به تصویر تبدیل کنید، کافیست در هنگام مقداردهی به پارامتر –sOutputFile درهرکجای آن علامت %d را قرار دهید تا بطور خودکار شمارنده ای برای نام فایل‌ها در نظر گرفته شود. بطور مثال img%d باعث می‌شود که تصاویر تولید شده بصورت img1 و img2 و img3 و غیره ایجاد شوند.
 
نکته دوم :
هنگامی که خواستم از این api درون وب استفاده کنم و از آنجا که سیستم عامل windows server 2008 x64 روی سرور نصب بود موقع دریافت خروجی با خطای زیر مواجه می‌شدم:

BadImageFormatException: An attempt was made to load a program with an incorrect format. Exception from HRESULT: 0x8007000B

برای حل این مشکل IIS  را باز میکنیم و بر روی ApplicationPool ای که برای وب سایت خودمان درنظر گرفتیم کلیک راست کرده و گزینه Advanced Setting را انتخاب میکنیم. با باز شدن این دیالوگ گزینه Enable 32-bit Application را به true تنظیم میکنیم. 

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

مطالب
React 16x - قسمت 6 - کامپوننت‌ها - بخش 3 - یک تمرین
در این قسمت می‌خواهیم دانسته‌های 5 قسمت قبل را در طی یک تمرین کنار هم قرار داده و مرور کنیم.


برپایی ساختار ابتدایی پروژه‌ی تمرین

ابتدا یک پروژه‌ی جدید React را ایجاد می‌کنیم:
> create-react-app sample-05
> cd sample-05
> npm start
سپس بسته‌های بوت استرپ و font-awesome را نیز در آن نصب می‌کنیم:
> npm install --save bootstrap
> npm install --save font-awesome

در ادامه نیاز است فایل‌های CSS این کتابخانه‌ها و قلم‌های وب را import کنیم. به همین جهت ابتدای فایل index.js را به نحو زیر ویرایش خواهیم کرد:
import "bootstrap/dist/css/bootstrap.css";
import "font-awesome/css/font-awesome.css";
در نهایت کار مدیریت این فایل‌ها و قرار دادن آن‌ها در بسته‌ی نهایی برنامه، توسط webpack به صورت خودکار انجام می‌شود.

همچنین به فایل index.css هم مراجعه کرده و یک padding را به بالای صفحه اضافه می‌کنیم؛ تا اطلاعات نمایش داده شده، با کمی فاصله از لبه‌ی مرورگر رندر شوند:
body {
  margin: 0;
  padding: 20px 0 0 0;
  font-family: sans-serif;
}

پس از نصب و import این کتابخانه‌های ثالث، به فایل App.js مراجعه کرده و کلاس container اصلی بوت استرپ را در آن تعریف می‌کنیم تا در برگیرنده‌ی محتوای برنامه شود:
  return (
    <main className="container">
      <h1>Hello world!</h1>
    </main>
  );
همانطور که در قسمت چهارم نیز بحث شد، برای ذکر classهای عناصر در React، از خاصیت className استفاده می‌شود.


معرفی سرویس‌های داده‌ی برنامه

کدهای نهایی این قسمت را از فایل پیوست شده‌ی در انتهای مطلب، می‌توانید دریافت کنید. در اینجا یک پوشه‌ی src\services تعریف شده‌است که داخل آن دو فایل fakeGenreService.js و fakeMovieService.js قرار دارند. این فایل‌ها، منبع داده‌ی درون حافظه‌ای مثال تمرین ما هستند.
سرویس fakeGenre چنین ساختاری را دارد و ژانرهای سینمایی، مانند اکشن، کمدی و غیره در آن لیست شده‌اند:
export const genres = [
  { _id: "5b21ca3eeb7f6fbccd471818", name: "Action" },
  // ...
];
این سرویس دارای متد ()getGenres، برای بازگشت لیست کامل genres است. علت ذکر خاصیت id با یک _، روش نامگذاری خاصیت id در mongo-db است.

و سرویس fakeMovie که دارای ساختار کلی زیر است، لیست 9 فیلم سینمایی را به همراه دارد:
const movies = [
  {
    _id: "5b21ca3eeb7f6fbccd471815",
    title: "Terminator",
    genre: { _id: "5b21ca3eeb7f6fbccd471818", name: "Action" },
    numberInStock: 6,
    dailyRentalRate: 2.5,
    publishDate: "2018-01-03T19:04:28.809Z"
  },

  //...
];
به علاوه این سرویس دارای متدهای ()getMovies برای دریافت لیست فیلم‌ها، getMovie(id) برای بازگشت یک فیلم خاص، saveMovie(movie) برای افزودن یک فیلم جدید به لیست و deleteMovie(id) برای حذف یک فیلم از لیست درون حافظه‌ای سرویس جاری است.


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

اکنون می‌خواهیم یک کامپوننت جدید را به نام Movies در فایل جدید src\components\movies.jsx ایجاد کنیم، تا لیست فیلم‌های سرویس fakeMovieService را نمایش دهد. برای اینکار مراحل زیر را طی خواهیم کرد:
- نمایش ساده‌ی لیست فیلم‌ها توسط یک جدول. برای دریافت لیست اشیاء موجود در fakeMovieService، از متد ()getMovies آن می‌توان استفاده کرد.
- اضافه کردن یک دکمه‌ی حذف، به هر ردیف، به نحوی که با کلیک بر روی آن، آن ردیف حذف شود.
- نمایش یک پیام بالای جدول که تعداد فیلم‌های موجود در سرویس درون حافظه‌ای را نمایش می‌دهد. همچنین پس از حذف تمام ردیف‌ها، باید پیام «فیلمی موجود نیست» را نمایش دهد.

خروجی نهایی مثال ما به صورت زیر است:


و اگر تمام آیتم‌های آن‌را حذف کنیم، چنین پیامی نمایش داده می‌شود:


پس از ایجاد فایل خالی جدید movies.jsx در پوشه‌ی جدید components، با استفاده از «simple react snippets» نصب شده‌ی در VSCode، یکبار imrc را تایپ کرده (مخفف import react component است) و سپس دکمه‌ی tab را فشار می‌دهیم، در آخر اینکار را برای cc نیز تکرار می‌کنیم (مخفف create class است) تا importها و سپس ساختار ابتدایی کامپوننت React ما تشکیل شوند. نام این کامپوننت را هم Movies که با حرف بزرگ شروع می‌شود، وارد می‌کنیم.

اکنون مجددا به App.js مراجعه می‌کنیم و بجای Hello world ای که نمایش دادیم، کامپوننت Movies را اضافه می‌کنیم. برای این منظور ابتدا import آن‌را به ابتدای فایل اضافه می‌کنیم:
import Movies from "./components/movies";
سپس متد return آن‌را جهت درج المان کامپوننت Movies اصلاح خواهیم کرد:
return (
    <main className="container">
      <Movies />
    </main>
  );


دریافت لیست اشیاء فیلم‌ها از سرویس fakeMovieService

برای دریافت لیست اشیاء فیلم‌ها، ابتدا تعریف سرویس آن‌را به ابتدای کامپوننت Movies اضافه می‌کنیم:
 import { getMovies } from "../services/fakeMovieService";
در اینجا از {} استفاده شده، چون یک named export را import کرده‌ایم.

سپس خاصیت state را جهت تعریف خاصیت movies که با متد ()getMovies سرویس fakeMovieService مقدار دهی می‌شود، به نحو زیر تکمیل می‌کنیم:
 state = {
    movies: getMovies()
  };
البته این روش مقدار دهی اولیه‌ی خاصیت state، برای دریافت اطلاعات سرویس‌ها، هرچند در اینجا بدون مشکل کار می‌کند، اما بهتر است توسط component life cycle hooks مدیریت شود که در قسمت‌های بعدی بیشتر به جزئیات آن‌ها خواهیم پرداخت.


نمایش لیست فیلم‌ها، به همراه مدیریت حذف هر ردیف

در ادامه، کدهای کامل و تکمیل شده‌ی این کامپوننت را ملاحظه می‌کنید:
import React, { Component } from "react";

import { getMovies } from "../services/fakeMovieService";

class Movies extends Component {
  state = {
    movies: getMovies()
  };

  handleDelete = movie => {
    const movies = this.state.movies.filter(m => m._id !== movie._id);
    this.setState({ movies });
  };

  render() {
    const { length: count } = this.state.movies;

    if (count === 0) return <p>There are no movies in the database.</p>;

    return (
      <React.Fragment>
        <p>Showing {count} movies in the database.</p>
        <table className="table">
          <thead>
            <tr>
              <th>Title</th>
              <th>Genre</th>
              <th>Stock</th>
              <th>Rate</th>
              <th />
            </tr>
          </thead>
          <tbody>
            {this.state.movies.map(movie => (
              <tr key={movie._id}>
                <td>{movie.title}</td>
                <td>{movie.genre.name}</td>
                <td>{movie.numberInStock}</td>
                <td>{movie.dailyRentalRate}</td>
                <td>
                  <button
                    onClick={() => this.handleDelete(movie)}
                    className="btn btn-danger btn-sm"
                  >
                    Delete
                  </button>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </React.Fragment>
    );
  }
}

export default Movies;
توضیحات:
همانطور که در ابتدای بحث نیز ذکر شد، هدف از این تمرین، مرور قسمت‌های قبل است و تمام نکات زیر را در قسمت‌های پیشین، با جزئیات بیشتری بررسی کرده‌ایم:

- ابتدا خاصیت state و سپس خاصیت movies شیء منتسب به آن، با لیست فیلم‌های موجود در سرویس مرتبط، مقدار دهی شده‌اند.
- سپس در ابتدای متد render، کار رندر شرطی انجام شده‌است. اگر تعداد فیلم‌های دریافتی صفر بود، پیام «فیلمی در بانک اطلاعاتی موجود نیست» نمایش داده می‌شود و در غیراینصورت، جدول اصلی بوت استرپی برنامه رندر خواهد شد.
در اینجا چون از خاصیت طول آرایه‌ی فیلم‌ها در چندین قسمت قرار است استفاده شود، آن‌را توسط Object Destructuring به یک متغیر نسبت داده‌ایم. همچنین توسط یک نام مستعار هم خاصیت length را با نام جدید count استفاده می‌کنیم.
- در ادامه بازگشت React.Fragment را مشاهده می‌کنید. علت اینجا است که نمی‌خواهیم div اضافه‌تری را در UI رندر کنیم. React.Fragment سبب می‌شود تا بتوانیم چندین فرزند را به المان جاری تبدیل شده‌ی به کدهای جاوا اسکریپتی اضافه کنیم، بدون اینکه خودش به المانی ترجمه شود.
- پس از return، یک () قابل مشاهده‌است. چون خروجی return ما چند سطری است، اگر در سطری که return قرار می‌گیرد، اطلاعاتی درج نشود، موتور جاوا اسکریپت آن‌را با یک سمی‌کالن خاتمه خواهد داد! و دیگر سطرهای بعدی دیده نمی‌شوند و پردازش نخواهند شد. به همین جهت از روش ذکر یک () پس از return در فایل‌های jsx زیاد استفاده می‌شود.
- در ابتدای return، همان خاصیت count را نمایش می‌دهیم.
- سپس کار رندر جدول اصلی برنامه که با کلاس‌های جداول بوت استرپ نیز مزین شده، انجام شده‌است. در React برای عدم تداخل ویژگی class با نام از پیش رزرو شده‌ی class، از خاصیت className برای ذکر کلاس‌های CSSای استفاده می‌شود.
- قسمت thead این جدول مشخص است و سرستو‌ن‌های جدول را مشخص می‌کند.
- پس از آن نیاز است ردیف‌های جدول را رندر کنیم. این‌کار را توسط متد Array.map، با نگاشت هر آیتم آرایه‌ی this.state.movies، به یک tr جدول انجام داده‌ایم.
- React برای اینکه بتواند DOM مجازی خودش را کنترل کند، نیاز دارد عناصر موجود در آن‌را به صورت منحصربفردی تشخص دهد. به همین جهت در اینجا ذکر key را بر روی المان tr که با movie._id مقدار دهی شده‌است، مشاهده می‌کنید.
- رندر مقادیر سلو‌ل‌های ردیف‌ها توسط درج {} و سپس ذکر مقداری از شیء movie دریافتی توسط متد Array.map انجام می‌شود.
- در اینجا ستون رندر دکمه‌ی Delete را نیز مشاهده می‌کنید. برای مدیریت this در آن و دسترسی به شیء movie جاری (ارسال پارامتر به رویداد گردان آن) و همچنین دسترسی به شیء this کلاس جاری برای کار با آرایه‌ی this.state.movies، از روش arrow functions برای تعریف رویدادگردان onClick استفاده کرده‌ایم.
- در متد handleDelete، یک آرایه‌ی جدید را که id ردیف‌های آن با id شیء ردیف انتخابی یکی نیست، بازگشت می‌دهیم. انتساب این آرایه‌ی جدید به آرایه‌ی this.state.movies، تغییری را در برنامه‌های React ایجاد نمی‌کند. در اینجا باید توسط متد this.setState که از کلاس پایه‌ی extends Component دریافت می‌شود، خاصیت movies را بازنویسی کرد تا React از تغییرات مطلع شده و DOM مجازی جدیدی را با مقایسه‌ی با نمونه جدید، محاسبه کرده و به DOM اصلی، جهت به روز رسانی UI اعمال کند.
- البته در اینجا this.setState({ movies }) را بجای this.setState({ movies: movies }) مشاهده می‌کنید. علت اینجا است که اگر عبارات key و value یکی باشند، می‌توان تنها همان عبارت key را جهت حذف تکرار واژه‌ها، ذکر کرد.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-05.zip
مطالب دوره‌ها
آشنایی با AOP Interceptors
در حین استفاده از Interceptors، کار مداخله و تحت نظر قرار دادن قسمت‌های مختلف کدها، توسط کامپوننت‌های خارجی صورت خواهد گرفت. این کامپوننت‌های خارجی، به صورت پویا، تزئین کننده‌هایی را جهت محصور سازی قسمت‌های مختلف کدهای شما تولید می‌کنند. این‌ها، بسته به توانایی‌هایی که دارند، در زمان اجرا و یا حتی در زمان کامپایل نیز قابل تنظیم می‌باشند.


ابزارهایی جهت تولید AOP Interceptors

متداول‌ترین کامپوننت‌های خارجی که جهت تولید AOP Interceptors مورد استفاده قرار می‌گیرند، همان IOC Containers معروف هستند مانند StructureMap، Ninject، MS Unity و غیره.
سایر ابزارهای تولید AOP Interceptors، از روش تولید Dynamic proxies بهره می‌گیرند. به این ترتیب مزین کننده‌هایی پویا، در زمان اجرا، کدهای شما را محصور خواهند کرد. (نمونه‌ای از آن‌را شاید در حین کار با ORMهای مختلف دیده باشید).


نگاهی به فرآیند Interception

زمانیکه از یک IOC Container در کدهای خود استفاده می‌کنید، مراحلی چند رخ خواهند داد:
الف) کد فراخوان، از IOC Container، یک شیء مشخص را درخواست می‌کند. عموما اینکار با درخواست یک اینترفیس صورت می‌گیرد؛ هرچند محدودیتی نیز وجود نداشته و امکان درخواست یک کلاس از نوعی مشخص نیز وجود دارد.
ب) در ادامه IOC Container به لیست اشیاء قابل ارائه توسط خود نگاه کرده و در صورت وجود، وهله سازی شیء درخواست شده را انجام و نهایتا شیء مطلوب را بازگشت خواهد داد.
ج) سپس، کد فراخوان، وهله دریافتی را مورد پردازش قرار داده و شروع به استفاده از متدها و خواص آن خواهد نمود.

اکنون با اضافه کردن Interception به این پروسه، چند مرحله دیگر نیز در این بین به آن اضافه خواهند شد:
الف) در اینجا نیز در ابتدا کد فراخوان، درخواست وهله‌ای را بر اساس اینترفیسی خاص به IOC Container ارائه می‌دهد.
ب) IOC Container نیز سعی در وهله سازی درخواست رسیده بر اساس تنظیمات اولیه خود می‌کند.
ج) اما در این حالت IOC Container تشخیص می‌دهد، نوعی که باید بازگشت دهد، علاوه بر وهله سازی، نیاز به مزین سازی توسط  Aspects و پیاده سازی Interceptors را نیز دارد. بنابراین نوع مورد انتظار را در صورت وجود، به یک Dynamic Proxy، بجای بازگشت مستقیم به فراخوان ارائه می‌دهد.
د) در  ادامه Dynamic Proxy، نوع مورد انتظار را توسط Interceptors محصور کرده و به فراخوان بازگشت می‌دهد.
ه) اکنون فراخوان، در حین استفاده از امکانات شیء وهله سازی شده، به صورت خودکار مراحل مختلف اجرای یک Aspect را که در قسمت قبل بررسی شدند، سبب خواهد شد.


نحوه ایجاد Interceptors

برای ایجاد یک Interceptor دو مرحله باید انجام شود:
الف) پیاده سازی یک اینترفیس
ب) اتصال آن به کدهای اصلی برنامه

در ادامه قصد داریم از یک IOC Container معروف به نام StructureMap در یک برنامه کنسول استفاده کنیم. برای دریافت آن نیاز است دستور پاورشل ذیل را در کنسول نوگت ویژوال استودیو فراخوانی کنید:
 PM> Install-Package structuremap
پس از آن یک برنامه کنسول جدید را ایجاد کنید. (هدف از استفاده از این نوع پروژه خاص، توضیح جزئیات یک فناوری، بدون درگیر شدن با لایه UI است)
البته باید دقت داشت که برای استفاده از StructureMap نیاز است به خواص پروژه مراجعه و سپس حالت Client profile را به Full profile تغییر داد تا برنامه قابل کامپایل باشد.
using System;
using StructureMap;

namespace AOP00
{
    public interface IMyType
    {
        void DoSomething(string data, int i);
    }

    public class MyType : IMyType
    {
        public void DoSomething(string data, int i)
        {
            Console.WriteLine("DoSomething({0}, {1});", data, i);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            ObjectFactory.Initialize(x =>
            {
                x.For<IMyType>().Use<MyType>();
            });

            var myType = ObjectFactory.GetInstance<IMyType>();
            myType.DoSomething("Test", 1);
        }
    }
}
اکنون کدهای این برنامه را به نحو فوق تغییر دهید.
در اینجا یک اینترفیس نمونه و پیاده سازی آن‌را ملاحظه می‌کنید. همچنین نحوه آغاز تنظیمات StructureMap و نحوه دریافت یک وهله متناظر با IMyType نیز بیان شده‌اند.
نکته‌ی مهمی که در اینجا باید به آن دقت داشت، وضعیت شیء myType حین فراخوانی متد myType.DoSomething است. شیء myType در اینجا، دقیقا یک وهله‌ی متداول از کلاس myType است و هیچگونه دخل و تصرفی در نحوه اجرای آن صورت نگرفته است.
خوب! تا اینجای کار را احتمالا پیشتر نیز دیده بودید. در ادامه قصد داریم یک Interceptor را طراحی و مراحل چهارگانه اجرای یک Aspect را در اینجا بررسی کنیم.

در ادامه نیاز خواهیم داشت تا یک Dynamic proxy را نیز مورد استفاده قرار دهیم؛ از این جهت که StructureMap تنها دارای Interceptorهای وهله سازی اطلاعات است و نه Method Interceptor. برای دسترسی به Method Interceptors نیاز به یک Dynamic proxy نیز می‌باشد. در اینجا از Castle.Core استفاده خواهیم کرد:
 PM> Install-Package Castle.Core
برای دریافت آن تنها کافی است دستور پاور شل فوق را در خط فرمان کنسول پاورشل نوگت در VS.NET اجرا کنید.
سپس کلاس ذیل را به پروژه جاری اضافه کنید:
using System;
using Castle.DynamicProxy;

namespace AOP00
{
    public class LoggingInterceptor : IInterceptor
    {
        public void Intercept(IInvocation invocation)
        {
            try
            {
                Console.WriteLine("Logging On Start.");

                invocation.Proceed(); //فراخوانی متد اصلی در اینجا صورت می‌گیرد

                Console.WriteLine("Logging On Success.");
            }
            catch (Exception ex)
            {
                Console.WriteLine("Logging On Error.");
                throw;
            }
            finally
            {
                Console.WriteLine("Logging On Exit.");
            }
        }
    }
}
در کلاس فوق کار Method Interception توسط امکانات Castle.Core انجام شده است. این کلاس باید اینترفیس IInterceptor را پیاده سازی کند. در این متد سطر invocation.Proceed دقیقا معادل فراخوانی متد مورد نظر است. مراحل چهارگانه شروع، پایان، خطا و موفقیت نیز توسط try/catch/finally پیاده سازی شده‌اند.

اکنون برای معرفی این کلاس به برنامه کافی است سطرهای ذیل را اندکی ویرایش کنیم:
        static void Main(string[] args)
        {
            ObjectFactory.Initialize(x =>
            {
                var dynamicProxy = new ProxyGenerator();
                x.For<IMyType>().Use<MyType>();
                x.For<IMyType>().EnrichAllWith(myTypeInterface => dynamicProxy.CreateInterfaceProxyWithTarget(myTypeInterface, new LoggingInterceptor()));
            });

            var myType = ObjectFactory.GetInstance<IMyType>();
            myType.DoSomething("Test", 1);
        }
در اینجا تنها سطر EnrichAllWith آن جدید است. ابتدا یک پروکسی پویا تولید شده است. سپس این پروکسی پویا کار دخالت و تحت نظر قرار دادن اجرای متدهای اینترفیس IMyType را عهده دار خواهد شد.
برای مثال اکنون با فراخوانی متد myType.DoSomething، ابتدا کنترل برنامه به پروکسی پویای تشکیل شده توسط Castle.Core منتقل می‌شود. در اینجا هنوز هم متد DoSomething فراخوانی نشده است. ابتدا وارد بدنه متد public void Intercept خواهیم شد. سپس سطر invocation.Proceed، فراخوانی واقعی متد DoSomething اصلی را انجام می‌دهد. در ادامه باز هم فرصت داریم تا مراحل موفقیت، خطا یا خروج را لاگ کنیم.
تنها زمانیکه کار متد public void Intercept به پایان می‌رسد، سطر پس از فراخوانی متد  myType.DoSomething اجرا خواهد شد.
در این حالت اگر برنامه را اجرا کنیم، چنین خروجی را نمایش می‌دهد:
 Logging On Start.
DoSomething(Test, 1);
Logging On Success.
Logging On Exit.
بنابراین در اینجا نحوه دخالت و تحت نظر قرار دادن اجرای متدهای یک کلاس عمومی خاص را ملاحظه می‌کنید. برای اینکه کنترل کامل را در دست بگیریم، کلاس پروکسی پویا وارد عمل شده و اینجا است که این کلاس پروکسی تصمیم می‌گیرد چه زمانی باید فراخوانی واقعی متد مورد نظر انجام شود.
برای اینکه فراخوانی قسمت On Error را نیز ملاحظه کنید، یک استثنای عمدی را در متد DoSomething قرار داده و مجددا برنامه را اجرا کنید.
مطالب
C# 12.0 - Experimental Attribute
گاهی از اوقات ممکن است یک ویژگی تکمیل نشده، سر از نگارش‌های release درآورد؛ چون نیاز به دریافت بازخوردی در این مورد وجود دارد و یا اینکه قرار است در طی چند مرحله تکمیل شود. برای اینکه یک چنین مساله‌ای خصوصا از طرف نویسندگان کتابخانه‌ها و فریم‌ورک‌ها مشخص شود، ویژگی جدید System.Diagnostics.CodeAnalysis.ExperimentalAttribute به دات‌نت 8 اضافه شده‌است.
در این حالت اگر کدی، شروع به استفاده‌ی از یک چنین عضو‌های آزمایشی کند، یک خطای زمان کامپایل رخ می‌دهد؛ مگر اینکه آن قطعه کد نیز دقیقا با همین ویژگی مزین شود. در اینجا می‌توان نوع‌ها، اسمبلی‌ها و حتی اعضای آن‌ها را آزمایشی تعریف کرد. اگر کل یک نوع را به صورت آزمایشی معرفی کنیم، تمام اعضای آن هم آزمایشی خواهند بود.


بررسی ویژگی Experimental با یک مثال

در ادامه نحوه‌ی اعمال ویژگی Experimental را به همراه یک diagnosticId که به کل یک کلاس اعمال شده‌است، مشاهده می‌کنید. از این diagnosticId در حین تولید متن خطاها و یا برای شناسایی آن‌ها، استفاده می‌شود:
using System.Diagnostics.CodeAnalysis;

namespace CS8Tests;

[Experimental(diagnosticId: "Test001")]
public class ExperimentalAttributeDemo
{
    public void Print()
    {
        Console.WriteLine("Hello Experimental Attribute");
    }
}
پس از این تعریف، اگر در قسمت دیگری از برنامه بخواهیم از این کلاس استفاده کنیم:
var experimentalAttributeDemo = new ExperimentalAttributeDemo();
با خطای زیر مواجه خواهیم شد:
error Test001: 'CS8Tests.ExperimentalAttributeDemo' is for evaluation purposes only
and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
برای مواجه شدن با یک چنین خطایی، می‌توان دو روش زیر را در پیش گرفت:
الف) غیرفعال کردن سراسری گزارش این نوع خطاها در فایل csproj. برنامه:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>

    <NoWarn>Test001</NoWarn>
  </PropertyGroup>
</Project>
در اینجا اضافه شدن NoWarn را بر اساس diagnosticId ویژگی آزمایشی تعریف شده، مشاهده می‌کنید. این تنظیم سراسری است و به تمام قسمت‌های پروژه‌ی جاری اعمال می‌شود. اضافه کردن آن هم فقط یکبار صورت می‌گیرد.

ب) غیرفعال کردن موضعی آن، صرفا در محل استفاده
برای غیرفعال کردن محلی این بررسی، تنها کافی است با استفاده از pragma warning# یکبار آن‌را غیرفعال کرده و پس از پایان کار، مجددا آن‌را فعال کنیم:
#pragma warning disable Test001
var demo = new ExperimentalAttributeDemo();
#pragma warning restore Test001
همانطور که مشاهده می‌کنید، این فعال و غیرفعال کردن هم بر اساس diagnosticId صورت می‌گیرد. بدیهی است این تنظیم سراسری نبوده و درصورت بکارگیری این قطعه کد در قسمت‌های دیگر برنامه، باید مجددا تکرار شود.

و اگر این مثال را کمی پیچیده‌تر کنیم، به حالت زیر می‌رسیم:
using System.Diagnostics.CodeAnalysis;

namespace CS8Tests;

[Experimental(diagnosticId: "Test001")]
public class ExperimentalAttributeDemo
{
    [Experimental(diagnosticId: "Test002")]
    public void Print()
    {
        Console.WriteLine("Hello Experimental Attribute");
    }
}
در اینجا دو ویژگی آزمایشی، با دو diagnosticId متفاوت تعریف شده‌اند. در این حالت اگر سعی کنیم قطعه کد زیر را کامپایل کنیم:
var demo = new ExperimentalAttributeDemo();
demo.Print();
به ازای هر ویژگی آزمایشی تعریف شده، یک خطای کامپایلر جداگانه را دریافت می‌کنیم. به همین جهت برای رفع این خطاها، یا باید از روش غیرفعال سازی سراسری آن‌ها پیش‌رفت:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>

    <NoWarn>Test001,Test002</NoWarn>
  </PropertyGroup>
</Project>
 و یا می‌توان به صورت محلی زیر عمل کرد:
#pragma warning disable Test001,Test002
var demo = new ExperimentalAttributeDemo();
demo.Print();
#pragma warning restore Test001,Test002
در اینجا ذکر هر دو diagnosticId، ضروری است.
مطالب
نحوه ایجاد یک گزارش فاکتور فروش توسط PdfReport
شکل زیر را که شبیه به یک فاکتور فروش است درنظر بگیرید:



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

در ادامه کدهای کامل این مثال را مشاهده می‌کنید. همچنین این کد و کلاس‌های وابسته به آن مانند User و TransparentTemplate به سورس‌های کتابخانه PdfReport نیز اضافه شده‌اند.
using System;
using System.Collections.Generic;
using iTextSharp.text;
using iTextSharp.text.pdf;
using PdfReportSamples.Models;
using PdfReportSamples.Templates;
using PdfRpt.Core.Contracts;
using PdfRpt.Core.Helper;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.Tax
{
    public class TaxPdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.RightToLeft);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
            })
            .DefaultFonts(fonts =>
            {
                fonts.Path(AppPath.ApplicationPath + "\\fonts\\irsans.ttf",
                                  Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
            })
            .PagesFooter(footer =>
            {
                footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy"));
            })
            .PagesHeader(header =>
            {
                header.DefaultHeader(defaultHeader =>
                {
                    defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                    defaultHeader.Message("گزارش جدید ما");
                });
            })
            .MainTableTemplate(template =>
            {
                template.CustomTemplate(new TransparentTemplate());
            })
            .MainTablePreferences(table =>
            {
                table.ColumnsWidthsType(TableColumnWidthType.Relative);
            })
            .MainTableDataSource(dataSource =>
            {
                var listOfRows = new List<User>();
                for (int i = 0; i < 7; i++)
                {
                    listOfRows.Add(new User { Id = i, LastName = "نام خانوادگی " + i, Name = "نام " + i, Balance = i + 1000 });
                }
                dataSource.StronglyTypedList<User>(listOfRows);
            })
            .MainTableSummarySettings(summarySettings =>
            {
                summarySettings.OverallSummarySettings("جمع کل");
                summarySettings.PreviousPageSummarySettings("نقل از صفحه قبل");
                summarySettings.PageSummarySettings("جمع صفحه");
            })
            .MainTableColumns(columns =>
            {
                columns.AddColumn(column =>
                {
                    column.PropertyName("rowNo");
                    column.IsRowNumber(true);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(0);
                    column.Width(1);
                    column.HeaderCell("ردیف");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Id);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(1);
                    column.Width(2);
                    column.HeaderCell("شماره");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Name);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(2);
                    column.Width(3);
                    column.HeaderCell("نام");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.LastName);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(3);
                    column.Width(3);
                    column.HeaderCell("نام خانوادگی");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Balance);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(4);
                    column.Width(2);
                    column.HeaderCell("موجودی");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.TextBlock();
                        template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                    column.AggregateFunction(aggregateFunction =>
                    {
                        aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                        aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                });

            })
            .MainTableEvents(events =>
            {
                events.DataSourceIsEmpty(message: "There is no data available to display.");

                events.MainTableAdded(args =>
                {
                    var balanceData = args.LastOverallAggregateValueOf<User>(u => u.Balance);
                    var balance = double.Parse(balanceData, System.Globalization.NumberStyles.AllowThousands);

                    var others = Math.Round(balance * 1.8 / 100);
                    var tax = Math.Round(balance * 2.2 / 100);
                    var total = balance + tax + others;

                    var taxTable = new PdfPTable(args.Table); // Create a clone of the MainTable's structure                   

                    taxTable.AddSimpleRow(
                        null /* null = empty cell */, null, null,
                        (data, cellProperties) =>
                        {
                            data.Value = "مالیات";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                        },
                        (data, cellProperties) =>
                        {
                            data.Value = string.Format("{0:n0}", tax);
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.BorderColor = BaseColor.LIGHT_GRAY;
                            cellProperties.ShowBorder = true;
                        });

                    taxTable.AddSimpleRow(
                        null, null, null,
                        (data, cellProperties) =>
                        {
                            data.Value = "عوارض";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                        },
                        (data, cellProperties) =>
                        {
                            data.Value = string.Format("{0:n0}", others);
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.BorderColor = BaseColor.LIGHT_GRAY;
                            cellProperties.ShowBorder = true;
                        });

                    taxTable.AddSimpleRow(
                        null, null, null,
                        (data, cellProperties) =>
                        {
                            data.Value = "جمع کل";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                        },
                        (data, cellProperties) =>
                        {
                            data.Value = string.Format("{0:n0}", total);
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.BorderColor = BaseColor.LIGHT_GRAY;
                            cellProperties.ShowBorder = true;
                        });

                    taxTable.AddSimpleRow(
                        null, null, null,
                        (data, cellProperties) =>
                        {
                            data.Value = "قابل پرداخت";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                        },
                        (data, cellProperties) =>
                        {
                            data.Value = total.NumberToText(Language.Persian) + " ریال";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.BorderColor = BaseColor.LIGHT_GRAY;
                            cellProperties.ShowBorder = true;
                            cellProperties.PdfFontStyle = DocumentFontStyle.Bold;
                        });

                    args.PdfDoc.Add(taxTable);
                });
            })
            .Export(export =>
            {
                export.ToExcel();
            })
            .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\TaxReportSample.pdf"));
        }
    }
}
توضیحات:
تنها تفاوت این مثال با مثال‌های قبلی، کدهای مرتبط با متد events.MainTableAdded می‌باشند.
توسط متد args.LastOverallAggregateValueOf، می‌توان به مقدار نهایی متد تجمعی تعریف شده برای یک ستون خاص دسترسی یافت:
var balanceData = args.LastOverallAggregateValueOf<User>(u => u.Balance);
var balance = double.Parse(balanceData, System.Globalization.NumberStyles.AllowThousands);
سپس بر این اساس، امکان محاسبه مالیات و عوارض میسر می‌شود:
var others = Math.Round(balance * 1.8 / 100);
var tax = Math.Round(balance * 2.2 / 100);
var total = balance + tax + others;
در ادامه نیاز داریم تا یک جدول جدید را ذیل جدول اصلی ایجاد کنیم. نکته مهم این جدول جدید، هماهنگی عرض ستون‌های آن با ستون‌های جدول اصلی است. به همین منظور می‌توان از خاصیت args.Table جهت دسترسی به خواص جدول اصلی استفاده کرد و جدول جدیدی را ایجاد نمود:
var taxTable = new PdfPTable(args.Table);
از اینجا به بعد دیگر به عهده خودتان است. می‌توانید از دانش iTextSharp خود استفاده کرده و ردیف‌های این جدول جدید را پر کنید. یا اینکه می‌توانید از متد کمکی توکار AddSimpleRow به نحو زیر استفاده نمائید:
taxTable.AddSimpleRow(
                        null /* null = empty cell */, null, null,
                        (data, cellProperties) =>
                        {
                            data.Value = "مالیات";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                        },
                        (data, cellProperties) =>
                        {
                            data.Value = string.Format("{0:n0}", tax);
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.BorderColor = BaseColor.LIGHT_GRAY;
                            cellProperties.ShowBorder = true;
                        });
با توجه به اینکه قصد نداریم در سه ستون اول این جدول جدید، عنصری را نمایش دهیم، آن‌ها را با null مقدار دهی کرده و سپس ستون برچسب و ستون مقدار را اضافه می‌کنیم (آرگومان‌های این متد به صورت params تعریف شده‌اند. بنابراین هر تعداد ستون که نیاز باشد قابل تعریف است).
با مقدار دهی data، مقدار مورد نظر در آن سلول ثبت می‌گردد. با مقدار دهی خواص cellProperties، نوع قلم، جهت قرارگیری و سایر تنظیماتی را که ملاحظه می‌کنید، می‌توان اعمال کرد.
و در آخر لازم است که این جدول جدید را به شیء Document اضافه کنیم تا نمایش داده شود:
args.PdfDoc.Add(taxTable);

یک نکته:
متد NumberToText جزئی از کتابخانه PdfReport (تعریف شده در فضای نام PdfRpt.Core.Helper) است و برای نمایش رقم به حروف می‌تواند مورد استفاده قرار گیرد:
total.NumberToText(Language.Persian)

 
مطالب
افزونه جی کوئری RowAdder
دیروز در یک برنامه میخواستم کاربر بتواند لیست مواد مصرفی یک کارخانه را ایجاد کند که نیاز بود کاربر بتواند از هر سطر به تعداد نامحدود ایجاد کند و برای انتخاب هر یک از مواد به همراه جزئیات آن یک سطر به لیست اضافه شود. برای اینکار میتوانیم با استفاده از فناوری جی کوئری اینکار را انجام دهیم ولی بهتر بود که این مورد به یک افزونه تبدیل میشد تا در دفعات بعدی بسیار راحت‌تر باشیم. جهت آشنایی با پلاگین نویسی بهتر هست این مقالات (+) را مطالعه فرمایید.

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

گام اول:
فایل‌های مورد نظر را بعد از صدا زدن کتابخانه‌ی جی کوئری صدا بزنید.
<link type="text/css" href="css/RowAdder.css" rel="stylesheet" />
    <script src="js/RowAdder.js" type="text/javascript"></script>


گام دوم :
 در تکه کدهای html، کدی را که قرار است در هر سطر تکرار شود، داخل یک div قرار داده و نامی مثل row-sample را برای آن قرار دهید (فعلا حتما این نام باشد)، بعدها پلاگین، کدهای داخل این تگ div را به عنوان هر سطر خواهد شناخت:
<div id="row-sample">
    <form style="margin: 0; padding: 0;">
        Name:<input type="text"/>
        <input type="radio" name="Gender" value="male" checked="checked">Male
        <input type="radio" name="Gender" value="female">Female
    </form>
</div>


گام سوم:
 سپس یک div دیگر ایجاد کنید و نامی مثل mypanel را به آن بدهید تا سطرهایی که ایجاد می‌شوند داخل این div قرار بگیرند.
<div id="mypanel"></div>

گام چهارم:
در بخش head یک تگ اسکریپت باز کرده و کدهای زیر را به آن اضافه می‌کنیم. این کد باعث می‌شود که پلاگین فعال شود.
<script>
$(document).ready(function() {
$("#mypanel").RowAdder();
});
</script>
گام پنجم:
 یک دکمه جهت افزودن سطر به صفحه اضافه می‌کنیم
<button id="addanotherform">Add New Form</button>

و در قسمت تگ اسکریپت هم کد زیر را اضافه می‌کنیم:
$("#addanotherform").on('click', function() {
                $("#mypanel").RowAdder('add');
            });

حال از صفحه تست می‌گیریم: با هر بار کلیک بر روی دکمه‌ی Add New Form یک سطر جدید ایجاد می‌گردد.


در تصویر بالا دکمه‌های دیگر هم دیده می‌شوند که به دیگر متدهای آن اشاره دارد:

جهت مخفی سازی:
 $("#mypanel").RowAdder('hide');

چهت نمایش:
$("#mypanel").RowAdder('show');

جهت افزودن سطر با کد:
$("#mypanel").RowAdder('add');

جهت دریافت تعداد سطرهای ایجاد شده:
$("#mypanel").RowAdder('count')


جهت دریافت کدهای یک سطر در اندیس x

$("#mypanel").RowAdder('content', 3)

جهت حذف یک سطر با اندیس x
$("#mypanel").RowAdder('remove', 3);

همانطور که با صدا زدن اولین متد پلاگین متوجه شدید و نتیجه‌ی آن را در دمو دیدید، این پلاگین از پیش فرض‌هایی جهت راه اندازی اولیه استفاده می‌کند که این پیش فرض‌ها عبارتند از تگ row-sample که بدون معرفی رسمی، آن را شناسایی کرد. همچنین ممکن است بخواهید عبارت Remove را با کلمه‌ی فارسی «حذف» جایگزین نمایید. برای اینکار می‌توانید پلاگین را به شکل زیر به کار ببرید:
    $("#mypanel").RowAdder({
                sample: '#my-custom-sample',
                type: 'text',
                value:'حذف'
        });

تغییر اولین پیش فرض، تغییر نام تگ row-sample به my-custom-sample بود و در مرحله‌ی بعد هم نام فارسی حذف را جایگزین remove کردیم. عبارت type به طور پیش فرض بر روی text قرار دارد که اجباری به ذکر آن در کد بالا نبود. ولی اگر دوست دارید که به جای نمایش عبارت حذف، از یک آیکن یا تصویر استفاده کنید، کد را به شکل زیر تغییر دهید:
  $("#mypanel").RowAdder({
                type: 'image',
                value: 'images/remove.png'
            });
در خطوط بالا عبارت type با image مقدار دهی شد و به پلاگین می‌گوید که به جای متن، از تصویر استفاده کن. همچنین value را به جای متن با آدرس تصویر مقداردهی کرده‌ایم و نتیجه را می‌توانید در دموی قرار گرفته در گیت هاب ببینید.

فایل RowAdder.css
در بردارنده هر سطر
.each-section {
    margin: 20px;
    padding: 5px;
}

جهت استایل بندی لینک چه تصویر و چه متن
.remove-link {
    color:#999;
     text-decoration: none;
}

a:hover.remove-link {
   color:#802727;
}
جهت تغییر استایل بر روی خود تصویر
.remove-image {
    
}

آشنایی با کد پلاگین
(function ($) {
    
    var settings = null;
  $.fn.RowAdder = function (method) {
    
            // call methods
            if (methods[method]) {
                return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
            } else if (typeof method === 'object' || !method) {
                return methods.init.apply(this, arguments);
            } else {
                $.error('Method ' + method + ' does not exist on jQuery.RowAdder');
            }
      

    };
})(jQuery);
در قسمت دوم آموزش پلاگین نویسی برای جی کوئری، متدها به طور واضح توضیح داده شده‌اند. این کدها وظیفه دارند متدهایی را که کاربر درخواست داده است، شناسایی و به همراه آرگومان‌های آن به سمت توابعی که به هر نام متد اختصاص داده‌ایم، ارسال کنند. در صورتیکه متدی با آرگومان‌های ناهماهنگی ارسال شوند، پیام خطایی ارسال می‌گردد و در صورتیکه تعریف نشود، به طور مستقیم init را صدا می‌زند. متغیر settings هم بعدا با تنظیمات پیش فرض پر می‌شود.

متدها
//methods
    var methods = {
        init: function (options) {
            //default-settings
             settings = $.extend({
                'sample': '#row-sample',
                'type': 'text',
                'value': 'Remove'
             }, options);
             this.attr('data-sample', settings.sample);
             this.attr('data-type', settings.type);
             this.attr('data-value', settings.value);
            Do(this);
        },
        show: function () {
            this.css("display", "inline");
        },
        hide: function () {
            this.css("display", "none");
        },
        add: function () {
            Do(this);
        },
        remove: function (index) {
            console.log(index);
           this.find(".each-section")[index].remove();
        },
        content: function (index) {
            return this.find(".each-section")[index];
        },
        count: function (index) {
            return this.find(".each-section").size();
        }
    };
متد init تنظیمات پیش فرض را دریافت می‌نماید و سپس بر روی المانی که پلاگین روی آن واقع شده‌است، مقادیر را ذخیره می‌کند تا در آینده با صدا زدن متدهای دیگر آن را استفاده نماید. کلمه‌ی this در واقع به تگی اشاره می‌کند که پلاگین روی آن اعمال شده است که در مثال‌های بالا mypanel نام داشت. متد Do تابع اصلی ما را در بر دارد که کدهای اصلی پلاگین را شامل می‌شود. مابقی متدها در واقع  جست و جویی بر المان‌ها هستند.

تابع Do
    function Do(panelDiv) {

        settings.sample = panelDiv.data('sample');
        settings.type = panelDiv.data('type');
        settings.value = panelDiv.data('value');
        //find sample code
        var rowsample = $(settings.sample);
        rowsample.css("display", "none");
        var sample = rowsample.html();


        var i = panelDiv.find(".each-section").size();
        //add html details to create a correct template
        var sectionDiv = $('<div />', { "class": 'each-section', 'id': 'section'+i });
        var image = $("<img />", { "src": settings.value,"class":"remove-image" });
        var link = $("<a />", { "text": settings.value,"class":"remove-link" });
        //remove event for remove selected form

        //create new form
        sectionDiv.html(sample);

        link.on('click', function (e) {

            e.preventDefault();
            var $this = $(this);
            $this.closest(".each-section").remove();
        });

        if (i > 0) {
            if (settings.type == 'image') {
                link.text('');
                link.append(image);

            }
            sectionDiv.append(link);
        }

        //add new created form on document
        panelDiv.append(sectionDiv);
       
    }
آرگومان داده شده، در واقع همان this هست که به این تابع ارسال شده است. در اولین گام تنظیمات ذخیره شده را که قبلا ذخیره کرده‌ایم، واکشی می‌کنیم. سپس تگ row-sample یا هر نامی را که به آن اختصاص داده شده است، می‌یابیم و محتوای آن را به شکل html در قالب string بیرون می‌کشیم. این کد html در واقع نمونه‌ای است که قرار است در سطر تکرار شود. البته تگ نمونه فقط برای نمونه به کار می‌رود و نیازی نیست روی صفحه نمایش داده شود؛ پس آن را مخفی می‌کنیم. از آنجا که ممکن است این سطری که ایجاد می‌شود، سطر اول نباشد و قبلا هم سطرهایی توسط همین متد ایجاد شده‌اند، بررسی می‌کنیم چند تگ با کلاس each-section داریم. اگر بیشتر از صفر باشد یعنی قبلا سطرهایی ایجاد شده است. در غیر اینصورت این اولین سطر ماست. اولین سطر توسط init صدا زده می‌شود و مابقی توسط متد add انجام می‌گیرد.
        settings.sample = panelDiv.data('sample');
        settings.type = panelDiv.data('type');
        settings.value = panelDiv.data('value');
        //find sample code
        var rowsample = $(settings.sample);
        rowsample.css("display", "none");
        var sample = rowsample.html();


        var i = panelDiv.find(".each-section").size();
در خطوط بعدی یک سری متغیر داریم که برای هر کدام یک قالب تگ div با کلاس‌های مختلف می‌سازیم. sectionDiv یک تگ  div  با کلاس each-section است که هر سطر را به طور کامل در خود قرار می‌دهد. link، جهت ساخت لینک حذف با کلاس remove-link به کار می‌رود. image هم یک تگ image می‌سازد تا اگر کاربر درخواست 'type:'image را داد، به جای لینک متنی حذف، از تصویر استفاده شود.
        //add html details to create a correct template
        var sectionDiv = $('<div />', { "class": 'each-section', 'id': 'section'+i });
        var image = $("<img />", { "src": settings.value,"class":"remove-image" });
        var link = $("<a />", { "text": settings.value,"class":"remove-link" });

در خط بعدی محتویات نمونه را داخل تگ sectiondiv قرار می‌دهیم:
//create new form
        sectionDiv.html(sample);

بعد از آن برای رویداد کلیک لینک حذف، کد زیر را وارد می‌کنیم:
   link.on('click', function (e) {

            e.preventDefault();
            var $this = $(this);
            $this.closest(".each-section").remove();
        });
متد closest در جی کوئری این وظیفه را دارد تا به سمت تگ‌های والد تگ this حرکت کند و با برخوردن با اولین تگ والد با کلاس each-section، آن تگ والد را بازگرداند و سپس متد remove را روی آن اجرا کند تا آن تگ به همراه تمام فرزندانش حذف شوند.

اولین شرط زیر بررسی می‌کند که آیا این سطری که ایجاد شده است سطر دوم به بعد است یا خیر؟ اگر آری پس باید دکمه‌ی حذف را به همراه داشته باشد. در صورتیکه سطر دوم به بعد باشد، وارد آن می‌شود. حالا بررسی می‌کند که کاربر برای دکمه‌ی حذف، درخواست لینک تصویری یا لینک متنی داده است و لینک مناسب را ساخته و آن را به انتهای sectionDiv اضافه می‌کند.
   if (i > 0) {
            if (settings.type == 'image') {
                link.text('');
                link.append(image);

            }
            sectionDiv.append(link);
        }

در انتها کل تگ sectionDiv را به تگ داده شده اضافه می‌کنیم تا به کاربر نمایش داده شود.
//add new created form on document
        panelDiv.append(sectionDiv);