بررسی کارآیی کوئریها در SQL Server - قسمت ششم - بررسی عملگرهای دسترسی به دادهها در یک Query Plan
عملگرهای Scans و Seeks
در حالت کلی میتوان دو نوع جدول بدون و با ایندکس را درنظر گرفت. در حالت جداول بدون ایندکس، برای جستجوی اطلاعات نیاز به Table Scan وجود دارد و برعکس آن شامل یک Clustered index scan خواهد بود. گاهی از اوقات Clustered index scanها بهترین روش دریافت اطلاعات هستند و گاهی از اوقات خیر و نیاز به بررسی بیشتری دارند. بنابراین قانون کلی، حذف آنها به محض مشاهده، نیست.
نوع دیگر عملگرهای دسترسی به دادهها، Seeks هستند که شامل Clustered index seeks و Non-clustered index seeks میشوند. در بسیاری از موارد عنوان میشود که Seeks کارآیی بهتری را به همراه دارند. هرچند این مورد نیاز به بررسی بیشتری دارد که در ادامه با مثالهایی آنها را مرور خواهیم کرد.
بررسی عملگر Table scan در یک Query Plan
در ادامه تعدادی از عملگرهای مرتبط با data access را از لحاظ نحوهی انتخاب و تغییر آنها توسط بهینه ساز کوئریهای SQL Server بررسی میکنیم. برای این منظور ابتدا در management studio از منوی Query، گزینهی Include actual execution plan را انتخاب میکنیم. سپس کوئریهای زیر را اجرا میکنیم:
SET STATISTICS IO ON; GO SET STATISTICS TIME ON; GO SELECT * INTO [Sales].[Copy_Orders] FROM [Sales].[Orders]; GO SELECT [CustomerID], [OrderID], [OrderDate] FROM [Sales].[Copy_Orders] WHERE [CustomerID] > 550; GO
همانطور که مشاهده میکنید، برای برآورده کردن قسمت where این کوئری، یک Table Scan صورت گرفتهاست؛ چون این جدول کپی، به همراه هیچ ایندکسی نیست. به همین جهت برای یافتن رکوردهای مدنظر، راه دیگری بجز اسکن کل جدول بانک اطلاعاتی وجود ندارد که بسیار ناکارآمد است.
همچنین اگر به برگهی messages دقت کنیم، با توجه به روشن بودن STATISTICS IO، میزان logical reads نیز قابل مشاهدهاست:
(33035 rows affected) Table 'Copy_Orders'. Scan count 1, logical reads 689, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times: CPU time = 79 ms, elapsed time = 762 ms.
بررسی عملگر Index Seek در یک Query Plan
اکنون سؤال اینجا است که آیا میتوان این وضعیت را بهبود بخشید؟
بله. برای این منظور یک NONCLUSTERED INDEX را بر روی جدول کپی، ایجاد میکنیم؛ به نحوی که CustomerID لحاظ شدهی در قسمت where کوئری را پوشش دهد:
CREATE NONCLUSTERED INDEX [IX_Copy_Orders_CustomerID] ON [Sales].[Copy_Orders] ( [CustomerID] ) INCLUDE ( [OrderID], [OrderDate] ); GO
در ادامه مجددا همان کوئری را اجرا میکنیم:
SELECT [CustomerID], [OrderID], [OrderDate] FROM [Sales].[Copy_Orders] WHERE [CustomerID] > 550; GO
اینبار عملگر Table Scan قبلی به یک عملگر Index Seek بر روی NONCLUSTERED INDEX تعریف شده، تغییر کردهاست و اگر به آمار I/O آن دقت کنیم، logical reads 106 قابل مشاهدهاست که بهبود قابل ملاحظهای است نسبت به عدد 689 قبلی.
بررسی عملگر Clustered index scan در یک Query Plan
در ادامه همین کوئری را بر روی جدول [Sales].[Orders] اصلی اجرا میکنیم:
SELECT [CustomerID], [OrderID], [OrderDate] FROM [Sales].[Orders] WHERE [CustomerID] > 550; GO
اجرای کوئری فوق، چنین کوئری پلنی را تولید میکند:
جدول [Sales].[Orders]، یک CLUSTERED INDEX را بر روی [OrderID] دارد و یک NONCLUSTERED INDEX را بر روی [CustomerID].
در کوئری پلن تولید شده، یک Clustered index scan مشاهده میشود. علت اینجا است که هرچند در جدول [Sales].[Orders] یک NONCLUSTERED INDEX بر روی [CustomerID] تعریف شدهاست:
CREATE NONCLUSTERED INDEX [FK_Sales_Orders_CustomerID] ON [Sales].[Orders] ( [CustomerID] ASC )
بنابراین وجود عملگر Clustered index scan در یک کوئری پلن، یعنی نیاز به خواندن و اسکن کل جدول وجود دارد. برای اثبات آن، همین کوئری قبلی را که بر روی [Sales].[Orders] انجام دادیم، اینبار بدون قسمت where آن اجرا کنید. یعنی کوئری بر روی کل جدول انجام شود:
SELECT [CustomerID], [OrderID], [OrderDate] FROM [Sales].[Orders]
سؤال: آیا Clustered index scan همواره کل یک جدول را اسکن میکند؟
پاسخ: خیر. اگر یک کوئری برای مثال دارای top/min/max باشد، کل جدول اسکن نخواهد شد:
SELECT TOP 10 [CustomerID], [OrderID], [OrderDate] FROM [Sales].[Orders] WHERE [CustomerID] > 550;
هرچند در اینجا هم یک Clustered index scan صورت گرفته، اما اگر به برگهی messages آن مراجعه کنیم، آمار I/O آن بیانگر تنها logical reads 5 است که معادل اسکن کل جدول نیست:
(10 rows affected) Table 'Orders'. Scan count 1, logical reads 5, physical reads 0, read-ahead reads 510, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
مقایسهی عملگرهای Index Scan و Index Seek
ابتدا کوئری زیر را اجرا میکنیم:
SELECT [CustomerID], [OrderID] FROM [Sales].[Orders] WHERE [OrderID] > 30000;
در این جدول ایندکسی بر روی CustomerID وجود دارد و همچنین کلید اصلی جدول، OrderID است.
پس از اجرای این کوئری، به کوئری پلن زیر خواهیم رسید:
که بیانگر یک Index Scan است و نکتهی جالب آن، استفادهی از ایندکس FK_Sales_Orders_CustomerID میباشد (نام این شیء، ذیل آیکن عملگر، مشخص است). یعنی SQL Server در اینجا از یک non-clustered index تعریف شدهی بر روی CustomerID استفاده کردهاست.
اکنون اگر OrderID را تغییر دهیم چه اتفاقی رخ میدهد؟
SELECT [CustomerID], [OrderID] FROM [Sales].[Orders] WHERE [OrderID] > 60000;
در این مثال با دو ورودی مختلف، دو کوئری پلن مختلف تولید شدهاست؛ که مرتبط است با میزان اطلاعاتی که قرار است بازگشت داده شود.
اگر این دو کوئری را با هم اجرا کنیم (در طی یک batch)، به پلن مقایسهای زیر خواهیم رسید که در آن هزینهی Index Scan بیشتر است از clustered index seek:
به همراه آمار CPU و I/O ای به صورت زیر که اولی مرتبط است با index scan و دومی با clustered index seek:
(43595 rows affected) Table 'Orders'. Scan count 1, logical reads 191, physical reads 1, read-ahead reads 182, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. SQL Server Execution Times: CPU time = 31 ms, elapsed time = 754 ms. (13595 rows affected) Table 'Orders'. Scan count 1, logical reads 131, physical reads 0, read-ahead reads 127, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. SQL Server Execution Times: CPU time = 16 ms, elapsed time = 276 ms.
علاوه بر Markup Extensionهای از پیش تعریف شده در XAML، میتوان Markup Extensionهای شخصی را نیز تولید کرد. در واقع به زبان سادهتر Markup Extension برای تولید سادهی دادههای پیچیده در XAML استفاده میشوند.
لازم به ذکر است کهMarkup Extension ها میتوانند به دو صورت Attribute Usage ،درون {} :
"{Binding path=something,Mode=TwoWay}”
<Binding Path="Something" Mode="TwoWay"></Binding>
public abstract Object ProvideValue(IServiceProvider serviceProvider)
var target = serviceProviderGetService(typeof(IProvideValueTarget))as IProvideValueTarget; var host = targetTargetObject as FrameworkElement;
public class ValueExtension : MarkupExtension { public ValueExtension () { } public ValueExtension (object value1) { Value1 = value1; } public object Value1 { get; set; } public override object ProvideValue(IServiceProvider serviceProvider) { return Value1; } }
<TextBox Text="{app:ValueExtension test}" ></TextBox>
<TextBox Text="{app:ValueExtension Value1=test}" ></TextBox>
public class EntityBase { public int Id{get;set} } public class MyGenericClass<TType> where TType : EntityBase { public int Id{get;set} public string Test{ get;set; } In XAML: <DataTemplate DataType="{app:GenericType ؟}">
public class GenericType : MarkupExtension { private readonly Type _of; public GenericType(Type of) { _of = of; } public override object ProvideValue(IServiceProvider serviceProvider) { return typeof(MyGenericClass<>)MakeGenericType(_of); } }
<DataTemplate DataType="{app:GenericType app:EntityBase}">
مروری بر نحوهی کارکرد مسیریابی اصلی برنامه
به router-outlet ایی که در فایل قالب src\app\app.component.html قرار گرفتهاست، primary outlet میگویند. زمانیکه کاربر، برنامه را در مرورگر مشاهده میکند، با هربار کلیک بر روی یکی از لینکهای منوی بالای سایت، قالب آنرا در این primary outlet مشاهده میکند. اگر بخواهیم پنل دیگری را در همین صفحه و در همین سطح از نمایش، درج کنیم، نیاز به تعریف outlet دیگری است که به همراه مسیرهای ثانویهای نیز خواهد بود.
تعریف یک router-outlet نامدار
با توجه به اینکه هر پنل به همراه مسیریابی ثانویه، نیاز به router-outlet خودش را خواهد داشت، مسیریاب برای اینکه بداند محتوای آنها را در کجای صفحه درج کند، به نامهای آنها مراجعه میکند. به این ترتیب میتوان چندین router-outlet را در یک سطح از نمایش تعریف کرد؛ اما هرکدام باید دارای نامی منحصربفرد باشند.
در مثال این سری میخواهیم پنلی را در سمت راست صفحهی اصلی درج کنیم. برای تعریف آن در همان سطحی که router-outlet اصلی قرار دارد، نیاز است فایل src\app\app.component.html را ویرایش کنیم:
<div class="container"> <div class="row"> <div class="col-md-10"> <router-outlet></router-outlet> </div> <div class="col-md-2"> <router-outlet name="popup"></router-outlet> </div> </div> </div>
افزودن ماژول جدید پیامهای سیستم
در ادامه ماژول جدید پیامهای سیستم را به همراه تنظیمات ابتدایی مسیریابی آن اضافه خواهیم کرد که در آن ماژول، مدیریت نمایش پیامهای مختلفی در router-outlet ثانویه popup صورت خواهد گرفت:
>ng g m message --routing
در ادامه نیاز است MessageModule را به قسمت imports فایل src\app\app.module.ts نیز معرفی کنیم (پیش از AppRoutingModule که حاوی مسیریابی catch all است):
import { MessageModule } from './message/message.module'; @NgModule({ declarations: [ ], imports: [ BrowserModule, FormsModule, HttpModule, InMemoryWebApiModule.forRoot(ProductData, { delay: 1000 }), ProductModule, UserModule, MessageModule, AppRoutingModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
سپس کامپوننت جدید Message را به ماژول Message برنامه اضافه میکنیم:
>ng g c message/message
پس از آن یک سرویس ابتدایی پیامهای کاربران را نیز اضافه خواهیم کرد:
>ng g s message/message -m message/message.module
installing service create src\app\message\message.service.spec.ts create src\app\message\message.service.ts update src\app\message\message.module.ts
پس از ایجاد قالب ابتدایی فایل message.service.ts آنرا به نحو ذیل تکمیل میکنیم:
import { Injectable } from '@angular/core'; @Injectable() export class MessageService { private messages: string[] = []; isDisplayed = false; addMessage(message: string): void { let currentDate = new Date(); this.messages.unshift(message + ' at ' + currentDate.toLocaleString()); } }
اکنون جهت تکمیل کامپوننت پیامها، ابتدا فایل قالب message.component.html را به نحو ذیل تکمیل میکنیم:
<div class="row"> <h4 class="col-md-10">Message Log</h4> <span class="col-md-2"> <a class="btn btn-default" (click)="close()">x</a> </span> </div> <div *ngFor="let message of messageService.messages; let i=index"> <div *ngIf="i<10" class="message-row"> {{ message }} </div> </div>
کدهای کامپوننت این قالب به صورت ذیل است:
import { MessageService } from './../message.service'; import { Router } from '@angular/router'; import { Component, OnInit } from '@angular/core'; @Component({ //selector: 'app-message', templateUrl: './message.component.html', styleUrls: ['./message.component.css'] }) export class MessageComponent implements OnInit { constructor(private messageService: MessageService, private router: Router) { } ngOnInit() { } close(): void { // Close the popup. this.router.navigate([{ outlets: { popup: null } }]); this.messageService.isDisplayed = false; } }
تکمیل سایر کامپوننتهای برنامه در جهت استفاده از سرویس پیامها
ابتدا به فایل src\app\product\product-edit\product-edit.component.ts مراجعه کرده و سرویس جدید پیامها را به سازندهی آن تزریق میکنیم:
import { MessageService } from './../../message/message.service'; @Component({ selector: 'app-product-edit', templateUrl: './product-edit.component.html', styleUrls: ['./product-edit.component.css'] }) export class ProductEditComponent implements OnInit { constructor(private productService: ProductService, private messageService: MessageService, private route: ActivatedRoute, private router: Router) { }
onSaveComplete(message?: string): void { if (message) { this.messageService.addMessage(message); }
تنظیم مسیرهای ثانویه
نحوهی تعریف مسیریابیهای مرتبط با router-outletهای غیراصلی برنامه، همانند سایر مسیریابیهای برنامهاست؛ با این تفاوت که در اینجا خاصیت outlet نیز به تنظیمات مسیر اضافه خواهد شد. به این ترتیب مشخص خواهیم کرد که محتوای این مسیر باید دقیقا در کدام router-outlet نامدار، درج شود.
برای این منظور فایل src\app\message\message-routing.module.ts را گشوده و تنظیمات مسیریابی آنرا که به صورت RouterModule.forChild تعریف میشوند (چون ماژول اصلی برنامه نیستند)، تکمیل خواهیم کرد:
const routes: Routes = [ { path: 'messages', component: MessageComponent, outlet: 'popup' } ];
فعالسازی یک مسیر ثانویه
در اینجا نیز همانند سایر مسیریابیها، از دایرکتیو routerLink برای فعالسازی مسیرهای ثانویه استفاده میکنیم؛ اما syntax آن کمی متفاوت است:
<a [routerLink]="[{ outlets: { popup: ['messages'] } }]">Messages</a> <a [routerLink]="['/products', product.id, 'edit', { outlets: { popup: ['summary', product.id] } }]">Messages</a>
در دومین لینک تعریف شده، ابتدا یک مسیر اصلی فعال شده و سپس یک مسیر ثانویه نمایش داده میشود.
یک نکته: هرچند به primary outlet نامی انتساب داده نمیشود، اما نام آن دقیقا primary است و میتوان قسمت outlets را به صورت ذیل نیز تعریف کرد:
{ outlets: { primary: ['/products', product.id,'edit'], popup: ['summary', product.id] }}
در ادامه فایل src\app\app.component.html را ویرایش کرده و لینک Show Messages را به آن اضافه میکنیم:
<ul class="nav navbar-nav navbar-right"> <li *ngIf="authService.isLoggedIn()"> <a>Welcome {{ authService.currentUser.userName }}</a> </li> <li> <a [routerLink]="[{ outlets: { popup: ['messages'] } }]">Show Messages</a> </li>
آدرس آن نیز چنین شکلی را پیدا میکند:
http://localhost:4200/products(popup:messages)
اکنون میخواهیم قابلیت مخفی سازی این پنل را نیز پیاده سازی کنیم. به همین جهت از خاصیت isDisplayed سرویس پیامها که توسط دکمهی بستن MessageComponent مدیریت میشود، استفاده خواهیم کرد. بنابراین لینک جدیدی را که در فایل src\app\app.component.html اضافه کردیم، به نحو ذیل تغییر خواهیم داد:
<li *ngIf="!messageService.isDisplayed"> <a (click)="displayMessages()">Show Messages</a> </li> <li *ngIf="messageService.isDisplayed"> <a (click)="hideMessages()">Hide Messages</a> </li>
import { MessageService } from './message/message.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { constructor(private authService: AuthService, private router: Router, private messageService: MessageService) { } displayMessages(): void { this.router.navigate([{ outlets: { popup: ['messages'] } }]); this.messageService.isDisplayed = true; } hideMessages(): void { this.router.navigate([{ outlets: { popup: null } }]); this.messageService.isDisplayed = false; } }
برای فعالسازی یک مسیرثانویه توسط متدهای برنامه، نیاز است از سرویس مسیریاب و متد navigate آن استفاده کرد که نمونههایی از آنرا در اینجا ملاحظه میکنید. پارامترهای ذکر شدهی در اینجا نیز همانند دایرکتیو routerLink هستند.
یک نکته: اگر به متد hideMessages دقت کنید، مقدار value کلید popup به نال تنظیم شدهاست. این مورد سبب خواهد شد تا outlet آن خالی شود. به این ترتیب متد hideMessages علاوه بر مخفی کردن لینک نمایش پیامها، پنل آنرا نیز از صفحه حذف میکند. شبیه به همین نکته در متد close کامپوننت پیامها که دکمهی بستن آنرا به همراه دارد، پیاده سازی شدهاست.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-routing-lab-07.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng s -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
FirstName
firstName
strFirstName lblFirstName
<Company>.<Technology|Produt|Project>[.<Feature>][.<SubNamespace>]
Microsoft.Reporting.WebForms Kara.Support.Manager.Enums Kara.CSS.HQ.WebUI.Configuration
PackageManager , PacakgeConfigGenerator Circle , Utility , Package
CreateConfig , classdata CManager , ClsPackage , Config_Creator , Config1389
CenterCollection , PackageCollection
public delegate void Logger (string log); public delegate void LoggingCallback (object sender, string reason);
AddEventArgs , EditEventArgs , DeleteEventArgs
Adding , Add , Added
public delegate void <EventName>EventHandler (object sender, <EventName>EventArgs e);
public event EventHandler <AddEventArgs> Adding;
DisplayNameAttribute , MessageTypeAttribute
IComponent (اسم) IConnectionProvider (موصوف) ICloneable (صفت)
public enum FileMode { Append, Read, … }
[Flag] public enum KeyModifiers { Alt = 1, Control = 2, Shift = 4 }
public enum OperationState { DoneState, FaultState, RollbackState }
AddDays , Save , DeleteRow , BindData , Close , Open
Radius , ReportType , DataSource , Mode , CurrentCenterId
public CenterCollection Centers { get; set; }
if (list.Contains(item)) if (regularExpression.Matches(text)) if (stream.CanSeek) if (context.Created) if (form.Enabled)
if (list.IsContains(item)) if (regularExpression.Match(text)) if (stream.Seekable) if (context.IsCreated) if (form.IsEnabled)
public Color Color { get; set; }
firstName , e , id , packageId , centerName , name
public static MyType operator +(MyType left, MyType right) public static bool operator ==(MyType left, MyType right)
public static MyType operator ++(MyType value)
public static MyType operator /(MyType dividend, MyType divisor)
public static MyType operator -(MyType d1, MyType d2) // incorrect!
Area , DataBinder , PublicCacheName
_centersList _firstName _currentCenter
parameterType , packageOperationTypeId
lblName (Label) txtHeader (TextBox) btnSave (Button)
ArgumentNullException , InvalidOperaionException
System.Data
<Company>.<Component>.dll <Company>.<Project|Product|Technology>.<Component>.dll
Microsoft.CSharp.dll , Kara.CSS.Manager.dll
public int IComparer<T> {…} public delegate bool Predicate<T> (T item)
public int ISessionChannel<TSession> {…} public delegate TOutput Converter<TInput, TOutput> (TInput from) public class Nullable<T> {…} public class List<T> {…}
ArgumentExceptionIllegalCharacters ArgumentExceptionInvalidName ArgumentExceptionFileNotFound
Pascal Casing |
camel Casing |
Wrong |
Callback |
callback |
CallBack |
BitFlag |
bitFlag |
Bitflag / bitflag |
Canceled |
canceled |
Cancelled |
DoNot |
doNot |
Donot / Don’t |
|
|
|
Endpoint |
endpoint |
EndPoint / endPoint |
FileName |
fileName |
Filename / filename |
Gridline |
gridline |
GridLine / gridLine |
Hashtable |
hashtable |
HashTable / hashTable |
Id |
id |
ID |
Indexes |
indexes |
Indices |
LogOff |
logOff |
Logoff / LogOut ! |
LogOn |
logOn |
Logon / LogIn ! |
SignOut |
signOut |
Signout / SignOff |
SignIn |
signIn |
Signin / SignOn |
Metadata |
metadata |
MetaData / metaData |
Multipanel |
multipanel |
MultiPanel / multiPanel |
Multiview |
multiview |
MultiView / multiView |
Namespace |
namespace |
NameSpace / nameSpace |
Ok |
ok |
OK |
Pi |
pi |
PI |
Placeholder |
placeholder |
PlaceHolder / placeHolder |
UserName |
username |
Username / username |
WhiteSpace |
whiteSpace |
Whitespace / whitespace |
Writable |
writable |
Writeable / writeable |
public class Color { … public static Color FromArgb(…) { … } }
بررسی ساختار کامپوننت Like
در پوشهی components، ابتدا پوشهی جدید common را ایجاد میکنید. در اینجا تمام کامپوننتهای عمومی برنامه را که منحصر به دومین آن برنامه نیستند، قرار میدهیم. کامپوننتهایی را که اگر آنها را به برنامههای دیگری نیز کپی کردیم، بدون هیچ مشکلی قابلیت استفادهی مجدد را داشته باشند و متصل به سرویسها و زیرساخت برنامهی جاری نباشند. سپس در پوشهی common، فایل جدید src\components\common\like.jsx را ایجاد میکنیم و داخل آن توسط میانبرهای imrc و cc در VSCode، ساختار ابتدایی کامپوننت Like را ایجاد میکنیم.
ساختار کلی این کامپوننت به صورت زیر است:
- ورودی این کامپوننت به این صورت است که در آن مشخص شده آیا یک فیلم، مورد علاقه واقع شده یا خیر؛ مانند خاصیت liked که یک boolean است. اگر true باشد، یک آیکن قلب توپر را نمایش میدهد و برعکس.
- خروجی این کامپوننت نیز به صورت یک رخداد است. هر زمانیکه بر روی آیکن قلب آن کلیک میشود، این کامپوننت یک رخداد onClick را سبب خواهد شد. اکنون هر کامپوننت دیگری که در حال استفادهی از آن است، مطلع شده و خاصیت liked شیء مرتبط را تغییر میدهد.
فعلا ساختار ابتدایی آنرا به رندر یک قلب خالی که توسط قلم آیکنهای font-awesome تامین میشود، تنظیم میکنیم:
import React, { Component } from "react"; class Like extends Component { render() { return <i className="fa fa-heart-o" aria-hidden="true"></i>; } } export default Like;
نمایش ابتدایی کامپوننت Like در جدول لیست فیلمها
فعلا مهم نیست که این کامپوننت کار خاصی را انجام نمیدهد. فقط قصد داریم آنرا در UI برنامه نمایش دهیم. به همین جهت ابتدا یک ستون جدید را مخصوص آن، در جدول فعلی نمایش لیست فیلمها، ایجاد کرده و المان آنرا درج میکنیم. برای این منظور به فایل movies.jsx مراجعه کرده و ابتدا این کامپوننت را import میکنیم:
import Like from "./common/like";
سپس در سرستونهای جدول، یک th جدید را تعریف میکنیم تا ستونی برای درج آن ایجاد شود. همچنین در قسمت بدنهی جدول، پیش از دکمهی حذف، یک td مخصوص درج المان </Like> را اضافه میکنیم:
تا اینجا ستون جدید Like را مشاهده میکنید که کار رندر کامپوننتهای Like در آن انجام شدهاست.
واکنش نشان دادن به ورودیها، در کامپوننت Like
در ادامه باید این کامپوننت بر اساس مقدار Boolean ای که از والد خود دریافت میکند، یک آیکن قلب توپر و یا خالی را نمایش دهد. برای این منظور فعلا در کامپوننت movies، جائیکه المان کامپوننت Like درج شدهاست، ویژگی جدید liked را به مقدار ثابت true تنظیم میکنیم </Like liked={true}> تا بتوان قسمت props این کامپوننت را تکمیل کرد.
در کامپوننت Like، تفاوت بین آیکن قلب توپر و خالی در یک o- در انتهای کلاسهای font-awesome است:
import React, { Component } from "react"; class Like extends Component { render() { let classes = "fa fa-heart"; if (!this.props.liked) { classes += "-o"; } return <i className={classes} aria-hidden="true"></i>; } } export default Like;
پس از این تغییرات اگر برنامه را ذخیره کرده و مجددا در مرورگر بارگذاری کنیم، با توجه به تنظیم liked={true} در کامپوننت movies، ستون like آن با آیکنهای قلب توپر نمایش داده میشود که بیانگر واکنش نشان دادن صحیح به ورودیها در کامپوننت Like است:
پویا سازی مقدار پیشفرض ویژگی liked در کامپوننت movies
برای پویاسازی نمایش مقدار liked در کامپوننت movies، از آنجائیکه هر ردیف بیانگر یک شیء movie است، میتوان به این صورت عمل کرد:
<Like liked={movie.liked} />
const movies = [ { _id: "5b21ca3eeb7f6fbccd471815", title: "Terminator", genre: { _id: "5b21ca3eeb7f6fbccd471818", name: "Action" }, numberInStock: 6, dailyRentalRate: 2.5, publishDate: "2018-01-03T19:04:28.809Z", liked: true },
افزودن رخداد کلیک به کامپوننت Like
برای اینکه کامپوننت Like، رویداد کلیک بر روی آیکن قلب را به والد خود گزارش دهد، ابتدا ویژگی جدید onClick را بر روی تعریف المان آن در کامپوننت movies اضافه میکنیم:
<Like liked={movie.liked} onClick={() => this.handleLike(movie)} />
handleLike = movie => { console.log("handleLike", movie); };
return ( <i className={classes} onClick={this.props.onClick} aria-hidden="true" style={{ cursor: "pointer" }} ></i> );
در اینجا همچنین style این المان نیز جهت نمایش cursor با آیکن pointer، توسط یک شیء از نوع inline style تنظیم شدهاست.
یک نکته: کامپوننت Like تا اینجا یک controlled component است؛ دارای state نیست و همچنین تمام اطلاعات خودش را از طریق props تامین میکند و تنها دارای یک متد render است. بنابراین اگر علاقمند بودید میتوان این کامپوننت را به یک «Stateless Functional Component» که در قسمت 8 معرفی شد نیز تبدیل کرد.
تغییر حالت کامپوننت Like جهت نمایش تغییرات
تا اینجا کامپوننت Like ما میتواند ورودی true/false را به آیکنهای متناظری تبدیل کند. همچنین اگر بر روی این آیکن کلیک شود، آنرا توسط رخدادی به والد خود اطلاع رسانی میکند. اکنون میخواهیم با تکمیل متد handleLike، خاصیت like اشیاء انتخابی (آیکنهایی که بر روی آنها کلیک شدهاند) را از true به false و برعکس تبدیل کرده و سپس UI را نیز به روز رسانی کنیم:
handleLike = movie => { console.log("handleLike", movie); const movies = [...this.state.movies]; // cloning an array const index = movies.indexOf(movie); movies[index] = { ...movies[index] }; // cloning an object movies[index].liked = !movies[index].liked; this.setState({ movies }); };
پس از این تغییرات اگر برنامه را اجرا کنیم، با کلیک بر روی هر آیکن، عکس آن آیکن نمایش داده میشود؛ برای مثال آیکن قلب توپر، تبدیل به آیکن قلب توخالی خواهد شد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-10.zip
برپایی پروژههای مورد نیاز
ابتدا یک پوشهی جدید را مانند DownloadFilesSample، ایجاد کرده و در داخل آن دستور زیر را اجرا میکنیم:
> dotnet new react
سپس در این پوشه، پوشهی ClientApp پیشفرض آنرا حذف میکنیم؛ چون کمی قدیمی است. همچنین فایلهای کنترلر و سرویس آب و هوای پیشفرض آنرا به همراه پوشهی صفحات Razor آن، حذف میکنیم.
به علاوه بجای تنظیم پیش فرض زیر در فایل کلاس آغازین برنامه:
spa.UseReactDevelopmentServer(npmScript: "start");
spa.UseProxyToSpaDevelopmentServer("http://localhost:3000");
اکنون در ریشهی پروژهی ASP.NET Core ایجاد شده، دستور زیر را صادر میکنیم تا پروژهی کلاینت React را با فرمت جدید آن ایجاد کند:
> create-react-app clientapp
> cd clientapp > npm install --save bootstrap axios
- برای استفاده از شیوهنامههای بوت استرپ، بستهی bootstrap نیز در اینجا نصب میشود که برای افزودن فایل bootstrap.css آن به پروژهی React خود، ابتدای فایل clientapp\src\index.js را به نحو زیر ویرایش خواهیم کرد:
import "bootstrap/dist/css/bootstrap.css";
- برای دریافت فایلها از سمت سرور، از کتابخانهی معروف axios استفاده خواهیم کرد.
کدهای سمت سرور دریافت فایلهای پویا
در اینجا کدهای سمت سرور برنامه، یک فایل PDF ساده را بازگشت میدهند. این محتوای باینری میتواند حاصل اجرای یک گزارش اکسل، PDF و یا کلا هر نوع فایلی باشد:
using Microsoft.AspNetCore.Mvc; namespace DownloadFilesSample.Controllers { [Route("api/[controller]")] public class ReportsController : Controller { [HttpGet("[action]")] public IActionResult GetPdfReport() { return File(virtualPath: "~/app_data/sample.pdf", contentType: "application/pdf", fileDownloadName: "sample.pdf"); } } }
روش دریافت محتوای باینری در برنامههای React
برای دریافت یک محتوای باینری از سرور توسط axios مانند تصاویر، فایلهای PDF و اکسل و غیره، مهمترین نکته، تنظیم ویژگی responseType آن به blob است:
const getResults = async () => { const { headers, data } = await axios.get(apiUrl, { responseType: "blob" }); }
ساخت URL برای دسترسی به اطلاعات باینری
تمام مرورگرهای جدید از ایجاد URL برای اشیاء Blob دریافتی از سمت سرور، توسط متد توکار URL.createObjectURL پشتیبانی میکنند. این متد، شیء URL را از شیء window جاری دریافت میکند و سپس اطلاعات باینری را دریافت کرده و آدرسی را جهت دسترسی موقت به آن تولید میکند. حاصل آن، یک URL ویژهاست مانند blob:https://localhost:5001/03edcadf-89fd-48b9-8a4a-e9acf09afd67 که گشودن آن در مرورگر، یا سبب نمایش آن تصویر و یا دریافت مستقیم فایل خواهد شد.
در ادامه کدهای تبدیل blob دریافت شدهی از سرور را به این URL ویژه، مشاهده میکنید:
import axios from "axios"; import React, { useEffect, useState } from "react"; export default function DisplayPdf() { const apiUrl = "https://localhost:5001/api/Reports/GetPdfReport"; const [blobInfo, setBlobInfo] = useState({ blobUrl: "", fileName: "" }); useEffect(() => { getResults(); }, []); const getResults = async () => { try { const { headers, data } = await axios.get(apiUrl, { responseType: "blob" }); console.log("headers", headers); const pdfBlobUrl = window.URL.createObjectURL(data); console.log("pdfBlobUrl", pdfBlobUrl); const fileName = headers["content-disposition"] .split(";") .find(n => n.includes("filename=")) .replace("filename=", "") .trim(); console.log("filename", fileName); setBlobInfo({ blobUrl: pdfBlobUrl, fileName: fileName }); } catch (error) { console.log(error); } };
- توسط useEffect Hook و بدون ذکر وابستگی خاصی در آن، سبب شبیه سازی رویداد componentDidUpdate شدهایم. به این معنا که متد getResults فراخوانی شدهی در آن، پس از رندر کامپوننت در DOM فراخوانی میشود و بهترین محلی است که از آن میتوان برای ارسال درخواستهای Ajaxای به سمت سرور و دریافت اطلاعات از backend، استفاده کرد و سپس setState را با اطلاعات جدید فراخوانی نمود. معادل setState در اینجا نیز، همان شیء حالتی است که توسط useState Hook و متد setBlobInfo آن تعریف کردهایم.
- پس از دریافت headers و data از سرور، با استفاده از متد createObjectURL، آنرا تبدیل به یک blob URL کردهایم.
- همچنین در سمت سرور، پارامتر fileDownloadName را نیز تنظیم کردهایم. این نام در سمت کلاینت، توسط هدری با کلید content-disposition ظاهر میشود:
ontent-disposition: "attachment; filename=sample.pdf; filename*=UTF-8''sample.pdf"
- اکنون که نام فایل و URL دسترسی به دادهی فایل باینری دریافتی از سرور را استخراج و ایجاد کردهایم. با فراخوانی متد setBlobInfo، سبب تنظیم متغیر حالت blobInfo خواهیم شد. این مورد، رندر مجدد UI را سبب شده و توسط آن میتوان برای مثال فایل PDF دریافتی را نمایش داد.
نمایش فایل PDF دریافتی از سرور، به همراه دکمههای دریافت، چاپ و بازکردن آن در برگهای جدید
در ادامه کدهای کامل قسمت رندر این کامپوننت را مشاهده میکنید:
import axios from "axios"; import React, { useEffect, useState } from "react"; export default function DisplayPdf() { // ... const { blobUrl } = blobInfo; return ( <> <h1>Display PDF Files</h1> <button className="btn btn-info" onClick={handlePrintPdf}> Print PDF </button> <button className="btn btn-primary ml-2" onClick={handleShowPdfInNewTab}> Show PDF in a new tab </button> <button className="btn btn-success ml-2" onClick={handleDownloadPdf}> Download PDF </button> <section className="card mb-5 mt-3"> <div className="card-header"> <h4>using iframe</h4> </div> <div className="card-body"> <iframe title="PDF Report" width="100%" height="600" src={blobUrl} type="application/pdf" ></iframe> </div> </section> <section className="card mb-5"> <div className="card-header"> <h4>using object</h4> </div> <div className="card-body"> <object data={blobUrl} aria-label="PDF Report" type="application/pdf" width="100%" height="100%" ></object> </div> </section> <section className="card mb-5"> <div className="card-header"> <h4>using embed</h4> </div> <div className="card-body"> <embed aria-label="PDF Report" src={blobUrl} type="application/pdf" width="100%" height="100%" ></embed> </div> </section> </> ); }
در اینجا با انتساب مستقیم blob URL ایجاد شده، به خواص src و یا data اشیائی مانند iframe ،object و یا embed، میتوان سبب نمایش فایل pdf دریافتی از سرور شد. این نمایش نیز توسط قابلیتهای توکار مرورگر صورت میگیرد و نیاز به نصب افزونهی خاصی را ندارد.
در ادامه کدهای مرتبط با سه دکمهی چاپ، دریافت و بازکردن فایل دریافتی از سرور را مشاهده میکنید.
مدیریت دکمهی چاپ PDF
پس از اینکه به blobUrl دسترسی یافتیم، اکنون میتوان یک iframe مخفی را ایجاد کرد، سپس src آنرا به این آدرس ویژه تنظیم نمود و در آخر متد print آنرا فراخوانی کرد که سبب نمایش خودکار دیالوگ چاپ مرورگر میشود:
const handlePrintPdf = () => { const { blobUrl } = blobInfo; if (!blobUrl) { throw new Error("pdfBlobUrl is null"); } const iframe = document.createElement("iframe"); iframe.style.display = "none"; iframe.src = blobUrl; document.body.appendChild(iframe); if (iframe.contentWindow) { iframe.contentWindow.print(); } };
مدیریت دکمهی نمایش فایل PDF در یک برگهی جدید
اگر علاقمند بودید تا این فایل PDF را به صورت تمام صفحه و در برگهای جدید نمایش دهید، میتوان از متد window.open استفاده کرد:
const handleShowPdfInNewTab = () => { const { blobUrl } = blobInfo; if (!blobUrl) { throw new Error("pdfBlobUrl is null"); } window.open(blobUrl); };
مدیریت دکمهی دریافت فایل PDF
بجای نمایش فایل PDF میتوان دکمهای را بر روی صفحه قرار داد که با کلیک بر روی آن، این فایل توسط مرورگر به صورت متداولی جهت دریافت به کاربر ارائه شود:
const handleDownloadPdf = () => { const { blobUrl, fileName } = blobInfo; if (!blobUrl) { throw new Error("pdfBlobUrl is null"); } const anchor = document.createElement("a"); anchor.style.display = "none"; anchor.href = blobUrl; anchor.download = fileName; document.body.appendChild(anchor); anchor.click(); };
اگر خواستید عملیات axios.get و دریافت فایل، با هم یکی شوند، میتوان متد handleDownloadPdf را پس از پایان کار await axios.get، فراخوانی کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: DownloadFilesSample.zip
برای اجرای آن، پس از صدور فرمان dotnet restore که سبب بازیابی وابستگیهای سمت کلاینت نیز میشود، ابتدا به پوشهی clientapp مراجعه کرده و فایل run.cmd را اجرا کنید. با اینکار react development server بر روی پورت 3000 شروع به کار میکند. سپس به پوشهی اصلی برنامهی ASP.NET Core بازگشته و فایل dotnet_run.bat را اجرا کنید. این اجرا سبب راه اندازی وب سرور برنامه و همچنین ارائهی برنامهی React بر روی پورت 5001 میشود.
ابتدا یک پروژه Asp.Net MVC ایجاد کنید. در فولدر scripts تمام فایلهای جاوااسکریپ پروژه قرار خواهند داشت. اگر قصد داشته باشیم که فایلهای جاوااسکریپی سایر فریم ورکها را استفاده نماییم (مثل backbone.js و ExtJs و...) برای طبقه بندی بهتر فایل ها، بهتر است که یک فولدر با نامی مشخص بسازیم و فایلهای مورد نیاز را در آن قرار دهیم. البته اگر از nuget برای نصب این فریم ورکها استفاده نمایید عموما این کار انجام خواهد شد.
حال با استفاده از Package Manager Console و اجرای دستور زیر، اقدام به نصب requireJs کنید
PM> Install-package requireJs
یک فولدر به نام MyFiles در فولدر Scripts بسازید و فایلهای purchase.js و product.js و credits.js در پروژه قبل را در آن کپی نمایید. کد فایلهای پروژه قبل به صورت زیر بوده است:
purchase.js
define(["credits","products"], function(credits,products) { console.log("Function : purchaseProduct"); return { purchaseProduct: function() { var credit = credits.getCredits(); if(credit > 0){ products.reserveProduct(); alert('purchase done');' return true;
} alert('purchase cancel'); return false; } } });
products.js
define(function(products) { return { reserveProduct: function() { console.log("Function : reserveProduct"); return true; } } });
define(function() { console.log("Function : getCredits"); return { getCredits: function() { var credits = "100"; return credits; } } });
برای قدم بعدی، در متد RegisterBundles فایل bundleConfig پروژه دستور زیر را وارد نمایید:
bundles.Add( new ScriptBundle( "~/bundles/require" ).Include( "~/Scripts/require.js" ) );
حال برای استفاده و لود ماژول purchase در انتهای فایل Index فولدر Home تغییرات زیر را اعمال نمایید:
@section scripts { <script type="text/javascript"> require(['purchase'], function (purchase) { purchase.purchaseProduct(); }); </script> }
بر اساس جستجوهایی که انجام دادهام، CHM پشتیبانی کاملی را از یونیکد انجام نمیدهد (مشکل جستجو و همچنین ایندکس کردن).
اما با ترفندی میتوان این مساله را حل کرد و آن هم تبدیل encoding فایلها به عربی است (windows-1256). در این حالت هم جستجو کار میکند و هم عنوان صفحات هنگام جستجو در لیست موارد یاد شده درست نمایش داده میشود و صفحه add to favorites نیز مشکلی در نمایش عنوانهای صفحهها نخواهد داشت. روش کار به شرح زیر است:
الف) encoding تمام فایلهای html خود را به صورت زیر تغییر دهید (از utf-8 به windows-1256):
<meta content="text/html; charset=Windows-1256" http-equiv="Content-Type">
using System.IO;
using System.Text;
public static void SaveAs1256(string fileName)
{
string content = File.ReadAllText(fileName);
File.WriteAllText(fileName, content, Encoding.GetEncoding("windows-1256"));
}
ج) اصلاح فایل hhp پروژه خود
فایل hhp مربوط به html help work shop را باز کنید. (همان فایل پروژه ساخت راهنما)
اگر مثال قبل را دنبال کرده باشید، محتوای فایل آن چیزی شبیه به خطوط زیر خواهد بود:
[OPTIONS]
Compatibility=1.1 or later
Compiled file=test.chm
Contents file=Table of Contents.hhc
Default Window=win1
Default topic=page1.html
Display compile progress=No
Full-text search=Yes
Index file=Index.hhk
Language=0x429 Farsi
Title=راهنمای یک
[WINDOWS]
win1=,"Table of Contents.hhc","Index.hhk","page1.html","page1.html",,,,,0x3420,,0x304e,,,,,,2,,0
[FILES]
page1.html
page2.html
[INFOTYPES]
به قسمت options چند سطر زیر را اضافه کنید: (زبان فارسی و فونت تاهومای عربی)
Default Font=Tahoma,8,178
Language=0x429 Farsi
محض نمونه، کل وبلاگ جاری را به یک فایل chm تبدیل کردهام که آنرا از آدرس زیر میتوانید دریافت نمائید:
دریافت فایل
برای آزمایش، یک عبارت فارسی را در آن جستجو نمائید.
پ.ن.
این راه حلی است که به نظر من رسیده و جواب داده. اگر شما با encoding های دیگر هم جواب گرفتهاید (مشکل جستجوی فارسی حل شده) لطفا پیغام بگذارید. با تشکر.