مطالب
OpenCVSharp #6
نمایش ویدیو و اعمال فیلتر بر روی آن

در قسمت قبل با نحوه‌ی نمایش تصاویر OpenCV در برنامه‌های دات نتی آشنا شدیم. در این قسمت قصد داریم همان نکات را جهت پخش یک ویدیو توسط OpenCVSharp بسط دهیم.


روش‌های متفاوت پخش ویدیو و یا کار با یک Capture Device

OpenCV امکان کار با یک WebCam، دوربین و یا فیلم‌های آماده را دارد. برای این منظور کلاس CvCapture در OpenCVSharp پیش بینی شده‌است. در اینجا قصد داریم جهت سهولت پیگیری بحث، یک فایل avi را به عنوان منبع CvCapture معرفی کنیم:
using (var capture = new CvCapture(@"..\..\Videos\drop.avi"))
{
     var image = capture.QueryFrame();
}
روش کلی کار با CvCapture را در اینجا ملاحظه می‌کنید. متد QueryFrame هربار یک frame از ویدیو را بازگشت می‌دهد و می‌توان آن‌را در یک حلقه، تا زمانیکه image نال بازگشت داده نشده، ادامه داد. همچنین برای نمایش آن نیز می‌توان از یکی از روش‌های مطرح شده، مانند picture box استاندارد یا PictureBoxIpl (روش توصیه شده) استفاده کرد. اگر از PictureBoxIpl استفاده می‌کنید، متد pictureBoxIpl1.RefreshIplImage آن دقیقا برای یک چنین مواردی طراحی شده‌است تا سربار نمایش تصاویر را به حداقل برساند.
در اینجا اولین روشی که جهت به روز رسانی UI به نظر می‌رسد، استفاده از متد Application.DoEvents است تا UI فرصت داشته باشد، تعداد فریم‌های بالا را نمایش دهد و خود را به روز کند:
IplImage image;
while ((image = Capture.QueryFrame()) != null)
{
    _pictureBoxIpl1.RefreshIplImage(image);
 
    Thread.Sleep(interval);
    Application.DoEvents();
}
این روش هرچند کار می‌کند اما همانند روش استفاده از متد رخدادگردان Application Do Idle که صرفا در زمان بیکاری برنامه فراخوانی می‌شود، سبب خواهد شد تا تعدادی فریم را از دست دهید، همچنین با CPU Usage بالایی نیز مواجه شوید.
روش بعدی، استفاده از یک تایمر است که Interval آن بر اساس نرخ فریم‌های ویدیو تنظیم شده‌است:
timer = new Timer();
timer.Interval = (int)(1000 / Capture.Fps);
timer.Tick += Timer_Tick;
این روش بهتر است از روش DoEvents و به خوبی کار می‌کند؛ اما باز هم کار دریافت و همچنین پخش فریم‌ها، در ترد اصلی برنامه انجام خواهد شد.
روش بهتر از این، انتقال دریافت فریم‌ها به تردی جداگانه و پخش آن‌ها در ترد اصلی برنامه است؛ زیرا نمی‌توان GUI را از طریق یک ترد دیگر به روز رسانی کرد. برای این منظور می‌توان از BackgroundWorker دات نت کمک گرفت. رخ‌داد DoWork آن در تردی جداگانه و مجزای از ترد اصلی برنامه اجرا می‌شود، اما رخ‌داد ProgressChanged آن در ترد اصلی برنامه اجرا شده و امکان به روز رسانی UI را فراهم می‌کند.


استفاده از BackgroundWorker جهت پخش ویدیو به کمک OpenCVSharp


ابتدا دو دکمه‌ی Start و Stop را به فرم اضافه خواهیم کرد (شکل فوق).
سپس در زمان آغاز برنامه، یک PictureBoxIpl را به فرم جاری اضافه می‌کنیم:
private void FrmMain_Load(object sender, System.EventArgs e)
{
    _pictureBoxIpl1 = new PictureBoxIpl
    {
        AutoSize = true
    };
    flowLayoutPanel1.Controls.Add(_pictureBoxIpl1);
}
و یا همانطور که در قسمت پیشین نیز عنوان شد، می‌توانید این کنترل را به نوار ابزار VS.NET اضافه کرده و سپس به سادگی آن‌را روی فرم قرار دهید.

در دکمه‌ی Start، کار آغاز BackgroundWorker انجام خواهد شد:
private void BtnStart_Click(object sender, System.EventArgs e)
{
    if (_worker != null && _worker.IsBusy)
    {
        return;
    }
 
    _worker = new BackgroundWorker
    {
        WorkerReportsProgress = true,
        WorkerSupportsCancellation = true
    };
    _worker.DoWork += workerDoWork;
    _worker.ProgressChanged += workerProgressChanged;
    _worker.RunWorkerCompleted += workerRunWorkerCompleted;
    _worker.RunWorkerAsync();
 
    BtnStart.Enabled = false;
}
در اینجا یک سری خاصیت را مانند امکان لغو عملیات، جهت استفاده‌ی در دکمه‌ی Stop، به همراه تنظیم رخ‌دادگردان‌هایی جهت دریافت و نمایش فریم‌ها تعریف کرده‌ایم. کدهای این روال‌های رخدادگردان را در ادامه ملاحظه می‌کنید:
private void workerDoWork(object sender, DoWorkEventArgs e)
{
    using (var capture = new CvCapture(@"..\..\Videos\drop.avi"))
    {
        var interval = (int)(1000 / capture.Fps);
 
        IplImage image;
        while ((image = capture.QueryFrame()) != null &&
                _worker != null && !_worker.CancellationPending)
        {
            _worker.ReportProgress(0, image);
            Thread.Sleep(interval);
        }
    }
}
 
private void workerProgressChanged(object sender, ProgressChangedEventArgs e)
{
    var image = e.UserState as IplImage;
    if (image == null) return;
 
    Cv.Not(image, image);
    _pictureBoxIpl1.RefreshIplImage(image);
}
 
private void workerRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    _worker.Dispose();
    _worker = null;
    BtnStart.Enabled = true;
}
متد workerDoWork کار دریافت فریم‌ها را در یک ترد مجزای از ترد اصلی برنامه به عهده دارد. این فریم‌ها توسط متد ReportProgress به متد workerProgressChanged جهت نمایش نهایی ارسال خواهند شد. این متد در ترد اصلی برنامه اجرا می‌شود و در اینجا کار با UI، مشکلی را به همراه نخواهد داشت و برنامه کرش نمی‌کند. اگر در متد workerDoWork کار به روز رسانی UI را مستقیما انجام دهیم، چون ترد اجرایی آن، با ترد اصلی برنامه یکی نیست، برنامه بلافاصله کرش خواهد کرد.
متد workerRunWorkerCompleted در پایان کار نمایش ویدیو، به صورت خودکار فراخوانی شده و در اینجا می‌توانیم دکمه‌ی Start را مجددا فعال کنیم.
همچنین در حین نمایش ویدیو، با کلیک بر روی دکمه‌ی Stop، می‌توان درخواست لغو عملیات را صادر کرد:
private void BtnStop_Click(object sender, System.EventArgs e)
{
    if (_worker != null)
    {
        _worker.CancelAsync();
        _worker.Dispose();
    }
    BtnStart.Enabled = true;
}


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید.
مطالب
React 16x - قسمت 21 - کار با فرم‌ها - بخش 4 - چند تمرین
پس از فراگیری اصول کار کردن با فرم‌ها در React، اکنون می‌خواهیم چند فرم جدید را برای تمرین بیشتر، به برنامه‌ی نمایش لیست فیلم‌ها اضافه کنیم؛ مانند فرم ثبت نام، فرمی برای ثبت و یا ویرایش فیلم‌ها و یک فرم جستجوی سریع در لیست فیلم‌های موجود.

تمرین 1 - ایجاد فرم ثبت نام


می‌خواهیم به برنامه، فرم ثبت نام را که حاوی سه فیلد نام کاربری، کلمه‌ی عبور و نام است، اضافه کنیم. نام کاربری باید از نوع ایمیل باشد. بنابراین اعتبارسنجی مرتبطی نیز باید برای این فیلد تعریف شود. کلمه‌ی عبور وارد شده باید حداقل 5 حرف باشد. همچنین تا زمانیکه اعتبارسنجی فرم تکمیل نشده‌است، باید دکمه‌ی submit فرم، غیرفعال باقی بماند. لینک ورود به این فرم نیز باید به منوی راهبری سایت اضافه شود.

برای حل این تمرین، فایل جدید registerForm.jsx را در پوشه‌ی components ایجاد می‌کنیم و سپس توسط میانبرهای imrc و cc در VSCode، ساختار ابتدایی کامپوننت RegisterForm را ایجاد کرده و سپس آن‌را به صورت زیر تکمیل می‌کنیم:
- ابتدا در فایل app.js، پس از import ماژول آن:
import RegisterForm from "./components/registerForm";
در ابتدای سوئیچ تعریف شده، مسیریابی آن‌را تعریف می‌کنیم:
<Route path="/register" component={RegisterForm} />
- سپس در فایل src\components\navBar.jsx، لینک به آن‌را، در انتهای لیست اضافه می‌کنیم، تا در منوی راهبری ظاهر شود:
<NavLink className="nav-item nav-link" to="/register">
   Register
</NavLink>
- در ادامه کدهای کامل کامپوننت ثبت نام را ملاحظه می‌کنید:
import Joi from "@hapi/joi";
import React from "react";

import Form from "./common/form";

class RegisterForm extends Form {
  state = {
    data: { username: "", password: "", name: "" },
    errors: {}
  };

  schema = {
    username: Joi.string()
      .required()
      .email({ minDomainSegments: 2, tlds: { allow: ["com", "net"] } })
      .label("Username"),
    password: Joi.string()
      .required()
      .min(5)
      .label("Password"),
    name: Joi.string()
      .required()
      .label("Name")
  };

  doSubmit = () => {
    // Call the server
    console.log("Submitted");
  };

  render() {
    return (
      <div>
        <h1>Register</h1>
        <form onSubmit={this.handleSubmit}>
          {this.renderInput("username", "Username")}
          {this.renderInput("password", "Password", "password")}
          {this.renderInput("name", "Name")}
          {this.renderButton("Register")}
        </form>
      </div>
    );
  }
}

export default RegisterForm;
- ابتدا این کامپوننت را بجای ارث بری از Component خود React، از کامپوننت Form که در قسمت قبل ایجاد کردیم، ارث بری می‌کنیم تا به تمام امکانات آن مانند اعتبارسنجی، مدیریت حالت و متدهای کمکی تعریف فیلدها و دکمه‌ها بهره‌مند شویم.
- سپس state این کامپوننت را با شیءای حاوی دو خاصیت data و error، مقدار دهی اولیه می‌کنیم. خواص متناظر با المان‌های فرم را نیز به صورت یک شیء، به خاصیت data انتساب داده‌ایم.
- پس از آن، خاصیت schema تعریف شده‌است؛ تا قواعد اعتبارسنجی تک تک فیلدهای فرم را به کمک کتابخانه‌ی Joi، مطابق نیازمندی‌هایی که در ابتدای تعریف این تمرین مشخص کردیم، ایجاد کند.
- در ادامه، متد doSubmit را ملاحظه می‌کنید. این متد پس از کلیک بر روی دکمه‌ی Register و پس از اعتبارسنجی موفقیت آمیز فرم، به صورت خودکار فراخوانی می‌شود.
- در آخر، تعریف فرم ثبت‌نام را مشاهده می‌کنید که نکات آن‌را در قسمت قبل، با معرفی کامپوننت Form و افزودن متدهای کمکی رندر input و button به آن، بررسی کردیم و در کل با نکات بررسی شده‌ی در فرم لاگینی که تا به اینجا ایجاد کردیم، تفاوتی ندارد.


تمرین 2- ایجاد فرم ثبت و یا ویرایش یک فیلم


فرم جدید ثبت و ویرایش یک فیلم، نکات بیشتری را به همراه دارد. در اینجا می‌خواهیم در بالای لیست نمایش فیلم‌ها، یک دکمه‌ی new movie را اضافه کنیم تا با کلیک بر روی آن، به فرم ثبت و ویرایش فیلم‌ها هدایت شویم. این فرم، از فیلدهای یک عنوان متنی، انتخاب ژانر از یک drop down list، تعداد موجود (بین 1 و 100) و امتیاز (بین صفر تا 10) تشکیل شده‌است. همچنین تا زمانیکه اعتبارسنجی فرم تکمیل نشده‌است، دکمه‌ی submit فرم باید غیرفعال باقی بماند. پس از ذخیره شدن این فیلم (در لیست درون حافظه‌ای برنامه)، با مراجعه‌ی به لیست فیلم‌ها و انتخاب آن از لیست (با کلیک بر روی لینک آن)، باید مجددا به همین فرم، در حالت ویرایش این رکورد هدایت شویم. به علاوه اگر در بالای صفحه یک id اشتباه وارد شد، باید صفحه‌ی «پیدا نشد» نمایش داده شود.

کامپوننت MovieForm و مسیریابی آن‌را در قسمت 17، تعریف و اضافه کردیم. برای تعریف لینکی به آن، به کامپوننت movies مراجعه کرده و بالای متنی که تعداد کل آیتم‌های موجود در بانک اطلاعاتی را نمایش می‌دهد، المان زیر را اضافه می‌کنیم:
import { Link } from "react-router-dom";
// ...


<div className="col">
  <Link
    to="/movies/new"
    className="btn btn-primary"
    style={{ marginBottom: 20 }}
  >
    New Movie
  </Link>
  <p>Showing {totalCount} movies in the database.</p>
این Link را هم با کلاس btn مزین کرده‌ایم تا شبیه به یک دکمه، به نظر برسد. با کلیک بر روی آن، به آدرس movies/new هدایت خواهیم شد؛ یعنی id جدید این مسیریابی را به "new" تنظیم کرده‌ایم که در ادامه بر اساس آن، تفاوت بین حالت ویرایش و حالت ثبت اطلاعات، مشخص می‌شود.


سپس به کامپوننت src\components\movieForm.jsx که پیشتر آن‌را اضافه کرده بودیم، مراجعه کرده و به صورت زیر آن‌را تکمیل می‌کنیم:
import Joi from "@hapi/joi";
import React from "react";

import { getGenres } from "../services/fakeGenreService";
import { getMovie, saveMovie } from "../services/fakeMovieService";
import Form from "./common/form";

class MovieForm extends Form {
  state = {
    data: {
      title: "",
      genreId: "",
      numberInStock: "",
      dailyRentalRate: ""
    },
    genres: [],
    errors: {}
  };
- ابتدا importهای مورد نیاز به Joi، React و همچنین سرویس‌های لیست فیلم‌ها و لیست ژانرهای سینمایی، به همراه کامپوننت فرم، تعریف شده‌اند.
- سپس این کامپوننت نیز از کامپوننت Form ارث بری می‌کند تا به امکانات ویژه‌ی آن دسترسی پیدا کند.
- در ادامه در خاصیت state، طبق روالی که در کامپوننت فرم درنظر گرفته‌ایم، دو خاصیت data و errors باید حضور داشته باشند. در خاصیت data، شیءای که نام خاصیت‌های آن با فیلدهای فرم تطابق دارد، ذکر شده‌اند. در اینجا برای ذخیره سازی اطلاعات انتخاب شده‌ی از drop down list مرتبط با ژانرهای سینمایی، از خاصیت genreId استفاده می‌شود؛ این تنها اطلاعاتی است که از کل آیتم‌های یک drop down list نیاز داریم. آرایه‌ی genres که آیتم‌های این drop down list را مقدار دهی می‌کند، در روال componentDidMount، از سرویس مرتبطی دریافت و مقدار دهی خواهد شد.

در ادامه‌ی کدهای کامپوننت MovieForm، کدهای schema اعتبارسنجی شیء data را ملاحظه می‌کنید:
  schema = {
    _id: Joi.string(),
    title: Joi.string()
      .required()
      .label("Title"),
    genreId: Joi.string()
      .required()
      .label("Genre"),
    numberInStock: Joi.number()
      .required()
      .min(0)
      .max(100)
      .label("Number in Stock"),
    dailyRentalRate: Joi.number()
      .required()
      .min(0)
      .max(10)
      .label("Daily Rental Rate")
  };
در اینجا، id به required تنظیم نشده‌است؛ چون زمانیکه قرار است یک شیء movie جدید را  ایجاد کنیم، هنوز این id نامشخص است. سایر موارد خاصیت schema، به لطف fluent api کتابخانه‌ی Joi، بسیار خوانا بوده و نیاز به توضیحات خاصی ندارند. برای مثال هر دو خاصیت numberInStock و  dailyRentalRate باید عددی وارد شده و بین بازه‌ی مشخصی قرار گیرند.

اکنون به مرحله‌ی componentDidMount می‌رسیم:
  componentDidMount() {
    const genres = getGenres();
    this.setState({ genres });

    const movieId = this.props.match.params.id;
    if (movieId === "new") return;

    const movie = getMovie(movieId);
    if (!movie) return this.props.history.replace("/not-found");

    this.setState({ data: this.mapToViewModel(movie) });
  }
- در اینجا لیست ژانرهای سینمایی از متد getGenres فایل src\services\fakeGenreService.js دریافت شده و پس از آن کار به روز رسانی خاصیت genres در state را انجام می‌دهیم. این به روز رسانی state، سبب می‌شود تا این خاصیت که آرایه‌ای است، در رندر بعدی این کامپوننت، به لیست options مربوط به drop down list درج شده‌ی در فرم، ارسال شده و در فرم رندر شود.
- پس از آن، نحوه‌ی دریافت پارامتر id مسیریابی رسیده را ملاحظه می‌کنید. این id اگر به "new" تنظیم شده بود، یعنی قرار است، اطلاعات جدیدی ثبت شوند. بنابراین متد جاری را خاتمه می‌دهیم (چون کار ادامه‌ی این متد، مقدار دهی اولیه‌ی تمام فیلدهای فرم، بر اساس اطلاعات شیء دریافت شد‌ه‌ی از سرویس فیلم‌ها است). در غیراینصورت (و با مشخص بودن id)، با استفاده از این id و متد getMovie سرویس src\services\fakeMovieService.js، سعی خواهیم کرد تا اطلاعات شیء movie متناظری را دریافت کنیم. اگر خروجی این متد null بود، یعنی id وارد شده معتبر نیست. به همین جهت کاربر را به صفحه‌ی not-found هدایت می‌کنیم. اگر دقت کنید در اینجا بجای متد push، از متد replace استفاده کرده‌ایم. چون اگر از متد push استفاده می‌کردیم و کاربر بر روی دکمه‌ی back مرورگر کلیک می‌کرد، دوباره به همین صفحه، با id غیرمعتبر قبلی وارد می‌شد و یک حلقه‌ی بی‌پایان رخ می‌داد. همچنین به return ای هم که به همراه متد replace استفاده شده، دقت کنید. کار redirect به یک صفحه‌ی دیگر، به معنای عدم اجرای کدهای پس از آن نیست. بنابراین اگر می‌خواهیم کار این متد با redirect، به پایان برسد، ذکر return الزامی است.
- در پایان این متد، خاصیت data موجود در state را به روز رسانی می‌کنیم؛ تا سبب رندر فرم، با اطلاعات شیء movie یافت شده گردد و چون ساختار شیء movie دریافت شده‌ی از سرویس، با ساختار data تعریف شده‌ی در state یکی نیست، نیاز به نگاشت این دو به هم، توسط متد سفارشی mapToViewModel زیر است:
  mapToViewModel(movie) {
    return {
      _id: movie._id,
      title: movie.title,
      genreId: movie.genre._id,
      numberInStock: movie.numberInStock,
      dailyRentalRate: movie.dailyRentalRate
    };
  }
این سناریو بسیار متداول است و اکثر داده‌های دریافت شده‌ی از سرور، الزاما با ساختار داده‌هایی که در فرم‌های خود تعریف می‌کنیم (که در اینجا view-model نام گرفته)، یکی نیستند و نیاز به نگاشت بین آن‌ها وجود دارد. برای مثال genreId موجود در view-model این فرم (همان شیء منتسب به data در state)، دقیقا به همین نام، در شیء movie تعریف نشده‌است و نیاز به نگاشت این دو به هم است.

در ادامه‌ی کدهای کامپوننت فرم فیلم‌ها، به متد doSubmit می‌رسیم:
  doSubmit = () => {
    saveMovie(this.state.data);

    this.props.history.push("/movies");
  };
این متد پس از کلیک کاربر بر روی دکمه‌ی submit و اعتبارسنجی کامل فرم، فراخوانی می‌شود. در این مرحله می‌توان اطلاعات موجود در شیء data را به متد saveMovie سرویس src\services\fakeMovieService.js ارسال کرد، تا آن‌را به لیست خودش اضافه کند. سپس کاربر را به لیست به روز شده‌ی فیلم‌ها هدایت می‌کنیم.

در انتهای این کامپوننت نیز به متد رندر آن می‌رسیم:
  render() {
    return (
      <div>
        <h1>Movie Form</h1>
        <form onSubmit={this.handleSubmit}>
          {this.renderInput("title", "Title")}
          {this.renderSelect("genreId", "Genre", this.state.genres)}
          {this.renderInput("numberInStock", "Number in Stock", "number")}
          {this.renderInput("dailyRentalRate", "Rate")}
          {this.renderButton("Save")}
        </form>
      </div>
    );
  }
تمام قسمت‌های این فرم را منهای متد جدید renderSelect آن، پیشتر در قسمت قبل، مرور کرده‌ایم و نکته‌ی جدیدی ندارند.
برای تعریف متد جدید renderSelect به این صورت عمل می‌کنیم:
- ابتدا فایل جدید src\components\common\select.jsx را ایجاد کرده و سپس آن‌را جهت نمایش یک drop down list، ویرایش می‌کنیم:
import React from "react";

const Select = ({ name, label, options, error, ...rest }) => {
  return (
    <div className="form-group">
      <label htmlFor={name}>{label}</label>
      <select name={name} id={name} {...rest} className="form-control">
        <option value="" />
        {options.map(option => (
          <option key={option._id} value={option._id}>
            {option.name}
          </option>
        ))}
      </select>
      {error && <div className="alert alert-danger">{error}</div>}
    </div>
  );
};

export default Select;
شبیه به یک چنین کامپوننتی را در قسمت قبل، در فایل src\components\common\input.jsx ایجاد کردیم و ساختار کلی آن‌ها با هم یکی است. ابتدا تمام تگ‌ها و کلاس‌های بوت استرپی مورد نیاز، در این کامپوننت محصور می‌شوند. سپس آرایه‌ای بر روی لیست options رسیده، ایجاد شده و به صورت پویا، لیست نمایش داده شده‌ی توسط drop down آن‌را تشکیل می‌دهد. در پایان آن هم کار نمایش اخطار اعتبارسنجی متناظری، در صورت وجود خطایی، قرار گرفته‌است.

- پس از آن به کامپوننت src\components\common\form.jsx مراجعه کرده و متد رندر آن‌را اضافه می‌کنیم:
import Select from "./select";
// ...

class Form extends Component {

  // ...

  renderSelect(name, label, options) {
    const { data, errors } = this.state;

    return (
      <Select
        name={name}
        value={data[name]}
        label={label}
        options={options}
        onChange={this.handleChange}
        error={errors[name]}
      />
    );
  }
}
کار این متد، مقدار دهی ویژگی‌های مورد نیاز کامپوننت Select، بر اساس نام فیلد، یک برچسب و آیتم‌های ارسالی به آن است. مزیت وجود یک چنین متد کمکی، کم شدن کدهای تکراری Selectهای مورد نیاز و همچنین عدم فراموشی قسمتی از این اتصالات و در نهایت یک‌دست شدن کدهای کل برنامه‌است. این متد در نهایت سبب رندر یک drop down list، بر اساس اطلاعات خاصیت genres موجود در state می‌شود:



تمرین 3- جستجوی در لیست فیلم‌ها


می‌خواهیم در بالای لیست نمایش فیلم‌ها، یک search box را قرار دهیم تا توسط آن بتوان بر اساس عنوان وارد شده، در فیلم‌های موجود جستجو کرد. همچنین این جستجو قرار است کلی بوده و حتی در صورت انتخاب ژانر خاصی از منوی کنار صفحه، باید در کل اطلاعات موجود جستجو کند. به علاوه اگر کاربر ژانری را انتخاب کرد، این text box باید خالی شود.

برای اینکار ابتدا فایل جدید src\components\searchBox.jsx را ایجاد کرده و به صورت زیر آن‌را تکمیل می‌کنیم:
import React from "react";

const SearchBox = ({ value, onChange }) => {
  return (
    <input
      type="text"
      name="query"
      className="form-control my-3"
      placeholder="Search..."
      value={value}
      onChange={e => onChange(e.currentTarget.value)}
    />
  );
};

export default SearchBox;
این SeachBox، یک controlled component است و دارای state خاص خودش نیست. تمام اطلاعات مورد نیاز خود را از طریق props دریافت کرده و خروجی خود را (اطلاعات تایپ شده‌ی در input box را) از طریق صدور رخ‌دادها، اطلاع رسانی می‌کند.

سپس به کامپوننت movies مراجعه کرده و آن‌را ذیل متن نمایش تعداد رکوردها، درج می‌کنیم:
<p>Showing {totalCount} movies in the database.</p>
<SearchBox value={searchQuery} onChange={this.handleSearch} />
که البته نیاز به import کامپوننت مربوطه، تعریف واژه‌ی جستجو شده در state و مدیریت رخ‌داد onChange را نیز دارد:
import SearchBox from "./searchBox";
//...

class Movies extends Component {
  state = {
    //...
    selectedGenre: {},
    searchQuery: ""
  };


  handleSearch = query => {
    this.setState({ searchQuery: query, selectedGenre: null, currentPage: 1 });
  };

  handleGenreSelect = genre => {
    console.log("handleGenreSelect", genre);
    this.setState({ selectedGenre: genre, searchQuery: "", currentPage: 1 });
  };
در متد handleSearch، اطلاعات وارد شده‌ی توسط کاربر دریافت شده و توسط آن سه خاصیت state به روز رسانی می‌شوند تا توسط آن‌ها در حین رندر مجدد کامپوننت، کار فیلتر صحیح اطلاعات صورت گیرد. همچنین selectedGenre نیز به حالت اول بازگشت داده می‌شود. به علاوه اگر کاربر در حین مشاهده‌ی صفحه‌ی 3 بود، نیاز است currentPage صحیحی را به او نمایش  داد.
متد handleGenreSelect را نیز اندکی تغییر داده‌ایم تا اگر گروهی انتخاب شد، مقدار searchQuery را خالی کند. اگر در اینجا searchQuery را به نال تنظیم می‌کردیم، controlled component جعبه‌ی جستجو، تبدیل به کامپوننت کنترل نشده‌ای می‌شد و در این حالت، React، اخطار تبدیل بین این دو را صادر می‌کرد.

در آخر، ابتدای متد getPageData هم جهت اعمال searchQuery، به صورت زیر تغییر می‌کند:
  getPagedData() {
    const {
      pageSize,
      currentPage,
      selectedGenre,
      movies: allMovies,
      sortColumn,
      searchQuery
    } = this.state;

    let filteredMovies = allMovies;
    if (searchQuery) {
      filteredMovies = allMovies.filter(m =>
        m.title.toLowerCase().startsWith(searchQuery.toLowerCase())
      );
    } else if (selectedGenre && selectedGenre._id) {
      filteredMovies = allMovies.filter(m => m.genre._id === selectedGenre._id);
    }
در اینجا اگر searchQuery مقداری داشته باشد، یک جستجوی غیرحساس به کوچکی و بزرگی حروف، بر روی خاصیت title اشیاء فیلم، انجام می‌شود.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-21.zip
مطالب
تزریق وابستگی‌ها در ASP.NET Core - بخش 6 - Implementation Factory

در بعضی از شرایط پیش رفته، ممکن است نمونه سازی از یک Implementation Type، نیاز به دخالت مستقیم ما را داشته باشد. Implementation Factory کنترل بیشتری بر چگونگی استفاده‌ی از Implementation Type‌ها را  به ما ارائه می‌دهد. در هنگام ثبت سرویسی که Implementation Factory را در اختیار ما قرار می‌دهد، ما یک Delegate را برای فراخوانی سرویس استفاده می‌کنیم. این Delegate مسئول ساخت یک نمونه از Service Type است. برای مثال وقتی از الگوهای builder یا factory برای ساخت یک شیء استفاده می‌کنید، شاید نیاز باشد که Implementation Factory را به صورت دستی پیاده سازی کنید. اولین قدم این است که کدتان را در صورت امکان چنان refactor کنید تا DI Container بتواند آن را به صورت خودکار بسازد؛ ولی اینکار همیشه ممکن نیست. برای مثال بعضی از برنامه نویسان ترجیح می‌دهند یک Config را مستقیما از IOptionMonitor   بگیرند و بعد در هر جائیکه خواستند، بجای تزریق IOptionMonitor به سرویس، مستقیما از همان سرویس ثبت شده استفاده کنند:

services.AddSingleton<ILiteDbConfig>(sp =>sp.GetRequiredService<IOptionsMonitor<LiteDbConfig>>().CurrentValue);

پیاده سازیComposite Pattern 

یک کاربرد بالقوه‌ی دیگر برای Implementation Factory ، استفاده از Composite Pattern است. هر چند Microsoft DI Container به صورت پیش فرض از Composite Pattern پشتیبانی نمی‌کند، ولی ما می‌توانیم آن‌را پیاده سازی کنیم. فرض کنید که قبلا به ازای انجام کاری، به کاربر یک ایمیل را می‌فرستادیم؛ ولی حالا مالک محصول می‌آید و می‌گوید که علاوه بر ایمیل، باید پیامک هم بفرستید و ما یا این سرویس پیامک را از قبل داریم و یا باید آن را بسازیم که فرض می‌کنیم از قبل آن را داریم. برای این کار ما یک اینترفیس کلی‌تر به نام INotificationService می‌سازیم که دو سرویس IEmailNotificationService و ISmsNotificaitonService از آن ارث بری می‌کنند:

public interface INotificationService
{
      void SendMessage(string msg, int userId);
}
حالا CompositeNotificationService را به این صورت تعریف می‌کنیم:
    public class CompositeNotificationService : INotificationService
    {
        private readonly IEnumerable<INotificationService> _notificationServices;
        public CompositeNotificationService(IEnumerable<INotificationService> notificationServices)
        {
            _notificationServices = notificationServices;
        }

        public void SendMessage(string msg, int userId)
        {
            foreach (var notificationServicei in _notificationServices) 
            {
                notificationServicei.SendMessage(msg, userId);
            }
        }
    }
و این سرویس‌ها را به این صورت در DI Container ثبت می‌کنیم: 
services.AddScoped<IEmailNotificationService, EmailNotificationService>();
services.AddScoped<ISMSNotificationService, SMSNotificationService>();

services.AddSingleton<INotificationService>(sp => new CompositeNotificationService(
      new INotificationService[] { 
          sp.GetRequiredService<IEmailNotificationService>() , 
          sp.GetRequiredService<ISMSNotificationService>()
      }
));
حالا هر زمانیکه بخواهیم همزمان، هم ایمیل و هم پیامک بفرستیم، کافی است که سرویس INotificationService را در سازنده‌ی کلاس مورد نظر تزریق کرده و از آن در مکان‌ها و شرایط مختلفی استفاده کنیم. اگر هر کدام از سرویس‌های ارسال ایمیل و سرویس‌های پیامک را به صورت جداگانه بخواهیم، می‌توانیم آنها را به صورت تکی ثبت و استفاده کنیم.  


وهله سازی سفارشی

در مثال بعدی نشان می‌دهیم که چطور می‌توانیم از Implementation Factory برای برگرداندن پیاده‌سازی سرویس‌هایی که Service Provider امکان ساخت خودکار آنها را ندارد، استفاده کنیم. فرض کنید یک کلاس Account داریم که از IAccount ارث بری می‌کند و برای ساخت آن باید از متدهای IAccountBuilder که فرآیند ساخت را انجام می‌دهند، استفاده کنیم. بنابراین امکان ساخت مستقیم یک شیء از IAccount وجود ندارد. در این حالت بدین صورت عمل می‌کنیم: 

services.AddTransient<IAccountBuilder, AccountBuilder>();

services.AddScoped<IAccount>(sp => {
    var builder = sp.GetService<IAccountBuilder>();
    builder.WithAccountType(AccountType.Guest);
    return builder.Build();
});


ثبت یک نمونه برای چندین اینترفیس

ممکن است بنا به دلایلی مجبور باشیم یک implementation Type را برای چند سرویس (اینترفیس) به ثبت برسانیم. در این حالت نمونه‌ی شیء ساخته شده‌، توسط هر کدام از اینترفیس‌ها قابل استفاده است. فرض کنید یک سرویس Greeting داریم که پیش از این فقط اینترفیس IHomeGreetingService را پیاده سازی می‌کرد؛ ولی بنا به دلایلی تصمیم گرفتیم که سرویسی جامع‌تر را با نیازمندی‌های دیگری نیز تعریف کنیم و GreetingService آن را پیاده سازی کند: 

public class GreetingService : IHomeGreetingService , IGreetingService
{ // code here }

احتمالا اولین چیزی که به ذهنمان می‌رسد این است: 

services.TryAddSingleton<IHomeGreetingService, GreetingService>();
services.TryAddSingleton<IGreetingService, GreetingService>();

مشکل روش بالا این است که دو نمونه از GreetingService ساخته می‌شود و درون حافظه باقی می‌ماند و در حقیقت برای هر اینترفیس، یک نوع جداگانه از GreetingService ثبت می‌شود؛ در حالیکه ما می‌خواهیم هر دو اینترفیس فقط از یک نمونه از شیء GreetingService استفاده کنند.  دو راه حل برای این مشکل وجود دارد:

var greetingService = new GreetingService(Environment);
services.TryAddSingleton<IHomeGreetingService>(greetingService);
services.TryAddSingleton<IGreetingService>(greetingService);

در اینجا سازنده‌ی کلاس GreetingService فقط به  environment نیاز داشت که یکی از سرویس‌های پایه‌ی فریم ورک هست و در این مرحله در دسترس است. به صورت کلی مشکل روش بالا این است که ما مسئول نمونه سازی از سرویس GreetingService هستیم! اگر GreetingService برای ساخته شدن به سرویس‌ها یا ورودی هایی نیاز داشته باشد که فقط در زمان اجرا در دسترس باشند، ما امکان نمونه سازی آن‌ها را نداریم؛ علاوه بر این نمی‌توان از روش‌های بالای برای حالت‌های Scoped یا Transient  استفاده کرد.

روش بعدی همان روش استفاده از Implementation Factory است که در ادامه آن را می‌بینید: 

services.TryAddSingleton<GreetingService>();
services.TryAddSingleton<IHomeGreetingService>(sp => sp.GetRequiredService<GreetingService>());
services.TryAddSingleton<IGreetingService>(sp => sp.GetRequiredService<GreetingService>());

در این روش خود DI Container مسئول نمونه سازی از GreetingService است. علاوه بر این می‌توان با استفاده از روش فوق از طول حیات‌های Scoped و Transient هم استفاده کرد؛ در حالیکه در روش قبلی این کار امکان پذیر نبود.

 

Open Generics Service

گاهی از اوقات می‌خواهید سرویس‌هایی را ثبت کنید که از اینترفیسی جنریک ارث بری می‌کنند. هر نوع جنریک در زمان اجرا، نوع مخصوص به خود را واکشی می‌کند. ثبت کردن دستی این سرویس‌ها می‌تواند خسته کننده باشد. برای همین مایکروسافت در DI Container خود قابلیت ثبت و واکشی سرویس‌های جنریک را نیز در اختیار ما گذاشته‌است. بیایید نگاهی به سرویس ILogger<T>  بیندازیم. این یک سرویس درونی فریمورک است و می‌تواند به ازای هر نوع، کارهای مربوط به ثبت log را انجام بدهد و در پروژه‌ها معمولا از این اینترفیس برای ثبت لاگ‌ها در سطح کنترلر و سرویس‌ها استفاده می‌شود: 

public interface ILogger<out TCategoryName> : ILogger
{
}

در حالت عادی اگر سرویسی مشابه سرویس فوق را داشته باشیم، برای ثبت کردن هر سرویس با نوع جنریک اختصاصی آن، مجبوریم به صورت دستی آن را درون DI Container ثبت کنیم؛ مثلا باید به این صورت عمل کنیم: 

services.TryAddScoped<ILogger<HomeController>,Logger<HomeController>>();
این کاری طاقت فرساست. به همین جهت مایکروسافت قابلیت Open Generics Service را در اختیار ما گذاشته تا بتوانیم اینگونه سرویس‌ها را فقط و فقط یکبار ثبت کنیم: 
services.TryAddScoped(typeof(ILogger<>) , typeof(Logger<>));
و اینگونه می‌توانیم نمونه‌های مختلف از ILogger<T> را به هر جایی که خواستیم، تزریق کنیم.

 

دسته بندی سرویس‌ها درون متدهای مختلف و پاکسازی  متد ConfigurationService

 تا اینجای کار ما سرویس‌های مختلفی را به روش‌های مختلفی ثبت کرده‌ایم. حتی در همین آموزش ساده، تعداد زیاد سرویس‌های ثبت شده، باعث شلوغی و در هم ریختگی کدهای ما می‌شوند که خوانایی و در ادامه اشکال زدایی و توسعه‌ی کدها را برای ما سخت‌تر می‌کنند.  ساده‌ترین کار برای دسته بندی کدها، استفاده از متدهای private محلی یا استفاده از متدهای توسعه‌ای(الحاقی) است که در اینجا مثالی از استفاده از متدهای توسعه‌ای را آورده‌ام:
namespace AspNetCoreDependencyInjection.Extensions
{
    public static class DICRegisterationExetnsion
    {
        /// <summary>
        /// مثال ثبت برای اپن جنریت
        /// </summary>
        /// <param name="services"></param>
        public static void OpenGenericRegisterationExample(this IServiceCollection services)
        {
            services.TryAddScoped<ILogger<HomeController>, Logger<HomeController>>();
            services.TryAddScoped(typeof(ILogger<>), typeof(Logger<>));
        }


        /// <summary>
        /// ثبت تنظیمات به روش‌های مختلف 
        /// </summary>
        public static void RegisterConfiguration(this IServiceCollection services, IConfiguration configuration)
        {
            services.AddSingleton(services => new AppConfig
            {
                ApplicationName = configuration["ApplicationName"],
                GreetingMessage = configuration["GreetingMessage"],
                AllowedHosts = configuration["AllowedHosts"]
            });

            services.AddSingleton(services => new AccountTypeBalanceConfig(
                    new List<(AccountType, long)> {
                        (AccountType.Guest , Convert.ToInt64 (configuration["AccountInitialBalance.Guest"]) ) ,
                        (AccountType.Silver , Convert.ToInt64 (configuration["AccountInitialBalance.Silver"]) ) ,
                        (AccountType.Gold , Convert.ToInt64 (configuration["AccountInitialBalance.Gold"]) ) ,
                        (AccountType.Platinum , Convert.ToInt64 (configuration["AccountInitialBalance.Platinum"]) ) ,
                        (AccountType.Titanium , Convert.ToInt64 (configuration["AccountInitialBalance.Titanium"]) ) ,
                    })
            );

            services.AddSingleton(services => new LiteDbConfig
            {
                ConnectionString = configuration["LiteDbConfig:ConnectionString"],
            });

            services.Configure<UserOptionConfig>(configuration.GetSection("UserOptionConfig"));
        }
    }
}

حالا در کلاس ConfigureServices ، درون متدStartup ، به این صورت از این متدهای توسعه‌ای استفاده می‌کنیم:
services.RegisterConfiguration(this.Configuration);
services.OpenGenericRegisterationExample();

می‌توانید کد منبع این آموزش را در اینجا  ببینید.
نظرات مطالب
MVC vs 3-Tier Pattern
دوست عزیز من متوجه منظور شما نشدم. حرف من اینه که MVC، MVVM، MVP و .. در سطح UI پیاده میشن.
مطالب
آشنایی با الگوی M-V-VM‌ - قسمت چهارم

در این قسمت، MVVM Light Toolkit مورد بررسی قرار گرفته است (دریافت، نصب، به همراه ارائه 4 مثال جهت معرفی توانمند‌ی‌های آن)

فهرست مطالب:
فصل 4- آشنایی با MVVM Light Toolkit
  • سایر کتابخانه‌ها و Framework های موجود MVVM
  • نصب قالب‌های MVVM Light Toolkit مخصوص VS.Net 2008
  • نصب قالب‌های MVVM Light Toolkit مخصوص VS.Net 2010
  • نصب Code Snippets مجموعه MVVM Light Toolkit در VS.Net 2008/2010
  • نصب فایل‌های بایناری کتابخانه‌ی MVVM Light Toolkit
  • نصب قالب‌های MVVM Light Toolkit مخصوص Expression Blend
  • بررسی صحت نصب کتابخانه‌ی MVVM Light Toolkit
  • استفاده از Code Snippets نصب شده
  • مثال اول - بررسی RelayCommand
  • مثال دوم - بررسی Messenger
  • مثال سوم - بررسی Blendability
  • مثال چهارم - بررسی EventToCommand


دریافت قسمت چهارم
دریافت مثال‌های قسمت چهارم

مطالب
C# 8.0 - Async Streams
امکان تعریف نوع‌های شمارشی async در C# 8.0

فرض کنید قصد دارید یک متد async از نوع IEnumerable را که تعدادی yield return به تاخیر افتاده را به همراه دارد (yield return‌ها فقط زمانی اجرا می‌شوند که بر روی آن‌ها متدهایی مانند ToList و یا حلقه‌ی foreach اجرا شوند) و همچنین توسط await Task.Delay، دریافت اطلاعات به صورت async را نیز شبیه سازی می‌کند، تهیه کنید:
public struct Statement
{
    public int Id { get; }
    public string Description { get; }
    public Statement(int id, string description) => (Id, Description) = (id, description);
    public override string ToString() => Description;
}

public static async Task<IEnumerable<Statement>> GetStatements(bool error)
{
    if (error)
    {
       throw new Exception("Oops, we messed up 😬");
    }

    await Task.Delay(1000); //Simulate waiting for data to come through. 

    yield return new Statement(1, "C# is cool!");
    yield return new Statement(2, "C# orginally named COOL.");
    yield return new Statement(3, "More examples...");
}
این قطعه کد حتی در C# 8.0 نیز چنین خطای کامپایلری را به همراه دارد:
The body of 'AsyncStreams.GetStatements(bool)' cannot be an iterator block because
'Task<IEnumerable<AsyncStreams.Statement>>' is not an iterator interface type (CS1624)
عنوان می‌کند که برای دریافت اطلاعات متد GetStatements باید یک iterator تشکیل شود؛ اما Task IEnumerable از این نوع نیست.

برای رفع یک چنین مشکلی، اکنون در C# 8.0 می‌توان از اینترفیس جدید IAsyncEnumerable بجای Task IEnumerable استفاده کرد. به این ترتیب تنها تغییری که در قطعه کد فوق نیاز است، تغییر امضای آن به صورت زیر است:
static async IAsyncEnumerable<Statement> GetStatements(bool error)


امکان تعریف حلقه‌های async در C# 8.0

مرحله‌ی بعد، ایجاد حلقه‌ای بر روی متد GetStatements است. اکنون مشکل دیگری وجود دارد: حلقه‌ی foreach به خودی خود، یک حلقه‌ی synchronous است و اگر از آن برای کار با یک استریم async استفاده شود، هربار که اطلاعاتی از آن بازگشت داده می‌شود، پایان یک Task نیز گزارش داده خواهد شد که می‌توان سبب خاتمه‌ی حلقه شود. بنابراین انجام اینکار نیز پیش از C# 8.0 میسر نبود که اکنون با امکان تعریف await پیش از یک حلقه‌ی foreach، ممکن شده‌است:
static async IAsyncEnumerable<Statement> GetStatementsAsync(bool error)
{
    await foreach (var statement in GetStatements(error))
    {
      await Task.Delay(1000);
      yield return statement;
    }
}
تا پیش از C# 8.0، از واژه‌ی await تنها برای دریافت یک تک مقدار استفاده می‌شد؛ اما حالا می‌توان از آن برای دریافت استریمی از نتایج (async streams) نیز استفاده کرد.


اینترفیس IAsyncEnumerable چگونه تعریف شده‌است؟

 اینترفیس IAsyncEnumerable متد GetAsyncEnumerator را تعریف می‌کند که یک IAsyncEnumerator را بازگشت می‌دهد و آن نیز به همراه متد MoveNextAsync است. اگر دقت کنید در این حالت از نگارش async اینترفیس IDisposable به نام IAsyncDisposable استفاده کرده‌است:
using System.Threading;

namespace System.Collections.Generic
{
    public interface IAsyncEnumerable<out T>
    {
        IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default);
    }

    public interface IAsyncEnumerator<out T> : IAsyncDisposable
    {
        T Current { get; }

        ValueTask<bool> MoveNextAsync();
    }
}

namespace System
{
    public interface IAsyncDisposable
    {
        ValueTask DisposeAsync();
    }
}
اینترفیس‌های IAsyncDisposable و IAsyncEnumerator یک ValueTask را توسط متدهای DisposeAsync و MoveNextAsync بازگشت می‌دهند و این مورد به C# 7x باز می‌گردد که امکان await را نه تنها بر روی Task، بلکه بر روی هر نوعی که متد GetAwaiter را پیاده سازی می‌کند، میسر می‌کند و ValueTask نیز یکی از آن‌ها است. ValueTask به صورت یک نوع مقدار (value type) تعریف شده‌است؛ بجای نوع ارجاعی Task که سربار کمتری را به همراه دارد.


مثالی از IAsyncDisposable و روش Dispose خودکار آن

با معرفی IAsyncDisposable، اگر یک مثال ساده از پیاده سازی آن به صورت زیر باشد:
public class AwaitUsingTest : IAsyncDisposable
{
   public async ValueTask DisposeAsync()
   {
     await Task.CompletedTask;
   }

   public void Dummy() { }
}
روش فراخوانی using declaration بر روی آن به همراه واژه‌ی کلیدی await در C# 8.0، مانند مثال زیر است:
async Task FooBar()
{
   await using var test = new AwaitUsingTest();
   test.Dummy();
}
مطالب
ساخت دیتابیس sqlite با EF6 Code First
تا نسخه EF6 و minor‌های آن به دلیل عدم پشتیبانی داریور sqlite از migration، ساخت دیتابیس با code first ممکن نیست برای همین مجبور هستند از پیاده سازی‌های خودشان و موجود بودن دیتابیس از قبل با استفاده از EF با آن کار کنند که یکی از مثال‌های آن در این آدرس قرار دارد و سعی دارد کلاسی مشابه sqlitehelper در اندروید که کار ساخت دیتابیس و مدیریت نسخه را دارد بسازد و از آن استفاده کند. البته در EF7 این مشکل حل شده است و تیم دات نت تمهیداتی را برای آن اندیشیده‌اند. در این نوشتار قصد داریم با استفاده از یک کتابخانه که توسط آقای مارک سالین نوشته شده است کار ساخت دیتابیس را آسانتر کنیم. این کتابخانه که با دات نت 4 به بعد کار میکند خیلی راحت می‌تواند دیتابیس شما را به روش Code First ایجاد کند.

در حال حاضر این کتابخانه از مفاهیم زیر پشتیبانی می‌کند:

  • تبدیل کلاس به جدول با پشتیبانی از خصوصیت Table
  • تبدیل پراپرتی‌ها به ستون با پشتیبانی از خصوصیت هایی چون Column,Key,MaxLength,Required,Notmapped,DatabaseGenerated,Index
  • پشتیبانی از primarykey و کلید‌های ترکیبی
  • کلید خارجی و روابط یک  به چند و پشتیبانی از cascade on delete
  • فیلد غیر نال


برای شروع ابتدا کتابخانه مورد نظر را از Nuget با دستور زیر دریافت کنید:
Install-Package SQLite.CodeFirst
خود این دستور باعث می‌شود که وابستگی‌هایش از قبیل sqlite provider‌ها نیز دریافت گردند.
solution من شامل سه پروژه است یکی برای مدل‌ها که شامل کلاس‌های زیر برای تهیه یک دفترچه تلفن ساده است:

Person
 public class Person
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }

        public virtual ICollection<PhoneBook> Numbers { get; set; }

    }

PhoneBook
  public class PhoneBook
    {
        public int Id { get; set; }

        public string Field{ get; set; }

        public string Number { get; set; }

        public virtual Person Person { get; set; }
    }

پروژه بعدی به نام سرویس که جهت پیاده سازی کلاس‌های EF است و دیگری هم یک پروژه‌ی WPF جهت تست برنامه.
در پروژه‌ی سرویس ما یک کلاس به نام Context داریم که مفاهیم مربوط به پیاده سازی Context در آن انجام شده است:
public class Context:DbContext
    {
        public Context():base("constr")
        {
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
            var initializer = new InitialDb(modelBuilder);
            Database.SetInitializer(initializer);        
        }

        public DbSet<PhoneBook> PhoneBook { get; set; }
        public DbSet<Person> Persons { get; set; }
    }
تا به الان چیز جدیدی نداشتیم و همه چیز طبق روال صورت گرفته است؛ ولی دو نکته‌ی مهم در این کد نهفته است:

 اول اینکه در سطر اول متد بازنویسی شده onModelCreating، قرارداد مربوط به نامگذاری جداول را حذف می‌کنیم چرا که در صورت نبودن این خط، اسامی که کلاس sqllite برای آن در نظر خواهد گرفت با اسامی که برای انجام عملیات CURD استفاده می‌شوند متفاوت خواهد بود. برای مثال برای Person جدولی به اسم People خواهد ساخت ولی برای درج، آن را در جدول Person انجام می‌دهد که به خاطر نبودن جدول با خطای چنین جدولی موجود نیست روبرو می‌شویم.

نکته‌ی دوم اینکه در همین کلاس Context ما یک پیاده سازی جدید بر روی کلاس InitialDb داشته ایم که در زیر نمونه کد آن را می‌بینید:
 public class InitialDb:SQLite.CodeFirst.SqliteCreateDatabaseIfNotExists<Context>
    {
        public InitialDb(DbModelBuilder modelBuilder) : base(modelBuilder)
        {
        }

        protected override void Seed(Context context)
        {
            var person = new Person()
            {
                FirstName = "ali",
                LastName = "yeganeh",
                Numbers = new List<PhoneBook>()
                {
                    new PhoneBook()
                    {
                        Field = "Work",
                        Number = "031551234"
                    },
                    new PhoneBook()
                    {
                        Field = "Mobile",
                        Number = "09123456789"
                    },
                    new PhoneBook()
                    {
                        Field = "Home",
                        Number = "031554321"
                    }
                }
            };

            context.Persons.Add(person);
            base.Seed(context);
        }
    }
در این کد کلاس InitialDb از کلاس SqliteCreateDatabaseIfNotExists ارث بری کرده‌است و متد seed آن را هم بازنویسی کرده‌ایم. کلاس SqliteCreateDatabaseIfNotExists برای زمانی کاربر دارد که اگر دیتابیس موجود نیست آن را ایجاد کند، در غیر اینصورت خیر. به غیر از آن، کلاس دیگری به نام SqliteDropCreateDatabaseAlways هم وجود دارد که با هر بار اجرا، جداول قبلی را حذف و مجددا آن‌ها را ایجاد میکند.
سپس در پروژه‌ی اصلی WPF در فایل AppConfig رشته اتصالی مورد نظر را وارد نمایید:
  <connectionStrings>
    <add name="constr" connectionString="data source=.\phonebook.sqlite;foreign keys=true" providerName="System.Data.SQLite" />
  </connectionStrings>
نکته‌ی مهم اینکه با افزودن کتابخانه از طریق nuget فایل app.config به روز می‌شود؛ ولی به نظر می‌رسد که تنظیمات به درستی انجام نمی‌شوند. در صورتیکه به مشکل زیر برخوردید و نتوانستید برنامه را اجرا کنید، کد زیر را که قسمتی از فایل app.config است، مطالعه فرمایید و موارد مربوط به آن را اصلاح کنید:

خطا:
The ADO.NET provider with invariant name 'System.Data.SQLite' is either not registered in the machine or application config file, or could not be loaded

قسمتی از فایل app.config:
<entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
      <parameters>
        <parameter value="mssqllocaldb" />
      </parameters>
    </defaultConnectionFactory>
    <providers>
          <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
      <provider invariantName="System.Data.SQLite" type="System.Data.SQLite.EF6.SQLiteProviderServices, System.Data.SQLite.EF6" />

    </providers>
  </entityFramework>
  <system.data>
    <DbProviderFactories>
      <remove invariant="System.Data.SQLite.EF6" />
      <remove invariant="System.Data.SQLite" />
       <add name="SQLite Data Provider" invariant="System.Data.SQLite" description=".Net Framework Data Provider for SQLite" type="System.Data.SQLite.SQLiteFactory, System.Data.SQLite" />
    </DbProviderFactories>
  </system.data>

کد Load پروژه WPF:
 public MainWindow()
        {
            InitializeComponent();
           var context=new Context();

            var list= context.Persons.ToList();

            var s = "";

            foreach (var person in list)
            {
                s += person.FirstName + " " + person.LastName +
            " has these numbers:" + Environment.NewLine;

                foreach (var number in person.Numbers)
                {
                    s += number.Field + " : " + number.Number + Environment.NewLine;
                }

                s += Environment.NewLine;
            }
           MessageBox.Show(s);


        }



دانلود مثال
 
مطالب
سفارشی کردن قابلیت طراحی ریپورت توسط END USER - بخش اول
در قسمتی از پروژه تجاری، برای طراحی ریپورت توسط کاربر نیاز به موضوع مطرح شده این پست داشتم و به نتایج مطلوبی در این زمینه توسط کامپوننت Stimulsoft Report.Net دست یافتم.
پروژه بنده به صورتی هست که در آن قرار است یک سری اطلاعات توسط DataTable به Report ارسال و ریپورت هم Field‌ها را که از قبل طراحی شده، روی فرم داشته باشد و کاربر فقط این امکان را داشته باشد که مکان فیلد‌ها ( یعنی مختصات ) روی صفحه A4 را با توجه به سلیقه خودش تنظیم و ذخیره نماید.

اهداف :
1.غیر فعال کردن بعضی از امکانات صفحه Design
2.اعمال یکسری تنظیمات از طریق کدنویسی (Code Behind) بر روی ریپورت
مانند : اینکه ShowGrid فعال باشه یا نه -- یا Toolbox.visible=false باشه
3.فارسی سازی محیط طراحی برای کاربر
4.ذخیره و ...

اسکرین شات : ( حالت پیشفرض بدون اعمال تغییرات Runtime )


رفرنس‌های مورد نیاز:
using Stimulsoft.Base.Services;
using Stimulsoft.Report;
using Stimulsoft.Report.Components;
using Stimulsoft.Report.Design;
using Stimulsoft.Report.Design.Toolbars;
using Stimulsoft.Report.Render;
using Stimulsoft.Report.Units;

//ایجاد یک شی از ریپورت            
StiReport report = new StiReport();
//ریست کردن تنظیمات به حالت پیشفرض
 report.Reset();
//ریست کردن تنظیمات سرویس‌های StiConfig.Reset();
//ریست کردن تنظیمات چاپ
 StiSettings.Clear();

            

            //تنظیم عنوان ریپورت به صورت دلخواه
            StiOptions.Designer.DesignerTitle = title + " طراحی فرم ";
            StiOptions.Designer.DesignerTitleText = title + " طراحی فرم ";
            //غیرفعال شدن نمایش تب کدنویسی
            StiOptions.Designer.CodeTabVisible = false;
            //فعال کردن امکان RightToLeft
            StiOptions.Designer.UseRightToLeftGlobalizationEditor = true;
            //غیرفعال شدن قابلیت تغییر نام ریپورت توسط کاربر
            StiOptions.Designer.CanDesignerChangeReportFileName = false;
            //غیر فعال کردن تشخیص اتوماتیک زبان پیش فرض UI طراحی
            StiOptions.Designer.UseSimpleGlobalizationEditor = false;

            //تنظیم تم ریپورت از حالت استاندارد به ریبون
            StiOptions.Windows.GlobalGuiStyle = StiGlobalGuiStyle.Office2010Blue;
            //فعال سازی تم ریبون
            StiOptions.Designer.IsRibbonGuiEnabled = true;
برای دسترسی به بخش Dictionary که به اطلاعات DataSoruce‌ها دسترسی می‌دهد البته برای قابلیت Design :
یک شی از StiOptions ایجاد کنید. سپس Designer و فعال و غیرفعال کردن نمایش هر بخش را و حتی تغییر آیتم‌های موجود در منوی راست کلیک هر شی، قابل تغییر خواهند بود.

پنل Dictionary از سه شاخه اصلی تشکیل می‌شود : که با دستورات زیر می‌توان نمایش این بخش‌ها را در صورت خالی بودن از داده غیر فعال نمود
BusinessObjectsCategory - DataSourcesCategory -VariablesCategory
StiOptions.Designer.Panels.Dictionary.ShowEmptyBusinessObjectsCategory = false;
StiOptions.Designer.Panels.Dictionary.ShowEmptyDataSourcesCategory = false;
StiOptions.Designer.Panels.Dictionary.ShowEmptyVariablesCategory = false;
در صفحه طراحی، 3 پنل وجود دارد :
1- Dictionary
2- Properties
3- Report Tree

که Properties نسبت به هر شیء ایی که از صفحه ریپورت انتخاب می‌کنید، تنظیمات مربوط به آن را برای ویرایش در اختیار کاربر قرار می‌دهد.
پنل Report Tree نیز از داده‌های موجود از دیتاسورس بخش Dictionary که به صورت شیء در صفحه ریپورت قرار داده شده‌اند نمایش درخت واره‌ای را در اختیار کاربر قرار می‌دهد و می‌تواند از اشیاء این درخت واره در ریپورت به صورت متعدد استفاده نماید.

می‌توان هر کدام از این پنل‌ها را ( که به صورت سرویس در Stimulsoft تعریف شده) از دید کاربر مخفی نمود یا به صورت محدود یکسری از قابلیت‌ها را در اختیار کاربر قرار داد:
//غیر فعال کردن سرویس‌های پنل
            Stimulsoft.Report.Design.Panels.StiPropertiesPanelService propPanel = Stimulsoft.Report.Design.Panels.StiPropertiesPanelService.GetService();
            propPanel.ServiceEnabled = false;

            Stimulsoft.Report.Design.Panels.StiDictionaryPanelService dictPanel = Stimulsoft.Report.Design.Panels.StiDictionaryPanelService.GetService();
            dictPanel.ServiceEnabled = true;
            
            Stimulsoft.Report.Design.Panels.StiReportTreePanelService treePanel = Stimulsoft.Report.Design.Panels.StiReportTreePanelService.GetService();
            treePanel.ServiceEnabled = false;

            Stimulsoft.Report.Design.Toolbars.StiToolsToolbarService cpanel = Stimulsoft.Report.Design.Toolbars.StiToolsToolbarService.GetService();
            cpanel.ServiceEnabled = false;
            StiOptions.Dictionary.BusinessObjects.AddBusinessObjectAssemblyToReferencedAssembliesAutomatically = false;
            StiOptions.Dictionary.BusinessObjects.AllowProcessNullItemsInEnumerables = false;
            StiOptions.Dictionary.BusinessObjects.AllowUseDataColumn = false;
            StiOptions.Dictionary.BusinessObjects.AllowUseFields = false;
            StiOptions.Dictionary.BusinessObjects.AllowUseProperties = false;
            StiOptions.Dictionary.BusinessObjects.CheckTableDuplication = false;
           
            

            StiOptions.Dictionary.ShowOnlyAliasForDataSource = true;
            StiOptions.Dictionary.ShowOnlyAliasForDataColumn = true;
            StiOptions.Dictionary.ShowOnlyAliasForTotal = true;
            dictPanel.ShowNewButton = false;
            dictPanel.ShowActionsButton = false;
            dictPanel.ShowBusinessObjectNewMenuItem = false;
            dictPanel.ShowCalcColumnNewMenuItem = false;
            dictPanel.ShowCategoryNewMenuItem = false;
            dictPanel.ShowCollapseAllMenuItem = true;
            dictPanel.ShowColumnNewMenuItem = false;
            dictPanel.ShowConnectionNewMenuItem = false;
            dictPanel.ShowContextMenu = false;
            dictPanel.ShowCreateFieldOnDoubleClick = false;
            dictPanel.ShowCreateLabel = false;
            dictPanel.ShowDataParameterNewMenuItem = false;
            dictPanel.ShowDataSourceNewMenuItem = false;
            dictPanel.ShowDataSourcesNewMenuItem = false;
            dictPanel.ShowDeleteButton = false;
            dictPanel.ShowDeleteForBusinessObject = false;
            dictPanel.ShowDeleteForDataColumn = false;
            dictPanel.ShowDeleteForDataConnection = false;
            dictPanel.ShowDeleteForDataParameter = false;
            dictPanel.ShowDeleteForDataRelation = false;
            dictPanel.ShowDeleteForDataSource = false;
            dictPanel.ShowDeleteForVariable = false;
            dictPanel.ShowDeleteMenuItem = false;
            dictPanel.ShowDictMergeMenuItem = false;
            dictPanel.ShowDictNewMenuItem = false;
            dictPanel.ShowDictOpenMenuItem = false;
            dictPanel.ShowDictSaveMenuItem = false;
            dictPanel.ShowDictXmlExportMenuItem = false;
            dictPanel.ShowDictXmlImportMenuItem = false;
            dictPanel.ShowDictXmlMergeMenuItem = false;
            dictPanel.ShowDownButton = false;
            dictPanel.ShowEditButton = false;
            dictPanel.ShowEditForBusinessObject = false;
            dictPanel.ShowEditForDataColumn = false;
            dictPanel.ShowEditForDataConnection = false;
            dictPanel.ShowEditForDataParameter = false;
            dictPanel.ShowEditForDataRelation = false;
            dictPanel.ShowEditForDataSource = false;
            dictPanel.ShowEditForVariable = false;
            dictPanel.ShowEditMenuItem = false;
            
            dictPanel.ShowExpandAllMenuItem = true;
            dictPanel.ShowMarkUsedMenuItem = false;
            dictPanel.ShowNewButton = false;
            dictPanel.ShowPropertiesForBusinessObject = false;
            dictPanel.ShowPropertiesForDataColumn = false;
            dictPanel.ShowPropertiesForDataConnection = false;
            dictPanel.ShowPropertiesForDataParameter = false;
            dictPanel.ShowPropertiesForDataRelation = false;
            dictPanel.ShowPropertiesForDataSource = false;
            dictPanel.ShowPropertiesForVariable = false;
            dictPanel.ShowPropertiesMenuItem = false;
            dictPanel.ShowRelationNewMenuItem = false;
            dictPanel.ShowRelationsImportMenuItem = false;
            dictPanel.ShowRemoveUnusedMenuItem = false;
            dictPanel.ShowSortItemsButton = false;
            dictPanel.ShowSynchronizeMenuItem = false;
            dictPanel.ShowUpButton = false;
            dictPanel.ShowUseAliases = true;
            dictPanel.ShowVariableNewMenuItem = false;
            dictPanel.ShowViewDataMenuItem = false;
یکی از قابلیت‌های خوب و کاربردی این کامپوننت پشتیبانی کامل حتی منوها از زبان فارسی می‌باشد که استفاده از این کامپوننت را در نرم افزار‌های تجاری در کشور قابل انعطاف پذیر‌تر می‌کند. برای فارسی سازی به یک فایل XML که در مسیر نصب کامپوننت قرار دارد، نیاز است.
نام فایل fa.xml می‌باشد. آن‌را در مسیر نرم افزار قرار دهید و سپس کد زیر را اضافه نمایید:
//تنظیم زبان به فارسی
StiConfig.LoadLocalization("fa.xml");
بازخوردهای دوره
الگوی معکوس سازی کنترل چیست؟
مثلا می‌توانید دسترسی به داده‌ها رو مستقیما در کدهای یک کنترلر یا فرم قرار دهید و یا با استفاده از معکوس سازی وابستگی‌ها، این دسترسی را به یک لایه سرویس منتقل و معکوس کرده و استفاده کنید (در کنترلر یا فرم برنامه). استفاده نهایی از امکانات اینترفیس‌های لایه سرویس خواهد بود، بدون اطلاع از نحوه پیاده سازی خاص لایه سرویس برنامه. به این ترتیب علاوه بر جداسازی لایه‌های مختلف برنامه، امکان ارائه نگارش‌های مختلفی از پیاده سازی‌های اینترفیس‌ها نیز وجود خواهد داشت. در برنامه واقعی، پیاده سازی کننده‌ها، از دیتابیس واقعی استفاده می‌کنند و در مثلا در حین آزمایش واحد، از پیاده سازی خاص استفاده کننده از یک بانک اطلاعاتی ساده تشکیل شده در حافظه برای بالا رفتن سرعت آزمون‌ها.