مطالب
مبانی TypeScript؛ جنریک‌ها
بخش عمده‌ای از مهندسی نرم افزار، مربوط به ساخت کامپوننت‌هایی است که نه تنها به خوبی و مستحکم توسعه داده شده‌اند، بلکه قابلیت استفاده دوباره را نیز دارند.
کامپوننت‌هایی که قادر هستند بر روی داده‌های فعلی و همچنین داده‌های آینده، کار کنند، قابلیت‌های انعطاف پذیری را برای ساخت سیستم‌های نرم افزاری بزرگ در اختیار شما قرار خواهند داد.
در زبان هایی نظیر جاوا و سی شارپ، یکی از ابزارهای اصلی برای ساخت کامپوننت‌هایی با قابلیت استفاده مجدد، "جنریک‌ها" میباشد که امکان ساخت کامپوننت‌هایی را می‌دهند که با انواع داده‌های متنوعی به جای یک نوع داده، کار میکنند.
برای شروع به تابع زیر توجه کنید:
function identity(arg: number): number {
    return arg;
}
تابع identity هر آنچه را که به عنوان پارامتر به آرگومان آن ارسال کنیم، بازگشت خواهد داد. میتوانید آن را به مانند دستور "echo" در نظر بگیرید.
بدون استفاده از جنریک ها، باید برای هر نوع داده، یک تابع جدید و یا تابعی را به صورت کلی زیر در نظر بگیریم:
function identity(arg: any): any {
    return arg;
}
در تابع بالا از نوع any استفاده شده است. با استفاده از any، قطعا تابع بالا به صورت عمومی خواهد بود و تمام نوع داده‌ها را به عنوان آرگومان خواهد پذیرفت. ولی در واقع ما اطلاعات مربوط به اینکه نوع داده بازگشتی توسط تابع چه چیزی است را از دست خواهیم داد.
برای مثال اگر یک عدد را به آن ارسال کنیم، تنها متوجه خواهیم شد که نوع آن any میباشد؛ بنابراین به روشی نیاز داریم تا بتوانیم نوع داده آرگومان‌های تابع مورد نظر را کنترل کنیم.
در پیاده سازی زیر، ما از یک type variable خاصی استفاده خواهیم کرد که به جای مقادیر برای انوع داده‌ها مورد استفاده قرار می‌گیرد.
function identity<T>(arg: T): T {
    return arg;
}
در تابع بالا با از T به عنوان یک type variable استفاده کرده‌ایم که امکان گرفتن انواع داده‌هایی را (برای مثال number) که توسط کاربر مهیا میشود، به ما خواهد داد.
این پیاده سازی از تابع identity، تحت عنوان تابع جنریک مطرح می‌شود که برای دامنه‌ی عظیمی از انواع داده‌ها می‌تواند مورد استفاده قرار گیرد و بر خلاف پیاده سازی قبل که از any استفاده کرده‌ایم، در این حالت دیگر اطلاعات نوع داده را از دست نخواهیم داد.
برای استفاده از تابع فوق ما دو روش را پیش رو خواهیم داشت:
  • ارسال تمام آرگومان‌ها که شامل آرگومان نوع داده هم میباشد
let output = identity<string>("myString");  // type of output will be 'string'
در کد بالا ما به صراحت T را با نوع داده string با استفاده از < > مقدار دهی کرده‌ایم.
  • روش دوم که شاید استفاده رایج از توابع جنریک هم هست، استفاده از امکان type argument inference میباشد.
let output = identity("myString");  // type of output will be 'string'
در کد بالا اینبار به صورت صریح نوع T را مشخص نکرده‌ایم و کامپایلر باتوجه به "myString"، نوع T را تعیین خواهد کرد. درحالیکه استفاده از امکان type argument inference خیلی مفید میباشد و کد را خیلی کم حجم و خوانا در اختیار ما قرار میدهد، ولی در مثال‌های پیچیده، امکان این وجود دارد که کامپایلر در تشخیص نوع داده، با خطا مواجه شود. در این صورت استفاده از روش اول مفید خواهد بود.
در ادامه اگر قصد لاگ کردن Length مربوط به آرگومان arg را در هر بار فراخوانی تابع داشته باشیم، می‌بایستی به شکل زیر عمل کنیم:
function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length
    return arg;
}
همانطور که انتظار داشتیم، کامپایلر خطایی مبنی بر نداشتن عضوی تحت عنوان length برای آرگومان arg را نمایش خواهد داد. همانطور که قبلا نیز اشاره کردیم، T جانشینی برای تمام نوع داده‌ها خواهد بود؛ بنابراین در اینجا میتوانیم یک داده‌ی از نوع number را که عضوی بنام length ندارد، هم به این تابع  پاس دهیم.
حال بیایید بگوییم که ما قصد داریم این تابع، با آرایه ای از T کار کند. در این صورت اگر با آرایه‌ها کار کنیم، عضوی به نام length را خواهیم داشت. به پیاده سازی زیر توجه کنید:
function loggingIdentity<T>(arg: T[]): T[] {
    console.log(arg.length);  // Array has a .length, so no more error
    return arg;
}
کد بالا را میتوانیم به این شکل تفسیر کنیم: تابع جنریک loggingIdentity یک type parameter را تحت عنوان T و یک آرگومان را تحت عنوان arg که آرایه ای از T هست، گرفته و آرایه‌ای از T را بازگشت خواهد داد. اگر ما آرایه‌ای از number را به آن پاس دهیم، آرایه‌ای از number‌ها را بازگشت خواهد داد.
در این حالت استفاده از T به عنوان type variable که بخشی از نوع داده‌هایی است که ما با آنها کار میکنیم، به جای پشتیبانی از تمام نوع داده‌ها، انعطاف پذیری بالایی را به ما خواهد داد.
حتی میتوانیم این مثال را به شکل زیر نیز پیاده سازی کنیم:
function loggingIdentity<T>(arg: Array<T>): Array<T> {
    console.log(arg.length);  // Array has a .length, so no more error
    return arg;
}
پیاده سازی بالا خیلی شبیه به پیاده سازی در سایر زبان‌ها هم میباشد.

Generic Types
در این قسمت ما به دنبال یافتن نوع خود توابع بوده و سعی خواهیم کرد اینترفیس‌های جنریک را هم پیاده سازی کنیم. نوع توابع جنریک هم بمانند توابع غیر جنریک میباشند؛ به طوری که می‌توان لیستی از type parameters هایی را که در حالت function declarations موجود هستند، در ابتدا بنویسیم.
function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: <T>(arg: T) => T = identity;
حتی می‌توانیم نام متفاوتی را هم برای type parameter در نظر بگیرم:
function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: <U>(arg: U) => U = identity;
یا حتی می‌توانیم به مانند امضای یک object literal هم کد بالا را بازنویسی کنیم:
function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: {<T>(arg: T): T} = identity;
حال میتوانیم این object literal را به یک اینترفیس منتقل کنیم:
interface GenericIdentityFn {
    <T>(arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn = identity;
کد بالا خوانایی بالاتری را نسبت به حالت قبل دارد و با تعریف یک اینترفیس به نام GenericIdentityFn و انتقال object literal به داخل آن، میتوانیم از نام اینترفیس به جای استفاده مستقیم از object literal، بهره ببریم.
حتی میتوانیم type parameter تابع جنریک خود را هم به اینترفیس منتقل کنیم. 
interface GenericIdentityFn<T> {
    (arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;
باید توجه داشت که پیاده سازی ما کمی متفاوت‌تر از قبل شده است.الان type parameter ما برای کل اعضای اینترفیس قابل رویت میباشد.فهم این مورد که چه زمانی type parameter را در امضای نامیدن داخل اینترفیس یا بر روی خود اینترفیس استفاده کنیم، خود میتوانید برای شرح اینکه کدام وجه‌های یک نوع داده جنریک هستند، مفید باشد.
نکته : امکان تعریف enum‌ها و namespace‌های جنریک وجود ندارد.
 
Generic Classes
تعریف کلاس‌های جنریک هم به مانند اینترفیس‌های جنریک میباشد. به مثال زیر توجه کنید:
class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
در کد بالا، استفاده‌ای واقعی از کلاس GenericNumber قابل مشاهده است. شاید متوجه شده باشید که هیچ محدودیتی برای استفاده‌ی نوع‌ها برای مثال تنها از نوع number در آن نیست و میتوانید از نوع string هم به شکل زیر استفاده کنید:
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };

alert(stringNumeric.add(stringNumeric.zeroValue, "test"));
نکته : برای اعضای استاتیک کلاس نمیتوانید از type parameter کلاس استفاده کنید.
 
Generic Constraints
اگر مثال اخیر را به یاد داشته باشید، شاید بعضی اوقات لازم باشد که یک تابع جنریک را تعریف کنیم تا تنها با مجموعه‌ای از نوع داده‌ها کار کند که اتفاقا از امکانات این مجموعه، آگاهی داریم. در همان مثال loggingIdentity، ما نیاز داشتیم تا به خصوصیت length آرگومان arg دسترسی داشته باشیم و کامپایلر در همان ابتدا، به دلیل اینکه همه نوع داده‌ها از این خصوصیت برخوردار نیستند، خطایی را به ما نشان میدهد.
در ادامه تابعی را پیاده سازی میکنیم که جوابگوی تمام نوع داده‌ها بوده، به شرطی که حداقل خصوصیت length را داشته باشند. لذا باید نیاز خود را در قالب یک محدودیت بر آنچه که T میتواند انجام دهد، فهرست کنیم.
interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // Now we know it has a .length property, so no more error
    return arg;
}
در کد بالا برای توصیف محدودیت خود از یک اینترفیس به نام Lengthwise استفاده کرده‌ایم که فقط یه خصوصیت length را دارد و با استفاده از آن و کلمه‌ی کلیدی extends، محدودیت خود را اعمال کرده ایم.
استفاده از تابع بالا:
loggingIdentity(3);  // Error, number doesn't have a .length property
چون تابع جنریک ما الان محدود میباشد و با تمام نوع داده‌ها کار نخواهد کرد، با خطای بالا روبرو خواهیم شد.
loggingIdentity({length: 10, value: 3});
در عوض مثال بالا، محدودیت ما را به همراه دارد (داشتن خصوصیت length) و بدون هیچ خطایی جواب خواهیم گرفت.

استفاده از Type Parameter‌ها در تعریف محدودیت
در برخی از سناریو‌ها شاید نیاز باشد که یکی از type parameter‌ها توسط دیگری محدود شده باشد. به مثال زیر توجه کنید:
function find<T, U extends Findable<T>>(n: T, s: U) {   // errors because type parameter used in constraint
  // ...
}
find (giraffe, myAnimals);
همانطور که مشخص است، کامپایلر ما را با نشان دادن خطایی متوقف خواهد کرد. چون اجازه‌ی استفاده از type parameter را در اعمال محدودیت، نداریم. در عوض میشود به شکل زیر عمل کرد:
function find<T>(n: T, s: Findable<T>) {
  // ...
}
find(giraffe, myAnimals);
این بار آرگومان s ما باید از نوع <Findable<T باشد که باز هم توانسته‌ایم محدودیت خود را توسط یک type parameter بر آن یکی اعمال کنیم.
نکته : دو پیاده سازی بالا اصلا یکسان نیستند؛ نوع بازگشی در تابع اول میبایستی از نوع U می‌بود، ولی در پیاده سازی دوم اینگونه نیست.(در صورت نبودن خطا)
 
استفاده از کلاس‌ها در جنریک‌ها
زمانی که قصد دارید با استفاده از جنریک‌ها، factory‌ها را پیاده سازی کنید، باید با استفاده از سازنده‌ی کلاس‌ها، به آنها اشاره کنید. به مثال زیر توجه کنید:
function create<T>(c: {new(): T; }): T {
    return new c();
}
تابع بالا به عنوان یک object factory می‌تواند مورد استفاده قرار بگیرد و نکته آن در تعریف نوع آرگومان c میباشد که باز هم به صورت object literal معرفی شده است. اگر در قسمت‌های بالا به یاد داشته باشید، می‌توان این مورد را هم داخل یک اینترفیس گنجاند.
به عنوان یک مثال پیشرفته‌تر هم میتوان به استفاده از prototype property برای استنتاج type parameter‌ها و تحمیل کردن ارتباط بین تابع سازنده و وهله کلاس‌ها، اشاره کرد. به مثال زیر توجه کنید:
class BeeKeeper {
    hasMask: boolean;
}

class ZooKeeper {
    nametag: string;
}

class Animal {
    numLegs: number;
}

class Bee extends Animal {
    keeper: BeeKeeper;
}

class Lion extends Animal {
    keeper: ZooKeeper;
}

function findKeeper<A extends Animal, K> (a: {new(): A;
    prototype: {keeper: K}}): K {

    return a.prototype.keeper;
}
در کد بالا از دو کلاس BeeKeeper و ZooKeeper برای نوع بازگشتی متد‌های موجود در کلاس‌های Bee و Lion استفاده شده‌است. کلاس Animal به عنوان کلاس پایه دو کلاس Bee و Lion که یک خصوصیت numLegs دارد، تعریف شده‌است. از تابع جنریک findKeeper برای مشخص کردن نگهبان مرتبط با Animal ای که به عنوان type parameter توسط A مشخص میشود، استفاده می‌گردد. محدودیتی که بر روی A اعمال شده است نشان دهنده‌ی این است که نوع داده‌ی مورد نظر باید حتما یک Animal باشد و همچنین با اعمال محدودیتی که در قالب object literal مشخص است، تعیین شده است که نوع مورد نظر باید یک کلاس باشد و در نهایت با استفاده از prototype مشخص کرده‌ایم که متدی به نام Keeper آن کلاس، باید نوع برگشتی از نوع K را که به عنوان type parameter مطرح شده‌ی در امضای تابع است، دارا باشد. K نشان دهنده نوع داده بازگشتی این تابع جنریک نیز میباشد.
استفاده از تابع بالا:
findKeeper(Lion).nametag;  // typechecks!
بله همانطور که مشخص است، type parameter‌های مورد نظر به اصطلاح infer شده‌اند و خصوصیت nametag نشان از این دارد که ZooKeeper به صورت خودکار به عنوان نوع داده K تشخیص داده شده است.
مطالب دوره‌ها
استفاده از async و await در برنامه‌های کنسول و سرویس‌های ویندوز NT
فرض کنید می‌خواهید از await در متد Main یک برنامه‌ی کنسول به نحو ذیل استفاده کنید:
using System;
using System.Net;

namespace Async15
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var webClient = new WebClient { })
            {
                webClient.Headers.Add("User-Agent", "AsyncContext 1.0");
                var data = await webClient.DownloadStringTaskAsync("https://www.dntips.ir");
                Console.WriteLine(data);
            }
        }
    }
}
کامپایلر چنین اجازه‌ای را نمی‌دهد. زیرا از await جایی می‌توان استفاده کرد که متد فراخوان آن با async مزین شده باشد و همچنین دارای یک Context باشد تا نتیجه را بتواند دریافت کند. اگر در اینجا سعی کنید async را به امضای متد Main اضافه نمائید، کامپایلر مجددا خطای an entry point cannot be marked with the 'async' modifier را صادر می‌کند.
اضافه کردن واژه‌ی کلیدی async به روال‌های رخدادگردان void برنامه‌های دسکتاپ مجاز است؛ با توجه به اینکه متد async پیش از پایان کار به فراخوان بازگشت داده می‌شوند (ذات متدهای async به این نحو است). در برنامه‌های دسکتاپ، این بازگشت به UI event loop است؛ بنابراین برنامه بدون مشکل به کار خود ادامه خواهد داد. اما در اینجا، بازگشت متد Main، به معنای بازگشت به OS است و خاتمه‌ی برنامه. به همین جهت کامپایلر از async کردن آن ممانعت می‌کند.
برای حل این مشکل در برنامه‌های کنسول و همچنین برنامه‌های سرویس ویندوز NT که دارای یک async-compatible context نیستند، می‌توان از یک کتابخانه‌ی کمکی سورس باز به نام Nito AsyncEx استفاده کرد. برای نصب آن دستور ذیل را در کنسول پاورشل نیوگت وارد کنید:
 PM> Install-Package Nito.AsyncEx
پس از نصب برای استفاده از آن خواهیم داشت:
using System;
using System.Net;
using Nito.AsyncEx;

namespace Async15
{
    class Program
    {
        static void Main(string[] args)
        {
            AsyncContext.Run(async () =>
            {
                using (var webClient = new WebClient())
                {
                    webClient.Headers.Add("User-Agent", "AsyncContext 1.0");
                    var data = await webClient.DownloadStringTaskAsync("https://www.dntips.ir");
                    Console.WriteLine(data);
                }
            });
        }
    }
}
Context ارائه شده در اینجا برخلاف مثال‌های قسمت‌های قبل، نیازی به فراخوانی متد همزمان Wait و یا خاصیت Result که هر دو از نوع blocking هستند ندارد و یک فراخوانی async واقعی است. همچنین می‌شد یک متد async void را نیز در اینجا برای استفاده از DownloadStringTaskAsync تعریف کرد (تا برنامه کامپایل شود). اما پیشتر عنوان شد که هدف از این نوع متدهای خاص async void صرفا استفاده از آن‌ها در روال‌های رخدادگردان UI هستند. زیرا ماهیت آن‌ها fire and forget است و برای دریافت نتیجه‌ی نهایی به نحوی باید ترد اصلی را قفل کرد. برای مثال در یک برنامه‌ی کنسول متد Console.ReadLine را در انتهای کار فراخوانی کرد. اما با استفاده از AsyncContext.Run نیازی به این کارها نیست.


async lambda
در مثال فوق از یک async lambda، برای فراخوانی استفاده شده است که به همراه دات نت 4.5 ارائه شده‌اند:
Action, () => { }
Func<Task>, async () => { await Task.Yield(); }

Func<TResult>, () => { return 13; }
Func<Task<TResult>>, async () => { await Task.Yield(); return 13; }
آرگومان متد AsyncContext.Run از نوع Func of Task است. بنابراین برای مقدار دهی inline آن توسط lambda expressions مطابق مثال‌های فوق می‌توان از async lambda استفاده کرد.
روش دوم استفاده از AsyncContext.Run و مقدار دهی Func of Task، تعریف یک متد مستقل async Task دار، به نحو ذیل است:
class Program
{
  static async Task<int> AsyncMain()
  {
    ..
  }

  static int Main(string[] args)
  {
    return AsyncContext.Run(AsyncMain);
  }
}


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

همانند متد Main که async تعریف کردن آن سبب بازگشت آنی روال کار به OS می‌شود و برنامه خاتمه می‌یابد، روال‌های رخدادگردانی که با طول عمر یک برنامه‌ی UI سر و کار دارند مانند Application_Launching، Application_Closing، Application_Activated و Application_Deactivated (خصوصا در برنامه‌های ویندوز 8) نیز نباید async void تعریف شوند (چون مطابق ذات متدهای async، بلافاصله به برنامه اعلام می‌کنند که کار تمام شد). در این موارد خاص نیز می‌توان از متد AsyncContext.Run برای انجام اعمال async استفاده کرد.
مطالب
پیاده سازی UnitOfWork برای BrightStarDb
 در این پست با BrightStarDb و مفاهیم اولیه آن آشنا شدید. همان طور که پیش‌تر ذکر شد BrightStarDb از تراکنش‌ها جهت ذخیره اطلاعات پشتیبانی می‌کند. قصد داریم روش شرح داده شده در اینجا را بر روی BrightStarDb فعال کنیم. ابتدا بهتر است با روش ساخت مدل در B*Db آشنا شویم.
*یکی از پیش نیاز‌های این پست مطالعه این دو مطلب (^ )  و (^ ) می‌باشد.
فرض می‌کنیم در دیتابیس مورد نظر یک Store به همراه یک جدول به صورت زیر داریم:
[Entity]
    public interface IBook
    {
        [Identifier]
        string Id { get; }

        string Title { get; set; }

        string Isbn { get; set; }
    }
بر روی پروژه مورد نظر کلیک راست کرده و گزینه Add new Item را انتخاب نمایید. از برگه Data  گزینه BrightStar Entity Context را انتخاب کنید

بعد از انخاب گزینه بالا یک فایل با پسوند tt به پروژه اضافه خواهد شد که وظیفه آن جستجو در اسمبلی مورد نظر و پیدا کردن تمام اینترفیس هایی که دارای  EntityAttribute هستند و همچنین ایجاد کلاس‌های متناظر جهت پیاده سازی اینترفیس‌های بالا است. در نتیجه ساختار پروژه تا این جا به صورت زیر خواهد شد.

واضح است که فایلی به نام Book به عنوان پیاده سازی مدل IBook  به عنوان زیر مجموعه فایل DatabaseContext.tt به پروژه اضافه شده است.

تا اینجا برای استفاده از Context مورد نظر باید به صورت زیر عمل نمود:

DatabaseContext context = new DatabaseContext();    
  context.Books.Add(new Book());
Context پیش فرض ساخته شده توسط B*Db از Generic DbSet‌های معادل EF پشتیبانی نمی‌کند و از طرفی IUnitOfWork مورد نظر به صورت زیر است
public interface IUnitOfWork
    {
        BrightstarEntitySet<T> Set<T>() where TEntity : class;
        void DeleteObject(object obj); 
         void SaveChanges();
    }
در اینجا فقط به جای  IDbSet از BrightStarDbSet استفاده شده است. همان طور که در این مقاله توضیح داده شده است، برای پیاده سازی مفهوم UnitOfWork؛ نیاز است تا کلاس DatabaseContext که نماینده BrightStarDbContext پروژه است، از اینترفیس IUnitOfWork طراحی شده ارث بری کند. جهت انجام این مهم  و همچنین جهت اضافه کردن قابلیت ایجاد Generic DbSet‌ها نیز باید کمی در فایل Template Generator تغییر ایجاد نماییم. این تغییرات را قبلا در طی یک پروژه ایجاد کرده‌ام و شما می‌توانید آن را از اینجا دریافت کنید. بعد از دانلود کافیست فایل DatabaseContext.tt مورد نظر را در پروژه خود کپی کرده و گزینه Run Custom Tools را فراخوانی نمایید.

نکته: برای حذف یک آبجکت از Store، باید از متد DeleteObject تعبیه شده در Context استفاده نماییم. در نتیجه متد مورد نظر نیز در اینترفیس بالا در نظر گرفته شده است.

استفاده از IOC Container جهت رجیستر کردن IUnitOfWrok
در این قدم باید IUnitOfWork را در یک IOC container رجیستر کرده تا در جای مناسب عملیات وهله سازی از آن میسر باشد. من در اینجا از Castle Windsor Container استفاده کردم. کلاس زیر این کار را برای ما انجام خواهد داد:
 public class DependencyResolver
    {
        public static void Resolve(IWindsorContainer container)
        {
            var context = new DatabaseContext("type=embedded;storesdirectory=c:\brightstar;storename=test ");
            container.Register(Component.For<IUnitOfWork>().Instance(context).LifestyleTransient());
        }
    }
حال کافیست در کلاس‌های سرویس برنامه UnitOfWork رجیستر شده را به سازنده آن‌ها تزریق نماییم.
public class BookService
    {
        public BookService(IUnitOfWork unitOfWork)
        {
            UnitOfWork = unitOfWork;
        }

        public IUnitOfWork UnitOfWork
        {
            get;
            private set;
        }

        public IList<IBook> GetAll()
        {
            return UnitOfWork.Set<IBook>().ToList();
        }

        public void Add()
        {
            UnitOfWork.Set<IBook>().Add(new Book());
        }

        public void Remove(IBook entity)
        {
            UnitOfWork.DeleteObject(entity);
        }
    }
سایر موارد دقیقا معادل مدل EF آن است.
نکته: در حال حاضر امکان جداسازی مدل‌های برنامه (تعاریف اینترفیس) در قالب یک پروژه دیگر(نظیر مدل CodeFirst در EF) در B*Db امکان پذیر نیست.
نکته : برای اضافه کردن آیتم جدید به Store نیاز به وهله سازی از اینترفیس IBook داریم. کلاس Book ساخته شده توسط DatabaseContext.tt در عملیات Insert و update کاربرد خواهد داشت.

مطالب
OpenCVSharp #9
تغییر اندازه، و چرخش تصاویر

در OpenCV با استفاده از مفهومی به نام affine transform، امکان تغییر اندازه و همچنین چرخش تصاویر میسر می‌شود. در اینجا، تصویر در یک ماتریس دو در سه ضرب می‌شود تا انتقالات یاد شده، انجام شوند.
private static void rotateImage(double angle, double scale, Mat src, Mat dst)
{
    var imageCenter = new Point2f(src.Cols / 2f, src.Rows / 2f);
    var rotationMat = Cv2.GetRotationMatrix2D(imageCenter, angle, scale);
    Cv2.WarpAffine(src, dst, rotationMat, src.Size());
}
متد فوق کار چرخش تصویر مبدا (src) را به تصویر مقصد (dst) انجام می‌دهد. این عملیات توسط متد WarpAffine مدیریت شده و مهم‌ترین پارامتر آن، پارامتر سوم آن است که ماتریس تعریف کننده‌ی انتقالات تعریف شده توسط متد GetRotationMatrix2D است. در اینجا مرکز مشخص شده، زاویه و مقیاس، نحوه‌ی چرخش را تعریف می‌کنند.
برای مشاهده‌ی بهتر تاثیر پارامترهای مختلف در اینجا، به مثال ذیل دقت کنید:
using OpenCvSharp;
using OpenCvSharp.CPlusPlus;
 
namespace OpenCVSharpSample09
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var src = new Mat(@"..\..\Images\Penguin.Png", LoadMode.AnyDepth | LoadMode.AnyColor))
            using (var dst = new Mat())
            {
                src.CopyTo(dst);
 
                using (var window = new Window("Resize/Rotate/Blur",
                                                image: dst, flags: WindowMode.AutoSize))
                {
                    var angle = 0.0;
                    var scale = 0.7;
 
                    var angleTrackbar = window.CreateTrackbar(
                        name: "Angle", value: 0, max: 180,
                        callback: pos =>
                        {
                            angle = pos;
                            rotateImage(angle, scale, src, dst);
                            window.Image = dst;
                        });
 
                    var scaleTrackbar = window.CreateTrackbar(
                        name: "Scale", value: 1, max: 10,
                        callback: pos =>
                        {
                            scale = pos / 10f;
                            rotateImage(angle, scale, src, dst);
                            window.Image = dst;
                        }); 
 
                    angleTrackbar.Callback.DynamicInvoke(0);
                    scaleTrackbar.Callback.DynamicInvoke(1);
 
                    Cv2.WaitKey();
                }
            }
        }
 
        private static void rotateImage(double angle, double scale, Mat src, Mat dst)
        {
            var imageCenter = new Point2f(src.Cols / 2f, src.Rows / 2f);
            var rotationMat = Cv2.GetRotationMatrix2D(imageCenter, angle, scale);
            Cv2.WarpAffine(src, dst, rotationMat, src.Size());
        }
    }
}
با این خروجی:


در این مثال، مانند مطلب قسمت قبل، ابتدا یک پنجره‌ی سازگار با C++ API ایجاد شده و سپس دو tracker به آن اضافه شده‌اند. این trackers کار دریافت ورودی اطلاعات را از کاربر به عهده دارند (دریافت مقادیر زاویه‌ی چرخش و مقیاس) و مقادیر دریافتی از آن‌ها، در نهایت به متد rotateImage ارسال می‌شوند. این متد کار چرخش و تغییر مقیاس تصویر اصلی را انجام داده و نتیجه را به تصویر dst کپی می‌کند. در آخر تصویر dst در پنجره به روز شده و نمایش داده می‌شود.


تغییر اندازه‌ی تصاویر

اگر صرفا قصد تغییر اندازه‌ی تصاویر را دارید (بدون چرخش آن‌ها)، متد ویژه‌ای به نام Resize برای این منظور تدارک دیده شده‌است:
var resizeTrackbar = window.CreateTrackbar(
    name: "Resize", value: 1, max: 100,
    callback: pos =>
    {
        Cv2.Resize(src, dst,
            new Size(src.Width + pos, src.Height + pos),
            interpolation: Interpolation.Cubic);
        window.Image = dst;
    });
در اینجا یک tracker دیگر به پنجره‌ی اصلی اضافه شده و توسط آن کار تعیین تغییر اندازه‌ی تصویر انجام می‌شود. نکته‌ی مهم این متد، امکان تعیین الگوریتم تغییر اندازه است که برای مثال در اینجا از Interpolation.Cubic استفاده شده‌است (احتمالا با این نام‌ها در برنامه‌های معروف کار با تصاویر، مانند فتوشاپ آشنایی دارید).

اگر می‌خواهید مقادیر پارامترهای چرخشی تصویر نیز در اینجا اعمال شوند، می‌توان به نحو ذیل عمل کرد:
var resizeTrackbar = window.CreateTrackbar(
    name: "Resize", value: 1, max: 100,
    callback: pos =>
    {
        rotateImage(angle, scale, src, dst);
        Cv2.Resize(dst, dst,
            new Size(src.Width + pos, src.Height + pos),
            interpolation: Interpolation.Cubic);
        window.Image = dst;
    });
در این کد ابتدا تصویر اصلی چرخش یافته و سپس در متد Resize از این تصویر چرخش یافته، به عنوان src استفاده می‌شود (هر دو پارامتر متد Resize به dst تنظیم شده‌اند).



مات کردن تصاویر

در OpenCV با استفاده از متدهای GaussianBlur و یا medianBlur ، می‌توان تصاویر را  مات کرد که نمونه‌ای از آن‌را در ادامه ملاحظه می‌کنید:
var blurTrackbar = window.CreateTrackbar(
   name: "Blur", value: 1, max: 100,
   callback: pos =>
   {
       if (pos % 2 == 0) pos++;
 
       rotateImage(angle, scale, src, dst);
       Cv2.GaussianBlur(dst, dst, new Size(pos, pos), sigmaX: 0);
       window.Image = dst;
   });
در اینجا ابتدا تصویر اصلی به متد چرخش تصویر ارسال شده و نتیجه‌ی آن در متد GaussianBlur استفاده خواهد شد. اندازه‌ی مشخص شده‌ی در این متد باید توسط اعداد فرد تعیین گردد. پارامتر sigmaX به معنای standard deviation در جهت x است و اگر صفر تعیین شود، برای محاسبه‌ی آن از پارامتر اندازه‌ی تعیین شده کمک گرفته خواهد شد.



کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید.
مطالب
شروع به کار با EF Core 1.0 - قسمت 13 - بررسی سیستم ردیابی تغییرات
هر Context در EF Core، دارای خاصیتی است به نام ChangeTracker که وظیفه‌ی آن ردیابی تغییراتی است که نیاز است به بانک اطلاعاتی منعکس شوند. برای مثال زمانیکه توسط یک کوئری، شیءایی را باز می‌گردانید و سپس مقدار یکی از خواص آن‌را تغییر داده و متد SaveChanges را فراخوانی می‌کنید، این ChangeTracker است که به EF اعلام می‌کند، کوئری Update ایی را که قرار است تولید کنی، فقط نیاز است یک خاصیت را به روز رسانی کند؛ آن هم تنها با این مقدار تغییر یافته.

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

متد DbSet.Add کار اطلاع رسانی تبدیل وهله‌‌های ثبت شده را به کوئری‌های Insert رکوردهای جدید، انجام می‌دهد:
using (var db = new BloggingContext())
{
   var blog = new Blog { Url = "http://sample.com" };
   db.Blogs.Add(blog);
   db.SaveChanges();
}

سیستم ردیابی اطلاعات، اگر تغییراتی را در خواص اشیاء تحت نظر خود مشاهده کند، سبب تولید کوئری‌های Update می‌گردد. یک چنین اشیایی تحت نظر Context هستند:
الف) اشیایی که در طول عمر Context از دیتابیس کوئری گرفته شده‌اند.
ب) اشیایی که در طول عمر Context به آن اضافه شده‌اند (حالت قبل).
using (var db = new BloggingContext())
{
  var blog = db.Blogs.First();
  blog.Url = "http://sample.com/blog";
  db.SaveChanges();
}

و متد DbSet.Remove کار اطلاع رسانی تبدیل وهله‌های حذف شده را به کوئری‌های Delete معادل، انجام می‌دهد:
using (var db = new BloggingContext())
{
  var blog = db.Blogs.First();
  db.Blogs.Remove(blog);
  db.SaveChanges();
}
اگر شیء حذف شده پیشتر توسط متد DbSet.Add اضافه شده باشد، تنها این شیء از Context حذف می‌شود و کوئری در مورد آن تولید نخواهد شد.

به علاوه امکان ترکیب متدهای Add، Remove و همچنین به روز رسانی اشیاء در طی یک Context و با فراخوانی یک SaveChanges در انتهای کار نیز وجود دارد. از این جهت که یک Context، الگوی واحد کار را پیاده سازی می‌کند و بیانگر یک تراکنش است. در این حالت ترکیبی، یا کل تراکنش با موفقیت به پایان می‌رسد و یا در صورت بروز مشکلی، هیچکدام از تغییرات درخواستی، اعمال نخواهند شد.


عملیات ردیابی، بر روی هر نوع Projections صورت نمی‌گیرد

اگر توسط LINQ Projections، نتیجه‌ی نهایی کوئری را تغییر دادید، فقط در زمانی سیستم ردیابی بر روی آن فعال خواهد بود که projection نهایی حاوی اصل موجودیت مدنظر باشد. برای مثال در کوئری ذیل چون در Projection صورت گرفته‌ی در متد Select، هنوز در خاصیت Blog، به اصل موجودیت Blog اشاره می‌شود، نتیجه‌ی این کوئری نیز تحت نظر سیستم ردیابی خواهد بود:
using (var context = new BloggingContext())
{
   var blog = context.Blogs
      .Select(b =>
            new
            {
               Blog = b,
               Posts = b.Posts.Count()
            });
 }
اما در کوئری ذیل، خیر:
using (var context = new BloggingContext())
{
   var blog = context.Blogs
            .Select(b =>
                 new
                 {
                   Id = b.BlogId,
                   Url = b.Url
                 });
 }
در اینجا در Projection انجام شده، نتیجه‌ی نهایی، به هیچکدام از موجودیت‌های ممکن اشاره نمی‌کند. بنابراین نتیجه‌ی آن تحت نظر سیستم ردیابی قرار نمی‌گیرد.


لغو سیستم ردیابی تغییرات، در زمانیکه به آن نیازی نیست

سیستم ردیابی تغییرات بر اساس مفاهیم AOP و تولید پروکسی‌های آن کار می‌کند. این پروکسی‌ها، اشیایی شفاف هستند که اشیاء شما را احاطه می‌کنند و هر تغییری را که اعمال می‌کنید، ابتدا از این غشاء رد شده و در سیستم ردیابی EF ثبت می‌شوند. سپس به وهله‌ی اصلی شیء موجود اعمال خواهند شد.
بدیهی است تولید این پروکسی‌ها، دارای سربار است و اگر هدف شما صرفا کوئری گرفتن از اطلاعات، جهت نمایش آن‌ها است، نیازی به تولید خودکار این پروکسی‌ها را ندارید و این مساله سبب کاهش مصرف حافظه‌ی برنامه و بالا رفتن سرعت آن می‌شود.
در قسمت قبل عنوان شد که «یک چنین اشیایی تحت نظر Context هستند: الف) اشیایی که در طول عمر Context از دیتابیس کوئری گرفته شده‌اند.»
اگر می‌خواهید این حالت پیش فرض را لغو کنید، از متد AsNoTracking استفاده نمائید:
using (var context = new BloggingContext())
{
  var blogs = context.Blogs.AsNoTracking().ToList();
}
یک چنین کوئری‌هایی برای سناریوهای فقط خواندنی (گزارشگیری‌ها) مناسب هستند و بدیهی است هرگونه تغییری در لیست blogs حاصل، توسط context جاری ردیابی نشده و در نهایت به بانک اطلاعاتی (در صورت فراخوانی SaveChanges) اعمال نمی‌گردد.

اگر می‌خواهید متد AsNoTracking را به صورت خودکار به تمام کوئری‌های یک context خاص اعمال کنید، روش کار و تنظیم آن به صورت زیر است:
using (var context = new BloggingContext())
{
    context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;


نکات به روز رسانی ارجاعات موجودیت‌ها

دو حالت زیر را درنظر بگیرید که در اولی، blog از بانک اطلاعاتی واکشی شده‌است و post به صورت مستقیم وهله سازی شده‌است:
using (var context = new BloggingContext())
{
  var blog = context.Blogs.First();
  var post = new Post { Title = "Intro to EF Core" };

  blog.Posts.Add(post);
  context.SaveChanges();
}
و در دومی blog به صورت مستقیم وهله سازی گردیده‌است و post از بانک اطلاعاتی واکشی شده‌است:
using (var context = new BloggingContext())
{
  var blog = new Blog { Url = "http://blogs.msdn.com/visualstudio" };
  var post = context.Posts.First();

  blog.Posts.Add(post);
  context.SaveChanges();
}
در حالت اول، Post، ابتدا به بانک اطلاعاتی اضافه شده و سپس این مطلب جدید به لیست ارجاعات blog اضافه می‌شود (Post جدیدی اضافه شده و اولین Blog، جهت درج آن به روز رسانی می‌شود).
در حالت دوم، ابتدا blog در بانک اطلاعاتی ثبت می‌شود (چون برخلاف حالت اول، تحت نظر context نیست) و سپس این post (که تحت نظر context است) به مجموعه مطالب آن اضافه می‌شود (بلاگ جدیدی اضافه شده و ارجاع مطلب موجودی به آن اضافه می‌شود).


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

در مثال قبل مشاهده کردیم که اگر موجودیتی تحت نظر context نباشد (برای مثال توسط یک کوئری به context وارد نشده باشد)، در حین ذخیره سازی ارجاعات، با آن به صورت یک وهله‌ی جدید رفتار شده و حتما در بانک اطلاعاتی به صورت یک رکورد جدید ذخیره می‌شود؛ حتی اگر Id آن‌را دستی تنظیم کرده باشید که ندید گرفته خواهد شد.
اگر Id و سایر اطلاعات شیءایی را دارید، نیازی نیست تا حتما توسط یک کوئری ابتدا آن‌را از بانک اطلاعاتی دریافت و سپس به صورت خودکار وارد سیستم ردیابی کنید؛ متد Attach نیز یک چنین کاری را انجام می‌دهد:
 var blog = new Blog { Id = 2, Url = "https://www.dntips.ir" };
 context.Blog.Attach(blog);
 context.SaveChanges();
در اینجا هرچند شیء Blog از بانک اطلاعاتی واکشی نشده‌است، اما چون توسط متد Attach به DbSet اضافه شده‌است، اکنون جزئی از اشیاء تحت نظر به حساب می‌آید؛ اما با یک شرط. حالت اولیه‌ی این شیء به EntityState.Unchanged تنظیم شده‌است. یعنی زمانیکه SaveChanges فراخوانی می‌شود، عملیات خاصی صورت نخواهد گرفت و هیچ اطلاعاتی در بانک اطلاعاتی درج نمی‌گردد.
علاوه بر متد Attach، متد AttachRange نیز برای افزودن لیستی از موجودیت‌ها در حالت EntityState.Unchanged، پیش بینی شده‌است.

روش دیگر انجام اینکار به صورت ذیل است:
در اینجا ابتدا یک وهله‌ی جدید از Blog ایجاد شده‌است و سپس توسط متد Entry به Context وارد شده و همچنین حالت آن به صورت صریح، به تغییر یافته، مشخص گردیده‌است:
 var blog = new Blog { Id = 2, Url = "https://www.dntips.ir" };
 context.Entry(blog).State = EntityState.Modified ;
 context.SaveChanges();
و یا می‌توان این عملیات را به صورت زیر ساده کرد:
 var blog = new Blog { Id = 2, Url = "https://www.dntips.ir" };
 context.Update(blog);
 context.SaveChanges();
در اینجا متد جدید Update، همان کار Attach و سپس تنظیم حالت را به EntityState.Modified انجام می‌دهد.
به علاوه متد UpdateRange نیز برای افزودن لیستی از موجودیت‌ها در حالت EntityState.Modified، پیش بینی شده‌است.

یک نکته: متدهای Attach و Update، هم بر روی یک DbSet و هم بر روی Context، قابل اجرا هستند. اگر بر روی Context اجرا شدند، نوع موجودیت دریافتی به نوع DbSet متناظر به صورت خودکار نگاشت شده و استفاده می‌شود (context.Set<T>().Attach(entity)). یعنی در حقیقت بین این دو حالت تفاوتی نیست و امکان فراخوانی این متدها بر روی Context، صرفا جهت سهولت کار درنظر گرفته شده‌است.


تفاوت رفتار context.Entry در EF Core با EF 6.x

متد  context.Entry در EF 6.x هم وجود دارد. اما در EF core سبب تغییر وضعیت گراف متصل به یک شیء نمی‌شود و ضعیت روابط آن‌را به روز رسانی نمی‌کند (برخلاف EF 6.x). اگر در EF Core نیاز به یک چنین به روز رسانی گراف مانندی را داشتید، باید از متد جدید context.ChangeTracker.TrackGraph به نحو ذیل استفاده نمائید:
 context.ChangeTracker.TrackGraph(blog, e => e.Entry.State = EntityState.Added);


کوئری گرفتن از سیستم ردیابی اطلاعات

این سناریوها را درنظر بگیرید:
 - می‌خواهم سیستمی شبیه به تریگرهای اس کیوال سرور را با EF داشته باشم.
 - می‌خواهم اطلاعات تمام رکوردهای ثبت شده، حذف شده و به روز رسانی شده را لاگ کنم.
 - می‌خواهم پس از ثبت رکوردی در هر جای برنامه، شبیه به مباحث SQL Server Service Broker و SqlDependency بلافاصله مطلع شده و توسط SignalR اطلاع رسانی کنم.

و در حالت کلی می‌خواهم پیش و یا پس از ثبت اطلاعات، بتوانم به تغییرات صورت گرفته دسترسی داشته باشم و عملیاتی را بر روی آن‌ها انجام دهم. تمام این موارد و سناریوها را با کوئری گرفتن از سیستم ردیابی اطلاعات EF می‌توان پیاده سازی کرد.
برای نمونه در مطلب قبل و قسمت «طراحی یک کلاس پایه، بدون تنظیمات ارث بری روابط»، یک کلاس پایه را که مقادیر پیش فرض خود را از SQL Server دریافت می‌کند، طراحی کردیم. در اینجا می‌خواهیم با استفاده از سیستم ردیابی EF، طراحی این کلاس پایه را عمومی کرده و سازگار با تمام بانک‌های اطلاعاتی موجود کنیم.
جهت یادآوری، کلاس پایه موجودیت‌ها، یک چنین شکلی را داشته:
public class BaseEntity
{
   public int Id { set; get; }
   public DateTime? DateAdded { set; get; }
   public DateTime? DateUpdated { set; get; }
}
و پس از آن، هر موجودیت برنامه به این شکل خلاصه شده و نشانه گذاری می‌شود:
public class Person : BaseEntity
{
   public string FirstName { get; set; }
   public string LastName { get; set; }
}
اکنون به کلاس Context برنامه مراجعه کرده و متد SaveChanges آن‌را بازنویسی می‌کنیم:
    public class ApplicationDbContext : DbContext
    {
        // same as before 

        public override int SaveChanges()
        {
            this.ChangeTracker.DetectChanges();

            var modifiedEntries = this.ChangeTracker
                                      .Entries<BaseEntity>()
                                      .Where(x => x.State == EntityState.Modified);
            foreach (var modifiedEntry in modifiedEntries)
            {
                modifiedEntry.Entity.DateUpdated = DateTime.UtcNow;
            }
 
            var addedEntries = this.ChangeTracker
                                      .Entries<BaseEntity>()
                                      .Where(x => x.State == EntityState.Added);
            foreach (var addedEntry in addedEntries)
            {
                addedEntry.Entity.DateAdded = DateTime.UtcNow;
            }
 
            return base.SaveChanges();
        }
    }
این متد SaveChanges، نقطه‌ی مشترک تمام تغییرات برنامه است. به همین دلیل است که اینجا را می‌توان جهت اعمالی، پیش و پس از فراخوانی متد اصلی base.SaveChanges که کار نهایی درج تغییرات را به بانک اطلاعاتی انجام می‌دهد، مورد استفاده قرار داد.
در اینجا کار با کوئری گرفتن از خاصیت ChangeTracker شروع می‌شود. سپس باید مشخص کنیم چه نوع موجودیت‌هایی را مدنظر داریم. چون تمام موجودیت‌های ما از کلاس پایه‌ی BaseEntity مشتق می‌شوند، بنابراین کوئری گرفتن بر روی این نوع، به معنای دسترسی به تمام موجودیت‌های برنامه نیز هست. سپس در اینجا اگر حالتی EntityState.Modified بود، فقط مقدار خاصیت DateUpdated را به صورت خودکار مقدار دهی می‌کنیم و اگر حالتی EntityState.Added بود، تنها مقدار خاصیت DateAdded را به روز رسانی خواهیم کرد.
در یک چنین حالتی دیگر نیازی نیست تا مقادیر این خواص را در حین ثبت اطلاعات برنامه به صورت دستی مشخص کنیم.

یک نکته: اگر به ابتدای متد بازنویسی شده دقت کنید، فراخوانی متد this.ChangeTracker.DetectChanges در آن انجام شده‌است. علت اینجا است که این فراخوانی به صورت خودکار توسط متد base.SaveChanges انجام می‌شود، اما چون این مرحله را تا انتهای متد بازنویسی شده، به تاخیر انداخته‌ایم، نیاز است خودمان به صورت دستی سبب محاسبه‌ی مجدد تغییرات صورت گرفته شویم.

نکته‌ای در مورد بهبود کیفیت کدهای متد SaveChanges: استفاده‌ی Change Tracker به این صورت با بازنویسی متد SaveChanges بسیار مرسوم است. اما پس از مدتی به متد SaveChanges ایی خواهید رسید که کنترل آن از دست خارج می‌شود. به همین جهت برای EF 6.x پروژه‌هایی مانند EFHooks طراحی شده‌اند تا کپسوله سازی بهتری را بتوان ارائه داد. انتقال کدهای آن به EF Core کار مشکلی نیست و اصل آن، بازنویسی HookedDbContext آن است که نحوه‌ی مدیریت شکیل‌تر کوئری گرفتن از ChangeTracker را بیان می‌کند.


خواص سایه‌ای یا Shadow properties

EF Core به همراه مفهوم کاملا جدیدی است به نام خواص سایه‌ای. این نوع خواص در سمت کدهای ما و در کلاس‌های موجودیت‌های برنامه وجود خارجی نداشته، اما در سمت جداول بانک اطلاعاتی وجود دارند و اکنون امکان کوئری گرفتن و کار کردن با آن‌ها در EF Core میسر شده‌است.
برای تعریف آن‌ها، بجای افزودن خاصیتی به کلاس‌های برنامه، کار از متد OnModelCreating به نحو ذیل شروع می‌شود:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
  modelBuilder.Entity<Blog>().Property<DateTime>("DateAdded");
در اینجا یک خاصیت جدید به نام DateAdded، از نوع DateTime که در کلاس Blog وجود خارجی ندارد، تعریف شده‌است. به این خاصیت، shadow property می‌گویند (سایه‌ای است از ستونی از جدول بلاگ).
سپس برای کار کردن و کوئری گرفتن از آن می‌توان از متد جدید EF.Property، به نحو ذیل استفاده کرد:
 var blogs = context.Blogs.OrderBy(b => EF.Property<DateTime>(b, "DateAdded"));
همچنین برای مقدار دهی آن تنها می‌توان توسط سیستم Change Tracker اقدام نمود:
 context.Entry(myBlog).Property("DateAdded").CurrentValue = DateTime.Now;
و یا در همان قطعه کد بازنویسی متد SaveChanges فوق، نحوه‌ی دسترسی به اینگونه خواص، به صورت زیر می‌باشد:
foreach (var addedEntry in addedEntries)
{
  addedEntry.Property("DateAdded").CurrentValue = DateTime.UtcNow;
}
مهم‌ترین دلیل وجودی این خواص، پیاده سازی روابطی مانند many-to-many، در نگارش‌های بعدی EF Core هستند. در حقیقت جدول واسطی که در اینجا به صورت خودکار تشکیل می‌شود، به همراه خواصی است که تاکنون امکان دسترسی به آن‌ها در کدهای EF وجود نداشت؛ اما Shadow properties این امر را میسر می‌کنند (فیلدهایی که در سمت بانک اطلاعاتی وجود دارند، اما در کدهای کلاس‌های ما، خیر).
مطالب
نوشتن پرس و جو در Entity Framework‌ با استفاده از LINQ To Entity قسمت اول

موجودیت‌های زیر را در نظر بگیرید: 

public class Customer
{
    public Customer()
    {
        Orders = new ObservableCollection<Order>();
    }
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Family { get; set; }

    public string FullName
    {
        get
        {
            return Name + " " + Family;
        }
    }

    public virtual IList<Order> Orders { get; set; }
}
public class Product
{
    public Product()
    {
    }

    public Guid Id { get; set; }
    public string Name { get; set; }
    public int Price { get; set; }
}
public class OrderDetail
{
    public Guid Id { get; set; }
    public Guid ProductId { get; set; }
    public int Count { get; set; }
    public Guid OrderId { get; set; }
    public int Price { get; set; }

    public virtual Order Order { get; set; }
    public virtual Product Product { get; set; }

    public string ProductName
    {
        get
        {
            return Product != null ? Product.Name : string.Empty;
        }
    }
}
public class Order
{
    public Order()
    {
        OrderDetail = new ObservableCollection<OrderDetail>();
    }
    public Guid Id { get; set; }
    public DateTime Date { get; set; }

    public Guid CustomerId { get; set; }
    public virtual Customer Customer { get; set; }
    public virtual IList<OrderDetail> OrderDetail { get; set; }

    public string CustomerFullName
    {
        get
        {
            return Customer == null ? string.Empty : Customer.FullName;
        }
    }

    public int TotalPrice
    {
        get
        {
            if (OrderDetail == null)
                return 0;

            return
                OrderDetail.Where(orderdetail => orderdetail.Product != null)
                .Sum(orderdetail => orderdetail.Price*orderdetail.Count);
        }
    }
}

و نگاشت موجودیت ها: 

public class CustomerConfiguration : EntityTypeConfiguration<Customer>
{
    public CustomerConfiguration()
    {
        HasKey(c => c.Id);
        Property(c => c.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
    }
}
public class ProductConfiguration : EntityTypeConfiguration<Product>
{
    public ProductConfiguration()
    {
        HasKey(p => p.Id);
        Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
    }
}
public class OrderDetailConfiguration : EntityTypeConfiguration<OrderDetail>
{
    public OrderDetailConfiguration()
    {
        HasKey(od => od.Id);
        Property(od => od.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
    }
}
public class OrderConfiguration: EntityTypeConfiguration<Order>
{
    public OrderConfiguration()
    {
        HasKey(o => o.Id);
        Property(o => o.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
    }
}

و برای معرفی موجودیت‌ها به Entity Framwork کلاس StoreDbContext را به صورت زیر تعریف می‌کنیم:

public class StoreDbContext : DbContext
{
    public StoreDbContext()
        : base("name=StoreDb")
    {
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new CustomerConfiguration());
        modelBuilder.Configurations.Add(new OrderConfiguration());
        modelBuilder.Configurations.Add(new OrderDetailConfiguration());
        modelBuilder.Configurations.Add(new ProductConfiguration());
    }

    public DbSet<Customer> Customers { get; set; }
    public DbSet<Product> Products { get; set; }
    public DbSet<Order> Orders { get; set; }
    public DbSet<OrderDetail> OrderDetails { get; set; }
}

جهت مقدار دهی اولیه به database تستی یک DataBaseInitializer به صورت زیر تعریف می‌کنیم:

public class MyTestDb : DropCreateDatabaseAlways<StoreDbContext>
{
    protected override void Seed(StoreDbContext context)
    {
        var customer1 = new Customer { Name = "Vahid", Family = "Nasiri" };
        var customer2 = new Customer { Name = "Mohsen", Family = "Jamshidi" };
        var customer3 = new Customer { Name = "Mohsen", Family = "Akbari" };

        var product1 = new Product {Name = "CPU", Price = 350000};
        var product2 = new Product {Name = "Monitor", Price = 500000};
        var product3 = new Product {Name = "Keyboard", Price = 30000};
        var product4 = new Product {Name = "Mouse", Price = 20000};
        var product5 = new Product {Name = "Power", Price = 70000};
        var product6 = new Product {Name = "Hard", Price = 250000};

        var order1 = new Order
        {
            Customer = customer1, Date = new DateTime(2013, 1, 1),
            OrderDetail = new List<OrderDetail>
            {
                new OrderDetail {Product = product1, Count = 1, Price = product1.Price},
                new OrderDetail {Product = product2, Count = 1, Price = product2.Price},
                new OrderDetail {Product = product3, Count = 1, Price = product3.Price},
            }
        };

        var order2 = new Order
        {
            Customer = customer1,
            Date = new DateTime(2013, 1, 5),
            OrderDetail = new List<OrderDetail>
            {
                new OrderDetail {Product = product1, Count = 2, Price = product1.Price},
                new OrderDetail {Product = product3, Count = 4, Price = product3.Price},
            }
        };

        var order3 = new Order
        {
            Customer = customer1,
            Date = new DateTime(2013, 1, 9),
            OrderDetail = new List<OrderDetail>
            {
                new OrderDetail {Product = product1, Count = 4, Price = product1.Price},
                new OrderDetail {Product = product3, Count = 5, Price = product3.Price},
                new OrderDetail {Product = product5, Count = 6, Price = product5.Price},
            }
        };

        var order4 = new Order
        {
            Customer = customer2,
            Date = new DateTime(2013, 1, 9),
            OrderDetail = new List<OrderDetail>
            {
                new OrderDetail {Product = product4, Count = 1, Price = product4.Price},
                new OrderDetail {Product = product3, Count = 1, Price = product3.Price},
                new OrderDetail {Product = product6, Count = 1, Price = product6.Price},
            }
        };

        var order5 = new Order
        {
            Customer = customer2,
            Date = new DateTime(2013, 1, 12),
            OrderDetail = new List<OrderDetail>
            {
                new OrderDetail {Product = product4, Count = 1, Price = product4.Price},
                new OrderDetail {Product = product5, Count = 2, Price = product5.Price},
                new OrderDetail {Product = product6, Count = 5, Price = product6.Price},
            }
        };

        context.Customers.Add(customer3);

        context.Orders.Add(order1);
        context.Orders.Add(order2);
        context.Orders.Add(order3);
        context.Orders.Add(order4);
        context.Orders.Add(order5);

        context.SaveChanges();
    }

و در ابتدای برنامه کد زیر را جهت مقداردهی اولیه به Database مان قرار می‌دهیم:

Database.SetInitializer(new MyTestDb());

در انتها ConnectionString را در App.Config به صورت زیر تعریف می‌کنیم:

<connectionStrings>
    <add name="StoreDb" connectionString="Data Source=.\SQLEXPRESS;
Initial Catalog=StoreDBTest;Integrated Security = true" providerName="System.Data.SqlClient"/>
</connectionStrings>

بسیار خوب، حالا همه چیز محیاست برای اجرای اولین پرس و جو:

using (var context = new StoreDbContext())
{
    var query = context.Customers;

    foreach (var customer in query)
    {
        Console.WriteLine("Customer Name: {0}, Customer Family: {1}", 
                                  customer.Name, customer.Family);
    }
}

پرس و جوی تعریف شده لیست تمام Customer‌ها را باز می‌گرداند. query فقط یک "عبارت" پرس و جو هست و زمانی اجرا می‌شود که از آن درخواست نتیجه شود. در مثال بالا این درخواست در اجرای حلقه foreach اتفاق می‌افتد و درست در این لحظه است که دستور SQL ساخته شده و به Database فرستاده می‌شود. EF در این حالت تمام داده‌ها را در یک لحظه باز نمی‌گرداند بلکه این ارتباط فعال است تا حلقه به پایان برسد و تمام داده‌ها از database واکشی شود. خروجی به صورت زیر خواهد بود:

Customer Name: Vahid, Customer Family: Nasiri
Customer Name: Mohsen, Customer Family: Jamshidi
Customer Name: Mohsen, Customer Family: Akbari

نکته: با هر بار درخواست نتیجه از query ، پرس و جوی مربوطه دوباره به database فرستاده می‌شود که ممکن است مطلوب ما نباشد و باعث افت سرعت شود. برای جلوگیری از تکرار این عمل کافیست با استفاده از متد ToList پرس و جو را در لحظه تعریف به اجرا در آوریم
var customers = context.Customers.ToList();

خط بالا دیگر یک عبارت پرس و جو نخواهد بود بلکه لیست تمام Customer هاست که به یکباره از database بازگشت داده شده است. در ادامه هرجا که از customers استفاده کنیم دیگر پرس و جویی به database فرستاده نخواهد شد.

پرس و جوی زیر مشتریهایی که نام آنها Mohsen هست را باز می‌گرداند:

private static void Query3()
{
    using (var context = new StoreDbContext())
    {
        var methodSyntaxquery = context.Customers
                   .Where(c => c.Name == "Mohsen");
        var sqlSyntaxquery = from c in context.Customers
                             where c.Name == "Mohsen"
                             select c;

        foreach (var customer in methodSyntaxquery)
        {
            Console.WriteLine("Customer Name: {0}, Customer Family: {1}", 
                                      customer.Name, customer.Family);
        }
    }

    // Output:
    // Customer Name: Mohsen, Customer Family: Jamshidi
    // Customer Name: Mohsen, Customer Family: Akbari
}

همانطور که مشاهده می‌کنید پرس و جو به دو روش Method Syntax و Sql Syntax نوشته شده است.

روش Method Syntax روشی است که از متدهای الحاقی (Extention Method)  ‌و عبارت‌های لامبدا (Lambda Expersion) برای نوشتن پرس و جو استفاده می‌شود. اما #C روش Sql Syntax را که همانند دستورات SQL هست، نیز فراهم کرده است تا کسانیکه آشنایی با این روش دارند، از این روش استفاده کنند. در نهایت این روش به Method Syntax تبدیل خواهد شد بنابراین پیشنهاد می‌شود که از همین روش استفاده شود تا با دست و پنجه نرم کردن با این روش، از مزایای آن در بخشهای دیگر کدنویسی استفاده شود.

اگر به نوع Customers که در DbContext تعریف شده است، دقت کرده باشید، خواهید دید که DbSet می‌باشد. DbSet کلاس و اینترفیس‌های متفاوتی را پیاده سازی کرده است که در ادامه با آنها آشنا خواهیم شد:

  • IQueryable<TEntity>, IEnumerable<TEntity>, IQueryable, IEnumerable: که امکان استفاده از متدهای نام آشنای LINQ را برای ما فراهم می‌کند. البته فراموش نشود که EF از Provider ای با نام LINQ To Entity برای تفسیر پرس و جوی ما و ساخت دستور SQL متناظر آن استفاده می‌کند. بنابراین تمامی متدهایی که در LINQ To Object استفاده می‌شوند در اینجا قابل استفاده نیستند. بطور مثال اگر در پرس و جو از LastOrDefault روی Customer استفاده شود در زمان اجرا با خطای زیر مواجه خواهیم شد و در نتیجه در استفاده از این متدها به این مسئله باید دقت شود. 
LINQ to Entities does not recognize the method 'Store.Model.Customer LastOrDefault[Customer](System.Linq.IQueryable`1[Store.Model.Customer], System.Linq.Expressions.Expression`1[System.Func`2[Store.Model.Customer,System.Boolean]])' method, and this method cannot be translated into a store expression.
  • <IDbSet<TEntity: که دارای متدهای Add, Attach, Create, Find, Remove, Local می‌باشد و برای بحث ما Find و Local جهت ساخت پرس و جو استفاده می‌شوند که  در ادامه توضیح داده خواهند شد.
  • <DbQuery<TEntity: که دارای متدهای AsNoTracking و Include می‌باشد و در ادامه توضیح داده خواهند شد.

متد Find: این متد کلید اصلی را به عنوان ورودی گرفته و برای بازگرداندن نتیجه مراحل زیر را طی می‌کند:
  1. داده‌های موجود در حافظه را بررسی می‌کند یعنی آنهایی که Load و یا Attach شده اند.
  2. داده هایی که به DbContext اضافه (Add) ولی هنوز در database درج نشده اند.
  3. داده هایی که در database هستند ولی هنوز Load نشده اند.
Find در صورت پیدا نکردن Exception ای صادر نمی‌کند بلکه مقدار null را بر می‌گرداند.
private static void Query4()
{
    using (var context = new StoreDbContext())
    {
        var customer = context.Customers.Find(new Guid("2ee2fd32-e0e9-4955-bace-1995839d4367"));

        if (customer == null)
            Console.WriteLine("Customer not found");
        else
            Console.WriteLine("Customer Name: {0}, Customer Family: {1}", customer.Name, customer.Family);
    }
}
با توجه به اینکه Id‌ها توسط Database ساخته می‌شوند. شما باید از Id دیگری که موجود می‌باشد، استفاده کنید تا نتیجه ای برگشت داده شود.
نکته: در صورتیکه کلید اصلی شما از دو یا چند فیلد تشکیل شده بود. می‌بایست این دو یا چند مقدار را به عنوان پارامتر به Find بفرستید.

متد Single: گاهی نیاز هست که داده‌ای پرس و جو شود اما نه با کلید اصلی بلکه با شرط دیگری، در این حالت از Single استفاده می‌شود. این متد یک مقدار را باز می‌گرداند و در صورتی که صفر یا بیش از یک مقدار در شرط صدق کند exception صادر می‌کند. متد SingleOrDefault رفتاری مشابه دارد اما اگر مقداری در شرط صدق نکند مقدار پیش فرض را باز می‌گرداند.
نکته: مقدار پیش فرض بستگی به نوع خروجی دارد که اگر object باشد مقدار null و اگر بطور مثال نوع عددی باشد، صفر می‌باشد.
private static void Query5()
{
    using (var context = new StoreDbContext())
    {
        try
        {
            var customer1 = context.Customers.Single(c => c.Name == "Unkown");  // Exception: Sequence contains no elements
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }

        try
        {
            var customer2 = context.Customers.Single(c => c.Name == "Mohsen");  // Exception: Sequence contains more than one element
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }

        var customer3 = context.Customers.SingleOrDefault(c => c.Name == "Unkown");  // customer3 == null

        var customer4 = context.Customers.Single(c => c.Name == "Vahid");  // customer4 != null
    }
}

متد First:
در صورتیکه به اولین نتیجه پرس و جو نیاز هست می‌توان از First استفاده کرد. اگر پرس و جو نتیجه در بر نداشته باشد یعنی null باشد exception صادر خواهد شد اما اگر FirstOrDefault استفاده شود مقدار پیش فرض برگردانده خواهد شد.
private static void Query6()
{
    using (var context = new StoreDbContext())
    {
        try
        {
            var customer1 = context.Customers.First(c => c.Name == "Unkown");  // Exception: Sequence contains no elements
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }

        var customer2 = context.Customers.FirstOrDefault(c => c.Name == "Unknown");  // customer2 == null
        var customer3 = context.Customers.First(c => c.Name == "Mohsen");
    }
}

مطالب
پلاگین 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



مطالب
اضافه شدن پشتیبانی از unsigned right shift به C# 11
به C# 11، عملگر جدیدی به شکل <<< و به معنای unsigned right shift اضافه شده‌است که ... در زبان جاوا از نگارش ابتدایی آن حضور داشته‌است. اما ... چرا از این لحاظ بین این دو زبان، تفاوت وجود داشته‌است؟


مفهوم عملگر شیفت در #C

عملگر شیفت به سمت راست و یا <<، عددی را به تعداد بیت مشخص شده (x >> count)، به سمت راست منتقل می‌کند و دو نوع دارد:
الف) شیفت به راست منطقی
برای مثال اگر عدد 12 را به صورت باینری نمایش دهیم، به صورت زیر خواهد بود:
00000000 00000000 00000000 00001100
و اگر آن‌را به اندازه‌ی یک بیت به سمت راست هدایت کنیم، که با 1 <<< 12  نمایش داده می‌شود:
00000000 00000000 00000000 00000110
به عدد 6 خواهیم رسید.
در این حالت همواره فرض می‌شود که عدد مدنظر، unsigned است.

ب) شیفت به راست ریاضی
شیفت به راست ریاضی، دقیقا مانند شیفت به راست منطقی است؛ مانند مثال زیر که عدد 1001  باینری را دو بیت به سمت راست منتقل می‌کند:
uint e = 0b_1001;
Console.WriteLine($"Before: {Convert.ToString(e, toBase: 2),4}");
// Before: 1001

uint f = e >> 2;
Console.WriteLine($"After:  {Convert.ToString(f, toBase: 2).PadLeft(4, '0'),4}");
// After:  0010
اما ... بجای اینکه همانند شیفت به راست منطقی، سمت چپ را با صفر پر کند، آن‌را با «با ارزش‌ترین بیت یا همان بیت علامت» پر می‌کند. یعنی در اینجا بیتی که بیانگر مثبت و منفی بودن عدد است، حفظ می‌شود. یعنی این نوع شیفت، با اعداد signed هم کار می‌کند.
برای مثال نمایش باینری عدد منفی 2,147,483,552-  به صورت زیر است:
10000000 00000000 00000000 01100000
و اگر آن‌را چهار بیت به سمت راست هدایت کنیم (یعنی 4 << 2,147,483,552 -)، به عدد 134,217,722- می‌رسیم که معادل عدد باینری زیر است:
11111000 00000000 00000000 00000110
به این ترتیب با شیفت به راست ریاضی، علامت عدد منفی حفظ شده‌است. مثالی دیگر:
int x = -8;
Console.WriteLine($"Before: {x,11}, hex: {x,8:x}, binary: {Convert.ToString(x, toBase: 2),32}");
// Before: -8, hex: fffffff8, binary: 11111111111111111111111111111000

int y = x >> 2;
Console.WriteLine($"After  >>: {y,11}, hex: {y,8:x}, binary: {Convert.ToString(y, toBase: 2),32}");
// After  >>: -2, hex: fffffffe, binary: 11111111111111111111111111111110

int z = x >>> 2;
Console.WriteLine($"After >>>: {z,11}, hex: {z,8:x}, binary: {Convert.ToString(z, toBase: 2).PadLeft(32, '0'),32}");
// After >>>:  1073741822, hex: 3ffffffe, binary: 00111111111111111111111111111110


سؤال: در زبان جاوا، عملگر <<< به معنای unsigned right shift است؛ اما چنین عملگری در زبان #C تا نگارش 11 آن وجود ندارد؛ چرا؟!

پاسخ: چون زبان جاوا، فاقد نوع‌های داده‌ای توکار unsigned integers است (^)؛ برخلاف #C از نگارش یک آن. حتی به ظاهر، این امکان در Java 8 اضافه شده‌است، اما در حقیقت با نوع‌های int و long، فقط مانند اینکه unsigned هم می‌توانند باشند، رفتار می‌کند (^). البته نوع char در زبان Java، تنها نوع unsigned واقعی است.
همانطور که عنوان شد، در زبان #C فقط کافی است بر روی unsigned types مانند ulong, uint, ushort، عملگر << را بکار برد تا به  unsigned right shift جاوا رسید (به همین جهت عملگر اضافه‌تری برای آن ارائه نشده بود). البته باید دقت داشت که در اینجا عملگر << کار پر کردن MSB یا «با ارزش‌ترین بیت یا همان بیت علامت» را هم با صفر انجام می‌دهد؛ حتی اگر MSB مقدار دهی شده باشد (چون این کاری است که << بر روی unsigned types انجام می‌دهد).
تا پیش از C# 11 اگر نیاز به کار بر روی signed types جهت رسیدن به نتیجه‌ی عملگر <<< وجود داشته باشد (انجام شیفت منطقی؛ یعنی صرفنظر کردن از نوع علامت عدد)، می‌توان از متدهای الحاقی زیر استفاده کرد که ابتدا آن‌ها را به نمونه‌های unsigned تبدیل می‌کند و کار شیفت را انجام می‌دهد و سپس نوع اصلی را بازیابی می‌کند:
public static int UnsignedRightShift(this int signed, int places)
{
    unchecked
    {
        var unsigned = (uint)signed;
        unsigned >>= places;
        return (int)unsigned;
    }
}

public static long UnsignedRightShift(this long signed, int places)
{
    unchecked
    {
        var unsigned = (ulong)signed;
        unsigned >>= places;
        return (long)unsigned;
    }
}
مطالب
ساخت یک بارکدخوان با استفاده از OpenCV و ZXing.Net
فرض کنید می‌خواهیم بارکد این قبض را یافته و سپس عدد متناظر با آن‌را در برنامه بخوانیم.


مراحل کار به این صورت هستند:


بارگذاری تصویر و چرخش آن در صورت نیاز

ابتدا تصویر بارکد دار را بارگذاری کرده و آن‌را تبدیل به یک تصویر سیاه و سفید می‌کنیم:
// load the image and convert it to grayscale
var image = new Mat(fileName);
 
if (rotation != 0)
{
    rotateImage(image, image, rotation, 1);
}
 
if (debug)
{
    Cv2.ImShow("Source", image);
    Cv2.WaitKey(1); // do events
}
 
var gray = new Mat();
var channels = image.Channels();
if (channels > 1)
{
    Cv2.CvtColor(image, gray, ColorConversion.BgrToGray);
}
else
{
    image.CopyTo(gray);
}
در این بین ممکن است بارکد موجود در تصویر، دقیقا در زاویه‌ای که در تصویر ابتدای بحث قرار گرفته‌است، وجود نداشته باشد؛ مثلا منهای 90 درجه، چرخیده باشد. به همین جهت می‌توان از متد چرخش تصویر مطلب «تغییر اندازه، و چرخش تصاویر» ارائه شده در قسمت نهم این سری استفاده کرد.


تشخیص گرادیان‌های افقی و عمودی

یکی از روش‌های تشخیص بارکد، استفاده از روشی است که در تشخیص خودرو قسمت 16 بیان شد. تعداد زیادی تصویر بارکد را تهیه و سپس آن‌ها را به الگوریتم‌های machine learning جهت تشخیص و یافتن محدوده‌ی بارکد موجود در یک تصویر، ارسال کنیم. هرچند این روش جواب خواهد داد، اما در این مورد خاص، قسمت بارکد، شبیه به گرادیانی از رنگ‌ها است. کتابخانه‌ی OpenCV برای یافتن این نوع گرادیان‌ها دارای متدی است به نام Sobel :
// compute the Scharr gradient magnitude representation of the images
// in both the x and y direction
var gradX = new Mat();
Cv2.Sobel(gray, gradX, MatType.CV_32F, xorder: 1, yorder: 0, ksize: -1);
//Cv2.Scharr(gray, gradX, MatType.CV_32F, xorder: 1, yorder: 0);
 
var gradY = new Mat();
Cv2.Sobel(gray, gradY, MatType.CV_32F, xorder: 0, yorder: 1, ksize: -1);
//Cv2.Scharr(gray, gradY, MatType.CV_32F, xorder: 0, yorder: 1);
 
// subtract the y-gradient from the x-gradient
var gradient = new Mat();
Cv2.Subtract(gradX, gradY, gradient);
Cv2.ConvertScaleAbs(gradient, gradient);
 
if (debug)
{
    Cv2.ImShow("Gradient", gradient);
    Cv2.WaitKey(1); // do events
}


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


کاهش نویز و یکی کردن نواحی تشخیص داده شده

در ادامه می‌خواهیم با استفاده از متدهای تشخیص کانتور (قسمت 12)، نواحی با بیشترین شدت گرادیان افقی را پیدا کنیم. اما تصویر حاصل از قسمت قبل برای اینکار مناسب نیست. به همین جهت با استفاده از متدهای کار با مورفولوژی تصاویر، این نواحی گرادیانی را یکی می‌کنیم (قسمت 8).
// blur and threshold the image
var blurred = new Mat();
Cv2.Blur(gradient, blurred, new Size(9, 9));
 
var threshImage = new Mat();
Cv2.Threshold(blurred, threshImage, thresh, 255, ThresholdType.Binary);
 
if (debug)
{
    Cv2.ImShow("Thresh", threshImage);
    Cv2.WaitKey(1); // do events
}
 
 
// construct a closing kernel and apply it to the thresholded image
var kernel = Cv2.GetStructuringElement(StructuringElementShape.Rect, new Size(21, 7));
var closed = new Mat();
Cv2.MorphologyEx(threshImage, closed, MorphologyOperation.Close, kernel);
 
if (debug)
{
    Cv2.ImShow("Closed", closed);
    Cv2.WaitKey(1); // do events
}
 
 
// perform a series of erosions and dilations
Cv2.Erode(closed, closed, null, iterations: 4);
Cv2.Dilate(closed, closed, null, iterations: 4);
 
if (debug)
{
    Cv2.ImShow("Erode & Dilate", closed);
    Cv2.WaitKey(1); // do events
}
این سه مرحله را در تصاویر ذیل مشاهده می‌کنید:


ابتدا با استفاده از متد Threshold، تصویر را به یک تصویر باینری تبدیل خواهیم کرد. در این تصویر تمام نقاط دارای شدت رنگ کمتر از مقدار thresh، به مقدار حداکثر 255 تنظیم می‌شوند.
سپس با استفاده از متدهای تغییر مورفولوژی تصویر، قسمت‌های مجاور به هم را می‌بندیم و یکی می‌کنیم. این مورد در یافتن اشیاء احتمالی که ممکن است بارکد باشند، بسیار مفید است.
متدهای Erode و Dilate در اینجا کار حذف نویزهای اضافی را انجام می‌دهند؛ تا بهتر بتوان بر روی نواحی بزرگتر یافت شده، تمرکز کرد.



یافتن بزرگترین ناحیه‌ی به هم پیوسته‌ی موجود در یک تصویر

تمام این مراحل را انجام دادیم تا بتوانیم بزرگترین ناحیه‌ی به هم پیوسته‌ای را که احتمال می‌رود بارکد باشد، در تصویر تشخیص دهیم. پس از این آماده سازی‌ها، اکنون با استفاده از متد یافتن کانتورها، تمام نواحی یکی شده را یافته و بزرگترین مساحت ممکن را به عنوان بارکد انتخاب می‌کنیم:
//find the contours in the thresholded image, then sort the contours
//by their area, keeping only the largest one
 
Point[][] contours;
HiearchyIndex[] hierarchyIndexes;
Cv2.FindContours(
    closed,
    out contours,
    out hierarchyIndexes,
    mode: ContourRetrieval.CComp,
    method: ContourChain.ApproxSimple);
 
if (contours.Length == 0)
{
    throw new NotSupportedException("Couldn't find any object in the image.");
}
 
var contourIndex = 0;
var previousArea = 0;
var biggestContourRect = Cv2.BoundingRect(contours[0]);
while ((contourIndex >= 0))
{
    var contour = contours[contourIndex];
 
    var boundingRect = Cv2.BoundingRect(contour); //Find bounding rect for each contour
    var boundingRectArea = boundingRect.Width * boundingRect.Height;
    if (boundingRectArea > previousArea)
    {
        biggestContourRect = boundingRect;
        previousArea = boundingRectArea;
    }
 
    contourIndex = hierarchyIndexes[contourIndex].Next;
}
 
 
var barcode = new Mat(image, biggestContourRect); //Crop the image
Cv2.CvtColor(barcode, barcode, ColorConversion.BgrToGray);
 
Cv2.ImShow("Barcode", barcode);
Cv2.WaitKey(1); // do events
حاصل این عملیات یافتن بزرگترین ناحیه‌ی گرادیانی به هم پیوسته‌ی موجود در تصویر است:


خواندن مقدار متناظر با بارکد یافت شده

خوب، تا اینجا موفق شدیم، محل قرارگیری بارکد را تصویر پیدا کنیم. مرحله‌ی بعد خواندن مقدار متناظر با این تصویر است. برای این منظور از کتابخانه‌ی سورس بازی به نام http://zxingnet.codeplex.com استفاده خواهیم کرد. این کتابخانه قادر است بارکد بسازد و همچنین تصاویر بارکدها را خوانده و مقادیر متناظر با آن‌ها را استخراج کند. برای نصب آن می‌توان از دستور ذیل استفاده کرد:
 PM> Install-Package ZXing.Net
پس از نصب این کتابخانه‌ی بارکدساز و بارکد خوان، اکنون تنها کاری که باید صورت گیرد، ارسال تصویر بارکد جدا شده‌ی توسط OpenCV به آن است:
private static string getBarcodeText(Mat barcode)
{
    // `ZXing.Net` needs a white space around the barcode
    var barcodeWithWhiteSpace = new Mat(new Size(barcode.Width + 30, barcode.Height + 30), MatType.CV_8U, Scalar.White);
    var drawingRect = new Rect(new Point(15, 15), new Size(barcode.Width, barcode.Height));
    var roi = barcodeWithWhiteSpace[drawingRect];
    barcode.CopyTo(roi);
 
    Cv2.ImShow("Enhanced Barcode", barcodeWithWhiteSpace);
    Cv2.WaitKey(1); // do events
 
    return decodeBarcodeText(barcodeWithWhiteSpace.ToBitmap());
}
 
private static string decodeBarcodeText(System.Drawing.Bitmap barcodeBitmap)
{
    var source = new BitmapLuminanceSource(barcodeBitmap);
 
    // using http://zxingnet.codeplex.com/
    // PM> Install-Package ZXing.Net
    var reader = new BarcodeReader(null, null, ls => new GlobalHistogramBinarizer(ls))
    {
        AutoRotate = true,
        TryInverted = true,
        Options = new DecodingOptions
        {
            TryHarder = true,
            //PureBarcode = true,
            /*PossibleFormats = new List<BarcodeFormat>
                    {
                        BarcodeFormat.CODE_128
                        //BarcodeFormat.EAN_8,
                        //BarcodeFormat.CODE_39,
                        //BarcodeFormat.UPC_A
                    }*/
        }
    };
 
    //var newhint = new KeyValuePair<DecodeHintType, object>(DecodeHintType.ALLOWED_EAN_EXTENSIONS, new Object());
    //reader.Options.Hints.Add(newhint);
 
    var result = reader.Decode(source);
    if (result == null)
    {
        Console.WriteLine("Decode failed.");
        return string.Empty;
    }
 
    Console.WriteLine("BarcodeFormat: {0}", result.BarcodeFormat);
    Console.WriteLine("Result: {0}", result.Text);
 
 
    var writer = new BarcodeWriter
    {
        Format = result.BarcodeFormat,
        Options = { Width = 200, Height = 50, Margin = 4},
        Renderer = new ZXing.Rendering.BitmapRenderer()
    };
    var barcodeImage = writer.Write(result.Text);
    Cv2.ImShow("BarcodeWriter", barcodeImage.ToMat());
 
    return result.Text;
}
چند نکته را باید در مورد کار با ZXing.Net بخاطر داشت؛ وگرنه جواب نمی‌گیرید:
الف) این کتابخانه حتما نیاز دارد تا تصویر بارکد، در یک حاشیه‌ی سفید در اختیار او قرار گیرد. به همین جهت در متد getBarcodeText، ابتدا تصویر بارکد یافت شده، به میانه‌ی یک مستطیل سفید رنگ بزرگ‌تر کپی می‌شود.
ب) برای تبدیل Mat به Bitmap مورد نیاز این کتابخانه می‌توان از متد الحاقی ToBitmap استفاده کرد (قسمت 7).
ج) پس از آن وهله‌ای از کلاس BarcodeReader آماده شده و در آن پارامترهایی مانند بیشتر سعی کن (TryHarder) و اصلاح درجه‌ی چرخش تصویر (AutoRotate) تنظیم شده‌اند.
د) بارکدهای موجود در قبض‌های ایران عموما بر اساس فرمت CODE_128 ساخته می‌شوند. بنابراین برای خواندن سریعتر آ‌نها می‌توان PossibleFormats را مقدار دهی کرد. اگر این مقدار دهی صورت نگیرد، تمام حالت‌های ممکن بررسی می‌شوند.

در آخر کار این متد، از متد Writer آن نیز برای تولید بارکد مشابهی استفاده شده‌است تا بتوان بررسی کرد این دو تا چه اندازه به هم شبیه هستند.


همانطور که مشاهده می‌کنید، عدد تشخیص داده شده، با عدد شناسه‌ی قبض و شناسه‌ی پرداخت تصویر ابتدای بحث یکی است.


بهبود تصویر، پیش از ارسال آن به متد Decode کتابخانه‌ی ZXing.Net

در تصویر قبلی، سطر decode failed را هم ملاحظه می‌کنید. علت اینجا است که اولین سعی انجام شده، موفق نبوده است؛ چون تصویر تشخیص داده شده، بیش از اندازه نویز و حاشیه‌ی خاکستری دارد. می‌توان این حاشیه‌ی خاکستری را با دوبار اعمال متد Threshold از بین برد:
var barcodeClone = barcode.Clone();
var barcodeText = getBarcodeText(barcodeClone);
 
if (string.IsNullOrWhiteSpace(barcodeText))
{
    Console.WriteLine("Enhancing the barcode...");
    //Cv2.AdaptiveThreshold(barcode, barcode, 255,
        //AdaptiveThresholdType.GaussianC, ThresholdType.Binary, 9, 1);
    //var th = 119;
    var th = 100;
    Cv2.Threshold(barcode, barcode, th, 255, ThresholdType.ToZero);
    Cv2.Threshold(barcode, barcode, th, 255, ThresholdType.Binary);
    barcodeText = getBarcodeText(barcode);
}
 
Cv2.Rectangle(image,
    new Point(biggestContourRect.X, biggestContourRect.Y),
    new Point(biggestContourRect.X + biggestContourRect.Width, biggestContourRect.Y + biggestContourRect.Height),
    new Scalar(0, 255, 0),
    2);
 
if (debug)
{
    Cv2.ImShow("Segmented Source", image);
    Cv2.WaitKey(1); // do events
}
 
Cv2.WaitKey(0);
Cv2.DestroyAllWindows();


اعداد یافت شده، دقیقا از روی تصویر بهبود یافته‌ی توسط متدهای Threshold خوانده شده‌اند و نه تصویر ابتدایی یافت شده. بنابراین به این موضوع نیز باید دقت داشت.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید.
نظرات مطالب
معرفی System.Text.Json در NET Core 3.0.
قسمت
if (int.TryParse (value, out _))
   return int.Parse (value);
به این نحو قابل بازنویسی است:
if (int.TryParse (value, out var result))
   return result;