اهمیت code review
ایجاد یک پروژهی جدید Blazor WASM
برای پیاده سازی و اجرای مثالهای این قسمت، نیاز به یک پروژهی جدید Blazor WASM را داریم که میتوان آنرا با اجرای دستور dotnet new blazorwasm --hosted در یک پوشهی خالی، ایجاد کرد.
یک نکته: دستور فوق به همراه یک سری پارامتر اختیاری مانند hosted-- نیز هست. برای مشاهدهی لیست آنها دستور dotnet new blazorwasm --help را صادر کنید. برای مثال ذکر پارامتر hosted-- سبب میشود تا یک ASP.NET Core host نیز برای Blazor WebAssembly app ایجاد شده تولید شود.
حالت hosted-- آن یک چنین ساختاری را دارد که از سه پروژه و پوشهی Client ،Server و Shared تشکیل میشود:
در اینجا یک پروژهی خالی WASM ایجاد شده که برخلاف حالت معمولی dotnet new blazorwasm که در قسمت قبل آنرا بررسی کردیم، دیگر از فایل استاتیک wwwroot\sample-data\weather.json در آن خبری نیست. بجای آن، یک پروژهی استاندارد ASP.NET Core Web API را در پوشهی جدید Server ایجاد کرده که کار ارائهی اطلاعات این سرویس آب و هوا را انجام میدهد و برنامهی WASM ایجاد شده، این اطلاعات را توسط HTTP Client خود، از سرور Web API دریافت میکند.
بنابراین اگر مدل برنامهای که قصد دارید تهیه کنید، ترکیبی از یک Web API و WASM است، روش hosted--، آغاز آنرا بسیار ساده میکند.
نکته: روش اجرای این نوع برنامهها با اجرای دستور dotnet run در داخل پوشهی Server پروژه، انجام میشود. با اینکار هم سرور ASP.NET Core آغاز میشود و هم برنامهی WASM توسط آن ارائه میگردد. در این حالت اگر آدرس https://localhost:5001 را در مرورگر باز کنیم، هم قسمتهای بدون نیاز به سرور پروژهی WASM قابل دسترسی است (مانند کار با شمارشگر آن) و هم قسمت دریافت اطلاعات از سرور آن، در منوی Fetch Data.
شروع به کار با Razor
پس از ایجاد یک پروژهی جدید WASM، به فایل Client\Pages\Index.razor آن مراجعه کرده و محتوای پیشفرض آنرا بجز سطر اول زیر، حذف میکنیم:
@page "/"
در فایلهای razor. میتوان ترکیبی از کدهای #C و HTML را نوشت. برای مثال:
@page "/" <p>Hello, @name</p> @code { string name = "Vahid N."; }
یک نکته: با توجه به اینکه تغییرات زیادی را در فایل جاری اعمال خواهیم کرد، بهتر است برنامه را با دستور dotnet watch run اجرا کرد، تا این تغییرات را تحت نظر قرار داده و آنها را به صورت خودکار کامپایل کند. به این صورت دیگر نیازی نخواهد بود به ازای هر تغییر، یکبار دستور dotnet run اجرا شود.
در زمان درج متغیرهای #C در بین کدهای HTML توسط razor، استفاده از تمام متدهای الحاقی زبان #C نیز مجاز هستند؛ مانند:
<p>Hello, @name.ToUpper()</p>
یا حتی میتوان یک متد جدید را مانند CustomToUpper در قطعه کد razor، تعریف کرد و از آن به صورت زیر استفاده نمود:
@page "/" <p>Hello, @name.ToUpper()</p> <p>Hello, @CustomToUpper(name)</p> @code { string name = "Vahid N."; string CustomToUpper(string value) => value.ToUpper(); }
<p>Let's add 2 + 2 : @2 + 2 </p>
<p>Let's add 2 + 2 : @(2 + 2) </p>
<button @onclick="@(()=>Console.WriteLine("Test"))">Click me</button>
در اینجا اگر از Console.WriteLine("Test")@ استفاده میشد، به معنای انتساب یک رشتهی محاسبه شده به رویداد onclick بود که مجاز نیست.
روش دیگر انجام اینکار به صورت زیر است:
@page "/" <button @onclick="@WriteLog">Click me 2</button> @code { void WriteLog() { Console.WriteLine("Test"); } }
@page "/" <button @onclick="@(()=>WriteLogWithParam("Test 3"))">Click me 3</button> @code { void WriteLogWithParam(string value) { Console.WriteLine(value); } }
یک نکته: اگر به اشتباه بجای WriteLogWithParam، همان WriteLog قبلی را بنویسیم، کامپایلر (در حال اجرای توسط دستور dotnet watch run) خطای زیر را نمایش میدهد؛ پیش از اینکه برنامه در مرورگر اجرا شود:
BlazorRazorSample\Client\Pages\Index.razor(12,25): error CS1501: No overload for method 'WriteLog' takes 1 arguments
امکان تعریف کلاسها در فایلهای razor.
در فایلهای razor.، محدود به تعریف یک سری متدها و متغیرهای ساده نیستیم. در اینجا امکان تعریف کلاسها نیز وجود دارد و همچنین میتوان از کلاسهای خارجی (کلاسهایی که خارج از فایل razor جاری تعریف شدهاند) نیز استفاده کرد.
@page "/" <p>Hello, @StringUtils.MyCustomToUpper(name)</p> @code { public class StringUtils { public static string MyCustomToUpper(string value) => value.ToUpper(); } }
البته این کلاس را تنها میتوان داخل همین کامپوننت استفاده کرد. برای اینکه بتوان از امکانات این کلاس، در سایر کامپوننتها نیز استفاده کرد، میتوان آنرا در پروژهی Shared قرار داد. اگر به تصویر ابتدای مطلب جاری دقت کنید، سه پروژه ایجاد شدهاست:
الف) پروژهی کلاینت: که همان WASM است.
ب) پروژهی سرور: که یک پروژهی ASP.NET Core Web API ارائه کنندهی سرویس و API آب و هوا است و همچنین هاست کنندهی WASM ما.
ج) پروژهی Shared: کدهای این پروژه، بین هر دو پروژه به اشتراک گذاشته میشوند و برای مثال محل مناسبی است برای تعریف DTO ها. برای نمونه WeatherForecast.cs قرار گرفتهی در آن، DTO یا data transfer object سرویس API برنامه است که قرار است به کلاینت بازگشت داده شود. به این ترتیب دیگر نیازی نخواهد بود تا این تعاریف را در پروژههای سرور و کلاینت تکرار کنیم و میتوان کدهای اینگونه را به اشتراک گذاشت.
کاربرد دیگر آن تعریف کلاسهای کمکی است؛ مانند StringUtils فوق. به همین به پروژهی Shared مراجعه کرده و کلاس StringUtils را به صورت زیر در آن تعریف میکنیم (و یا حتی میتوان این قطعه کد را داخل یک پوشهی جدید، در همان پروژهی WASM نیز قرار داد):
namespace BlazorRazorSample.Shared { public class StringUtils { public static string MyNewCustomToUpper(string value) => value.ToUpper(); } }
پس از آن روش استفادهی از این کلاس کمکی خارجی اشتراکی به صورت زیر است:
@page "/" @using BlazorRazorSample.Shared <p>Hello, @StringUtils.MyNewCustomToUpper(name)</p>
یک نکته: میتوان به فایل Client\_Imports.razor مراجعه و مدخل زیر را به انتهای آن اضافه کرد:
@using BlazorRazorSample.Shared
کار با حلقهها در فایلهای razor.
همانطور که عنوان شد، یکی از کاربردهای پروژهی Shared، امکان به اشتراک گذاشتن مدلها، در برنامههای کلاینت و سرور است. برای مثال یک پوشهی جدید Models را در این پروژه ایجاد کرده و کلاس MovieDto را به صورت زیر در آن تعریف میکنیم:
using System; namespace BlazorRazorSample.Shared.Models { public class MovieDto { public string Title { set; get; } public DateTime ReleaseDate { set; get; } } }
@using BlazorRazorSample.Shared.Models
@page "/" <div> <h3>Movies</h3> @foreach(var movie in movies) { <p>Title: <b>@movie.Title</b></p> <p>ReleaseDate: @movie.ReleaseDate.ToString("dd MMM yyyy")</p> } </div> @code { List<MovieDto> movies = new List<MovieDto> { new MovieDto { Title = "Movie 1", ReleaseDate = DateTime.Now.AddYears(-1) }, new MovieDto { Title = "Movie 2", ReleaseDate = DateTime.Now.AddYears(-2) }, new MovieDto { Title = "Movie 3", ReleaseDate = DateTime.Now.AddYears(-3) } }; }
یک نکته: در حین تعریف فیلدهای code@، امکان استفادهی از var وجود ندارد؛ مگر اینکه از آن بخواهیم در داخل بدنهی یک متد استفاده کنیم.
و یا نمونهی دیگری از حلقههای #C مانند for را میتوان به صورت زیر تعریف کرد:
@for(var i = 0; i < movies.Count; i++) { <div style="background-color: @(i % 2 == 0 ? "blue" : "red")"> <p>Title: <b>@movies[i].Title</b></p> <p>ReleaseDate: @movies[i].ReleaseDate.ToString("dd MMM yyyy")</p> </div> }
نمایش شرطی عبارات در فایلهای razor.
اگر به مثال توکار Client\Pages\FetchData.razor مراجعه کنیم (مربوط به حالت host-- که در ابتدای مطلب عنوان شد)، کدهای زیر قابل مشاهده هستند:
@page "/fetchdata" @using BlazorRazorSample.Shared @inject HttpClient Http <h1>Weather forecast</h1> <p>This component demonstrates fetching data from the server.</p> @if (forecasts == null) { <p><em>Loading...</em></p> } else { <table class="table"> <thead> <tr> <th>Date</th> <th>Temp. (C)</th> <th>Temp. (F)</th> <th>Summary</th> </tr> </thead> <tbody> @foreach (var forecast in forecasts) { <tr> <td>@forecast.Date.ToShortDateString()</td> <td>@forecast.TemperatureC</td> <td>@forecast.TemperatureF</td> <td>@forecast.Summary</td> </tr> } </tbody> </table> } @code { private WeatherForecast[] forecasts; protected override async Task OnInitializedAsync() { forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast"); } }
برای رفع این مشکل، ابتدا یک if@ مشاهده میشود، تا نال بودن forecasts را بررسی کند:
@if (forecasts == null) { <p><em>Loading...</em></p> }
روش نمایش عبارات HTML در فایلهای razor.
فرض کنید عنوان اول فیلم مثال جاری، به همراه یک تگ HTML هم هست:
new MovieDto { Title = "<i>Movie 1</i>", ReleaseDate = DateTime.Now.AddYears(-1) },
<p>Title: <b>@((MarkupString)movie.Title)</b></p>
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-03.zip
برای اجرای آن وارد پوشهی Server شده و دستور dotnet run را اجرا کنید.
در اینجا نیز برای بررسی ویژگیهای جاوا اسکریپت مدرن، یک پروژهی جدید React را ایجاد میکنیم.
> create-react-app sample-03 > cd sample-03 > npm start
همچنین چون در این قسمت خروجی UI نخواهیم داشت، تمام خروجی را در کنسول developer tools مرورگر خود میتوانید مشاهده کنید (فشردن دکمهی F12).
متد Array.map
در برنامههای مبتنی بر React، از متد Array.map برای رندر لیستها استفاده میشود و نمونههای بیشتری از آنرا در قسمتهای بعدی مشاهده خواهید کرد.
فرض کنید آرایهای از رنگها را داریم. اکنون میخواهیم لیستی را به صورت <li>color</li> به ازای هر آیتم آن، تشکیل دهیم:
const colors = ["red", "green", "blue"];
const items = colors.map(function(color) { return "<li>" + color + "</li>"; }); console.log(items);
این مثال را توسط arrow functions نیز میتوان بازنویسی کرد:
const items2 = colors.map(color => "<li>" + color + "</li>"); console.log(items2);
یک مرحلهی دیگر هم میتوانیم این قطعه کد را زیباتر کنیم؛ جمع زدن رشتهها در ES6 معادل بهتری پیدا کردهاست که template literals نام دارد:
const items3 = colors.map(color => `<li>${color}</li>`); console.log(items3);
Object Destructuring
فرض کنید شیء آدرس را به صورت زیر تعریف کردهایم:
const address = { street: "street 1", city: "city 1", country: "country 1" };
const street1 = address.street; const city1 = address.city; const country1 = address.country;
const { street, city, country } = address;
در اینجا باید نام متغیرهای تعریف شده با نام خواص شیء آدرس یکی باشند. همچنین ذکر تمامی این متغیرها نیز ضرورتی ندارد و برای مثال اگر فقط نیاز به street بود، میتوان تنها آنرا ذکر کرد.
اگر خواستیم نام متغیر دیگری را بجای نام خواص شیء آدرس انتخاب کنیم، میتوان از یک نام مستعار ذکر شدهی پس از : استفاده کرد:
const { street: st } = address; console.log(st);
Spread Operator
فرض کنید دو آرایهی زیر را داریم:
const first = [1, 2, 3]; const second = [4, 5, 6];
const combined = first.concat(second); console.log(combined);
در ES6 با استفاده از عملگر ... که spread نیز نام دارد، میتوان قطعه کد فوق را به صورت زیر بازنویسی کرد:
const combined2 = [...first, ...second]; console.log(combined2);
شاید اینطور به نظر برسد که بین دو راه حل ارائه شده آنچنانی تفاوتی نیست. اما مزیت قطعه کد دوم، سهولت افزودن المانهای جدید، به هر قسمتی از آرایه است:
const combined2 = [...first, "a", ...second, "b"]; console.log(combined2);
کاربرد دیگر عملگر spread امکان clone سادهی یک آرایهاست:
const clone = [...first]; console.log(clone);
به علاوه امکان اعمال آن به اشیاء نیز وجود دارد:
const firstObject = { name: "User 1" }; const secondObject = { job: "Job 1" }; const combinedObject = { ...firstObject, ...secondObject, location: "Here" }; console.log(combinedObject);
{name: "User 1", job: "Job 1", location: "Here"}
و امکان clone اشیاء توسط آن هم وجود دارد:
const clonedObject = { ...firstObject }; console.log(clonedObject);
کلاسها در ES 6
قطعه کد کلاسیک زیر را که کار ایجاد اشیاء را در جاوا اسکریپت انجام میدهد، در نظر بگیرید:
const person = { name: "User 1", walk() { console.log("walk"); } }; const person2 = { name: "User 2", walk() { console.log("walk"); } };
class Person { constructor(name) { this.name = name; } walk() { console.log("walk"); } }
باید دقت داشت که class Person تنها یک قالب است و const person ای که پیشتر تعریف شد، یک شیء. برای اینکه از روی قالب تعریف شدهی Person، یک شیء را ایجاد کنیم، به صورت زیر توسط واژهی کلیدی new عمل میشود:
const person3 = new Person("User 3"); console.log(person3.name); person3.walk();
یک نکته: در جاوا اسکریپت، کلاسها نیز شیء هستند! از این جهت که کلاسها در جاوا اسکریپت صرفا یک بیان نحوی زیبای تابع constructor هستند و توابع در جاوا اسکریپت نیز شیء میباشند!
ارث بری کلاسها در ES6
فرض کنید میخواهیم کلاس Teacher را به نحو زیر تعریف کنیم:
class Teacher { teach() { console.log("teach"); } }
class Teacher extends Person { teach() { console.log("teach"); } }
علت اینجا است که کلاس Teacher، نه فقط متد walk کلاس Person را به ارث بردهاست، بلکه سازندهی آنرا نیز به ارث میبرد:
const teacher = new Teacher("User 4");
console.log(teacher.name); teacher.teach(); teacher.walk();
در ادامه فرض کنید علاوه بر ذکر نام، نیاز به ذکر مدرک معلم نیز در سازندهی کلاس وجود دارد:
class Teacher extends Person { constructor(name, degree) {}
Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
class Teacher extends Person { constructor(name, degree) { super(name); this.degree = degree; } teach() { console.log("teach"); } }
const teacher = new Teacher("User 4", "MSc");
در برنامههای React، هر زمانیکه یک کامپوننت جدید تعریف میشود، کلاس آن، از کلاس پایهی کامپوننت، ارث بری خواهد کرد. به این ترتیب میتوان به تمام امکانات این کلاس پایه، بدون نیاز به تکرار آنها در کلاسهای مشتق شدهی از آن، دسترسی یافت.
ماژولها در ES 6
تا اینجا اگر مثالها را دنبال کرده باشید، تمام آنها را داخل همان فایل index.js درج کردهایم. به این ترتیب کم کم دارد مدیریت این فایل از دست خارج میشود. امکان تقسیم کدهای index.js به چندین فایل، مفهوم ماژولها را در ES6 تشکیل میدهد. برای این منظور قصد داریم هر کلاس تعریف شده را به یک فایل جداگانه که ماژول نامیده میشود، منتقل کنیم. از کلاس Person شروع میکنیم و آنرا به فایل جدید person.js و کلاس Teacher را به فایل جدید teacher.js منتقل میکنیم.
البته اگر از افزونههای VSCode استفاده میکنید، اگر کرسر را بر روی نام کلاس قرار دهید، یک آیکن لامپ مانند ظاهر میشود. با کلیک بر روی آن، منویی که شامل گزینهی move to a new file هست، برای انجام سادهتر این عملیات (ایجاد یک فایل جدید js، سپس انتخاب و cut کردن کل کلاس و در آخر کپی کردن آن در این فایل جدید) پیشبینی شدهاست.
هرچند این عملیات تا به اینجا خاتمه یافته به نظر میرسد، اما نیاز به اصلاحات زیر را نیز دارد:
- هنگام کار با ماژولها، اشیاء تعریف شدهی در آن به صورت پیشفرض، خصوصی و private هستند و خارج از آنها قابل دسترسی نمیباشند. به این معنا که class Teacher ما که اکنون در یک ماژول جدید قرار گرفتهاست، توسط سایر قسمتهای برنامه قابل مشاهده و دسترسی نیست.
- برای public تعریف کردن یک کلاس تعریف شدهی در یک ماژول، نیاز است آنرا export کنیم. انجام این کار نیز سادهاست. فقط کافی است واژهی کلیدی export را به پیش از class اضافه کنیم:
export class Teacher extends Person {
برای رفع این مشکل، باید این وابستگی را import کرد:
import { Person } from "./Person"; export class Teacher extends Person {
- مرحلهی آخر، اصلاح فایل index.js است؛ چون اکنون تعاریف Person و Teacher را نمیشناسد.
import { Person } from "./Person"; import { Teacher } from "./Teacher";
Exportهای پیشفرض و نامدار در ES6
اشیاء تعریف شدهی در یک ماژول، به صورت پیشفرض private هستند؛ مگر اینکه export شوند. برای مثال export class Teacher و یا export function xyz. به اینها named exports گویند. حال اگر ماژول ما تنها یک شیء عمومی شده را داشت (کلاسها هم شیء هستند!)، میتوان از واژهی کلیدی default نیز در اینجا استفاده کرد:
export default class Teacher extends Person {
import Teacher from "./Teacher";
در ادامه اگر یک export نامدار دیگر را به این ماژول اضافه کنیم (مانند تابع testTeacher):
import { Person } from "./Person"; export function testTeacher() { console.log("Test Teacher"); } export default class Teacher extends Person {
import Teacher, { testTeacher } from "./Teacher";
import React, { Component } from 'react';
یک نکته: اگر در VSCode داخل {}، دکمههای ctrl+space را فشار دهید، میتوانید منوی exportهای ماژول تعریف شده را مشاهده کنید.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-03.zip
بازسازی LocalDb
sqllocaldb create "v11.0"
Creation of LocalDB instance "v11.0" with version 11.0 failed because of the following error: LocalDB instance is corrupted. See the Windows Application event log for event details.
sqllocaldb delete "v11.0"
sqllocaldb create "v11.0"
export const genres = [ { _id: "5b21ca3eeb7f6fbccd471818", name: "Action" }, { _id: "5b21ca3eeb7f6fbccd471814", name: "Comedy" }, { _id: "5b21ca3eeb7f6fbccd471820", name: "Thriller" } ]; export function getGenres() { return genres.filter(g => g); }
بررسی ساختار کامپوننت ListGroup
شبیه به کامپوننت صفحه بندی که در قسمت قبل ایجاد کردیم، میخواهیم کامپوننت ListGroup نیز به طور کامل از اشیاء movie مستقل باشد؛ تا در آینده بتوان از آن در جاهای دیگری نیز استفاده کرد. به همین جهت فایل جدید src\components\common\listGroup.jsx را ایجاد کرده و سپس با استفاده از میانبرهای imrc و cc در VSCode، ساختار ابتدایی این کامپوننت را ایجاد میکنیم. هرچند میتوان این کامپوننت را به صورت «Stateless Functional Component» نیز طراحی کرد؛ چون state و متد دیگری بجز render نخواهد داشت و تمام اطلاعات خودش را از والد خود دریافت میکند.
سپس به کامپوننت movies مراجعه کرده و این کامپوننت خالی را import میکنیم:
import ListGroup from "./common/listGroup";
برای این منظور ابتدا React.Fragment موجود را با یک div با "className="row جایگزین میکنیم. سپس داخل این row، دو ستون را تعریف خواهیم کرد که در اولی، المان جدید ListGroup قرار میگیرد و در دومی، مابقی عناصری که تاکنون اضافه کردهایم؛ مانند جدول، صفحه بندی و نمایش تعداد آیتمها:
return ( <div className="row"> <div className="col-2"> <ListGroup /> </div> <div className="col"> ... </div> </div> );
import { getGenres } from "../services/fakeGenreService"; // ... class Movies extends Component { state = { // ... genres: getGenres() };
class Movies extends Component { state = { movies: [], pageSize: 4, currentPage: 1, genres: [] }; componentDidMount() { this.setState({ movies: getMovies(), genres: getGenres() }); }
پس از آن میتوان ویژگی جدید items این کامپوننت را به آرایهی genres دریافتی از state، تنظیم کرد:
<ListGroup items={this.state.genres} />
بهتر است هر زمانیکه کاربر، آیتمی را از این لیست انتخاب کرد، توسط بروز رخدادی مانند onItemSelect از وقوع آن مطلع شد و سپس نسبت به آن توسط متد handleGenreSelect، واکنش نشان داد؛ مانند فیلتر کردن لیست فیلمها بر اساس آیتم انتخابی و نمایش آن. به همین جهت ویژگی onItemSelect را به تعریف المان ListGroup اضافه میکنیم:
<ListGroup items={this.state.genres} onItemSelect={this.handleGenreSelect} />
handleGenreSelect = genre => { console.log("handleGenreSelect", genre); };
پیاده سازی نمایش آیتمها در کامپوننت ListGroup
پیاده سازی ابتدایی کامپوننت ListGroup را در اینجا مشاهده میکنید:
import React, { Component } from "react"; class ListGroup extends Component { render() { return ( <ul className="list-group"> {this.props.items.map(item => ( <li key={item._id} className="list-group-item"> {item.name} </li> ))} </ul> ); } } export default ListGroup;
تا اینجا اگر برنامه را ذخیره کرده و در مرورگر نمایش دهیم، به خروجی زیر میرسیم:
البته به نظر عرض ستون آن نامناسب است. به همین جهت به کامپوننت movies مراجعه کرده و col-2 ستون آنرا به col-3 تبدیل میکنیم.
پویا سازی انتخاب نام خواص شیء دریافتی، در کامپوننت ListGroup
در حال حاضر پیاده سازی کامپوننت ListGroup، به شیءای دقیقا با خواص id_ و name وابستهاست و اگر شیء دیگری را که دارای خواصی معادل این نامها نیست، به آن ارسال کنیم، دیگر کار نخواهد کرد. به همین جهت در محل تعریف المان این کامپوننت در کامپوننت movies، دو ویژگی دیگر نام خواص شیء مدنظر را تنظیم میکنیم تا بتوانیم با هر نوع شیءای در اینجا کار کنیم:
<ListGroup items={this.state.genres} textProperty="name" valueProperty="_id" onItemSelect={this.handleGenreSelect} />
import React, { Component } from "react"; class ListGroup extends Component { render() { return ( <ul className="list-group"> {this.props.items.map(item => ( <li key={item[this.props.valueProperty]} className="list-group-item"> {item[this.props.textProperty]} </li> ))} </ul> ); } } export default ListGroup;
تعیین مقادیر پیشفرضی برای خواص props
با زیاد شدن تعداد خواص props، اینترفیس کامپوننتها پیچیدهتر میشوند. در یک چنین حالتی میتوان در کامپوننتها defaultProps را تعریف کرد و توسط آن مقادیر پیشفرضی را برای خواص props درنظر گرفت. به این صورت در حین تعریف المان این کامپوننت، اگر مقادیر مدنظر با مقادیر پیشفرض تعیین شده یکی باشند، دیگر نیازی به ذکر این پارامترها نخواهد بود. برای مثال در انتهای کامپوننت ListGroup، خاصیت جدید defaultProps را تعریف میکنیم (املای آن باید دقیقا به همین شکل باشد؛ و گرنه شناخته نخواهد شد). سپس در اینجا خواصی را که میخواهیم مقادیر پیشفرضی را برای آنها تعیین کنیم، ذکر خواهیم کرد:
ListGroup.defaultProps = { textProperty: "name", valueProperty: "_id" }; export default ListGroup;
<ListGroup items={this.state.genres} onItemSelect={this.handleGenreSelect} />
مدیریت انتخاب گروههای فیلمها
در ادامه میخواهیم رخداد onClick بر روی هر li این لیست را مدیریت کنیم و سبب بروز رخدادی به نام onItemSelect شویم که در ابتدای بحث، آنرا به عنوان خروجی این کامپوننت تعریف کردیم. این رخداد نیز در کامپوننت movies به متد handleGenreSelect متصل است. به همین جهت تعریف ویژگی onClick را که سبب انتقال شیء جاری رندر شده، توسط رویداد onItemSelect به خارج از آن میشود، به المان li کامپوننت ListGroup اضافه میکنیم:
<li key={item[this.props.valueProperty]} className="list-group-item" onClick={() => this.props.onItemSelect(item)} style={{ cursor: "pointer" }} > {item[this.props.textProperty]} </li>
پس از فعالسازی امکان کلیک بر روی هر آیتم لیست رندر شده، اکنون میخواهیم با انتخاب هر گروه، این گروه در این لیست، به صورت انتخاب شده، همانند شماره صفحهی انتخاب شدهی در کامپوننت صفحه بندی، تغییر رنگ دهد و متمایز نمایش داده شود تا مشخص باشد که هم اکنون با کدام آیتم در حال کار هستیم. برای اینکار تنها کافی است کلاس active را به صورت پویا به className هر li، اضافه یا کم کنیم. البته برای این منظور این کامپوننت باید از آیتم انتخاب شده مطلع باشد؛ به همین جهت selectedItem را در لیست ویژگیهای اینترفیس تعریف این المان اضافه میکنیم. برای اینکار ابتدا selectedGenre را با هربار فراخوانی handleGenreSelect که به onItemSelect کامپوننت متصل است، با فراخوانی متد setState به روز رسانی میکنیم:
handleGenreSelect = genre => { console.log("handleGenreSelect", genre); this.setState({selectedGenre: genre}); };
class Movies extends Component { state = { // ... selectedGenre: {} };
<ListGroup items={this.state.genres} onItemSelect={this.handleGenreSelect} selectedItem={this.state.selectedGenre} />
<li key={item[this.props.valueProperty]} className={ item === this.props.selectedItem ? "list-group-item active" : "list-group-item" } style={{ cursor: "pointer" }} onClick={() => this.props.onItemSelect(item)} > {item[this.props.textProperty]} </li>
مدیریت فیلتر کردن اطلاعات گروه فیلم انتخابی
در قسمت قبل، در ابتدای متد رندر کامپوننت movies، از متد paginate برای صفحه بندی اطلاعات استفاده کردیم. فیلتر گروه جاری انتخاب شده را باید پیش از این متد قرار دارد؛ چون تعداد صفحات و اطلاعات نمایش داده شدهی در هر کدام باید بر اساس لیست فیلمهای فیلتر شده باشد.
برای انجام اینکار تغییرات زیر را اعمال خواهیم کرد:
الف) بجای متد paginate، از متد getPagedData زیر استفاده میکنیم:
getPagedData() { const { pageSize, currentPage, selectedGenre, movies: allMovies } = this.state; const filteredMovies = selectedGenre && selectedGenre._id ? allMovies.filter(m => m.genre._id === selectedGenre._id) : allMovies; const first = (currentPage - 1) * pageSize; const last = first + pageSize; const pagedMovies = filteredMovies.slice(first, last); return { totalCount: filteredMovies.length, data: pagedMovies }; }
- در حین Object Destructuring، نام خاصیت movies را نیز به allMovies تغییر دادهایم تا واضحتر باشد.
- در ادامه با استفاده از متد filter جاوااسکریپت، بر اساس id هر گروه انتخاب شده، اشیاء مرتبط با آن، از allMovies جدا شده و بازگشت داده میشود. البته اگر id هم انتخاب نشده باشد (اولین بار نمایش صفحه)، تمام رکوردها یعنی allMovies، مورد استفاده قرار میگیرد.
- پس از آن، همان کدهای صفحه بندی اطلاعات را که در قسمت قبل بررسی کردیم، مشاهده میکنید که اینبار بجای allMovies قسمت قبل، بر روی filteredMovies اعمال شدهاست.
- در آخر، این متد، یک شیء را با دو خاصیت که بیانگر تعداد کل رکوردهای انتخاب شده و دادههای فیلتر شدهی صفحه بندی شدهاست، بازگشت میدهد.
ب) تغییرات متد رندر کامپوننت movies به صورت زیر است:
- ابتدا متد getPagedData فوق، فراخوانی شده و شیء دریافتی از آن با استفاده از ویژگی Object Destructuring، به دو خاصیت totalCount و movies انتساب داده میشود:
render() { const { length: count } = this.state.movies; if (count === 0) return <p>There are no movies in the database.</p>; const { totalCount, data: movies } = this.getPagedData();
- همچنین کامپوننت صفحه بندی، اینبار باید totalCount آیتمهای فیلتر شده را نمایش دهد و نه totalCount تمام فیلمهای موجود را:
<Pagination itemsCount={totalCount}
<p>Showing {totalCount} movies in the database.</p>
handleGenreSelect = genre => { console.log("handleGenreSelect", genre); this.setState({ selectedGenre: genre, currentPage: 1 }); };
افزودن گزینهی نمایش تمام اطلاعات به لیست گروههای فیلمها
در ادامه قصد داریم به بالای لیست گروههای موجود، گزینهی All Genres را نیز اضافه کنیم تا با کلیک بر روی آن، مجددا بتوان لیست تمام فیلمهای موجود را مشاهده کرد.
برای این منظور در جائیکه لیست getGenres را دریافت و نمایش میدهیم، یعنی متد componentDidMount، اندکی تغییر ایجاد کرده و یک آرایهی جدید را ایجاد میکنیم؛ بطوریکه اولین عنصر آن، گزینهی جدید All Genres باشد و سپس توسط spread operator، مابقی عناصر آرایهی گروهها را به این آرایهی جدید اضافه میکنیم:
componentDidMount() { const genres = [{ _id: "", name: "All Genres" }, ...getGenres()]; this.setState({ movies: getMovies(), genres }); }
const filteredMovies = selectedGenre && selectedGenre._id ? allMovies.filter(m => m.genre._id === selectedGenre._id) : allMovies;
روش TPH
در این روش، ارث بری از طریق فقط یک جدول ایجاد میشود و زیر مجموعهها بر اساس مقدار یک فیلد از یکدیگر متمایز میشوند. پس اگر جدولی دارید که برای متمایز کردن رکوردهای آن از یک فیلد استفاده میکنید، روش TPH مناسب شما است. با روش TPH نیز میتوانید به همان مدلی که در روش TPT دارید برسید، تنها تفاوت این هست که در روش TPH، تمامی دادهها در یک جدول قرار دارند و یک فیلد برای متمایز کردن رکوردها استفاده میشود. همه چیز با مثال عملی واضحتر است. پس کار خود را با یک مثال ادامه میدهیم. جدول مثال ما در شکل زیر مشخص است.
به نظر میرسد که این جدول با جداول قسمت قبل شباهتی دارد. بله! فیلدهای جداول مثال قبل در این جدول آمده اند.
- فیلدهای FirstName و LastName از جدول Persons
- فیلد HireDate از جدول Instructors
- فیلد EnrollmentDate، Credits و Degree از جدول Students
- فیلد AdminDate از جدول Admins
- فیلدهای BusinessCredits و Discipline از جدول BusinessStudents
یک فیلد با نام PersonCategory نیز اضافه شده است که «مقداری عددی» میپذیرد و برای «متمایز کردن رکوردها» استفاده میشود:
- 1 ، نمایانگر Student
- 2 ، نمایانگر Instructor
- 3 ، نمایانگر Admin
- 4 ، نمایانگر BusinessStudent
از این جدول میخواهیم به مدل قسمت اول برسیم اما این بار با استفاده از روش TPH. در شکل زیر، جدول Persons به صورت مدل شده در برنامه نشان داده شده است.
حال باید خصیصههای موجودیت Person را به موجودیتهای مشتق شده منتقل کرد. بدین منظور، هر خصیصه از موجودیت Person را انتخاب، کلیدهای Ctrl+X را فشار دهید، سپس بر روی قسمت Properties موجودیت مشتق شدهی مورد نظر رفته و کلیدهای Ctrl+V را فشار دهید. نتیجه در شکل زیر نشان داده شده است.
اکنون زمان آن رسیده است تا جدول متناظر با هر یک از موجودیتهای مشتق شده را معرفی کنیم. تمامی موجودیتهای مشتق شده از جدول Persons استفاده میکنند. بر روی هر یک از آنها کلیک راست کرده و گزینهی Table Mapping را انتخاب کنید. پنجره ی Mapping Details نشان داده میشود. ابتدا بر روی عبارت Add a Table or View و سپس بر روی نشانگر رو به پایینی که کنار آن ظاهر میشود کلیک کنید (شکل زیر).
آیتم Persons را انتخاب کنید. اکنون باید فیلد تفکیک کنندهی رکوردها را مشخص کنیم. برای این حالت باید یک شرط ایجاد نمود. در همان پنجرهی Mapping Details، عبارتی با عنوان Add a Condition وجود دارد. بر روی آن کلیک و در لیستی که ظاهر میشود، آیتم PersonCategory را انتخاب کنید (شکل زیر).
سپس در ستون Value/Property، مقدار آن را "1" قرار دهید (شکل زیر).
تناظر میان موجودیت و جدول Persons و مقداردهی مناسب به فیلد متمایز کننده را برای تمامی موجودیتهای مشتق شده انجام دهید. دلیل این کار این است که EF بداند هر رکورد در چه زمانی باید به چه موجودیتی تبدیل شود. دقت کنید که پیشتر به مقدار فیلد متمایز کننده برای هر موجودیت اشاره کردیم. نکتهی مهم اینکه یک شرط نیز باید برای موجودیت Person ایجاد و مقدار فیلد متمایز کنندهی آن را "صفر" تعریف کنید.
مثال ما آماده است. آن را امتحان میکنیم.
using (PersonDbEntities context = new PersonDbEntities()) { var people = from p in context.Persons select p; foreach (Person person in people) { Console.WriteLine("{0}, {1}", person.LastName, person.FirstName); if (person is Student) Console.WriteLine(" Degree: {0}", ((Student)person).Degree); if (person is BusinessStudent) Console.WriteLine(" Discipline: {0}", ((BusinessStudent)person).Discipline); } Console.ReadLine(); }
مزایای روش TPH
- سرعت بالای عملیات CRUD، به دلیل وجود تمامی دادهها در یک جدول
- تعداد جداول در پایگاه داده، کم و مدیریت آنها آسانتر است
معایب روش TPH
- افزونگی داده ها. مقادیر برخی ستونها برای بعضی از رکوردها، حاوی مقدار NULL است و تعداد این ستونها به تعداد زیر مجموعهها ارتباط دارد
- عیب اول، باعث میشود تا در صورتی که دادهها به صورت دستی تغییر پیدا کنند، جامعیت دادهها از بین برود
- افزایش بی دلیل حجم داده ها
- اضافه و حذف کردن موجودیتها به مدل، عملی زمانبر و پیچیده است
public static void CreateUniqueIndex(this DbContext context, string tableName, string fieldName) { context.Database.ExecuteSqlCommand("CREATE UNIQUE INDEX [IX_Unique_" + tableName + "_" + fieldName + "] ON [" + tableName + "]([" + fieldName + "] ASC);"); }
public class MyDbMigrationsConfiguration : DbMigrationsConfiguration<MyContext> { public BlogDbMigrationsConfiguration() { AutomaticMigrationsEnabled = true; AutomaticMigrationDataLossAllowed = true; } protected override void Seed(MyContext context) { CreateUniqueIndex(context, "table1", "field1"); base.Seed(context); } }
برای حل این مشکل و تبدیل کدهای فوق به کدهایی Refactoring friendly، نیاز است نام جدول به صورت خودکار از DbContext دریافت شود. همچنین نام خاصیت یا فیلد نیز به صورت strongly typed قابل تعریف باشد.
کدهای کامل نمونه بهبود یافته را در ذیل مشاهده میکنید:
using System; using System.Data.Entity; using System.Data.Entity.Infrastructure; using System.Data.Objects; using System.Linq; using System.Linq.Expressions; using System.Text.RegularExpressions; namespace General { public static class ContextExtensions { public static string GetTableName<T>(this DbContext context) where T : class { ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext; return objectContext.GetTableName<T>(); } public static string GetTableName<T>(this ObjectContext context) where T : class { var sql = context.CreateObjectSet<T>().ToTraceString(); var regex = new Regex("FROM (?<table>.*) AS"); var match = regex.Match(sql); string table = match.Groups["table"].Value; return table .Replace("`", string.Empty) .Replace("[", string.Empty) .Replace("]", string.Empty) .Replace("dbo.", string.Empty) .Trim(); } private static bool hasUniqueIndex(this DbContext context, string tableName, string indexName) { var sql = "SELECT count(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS where table_name = '" + tableName + "' and CONSTRAINT_NAME = '" + indexName + "'"; var result = context.Database.SqlQuery<int>(sql).FirstOrDefault(); return result > 0; } private static void createUniqueIndex(this DbContext context, string tableName, string fieldName) { var indexName = "IX_Unique_" + tableName + "_" + fieldName; if (hasUniqueIndex(context, tableName, indexName)) return; var sql = "ALTER TABLE [" + tableName + "] ADD CONSTRAINT [" + indexName + "] UNIQUE ([" + fieldName + "])"; context.Database.ExecuteSqlCommand(sql); } public static void CreateUniqueIndex<TEntity>(this DbContext context, Expression<Func<TEntity, object>> fieldName) where TEntity : class { var field = ((MemberExpression)fieldName.Body).Member.Name; createUniqueIndex(context, context.GetTableName<TEntity>(), field); } } }
توضیحات
متد GetTableName ، به کمک SQL تولیدی حین تعریف جدول متناظر با کلاس جاری، نام جدول را با استفاده از عبارات باقاعده جدا کرده و باز میگرداند. به این ترتیب به دقیقترین نامی که واقعا جهت تولید جدول مورد استفاده قرار گرفته است خواهیم رسید.
در مرحله بعد آن، همان متد createUniqueIndex ابتدای بحث را ملاحظه میکنید. در اینجا جهت حفظ سازگاری بین SQL Server کامل و SQL CE از UNIQUE CONSTRAINT استفاده شده است که همان کار ایجاد ایندکس منحصربفرد را نیز انجام میدهد. به علاوه مزیت دیگر آن امکان دسترسی به تعاریف قید اضافه شده توسط view ایی به نام INFORMATION_SCHEMA.TABLE_CONSTRAINTS است که در نگارشهای مختلف SQL Server به یک نحو تعریف گردیده و قابل دسترسی است. از این view در متد hasUniqueIndex جهت بررسی تکراری نبودن UNIQUE CONSTRAINT در حال تعریف، استفاده میشود. اگر این قید پیشتر تعریف شده باشد، دیگر سعی در تعریف مجدد آن نخواهد شد.
متد CreateUniqueIndex تعریف شده در انتهای کلاس فوق، امکان دریافت نام خاصیتی از TEntity را به صورت strongly typed میسر میسازد.
اینبار برای تعریف یک قید و یا ایندکس منحصربفرد بر روی خاصیتی مشخص در متد Seed، تنها کافی است بنویسیم:
context.CreateUniqueIndex<User>(x=>x.Name);
آموزش LINQ بخش پنجم
• عبارت group
• عبارت orderby
• کلمات کلیدی ascending و descending
• کلمه کلیدی by
بررسی عبارت group و کلمهی کلیدی by
عبارت group یک توالی یک بعدی (flat sequence) یا ساده را از ورودی دریافت و یک توالی گروه بندی شده را تولید میکند.
Ingredient[] ingredients = { new Ingredient{Name = "Sugar", Calories=500}, new Ingredient{Name = "Lard", Calories=500}, new Ingredient{Name = "Butter", Calories=500}, new Ingredient{Name = "Egg", Calories=100}, new Ingredient{Name = "Milk", Calories=100}, new Ingredient{Name = "Flour", Calories=50}, new Ingredient{Name = "Oats", Calories=50} }; IEnumerable<IGrouping<int, Ingredient>> query = from i in ingredients group i by i.Calories; foreach (IGrouping<int, Ingredient> group in query) { Console.WriteLine($"Ingredients with {group.Key} calories"); foreach (Ingredient ingredient in group) { Console.WriteLine($"- { ingredient.Name}"); } }
Ingredients with 500 calories - Sugar - Lard - Butter Ingredients with 100 calories - Egg - Milk Ingredients with 50 calories - Flour - Oats
برای آشنایی بیشتر با اینترفیس IGrouping اینجا را مطالعه کنید.
عبارت orderby و کلمات کلیدی ascending و descending
عبارت orderby برای تولید یک توالی مرتب شدهی بصورت صعودی (ascending) و یا نزولی (descending) مورد استفاده قرار میگیرد.
مثال: توالی مثال قبل را در نظر بگیرد:
IOrderedEnumerable<Ingredient> sortedByNameQuery = from i in ingredients orderby i.Name select i; foreach (var ingredient in sortedByNameQuery) { Console.WriteLine(ingredient.Name); }
Butter Egg Flour Lard Milk Oats Sugar
IOrderedEnumerable<Ingredient> sortedByNameQuery = from i in ingredients orderby i.Name descending select i;
Sugar Oats Milk Lard Flour Egg Butter
Ingredient[] ingredients = { new Ingredient{Name = "Sugar", Calories=500}, new Ingredient{Name = "Lard", Calories=500}, new Ingredient{Name = "Butter", Calories=500}, new Ingredient{Name = "Egg", Calories=100}, new Ingredient{Name = "Milk", Calories=100}, new Ingredient{Name = "Flour", Calories=50}, new Ingredient{Name = "Oats", Calories=50} }; IEnumerable<IGrouping<int, Ingredient>> query = from i in ingredients group i by i.Calories into calorieGroup orderby calorieGroup.Key select calorieGroup; foreach (IGrouping<int, Ingredient> group in query) { Console.WriteLine($"Ingredients with {group.Key} calories"); foreach (Ingredient ingredient in group) { Console.WriteLine($"- { ingredient.Name}"); } }
Ingredients with 50 calories - Flour - Oats Ingredients with 100 calories - Egg - Milk Ingredients with 500 calories - Sugar - Lard - Butter
در این سری آموزشی با دو روش نوشتن پرس و جو، در LINQ آشنا شدیم:
1- Fluent Style (استفاده از متدهای الحاقی برای انجام عملیاتهای مختلف بر روی توالی)
2- Expression Style (استفاده از کلمات کلیدی (key word) برای انجام عملیاتهای مختلف بر روی توالی)
هر یک از روشهای فوق مزایایی دارند که با توجه به شرایطی که با آن روبرو هستیم از آنها استفاده میکنیم:
var q1 = ingredients.Where(x => x.Calories > 100); var q2 = from i in ingredients where i.Calories > 100 select i;
• پرس و جویی که قرار است نوشته شود، از نظر عملیات بر روی توالی ساده باشد: در این حالت روش استفاده شده تفاوتی نمیکند و بستگی به روش برگزیده و مورد علاقهی برنامه نویس و یا تیم برنامه نویسی دارد. مثلا زمانیکه تنها عملگرهای Where و orderby بخواهند اجرا شود.
• پرس و جوهایی که با متغیرهای range مختلفی رو برو هستند: در این حالت استفاده از عبارتهای پرس و جو راحت از روش عملگرهای پرس و جو میباشد.
لیستی از عملگرهای جستجو که در روش عبارتهای جستجو معادل آنها مهیا شده است :
• GroupBy
• GroupJoin
• Join
• OrderBy
• OrderByDescending
• Select
• SelectMany
• ThenBy
• ThenByDescending
• Where
در بسیاری از پرس و جوها میتوانیم هر دو روش را با هم ترکیب کنیم که نمونهای از آن، در جلسهی چهارم در زمان استفادهی از متد DefaultIfEmpty استفاده شد. درمثال زیر استفاده از عملگر count، با استفاده از روش اول، به همراه عبارت پرس و جوی تولید شدهی با روش دوم، نمایش داده شده است:
int mixedQuery = (from i in ingredients where i.Calories > 100 select i).Count();
کتابخانه PdfReport به عمد جهت دات نت 3.5 تهیه شده است تا بازه وسیعی از سیستم عاملها را پوشش دهد.
این کتابخانه علاوه بر تبدیل اطلاعات شما به گزارشات مبتنی بر PDF، امکان تهیه خروجی خودکار اکسل (2007 به بعد) را نیز دارد. فایل خروجی آن، به صورت پیوست درون فایل PDF تهیه شده قرار میگیرد و جزئی از آن میشود.
بدیهی است اینبار با کتابخانه گزارش سازی روبرو هستید که با راست به چپ مشکلی ندارد!
کتابخانه PdfReport بر پایه کتابخانههای معروف سورس باز iTextSharp و EPPlus تهیه شده است. حداقل مزیت استفاده از آن، صرفه جویی در وقت شما جهت آموختن ریزه کاریهای مرتبط با هر کدام از کتابخانههای یاده شده است. برای نمونه جهت فراگیری کار با iTextSharp نیاز است یک کتاب 600 صفحهای به نام iText in action را مطالعه و تمرین کنید. این مورد منهای مسایل و نکات متعدد مرتبط با زبان فارسی است که در این کتاب به آنها اشارهای نشده است.
برای تهیه آن، مشکلات متداولی که کاربران مدام در انجمنهای برنامه نویسی مطرح میکنند و ابزارهای موجود عاجز از ارائه راه حلی ساده برای حل آنها هستند، مد نظر بوده و امید است نگارش یک این کتابخانه بتواند بسیاری از این دردها را کاهش دهد.
کار با این کتابخانه صرفا با کدنویسی میسر است (code first) و همین مساله انعطاف پذیری قابل توجهی را ایجاد کرده که در طی روزهای آینده با جزئیات بیشتر آن آشنا خواهید شد.
اما چرا PDF؟
استفاده از قالب PDF برای تهیه گزارشات، این مزایا را به همراه دارد:
- دقیقا همان چیزی که مشاهده میشود، در هر مکانی قابل چاپ است. با همان کیفیت، همان اندازه صفحه، همان فونت و غیره. به این ترتیب به صفحه بندی بسیار مناسب و بهینهای میتوان رسید و مشکلات گزارشات HTML ایی وب را ندارد.
- امکان استفاد از فونتهای شکیلتر در آن بدون مشکل و بدون نیاز به نصب بر روی کامپیوتری میسر است؛ چون فونت را میتوان در فایل PDF نیز قرار داد.
- این فایل در تمام سیستم عاملها پشتیبانی میشود. خصوصا اینکه فایل نهایی در تمام کامپیوترها و در انواع و اقسام سیستم عاملها به یک شکل و اندازه نمایش داده خواهد شد. برای مثال اینطور نیست که در ویندوز XP ،اعداد آن فارسی نمایش داده شوند و در ویندوز 7 با تنظیمات محلی خاصی، دیگر اینطور نباشد. حتی تحت لینوکس هم اعداد آن فارسی نمایش داده خواهد شد چون فونت مخصوص بکار رفته، در خود فایل PDF قابل قرار دادن است.
- برنامه معروف و رایگان Adobe reader برای خواندن و مشاهده آن کفایت میکند و البته کلاینت یکبار باید این برنامه را نصب کند. همچنین از این نوع برنامههای رایگان برای مشاهده فایلهای PDF زیاد است.
- تمام صفحات گزارش را در یک فایل میتوان داشت و به یکباره تمام آن نیز به سادگی قابل چاپ است. این مشکلی است که با گزارشات تحت وب وجود دارد که نمیشود مثلا یک گزارش 100 صفحهای را به یکباره به چاپگر ارسال کرد. به همین جهت عموما کاربران درخواست میدهند تا کل گزارش را در یک صفحه HTML نمایش دهید تا ما راحت آنرا چاپ کنیم یا راحت از آن خروجی بگیریم. اما زمانیکه فایل PDF تهیه میشود این مشکلات وجود نخواهد داشت و جهت Print بسیار بهینه سازی شده است. تا حدی که الان فرمت برگزیده تهیه کتابهای الکترونیکی نیز PDF است. مثلا سایت معروف آمازون امکان فروش نسخه PDF کتابها را هم پیش بینی کرده است.
-امکان صفحه بندی دقیق به همراه مشخص سازی landscape یا portrait بودن صفحه نهایی میسر است. چیزی که در گزارشات صفحات وب آنچنان معنایی ندارد.
- امکان رمزنگاری اطلاعات در آن پیش بینی شده است. همچنین میتوان به فایلهای PDF امضای دیجیتال نیز اضافه کرد. به این ترتیب هرگونه تغییری در محتوای فایل توسط برنامههای PDF خوان معتبر گزارش داده شده و میتوان از صحت اطلاعات ارائه شده توسط آن اطمینان حاصل کرد.
- از فشرده سازی مطالب، فایلها و تصاویر قرار داده شده در آن پشتیبانی میکند.
- از گرافیک برداری پشتیبانی میکند.
مجوز استفاده از این کتابخانه:
کار من مبتنی بر LGPL است. به این معنا که به صورت باینری (فایل dll) در هر نوع پروژهای قابل استفاده است.
اما ... PdfReport از دو کتابخانه دیگر نیز استفاده میکند:
- کتابخانه iTextSharp که دارای مجوز AGPL است. این مجوز رایگان نیست.
- کتابخانه EPPlus برای تولید فایلهای اکسل با کیفیت. مجوز استفاده از این کتابخانه LGPL است و تا زمانیکه به صورت باینری از آن استفاده میکنید، محدودیتی را برای شما ایجاد نخواهد کرد.
کتابخانه PdfReport به صورت سورس باز در CodePlex قرار گرفته ؛ اما جهت پرسیدن سؤالات، پیشنهادات، ارائه بهبود و غیره میتوانید (و بهتر است) از قسمت مدیریت پروژه مرتبط در سایت جاری نیز استفاده کنید.
نحوه تهیه اولین گزارش، با کتابخانه PdfReport
الف) یک پروژه Class library جدید را شروع کنید. از این جهت که گزارشات PdfReport در انواع و اقسام پروژههای VS.NET قابل استفاده است، میتوان از این پروژه Class library به عنوان کلاسهای پایه قابل استفاده در انواع و اقسام پروژههای مختلف، بدون نیاز به تغییری در کدهای آن استفاده کرد.
ب) آخرین نگارش فایلهای مرتبط با PdfReport را از اینجا دریافت کنید و سپس ارجاعاتی را به اسمبلیهای موجود در بسته آن به پروژه خود اضافه نمائید (ارجاعاتی به PdfReport، iTextSharp و EPPlus). فایل XML راهنمای کتابخانه نیز به همراه بسته آن میباشد که در حین استفاده از متدها و خواص PdfReport کمک بزرگی خواهد بود.
ج) کلاسهای زیر را به آن اضافه کنید:
using System.Web; using System.Windows.Forms; namespace PdfReportSamples { public static class AppPath { public static string ApplicationPath { get { if (isInWeb) return HttpRuntime.AppDomainAppPath; return Application.StartupPath; } } private static bool isInWeb { get { return HttpContext.Current != null; } } } }
همانطور که مشاهده میکنید ارجاعاتی را به System.Windows.Forms.dll و System.Web.dll نیاز دارد.
در ادامه کلاس User را جهت ساخت یک منبع داده درون حافظهای تعریف خواهیم کرد:
using System; namespace PdfReportSamples.IList { public class User { public int Id { set; get; } public string Name { set; get; } public string LastName { set; get; } public long Balance { set; get; } public DateTime RegisterDate { set; get; } } }
using System; using System.Collections.Generic; using PdfRpt.Core.Contracts; using PdfRpt.FluentInterface; namespace PdfReportSamples.IList { public class IListPdfReport { 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(Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf", Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf"); }) .PagesFooter(footer => { footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy")); }) .PagesHeader(header => { header.DefaultHeader(defaultHeader => { defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png"); defaultHeader.Message("گزارش جدید ما"); }); }) .MainTableTemplate(template => { template.BasicTemplate(BasicTemplate.ClassicTemplate); }) .MainTablePreferences(table => { table.ColumnsWidthsType(TableColumnWidthType.Relative); table.NumberOfDataRowsPerPage(5); }) .MainTableDataSource(dataSource => { var listOfRows = new List<User>(); for (int i = 0; i < 200; i++) { listOfRows.Add(new User { Id = i, LastName = "نام خانوادگی " + i, Name = "نام " + i, Balance = i + 1000 }); } dataSource.StronglyTypedList<User>(listOfRows); }) .MainTableSummarySettings(summarySettings => { summarySettings.OverallSummarySettings("جمع کل"); summarySettings.PerviousPageSummarySettings("نقل از صفحه قبل"); 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: "رکوردی یافت نشد."); }) .Export(export => { export.ToExcel(); export.ToCsv(); export.ToXml(); }) .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptIListSample.pdf")); } } }
var rpt = new IListPdfReport().CreatePdfReport(); // rpt.FileName
برای نمونه، جهت مشاهده نمایش این خروجی در یک برنامه ویندوزی، به مثالهای همراه سورس پروژه در مخزن کد آن مراجعه نمائید.
توضیحات بیشتر:
- در قسمت DocumentPreferences، جهت راست به چپ (PdfRunDirection)، اندازه صفحه (PdfPageSize)، جهت صفحه (PageOrientation) و امثال آن تنظیم میشوند.
- سپس نیاز است قلمهای مورد استفاده در گزارش مشخص شوند. در متد DefaultFonts باید دو قلم را معرفی کنید. قلم اول، قلم پیش فرض خواهد بود و قلم دوم برای رفع نواقص قلم اول مورد استفاده قرار میگیرد. برای مثال اگر قلم اول فاقد حروف انگلیسی است، به صورت خودکار به قلم دوم رجوع خواهد شد.
- در ادامه در متد PagesFooter، تاریخ درج شده در پایین تمام صفحات مشخص میشود. در مورد ساخت Footer سفارشی در قسمتهای بعدی بحث خواهد شد.
- در متد PagesHeader، متن و تصویر قرار گرفته در Header تمام صفحات گزارش قابل تنظیم است. این مورد نیز قابل سفارشی سازی است که در قسمتهای بعد به آن خواهیم پرداخت.
- توسط MainTableTemplate، قالب ظاهری ردیفهای گزارش مشخص میشود. یک سری قالب پیش فرض در کتابخانه PdfReport موجود است که توسط متد BasicTemplate آن قابل دسترسی است. در مورد نحوه تعریف قالبهای سفارشی به مرور در قسمتهای بعد، بحث خواهد شد.
- در قسمت MainTablePreferences تنظیمات جدول اصلی گزارش تعیین میشود. برای مثال چه تعداد ردیف در صفحه نمایش داده شود. اگر این مورد را تنظیم نکنید، به صورت خودکار محاسبه خواهد شد. نحوه تعیین عرض ستونهای گزارش به کمک متد ColumnsWidthsType مشخص میشود که در اینجا حالت نسبی درنظر گرفته شده است.
- منبع داده مورد استفاده توسط متد MainTableDataSource مشخص میشود که در اینجا یک لیست جنریک تعیین شده و سپس توسط متد StronglyTypedList در اختیار گزارش ساز جاری قرار میگیرد. تعدادی منبع داده پیش فرض در PdfReport وجود دارند که هر کدام را در قسمتهای بعدی بررسی خواهیم کرد. همچنین امکان تعریف منابع داده سفارشی نیز وجود دارد.
- با کمک متد MainTableSummarySettings، برچسبهای جمعهای پایین صفحات مشخص میشود.
- در قسمت MainTableColumns، ستونهایی را که علاقمندیم در گزارش ظاهر شوند، قید میکنیم. هر ستون باید با یک فیلد یا خاصیت منبع داده متناظر باشد. همچنین همانطور که مشاهده میکنید امکان تعیین Visibility، عرض و غیره آن نیز مهیا است (قابلیت ساخت گزارشاتی که به انتخاب کاربر، ستونهای آن ظاهر یا مخفی شوند). در اینجا توسط callbackهایی که در متد ColumnItemsTemplate قابل دسترسی هستند، میتوان اطلاعات را پیش از نمایش فرمت کرد. برای مثال سه رقم جدا کننده به اعداد اضافه کرد (برای نمونه در خاصیت موجودی فوق) و یا توسط متد AggregateFunction، میتوان متد تجمعی مناسبی را جهت ستون جاری مشخص کرد.
- توسط متد MainTableEvents به بسیاری از رخدادهای داخلی PdfReport دسترسی خواهیم یافت. برای مثال اگر در اینجا رکوردی موجود نباشد، رخداد DataSourceIsEmpty صادر خواهد شد.
- به کمک متد Export، خروجیهای دلخواه مورد نظر را میتوان مشخص کرد. تعدادی خروجی، مانند اکسل، XML و CSV در این کتابخانه موجود است. امکان سفارشی سازی آنها نیز پیش بینی شده است.
- و نهایتا توسط متد Generate مشخص خواهیم کرد که فایل گزارش کجا ذخیره شود.
لطفا برای طرح مشکلات و سؤالات خود در رابطه با کتابخانه PdfReport از این قسمت سایت استفاده کنید.