مطالب
تزریق وابستگی‌ها فراتر از کلاس‌ها در برنامه‌های Angular
عموما تزریق وابستگی‌های کلاس‌ها، در برنامه‌های Angular صورت می‌گیرند. برای مثال در یک NgModule در قسمت providers آن نام کلاسی را معرفی می‌کنیم و سپس می‌توان این کلاس را به سازنده‌ی کامپوننت‌ها تزریق کرد و از امکانات آن استفاده کرد. اما سیستم تزریق وابستگی‌های Angular محدود به تزریق وهله‌های کلاس‌ها نیست و می‌توان قسمت providers را با یک سری شیء تعریف شده‌ی با {} نیز مقدار دهی کرد. در اینجا می‌توان یک token را به یک وابستگی انتساب داد.


انواع providers در Angular

سیستم تزریق وابستگی‌های Angular، تامین کننده‌های ذیل را نیز به همراه دارد:
 - تامین کننده‌ی مقادیر که با useValue مشخص می‌شود.
 - تامین کننده‌ی Factory‌ها که با useFactory تعریف خواهد شد.
 - تامین کننده‌ی کلاس‌ها که با useClass تعریف می‌شود.
 - تامین کننده‌ی کلاس‌هایی با نام‌های مستعار که توسط useExisting مشخص می‌شود.

یک تامین کننده مشخص می‌کند که سیستم تزریق کننده‌ی وابستگی‌ها، با درخواست توکن/کلیدی مشخص، چه وابستگی را باید وهله سازی کند.


تزریق وابستگی‌هایی از نوع ثوابت در برنامه‌های Angular

فرض کنید برنامه‌ی Angular شما در مسیر دیگری نسبت به Web API سمت سرور آن قرار دارد. به همین جهت در تمام سرویس‌های برنامه نیاز به تعریف مسیر پایه‌ی Web API مانند API_BASE_HREF را خواهید داشت. یک روش حل این مساله، تعریف این ثابت به صورت یک وابستگی و سپس تزریق آن به کلاس‌های سرویس‌ها و یا کامپوننت‌های برنامه است:
@NgModule({
  imports: [
    CommonModule,
    InjectionBeyondClassesRoutingModule
  ],
  declarations: [TestProvidersComponent],
  providers: [
    { provide: "API_BASE_HREF", useValue: "http://localhost:5000" },
    { provide: "APP_BASE_HREF", useValue: document.location.pathname },
    { provide: "IS_PROD", useValue: true },
    { provide: "APIKey", useValue: "XYZ1234ABC" },
    { provide: "Random", useValue: Math.random() },
    {
      provide: "emailApiConfig", useValue: Object.freeze({
        apiKey: "email-key",
        context: "registration"
      })
    },
    { provide: "languages", useValue: "en", multi: true },
    { provide: "languages", useValue: "fa", multi: true }
  ]
})
export class InjectionBeyondClassesModule { }
- در اینجا چندین مثال از تکمیل قسمت providers یک ماژول را با شیء‌های token دار provide مشاهده می‌کنید. هر provide یک token را مشخص می‌کند که از آن جهت دریافت مقدار وابستگی منتسب به آن استفاده خواهد شد.
- در این مثال، حالت‌های مختلفی از تامین کننده‌ی useValue را نیز مشاهده می‌کنید. انتساب یک رشته، یک مقدار boolean و یا یک مقدار که در زمان انتساب محاسبه خواهد شد مانند Math.random.
- همچنین در اینجا می‌توان در قسمت useValue مانند emailApiConfig، یک شیء را نیز تعریف کرد. علت استفاده‌ی از Object.freeze، تعریف این شیء به صورت read only است.
- در حین تعریف provideها اگر کلید توکن بکار رفته یکی باشد، آخرین مقدار، مابقی را بازنویسی می‌کند؛ مانند حالت languages که در اینجا دوبار تعریف شده‌است. اما با ذکر خاصیت multi، می‌توان به کلید languages به صورت یک آرایه دسترسی یافت و در این حالت مقادیر آن بازنویسی نمی‌شوند.

اکنون برای استفاده‌ی از این توکن‌های تعریف شده توسط سیستم تزریق وابستگی‌ها، می‌توان به صورت ذیل عمل کرد:
import { Component, OnInit, Inject } from "@angular/core";
import { inject } from "@angular/core/testing";

@Component({
  selector: "app-test-providers",
  templateUrl: "./test-providers.component.html",
  styleUrls: ["./test-providers.component.css"]
})
export class TestProvidersComponent implements OnInit {

  constructor(
    @Inject("API_BASE_HREF") public apiBaseHref: string,
    @Inject("APP_BASE_HREF") public appBaseHref: string,
    @Inject("IS_PROD") public isProd: boolean,
    @Inject("APIKey") public apiKey: string,
    @Inject("Random") public random: string,
    @Inject("emailApiConfig") public emailApiConfig: any,
    @Inject("languages") public languages: string[]
  ) { }

  ngOnInit() {
  }
}
در اینجا هر توکن توسط ویژگی Inject به سازنده‌ی کلاس تزریق شده‌است. از این جهت آن‌ها را public تعریف کرده‌ایم که بتوان در قالب این کامپوننت، به مقادیر تزریق شده، دسترسی یافت:
<h1>
  Injection Beyond Classes
</h1>
<div class="alert alert-info">
  <ul>
    <li>API_BASE_HREF: {{apiBaseHref}}</li>
    <li>APP_BASE_HREF: {{appBaseHref}}</li>
    <li>IS_PROD: {{isProd}}</li>
    <li>APIKey: {{apiKey}}</li>
    <li>Random-1: {{random}}</li>
    <li>Random-2: {{random}}</li>
    <li>emailApiConfig {{emailApiConfig | json}}</li>
    <li>languages: {{languages | json}}</li>
  </ul>
</div>
با این خروجی:


در اینجا همانطور که مشاهده می‌کنید، languages از نوع multi: true به یک آرایه تبدیل شده‌است و یا emailApiConfig نیز یک شیء است که توسط کلیدهای آن می‌توان به مقادیر متناظر آن دسترسی یافت. Random نیز تنها یکبار دریافت شده‌است و مهم نیست که چندبار صدا زده شود؛ همواره مقدار آن مساوی اولین مقداری است که در زمان انتساب دریافت می‌کند.


تزریق تنظیمات برنامه توسط تامین کننده‌ی مقادیر

یک نمونه از تزریق شیء emailApiConfig: any را در مثال فوق ملاحظه کردید. روش بهتر و نوع دار آن به صورت ذیل است. ابتدا یک فایل جدید thismodule.config.ts یا app.config.ts را ایجاد می‌کنیم:
import { InjectionToken } from "@angular/core";

export let APP_CONFIG = new InjectionToken<string>("this.module.config");

export interface IThisModuleConfig {
  apiEndpoint: string;
}

export const ThisModuleConfig: IThisModuleConfig = {
  apiEndpoint: "http://localhost:45043/api/"
};
تاکنون توکن‌های تعریف شده را توسط یک رشته‌ی ثابت مانند "API_BASE_HREF" تعریف کردیم. مشکل این روش، امکان تداخل آن‌ها در یک برنامه‌ی بزرگ است. به همین جهت روش توصیه شده، قرار دادن این کلید داخل یک InjectionToken است تا همواره بتوان به یک توکن منحصربفرد در طول عمر برنامه دست یافت که نمونه‌ی آن‌را در تعریف APP_CONFIG مشاهده می‌کنید. در برنامه اگر دو new InjectionToken، با یک سازنده‌ی یکسان تعریف شوند، با هم مساوی نخواهند بود و توکن نهایی آن منحصربفرد است:
import { InjectionToken } from '@angular/core';
export const EmailService1 = new InjectionToken<string>("EmailService");
export const EmailService2 = new InjectionToken<string>("EmailService");
console.log(EmailService1 === EmailService2); // false

سپس نوع تنظیمات را توسط اینترفیس IThisModuleConfig تعریف کرده‌ایم (که نسبت به استفاده‌ی از any یک پیشرفت محسوب می‌شود). در آخر وهله‌ای از این اینترفیس را به نحوی که مشاهده می‌کنید export کرده‌ایم.

اکنون نحوه‌ی تعریف تزریق وابستگی از نوع IThisModuleConfig در یک NgModule به صورت ذیل است:
import { ThisModuleConfig, APP_CONFIG } from "./thismodule.config";

@NgModule({
  providers: [
    { provide: APP_CONFIG, useValue: ThisModuleConfig }
  ]
})
export class InjectionBeyondClassesModule { }
اینبار توکن تعریف شده توسط InjectionToken مشخص شده‌است و مقدار آن توسط ThisModuleConfig تامین خواهد شد.

در آخر، تزریق آن به سازنده‌ی یک کامپوننت بر اساس توکن APP_CONFIG و از نوع مشخص اینترفیس آن خواهد بود:
import { IThisModuleConfig, APP_CONFIG } from "./../thismodule.config";
@Component()
export class TestProvidersComponent implements OnInit {

  constructor(
    @Inject(APP_CONFIG) public config: IThisModuleConfig
  ) { }

  ngOnInit() {
  }

}


تزریق وابستگی‌ها توسط تامین کننده‌ی Factory ها

تا اینجا useValue را بررسی کردیم. نوع دیگر تامین کننده‌های قابل تعریف، useFactory هستند:
@NgModule({
  providers: [
    // ------ useFactory
    { provide: "BASE_URL", useFactory: getBaseUrl },
    { provide: "RandomFactory", useFactory: randomFactory }
  ]
})
export class InjectionBeyondClassesModule { }

export function getBaseUrl() {
  return document.getElementsByTagName("base")[0].href;
}

export function randomFactory() {
  return Math.random();
}
در اینجا روش استفاده‌ی از useFactory را مشاهده می‌کنید. کار کرد آن با useValue دقیقا یکی است؛ یک توکن را مشخص می‌کنیم و سپس مقداری به آن نسبت داده می‌شود. اما در اینجا می‌توان یک متد را که بیانگر نحوه‌ی تامین این مقدار است نیز مشخص کرد و نسبت به حالت useValue که تنها یک مقدار ثابت و مشخص را دریافت می‌کند، انعطاف پذیری بیشتر دارد و می‌توان منطق سفارشی خاصی را نیز در اینجا پیاده سازی کرد.

روش استفاده‌ی از آن نیز همانند توکن‌های useValue است که توسط ویژگی Inject مشخص می‌شوند:
export class TestProvidersComponent implements OnInit {

  constructor(
    @Inject("BASE_URL") public baseUrl: string,
    @Inject("RandomFactory") public randomFactory: string
  ) { }

حالت useFactory علاوه بر امکان دریافت یک منطق سفارشی توسط یک function، امکان دریافت یک سری وابستگی را نیز دارد. فرض کنید کلاس سرویس خودرو به صورت زیر تعریف شده‌است که دارای وابستگی از نوع HttpClient تزریق شده‌ی در سازنده‌ی آن است:
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";

@Injectable()
export class CarService {

  constructor(private http: HttpClient) { }

}
در این حالت useFactory آن جهت تامین پارامتر سازنده‌ی  new CarService، به همراه متدی خواهد بود که پارامتری از نوع HttpClient را دریافت می‌کند:
import { CarService } from "./car.service";
import { HttpClient } from "@angular/common/http";

@NgModule({
  providers: [
    // ------ useFactory
    { provide: "Car_Service", useFactory: carServiceFactory, deps: [HttpClient] }
  ]
})
export class InjectionBeyondClassesModule { }

export function carServiceFactory(http: HttpClient) {
  return new CarService(http);
}
در اینجا برای تامین این پارامتر سازنده، خاصیت دیگری به نام deps قابل تعریف است که می‌تواند یک یا چند سرویس و وابستگی را تزریق و تامین کند. برای مثال سرویس HttpClient در اینجا توسط deps: [HttpClient] تزریق شده‌است.


تزریق وابستگی‌ها توسط تامین کننده‌ی کلاس‌ها

تا اینجا useValue و useFactory را بررسی کردیم. نوع دیگر تامین کننده‌های قابل تعریف، useClass هستند. در حالت استفاده‌ی useClass، نام یک نوع مشخص می‌شود و سپس Angular وهله‌ای از آن‌را تامین خواهد کرد. در این حالت اگر این وابستگی دارای پارامترهای تزریق شده‌ای در سازنده‌ی آن باشد، آن‌ها نیز به صورت خودکار وهله سازی می‌شوند.
import { CarService } from "./car.service";

@NgModule({
  providers: [
    // ------ useClass
    { provide: "Car_Service_Name1", useClass: CarService },
  ]
})
export class InjectionBeyondClassesModule { }
این حالت دقیقا معادل تعریف متداول سرویس ذیل است؛ با این تفاوت که توکن آن مساوی مقدار سفارشی Car_Service_Name1 است:
import { CarService } from "./car.service";

@NgModule({
  providers: [
        CarService
  ]
})
export class InjectionBeyondClassesModule { }


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

چگونه می‌توان دو تامین کننده را برای کلاسی مشابه، با توکن‌هایی متفاوت ایجاد کرد؟ در این حالت از useExisting استفاده می‌شود:
import { CarService } from "./car.service";

@NgModule({
  providers: [
    // ------ useClass
    { provide: "Car_Service_Name1", useClass: CarService },
    // ------ useExisting
    { provide: "Car_Service_Token2", useExisting: "Car_Service_Name1" },
  ]
})
export class InjectionBeyondClassesModule { }
در اینجا CarService توسط دو توکن مختلف در معرض دید قرار گرفته‌است. باید دقت داشت که درخواست "Car_Service_Token2" دقیقا همان وهله‌ی ایجاد شده‌ی توسط توکن "Car_Service_Name1" را بازگشت می‌دهد و وهله‌ی جدیدی در این حالت ایجاد نخواهد شد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
مطالب
آشنایی با مفهوم Indexer در C#.NET
زمانی که صحبت از Indexer می‌شود، بطور ناخوداگاه ذهنمان به سمت آرایه‌ها می‌رود. آرایه‌ها در واقع ساده‌ترین اشیاء ی هستند که مفهوم Index در آنها معنا دار است.
اگر با آرایه‌ها کار کرده باشید با عملگر [] در سی شارپ آشنایی دارید. یک Indexer در واقع نوع خاصی از خاصیت (property) است که در بدنه کلاس تعریف می‌شود و به ما امکان استفاده از عملگر [] را برای نمونه کلاس فراهم می‌کند.
همانطور که به شباهت Indexer و Property اشاره شد نحوه تعریف Indexer بصورت زیر می‌باشد:
public type this [type identifier]
{
     get{ ... }
     set{ ... }
}
در پیاده سازی Indexer به نکات زیر دقت کنید:
  • از کلمه کلیدی this برای نعریف Indexer استفاده می‌شود و یک Indexer نام ندارد.
  • از دستیاب get برای برگرداندن مقدار و از دستیاب set جهت مقدار دهی و انتساب استفاده می‌شود.
  • الزامی به پیاده سازی Indexer با مقادیر صحیح عددی نیست و شما می‌توانید Indexer ی با پارامترهایی از نوع string یا double داشته باشید.
  • Indexer‌ها قابلیت سربارگذاری (overloaded) دارند.
  • Indexer‌ها می‌توانند چندین پارامتری باشند مانند آرایه‌های دو بعدی که دو پارامتری هستند.
به مثالی جهت پیاده سازی کلاس ماتریس توجه کنید:
public class Matrix
{
    // فیلدها
    private int _row, _col;
    private readonly double[,] _values;

    // تعداد ردیف‌های ماتریس
    public int Row
    {
        get { return _row; }
        set
        {
            _row = value > 0 ? value : 3;
        }
    }

    // تعداد ستون‌های ماتریس
    public int Col
    {
        get { return _col; }
        set
        {
            _col = value > 0 ? value : 3;
        }
    }

    // نعریف یک ایندکسر
    public double this[int r, int c]
    {
        get { return Math.Round(_values[r, c], 3); }
        set { _values[r, c] = value; }
    }

    // سازنده 1
    public Matrix()
    {
        _values = new double[_row,_col];
    }

    // سازنده 2
    public Matrix(int row, int col)
    {
        _row = row;
        _col = col;
        _values = new double[_row,_col];
    }

}
نحوه استفاده از کلاس ایجاد شده:
public class UseMatrixIndexer
{
    // ایجاد نمونه از شیء ماتریس
    private readonly Matrix m = new Matrix(5, 5);

    private double item;

    public UseMatrixIndexer()
    {
        // دسترسی به عنصر واقع در ردیف چهار و ستون سه
        item = m[4, 3];
    }
}
مطالب
فعال سازی و پردازش Inline Add در jqGrid
پیشنیازها
فعال سازی و پردازش صفحات پویای افزودن، ویرایش و حذف رکوردهای jqGrid در ASP.NET MVC
اعتبارسنجی سفارشی سمت کاربر و سمت سرور در jqGrid


پیشتر با نحوه‌ی فعال سازی صفحات پویای افزودن، ویرایش و حذف رکوردهای jqGrid آشنا شدیم. اما ... شاید علاقمند نباشید که اصلا از این صفحات استفاده کنید. شاید به نظر شما با کلیک بر روی دکمه‌ی + افزودن یک رکورد جدید، بهتر باشد داخل خود گرید، یک سطر خالی جدید باز شده تا بتوان آن‌را پر کرد. شاید این نحو کار کردن با گرید، از دید عده‌ای طبیعی‌تر باشد نسبت به حالت نمایش صفحات popup افزودن و یا ویرایش رکوردها. در ادامه این مورد را بررسی خواهیم کرد.


فعال سازی افزودن، ویرایش و حذف Inline


فعال سازی ویرایش و حذف Inline را پیشتر نیز بررسی کرده بودیم. تنها کافی است یک ستون جدید را با 'formatter: 'actions تعریف کنیم. به صورت خودکار، دکمه‌ی ویرایش، حذف، ذخیره سازی و لغو Inline ظاهر می‌شوند و همچنین بدون نیاز به کدنویسی بیشتری کار می‌کنند.
اما در کدهای ذیل اندکی این ستون را سفارشی سازی کرده‌ایم. در قسمت formatter آن، دکمه‌های edit و delete یک سطر جدید توکار اضافه شده را حذف کرده‌ایم. زیرا در این حالت خاص، وجود این دکمه‌ها ضروری نیستند. بهتر است در این حالت دکمه‌‌های save و cancel ظاهر شوند:
            $('#list').jqGrid({
                caption: "آزمایش نهم",
                // ....
                colModel: [
                    {
                        name: 'myac', width: 80, fixed: true, sortable: false,
                        resize: false,
                        //formatter: 'actions',
                        formatter: function (cellvalue, options, rowObject) {
                            if (cellvalue === undefined && options.rowId === "_empty") {
                                // در حالت نمایش ردیف توکار جدید دکمه‌های ویرایش و حذف معنی ندارند
                                options.colModel.formatoptions.editbutton = false;
                                options.colModel.formatoptions.delbutton = false;
                            }
                            return $.fn.fmatter.actions(cellvalue, options, rowObject);
                        },
                        formatoptions: {
                            keys: true,
                            afterSave: function (rowid, response) {
                            },
                            delbutton: true,
                            delOptions: {
                                url: "@Url.Action("DeleteUser", "Home")"
                            }
                        }
                    }
                ],
                //...
            }).navGrid(
                '#pager',
//...
)
                .jqGrid('gridResize', { minWidth: 400, minHeight: 150 })
                .jqGrid('inlineNav', '#pager',
                {
                    edit: true, add: true, save: true, cancel: true,
                    edittext: "ویرایش", addtext: "جدید", savetext: "ذخیره", canceltext: "لغو",
                    addParams: {
                        // اگر می‌خواهید ردیف‌های جدید در ابتدا ظاهر شوند، این سطر را حذف کنید
                        position: "last", //ردیف‌های جدید در آخر ظاهر می‌شوند
                        rowID: '_empty',
                        useDefValues: true,
                        addRowParams: getInlineNavParams(true)
                    },
                    editParams: getInlineNavParams(false)
                });
قسمتی که کار فعال سازی Inline Add را انجام می‌دهد، تعریف مرتبط با inlineNav است که به انتهای تعاریف متداول گرید اضافه شده‌است.
در اینجا 4 دکمه‌ی ویرایش، جدید، ذخیره و لغو، در نوار pager پایین گرید ظاهر خواهند شد (سمت چپ؛ سمت راست همان دکمه‌‌های نمایش فرم‌های پویا هستند).


سپس باید دو قسمت مهم addParams و editParams آن‌را مقدار دهی کرد.
در قسمت addParams، مشخص می‌کنیم که ID ردیف اضافه شده، مساوی کلمه‌ی _empty باشد. اگر به کدهای formatter ستون action دقت کنید، از این ID برای تشخیص افزوده شدن یک ردیف جدید استفاده شده‌است.
position در اینجا به معنای محل افزوده شدن یک ردیف خالی است. مقدار پیش فرض آن first است؛ یعنی همیشه در اولین ردیف گرید، این ردیف جدید اضافه می‌شود. در اینجا به last تنظیم شده‌است تا در پایین گرید و پس از رکوردهای موجود، نمایش داده شود.
useDefValues سبب استفاده از مقادیر پیش فرض تعریف شده در ستون‌های گرید در حین افزوده شدن یک ردیف جدید می‌گردد.
addRowParams و editParams هر دو ساختار تقریبا یکسانی دارند که به نحو ذیل تعریف می‌شوند:
        function getInlineNavParams(isAdd) {
            return {
                // استفاده از آدرس‌های مختلف برای حالات ویرایش و ثبت اطلاعات جدید
                url: isAdd ? '@Url.Action("AddUser", "Home")' : '@Url.Action("EditUser","Home")',
                key: true,
                restoreAfterError: false, // این مورد سبب می‌شود تا اعتبارسنجی سمت سرور قابل اعمال شود
                oneditfunc: function (rowId) {
                    // نمایش دکمه‌های ذخیره و لغو داخل همان سطر
                    $("#jSaveButton_" + rowId).show();
                    $("#jCancelButton_" + rowId).show();
                },
                successfunc: function () {
                    var $self = $(this);
                    setTimeout(function () {
                        $self.trigger("reloadGrid"); // دریافت کلید اصلی ردیف از سرور
                    }, 50);
                },
                errorfunc: function (rowid, response, stat) {
                    if (stat != 'error') // this.Response.StatusCode == 200
                        return;

                    var result = $.parseJSON(response.responseText);
                    if (result.success === false) {
                        //نمایش خطای اعتبار سنجی سمت سرور پس از ویرایش یا افزودن
                        $.jgrid.info_dialog($.jgrid.errors.errcap,
                            '<div class="ui-state-error">' + result.message + '</div>',
                            $.jgrid.edit.bClose,
                            { buttonalign: 'center' });
                    }
                }
            };
        }
در ابتدای کار مشخص می‌کنیم که آدرس‌های ذخیره سازی اطلاعات در سمت سرور برای حالت‌های Add و Edit کدام‌اند.
تنظیم restoreAfterError به false بسیار مهم است. اگر در سمت سرور خطای اعتبارسنجی گزارش شود و restoreAfterError مساوی true باشد (مقدار پیش فرض)، کاربر مجبور خواهد شد اطلاعات را دوباره وارد کند.
در روال رویدادگران oneditfunc دکمه‌ی save و cancel ردیف را که مخفی هستند، ظاهر می‌کنیم (مکمل formatter ستون action است).
در قسمت successfunc، پس از پایان موفقیت آمیز کار، متد reloadGrid را فراخوانی می‌کنیم. اینکار سبب می‌شود تا Id واقعی رکورد، از سمت سرور دریافت شود. از این Id برای ویرایش و همچنین حذف، استفاده خواهد شد. علت استفاده از setTimeout در اینجا این است که اندکی به DOM فرصت داده شود تا کارش به پایان برسد.
در قسمت errorfunc خطاهای اعتبارسنجی سفارشی سمت سرور را می‌توان دریافت و سپس توسط متد توکار info_dialog به کاربر نمایش داد.



یک نکته‌ی مهم در مورد ارسال خطاهای اعتبارسنجی از سمت سرور در حالت Inline Add

            if (_usersInMemoryDataSource.Any(
                    user => user.Name.Equals(postData.Name, StringComparison.InvariantCultureIgnoreCase)))
            {
                this.Response.StatusCode = 500; //این مورد برای افزودن داخل ردیف‌های گرید لازم است
                return Json(new { success = false, message = "نام کاربر تکراری است" }, JsonRequestBehavior.AllowGet);
            }
روال رویداد گردان errorfunc، اگر مقدار StatusCode بازگشتی از سمت سرور مساوی 200 باشد (حالت عادی و موفقیت آمیز)، مقدار stat مساوی error را باز نمی‌گرداند. به همین جهت است که در کدهای فوق، مقدار دهی this.Response.StatusCode را به 500 مشاهده می‌کنید. هر عددی غیر از 200 در اینجا به error تفسیر می‌شود. همچنین اگر این StatusCode سمت سرور تنظیم نشود، گرید فرض را بر موفقیت آمیز بودن عملیات گذاشته و successfunc را فراخوانی می‌کند.



مدیریت StatusCodeهای غیر از 200 در حالت کار با فرم‌های jqGrid

اگر هر دو حالت Inline Add و فرم‌های پویا را فعال کرده‌اید، بازگشت StatusCode = 500 سبب می‌شود تا دیگر نتوان خطاهای سفارشی سمت سرور را در بالای فرم‌ها به کاربر نمایش داد و در این حالت تنها یک internal server error را مشاهده خواهند کرد. برای رفع این مشکل فقط کافی است روال رویدادگران errorTextFormat را مدیریت کرد:
            $('#list').jqGrid({
                caption: "آزمایش نهم",
                //.........
            }).navGrid(
                '#pager',
                //enabling buttons
                { add: true, del: true, edit: true, search: false },
                //edit option
                {
                    //......... 
                    errorTextFormat: serverErrorTextFormat
                },
                //add options
                {
                    //......... 
                    errorTextFormat: serverErrorTextFormat
                },
                //delete options
                {
                    //......... 
                })
                .jqGrid('gridResize', { minWidth: 400, minHeight: 150 })
                .jqGrid('inlineNav', '#pager',
                {
                    //......... 
                });

        function serverErrorTextFormat (response) {
            // در حالتیکه وضعیت خروجی از سرور 200 نیست فراخوانی می‌شود
            var result = $.parseJSON(response.responseText);
            if (result.success === false) {
                return result.message;
            }
            return "لطفا ورودی‌های وارد شده را بررسی کنید";
        }
errorTextFormat تنها در حالتیکه StatusCode بازگشتی از طرف سرور مساوی 200 نیست، فراخوانی می‌شود. در اینجا می‌توان response دریافتی را آنالیز و سپس پیام خطای سفارشی آن‌را جهت نمایش در فرم‌های پویای گرید، بازگشت داد.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید:
jqGrid09.zip
اشتراک‌ها
معماری SQL Server Graph Database (بخش دوم)

ک گره جدول ، موجودی موجود در یک طرح گراف را نشان می‌دهد. هر بار که یک گره جدول ایجاد می‌شود ، همراه با ستون‌های تعریف شده توسط کاربر ، یک ستون ضمنی  $node_id ایجاد می‌شود ، که به طور یونیک به یک مپ می‌شود. مقادیر $node_id به طور خودکار تولید می‌شوند و ترکیبی از object_id آن گره جدول و مقدار bigint تولید شده در داخل هستند. با این حال ، وقتی ستون  $node_id انتخاب می‌شود ،...

معماری SQL Server Graph Database (بخش دوم)
نظرات مطالب
فعال سازی و پردازش صفحات پویای افزودن، ویرایش و حذف رکوردهای jqGrid در ASP.NET MVC
- مثال‌های سری jqGrid تغییرات زیادی داشتند. برای دریافت آن‌ها به این مخزن کد مراجعه کنید.
- برای نمونه، این فایل بهبود یافته مثال جاری است. در آن نحوه‌ی تعریف ستون Id، به صورت مخفی و کلید، معرفی شده. همچنین در ستون actions آن نحوه‌ی معرفی آدرس حذف به نحو بهتری درج شده‌است. به علاوه نحوه‌ی استفاده از anti-forgery token در آن ذکر شده، به همراه StronglyTyped.PropertyName ها.
نظرات مطالب
چگونگی رسیدگی به Null property در AutoMapper

با سلام

مهندس من یه دیتابیس دارم که حاوی اطلاعات است در جداول اون در تمام ستون‌ها به غیر از ستون کلید اومده تیک alow null  رو فعال کرده یعنی این ستون‌ها می‌تونه مقدار null رو بگیره .

حالا اون برنامه که این اطلاعات رو وارد دیتابیس کرده اومده هر ستونی که نوعش رشته بوده مقدار empty وارد کرده نه null و ستون هایی که نوعشون int هست مقدار صفر وارد کرده , مثل همین مطلبی که شما گفتید اما به صورت سنتی .

به نظر شما من باید همین رویه رو با روش شما انجام بدم یا نه همون مقدار null و در دیتابیس ذخیره کنم ؟

نظرات مطالب
روش‌هایی برای حذف رکوردهای تکراری
جناب نصیری من دقیقا کد های شما را نخواندمو اما من از تیکه کد زیر برای حدف رکورد های تکراری استفاده میکنم :

DELETE
FROM MyTable
WHERE ID NOT IN
(
SELECT MAX(ID)
FROM MyTable
GROUP BY COLUMN1, COLUMN2

که در اینجا ستون ها ستون هایی هستند که امکان یونیک بودن داده ها در آن وجود دارد.

با سپاس
پاسخ به بازخورد‌های پروژه‌ها
عدم اعمال style در ستون های سفارشی
- حالت HTML بر اساس HTML Worker داخلی iTextSharp است. زیاد قابلیت‌های پیشرفته اعمال style و css رو نداره. برای حالت‌های ساده خوبه.
- اما ... داشتن دو ستون برای هدر و مرج مثلا ردیف بالاتر در PdfReport پشتیبانی می‌شود و نیازی به استفاده از HTML template برای آن ندارید.
راحت دو ستون مستقل FromDate و ToDate رو اضافه کنید و بعد هم توسط متد AddHeadingCell سطر بالای اون‌ها رو یکی کنید.یک مثال آماده برای نمایش این قابلیت
مطالب
OpenCVSharp #18
ساخت یک OCR ساده تشخیص اعداد انگلیسی به کمک OpenCV

این مطلب را می‌توان به عنوان جمع بندی مطالبی که تاکنون بررسی شدند درنظر گرفت و در اساس مطلب جدیدی ندارد و صرفا ترکیب یک سری تکنیک است؛ برای مثال:
چطور یک تصویر را به نمونه‌ی سیاه و سفید آن تبدیل کنیم؟
کار با متد Threshold جهت بهبود کیفیت یک تصویر جهت تشخیص اشیاء
تشخیص کانتورها (Contours) و اشیاء موجود در یک تصویر
آشنایی با نحوه‌ی گروه بندی تصاویر مشابه و مفاهیمی مانند برچسب‌های تصاویر که بیانگر یک گروه از تصاویر هستند.


تهیه تصاویر اعداد انگلیسی جهت آموزش دادن به الگوریتم CvKNearest

در اینجا نیز از یکی دیگر از الگوریتم‌های machine learning موجود در OpenCV به نام CvKNearest برای تشخیص اعداد انگلیسی استفاده خواهیم کرد. این الگوریتم نزدیک‌ترین همسایه‌ی اطلاعاتی مفروض را در گروهی از داده‌های آموزش داده شده‌ی به آن پیدا می‌کند. خروجی آن شماره‌ی این گروه است. بنابراین نحوه‌ی طبقه‌ی بندی اطلاعات در اینجا چیزی شبیه به شکل زیر خواهد بود:


مجموعه‌ای از تصاویر 0 تا 9 را جمع آوری کرده‌ایم. هر کدام از پوشه‌ها، بیانگر اعدادی از یک خانواده هستند. این تصویر را با فرمت ذیل جمع آوری می‌کنیم:
public class ImageInfo
{
    public Mat Image { set; get; }
    public int ImageGroupId { set; get; }
    public int ImageId { set; get; }
}
به این ترتیب
public IList<ImageInfo> ReadTrainingImages(string path, string ext)
{
    var images = new List<ImageInfo>();
 
    var imageId = 1;
    foreach (var dir in new DirectoryInfo(path).GetDirectories())
    {
        var groupId = int.Parse(dir.Name);
        foreach (var imageFile in dir.GetFiles(ext))
        {
            var image = processTrainingImage(new Mat(imageFile.FullName, LoadMode.GrayScale));
            if (image == null)
            {
                continue;
            }
 
            images.Add(new ImageInfo
            {
                Image = image,
                ImageId = imageId++,
                ImageGroupId = groupId
            });
        }
    }
 
    return images;
}
در متد خواندن تصاویر آموزشی، ابتدا پوشه‌های اصلی مسیر Numbers تصویر ابتدای بحث دریافت می‌شوند. سپس نام هر پوشه، شماره‌ی گروه تصاویر موجود در آن پوشه را تشکیل خواهد داد. به این نام در الگوریتم‌های machine leaning، کلاس هم گفته می‌شود. سپس هر تصویر را با فرمت سیاه و سفید بارگذاری کرده و به لیست تصاویر موجود اضافه می‌کنیم. در اینجا از متد processTrainingImage نیز استفاده شده‌است. هدف از آن بهبود کیفیت تصویر دریافتی جهت کار تشخیص اشیاء است:
private static Mat processTrainingImage(Mat gray)
{
    var threshImage = new Mat();
    Cv2.Threshold(gray, threshImage, Thresh, ThresholdMaxVal, ThresholdType.BinaryInv); // Threshold to find contour
 
    Point[][] contours;
    HiearchyIndex[] hierarchyIndexes;
    Cv2.FindContours(
        threshImage,
        out contours,
        out hierarchyIndexes,
        mode: ContourRetrieval.CComp,
        method: ContourChain.ApproxSimple);
 
    if (contours.Length == 0)
    {
        return null;
    }
 
    Mat result = null;
 
    var contourIndex = 0;
    while ((contourIndex >= 0))
    {
        var contour = contours[contourIndex];
 
        var boundingRect = Cv2.BoundingRect(contour); //Find bounding rect for each contour
        var roi = new Mat(threshImage, boundingRect); //Crop the image
 
        //Cv2.ImShow("src", gray);
        //Cv2.ImShow("roi", roi);
        //Cv.WaitKey(0);
 
        var resizedImage = new Mat();
        var resizedImageFloat = new Mat();
        Cv2.Resize(roi, resizedImage, new Size(10, 10)); //resize to 10X10
        resizedImage.ConvertTo(resizedImageFloat, MatType.CV_32FC1); //convert to float
        result = resizedImageFloat.Reshape(1, 1);
 
        contourIndex = hierarchyIndexes[contourIndex].Next;
    }
 
    return result;
}
عملیات صورت گرفته‌ی در این متد را با تصویر ذیل بهتر می‌توان توضیح داد:


ابتدا تصویر اصلی بارگذاری می‌شود؛ همان تصویر سمت چپ. سپس با استفاده از متد Threshold، شدت نور نواحی مختلف آن یکسان شده و آماده می‌شود برای تشخیص کانتورهای موجود در آن. در ادامه با استفاده از متد FindContours، شیء مرتبط با عدد جاری یافت می‌شود. سپس متد Cv2.BoundingRect مستطیل دربرگیرنده‌ی این شیء را تشخیص می‌دهد (تصویر سمت راست). بر این اساس می‌توان تصویر اصلی ورودی را به یک تصویر کوچکتر که صرفا شامل ناحیه‌ی عدد مدنظر است، تبدیل کرد. در ادامه برای کار با الگوریتم  CvKNearest نیاز است تا این تصویر بهبود یافته را تبدیل به یک ماتریس یک بعدی کردی که روش انجام کار توسط متد Reshape مشاهده می‌کنید.
از همین روش پردازش و بهبود تصویر ورودی، جهت پردازش اعداد یافت شده‌ی در یک تصویر با تعداد زیادی عدد نیز استفاده خواهیم کرد.


آموزش دادن به الگوریتم CvKNearest

تا اینجا تصاویر گروه بندی شده‌ای را خوانده و لیستی از آن‌ها را مطابق فرمت الگوریتم CvKNearest تهیه کردیم. مرحله‌ی بعد، معرفی این لیست به متد Train این الگوریتم است:
public CvKNearest TrainData(IList<ImageInfo> trainingImages)
{
    var samples = new Mat();
    foreach (var trainingImage in trainingImages)
    {
        samples.PushBack(trainingImage.Image);
    }
 
    var labels = trainingImages.Select(x => x.ImageGroupId).ToArray();
    var responses = new Mat(labels.Length, 1, MatType.CV_32SC1, labels);
    var tmp = responses.Reshape(1, 1); //make continuous
    var responseFloat = new Mat();
    tmp.ConvertTo(responseFloat, MatType.CV_32FC1); // Convert  to float
 
 
    var kNearest = new CvKNearest();
    kNearest.Train(samples, responseFloat); // Train with sample and responses
    return kNearest;
}
متد Train دو ورودی دارد. ورودی اول آن یک تصویر است که باید از طریق متد PushBack کلاس Mat تهیه شود. بنابراین لیست تصاویر اصلی را تبدیل به لیستی از Matها خواهیم کرد.
سپس نیاز است لیست گروه‌های متناظر با تصاویر اعداد را تبدیل به فرمت مورد انتظار متد Train کنیم. در اینجا صرفا لیستی از اعداد صحیح را داریم. این لیست نیز باید تبدیل به یک Mat شود که روش انجام آن در متد فوق بیان شده‌است. کلاس Mat سازنده‌ی مخصوصی را جهت تبدیل لیست اعداد، به همراه دارد. این Mat نیز باید تبدیل به یک ماتریس یک بعدی شود که برای این منظور از متد Reshape استفاده شده‌است.


انجام عملیات OCR نهایی

پس از تهیه‌ی لیستی از تصاویر و آموزش دادن آن‌ها به الگوریتم CvKNearest، تنها کاری که باید انجام دهیم، یافتن اعداد در تصویر نمونه‌ی مدنظر و سپس معرفی آن به متد FindNearest الگوریتم CvKNearest است. روش انجام اینکار بسیار شبیه است به روش معرفی شده در متد processTrainingImage که پیشتر بررسی شد:
public void DoOCR(CvKNearest kNearest, string path)
{
    var src = Cv2.ImRead(path);
    Cv2.ImShow("Source", src);
 
    var gray = new Mat();
    Cv2.CvtColor(src, gray, ColorConversion.BgrToGray);
 
    var threshImage = new Mat();
    Cv2.Threshold(gray, threshImage, Thresh, ThresholdMaxVal, ThresholdType.BinaryInv); // Threshold to find contour
 
 
    Point[][] contours;
    HiearchyIndex[] hierarchyIndexes;
    Cv2.FindContours(
        threshImage,
        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.");
    }
 
    //Create input sample by contour finding and cropping
    var dst = new Mat(src.Rows, src.Cols, MatType.CV_8UC3, Scalar.All(0));
 
    var contourIndex = 0;
    while ((contourIndex >= 0))
    {
        var contour = contours[contourIndex];
 
        var boundingRect = Cv2.BoundingRect(contour); //Find bounding rect for each contour
 
        Cv2.Rectangle(src,
            new Point(boundingRect.X, boundingRect.Y),
            new Point(boundingRect.X + boundingRect.Width, boundingRect.Y + boundingRect.Height),
            new Scalar(0, 0, 255),
            2);
 
        var roi = new Mat(threshImage, boundingRect); //Crop the image
 
        var resizedImage = new Mat();
        var resizedImageFloat = new Mat();
        Cv2.Resize(roi, resizedImage, new Size(10, 10)); //resize to 10X10
        resizedImage.ConvertTo(resizedImageFloat, MatType.CV_32FC1); //convert to float
        var result = resizedImageFloat.Reshape(1, 1);
 
 
        var results = new Mat();
        var neighborResponses = new Mat();
        var dists = new Mat();
        var detectedClass = (int)kNearest.FindNearest(result, 1, results, neighborResponses, dists);
 
        //Console.WriteLine("DetectedClass: {0}", detectedClass);
        //Cv2.ImShow("roi", roi);
        //Cv.WaitKey(0);
 
        //Cv2.ImWrite(string.Format("det_{0}_{1}.png",detectedClass, contourIndex), roi);
 
        Cv2.PutText(
            dst,
            detectedClass.ToString(CultureInfo.InvariantCulture),
            new Point(boundingRect.X, boundingRect.Y + boundingRect.Height),
            0,
            1,
            new Scalar(0, 255, 0),
            2);
 
        contourIndex = hierarchyIndexes[contourIndex].Next;
    }
 
    Cv2.ImShow("Segmented Source", src);
    Cv2.ImShow("Detected", dst);
 
    Cv2.ImWrite("dest.jpg", dst);
 
    Cv2.WaitKey();
}
این عملیات به صورت خلاصه در تصویر ذیل مشخص شده‌است:


ابتدا تصویر اصلی که قرار است عملیات OCR روی آن صورت گیرد، بارگذاری می‌شود. سپس کانتورها و اعداد موجود در آن تشخیص داده می‌شوند. مستطیل‌های قرمز رنگ در برگیرنده‌ی این اعداد را در تصویر دوم مشاهده می‌کنید. سپس این کانتور‌های یافت شده را که شامل یکی از اعداد تشخیص داده شده‌است، تبدیل به یک ماتریس یک بعدی کرده و به متد FindNearest ارسال می‌کنیم. خروجی آن نام گروه یا پوشه‌ای است که این عدد در آن قرار دارد. در همینجا این خروجی را تبدیل به یک رشته کرده و در تصویر سوم با رنگ سبز رنگ نمایش می‌دهیم.
بنابراین در این تصویر، پنجره‌ی segmented image، همان اشیاء تشخیص داده شده‌ی از تصویر اصلی هستند.
پنجره‌ی با زمینه‌ی سیاه رنگ، نتیجه‌ی نهایی OCR است که نسبتا هم دقیق عمل کرده‌است.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید.
مطالب
پردازش توالی توالی‌ها در Reactive extensions
به صورت پیش فرض، Rx هر بار تنها یک مقدار را بررسی می‌کند. اما گاهی از اوقات نیاز است تا در هربار، بیشتر از یک مقدار دریافت و پردازش شوند. برای این منظور Rx متدهای الحاقی ویژه‌ای را به نام‌های Buffer ،Scan و Window تدارک دیده‌است تا بتواند از یک توالی، چندین توالی را تولید کند (توالی توالی‌ها = Sequence of sequences).


متد Scan

فرض کنید قصد دارید تعدادی عدد را با هم جمع بزنید. برای اینکار عموما عدد اول با عدد دوم جمع زده شده و سپس حاصل آن با عدد سوم جمع زده خواهد شد و به همین ترتیب تا آخر توالی. کار متد Scan نیز دقیقا به همین نحو است. هربار که قرار است توالی پردازش شود، حاصل عملیات مرحله‌ی قبل را در اختیار مصرف کننده قرار می‌دهد.
در مثال ذیل، قصد داریم حاصل جمع اعداد موجود در آرایه‌ای را بدست بیاوریم:
var sequence = new[] { 12, 3, -4, 7 }.ToObservable();
var runningSum = sequence.Scan((accumulator, value) =>
{
    Console.WriteLine("accumulator {0}", accumulator);
    Console.WriteLine("value {0}", value);
    return accumulator + value;
});
runningSum.Subscribe(result => Console.WriteLine("result {0}\n", result));
با این خروجی
result 12

accumulator 12
value 3
result 15

accumulator 15
value -4
result 11

accumulator 11
value 7
result 18
در اولین بار اجرای متد Subscribe، کار مقدار دهی accumulator با اولین عنصر آرایه صورت می‌گیرد.
در دفعات بعدی، مقدار این accumulator با عدد جاری جمع زده شده و حاصل این عملیات در تکرار آتی، مجددا توسط accumulator قابل دسترسی خواهد بود.

یک نکته: اگر علاقمند باشیم که مقدار اولیه‌ی accumulator، اولین عنصر توالی نباشد، می‌توان آن‌را توسط پارامتر seed متد Scan مقدار دهی کرد:
 var runningSum = sequence.Scan(seed: 10, accumulator: (accumulator, value) =>


متد Buffer

متد بافر، کار تقسیم یک توالی را به توالی‌های کوچکتر، بر اساس زمان، یا تعداد عنصر مشخص شده، انجام می‌دهد. برای مثال در برنامه‌های دسکتاپ شاید نیازی نباشد تا به ازای هر عنصر توالی، یکبار رابط کاربری را به روز کرد. عموما بهتر است تا تعداد مشخصی از عناصر یکجا پردازش شده و نتیجه‌ی این پردازش به تدریج نمایش داده شود.
var sequence = Enumerable.Range(1, 200)
                 .ToObservable()
                 .Buffer(count: 10);

sequence.Subscribe(onNext: numbers =>
{
   Console.WriteLine(numbers.Sum());
});
در اینجا نحوه‌ی استفاده از متد بافر را به همراه مشخص کردن تعداد اعضای بافر ملاحظه می‌کنید. هربار که onNext متد Subscribe فراخوانی شود، 10 عنصر از توالی را در اختیار خواهیم داشت (بجای یک عنصر حالت متداول بافر نشده).
به این ترتیب می‌توان فشار حجم اطلاعات ورودی با فرکانس بالا را کنترل کرد و در نتیجه از منابع موجود بهتر استفاده نمود. برای مثال اگر می‌خواهید عملیات bulk insert را انجام دهید، می‌توان بر اساس یک batch size مشخص، گروه گروه اطلاعات را به بانک اطلاعاتی اضافه کرد تا فشار کار کاهش یابد.

همینکار را بر اساس زمان نیز می‌توان انجام داد:
 var sequence = Enumerable.Range(1, 200)
                   .ToObservable()
                   .Buffer(timeSpan: TimeSpan.FromSeconds(2));
در مثال فوق هر 2 ثانیه یکبار، مجموعه‌ای از عناصر به متد onNext ارسال خواهند شد.


متد Window

متد Window نیز دقیقا همان پارامترهای متد بافر را قبول می‌کند. با این تفاوت که هربار، یک توالی obsevable را به متد onNext ارسال می‌کند.
نوع numbers پارامتر onNext، در حین بکارگیری متد بافر در مثال‌های فوق، IList of int است. اما اگر متدهای Buffer را تبدیل به متد Window کنیم، اینبار نوع numbers، معادل IObservable of int خواهد شد.
 var sequence = Enumerable.Range(1, 200)
                           .ToObservable()
                           .Window(timeSpan: TimeSpan.FromSeconds(2));

sequence.Subscribe(onNext: numbers =>
{
       numbers.Subscribe(onNext: number => Console.WriteLine(number));
});


چه زمانی باید از Buffer استفاده کرد و چه زمانی از Window؟

در متد بافر، به ازای هر توالی که به پارامتر onNext ارسال می‌شود، یکبار وهله‌ی جدیدی از توالی مدنظر در حافظه ایجاد و ارسال خواهد شد. در متد Window صرفا اشاره‌گرهایی به این توالی را در اختیار داریم؛ بنابراین مصرف حافظه‌ی کمتری را شاهد خواهیم بود. متد Window بسیار مناسب است برای اعمال aggregation. مثلا اگر نیاز است جمع، میانگین، حداقل و حداکثر عناصر دریافتی محاسبه شوند، بهتر است از متد Window استفاده شود که نهایتا قابلیت استفاده از متدهای الحاقی Sum و Max و Min را به همراه دارد. با این تفاوت که حاصل این‌ها نیز یک IObservable است که باید Subscribe آن‌را برای دریافت نتیجه فراخوانی کرد:
 var sequence = Enumerable.Range(1, 200)
                          .ToObservable()
                          .Window(10);

sequence.Subscribe(onNext: numbers =>
{
     numbers.Sum().Subscribe(onNext: number => Console.WriteLine(number));
});
در این حالت متد Window، برخلاف متد Buffer، توالی numbers را هربار کش نمی‌کند و به این ترتیب می‌توان به مصرف حافظه‌ی کمتری رسید.


کاربردهای دنیای واقعی

در اینجا دو مثال از بکارگیری متد Buffer را جهت پردازش مجموعه‌های عظیمی از اطلاعات و نمایش همزمان آن‌ها در رابط کاربری ملاحظه می‌کنید.
مثال اول: فرض کنید قصد دارید تمام فایل‌های درایو C خود را توسط یک TreeView نمایش دهید. در این حالت نباید رابط کاربری برنامه در حالت هنگ به نظر برسد. همچنین به علت زیاد بودن تعداد فایل‌ها و نمایش همزمان آن‌ها در UI، نباید CPU Usage برنامه تا حدی باشد که در کار سایر برنامه‌ها اخلال ایجاد کند. در این مثال‌ها با استفاده از Rx و متد بافر آن، هربار مثلا 1000 آیتم را بافر کرده و سپس یکجا در TreeView نمایش می‌دهند. به این ترتیب دو شرط یاد شده محقق می‌شوند.
The Rx Framework By Example

مثال دوم: خواندن تعداد زیادی رکورد از بانک اطلاعاتی به همراه نمایش همزمان آن‌ها در UI بدون اخلالی در کار سیستم و همچنین هنگ کردن برنامه.
Using Reactive Extensions for Streaming Data from Database