<div class="table-responsive"> <table class="table"> ... </table> </div>
کار با فرمهای مبتنی بر قالبها سادهتر است؛ اما کنترل کمتری را بر روی مباحث اعتبارسنجی دادههای ورودی توسط کاربر، در اختیار ما قرار میدهند. اما فرمهای مبتنی بر مدلها هر چند به همراه اندکی کدنویسی بیشتر هستند، اما کنترل کاملی را جهت اعتبارسنجی ورودیهای کاربران، ارائه میدهند. در این قسمت فرمهای مبتنی بر قالبها (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>
هر دو کلاس 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>
الف) ایجاد یک 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>
همچنین چون در این حالت 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)">
پس از تعریف این رخداد و اتصال آن در قالب کامپوننت، اکنون میتوان متد onSubmit را در کلاس آن نیز اضافه کرد.
onSubmit(form): void { console.log(form); }
غیرفعال کردن دکمهی submit در صورت وجود خطاهای اعتبارسنجی
در قسمت بررسی ngForm، یک متغیر محلی را به نام f ایجاد کردیم که به شیء ControlGroup فرم جاری اشاره میکند. از این متغیر و خاصیت valid آن میتوان با کمک property binding به خاصیت disabled یک دکمه، آنرا به صورت خودکار فعال یا غیرفعال کرد:
<button [disabled]="!f.valid" type="submit" class="btn btn-primary"> Submit </button>
نمایش فرم افزودن محصولات توسط سیستم 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...
اتصال المانهای فرم به مدلی جهت ارسال به سرور
برای اتصال المانهای فرم به یک مدل، این مدل را به صورت یک خاصیت عمومی، در سطح کلاس کامپوننت فرم، تعریف میکنیم:
productModel = <IProduct>{}; // creates an empty object of an interface
پس از تعریف خاصیت productModel، اکنون کافی است با استفاده از two-way data binding، آنرا به المانهای فرم نسبت دهیم. برای مثال:
<textarea ngControl="description" id="description" required #description="ngForm" rows="10" type="text" class="form-control" [(ngModel)]="productModel.description"></textarea>
پس از تعریف یک چنین اتصالی، دیگر نیازی به مقدار دهی پارامتر 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); }
نکتهی مهم اینجا است که 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']); }); } }
در اینجا پس از فراخوانی متد 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 بودن این اطلاعات نخواهد شد.
سطح اول کش در 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();
}
}
}
د) اعمال متد Cacheable به کوئریها
سه مرحله قبل نحوه برپایی مقدماتی سطح دوم کش را بیان میکنند و تنها یکبار نیاز است انجام شوند. در ادامه هر جایی که نیاز داشتیم نتایج کوئری مورد نظر کش شوند (و باید دقت داشت که این کش شدن سطح دوم به معنی در دسترس بودن نتایج آن جهت تمام کاربران برنامه در تمام تراکنشهای جاری برنامه هستند؛ برای مثال نتایج آمار سایت که دسترسی عمومی دارد) تنها کافی است متد Cacheable را به کوئری مورد نظر اضافه کرد؛ برای مثال:
var data = session.QueryOver<Account>()
.Where(s => s.Name == "name")
.Cacheable()
.List();
ه) چگونه صحت اعمال سطح دوم کش را بررسی کنیم؟
برای بررسی این امر باید به خروجی SQL نهایی مراجعه کرد (+). سه تراکنش مجزا را تعریف کنید. در تراکنش اول یک insert ساده، در تراکنش دوم کوئری از اطلاعات قبل (به همراه اعمال متد Cacheable) و در تراکنش سوم مجددا همان کوئری تراکنش دوم را (به همراه اعمال متد Cacheable) تکرار کنید. حاصل کار تنها باید دو عبارت SQL باشند. یک مورد جهت insert و یک مورد هم select . در تراکنش سوم، از نتایج کش شده تراکنش دوم استفاده خواهد شد؛ به همین جهت دیگری کوئری سومی به بانک اطلاعاتی ارسال نخواهد شد.
اگر اعمال مورد (ج) فوق را فراموش کنید، سه کوئری را مشاهده خواهید کرد، اما کوئری سوم با کوئری دوم اندکی متفاوت خواهد بود و بهینهتر؛ چون به صورت هوشمند بر اساس جستجوی بر روی primary key تغییر کرده است (صرفنظر از اینکه قسمت where کوئری شما چیست).
در مطلبی که در همین سایت اشاره شد با استفاده از Adobe Acrobat میتوان فایلهای pdf را به تصویر تبدیل کرد اما چون نیاز بود تا در وب از آن استفاده کنیم و گاهی اوقات امکان نصب Adobe Acrobat Sdk در سرور وجود ندارد میتوان از روش زیر نیز استفاده کرد.
ابتدا فایل gsdll32.dll را در پوشه bin پروژه کپی کنید ( این فایل به همراه مثال ارائه شده وجود دارد).
سپس برای متدهای موردنیاز موجود در Api که بصورت Unmanaged میباشند یکسری wrapper ایجاد میکنیم. این متدها شامل :
- gsapi_new_instance که برای ایجاد یک نمونه جدید از api بکار میرود.
- gsapi_init_with_args که برای مقداردهی نمونه ایجاد شده بوسیله آرگومانها بکار میرود .
- gsapi_delete_instance و gsapi_exit برای آزادسازی منابع ایجاد شده.
در زیر چند آرگومان مهم که باید به api ارسال شوند نیز آمده است:
-sDEVICE | فرمت تصویر خروجی |
dFirstPage- | صفحه آغازین برای تبدیل |
dLastPage - | صفحه پایانی برای تبدیل |
dDEVICEWIDTHPOINTS- | اندازه width فایل pdf |
dDEVICEHEIGHTPOINTS- | اندازه height فایل pdf |
dDEVICEXRESOLUTION- | resolutionX |
-dDEVICEYRESOLUTION | resolutionY |
sOutputFile- | مسیر فایل(های) خروجی |
مسیر فایل ورودی |
نکته دوم : هنگامی که خواستم از این 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 را ایجاد میکنیم:
> create-react-app sample-05 > cd sample-05 > npm start
> 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";
همچنین به فایل 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> );
معرفی سرویسهای دادهی برنامه
کدهای نهایی این قسمت را از فایل پیوست شدهی در انتهای مطلب، میتوانید دریافت کنید. در اینجا یک پوشهی src\services تعریف شدهاست که داخل آن دو فایل fakeGenreService.js و fakeMovieService.js قرار دارند. این فایلها، منبع دادهی درون حافظهای مثال تمرین ما هستند.
سرویس fakeGenre چنین ساختاری را دارد و ژانرهای سینمایی، مانند اکشن، کمدی و غیره در آن لیست شدهاند:
export const genres = [ { _id: "5b21ca3eeb7f6fbccd471818", name: "Action" }, // ... ];
و سرویس 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" }, //... ];
ایجاد کامپوننت 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 ( <main className="container"> <Movies /> </main> );
دریافت لیست اشیاء فیلمها از سرویس fakeMovieService
برای دریافت لیست اشیاء فیلمها، ابتدا تعریف سرویس آنرا به ابتدای کامپوننت Movies اضافه میکنیم:
import { getMovies } from "../services/fakeMovieService";
سپس خاصیت state را جهت تعریف خاصیت movies که با متد ()getMovies سرویس fakeMovieService مقدار دهی میشود، به نحو زیر تکمیل میکنیم:
state = { movies: getMovies() };
نمایش لیست فیلمها، به همراه مدیریت حذف هر ردیف
در ادامه، کدهای کامل و تکمیل شدهی این کامپوننت را ملاحظه میکنید:
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
ابزارهایی جهت تولید 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
البته باید دقت داشت که برای استفاده از 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
سپس کلاس ذیل را به پروژه جاری اضافه کنید:
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."); } } } }
اکنون برای معرفی این کلاس به برنامه کافی است سطرهای ذیل را اندکی ویرایش کنیم:
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); }
برای مثال اکنون با فراخوانی متد 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 قرار داده و مجددا برنامه را اجرا کنید.
در این حالت اگر کدی، شروع به استفادهی از یک چنین عضوهای آزمایشی کند، یک خطای زمان کامپایل رخ میدهد؛ مگر اینکه آن قطعه کد نیز دقیقا با همین ویژگی مزین شود. در اینجا میتوان نوعها، اسمبلیها و حتی اعضای آنها را آزمایشی تعریف کرد. اگر کل یک نوع را به صورت آزمایشی معرفی کنیم، تمام اعضای آن هم آزمایشی خواهند بود.
بررسی ویژگی 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>
ب) غیرفعال کردن موضعی آن، صرفا در محل استفاده
برای غیرفعال کردن محلی این بررسی، تنها کافی است با استفاده از pragma warning# یکبار آنرا غیرفعال کرده و پس از پایان کار، مجددا آنرا فعال کنیم:
#pragma warning disable Test001 var demo = new ExperimentalAttributeDemo(); #pragma warning restore Test001
و اگر این مثال را کمی پیچیدهتر کنیم، به حالت زیر میرسیم:
using System.Diagnostics.CodeAnalysis; namespace CS8Tests; [Experimental(diagnosticId: "Test001")] public class ExperimentalAttributeDemo { [Experimental(diagnosticId: "Test002")] public void Print() { Console.WriteLine("Hello Experimental Attribute"); } }
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
نکتهای که در اینجا مدنظر است، دسترسی به عدد جمع آخر گزارش و سپس بر اساس آن، ساخت دو ستون اضافی ذیل جدول اصلی گزارش است که موارد مالیات، عوارض، جمع کل و مبلغ به حروف را نسبت به مثالهای قبلی، اضافهتر دارد.
در ادامه کدهای کامل این مثال را مشاهده میکنید. همچنین این کد و کلاسهای وابسته به آن مانند 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;
var taxTable = new PdfPTable(args.Table);
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; });
با مقدار دهی data، مقدار مورد نظر در آن سلول ثبت میگردد. با مقدار دهی خواص cellProperties، نوع قلم، جهت قرارگیری و سایر تنظیماتی را که ملاحظه میکنید، میتوان اعمال کرد.
و در آخر لازم است که این جدول جدید را به شیء Document اضافه کنیم تا نمایش داده شود:
args.PdfDoc.Add(taxTable);
یک نکته:
متد NumberToText جزئی از کتابخانه PdfReport (تعریف شده در فضای نام PdfRpt.Core.Helper) است و برای نمایش رقم به حروف میتواند مورد استفاده قرار گیرد:
total.NumberToText(Language.Persian)
نحوه استفاده
نحوه استفاده از آن بسیار راحت است و در دموی 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' });
فایل 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);
متدها
//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(); } };
تابع 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); }
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" });
در خط بعدی محتویات نمونه را داخل تگ sectiondiv قرار میدهیم:
//create new form sectionDiv.html(sample);
بعد از آن برای رویداد کلیک لینک حذف، کد زیر را وارد میکنیم:
link.on('click', function (e) { e.preventDefault(); var $this = $(this); $this.closest(".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);