نظرات مطالب
اهمیت code review
این کد بالا هم «کار می‌کنه». این فرد یا این شرکت اگر جرات داره اون برنامه رو در طی یک سایت بذاره روی اینترنت! خیلی‌ها علاقمند هستند تا کمی اهمیت مرور کدها رو به اون‌ها به نحو مقتضی یادآوری کنند!
مطالب
Blazor 5x - قسمت سوم - مبانی Razor
پیش از شروع به کار توسعه‌ی برنامه‌های مبتنی بر Blazor، باید با مبانی Razor آشنایی داشت. Razor امکان ترکیب کدهای #C و HTML را در یک فایل میسر می‌کند. دستور زبان آن از @ برای سوئیچ بین کدهای #C و HTML استفاده می‌کند. کدهای Razor را می‌توان در فایل‌های cshtml. نوشت که عموما مخصوص صفحات و Viewها هستند و یا در فایل‌های razor. که برای توسعه‌ی کامپوننت‌های Balzor بکار گرفته می‌شوند. در اینجا مهم نیست که پسوند فایل مورد استفاده چیست؛ چون اصول razor بکار گرفته شده در آن‌ها یکی است. البته در اینجا تاکید ما بیشتر بر روی فایل‌های razor. است که در برنامه‌های مبتنی بر Blazor بکار گرفته می‌شوند.


ایجاد یک پروژه‌ی جدید 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 "/"
این سطر، بیانگر مسیریابی منتهی به کامپوننت جاری است. یعنی با گشودن برنامه‌ی WASM در مرورگر و مراجعه به ریشه‌ی سایت، محتوای این کامپوننت را مشاهده خواهیم کرد.
در فایل‌های razor. می‌توان ترکیبی از کدهای #C و HTML را نوشت. برای مثال:
@page "/"

<p>Hello, @name</p>

@code
{
    string name = "Vahid N.";
}
در اینجا قصد داریم مقدار یک متغیر را در یک پاراگراف درج کنیم. به همین جهت برای تعریف آن و شروع به کدنویسی می‌توان با تعریف یک قطعه کد که در فایل‌های razor با code@ شروع می‌شود، اینکار را انجام داد. در این قطعه کد، نوشتن هر نوع کد #C ای مجاز است که نمونه‌ای از آن‌را در اینجا با تعریف یک متغیر مشاهده می‌کنید. اکنون برای درج مقدار این متغیر در بین کدهای HTML از حرف @ استفاده می‌کنیم؛ مانند name@ در اینجا. نمونه‌ای از خروجی تغییرات فوق را در تصویر زیر مشاهده می‌کنید:


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

در زمان درج متغیرهای #C در بین کدهای HTML توسط razor، استفاده از تمام متدهای الحاقی زبان #C نیز مجاز هستند؛ مانند:
 <p>Hello, @name.ToUpper()</p>
بنابراین درج حرف @ در بین کدهای HTML به این معنا است که به کامپایلر razor اعلام می‌کنیم، پس از این حرف، هر عبارتی که قرار می‌گیرد، یک عبارت معتبر #C است.

یا حتی می‌توان یک متد جدید را مانند CustomToUpper در قطعه کد razor، تعریف کرد و از آن به صورت زیر استفاده نمود:
@page "/"

<p>Hello, @name.ToUpper()</p>
<p>Hello, @CustomToUpper(name)</p>

@code
{
    string name = "Vahid N.";

    string CustomToUpper(string value) => value.ToUpper();
}
در این مثال‌ها، ابتدای عبارت #C تعریف شده با حرف @ شروع می‌شود و انتهای آن‌را خود کامپایلر razor بر اساس بسته شدن تگ p تعریف شده، تشخیص می‌دهد. اما اگر قصد داشته باشیم برای مثال جمع دو عدد را در اینجا محاسبه کنیم چطور؟
<p>Let's add 2 + 2 : @2 + 2 </p>
در این حالت امکان تشخیص ابتدا و انتهای عبارت #C توسط کامپایلر میسر نیست. برای رفع این مشکل می‌توان از پرانتزها استفاده کرد:
<p>Let's add 2 + 2 : @(2 + 2) </p>
نمونه‌ی دیگر نیاز به تعریف ابتدا و انتهای یک قطعه کد، در حین تعریف مدیریت کنندگان رویدادها است:
<button @onclick="@(()=>Console.WriteLine("Test"))">Click me</button>
در اینجا onclick@ مشخص می‌کند که با کلیک بر روی این دکمه قرار است قطعه کد #C ای اجرا شود. سپس با استفاده از ()@ محدوده‌ی این قطعه کد، مشخص می‌شود و اکنون در داخل آن می‌توان یک anonymous function را تعریف کرد که خروجی آن را در قسمت console ابزارهای توسعه دهندگان مرورگر می‌توان مشاهده کرد:


در اینجا اگر از Console.WriteLine("Test")@ استفاده می‌شد، به معنای انتساب یک رشته‌ی محاسبه شده به رویداد onclick بود که مجاز نیست.
روش دیگر انجام اینکار به صورت زیر است:
@page "/"

<button @onclick="@WriteLog">Click me 2</button>

@code
{
    void WriteLog()
    {
        Console.WriteLine("Test");
    }
}
می‌توان یک متد void را تعریف کرد و سپس فقط نام آن‌را توسط @ به onlick انتساب داد. ذکر این نام، اشاره‌گری خواهد بود به متد اجرا نشده‌ی WriteLog. در این حالت اگر نیاز به ارسال پارامتری به متد WriteLog بود، چطور؟
@page "/"

<button @onclick="@(()=>WriteLogWithParam("Test 3"))">Click me 3</button>

@code
{
    void WriteLogWithParam(string value)
    {
        Console.WriteLine(value);
    }
}
در این حالت نیز می‌توان از روش بکارگیری anonymous function‌ها برای تعریف پارامتر استفاده کرد.

یک نکته: اگر به اشتباه بجای 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();
    }
}
برای نمونه در اینجا یک کلاس کمکی را جهت تعریف متد MyCustomToUpper، اضافه کرده‌ایم. در ادامه نحوه‌ی استفاده از این متد را در پاراگراف تعریف شده، مشاهده می‌کنید که همانند کار با کلاس و متدهای متداول #C است.
البته این کلاس را تنها می‌توان داخل همین کامپوننت استفاده کرد. برای اینکه بتوان از امکانات این کلاس، در سایر کامپوننت‌ها نیز استفاده کرد، می‌توان آن‌را در پروژه‌ی 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();
    }
}
اگر به فایل‌های csproj دو پروژه‌ی سرور و کلاینت جاری مراجعه کنیم، از پیش، مدخلی را به فایل Shared\BlazorRazorSample.Shared.csproj دارند. بنابراین جهت معرفی این اسمبلی به آن‌ها، نیاز به کار خاصی نیست و از پیش، ارجاعی به آن تعریف شده‌است.

پس از آن روش استفاده‌ی از این کلاس کمکی خارجی اشتراکی به صورت زیر است:
@page "/"

@using BlazorRazorSample.Shared

<p>Hello, @StringUtils.MyNewCustomToUpper(name)</p>
ابتدا فضای نام این کلاس را با استفاده از using@ مشخص می‌کنیم و سپس امکان دسترسی به امکانات آن میسر می‌شود.

یک نکته: می‌توان به فایل Client\_Imports.razor مراجعه و مدخل زیر را به انتهای آن اضافه کرد:
@using BlazorRazorSample.Shared
به این ترتیب دیگر نیازی به ذکر این using@ تکراری، در هیچکدام از فایل‌های razor. پروژه‌ی کلاینت نخواهد بود؛ چون تعاریف درج شده‌ی در فایل Client\_Imports.razor سراسری هستند.


کار با حلقه‌ها در فایل‌های razor.

همانطور که عنوان شد، یکی از کاربردهای پروژه‌ی Shared، امکان به اشتراک گذاشتن مدل‌ها، در برنامه‌های کلاینت و سرور است. برای مثال یک پوشه‌ی جدید Models را در این پروژه ایجاد کرده و کلاس MovieDto را به صورت زیر در آن تعریف می‌کنیم:
using System;

namespace BlazorRazorSample.Shared.Models
{
    public class MovieDto
    {
        public string Title { set; get; }

        public DateTime ReleaseDate { set; get; }
    }
}
سپس به فایل Client\_Imports.razor مراجعه کرده و فضای نام این پوشه را اضافه می‌کنیم؛ تا دیگر نیازی به تکرار آن در تمام فایل‌های razor. برنامه‌ی کلاینت نباشد:
@using BlazorRazorSample.Shared.Models
اکنون می‌خواهیم لیستی از فیلم‌ها را در فایل Client\Pages\Index.razor نمایش دهیم:
@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)
        }
    };
}
در اینجا در ابتدا لیستی از MovieDto‌ها در قسمت code@ تعریف شده و سپس روش استفاده‌ی از یک حلقه‌ی foreach سی‌شارپ را در کدهای razor نوشته شده، مشاهده می‌کنید که این خروجی را ایجاد می‌کند:


یک نکته: در حین تعریف فیلدهای 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>
    }
در اینجا روش تغییر پویای background-color هر ردیف را نیز به کمک کدهای razor، مشاهده می‌کنید. اگر شماره‌ی ردیفی زوج بود، با آبی نمایش داده می‌شود؛ در غیراینصورت با قرمز. در اینجا نیز از ()@ برای تعیین محدوده‌ی کدهای #C نوشته شده، کمک گرفته‌ایم.


نمایش شرطی عبارات در فایل‌های 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");
    }

}
در این مثال، روش کار با یک سرویس تزریق شده‌ی async که قرار است از Web API اطلاعاتی را دریافت کند، مشاهده می‌کنید. در اینجا برخلاف مثال قبلی ما، از روال رویدادگردان OnInitializedAsync برای مقدار دهی لیست یا آرایه‌ای از اطلاعات وضعیت هوا استفاده شده‌است (و نه به صورت مستقیم در یک فیلد قسمت code@). این مورد جزو life-cycle‌های کامپوننت‌های razor است که در قسمت‌های بعد بیشتر بررسی خواهد شد. متد OnInitializedAsync برای بارگذاری اطلاعات یک سرویس از راه دور استفاده می‌شود و در اولین بار اجرای کامپوننت فراخوانی خواهد شد. نکته‌ی مهمی که در اینجا وجود دارد، نال بودن فیلد forecasts در زمان رندر اولیه‌ی کامپوننت جاری است؛ از این جهت که کار دریافت اطلاعات از سرور زمان‌بر است ولی رندر کامپوننت، به صورت آنی صورت می‌گیرد. در این حالت زمانیکه نوبت به اجرای foreach (var forecast in forecasts)@ می‌رسد، برنامه با یک استثنای نال بودن forecasts، متوقف خواهد شد؛ چون هنوز کار OnInitializedAsync به پایان نرسیده‌است:


 برای رفع این مشکل، ابتدا یک if@ مشاهده می‌شود، تا نال بودن forecasts را بررسی کند:
@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
و همچنین عبارت در حال بارگذاری را نمایش می‌دهد. سپس در قسمت else آن، نمایش اطلاعات دریافت شده را توسط یک حلقه‌ی foreach مشاهده می‌کنید. با مقدار دهی forecasts در متد OnInitializedAsync، مجددا کار رندر جدول انجام خواهد شد.


روش نمایش عبارات HTML در فایل‌های razor.

فرض کنید عنوان اول فیلم مثال جاری، به همراه یک تگ HTML هم هست:
new MovieDto
{
   Title = "<i>Movie 1</i>",
   ReleaseDate = DateTime.Now.AddYears(-1)
},
در این حالت اگر برنامه را اجرا کنیم، خروجی آن دقیقا به صورت <Title: <i>Movie 1</i خواهد بود. این مورد به دلایل امنیتی انجام شده‌است. اگر پیشتر تگ‌های HTML را تمیز کرده‌اید و مطمئن هستید که خطری را ایجاد نمی‌کنند، می‌توانید با استفاده از روش زیر، آن‌ها را رندر کرد:
<p>Title: <b>@((MarkupString)movie.Title)</b></p>


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-03.zip
برای اجرای آن وارد پوشه‌ی Server شده و دستور dotnet run را اجرا کنید.
مطالب
React 16x - قسمت 3 - بررسی پیشنیازهای جاوا اسکریپتی - بخش 2
در قسمت قبل، بخشی از تازه‌های ES6 را که بیشتر در برنامه‌های مبتنی بر React مورد استفاده قرار می‌گیرند، بررسی کردیم. در این قسمت نیز سایر موارد مهم باقیمانده را بررسی می‌کنیم.

در اینجا نیز برای بررسی ویژگی‌های جاوا اسکریپت مدرن، یک پروژه‌ی جدید React را ایجاد می‌کنیم.
> create-react-app sample-03
> cd sample-03
> npm start
سپس تمام کدهای داخل index.js را نیز حذف می‌کنیم. اکنون تمام کدهای خالص جاوا اسکریپتی خود را داخل این فایل خواهیم نوشت.
همچنین چون در این قسمت خروجی UI نخواهیم داشت، تمام خروجی را در کنسول developer tools مرورگر خود می‌توانید مشاهده کنید (فشردن دکمه‌ی F12).


متد Array.map

در برنامه‌های مبتنی بر React، از متد Array.map برای رندر لیست‌ها استفاده می‌شود و نمونه‌های بیشتری از آن‌را در قسمت‌های بعدی مشاهده خواهید کرد.
فرض کنید آرایه‌ای از رنگ‌ها را داریم. اکنون می‌خواهیم لیستی را به صورت <li>color</li> به ازای هر آیتم آن، تشکیل دهیم:
const colors = ["red", "green", "blue"];
برای این منظور می‌توان از متد map بر روی این آرایه به نحو زیر استفاده کرد:
const items = colors.map(function(color) {
  return "<li>" + color + "</li>";
});
console.log(items);
متد map یک callback function را دریافت می‌کند که با هر بار فراخوانی آن، یک عنصر از عناصر آرایه را دریافت کرده، آن‌را تغییر شکل داده و بازگشت می‌دهد (چیزی شبیه به متد Select در LINQ).
این مثال را توسط arrow functions نیز می‌توان بازنویسی کرد:
const items2 = colors.map(color => "<li>" + color + "</li>");
console.log(items2);
ابتدا function را حذف می‌کنیم. سپس { return } را تبدیل به یک <= خواهیم کرد. چون تک پارامتری است، نیازی به ذکر پرانتز color وجود ندارد. همچنین نیازی به ذکر سمی‌کالن انتهای return هم نیست؛ چون کل بدنه‌ی این تابع، یک سطر return بیشتر نیست.

یک مرحله‌ی دیگر هم می‌توانیم این قطعه کد را زیباتر کنیم؛ جمع زدن رشته‌ها در ES6 معادل بهتری پیدا کرده‌است که template literals نام دارد:
const items3 = colors.map(color => `<li>${color}</li>`);
console.log(items3);
در اینجا بجای ' و یا " از حرف back-tick استفاده می‌شود. سپس قالب کلی رشته‌ی خود را مشخص می‌کنیم و جائیکه قرار است متغیری را درج کنیم، از {}$ استفاده می‌کنیم که بسیار شبیه به ویژگی string interpolation در #C است. فقط برخلاف آن، حرف $ در ابتدای رشته قرار نمی‌گیرد و باید دقیقا پیش از متغیر مدنظر تعریف شود.


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;
برای کاهش این حجم کد تکراری که با .address شروع می‌شود، می‌توان از ویژگی Object Destructuring استفاده کرد:
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];
و می‌خواهیم آن‌ها را با هم ترکیب کنیم. یک روش انجام اینکار توسط متد concat آرایه‌ها است:
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");
  }
};
ابتدا یک شیء person را با دو عضو، ایجاد کرده‌ایم. اکنون برای ایجاد یک شیء person دیگر، باید دقیقا همان قطعه کد را تکرار کنیم. به همین جهت برای حذف کدهای تکراری، نیاز به قالبی برای ایجاد اشیاء داریم و اینجا است که از کلاس‌ها استفاده می‌شود:
class Person {
  constructor(name) {
    this.name = name;
  }

  walk() {
    console.log("walk");
  }
}
برای تعریف یک کلاس ES6، با واژه‌ی کلیدی class شروع می‌کنیم. نام یک کلاس با حروف بزرگ شروع می‌شود (pascal case) و اگر برای نمونه این نام قرار است دو قسمتی باشد، به مانند CoolPerson عمل می‌کنیم. در مرحله‌ی بعد، متد walk را از تعریف شیء شخص، به کلاس شخص انتقال داده‌ایم. سپس متد ویژه‌ی constructor را در اینجا تعریف کرده‌ایم. توسط آن زمانیکه یک نمونه از این کلاس ساخته می‌شود، پارامتری را دریافت و به یک خاصیت جدید در آن کلاس که توسط this.name تعریف شده‌است، انتساب می‌دهیم.
باید دقت داشت که  class Person تنها یک قالب است و const person ای که پیشتر تعریف شد، یک شیء. برای اینکه از روی قالب تعریف شده‌ی Person، یک شیء را ایجاد کنیم، به صورت زیر توسط واژه‌ی کلیدی new عمل می‌شود:
const person3 = new Person("User 3");
console.log(person3.name);
person3.walk();
در اینجا اگر دقت کنید، عبارت Person("User 3") شبیه به فراخوانی یک متد است. این متد دقیقا همان متد ویژه‌ی constructor ای است که تعریف کردیم. اکنون توسط شیء person3، می‌توان به خاصیت name و یا متد walk آن دسترسی یافت.

یک نکته: در جاوا اسکریپت، کلاس‌ها نیز شیء هستند! از این جهت که کلاس‌ها در جاوا اسکریپت صرفا یک بیان نحوی زیبای تابع constructor هستند و توابع در جاوا اسکریپت نیز شیء می‌باشند!


ارث بری کلاس‌ها در ES6

فرض کنید می‌خواهیم کلاس Teacher را به نحو زیر تعریف کنیم:
class Teacher {
  teach() {
    console.log("teach");
  }
}
این کلاس دارای متد teach است؛ اما تمام معلم‌ها باید بتوانند راه هم بروند. همچنین قصد نداریم متد walk کلاس Person را هم با توجه به اینکه Teacher یک Person نیز هست، در اینجا تکرار کنیم. یک روش حل این مشکل، استفاده از ارث‌بری کلاس‌ها است که با افزودن extends Person به نحو زیر میسر می‌شود:
class Teacher extends Person {
  teach() {
    console.log("teach");
  }
}
پس از این تعریف، اگر بخواهیم توسط واژه‌ی کلیدی new، یک شیء را بر اساس این کلاس تهیه کنیم، در VSCode، تقاضای ثبت یک سازنده نیز می‌شود:


علت اینجا است که کلاس Teacher، نه فقط متد walk کلاس Person را به ارث برده‌است، بلکه سازنده‌ی آن‌را نیز به ارث می‌برد:
const teacher = new Teacher("User 4");
اکنون می‌توان با استفاده از شیء معلم ایجاد شده، نه فقط به متدهای کلاس Teacher دسترسی یافت، بلکه امکان دسترسی به خواص و متدهای کلاس پایه‌ی Person نیز در اینجا وجود دارد:
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
عنوان می‌کند که نیاز است متد ویژه‌ی super را در سازنده‌ی سفارشی کلاس Teacher فراخوانی کنیم. در ES6، فراخوانی سازنده‌ی کلاس پایه، در سازنده‌های سفارشی کلاس‌های مشتق شده‌ی از آن، اجباری است:
class Teacher extends Person {
  constructor(name, degree) {
    super(name);
    this.degree = degree;
  }

  teach() {
    console.log("teach");
  }
}
با اینکار، مقدار دهی خاصیت name کلاس پایه نیز صورت خواهد گرفت. در اینجا همچنین تعریف خاصیت جدید degree و مقدار دهی آن‌را نیز مشاهده می‌کنید. در ادامه باید این پارامتر دوم سازنده را نیز در حین نمونه سازی از کلاس Teacher تعریف کنیم:
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 {
- اگر افزونه‌ی eslint را نصب کرده باشید، اکنون در فایل یا ماژول جدید teacher.js، زیر کلمه‌ی Person خط قرمز کشیده‌است و عنوان می‌کند که کلاس Person را نمی‌شناسد:


برای رفع این مشکل، باید این وابستگی را import کرد:
import { Person } from "./Person";

export class Teacher extends Person {
در اینجا شیء Person، از فایل محلی واقع شده‌ی در پوشه‌ی جاری Person.js تامین می‌شود. نیازی به ذکر پسوند فایل در اینجا نیست.

- مرحله‌ی آخر، اصلاح فایل index.js است؛ چون اکنون تعاریف Person و Teacher را نمی‌شناسد.
import { Person } from "./Person";
import { Teacher } from "./Teacher";
دو سطر فوق را نیز به ابتدای فایل index.js اضافه می‌کنیم تا بتوان new Person و new Teacher نوشته شده‌ی در آن‌را کامپایل کرد.


Exportهای پیش‌فرض و نامدار در ES6

اشیاء تعریف شده‌ی در یک ماژول، به صورت پیش‌فرض private هستند؛ مگر اینکه export شوند. برای مثال export class Teacher و یا export function xyz. به این‌ها named exports گویند. حال اگر ماژول ما تنها یک شیء عمومی شده را داشت (کلاس‌ها هم شیء هستند!)، می‌توان از واژه‌ی کلیدی default نیز در اینجا استفاده کرد:
 export default class Teacher extends Person {
پس از این دیگر نیازی به ذکر {} در حین import چنین شیءای نخواهد بود:
 import Teacher from "./Teacher";

در ادامه اگر یک export نامدار دیگر را به این ماژول اضافه کنیم (مانند تابع testTeacher):
import { Person } from "./Person";

export function testTeacher() {
  console.log("Test Teacher");
}

export default class Teacher extends Person {
نحوه‌ی import آن به صورت زیر تغییر می‌کند:
 import Teacher, { testTeacher } from "./Teacher";
یک default export و یک named export را در اینجا داریم که اولی بدون {} و دومی با {} تعریف شده‌است. این الگویی است که در برنامه‌های React زیاد دیده می‌شود؛ مانند:
import React, { Component } from 'react';

یک نکته: اگر در VSCode داخل {}، دکمه‌های ctrl+space را فشار دهید، می‌توانید منوی exportهای ماژول تعریف شده را مشاهده کنید.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-03.zip
مطالب
بازسازی LocalDb
LocalDb یک نسخه‌ی کوچک شده از SQL Express 2012 است که نیازی به سرویس ندارد که سبب سهولت استفاده از آن را در پروژه‌های Visual Studio  فراهم می‌آورد و یک ویژگی بسیار خوب برای برنامه‌هایی است که نیاز به استفاده از کلیه‌ی امکانات مهیای SQL Server را ندارند. یکی دیگر از ویژگی‌های آن استفاده در محیط هایی است که اطمینانی از نصب بودن SQL Express در آن‌ها نداریم.
در حین توسعه‌ی نرم افزار ممکن است استفاده از LocalDb امکان پذیر نباشد و خللی در کارکرد آن ایجاد شده باشد و عملا استفاده از آن میسر نشود. اتفاقی که برای من روز گذشته رخ داد و مجبور شدم LocalDb را مجددا نصب نمایم ولی همچنان مشکل وجود داشت. بعد از جستجو به این لینک و این لینک رسیدم که جمع بندی آن بشرح زیر است:
ابتدا command  prompt را اجرا می‌نماییم (ترجیحا بصورت Administrator اجرا شود) سپس به محلی که Instanceهای LocalDb قرار دارد
 (localappdata\Microsoft\Microsoft SQL Server Local DB\Instances\v11.0) رفته و دستور زیر را صادر می‌نماییم:
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.
هنگامیکه خطای فوق نمایش داده شده، باید ابتدا LocalDb را حذف و مجددا آن را ساخت.
sqllocaldb delete "v11.0"
دستور فوق عملیات حذف را انجام می‌دهد.
و دستور زیر مجددا یک وهله‌ی نسخه 11.0 بنام"v11.0" را از LocalDb ایجاد می‌کند.
sqllocaldb create "v11.0"
تا اینجا کلیه‌ی اقدامات لازم جهت راه اندازی مجدد LocalDb انجام شد و هم اکنون می‌توانید از آن استفاده نمایید.
مطالب
React 16x - قسمت 12 - طراحی یک گرید - بخش 2 - فیلتر کردن اطلاعات
تا اینجا کامپوننت صفحه بندی را به همراه اعمال آن به لیست نمایش داده شده، پیاده سازی کردیم. در ادامه می‌خواهیم لیست ژانرهای سینمایی را که در فایل fakeGenreService.js تعریف شده‌اند:
export const genres = [
  { _id: "5b21ca3eeb7f6fbccd471818", name: "Action" },
  { _id: "5b21ca3eeb7f6fbccd471814", name: "Comedy" },
  { _id: "5b21ca3eeb7f6fbccd471820", name: "Thriller" }
];

export function getGenres() {
  return genres.filter(g => g);
}
توسط list-group‌های بوت استرپی، در کنار صفحه نمایش داده و سپس به ازای هر گروه انتخابی توسط کاربر، فیلم‌های مرتبط با آن گروه را فیلتر کرده و نمایش دهیم.


بررسی ساختار کامپوننت ListGroup

شبیه به کامپوننت صفحه بندی که در قسمت قبل ایجاد کردیم، می‌خواهیم کامپوننت ListGroup نیز به طور کامل از اشیاء movie مستقل باشد؛ تا در آینده بتوان از آن در جاهای دیگری نیز استفاده کرد. به همین جهت فایل جدید src\components\common\listGroup.jsx را ایجاد کرده و سپس با استفاده از میانبرهای imrc و cc در VSCode، ساختار ابتدایی این کامپوننت را ایجاد می‌کنیم. هرچند می‌توان این کامپوننت را به صورت «Stateless Functional Component» نیز طراحی کرد؛ چون state و متد دیگری بجز render نخواهد داشت و تمام اطلاعات خودش را از والد خود دریافت می‌کند.
سپس به کامپوننت movies مراجعه کرده و این کامپوننت خالی را import می‌کنیم:
import ListGroup from "./common/listGroup";
پس از آن به متد رندر کامپوننت movies مراجعه کرده و با اضافه کردن یک row بوت استرپی دو ستونی، قصد داریم کامپوننت لیست فیلم‌ها را در ستون اول این ردیف نمایش دهیم. به همین جهت المان آن‌را در این محل قرار می‌دهیم تا بتوانیم اینترفیس ابتدایی آن‌را پیش از پیاده سازی آن، طراحی کنیم.
برای این منظور ابتدا React.Fragment موجود را با یک div با "className="row جایگزین می‌کنیم. سپس داخل این row، دو ستون را تعریف خواهیم کرد که در اولی، المان جدید ListGroup قرار می‌گیرد و در دومی، مابقی عناصری که تاکنون اضافه کرده‌ایم؛ مانند جدول، صفحه بندی و نمایش تعداد آیتم‌ها:
    return (
      <div className="row">
        <div className="col-2">
          <ListGroup />
        </div>
        <div className="col">
          ...
        </div>
      </div>
    );
این listGroup، حداقل نیاز به لیست آیتم‌هایی را دارد که باید نمایش دهد. این لیست نیز از fakeGenreService و متد getGenres آن تامین می‌شود که به صورت یک خاصیت جدید در state به نحو زیر درج خواهد شد:
import { getGenres } from "../services/fakeGenreService";
// ...

class Movies extends Component {
  state = {
    // ...
    genres: getGenres()
  };
همانطور که در قسمت 9 این سری نیز بررسی کردیم، اگر getGenres قرار است از سمت سرور و توسط یک درخواست Ajax ای تامین شود، محل صحیح قرارگیری آن در متد lifecycle hook ویژه‌ای به نام componentDidMount است. اما در اینجا چون genres یک لیست درون حافظه‌ای است، مقدار دهی فوق، مشکلی را ایجاد نمی‌کند. هرچند می‌توان هم اکنون نیز تعریف فوق را کمی اصولی‌تر نوشت. برای اینکار متد componentDidMount را اضافه کرده و به نحو زیر تنظیم می‌کنیم:
class Movies extends Component {
  state = {
    movies: [],
    pageSize: 4,
    currentPage: 1,
    genres: []
  };

  componentDidMount() {
    this.setState({ movies: getMovies(), genres: getGenres() });
  }
ابتدا آرایه‌های مورد نیاز movies و genres را در state تعریف کرده و آن‌ها را با یک آرایه‌ی خالی، مقدار دهی اولیه می‌کنیم. از این جهت که تا رسیدن به مرحله‌ی componentDidMount که اندکی طول می‌کشد، خطاهای زمان اجرای عدم دسترسی به این آرایه‌ها در برنامه رخ ندهد. سپس زمانیکه وهله‌ای از این کامپوننت در DOM رندر شد، متد componentDidMount فراخوانی شده و دو خاصیت state را با مقادیر دریافتی، به روز رسانی می‌کند.

پس از آن می‌توان ویژگی جدید items این کامپوننت را به آرایه‌ی genres دریافتی از state، تنظیم کرد:
<ListGroup items={this.state.genres} />
در این مرحله، ورودی دیگری به نظر نمی‌رسد که مورد نیاز باشد. اکنون این سؤال مطرح می‌شود که چه رخ‌دادهایی را قرار است از این کامپوننت دریافت کنیم یا به عبارتی خروجی آن چیست؟
بهتر است هر زمانیکه کاربر، آیتمی را از این لیست انتخاب کرد، توسط بروز رخدادی مانند onItemSelect از وقوع آن مطلع شد و سپس نسبت به آن توسط متد handleGenreSelect، واکنش نشان داد؛ مانند فیلتر کردن لیست فیلم‌ها بر اساس آیتم انتخابی و نمایش آن. به همین جهت ویژگی onItemSelect را به تعریف المان ListGroup اضافه می‌کنیم:
<ListGroup
  items={this.state.genres}
  onItemSelect={this.handleGenreSelect}
/>
و سپس متد handleGenreSelect متصل به آن‌‌را به نحو زیر تعریف خواهیم کرد:
  handleGenreSelect = genre => {
    console.log("handleGenreSelect", genre);
  };
تا اینجا اینترفیس کامپوننت ListGroup را پیش از پیاده سازی آن تعریف کردیم (تعیین ورودی و خروجی آن). در مرحله‌ی بعد، این کامپوننت را تکمیل می‌کنیم.


پیاده سازی نمایش آیتم‌ها در کامپوننت 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;
کار با درج یک ul که با کلاس list-group مزین شده‌است، شروع می‌شود. سپس باید liهای آن‌را که نمایانگر آیتم‌های این لیست است، به صورت پویا با کلاس‌های list-group-item رندر کرد. برای اینکار از آرایه‌ی دریافتی this.props.items و فراخوانی متد map بر روی آن کمک می‌گیریم. در اینجا key هر ردیف با استفاده از خاصیت id هر آیتم و برچسب هر کدام از طریق خاصیت name هر شیء دریافتی، تامین می‌شود.

تا اینجا اگر برنامه را ذخیره کرده و در مرورگر نمایش دهیم، به خروجی زیر می‌رسیم:


البته به نظر عرض ستون آن نامناسب است. به همین جهت به کامپوننت movies مراجعه کرده و col-2 ستون آن‌را به col-3 تبدیل می‌کنیم.


پویا سازی انتخاب نام خواص شیء دریافتی، در کامپوننت ListGroup

در حال حاضر پیاده سازی کامپوننت ListGroup، به شیءای دقیقا با خواص id_ و name وابسته‌است و اگر شیء دیگری را که دارای خواصی معادل این نام‌ها نیست، به آن ارسال کنیم، دیگر کار نخواهد کرد. به همین جهت در محل تعریف المان این کامپوننت در کامپوننت movies، دو ویژگی دیگر نام خواص شیء مدنظر را تنظیم می‌کنیم تا بتوانیم با هر نوع شیءای در اینجا کار کنیم:
<ListGroup
  items={this.state.genres}
  textProperty="name"
  valueProperty="_id"
  onItemSelect={this.handleGenreSelect}
/>
پس از این تغییر و افزودن textProperty و valueProperty، برای پویا سازی نام‌های خواص دریافتی در کامپوننت ListGroup، از روش کار با []، جهت دسترسی پویای به خواص یک شیء، استفاده می‌کنیم تا دیگر این کامپوننت به شیء خاص genre، وابستگی نداشته باشد و قابلیت استفاده‌ی مجدد از آن افزایش یابد:
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;
برای نمونه در اینجا دو خاصیت جدید textProperty و valueProperty را به همان مقادیر name و id_ مورد استفاده‌ی در این مثال تنظیم کرده‌ایم. پس از این تعریف، می‌توان به کامپوننت movies که از این ویژگی‌ها استفاده می‌کند مراجعه کرده و آن‌هایی را که با defaultProps تطابق دارند، از لیست ویژگی‌های ذکر شده حذف کرد؛ یعنی تعریف المان 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});
  };
در یک چنین حالتی الزامی به تعریف selectedGenre در خاصیت state ابتدای کامپوننت نیست. چون با فراخوانی متد setState اگر یکی از خواص منتسب به شیء state به روز شده باشد، آن خاصیت نیز به روز می‌شود و یا اگر این خاصیت جدید باشد، با state موجود یکی خواهد شد؛ هرچند آن‌را به صورت زیر نیز می‌توان تعریف کرد که با یک شیء خالی مقدار دهی شده‌است:
class Movies extends Component {
  state = {
    // ...
    selectedGenre: {}
  };
سپس ویژگی selectedItem کامپوننت را به این مقدار تغییر یافته‌ی this.state.selectedGenre تنظیم می‌کنیم تا با هر بار فراخوانی setState که سبب رندر مجدد کامپوننت Movies در DOM مجازی React می‌شود، کامپوننت از selectedItem تغییر یافته مطلع شده و با افزودن کلاس active به آن آیتم، واکنش نشان دهد:
<ListGroup
  items={this.state.genres}
  onItemSelect={this.handleGenreSelect}
  selectedItem={this.state.selectedGenre}
/>
اکنون به کامپوننت ListGroup مراجعه کرده و بر اساس ویژگی جدید selectedItem، تغییرات زیر را به className اعمال می‌کنیم:
<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>
در اینجا اگر item در حال رندر با this.props.selectedItem دریافتی یکی باشد، کلاس active به کلاس list-group-item اضافه می‌شود و برعکس.



مدیریت فیلتر کردن اطلاعات گروه فیلم انتخابی

در قسمت قبل، در ابتدای متد رندر کامپوننت 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 };
  }
- در اینجا بجای اینکه مدام this.stat‌ها را جهت دریافت خواص آن تکرار کنیم، با استفاده از ویژگی Object Destructuring، خواصی را که نیاز داریم یکبار انتخاب کرده و سپس به دفعات از آن‌ها استفاده می‌کنیم. به همین جهت در این قطعه کد، فقط یکبار this.state را مشاهده می‌کنید که بسیار تمیزتر است و همچنین کارآیی آن نیز به علت عدم انتخاب مداوم مقدار خاصیتی از یک شیء، بالاتر از حالت قبل است.
- در حین 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();
- از آرایه‌ی movies، در قسمت قبل برای رندر لیست فیلم‌ها استفاده شد. به همین جهت در اینجا تغییر نام data به movies را مشاهده می‌کنید.
- همچنین کامپوننت صفحه بندی، اینبار باید totalCount آیتم‌های فیلتر شده را نمایش دهد و نه totalCount تمام فیلم‌های موجود را:
<Pagination
    itemsCount={totalCount}
در اینجا برچسب نمایش تعداد آیتم‌های موجود نیز باید تغییر کند:
<p>Showing {totalCount} movies in the database.</p>
ج) ممکن است در اولین بار مشاهده‌ی صفحه، کاربر صفحه‌ی شماره‌ی 3 را انتخاب کند که سبب تغییر currentPage موجود در state، به عدد 3 می‌شود. اکنون اگر کاربر نمایش فیلتر شده‌ی فیلم‌های یک گروه خاص را انتخاب کند، باید این شماره، به عدد 1 مجددا تنظیم شود:
  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 });
  }
همین اندازه تغییر برای فعالسازی این گزینه کفایت می‌کند؛ از این جهت که در متد getPagedData، ابتدا بررسی می‌شود که اگر آیتمی انتخاب شده بود و همچنین دارای id نیز بود، آنگاه کار فیلتر کردن صورت گیرد، درغیراینصورت، تمام رکوردها را بازگشت دهد:
const filteredMovies =
      selectedGenre && selectedGenre._id
        ? allMovies.filter(m => m.genre._id === selectedGenre._id)
        : allMovies;

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:  sample-12.zip
مطالب
روش های ارث بری در Entity Framework - قسمت دوم
در قسمت اول در مورد روش TPT خواندید. در این قسمت به روش TPH می‌پردازیم.

روش 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 به صورت مدل شده در برنامه نشان داده شده است.

حال باید چهار موجودیت مشتق شده را به مدل اضافه کنیم. موجودیت‌های Student، Instructor و Admin را با کلیک راست بر روی EDM Designer و انتخاب گزینه‌ی Entity از منوی Add New ایجاد کنید. در فرمی که باز می‌شود، از قسمت  Person ،Base type را انتخاب کنید. موجودیت BusinessStudent را نیز ایجاد و موجودیت پایه‌ی آن را موجودیت Student در نظر بگیرید. مدل ایجاد شده تا اینجای کار در شکل زیر مشخص است.

حال باید خصیصه‌های موجودیت 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();
}
چه کدهای آشنایی! بله، این کدها همان کدهایی هستند که برای مثال روش TPT استفاده کردیم و بدون کوچکترین تغییری در اینجا نیز قابل استفاده هستند.

مزایای روش TPH
  • سرعت بالای عملیات CRUD، به دلیل وجود تمامی داده‌ها در یک جدول
  • تعداد جداول در پایگاه داده، کم و مدیریت آنها آسان‌تر است

معایب روش TPH
  • افزونگی داده ها. مقادیر برخی ستون‌ها برای بعضی از رکوردها، حاوی مقدار NULL است و تعداد این ستون‌ها به تعداد زیر مجموعه‌ها ارتباط دارد
  • عیب اول، باعث می‌شود تا در صورتی که داده‌ها به صورت دستی تغییر پیدا کنند، جامعیت داده‌ها از بین برود
  • افزایش بی دلیل حجم داده ها
  • اضافه و حذف کردن موجودیت‌ها به مدل، عملی زمانبر و پیچیده است
سومین روش، TPC است که در EF Designer، پشتیبانی از پیش تعبیه شده برای آن وجود ندارد و باید از طریق ویرایش فایل edmx. به آن رسید. استفاده از این روش توصیه نمی‌شود.

مطالب
ایجاد ایندکس منحصربفرد در EF Code first
در EF Code first برای ایجاد UNIQUE INDEX ویژگی یا تنظیمات Fluent API خاصی درنظر گرفته نشده است و می‌توان از همان روش‌های متداول اجرای مستقیم کوئری SQL بر روی بانک اطلاعاتی جهت ایجاد UNIQUE INDEX‌ها کمک گرفت:
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);");
}
و این کل کاری است که باید در متد Seed کلاس مشتق شده از DbMigrationsConfiguration انجام شود. مثلا:
public class MyDbMigrationsConfiguration : DbMigrationsConfiguration<MyContext>
{
        public BlogDbMigrationsConfiguration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
        }

        protected override void Seed(MyContext context)
        {
            CreateUniqueIndex(context, "table1", "field1");
            base.Seed(context);
        }
}
روش فوق کار می‌کند اما آنچنان مناسب نیست؛ چون به صورت strongly typed تعریف نشده است و اگر نام جداول یا فیلدها را بعدها تغییر دادیم، به مشکل برخواهیم خورد و کامپایلر خطایی را صادر نخواهد کرد؛ چون table1 و field1 در اینجا به صورت رشته تعریف شده‌اند.
برای حل این مشکل و تبدیل کدهای فوق به کدهایی 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);
در اینجا دیگر از رشته‌ها خبری نبوده و حاصل نهایی Refactoring friendly است. به علاوه نام جدول را نیز به صورت خودکار از context استخراج می‌کند.
 
مطالب
آموزش LINQ بخش پنجم
در ادامه سری آموزشی LINQ، موارد دیگری از عبارت‌های پرس و جو را بررسی می‌کنیم:
 • عبارت group
 • عبارت orderby
 • کلمات کلیدی ascending و descending
 • کلمه کلیدی by


بررسی عبارت group و کلمه‌ی کلیدی by

عبارت group یک توالی یک بعدی (flat sequence) یا ساده را از ورودی دریافت و یک توالی گروه بندی شده را تولید می‌کند.
مثال: در بخش دوم این سری آموزشی، کلاسی به نام Ingredient تعریف کردیم که مواد غذایی و کالری آنها را نگهداری می‌کرد. در این مثال قصد داریم مواد غذایی دریافت شده از توالی ورودی را بر اساس کالری آنها، به گروه‌هایی تقسیم کنیم، بطوریکه مواد غذایی با کالری‌های یکسان در یک گروه قرار بگیرند:
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}");
   }
}
در این مثال، از نوع جنریکی به نام IGrouping استفاده شده‌است که اولین پارامتر آن کلید گروه است (در این مثال کالری مواد غذایی) و دومین پارامتر آن، نوع لیست را مشخص می‌کند که در اینجا کلاس Ingredient است. اعضای لیست تولید شده، کالری یکسانی دارند. دلیل استفاده‌ی از نوع‌ها بصورت صریح و آشکار (Explicit) خوانایی بیشتر برای تشخیص خروجی‌های تولید شده‌ی در کد‌ها می‌باشد. ولی راه بهتر، جایگزین کردن کد <<IEnumerable<IGrouping<int, Ingredient با کلمه‌ی کلیدی var  می‌باشد.
خروجی مثال بالا:
 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
علملیات مرتب سازی بصورت پیش فرض بصورت صعودی می‌باشد (Ascending). برای تغییر این حالت می‌توانید از کلمه‌ی کلیدی descending استفاده کنید:
IOrderedEnumerable<Ingredient> sortedByNameQuery =
from i in ingredients
orderby i.Name descending
select i;
با اجرای مجدد برنامه، خروجی زیر را مشاهده خواهید کرد:
 Sugar
Oats
Milk
Lard
Flour
Egg
Butter
عبارت orderby  را می‌توان با عبارت groupby ترکیب کرد و نتیجه‌ی حاصل از مثال اول این مطلب را بصورت مرتب شده بر اساس کالری مواد غذایی مشاهده کرد.
مثال:
 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;
در مثال فوق نتیجه‌ی اجرای دو پرس و جو، موادی است که کالری آنها بیشتر از 100 می‌باشد. همانطور که مشاهده می‌کنید روش اول مختصرتر از روش دوم است. این مورد در زمانی است که تنها یک عملیات بر روی توالی اجرا شود.
 • پرس و جویی که قرار است نوشته شود، از نظر عملیات بر روی توالی ساده باشد: در این حالت روش استفاده شده تفاوتی نمی‌کند و بستگی به روش برگزیده و مورد علاقه‌ی برنامه نویس و یا تیم برنامه نویسی دارد. مثلا زمانیکه تنها عملگر‌های 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
مدتی هست که در حال تهیه کتابخانه‌ی گزارش سازی مبتنی بر iTextSharp هستم و عمده‌ی استفاده از آن برای من تاکنون، تهیه گزارشات باکیفیت PDF فارسی تحت وب بوده؛ هر چند این کتابخانه وابستگی خاصی به فناوری مورد استفاده ندارد و با WinForms، WPF، مشتقات ASP.NET ، سرویس‌های WCF و کلا هرجایی که دات نت فریم ورک کامل در دسترس باشد، قابل استفاده است. همچنین به منبع داده خاصی گره نخورده است و حتی می‌توانید از یک anonymously typed list عدم متصل به بانک اطلاعاتی خاصی نیز به عنوان منبع داده آن استفاده کنید.
کتابخانه 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;
            }
        }
    }
}
از این کلاس برای مشخص سازی محل ذخیره سازی فایل‌های نهایی PDF تولیدی استفاده خواهیم کرد.
همانطور که مشاهده می‌کنید ارجاعاتی را به 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 از این قسمت سایت استفاده کنید.