مطالب
بررسی تغییرات Blazor 8x - قسمت چهارم - معرفی فرم‌های جدید تعاملی
در قسمت قبل مشاهده کردیم که چگونه می‌توان کل برنامه را به صورت سراسری، تعاملی کرد تا بتوان توسط آن، Blazor Server سنتی را شبیه سازی نمود؛ اما ... آیا واقعا نیاز است چنین کاری را انجام دهیم؟! چون در این صورت از قابلیت‌‌های جدید SSR به همراه Blazor 8x محروم می‌شویم. اگر کل قابلیت‌های تعاملی مورد نیاز ما در حد یک فرم و ارسال اطلاعات آن به سمت سرور است، می‌توان در Blazor 8x هنوز هم در همان حالت SSR قرار گرفت و از فرم‌های جدید تعاملی آن استفاده کرد تا برای پردازش چنین مواردی، نیازی به برقراری اتصال دائم SignalR نباشد. جزئیات نحوه‌ی کار با اینگونه فرم‌ها را در ادامه بررسی می‌کنیم.


امکان تعریف HTML Forms استاندارد در Blazor 8x

فرم‌های استاندارد HTML، پیش از ظهور جاوااسکریپت و SPAها وجود داشتند (دقیقا همان زمانیکه که فقط مفهوم SSR وجود خارجی داشت) و هنوز هم جزء مهمی از اغلب برنامه‌های وب را تشکیل می‌دهند. با ارائه‌ی دات نت 8 و قابلیت server side rendering آن، کامپوننت‌های برنامه، فقط یکبار در سمت سرور رندر شده و HTML ساده‌ی آن‌ها به سمت مرورگر کاربر بازگشت داده می‌شود. در این حالت، فرم‌های استاندارد HTML، امکان دریافت ورودی‌های کاربر و ارسال داده‌های آن‌ها را به سمت سرور میسر می‌کنند (چون دیگر خبری از اتصال دائم SignalR نیست و باید اطلاعات را به همان نحو استاندارد پروتکل HTTP، به سمت سرور Post کرد). در دات نت 8، دو راه‌حل برای کار با فرم‌ها در برنامه‌های Blazor وجود دارد: استفاده از EditForm خود Blazor و یا استفاده از HTML forms استاندارد و ساده، به همان نحوی که بوده و هست.


روش کار با EditForm در برنامه‌های Blazor SSR

البته ما قصد استفاده از فرم‌های ساده‌ی HTML را در اینجا نداریم و ترجیح می‌دهیم که از همان EditForm استفاده کنیم. EditForms در Blazor بسیار مفید بوده و امکان بایند خواص یک مدل را به اجزای مختلف ورودی‌های تعریف شده‌ی در آن میسر می‌کند و همچنین قابلیت‌هایی مانند اعتبارسنجی و امثال آن‌را نیز به همراه دارد (اطلاعات بیشتر). اما چگونه می‌توان از این امکان در برنا‌مه‌های Blazor SSR نیز استفاده کرد؟
برای این منظور، ابتدا مثالی را به صورت زیر تکمیل می‌کنیم (که بر اساس قالب dotnet new blazor --interactivity Server تهیه شده) و سپس توضیحات آن ارائه خواهد شد:

الف) تهیه یک مدل برای تعریف محل‌های مرتبط با یک سفارش در فایل Models/OrderPlace.cs

using System.ComponentModel.DataAnnotations;

namespace Models;

public record OrderPlace
{
    public Address BillingAddress { get; set; } = new();
    public Address ShippingAddress { get; set; } = new();
}

public class Address
{
    [Required] public string Name { get; set; } = default!;
    public string? AddressLine1 { get; set; }
    public string? AddressLine2 { get; set; }
    public string? City { get; set; }
    [Required] public string PostCode { get; set; } = default!;
}

ب) تهیه‌ی یک کامپوننت Editor برای دریافت اطلاعات آدرس فوق در فایل Components\Pages\Chekout\AddressEntry.razor

@inherits Editor<Models.Address>

<div>
    <label>Name</label>
    <InputText @bind-Value="Value.Name"/>
</div>
<div>
    <label>Address 1</label>
    <InputText @bind-Value="Value.AddressLine1"/>
</div>
<div>
    <label>Address 2</label>
    <InputText @bind-Value="Value.AddressLine2"/>
</div>
<div>
    <label>City</label>
    <InputText @bind-Value="Value.City"/>
</div>
<div>
    <label>Post Code</label>
    <InputText @bind-Value="Value.PostCode"/>
</div>

ج) استفاده از مدل و ادیتور فوق در یک EditForm تغییر یافته برای کار با برنامه‌های Blazor SSR در فایل Components\Pages\Chekout\Checkout.razor

@page "/checkout"

@using Models
@if (!_submitted && PlaceModel != null)
{
    <EditForm Model="PlaceModel" method="post" OnValidSubmit="SubmitOrder" FormName="checkout">
        <DataAnnotationsValidator/>

        <h4>Bill To:</h4>
        <AddressEntry @bind-Value="PlaceModel.BillingAddress"/>

        <h4>Ship To:</h4>
        <AddressEntry @bind-Value="PlaceModel.ShippingAddress"/>

        <button type="submit">Submit</button>
        <ValidationSummary/>
    </EditForm>
}

@if (_submitted && PlaceModel != null)
{
    <div>
        <h2>Order Summary</h2>

        <h3>Shipping To:</h3>
        <dl>
            <dt>Name</dt>
            <dd>@PlaceModel.BillingAddress.Name</dd>
            <dt>Address 1</dt>
            <dd>@PlaceModel.BillingAddress.AddressLine1</dd>
            <dt>Address 2</dt>
            <dd>@PlaceModel.BillingAddress.AddressLine2</dd>
            <dt>City</dt>
            <dd>@PlaceModel.BillingAddress.City</dd>
            <dt>Post Code</dt>
            <dd>@PlaceModel.BillingAddress.PostCode</dd>
        </dl>
    </div>
}

@code {
    bool _submitted;

    [SupplyParameterFromForm]
    public OrderPlace? PlaceModel { get; set; }

    protected override void OnInitialized()
    {
        PlaceModel ??= GetOrderPlace();
    }

    private void SubmitOrder()
    {
        _submitted = true;
    }

    private static OrderPlace GetOrderPlace() =>
        new()
        {
            BillingAddress = new Address
                             {
                                 PostCode = "12345",
                                 Name = "Test 1",
                             },
            ShippingAddress = new Address
                              {
                                  PostCode = "67890",
                                  Name = "Test 2",
                              },
        };

}
توضیحات:
باید بخاطر داشت که این فرم بر اساس حالت Server Side Rendering در اختیار مرورگر کاربر قرار می‌گیرد. یعنی برای بار اول، یک HTML خالص، در سمت سرور بر اساس اطلاعات آن تهیه شده و بازگشت داده می‌شود و زمانیکه به کاربر نمایش داده شد، دیگر برخلاف Blazor Server پیشین، اتصال SignalR ای وجود ندارد تا قابلیت‌های تعاملی آن‌را مدیریت کند. در این حالت اگر به view source صفحه‌ی جاری رجوع کنیم، چنین خروجی قابل مشاهده‌است:
<form method="post">
   <input type="hidden" name="_handler" value="checkout" />
   <input type="hidden" name="__RequestVerificationToken" value="CfDxxx" />
.
.
.
   <button type="submit">Submit</button>
</form>
یعنی زمانیکه این فرم به سمت سرور ارسال می‌شود، همان HTTP POST استاندارد رخ می‌دهد و برای اینکار، نیازی به اتصال وب‌سوکت SignalR ندارد.
این EditForm تعریف شده، دو قسمت اضافه‌تر را نسبت به EditFormهای نگارش‌های قبلی Blazor دارد:
<EditForm Model="PlaceModel" method="post" OnValidSubmit="SubmitOrder" FormName="checkout">
در اینجا نوع HTTP Method ارسال فرم، مشخص شده و همچنین یک FormName نیز تعریف شده‌است. علت اینجا است که Blazor باید بتواند اطلاعات POST شده و دریافتی در سمت سرور را به کامپوننت متناظری نگاشت کند؛ به همین جهت این نامگذاری، ضروری است.
همانطور که در نحوه‌ی تعریف فرم HTML ای فوق مشخص است، فیلد مخفی handler_، کار متمایز ساختن این فرم را به عهده داشته و از مقدار آن در سمت سرور جهت یافتن کامپوننت متناظر، استفاده خواهد شد.

همچنین برای دریافت و پردازش این اطلاعات در سمت سرور، تنها کافی است خاصیت مرتبط با آن‌را با ویژگی SupplyParameterFromForm مزین کنیم:
[SupplyParameterFromForm]
public OrderPlace? PlaceModel { get; set; }

جریان کاری این فرم به صورت خلاصه به نحو زیر است (که در آن متد OnInitialized دوبار فراخوانی می‌شود و باید به آن دقت داشت):
- در بار اول نمایش این صفحه (با فراخوانی مسیر /checkout در مرورگر)، متد OnInitialized فراخوانی شده و در آن، مقدار شیء PlaceModel نال است.
- بنابراین به متد GetOrderPlace مراجعه کرده و اطلاعاتی را دریافت می‌کند؛ برای مثال، این اطلاعات را از سرویسی می‌خواند.
- پس از پایان هر روال رخدادگردانی در Blazor، در پشت صحنه به صورت خودکار، متد تغییر حالت جاری کامپوننت (متد StateHasChanged) هم فراخوانی می‌شود. این فراخوانی خودکار، باعث رندر مجدد UI آن بر اساس اطلاعات جدید خواهد شد. یعنی قسمت‌های نمایش فرم و نمایش اطلاعات ارسالی، یکبار ارزیابی شده و در صورت برقراری شرط‌ها، نمایش داده می‌شوند.
- در ادامه، کاربر فرم را پر کرده و به سمت سرور POST می‌کند.
- پیش از هر رخ‌دادی، خواص شیء PlaceModel به علت مزین بودن به ویژگی SupplyParameterFromForm، بر اساس اطلاعات ارسالی به سرور، مقدار دهی می‌شوند.
- سپس متد OnInitialized فراخوانی شده و چون اینبار مقدار PlaceModel نال نیست، به متد GetOrderPlace جهت دریافت مقادیر ابتدایی خود مراجعه نمی‌کند. سطر تعریف شده‌ی در متد OnInitialized فقط زمانی سبب مقدار دهی شیء PlaceModel می‌شود که مقدار این شیء، نال باشد (یعنی فقط در اولین بار نمایش صفحه)؛ اما اگر این مقدار توسط پارامتر مزین شده‌ی به SupplyParameterFromForm به علت ارسال داده‌های فرم به سرور، مقدار دهی شده باشد، دیگر به منبع داده‌ی ابتدایی رجوع نمی‌کند.
- چون متد رخ‌دادگردان OnInitialized فراخوانی شده، پس از پایان آن (و فراخوانی خودکار متد StateHasChanged در انتهای آن)، یکبار دیگر کار رندر UI فرم جاری بر اساس اطلاعات جدید، انجام خواهد شد.
- اکنون است که پس از طی این رخ‌دادها، متد رویدادگردان SubmitOrder فراخوانی می‌شود. یعنی زمانیکه این متد فراخوانی می‌شود، شیء PlaceModel بر اساس اطلاعات رسیده‌ی از طرف کاربر، مقدار دهی شده و آماده‌ی استفاده است (برای مثال آماده‌ی ذخیره سازی در بانک اطلاعاتی؛ با فراخوانی سرویسی در اینجا).
- پس از پایان فراخوانی متد رویدادگردان SubmitOrder، به علت تغییر حالت کامپوننت (و فراخوانی خودکار متد StateHasChanged در انتهای آن)، یکبار دیگر نیز کار رندر UI فرم جاری بر اساس اطلاعات جدید انجام خواهد شد. یعنی اینبار قسمت Order Summary نمایش داده می‌شود.


مدیریت تداخل نام‌های HTML Forms در Blazor 8x SSR

تمام فرم‌هایی که به این صورت در برنامه‌های Blazor SSR مدیریت می‌شوند، باید دارای نام منحصربفردی که توسط خاصیت FormName مشخص می‌شود، باشند. برای جلوگیری از این تداخل نام‌ها، کامپوننت جدیدی به نام FormMappingScope معرفی شده‌است که نمونه‌ای از آن‌را در فایل فرضی Components\Pages\Chekout\CheckoutForm.razor تعریف شده‌ی به صورت زیر مشاهده می‌کنید:

@page "/checkout"

<FormMappingScope Name="store-checkout">
    <CheckoutForm />
</FormMappingScope>
در اینجا ابتدا ویژگی page@ کامپوننت CheckoutForm را حذف کرده و آن‌را تبدیل به یک کامپوننت معمولی بدون قابلیت مسیریابی کرده‌ایم. سپس آن‌را توسط کامپوننت FormMappingScope در صفحه‌ای دیگر معرفی و محصور می‌کنیم.
اکنون اگر برنامه را اجرا کرده و خروجی HTML آن‌را بررسی کنیم، به فرم زیر خواهیم رسید:
<form method="post">
   <input type="hidden" name="_handler" value="[store-checkout]checkout" />
   <input type="hidden" name="__RequestVerificationToken" value="CfDxxxxx" />
.
.
.
   <button type="submit">Submit</button>
</form>
همانطور که ملاحظه می‌کنید، اینبار مقدار فیلد مخفی handler_ که کار متمایز ساختن این فرم را به عهده دارد و از آن در سمت سرور جهت یافتن کامپوننت متناظری استفاده می‌شود، با حالتی‌که از کامپوننت FormMappingScope استفاده نشده بود، متفاوت است و نام FormMappingScope را در ابتدای خود به همراه دارد تا به این نحو، از تداخل احتمالی نام‌های فرم‌ها جلوگیری شود.

یک نکته: اگر به تگ‌های فرم HTML ای فوق دقت کنید، به همراه یک anti-forgery token نیز هست که کار تولید و مدیریت آن، به صورت خودکار صورت می‌گیرد و میان‌افزاری نیز برای آن طراحی شده که در فایل Program.cs برنامه، به صورت app.UseAntiforgery بکارگرفته شده‌است.


یک نکته: در Blazor 8x SSR می‌توان بجای EditForm، از همان HTML form متداول هم استفاده کرد

اگر بخواهیم بجای استفاده از EditForm، از فرم‌های استاندارد HTML هم در حالت SSR استفاده کنیم، این کار میسر بوده و روش کار به صورت زیر است:
<form method="post" @onsubmit="SaveData" @formname="MyFormName">
    <AntiforgeryToken />

    <InputText @bind-Value="Name" />

    <button>Submit</button>
</form>
در اینجا ذکر دایرکتیوهای onsubmit@ و formname@ را (شبیه به خواص و رویدادگردان‌های مشابهی در EditForm) به همراه ذکر صریح کامپوننت AntiforgeryToken، مشاهده می‌کنید. در حین استفاده از EditForm، نیازی به درج این کامپوننت نیست و به صورت خودکار اضافه می‌شود.


پردازش فرم‌های GET در Blazor 8x

در حالتی‌که از فرم‌های استاندارد HTML ای استفاده می‌شود، ممکن است method فرم، بجای post، حالت get باشد که نتایج آن به صورت کوئری استرینگ در نوار آدرس مرورگر ظاهر می‌شوند؛ مانند جستجوی گوگل که اشخاص می‌توانند کوئری استرینگ و لینک نهایی را به اشتراک بگذارند. روش پردازش یک چنین فرم‌هایی به صورت زیر است:
@page "/"

<form method="GET">
    <input type="text" name="q"/>
    <button type="submit">Search</button>
</form>


@code {
    [SupplyParameterFromQuery(Name="q")]
    public string SearchTerm { get; set; }
    
    protected override async Task OnInitializedAsync()
    {
       // do something with the search term
    }
}
در اینجا از ویژگی SupplyParameterFromQuery برای دریافت کوئری استرینگ استفاده شده و چون نام پارامتر تعریف شده با نام input فرم یکی نیست، این نام به صورت صریحی توسط خاصیت Name آن مشخص شده‌است.


یک ابتکار! تعاملی کردن قسمتی از صفحه بدون فعالسازی کامل Blazor Server و یا Blazor WASM کامل

این دکمه‌ی قرار گرفته‌ی در یک صفحه‌ی SSR را ملاحظه کنید:
<button class="nav-link border-0" @onclick="BeginSignOut">Log out</button>
در اینجا می‌خواهیم، اگر کاربری بر روی آن کلیک کرد، روال رویدادگردان منتسب به onclick اجرا شود. اما ... اگر در این حالت برنامه را اجرا کرده و بر روی دکمه‌ی Log out کلیک کنیم، هیچ اتفاقی رخ نمی‌دهد! یعنی روال رویدادگران BeginSignOut اصلا اجرا نمی‌شود. علت اینجا است که صفحات SSR، در نهایت یک static HTML بیشتر نیستند و فاقد قابلیت‌های تعاملی، مانند واکنش نشان دادن به کلیک بر روی یک دکمه هستند. برای رفع این مشکل یا می‌توان این قسمت از صفحه را کاملا تعاملی کرد که روش انجام آن‌را در قسمت‌های بعدی با جزئیات کاملی بررسی می‌کنیم و یا ... می‌توان این دکمه را داخل یک فرم جدید تعاملی به صورت زیر محصور کرد:
<EditForm Context="ctx" FormName="LogoutForm" method="post" Model="@Foo" OnValidSubmit="BeginSignOut">
     <button type="submit" class="nav-link border-0">Log out</button>
</EditForm>

@code{
    [SupplyParameterFromForm(Name = "LogoutForm")]
    public string? Foo {  get; set; }

    protected override void OnInitialized() => Foo = "";

    async Task BeginSignOut()
    {
        // TODO: SignOutAsync();
        // TODO: NavigateTo("/authentication/logout");
    }
}
در این حالت چون این فرم، از نوع فرم‌های جدید تعاملی است، برای پردازش آن نیازی به اتصال دائم SignalR و یا فعالسازی یک وب‌اسمبلی نیست. پردازش آن بر اساس استاندارد HTTP Post و فرم‌های آن، صورت گرفته و به این ترتیب می‌توان عملکرد onclick@ کاملا تعاملی را با یک فرم تعاملی جدید، شبیه سازی کرد.


یک نکته: می‌توان حالت post-back مانند فرم‌های تعاملی Blazor 8x را تغییر داد.

به همراه ویژگی‌های جدید مرتبط با صفحات SSR، ویژگی هدایت بهبودیافته هم وجود دارد که جزئیات بیشتر آن‌را در قسمت‌های بعدی این سری بررسی می‌کنیم. برای نمونه اگر مثال این قسمت را اجرا کنید، فرم آن به همراه یک post-back مانند به سمت سرور است که کاملا قابل احساس است؛ این رفتار هرچند استاندارد است، اما بی‌شباهت به برنامه‌های MVC ، Razor pages و یا وب‌فرم‌ها نیست و با فرم‌های بی‌صدا و سریع نگارش‌های قبلی Blazor متفاوت است. در Blazor8x می‌توان این نوع ارسال اطلاعات را Ajax ای هم کرد که به آن enhanced navigation می‌گویند. برای اینکار فقط کافی است ویژگی Enhance را به تگ EditForm اضافه کرد و یا ویژگی جدید data-enhance را به تگ‌های فرم‌های استاندارد HTML ای افزود. پس از آن اگر برنامه را اجرا کنیم، دیگر یک post-back استاندارد وب‌فرم‌ها مشاهده نمی‌شود و رفتار این صفحه بسیار سریع، نرم و روان خواهد بود.
<EditForm Model="PlaceModel" method="post" OnValidSubmit="SubmitOrder" FormName="checkout" Enhance>
در اینجا تنها تغییری که حاصل شده، اضافه شدن ویژگی Enhance به المان EditForm است. این ویژگی به صورت پیش‌فرض غیرفعال است که جزئیات بیشتر آن‌را در قسمت‌های بعدی بررسی خواهیم کرد.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: Blazor8x-Server-Normal.zip
مطالب
استفاده از خاصیت Local در Entity Framework
زمانی که از LINQ To Entity  استفاده می‌کنیم، با هر بار اجرای یک کوئری، این کوئری به سمت دیتابیس ارسال شده و اطلاعات مورد نظر را بازیابی می‌کند. حال اگر ما موجودیت جدیدی را به Context جاری اضافه کرده ولی آن را ذخیره نکرده باشیم، به علت عدم وجود موجودیت در دیتابیس (در حافظه وجود دارد) کوئری ارسالی ما این موجودیت جدید را شامل نمی‌شود. البته شایان ذکر است زمانیکه از متد Find استفاده می‌کنیم، به صورت پیش فرض ابتدا داخل حافظه کاوش شده و در صورت عدم وجود اطلاعات، کوئری را در دیتابیس اجرا می‌کند.
نکته قابل توجه این است که بعنوان مثال ما نیاز داریم یک لیست از موجودیت‌ها را به اشکال زیر داشته باشیم.
  1. به صورت لیست
  2. به صورت لیست sort شده براساس Name
  3. فیلتر بر روی لیستی از فیلدهای موجود
  4. این  لیست باید شامل تمامی داده‌های موجود (چه در رم و چه در دیتابیس) باشد.
نکته: توسط متد ToList میتوان به لیستی از موجودیت‌های مورد نظر دست یافت ولی امکان استفاده از تمامی داده‌های موجود (چه در رم و چه در دیتابیس) میسر نمی‌باشد.
کلاس DbSet خاصیتی به نام Local دارد که امکان استفاده از  تمامی داده‌های موجود را به ما می‌دهد و شامل هر داده‌ای که از دیتابیس Load شده، هر داده‌ای که اضافه شده، هر داده‌ای  که پاک شده (Delete Flag) ولی هنوز ذخیره نشده می‌شود. بنابراین در هنگام استفاده باید توجه داشت به علت اینکه هیچ نوع کوئری به دیتابیس ارسال نشده، قطعه کد زیر دارای مقدار Destinations in memory: 0 خواهد بود
    private static void GetLocalDestinationCount()
    {
        using (var context = new BreakAwayContext())
        {
            var count = context.Destinations.Local.Count;
            Console.WriteLine("Destinations in memory: {0}", count);
        }
    }

استفاده از متد Load
برای مثال می‌توانیم از Foreach استفاده کنیم و تمام اطلاعات مورد نظر را بدست آوریم
    private static void GetLocalDestinationCount()
    {
        using (var context = new BreakAwayContext())
        {
            foreach (var destination in context.Destinations)
            {
                Console.WriteLine(destination.Name);
            }
            var count = context.Destinations.Local.Count;
            Console.WriteLine("Destinations in memory: {0}", count);
        }
    }
کد بالا یک حلقه بر روی موجودیت‌های Destinations است که نیازی به توضیح خاصی ندارد.
حال با استفاده از متد Load قادر به جمع آوری اطلاعات دیتابیس به داخل رم نیز خواهیم بود و کد بالا تمیزتر خواهد شد.
    private static void GetLocalDestinationCountWithLoad()
    {
        using (var context = new BreakAwayContext())
        {
            context.Destinations.Load();
            var count = context.Destinations.Local.Count;
            Console.WriteLine("Destinations in memory: {0}", count);
        }
    }
متد Load یک extension method روی IQueryable<T> است که در فضای نام System.Data.Entity موجود است. پس امکان اجرای یک  LINQ query و سپس Load کردن آن را در حافظه را خواهیم داشت.
به کد زیر توجه کنید:
    private static void LoadAustralianDestinations()
    {
        using (var context = new BreakAwayContext())
        {
            var query = from d in context.Destinations
                        where d.Country == "Australia"
                        select d;
            query.Load();
            var count = context.Destinations.Local.Count;
            Console.WriteLine("Aussie destinations in memory: {0}", count);
        }
    }
همچنین ما قادر به استفاده از  LINQ query روی داده‌های Local که این متد در حافظه جمع آوری کرده، نیز خواهیم بود. 
به کد زیر توجه کنید.
    private static void LocalLinqQueries()
    {
        using (var context = new BreakAwayContext())
        {
            context.Destinations.Load();
            var sortedDestinations = from d in context.Destinations.Local
                                     orderby d.Name
                                     select d;
            Console.WriteLine("All Destinations:");
            foreach (var destination in sortedDestinations)
            {
                Console.WriteLine(destination.Name);

            }
        }
        var aussieDestinations = from d in context.Destinations.Local
                                 where d.Country == "Australia"
                                 select d;
        Console.WriteLine();
        Console.WriteLine("Australian Destinations:");
        foreach (var destination in aussieDestinations)
        {
            Console.WriteLine(destination.Name);
        }
    }
ابتدا کلیه داده‌ها Load می‌شود سپس به وسیله Foreach نام‌ها استخراج می‌شوند. سپس ار همان داده‌ها جهت اعمال فیلتر، استفاده مشود.

تفاوت بین Linq provider‌های مختلف:

عموماً دیتابیسها حساس به حروف کوچک و بزرگ نیستند؛ به عنوان مثال اگر Great و great در دیتابیس وجود داشته باشند اگر به دیتابیس کوئری اسال شود و درخواست great  داشته باشد هر دو را شامل میشود. حال اگر از Local استفاده شود به جهت اینکه در واقع از Linq to Object استفاده می‌کند فقط great را شامل خواهد شد.
تفاوت دیگر این است که LINQ to Object از متد Last پشتیبانی می‌کند ولی LINQ to Entities خیر.
در پست بعدی قصد دارم در مورد ObservableCollection توضیحاتی کلی بدهم.
مطالب
مقدمه‌ای بر داکر، قسمت سوم
در قسمت قبلی با Volume آشنا شدیم و نحوه‌ی اجرا کردن یک Source Code را درون Container یاد گرفتیم. در این قسمت میخواهیم یک Image شخصی ساخته، آن‌را اجرا و درون Docker hub ارسال نماییم.


Dockerfile چیست؟
Dockerfile عملا چیزی بیشتر از یک دستور العمل از نوع متنی برای build و ساخت یک docker image از آن نمیباشد. ضمن اینکه مراحل build شدن، cache شده و build‌های بعدی با سرعت خیلی بیشتری اجرا خواهند شد. بعد از نوشتن چند dockerfile متوجه خواهیم شد که مراحلش بسیار ساده است.
ساخت اولین Dockerfile
قبل از ساخت dockerfile، مثل جلسه‌ی قبل یک پروژه‌ی ساده‌ی nodejsی را با فایل index.js میسازیم:
const express = require('express')
const app = express()
const PORT = 3000;
app.get('/', (req, res) => {
  res.send('Hello World')
})
app.listen(PORT, () => {
  console.log(`listening on port ${PORT}!`)
})
درون  package.json هم این قسمت را تغییر میدهیم:
"scripts": {
    "start": "node index"
  },
حال فایل Dockerfile را ساخته و دستورالعمل‌های زیر را درون آن مینویسیم:
FROM node
ENV NODE_ENV=production
COPY . /var/www
WORKDIR /var/www
RUN npm i
EXPOSE 3000
ENTRYPOINT npm start
توضیحات دستورات فوق
1) FROM node یک imageی است که برنامه‌ی شما از آن استفاده میکند.
2) از environment variable استفاده کرده و نوع آن را روی production میگذاریم.
3) COPY کردن تمام فایل‌های دایرکتوری جاری پروژه درون فایل سیستم container به آدرس فوق.
4) عوض کردن work directory روی آدرسی که پروژه کپی شده است.
5) اجرا کردن دستور npm i برای نصب شدن Dependencies‌های پروژه.
6) EXPOSE کردن یک port برای ایجاد دسترسی.
7) نقطه‌ی شروع برنامه و دستور npm start که درون package.json قبل تعریف نموده بودیم.
 
بعد از ساخت Dockerfile فوق نوبت به build گرفتن از آن میرسد که با استفاده از دستور زیر میباشد:
docker build -f Dockerfile -t alikhll/testnode1 .
نکته: اگر image node را روی سیستم خود نداشته باشید ابتدا بصورت خودکار آن را pull مینماید.
1) پرچم f- که برای شناساندن فایل Dockefile میباشد، بدلیل این است که نام این فایل قابل تغییر میباشد.
2) پرچم t- برای نام image ساخته شده میباشد. همچنین . نیز به دایرکتوری جاری اشاره میکند.
بعد از ساخته شدن image با استفاده از دستور docker images میتوانید آن را مشاهده نمایید.
برای اجرای image نیز از دستور زیر استفاده میکنیم:
docker run -d -p 8080:3000 alikhll/testnode1
حال با استفاده از port 8080 میتوانید اپلیکیشن را اجرا نمایید.
از آنجایی که اکثر خوانندگان این مجموعه برنامه نویسان دات نت هستند یک Dockerfile دات نتی نیز برای تسلط بیشتر مینویسیم.
ابتدا دستورات زیر را درون ترمینال خود وارد کرده و یک پروژه‌ی وب از نوع Net Core. را میسازیم:
dotnet new web
dotnet restore
dotnet run
حال روی localhost قابلیت اجرا خواهد داشت؛ اما میخواهیم این app را بر روی container اجرا کنیم. بنابراین Dockerfile را اینگونه مینویسیم:
FROM microsoft/dotnet
ENV ASPNETCORE_URLS http://*:3000
COPY . /var/www
WORKDIR /var/www
RUN dotnet restore
EXPOSE 3000
ENTRYPOINT dotnet run
همه چیز خیلی شبیه به داکرفایل قبلی است، با این تفاوت که از ایمیج microsoft/dotnet استفاده کرده‌ایم (از imageهای سبکتری برای محیط production استفاده میکنیم! ضمن اینکه image فوق از Debian استفاده میکند. image جدیدی روی توزیع Alpine ایجاد شده است که حجم خیلی پایینی داشته و برای مطالعه بیشتر به اینجا رجوع کنید).
نکته‌ی مهم ASPNETCORE_URLS میباشد چون میخواهیم بتوانیم از خارج از محیط container با استفاده از IP، به آن دسترسی داشته باشیم (محیط local نیست).
 دستورات زیر را برای build و اجرا وارد مینماییم:
docker build -f Dockerfile -t alikhll/testasp1 .
docker run -d -p 8080:3000 alikhll/testasp1
اکنون app شما باید روی پورت خارجی 8080 قابل اجرا باشد.
نکته: من container قبلی nodejsی را stop کرده بودم وگرنه این پورت قابل استفاده‌ی مجدد نبود!
پابلیش کردن روی Docker Hub
انتشار دادن روی Docker hub ّبسیار ساده است. میتوانید یک اکانت به صورت رایگان ساخته و image‌های خود را بر روی آن انتشار دهید.
نکته: پروژه‌های تستی خود را میتوانید آنجا انتشار داده که البته قابلیت private بودن را ندارند. در صورتیکه برای یک پروژه‌ی واقعی که image‌های عمومی نیستند و فقط درون سازمان باید به آن دسترسی داشته باشند، میتوانید اکانت enterprise تهیه کرده و image‌های خود را درون آن مدیریت نمایید. همچنین از سرویس‌های ابری Azure, Amazon نیز برای انتشار دادن imageهای خصوصی میتوان استفاده نمود.
دستور زیر برای انتشار دادن imageی که ساختیم روی docker hub میباشد. ابتدا login کرده user/password را وارد کرده سپس push مینماییم:
docker login
docker push alikhll/testnode1
نکته: به جای alikhll باید username شخصی خود را وارد نمایید.
اکنون به راحتی با استفاده از دستور زیر روی یک ماشین دیگر که داکر روی آن نصب شده است، میتوانید image را pull کرده و اجرا نمایید:
docker pull alikhll/testnode1
مطالب
OpenCVSharp #4
کار با فیلترها در OpenCVSharp

فرض کنید قصد داریم یک چنین مثال زبان C را که در مورد کار با فیلترها در OpenCV است، به نمونه‌ی دات نتی آن تبدیل کنیم:
        #include <cv.h>
        #include <highgui.h>
        #include <stdio.h>
 
        int main (int argc, char **argv)
        {
          IplImage *src_img = 0, *dst_img;
          float data[] = { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
          };
          CvMat kernel = cvMat (1, 21, CV_32F, data);
 
          if (argc >= 2)
            src_img = cvLoadImage (argv[1], CV_LOAD_IMAGE_ANYDEPTH | CV_LOAD_IMAGE_ANYCOLOR);
          if (src_img == 0)
            exit (-1);
 
          dst_img = cvCreateImage (cvGetSize (src_img), src_img->depth, src_img->nChannels);
 
          cvNormalize (&kernel, &kernel, 1.0, 0, CV_L1);
          cvFilter2D (src_img, dst_img, &kernel, cvPoint (0, 0));
 
          cvNamedWindow ("Filter2D", CV_WINDOW_AUTOSIZE);
          cvShowImage ("Filter2D", dst_img);
          cvWaitKey (0);
 
          cvDestroyWindow ("Filter2D");
          cvReleaseImage (&src_img);
          cvReleaseImage (&dst_img);
 
          return 0;
        }
معادل OpenCVSharp آن به صورت ذیل خواهد بود:
using (var src = new IplImage(@"..\..\Images\Penguin.Png", LoadMode.AnyDepth | LoadMode.AnyColor))
using (var dst = new IplImage(src.Size, src.Depth, src.NChannels))
{
    float[] data = { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 };
    var kernel = new CvMat(rows: 1, cols: 21, type: MatrixType.F32C1, elements: data);
 
    Cv.Normalize(src: kernel, dst: kernel, a: 1.0, b: 0, normType: NormType.L1);
    Cv.Filter2D(src, dst, kernel, anchor: new CvPoint(0, 0));
 
    using (new CvWindow("src", image: src))
    using (new CvWindow("dst", image: dst))
    {
        Cv.WaitKey(0);
    }
}
با این خروجی:


در قسمت‌های قبلی در مورد بارگذاری تصاویر، تهیه‌ی یک Clone از آن و همچنین ساخت یک پنجره به روش‌های مختلف و رها سازی خودکار منابع مرتبط، بیشتر بحث شد. در اینجا تصویر اصلی با همان عمق و وضوح تغییر نیافته‌ی آن بارگذاری می‌شود.
کار cvMat، آغاز یک ماتریس OpenCV است. پارامترهای آن، تعداد ردیف‌ها، ستون‌ها، نوع داده‌ی المان‌ها و داده‌های مرتبط را مشخص می‌کنند.
در این مثال آرایه‌ی data، یک فیلتر را تعریف می‌کند که در اینجا، حالت یک بردار را دارد تا یک ماتریس. برای تبدیل آن به ماتریس، از شیء CvMat استفاده خواهد شد که آن‌را تبدیل به ماتریسی با یک ردیف و 21 ستون خواهد کرد.
در اینجا از نام کرنل استفاده شده‌است. کرنل در OpenCV به معنای ماتریسی از داده‌ها با یک نقطه‌ی anchor (لنگر) است. این لنگر به صورت پیش فرض در میانه‌ی ماتریس قرار دارد (نقطه‌ی 1- , 1- ).


مرحله‌ی بعد، نرمال سازی این فیلتر است. تاثیر نرمال سازی اطلاعات را به این نحو می‌توان نمایش داد:
 double sum = 0;
foreach (var item in data)
{
     sum += Math.Abs(item);
}
Console.WriteLine(sum); // => .999999970197678
در اینجا پس از نرمال سازی، جمع عناصر بردار data، تقریبا مساوی 1 خواهد بود و تمام عناصر بردار data، به داده‌هایی بین یک و صفر، نگاشت خواهند شد.
اگر مرحله‌ی نرمال سازی اطلاعات را حذف کنیم، تصویر نهایی حاصل، چنین شکلی را پیدا می‌کند:


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




فیلترهای توکار OpenCV

علاوه بر امکان طراحی فیلترهای سفارشی خطی مانند مثال فوق، کتابخانه‌ی OpenCV دارای تعدادی فیلتر توکار نیز می‌باشد که نمونه‌ای از آن‌را در مثال ذیل می‌توانید مشاهده کنید:
using (var src = new IplImage(@"..\..\Images\Car.jpg", LoadMode.AnyDepth | LoadMode.AnyColor))
{
    using (var dst = new IplImage(src.Size, src.Depth, src.NChannels))
    {
        using (new CvWindow("src", image: src))
        {
            Cv.Erode(src, dst);
            using (new CvWindow("Erode", image: dst))
            {
                Cv.Dilate(src, dst);
                using (new CvWindow("Dilate", image: dst))
                {
                    Cv.Not(src, dst);
                    using (new CvWindow("Invert", image: dst))
                    {
                        Cv.WaitKey(0);
                    }
                }
            }
        }
    }
}
با این خروجی



کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید.
نظرات مطالب
ایجاد alert,confirm,prompt هایی متفاوت با jQuery Impromptu
زمانیکه از jQuery استفاده می‌کنید، دیگر نباید از Ajax خام استفاده کنید. خود jQuery تابع سازگار با انواع و اقسام مرورگرها رو برای کار با Ajax داره. این مثال تست شده و مشکلی نداره:
using System.Web.Mvc;

namespace jQueryImpromptu.Controllers
{
    public class HomeController : Controller
    {
        [HttpGet]
        public ActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public ActionResult Index(string postId, string filename)
        {
            return Content("ok");
        }
    }
}

@{
    ViewBag.Title = "Index";
    var postUrl = Url.Action(actionName: "Index", controllerName: "Home");
}
<h2>
    Index</h2>
<input type="button" id="btnDelete" value="delete" />
<div id="infoDiv">
    Some Info ....
</div>
@section JavaScript
{
    <script type="text/javascript">
        $(function () {
            $('#btnDelete').click(function () {
                $.prompt("آیا برای حذف اطلاعات موجود اطمینان دارید ؟",
             {
                 title: 'Title',
                 buttons: { "بله": true, "خیر": false },
                 focus: 2,
                 submit: function (e, v, m, f) {
                     //debugger;
                     if (!v) {
                         return;
                     }
                     var postId = 10;
                     var fileName = "test.jpg"
                     $.ajax({
                         type: "POST",
                         url: '@postUrl',
                         data: JSON.stringify({ postId: postId, fileName: fileName }),
                         contentType: "application/json; charset=utf-8",
                         dataType: "json",
                         complete: function (xhr, status) {
                             var data = xhr.responseText;
                             if (status === 'error' || !data || data == "nok") {
                                 $.prompt("! حذف فایل با خطا مواجه شده است",
                                    {
                                        title: 'Title',
                                        buttons: { "بستن": true }
                                    });
                             }
                             else {
                                 $("#infoDiv").fadeOut("slow").remove();
                             }
                         }
                     });
                 }
             });
            });
        });
    </script>
}
مطالب
تزریق وابستگی‌ها در ASP.NET Core - بخش 2 - ثبت اولین سرویس
یک پروژه‌ی ASP.NET Core را با قرار دادن نسخه‌ی NET Core. بر روی 3.1 و با استفاده از قالب Model View Controller ایجاد کنید. در اینجا نام پروژه را AspNetCoreDependencyInjection گذاشته‌ام. حالا در  پوشه‌ی Models، فایلی را با نام HomeViewModel.cs با محتویاتی به صورت زیر اضافه کنید:
public class HomeViewModel
{
     public string Id { get; set; }
     public string Message { get; set; }
     public DateTime DateTime { get; set; }
}

اکنون به پوشه‌ی Views بروید و فایل Index.cshtml را به این صورت تغییر دهید:

@model AspNetCoreDependencyInjection.Models.HomeViewModel
@{
ViewData["Title"] = "Home";
}

<div>
<div>
<div>
<p>
<b>Id : </b><span>@Model.Id</span> <br />
<b>Date And Time : </b><span> @Model.DateTime </span> <br/>
<b>Message : </b><span>@Model.Message</span>
</p>
</div>
</div>
</div>
و فایل MessageServiceA.cs را به پروژه اضافه کنید:
using AspNetCoreDependencyInjection.Services;

namespace AspNetCoreDependencyInjection.ServicesImplentaions
{
    public class MessageServiceAA 
    {
        public string Message()
        {
            return "A message from MessageServiceAA";
        }
    }
}
و همچنین فایل GuidHelper.cs را نیز اضافه می‌کنیم:
namespace AspNetCoreDependencyInjection.Helpers
{
    public class GuidProvider
    {
        private readonly Guid _serviceGuid;

        public GuidProvider()
        {
            _serviceGuid = Guid.NewGuid();
        }

        public Guid GetNewGuid() => Guid.NewGuid();

        public string GetGuidAsFormatedString(string prefix = "") => getFormatedGuid(_serviceGuid, prefix);

        private string getFormatedGuid(Guid guid, string prefix = "")
        {
            var guidString = guid.GetHashCode().ToString("x");
            if (string.IsNullOrEmpty(prefix) == false)
                guidString = new StringBuilder($"{prefix}-").Append(guidString).ToString();
            return guidString;
        }
    }
}

حالا درون کنترل HomeController، این تغییرات را انجام می‌دهیم:

private readonly ILogger<HomeController> _logger;
private readonly MessageServiceAA _messageService;
private readonly GuidProvider _ guidProvider;

public HomeController(ILogger<HomeController> logger)
{
            _logger = logger;
            _messageService = new MessageServiceAA();
            _guidProvider = new GuidProvider();
}

public IActionResult Index()
{
            var model = new HomeViewModel()
            {
                Id = _ guidProvider.GetGuidAsFormatedString(),
                Message = _messageService.Message(),
                DateTime = DateTime.Now,
            };
            return View(model);
}

همانطور که می‌بینید، در کد بالا، کنترلر HomeController، به دو شیء از کلاس‌ها و یا سرویس‌های GuidProvider و MessageServiceAA به صورت مستقیم وابسته شده‌است و با هر تغییری در هر کدام از این سرویسها، باید دوباره کامپایل شود. علاوه بر این اگر بخواهیم پیاده سازی‌های مختلفی را برای هر کدام از این موارد، ارائه دهیم، به مشکل بر می‌خوریم. خب بیاید تغییراتی را در کد بالا بدهیم تا مشکلات ذکر شده را حل کنیم.

برای این منظور پوشه‌ای را به نام Services می‌سازیم و اینترفیسی را به نام IMessageBrokerA ایجاد می‌کنیم و سپس کاری می‌کنیم که MessageServiceAA از این اینترفیس ارث بری کند:

namespace AspNetCoreDependencyInjection.Services
{
    public interface IMessageServiceA
    {
        string Message();
    }
}

و حالا می‌خواهیم با استفاده از تزریق وابستگی، وابستگی کنترلر HomeController را از کلاس MessageBrokerAA لغو کرده و آن را به اینترفیس IMessageBrokerA (انتزاع) وابسته کنیم. در اینجا ما از تکنیک تزریق درون سازنده یا Constructor Injection استفاده می‌کنیم.


تزریق درون سازنده

در این تکنیک، ما لیستی از وابستگی‌های مورد نیاز را به عنوان پارامترهای ورودی سازنده‌ی کلاس، تعریف می‌کنیم:
private readonly ILogger<HomeController> _logger;
private readonly IMessageServiceA _messageService;
private readonly GuidProvider _guidHelper;
public HomeController(ILogger<HomeController> logger , IMessageServiceA messageService)
{
        _logger = logger;
        _messageService = messageService;
        _messageService = new MessageServiceAA();
        _guidHelper = new GuidProvider();
}
و حالا اگر برنامه را اجرا کنیم، با خطایی روبه رو می‌شویم که در آن می‌گوید امکان واکشی (Resolve) سرویس‌های مورد نظر وجود ندارد. این خطا به دلیل ثبت نشدن اینترفیس IMessageServiceA و پیاده سازی آن، درون Microsoft Dependency Injection Container است   DI Container‌ها معمولا باید در زمان شروع برنامه، پیکربندی و مقدار دهی شودند، تا در ادامه‌ی چرخه‌ی حیات برنامه، بتوانند سرویس‌ها و اشیاء مورد نیاز را به کلاس‌هایی که نیاز دارند، واکشی و تزریق کنند. اولین مرحله از کار با DI Container‌ها، ثبت کردن سرویس‌ها درون آنهاست.
در ASP.NET Core از IServiceCollection برای ثبت کردن سرویس‌های برنامه‌ی خودمان استفاده می‌کنیم. تمامی سرویس‌هایی را که انتظار داریم توسط DI Container به کلاس‌هایی تزریق شوند، باید درون IServiceCollection ثبت گردند. تمام سرویس‌هایی که به وسیله‌ی IServiceCollection ثبت شده‌اند، پس از ساخته شدن، توسط اینترفیس IServiceProvider قابل واکشی هستند.

بنابراین دو اینترفیس حیاتی برای کار کرد صحیح Microsoft Dependency Injection Container درون ASP.NET Core وجود دارند:
  • IServiceCollection : برای ثبت سرویس‌ها
  • IServiceProvider : برای واکشی سرویس‌ها

در ASP.NET Core معمول‌ترین مکان برای ثبت کردن سرویس‌ها درون Container، به صورت پیش فرض درون کلاس Startup و درون متد ConfigureServices انجام می‌گیرد.
به صورت پیش فرض کلاس Startup دو متد دارد:
  • ConfigureServices : برای پیکربندی و ثبت سرویس‌های درونی DI Container استفاده می‌شود.
  • Configure : برای تنظیمات pipeline میان افزارها ( Middlewares ) بکار می‌رود.

در اینجا پیاده سازی پیش فرض کلاس Startup را می‌بینیم که البته کدهای درون متد Configure را برای درگیر نکردن ذهن شما، مخفی کرده‌ایم: 
public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            //  کدها جهت خوانایی بیشتر مخفی شده اند
        }
    }

همانطورکه می‌بینید، متد ConfigureService پارامتر IServiceCollection را می‌گیرد که به وسیله‌ی WebHost در زمان اجرای برنامه، مقدار دهی می‌شود.

تعداد زیادی Extension method برای IServiceCollection وجود دارند که برای پشتیبانی از ثبت کردن سرویس‌های مختلف در سناریوهای گوناگون به کار می‌روند. در اینجا ما از نسخه‌ی 3.1 چارچوب ASP.NET Core استفاده می‌کنیم. برای همین هم برای ثبت سرویس‌های پیش فرض فریمورک MVC از متد توسعه‌ی services.AddControllersWithViews()    استفاده می‌کنیم.  متد توسعه‌ی AddControllersWithViews() سرویس‌هایی را که معمولا در فریم ورک MVC استفاده می‌شوند، درون IServiceCollection ثبت می‌کند. در نسخه‌های قبلی چارچوب ASP.NET Core،  مانند نسخه‌های 2.1 و 2.2 برای این کار از متد توسعه‌ی AddMvc() استفاده می‌شد.

در Microsoft Dependency Injection Container ، معمولا  ترتیب ثبت سرویس‌ها مهم نیست.

خب، اولین سرویس اختصاصی برنامه‌ی خودمان را با چرخه‌ی حیات Transient و زیر سرویس پیشین، به شکل زیر ثبت می‌کنیم :

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
            services.AddTransient<IMessageServiceA, MessageServiceAA>();
        }
همانطور که می‌بینید، در اینجا ما از متد AddTransient() استفاده کرده‌ایم. متد AddTransient() درون فضای نام Microsoft.Extensions.DependencyInjection قرار دارد. این متد Overload ‌های گوناگونی دارد و ما از نوعی از آن استفاده کرده‌ایم که دو نوع generic را می‌پذیرد و تعریف آن به صورت زیر است: 
public static IServiceCollection AddTransient<TService, TImplementation>(this IServiceCollection services)
در اینجا TService ، اینترفیس سرویس ماست. این نوع، همان نوعی است که کلاس‌های ما می‌توانند به آن وابسته باشند. پارامتر دوم، از نوع TImplemention است که پیاده سازی مورد نظر برای TService را ثبت می‌کند. TImplmention   نوعی است که Container در زمان واکشی و تزریق TService از آن نمونه سازی کرده و به کلاس مورد نظر تزریق می‌کند.

در اینجا وقتی ما برای IMessageServiceA ، پیاده سازی MessageServiceA را ثبت می‌کنیم، از این به بعد DI Container، هر زمانیکه در لیست پارامترهای سازنده‌ی یک کلاس، IMessageServiceA را مشاهده کند، بررسی می‌کند که چه کلاسی به عنوانی پیاده سازی این اینترفیس ثبت شده‌است، سپس از آن نمونه سازی می‌کند و درون سازنده‌ی مورد نظر تزریق می‌کند. خب، حالا برنامه را دوباره اجرا کنید؛ می‌بینید که برنامه اجرا می‌شود.

 
در ادامه ابتدا در مورد روش‌های مختلف ثبت سرویس‌ها و بعد روش‌های واکشی سرویس‌ها را بررسی می‌کنیم.
مطالب
شروع به کار با EF Core 1.0 - قسمت 2 - به روز رسانی ساختار بانک اطلاعاتی
پس از برپایی تنظیمات اولیه‌ی کار با EF Core در ASP.NET Core، اکنون نوبت به تبدیل کلاس Person، به جدول معادل آن در بانک اطلاعاتی برنامه است. در EF Core نیز همانند EF Code First 6.x، برای انجام یک چنین اعمالی از مفهومی به نام Migrations استفاده می‌شود که در ادامه به آن خواهیم پرداخت.


پیشنیازهای کار با EF Core Migrations

در قسمت قبل در حین بررسی «برپایی تنظیمات اولیه‌ی EF Core 1.0 در یک برنامه‌ی ASP.NET Core 1.0»، چهار مدخل جدید را به فایل project.json برنامه اضافه کردیم. مدخل جدید Microsoft.EntityFrameworkCore.Tools که به قسمت tools آن اضافه شد، پیشنیاز اصلی کار با EF Core Migrations است.


بررسی ابزارهای خط فرمان EF Core و تشکیل ساختار بانک اطلاعاتی بر اساس کلاس‌های برنامه

پس از تکمیل پیشنیازهای کار با EF Core، از طریق خط فرمان به پوشه‌ی جاری پروژه وارد شده و دستور dotnet ef را صادر کنید.
یک نکته: در ویندوز اگر در پوشه‌ای، کلید shift را نگه دارید و در آن پوشه کلیک راست کنید، در منوی باز شده، گزینه‌ی جدیدی را به نام Open command window here مشاهده خواهید کرد که می‌تواند به سرعت خط فرمان را از پوشه‌ی جاری شروع کند.

خروجی صدور فرمان dotnet ef را در ذیل مشاهده می‌کنید:
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef
                     _/\__
               ---==/    \\
         ___  ___   |.    \|\
        | __|| __|  |  )   \\\
        | _| | _|   \_/ |  //|\\
        |___||_|       /   \\\/\\
Entity Framework .NET Core CLI Commands 1.0.0-preview2-21431
Usage: dotnet ef [options] [command]
Options:
  -h|--help                      Show help information
  -v|--verbose                   Enable verbose output
  --version                      Show version information
  --assembly <ASSEMBLY>          The assembly file to load.
  --startup-assembly <ASSEMBLY>  The assembly file containing the startup class.
  --data-dir <DIR>               The folder used as the data directory (defaults to current working directory).
  --project-dir <DIR>            The folder used as the project directory (defaults to current working directory).
  --content-root-path <DIR>      The folder used as the content root path for the application (defaults to application base directory).
  --root-namespace <NAMESPACE>   The root namespace of the target project (defaults to the project assembly name).
Commands:
  database    Commands to manage your database
  dbcontext   Commands to manage your DbContext types
  migrations  Commands to manage your migrations
Use "dotnet ef [command] --help" for more information about a command.
در قسمت Commands آن در انتهای لیست، از فرمان migrations آن استفاده خواهیم کرد. برای این منظور در همین پوشه‌ی جاری، دستور ذیل را صادر کنید:
 D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef migrations add InitialDatabase
دستورات migrations با dotnet ef migrations شروع شده و سپس یک سری پارامتر را دریافت می‌کنند برای مثال در اینجا سوئیچ add، به همراه یک نام دلخواه ذکر شده‌است (نام این مرحله را InitialDatabase گذاشته‌ایم). پس از فراخوانی این دستور، اگر به Solution explorer مراجعه کنید، پوشه‌ی جدید Migrations، قابل مشاهده است:


نام دلخواه InitialDatabase را در انتهای نام فایل 13950526050417_InitialDatabase مشاهده می‌کنید.
اگر قصد حذف این مرحله را داشته باشیم، می‌توان دستور dotnet ef migrations remove را مجددا صادر کرد.

فایل 13950526050417_InitialDatabase به همراه کلاسی است که در آن دو متد Up و Down قابل مشاهده هستند. متد Up نحوه‌ی ایجاد جدول جدیدی را از کلاس Person بیان می‌کند و متد Down نحوه‌ی Drop این جدول را پیاده سازی کرده‌است.
فایل ApplicationDbContextModelSnapshot.cs دارای کلاسی است که خلاصه‌ای از تعاریف موجودیت‌های ذکر شده‌ی در DB Context برنامه را به همراه دارد و تفسیر آن‌ها را از دیدگاه  EF در اینجا می‌توان مشاهده کرد.

پس از مرحله‌ی افزودن migrations، نوبت به اعمال آن به بانک اطلاعاتی است. تا اینجا EF تنها متدهای Up و Down مربوط به ساخت و حذف ساختار جداول را ایجاد کرده‌است. اما هنوز آن‌ها را به بانک اطلاعاتی برنامه اعمال نکرده‌است. برای اینکار در پوشه‌ی جاری دستور ذیل را صادر کنید:
 D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef database update
Applying migration '13950526050417_InitialDatabase'.
Done.
همانطور که ملاحظه می‌کنید، دستور dotnet ef database update سبب اعمال اطلاعات فایل 13950526050417_InitialDatabase به بانک اطلاعاتی شده‌است.
اکنون اگر به لیست بانک‌های اطلاعاتی مراجعه کنیم، بانک اطلاعاتی جدید TestDbCore2016 را به همراه جدول متناظر کلاس Person می‌توان مشاهده کرد:


در اینجا جدول دیگری به نام __EFMigrationsHistory نیز قابل مشاهده‌است که کار آن ذخیره سازی وضعیت فعلی Migrations در بانک اطلاعاتی، جهت مقایسه‌های آتی است. این جدول صرفا توسط ابزارهای EF استفاده می‌شود و نباید به صورت مستقیم تغییری در آن ایجاد کنید.


مقدار دهی اولیه‌ی جداول بانک‌های اطلاعاتی در EF Core

در همین حالت اگر کنترلر TestDB مطرح شده‌ی در انتهای بحث قسمت قبل را اجرا کنیم، به این استثناء خواهیم رسید:


این تصویر بدین معنا است که کار Migrations موفقیت آمیز بوده‌است و اینبار امکان اتصال و کار با بانک اطلاعاتی وجود دارد، اما این جدول حاوی اطلاعات اولیه‌ای برای نمایش نیست.
در نگارش قبلی EF Code First، امکانات Migrations به همراه یک متد Seed نیز بود که توسط آن کار مقدار دهی اولیه‌ی جداول را می‌توان انجام داد (زمانیکه جدولی ایجاد می‌شود، در همان هنگام، چند رکورد خاص نیز به آن اضافه شوند. برای مثال به جدول کاربران، رکورد اولین کاربر یا همان Admin اضافه شود). این متد در EF Core 1.0 وجود ندارد.
برای این منظور کلاس جدیدی را به نام ApplicationDbContextSeedData به همان پوشه‌ی جدید Migrations اضافه کنید؛ با این محتوا:
using System.Collections.Generic;
using System.Linq;
using Core1RtmEmptyTest.Entities;
using Microsoft.Extensions.DependencyInjection;

namespace Core1RtmEmptyTest.Migrations
{
    public static class ApplicationDbContextSeedData
    {
        public static void SeedData(this IServiceScopeFactory scopeFactory)
        {
            using (var serviceScope = scopeFactory.CreateScope())
            {
                var context = serviceScope.ServiceProvider.GetService<ApplicationDbContext>();
                if (!context.Persons.Any())
                {
                    var persons = new List<Person>
                    {
                        new Person
                        {
                            FirstName = "Admin",
                            LastName = "User"
                        }
                    };
                    context.AddRange(persons);
                    context.SaveChanges();
                }
            }
        }
    }
}
و سپس نحوه‌ی فراخوانی آن در متد Configure کلاس آغازین برنامه به صورت زیر است:
public void Configure(IServiceScopeFactory scopeFactory)
{
    scopeFactory.SeedData();
به همراه این تغییر در نحوه‌ی معرفی Db Context برنامه:
public void ConfigureServices(IServiceCollection services)
{
   services.AddDbContext<ApplicationDbContext>(ServiceLifetime.Scoped);
توضیحات:
- برای پیاده سازی الگوی واحد کار، اولین قدم، مشخص سازی طول عمر Db Context برنامه است. برای اینکه تنها یک Context در طول یک درخواست وهله سازی شود، نیاز است به نحو صریحی طول عمر آن‌را به حالت Scoped تنظیم کرد. متد AddDbContext دارای پارامتری است که این طول عمر را دریافت می‌کند. بنابراین در اینجا ServiceLifetime.Scoped ذکر شده‌است. همچنین در این مثال از نمونه‌ای که IConfigurationRoot به سازنده‌ی کلاس ApplicationDbContext تزریق شده (نکته‌ی انتهای بحث قسمت قبل)، استفاده شده‌است. به همین جهت تنظیمات options آن‌را ملاحظه نمی‌کنید.
- مرحله‌ی بعد نحوه‌ی دسترسی به این سرویس ثبت شده در یک کلاس static دارای متدی الحاقی است. در اینجا دیگر دسترسی مستقیمی به تزریق وابستگی‌ها نداریم و باید کار را با  IServiceScopeFactory شروع کنیم. در اینجا می‌توانیم به صورت دستی یک Scope را ایجاد کرده، سپس توسط ServiceProvider آن، به سرویس ApplicationDbContext دسترسی پیدا کنیم و در ادامه از آن به نحو متداولی استفاده نمائیم. IServiceScopeFactory جزو سرویس‌های توکار ASP.NET Core است و در صورت ذکر آن به عنوان پارامتر جدیدی در متد Configure، به صورت خودکار وهله سازی شده و در اختیار ما قرار می‌گیرد.
- نکته‌ی مهمی که در اینجا بکار رفته‌است، ایجاد Scope و dispose خودکار آن توسط عبارت using است. باید دقت داشت که ایجاد Scope و تخریب آن به صورت خودکار در ابتدا و انتهای درخواست‌ها توسط ASP.NET Core انجام می‌شود. اما چون شروع کار ما از متد Configure است، در اینجا خارج از Scope قرار داریم و باید مدیریت ایجاد و تخریب آن‌را به صورت دستی انجام دهیم که نمونه‌ای از آن‌را در متد SeedData کلاس ApplicationDbContextSeedData ملاحظه می‌کنید. در اینجا Scope ایی ایجاد شده‌است. سپس داده‌های اولیه‌ی مدنظر به بانک اطلاعاتی اضافه گردیده و در آخر این Scope تخریب شده‌است.
- اگر کار ایجاد و تخریب scope، به نحوی که مشخص شده‌است انجام نگیرد، طول عمر درخواستی خارج از Scope، همواره Singleton خواهد بود. چون خارج از طول عمر درخواست جاری قرار داریم و هنوز کار به سرویس دهی درخواست‌ها نرسیده‌است. بنابراین مدیریت Scopeها هنوز شروع نشده‌است و باید به صورت دستی انجام شود.

در این حالت اگر برنامه را اجرا کنیم، این خروجی قابل مشاهده است:


که به معنای کار کردن متد SeedData و ثبت اطلاعات اولیه‌ای در بانک اطلاعاتی است.


اعمال تغییرات به مدل‌های برنامه و به روز رسانی ساختار بانک اطلاعاتی

فرض کنید به کلاس Person قسمت قبل، خاصیت Age را هم اضافه کرده‌ایم:
namespace Core1RtmEmptyTest.Entities
{
    public class Person
    {
        public int PersonId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }

        public int Age { get; set; }
    }
}
در این حالت اگر برنامه را اجرا کنیم، به استثنای ذیل خواهیم رسید:
 An unhandled exception occurred while processing the request.
SqlException: Invalid column name 'Age'.
برای رفع این مشکل نیاز است مجددا مراحل Migrations را اجرا کرد:
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef migrations add v2
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef database update
در اینجا همان دستورات قبل را مجددا اجرا می‌کنیم. با این تفاوت که اینبار نام دلخواه این مرحله را مثلا v2، به معنای نگارش دوم وارد کرده‌ایم.
با اجرا این دستورات، فایل جدید 13950526073248_v2 به پوشه‌ی Migrations اضافه می‌شود. این فایل حاوی نحوه‌ی به روز رسانی بانک اطلاعاتی، بر اساس خاصیت جدید Age است. سپس با اجرای دستور dotnet ef database update، کار به روز رسانی بانک اطلاعاتی بر اساس مرحله‌ی v2 انجام می‌شود.


بنابراین هر بار که تغییری را در مدل‌های خود ایجاد می‌کنید، یکبار باید کلاس مهاجرت آن‌را ایجاد کنید و سپس آن‌را به بانک اطلاعاتی اعمال نمائید.


تهیه اسکریپت تغییرات بجای اعمال تغییرات توسط ابزارهای EF

شاید علاقمند باشید که پیش از اعمال تغییرات به بانک اطلاعاتی، یک اسکریپت SQL از آن تهیه کنید (جهت مطالعه و یا اعمال دستی آن توسط خودتان). برای اینکار می‌توانید دستور ذیل را در پوشه‌ی جاری پروژه اجرا کنید:
 D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef migrations script -o v2.sql
در این حالت اسکریپت SQL تغییرات، در فایلی به نام v2.sql، در ریشه‌ی جاری پروژه تولید می‌شود.


تغییرات ساختار جدول __EFMigrationsHistory در EF Core 1.0


در EF 6.x، ساختار اطلاعات جدول نگهداری تاریخچه‌ی تغییرات، بسیار پیچیده بود و شامل رشته‌ای gzip شده‌ی حاوی یک snapshot از کل ساختار دیتابیس در هر مرحله‌ی migration بود. در این نگارش، این snapshot حذف شده‌است و بجای آن فایل ApplicationDbContextModelSnapshot.cs را مشاهده می‌کنید (تنها یک snapshot به ازای کل context برنامه). همچنین در اینجا کاملا مشخص است که چه مراحلی به بانک اطلاعاتی اعمال شده‌اند و دیگر خبری از رشته‌ی gzip شده‌ی قبلی نیست (تصویر فوق).

در شکل زیر ساختار قبلی این جدول را در EF 6.x مشاهده می‌کنید. در EF 6.x حتی فضای نام کلاس‌های موجودیت‌های برنامه هم مهم هستند و در صورت تغییر، مشکل ایجاد می‌شود:



مهاجرت خودکار از EF Core حذف شده‌است

در EF 6.x در کنار کلاس Db Context یک کلاس Configuration هم وجود داشت که برای مثال امکان چنین تعریفی در آن میسر هست:
public Configuration()
{
   AutomaticMigrationsEnabled = true;
}
کار آن مهاجرت خودکار اطلاعات context به بانک اطلاعاتی بود؛ بدون نیازی به استفاده از دستورات خط فرمان مرتبط. تمام این موارد از EF Core حذف شده‌اند و علت آن‌را می‌توانید در توضیحات یکی از اعضای تیم EF Core در اینجا مطالعه کنید و خلاصه‌ی آن به این شرح است:
با حذف مهاجرت خودکار:
- دیگر نیازی نیست تا model snapshots در بانک اطلاعاتی ذخیره شوند (همان ساده شدن ساختار جدول ذخیره سازی تاریخچه‌ی مهاجرت‌های فوق).
- در حالت افزودن یک مرحله‌ی مهاجرت، دیگر نیازی به کوئری گرفتن از بانک اطلاعاتی نخواهد بود (سرعت بیشتر).
- می‌توان چندین مرحله‌ی مهاجرت را افزود بدون اینکه الزاما مجبور به اعمال آن‌ها به بانک اطلاعاتی باشیم.
- کاهش کدهای مدیریت ساختار بانک اطلاعاتی.
- تیم‌ها برای یکی کردن تغییرات خود مشکلی نخواهند داشت چون دیگر snapshot مدل‌ها در جدول __EFMigrationsHistory ذخیره نمی‌شود.

بنابراین در EF Core می‌توان مهاجرت v1 را اضافه کرد. سپس تغییراتی را در کدها اعمال کرد. در ادامه مهاجرت v2 را تولید کرد و در آخر کار اعمال یکجای این‌ها را به بانک اطلاعاتی انجام داد.

هرچند در اینجا اگر می‌خواهید مرحله‌ی اجرای دستور dotnet ef database update را حذف کنید، می‌توانید از کدهای ذیل نیز استفاده نمائید:
using Core1RtmEmptyTest.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace Core1RtmEmptyTest.Migrations
{
    public static class DbInitialization
    {
        public static void Initialize(this IServiceScopeFactory scopeFactory)
        {
            using (var serviceScope = scopeFactory.CreateScope())
            {
                var context = serviceScope.ServiceProvider.GetService<ApplicationDbContext>();
                // Applies any pending migrations for the context to the database.
                // Will create the database if it does not already exist.
                context.Database.Migrate();
            }
        }
    }
}
روش فراخوانی آن نیز همانند روش فراخوانی متد SeedData است که پیشتر بحث شد.
کار متد Migrate، ایجاد بانک اطلاعاتی در صورت عدم وجود و سپس اعمال تمام مراحل migration ایی است که در جدول __EFMigrationsHistory ثبت نشده‌اند (دقیقا همان کار دستور dotnet ef database update را انجام می‌دهد).


تفاوت متد Database.EnsureCreated با متد Database.Migrate

اگر به متدهای context.Database دقت کنید، یکی از آن‌ها EnsureCreated نام دارد. این متد نیز سبب تولید بانک اطلاعاتی بر اساس ساختار Context برنامه می‌شود. اما هدف آن صرفا استفاده‌ی از آن در آزمون‌های واحد سریع است. از این جهت که جدول __EFMigrationsHistory را تولید نمی‌کند (برخلاف متد Migrate). بنابراین بجز آزمون‌های واحد، در جای دیگری از آن استفاده نکنید چون به دلیل عدم تولید جدول __EFMigrationsHistory توسط آن، قابلیت استفاده‌ی از بانک اطلاعاتی تولید شده‌ی توسط آن با امکانات migrations وجود ندارد. در پایان آزمون واحد نیز می‌توان از متد EnsureDeleted برای حذف این بانک اطلاعاتی موقتی استفاده کرد.



در قسمت بعد، مطالب تکمیلی مهاجرت‌ها را بررسی خواهیم کرد. برای مثال چگونه می‌توان کلاس‌های موجودیت‌ها را به اسمبلی‌های دیگری منتقل کرد.
مطالب
ASP.NET MVC #20

تهیه گزارشات تحت وب به کمک WebGrid

WebGrid از ASP.NET MVC 3.0 به صورت توکار به شکل یک Html Helper در دسترس می‌باشد و هدف از آن ساده‌تر سازی تهیه گزارشات تحت وب است. البته این گرید، تنها گرید مهیای مخصوص ASP.NET MVC نیست و پروژه MVC Contrib یا شرکت Telerik نیز نمونه‌های دیگری را ارائه داده‌اند؛ اما از این جهت که این Html Helper، بدون نیاز به کتابخانه‌های جانبی در دسترس است، بررسی آن ضروری می‌باشد.


صورت مساله

لیستی از کارمندان به همراه حقوق ماهیانه آن‌ها در دست است. اکنون نیاز به گزارشی تحت وب، با مشخصات زیر می‌باشد:
1- گزارش باید دارای صفحه بندی بوده و هر صفحه تنها 10 ردیف را نمایش دهد.
2- سطرها باید یک در میان دارای رنگی متفاوت باشند.
3- ستون حقوق کارمندان در پایین هر صفحه، باید دارای جمع باشد.
4- بتوان با کلیک بر روی عنوان هر ستون، اطلاعات را بر اساس ستون انتخابی، مرتب ساخت.
5- لینک‌های حذف یا ویرایش یک ردیف نیز در این گزارش مهیا باشد.
6- لیست تهیه شده، دارای ستونی به نام «ردیف» نیست. این ستون را نیز به صورت خودکار اضافه کنید.
7- لیست نهایی اطلاعات، دارای ستونی به نام مالیات نیست. فقط حقوق کارمندان ذکر شده است. ستون محاسبه شده مالیات نیز باید به صورت خودکار در این گزارش نمایش داده شود. این ستون نیز باید دارای جمع پایین هر صفحه باشد.
8- تمام اعداد این گزارش در حین نمایش باید دارای جدا کننده سه رقمی باشند.
9- تاریخ‌های موجود در لیست، میلادی هستند. نیاز است این تاریخ‌ها در حین نمایش شمسی شوند.
10- انتهای هر صفحه گزارش باید بتوان برچسب «صفحه y/n» را مشاهده کرد. n در اینجا منظور تعداد کل صفحات است و y شماره صفحه جاری می‌باشد.
11- انتهای هر صفحه گزارش باید بتوان برچسب «رکوردهای y تا x از n» را مشاهده کرد. n در اینجا منظور تعداد کل رکوردها است.
12- نام کوچک هر کارمند، ضخیم نمایش داده شود.
13- به ازای هر شماره کارمندی، یک تصویر در پوشه images سایت وجود دارد. برای مثال images/id.jpg. ستونی برای نمایش تصویر متناظر با هر کارمند نیز باید اضافه شود.
14- به ازای هر کارمند، تعدادی پروژه هم وجود دارد. پروژه‌های متناظر را توسط یک گرید تو در تو نمایش دهید.


راه حل به کمک استفاده از WebGrid

ابتدا یک پروژه خالی ASP.NET MVC را آغاز کنید. سپس مدل‌های زیر را به آن اضافه نمائید (یک کارمند که می‌تواند تعداد پروژه منتسب داشته باشد):

using System;
using System.Collections.Generic;

namespace MvcApplication17.Models
{
public class Employee
{
public int Id { set; get; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime AddDate { get; set; }
public double Salary { get; set; }
public IList<Project> Projects { get; set; }
}
}

namespace MvcApplication17.Models
{
public class Project
{
public int Id { set; get; }
public string Name { set; get; }
}
}

سپس منبع داده نمونه زیر را به پروژه اضافه کنید. به عمد از ORM‌ خاصی استفاده نشده تا بتوانید پروژه جاری را به سادگی در یک پروژه آزمایشی جدید،‌ تکرار کنید.
using System;
using System.Collections.Generic;

namespace MvcApplication17.Models
{
public static class EmployeeDataSource
{
public static IList<Employee> CreateEmployees()
{
var list = new List<Employee>();
var rnd = new Random();
for (int i = 1; i <= 1000; i++)
{
list.Add(new Employee
{
Id = i + 1000,
FirstName = "fName " + i,
LastName = "lName " + i,
AddDate = DateTime.Now.AddYears(-rnd.Next(1, 10)),
Salary = rnd.Next(400, 3000),
Projects = CreateRandomProjects()
});
}
return list;
}

private static IList<Project> CreateRandomProjects()
{
var list = new List<Project>();
var rnd = new Random();
for (int i = 0; i < rnd.Next(1, 7); i++)
{
list.Add(new Project
{
Id = i,
Name = "Project " + i
});
}
return list;
}
}
}


در ادامه یک کنترلر جدید را با محتوای زیر اضافه نمائید:
using System.Web.Mvc;
using MvcApplication17.Models;

namespace MvcApplication17.Controllers
{
public class HomeController : Controller
{
[HttpPost]
public ActionResult Delete(int? id)
{
return RedirectToAction("Index");
}

[HttpGet]
public ActionResult Edit(int? id)
{
return View();
}

[HttpGet]
public ActionResult Index(string sort, string sortdir, int? page = 1)
{
var list = EmployeeDataSource.CreateEmployees();
return View(list);
}
}
}

علت تعریف متد index با پارامترهای sort و غیره به URLهای خودکاری از نوع زیر بر می‌گردد:

http://localhost:3034/?sort=LastName&sortdir=ASC&page=3

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

سپس یک View خالی را نیز برای متد Index ایجاد کنید. تا اینجا تنظیمات اولیه پروژه انجام شد.
کدهای کامل View را در ادامه ملاحظه می‌کنید:

@using System.Globalization
@model IList<MvcApplication17.Models.Employee>

@{
ViewBag.Title = "Index";
}

@helper WebGridPageFirstItem(WebGrid grid)
{
@(((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1));
}

@helper WebGridPageLastItem(WebGrid grid)
{
if (grid.TotalRowCount < (grid.PageIndex + 1 * grid.RowsPerPage))
{
@grid.TotalRowCount;
}
else
{
@((grid.PageIndex + 1) * grid.RowsPerPage);
}
}

<h2>Employees List</h2>

@{
var grid = new WebGrid(
source: Model,
canPage: true,
rowsPerPage: 10,
canSort: true,
defaultSort: "FirstName"
);
var salaryPageSum = 0;
var taxPageSum = 0;
var rowIndex = ((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1);
}

<div id="container">
@grid.GetHtml(
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style",
htmlAttributes: new { id = "MyGrid" },
mode: WebGridPagerModes.All,
columns: grid.Columns(
grid.Column(header: "#",
style: "text-align-center-col",
format: @<text>@(rowIndex++)</text>),
grid.Column(columnName: "FirstName", header: "First Name",
format: @<span style='font-weight: bold'>@item.FirstName</span>,
style: "text-align-center-col"),
grid.Column(columnName: "LastName", header: "Last Name"),
grid.Column(header: "Image",
style: "text-align-center-col",
format: @<text><img alt="@item.Id" src="@Url.Content("~/images/" + @item.Id + ".jpg")" /></text>),
grid.Column(columnName: "AddDate", header: "Start",
style: "text-align-center-col",
format: item =>
{
int ym = item.AddDate.Year;
int mm = item.AddDate.Month;
int dm = item.AddDate.Day;
var persianCalendar = new PersianCalendar();
int ys = persianCalendar.GetYear(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ms = persianCalendar.GetMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ds = persianCalendar.GetDayOfMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
return ys + "/" + ms.ToString("00") + "/" + ds.ToString("00");

}),
grid.Column(columnName: "Salary", header: "Salary",
format: item =>
{
salaryPageSum += item.Salary;
return string.Format("${0:n0}", item.Salary);
},
style: "text-align-center-col"),
grid.Column(header: "Tax", canSort: true,
format: item =>
{
var tax = item.Salary * 0.2;
taxPageSum += tax;
return string.Format("${0:n0}", tax);
}),
grid.Column(header: "Projects", columnName: "Projects",
style: "text-align-center-col",
format: item =>
{
var subGrid = new WebGrid(
source: item.Projects,
canPage: false,
canSort: false
);
return subGrid.GetHtml(
htmlAttributes: new { id = "MySubGrid" },
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style"
);
}),
grid.Column(header: "",
style: "text-align-center-col",
format: item => @Html.ActionLink(linkText: "Edit", actionName: "Edit",
controllerName: "Home", routeValues: new { id = item.Id },
htmlAttributes: null)),
grid.Column(header: "",
format: @<form action="/Home/Delete/@item.Id" method="post"><input type="submit"
onclick="return confirm('Do you want to delete this record?');"
value="Delete"/></form>),
grid.Column(header: "", format: item => item.GetSelectLink("Select"))
)
)

<strong>Page:</strong> @(grid.PageIndex + 1) / @grid.PageCount,
<strong>Records:</strong> @WebGridPageFirstItem(@grid) - @WebGridPageLastItem(@grid) of @grid.TotalRowCount

@*
@if (@grid.HasSelection)
{
@RenderPage("~/views/path/_partial_view.cshtml", new { Employee = grid.SelectedRow })
}
*@
</div>

@section script{
<script type="text/javascript">
$(function () {
$('#MyGrid tbody:first').append(
'<tr class="total-row"><td></td>\
<td></td><td></td><td></td>\
<td><strong>Total:</strong></td>\
<td>@string.Format("${0:n0}", @salaryPageSum)</td>\
<td>@string.Format("${0:n0}", @taxPageSum)</td>\
<td></td><td></td><td></td></tr>');
});
</script>
}


توضیحات ریز جزئیات View فوق


تعریف ابتدایی شیء WebGrid و مقدار دهی آن
در ابتدا نیاز است یک وهله از شیء WebGrid را ایجاد کنیم. در اینجا می‌توان تنظیم کرد که آیا نیاز است اطلاعات نمایش داده شده دارای صفحه بندی (canPage) خودکار باشند؟ منبع داده (source) کدام است. در صورت فعال سازی صفحه بندی خودکار، چه تعداد ردیف (rowsPerPage) در هر صفحه نمایش داده شود. آیا نیاز است بتوان با کلیک بر روی سر ستون‌ها، اطلاعات را بر اساس فیلد متناظر با آن مرتب (canSort) ساخت؟ همچنین در صورت نیاز به مرتب سازی، اولین باری که گرید نمایش داده می‌شود، بر اساس چه فیلدی (defaultSort) باید مرتب شده نمایش داده شود:

@{ 
var grid = new WebGrid(
source: Model,
canPage: true,
rowsPerPage: 10,
canSort: true,
defaultSort: "FirstName"
);
var salaryPageSum = 0;
var taxPageSum = 0;
var rowIndex = ((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1);
}

در اینجا همچنین سه متغیر کمکی هم تعریف شده که از این‌ها برای تهیه جمع ستون‌های حقوق و مالیات و همچنین نمایش شماره ردیف جاری استفاده می‌شود. فرمول نحوه محاسبه اولین ردیف هر صفحه را هم ملاحظه می‌کنید. شماره ردیف‌های بعدی، rowIndex++ خواهند بود.


تعریف رنگ و لعاب گرید نمایش داده شده
در ادامه به کمک متد grid.GetHtml، رشته‌ای معادل اطلاعات HTML صفحه جاری، بازگشت داده می‌شود. در اینجا می‌توان یک سری خواص تکمیلی را تنظیم نمود. برای مثال:
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style",
htmlAttributes: new { id = "MyGrid" },

هر کدام از این رشته‌ها در حین رندر نهایی گرید،‌ تبدیل به یک class خواهند شد. برای نمونه:

<div id="container">
<table class="webgrid" id="MyGrid">
<thead>
<tr class="webgrid-header">

به این ترتیب با اندکی ویرایش css سایت، می‌توان انواع و اقسام رنگ‌ها را به سطرها و ستون‌های گرید نهایی اعمال کرد. برای مثال اطلاعات زیر را به فایل css سایت اضافه نمائید:

/* Styles for WebGrid
-----------------------------------------------------------*/
.webgrid
{
width: 100%;
margin: 0px;
padding: 0px;
border: 0px;
border-collapse: collapse;
font-family: Tahoma;
font-size: 9pt;
}

.webgrid a
{
color: #000;
}

.webgrid-header
{
padding: 0px 5px;
text-align: center;
border-bottom: 2px solid #739ace;
height: 20px;
border-top: 2px solid #D6E8FF;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}

.webgrid-header th
{
background-color: #eaf0ff;
border-right: 1px solid #ddd;
}

.webgrid-footer
{
padding: 6px 5px;
text-align: center;
background-color: #e8eef4;
border-top: 2px solid #3966A2;
height: 25px;
border-bottom: 2px solid #D6E8FF;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}

.webgrid-alternating-row
{
height: 22px;
background-color: #f2f2f2;
border-bottom: 1px solid #d2d2d2;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}

.webgrid-row-style
{
height: 22px;
border-bottom: 1px solid #d2d2d2;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}

.webgrid-selected-row
{
font-weight: bold;
}

.text-align-center-col
{
text-align: center;
}

.total-row
{
background-color:#f9eef4;
}

همانطور که ملاحظه می‌کنید، رنگ‌های ردیف‌ها، هدر و فوتر گرید و غیره در اینجا تنظیم می‌شوند.
به علاوه اگر دقت کرده باشید در تعاریف گرید، htmlAttributes هم مقدار دهی شده است. در اینجا به کمک یک anonymously typed object، مقدار id گرید مشخص شده است. از این id در حین کار با jQuery‌ استفاده خواهیم کرد.


تعیین نوع Pager
پارامتر دیگری که در متد grid.GetHtml تنظیم شده است، mode: WebGridPagerModes.All می‌باشد. WebGridPagerModes یک enum با محتوای زیر است و توسط آن می‌توان نوع Pager گرید را تعیین کرد:

[Flags]
public enum WebGridPagerModes
{
Numeric = 1,
//
NextPrevious = 2,
//
FirstLast = 4,
//
All = 7,
}

نحوه تعریف ستون‌های گرید
اکنون به مهم‌ترین قسمت تهیه گزارش رسیده‌ایم. در اینجا با مقدار دهی پارامتر columns، نحوه نمایش اطلاعات ستون‌های مختلف مشخص می‌گردد. مقداری که باید در اینجا تنظیم شود، آرایه‌ای از نوع WebGridColumn می‌باشد و مرسوم است به کمک متد کمکی grid.Columns،‌ اینکار را انجام داد.
متد کمکی grid.Column، یک وهله از شیء WebGridColumn را بر می‌گرداند و از آن برای تعریف هر ستون استفاده خواهیم کرد. توسط پارامتر columnName آن،‌ نام فیلدی که باید اطلاعات ستون جاری از آن اخذ شود مشخص می‌شود. به کمک پارامتر header،‌ عبارت سرستون متناظر تنظیم می‌گردد. پارامتر format، مهم‌ترین و توانمندترین پارامتر متد grid.Column است:

grid.Column(columnName: "FirstName", header: "First Name",
format: @<span style='font-weight: bold'>@item.FirstName</span>,
style: "text-align-center-col"),
grid.Column(columnName: "LastName", header: "Last Name"),

پارامتر format، به نحو زیر تعریف شده است:

Func<dynamic, object> format

به این معنا که هر بار پیش از رندر سطر جاری، زمانیکه قرار است سلولی رندر شود، یک شیء dynamic در اختیار شما قرار می‌گیرد. این شیء dynamic یک رکورد از اطلاعات Model جاری است. به این ترتیب به اطلاعات تمام سلول‌های ردیف جاری دسترسی خواهیم داشت. بر این اساس هر نوع پردازشی را که لازم بود، انجام دهید (شبیه به فرمول نویسی در ابزارهای گزارش سازی، اما اینبار با کدهای سی شارپ) و مقدار فرمت شده نهایی را به صورت یک رشته بر گردانید. این رشته نهایتا در سلول جاری درج خواهد شد.
اگر از پارامتر فرمت استفاده نشود، همان مقدار فیلد جاری بدون تغییری رندر می‌گردد.
حداقل به دو نحو می‌توان پارامتر فرمت را مقدار دهی کرد:

format: @<span style='font-weight: bold'>@item.FirstName</span>
or
format: item =>
{
salaryPageSum += item.Salary;
return string.Format("${0:n0}", item.Salary);
}

مستقیما از توانمندی‌های Razor استفاده کنید. مثلا یک تگ کامل را بدون نیاز به محصور سازی آن بین "" شروع کنید. سپس @item به وهله‌ای از رکورد در دسترس اشاره می‌کند که در اینجا وهله‌ای از شیء کارمند است.
و یا همانند روشی که برای محاسبه جمع حقوق هر صفحه مشاهده می‌کنید، مستقیما از lambda expressions برای تعریف یک anonymous delegate استفاده کنید.


نحوه اضافه کردن ستون ردیف
ستون ردیف، یک ستون محاسبه شده (calculated field) است:

grid.Column(header: "#",
style: "text-align-center-col",
format: @<text>@(rowIndex++)</text>),

نیازی نیست حتما یک grid.Column، به فیلدی در کلاس کارمند اشاره کند. مقدار سفارشی آن را به کمک پارامتر format تعیین خواهیم کرد. هر بار که قرار است یک ردیف رندر شود، یکبار این پارامتر فراخوانی خواهد شد. فرمول محاسبه rowIndex ابتدای صفحه را نیز پیشتر ملاحظه نمودید.


نحوه اضافه کردن ستون سفارشی تصاویر کارمندها
ستون تصویر کارمندها نیز مستقیما در کلاس کارمند تعریف نشده است. بنابراین می‌توان آن‌را با مقدار دهی صحیح پارامتر format ایجاد کرد:

grid.Column(header: "Image",
style: "text-align-center-col",
format: @<text><img alt="@item.Id" src="@Url.Content("~/images/" + @item.Id + ".jpg")" /></text>),


در این مثال، تصاویر کارمندها در پوشه images واقع در ریشه سایت، قرار دارند. به همین جهت از متد Url.Content برای مقدار دهی صحیح آن استفاده کردیم. به علاوه در اینجا @item.Id به Id رکورد در حال رندر اشاره می‌کند.


نحوه تبدیل تاریخ‌ها به تاریخ شمسی
در ادامه بازهم به کمک پارامتر format، یک وهله از شیء dynamic اشاره کننده به رکورد در حال رندر را دریافت می‌کنیم. سپس فرصت خواهیم داشت تا بر این اساس، فرمول نویسی کنیم. دست آخر هم رشته مورد نظر نهایی را بازگشت می‌دهیم:

grid.Column(columnName: "AddDate", header: "Start",
style: "text-align-center-col",
format: item =>
{
int ym = item.AddDate.Year;
int mm = item.AddDate.Month;
int dm = item.AddDate.Day;
var persianCalendar = new PersianCalendar();
int ys = persianCalendar.GetYear(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ms = persianCalendar.GetMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ds = persianCalendar.GetDayOfMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
return ys + "/" + ms.ToString("00") + "/" + ds.ToString("00");
}),


اضافه کردن ستون سفارشی مالیات
در کلاس کارمند، خاصیت حقوق وجود دارد اما مالیات خیر. با توجه به آن می‌توانیم به کمک پارامتر format، به اطلاعات شیء dynamic در حال رندر دسترسی داشته باشیم. بنابراین به اطلاعات حقوق دسترسی داریم و سپس با کمی فرمول نویسی، مقدار نهایی مورد نظر را بازگشت خواهیم داد. همچنین در اینجا می‌توان نحوه بازگشت مقدار حقوق را به صورت رشته‌ای حاوی جدا کننده‌های سه رقمی نیز مشاهده کرد:

grid.Column(columnName: "Salary", header: "Salary",
format: item =>
{
salaryPageSum += item.Salary;
return string.Format("${0:n0}", item.Salary);
},
style: "text-align-center-col"),
grid.Column(header: "Tax", canSort: true,
format: item =>
{
var tax = item.Salary * 0.2;
taxPageSum += tax;
return string.Format("${0:n0}", tax);
}),


اضافه کردن گردید‌های تو در تو
متد Grid.GetHtml، یک رشته را بر می‌گرداند. بنابراین در هر چند سطح که نیاز باشد می‌توان یک گرید را بر اساس اطلاعات دردسترس رندر کرد و سپس بازگشت داد:

grid.Column(header: "Projects", columnName: "Projects",
style: "text-align-center-col",
format: item =>
{
var subGrid = new WebGrid(
source: item.Projects,
canPage: false,
canSort: false
);
return subGrid.GetHtml(
htmlAttributes: new { id = "MySubGrid" },
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style"
);
}),


در اینجا کار اصلی از طریق پارامتر format شروع می‌شود. سپس به کمک item.Projects به لیست پروژه‌های هر کارمند دسترسی خواهیم داشت. بر این اساس یک گرید جدید را تولید کرد و سپس رشته معادل با آن را به کمک متد subGrid.GetHtml دریافت و بازگشت می‌دهیم. این رشته در سلول جاری درج خواهد شد. به نوعی یک گزارش master detail یا sub report را تولید کرده‌ایم.


اضافه کردن دکمه‌های ویرایش، حذف و انتخاب
هر سه دکمه ویرایش، حذف و انتخاب در ستون‌هایی سفارشی قرار خواهند گرفت. بنابراین مقدار دهی header و format متد grid.Column کفایت می‌کند:

grid.Column(header: "",
style: "text-align-center-col",
format: item => @Html.ActionLink(linkText: "Edit", actionName: "Edit",
controllerName: "Home", routeValues: new { id = item.Id },
htmlAttributes: null)),
grid.Column(header: "",
format: @<form action="/Home/Delete/@item.Id" method="post"><input type="submit"
onclick="return confirm('Do you want to delete this record?');"
value="Delete"/></form>),
grid.Column(header: "", format: item => item.GetSelectLink("Select"))


نکته جدیدی که در اینجا وجود دارد متد item.GetSelectLink می‌باشد. این متد جزو متدهای توکار گرید است و کار آن بازگشت دادن شیء grid.SelectedRow می‌باشد. این شیء پویا، حاوی اطلاعات رکورد انتخاب شده است. برای مثال اگر نیاز باشد این اطلاعات به صفحه‌ای ارسال شود، می‌توان از روش زیر استفاده کرد:

@if (@grid.HasSelection)
{
@RenderPage("~/views/path/_partial_view.cshtml", new { Employee = grid.SelectedRow })
}


نمایش برچسب‌های صفحه x از n و رکوردهای x تا y از z
در یک گزارش خوب باید مشخص باشد که صفحه جاری، کدامین صفحه از چه تعداد صفحه کلی است. یا رکوردهای صفحه جاری چه بازه‌ای از تعداد رکوردهای کلی را تشکیل می‌دهند. برای این منظور چند متد کمکی به نام‌های WebGridPageFirstItem و WebGridPageLastItem تهیه شده‌اند که آن‌ها را در ابتدای View ارائه شده، مشاهده نمودید:

<strong>Page:</strong> @(grid.PageIndex + 1) / @grid.PageCount, 
<strong>Records:</strong> @WebGridPageFirstItem(@grid) - @WebGridPageLastItem(@grid) of @grid.TotalRowCount

نمایش جمع ستون‌های حقوق و مالیات در هر صفحه
گرید توکار همراه با ASP.NET MVC در این مورد راه حلی را ارائه نمی‌دهد. بنابراین باید اندکی دست به ابتکار زد. مثلا:

@section script{
<script type="text/javascript">
$(function () {
$('#MyGrid tbody:first').append(
'<tr class="total-row"><td></td>\
<td></td><td></td><td></td>\
<td><strong>Total:</strong></td>\
<td>@string.Format("${0:n0}", @salaryPageSum)</td>\
<td>@string.Format("${0:n0}", @taxPageSum)</td>\
<td></td><td></td><td></td></tr>');
});
</script>
}

در این مثال به کمک jQuery با توجه به اینکه id گرید ما MyGrid است، یک ردیف سفارشی که همان جمع محاسبه شده است، به tbody جدول نهایی تولیدی اضافه می‌شود. از tbody:first هم در اینجا استفاده شده است تا ردیف اضافه شده به گریدهای تو در تو اعمال نشود.
سپس فایل Views\Shared\_Layout.cshtml را گشوده و از section تعریف شده، برای مقدار دهی master page سایت، استفاده نمائید:

<head>
<title>@ViewBag.Title</title>
<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
@RenderSection("script", required: false)
</head>

مطالب
Highlight کردن لینک صفحه جاری در ASP.NET MVC
راه حل‌های مختلفی جهت Highlight کردن لینک صفحه جاری وجود دارد و مهم‌ترین کاربرد آن در منوی اصلی سایت است.

در این مطلب سعی داریم با ارائه یک Helper راه حل مناسبی را برای این موضوع ارائه کنیم. مسئولیت این Helper ایجاد لینک است با در نظر گرفتن یک شرط: آیا لینک ایجاد شده به Action جاری اشاره دارد؟ اگر بله یک CSS Class با عنوان currentMenuItem به آن اضافه کن.

public static MvcHtmlString MenuLink(this HtmlHelper helper, string text, string action, string controller)
{            
    var routeData = helper.ViewContext.RouteData.Values;
    var currentController = routeData["controller"];
    var currentAction = routeData["action"];             
 
    if(String.Equals(action, currentAction as string,
              StringComparison.OrdinalIgnoreCase)
        &&
       String.Equals(controller, currentController as string,
               StringComparison.OrdinalIgnoreCase))
        
    {
       return helper.ActionLink(
           text,action, controller, null,
           new { @class="currentMenuItem"}
           );
    }
    return helper.ActionLink(text, action, controller);
}
نحوه استفاده:
<li>@Html.MenuLink("Contact", "Contact", "Home")</li>
و البته کمی تغییرات در فایل CSS خود را فراموش نکنید:
ul#menu li a {
    background: none;
    color: #999;
    padding: 5px;
    border-radius: 15px;
    text-decoration: none;
}
 
ul#menu li a.currentMenuItem {
    background-color: black;   
    color: white;
}
همچنین دوستانی که از Bootstrap و البته Navbar آن استفاده می‌کنند می‌توانند با کمی تغییرات در این Helper استفاده بهینه ای از آن داشته باشند:
public static MvcHtmlString MenuLinkBootstrap(this HtmlHelper helper, string text, string action, string controller)
{

    var routeData = helper.ViewContext.RouteData.Values;
    var currentController = routeData["controller"];
    var currentAction = routeData["action"];

   if (String.Equals(action, currentAction as string, StringComparison.OrdinalIgnoreCase) && String.Equals(controller, currentController as string, StringComparison.OrdinalIgnoreCase))
   {
        return new MvcHtmlString("<li class=\"active\">" + helper.ActionLink(text, action, controller) + "</li>");
   }
   
    return new MvcHtmlString("<li>" + helper.ActionLink(text, action, controller) + "</li>");

}

نظرات مطالب
رمزنگاری کانکشن استرینگ در ASP.Net
@ نیما
- در وب برای اینکه این کاراکترهای عجیب و غریب مشکل ساز نشوند یکبار دیگر هم اطلاعات رمزنگاری شده را از فیلتر base64 encoding عبور می‌دهند. به این صورت مشکلی برای نگهداری آن‌ها در فایل‌ها وجود نخواهد داشت.
- نگهداری اطلاعات حساس در حافظه به صورت plain کار اشتباهی است چون دامپ حافظه ویندوز و یا تمام سیستم‌ عامل‌های دیگر کار ساده‌ای است. برنامه‌های زیادی هستند که پروسس‌های ویندوز را لیست می‌کنند و به شما اجازه می‌دهند حافظه آن‌ها را دامپ کنید (به راحتی چند کلیک). استخراج اطلاعات حساس هم از یک فایل دامپ تر و تازه زیاد مشکل نیست.
- سرعت الگوریتم‌های رمزنگاری واقعا بالا است. پیاده سازی‌های خیلی خوبی هم دارند. بنابراین زیاد نگران این سربار نباشید. چون در حد یک رشته ساده و امثال آن اصلا سرباری به حساب نمی‌آیند و بسیار سریع عمل می‌کنند.