مطالب
برش تصاویر قبل از آپلود (Crop)
چند روز پیش در یکی از سیستم‌های مدیریتی که امکان آپلود چند تصویر وجود داشت، مسئله‌ای پیش آمد که نیاز بود تصاویر وارد شده، اندازه‌ای به یک نسبت را داشته باشند و کاربران مرتبا تصاویری با اندازه‌های مختلف را وارد میکردند که باعث میشد UI در حین نمایش تصاویر، از شکل اصلی خود دور شود. به همین دلیل امکان برش تصاویر یا Crop برای این امر احساس میشد. کتابخانه‌های مختلف و زیادی برای برش تصاویر و کار بر روی تصاویر وجود دارند، ولی این کتابخانه باید چند خصوصیت زیر را دارا باشد:

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

کتابخانه jcrop کتابخانه‌ای است که این امکانات را برای شما فراهم می‌کند. این کتابخانه بدین صورت است که در حین برش به شما 4 عدد x1,x2,y1,y2 را داده و شما با ارسال آن به سمت سرور میتوانید بر اساس این اعداد، عکس اصلی را برش بزنید. بدین صورت شما هم عکس اصلی را دارید و هم مختصات برش را دارید و اگر دوست دارید در جاهای مختلف از عکس اصلی برش داشته باشید، بسیار مفید خواهد بود.

مرحله اول:
ابتدا فایل jcrop را دانلود نمایید.

مرحله دوم:
 کد Html زیر را به صفحه اضافه کنید:
<div>
    <!-- upload form -->
    <!-- hidden crop params -->
    <input type="hidden" id="x1" name="x1" />
    <input type="hidden" id="y1" name="y1" />
    <input type="hidden" id="x2" name="x2" />
    <input type="hidden" id="y2" name="y2" />
    <h2>ابتدا تصویر خود را انتخاب کنید</h2>
    <div><input type="file" name="postedFileBase" data-buttonText="انتخاب تصویر" id="image_file" onchange="fileSelectHandler()" /></div>
    <div></div>
    <div>
        <h2>قسمتی از تصویر را انتخاب نمایید</h2>
        <img id="preview" />
        <div>
            <label>حجم فایل </label> <input type="text" id="filesize" name="filesize" />
            <label>نوع فایل</label> <input type="text" id="filetype" name="filetype" />
            <label>ابعاد فایل</label> <input style="direction: ltr;" type="text" id="filedim" name="filedim" />

        </div>
    </div>

</div>
در اینجا 4 عدد input به صورت مخفی قرار گرفته‌اند که مختصات برش به ترتیب در آن‌ها ذخیره می‌شوند و هنگام post یا قرار دادن در formData جهت ارسال ایجکسی نیز می‌توانند مورد استفاده قرار بگیرند. جهت input file مورد نظر برای زیبایی و پشتیبانی از عبارت فارسی دلخواه به جای «Browse» از کتابخانه  Bootstrap FileStyle استفاده شده است و رویداد Onchange آن نیز به یک تابع جاوا اسکریپتی بایند شده تا بعد از تایید تصویر، باقی عملیات برش تصویر انجام شوند.

 تگ step2 نیز بعد از نمایش موفقیت آمیز تصویر نشان داده میشود که کاربر میتواند در آن تصویر را برش دهد و شامل بخش info نیز می‌باشد تا بتوان اندازه اصلی تصویر، نوع فایل تصویر Content Type و حجم آن را نمایش داد.

مرحله سوم:
سپس برای استایل دهی کدهای بالا از کد Css زیر استفاده میکنیم:
.bheader {
background-color: #DDDDDD;
border-radius: 10px 10px 0 0;
padding: 10px 0;
text-align: center;
}
.bbody {
color: #000;
overflow: hidden;
padding-bottom: 20px;
text-align: center;
background: -moz-linear-gradient(#ffffff, #f2f2f2);
background: -ms-linear-gradient(#ffffff, #f2f2f2);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ffffff), color-stop(100%, #f2f2f2));
background: -webkit-linear-gradient(#ffffff, #f2f2f2);
background: -o-linear-gradient(#ffffff, #f2f2f2);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f2f2f2');
-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f2f2f2')";
background: linear-gradient(#ffffff, #f2f2f2);
}
.bbody h2, .info, .error {
margin: 10px 0;
}
.step2, .error {
display: none;
}
.error {
color: red;
}
.info {
}
label {
margin: 0 5px;
}
.roundinput {
border: 1px solid #CCCCCC;
border-radius: 10px;
padding: 4px 8px;
text-align: center;
width: 150px;
}
.jcrop-holder {
display: inline-block;
}
input[type=submit] {
background: #e3e3e3;
border: 1px solid #bbb;
border-radius: 3px;
-webkit-box-shadow: inset 0 0 1px 1px #f6f6f6;
box-shadow: inset 0 0 1px 1px #f6f6f6;
color: #333;
padding: 8px 0 9px;
text-align: center;
text-shadow: 0 1px 0 #fff;
width: 150px;
}
input[type=submit]:hover {
background: #d9d9d9;
-webkit-box-shadow: inset 0 0 1px 1px #eaeaea;
box-shadow: inset 0 0 1px 1px #eaeaea;
color: #222;
cursor: pointer;
}
input[type=submit]:active {
background: #d0d0d0;
-webkit-box-shadow: inset 0 0 1px 1px #e3e3e3;
box-shadow: inset 0 0 1px 1px #e3e3e3;
color: #000;
}

مرحله چهارم:
افزودن کد جاوااسکریپتی زیر برای کار کردن با کتابخانه Jcrop می‌باشد:
function bytesToSize(bytes) {
    var sizes = ['بایت', 'کیلو بایت', 'مگابایت'];
    if (bytes == 0) return 'n/a';
    var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
    return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + sizes[i];
};

function updateInfo(e) {
    $('#x1').val(e.x);
    $('#y1').val(e.y);
    $('#x2').val(e.x2);
    $('#y2').val(e.y2);
};

var jcrop_api, boundx, boundy;
function fileSelectHandler() {

    var oFile = $('#image_file')[0].files[0];

    $('.error').hide();
    var rFilter = /^(image\/jpeg|image\/png)$/i;
    if (!rFilter.test(oFile.type)) {
        $('.error').html('فقط تصویر معتبر انتخاب نمایید').show();
        return;
    }

   
    var oImage = document.getElementById('preview');

    var oReader = new FileReader();
    oReader.onload = function (e) {

        oImage.src = e.target.result;
        

        oImage.onload = function () { 
          

            $('.step2').fadeIn(500);

            var sResultFileSize = bytesToSize(oFile.size);
            $('#filesize').val(sResultFileSize);
            $('#filetype').val(oFile.type);
            $('#filedim').val(oImage.naturalWidth + ' x ' + oImage.naturalHeight);
     
            if (typeof jcrop_api != 'undefined') {
                jcrop_api.destroy();
                jcrop_api = null;
                $('#preview').width(oImage.naturalWidth);
                $('#preview').height(oImage.naturalHeight);
            }
  
                $('#preview').Jcrop({

                    aspectRatio: 2,
                    bgFade: true,
                    bgOpacity: .3, 
                    onChange: updateInfo,
                    onSelect: updateInfo
                }, function () {
 
                    //var bounds = this.getBounds();
                    //var boundx = bounds[0];
                    //var boundy = bounds[1];

                    // Store the Jcrop API in the jcrop_api variable
                    jcrop_api = this;
                });
        };
    };
    oReader.readAsDataURL(oFile);
}

تابع fileSelectHandler

function fileSelectHandler() {
    var oFile = $('#image_file')[0].files[0];
    $('.error').hide();
    var rFilter = /^(image\/jpeg|image\/png)$/i;
    if (!rFilter.test(oFile.type)) {
        $('.error').html('فقط تصویر معتبر انتخاب نمایید').show();
        return;
    }
این تابع که مستقیما به input file رویداد Onchange متصل است. فایل انتخابی آن را خوانده و نوع تصویر را بررسی میکند. اگر تصویر از نوع Jpeg یا Png نباشد، یک خطا را به کاربر نشان میدهد و با return شدن از ادامه کد جلوگیری میکنیم.
در ادامه همین تابع بالا، کدهای زیر را اضافه می‌کنیم:
   var oImage = document.getElementById('preview');
    var oReader = new FileReader();
    oReader.onload = function (e) {

        oImage.src = e.target.result;
        oImage.onload = function () {
       
            $('.step2').fadeIn(500);

            var sResultFileSize = bytesToSize(oFile.size);
            $('#filesize').val(sResultFileSize);
            $('#filetype').val(oFile.type);
            $('#filedim').val(oImage.naturalWidth + ' x ' + oImage.naturalHeight);
   
            if (typeof jcrop_api != 'undefined') {
                jcrop_api.destroy();
                jcrop_api = null;
                $('#preview').width(oImage.naturalWidth);
                $('#preview').height(oImage.naturalHeight);
            }
            $('#preview').Jcrop({

                aspectRatio: 2,
                bgFade: true, 
                bgOpacity: .3, 
                onChange: updateInfo,
                onSelect: updateInfo,
                onRelease: clearInfo
            }, function () {

                //var bounds = this.getBounds();
                //var boundx = bounds[0];
                //var boundy = bounds[1];

                jcrop_api = this;
            });
        };
    };
    oReader.readAsDataURL(oFile);

FileReader یکی از توابع موجود در HTML است که مستندات آن در سایت موزیلا موجود است و قابلیت خواندن غیرهمزمان فایلها و اشیا Blob را دارد. در خط آخر به عنوان پارامتر ما فایلی را که در آپلودر خوانده ایم و در مرحله قبل نوع فایل آن را بررسی کردیم، پاس میکنیم و باعث می‌شود که رویداد Load شیء FileReader صدا زده شود.
در این رویداد ابتدا اطلاعات این فایل را از قبیل سایز و ابعاد و نوع فایل، خوانده و در همان تگ Div که با کلاس info تعیین شده بود، نمایش می‌دهیم. سپس متغیر jcrop_api  را که به صورت global در بالای تابع صدا زدیم، بررسی میکنم که آیا از قبل پر شده‌است یا خیر؟ اگر از قبل پرشده‌است باید شیء Jcrop را که به آن اعمال شده است، نابود و آن را نال کنیم تا برای تصویر جدید آماده شود. این کد زمانی کاربرد دارد که کاربر از تصویر قبلی انصراف داده‌است و تصویر جدیدی را انتخاب نموده است یا اینکه عملیات دارد به صورت ایجکسی پیاده می‌شود. اگر عملیات نابودی روی این پلاگین صورت نگیرد، برای مرتبه دوم کار نخواهد کرد.


سپس پلاگین جی‌کوئری Jcrop را بر روی آن اعمال می‌کنیم. در پرامتر اول یک سری تنظیمات اولیه را انجام می‌دهیم که در ادامه با آن آشنا می‌شویم و در پارامتر دوم یک callback را به آن پاس میکنیم تا بعد از آماده شدن پلاگین اجرا شود که در آن شیء جدید ایجاد شده یعنی this را در متغیری به اسم jcrop_api دخیره میکنیم تا در بررسی‌های آتی که در بند بالا توضیح داده شد، در دسترس داشته باشیم. همچنین در این تابع شما می‌توانید اندازه تصویر انتخابی را نیز داشته باشید.

این پلاگین شامل option‌های متفاوتی در پارامتر اول است که آن‌ها را بررسی می‌کنیم:
MinSize : شما میتوانید حداقل پهنا و ارتفاعی را برای برش زدن تصویر در نظر بگیرید.
minSize:[40,20]
پارامتر دوم aspectRatio جهت نگهداری تناسب بین پهنا و ارتفاع می‌باشد. تنظیم زیر برای تناسب 3 به دو می‌باشد.
aspectRatio:1.5
bgFade اگر با true مقدار دهی شود عملیات برش در حین تاریک شدن صفحه (محلی که جزء برش نیست) و یا بالعکس آن با یک Fade صورت خواهد گرفت.
bgOpacity از 0 تا یک مقدار میگیرد و میزان opacity محل‌های تاریک را تعیین می‌کند. همچنین شامل سه رویداد onSelect,onChange,onrelease هم می‌باشد که به ترتیب در موارد زیر رخ میدهند:
ناحیه مورد نظر انتخاب شد.
ناحیه مورد نظر در حالت انتخاب است و ماوس در حال درگ شدن است و با هر حرکتی ماوس اجرا می‌گردد.
ناحیه انتخابی از حالت انتخاب خارج شد.


دو رویداد اول یعنی onchange و onSelect را برای به روزسانی فیلدهای مخفی و مختصات استفاده میکنیم:

function updateInfo(e) {
    $('#x1').val(e.x);
    $('#y1').val(e.y);
    $('#x2').val(e.x2);
    $('#y2').val(e.y2);
};

این مختصات از طریق یک پارامتر به آن‌ها پاس می‌شود. به غیر از این چهار عدد مختصات می‌توانید با استفاده از متغیرهای w و h هم اندازه پهنا و ارتفاع محل برش خورده را نیز به دست آورید. هر چند که این اعداد، از تفریق خود مختصات هم به دست می‌آیند.
یک تابع جزئی دیگر هم در این فایل وجود دارد که حین نمایش اندازه تصویر، واحد نمایش مناسب آن را برای ما انتخاب میکند:
function bytesToSize(bytes) {
    var sizes = ['بایت', 'کیلو بایت', 'مگابایت'];
    if (bytes == 0) return 'n/a';
    var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
    return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + sizes[i];
};

بعد از اینکه کدهای سمت کلاینت را تمام کردیم لازم است با نحوه برش تصویر در سمت سرور هم آشنا شویم:
public static byte[] Resize(this byte[] byteImageIn, int x1,int y1,int x2,int y2)
        {

            ImageConverter ic = new ImageConverter();
            Image src = (Image)(ic.ConvertFrom(byteImageIn)); 

            Bitmap target = new Bitmap(x2 - x1, y2 - y1);
            using (Graphics graphics = Graphics.FromImage(target))
                graphics.DrawImage(src, new Rectangle(0, 0, target.Width, target.Height),
                    new Rectangle(x1,y1,x2-x1,y2-y1), 
                    GraphicsUnit.Pixel);
            src = target;
            using (var ms = new MemoryStream())
            {
                src.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
                return ms.ToArray();
            }

 }

از آنجا که ما تصاویر را در دیتابیس به صورت آرایه‌ای از بایت‌ها ذخیره میکنیم، extension method ذکر شده در بالا تصویر را در حالت آرایه‌ای از بایت‌ها برش می‌دهد. بدیهی که بسته به نیاز شما کد بالا دست خوش تغییراتی خواهد شد. ابتدا تصویر باینری را به شی Image تبدیل میکنیم و یک شیء Bitmap جدید را به عنوان بوم خالی و به اندازه کادر برش ایجاد می‌کنیم تا تصویر برش خورده در آن قرار بگیرد و سپس توسط متد DrawImage میخواهیم که تصویر مبدا را با مختصات شیء Rectangle از نقطه 0 و 0 بوم آغاز کرده و تا انتهای آن شروع به ترسیم کند. سپس آن را ذخیره و مجددا در قالب همان آرایه‌ای از بایت‌ها بر می‌گردانیم.

تنها یک نکته را به خاطر داشته باشید که مقادیر مختصاتی که پلاگین جی کوئری ارسال میکند در قالب اعداد اعشاری هستند و برای ارسال و دریافت آن‌ها در سرور این نکته را به خاطر داشته باشید.

مطالب
مدیریت پیشرفته‌ی حالت در React با Redux و Mobx - قسمت هشتم - تنظیمات پروژه‌های React برای کار با Mobx decorators
تا اینجا دو مثالی را که از Mobx بررسی کردیم (مثال ورود متن و مثال کامپوننت شمارشگر)، به عمد به همراه decoratorهای @ دار آن نبودند. برای مثال در قسمت قبل، یک کلاس را با یک خاصیت ایجاد کردیم که روش مزین سازی خاصیت value آن را با  observable decorator، توسط متد decorate انجام دادیم و این هم یک روش کار با MobX است؛ بدون اینکه نیاز به تنظیمات خاصی را داشته باشد:
import { decorate } from "mobx";

class Count {
  value = 0;
}

decorate(Count, { value: observable });
const count = new Count();
اما اگر همان مثال بسیار ساده‌ی ورود متن را بخواهیم توسط decoratorهای @ دار MobX پیاده سازی کنیم ... پروژه‌ی استاندارد React ما کامپایل نخواهد شد که در این قسمت، روش رفع این مشکل را بررسی می‌کنیم.


بازنویسی مثال ورود متن و نمایش آن با Mobx decorators

در اینجا یک text-box، به همراه دو div در صفحه رندر خواهند شد که قرار است با ورود اطلاعاتی در text-box، یکی از آن‌ها (text-display) این اطلاعات را به صورت معمولی و دیگری (text-display-uppercase) آن‌را به صورت uppercase نمایش دهد. روش کار انجام شده هم مستقل از React است و به صورت مستقیم با استفاده از DOM API عمل شده‌است. این مثال را پیشتر در اولین قسمت بررسی MobX، ملاحظه کردید. اکنون اگر بخواهیم بجای شیءای که توسط متد observable کتابخانه‌ی MobX محصور شده‌است:
const text = observable({
  value: "Hello world!",
  get uppercase() {
    return this.value.toUpperCase();
  }
});
از یک کلاس ES6 به همراه Mobx decorators استفاده کنیم، به یک چنین پروژه‌ی جدیدی خواهیم رسید:
ابتدا یک پروژه‌ی جدید React را ایجاد می‌کنیم:
> create-react-app state-management-with-mobx-part3
> cd state-management-with-mobx-part3
> npm start
در ادامه کتابخانه‌ی mobx را نیز نصب می‌کنیم. برای این منظور پس از باز کردن پوشه‌ی اصلی برنامه توسط VSCode، دکمه‌های ctrl+` را فشرده (ctrl+back-tick) و دستور زیر را در ترمینال ظاهر شده وارد کنید:
> npm install --save mobx
در ادامه، ابتدا فایل public\index.html را جهت نمایش دو div و یک text-box، ویرایش می‌کنیم:
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>MobX Basics, part 3</title>
    <meta charset="UTF-8" />
    <link href="src/styles.css" />
  </head>

  <body>
    <main>
      <input id="text-input" />
      <p id="text-display"></p>
      <p id="text-display-uppercase"></p>
    </main>

    <script src="src/index.js"></script>
  </body>
</html>
سپس محتویات فایل src\index.js را نیز به نحو زیر تغییر می‌دهیم:
import { autorun, computed, observable } from "mobx";

const input = document.getElementById("text-input");
const textDisplay = document.getElementById("text-display");
const loudDisplay = document.getElementById("text-display-uppercase");

class Text {
  @observable value = "Hello World";
  @computed get uppercase() {
    return this.value.toUpperCase();
  }
}

const text = new Text();

input.addEventListener("keyup", event => {
  text.value = event.target.value;
});

autorun(() => {
  input.value = text.value;
  textDisplay.textContent = text.value;
  loudDisplay.textContent = text.uppercase;
});
تنها تفاوت این نگارش با نگارش قبلی آن، استفاده از کلاس Text که یک کلاس ES6 به همراه MobX Decorators است، بجای یک شیء ساده‌ی جاوا اسکریپتی می‌باشد. در اینجا خاصیت value به صورت observable@ تعریف شده و در نتیجه‌ی تغییر مقدار آن در کدهای برنامه، خاصیت محاسباتی وابسته‌ی به آن یا همان uppercase که با computed@ تزئین شده، به صورت خودکار به روز رسانی خواهد شد. متد autorun نیز به این تغییرات که حاصل فشرده شدن کلیدها هستند، واکنش نشان داده و متن دو div موجود در صفحه را به روز رسانی می‌کند.

اکنون اگر در همین حال، برنامه را با دستور npm start اجرا کنیم، با خطای زیر متوقف خواهیم شد:
./src/index.js
SyntaxError: \src\index.js: Support for the experimental syntax 'decorators-legacy' isn't currently enabled (8:3):

   6 | 
   7 | class Text {
>  8 |   @observable value = "Hello World";
     |   ^
   9 |   @computed get uppercase() {
  10 |     return this.value.toUpperCase();
  11 |   }

راه حل اول: از Decorators استفاده نکنیم!

یک راه حل مشکل فوق این است که بدون هیچ تغییری در ساختار پروژه‌ی React خود، اصلا از decorator syntax استفاده نکنیم. برای مثال اگر یک کلاس متداول MobX ای چنین شکلی را دارد:
import { observable, computed, action } from "mobx";

class Timer {
  @observable start = Date.now();
  @observable current = Date.now();

  @computed
  get elapsedTime() {
    return this.current - this.start + "milliseconds";
  }

  @action
  tick() {
    this.current = Date.now();
  }
}
می‌توان آن‌را بدون استفاده از decorator syntax، به صورت زیر نیز تعریف کرد:
import { observable, computed, action, decorate } from "mobx";

class Timer {
  start = Date.now();
  current = Date.now();

  get elapsedTime() {
    return this.current - this.start + "milliseconds";
  }

  tick() {
    this.current = Date.now();
  }
}

decorate(Timer, {
  start: observable,
  current: observable,
  elapsedTime: computed,
  tick: action
});
نمونه‌ی این روش را در قسمت قبل با تعریف شیء شمارشگر مشاهده کرده‌اید. در اینجا با توجه به اینکه Decorators در جاوا اسکریپت چیزی نیستند بجز بیان زیبای higher-order functions و higher-order functions هم توابعی هستند که توابع دیگر را با ارائه‌ی قابلیت‌های بیشتری، محصور می‌کنند، به همین جهت هر کاری را که بتوان با تزئین کننده‌ها انجام داد، همان را با توابع معمولی جاوا اسکریپتی نیز می‌توان انجام داد. اینکار را در مثال فوق توسط متد decorate مشاهده می‌کنید. این متد ابتدا نوع کلاس خاصی را دریافت کرده و سپس در پارامتر دوم آن می‌توان شیءای را تعریف کرد که خواص آن، همان خواص کلاس پارامتر اول است و مقادیر این خواص، تزئین کننده‌هایی هستند که قرار است برای آن‌ها بکار گرفته شوند. مزیت این روش بدون تغییر باقی ماندن تعریف کلاس Timer در اینجا و همچنین انجام هیچگونه تغییری در ساختار پروژه‌ی React، بدون نیاز به نصب بسته‌های کمکی اضافی است.

همچنین در این حالت بجای استفاده از کامپوننت‌های کلاسی، باید از روش بکارگیری متد observer برای محصور کردن کامپوننت تابعی تعریف شده استفاده کرد (تا دیگر نیازی به ذکر observer class@ نباشد):
const Counter = observer(({ count }) => {
  return (
   // ...
  );
});


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

create-react-app امکان ایجاد پروژه‌های React تایپ‌اسکریپتی را با ذکر سوئیچ typescript نیز دارد:
> create-react-app my-proj1 --typescript
پس از ایجاد پروژه، فایل tsconfig.json آن‌را یافته و experimentalDecorators آن‌را به true تنظیم کنید:
{
  "compilerOptions": {
    // ...
    "experimentalDecorators": true
  }
}
این تنها تغییری است که مورد نیاز می‌باشد و پس از آن برنامه‌ی React جاری، بدون مشکلی می‌تواند با decorators کار کند.


فعالسازی MobX Decorators در پروژه‌های استاندارد React مبتنی بر ES6

MobX از legacy" decorators spec" پشتیبانی می‌کند. یعنی اگر پروژه‌ای از spec جدید استفاده کند، دیگر نخواهد توانست با MobX فعلی کار کند. این هم مشکل MobX نیست. مشکل اینجا است که باید دانست کلا decorators در زبان جاوااسکریپت هنوز در مرحله‌ی آزمایشی قرار دارند و تکلیف spec نهایی و تائید شده‌ی آن مشخص نیست.
برای فعالسازی decorators در یک پروژه‌ی React استاندارد مبتنی بر ES6، شاید کمی جستجو کنید و به نتایجی مانند افزودن فایل babelrc. به ریشه‌ی پروژه و نصب افزونه‌هایی مانند babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties@ برسید. اما ... این‌ها بدون اجرای دستور npm run eject کار نمی‌کنند و اگر این دستور را اجرا کنیم، در نهایت به یک فایل package.json بسیار شلوغ خواهیم رسید (اینبار ارجاعات به Babel، Webpack و تمام ابزارهای دیگر نیز ظاهر می‌شوند). همچنین این عملیات نیز یک طرفه‌است. یعنی از این پس قرار است کنترل تمام این پشت صحنه، در اختیار ما باشد و به روز رسانی‌های بعدی create-react-app را با مشکل مواجه می‌کند. این گزینه صرفا مختص توسعه دهندگان پیشرفته‌ی React است. به همین جهت نیاز به روشی را داریم تا بتوانیم تنظیمات Webpack و کامپایلر Babel را بدون اجرای دستور npm run eject، تغییر دهیم تا در نتیجه، decorators را در آن فعال کنیم و خوشبختانه پروژه‌ی react-app-rewired دقیقا برای همین منظور طراحی شده‌است.

بنابراین ابتدا بسته‌های زیر را نصب می‌کنیم:
> npm i --save-dev customize-cra react-app-rewired
بسته‌ی react-app-rewired، امکان بازنویسی تنظیمات webpack پروژه‌ی react را بدون eject آن میسر می‌کند. customize-cra نیز با استفاده از امکانات همین بسته، نگارش‌های جدیدتر create-react-app را پشتیبانی می‌کند.

پس از نصب این پیشنیازها، فایل جدید config-overrides.js را به ریشه‌ی پروژه، جائیکه فایل package.json قرار گرفته‌است، با محتوای زیر اضافه کنید تا پشتیبانی ازlegacy" decorators spec" فعال شوند:
const {
  override,
  addDecoratorsLegacy,
  disableEsLint
} = require("customize-cra");

module.exports = override(
  // enable legacy decorators babel plugin
  addDecoratorsLegacy(),

  // disable eslint in webpack
  disableEsLint()
);
در ادامه فایل package.json را گشوده و قسمت scripts آن‌را برای استفاده‌ی از react-app-rewired، به صورت زیر بازنویسی کنید تا امکان تغییر تنظیمات webpack به صورت پویا در زمان اجرای برنامه، میسر شود:
  "scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-app-rewired eject"
  },
پس از این تغییرات، نیاز است دستور npm start را یکبار دیگر از ابتدا اجرا کنید. اکنون برنامه بدون مشکل کامپایل شده و خروجی بدون خطایی در مرورگر نمایش داده خواهد شد.


تنظیمات ESLint مخصوص کار با decorators

فایل ویژه‌ی eslintrc.json. که در ریشه‌ی پروژه قرار می‌گیرد (این فایل بدون نام است و فقط از پسوند تشکیل شده)، برای پروژه‌های MobX، باید حداقل تنظیم زیر را داشته باشد تا ESLint بتواند legacyDecorators را نیز پردازش کند:
{
  "extends": "react-app",
  "parserOptions": {
    "ecmaFeatures": {
      "legacyDecorators": true
    }
  }
}
و یا یک نمونه‌ی غنی شده‌ی فایل eslintrc.json. مخصوص برنامه‌های React به صورت زیر است:
{
    "env": {
        "node": true,
        "commonjs": true,
        "browser": true,
        "es6": true,
        "mocha": true
    },
    "settings": {
        "react": {
            "version": "detect"
        }
    },
    "parserOptions": {
        "ecmaFeatures": {
            "jsx": true,
            "legacyDecorators": true
        },
        "ecmaVersion": 2018,
        "sourceType": "module"
    },
    "plugins": [
        "babel",
        "react",
        "react-hooks",
        "react-redux",
        "no-async-without-await",
        "css-modules",
        "filenames",
        "simple-import-sort"
    ],
    "rules": {
        "no-const-assign": "warn",
        "no-this-before-super": "warn",
        "constructor-super": "warn",
        "strict": [
            "error",
            "safe"
        ],
        "no-debugger": "error",
        "brace-style": [
            "error",
            "1tbs",
            {
                "allowSingleLine": true
            }
        ],
        "no-trailing-spaces": "error",
        "keyword-spacing": "error",
        "space-before-function-paren": [
            "error",
            "never"
        ],
        "spaced-comment": [
            "error",
            "always"
        ],
        "vars-on-top": "error",
        "no-undef": "error",
        "no-undefined": "warn",
        "comma-dangle": [
            "error",
            "never"
        ],
        "quotes": [
            "error",
            "double"
        ],
        "semi": [
            "error",
            "always"
        ],
        "guard-for-in": "error",
        "no-eval": "error",
        "no-with": "error",
        "valid-typeof": "error",
        "no-unused-vars": "error",
        "no-continue": "warn",
        "no-extra-semi": "warn",
        "no-unreachable": "warn",
        "no-unused-expressions": "warn",
        "max-len": [
            "warn",
            80,
            4
        ],
        "react/prefer-es6-class": "warn",
        "react/jsx-boolean-value": "warn",
        "react-hooks/rules-of-hooks": "error",
        "react-hooks/exhaustive-deps": "warn",
        "react/prop-types": "off",
        "react-redux/mapDispatchToProps-returns-object": "off",
        "react-redux/prefer-separate-component-file": "off",
        "no-async-without-await/no-async-without-await": "warn",
        "css-modules/no-undef-class": "off",
        "filenames/match-regex": [
            "off",
            "^[a-zA-Z]+\\.*\\b(typescript|module|locale|validate|test|action|api|reducer|saga)?\\b$",
            true
        ],
        "filenames/match-exported": "off",
        "filenames/no-index": "off",
        "simple-import-sort/sort": "error"
    },
    "extends": [
        "react-app",
        "eslint:recommended",
        "plugin:react/recommended",
        "plugin:react-redux/recommended",
        "plugin:css-modules/recommended"
    ],
    "globals": {
        "Atomics": "readonly",
        "SharedArrayBuffer": "readonly",
        "process": true
    }
}
البته برای اینکه این تنظیمات کار کند، باید افزونه‌های زیر را نیز به صورت محلی در ریشه‌ی پروژه‌ی جاری نصب کنید (این مورد از ESLint 6x به بعد اجباری است و از بسته‌های global استفاده نمی‌کند):
>npm i --save-dev eslint babel-eslint eslint-config-react-app eslint-loader eslint-plugin-babel eslint-plugin-react eslint-plugin-css-modules eslint-plugin-filenames eslint-plugin-flowtype eslint-plugin-import eslint-plugin-no-async-without-await eslint-plugin-react-hooks eslint-plugin-react-redux eslint-plugin-redux-saga eslint-plugin-simple-import-sort eslint-loader typescript
پس از آن می‌توان فایل config-overrides.js را به صورت زیر نیز بر اساس تنظیمات فوق، بهبود بخشید:
const {
  override,
  addDecoratorsLegacy,
  useEslintRc
} = require("customize-cra");

module.exports = override(
  addDecoratorsLegacy(),
  useEslintRc(".eslintrc.json")
);


رفع اخطار مرتبط با decorators در VSCode

تا اینجا کار تنظیم کامپایلر babel، جهت پردازش decorators انجام شد. اما خود VSCode نیز چنین اخطاری را در پروژه‌هایی که از decorates استفاده می‌کنند، نمایش می‌دهد:
Experimental support for decorators is a feature that is subject to change in a future release.
Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning.ts(1219)
برای رفع آن، فایل جدید tsconfig.json را در ریشه‌ی پروژه ایجاد کرده و آن‌را به صورت زیر تکمیل کنید تا ادیتور تایپ‌اسکریپتی VSCode، دیگر خطاهای مرتبط با decorators را نمایش ندهد:
{
    "compilerOptions": {
        "experimentalDecorators": true,
        "allowJs": true
    }
}

کدهای کامل این قسمت را می‌توانید از اینجا دریافت کنید: state-management-with-mobx-part3.zip
مطالب
آموزش (jQuery) جی کوئری 4#
در ادامه مطلب قبلی  آموزش (jQuery) جی کوئری 3# به ادامه بحث  می‌پردازیم.

با توجه به حالت‌های مختلف و گزینه‌های گوناگونی که انتخاب کننده‌ها در اختیار ما گذاشته اند، اگر هنوز دنبال قدرت بیشتری از انتخاب کننده‌ها هستید در ادامه به چند مورد از آنها اشاره خواهیم کرد.
3-1- انتخاب عناصر بر اساس موقعیت
گاهی اوقات انتخاب عناصر با توجه به مکان آنها و یا موقعیت مکانی آن‌ها نسبت به سایر اجزا صورت می‌پذیرد؛ برای مثال اولین لینک  صفحه و یا اولین لینک هر پاراگراف و یا گزینه‌ی آخر از لیست، jQuery شیوه ای خاص را برای چنین انتخاب هایی ارایه کرده است. برای مثال دستور زیر اولین لینک موجود در صفحه را انتخاب می‌کند:
a:first

دستور زیر چکاری انجام می‌دهد؟
p:odd

دستور بالا تمامی پاراگراف‌های فرد را انتخاب می‌کند. روش‌های دیگری هم ممکن است بخواهیم استفاده کنیم؛ مثلا دستور زیر تمامی پاراگراف‌های زوج را انتخاب می‌کند:
p:even

یا با استفاده از دستور زیر میتوان آخرین فرزند یک والد را انتخاب کرد؛ در زیر آخرین <lii> فرزند یک <ul> انتخاب می‌شود. علاوه بر انتخاب کننده هایی که ذکر شد؛ تعداد قابل توجه دیگری نیز وجود دارند که در جدول 2-2 ذکر شده اند.

جدول 2-2: انتخاب گر‌های پیشرفته موقعیت عناصر که توسط jQuery پشتیبانی می‌شوند
 توضیح فیلتر
اولین عنصر که با شرط ما مطابقت می‌کند را انتخاب می‌کند، li a:first اولین لینکی را که فرزند لیست به حساب می‌آیند؛ را بر می‌گرداند
:first
آخرین عنصری که با شرط ما مطابقت کند را انتخاب می‌کند. li a:last آخرین لینک از فرزندان لیست را برمی گرداند.
:last
اولین فرزند عنصر که با شرط ما مطابقت می‌کند را انتخاب می‌کند. li a:first-child اولین عنصر لینک از هر لیست را برمی گرداند.
:first-child
آخرین فرزند عنصر که با شرط ما مطابقت می‌کند را انتخاب می‌کند. li a:last-child اولین عنصر لینک از هر لیست را برمی گرداند.
:last-child
تمام عناصری که پدر انها تنها همان فرزند را داد، برمی گرداند.
:only-child
nامین فرزند عنصری که با شرط ما مطابقت داشته باشد را انتخاب می‌کند. li: nth-child:(2) //comment دومین عنصر از هر لیست را برمی گرداند.
:nth-child(n)
فرزندان زوج یا فرد عنصر را انخاب می‌کند. li:nth-child(even) //commentتمام عناصر زوج لیست‌ها را بر می‌گرداند.
:nth-child(even یا odd)
nامین فرزند عنصری که از طریق فرمول ارایه شده به دست می‌آید را انتخاب می‌کند. اگر Y صفر باشد، نیازی به نوشتن آن نیست. li:nth-child(3n)//comment تمام عناصر ضریب 3 لیست‌ها را بر می‌گرداند، در حالی که li:nth-child(5n+1)// comment عناصری از لیست را برمیگرداند که بعد از عنصرهای ضریب 5 لیست‌ها قرار گرفته باشند.
:nth-child(Xn+Y)
تمام عناصر زوج یا فرد که با شرط ما مطابقت کنند را انتخاب می‌کند. li:evenتمامی عناصر زوج لیست‌ها را بر می‌گرداند.
:even 
یا
:odd
n امین عنصر انتخاب شده را برمی گرداند.
:eq(n)
عناصر بعد از n امین عنصر را بر میگرداند. (در واقع عناصری که بزرگتر از عنصر n ام هستند را بر می‌گرداند)
:gt(n)
عناصر قبل از n امین عنصر را بر میگرداند. (در واقع عناصری که کوچکتر از عنصر n ام هستند را بر می‌گرداند) 
:lt(n)
  پ.ن : در جدول بالا در توضیحات بعضی از انتخاب گرها comment// نوشه شده است، اینها جز دستور نبوده و فقط برای نمایش صحیح پردانتز در صفحه اینترنتی نوشته شده است، در عمل نیازی به اینها نیست.

نکته ای که در مورد انتخاب گرهای جدول بالا وجود دارد این است که در فیلتر nth-child: برای سازگاری با CSS، مقدار شمارشگر از 1 آغاز می‌شود، اما در سایر فیلترها از قاعده ای که اکثر زبانهای برنامه نویسی استفاده شده است و شمارشگر آنها از صفر اغاز می‌شود. برای درک این موضوع مثال زیر را در نظر بگیرید:
<table id="languages">
     <thead>
          <tr>
               <th>Language</th>
               <th>Type</th>
               <th>Invented</th>
          </tr>
     </thead>
<tbody>
<tr>
     <td>Java</td>
     <td>Static</td>
     <td>1995</td>
</tr>
<tr>
     <td>Ruby</td>
     <td>Dynamic</td>
     <td>1993</td>
</tr>
<tr>
     <td>Smalltalk</td>
     <td>Dynamic</td>
     <td>1972</td>
</tr>
<tr>
     <td>C++</td>
     <td>Static</td>
     <td>1983</td>
</tr>
</tbody>
</table>

حال می‌خواهیم از این جدول، محتویات تمام خانه هایی که نام یک زبان برنامه نویسی در آنهاست را انتخاب نماییم. از انجا که نام این زبانها در اولین ستون از هر سطر قرار دارد. می‌توانیم دستوری مانند زیر بنویسیم:
table#languages tbody td:first-child

و یا با استفاده از دستور زیر این کار را انجام دهیم:
table#languages tbody td:nth-child(1)

اما دستور اول مختصر‌تر و خواناتر است. پس از آن برای دسترسی به نوع هریک از زبانهای برنامه نویسی، دستور انتخاب کننده دوم را به صورت
:nth-child(2)
تغییر می‌دهیم، و همچنین با تغییر پارامتر 2 به 3 سالی که هر یک از زبانها ابداع شده اند ، انتخاب می‌شوند. بدیهی است در این حالت دو دستور
:nth-child(3)
یا
:last-child
با یکدیگر برابرند. اما هردوی آنها ستون آخر از هر سطر را انخاب می‌کنند، در شرایطی که بخواهیم آخرین خانه جدول انتخاب شود (خانه ای با مقدار 1983)، از td:last استفاده می‌کنیم. توجه کنید در حالی که دستور td:eq(2) //comment خانه ای با مقدار 1995 را انتخاب می‌کند، دستور td:nth-child(2)//comment تمام خانه‌های بیان کننده نوع زبانها را انتخاب می‌کند. بنابراین به خاطر داشته باشید که مقدار ابتدایی شمارشگر فیلتر eq: از صفر است و این مقدار برای فیلتر nth-child: یک تعیین شده است.

در پست بعدی انتخاب گرهای CSS و فیلتر‌های سفارشی jQuery را بررسی خواهیم کرد.
نظرات مطالب
Blazor 5x - قسمت یازدهم - مبانی Blazor - بخش 8 - کار با جاوا اسکریپت
آیا با استفاده از یکی از این روشها امکان استفاده از کنترل‌های جاوا اسکریپتی دوو اکستریم شرکت دوو اکسپرس نیز در بلیزر وجود دارد؟ به عنوان مثال استفاده از کامپوننت گرید آن فریموورک به شکلی منطقی در بلیزر قابلیت پیاده سازی و بهره برداری دارد یا خیر؟ 
مطالب
روش‌های مختلف انجام چند کار به صورت همزمان در C# .NET - قسمت اول
آیا تا به حال لیستی از دیتا داشته‌اید که بخواهید بر روی آنها کاری را انجام دهید؟ مثلا لیستی از مشتریان که باید برای تک تک آنها Pdf ای را بسازید، یا لیستی از مشتریان که باید برای تک تک آنها بیمه نامه صادر کنید، یا مثلا لیست اطلاعات بلیط‌های قابل فروش را گرفته‌اید و برای تک تک آنها می‌خواهید کمیسیون حساب کنید و ...

در اکثر مواقعی کاری که برای تک تک آیتم‌ها قرار است انجام شود، ساده است و با استفاده از یک حلقه foreach کار تمام میشود. اما در بعضی مواقع کار زمانبر است؛ حال یا به علت وجود کاری CPU bound مثل درست کردن Pdf و محاسبات، یا کار IO Bound است مثل ارسال یک HTTP Request به ازای هر مشتری، یا ذخیره کردن چیزی در دیتابیس که هم CPU bound است و هم IO bound و ترکیبی از مواردی که گفتیم را دارد.

فرض کنیم صد مشتری داریم و برای تک تک آنها میخواهیم کاری انجام دهیم. اگر از یک foreach ساده استفاده کنیم و هر عمل یک ثانیه طول بکشد، کل روال 100 ثانیه طول می‌کشد که جالب نیست.
public async Task Sample()
{
    var customers = await GetCustomersFromSomeWhere();

    foreach (var customer in customers)
    {
        await DoSomethingWithCustomer(customer);
    }
}
با اندکی جستجو در اینترنت به Task.WhenAll می‌رسیم و مشکلی که دارد این است که هر 100 کار را با هم شروع می‌کند که میتواند اثرات مخربی روی کلیت عملکرد سرور بگذارد:
public async Task Sample()
{
    var customers = await GetCustomersFromSomeWhere();
    await Task.WhenAll(customers.Select(c => DoSomethingWithCustomer(c)));
}
اگر چه می‌توانیم خودمان آیتم‌ها را دسته بندی کنیم و مثلا هر 25 تا 25 آنها را با هم پردازش کنیم، ولی این دسته بندی خیلی معقول نیست، چون RX اینجاست!
public async Task Sample()
{
    var customers = await GetCustomersFromSomeWhere();

    await customers.Select(c => Observable.FromAsync(() => DoSomethingWithCustomer(c))).Merge(maxConcurrent: 25);
}
به خاطر وجود maxConcurrent: 25 در دستور فوق، مشتری‌ها بسته به وضعیت کلی سرور، حداکثر 25 تا 25 تا پردازش می‌شوند، ولی ممکن است مثلا 10 تا 10 پردازش شوند. البته انتظار هوشمندی خیلی زیادی از آن نداشته باشید.

استفاده از Rx وقتی که دستورات داخل DoSomethingWithCustomer به صورت IO bound باشند (اتصال به دیتابیس و ارسال Http Request و ...) به خوبی جواب می‌دهد. ولی اگر دستورات داخل DoSomethingWithCustomer به صورت CPU bound باشند، مثل محاسبات یا ساختن Pdf و ... دیگر این روش جواب نمی‌دهد و اینجاست که باید از Task Parallel Library استفاده کنیم ( البته Task Parallel Libraray یا به اختصار TPL هم برای IO Bound و هم برای CPU Bound مناسب است).
برای استفاده از TPL داریم:
public async Task Sample()
{
    var customers = await GetCustomersFromSomeWhere();

    ActionBlock<Customer> action = new ActionBlock<Customer>(c => DoSomethingWithCustomer(c), new ExecutionDataflowBlockOptions
    {
        MaxDegreeOfParallelism = 25
    }); 

    foreach (var customer in customers)
    {
        action.Post(customer);
    }

    action.Complete();

    await action.Completion;
}
همانطور که می‌بینید، بحث 25 تا 25 تا اجرا کردن در اینجا نیز وجود دارد، با این تفاوت که بسیار هوشمندانه‌تر کارها را به صورتی پیش می‌برد که از منابع سرور به بهینه‌ترین شکل ممکن استفاده شود و همین TPL را هم برای اعمال IO bound و هم اعمال CPU bound مناسب می‌کند.

سایر گزینه هایی که داریم شامل Parallel Linq و Parallel.ForEach است که عموما برای کارهای CPU bound مناسبند.
گزینه‌هایی از قدیم نیز وجود دارند، مانند استفاده از Thread و Semaphore و ... که ابدا استفاده مستقیم از آنها توصیه نمیشود.
البته با TPL و RX میشود کارهای خیلی بیشتری نیز انجام داد و این دو فقط برای این سناریو ساخته نشده‌اند و همه جا جایگزین یکدیگر نیستند و هر دو دنیای وسیعی هستند که توصیه می‌کنم به هر دو نگاهی بیاندازید. همچنین TPL تا جایی که می‌دانم جزو مواردی است که در بیرون از دنیای NET. وجود ندارد و یکی از ارزشمندترین ویژگی‌های Unique در NET. است که به این سادگی چنین مسئله‌ای با این کیفیت حل شود.

توجه داشته باشید که اگر فرآیندی که برای تک تک Customer‌ها در مثال فوق قرار است انجام شود، خود یک روال سنگین و زمان بر باشد، بهتر است از روش‌های دیگری مبتنی بر Event processing و ابزارهایی چون Azure Service Bus یا Mass Transit استفاده کنیم که کمک می‌کنند اگر مثلا سه سرور داریم، بار پردازش آن 100 مشتری مثال ما، بین سه سرور هم پخش شود که این مورد پیچیدگی‌های خود را دارد و در اینجا که فرض بر این است که سناریو خیلی پیچیده و میزان بار خیلی زیاد نیست و همچنین نیازی هم به استفاده از این موارد و اضافه کردن پیچیدگی‌های بیشتر به برنامه نیست. در واقع TPL علیرغم کار بسیار ارزشمندی که می‌کند، در نهایت یک Nuget Package است که در یک دستگاه موبایل Android و با Xamarin نیز قابل استفاده است.

البته این همه داستان نیست. برای مثال در صورتی که فرآیندی بخواهد به صورت Concurrent / Parallel انجام شود و در انجام آن از Entity Framework Db Context استفاده شده باشد، کد به مشکل بر میخورد و خطا می‌دهد، چون یک Instance از DbContext مناسب انجام چند کار همزمان نیست. به واقع تمامی Objectهایی که Thread Safe نباشند، در روش‌های فوق به مشکل بر میخوردند. همچنین بحث مدیریت کردن Transaction در صورتی که بخواهید با دیتابیس هم کار کنید نیز خود مسئله‌ای است که باید حل شود.
حل مسائلی که گفته شد و ادغام کردن روش‌های فوق با بحث Dependency Injection و ... موضوع بحث قسمت بعدی این مطلب است.
مطالب
مروری بر طراحی Schema less بانک اطلاعاتی SisoDb
اس کیوال سرور، از سال 2005 به بعد، به صورت توکار امکان تعریف و ذخیره سازی اطلاعات schema less و یا schema free را به کمک فیلدهایی از نوع XML ارائه داده است؛ به همراه یکپارچگی آن با زبان XQuery برای تهیه کوئری‌های سریع سمت سرور. در فیلدهای XML می‌توان اطلاعات انواع و اقسام اشیاء را بدون اینکه نیازی به تعریف تک تک فیلدهای مورد نیاز، در بانک اطلاعاتی وجود داشته باشد، ذخیره کرد. یک نمونه از کاربرد چنین امکانی، نوشتن برنامه‌های «فرم ساز» است. برنامه‌هایی که کاربران آن می‌توانند فیلد اضافه و کم کرده و نهایتا اطلاعات را ذخیره و از آن‌ها کوئری بگیرند.
خوب، این فیلد کمتر بحث شده XML، فقط در اس کیوال سرور و نگارش‌های اخیر آن وجود دارد. اگر نیاز به کار با بانک‌های اطلاعاتی سبک‌تری وجود داشت چطور؟ یک راه حل عمومی برای این مساله مراجعه به روش‌های NoSQL است. یعنی بطور کلی بانک‌های اطلاعاتی رابطه‌ای کنار گذاشته شده و به یک سکوی کاری دیگر سوئیچ کرد. در این بین، SisoDb راه حل میانه‌ای را ارائه داده است. با کمک SisoDb می‌توان اطلاعات را به صورت schema less و بدون نیاز به تعریف فیلدهای متناظر آن‌ها، در انواع و اقسام بانک‌های اطلاعاتی SQL Server با فرمت JSON ذخیره و بازیابی کرد. این انواع و اقسام، شامل SQL Server CE نیز می‌شود.


دریافت و نصب SisoDb

دریافت و نصب SisoDb بسیار ساده است. به کمک package manager و امکانات NuGet، کلمه Sisodb را جستجو کنید. در بین مداخل ظاهر شده، پروایدر مورد علاقه خود را انتخاب و نصب نمائید. برای مثال اگر قصد دارید با SQL Server CE کار کنید، SisoDb.SqlCe4 را انتخاب و یا اگر SQL Server 2008 مدنظر شما است، SisoDb.Sql2008 را انتخاب و نصب نمائید.


ثبت و بازیابی اطلاعات به کمک SisoDb

کار با SisoDb بسیار روان است. نیازی به تعاریف نگاشت‌ها و ORM خاصی نیست. یک مثال مقدماتی آن‌را در ادامه ملاحظه می‌کنید:
using SisoDb.Sql2008;

namespace SisoDbTests
{
    public class Customer 
    {
        public int Id { get; set; } 
        public int CustomerNo { get; set; }
        public string Name { get; set; } 
    }

    class Program
    {
        static void Main(string[] args)
        {
            /*var cnInfo = new SqlCe4ConnectionInfo(@"Data source=sisodb2013.sdf;");
            var db = new SqlCe4DbFactory().CreateDatabase(cnInfo);
            db.EnsureNewDatabase();*/

            var cnInfo = new Sql2008ConnectionInfo(@"Data Source=(local);Initial Catalog=sisodb2013;Integrated Security = true");            
            var db = new Sql2008DbFactory().CreateDatabase(cnInfo);            
            db.EnsureNewDatabase();

            var customer = new Customer 
            {
                CustomerNo = 20,
                Name = "Vahid" 
            };
            db.UseOnceTo().Insert(customer);

            using (var session = db.BeginSession()) 
            {
                var info = session.Query<Customer>().Where(c => c.CustomerNo == 20).FirstOrDefault();

                var info2 = session.Query<Customer>().Where(c => c.CustomerNo == 20 && c.Name=="Vahid").FirstOrDefault();
            }
        }
    }
}
در این مثال، ابتدا اتصال به بانک اطلاعاتی برقرار شده و سپس بانک اطلاعاتی جدید تهیه می‌شود. سپس یک وهله از شیء مشتری ایجاد و ذخیره می‌گردد. در ادامه دو کوئری بر روی بانک اطلاعاتی انجام شده است.


ساختار داخلی SisoDb

SisoDb به ازای هر کلاس، حداقل 9 جدول را ایجاد می‌کند. در ادامه نحوه ذخیره سازی شیء مشتری ایجاد شده و مقادیر خواص آن‌را نیز مشاهده می‌نمائید:


ذخیره سازی جداگانه خواص عددی

ذخیره سازی جداگانه خواص رشته‌ای

ذخیره سازی کلی شیء مشتری با فرمت JSON به صورت یک رشته


همانطور که ملاحظه می‌کنید، یک جدول کلی SisoDbIdentities ایجاد شده است که اطلاعات نام اشیاء را در خود نگهداری می‌کند. سپس اطلاعات خواص اشیاء یکبار به صورت JSON ذخیره می‌شوند؛ با تمام اطلاعات تو در توی ذخیره شده در آن‌ها و همچنین یکبار هم هر خاصیت را به صورت یک رکورد جداگانه، بر اساس نوع کلی آن‌ها، در جداول رشته‌ای، عددی و امثال آن ذخیره می‌کند.
شاید بپرسید که چرا به همان فیلد رشته‌ای JSON اکتفاء نشده است؟ از این جهت که پردازشگر سمت بانک اطلاعاتی آن همانند فیلدهای XML در SQL Server و نگارش‌های مختلف آن وجود ندارد (برای مثال به کمک زبان T-SQL می‌توان از زبان XQuery در خود بانک اطلاعاتی، بدون نیاز به واکشی کل اطلاعات در سمت کلاینت، به صورت یکپارچه استفاده کرد). به همین جهت برای کوئری گرفتن و یا تهیه ایندکس، نیاز است این موارد جداگانه ذخیره شوند.
به این ترتیب زمانیکه کوئری تهیه می‌شود، برای مثال:
 var info = session.Query<Customer>().Where(c => c.CustomerNo == 20).FirstOrDefault();
به کوئری زیر ترجمه می‌گردد:
SELECT DISTINCT TOP(1) (s.[StructureId]),
                           s.[Json]
                    FROM   [CustomerStructure] s
                           LEFT JOIN [CustomerIntegers] mem0
                                ON  mem0.[StructureId] = s.[StructureId]
                                AND mem0.[MemberPath] = 'CustomerNo'
                    WHERE  (mem0.[Value] = 20);
و یا کوئری ذیل:
var info2 = session.Query<Customer>().Where(c => c.CustomerNo == 20 && c.Name=="Vahid").FirstOrDefault();
معادل زیر را خواهد داشت:
SELECT DISTINCT TOP(1) (s.[StructureId]),
                           s.[Json]
                    FROM   [CustomerStructure] s
                           LEFT JOIN [CustomerIntegers] mem0
                                ON  mem0.[StructureId] = s.[StructureId]
                                AND mem0.[MemberPath] = 'CustomerNo'
                           LEFT JOIN [CustomerStrings] mem1
                                ON  mem1.[StructureId] = s.[StructureId]
                                AND mem1.[MemberPath] = 'Name'
                    WHERE  ((mem0.[Value] = 20) AND (mem1.[Value] = 'Vahid'));
در هر دو حالت از جداول کمکی تعریف شده برای تهیه کوئری استفاده کرده و نهایتا فیلد JSON اصلی را برای نگاشت نهایی به اشیاء تعریف شده در برنامه بازگشت می‌دهد.

در کل این هم یک روش تفکر و طراحی Schema less است که با بسیاری از بانک‌های اطلاعاتی موجود سازگاری دارد.
برای مشاهده اطلاعات بیشتری در مورد جزئیات این روش می‌توان به Wiki آن مراجعه کرد.
مطالب
تعامل MATLAB (متلب) با دات نت - قسمت دوم
در قسمت قبل در مورد استفاده دات نت در متلب توضیح داده شد. در این قسمت به نحوه استفاده توابع متلب در دات نت بصورت ساده می‌پردازیم.
فرض کنید تیم برنامه‌نویس متلب و تیم برنامه‌نویس دات نت در تعامل با یکدیگر هستند. وظیفه تیم برنامه نویسی متلب به شرح زیر می‌باشد :

1- نوشتن توابع در متلب و تست کردن آنها جهت توسعه و ارائه مناسب به تیم مقابل
2- درست کردن کامپوننت دات نت در متلب با استفاده از محیط  Deployment Tool GUI  (با اجرای دستور deploytool در متلب)
3- استفاده از یک پکیج بسته‌بندی شده از فایل‌های قابل ارائه به تیم مقابل (اختیاری)
4- کپی پکیج در محل از قبل تعیین شده توسط دو تیم یا ارائه آن به تیم مقابل جهت استفاده

برای مثال M فایل (اصطلاح فایل‌ها در متلب همانند کلاس در دات نت) makesquare.m را که در مسیر
matlabroot\toolbox\dotnetbuilder\Examples\VS8\NET\MagicSquareExample\MagicSquareComp
است را در نظر بگیرید :  
function y = makesquare(x)
%MAKESQUARE Magic square of size x.
%   Y = MAKESQUARE(X) returns a magic square of size x.
%   This file is used as an example for the MATLAB 
%   Builder NE product.

%   Copyright 2001-2012 The MathWorks, Inc.

y = magic(x);
تابع majic یک ماتریس در ابعاد x در x درست می‌کند که درایه‌های آن اعداد صحیح از 1 تا X^2 بوده و مجموع سطر و ستون‌های آن با هم برابر است. X باید بزرگتر یا مساوی 3 باشد.
در صورتی که x  برابر 5 انتخاب شود خروجی متلب بصورت زیر خواهد بود :
17 24  1  8 15
23  5  7 14 16
 4  6 13 20 22
10 12 19 21  3
11 18 25  2  9
در قسمت تهیه یک کامپوننت دات نت اطلاعات زیر را در نظر بگیرید :
 
 makeSqr  
 Project Name 
 MLTestClass 
Class Name 
 makesquare.m  File to compile 
سپس برای درست کردن کامپوننت در محیط  Deployment Tool GUI برنامه متلب را اجرا کرده و در پنجره command دستور deploytool  را اجرا کنید تا پنجره زیر باز شود :

نام و مسیر پروژه را تعیین کنید سپس از منوی کشویی نوع پروژه، که دات نت اسمبلی باشد را انتخاب کنید. پنجره‌ای در به شکل زیر مشاهده خواهد شد : 

در تب build اگر قصد استفاده از اپلیکیشن COM را دارید و یا فایل‌هایی جهت تکمیل پروژه قصد پیوست دارید را در قسمت پایین Add files را انتخاب کنید. و اگر قصد استفاده از اپلیکیشن دات نت را دارید قسمت بالایی Add classes را انتخاب کنید و نام کلاس را وارد کنید.

سپس برای کلاس مورد نظر فایل‌های متلبی که قصد کامپایل کردن آنها را دارید از قسمت Add files پیوست کنید. در صورتیکه قصد اضافه کردن کلاس اضافی را داشتید مجددا مراحل را طی کنید. در انتها دکمه build را زده تا عملیات کامپایل آغاز شود. اما برای استفاده تیم برنامه‌نویسی دات نت احتیاج به کامپایلر متلب می‌باشد که این مهم در پکیجی که به این تیم ارائه خواهد شد مد نظر قرار خواهد گرفت.در قسمت تب Package گزینه Add MCR را انتخاب نمائید :

بعد از انتخاب، دو گزینه برای انتخاب وجود دارد که بطور خلاصه گزینه اول فایل‌های کامپایلر متلب در داخل پروژه جهت ارائه قرار می‌گیرد. همچنین این گزینه جهت استفاده در مواقع درون شبکه‌ای، مواردی که فضای دیسک و عملکرد و .... چندان اهمیت ندارد مورد استفاده قرار می‌گیرد. اما گزینه دوم عکس قضیه بالا عمل می‌کند و برای تعداد یوزر بالا و شبکه‌ای و ... مورد استفاده می‌باشد.

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

سپس کلید پکیج  را زده تا پکیج مورد نظر آماده شود. دقت داشته باشید که بعد از انتخاب کامپایلر متلب، حجم پکیج نزدیک به 400 مگابایت خواهد شد. پکیج مورد نظر بصورت یک فایل exe فشرده خواهد شد.
معمولا پکیج شامل فایل‌های زیر باید باشد :

 Documentation files   componentName.xml 
 Program Database File, which contains debugging information   componentName.pdb (if Debug optionis selected)
 Component assembly file   componentName.dll 
 MCR Installer (if not already installed on the target machine).   MCR Installer 

بعد از طی مراحل فوق نوبت به تیم برنامه‌نویسی دات نت می‌رسد. بعد از دریافت پکیج از تیم برنامه‌نویسی متلب در صورتیکه بر روی سیستم هدف کامپایلر متلب و یا خود متلب نصب نیست باید از داخل پکیج این کامپایلر نصب شود.
دقت داشته باشید که ورژن کامپایلر بر روی سیستم باید با ورژن پکیج دریافتی یکی باشد.
در VS یک پروژه کنسول ایجاد کنید و از فولدر پکیج پروژه دریافتی در زیرفولدر distrib فایل makeSqr.dll را به رفرنس برنامه VS اضافه کنید.
در ادامه از مسیر نصب کامپایلر فایل MWArray.dll را هم به رفرنس پروژه اضافه کنید. این فایل جهت تبادل داده اپلیکیشن با کامپایلر متلب مورد استفاده قرار می‌گیرد.

installation_folder\toolbox\dotnetbuilder\bin\architecture\framework_version
اسمبلی‌های زیر را به کلاس Program  برنامه اضافه کنید :
using System;
using MathWorks.MATLAB.NET.Arrays;
using MyComponentName;
سپس کدهای زیر را به کلاس فوق اضافه نمائید :
static void Main(string[] args)
        {
            MLTestClass obj = new MLTestClass();
            MWArray[] result = obj.makesquare(1, 5);

            MWNumericArray output = (MWNumericArray)result[0];
            Console.WriteLine(output);

        }
توضیحات کدهای فوق :
1- MWNumericArray یک اینترفیس جهت تعیین و نمایش نوع آرایه‌های عددی در متلب است.
2- MWArray یک کلاس abstract جهت دسترسی، فرمت‌دهی و مدیریت آرایه‌های متلب می‌باشد.
3- عدد 1 مشخص کننده تعداد خروجی تابع متلب و عدد 5 ورودی تابع می‌باشد.

خروجی برنامه همانند خروجی متلب بصورت زیر خواهد بود :

نکته:
ورژن فریمورک دات نت در هنگام کامپایل با ورژن Mwarray.dll باید یکی باشد.

مطالب
Blazor 5x - قسمت 22 - احراز هویت و اعتبارسنجی کاربران Blazor Server - بخش 2 - ورود به سیستم و خروج از آن
در قسمت قبل، نحوه‌ی افزودن قالب ابتدایی ASP.NET Core Identity را به یک برنامه‌ی Blazor Server بررسی کردیم. در این مطلب، قسمت‌های ورود و خروج آن‌را به همراه نمایش قسمتی از صفحه، تنها به کاربران اعتبارسنجی شده، بررسی می‌کنیم تا روش دسترسی به اطلاعات ASP.NET Core Identity را در یک برنامه‌ی Blazor Server یکپارچه شده‌ی با آن، مطالعه کنیم.


نمایش قسمتی از صفحه بر اساس وضعیت اعتبارسنجی کاربر

فرض کنید می‌خواهیم در کامپوننت Shared\LoginDisplay.razor که در قسمت قبل آن‌را اضافه کردیم، لینک‌های ثبت نام و لاگین را به کاربران غیر اعتبارسنجی شده (هنوز لاگین نکرده) نمایش دهیم و اگر کاربر، اعتبارسنجی شده بود (لاگین کرده بود)، لینک خروج را به او نمایش دهیم. برای این منظور کامپوننت Shared\LoginDisplay.razor را به صورت زیر تغییر می‌دهیم:
<AuthorizeView>
    <Authorized>
        <a href="Identity/Account/Logout">Logout</a>
    </Authorized>
    <NotAuthorized>
        <a href="Identity/Account/Register">Register</a>
        <a href="Identity/Account/Login">Login</a>
    </NotAuthorized>
</AuthorizeView>
AuthorizeView، یکی از کامپوننت‌های استاندارد Blazor Server است. زمانیکه کاربری به سیستم لاگین کرده باشد، فرگمنت Authorized و در غیر اینصورت قسمت NotAuthorized آن‌را مشاهده خواهد کرد.
البته اگر برنامه را در همین حالت اجرا کنیم، به استثنای زیر خواهیم رسید:
InvalidOperationException: Authorization requires a cascading parameter of type Task<AuthenticationState>.
Consider using CascadingAuthenticationState to supply this.
Microsoft.AspNetCore.Components.Authorization.AuthorizeViewCore.OnParametersSetAsync()
برای رفع این مشکل و ارائه‌ی AuthenticationState به تمام کامپوننت‌های یک برنامه‌ی Blazor Server، نیاز است از کامپوننت CascadingAuthenticationState استفاده کرد. در مورد پارامترهای آبشاری، در قسمت نهم این سری بیشتر بحث شد و هدف از آن، ارائه‌ی یکسری اطلاعات، به تمام زیر کامپوننت‌های یک کامپوننت والد است؛ بدون اینکه نیاز باشد مدام این پارامترها را در هر زیر کامپوننتی، تعریف و تنظیم کنیم. همینقدر که آن‌ها را در بالاترین سطح سلسله مراتب کامپوننت‌های تعریف شده تعریف کردیم، در تمام زیر کامپوننت‌های آن نیز در دسترس خواهند بود.
بنابراین به فایل BlazorServer.App\App.razor که محل تعریف ریشه‌ی مسیریابی برنامه‌است، مراجعه کرده و کامپوننت آن‌را با کامپوننت توکار CascadingAuthenticationState محصور می‌کنیم:
<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
        <Found Context="routeData">
            <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>
اینکار سبب می‌شود تا اطلاعات AuthenticationState، بین تمام کامپوننت‌های یک برنامه‌ی Blazor به اشتراک گذاشته شود.

اکنون اگر برنامه را اجرا کنیم، مشاهده خواهیم کرد که در اولین بار مراجعه‌ی به آن (پیش از لاگین)، لینک به صفحه‌ی خروج، نمایش داده نشده‌است؛ چون آن‌را در فرگمنت مخصوص Authorized قرار دادیم:



آزمایش نمایش منوی خروج برنامه

برای آزمایش برنامه، نیاز است ابتدا یک کاربر جدید را ثبت کنیم؛ چون هنوز هیچ کاربری در آن ثبت نشده‌است و همچنین کاربر پیش‌فرضی را هم به همراه ندارد. در مورد روش ثبت کاربران پیش‌فرض ASP.NET Core Identity، می‌توانید به مطلب «بازنویسی متد مقدار دهی اولیه‌ی کاربر ادمین در ASP.NET Core Identity توسط متد HasData در EF Core» مراجعه کنید و تمام نکات آن، در اینجا هم صادق است (چون پایه‌ی سیستم Identity مورد استفاده، یکی است و هدف ما در اینجا بیشتر بررسی نکات یکپارچه سازی آن با Blazor Server است و نه مرور تمام نکات ریز Identity).
بنابراین ابتدا از منوی بالای صفحه، گزینه‌ی Register را انتخاب کرده و کاربری را ثبت می‌کنیم. پس از ثبت نام، بلافاصله به منوی جدید زیر می‌رسیم که در آن گزینه‌های ورود و ثبت نام، مخفی شده‌اند و اکنون گزینه‌ی خروج از سیستم را نمایش می‌دهد:



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

در همین حال که گزینه‌ی خروج نمایش داده شده‌است، اگر بر روی لینک آن کلیک کنیم، ابتدا ما را به صفحه‌ی مجزای logout هدایت می‌کند. سپس باید در این صفحه، مجددا بر روی لینک logout بالای آن کلیک کنیم. زمانیکه اینکار را انجام دادیم، اکنون صفحه‌ی دیگری را نمایش می‌دهد که به همراه پیام «خروج موفقیت آمیز از سیستم» است! در این پروسه، کاربر احساس می‌کند که کاملا از برنامه‌ی اصلی خارج شده‌است و همچنین مراحل طولانی را نیز باید طی کند.
مدیریت این مراحل توسط دو فایل زیر انجام می‌شوند:
Areas\Identity\Pages\Account\Logout.cshtml
Areas\Identity\Pages\Account\Logout.cshtml.cs

می‌خواهیم کدهای این دو فایل را به نحوی تغییر دهیم که اگر کاربری بر روی لینک logout برنامه‌ی اصلی کلیک کرد، به صورت خودکار logout شده و سپس مجددا به صفحه‌ی اصلی برنامه‌ی Blazor Server هدایت شود و مجبور نباشد تا مراحل طولانی یاد شده را تکرار کند.
به همین جهت ابتدا فایل Logout.cshtml.cs را حذف می‌کنیم؛ چون نیازی به آن نداریم. سپس محتوای فایل Logout.cshtml را به صورت زیر تغییر می‌دهیم:
@page
@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager

@functions
{
    public async Task<IActionResult> OnGet()
    {
        if (SignInManager.IsSignedIn(User))
        {
            <p>You have successfully logged out of the application.</p>
            await SignInManager.SignOutAsync();
        }
        return Redirect("~/");
    }
}
با استفاده از سرویس SignInManager در ASP.NET Core Identity می‌توان یک کاربر را logout کرد که نمونه‌ای از آن‌را در اینجا مشاهده می‌کنید. در این حالت بررسی می‌شود که آیا کاربر جاری، به سیستم وارد شده‌است؟ اگر بله، کوکی‌های او حذف شده و سپس به صفحه‌ی اصلی برنامه، Redirect می‌شود. به این ترتیب به تجربه‌ی کاربری خروج بهتری خواهیم رسید.


نمایش User Claims، در یک برنامه‌ی Blazor Server

سیستم ASP.NET Core Identity، بر اساس User Claims کار می‌کند؛ اطلاعات بیشتر. پس از استفاده از CascadingAuthenticationState در بالاترین سطح برنامه، اطلاعات آن در سراسر برنامه‌ی Blazor Server هم قابل دسترسی است. برای مثال در کامپوننت Shared\LoginDisplay.razor، به نحو زیر می‌توان نام کاربر ثبت نام شده را که یکی از User Claims او است، نمایش داد:
<AuthorizeView>
    <Authorized>
        Hello, @context.User.Identity.Name
        <a href="Identity/Account/Logout">Logout</a>
    </Authorized>



محدود کردن دسترسی به صفحات برنامه تنها برای کاربران اعتبارسنجی شده

پس از لاگین موفق به سیستم، اکنون می‌خواهیم دسترسی به صفحات تعریف اتاق‌ها و یا امکانات رفاهی هتل را تنها به کاربران لاگین شده، محدود کنیم. برای اینکار تنها کافی است از ویژگی Authorize استفاده کنیم. برای مثال به کامپوننت Pages\HotelRoom\HotelRoomList.razor مراجعه کرده و یک سطر زیر را به آن اضافه می‌کنیم:
@attribute [Authorize]
دسترسی به کامپوننتی که دارای دایرکتیو فوق باشد، تنها مختص به کاربران اعتبارسنجی شده‌ی سیستم است.

مشکل! با اینکه تمام کامپوننت‌های مثال جاری را به ویژگی Authorize مزین کرده‌ایم، اما ... کار نمی‌کند! و هنوز هم می‌توان بدون لاگین به سیستم، به محتوای آن‌ها دسترسی داشت.
برای رفع این مشکل، مجددا نیاز است کامپوننت BlazorServer.App\App.razor را ویرایش کرد:
<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
        <Found Context="routeData">
            @*<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />*@
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    <p>Sorry, you do not have access to this page</p>
                </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>
در اینجا RouteView پیشین را کامنت کرده و با AuthorizeRouteView، جایگزین کرده‌ایم. کار آن فعالسازی پردازش ویژگی Authorize افزوده شده‌ی به کامپوننت‌های برنامه‌است. همچنین در اینجا محتوای سفارشی را که در صورت درخواست یک چنین کامپوننت‌هایی نمایش داده می‌شود، در فرگمنت NotAuthorized مشاهده می‌کنید؛ که حتی می‌تواند یک کامپوننت مجزا هم باشد:



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-22.zip
مطالب
بررسی استفاده از ابزارهای آماده در پروژه‌ها
بدون شک علم برنامه نویسی در پیشرفت تکنولوژی دنیا، نقش بسیار کلیدی را ایفا کرده است بطوریکه حتی تصور یک روز بدون گوگل هم بسیار نگران کننده‌است. امروزه همه‌ی صنعت‌های دنیا، از اینترنت و سایت‌هایی که توسط برنامه نویسان راه اندازی می‌شوند، در توسعه کسب و کارهای خود استفاده میکنند. اصولا برنامه نویسی باید در استفاده از ساخته‌های خود برای پیشرفت و توسعه‌ی علم خود پیشرو باشد. بدیهی ست استفاده‌ی درست از تجربیات دیگران باعث صرفه جویی در زمان و هزینه تولید نرم افزار خواهد بود.
 

یک تجربه
سالها پیش یکی از همکاران تعریف می‌کردند که یک شرکت نرم افزاری برای مشاوره معماری نرم افزار از ایشان دعوت به همکاری کرده است. پس از مراجعه به شرکت متوجه شدند که تیم اصلی برنامه نویسان درگیر تولید ORM ای برای پروژه جدید شرکت هستند که برای تولید این ابزار بیش از 4 ماه را وقت صرف کرده‌اند؛ اما در مراحل نهایی کار دچار مشکلات زیادی شده اند. به نحوی که از ایشان برای کمک به رفع مشکل ORM ( به جای تولید نرم افزار مشتری) دعوت کرده‌اند.
 
در آن زمان یادم هست که EF 5 (که تقریبا نسخه سوم  بعد از 3.5 و 4 می‌باشد - جزئیات در اینجا) توسط مایکروسافت ارائه شده بود. همچنین NHibernate هم همزمان با EFها (تاریخچه نسخه‌ها در اینجا) قابل دسترسی بوده‌است. با این حال تیم فنی به این دلیل که کوئری‌های تولیدی توسط EF کند هستند، اقدام به ساخت ORM کرده بودند. جالب اینکه با بررسی بیشتر مشخص شده‌است که حجم داده‌های پروژه در بدترین حالت در یک جدول به 5 هزار رکورد می‌رسد.

4 ماه صرف وقت و هزینه تیم 2 نفره برای طراحی و پیاده سازی و تست ORM ای که در نهایت به دلیل مشکلات Performance کنار گذاشته شد و از EF استفاده کردند. شاید در این 4 ماه می‌توانستند 30 درصد پروژه اصلی را پیاده سازی کنند.

شاید بتوان 3 دلیل عمده «فنی» شکست برخی از پروژه‌های نرم افزاری در ایران را به شرح زیر عنوان کرد:
- عدم استفاده مناسب از ابزارها و راهکار‌های موجود و انجام دوباره کاری
- استفاده غیر ضروری و عجولانه از تکنولوژی‌های جدید (بدون داشتن نیروی کار مسلط)
- پایین بودن سطح فنی و به‌روز نبودن برخی از برنامه نویسان ایرانی


متن باز (Open Source)
با پیشرفت توسعه نرم افزار و تمایل شرکت‌های بزرگ دنیا به تولید کامپوننت‌های متن باز (Open Source) ریسک استفاده از این نوع ابزار‌ها نیز کمتر شده است. بطوریکه درصورت نیاز می‌توان کامپوننت را برای پروژه‌ها سفارش سازی کرد.
شاید کمتر کسی باور می‌کرد که روزی شرکت مایکروسافت محصولات خود را Open Source کند. اما امروز، در سال 2017 میلادی، شرکت مایکروسافت اقدامات مهمی را در این زمینه انجام داده است که می‌توانید جزئیات پروژه‌های متن باز این غول کامپیوتری دنیا را در اینجا و همچنین اینجا ملاحظه کنید.

 
یک سناریو
فرض کنید یک پروژه تحت وب را شروع کرده اید. بدون در نظر گرفتن جزئیات پروژه می‌توان گفت به ابزارهای زیر نیاز خواهید داشت:

ابزار
مثال
  ORM   EF , NHibernate , Dapper , LLBLGEN 
 IOC COntainer   Unity , StructureMap , Autofac , Castle.Windsor, LightInject , Ninject 
 Report Tools   CrsytalReport , Stimusoft , DevExpress Report, Telerik Report Tools, EasyReport 
 UI Component   Telerik , JqueryUI , Bootsrap ,CompnentArt, ComponentOne 
 Error Logger   ELMAH , NLog , log4net 
 Mapper Tools   AutoMapper , ValueInjecter 
همانطور که ملاحظه می‌کنید برای همه‌ی موارد فوق ابزارهای مناسبی وجود دارند که برای پیاده سازی هر کدام، سالها وقت و هزینه صرف شده‌است. همچنین قابلیت اطمینان این ابزار‌ها به مراتب بالاتر از ابزارهای دست ساز خواهد بود. شاید برای ساده‌ترین ابزار فوق 3 ماه زمان لازم باشد تا یک نسخه  باگ دار تهیه شود!


ملاحظات استفاده از ابزارها
توجه به چند نکته در استفاده از ابزارها و کتابخانه‌های آماده ضروری می‌باشد، بدین شرح:
- ابزار مورد نیاز را با R&D (تحقیق و توسعه) انتخاب کنید. ابزارهایی که در پروژه‌های واقعی استفاده شده‌اند، بسیار مناسب می‌باشند.
- توجه داشته باشیدکه استفاده از چندین ابزار باعث ایجاد تداخل در پروژه نشود (این مورد معمولا در کامپوننت‌های UI مانند JqueryUI و Bootsrtap اتفاق می‌افتد)
- مستندات مربوط به ابزار‌ها را حتما مطالعه کنید. لطفا بدون تسلط از ابزاری استفاده نکنید.

گاهی پیش می‌آید که یک برنامه نویس بدون مطالعه مستندات مربوط به یک IOC Container از آن ابزار استفاده میکند و در Register اولیه ویژگی LifeCycle مربوط به Context  را با حالت Singleton مقداردهی میکند. بدین ترتیب پس از نیم ساعت، پروژه به دلیل آنچه که می‌توان "چاقی Context" نامید، DONE یا حداقل کند می‌شود که رفع این مشکل ساعت‌ها زمان می‌برد.

درصورت امکان از ابزارها بصورت مستقیم استفاده نکنید. یک لایه واسط مخصوص خودتان را برای تنظیمات کلی ابزار‌ها تهیه کنید که در آینده به دردتان خواهد خورد! (بیشتر در سمت سرور)

فرض کنید در پروژه WPF از کامپوننت‌های زیبای DevExpress استفاده میکنید. به ازای هر کامپوننت یک کلاس به پروژه اضافه کنید که از کلاس اصلی آن کامپوننت Devexspress ارث می‌برد و در لایه UI خود از کلاس جدید خود استفاده کنید. با این کار می‌توانید ویژگی‌های عمومی کامپوننت‌ها را یکبار برای کل پروژه اعمال کنید.


  نتیجه گیری
  اگر بخواهیم چرخ را اختراع نکنیم و از تجربیات موفق موجود استفاده کنیم، می‌توان نتیجه گرفت که استفاده از ابزارهای آماده برای توسعه نرم افزار با رعایت دستورالعمل استفاده امری مفید می‌باشد. اما باید توجه داشته باشیم که استفاده از هر ابزاری به هرقیمتی در هرپروژه‌ای، حرفه ای نیست. همه‌ی راهکارها، ابزراها و تکنولوژی‌های مورد استفاده باید در راستای هدف اصلی «تولید و تحویل به موقع نرم افزار با کیفیت به مشتری» باشد؛ هدفی که در بسیاری از موارد فراموش شده و بیشتر زمان پروژه، صرف کارهای غیر ضروری می‌شود.
نظرات مطالب
Blazor 5x - قسمت چهارم - مبانی Blazor - بخش 1 - Data Binding
ساده شدن «روش تعریف data binding دو طرفه در کامپوننت‌ها» در دات نت 7

در نکته‌ی فوق، روش تعریف data binding دو طرفه را در کامپوننت‌ها بررسی کردیم. برای مثال اگر بخواهیم کامپوننت سفارشی ما دارای ویژگی bind-Value@ باشد، باید حداقل توسط دو پارامتر زیر پشتیبانی شود:
[Parameter] public string Value { set; get; }
[Parameter] public EventCallback<string> ValueChanged { get; set; }
که در اینجا باید قسمت setter مربوط به خاصیت Value را هم بازنویسی کنیم و توسط آن، با فراخوانی ValueChanged.InvokeAsync، به استفاده کننده اطلاع دهیم که مقدار Value تغییر کرده‌است. این قسمت بازنویسی در دات نت 7 به صورت زیر قابل حذف شدن است (نام فرضی کامپوننت زیر، MyInput است):
<input @bind:get="Value" @bind:set="ValueChanged" />

@code {
    [Parameter]
    public TValue Value { get; set; }
    
    [Parameter]
    public EventCallback<TValue> ValueChanged { get; set; }
}
در دات نت 7، دو binding modifier جدید bind:get و bind:set جهت تعریف ساده‌تر two-way data-binding اضافه شده‌اند که باید به همراه هم اضافه شوند. توسط bind:get، مقدار در حال تغییر و توسط bind:set، یک callback را که پس از تغییر این مقدار فراخوانی می‌شود، مشخص می‌کنیم و ... همین! پس از این تعاریف دیگر نیازی به بازنویسی قسمت set پارامتر Value مانند قبل نیست و اکنون می‌توان به bind-Value@ دو طرفه به صورت زیر دسترسی یافت:
<MyInput @bind-Value="text" />

@code {
    string text = "Type something great!";
}