مطالب
شروع به کار با 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 برای حذف این بانک اطلاعاتی موقتی استفاده کرد.



در قسمت بعد، مطالب تکمیلی مهاجرت‌ها را بررسی خواهیم کرد. برای مثال چگونه می‌توان کلاس‌های موجودیت‌ها را به اسمبلی‌های دیگری منتقل کرد.
مسیرراه‌ها
WPF
          مطالب
          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>

          اشتراک‌ها
          دوره 8 ساعته ASP.NET Core Razor Pages

          Web Development with ASP.NET Core Razor Pages || FULL COURSE || Trevoir Williams

          00:00 Introduction
          1:00 Lesson 1: Setting Up
          4:36 Lesson 2: Folder Tour
          23:33 Lesson 3: Understanding Razor Syntax
          35:52 Lesson 4: Message From Settings
          43:44 Lesson 5: Adding Entity Framework Core
          55:48 Lesson 6: Connect to Database with Entity Framework Core
          1:10:20 Lesson 7: Scaffolding Database Tables as Classes
          1:23:26 Lesson 8: GitHub Commit 1
          1:32:50 Lesson 9: Create Page
          1:54:32 Lesson 10: Update Page
          2:18:25 Lesson 11: Details Page
          2:23:56 Lesson 12: Delete Page
          2:46:26 Lesson 13: UI Enhancements
          3:19:38 Lesson 14: Check-In Module
          3:21:04 Lesson 15: Added New Table
          3:43:19 Lesson 16: Enhance Forms
          4:07:40 Lesson 17: Further Form Enhancements
          4:20:00 Lesson 18: Adding More Requirements
          4:46:35 Lesson 19: Adding More Requirements Continued
          5:05:23 Lesson 20: Add Cascading Dropdown
          5:29:26 Lesson 21: Finish Cascading Dropdown
          5:45:59 Lesson 22: Cleanup Labels
          5:52:42 Lesson 23: Finish Interface Cleanup
          6:23:57 Lesson 24: Setup Repositories
          6:49:54 Lesson 25: Add First Repository Code
          7:06:41 Lesson 26: Refactoring Pages
          7:21:02 Lesson 27: Complete Repositories
          7:42:27 Lesson 28: Section Conclusion
          7:52:14 Lesson 29: User Authentication Setup
          8:03:08 Lesson 30: Extend Users Table
          8:08:01 Lesson 31: Setup Registration Page
          8:28:27 Lesson 32: Setup Login
          8:38:12 Lesson 33: Setup Authorization
          8:47:47 Lesson 34: Add Authorization

          دوره 8 ساعته ASP.NET Core Razor Pages