مطالب
به روز رسانی اطلاعات Master-Detail یا Master-Detail-DetailOfDetail با استفاده از EF Core

یکی از چالش‌هایی که در طراحی زیرساخت برای Domain هایی که تعداد زیادی عملیات CRUD را در back office سیستم خود دارند، داشتن مکانیزمی برای ذخیره سازی اطلاعات Master-Detail یا چه بسا Master-Detail-DetailOfDetail می‌باشد. در ادامه نحوه برخورد با چنین سناریوهایی را در EF Core و همچنین با استفاده از AutoMapper و FluentValidation بررسی خواهیم کرد.


موجودیت‌های فرضی

public abstract class Entity : IHaveTrackingState
{
    public long Id { get; set; }
    [NotMapped] public TrackingState TrackingState { get; set; }
}

public class Master : Entity
{
    public string Title { get; set; }
    public ICollection<Detail> Details { get; set; }
}

public class Detail : Entity
{
    public string Title { get; set; }

    public ICollection<DetailOfDetail> Details { get; set; }
    public Master Master { get; set; }
    public long MasterId { get; set; }
}

public class DetailOfDetail : Entity
{
    public string Title { get; set; }
    public Detail Detail { get; set; }
    public long DetailId { get; set; }
}

DbContext برنامه

public class ProjectDbContext : DbContext
{
    public DbSet<Master> Masters { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);

        optionsBuilder.UseInMemoryDatabase("SharedDatabaseName");
    }
}

واسط IHaveTrackingState
public interface IHaveTrackingState
{
    TrackingState TrackingState { get; set; }
    //ICollection<string> ModifiedProperties { get; set; }
}

public enum TrackingState
{
    Unchanged = 0,
    Added = 1,
    Modified = 2,
    Deleted = 3
}

با استفاده از پراپرتی TrackingState بالا، امکان مشخص کردن صریح State رکورد ارسالی توسط کلاینت مهیا می‌شود. قبلا نیز مطلبی در راستای STE یا همان Self-Tracking Entity تهیه شده است؛ و همچنین نظرات ارسالی این مطلب نیز می‌تواند مفید واقع شود. 


DTO‌های متناظر با موجودیت‌های فرضی

public abstract class Model : IHaveTrackingState
{
    public long Id { get; set; }
    public TrackingState TrackingState { get; set; }
}

public class MasterModel : Model
{
    public string Title { get; set; }
    public ICollection<DetailModel> Details { get; set; }
}

public class DetailModel : Model
{
    public string Title { get; set; }
    public ICollection<DetailOfDetailModel> Details { get; set; }
}

public class DetailOfDetailModel : Model
{
    public string Title { get; set; }
}

تنظیمات نگاشت موجودیت‌ها و DTOها
Mapper.Initialize(expression =>
{
    expression.CreateMap<MasterModel, Master>(MemberList.None).ReverseMap();
    expression.CreateMap<DetailModel, Detail>(MemberList.None).ReverseMap();
    expression.CreateMap<DetailOfDetailModel, DetailOfDetail>(MemberList.None).ReverseMap();
});

البته بهتر است این تنظیمات در درون Profile‌های مرتبط با AutoMapper کپسوله شوند و در زمان مورد نیاز نیز برای انجام نگاشت‌ها، واسط IMapper تزریق شده و استفاده شود.


تهیه داده ارسالی فرضی توسط کلاینت

var masterModel = new MasterModel
    {
        Title = "Master-Title",
        TrackingState = TrackingState.Added,
        Details = new List<DetailModel>
        {
            new DetailModel
            {
                Title = "Detail-Title",
                TrackingState = TrackingState.Added,
                Details = new List<DetailOfDetailModel>
                {
                    new DetailOfDetailModel
                    {
                        Title = "DetailOfDetail-Title",
                        TrackingState = TrackingState.Added,
                    }
                }
            }
        }
    };

ذخیره سازی اطلاعات

در EF Core، متد جدید context.ChangeTracker.TrackGraph برای به روز رسانی وضعیت یک گراف از اشیاء مشابه به اطلاعات ارسالی ذکر شده در بالا، اضافه شده است. این مکانیزم مفهوم کاملا جدیدی در EF Core می‌باشد که امکان کنترل نهایی برروی اشیایی را که قرار است توسط Context ردیابی شوند، مهیا می‌کند. با پیمایش یک گراف، امکان اجرای عملیات مورد نظر شما را برروی تک تک اشیاء، مهیا می‌سازد. 

using (var context = new ProjectDbContext())
{
    Console.WriteLine("################ Create Master and Details and DetailsOfDetail ##################");
    Print(masterModel);

    var masterEntity = Mapper.Map<Master>(masterModel);

    context.ChangeTracker.TrackGraph(
        masterEntity,
        n =>
        {
            var entity = (IHaveTrackingState) n.Entry.Entity;
            n.Entry.State = entity.TrackingState.ToEntityState();
        });

    context.SaveChanges();
}

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

public static class TrackingStateExtensions
{
    public static EntityState ToEntityState(this TrackingState trackingState)
    {
        switch (trackingState)
        {
            case TrackingState.Added:
                return EntityState.Added;

            case TrackingState.Modified:
                return EntityState.Modified;

            case TrackingState.Deleted:
                return EntityState.Deleted;

            case TrackingState.Unchanged:
                return EntityState.Unchanged;

            default:
                return EntityState.Unchanged;
        }
    }

    public static TrackingState ToTrackingState(this EntityState state)
    {
        switch (state)
        {
            case EntityState.Added:
                return TrackingState.Added;

            case EntityState.Modified:
                return TrackingState.Modified;

            case EntityState.Deleted:
                return TrackingState.Deleted;

            case EntityState.Unchanged:
                return TrackingState.Unchanged;

            default:
                return TrackingState.Unchanged;
        }
    }
}

شبیه سازی عملیات ویرایش
//GetForEditAsync
var masterModel = context.Masters
    .ProjectTo<MasterModel>()
    .AsNoTracking().Single(a => a.Id == 1);

//Client
var detail1 = masterModel.Details.First();
detail1.Title = "Details-EditedTitle";
detail1.TrackingState = TrackingState.Modified;

foreach (var detail in detail1.Details)
{
    detail.TrackingState = TrackingState.Deleted;
    //detail.Title = "DetailOfDetails-EditedTitle";
}

متدی تحت عنوان GetForEditAsync که یک MasterModel را بازگشت می‌دهد، در نظر بگیرید؛ کلاینت از طریق API، این Object Graph را دریافت می‌کند و تغییرات خود را اعمال کرده و همانطور که مشخص می‌باشد به دلیل اینکه تنظیمات نگاشت بین Detail و DetailModel در ابتدای بحث نیز انجام شده است، این بار دیگر نیاز به استفاده از متد Include نمی‌باشد و این عملیات توسط متد ProjectTo خودکار می‌باشد. در نهایت داده ارسالی توسط کلاینت را دریافت کرده و به شکل زیر عملیات به روز رسانی انجام می‌شود:

using (var context = new ProjectDbContext())
{
    Console.WriteLine(
        "################ Unchanged Master and Modified Details and Deleted DetailsOfDetail ##################");
    Print(masterModel);

    var masterEntity = Mapper.Map<Master>(masterModel);

    context.ChangeTracker.TrackGraph(
        masterEntity,
        n =>
        {
            var entity = (IHaveTrackingState) n.Entry.Entity;
            n.Entry.State = entity.TrackingState.ToEntityState();
        });

    context.SaveChanges();
}

با خروجی زیر:

برای بحث اعتبارسنجی هم می‌توان به شکل زیر عمل کرد:

public class MasterValidator : AbstractValidator<MasterModel>
{
    public MasterValidator()
    {
        RuleFor(a => a.Title).NotEmpty();
        RuleForEach(a => a.Details).SetValidator(new DetailValidator());
    }
}

public class DetailValidator : AbstractValidator<DetailModel>
{
    public DetailValidator()
    {
        RuleFor(a => a.Title).NotEmpty();
        RuleForEach(a => a.Details).SetValidator(new DetailOfDetailValidator());
    }
}

public class DetailOfDetailValidator : AbstractValidator<DetailOfDetailModel>
{
    public DetailOfDetailValidator()
    {
        RuleFor(a => a.Title).NotEmpty();
    }
}

با استفاده از متد RuleForEach و SetValidator موجود در کتابخانه FluentValidation، امکان مشخص کردن اعتبارسنج برای Detail موجود در شیء Master را خواهیم داشت.

همچنین با توجه به این که برای عملیات Create و Edit از یک مدل (DTO) استفاده خواهیم کرد، شاید لازم باشد اعتبارسنجی خاصی را فقط در زمان ویرایش لازم داشته باشیم، که در این صورت می‌توان از امکانات RuleSet استفاده کنید. در مطلب «طراحی و پیاده سازی ServiceLayer به همراه خودکارسازی Business Validationها» با استفاده ValidateWithRuleAttribute امکان مشخص کردن RuleSet مورد نظر برای اعتبارسنجی ورودی متد سرویس نیز در نظر گرفته شده است.


منابع تکمیلی

کتابخانه کمکی

کدهای کامل مطلب جاری را  از اینجا می‌توانید دریافت کنید.
مطالب
تولید پویای ستون‌ها در PdfReport
همانطور که در نکته انتهای قسمت قبل «کار با بانک‌های اطلاعاتی مختلف در PdfReport» عنوان شد، ذکر قسمت MainTableColumns و تمام تعاریف مرتبط با آن در PdfReports اختیاری است. برای تهیه یک گزارش توسط PdfReport فقط کافی است تا منبع داده را جهت تولید ستون‌های گزارش مشخص کنید.
این مورد انعطاف پذیری زیادی را به همراه خواهد داشت؛ اما ... پس از مدتی این سؤالات مطرح می‌شوند: آیا می‌شود در این ستون‌های خودکار، فیلدهای DateTime، با تاریخ شمسی نمایش داده شوند؟ آیا امکانپذیر است که ستونهای عددی، جمع پایین صفحه داشته باشند؟ و مواردی از این دست که در مورد نحوه مدیریت این نوع ستون‌های خودکار در ادامه بحث خواهد شد.

ابتدا سورس کامل مثال جاری را در ادامه ملاحظه خواهید کرد. تقریبا همان مثال قسمت قبل است که تعاریف ستون‌های آن حذف شده است:
using System;
using PdfRpt;
using PdfRpt.Core.Contracts;
using PdfRpt.Core.Helper;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.AdHocColumns
{
    public class AdHocColumnsPdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.RightToLeft);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
            })
             .DefaultFonts(fonts =>
             {
                 fonts.Path(AppPath.ApplicationPath + "\\fonts\\irsans.ttf",
                            Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
             })
             .PagesFooter(footer =>
             {
                 footer.DefaultFooter(printDate: DateTime.Now.ToString("MM/dd/yyyy"));
             })
             .PagesHeader(header =>
             {
                 header.DefaultHeader(defaultHeader =>
                 {
                     defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                     defaultHeader.Message("گزارش جدید ما");
                 });
             })
             .MainTableTemplate(template =>
             {
                 template.BasicTemplate(BasicTemplate.SilverTemplate);
             })
             .MainTablePreferences(table =>
             {
                 table.ColumnsWidthsType(TableColumnWidthType.Relative);
             })
             .MainTableDataSource(dataSource =>
             {
                 dataSource.GenericDataReader(
                    providerName: "System.Data.SQLite",
                    connectionString: "Data Source=" + AppPath.ApplicationPath + "\\data\\blogs.sqlite",
                    sql: @"SELECT [url] as 'آدرس', [name] as 'نام', [NumberOfPosts] as 'تعداد مطالب', [AddDate] as 'تاریخ ارسال'
                           FROM [tblBlogs]
                           WHERE [NumberOfPosts]>=@p1",
                    parametersValues: new object[] { 10 }
                );
             })
             .MainTableSummarySettings(summary =>
             {
                 summary.OverallSummarySettings("جمع کل");
                 summary.PreviousPageSummarySettings("نقل از صفحه قبل");
                 summary.PageSummarySettings("جمع صفحه");
             })
             .MainTableAdHocColumnsConventions(adHocColumns =>
             {
                 //We want sum of the int columns
                 adHocColumns.AddTypeAggregateFunction(
                     typeof(Int64),
                     new AggregateProvider(AggregateFunction.Sum)
                     {
                         DisplayFormatFormula = obj => obj == null ? string.Empty : string.Format("{0:n0}", obj)
                     });

                 //We want to dispaly all of the dateTimes as ShamsiDateTime
                 adHocColumns.AddTypeDisplayFormatFormula(
                     typeof(DateTime),
                     data => { return PersianDate.ToPersianDateTime((DateTime)data); }
                 );
                 adHocColumns.ShowRowNumberColumn(true);
                 adHocColumns.RowNumberColumnCaption("ردیف");
             })
             .MainTableEvents(events =>
             {
                 events.DataSourceIsEmpty(message: "There is no data available to display.");
             })
             .Export(export =>
             {
                 export.ToExcel();
                 export.ToXml();
             })
             .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\AdHocColumnsSampleRpt.pdf"));
        }
    }
}

توضیحات:

- با توجه به اینکه تعاریف ستون‌ها را حذف کرده‌ایم و به این ترتیب ستون‌ها به صورت خودکار بر اساس فیلدهای معرفی شده در منبع داده تشکیل می‌شوند، نیاز است سر ستون‌ها را بتوانیم فارسی نمایش دهیم. به همین جهت اینبار کوئری SQL ما با استفاده از aliasها، نامی فارسی را جهت فیلدها تدارک دیده است:
SELECT [url] as 'آدرس', [name] as 'نام', [NumberOfPosts] as 'تعداد مطالب', [AddDate] as 'تاریخ ارسال'
FROM [tblBlogs]
WHERE [NumberOfPosts]>=@p1
- در مرحله بعد توسط متد MainTableAdHocColumnsConventions، یک سری روال را جهت پردازش و نمایش این ستون‌های پویا مشخص می‌کنیم. برای مثال علاقمندیم در این نوع گزارشات هم ستون خودکار ردیف ظاهر شود:
adHocColumns.ShowRowNumberColumn(true);
adHocColumns.RowNumberColumnCaption("ردیف");
همچنین هر ستونی که نوع داده‌اش DateTime بود، از طریق فرمولی که مشخص می‌کنیم، به صورت شمسی نمایش داده شود:
adHocColumns.AddTypeDisplayFormatFormula(
                     typeof(DateTime),
                     data => { return PersianDate.ToPersianDateTime((DateTime)data); }
                 );
به علاوه می‌خواهیم تمام ستون‌هایی از نوع Int64، دارای جمع پایین صفحه هم باشند:
adHocColumns.AddTypeAggregateFunction(
                     typeof(Int64),
                     new AggregateProvider(AggregateFunction.Sum)
                     {
                         DisplayFormatFormula = obj => obj == null ? string.Empty : string.Format("{0:n0}", obj)
                     });
نوع int در بانک اطلاعاتی SQLite معادل نوع Int64 در دات نت است.
مطالب
React 16x - قسمت 19 - کار با فرم‌ها - بخش 2 - اعتبارسنجی ورودی‌های کاربران
تمام فرم‌های تعریف شده، نیاز به اعتبارسنجی اطلاعات وارد شده‌ی توسط کاربران خود را دارند. ابتدا اعتبارسنجی اطلاعات را در حین ارسال فرم و سپس آن‌را همزمان با ورود اطلاعات، بررسی می‌کنیم.


اصول کلی طراحی یک اعتبارسنج ساده

در قسمت قبل، تمام اطلاعات فرم لاگین را درون شیء account خاصیت state قرار دادیم. در اینجا نیز شبیه به چنین شیءای را برای ذخیره سازی خطاهای اعتبارسنجی فیلدهای فرم، تعریف می‌کنیم:
class LoginForm extends Component {
  state = {
    account: { username: "", password: "" },
    errors: {
      username: "Username is required"
    }
  };
خاصیت errors تعریف شده، یک شیء را باز می‌گرداند که حاوی اطلاعات و خطاهای مرتبط با اعتبارسنجی فیلدهای مشکل دار است. بنابراین نام خواص این شیء، با نام فیلدهای فرم تطابق دارند. کار کردن با یک شیء هم جهت یافتن خطاهای یک فیلد مشخص، ساده‌تر است از کار کردن با یک آرایه؛ از این جهت که نیازی به جستجوی خاصی در این شیء نبوده و با استفاده از روش دسترسی پویای به خواص یک شیء جاوا اسکریپتی مانند errors["username"]، می‌توان خطاهای مرتبط با هر فیلد را به سادگی نمایش داد.
البته در ابتدای کار، خاصیت errors را با یک شیء خالی ({}) مقدار دهی می‌کنیم و سپس در متد مدیریت ارسال فرم به سرور:
  validate = () => {
    return { username: "Username is required." };
  };

  handleSubmit = e => {
    e.preventDefault();

    const errors = this.validate();
    console.log("Validation errors", errors);
    this.setState({ errors });
    if (errors) {
      return;
    }

    // call the server
    console.log("Submitted!");
  };
- ابتدا خروجی متد validate سفارشی را بررسی می‌کنیم که خروجی آن، خطاهای ممکن است.
- اگر خطایی وجود داشت، به مرحله‌ی بعد که ارسال فرم به سمت سرور می‌باشد، نخواهیم رسید و کار را با یک return، خاتمه می‌دهیم.
- علت فراخوانی متد setState در اینجا، درخواست رندر مجدد فرم، با توجه به خطاهای اعتبارسنجی ممکنی است که به خاصیت errors، اضافه یا به روز رسانی کرده‌ایم.
- نمونه‌ای از خروجی متد validate را نیز در اینجا مشاهده می‌کنید که تشکیل شده‌است از یک شیء، که هر خاصیت آن، به نام یک فیلد موجود در فرم، اشاره می‌کند.


پیاده سازی یک اعتبارسنج ساده

در اینجا یک نمونه پیاده سازی ساده و ابتدایی منطق اعتبارسنجی فیلدهای فرم را ملاحظه می‌کنید:
  validate = () => {
    const { account } = this.state;

    const errors = {};
    if (account.username.trim() === "") {
      errors.username = "Username is required.";
    }

    if (account.password.trim() === "") {
      errors.password = "Password is required.";
    }

    return Object.keys(errors).length === 0 ? null : errors;
  };
- ابتدا توسط Object Destructuring، خاصیت account شیء منتسب به خاصیت state کامپوننت را دریافت می‌کنیم، تا مدام نیاز به نوشتن this.state.account نباشد.
- سپس یک شیء خالی error را تعریف کرده‌ایم.
- در ادامه با توجه به اینکه مقادیر المان‌های فرم در state وجود دارند، خالی بودن آن‌ها را بررسی می‌کنیم. اگر خالی بودند، یک خاصیت جدید را با همان نام المان مورد بررسی، به شیء errors اضافه کرده و پیام خطایی را درج می‌کنیم.
- در نهایت این شیء errors و یا نال را (در صورت عدم وجود خطایی) بازگشت می‌دهیم.

برای آزمایش آن، پس از اجرای برنامه، یکبار بدون وارد کردن اطلاعاتی، بر روی دکمه‌ی Login کلیک کنید؛ یکبار هم با وارد کردن اطلاعاتی در فیلدهای مختلف. در این بین کنسول توسعه دهندگان مرورگر را نیز جهت مشاهده‌ی شیء‌های error لاگ شده، بررسی نمائید.



نمایش خطاهای اعتبارسنجی فیلدهای فرم

در قسمت قبل، کامپوننت جدید src\components\common\input.jsx را جهت کاهش کدهای تکراری تعاریف المان‌های ورودی، ایجاد کردیم. در اینجا نیز می‌توان کار نمایش خطاهای اعتبارسنجی را قرار داد:
import React from "react";

const Input = ({ name, label, value, error, onChange }) => {
  return (
    <div className="form-group">
      <label htmlFor={name}>{label}</label>
      <input
        value={value}
        onChange={onChange}
        id={name}
        name={name}
        type="text"
        className="form-control"
      />
      {error && <div className="alert alert-danger">{error}</div>}
    </div>
  );
};

export default Input;
- در اینجا ابتدا خاصیت error را به لیست خواص مورد انتظار از شیء props، اضافه کرده‌ایم.
- سپس با توجه به نکته‌ی «رندر شرطی عناصر در کامپوننت‌های React» در قسمت 5، اگر error مقداری داشته باشد و به true تفسیر شود، آنگاه به صورت خودکار، div ای که دارای کلاس‌های بوت استرپی اخطار است به همراه متن خطا، رندر خواهد شد؛ در غیراینصورت هیچ div ای به صفحه اضافه نمی‌شود.
- اکنون متد رندر کامپوننت فرم لاگین را به صورت زیر تکمیل می‌کنیم:
  render() {
    const { account, errors } = this.state;
    return (
      <form onSubmit={this.handleSubmit}>
        <Input
          name="username"
          label="Username"
          value={account.username}
          onChange={this.handleChange}
          error={errors.username}
        />
        <Input
          name="password"
          label="Password"
          value={account.password}
          onChange={this.handleChange}
          error={errors.password}
        />
        <button className="btn btn-primary">Login</button>
      </form>
    );
  }
در ابتدای متد رندر، با استفاده از Object Destructuring، خاصیت errors شیء منتسب به خاصیت state کامپوننت را دریافت کرده‌ایم. سپس با استفاده از آن، ویژگی جدید error را که به تعریف کامپوننت Input اضافه کردیم، در دو فیلد username و password، مقدار دهی می‌کنیم.

تا اینجا اگر تغییرات را ذخیره کرده و برنامه را اجرا کنیم، با کلیک بر روی دکمه‌ی Login، خطاهای اعتبارسنجی به صورت زیر ظاهر می‌شوند:


در این حالت اگر هر دو فیلد را تکمیل کرده و بر روی دکمه‌ی لاگین کلیک کنیم، به خطای زیر در کنسول توسعه دهندگان مرورگر می‌رسیم:
loginForm.jsx:55 Uncaught TypeError: Cannot read property 'username' of null
at LoginForm.render (loginForm.jsx:55)
علت اینجاست که چون فرم اعتبارسنجی شده و مشکلی وجود نداشته‌است، خروجی متد validate در این حالت، null است. بنابراین دیگر نمی‌توان به خاصیت برای مثال username آن دسترسی یافت. برای رفع این مشکل در متد handleSubmit، جائیکه errors را در خاصیت state به روز رسانی می‌کنیم، اگر errors نال باشد، بجای آن یک شیء خالی را بازگشت می‌دهیم:
this.setState({ errors: errors || {} });
این قطعه کد، به این معنا است که اگر errors مقدار دهی شده بود، از آن استفاده کن، در غیراینصورت {} (یک شیء خالی جاوا اسکریپتی) را بازگشت بده.


اعتبارسنجی فیلدهای یک فرم در حین ورود اطلاعات در آن‌ها

تا اینجا نحوه‌ی اعتبارسنجی فیلدهای ورودی را در حین submit بررسی کردیم. شبیه به همین روش را به حالت onChange و متد handleChange فرم لاگین که در قسمت قبل تکمیل کردیم نیز می‌توان اعمال کرد:
  handleChange = ({ currentTarget: input }) => {
    const errors = { ...this.state.errors }; //cloning an object
    const errorMessage = this.validateProperty(input);
    if (errorMessage) {
      errors[input.name] = errorMessage;
    } else {
      delete errors[input.name];
    }

    const account = { ...this.state.account }; //cloning an object
    account[input.name] = input.value;

    this.setState({ account, errors });
  };
- ابتدا شیء errors را clone می‌کنیم؛ چون می‌خواهیم خواصی را به آن کم و زیاد کرده و سپس بر اساس آن مجددا state را به روز رسانی کنیم.
- سپس اینبار فقط نیاز داریم اعتبار اطلاعات ورودی یک فیلد را بررسی کنیم و متد validate فعلی، فیلدهای کل فرم را با هم تعیین اعتبار می‌کند. به همین جهت متد جدید validateProperty را به صورت زیر تعریف می‌کنیم. اگر این متد خروجی داشت، خاصیت متناظر با آن‌را در شیء errors به روز رسانی می‌کنیم؛ در غیراینصورت این خاصیت را از شیء errors حذف می‌کنیم تا پیام اشتباهی را نمایش ندهد. در نهایت توسط متد setState، مقدار خاصیت errors را با شیء errors جاری به روز رسانی می‌کنیم:
  validateProperty = ({ name, value }) => {
    if (name === "username") {
      if (value.trim() === "") {
        return "Username is required.";
      }
      // ...
    }

    if (name === "password") {
      if (value.trim() === "") {
        return "Password is required.";
      }
      // ...
    }
  };
در متد validateProperty، خواص name و value از شیء input ارسالی به آن استخراج شده‌اند و سپس بر اساس آن‌ها کار اعتبارسنجی صورت می‌گیرد.

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


معرفی Joi


تا اینجا، هدف نمایش ساختار یک اعتبارسنج ساده بود. این روش مقیاس پذیر نیست و در ادامه آن‌را با یک کتابخانه‌ی اعتبارسنجی بسیار پیشرفته به نام Joi، جایگزین خواهیم کرد که نمونه مثال‌های آن‌را در اینجا می‌توانید مشاهده کنید:
const Joi = require('@hapi/joi');
const schema = Joi.object({
    username: Joi.string().alphanum().min(3).max(30).required(),
    password: Joi.string().pattern(/^[a-zA-Z0-9]{3,30}$/),
    email: Joi.string().email({ minDomainSegments: 2, tlds: { allow: ['com', 'net'] } })
});
schema.validate({ username: 'abc', birth_year: 1994 });
ایده‌ی اصلی Joi، تعریف یک اسکیما برای object جاوا اسکریپتی خود است. در این اسکیما، تمام خواص شیء مدنظر ذکر شده و سپس توسط fluent api آن، نیازمندی‌های اعتبارسنجی هرکدام ذکر می‌شوند. برای مثال username باید رشته‌ای بوده، تنها از حروف و اعداد تشکیل شود. حداقل طول آن، 3 و حداکثر طول آن، 30 باشد و همچنین ورود آن نیز اجباری است. با استفاده از pattern آن می‌توان عبارات باقاعده را ذکر کرد و یا با متدهایی مانند email، از قالب خاص مقدار یک خاصیت، اطمینان حاصل کرد.

برای نصب آن، پس از باز کردن پوشه‌ی اصلی برنامه توسط VSCode، دکمه‌های ctrl+` را فشرده (ctrl+back-tick) و دستورات زیر را در ترمینال ظاهر شده وارد کنید:
> npm install @hapi/joi --save
> npm i --save-dev @types/hapi__joi
که در نهایت سبب نصب کتابخانه‌ی node_modules\@hapi\joi\dist\joi-browser.min.js خواهند شد و همچنین TypeScript definitions آن‌را نیز نصب می‌کنند که بلافاصله سبب فعالسازی intellisense مخصوص آن در VSCode خواهد شد. بدون نصب types آن، پس از تایپ Joi.، از مزایای تکمیل خودکار fluent api آن توسط VSCode، برخوردار نخواهیم بود.

سپس به کامپوننت فرم لاگین مراجعه کرده و در ابتدای آن، Joi را import می‌کنیم:
import Joi from "@hapi/joi";
پس از آن، اسکیمای شیء account را تعریف خواهیم کرد. اسکیما نیازی نیست جزئی از state باشد؛ چون قرار نیست تغییر کند. به همین جهت آن‌را به صورت یک خاصیت جدید در سطح کلاس کامپوننت تعریف می‌کنیم:
  schema = {
    username: Joi.string()
      .required()
      .label("Username"),
    password: Joi.string()
      .required()
      .label("Password")
  };
خاصیت اسکیما را با یک شیء با ساختار از نوع Joi.object، که خواص آن، با خواص شیء account مرتبط با فیلدهای فرم لاگین، تطابق دارد، تکمیل می‌کنیم. مقدار هر خاصیت نیز با Joi. شروع شده و سپس نوع و محدودیت‌های مدنظر اعتبارسنجی را می‌توان تعریف کرد که در اینجا هر دو مورد باید رشته‌ای بوده و به صورت اجباری وارد شوند. توسط متد label، برچسب نام خاصیت درج شده‌ی در پیام خطای نهایی را می‌توان تنظیم کرد. اگر از این متد استفاده نشود، از همان نام خاصیت ذکر شده استفاده می‌کند.
سپس ابتدای متد validate قبلی را به صورت زیر بازنویسی می‌کنیم:
  validate = () => {
    const { account } = this.state;
    const validationResult = Joi.object(this.schema).validate(account, {
      abortEarly: false
    });
    console.log("validationResult", validationResult);
ابتدا مقدار خاصیت account، از شیء state استخراج شده‌است که حاوی شیءای با اطلاعات نام کاربری و کلمه‌ی عبور است. سپس این شیء را به متد validate خاصیت اسکیمایی که تعریف کردیم، ارسال می‌کنیم. Joi، اعتبارسنجی را به محض یافتن خطایی، متوقف می‌کند. به همین جهت تنظیم abortEarly آن به false صورت گرفته‌است تا تمام خطاهای اعتبارسنجی را نمایش دهد.
اکنون اگر برنامه را اجرا کرده و بدون ورود اطلاعاتی، بر روی دکمه‌ی لاگین کلیک کنیم، خروجی زیر در کنسول توسعه دهندگان مرورگر ظاهر می‌شود:


همانطور که مشاهده می‌کنید، خروجی Joi، یک شیء است که اگر دارای خاصیت error بود، یعنی خطای اعتبارسنجی رخ‌داده‌است. سپس باید خاصیت آرایه‌ای details این شیء error را جهت یافتن خواص مشکل دار بررسی کرد. هر خاصیت در اینجا با path مشخص می‌شود. بنابراین قدم بعدی، تبدیل این ساختار، به ساختار شیء errors موجود در state کامپوننت جاری است تا مابقی برنامه بتواند بدون تغییری از آن استفاده کند.


نگاشت شیء دریافتی از Joi، به شیء errors موجود در state کامپوننت لاگین

خاصیت error شیء دریافتی از متد validate کتابخانه‌ی Joi، تنها زمانی ظاهر می‌شود که خطایی وجود داشته باشد. همچنین خاصیت details آن نیز آرایه‌ا‌ی از اشیاء با خواص message و path است. این path نیز یک آرایه است که اولین المان آن، نام خاصیت در حال بررسی است. اکنون می‌خواهیم این آرایه را تبدیل به یک شیء قابل درک برای برنامه کنیم:
  validate = () => {
    const { account } = this.state;
    const validationResult = Joi.object(this.schema).validate(account, {
      abortEarly: false
    });
    console.log("validationResult", validationResult);

    if (!validationResult.error) {
      return null;
    }

    const errors = {};
    for (let item of validationResult.error.details) {
      errors[item.path[0]] = item.message;
    }
    return errors;
  };
ابتدا بررسی می‌کنیم که آیا خاصیت error، تنظیم شده‌است یا خیر؟ اگر خیر، کار این متد به پایان می‌رسد. سپس حلقه‌ای را بر روی آرایه‌ی details، تشکیل می‌دهیم تا شیء errors مدنظر ما را با خاصیت دریافتی از path و پیام دریافتی متناظری تکمیل کند. در آخر این شیء errors با ساختار مدنظر خود را بازگشت می‌دهیم.

اکنون اگر تغییرات را ذخیره کرده و برنامه را اجرا کنیم، همانند قبل می‌توان خطاهای اعتبارسنجی در حین submit را مشاهده کرد:



بازنویسی متد validateProperty توسط Joi

تا اینجا متد validate ساده و ابتدایی خود را با استفاده از امکانات کتابخانه‌ی Joi، بازنویسی کردیم. اکنون نوبت بازنویسی متد اعتبارسنجی در حین تایپ اطلاعات است:
  validateProperty = ({ name, value }) => {
    const userInputObject = { [name]: value };
    const propertySchema = Joi.object({ [name]: this.schema[name] });
    const { error } = propertySchema.validate(userInputObject, {
      abortEarly: true
    });
    return error ? error.details[0].message : null;
  };
تفاوت این متد با متد validate، در اعتبارسنجی تنها یک خاصیت از شیء account موجود در state است؛ به همین جهت نمی‌توان کل this.state.account را به متد validate کتابخانه‌ی Joi ارسال کرد. بنابراین نیاز است بر اساس name و value رسیده‌ی از کاربر، یک شیء جدید را به صورت پویا تولید کرد. در اینجا روش تعریف { [name]: value } به computed properties معرفی شده‌ی در ES6 اشاره می‌کند. اگر در حین تعریف یک شیء، برای مثال بنویسیم {"username:"value}، این username به صورت "username" ثابتی تفسیر می‌شود و پویا نیست. اما در ES6 می‌توان با استفاده از [] ها، تعریف نام یک خاصیت را پویا کرد که نمونه‌ای از آن‌را در userInputObject و همچنین propertySchema مشاهده می‌کنید.
تا اینجا بجای this.state.account که به کل فرم اشاره می‌کند، شیء اختصاصی‌تر userInputObject را ایجاد کرده‌ایم که معادل اطلاعات فیلد ورودی کاربر است. یک چنین نکته‌ای را در مورد schema نیز باید رعایت کرد. در اینجا نمی‌توان اعتبارسنجی را با this.schema شروع کرد؛ چون این شیء نیز به اطلاعات کل فرم اشاره می‌کند و نه تک فیلدی که هم اکنون در حال کار با آن هستیم. بنابراین نیاز است یک Joi.object جدید را بر اساس name رسیده، از this.schema کلی، استخراج و تولید کرد؛ مانند propertySchema که مشاهده می‌کنید.
اکنون می‌توان متد validate این شیء اسکیمای جدید را فراخوانی کرد و خاصیت error شیء حاصل از آن‌را توسط Object Destructuring، استخراج نمود. در اینجا abortEarly به true تنظیم شده‌است (البته حالت پیش‌فرض آن است و نیازی به ذکر صریح آن نیست). علت اینجا است که نمایش خطاهای بیش از اندازه به کاربر، برای او گیج کننده خواهند بود؛ به همین جهت هربار، اولین خطای حاصل را به او نمایش می‌دهیم.


غیرفعالسازی دکمه‌ی submit در صورت شکست اعتبارسنجی‌های فرم

در ادامه می‌خواهیم دکمه‌ی submit فرم لاگین، تا زمانیکه اعتبارسنجی آن با موفقیت به پایان برسد، غیرفعال باشد:
 <button disabled={this.validate()} className="btn btn-primary">
 Login
</button>
ویژگی disabled را اگر به true تنظیم کنیم، این دکمه را غیرفعال می‌کند. در اینجا متد validate تعریف شده، نال و یا یک شیء را بازگشت می‌دهد. اگر نال را بازگشت دهد، در جاوااسکریپت به false تفسیر شده و دکمه را فعال می‌کند و برعکس؛ مانند تصویر حاصل زیر:


فراخوانی setState‌های تعریف شده‌ی در متدهای رویدادگردان این فرم هستند که سبب رندر مجدد کامپوننت شده و در این بین در متد رندر، کار بررسی مجدد مقدار نهایی متد validate صورت می‌گیرد تا بر اساس آن، فعال یا غیرفعال بودن دکمه‌ی Login، مشخص شود.

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-19.zip
نظرات مطالب
طبقه بندی Bad Code Smell ها
به صورت کلی استفاده  enum‌ها به تنهایی پیشنهاد نمیشود چون enum نشان دهنده یک وضعیت ( state ) است و ما در برنامه نویسی شی گرا نیاز به  وضعیت و رفتار ( state & behavior) داریم و یکی از مشکلات اصلی enum‌ها این است که قابلیت refactoring رو به خوبی ندارن به همین دلایل ما enum‌ها رو در یک کلاس قرار می‌دهیم مانند این مثال:
sealed class DeviceStatus : IEquatable<DeviceStatus>
    {
        [Flags]
        private enum StatusRepresentation
        {
            AllFine = 0,
            NotOperational = 1,
            VisiblyDamaged = 2,
            CircuitryFailed = 4
        }

        private StatusRepresentation Representation { get; }

        private DeviceStatus(StatusRepresentation representation)
        {
            this.Representation = representation;
        }

        public static DeviceStatus AllFine() =>
            new DeviceStatus(StatusRepresentation.AllFine);

        public DeviceStatus WithVisibleDamage() =>
            new DeviceStatus(this.Representation | StatusRepresentation.VisiblyDamaged);

        public DeviceStatus NotOperational() =>
            new DeviceStatus(this.Representation | StatusRepresentation.NotOperational);

        public DeviceStatus Repaired() =>
            new DeviceStatus(this.Representation & ~StatusRepresentation.NotOperational);

        public DeviceStatus CircuitryFailed() =>
            new DeviceStatus(this.Representation | StatusRepresentation.CircuitryFailed);

        public DeviceStatus CircuitryReplaced() =>
            new DeviceStatus(this.Representation & ~StatusRepresentation.CircuitryFailed);

        public override int GetHashCode() => (int)this.Representation;

        public override bool Equals(object obj) => this.Equals(obj as DeviceStatus);

        public bool Equals(DeviceStatus other) =>
            other != null && this.Representation == other.Representation;

        public static bool operator ==(DeviceStatus a, DeviceStatus b) =>
            (object.ReferenceEquals(a, null) && object.ReferenceEquals(b, null)) ||
            (!object.ReferenceEquals(a, null) && a.Equals(b));

        public static bool operator !=(DeviceStatus a, DeviceStatus b) => !(a == b);
    }
اگه دقت کرده باشید این کلاس به صورت value object طراحی شده است و از مهمترین ویژگی اینگونه کلاس‌ها این که تغییرناپذیر هستند. 
نظرات مطالب
طراحی گردش کاری با استفاده از State machines - قسمت اول
ممنون از توجه شما
بدلیل اینکه شروع بحث از این پست بود و جناب نصیری به ان پاسخ داده بودند در اینجا دوباره مطرح کردم.
در بخش دوم با استفاده از افزونه مورد نظر و code generator آن کلاس سازنده ماشین حالت ایجاد می‌شود. یعنی در حال Design با رسم گرافیکی آن کلاس مزبور:
public class BlogPostStateMachine
  {
    // .... مثال قسمت دوم
  }
ساخته می‌شود و به گردش درآوردن شیء در آن هم بصورت واضح در بخش دوم توضیح داده شده است. اما نحوه ایجاد این کلاس بصورت dynamic و با استفاده از واکشی داده‌های مربوط به State ها، Transition‌ها و ... از بانک اطلاعاتی سئوال اصلی من است. همانگونه در پست قبلی اشاره کردم با استفاده از کلاس اشاره شده و ارسال لیست State‌ها و Transition‌ها به آن می‌توان تنها با :
StateMachineRequest smr = new StateMachineRequest(smc, startId);
            smr.Configure();
ماشین حالت را ایجاد و کانفیگ نمود و به آن دسترسی داشت. البته بررسی شروط هنگام تغییر وضعیت یا هنگام ورود و خروج از یک وضعیت یکی از ابهامات آن است.
مطالب دوره‌ها
نحوه‌ی ارتقاء برنامه‌های SignalR 1.x به SignalR 2.x
1) اگر هم اکنون یک پروژه جدید SignalR را آغاز و از طریق NuGet وابستگی‌های آن‌را اضافه کنید، به صورت خودکار SignalR نگارش 2 را در این تاریخ دریافت خواهید کرد. این نگارش صرفا با دات نت 4 و نیم به بعد سازگار است. بنابراین اولین کاری که باید برای ارتقاء پروژه‌های SignalR 1.x به نگارش جدید انجام دهید، تغییر Target framework پروژه به نگارش 4.5 است.
2) حذف وابستگی‌های قدیمی
 Uninstall-Package Microsoft.AspNet.SignalR -RemoveDependencies
فرمان فوق را اگر در کنسول پاورشل نیوگت اجرا کنید، به صورت خودکار وابستگی‌های قدیمی SignalR را حذف می‌کند.
3) نصب فایل‌های جدید SignalR
 Install-Package Microsoft.AspNet.SignalR
برای این منظور تنها کافی است دستور فوق را اجرا نمائید.
4) به روز رسانی ارجاعات اسکریپتی
 <script src="Scripts/jquery.signalR-2.0.0.min.js"></script>
ارجاع به افزونه جی‌کوئری SignalR نیز باید به نگارش 2 ارتقاء یابد.
5) حذف نحوه‌ی تعریف مسیریابی هاب‌های SignalR از فایل global.asax برنامه.
 protected void Application_Start(object sender, EventArgs e)
{
   //RouteTable.Routes.MapHubs();
}
فایل یاد شده را گشوده و سطر فوق را از آن حذف کنید. سپس یک کلاس دلخواه جدید را مثلا به نام Startup، ایجاد و محتوای آن را به نحو ذیل تغییر دهید:
using Microsoft.Owin;
using Owin;

[assembly: OwinStartup(typeof(SignalRChat.Startup))]
namespace SignalRChat
{   
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.MapSignalR();
        }
    }
}
این فایل به صورت خودکار در زمان آغاز برنامه‌های SignalR 2 مورد استفاده قرار می‌گیرد (با کمک ویژگی assembly: OwinStartup آن).
اگر از آخرین نگارش VS.NET استفاده می‌کنید، این کلاس را توسط گزینه Add -> New Item -> Owin Startup Class نیز می‌توانید اضافه نمائید.
مطالب
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 5#
در ادامه مطلب پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 4# به تشریح مابقی کلاس‌های برنامه می‌پردازیم.  

در این پست به شرح کلاس Rectangle جهت رسم مستطیل و Square جهت رسم مربع می‌پردازیم

using System.Drawing;

namespace PWS.ObjectOrientedPaint.Models
{
    /// <summary>
    /// Rectangle
    /// </summary>
    public class Rectangle : Shape
    {
        #region Constructors (2)

        /// <summary>
        /// Initializes a new instance of the <see cref="Rectangle" /> class.
        /// </summary>
        /// <param name="startPoint">The start point.</param>
        /// <param name="endPoint">The end point.</param>
        /// <param name="zIndex">Index of the z.</param>
        /// <param name="foreColor">Color of the fore.</param>
        /// <param name="thickness">The thickness.</param>
        /// <param name="isFill">if set to <c>true</c> [is fill].</param>
        /// <param name="backgroundColor">Color of the background.</param>
        public Rectangle(PointF startPoint, PointF endPoint, int zIndex, Color foreColor, byte thickness, bool isFill, Color backgroundColor)
            : base(startPoint, endPoint, zIndex, foreColor, thickness, isFill, backgroundColor)
        {
            ShapeType = ShapeType.Rectangle;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Rectangle" /> class.
        /// </summary>
        public Rectangle()
        {
            ShapeType = ShapeType.Rectangle;
        }

        #endregion Constructors

        #region Methods (1)

        // Public Methods (1) 

        /// <summary>
        /// Draws the specified g.
        /// </summary>
        /// <param name="g">The g.</param>
        public override void Draw(Graphics g)
        {
            if (IsFill)
                g.FillRectangle(BackgroundBrush, StartPoint.X, StartPoint.Y, Width, Height);
            g.DrawRectangle(Pen, StartPoint.X, StartPoint.Y, Width, Height);
            base.Draw(g);
        }

        #endregion Methods
    }
}
کلاس Rectangle از کلاس پایه طراحی شده در ^ ارث بری دارد. این کلاس ساده بوده و تنها شامل یک سازنده و متد ترسیم شی مستطیل می‌باشد.

کلاس بعدی کلاس Square می‌باشد، که از کلاس بالا (Rectangle) ارث بری داشته است، کد‌های این کلاس را در زیر مشاهده می‌کنید.
using System;
using System.Drawing;

namespace PWS.ObjectOrientedPaint.Models
{
    /// <summary>
    /// Square
    /// </summary>
    public class Square : Rectangle
    {
#region Constructors (2) 

        /// <summary>
        /// Initializes a new instance of the <see cref="Square" /> class.
        /// </summary>
        /// <param name="startPoint">The start point.</param>
        /// <param name="endPoint">The end point.</param>
        /// <param name="zIndex">Index of the z.</param>
        /// <param name="foreColor">Color of the fore.</param>
        /// <param name="thickness">The thickness.</param>
        /// <param name="isFill">if set to <c>true</c> [is fill].</param>
        /// <param name="backgroundColor">Color of the background.</param>
        public Square(PointF startPoint, PointF endPoint, int zIndex, Color foreColor, byte thickness, bool isFill, Color backgroundColor)
        {
            float x = 0, y = 0;
            float width = Math.Abs(endPoint.X - startPoint.X);
            float height = Math.Abs(endPoint.Y - startPoint.Y);
            if (startPoint.X <= endPoint.X && startPoint.Y <= endPoint.Y)
            {
                x = startPoint.X;
                y = startPoint.Y;
            }
            else if (startPoint.X >= endPoint.X && startPoint.Y >= endPoint.Y)
            {
                x = endPoint.X;
                y = endPoint.Y;
            }
            else if (startPoint.X >= endPoint.X && startPoint.Y <= endPoint.Y)
            {
                x = endPoint.X;
                y = startPoint.Y;
            }
            else if (startPoint.X <= endPoint.X && startPoint.Y >= endPoint.Y)
            {
                x = startPoint.X;
                y = endPoint.Y;
            }
            StartPoint = new PointF(x, y);
            var side = Math.Max(width, height);
            EndPoint = new PointF(x+side, y+side);
            ShapeType = ShapeType.Square;
            Zindex = zIndex;
            ForeColor = foreColor;
            Thickness = thickness;
            BackgroundColor = backgroundColor;
            IsFill = isFill;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Square" /> class.
        /// </summary>
        public Square()
        {
            ShapeType = ShapeType.Square;
        }

#endregion Constructors 
    }
}
این کلاس شامل دو سازنده می‌باشد که سازنده دوم فقط نوع شی را تعیین می‌کند و بقیه کارهای آن مانند مستطیل است، در واقع می‌توان از یک دیدگاه گفت که مربع یک مستطیل است که اندازه طول و عرض آن یکسان است. در سازنده اول (نحوه ترسیم شکل) ابتدا نقاط ابتدا و انتهای رسم شکل تعیین شده و سپس با توجه به پارامترهای محاسبه شده نوع شی جهت ترسیم و دیگر خصوصیات کلاس مقدار دهی می‌شود، با این تفاوت که در نقطه EndPoint طول و عرض مربع برابر با بزرگترین مقدار طول و عرض وارد شده در سازنده کلاس تعیین شده و مربع شکل می‌گیرد. مابقی متدهای ترسیم و ... طبق کلاس پایه مستطیل و Shape تعیین می‌شود.
مطالب قبل:
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 1# 
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 2# 
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 3#
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 4# 
نظرات مطالب
EF Code First #1
سلام مهندس نصیری، چرا این کد توی EF5 خطای کلید خارجی میده؟
کدش از کتاب Code First که معرفی کردین استفاده کردم اما کد خودتون خطا نداره

using System;
using System.Collections.Generic;
namespace ChapterOneProject
{
public class Patient
    {
        public Patient()
        {
            Visits = new List<Visit>();
        }

        public int Id { get; set; }
        public string Name { get; set; }
        public DateTime BirthDate { get; set; }
        //[ForeignKey("AnimalTypeId")]
        
        public AnimalType AnimalType { get; set; }
        //public int AnimalTypeId { get; set; }

        public DateTime FirstVisit { get; set; }
        public List<Visit> Visits { get; set; }
    }

public class Visit
    {
        [Key]
        public int Id { get; set; }
        public DateTime Date { get; set; }
        public String ReasonForVisit { get; set; }
        public String Outcome { get; set; }
        public Decimal Weight { get; set; }

        //[ForeignKey("PatientId")]
        //public virtual Patient Patient { get; set; }
        public int PatientId { get; set; }
    }

public class AnimalType
    {
        public int Id { get; set; }
        public string TypeName { get; set; }
    }
}

کد کانتکست
public class VetContext : DbContext
    {
        public DbSet<Patient> Patients { get; set; }
        public DbSet<Visit> Visits { get; set; }
        //public DbSet<AnimalType> AnimalTypes { get; set; }
    }
و در تابع Main برنامه Console این نوشته شده اما خطا میده و ثبت نمی‌شه
var dog = new AnimalType { TypeName = "Dog" };
            var visit = new List<Visit>
                            {
                                new Visit
                                    {
                                        Date = new DateTime(2011, 9, 1),
                                        Outcome = "Test",
                                        ReasonForVisit = "Test",
                                        Weight = 32,
                                    }
                            };
            var patient = new Patient
                              {
                                  Name = "Sampson",
                                  BirthDate = new DateTime(2008, 1, 28),
                                  AnimalType = dog,
                                  Visits = visit,
                              };
            using (var context = new VetContext())
            {
                context.Patients.Add(patient);
                context.SaveChanges();
            }

کد‌های دیگه تست کردم مشکلی نداشت اما این مورد ؟
با profiler چک کردم خطای عدم توانایی در تبدیل نوع datetime2 به datetime میده
مطالب
فقط به خاطر یک نیم فاصله!
نسخه‌ی 64 بیتی ویندوز 7 را نصب کرده‌ام و اولین مشکل، نبود صفحه کلید مطابق استاندارد 2091 برای نسخه‌های 64 بیتی ویندوز است. پروژه وب فارسی دانشگاه صنعتی شریف، سال‌ها قبل فقط یک نسخه‌ی 32 بیتی از آن را تهیه کرده و نسخه‌های 64 بیتی موجود، مطابق استاندارد 9147 هستند و من به دلایل ذیل حاضر به استفاده از آن نیستم!
  1. در استاندارد 9147 ، جای ژ و پ مطابق صفحه کلیدی که در بازار ایران فروخته می‌شود نیست (و باید به همه پاسخگو باشید که چرا اینطوری است).
  2. ی و ک در آن اصلاح شده و دیگر عربی نیست که چقدر هم خوب، اما من تعداد زیادی برنامه و همچنین تعداد قابل توجهی فایل word دارم که مطابق استاندارد 2091 تهیه شده‌اند. مشکلات ی و ک فارسی و عربی را هم در اینجا ذکر کرده‌ام و به دنبال باگ‌های جدید در برنامه‌ها نمی‌گردم.
  3. با ی و ک فارسی اگر در گوگل جستجو کنید تعداد پاسخ‌های مرتبط یافت شده بسیار بسیار کمتر از حالتی خواهد بود که با ی و ک عربی جستجو کنید.
  4. در یک سازمان باید رویه‌ا‌ی واحد در این مورد برقرار باشد. یا همه باید از 2091 استفاده کنند و یا همه از 9147.

نسخه‌ی کامل استاندارد 9147 را از سایت آقای اخگری می‌توانید دریافت نمائید:

به همین دلایل خصوصا قسمت دوم (هر چند ممکن است با آن موافق نباشید)، نیاز به صفحه کلید مطابق استاندارد 2091 نسخه‌ی 64 بیتی داشتم.

برای تهیه صفحه کلید فارسی از برنامه Microsoft Keyboard Layout Creator استفاده می‌شود که نسخه‌ی 1.4 آن‌را از آدرس زیر می‌توانید دریافت نمائید. این نسخه قابلیت تولید فایل‌های dll مرتبط 64 بیتی را هم دارد:


یکی از قابلیت‌های این برنامه بارگذاری صفحه کلید فارسی جاری سیستم است:



صفحه کلید استاندارد 2091 پروژه وب فارسی را اگر با آن باز کرده و سپس از منوی Project گزینه‌ی Build DLL and setup package را انتخاب نمائید، با خطای زیر متوقف خواهید شد:
ERROR: 'VK_SPACE' in Shift State 'Shift' must be made up of white space character(s), but is defined as '‌' (U+200c) instead.
که دقیقا مربوطه به این سطر در تعاریف صفحه کلید است (صفحه کلید را می‌شود به صورت فایلی با پسوند klc هم save as کرد):
39 SPACE  0 0020 200c 0020 -1  // SPACE, ZERO WIDTH NON-JOINER, SPACE, <none>
مفاهیم این ستون‌ها هم به شرح زیر هستند:
0 //Column 4
1 //Column 5 : Shft
2 //Column 6 :       Ctrl
3 //Column 7 : Shft  Ctrl
یعنی جهت نمایش نیم فاصله حاصل از ترکیب shift space مطابق استاندارد تایپ ایران، بجای 0x0020 یا همان فاصله، از 0x200C استفاده شده است (مطابق استاندارد تایپ ایران باید نوشت "می‌روم" و نه "می روم". به نیم فاصله و فاصله دقت بفرمائید).
اما این برنامه‌ی محترم دقیقا همین مورد را غلط گرفته و فایل dll نهایی را تولید نمی‌کند (مطابق خطایی که ذکر شد).
برنامه‌ی Microsoft Keyboard Layout Creator هم با دات نت فریم ورک نوشته شده است. اگر کمی با reflector به آنالیز آن بپردازیم به کلاس Accept و متد زیر خواهیم رسید:
private bool VerifySpaceBarIntegrity(ShiftState ss, bool fMustBeDefined)
در این متد جایی که خطای ذکر شده صادر می‌شود، مربوط به چند سطر زیر است:
if (!Utilities.IsSpacing(ss.Characters))
{
    this.WriteErrorToLogFile("SpaceKeyNeedsWhiteSpaceCharacters", new string[] { "VK_" + Utilities.VkStringOfIvk(ss.VK), this.m_stStateLabel[ss.State], ss.Characters, Utilities.FromCharacterToUPlusForm(ss.Characters) });
    flag = false; /*اینجا باید اصلاح شود!*/
}
خوب! به نظر شما ایرادی دارد که این flag همیشه true باشد؟! برای همین منظور، فایل MSKLC.exe را پچ کرده‌ام که از آدرس زیر قابل دریافت است:

  دریافت پچ شده نگارش 1.4.6000.2 : MSKLC.Patched.7z  

با استفاده از این نسخه (ابتدا برنامه اصلی را نصب کرده و سپس فایل exe را جایگزین نمائید)، به راحتی می‌توان نسخه‌ی استاندارد و اصلی 2091 را بارگذاری و مجددا کامپایل کرد (بدون توقف کامپایل به خاطر فقط یک نیم فاصله‌ی غیرمعتبر از دیدگاه نویسندگان اصلی برنامه). به این صورت فایل‌های 64 بیتی لازم هم تولید می‌شوند ( ممکن است در حین کار با برنامه‌ی patch شده، نمایش خطای کار با نیم فاصله را مجددا دریافت کنید؛ اما مهم نیست . نمونه‌ی وصله شده، بدون مشکل حاصل را کامپایل می‌کند که در نمونه‌ی اصلی اینطور نیست. یعنی پس از overwrite کردن فایل exe موجود، با نمونه patch شده، برنامه کار کامپایل را کامل می‌کند) که حاصل نهایی را از آدرس زیر می‌توانید دریافت نمائید:


این فایل‌ها دقیقا بر مبنای همان فایل‌های پروژه وب فارسی استاندارد 2091 هستند؛ با این تفاوت که نسخه‌‌های غیر 32 بیتی هم در آن لحاظ شده‌اند.

نصب این dll‌ها هم از ویندوز ویستا به بعد داستان خودش را پیدا کرده؛ ابتدا باید take ownership شود، سپس می‌توان فایل اصلی را به سادگی جایگزین کرد و سپس reboot .
برای این منظور ابتدا به پوشه‌ی system32 ویندوز مراجعه کرده و فایل KBDFA.DLL را پیدا کنید.
در ادامه به پنجره خواص آن (کلیک راست و انتخاب properties) مراجعه و برگه‌ی Security آن‌را انتخاب کنید.
در این قسمت بر روی دکمه‌ی Advanced کلیک نمائید.
در صفحه‌ی باز شده به برگه‌ی Owner مراجعه کنید.
در این قسمت بر روی دکمه‌ی Edit کلیک نموده و کاربر خودتان را اضافه نمائید.
پس از طی این مرحله (یا همان take ownership) به برگه security مراجعه کرده و به کاربر خودتان دسترسی full control بدهید.
اکنون مجوز تغییر این فایل را بدون هیچ مشکلی خواهید یافت.
در پایان reboot را فراموش نکنید.

راه دوم:
یا برنامه نصاب حاصل از برنامه‌ی Microsoft Keyboard Layout Creator، مدخل جدیدی را به صفحه کلیدهای قابل انتخاب ویندوز در کنترل پنل اضافه می‌کند که نیازی به reboot ندارد.
مطالب
پلاگین DataTables کتابخانه jQuery - قسمت چهارم
همان طور که قبلا اشاره کردیم، این پلاگین می‌تواند از یک زبان برنامه نویسی سمت سرور داده‌های مورد نیاز خودش را دریافت کند. می‌توانید داده‌ها را با استفاده از AJAX و به صورت JSON از سرور دریافت کرده و با استفاده از DataTables آنها را در جدول تزریق کنید. در این قسمت سعی خواهیم کرد تا با استفاده از jQuery DataTables یک گرید را در MVC ایجاد کنیم.  البته برای حذف جزئیات داده‌ها به جای این که از یک بانک اطلاعاتی دریافت شوند، در حافظه ساخته می‌شوند. در هر صورت اساس کار یکی است.

قصد داریم تا مانند مثال قسمت قبل، مجموعه ای از اطلاعات مربوط به مرورگرهای مختلف را در یک جدول نشان دهیم، اما این بار منبع داده ما فرق می‌کند. منبع داده از طرف سرور فراهم می‌شود. هر مرورگر - همان طور که در قسمت قبل مشاهده نمودید - شامل اطلاعات زیر خواهد بود:
  1. موتور رندرگیری (Engine)
  2. نام مرورگر (Name)
  3. پلتفرم (Platform)
  4. نسخه موتور (Version)
  5. نمره سی اس اس (Grade)

به همین دلیل در سمت سرور، کلاسی خواهیم ساخت که نمایانگر یک مرورگر باشد. بدین صورت:

public class Browser
{
    public int Id { get; set; }
    public string Engine { get; set; }
    public string Name { get; set; }
    public string Platform { get; set; }
    public float Version { get; set; }
    public string Grade { get; set; }
}

استفاده از روش server side processing برای دریافت داده‌ها از سرور

این روش، یکی از امکانات jQuery DataTables است که با استفاده از آن، کلاینت تنها یک مصرف کننده صرف خواهد بود و وظیفه پردازش اطلاعات - یعنی تعداد رکوردهایی که برگشت داده می‌شود، صفحه بندی، مرتب سازی، جستجو، و غیره - به عهده سرور خواهد بود.

برای به کار گیری این روش، اولین کار این است که ویژگی bServerSide را true کنیم، مثلا بدین صورت:
var $table = $('#browsers-grid');
$table.dataTable({
      "bServerSide": true,
      "sAjaxSource": "/Home/GetBrowsers"
 });

همچنین ویژگی sAjaxSource را به Url ی که باید داده‌ها از آن دریافت شوند مقداردهی می‌کنیم.

به صورت پیش فرض مقدار ویژگی bServerSide مقدار false است؛ که یعنی منبع داده این پلاگین از سمت سرور خوانده نشود. اگر true باشد منبع داده و خیلی اطلاعات دیگر مربوط به داده‌های درون جدول باید از سرور به مرورگر کاربر پس فرستاده شوند. با true کردن مقدار bServerSide، آنگاه DataTables اطلاعاتی را راجع به شماره صفحه جاری، اندازه هر صفحه، شروط فیلتر کردن داده ها، مرتب سازی ستون ها، و غیره را به سرور می‌فرستد. همجنین انتظار می‌رود تا سرور در پاسخ به این درخواست، داده‌های مناسبی را به فرمت JSON به مرورگر پس بفرستد. در حالتی که bServerSide مقدار true به خود بگیرد، پلاگین فقط رابطه متقابل بین کاربر و سرور را مدیریت می‌کند و هیچ پردازشی را انجام نمی‌دهد.


در این درخواست XHR یا Ajax ی پارامترهایی که به سرور ارسال می‌شوند این‌ها هستند:

iDisplayStart عدد صحیح
نقظه شروع مجموعه داده جاری

iDisplayLength عدد صحیح
تعداد رکوردهایی که جدول می‌تواند نمایش دهد. تعداد رکوردهایی که از طرف سرور برگشت داده می‌شود باید با این عدد یکسان باشند.

iColumns عدد صحیح
تعداد ستونهایی که باید نمایش داده شوند.

sSearch رشته
فیلد جستجوی عمومی

bRegex بولین
اگر true باشد معنی آن این است که می‌توان از عبارات باقاعده برای جستجوی عبارتی خاص در کل ستون‌های جدول استفاده کرد. مثلا در کادر جستجو نوشت :

^[1-5]$
که یعنی 1 و 5 همه عددهای بین 1و 5.

bSearchable_(int)    بولین
نمایش می‌دهد که یک ستون در طرف کاربر قابلیت searchable آن true هست یا نه.

sSearch_(int)   رشته
فیلتر مخصوص هر ستون. اگر از ویژگی multi column filtering پلاگین استفاده شود به صورت sSearch0 ، sSearch1 ، sSeach2 و ... به طرف سرور ارسال می‌شوند. شماره انتهای هر کدام از پارامترها بیانگر شماره ستون جدول است.

bRegex_(int)  بولین
اگر true باشد، بیان می‌کند که می‌توان از عبارت با قاعده در ستون شماره int جهت جستجو استفاده کرد.

bSortable_(int) بولین
مشخص می‌کند که آیا یک ستون در سمت کلاینت، قابلیت مرتب شدن بر اساس آن وجود دارد یا نه. (در اینجا int اندیس ستون را مشخص می‌کند)

iSortingCols   عدد صحیح
تعداد ستون هایی که باید مرتب سازی بر اساس آنها صورت پذیرد. در صورتی که از امکان multi column sorting استفاده کنید این مقدار می‌تواند بیش از یکی باشد.

iSortCol_(int)   عدد صحیح
شماره ستونی که باید بر اساس آن عملیات مرتب سازی صورت پذیرد.

sSortDir_(int)    رشته
نحوه مرتب سازی ؛ شامل صعودی (asc) یا نزولی (desc)

mDataProp_(int)    رشته
اسم ستون‌های درون جدول را مشخص می‌کند.

sEcho     رشته
اطلاعاتی که datatables از آن برای رندر کردن جدول استفاده می‌کند.

شکل زیر نشان می‌دهد که چه پارامترهایی به سرور ارسال می‌شوند.



شکل ب ) پارامترهای ارسالی به سرور به صورت json

بعضی از این پارامترها بسته به تعداد ستون‌ها قابل تغییر هستند. (آن پارامترهایی که آخرشان یک عدد هست که نشان دهنده شماره ستون مورد نظر می‌باشد)

در پاسخ به هر درخواست XHR که datatables به سرور می‌فرستد، انتظار دارد تا سرور نیز یک شیء json را با فرمت مخصوص که شامل پارامترهای زیر می‌شود به او پس بفرستد:

iTotalRecords    عدد صحیح
تعداد کل رکوردها (قبل از عملیات جستجو) یا به عبارت دیگر تعداد کل رکوردهای درون آن جدول از دیتابیس که داده‌ها باید از آن دریافت شوند. تعداد کل رکوردهایی که در طرف سرور وجود دارند. این مقدار فقط برای نمایش به کاربر برگشت داده می‌شود و نیز از آن برای صفحه بندی هم استفاده می‌شود. 


iTotalDisplayRecords    عدد صحیح
تعداد کل رکوردها (بعد از عملیات جستجو) یا به عبارت دیگر تعداد کل رکوردهایی که بعد از عملیات جستجو پیدا می‌شوند نه فقط آن تعداد رکوردی که به کاربر پس فرستاده می‌شوند. تعداد کل رکوردهایی که با شرط جستجو مطابقت دارند. اگر کاربر چیزی را جستجو نکرده باشد مقدار این پارامتر با پارامتر iTotalRecords یکسان خواهد بود.  

sEcho    عدد صحیح 
یک عدد صحیح است که در قالب رشته در تعامل بین سرور و کلاینت جا به جا می‌شود. این مقدار به ازاء هر درخواست تغییر می‌کند. همان مقداری که مرورگر به سرور می‌دهد را سرور هم باید به مرورگر تحویل بدهد. برای جلوگیری از حملات XSS باید آن را تبدیل به عدد صحیح کرد. پلاگین DataTables مقدار این پارامتر را برای هماهنگ کردن و منطبق کردن درخواست ارسال شده و جواب این درخواست استفاده می‌کند. همان مقداری که مروگر به سرور می‌دهد را باید سرور تحویل به مرورگر بدهد. 

sColumns    رشته
اسم ستون‌ها که با استفاده از کاما از هم جدا شده اند. استفاده از آن اختیاری است و البته منسوخ هم شده است و در نسخه‌های جدید jQuery DataTables از آن پشتیبانی نمی‌شود.

aaData    آرایه
همان طور که قبلا هم گفتیم، مقادیر سلول هایی را که باید در جدول نشان داده شوند را در خود نگهداری می‌کند. یعنی در واقع داده‌های جدول در آن ریخته می‌شوند. هر وقت که DataTables داده‌های مورد نیازش را دریافت می‌کند، سلول‌های جدول html مربوطه اش را از روی آرایه aaData ایجاد می‌کند. تعداد ستون‌ها در این آرایه دو بعدی، باید با تعداد ستون‌های جدول html مربوطه به آن یکسان باشد

شکل زیر پارامترها دریافتی از سرور را نشان می‌دهند:


شکل ب ) پارامترهای دریافتی از سرور به صورت json

استفاده از روش server side processing در mvc
همان طور که گفتیم، کلاینت به سرور یک سری پارامترها را ارسال می‌کند و آن پارامترها را هم شرح دادیم. برای دریافت این پارامتر‌ها طرف سرور، احتیاج به یک مدل هست. این مدل به صورت زیر پیاده سازی خواهد شد:
/// <summary>
/// Class that encapsulates most common parameters sent by DataTables plugin
/// </summary>
public class jQueryDataTableParamModel
{
    /// <summary>
    /// Request sequence number sent by DataTable,
    /// same value must be returned in response
    /// </summary>
    public string sEcho { get; set; }
    /// <summary>
    /// Text used for filtering
    /// </summary>
    public string sSearch { get; set; }
    /// <summary>
    /// Number of records that should be shown in table
    /// </summary>
    public int iDisplayLength { get; set; }
    /// <summary>
    /// First record that should be shown(used for paging)
    /// </summary>
    public int iDisplayStart { get; set; }
    /// <summary>
    /// Number of columns in table
    /// </summary>
    public int iColumns { get; set; }
    /// <summary>
    /// Number of columns that are used in sorting
    /// /// </summary>
    public int iSortingCols { get; set; }
    /// <summary>
    /// Comma separated list of column names
    /// </summary>
    public string sColumns { get; set; }
}

مدل بایندر mvc وظیفه مقداردهی به خصوصیات درون این کلاس را بر عهده دارد، بقیه پارامترهایی که به سرور ارسال می‌شوند و در این کلاس نیامده اند، از طریق شیء Request در دسترس خواهند بود.


اکشن متدی که مدل بالا را دریافت می‌کند، می‌تواند به صورت زیر پیاده سازی شود. این اکشن متد وظیفه پاسخ دادن به درخواست DataTables بر اساس پارامترهای ارسال شده در مدل DataTablesParam را دارد. خروجی این اکشن متد شامل پارارمترهای مورد نیاز پلاگین DataTables برای تشکیل جدول است که آنها را هم شرح دادیم.

public JsonResult GetBrowsers(jQueryDataTableParamModel param)
{
        IQueryable<Browser> allBrowsers = new Browsers().CreateInMemoryDataSource().AsQueryable();

        IEnumerable<Browser> filteredBrowsers;

        // Apply Filtering
        if (!string.IsNullOrEmpty(param.sSearch))
        {
                filteredBrowsers = new Browsers().CreateInMemoryDataSource()
                    .Where(x => x.Engine.Contains(param.sSearch)
                                       || x.Grade.Contains(param.sSearch)
                                       || x.Name.Contains(param.sSearch)
                                       || x.Platform.Contains(param.sSearch)
                    ).ToList();
                float f;
                if (float.TryParse(param.sSearch, out f))
                {
                    filteredBrowsers = filteredBrowsers.Where(x => x.Version.Equals(f));
                }
        }
        else
        {
                filteredBrowsers = allBrowsers;
        }

        // Apply Sorting
        var sortColumnIndex = Convert.ToInt32(Request["iSortCol_0"]);
        Func<Browser, string> orderingFunction = (x => sortColumnIndex == 0 ? x.Engine :
                                                            sortColumnIndex == 1 ? x.Name :
                                                            sortColumnIndex == 2 ? x.Platform :
                                                            sortColumnIndex == 3 ? x.Version.ToString() :
                                                            sortColumnIndex == 4 ? x.Grade :
                                                                x.Name);

        var sortDirection = Request["sSortDir_0"]; // asc or desc
        filteredBrowsers = sortDirection == "asc" ? filteredBrowsers.OrderBy(orderingFunction) : filteredBrowsers.OrderByDescending(orderingFunction);

        // Apply Paging
        var enumerable = filteredBrowsers.ToArray();
        IEnumerable<Browser> displayedBrowsers = enumerable.Skip(param.iDisplayStart).
                Take(param.iDisplayLength).ToList();

        return Json(new
        {
                sEcho = param.sEcho,
                iTotalRecords = allBrowsers.Count(),
                iTotalDisplayRecords = enumerable.Count(),
                aaData = displayedBrowsers
        }, JsonRequestBehavior.AllowGet);
}

تشریح اکشن متد GetBrowsers :

این اکشن متد از مدل jQueryDataTableParamModel به عنوان پارامتر ورودی خود استفاده می‌کند. این مدل همان طور هم که گفتیم، شامل یک سری خصوصیت است که توسط پلاگین jQuery DataTables مقداردهی می‌شوند و همچنین مدل بایندر mvc وظیفه بایند کردن این مقادیر به خصوصیات درون این کلاس را بر عهده خواهد داشت. درون بدنه اکشن متد GetBrowsers داده‌ها بعد از اعمال عملیات فیلترینگ، مرتب سازی، و صفحه بندی به فرمت مناسبی درآمده و به طرف مرورگر فرستاده خواهند شد.

برای پیاده سازی کدهای طرف کلاینت نیز، درون یک View کدهای زیر قرار خواهند گرفت:
$(function () {
        var $table = $('#browsers-grid');
        $table.dataTable({
                "bProcessing": true,
                "bStateSave": true,
                "bServerSide": true,
                "bFilter": true,
                "sDom": 'T<"clear">lftipr',
                "aLengthMenu": [[5, 10, 25, 50, -1], [5, 10, 25, 50, "All"]],
                "bAutoWidth": false,
                "sAjaxSource": "/Home/GetBrowsers",
                "fnServerData": function (sSource, aoData, fnCallback) {
                    $.ajax({
                        "dataType": 'json',
                        "type": "POST",
                        "url": sSource,
                        "data": aoData,
                        "success": fnCallback
                    });
                },
                "aoColumns": [
                    { "mDataProp": "Engine" },
                    { "mDataProp": "Name" },
                    { "mDataProp": "Platform" },
                    { "mDataProp": "Version" },
                    { "mDataProp": "Grade" }
                ],
                "oLanguage": {
                        "sUrl": "/Content/dataTables.persian.txt"
                }
        });
});

تشریح کدها:

fnServerData :

این متد، در واقع نحوه تعامل سرور و کلاینت را با استفاده از درخواستهای XHR مشخص خواهد کرد.

oLanguage :

برای فعال سازی زبان فارسی، فیلدهای مورد نیاز ترجمه شده و در یک فایل متنی قرار داده شده اند. کافی است آدرس این فایل متنی به ویژگی oLanguage اختصاص داده شوند.

مثال این قسمت را از لینک زیر دریافت کنید:
DataTablesTutorial04.zip

لازم به ذکر است پوشه bin، obj، و packages جهت کاهش حجم این مثال از solution حذف شده اند. برای اجرای این مثال از اینجا کمک بگیرید.


مطالعه بیشتر

برای مطالعه بیشتر در مورد این پلاگین و نیز پیاده سازی آن در MVC می‌توانید به لینک زیر نیز مراجعه بفرمائید که بعضی از قسمتهای این مطلب هم از مقاله زیر استفاده کرده است:
jQuery DataTables and ASP.NET MVC Integration - Part I