مطالب
بهبودهای تولید برنامه‌های اجرایی تک فایلی در NET 6x.
تولید برنامه‌های اجرایی تک فایلی در زمان NET Core 3x. ارائه شد؛ اما به همراه این مسائل نیز بود:
- فایل اجرایی تک فایلی تولید شده در اصل یک فایل zip خود باز شونده بود که در یک مکان موقتی به صورت خودکار باز و اجرا می‌شد. این حالت با آنتی‌ویروس‌ها و یا سیستم‌هایی که قسمت‌های اصلی آن‌ها جهت کاربران عادی قفل شده‌اند، مشکلاتی را ایجاد می‌کرد.
- حجم فایل نهایی تولید شده قابل توجه بود. برای نمونه یک برنامه‌ی کنسول Hello world آن حدود 70 مگابایت می‌شد. البته باید درنظر داشت که یک چنین خروجی به همراه یک NET Core runtime. کامل نیز می‌شد.

از آن زمان تغییرات تدریجی مفیدی در این زمینه رخ داده‌اند که خلاصه‌ا‌ی از آن‌ها را تا دات نت 6 در ادامه مرور می‌کنیم.


اصول تولید برنامه‌های اجرایی تک فایلی دات نت

فرض کنید برنامه‌ی کنسول ما از این سه سطر تشکیل شده‌است:
using System;
Console.WriteLine("Hello, World!");
Console.ReadLine();
برای تولید یک برنامه‌ی اجرایی تک فایلی بر اساس آن، می‌توان دستور زیر را در خط فرمان اجرا کرد:
dotnet publish -p:PublishSingleFile=true -r win-x64 -c Release --self-contained true
در این حالت ذکر سیستم عامل هدف، اجباری است؛ از این جهت که خروجی نهایی تنها برای یک سیستم عامل تهیه می‌شود.
پس از اجرای دستور فوق، اگر به مکان C:\MyProject\bin\Release\net6.0\win-x64\publish مراجعه کنیم، به یک فایل exe حدود 62 مگابایتی خواهیم رسید که کمی کم حجم‌تر از نمونه‌ی NET Core 3x. آن است! البته همانطور که عنوان شد این خروجی به همراه runtime متناظری نیز هست. اگر بخواهیم این runtime را از آن حذف کنیم می‌توان به صورت زیر عمل کرد:
dotnet publish -p:PublishSingleFile=true -r win-x64 -c Release --self-contained false
با استفاده از سوئیچ self-contained false دیگر خروجی نهایی به همراه runtime دات نت تشکیل نخواهد شد و حجم حاصل تنها 150 کیلوبایت خواهد بود. در این حالت استفاده کننده‌ی نهایی باید runtime را خودش به صورت مجزایی نصب کند.

یک نکته: می‌توان سوئیچ‌های فوق را به فایل csproj نیز به صورت زیر اضافه کرد:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <PublishSingleFile>true</PublishSingleFile>
    <SelfContained>true</SelfContained>
    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
    <PublishReadyToRun>true</PublishReadyToRun>
  </PropertyGroup>
</Project>


تک فایل‌های اجرایی دات نت 6 دیگر فایل‌های zip خود باز شونده نیستند

همانطور که عنوان شد، تک فایل‌های اجرایی تولید شده‌ی در نگارش‌های پیشین دات نت، چیزی بجز یک فایل zip خود بازشونده که همه چیز داخل آن قرار گرفته بودند، نبودند. این حالت دیگر در دات نت 6 صادق نیست و اینبار خروجی نهایی در حافظه بارگذاری می‌شود و نیاز به باز شدن آن در مکان‌های temp برطرف شده‌است. تا زمان دات نت 5، این قابلیت فقط برای خروجی‌های لینوکس تدارک دیده شده بود، اما با ارائه‌ی دات نت 6، خروجی‌های ویندوز و مک هم فایل‌های اجرایی واقعی هستند.


فعالسازی IL Trimming

به صورت پیش‌فرض با اجرای دستورات تولید تک فایل‌های اجرایی برنامه‌های دات نت، تمام وابستگی‌های استفاده شده بدون هیچگونه بهینه سازی در کنار هم قرار می‌گیرند. با فعالسازی قابلیت IL Trimming می‌توان وابستگی‌هایی را که برنامه از آن‌ها استفاده نمی‌کند، از خروجی نهایی حذف کرد که در نتیجه‌ی آن، شاهد کاهش حجم قابل ملاحظه‌ی فایل تولیدی نهایی خواهیم بود. برای اینکار می‌توان سوئیچ PublishTrimmed را فعالسازی کرد:
dotnet publish -p:PublishSingleFile=true -r win-x64 -c Release --self-contained true -p:PublishTrimmed=true
پس از آن برنامه‌ی 60 مگابایتی تولیدی در ابتدای بحث، تبدیل به یک برنامه‌ی اجرایی تک فایلی 11 مگابایتی می‌شود که کاهش حجم قابل توجهی است.
باید دقت داشت که این حجم نهایی، یک فایل اجرایی واقعی بدون نیاز به نصب هیچ نوع runtime ای است و کاملا متکی به خود است.


فعالسازی فشرده سازی

به همراه دات نت 6، امکان فشرده سازی خودکار این خروجی نهایی تک فایلی، جهت کاهش هرچه بیشتر حجم آن نیز میسر شده‌است. برای اینکار می‌توان سوئیچ EnableCompressionInSingleFile را فعالسازی کرد:
dotnet publish -p:PublishSingleFile=true -r win-x64 -c Release --self-contained true -p:EnableCompressionInSingleFile=true
خروجی آن یک فایل 30 مگابایتی بدون IL Trimming است که نسبت به خروجی 60 مگابایتی ابتدای بحث، باز هم کاهش قابل ملاحظه‌ای داشته‌است.
نظرات مطالب
چگونه نرم افزارهای تحت وب سریعتری داشته باشیم؟ قسمت پنجم
  1. فشرده سازی قبل از اجرای برنامه (Pre-Compression) یعنی که شما قبل از اینکه برنامه خود را در محیط اصلی نصب و اجرا کنید، فایل‌های اسکریپت آن را فشرده کنید. یعنی کاربران فایل اسکریپت فشرده شده را درخواست و دانلود می‌کنند و عملیات اضافی در سمت سرور انجام نمی‌شود. به عنوان مثال شما از فایل JQuery.min.js به جای jquery.js استفاده کنید. یعنی استفاده از نسخه فشرده شده اسکریپت ها.
  2. فشرده سازی زمان اجرا (Run-time Compression) یعنی فشرده سازی اسکریپت‌های مورد نیاز کاربر توسط خود برنامه وب (به صورت خودکار و یا توسط یک ماژول اضافی). این عمل باعث می‌شود که در هر بار درخواست هر کاربر برای یک فایل، برنامه آن را مجدد فشرده سازی کند (و یا از cache استفاده کند). این عمل به معنی استفاده بیشتر از منابع پر ارزش سرور شما می‌باشد. به عنوان مثال شما بخواهید در هر مرحله درخواست هر کاربر jquery.js را فشرده کنید!
از مقایسه دو حالت بالا مشخص است وقتی شما فقط یکبار اسکریپت‌های خود را فشرده می‌کنید بسیار از حالتی که در هر مرتبه از درخواست کاربران آن را فشرده کنید بهتر است و کمتر منابع سرور را هدر می‌دهد.
مطالب
پیاده سازی یک سیستم دسترسی Role Based در Web API و AngularJs - بخش اول
در این مجموعه مقالات قصد دارم یک روش را برای پیاده سازی سیستم Role Based سفارشی شده، به صورت پروژه محور، در اختیار شما دوستان قرار دهم. این مجموعه مقالات در هر دو بخش سرور و کلاینت، در قالب یک پروژه‌ی واقعی ارائه خواهد شد. در سمت سرور از Web API و سیستم Identity 2.1 و در سوی کلاینت از تکنولوژی قدرتمند AngularJs کمک گرفته شده‌است. در این مجموعه قصد دارم تا مراحل کلی و اصلی این سیستم دسترسی را تشریح کنم.

مقدمه ای بر سیستم مبتنی بر نقش کاربران

شاید برای شما هم پیش آمده باشد که برای وب سایت یا وب اپلیکیشن خود، به دنبال یک سیستم دسترسی بوده‌اید که بتوانید در آن نقش‌های گوناگونی را تعریف کنید و هر نقش شامل یک سری دسترسی خاص باشد و همچنین با اضافه شدن هر کاربر به این سیستم، بتوانید آنها را به یک نقش خاص انتساب دهید. برای اجرای این امر، روش‌های گوناگونی وجود دارد. یکی از این روش‌ها در سایت CodeProject توسط آقای Stefan Wloch تشریح شده است. در این مقاله کلیت و هدف یک سیستم Role Based، ساختار نقش‌ها و کاربران و دسترسی‌های آنها به خوبی بیان شده است.
 
و اما کمی درباره روشهای موجود حول توسعه یک سیستم مبتنی بر نقش (Role Based) صحبت کنیم. در ابتدا لازم است یک تعریف کلی از یک سیستم Role Based داشته باشیم:
یک سیستم مدیریت کاربر مبتنی بر نقش، سیستمی است که در آن نقش‌های گوناگونی تعریف می‌گردد. به گونه‌ای که هر نقش شامل یک سری دسترسی هایی است که به کاربر اجازه می‌دهد تا به بخشی از سیستم دسترسی داشته باشد. هر کاربر در این سیستم می‌تواند یک و یا چند نقش متفاوت داشته باشد و بر اساس آن نقش‌ها و دسترسی‌های تعریف شده، درون هر نقش به قسمتی از سیستم دسترسی داشته باشد.
بر اساس این تعریف ما در نهایت سه موجودیت نقش (Role)، دسترسی (Permission) و کاربر (User) را خواهیم داشت. در این سیستم دسترسی را اینگونه تعریف میکنیم:
دسترسی در یک سیستم، مجموعه‌ای از حوزه‌ها و یا ناحیه‌هایی است که در سیستم تعریف می‌شود. این حوزه‌ها می‌توانند شامل یک متد یا مجموعه‌ای از متدهای نوشته شده و یا فراتر از آن، شامل مجموعه‌ای از کنترلرها باشد که کاربر اجازه‌ی فراخوانی آنها را دارد.

بدیهی است کاربر برای ما موجودیتی مشخص است. یک کاربر ویژگی‌هایی مانند نام، نام کاربری، سن، رمز عبور و ... خاص خود را داراست که به واسطه این ویژگی‌ها از کاربری دیگر تمییز داده میشود. کاربر در سیستم طی دو مرحله جداگانه Authentication و Authorization مجاز است تا از بخش‌هایی از سیستم استفاده کند. مرحله Authentication به طور خلاصه شامل مرحله‌ای است که هویت کاربر (به عنوان مثال نام کاربری و رمز عبور) تایید صلاحیت میشود. این مرحله در واقع تایید کننده کاربر است و اما بخش بعدی که ما قصد داریم تا در این مورد راهکاری را ارائه دهیم بخش Authorization است. در این بخش به کاربر بر اساس نقش وی دسترسی‌هایی اعطا می‌گردد و کاربر را به استفاده از بخش‌هایی از سیستم مجاز میدارد.

دیاگرام زیر نمود سه موجودیت کاربر، نقش و دسترسی میباشد.

برای شرح دیاگرام فوق این چنین میتوان گفت که هر کاربر مجاز است چندین نقش داشته باشد و هر نقش نیز میتواند به چندین کاربر انتساب شود. در مورد دسترسی‌ها نیز به همین صورت، هر دسترسی نیز میتواند به چندین نقش انتساب شود.

ارائه یک سیستم مبتنی بر نقش کاربران با استفاده از تکنولوژی Web API و AngularJs

حال که از محتوا و عملکرد یک سیستم مبتنی بر نقش (Role Based) دانش کافی را کسب کردیم، قصد داریم تا به پیاده سازی کامل این سیستم بپردازیم. شاید برای شما سوال باشد که چرا ما قصد داریم تا این معماری را در Web API و AngularJs پیاده سازی کنیم. در جواب به این سوال باید بگوییم که پیاده سازی این روشها در تکنولوژی‌هایی مانند ASP.NET MVC و تکنولوژی‌ةای مشابه آن که صفحه را در سمت سرور می‌سازند، در بسیاری از منابع و مقالات اینترنتی موجود است. ما قصد داریم تا آنچه را که در یک Single Page Application پیاده سازی کرده‌ایم، به اشتراک بگذاریم. بنابراین قصد داریم تا علاوه بر تشریح کامل این سیستم که قسمت اعظم آن در سمت سرور انجام میشود، به تشریح نحوه تبادل اطلاعات و مدیریت دسترسی‌ها در سمت کلاینت نیز بپردازیم.
همانطور که میدانید ما در یک سیستم بر پایه SPA، برای ساخت یک صفحه ممکن است چندین API را فراخوانی کنیم. بنابراین APIهای نوشته شده به تعداد زیاد ولی با عملکرد اتمیک خواهند بود. به عنوان مثال در یک سیستم فروشگاه اینترنتی، برای ویرایش یک محصول، ممکن است دو یا چند متد API (فراخوانی آن محصول برای قرار دادن در فیلدهای ویرایش، ارسال اطلاعات پس از ویرایش به سرور و ...) فراخوانی گردند. با این حساب ما در تعریف و ایجاد دسترسی‌ها دو راه را خواهیم داشت.
  1. تعریف هر یک از متدهای اتمیک به عنوان یک مجوز دسترسی: در این روش نام کنترلر و نام متد به عنوان یک دسترسی تعریف خواهد شد. در این روش به ازای هر متد ما یک آیتم جدید را باید به جدول Permissions، اضافه نماییم و در نهایت برای تعریف یک نقش و انتساب دسترسی‌ها به یک کاربر، بایستی یک مجموعه چک باکس را که در نهایت به یک متد API ختم میشود، فعال یا غیر فعال کنیم.
  2. تعریف ناحیه‌های مختلف و کنترل‌های قابل انجام در آن ناحیه: در این روش ما قصد داریم تا مجموعه‌ای از متد‌ها را که هدف مشترکی را انجام خواهند داد، در یک ناحیه و یک کنترل بگنجانیم و از به وجود آمدن تعداد زیادی مجوز دسترسی جلوگیری نماییم. به عنوان مثال فرض کنید میخواهیم یک سطح دسترسی را با نام ویرایش کاربران تعریف کنیم. همانگونه که گفتیم ممکن است برای یک عملیات، دو و یا چندین متد درون یک کنترلر تعریف شوند. حال ما این متد‌ها را درون یک ناحیه دسترسی قرار خواهیم داد و آن را در یک حوزه و یک کنترل (Area & Control) میگنجانیم.
در بخش بعدی سعی میکنیم تا این مبحث را بازتر کنیم و به صورت عملی مراحل توسعه این مدل را تشریح نماییم.
مطالب
مدیریت کلیدهای کیبرد در جاوا اسکریپت
با پیشرفت بسترهای موجود در زمینه شبکه و اینترنت، گرایش به استفاده از اپلیکیشنهای تحت وب روز به روز بیشتر میشود. با گسترش این برنامه‌ها نیازها و درنتیجه ابزارهای موجود توسعه پیدا می‌کنند. درحال حاضر ابزارها و نیز محیطهای توسعه مختلفی برای تولید این اپلیکیشنها وجود دارد. به دلیل نوع رابط کاربری موجود در این برنامه‌ها (اکثراً مرورگرهای وب مثل اینترنت اکسپلورر، گوگل کروم، فایرفاکس و ...) استفاده از زبانهای سمت کلاینت (مثل جاوا اسکریپت که در تمامی مرورگرهای مدرن پشتیبانی کاملی از آن میشود) جایگاه ویژه ای در این نوع برنامه‌ها دارد. درضمن وقتی صحبت از اپلیکیشن به میان می‌آید استفاده از کلیدهای میانبر کیبرد برای راحتی کار کاربران کاربرد ویژه ای دارد. اما متاسفانه زبان جاوا اسکریپت به دلیل محدودیتهایی منطقی موجود، پشتیبانی مناسبی از رویدادهای کیبرد ندارد و مشکل تفاوتها و تناقضات میان سخت افزارها، سیستم عامل‌ها و مرورگرها هم به این مسئله بیشتر دامن میزند. در مطلب جاری هدف این است تا آشنایی مقدماتی با این مبحث فراهم شود.
رویدادهای کیبرد
در جاوا اسکریپت سه رویداد زیر برای کلیدهای کیبرد وجود دارد (به ترتیب زمان رخ دادن):
keydown: زمانی که یک کلید فشرده می‌شود.
keypress: زمانی که یک کلید کاراکتری فشرده می‌شود.
keyup: زمانی که یک کلیدِ فشرده شده، رها می‌شود.
یک تفاوت اساسی میان رویدادهای keydown و keypress در جاوا اسکریپت وجود دارد: رویداد keydown پس از فشردن هر کلیدی روی کیبرد رخ میدهد و یک کد مخصوص آن کلید (scan code ^) را ارائه میدهد. اما رویداد keypress که بعد از keydown رخ میدهد کد کاراکتر آن کلید (char code) را ارائه میدهد، بنابراین تنها برای کلیدهای کاراکتری بدرستی کار میکند. برای درک بهتر کد زیر را در یک فایل html ذخیره کرده و در مرورگرهای مختلف آزمایش کنید:
<html>
<body>
  <div>
    Prevent default:
    <input type="checkbox" id="keydownStop" value="1" />
    keydown&nbsp;&nbsp;&nbsp;
    <input type="checkbox" id="keypressStop" value="1" />
    keypress&nbsp;&nbsp;&nbsp;
    <input type="checkbox" id="keyupStop" value="1" />
    keyup
  </div>
  Ignore:
  <input type="checkbox" id="keydownIgnore" value="1" />
  keydown &nbsp;&nbsp;&nbsp;
  <input type="checkbox" id="keypressIgnore" value="1" />
  keypress &nbsp;&nbsp;&nbsp;
  <input type="checkbox" id="keyupIgnore" value="1" />
  keyup
  <div>
    Focus on the input below and press any key.
  </div>
  <div>
    <input type="text" style=" width: 600px" id="keyInput" />
  </div>
  Log:
  <div>
    <textarea id="keyLogger" rows="18" onfocus="this.blur()" style="width: 600px; border: 1px solid black"></textarea>
  </div>
  <input type="button" value="Clear" onclick="clearLog()" />
  <script type="text/javascript">
    document.getElementById('keyInput').onkeydown = keyHandler;
    document.getElementById('keyInput').onkeyup = keyHandler;
    document.getElementById('keyInput').onkeypress = keyHandler;
    document.getElementById('keyInput').focus();
    function keyHandler(e) {
      e = e || window.event;
      if (document.getElementById(e.type + 'Ignore').checked) return;
      var evt = e.type;
      while (evt.length < 10) evt += ' ' +
        log(evt +
          ' keyCode=' + e.keyCode +
          ' which=' + e.which +
          ' charCode=' + e.charCode +
          ' char=' + String.fromCharCode(e.keyCode || e.charCode) +
          (e.shiftKey ? ' +shift' : '') +
          (e.ctrlKey ? ' +ctrl' : '') +
          (e.altKey ? ' +alt' : '') +
          (e.metaKey ? ' +meta' : ''));
      if (document.getElementById(e.type + 'Stop').checked) {
        e.preventDefault ? e.preventDefault() : (e.returnValue = false);
      }
    }
    function clearLog() {
      document.getElementById('keyLogger').value = '';
      document.getElementById('keyInput').focus();
    }
    function log(text) {
      var area = document.getElementById('keyLogger');
      area.value += text + '\n';
      area.scrollTop = area.scrollHeight;
    }
  </script>
</body>
</html>
نکته: برای جلوگیری از اجرای مرورگرها در حالت Quirks حتما از تگ doctype در ابتدای فایلهای html خود استفاده کنید. درغیراینصورت رفتارهای غیرمنتظره ای (مخصوصا در IE) مشاهده خواهید کرد. برای اجرای مرورگرها در حالت استاندارد html5 (بهترین حالت در حال حاضر) میتوانید از تگ زیر استفاده کنید:
<!doctype html>
دقت کنید که قبل از این خط هیچ چیز دیگری نوشته نشود وگرنه در IE از آن صرفنظر میشود!
یا اینکه در IE با استفاده از developer tools (دکمه F12) برای Document Mode گزینه ای غیر از Quirks mode (بهتر است از حالت IE9 یا بالاتر استفاده کنید) را انتخاب کنید.

برای کسب اطلاعات بیشتر راجع به doctypeهای مختلف و نیز حالت quirks میتوانید به ^ و ^ و ^ و ^ رجوع کنید. پیشنهاد میکنم که این منابع را حتما مطالعه کنید.
نکته: در کد بالا متد preventDefault در  -8 IE تعریف نشده است (درواقع در IE تنها در نسخه 9 تعریف شده است). همچنین استفاده از پراپرتی returnValue در فایرفاکس و IE9 کار نمیکند! از این خط کد برای جلوگیری از رفتار پیشفرض رویداد استفاده شده است. همانطور که در ادامه میخوانید راه حل ساده‌تری نیز برای اینکار وجود دارد.
متد String.fromCharCode برای نمایش کاراکتر کلید فشرده شده استفاده شده است. البته اگر کلید غیرکاراکتری فشرده شود ممکن است با نتایج غیرمنتظره ای روبرو شوید.
با استفاده از html تولیدی در مرورگرهای مختلف سعی کنید موارد زیر را آزمایش کنید:
کلیدهای کاراکتری چون a / | { 6 را  بفشارید. در این حالت رویدادهای keydown و سپس keypress رخ خواهند داد. پس از رها کردن کلیدها نیز رویداد keyup رخ میدهد.
یکی از کلیدهای غیرکاراکتری مثل ctrl یا alt را بفشارید. در این حالت تنها رویدادهای keydown و keyup رخ خواهند داد و خبری از رویداد keypress نیست.
نکته: مرورگرهای FireFox و Opera در مورد بیشتر کلیدهای غیرکاراکتری نیز رویداد keypress را صدا خواهند زد! مرورگر IE این رفتار را تنها در مورد کلید Esc نشان میدهد. همچنین در IE و Opera کلید PrtScr هیچ رویدادی را فرا نمیخواند. ظاهرا تنها مرورگر Chrome بدرستی عمل میکند. 
درحالت کلی فشردن کلیدهای غیرکاراکتری نباید رویداد keypress را فراخوانی کند.
بنابراین:
keydown و keyup برای همه کلیدها
keypress برای کلیدهای کاراکتری

پراپرتی‌های رویدادهای کیبرد
برخلاف نسخه‌های قدیمی مرورگرها که هرکدام راه و روش خودشان را برای تعامل با این رویدادها برگزیده بودند، امروزه تمامی مرورگرها تقریبا از یک روش استاندارد و مشترک برای اینکار استفاده میکنند. در تصاویر زیر تمام اجزای آبجکت KeyboardEvent با استفاده از کد زیر در مرورگرهای اپرا و کروم نشان داده شده است. استفاده از کد زیر در مرورگرهای فایرفاکس و IE نتایج جالبی مانند تصاویر زیر فراهم نمیکند!
    document.onkeydown = function (e) {
      e = e || event;
      console.log(e);
    }

همانطور که مشاهده میکنید تفاوتهایی بین مرورگرها در این آبجکت به چشم میخورد. برای کسب اطلاعات بیشتر در مورد این آبجکت و اجزای استاندارد آن در DOM Level 3 به اینجا مراجعه کنید. در ادامه به بررسی پراپرتی‌های مهم آرگومان این رویدادها (همان KeyboardEvent) میپردازیم.
keyCode
همان scan code کلید فشرده شده است. برای مثال اگر کلید a فشرده شود کاراکتر تولیدی ممکن است a یا A یا 'ش' (یا کاراکتری دیگر در زبانهای مختلف) باشد اما در تمامی حالات scan code مربوطه یا همان keyCode همیشه یکسان (65 برای کلید a) خواهد بود. این کد تنها به کلید فشرده شده بستگی دارد و نه به کاراکتر حاصله! البته در IE به هنگام رخ دادن رویداد keypress کد کاراکتر (همان char code) کلید فشرده شده در این پراپرتی قرار میگیرد!
در این بین میان مرورگرهای مختلف تفاوتهایی وجود دارد که با یک جستجو در اینترنت میتوان به تمامی این کدها دسترسی پیدا کرد. خواندن مقاله کامل JavaScript Madness: Keyboard Events نیز خالی از لطف نیست.
charCode
همان کد ASCII (یا کد UTF-16 برای کاراکترهای یونیکد. اطلاعات بیشتر ^ و ^) کاراکتر کلید فشرده شده است. ممکن است با keyCode برابر باشد. این پراپرتی در IE و Opera تعریف نشده است.
در عمل ممکن است keyCode و charCode در پلتفرمهای مختلف و حتی بین سیستم عامل‌های مختلف در یک سخت افزار نتایج متفاوتی ارائه دهند. بنابراین آزمودن هر مورد مشکوک قبل از ریلیز نهایی محصول میتواند مفید باشد.
which
یک پراپرتی نسبتا غیراستاندارد! است که ترکیبی از keyCode و charCode را برمیگرداند (اطلاعات بیشتر در ^ و ^). این پراپرتی در IE تعریف نشده است.
shiftKey, ctrlKey, altKey, metaKey 
پراپرتی هایی از نوع بولین که وضعیت کلیدهای Shift, Ctrl, Alt و Command (تنها در مک) را نشان میدهند. 
نکته: بدستن آوردن کد درست کاراکتر فشرده شده با توجه به وضعیت کلیدها و زبان انتخابی تنها با استفاده از رویداد keypress امکان پذیر است. با توجه به تفاوتهایی که بین مرورگرهای مختلف وجود دارد برای یافتن کاراکتر فشرده شده در رویداد keypress استفاده از متد زیر توصیه میشود:
function getCharacter(event) {
  if (event.which == null)
     return String.fromCharCode(event.keyCode);    // IE
  else if (event.which != 0 && event.charCode != 0)
     return String.fromCharCode(event.which);  // All others
  return null; // special key
}
توضیحات بیشتر و کاملتر این مبحث را میتوانید از Document Object Model (DOM) Level 3 Events Specification که آخرین نسخه آن در زمان تهیه این مطلب در تاریخ 14 June 2012 انتشار یافته تهیه کنید. بطور ویژه برای مبحث رویدادهای کیبرد (^) اطلاعات بسیار بیشتری در دسترس است.
نکته: با توجه به اطلاعاتی که در سند فوق وجود دارد، به دلیل ماهیت همزمانی (Sync) رویدادهای کیبرد تا زمانی که تمام عملیات موجود در متد تعیین شده برای این رویدادها انجام نشود، رویداد بعدی (با توجه به ترتیبی که در ابتدای این مطلب آورده شده است) رخ نخواهد داد. برای تست این موضوع قطعه کد زیر را آماده کردم:
<html>
<body>
  <input id="inputText" type="text"  />
  <script>
    document.onkeydown = keyDown;
    document.onkeypress = keyPress;
    document.onkeyup = keyUp;
    function keyDown(e) {
      var input = document.getElementById('inputText');
      input.value = 'keyDown started ...';
      input.disabled = true;
      var j = 0;
      for (var i = 0; i < 999999999; i++) {
        j = i - j;
      }
      console.log(j);
      //alert('keyDown');
      input.value = 'keyDown finished.';
      input.disabled = false;
    }
    function keyPress(e) {
      alert('keyPress');
      //console.log('keyPress');
    } function keyUp(e) {
      alert('keyUp');
      //console.log('keyUp');
    }
  </script>
</body>
</html>
اگر کد بالا در مرورگرهای مختلف امتحان کنید مشاهده میکنید که انجام عملیات سنگین در رویدادهای کیبرد موجب ایجاد وقفه در فراخوانی سایر رویدادها میشود.
با اجرای کد فوق در مرورگرهای مختلف نکات جالب زیر بدست آمد:
- در IE و کروم نمایش یک alert موجب از دست دادن فوکس document شده و بنابراین رویدادهای کیبرد بعد از نمایش alert کار نخواهند کرد! مثلا اگر در keydown یک alert نمایش داده شود چون رویداد keyup بر روی پنجره alert رخ میدهد بنابراین keyup فراخوانی نمیشود ولی چون رویداد keypress با keydown همزمان است این اتفاق برای keypress نمی‌افتد. این مشکل در فایرفاکس پیش نمی‌آید. در اپرا در این حالت رویداد keypress هم رخ نمیدهد! البته رفتار IE در اجرای کد فوق کمی غیرمنتظره‌تر است و ظاهرا رویداد keypress هم رخ نمیدهد.
- در اجرای کد فوق در FireFox ظاهرا alert مربوط به keyup قبل از keypress نمایش داده میشود. البته اگر به جای alert از console.log (البته نیاز به نصب Firebug است) استفاده شود این به هم خوردگی ترتیب رویدادها وجود ندارد.
- در مرورگر Opera پس از فشردن کلید enter در نوار آدرس و پس از بارگذاری صفحه، فوکس بلافاصله به عنصر document سپرده میشود طوریکه که رویداد keyup کلید enter در document بارگذاری شده فراخوانی میشود و درصورت سرعت بالای بارگذاری صفحه، کدهای مروبوط به این رویداد اجرا میشوند. در سایر مرورگرها این مورد مشاهده نشد. 
- ظاهرا در تمام مرورگرها به غیر Opera کدهای جاوا اسکریپ در ثرد UI اجرا شده و موجب قفل شدن document میشود. بنابراین وسط اجرای کدهای سنگین نمیتوان مثلا خواص عناصر UI را تغییر داد. درواقع مرورگر اپرا برخلاف سایر مرورگرها، رفتار ویژه ای در برخورد با جاوا اسکریپت و رویدادها و تغییرات عناصر DOM دارد. برای کسب اطلاعات بیشتر در این زمینه به اینجا مراجعه کنید. درضمن این مبحث کمی پیچیده‌تر از آن است که به نظر می‌آید(^). برای بررسی بیشتر میتوانید کد زیر را در مرورگرهای مختلف آزمایش کنید:
<html>
<head>
  <script type="text/javascript">
    function process() {
      var above = 0, below = 0;
      for (var i = 0; i < 200000; i++) {
        if (Math.random() * 2 > 1) {
          above++;
        }
        else {
          below++;
        }
      }
    }
    function test() {
      var result1 = document.getElementById('log');
      var start = new Date().getTime();
      console.log('start');
      for (var i = 0; i < 200; i++) {
        result1.value = 'time=' + (new Date().getTime() - start) + ' [i=' + i + ']';
        process();
      }
      result1.value = 'time=' + (new Date().getTime() - start) + ' [done]';
      console.log('end');
    }
    window.onload = test;
  </script>
</head>
<body>
  <input id='log' />
</body>
</html>
اگر کد فوق را در مرورگرهایی غیر از اپرا اجرا کنید میبینید که تنها نتیجه نهایی نمایش داده میشود و فرایندهای میانی درون حلقه نمیتوانند تغییری در محتوای UI ایجاد کنند. طبق انتظار کروم از بقیه بسیار سریعتر بوده و سپس IE و پس از آن فایرفاکس قرار دارد. اما در مورد Opera وضع کاملا فرق میکند و به دلیل به روز رسانی همزمان UI عملیات بسیار بسیار کندتر از بقیه مرورگرها به اتمام میرسد.
نکته: حالات استثنایی دیگری هم در اجرای کدهای مشابه در مرورگرهای مختلف پیش می‌آید که به دلیل پیچیده کردن بیش از حد بحث آورده نشده اند. فقط ذکر این نکته الزامی است که درحال حاضر میزان تفاوت رفتار مرورگرهای مختلف دربرخورد با کدهای یکسان قابل ملاحظه است. بنابراین در هنگام توسعه سعی کنید حداقل در این چهار مرورگر معروف آزمایشات خود را به سرانجام برسانید.
و در ادامه چند مثال ...

غیرفعال کردن ورودی کاربر
برای اینکار فقط کافی است در رویدادهای keydown یا keypress مقدار false برگشت داده شود. البته در مروگر Opera برخی از کلیدها از این رفتار پیروی نمیکنند. مثل کاراکترهای '`' و '+' و '=' که درصورت برگشت false در رویداد keydown باز هم رفتار پیش فرض را از خود نشان میدهد. اما برگرداندن مقدار false در رویداد keypress این مشکل را حل میکند(این موارد در نسخه 11.51 تست شدند). میتوانید با استفاده از کد زیر در تمام مرورگرها این موارد را آزمایش کنید:
<input onkeydown="return false" />
<input onkeypress="return false" /> 

تبدیل به حروف بزرگ
document.getElementById('myInputText').onkeypress = function (e) {
  var char = getCharacter(e || window.event);
  if (!char) return; // special key
  this.value += char.toUpperCase();
  return false; // برای اینکه کاراکتر اضافی نمایش داده نشود
}
در کد فوق متد getCharacter در بالا در قسمت پراپرتی‌های آرگومان رویداد نشان داده شده است. کد فوق چندان کامل نیست و همیشه کاراکتر فشرده شده را بدون توجه به موقعیت کرسر کیبرد در انتهای متن قرار میدهد.
نکته: استفاده از عبارتی چون e || window.event به این دلیل است که در مرورگر IE آرگومان رویداد (همان e که به آن implicit event object نیز میگویند) به متد مربوطه ارسال نمیشود (تا نسخه 9 که تست کردم مسئله به همین صورت است) در عوض پراپرتی‌های این آرگومان از طریق window.event (یا همان event که به آن explicit event object نیز میگویند) در دسترس هستند. این فیلد در واقع آرگومان آخرین رویداد رخ داده در پنجره جاری را در خود ذخیره میکند. اما در سایر مرورگرها به صورت استاندارد مقدار این آرگومان به عنوان پارامتر رویداد به متد مربوطه ارسال میشود. جالب است که بدانید مرورگرهای Opera و Chrome از هر دو روش پشتیبانی میکنند. مرورگر فایرفاکس تنها از ارسال آرگومان رویداد به متد مربوطه پشتیبانی میکند.
عبارت e ||window.event درواقع شکل دیگر عبارت زیر است:
e ? e : window.event;
در جاوا اسکریپت اگر عملگر مقایسه ای در عبارت مقایسه آورده نشود مقدار عبارت با false مقایسه میشود. این مقایسه از نوع abstract است (در ادامه این مطلب توضیح داده شده است). در جاوا اسکریپت 0 و رشته خالی و null و undefined و امثال اینها در مقایسه abstract برابر false درنظر گرفته میشوند. بنابراین در عبارت مقایسه بالا اگر مقدار e مثلا undefined (در IE) باشد مقدار window.event بازگشت داده میشود و درغیراینصورت خود e برگشت داده میشود.

تنها عدد
document.getElementById('numberInputText').onkeypress = function (e) {
  e = e || window.event;
  var chr = getCharacter(e);
  if (!isNumeric(chr) && chr !== null) return false;
}
function isNumeric(n) {
  return !isNaN(parseFloat(n)) && isFinite(n);
}
function isNumber(val) {
  return val !== "NaN" && (+val) + '' === val + ''
}
در کد بالا متد isNumeric از اینجا گرفته شده است. متد isNumeric جی کوئری (که از نسخه 1.7 اضافه شده است) هم دقیقا از این روش استفاده میکند.
متد دوم یعنی  isNumber (که در کد فوق از آن استفاده نشده است) روش دیگری را برای اطمینان از مقدار عددی بودن استفاده میکند. در این روش درصورتیکه val یک مقدار عددی نباشد val+ برابر NaN میشود که در نهایت عبارت مقایسه ای مقدار false را برمیگرداند. البته به غیر از خود مقدار NaN که در شرط اول مورد بررسی قرار گرفته است. برای کسب اطلاعات بیشتر در مورد رفتار نسبتا عجیب جاوا اسکریپت با NaN و متد isNaN به اینجا سر بزنید.
نکته: تفاوت == با === (یا =! و ==!) - در جاوا اسکریپت دو روش برای مقایسه مقادیر متغیرها وجود دارد. اولی (== یا =!) مقایسه را با تبدل نوع داده‌ها انجام میدهد (که اصطلاحا به آن type-converting equality comparison یا abstract comparison میگویند) و دومی (=== یا ==!) مقایسه را با مقادیر واقعی و بدون تبدیل نوع داده انجام میدهد (که به آن equality without type coercion یا strict equality comparison گفته میشود). در واقع در مقایسه strict تنها وقتی که دو متغیر از یک نوع باشند ممکن است مقدار true برگشت داده شود. برای روشنتر شدن مطلب به مثالهای زیر توجه کنید (^):
0==false   // true
0===false  // false, because they are of a different type
1=="1"     // true, auto type coercion
1==="1"    // false, because they are of a different type 
توضیحات بهتری در اینجا آورده شده است.
برای کسب اطلاعات کاملتر میتوانید به ECMAScript Language Specification و قسمت مقایسه strict و abstract مراجعه کنید. (رابطه‌ها و تفاوت‌های میان ECMAScript و JavaScript و JScript و ActionScript در اینجا آورده شده است.)

کار با Scan Code و Char Code
همانطور که قبلا هم اشاره شد میان کد کاراکتر و کلید فشرده شده بر روی کیبرد تفاوت وجود دارد. برای بهره برداری از کلیدهای میانبر در صفحات وب به کد کلید فشرده شده (همان scan code) نیاز است و نه به کد کاراکتر آن. همچنین کلیدهای ویژه و غیرکاراکتری دارای کد کاراکتر نیستند. بیشتر مرورگرها رویداد Keypress را برای کلیدهای غیرکاراکتری فرا نمیخوانند. بنابراین برای این موارد رویدادهای keydown و keyup مفید هستند. امروز تمام مرورگرها از جدول کدهای یکسانی برای scan codeها استفاده میکنند که نمونه‌های آن را میتوانید در ^ و ^ و ^ مشاهده کنید. نکته ای که باید درباره پراپرتی keyCode یادآوری شود این است که جدای از وضعیت کیبرد (مثل زبان یا موقعیت کلید capsLock) در تمام حالات در ازای فشردن شده یک کلید خاص (یا ترکیبی از کلیدهای کنترلی با سایر کلیدها) همواره باید یک کد یکسان برگشت داده شود.
پس یک charCode کد یونیکد کاراکتر کلید فشرده شده است که تنها در رویداد keypress در دسترس است. یک keyCode کد خود کلید فشرده شده یا همان scan code است که در رویدادهای keydown و keyup در دسترس است.
نکته: برای تمام کلیدهای الفبایی-عددی scan code با char code یکی است. مثلا برای حروف الفبایی scan code یک کلید با کد ASCII حرف انگلیسی بزرگ آن کلید برابر است.
بنابراین برای بررسی کلید ترکیبی ctrl + a میتوان از کد زیر در رویداد keydown استفاده کرد:
e.ctrlKey && e.keyCode == 'A'.charCodeAt(0)
و فرقی نمیکند که کاراکتر حاصله 'a' یا 'A' یا 'ش' باشد. 
نکته: برای تمام کلیدهای کیبرد به غیر از ';' و '=' و '-' تمام مرورگرها از کدهای یکسانی استفاده میکنند. میتوانید این کلیدها را با استفاده از قطعه کد اول این مطلب در مرورگرهای مختلف آزمایش کنید.
نکته: با برگشت مقدار false در رویداد keydown یا keypress رفتار پیشفرض کلید یا ترکیب کلیدهای مربوطه غیرفعال میشود. البته به غیر از عملیاتهای سطح سیستم عامل (مثل alt+F4).
چند مثال دیگر در ادامه ...

جابجایی تصویر
کدهای زیر را در یک فایل html ذخیره کرده و توسط یک مرورگر فایل حاصله را باز کنید.
<html>
<body>
  <div id="dotnettips" style="width: 35px; height: 35px; background-image: url(https://www.dntips.ir/favicon.ico);
    position: absolute; left: 10px; top: 10px;" tabindex="0">
  </div>
  <script>
    document.getElementById('dotnettips').onkeydown = function (e) {
      e = e || event;
      switch (e.keyCode) {
        case 37: // left
          this.style.left = parseInt(this.style.left) - this.offsetWidth + 'px';
          return false;
        case 38: // up
          this.style.top = parseInt(this.style.top) - this.offsetHeight + 'px';
          return false;
        case 39: // right
          this.style.left = parseInt(this.style.left) + this.offsetWidth + 'px';
          return false;
        case 40: // down
          this.style.top = parseInt(this.style.top) + this.offsetHeight + 'px';
          return false;
      }
    }
  </script>
</body>
</html>
بر روی تصویر کلید کرده (یا با استفاده از Tab فوکس را روی تصویر قرار دهید) و با استفاده از کلیدهای مکان نما (Arrow keys) سعی کنید تصویر را در صفحه جابجا کنید.
در کد فوق برای جلوگیری از رفتار پیش فرض کلیدهای مکان نما (جابجایی محتوای صفحه در صورت وجود اسکرول) مقدار false برگشت داده شده است.
نکته: استفاده از خاصیت tabindex برای امکان فوکس بر روی div اجباری است.
نکته: استفاده از event به جای window.event تا زمانی که یک متغیر در اسکوپ جاری با نام event وجود نداشته باشد مشکلی ایجاد نمیکند.

Caps Lock
متاسفانه راه مستقیمی برای دریافت وضعیت کلید caps lock در جاوا اسکریپت وجود ندارد. ظاهرا تنها راه حل موجود برای این مسئله بررسی کد کاراکتر کلید فشرده شده در رویداد keypress و بررسی آن با توجه به وضعیت پراپرتی shift این رویداد است. بدین ترتیب که اگر کد کاراکتر مربوط به حروف بزرگ بوده درحالیکه کلید شیفت نگه داشته نشده است بنابراین کلید Caps Lock روشن است و بالعکس. البته با ترکیب این روش و نیز رصد scan code کلید Caps Lock (کد آن برابر 20 است) در رویداد keydown میتوان وضعیت بهتری پدید آورد. قطعه کد زیر برای اینکار است. در این کد از یک متغیر گلوبال برای نگهداری وضعیت دکمه caps lock استفاده میشود.
<html>
<body>
  <input id="inputText" type="text" />
  <div id="divCapsLock" style="color: Red; display: none;">Caps Lock is ON!</div>
  <script>
    var capsLock = null;
    document.onkeypress = keyPress;
    document.onkeydown = keyDown;
    function keyDown(e) {
      e = e || event;
      capsLock = (e.keyCode == 20 && capsLock !== null) ? !capsLock : capsLock;
      document.getElementById('divCapsLock').style.display = capsLock ? 'block' : 'none';
    }
    function keyPress(e) {
      if (capsLock != null) return;
      e = e || window.event;
      var charCode = e.charCode || e.keyCode;
      capsLock = (charCode >= 97 && charCode <= 122 && e.shiftKey) || (charCode >= 65 && charCode <= 90 && !e.shiftKey);
      document.getElementById('divCapsLock').style.display = capsLock ? 'block' : 'none';
    }
  </script>
</body>
</html>
در متد رویداد kepress تکست باکس ابتدا بررسی میشود که آیا قبلا متغیر گلوبال capsLock مقداری غیرنال دارد. اگر مقدار غیرنال داشته باشد دیگر نیازی نیست تا کد کاراکتر کلید بررسی شود زیرا وضعیت جاری کلید معلوم است. بنابراین در ادامه کار صرفه جویی میشود. دلیل استفاده از رویدادهای سطح docment به جای خود تکست باکس این است تا از کوچکترین فرصتها! برای تعیین وضعیت جاری کلید caps lock استفاده شود. یعنی بتوان پس از فشرده شدن اولین کلید کاراکتری بدون توجه به موقعیت فوکس در صفحه، این وضعیت را از حالت نامشخص خارج کرد.
نکته: در سلسله مراتب رویدادهای اجزای یک document همیشه ابتدا رویدادهای مربوط به عناصر فرزند فراخوانده میشود و رویدادهای مربوط به عناصر document و window در پایان صدا زده میشوند. برای آزمودن این مورد قطعه کد زیر را در مرورگرهای مختلف امتحان کنید:
<html>
<body>
  <div id='divInput'>
    <input id="inputText" type="text" />
  </div>
  <script>
    document.getElementById('inputText').onkeydown = inputKeyDown;
    document.getElementById('divInput').onkeydown = divKeyDown;
    document.onkeydown = documentKeyDown;
    window.onkeydown = windowKeyDown;
    function divKeyDown(e) {
      console.log('divKeyDown');
    }
    function inputKeyDown(e) {
      console.log('inputKeyDown');
    }
    function documentKeyDown(e) {
      console.log('documentKeyDown');
    }
    function windowKeyDown(e) {
      console.log('windowKeyDown');
    }
  </script>
</body>
</html>
نکته: اگر از تگ doctype استفاده نشود (همانطور که در ابتدای این مطلب اشاره شد)، در IE رویدادهای عنصر window فراخوانی نمیشوند.
درهرصورت برای مشخص کردن وضعیت کلید caps lock نیاز است تا کاربر ابتدا کلیدی را بفشارد و در حال حاضر روش و راه حل دیگری وجود ندارد! درضمن اگر صفحه کلیدی غیر از انگلیسی استفاده شود روش فوق جواب نخواهد داد و باید بررسی‌های بیشتری انجام شود. البته در مورد زیانهایی چون فارسی که روشن یا خاموش بودن caps lock تاثیری در کاراکتر حاصله ندارد کاری نمیتوان کرد و نمیتوان از وضعیت caps lock باخبر شد! هرچند در این موارد معمولا وضعیت این دکمه مهم نیست زیرا بیشترین کاربرد این گونه هشدارها در ورودی‌های رمز عبور است در صورتیکه زبان صفحه کلید انگلیسی (یا مشابه آن) باشد. برای اینکار کد زیر به عنوان راه حل بهتر پیشنهاد میشود:
<html>
<body>
  <input id="inputText" type="text" />
  <div id="divCapsLock" style="color: Red; display: none;">Caps Lock is ON!</div>
  <script>
    var capsLock = null;
    var hasFocus = false;
    document.onkeyup = keyUp;
    document.onkeypress = keyPress;
    document.getElementById('inputText').onfocus = focus;
    document.getElementById('inputText').onblur = focus;
    function warnCapsLock() {
      document.getElementById('divCapsLock').style.display = (capsLock != null && capsLock && hasFocus) ? 'block' : 'none';
    }
    function focus() {
      hasFocus = !hasFocus;
      warnCapsLock();
    }
    function keyUp(e) {
      e = e || event;
      capsLock = (e.keyCode == 20 && capsLock !== null) ? !capsLock : capsLock;
      warnCapsLock();
    }
    function keyPress(e) {
      if (capsLock != null) return;
      e = e || window.event;
      var charCode = e.charCode || e.keyCode;
      capsLock = (charCode >= 97 && charCode <= 122 && e.shiftKey) || (charCode >= 65 && charCode <= 90 && !e.shiftKey);
      warnCapsLock();
    }
  </script>
</body>
</html>
اگر دقت کنین میبیند که رویداد keydown در این کد نهایی با keyup جایگزین شده است. درصورت استفاده از رویداد keypress یک مشکل کوچک بوجود می‌آید و آن این است که اگر کاربر کلید caps lock را فشرده و سپس آن را در حالت فشرده نگه دارد اتفاق بدی خواهد افتاد! در این حال چون رویداد keydown مرتبا فراخوانده میشود وضعیت متغیر capsLock در این کد کاملا نامعتبر خواهد بود. بنابراین بهتر است تا از رویداد keyup که تنها یکبار فراخوانده میشود استفاده شود.
نکته: اگر کاربر پس از مشخص شدن وضعیت کلید Capa Lock در این کد، فوکس را به پنجره دیگری تغییر داده و سپس وضعیت این دکمه در آنجا تغییر دهد این کد دیگر درست کار نخواهد کرد. برای حل این مشکل هم راه حل زیر وجود دارد:
window.onblur = function () { capsLock = null; }
با این کار وضعیت متغیر capsLock ریست میشود.
نکته: در آزمایش این کد دقت کنید که زبان کیبرد حتما انگلیسی باشد.

مدیریت کلیدها
برای مدیریت کلیدهای کیبرد راههای متنوعی وجود دارد. مثلا میتوان از قطعه کد زیر استفاده کرد:
var Client = {};
Client.Keyboard = {};
Client.Keyboard.EnableKeyDown = true;
Client.Keyboard.EnableKeyUp = false;
Client.Keyboard.CurrentKeyEvent = null;

window.onkeydown = function (event) {
  if (!Client.Keyboard.EnableKeyDown) return true;
  return KeyboardEvents(event);
};

window.onkeyup = function (event) {
  if (!Client.Keyboard.EnableKeyUp) return true;
  return KeyboardEvents(event);
};

function Rise(event) {
  var e = Client.Keyboard.CurrentKeyEvent;
  if (event) {
    var data = { shift: e.shiftKey, ctrl: e.ctrlKey, alt: e.altKey };
    if (!event(data)) return false;
    return event(data);
  }
  return true;
}

function KeyboardEvents(e) {
  e = e || window.event;
  Client.Keyboard.CurrentKeyEvent = e;
  switch (e.keyCode) {
    case 8:
      return Rise(Client.Keyboard.Backspace);
    case 9:
      return Rise(Client.Keyboard.Tab);
    case 13:
      return Rise(Client.Keyboard.Enter);
    case 16:
      return Rise(Client.Keyboard.Shift);
    case 17:
      return Rise(Client.Keyboard.Ctrl);
    case 18:
      return Rise(Client.Keyboard.Alt);
    case 19:
      return Rise(Client.Keyboard.Pause);
    case 20:
      return Rise(Client.Keyboard.CapsLock);
    case 27:
      return Rise(Client.Keyboard.Esc);
    case 33:
      return Rise(Client.Keyboard.PageUp);
    case 34:
      return Rise(Client.Keyboard.PageDown);
    case 35:
      return Rise(Client.Keyboard.End);
    case 36:
      return Rise(Client.Keyboard.Home);
    case 37:
      return Rise(Client.Keyboard.Left);
    case 38:
      return Rise(Client.Keyboard.Up);
    case 39:
      return Rise(Client.Keyboard.Right);
    case 40:
      return Rise(Client.Keyboard.Down);
    case 44:
      return Rise(Client.Keyboard.PrtScr);
    case 45:
      return Rise(Client.Keyboard.Insert);
    case 46:
      return Rise(Client.Keyboard.Delete);
      //////////////////////////////////////////////////////////////////////////////////////////////////
    case 48:
      return Rise(Client.Keyboard.Num0);
    case 49:
      return Rise(Client.Keyboard.Num1);
    case 50:
      return Rise(Client.Keyboard.Num2);
    case 51:
      return Rise(Client.Keyboard.Num3);
    case 52:
      return Rise(Client.Keyboard.Num4);
    case 53:
      return Rise(Client.Keyboard.Num5);
    case 54:
      return Rise(Client.Keyboard.Num6);
    case 55:
      return Rise(Client.Keyboard.Num7);
    case 56:
      return Rise(Client.Keyboard.Num8);
    case 57:
      return Rise(Client.Keyboard.Num9);
      //////////////////////////////////////////////////////////////////////////////////////////////////
    case 65:
      return Rise(Client.Keyboard.A);
    case 66:
      return Rise(Client.Keyboard.B);
    case 67:
      return Rise(Client.Keyboard.C);
    case 68:
      return Rise(Client.Keyboard.D);
    case 69:
      return Rise(Client.Keyboard.E);
    case 70:
      return Rise(Client.Keyboard.F);
    case 71:
      return Rise(Client.Keyboard.G);
    case 72:
      return Rise(Client.Keyboard.H);
    case 73:
      return Rise(Client.Keyboard.I);
    case 74:
      return Rise(Client.Keyboard.J);
    case 75:
      return Rise(Client.Keyboard.K);
    case 76:
      return Rise(Client.Keyboard.L);
    case 77:
      return Rise(Client.Keyboard.M);
    case 78:
      return Rise(Client.Keyboard.N);
    case 79:
      return Rise(Client.Keyboard.O);
    case 80:
      return Rise(Client.Keyboard.P);
    case 81:
      return Rise(Client.Keyboard.Q);
    case 82:
      return Rise(Client.Keyboard.R);
    case 83:
      return Rise(Client.Keyboard.S);
    case 84:
      return Rise(Client.Keyboard.T);
    case 85:
      return Rise(Client.Keyboard.U);
    case 86:
      return Rise(Client.Keyboard.V);
    case 87:
      return Rise(Client.Keyboard.W);
    case 88:
      return Rise(Client.Keyboard.X);
    case 89:
      return Rise(Client.Keyboard.Y);
    case 90:
      return Rise(Client.Keyboard.Z);
      ////////////////////////////////////////////////////////////////////////////
    case 91:
      //case 219: // opera
      return Rise(Client.Keyboard.LeftWindow);
    case 92:
      return Rise(Client.Keyboard.RightWindow);
    case 93:
      return Rise(Client.Keyboard.ContextMenu);
      ////////////////////////////////////////////////////////////////////////////
    case 96:
      return Rise(Client.Keyboard.NumPad0);
    case 97:
      return Rise(Client.Keyboard.NumPad1);
    case 98:
      return Rise(Client.Keyboard.NumPad2);
    case 99:
      return Rise(Client.Keyboard.NumPad3);
    case 100:
      return Rise(Client.Keyboard.NumPad4);
    case 101:
      return Rise(Client.Keyboard.NumPad5);
    case 102:
      return Rise(Client.Keyboard.NumPad6);
    case 103:
      return Rise(Client.Keyboard.NumPad7);
    case 104:
      return Rise(Client.Keyboard.NumPad8);
    case 105:
      return Rise(Client.Keyboard.NumPad9);
      ////////////////////////////////////////////////////////////////////////////
    case 106:
      return Rise(Client.Keyboard.Multiply);
    case 107:
      return Rise(Client.Keyboard.Add);
    case 109:
      return Rise(Client.Keyboard.Subtract);
    case 110:
      return Rise(Client.Keyboard.DecimalPoint);
    case 111:
      return Rise(Client.Keyboard.Divide);
      ////////////////////////////////////////////////////////////////////////////
    case 112:
      return Rise(Client.Keyboard.F1);
    case 113:
      return Rise(Client.Keyboard.F2);
    case 114:
      return Rise(Client.Keyboard.F3);
    case 115:
      return Rise(Client.Keyboard.F4);
    case 116:
      return Rise(Client.Keyboard.F5);
    case 117:
      return Rise(Client.Keyboard.F6);
    case 118:
      return Rise(Client.Keyboard.F7);
    case 119:
      return Rise(Client.Keyboard.F8);
    case 120:
      return Rise(Client.Keyboard.F9);
    case 121:
      return Rise(Client.Keyboard.F10);
    case 122:
      return Rise(Client.Keyboard.F11);
    case 123:
      return Rise(Client.Keyboard.F12);
      ////////////////////////////////////////////////////////////////////////////
    case 144:
      return Rise(Client.Keyboard.NumLock);
    case 145:
      return Rise(Client.Keyboard.ScrollLock);
    case 186:
    case 59: // opera & firefox
      return Rise(Client.Keyboard.SemiColon);
    case 187:
    case 61: // opera
      //case 107: //firefox
      return Rise(Client.Keyboard.Equal);
    case 188:
      return Rise(Client.Keyboard.Camma);
    case 189:
      return Rise(Client.Keyboard.Dash);
    case 190:
      return Rise(Client.Keyboard.Period);
    case 191:
      return Rise(Client.Keyboard.Slash);
    case 192:
      return Rise(Client.Keyboard.GraveAccent);
    case 219:
      return Rise(Client.Keyboard.OpenBracket);
    case 220:
      return Rise(Client.Keyboard.BackSlash);
    case 221:
      return Rise(Client.Keyboard.CloseBracket);
    case 222:
      return Rise(Client.Keyboard.SingleQuote);
  }
};
نحوه استفاده از این کد به صورت زیر است:
  // ctrl + s
  Client.Keyboard.S = function (e) {
    if (e.ctrl) {
      // انجام عملیات موردنظر
    }
    return true;
  }
اگر در متد الصاق شده به پراپرتیهای Client.Keyboard هیچ مقداری برگشت داده نشود، باتوجه به کد موجود در متد Rise، عملیات پیشفرض کلید مربوطه در پنجره مرورگر غیرفعال خواهد شد. بنابراین در کد بالا بعد از شرط، مقدار true برگشت داده شده است.
هرچند سعی کردم که تفاوت میان مرورگرهای مختلف را درنظر بگیرم ولی احتمال اینکه برخی از کدها به دلیل تفاوت میان مرورگرهای مختلف در کد بالا آورده نشده باشد وجود دارد!
نکته: کد فوق بیشتر برای روشن‌تر شدن موضوع ارائه شده است چون راه حل‌های بهتری هم برای مدیریت کلیدهای کیبرد وجود دارد. مثل استفاده از کتابخانه های Mousetrap یا HotKey یا jQuery Hotkeys یا KeyboardJS. البته در این میان بهترین کتابخانه موجود به نظر من همان Mousetrap است که تنها کتابخانه موجود است که علاوه بر پشتیبانی از ترکیب کلیدها با کلیدهای کنترلی، از توالی کلیدها (keys sequence) نیز پشتیبانی میکند که کار جالبی است.
در پایان این نکته را یادآور میشوم که امروزه توسعه اپلیکیشنهای تحت وب بدون استفاده مناسب از امکانات سمت کلاینت طرفداری ندارد. پس بهتر است هرچه بیشتر در مورد این زبان مرموز و پر از رمز و راز (JavaScript) بدانیم.
مطالب
بررسی Source Generators در #C - قسمت دوم - یک مثال
یک مثال: پیاده سازی INotifyPropertyChanged توسط Source Generators

هدف از اینترفیس INotifyPropertyChanged که به همراه یک رخ‌داد است:
public interface INotifyPropertyChanged  
{ 
   event PropertyChangedEventHandler PropertyChanged;  
}
مطلع سازی استفاده کننده‌ی از یک شیء، از تغییرات رخ‌داده‌ی در مقادیر خواص آن است که نمونه‌ی آن، در برنامه‌های WPF، جهت به روز رسانی UI، زیاد مورد استفاده قرار می‌گیرد. البته این رخ‌داد به خودی خود کار خاصی را انجام نمی‌دهد و برای استفاده‌ی از آن، باید مقدار زیادی کد نوشت و این مقدار کد نیز باید به ازای تک تک خواص یک کلاس مدل، تکرار شوند:
  partial class CarModel : INotifyPropertyChanged
  {

    private double _speedKmPerHour;
    
    public double SpeedKmPerHour
    {
      get => _speedKmPerHour;
      set
      {
        _speedKmPerHour = value;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SpeedKmPerHour)));
      }
    }

    public event PropertyChangedEventHandler? PropertyChanged;
  }
همچنین باید درنظر داشت که با تغییر نام خاصیتی، میزان قابل ملاحظه‌ای از این کدهای تکراری نیز باید به روز رسانی شوند که این عملیات می‌تواند ایده‌ی خوبی برای استفاده‌ی از Source Generators باشد.
اگر بخواهیم تولید این کدهای تکراری را به Source Generators محول کنیم، می‌توان برای مثال فیلد خصوصی مرتبط را نگه داشت و تولید مابقی کدها را خودکار کرد:
  partial class CarModel : INotifyPropertyChanged
  {
    private double _speedKmPerHour;    
  }
در این حالت کلاس مدل، به صورت partial تعریف می‌شود و فقط فیلد خصوصی، در کدهای ما حضور خواهد داشت. مابقی کدهای این کلاس partial به صورت خودکار توسط یک Source Generator سفارشی تولید خواهد شد. همانطور که ملاحظه می‌کنید، کاهش حجم قابل ملاحظه‌ای حاصل شده و همچنین اگر فیلد خصوصی دیگری نیز در اینجا اضافه شود، واکنش Source Generator ما آنی خواهد بود و بلافاصله کدهای مرتبط را تولید می‌کند و برنامه، بدون مشکلی کامپایل خواهد شد؛ هرچند به ظاهر INotifyPropertyChanged ذکر شده، در این کلاس اصلا پیاده سازی نشده‌است.


ایجاد پروژه‌ی Source Generator

در ابتدا برای ایجاد تولید کننده‌ی خودکار کدهای INotifyPropertyChanged، یک class library را به solution جاری اضافه می‌کنیم. سپس نیاز است ارجاعاتی را به دو بسته‌ی نیوگت زیر نیز افزود:
<Project Sdk="Microsoft.NET.Sdk">

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" PrivateAssets="all" />
  </ItemGroup>
</Project>
سپس کلاس جدید NotifyPropertyChangedGenerator را به نحو زیر به آن اضافه می‌کنیم:
  [Generator]
  public class NotifyPropertyChangedGenerator : ISourceGenerator
  {
    public void Initialize(GeneratorInitializationContext context)
    {
    }

    public void Execute(GeneratorExecutionContext context)
    {
- این کلاس باید اینترفیس ISourceGenerator را پیاده سازی کرده و همچنین مزین به ویژگی Generator باشد.
- اینترفیس ISourceGenerator به همراه دو متد Initialize و Execute است که در صورت نیاز باید پیاده سازی شوند.

در متد Execute، به خاصیت context.Compilation دسترسی داریم. این خاصیت تمام اطلاعاتی را که کامپایلر از Solution جاری در اختیار دارد، به توسعه دهنده ارائه می‌دهد. برای نمونه پیاده سازی متد Execute تولید کننده‌ی کد مثال جاری، چنین شکلی را دارد:
    public void Execute(GeneratorExecutionContext context)
    {
      // uncomment to debug the actual build of the target project
      // Debugger.Launch();
      var compilation = context.Compilation;
      var notifyInterface = compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged");

      foreach (var syntaxTree in compilation.SyntaxTrees)
      {
        var semanticModel = compilation.GetSemanticModel(syntaxTree);
        var immutableHashSet = syntaxTree.GetRoot()
          .DescendantNodesAndSelf()
          .OfType<ClassDeclarationSyntax>()
          .Select(x => semanticModel.GetDeclaredSymbol(x))
          .OfType<ITypeSymbol>()
          .Where(x => x.Interfaces.Contains(notifyInterface))
          .ToImmutableHashSet();

        foreach (var typeSymbol in immutableHashSet)
        {
          var source = GeneratePropertyChanged(typeSymbol);
          context.AddSource($"{typeSymbol.Name}.Notify.cs", source);
        }
      }
    }
در اینجا با استفاده از context.Compilation به اطلاعات کامپایلر دسترسی پیدا کرده و سپس SyntaxTrees آن‌را یکی یکی، جهت یافتن کلاس‌ها و یا همان ClassDeclarationSyntax ها، پیمایش و بررسی می‌کنیم. سپس از بین این کلاس‌ها، کلاس‌هایی که INotifyPropertyChanged را پیاده سازی کرده باشند، انتخاب می‌کنیم که اطلاعات آن در پایان کار، به متد GeneratePropertyChanged جهت تولید مابقی کدهای partial class ارسال شده و کد تولیدی، به context اضافه می‌شود تا به نحو متداولی همانند سایر کدهای برنامه، به مجموعه کدهای مورد بررسی کامپایلر اضافه شود.

نکته‌ی مهم و جالب در اینجا این است که نیازی نیست تا قطعه کد جدید را به صورت SyntaxTrees در آورد و به کامپایلر اضافه کرد. می‌توان این قطعه کد را به نحو متداولی، به صورت یک قطعه رشته‌ی استاندارد #C، تولید و به کامپایلر با متد context.AddSource ارائه کرد که نمونه‌ای از آن‌را در ذیل مشاهده می‌کنید:
    private string GeneratePropertyChanged(ITypeSymbol typeSymbol)
    {
      return $@"
using System.ComponentModel;

namespace {typeSymbol.ContainingNamespace}
{{
  partial class {typeSymbol.Name}
  {{
    {GenerateProperties(typeSymbol)}
    public event PropertyChangedEventHandler? PropertyChanged;
  }}
}}";
    }

    private static string GenerateProperties(ITypeSymbol typeSymbol)
    {
      var sb = new StringBuilder();
      var suffix = "BackingField";

      foreach (var fieldSymbol in typeSymbol.GetMembers().OfType<IFieldSymbol>()
        .Where(x=>x.Name.EndsWith(suffix)))
      {
        var propertyName = fieldSymbol.Name[..^suffix.Length];
        sb.AppendLine($@"
    public {fieldSymbol.Type} {propertyName}
    {{
      get => {fieldSymbol.Name};
      set
      {{
        {fieldSymbol.Name} = value;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof({propertyName})));
      }}
    }}");
      }

      return sb.ToString();
    }
در اینجا در ابتدا بدنه‌ی کلاس partial تکمیل می‌شود. سپس خواص عمومی آن بر اساس فیلدهای خصوصی تعریف شده، تکمیل می‌شوند. در این مثال اگر یک فیلد خصوصی به عبارت BackingField ختم شود، به عنوان فیلدی که قرار است معادل کدهای INotifyPropertyChanged را داشته باشد، شناسایی می‌شود و به همراه کدهای تولید شده‌ی خودکار خواهد بود.

کدهای source generator ما همین مقدار بیش‌تر نیست. اکنون می‌خواهیم از آن در یک برنامه‌ی کنسول جدید (برای مثال به نام NotifyPropertyChangedGenerator.Demo) استفاده کنیم. برای اینکار نیاز است ارجاعی را به آن اضافه کنیم؛ اما این ارجاع، یک ارجاع متداول نیست و نیاز به ذکر چنین ویژگی خاصی وجود دارد:
<Project Sdk="Microsoft.NET.Sdk">

  <ItemGroup>
    <ProjectReference Include="..\NotifyPropertyChangedGenerator\NotifyPropertyChangedGenerator.csproj"
                      OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
  </ItemGroup>
</Project>
در اینجا میسر دهی پروژه‌ی تولید کننده‌ی کد، همانند سایر پروژه‌ها است؛ اما نوع آن باید آنالایزر معرفی شود. به همین جهت از خاصیت OutputItemType با مقدار Analyzer استفاده شده‌است. همچنین تنظیم ReferenceOutputAssembly به false به این معنا است که این اسمبلی ویژه، یک وابستگی و dependency واقعی پروژه‌ی جاری نیست و ما قرار نیست به صورت مستقیمی از کدهای آن استفاده کنیم.

برای آزمایش این تولید کننده‌ی کد، کلاس CarModel را به صورت زیر به پروژه‌ی کنسول آزمایشی اضافه می‌کنیم:
using System.ComponentModel;

namespace NotifyPropertyChangedGenerator.Demo
{
  public partial class CarModel : INotifyPropertyChanged
  {
    private double SpeedKmPerHourBackingField;
    private int NumberOfDoorsBackingField;
    private string ModelBackingField = "";

    public void SpeedUp() => SpeedKmPerHour *= 1.1;
  }
}
این کلاس پیاده سازی کننده‌ی INotifyPropertyChanged است؛ اما به همراه هیچ خاصیت عمومی نیست. فقط به همراه یکسری فیلد خصوصی ختم شده‌ی به «BackingField» است که توسط تولید کننده‌ی کد شناسایی شده و اطلاعات آن‌ها تکمیل می‌شود. فقط باید دقت داشت که این کلاس حتما باید به صورت partial تعریف شود تا امکان تکمیل خودکار کدهای آن وجود داشته باشد.

یک نکته:   در این حالت هرچند برنامه بدون مشکل کامپایل و اجرا می‌شود، ممکن است خطوط قرمزی را در IDE خود مشاهده کنید که عنوان می‌کند این قطعه از کد قابل کامپایل نیست. اگر با چنین صحنه‌ای مواجه شدید، یکبار solution را بسته و مجددا باز کنید تا تولید کننده‌ی کد، به خوبی شناسایی شود. البته نگارش‌های جدیدتر Visual Studio و Rider به همراه قابلیت auto reload پروژه برای کار با تولید کننده‌‌های کد هستند و دیگر شاهد چنین صحنه‌هایی نیستیم و حتی اگر برای مثال فیلد جدیدی را به CarModel اضافه کنیم، نه فقط بلافاصله کدهای متناظر آن تولید می‌شوند، بلکه خواص عمومی تولید شده در Intellisense نیز قابل دسترسی هستند.


نحوه‌ی مشاهده‌ی کدهای خودکار تولید شده

اگر علاقمند باشید تا کدهای خودکار تولید شده را مشاهده کنید، در Visual Studio، در قسمت و درخت نمایشی dependencies پروژه، گره‌ای به نام Analyzers وجود دارد که در آن برای مثال نام NotifyPropertyChangedGenerator و ذیل آن، کلاس‌های تولید شده‌ی توسط آن، قابل مشاهده و دسترسی هستند و حتی قابل دیباگ نیز می‌باشند؛ یعنی می‌توان بر روی سطور مختلف آن، break-point قرار داد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: SourceGeneratorTests.zip

معرفی تعدادی منبع تکمیلی
- برنامه Source generator playground
در اینجا تعدادی مثال را که توسط مایکروسافت توسعه یافته‌است، مشاهده می‌کنید که اتفاقا یکی از آن‌ها پیاده سازی تولید کننده‌ی کد اینترفیس INotifyPropertyChanged است. در این برنامه، خروجی کدهای تولیدی نیز به سادگی قابل مشاهده‌است.

- برنامه SharpLab
برای توسعه‌ی تولید کننده‌های کد، عموما نیاز است تا با Roslyn API آشنا بود. در این برنامه اگر از منوی بالای صفحه قسمت results، گزینه‌ی «syntax tree» را انتخاب کنید و سپس قسمتی از کد خود را انتخاب کنید، بلافاصله معادل Roslyn API آن، در سمت راست صفحه نمایش داده می‌شود.

- معرفی مجموعه‌ای از Source Generators
در اینجا می‌توان مجموعه‌ای از پروژه‌های سورس باز Source Generators را مشاهده و کدهای آن‌ها را مطالعه کنید و یا از آن‌ها در پروژه‌های خود استفاده نمائید.

- معرفی یک cookbook در مورد Source Generators
این cookbook توسط خود مایکروسافت تهیه شده‌است و جهت شروع به کار با این فناوری، بسیار مفید است.

- مجموعه مثال‌های Source generators از مایکروسافت
در اینجا می‌توانید مجموعه مثال‌هایی از Source generators را که توسط مایکروسافت تهیه شده‌است، مشاهده کنید. شرح و توضیحات تعدادی از آن‌ها را هم در اینجا مطالعه کنید.
نظرات مطالب
EF Code First #5
ضمن تشکر از مطالبتون 
آیا میشه این مراحلی رو که گفتید رو خود برنامه انجام بده و کاربر نهایی از اون اطلاعی نداشته باشه ؟
مثلا من یه برنامه دادم تحویل کاربر و دیتابیس اون هم حاوی اطلاعات هست حالا من به این نتیجه رسیدم که تغییری در دیتابیس بدم. طبق گفته شما من باید یه اسکریپت از تغییرات خودم درست کنم و تحویل کاربر بدم تا اونو ایجاد کنه اما من دنبال راهی می‌گردم که در برنامه وقتی کلاس‌ها رو تغییر دادم برنامه‌ی جدید رو تحویل کاربر بدم و کاربر بیاد برنامه قدیمشو حذف و برنامه‌ی جدید و نصب کنه و وقتی که برنامه‌ی جدید برای اولین بار اجرا شد تغییرات رو در دیتابیس قدیمی اعمال کنه بدون اینکه کاربر از پشت پرده اطلاعی داشته باشته
مطالب
آشنایی با NHibernate - قسمت هشتم

معرفی الگوی Repository

روش متداول کار با فناوری‌های مختلف دسترسی به داده‌ها عموما بدین شکل است:
الف) یافتن رشته اتصالی رمزنگاری شده به دیتابیس از یک فایل کانفیگ (در یک برنامه اصولی البته!)
ب) باز کردن یک اتصال به دیتابیس
ج) ایجاد اشیاء Command برای انجام عملیات مورد نظر
د) اجرا و فراخوانی اشیاء مراحل قبل
ه) بستن اتصال به دیتابیس و آزاد سازی اشیاء

اگر در برنامه‌های یک تازه کار به هر محلی از برنامه او دقت کنید این 5 مرحله را می‌توانید مشاهده کنید. همه جا! قسمت ثبت، قسمت جستجو، قسمت نمایش و ...
مشکلات این روش:
1- حجم کارهای تکراری انجام شده بالا است. اگر قسمتی از فناوری دسترسی به داده‌ها را به اشتباه درک کرده باشد، پس از مطالعه بیشتر و مشخص شدن نحوه‌ی رفع مشکل، قسمت عمده‌ای از برنامه را باید اصلاح کند (زیرا کدهای تکراری همه جای آن پراکنده‌اند).
2- برنامه نویس هر بار باید این مراحل را به درستی انجام دهد. اگر در یک برنامه بزرگ تنها قسمت آخر در یکی از مراحل کاری فراموش شود دیر یا زود برنامه تحت فشار کاری بالا از کار خواهد افتاد (و متاسفانه این مساله بسیار شایع است).
3- برنامه منحصرا برای یک نوع دیتابیس خاص تهیه خواهد شد و تغییر این رویه جهت استفاده از دیتابیسی دیگر (مثلا کوچ برنامه از اکسس به اس کیوال سرور)، نیازمند بازنویسی کل برنامه می‌باشد.
و ...

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

این روزها تشکیل این لایه دسترسی به داده‌ها (data access layer یا DAL) نیز مزموم است! و دلایل آن در مباحث چرا به یک ORM نیازمندیم برشمرده شده است. جهت کار با ORM ها نیز نیازمند یک لایه دیگر می‌باشیم تا یک سری اعمال متداول با آن‌هارا کپسوله کرده و از حجم کارهای تکراری خود بکاهیم. برای این منظور قبل از اینکه دست به اختراع بزنیم، بهتر است به الگوهای طراحی برنامه نویسی شیء گرا رجوع کرد و از رهنمودهای آن استفاده نمود.

الگوی Repository یکی از الگوهای برنامه‌ نویسی با مقیاس سازمانی است. با کمک این الگو لایه‌ای بر روی لایه نگاشت اشیاء برنامه به دیتابیس تشکیل شده و عملا برنامه را مستقل از نوع ORM مورد استفاه می‌کند. به این صورت هم از تشکیل یک سری کدهای تکراری در سطح برنامه جلوگیری شده و هم از وابستگی بین مدل برنامه و لایه دسترسی به داده‌ها (که در اینجا همان NHibernate می‌باشد) جلوگیری می‌شود. الگوی Repository (مخزن)، کار ثبت،‌ حذف، جستجو و به روز رسانی داده‌ها را با ترجمه آن‌ها به روش‌های بومی مورد استفاده توسط ORM‌ مورد نظر، کپسوله می‌کند. به این شکل شما می‌توانید یک الگوی مخزن عمومی را برای کارهای خود تهیه کرده و به سادگی از یک ORM به ORM دیگر کوچ کنید؛ زیرا کدهای برنامه شما به هیچ ORM خاصی گره نخورده و این عملیات بومی کار با ORM توسط لایه‌ای که توسط الگوی مخزن تشکیل شده، صورت گرفته است.

طراحی کلاس مخزن باید شرایط زیر را برآورده سازد:
الف) باید یک طراحی عمومی داشته باشد و بتواند در پروژه‌های متعددی مورد استفاده مجدد قرار گیرد.
ب) باید با سیستمی از نوع اول طراحی و کد نویسی و بعد کار با دیتابیس، سازگاری داشته باشد.
ج) باید امکان انجام آزمایشات واحد را سهولت بخشد.
د) باید وابستگی کلاس‌های دومین برنامه را به زیر ساخت ORM مورد استفاده قطع کند (اگر سال بعد به این نتیجه رسیدید که ORM ایی به نام XYZ برای کار شما بهتر است، فقط پیاده سازی این کلاس باید تغییر کند و نه کل برنامه).
ه) باید استفاده از کوئری‌هایی از نوع strongly typed را ترویج کند (مثل کوئری‌هایی از نوع LINQ).


بررسی مدل برنامه

مدل این قسمت (برنامه NHSample4 از نوع کنسول با همان ارجاعات متداول ذکر شده در قسمت‌های قبل)، از نوع many-to-many می‌باشد. در اینجا یک واحد درسی توسط چندین دانشجو می‌تواند اخذ شود یا یک دانشجو می‌تواند چندین واحد درسی را اخذ نماید که برای نمونه کلاس دیاگرام و کلاس‌های متشکل آن به شکل زیر خواهند بود:



using System.Collections.Generic;

namespace NHSample4.Domain
{
public class Course
{
public virtual int Id { get; set; }
public virtual string Teacher { get; set; }
public virtual IList<Student> Students { get; set; }

public Course()
{
Students = new List<Student>();
}
}
}


using System.Collections.Generic;

namespace NHSample4.Domain
{
public class Student
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual IList<Course> Courses { get; set; }

public Student()
{
Courses = new List<Course>();
}
}
}

کلاس کانفیگ برنامه جهت ایجاد نگاشت‌ها و سپس ساخت دیتابیس متناظر

using FluentNHibernate.Automapping;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate.Tool.hbm2ddl;

namespace NHSessionManager
{
public class Config
{
public static FluentConfiguration GetConfig()
{
return
Fluently.Configure()
.Database(
MsSqlConfiguration
.MsSql2008
.ConnectionString(x => x.FromConnectionStringWithKey("DbConnectionString"))
)
.Mappings(
m => m.AutoMappings.Add(
new AutoPersistenceModel()
.Where(x => x.Namespace.EndsWith("Domain"))
.AddEntityAssembly(typeof(NHSample4.Domain.Course).Assembly))
.ExportTo(System.Environment.CurrentDirectory)
);
}

public static void CreateDb()
{
bool script = false;//آیا خروجی در کنسول هم نمایش داده شود
bool export = true;//آیا بر روی دیتابیس هم اجرا شود
bool dropTables = false;//آیا جداول موجود دراپ شوند
new SchemaExport(GetConfig().BuildConfiguration()).Execute(script, export, dropTables);
}
}
}
چند نکته در مورد این کلاس:
الف) با توجه به اینکه برنامه از نوع ویندوزی است، برای مدیریت صحیح کانکشن استرینگ، فایل App.Config را به برنامه افروده و محتویات آن‌را به شکل زیر تنظیم می‌کنیم (تا کلید DbConnectionString توسط متد GetConfig مورد استفاده قرارگیرد ):

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<!--NHSessionManager-->
<add name="DbConnectionString"
connectionString="Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true"/>
</connectionStrings>
</configuration>

ب) در NHibernate سنتی (!) کار ساخت نگاشت‌ها توسط یک سری فایل xml صورت می‌گیرد که با معرفی فریم ورک Fluent NHibernate و استفاده از قابلیت‌های Auto Mapping آن، این‌کار با سهولت و دقت هر چه تمام‌تر قابل انجام است که توضیحات نحوه‌ی انجام ‌آن‌را در قسمت‌های قبل مطالعه فرمودید. اگر نیاز بود تا این فایل‌های XML نیز جهت بررسی شخصی ایجاد شوند، تنها کافی است از متد ExportTo آن همانگونه که در متد GetConfig استفاده شده، کمک گرفته شود. به این صورت پس از ایجاد خودکار نگاشت‌ها، فایل‌های XML متناظر نیز در مسیری که به عنوان آرگومان متد ExportTo مشخص گردیده است، تولید خواهند شد (دو فایل NHSample4.Domain.Course.hbm.xml و NHSample4.Domain.Student.hbm.xml را در پوشه‌ای که محل اجرای برنامه است خواهید یافت).

با فراخوانی متد CreateDb این کلاس، پس از ساخت خودکار نگاشت‌ها، database schema متناظر، در دیتابیسی که توسط کانکشن استرینگ برنامه مشخص شده، ایجاد خواهد شد که دیتابیس دیاگرام آن‌را در شکل ذیل مشاهده می‌نمائید (جداول دانشجویان و واحدها هر کدام به صورت موجودیتی مستقل ایجاد شده که ارجاعات آن‌ها در جدولی سوم نگهداری می‌شود).



پیاده سازی الگوی مخزن

اینترفیس عمومی الگوی مخزن به شکل زیر می‌تواند باشد:

using System;
using System.Linq;
using System.Linq.Expressions;

namespace NHSample4.NHRepository
{
//Repository Interface
public interface IRepository<T>
{
T Get(object key);

T Save(T entity);
T Update(T entity);
void Delete(T entity);

IQueryable<T> Find();
IQueryable<T> Find(Expression<Func<T, bool>> predicate);
}
}

سپس پیاده سازی آن با توجه به کلاس SingletonCore ایی که در قسمت قبل تهیه کردیم (جهت مدیریت صحیح سشن فکتوری)، به صورت زیر خواهد بود.
این کلاس کار آغاز و پایان تراکنش‌ها را نیز مدیریت کرده و جهت سهولت کار اینترفیس IDisposable را نیز پیاده سازی می‌کند :

using System;
using System.Linq;
using NHSessionManager;
using NHibernate;
using NHibernate.Linq;

namespace NHSample4.NHRepository
{
public class Repository<T> : IRepository<T>, IDisposable
{
private ISession _session;
private bool _disposed = false;

public Repository()
{
_session = SingletonCore.SessionFactory.OpenSession();
BeginTransaction();
}

~Repository()
{
Dispose(false);
}

public T Get(object key)
{
if (!isSessionSafe) return default(T);

return _session.Get<T>(key);
}

public T Save(T entity)
{
if (!isSessionSafe) return default(T);

_session.Save(entity);
return entity;
}

public T Update(T entity)
{
if (!isSessionSafe) return default(T);

_session.Update(entity);
return entity;
}

public void Delete(T entity)
{
if (!isSessionSafe) return;

_session.Delete(entity);
}

public IQueryable<T> Find()
{
if (!isSessionSafe) return null;

return _session.Linq<T>();
}

public IQueryable<T> Find(System.Linq.Expressions.Expression<Func<T, bool>> predicate)
{
if (!isSessionSafe) return null;

return Find().Where(predicate);
}

void Commit()
{
if (!isSessionSafe) return;

if (_session.Transaction != null &&
_session.Transaction.IsActive &&
!_session.Transaction.WasCommitted &&
!_session.Transaction.WasRolledBack)
{
_session.Transaction.Commit();
}
else
{
_session.Flush();
}
}

void Rollback()
{
if (!isSessionSafe) return;

if (_session.Transaction != null && _session.Transaction.IsActive)
{
_session.Transaction.Rollback();
}
}

private bool isSessionSafe
{
get
{
return _session != null && _session.IsOpen;
}
}

void BeginTransaction()
{
if (!isSessionSafe) return;

_session.BeginTransaction();
}


public void Dispose()
{
Dispose(true);
// tell the GC that the Finalize process no longer needs to be run for this object.
GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposeManagedResources)
{
if (_disposed) return;
if (!disposeManagedResources) return;
if (!isSessionSafe) return;

try
{
Commit();
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
Rollback();
}
finally
{
if (isSessionSafe)
{
_session.Close();
_session.Dispose();
}
}

_disposed = true;
}
}
}
اکنون جهت استفاده از این کلاس مخزن به شکل زیر می‌توان عمل کرد:

using System;
using System.Collections.Generic;
using NHSample4.Domain;
using NHSample4.NHRepository;

namespace NHSample4
{
class Program
{
static void Main(string[] args)
{
//ایجاد دیتابیس در صورت نیاز
//NHSessionManager.Config.CreateDb();


//ابتدا یک دانشجو را اضافه می‌کنیم
Student student = null;
using (var studentRepo = new Repository<Student>())
{
student = studentRepo.Save(new Student() { Name = "Vahid" });
}

//سپس یک واحد را اضافه می‌کنیم
using (var courseRepo = new Repository<Course>())
{
var course = courseRepo.Save(new Course() { Teacher = "Shams" });
}

//اکنون یک واحد را به دانشجو انتساب می‌دهیم
using (var courseRepo = new Repository<Course>())
{
courseRepo.Save(new Course() { Students = new List<Student>() { student } });
}

//سپس شماره دروس استادی خاص را نمایش می‌دهیم
using (var courseRepo = new Repository<Course>())
{
var query = courseRepo.Find(t => t.Teacher == "Shams");

foreach (var course in query)
Console.WriteLine(course.Id);
}

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}

همانطور که ملاحظه می‌کنید در این سطح دیگر برنامه هیچ درکی از ORM مورد استفاده ندارد و پیاده سازی نحوه‌ی تعامل با NHibernate در پس کلاس مخزن مخفی شده است. کار آغاز و پایان تراکنش‌ها به صورت خودکار مدیریت گردیده و همچنین آزاد سازی منابع را نیز توسط اینترفیس IDisposable مدیریت می‌کند. به این صورت امکان فراموش شدن یک سری از اعمال متداول به حداقل رسیده، میزان کدهای تکراری برنامه کم شده و همچنین هر زمانیکه نیاز بود، صرفا با تغییر پیاده سازی کلاس مخزن می‌توان به ORM دیگری کوچ کرد؛ بدون اینکه نیازی به بازنویسی کل برنامه وجود داشته باشد.

دریافت سورس برنامه قسمت هشتم

ادامه دارد ...


مطالب
Blazor 5x - قسمت 13 - کار با فرم‌ها - بخش 1 - کار با EF Core در برنامه‌های Blazor Server
در ادامه قصد داریم یک پروژه‌ی مدیریت هتل را پیاده سازی کنیم. این پروژه، دو قسمتی است. قسمت اول آن یک پروژه‌ی Blazor Server، برای مدیریت هتل مانند تعاریف اتاق‌ها است و پروژه‌ی دوم آن از نوع Blazor WASM، برای مراجعه‌ی کاربران عمومی و رزرو اتاق‌ها است. هدف، بررسی نحوه‌ی کار با هر دو نوع فناوری است. وگرنه می‌توان کل پروژه را با Blazor Server و یا کل آن‌را با Blazor WASM هم پیاده سازی کرد. در مورد نحوه‌ی انتخاب و مزایا و معایب هرکدام از این فناوری‌ها، در قسمت‌های اول و دوم این سری بیشتر بحث شده‌است.


ساختار پوشه‌ها و پروژه‌های قسمت Blazor Server


قسمت Blazor Server مدیریت هتل ما از 7 پروژه و پوشه‌ی زیر تشکیل می‌شود:
- BlazorServer.App: پروژه‌ی اصلی Blazor Server است که با اجرای دستور dotnet new blazorserver در پوشه‌ی خالی آن آغاز می‌شود.
- BlazorServer.Common: پروژه‌ای از نوع classlib، جهت قرارگیری کدهای مشترک بین پروژه‌ها است که با اجرای دستور dotnet new classlib در این پوشه آغاز می‌شود.
- BlazorServer.DataAccess: پروژه‌ای از نوع classlib، برای تعریف DbContext برنامه است که با اجرای دستور dotnet new classlib در این پوشه آغاز می‌شود.
- BlazorServer.Entities: پروژه‌ای از نوع classlib، جهت تعریف کلاس‌های متناظر با جداول بانک اطلاعاتی برنامه است که با اجرای دستور dotnet new classlib در این پوشه آغاز می‌شود.
- BlazorServer.Models: پروژه‌ای از نوع classlib، برای تعریف کلاس‌های data transfer objects برنامه (DTO's) است که با اجرای دستور dotnet new classlib در این پوشه آغاز می‌شود.
- BlazorServer.Models.Mappings: پروژه‌ای از نوع classlib، برای تعریف نگاشت‌های بین DTO's و مجودیت‌های برنامه و برعکس است که با اجرای دستور dotnet new classlib در این پوشه آغاز می‌شود.
- BlazorServer.Services: پروژه‌ای از نوع classlib، جهت تعریف کدهایی که منطق تجاری تعامل با بانک اطلاعاتی را از طریق BlazorServer.DataAccess میسر می‌کند که با اجرای دستور dotnet new classlib در این پوشه آغاز می‌شود.


اصلاح پروژه‌ی BlazorServer.App جهت استفاده از LibMan

قالب پیش‌فرض BlazorServer.App، به همراه پوشه‌ی wwwroot\css است که در آن بوت استرپ و open-iconic به همراه فایل site.css قرار دارند. چون این پروژه به همراه هیچ نوع روشی برای مدیریت نگهداری بسته‌های سمت کلاینت خود نیست، دو پوشه‌ی بوت استرپ و open-iconic آن‌را حذف کرده و از روش مطرح شده‌ی در مطلب «Blazor 5x - قسمت یازدهم - مبانی Blazor - بخش 8 - کار با جاوا اسکریپت» استفاده خواهیم کرد:
dotnet tool update -g Microsoft.Web.LibraryManager.Cli
libman init
libman install bootstrap --provider unpkg --destination wwwroot/lib/bootstrap
libman install open-iconic --provider unpkg --destination wwwroot/lib/open-iconic
در پوشه‌ی ریشه‌ی پروژه‌ی BlazorServer.App، دستورات فوق را اجرا می‌کنیم تا بسته‌های bootstrap و open-iconic را در پوشه‌ی wwwroot/lib نصب کند و همچنین فایل libman.json متناظری را نیز جهت اجرای دستور libman restore برای دفعات آتی، تولید کند.

بعد از نصب بسته‌های ذکر شده، ابتدا سطر زیر را از ابتدای فایل پیش‌فرض wwwroot\css\site.css حذف می‌کنیم:
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
سپس فایل Pages\_Host.cshtml را به صورت زیر به روز رسانی می‌کنیم تا به مسیرهای جدید بسته‌های CSS، اشاره کند:
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>BlazorServer.App</title>
    <base href="~/" />
    <link href="lib/open-iconic/font/css/open-iconic-bootstrap.min.css" rel="stylesheet" />
    <link href="lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
    <link href="css/site.css" rel="stylesheet" />
    <link href="BlazorServer.App.styles.css" rel="stylesheet" />
</head>

برای bundling & minification این فایل‌ها می‌توان از «Bundler Minifier» استفاده کرد.


پروژه‌ی موجودیت‌های مدیریت هتل

فایل BlazorServer.Entities.csproj وابستگی خاصی را نداشته و به صورت زیر تعریف شده‌است:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>
</Project>
در این پروژه، کلاس جدید HotelRoom را که بیانگر ساختار جدول متناظری در بانک اطلاعاتی است، به صورت زیر تعریف می‌کنیم:
using System;
using System.ComponentModel.DataAnnotations;

namespace BlazorServer.Entities
{
    public class HotelRoom
    {
        [Key]
        public int Id { get; set; }

        [Required]
        public string Name { get; set; }

        [Required]
        public int Occupancy { get; set; }

        [Required]
        public decimal RegularRate { get; set; }

        public string Details { get; set; }

        public string SqFt { get; set; }

        public string CreatedBy { get; set; }

        public DateTime CreatedDate { get; set; } = DateTime.Now;

        public string UpdatedBy { get; set; }

        public DateTime UpdatedDate { get; set; }
    }
}
که شامل فیلدهایی مانند نام، ظرفیت، مساحت و ... یک اتاق هتل است.


پروژه‌ی تعریف DbContext برنامه‌ی مدیریت هتل

فایل BlazorServer.DataAccess.csproj به این صورت تعریف شده‌است:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\BlazorServer.Entities\BlazorServer.Entities.csproj" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.3" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.3">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>
</Project>
- در اینجا چون نیاز است موجودیت HotelRoom را به صورت یک DbSet معرفی کنیم، ارجاعی را به پروژه‌ی BlazorServer.Entities.csproj تعریف کرده‌ایم.
- همچنین دو وابستگی مورد نیاز جهت کار با EntityFrameworkCore و اجرای مهاجرت‌ها را نیز به آن افزوده‌ایم.

پس از تامین این وابستگی‌ها، اکنون می‌توان DbContext ابتدایی برنامه را به صورت زیر تعریف کرد که کار آن، در معرض دید قرار دادن HotelRoom به صورت یک DbSet است:
using BlazorServer.Entities;
using Microsoft.EntityFrameworkCore;

namespace BlazorServer.DataAccess
{
    public class ApplicationDbContext : DbContext
    {
        public DbSet<HotelRoom> HotelRooms { get; set; }

        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
        { }
    }
}
پس از این مرحله، نیاز است این DbContext را به سیستم تزریق وابستگی‌های برنامه‌ی اصلی معرفی کرد. بنابراین فایل BlazorServer.App.csproj پروژه‌ی اصلی Blazor Server را گشوده و تغییرات زیر را اعمال می‌کنیم:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\BlazorServer.DataAccess\BlazorServer.DataAccess.csproj" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.3" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.3">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>
</Project>
- چون می‌خواهیم ApplicationDbContext را به سیستم تزریق وابستگی‌ها معرفی کنیم، بنابراین باید بتوان به کلاس آن نیز دسترسی داشت که اینکار، با تعریف ارجاعی به BlazorServer.DataAccess.csproj میسر شده‌است.
- سپس چون می‌خواهیم از تامین کننده‌ی بانک اطلاعاتی SQL Server نیز استفاده کنیم، وابستگی‌های آن‌را نیز افزوده‌ایم.

با این تنظیمات، به فایل BlazorServer\BlazorServer.App\Startup.cs مراجعه کرده و کار افزودن AddDbContext و UseSqlServer را انجام می‌دهیم تا DbContext برنامه از طریق تزریق وابستگی‌ها قابل دسترسی شود و همچنین رشته‌ی اتصالی مشخص شده نیز به تامین کننده‌ی SQL Server ارسال شود:
namespace BlazorServer.App
{
    public class Startup
    {
        // ...
 
        public void ConfigureServices(IServiceCollection services)
        {
            var connectionString = Configuration.GetConnectionString("DefaultConnection");
            services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString));

            // ...
این رشته‌ی اتصالی را به صورت زیر در فایل BlazorServer\BlazorServer.App\appsettings.json تعریف کرده‌ایم:
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=HotelManagement;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}
که در حقیقت یک رشته‌ی اتصالی جهت کار با LocalDB است.


اجرای مهاجرت‌ها و تشکیل ساختار بانک اطلاعاتی

پس از این تنظیمات، اکنون می‌توانیم به پوشه‌ی BlazorServer\BlazorServer.DataAccess مراجعه کرده و از طریق خط فرمان، دستورات زیر را صادر کنیم:
dotnet tool update --global dotnet-ef --version 5.0.3
dotnet build
dotnet ef migrations --startup-project ../BlazorServer.App/ add Init --context ApplicationDbContext
dotnet ef --startup-project ../BlazorServer.App/ database update --context ApplicationDbContext
- در ابتدا نیاز است ابزارهای مهاجرت EF-Core را نصب کنیم که سطر اول، اینکار را انجام می‌دهد.
- همیشه بهتر است پیش از اجرای عملیات Migration، یکبار dotnet build را اجرا کرد؛ تا اگر خطایی وجود دارد، بتوان جزئیات دقیق آن‌را مشاهده کرد. چون عموما این جزئیات در حین اجرای دستورات بعدی، با پیام مختصر «عملیات شکست خورد»، نمایش داده نمی‌شوند.
- دستور سوم، کار تشکیل پوشه‌ی BlazorServer\BlazorServer.DataAccess\Migrations و تولید خودکار دستورات تشکیل بانک اطلاعاتی را بر اساس ساختار DbContext برنامه انجام می‌دهد.
- دستور چهارم، بر اساس اطلاعات موجود در پوشه‌ی BlazorServer\BlazorServer.DataAccess\Migrations، بانک اطلاعاتی واقعی را تولید می‌کند.
در این دستورات ذکر پروژه‌ی آغازین برنامه جهت یافتن وابستگی‌های پروژه ضروری است.



تکمیل پروژه‌ی DTO‌های برنامه

همواره توصیه شده‌است که موجودیت‌های برنامه را مستقیما در معرض دید UI قرار ندهید. حداقل مشکلی را که در اینجا ممکن است مشاهده کنید، حملات از نوع mass assignment هستند. برای مثال قرار است از کاربر، کلمه‌ی عبور جدید آن‌را دریافت کنید، ولی چون اطلاعات دریافتی، به اصل موجودیت متناظر با بانک اطلاعاتی نگاشت می‌شود، کاربر می‌تواند فیلد IsAdmin را هم خودش مقدار دهی کند! و چون سیستم Binding بسیار پیشرفته عمل می‌کند، این ورودی را معتبر یافته و در اینجا علاوه بر به روز رسانی کلمه‌ی عبور، خواص دیگری را هم که نباید به روز رسانی شوند، به روز رسانی می‌کند و یا در بسیاری از موارد نیاز است data annotations خاصی را برای فیلدها تعریف کرد که ربطی به موجودیت اصلی ندارند و یا نیاز است فیلدهایی را در UI قرار داد که باز هم تناظر یک به یکی با موجودیت اصلی ندارند (گاهی کمتر و گاهی بیشتر هستند و باید بر روی آن‌ها محاسباتی صورت گیرد تا قابلیت ذخیره سازی در بانک اطلاعاتی را پیدا کنند). به همین جهت کار مدل سازی UI و یا بازگشت اطلاعات نهایی از سرویس‌ها را توسط DTO‌ها که یک سری کلاس ساده‌ی C# 9.0 از نوع record هستند، انجام می‌دهیم:
using System;
using System.ComponentModel.DataAnnotations;

namespace BlazorServer.Models
{
    public record HotelRoomDTO
    {
        public int Id { get; init; }

        [Required(ErrorMessage = "Please enter the room's name")]
        public string Name { get; init; }

        [Required(ErrorMessage = "Please enter the occupancy")]
        public int Occupancy { get; init; }

        [Range(1, 3000, ErrorMessage = "Regular rate must be between 1 and 3000")]
        public decimal RegularRate { get; init; }

        public string Details { get; init; }

        public string SqFt { get; init; }
    }
}
Record‌های C# 9.0، انتخاب بسیار مناسبی برای تعریف DTO‌ها هستند. از این لحاظ که قرار نیست اطلاعات دریافتی از کاربر، در این بین و پس از مقدار دهی اولیه، تغییر کنند.
در اینجا فیلدهای UI برنامه را که در قسمت بعد تکمیل خواهیم کرد، مشاهده می‌کنید؛ به همراه یک سری data annotation برای تعریف اجباری و یا بازه‌ی مورد قبول، به همراه پیام‌های خطای مرتبط.


نگاشت DTO‌های برنامه به موجودیت‌ها و بر عکس

یا می‌توان خواص DTO تعریف شده را یکی یکی به موجودیتی متناظر با آن انتساب داد و یا می‌توان از AutoMapper برای اینکار استفاده کرد. به همین جهت به BlazorServer.Models.Mappings.csproj مراجعه کرده و تغییرات زیر را اعمال می‌کنیم:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\BlazorServer.Entities\BlazorServer.Entities.csproj" />
    <ProjectReference Include="..\BlazorServer.Models\BlazorServer.Models.csproj" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1" />
  </ItemGroup>
</Project>
- پروژه‌ای که کار تعریف نگاشت‌ها را انجام می‌دهد، نیاز به اطلاعات موجودیت‌ها و مدل‌ها (DTO ها)ی متناظر را دارد. به همین جهت ارجاعاتی را به این دو پروژه، تعریف کرده‌ایم.
- همچنین بسته‌ی مخصوص AutoMapper را که به همراه امکانات تزریق وابستگی‌های آن نیز هست، در اینجا افزوده‌ایم.

پس از افزودن این ارجاعات، نگاشت دو طرفه‌ی بین مدل و موجودیت تعریف شده را به صورت زیر تعریف می‌کنیم:
using AutoMapper;
using BlazorServer.Entities;

namespace BlazorServer.Models.Mappings
{
    public class MappingProfile : Profile
    {
        public MappingProfile()
        {
            CreateMap<HotelRoomDTO, HotelRoom>().ReverseMap(); // two-way mapping
        }
    }
}
اکنون برای شناسایی پروفایل فوق و معرفی آن به AutoMapper، به فایل BlazorServer\BlazorServer.App\Startup.cs مراجعه کرده و تزریق وابستگی و ردیابی خودکار آن‌را اضافه می‌کنیم که شامل اسکن تمام اسمبلی‌های موجود، جهت یافتن Profile‌های AutoMapper است:
namespace BlazorServer.App
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());

            // ...


تعریف سرویس مدیریت اتاق‌های هتل

پس از راه اندازی برنامه و تعریف موجودیت‌ها، DbContext و غیره، اکنون می‌توانیم از آن‌ها جهت ارائه‌ی منطق مدیریتی برنامه استفاده کنیم:
using System.Collections.Generic;
using System.Threading.Tasks;
using BlazorServer.Models;

namespace BlazorServer.Services
{
    public interface IHotelRoomService
    {
        Task<HotelRoomDTO> CreateHotelRoomAsync(HotelRoomDTO hotelRoomDTO);

        Task<int> DeleteHotelRoomAsync(int roomId);

        IAsyncEnumerable<HotelRoomDTO> GetAllHotelRoomsAsync();

        Task<HotelRoomDTO> GetHotelRoomAsync(int roomId);

        Task<HotelRoomDTO> IsRoomUniqueAsync(string name);

        Task<HotelRoomDTO> UpdateHotelRoomAsync(int roomId, HotelRoomDTO hotelRoomDTO);
    }
}

که پیاده سازی ابتدایی آن به صورت زیر است:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using BlazorServer.DataAccess;
using BlazorServer.Entities;
using BlazorServer.Models;
using Microsoft.EntityFrameworkCore;

namespace BlazorServer.Services
{
    public class HotelRoomService : IHotelRoomService
    {
        private readonly ApplicationDbContext _dbContext;
        private readonly IMapper _mapper;
        private readonly IConfigurationProvider _mapperConfiguration;

        public HotelRoomService(ApplicationDbContext dbContext, IMapper mapper)
        {
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
            _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
            _mapperConfiguration = mapper.ConfigurationProvider;
        }

        public async Task<HotelRoomDTO> CreateHotelRoomAsync(HotelRoomDTO hotelRoomDTO)
        {
            var hotelRoom = _mapper.Map<HotelRoom>(hotelRoomDTO);
            hotelRoom.CreatedDate = DateTime.Now;
            hotelRoom.CreatedBy = "";
            var addedHotelRoom = await _dbContext.HotelRooms.AddAsync(hotelRoom);
            await _dbContext.SaveChangesAsync();
            return _mapper.Map<HotelRoomDTO>(addedHotelRoom.Entity);
        }

        public async Task<int> DeleteHotelRoomAsync(int roomId)
        {
            var roomDetails = await _dbContext.HotelRooms.FindAsync(roomId);
            if (roomDetails == null)
            {
                return 0;
            }

            _dbContext.HotelRooms.Remove(roomDetails);
            return await _dbContext.SaveChangesAsync();
        }

        public IAsyncEnumerable<HotelRoomDTO> GetAllHotelRoomsAsync()
        {
            return _dbContext.HotelRooms
                        .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                        .AsAsyncEnumerable();
        }

        public Task<HotelRoomDTO> GetHotelRoomAsync(int roomId)
        {
            return _dbContext.HotelRooms
                            .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                            .FirstOrDefaultAsync(x => x.Id == roomId);
        }

        public Task<HotelRoomDTO> IsRoomUniqueAsync(string name)
        {
            return _dbContext.HotelRooms
                            .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                            .FirstOrDefaultAsync(x => x.Name == name);
        }

        public async Task<HotelRoomDTO> UpdateHotelRoomAsync(int roomId, HotelRoomDTO hotelRoomDTO)
        {
            if (roomId != hotelRoomDTO.Id)
            {
                return null;
            }

            var roomDetails = await _dbContext.HotelRooms.FindAsync(roomId);
            var room = _mapper.Map(hotelRoomDTO, roomDetails);
            room.UpdatedBy = "";
            room.UpdatedDate = DateTime.Now;
            var updatedRoom = _dbContext.HotelRooms.Update(room);
            await _dbContext.SaveChangesAsync();
            return _mapper.Map<HotelRoomDTO>(updatedRoom.Entity);
        }
    }
}
- در اینجا DbContext برنامه و همچنین نگاشت‌گر AutoMapper، به سازنده‌ی سرویس، تزریق شده و توسط آن‌ها، ابتدا اطلاعات DTOها به موجودیت‌ها تبدیل شده‌اند (و یا برعکس) و سپس با استفاده از dbContext برنامه، کوئری‌هایی را بر روی بانک اطلاعاتی اجرا کرده‌ایم.
- در این کدها استفاده از متد ProjectTo را هم مشاهده می‌کنید. استفاده از این متد، بسیار بهینه‌تر از کار با متد Map درون حافظه‌ای است. از این جهت که بر روی SQL نهایی ارسالی به سمت سرور تاثیرگذار است و تعداد فیلدهای بازگشت داده شده را بر اساس DTO تعیین شده، کاهش می‌دهد. درغیراینصورت باید تمام ستون‌های جدول را بازگشت داد و سپس با استفاده از متد Map درون حافظه‌‌ای، کار نگاشت نهایی را انجام داد که آنچنان بهینه نیست.

در آخر نیاز است این سرویس را نیز به سیستم تزریق وابستگی‌های برنامه معرفی کنیم. به همین جهت در فایل BlazorServer\BlazorServer.App\Startup.cs، تغییر زیر را اعمال خواهیم کرد:
namespace BlazorServer.App
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IHotelRoomService, HotelRoomService>();

         // ...


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-13.zip
مطالب
معرفی Reactive extensions
Reactive extensions یا به صورت خلاصه Rx ،کتابخانه‌ی سورس باز تهیه شده‌ای توسط مایکروسافت است که اگر بخواهیم آن‌را به ساده‌ترین شکل ممکن تعریف کنیم، معنای Linq to events را می‌دهد و امکان مدیریت تعامل‌های پیچیده‌ی async را به صورت declaratively فراهم می‌کند. هدف آن بسط فضای نام System.Linq و تبدیل نتایج یک کوئری LINQ به یک مجموعه‌ی Observable است؛ به همراه مدیریت مسایل همزمانی آن.
این افزونه جزو موفق‌ترین کتابخانه‌های دات نتی مایکروسافت در سال‌های اخیر به شما می‌رود؛ تا حدی که معادل‌های بسیاری از آن برای زبان‌های دیگر مانند Java، JavaScript، Python، ‍CPP و غیره نیز تهیه شده‌اند.


استفاده از Rx به همراه یک کوئری LINQ

یک برنامه‌ی کنسول جدید را ایجاد کنید. سپس برای نصب کتابخانه‌ی Rx، دستور ذیل را در کنسول پاورشل نیوگت اجرا نمائید:
 PM> Install-Package Rx-Main
نصب آن از طریق نیوگت، به صورت خودکار کلیه وابستگی‌های مرتبط با آن‌را نیز به پروژه‌ی جاری اضافه می‌کند:
<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="Rx-Core" version="2.2.4" targetFramework="net45" />
  <package id="Rx-Interfaces" version="2.2.4" targetFramework="net45" />
  <package id="Rx-Linq" version="2.2.4" targetFramework="net45" />
  <package id="Rx-Main" version="2.2.4" targetFramework="net45" />
  <package id="Rx-PlatformServices" version="2.2.4" targetFramework="net45" />
</packages>
سپس متد Main این برنامه را به نحو ذیل تغییر دهید:
using System;
using System.Linq;

namespace Rx01
{
    class Program
    {
        static void Main(string[] args)
        {
            var query = Enumerable.Range(1, 5).Select(number => number);
            foreach (var number in query)
            {
                Console.WriteLine(number);
            }
            finished();
        }

        private static void finished()
        {
            Console.WriteLine("Done!");
        }
    }
}
در اینجا یک سری عملیات متداول را مشاهده می‌کنید. بازه‌ای از اعداد توسط متد Enumerable.Range ایجاد شده و سپس به کمک یک حلقه‌، تمام آیتم‌های آن نمایش داده می‌شوند. همچنین در پایان کار نیز یک متد دیگر فراخوانی شده‌است.
اکنون اگر بخواهیم همین عملیات را توسط Rx انجام دهیم، به شکل زیر خواهد بود:
using System;
using System.Linq;
using System.Reactive.Linq;

namespace Rx01
{
    class Program
    {
        static void Main(string[] args)
        {
            var query = Enumerable.Range(1, 5).Select(number => number);
            var observableQuery = query.ToObservable();
            observableQuery.Subscribe(onNext: number => Console.WriteLine(number), onCompleted: () => finished());
        }

        private static void finished()
        {
            Console.WriteLine("Done!");
        }
    }
}
ابتدا نیاز است تا کوئری متداول LINQ را تبدیل به نمونه‌ی Observable آن کرد. اینکار را توسط متد الحاقی ToObservable که در فضای نام System.Reactive.Linq تعریف شده‌است، انجام می‌دهیم. به این ترتیب، هر زمانیکه که عددی به query اضافه می‌شود، با استفاده از متد Subscribe می‌توان تغییرات آن‌را تحت کنترل قرار داد. برای مثال در اینجا هربار که عددی در بازه‌ی 1 تا 5 تولید می‌شود، یکبار پارامتر onNext اجرا خواهد شد. برای نمونه در مثال فوق، از نتیجه‌ی آن برای نمایش مقدار دریافتی، استفاده شده‌است. سپس توسط پارامتر اختیاری onCompleted، در پایان کار، یک متد خاص را می‌توان فراخوانی کرد. خروجی برنامه در این حالت نیز به صورت ذیل است:
1
2
3
4
5
Done!
البته اگر قصد خلاصه نویسی داشته باشیم، سطر آخر متد Main، با سطر ذیل یکی است:
 observableQuery.Subscribe(Console.WriteLine, finished);

در این مثال ساده صرفا یک Syntax دیگر را نسبت به حلقه‌ی foreach متداول مشاهده کردیم که اندکی فشرده‌تر است. در هر دو حالت نیز عملیات انجام شده در تردجاری صورت گرفته‌اند. اما قابلیت‌ها و ارزش‌های واقعی Rx زمانی آشکار خواهند شد که پردازش موازی و پردازش در تردهای دیگر را در آن فعال کنیم.


الگوی Observer

Rx پیاده سازی کننده‌ی الگوی طراحی شیءگرایی به نام Observer است. برای توضیح آن یک لامپ و سوئیچ برق را درنظر بگیرید. زمانیکه لامپ مشاهده می‌کند سوئیچ برق در حالت روشن قرار گرفته‌است، روشن خواهد شد و برعکس. در اینجا به سوئیچ، subject و به لامپ، observer گفته می‌شود. هر زمان که حالت سوئیچ تغییر می‌کند، از طریق یک callback، وضعیت خود را به observer اعلام خواهد کرد. علت استفاده از callbackها، ارائه راه‌حل‌های عمومی است تا بتواند با انواع و اقسام اشیاء کار کند. به این ترتیب هر بار که شیء observer از نوع متفاوتی تعریف می‌شود (مثلا بجای لامپ یک خودرو قرار گیرد)، نیازی نخواهد بود تا subject را تغییر داد.
در Rx دو اینترفیس معادل observer و subject تعریف شده‌اند. در اینجا اینترفیس IObserver معادل observer است و اینترفیس IObservable معادل subject می‌باشد:
    class Subject : IObservable<int>
    {
        public IDisposable Subscribe(IObserver<int> observer)
        {
        }
    }
کار متد Subscribe، اتصال به Observer است و برای این حالت نیاز به کلاسی دارد که اینترفیس IObserver را پیاده سازی کند.
    class Observer : IObserver<int>
    {
        public void OnCompleted()
        {
        }

        public void OnError(Exception error)
        {
        }

        public void OnNext(int value)
        {
        }
    }
در اینجا OnCompleted زمانی اجرا می‌شود که پردازش مجموعه‌ای از اعداد int پایان یافته باشد. OnError در زمان وقوع استثنایی اجرا می‌شود و OnNext به ازای هر عدد موجود در مجموعه‌ی در حال پردازش، یکبار اجرا می‌شود. البته نیازی به پیاده سازی صریح این اینترفیس نیست و توسط متد توکار Observer.Create می‌توان به همین نتیجه رسید.
مجموعه‌های Observable کلید کار با Rx هستند. در مثال قبل ملاحظه کردیم که با استفاده از متد الحاقی ToObservable بر روی یک کوئری LINQ و یا هر نوع IEnumerable ایی،  می‌توان یک مجموعه‌ی Observable را ایجاد کرد. خروجی کوئری حاصل از آن به صورت خودکار اینترفیس IObservable را پیاده سازی می‌کند که دارای یک متد به نام Subscribe است.
در متد Subscribe کاری که به صورت خودکار صورت خواهد گرفت، ایجاد یک حلقه‌ی foreach بر روی مجموعه‌ی مورد آنالیز و سپس فراخوانی متد OnNext کلاس پیاده سازی کننده‌ی IObserver به ازای هر آیتم موجود در مجموعه است (فراخوانی observer.OnNext). در پایان کار هم فقط return this در اینجا صورت خواهد گرفت. در حین پردازش حلقه، اگر خطایی رخ دهد، متد observer.OnError انجام می‌شود.

در مثال قبل،کوئری LINQ نوشته شده، خروجی از نوع IObservable ندارد. به کمک متد الحاقی ToObservable:
public static System.IObservable<TSource> ToObservable<TSource>(
    this System.Collections.Generic.IEnumerable<TSource> source,
    System.Reactive.Concurrency.IScheduler scheduler)
به صورت خودکار، IEnumerable حاصل از کوئری LINQ را تبدیل به یک IObservable کرده‌ایم. به این ترتیب اکنون کوئری LINQ ما همانند سوئیچ برق عمل می‌کند و با تغییر آیتم‌های موجود در آن، مشاهده‌گرهایی که به آن متصل شده‌اند (از طریق فراخوانی متد Subscribe)، امکان دریافت سیگنال‌های تغییر وضعیت آن‌را خواهند داشت.
البته استفاده از متد Subscribe به نحوی که در مثال قبل ذکر شد، خلاصه شده‌ی الگوی Observer است. اگر بخواهیم دقیقا مانند الگو عمل کنیم، چنین شکلی را خواهد داشت:
 var query = Enumerable.Range(1, 5).Select(number => number);
var observableQuery = query.ToObservable();
var observer = Observer.Create<int>(onNext: number => Console.WriteLine(number));
observableQuery.Subscribe(observer);
ابتدا توسط متد ToObservable یک IObservable (سوئیچ) را ایجاد کرده‌ایم. سپس توسط کلاس Observer موجود در فضای نام System.Reactive، یک IObserver (لامپ) را ایجاد کرده‌ایم. کار اتصال سوئیچ به لامپ در متد Subscribe انجام می‌شود. اکنون هر زمانیکه تغییری در وضعیت observableQuery حاصل شود، سیگنالی را به observer ارسال می‌کند. در اینجا callbacks کار مدیریت observer را انجام می‌دهند.


پردازش نتایج یک کوئری LINQ در تردی دیگر توسط Rx

برای اجرای نتایج متد Subscribe در یک ترد جدید، می‌توان پارامتر scheduler متد ToObservable را مقدار دهی کرد:
using System;
using System.Linq;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Threading;

namespace Rx01
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Thread-Id: {0}", Thread.CurrentThread.ManagedThreadId);
            var query = Enumerable.Range(1, 5).Select(number => number);
            var observableQuery = query.ToObservable(scheduler: NewThreadScheduler.Default);
            observableQuery.Subscribe(onNext: number =>
            {
                Console.WriteLine("number: {0}, on Thread-id: {1}", number, Thread.CurrentThread.ManagedThreadId);
            }, onCompleted: () => finished());
        }

        private static void finished()
        {
            Console.WriteLine("Done!");
        }
    }
}
خروجی این مثال به نحو ذیل است:
 Thread-Id: 1
number: 1, on Thread-id: 3
number: 2, on Thread-id: 3
number: 3, on Thread-id: 3
number: 4, on Thread-id: 3
number: 5, on Thread-id: 3
Done!
پیش از آغاز کار و در متد Main، ترد آی دی ثبت شده مساوی 1 است. سپس هربار که callback متد Subscribe فراخوانی شده‌است، ملاحظه می‌کنید که ترد آی دی آن مساوی عدد 3 است. به این معنا که کلیه نتایج در یک ترد مشخص دیگر پردازش شده‌اند.
NewThreadScheduler.Default در فضای نام System.Reactive.Concurrency واقع شده‌است.


یک نکته
در نگارش‌های آغازین Rx، مقدار scheduler را می‌شد معادل Scheduler.NewThread نیز قرار داد که در نگارش‌های جدید منسوخ شده درنظر گرفته شده و به زودی حذف خواهد شد. معادل‌های جدید آن اکنون NewThreadScheduler.Default، ThreadPoolScheduler.Default و امثال آن هستند.


مدیریت خاتمه‌ی اعمال انجام شده‌ی در تردهای دیگر توسط Rx

یکی از مواردی که حین اجرای نتیجه‌ی callbackهای پردازش شده‌ی در تردهای دیگر نیاز است بدانیم، زمان خاتمه‌ی کار آن‌ها است. برای نمونه در مثال قبل، نمایش Done پس از پایان تمام callbacks انجام شده‌است. فرض کنید، callback پایان عملیات را حذف کرده و متد finished را پس از فراخوانی متد observableQuery.Subscribe قرار دهیم:
observableQuery.Subscribe(onNext: number =>
{
   Console.WriteLine("number: {0}, on Thread-id: {1}", number,     
                              Thread.CurrentThread.ManagedThreadId);
}/*, onCompleted: () => finished()*/);
finished();
اینبار اگر برنامه را اجرا کنیم به خروجی ذیل خواهیم رسید:
 Thread-Id: 1
number: 1, on Thread-id: 3
Done!
number: 2, on Thread-id: 3
number: 3, on Thread-id: 3
number: 4, on Thread-id: 3
number: 5, on Thread-id: 3
این خروجی بدین معنا است که متد  observableQuery.Subscribeدر حین اجرا شدن در تردی دیگر، صبر نخواهد کرد تا عملیات مرتبط با آن خاتمه یابد و سپس سطر بعدی را اجرا کند. بنابراین برای حل این مشکل، تنها کافی است به آن اعلام کنیم که پس از پایان عملیات، onCompleted را اجرا کن.


مدیریت استثناهای رخ داده در حین پردازش مجموعه‌های واکنشگرا

متد Subscribe دارای چندین overload است. تا اینجا نمونه‌ای که دارای پارامترهای onNext و onCompleted بودند را بررسی کردیم. اگر بخواهیم مدیریت استثناءها را نیز در اینجا اضافه کنیم، فقط کافی است از overload دیگر آن که دارای پارامتر onError است، استفاده نمائیم:
observableQuery.Subscribe(
  onNext: number => Console.WriteLine(number),
  onError: exception => Console.WriteLine(exception.Message),
  onCompleted: () => finished());
اگر callback پارامتر onError اجرا شود، دیگر به onCompleted نخواهیم رسید. همچنین دیگر onNext ایی نیز اجرا نخواهد شد.


مدیریت ترد اجرای نتایج حاصل از Rx در یک برنامه‌ی دسکتاپ WPF یا WinForms

تا اینجا مشاهده کردیم که اجرای callbackهای observer در یک ترد دیگر، به سادگی تنظیم پارامتر scheduler متد ToObservable است. اما در برنامه‌های دسکتاپ برای به روز رسانی عناصر رابط کاربری، حتما باید در تردی قرار داشته باشیم که آن رابط کاربری در آن ایجاد شده‌است یا به عبارتی در ترد اصلی برنامه؛ در غیر اینصورت برنامه کرش خواهد کرد. مدیریت این مساله نیز در Rx بسیار ساده‌است. ابتدا نیاز است بسته‌ی Rx-WPF را نصب کرد:
 PM> Install-Package Rx-WPF
سپس توسط متد ObserveOn می‌توان مشخص کرد که نتیجه‌ی عملیات باید بر روی کدام ترد اجرا شود:
 observableQuery.ObserveOn(DispatcherScheduler.Current).Subscribe(...)
روش دیگر آن استفاده از متد ObserveOnDispatcher می‌باشد:
 observableQuery.ObserveOnDispatcher().Subscribe(...)
بنابراین مشخص سازی پارامتر scheduler متد ToObservable، به معنای اجرای query آن در یک ترد دیگر و استفاده از متد ObserveOn، به معنای مشخص سازی ترد اجرای callbackهای مشاهده‌گر است.

و یا اگر از WinForms استفاده می‌کنید، ابتدا بسته‌ی Rx خاص آن‌را نصب کنید:
 PM> Install-Package Rx-WinForms
و سپس ترد اجرای callbackها را SynchronizationContext.Current مشخص نمائید:
 observableQuery.ObserveOn(SynchronizationContext.Current).Subscribe(...)

یک نکته‌
در Rx فرض می‌شود که کوئری شما زمانبر است و callbackهای مشاهده‌گر سریع عمل می‌کنند. بنابراین هدف از callbackهای آن، پردازش‌های سنگین نیست. جهت آزمایش این مساله، اینبار query ابتدایی برنامه را به شکل ذیل تغییر دهید که در آن بازگشت زمانبر یک سری داده شبیه سازی شده‌اند.
 var query = Enumerable.Range(1, 5).Select(number =>
{
   Thread.Sleep(250);
   return number;
});
سپس با استفاده از متد ToObservable، ترد دیگری را برای اجرای واقعی آن مشخص کنید تا در حین اجرای آن برنامه در حالت هنگ به نظر نرسد و سپس نمایش آن‌را به کمک متد ObserveOn، بر روی ترد اصلی برنامه انجام دهید.
مطالب
نحوه استفاده از افزونه Firebug برای دیباگ برنامه‌های ASP.NET مبتنی بر jQuery
هر از چندگاهی سؤال «این مثال jQuery رو نمی‌تونم اجرا یا باز سازی کنم» در این سایت یا سایت‌های مشابه تکرار می‌شوند. بنابراین بهتر است نحوه عیب یابی برنامه‌های ASP.NET مبتنی بر jQuery را یکبار با هم مرور کنیم. در اینجا، مثال تهیه یک Image Slider را که پیشتر در سایت مطرح شده است، به نحوی دیگر بررسی خواهیم کرد:
1) فراموش می‌کنیم تا اسکریپت اصلی jQuery را به درستی پیوست و مسیردهی کنیم.
2) مسیر Generic handler دیگری را ذکر می‌کنیم.
3) مسیرهای تصاویری را که Image slider باید نمایش دهد، کاملا بی‌ربط ذکر می‌کنیم.
4) خروجی JSON نامربوطی را بازگشت می‌دهیم.
5) یکبار هم یک استثنای عمدی دستی را در بین کدها قرار خواهیم داد.

و ... بعد سعی می‌کنیم با استفاده از Firebug عیوب فوق را یافته و اصلاح کنیم؛ تا به یک برنامه قابل اجرا برسیم.


معرفی برنامه‌ای که کار نمی‌کند!

یک برنامه ASP.NET Empty web application را آغاز کنید. سپس سه پوشه Scripts، Content و Images را به آن اضافه نمائید. در این پوشه‌ها، اسکریپت‌های نمایش دهنده تصاویر، Css آن و تصاویری که قرار است نمایش داده شوند، قرار می‌گیرند:


سپس یک فایل default.aspx و یک فایل OrbitHandler.ashx را نیز به پروژه با محتویات ذیل اضافه کنید: (در این دو فایل، 5 مورد مشکل ساز یاد شده لحاظ شده‌اند)
محتویات فایل OrbitHandler.ashx.cs مطابق کدهای ذیل است:
using System.Collections.Generic;
using System.IO;
using System.Web;
using System.Web.Script.Serialization;

namespace OrbitWebformsTest
{
    public class Picture
    {
        public string Title { set; get; }
        public string Path { set; get; }
    }

    public class OrbitHandler : IHttpHandler
    {
        IList<Picture> PicturesDataSource()
        {
            var results = new List<Picture>();
            var path = HttpContext.Current.Server.MapPath("~/Images");

            foreach (var item in Directory.GetFiles(path, "*.*"))
            {
                var name = Path.GetFileName(item);
                results.Add(new Picture
                {
                    Path = /*"Images/" + name*/ name,
                    Title = name
                });
            }

            return results;
        }

        public void ProcessRequest(HttpContext context)
        {
            var items = PicturesDataSource();
            var json = /*new JavaScriptSerializer().Serialize(items)*/ string.Empty;
            throw new InvalidDataException("همینطوری");
            context.Response.ContentType = "text/plain";
            context.Response.Write(json);
        }

        public bool IsReusable
        { 
            get { return false; } 
        }
    }
}
در اینجا جهت سهولت دموی برنامه (و همچنین امکان باز تولید آن توسط خوانندگان)، از بانک اطلاعاتی استفاده نشده و عمدا از یک لیست جنریک تشکیل شده در حافظه کمک گرفته شده است. تصاویر برنامه در پوشه Images واقع در ریشه سایت، قرار دارند. بنابراین توسط متد PicturesDataSource، فایل‌های این پوشه را یافته و مطابق ساختار کلاس Picture بازگشت می‌دهیم. نهایتا این اطلاعات به ظاهر قرار است با فرمت JSON بازگشت داده شوند تا بتوان نتیجه را توسط افزونه Orbit استفاده کرد.

همچنین کدهای صفحه ASPX ایی که قرار است (به ظاهر البته) از این Generic handler استفاده کند به نحو ذیل است:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="default.aspx.cs" Inherits="OrbitWebformsTest._default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
    <link href="Content/orbit-1.2.3.css" rel="stylesheet" type="text/css" />
    <script src="Script/jquery-1.5.1.min.js" type="text/javascript"></script>
    <script src="Scripts/jquery.orbit-1.2.3.min.js" type="text/javascript"></script>
</head>
<body>
    <form id="form1" runat="server">
    <div id="featured">
    </div>
    </form>
    <script type="text/javascript">
        $(function () {
            $.ajax({
                url: "Handler.ashx",
                contentType: "application/json; charset=utf-8",
                success: function (data) {
                    $.each(data, function (i, b) {
                        var str = '<img src="' + b.Path + '" alt="' + b.Title + '"/>';
                        $("#featured").append(str);
                    });
                    $('#featured').orbit();
                },
                dataType: "json"
            });
        });
    </script>
</body>
</html>
خوب! اگر پروژه را اجرا کنیم، کار نمی‌کند. یک مستطیل مشکی رنگ در کنار صفحه ظاهر شده و همین! حالا چکار باید کرد؟


مراحل عیب یابی برنامه‌ای که کار نمی‌کند!

ابتدا برنامه را در فایرفاکس باز کرده و سپس افزونه Firebug را با کلیک بر روی آیکن آن، بر روی سایت فعال می‌کنیم. سپس یکبار بر روی دکمه F5 کلیک کنید تا مجددا مراحل بارگذاری سایت تحت نظر افزونه Firebug فعال شده، طی شود.


اولین موردی که مشهود است، نمایش عدد 3، کنار آیکن فایرباگ می‌باشد. این عدد به معنای وجود خطاهای اسکریپتی در کدهای ما است.
برای مشاهده این خطاها، بر روی برگه Console آن کلیک کنید: 


بله. مشخص است که مسیر دهی فایل jquery-1.5.1.min.js صحیح نبوده و همین مساله سبب بروز خطاهای اسکریپتی گردیده است. برای اصلاح آن سطر زیر را در برنامه تغییر دهید:
 <script src="Scripts/jquery-1.5.1.min.js" type="text/javascript"></script>
پیشتر پوشه Script ذکر شده بود که باید تبدیل به Scripts شود.

مجددا دکمه F5 را فشرده و سایت را با تنظیمات جدید اجرا کنید. اینبار در برگه Console و یا در برگه شبکه فایرباگ، خطای یافت نشدن Generic handler نمایان می‌شوند:


برای رفع آن به فایل default.aspx مراجعه و بجای معرفی Handler.ashx، نام OrbitHandler.ashx را وارد کنید.
مجددا دکمه F5 را فشرده و سایت را با تنظیمات جدید اجرا کنید.


اگر به برگه کنسول دقت کنیم، بروز استثناء در کدها تشخیص داده شده و همچنین در برگه Response پاسخ دریافتی از سرور، جزئیات صفحه خطای بازگشتی از آن نیز قابل بررسی و مشاهده است.
اینبار به فایل OrbitHandler.ashx.cs مراجعه کرده و سطر throw new InvalidDataException را حذف می‌کنیم. در ادامه برنامه را کامپایل و مجددا اجرا خواهیم کرد.



با اجرای مجدد سایت، تبادل اطلاعات صحیحی با فایل OrbitHandler.ashx برقرار شده است، اما خروجی خاصی قابل مشاهده نیست. بنابراین بازهم سایت کار نمی‌کند.
برای رفع این مشکل، متد ProcessRequest را به نحو ذیل تغییر خواهیم داد:
        public void ProcessRequest(HttpContext context)
        {
            var items = PicturesDataSource();
            var json = new JavaScriptSerializer().Serialize(items);            
            context.Response.ContentType = "text/plain";
            context.Response.Write(json);
        }
برنامه را کامپایل کرده و اجرا می‌کنیم. برنامه اجرا می‌شود، اما باز هم کار نمی‌کند. مشکل از کجاست؟


بله. تمام تنظیمات به نظر درست هستند، اما در برگه شبکه فایرباگ تعدادی خطای 404 و یا «یافت نشد»، مشاهده می‌شوند. مشکل اینجا است که مسیرهای بازگشت داده شده توسط متد Directory.GetFiles، مسیرهای مطلقی هستند؛ مانند c:\path\images\01.jpg و جهت نمایش در یک وب سایت مناسب نمی‌باشند. برای تبدیل آن‌ها به مسیرهای نسبی، اینبار کدهای متد تهیه منبع داده را به نحو ذیل ویرایش می‌کنیم:
        IList<Picture> PicturesDataSource()
        {
            var results = new List<Picture>();
            var path = HttpContext.Current.Server.MapPath("~/Images");

            foreach (var item in Directory.GetFiles(path, "*.*"))
            {
                var name = Path.GetFileName(item);
                results.Add(new Picture
                {
                    Path = "Images/" + name,
                    Title = name
                });
            }

            return results;
        }
در این کدها فقط قسمت Path ویرایش شده است تا به مسیر پوشه Images واقع در ریشه سایت اشاره کند.
اینبار اگر برنامه را اجرا کنیم، بدون مشکل کار خواهد کرد.

بنابراین در اینجا مشاهده کردیم که اگر «برنامه‌ای مبتنی بر jQuery کار نمی‌کند»، چگونه باید قدم به قدم با استفاده از فایرباگ و امکانات آن، به خطاهایی که گزارش می‌دهد و یا مسیرهایی را که یافت نشد بیان می‌کند، دقت کرد تا بتوان برنامه را عیب یابی نمود.


سؤال مهم: اجرای کدهای jQuery Ajax فوق، چه تغییری را در صفحه سبب می‌شوند؟

اگر به برگه اسکریپت‌ها در کنسول فایرباگ مراجعه کنیم، امکان قرار دادن breakpoint بر روی سطرهای کدهای جاوا اسکریپتی نمایش داده شده نیز وجود دارد:


در اینجا همانند VS.NET می‌توان برنامه را در مرورگر اجرا کرده و تگ‌های تصویر پویای تولید شده را پیش از اضافه شدن به صفحه، مرحله به مرحله بررسی کرد. به این ترتیب بهتر می‌توان دریافت که آیا src بازگشت داده شده از سرور فرمت صحیحی دارد یا خیر و آیا به محل مناسبی اشاره می‌کند یا نه. همچنین در برگه HTML آن، عناصر پویای اضافه شده به صفحه نیز بهتر مشخص هستند: