مطالب
آشنایی با ساختار IIS قسمت اول
در مقاله قبل در مورد نحوه ذخیره سازی در حافظه نوشتیم و به user mode و kernel mode اشاراتی کردیم که می‌توانید به آن رجوع کنید.
در این سری مقالات قصد داریم به بررسی اجزا و روند کاری موجود در IIS بپردازیم که چگونه IIS کار می‌کند و شامل چه بخش هایی می‌شود. مطمئنا آشنایی با این بخش‌ها در روند شناسایی رفتارهای وب اپلیکیشن‌ها و واکنش‌های سرور، کمک زیادی به ما خواهد کرد. در اینجا نسخه IIS7 را به عنوان مرجع در نظر گرفته‌ایم.
وب سرور IIS در عبارت مخفف Internet information services به معنی سرویس‌های اطلاعاتی اینترنت می‌باشد. IIS شامل کامپوننت‌های زیادی است که هر کدام ازآن‌ها کار خاصی را انجام میدهند؛ برای مثال گوش دادن به درخواست‌های ارسال شده به سرور، مدیریت فرآیندها Process و خواندن فایل‌های پیکربندی Configuration؛ این اجزا شامل protocol listener ،Http.sys و WSA و .. می‌شوند.
Protocol Listeners
این پروتکل‌ها به درخواست‌های رسیده گوش کرده و آن‌ها را مورد پردازش قرار می‌دهند و پاسخی را به درخواست کننده، ارسال می‌کنند. هر listener بر اساس نوع پروتکل متفاوت هست. به عنوان مثال کلاینتی، درخواست صفحه‌ای را می‌کند و http listener که به آن Http.sys می‌گویند به آن پاسخ می‌دهد. به طور پیش فرض http.sys به درخواست‌های http و https گوش فرا می‌دهد، این کامپوننت از IIS6 اضافه شده است ولی در نسخه 7 از SSL نیز پشتیبانی می‌کند.
Http.sys یا Hypertext transfer protocol stack
کار این واحد در سه مرحله دریافت درخواست، ارسال آن به واحد پردازش IIS و ارسال پاسخ به کلاینت است؛ قبل از نسخه 6 از Winsock یا windows socket api  که یک کامپوننت user-mod بود استفاده می‌شد ولی Http.sys یک کامپوننت Kernel-mod هست.

Http.sys مزایای زیر را به همراه دارد:

  • صف درخواست مد کرنل: به خاطر اینکه کرنل مستقیما درخواست‌ها را به پروسه‌های مربوطه میفرستد و اگر پروسه موجود نباشد، درخواست را در صف گذاشته تا بعدا پروسه مورد نظر آن را از صف بیرون بکشد.
  • برای درخواست‌ها یک پیش پردازش و همچنین اعمال فیلترهای امنیتی اعمال می‌گردد. 
  • عملیات کش کردن تماما در محیط کرنل مد صورت می‌گیرد؛ بدون اینکه به حالت یوزرمد سوییچ کند. مد کرنل دسترسی بسیار راحت و مستقیمی را برای استفاده از منابع دارد و لازم نیست مانند مد کاربر به لایه‌های زیرین، درخواست کاری را بدهد؛ چرا که خود مستقیما وارد عمل می‌شود و برداشته شدن واسط در سر راه، موجب افزایش عمل caching می‌شود. همچنین دسترسی به کش باعث می‌شود که مستقیما پاسخ از کش به کاربر برسد و توابع پردازشی در حافظه بارگذاری نشوند. البته این کش کردن محدودیت هایی را هم به همراه دارد:
    1. کش کرنل به صورت پیش فرض بر روی صفحات ایستا فعال شده است؛ نه برای صفحاتی با محتوای پویا که البته این مورد قابل تغییر است که نحوه این تغییر را پایینتر توضیح خواهیم داد.
    2. اگر آدرس درخواستی شامل کوئری باشد صفحه کش نخواهد شد:    http://www.site.info/postarchive.htm?id=25 
    3. برای پاسخ ازمکانیزم‌های فشرده سازی پویا استفاده شده باشد مثل gzip کش نخواهد شد
    4. صفحه درخواست شده صفحه اصلی سایت باشد کش نخواهد شد :   http://www.dotnettip.info ولی اگر درخواست بدین صورت باشه http://www.domain.com/default.htm  کش خواهد کرد.
    5. درخواست به صورت ناشناس anonymous نباشد  و نیاز به authentication داشته باشد کش نخواهد شد (یعنی در هدر شامل گزینه authorization می‌باشد).
    6. درخواست باید از نوع نسخه http1 به بعد باشد.
    7. اگر درخواست شامل Entity-body باشد کش نخواهد کرد.
    8. درخواست شامل If-Range/Range header باشد کش نمی‌شود.
    9. کل حجم response بییشتر از اندازه تعیین شده باشد کش نخواهد گردید، این اندازه در کلید ریجستری UriMaxUriBytes قرار دارد. اطلاعات بیشتر
    10. اندازه هدر بیشتر از اندازه تعیین شده باشد که عموما اندازه تعیین شده یک کیلو بایت است.
    11. کش پر باشد، کش انجام نخواهد گرفت.
    برای فعال سازی کش کرنل راهنمای زیر را دنبال کنید:
    گزینه output cache را در IIS، فعال کنید و سپس گزینه Add را بزنید. کادر add cache rule که باز شود، از شما میخواهد یکی از دو نوع کش مد کاربر و مد کرنل را انتخاب کنید و  مشخص کنید چه نوع فایل‌هایی (مثلا aspx) از این قوانین پیروری کنند و مکانیزم کش کردن به سه روش جلوگیری از کش کردن، کش زمان دار و کش بر اساس آخرین تغییر فایل انجام گردد.


    برای تعیین مقدار سایز کش response که در بالا اشاره کردیم می‌توانید در همان پنجره، گزینه edit feature settings را انتخاب کنید.


    این قسمت از مطلب که به نقل از مقاله  آقای Karol Jarkovsky در این آدرس است یک سری تست هایی با نرم افزار(Web Capacity Analysis Tool (WCAT  گرفته است که به نتایج زیر دست پیدا کرده است:
    Kernel Cache Disabled    4 clients/160 threads/30 sec      257 req/sec
    Kernel Cache Enabled     4 clients/160 threads/30 sec      553 req/sec 
    همانطور که می‌بینید نتیجه فعال سازی کش کرنل پاسخ به بیش از دو برابر درخواست در حالت غیرفعال آن است که یک عدد فوق العاده به حساب میاد.
    برای اینکه خودتان هم تست کرده باشید در این آدرس  برنامه را دانلود کنید و به دنبال فایل request.cfg بگردید و از صحت پارامترهای server و url اطمینان پیدا کنید. در گام بعدی 5 پنجره خط فرمان باز کرده و در یکی از آن‌ها دستور netsh http show cachestate را بنویسید تا تمامی وروردی‌های entry که در کش کرنل ذخیره شده اند لیست شوند. البته در اولین تست کش را غیرفعال کنید و به این ترتیب نباید چیزی نمایش داده شود. در همان پنجره فرمان wcctl –a localhost –c config.cfg –s request.cfg  را زده تا کنترلر برنامه در وضعیت listening قرار بگیرد. در 4 پنجره دیگر فرمان wcclient localhost از شاخه کلاینت را نوشته تا تست آغاز شود. بعد از انجام تست به شاخه نصب کنترلر WCAT رفته و فایل log را بخوانید و اگر دوباره دستور نمایش کش کرنل را بزنید باید خالی باشد. حالا کش را فعال کنید و دوباره عملیات تست را از سر بگیرید و اگر دستور netsh را ارسال کنید باید کش کرنل دارای ورودی باشد.
    برای تغییرات در سطح http.sys می‌توانید از ریجستری کمک بگیرید. در اینجا تعداد زیادی از تنظیمات ذخیره شده در ریجستری برای http.sys لیست شده است.
    مطالب
    افزونه نویسی برای مرورگرها : فایرفاکس : قسمت اول
    در دو مقاله پیشین ^ ، ^ به بررسی نوشتن افزونه در مرورگر کروم پرداختیم و اینبار قصد داریم همان پروژه را برای فایرفاکس پیاده کنیم. پس در مورد کدهای تکراری توضیحی داده نخواهد شد و برای فهم آن می‌توانید به دو مقاله قبلی رجوع کنید. همه‌ی ما فایرفاکس را به خوبی می‌شناسیم. اولین باری که این مرورگر آمد سرو صدای زیادی به پا کرد و بازار وسیعی از مرورگر‌ها را که در چنگ IE بود، به دست آورد . این سر و صدا بیشتر به خاطر امنیت و کارآیی بالای این مرورگر، استفاده از آخرین فناوری‌های تحت وب و دوست داشتنی برای طراحان وب بود. همچنین یکی دیگر از مهمترین ویژگی‌های آن، امکان سفارشی سازی آن با افزونه‌ها extensions یا addon بود که این ویژگی در طول این سال‌ها تغییرات زیادی به خود دیده است. در مورد افزونه نویسی برای فایرفاکس در سطح نت مطالب زیادی وجود دارند که همین پیشرفت‌های اخیر در مورد افزونه‌ها باعث شده خیلی از این مطالب به روز نباشند. اگر در مقاله پیشین فکر می‌کنید که کروم چقدر در نوشتن افزونه جذابیت دارد و امکانات خوبی را در اختیار شما می‌گذارد، الان دیگر وقت آن است که نظر خودتان را عوض کنید و فایرفاکس را نه تنها یک سرو گردن بلکه بیشتر از این حرف‌ها بالاتر بدانید.
    شرکت موزیالا برای قدرتمندی و راحتی کار طراحان یک sdk طراحی کرده است است و شما با استفاده از کدهای موجود در این sdk قادرید کارهای زیادی را انجام دهید. برای نصب این sdk باید پیش نیازهایی بر روی سیستم شما نصب باشد:
    • نصب پایتون  2.5 یا 2.6 یا 2.7 که فعلا در سایت آن، نسخه‌ی 2.7 در دسترس هست. توجه داشته باشید که هنوز برای نسخه‌ی 3 پایتون پشتیبانی صورت نگرفته است. 
    • آخرین نسخه‌ی sdk را هم می‌توانید از این آدرس  به صورت zip و یا از این آدرس به صورت tar دانلود کنید و در صورتیکه دوست دارید به سورس آن دسترسی داشته باشید یا اینکه از سورس‌های مشارکت شده یا غیر رسمی استفاده کنید، از این صفحه آن را دریافت کنید.
    بعد از دانلود sdk به شاخه‌ی bin رفته و فایل activate.bat را اجرا کنید. موقعی که فایل activate اجرا شود، باید چنین چیزی دیده شود:
    (C:\Users\aym\Downloads\addon-sdk-1.17) C:\Users\aym\Downloads\addon-sdk-1.17\bin>
    برای سیستم‌های عامل Linux,FreeBSD,OS X دستورات زیر را وارد کنید:
    اگر یک کاربر پوسته‌ی bash هستید کلمه زیر را در کنسول برای اجرای activate بزنید:
    source bin/activate
    اگر کاربر پوسته‌ی بش نیستید:
    bash bin/activate
    نهایتا باید کنسول به شکل زیر در آید یا شبیه آن:
    (addon-sdk)~/mozilla/addon-sdk >
    بعد از اینکه به کنسول آن وارد شدید، کلمه cfx را در آن تایپ کنید تا راهنمای دستورات و سوییچ‌های آن‌ها نمایش داده شوند. از این ابزار میتوان برای راه اندازی فایرفاکس و اجرای افزونه بر روی آن، پکیج کردن افزونه، دیدن مستندات و آزمون‌های واحد استفاده کرد.

    آغاز به کار
    برای شروع، فایل‌های زیادی باید ساخته شوند، ولی نگران نباشید cfx این کار را برای شما خواهد کرد. دستورات زیر را جهت ساخت یک پروژه خالی اجرا کنید:
    mkdir fxaddon
    cd fxaddon
    cfx init
    یک پوشه را در مسیری که کنسول بالا اشاره میکرد، ساختم و وارد آن شدم و با دستور cfx init دستور ساخت یک پروژه‌ی خالی را دادم و باید بعد از این دستور، یک خروجی مشابه زیر نشان بدهد:
    * lib directory created
    * data directory created
    * test directory created
    * doc directory created
    * README.md written
    * package.json written
    * test/test-main.js written
    * lib/main.js written
    * doc/main.md written
    Your sample add-on is now ready for testing:
    try "cfx test" and then "cfx run". Have fun!"
    در این پوشه یک فایل به اسم package.json هم وجود دارد که اطلاعات زیر داخلش هست:
    {
      "name": "fxaddon",
      "title": "fxaddon",
      "id": "jid1-QfyqpNby9lTlcQ",
      "description": "a basic add-on",
      "author": "",
      "license": "MPL 2.0",
      "version": "0.1"
    }
    این اطلاعات شامل نام و عنوان افزونه، توضیحی کوتاه در مورد آن، نویسنده‌ی افزونه، ورژن افزونه و ... است. این فایل دقیقا معادل manifest.json در کروم است. در افزونه نویسی‌های قدیم این فایل install.rdf نام داشت و بر پایه‌ی فرمت rdf بود. ولی در حال حاضر با تغییرات زیادی که افزونه نویسی در فایرفاکس کرده‌است، الان این فایل بر پایه یا فرمت json است. اطلاعات package را به شرح زیر تغییر می‌دهیم:
    {
      "name": "dotnettips",
      "title": ".net Tips Updater",
      "id": "jid1-QfyqpNby9lTlcQ",
      "description": "This extension keeps you updated on current activities on dotnettips.info",
      "author": "yeganehaym@gmail.com",
      "license": "MPL 2.0",
      "version": "0.1"
    }

    رابط‌های کاربری
    Action Button و Toggle Button
    فایل main.js را در دایرکتوری lib باز کنید:
    موقعی که در کروم افزونه می‌نوشتیم امکانی به اسم browser action داشتیم که در اینجا با نام action button شناخته می‌شود. در اینجا باید کدها را require کرد، همان کاری در خیلی از زبان‌ها مثلا مثل سی برای صدا  زدن سرآیندها می‌کنید. مثلا برای action button اینگونه است:
    var button= require('sdk/ui/button/action');
    نحوه‌ی استفاده هم بدین صورت است:
    buttons.ActionButton({...});
    که در بین {} خصوصیات دکمه‌ی مورد نظر نوشته می‌شود. ولی من بیشتر دوست دارم از شیء دیگری استفاده کنم. به همین جهت ما از یک مدل دیگر button که به اسم toggle button شناخته می‌شود، استفاده می‌کنیم. از آن جا که این button دارای دو حالت انتخاب (حالت فشرده شده) و غیر انتخاب (معمولی و آماده فشرده شدن توسط کلیک کاربر) است، بهترین انتخاب هست.

    کد زیر یک toggle button را برای فایرفاکس می‌سازد که با کلیک بر روی آن، صفحه‌ی popup.htm  به عنوان یک پنل روی آن رندر می‌شود:
    var tgbutton = require('sdk/ui/button/toggle');
    var panels = require("sdk/panel");
    var self = require("sdk/self");
    
    var button = tgbutton.ToggleButton({
      id: "updaterui",
      label: ".Net Updater",
      icon: {
        "16": "./icon-16.png",
        "32": "./icon-32.png",
        "64": "./icon-64.png"
      },
      onChange: handleChange
    });
    
    var panel = panels.Panel({
      contentURL: self.data.url("./popup.html"),
      onHide: handleHide
    });
    
    function handleChange(state) {
      if (state.checked) {
        panel.show({
          position: button
        });
      }
    }
    
    function handleHide() {
      button.state('window', {checked: false});
    }
    در سه خط اول، فایل‌هایی را که نیاز است Required شوند، می‌نویسیم و در یک متغیر ذخیره می‌کنیم. اگر در متغیر نریزیم مجبور هستیم همیشه هر کدی را به جای نوشتن عبارت زیر:
     tgbutton.ToggleButton
    به صورت زیر بنویسیم:
    require('sdk/ui/button/toggle').ToggleButton
    که اصلا کار جالبی نیست. اگر مسیرهای نوشته شده را از مبدا فایل zip که اکسترکت کرده‌اید، در دایرکتوری sdk در شاخه lib بررسی کنید، با دیگر موجودیت‌های sdk آشنا خواهید شد.
    در خط بعدی به تعریف یک شیء از نوع toggle button به اسم button میپردازیم و خصوصیاتی که به این دکمه داده ایم، مانند یک کد شناسایی، یک برچسب که به عنوان tooltip نمایش داده خواهد شد و آیکن‌هایی در اندازه‌های مختلف که در هرجایی کاربر آن دکمه را قرار داد، در اندازه‌ی مناسب باشد و نهایتا به تعریف یک رویداد می‌پردازیم. تابع handlechange زمانی صدا زده می‌شود که در وضعیت دکمه‌ی ایجاد شده تغییری حاصل شود. در خط بعدی شیء panel را به صورت global میسازیم. شیء self دسترسی ما را به اجزا یا فایل‌های افزونه خودمان فراهم می‌کند که در اینجا دسترسی ما به فایل html در شاخه‌ی data میسر شده است و مقدار مورد نظر را در contentURL قرار می‌دهد. نهایتا هم برای رویداد onhide تابعی را در نظر می‌گیریم تا موقعی که پنجره بسته شد بتوانیم وضعیت toggle button را به حالت قبلی بازگردانیم و حالت فشرده نباشد. چرا که این دکمه تنها با کلیک ماوس به حالت فشرده و حالت معمولی سوییچ میکند. پس اگر کاربر با کلیک بر روی صفحه‌ی مرورگر پنجره را ببندد، دکمه در همان وضعیت فشرده باقی می‌ماند.
    همانطور که گفتیم تابع handlechnage موقعی رخ میدهد که در وضعیت دکمه، تغییری رخ دهد و نمیدانیم که این وضعیت فشرده شدن دکمه هست یا از حالت فشرده خارج شده است. پس با استفاده از ویژگی checked بررسی میکنم که آیا دکمه‌ای فشرده شده یا خیر؛ اگر برابر true بود یعنی کاربر روی دکمه، کلیک کرده و دکمه به حالت فشرده رفته، پس ما هم پنل را به آن نشان می‌دهیم و خصوصیات دلخواهی را برای مشخص کردن وضعیت پنل نمایشی به آن پاس می‌کنیم. خصوصیت یا پارامترهای زیادی را می‌توان در حین ساخت پنل برای آن ارسال کرد. با استفاده از خصوصیت position محل نمایش پنجره را مشخص می‌کنیم. در صورتی که ذکر نشود پنجره در وسط مرورگر ظاهر خواهد شد.
    تابع onhide زمانی رخ میدهد که به هر دلیلی پنجره بسته شده باشد که در بالا یک نمونه‌ی آن را عرض کردیم. ولی اتفاقی که می‌افتد، وضعیت تابع را با متد state تغییر می‌دهیم و خصوصیت checked آن را false می‌کنیم. بجای پارامتر اولی، دو گزینه را میتوان نوشت؛ یکی window و دیگری tab است. اگر شما گزینه tab را جایگزین کنید، اگر در یک تب دکمه به حالت فشرده برود و به تب دیگر بروید و باعث بسته شدن پنجره بشوید، دکمه تنها در تبی که فعال است به حالت قبلی باز می‌گردد و تب اولی همچنان حالت خود را حفظ خواهد کرد پس می‌نویسیم window تا این عمل در کل پنجره اعمال شود.

    Context Menus
    برای ساخت منوی کانتکست از کد زیر استفاده می‌کنیم:
    var contextMenu = require("sdk/context-menu");
    
    var home = contextMenu.Item({
      label: "صفحه اصلی",
      data: "https://www.dntips.ir/"
    });
    var postsarchive = contextMenu.Item({
      label: "مطالب سایت",
      data: "https://www.dntips.ir/postsarchive"
    });
    
    var menuItem = contextMenu.Menu({
      label: "Open .Net Tips",
      context: contextMenu.PageContext(),
       items: [home, postsarchive],
      image: self.data.url("icon-16.png"),
      contentScript: 'self.on("click", function (node, data) {' +
                     '  window.location.href = data;' +
                     '});'
    });
    این منو هم مثل کروم دو زیر منو دارد که یکی برای باز کردن صفحه‌ی اصلی و دیگر‌ی برای باز کردن صفحه‌ی مطالب است. هر کدام یک برچسب برای نمایش متن دارند و یکی هم دیتا که برای نگهداری آدرس است. در خط بعدی منوی پدر یا والد ساخته می‌شود که با خصوصیت items، زیر منوهایش را اضافه می‌کنیم و با خصوصیت image، تصویری را در پوشه‌ی دیتا به آن معرفی می‌کنیم که اندازه‌ی آن 16 پیکسل است و دومی هم خصوصیت context است که مشخص می‌کند این گزینه در چه مواردی بر روی context menu نمایش داده شود. الان روی همه چیزی نمایش داده می‌شود. اگر گزینه، SelectionContext باشد، موقعی که متنی انتخاب شده باشد، نمایش می‌یابد. اگر SelectorContext باشد، خود شما مشخص می‌کنید بر روی چه مواردی نمایش یابد؛ مثلا عکس یا تگ p  یا هر چیز دیگری، کد زیر باعث می‌شود فقط روی عکس نمایش یابد:
    SelectorContext("img")
    کد زیر هم روی عکس و هم روی لینکی که href داشته باشد:
    SelectorContext("img,a[href]")
    موارد دیگری هم وجود دارند که میتوانید مطالب بیشتری را در مورد آن‌ها در اینجا مطالعه کنید. آخرین خصوصیت باقی مانده، content script است که می‌توانید با استفاده از جاوااسکریپت برای آن کد بنویسید.  موقعی که برای آن رویداد کلیک رخ داد، مشخص شود تابعی را صدا میزند با دو آرگومان؛ گره ای که انتخاب شده و داده‌ای که به همراه دارد که آدرس سایت است و آن را در نوار آدرس درج می‌کند.
    آن منوهایی که با متد item ایجاد شده‌اند منوهایی هستند که با کلیک کاربر اجرا می‌شوند؛ ولی والدی که با متد menu ایجاد شده است، برای منویی است که زیر منو دارد و خودش لزومی به اجرای کد ندارد. پس اگر منویی میسازید که زیرمنو ندارد و خودش قرار است کاری را انجام دهد، به صورت همان item بنویسید که پایین‌تر نمونه‌ی آن را خواهید دید.
    الان مشکلی که ایجاد می‌شود این است که موقعی که سایت را باز می‌کند، در همان تبی رخ می‌دهد که فعال است و اگر کاربر بر روی صفحه‌ی خاصی باشد، آن صفحه به سمت سایت مقصد رفته و سایت فعلی از دست میرود. روش صحیح‌تر اینست که تبی جدید بار شود و آدرس مقصد در آن نمایش یابد. پس باید از روشی استفاده کنیم که رویداد کلیک توسط کد خود افزونه مدیریت شود، تا با استفاده از شیء tab، یک تب جدید با آدرسی جدید ایجاد کنیم. پس کد را با کمی تغییر می‌نویسیم:
    var tabs = require("sdk/tabs");
    var menuItem = contextMenu.Menu({
      label: "Open .Net Tips",
      context: contextMenu.PageContext(),
       items: [home, postsarchive],
      image: self.data.url("icon-16.png"),
      contentScript: 'self.on("click", function (node, data) {' +
                     '  self.postMessage(data);' +
                     '});',
     onMessage: function (data) {
         tabs.open(data);
      }
    });
    با استفاده از postmessage، هر پارامتری را که بخواهیم ارسال می‌کنیم و بعد با استفاده از رویداد onMessage، داده‌ها را خوانده و کد خود را روی آن‌ها اجرا می‌کنیم.
    بگذارید کد زیر را هم جهت سرچ مطالب بر روی سایت پیاده کنیم: 
    var Url="https://www.dntips.ir/search?term=";
    var searchMenu = contextMenu.Item({
      label: "search for",
      context: [contextMenu.PredicateContext(checkText),contextMenu.SelectionContext()],
        image: self.data.url("icon-16.png"),
      contentScript: 'self.on("click", function () {' +
      '  var text = window.getSelection().toString();' +
                     '  if (text.length > 20)' +
                     '   text = text.substr(0, 20);' +
                     '  self.postMessage(text);'+
                    '})',
    onMessage: function (data) {
         tabs.open(Url+data);
      }
     
    });
    
    function checkText(data) {
    
           if(data.selectionText === null)
               return false;
    
           console.log('selectionText: ' + data.selectionText);
    
           //handle showing or hiding of menu items based on the text content.
           menuItemToggle(data.selectionText);
    
           return true;
    };
    
    function menuItemToggle(text){
    var searchText="جست و جو برای ";
        searchMenu.label=searchText+text;
    
    };
    در ساخت این منو، ما از ContextSelection استفاده کرده‌ایم. بدین معنی که موقعی که چیزی روی صفحه انتخاب شد، این منو ظاهر شود و گزینه‌ی دیگری که در کنارش هست، گزینه contextMenu.PredicateContext وظیفه دارد تابعی که به عنوان آرگومان به آن دادیم را موقعی که منو کانتکست ایجاد شد، صدا بزند و اینگونه میتوانیم بر حسب اطلاعات کانتکست، منوی خود را ویرایش کنیم. مثلا من دوست دارم موقعی که متنی انتخاب می‌شود و راست کلیک می‌کنم گزینه‌ی "جست و جو برای..." نمایش داده شود و به جای ... کلمه‌ی انتخاب شده نمایش یابد. به شکل زیر دقت کنید. این چیزی است که ما قرار است ایجاد کنیم:
    در کل موقع ایجاد منو تابع checkText اجرا شده و متن انتخابی را خوانده به عنوان یک آرگومان برای تابع menuItemToggle ارسال می‌کند و به رشته "جست و جو برای" می‌چسباند. در خود پارامترهای آیتم اصلی، گزینه content scrip، با استفاده از جاوااسکریپت، متن انتخاب شده را دریافت کرده و با استفاده از متد postmessage برای تابع  onMessage ارسال کرده و با ساخت یک تب و چسباندن عبارت به آدرس جست و جو سایت، کاربر را به صفحه مورد نظر هدایت کرده و عمل جست و جو در سایت انجام می‌گیرد.

    در قسمت آینده موارد بیشتری را در مورد افزونه نویسی در فایرفاکس بررسی خواهیم کرد و افزونه را تکمیل خواهیم کرد
    مطالب
    Blazor 5x - قسمت 29 - برنامه‌ی Blazor WASM - یک تمرین: رزرو کردن یک اتاق انتخابی


    در قسمت قبل که لیست اتاق‌های دریافتی از Web API را نمایش دادیم، هرکدام از آن‌ها، به همراه یک دکمه‌ی Book هم هستند (تصویر فوق) که هدف از آن، فراهم آوردن امکان رزرو کردن آن اتاق، توسط کاربران سایت است. این قسمت را می‌توان به عنوان تمرینی جهت یادآوری مراحل مختلف تهیه‌ی یک Web API و قسمت‌های سمت کلاینت آن، تکمیل کرد.



    تهیه موجودیت و مدل متناظر با صفحه‌ی ثبت رزرو یک اتاق

    تا اینجا در برنامه‌ی سمت کلاینت، زمانیکه بر روی دکمه‌ی Go صفحه‌ی اول کلیک می‌کنیم، تاریخ شروع رزرو و تعداد روز مدنظر، به صفحه‌ی مشاهده‌ی لیست اتاق‌ها ارسال می‌شود. اکنون می‌خواهیم در این لیست اتاق‌های نمایش داده شده، اگر بر روی لینک Book اتاقی کلیک شد، به صفحه‌ی اختصاصی رزرو آن اتاق هدایت شویم (مانند تصویر فوق). به همین جهت نیاز است موجودیت متناظر با اطلاعاتی را که قرار است از کاربر دریافت کنیم، به صورت زیر به پروژه‌ی BlazorServer.Entities اضافه کنیم:
    using System;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    
    namespace BlazorServer.Entities
    {
        public class RoomOrderDetail
        {
            public int Id { get; set; }
    
            [Required]
            public string UserId { get; set; }
    
            [Required]
            public string StripeSessionId { get; set; }
    
            public DateTime CheckInDate { get; set; }
    
            public DateTime CheckOutDate { get; set; }
    
            public DateTime ActualCheckInDate { get; set; }
    
            public DateTime ActualCheckOutDate { get; set; }
    
            public long TotalCost { get; set; }
    
            public int RoomId { get; set; }
    
            public bool IsPaymentSuccessful { get; set; }
    
            [Required]
            public string Name { get; set; }
    
            [Required]
            public string Email { get; set; }
    
            public string Phone { get; set; }
    
            [ForeignKey("RoomId")]
            public HotelRoom HotelRoom { get; set; }
    
            public string Status { get; set; }
        }
    }
    در اینجا مشخصات شروع و پایان رزرو یک اتاق مشخص و مشخصات کاربری که قرار است این فرم را پر کند، مشاهده می‌کنید که Status یا وضعیت آن، در پروژه‌ی مشترک BlazorServer.Common به صورت زیر تعریف می‌شود:
    namespace BlazorServer.Common
    {
        public static class BookingStatus
        {
            public const string Pending = "Pending";
            public const string Booked = "Booked";
            public const string CheckedIn = "CheckedIn";
            public const string CheckedOutCompleted = "CheckedOut";
            public const string NoShow = "NoShow";
            public const string Cancelled = "Cancelled";
        }
    }
    پس از این تعاریف، DbSet آن‌را نیز به ApplicationDbContext اضافه می‌کنیم:
    namespace BlazorServer.DataAccess
    {
        public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
        {
            public DbSet<RoomOrderDetail> RoomOrderDetails { get; set; }
    
            // ...
        }
    }
    بنابراین مرحله‌ی بعدی، ایجاد و اجرای Migrations متناظر با این جدول جدید است. برای این منظور با استفاده از خط فرمان به پوشه‌ی BlazorServer.DataAccess وارد شده و دستورات زیر را اجرا می‌کنیم:
    dotnet tool update --global dotnet-ef --version 5.0.4
    dotnet build
    dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddRoomOrderDetails --context ApplicationDbContext
    dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context ApplicationDbContext
    این دستورات به پروژه‌ی آغازین WebApi اشاره می‌کنند که قرار است از طریق سرویسی، با بانک اطلاعاتی ارتباط برقرار کند.

    پس از تعریف یک موجودیت، یک DTO متناظر با آن‌را که جهت مدلسازی UI از آن استفاده خواهیم کرد، در پروژه‌ی BlazorServer.Models ایجاد می‌کنیم:
    using System;
    using System.ComponentModel.DataAnnotations;
    
    namespace BlazorServer.Models
    {
        public class RoomOrderDetailsDTO
        {
            public int Id { get; set; }
    
            [Required]
            public string UserId { get; set; }
    
            [Required]
            public string StripeSessionId { get; set; }
    
            [Required]
            public DateTime CheckInDate { get; set; }
    
            [Required]
            public DateTime CheckOutDate { get; set; }
    
            public DateTime ActualCheckInDate { get; set; }
    
            public DateTime ActualCheckOutDate { get; set; }
    
            [Required]
            public long TotalCost { get; set; }
    
            [Required]
            public int RoomId { get; set; }
    
            public bool IsPaymentSuccessful { get; set; }
    
            [Required]
            public string Name { get; set; }
    
            [Required]
            public string Email { get; set; }
    
            public string Phone { get; set; }
    
            public HotelRoomDTO HotelRoomDTO { get; set; }
    
            public string Status { get; set; }
        }
    }
    و همچنین در پروژه‌ی BlazorServer.Models.Mappings، نگاشت دوطرفه‌ی AutoMapper آن‌را نیز برقرار می‌کنیم؛ تا در حین تبدیل اطلاعات بین این دو، نیازی به تکرار سطرهای مقدار دهی اطلاعات خواص، نباشد:
    namespace BlazorServer.Models.Mappings
    {
        public class MappingProfile : Profile
        {
            public MappingProfile()
            {
                // ... 
                CreateMap<RoomOrderDetail, RoomOrderDetailsDTO>().ReverseMap(); // two-way mapping
            }
        }
    }


    ایجاد سرویسی برای کار با جدول RoomOrderDetails

    در برنامه‌ی سمت کلاینت برای کار با بانک اطلاعاتی، دیگر نمی‌توان از سرویس‌های سمت سرور به صورت مستقیم استفاده کرد. به همین جهت آن‌ها را از طریق یک Web API endpoint، در معرض دید استفاده کننده قرار می‌دهیم. اما پیش از اینکار، سرویس سمت سرور Web API باید بتواند با سرویس دسترسی به اطلاعات جدول RoomOrderDetails، کار کند. بنابراین در ادامه این سرویس را تهیه می‌کنیم:
    namespace BlazorServer.Services
    {
        public interface IRoomOrderDetailsService
        {
            Task<RoomOrderDetailsDTO> CreateAsync(RoomOrderDetailsDTO details);
    
            Task<List<RoomOrderDetailsDTO>> GetAllRoomOrderDetailsAsync();
    
            Task<RoomOrderDetailsDTO> GetRoomOrderDetailAsync(int roomOrderId);
    
            Task<bool> IsRoomBookedAsync(int RoomId, DateTime checkInDate, DateTime checkOutDate);
    
            Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(int id);
    
            Task<bool> UpdateOrderStatusAsync(int RoomOrderId, string status);
        }
    }
    که به صورت زیر پیاده سازی می‌شود:
    namespace BlazorServer.Services
    {
        public class RoomOrderDetailsService : IRoomOrderDetailsService
        {
            private readonly ApplicationDbContext _dbContext;
            private readonly IMapper _mapper;
            private readonly IConfigurationProvider _mapperConfiguration;
    
            public RoomOrderDetailsService(ApplicationDbContext dbContext, IMapper mapper)
            {
                _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
                _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
                _mapperConfiguration = mapper.ConfigurationProvider;
            }
    
            public async Task<RoomOrderDetailsDTO> CreateAsync(RoomOrderDetailsDTO details)
            {
                var roomOrder = _mapper.Map<RoomOrderDetail>(details);
                roomOrder.Status = BookingStatus.Pending;
                var result = await _dbContext.RoomOrderDetails.AddAsync(roomOrder);
                await _dbContext.SaveChangesAsync();
                return _mapper.Map<RoomOrderDetailsDTO>(result.Entity);
            }
    
            public Task<List<RoomOrderDetailsDTO>> GetAllRoomOrderDetailsAsync()
            {
                return _dbContext.RoomOrderDetails
                                .Include(roomOrderDetail => roomOrderDetail.HotelRoom)
                                .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration)
                                .ToListAsync();
            }
    
            public async Task<RoomOrderDetailsDTO> GetRoomOrderDetailAsync(int roomOrderId)
            {
                var roomOrderDetailsDTO = await _dbContext.RoomOrderDetails
                                                .Include(u => u.HotelRoom)
                                                    .ThenInclude(x => x.HotelRoomImages)
                                                .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration)
                                                .FirstOrDefaultAsync(u => u.Id == roomOrderId);
    
                roomOrderDetailsDTO.HotelRoomDTO.TotalDays =
                    roomOrderDetailsDTO.CheckOutDate.Subtract(roomOrderDetailsDTO.CheckInDate).Days;
                return roomOrderDetailsDTO;
            }
    
            public Task<bool> IsRoomBookedAsync(int RoomId, DateTime checkInDate, DateTime checkOutDate)
            {
                return _dbContext.RoomOrderDetails
                    .AnyAsync(
                        roomOrderDetail =>
                            roomOrderDetail.RoomId == RoomId &&
                            roomOrderDetail.IsPaymentSuccessful &&
                            (
                                (checkInDate < roomOrderDetail.CheckOutDate && checkInDate > roomOrderDetail.CheckInDate) ||
                                (checkOutDate > roomOrderDetail.CheckInDate && checkInDate < roomOrderDetail.CheckInDate)
                            )
                    );
            }
    
            public Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(int id)
            {
                throw new NotImplementedException();
            }
    
            public Task<bool> UpdateOrderStatusAsync(int RoomOrderId, string status)
            {
                throw new NotImplementedException();
            }
        }
    }
    توضیحات:
    - از متد CreateAsync برای تبدیل مدل فرم ثبت اطلاعات، به یک رکورد جدول RoomOrderDetails، استفاده می‌کنیم.
    - متد GetAllRoomOrderDetailsAsync، لیست تمام سفارش‌های ثبت شده را بازگشت می‌دهد.
    - متد GetRoomOrderDetailAsync بر اساس شماره اتاقی که دریافت می‌کند، لیست سفارشات آن اتاق خاص را بازگشت می‌دهد. این لیست به علت استفاده از Include‌های تعریف شده، به همراه مشخصات اتاق و همچنین تصاویر مرتبط با آن اتاق نیز هست.
    - متد IsRoomBookedAsync بر اساس شماره اتاق و بازه‌ی زمانی درخواستی توسط یک کاربر مشخص می‌کند که آیا اتاق خالی شده‌است یا خیر؟

    پس از تعریف این سرویس، به کلاس آغازین پروژه‌ی Web API مراجعه کرده و آن‌را به سیستم تزریق وابستگی‌ها، معرفی می‌کنیم:
    namespace BlazorWasm.WebApi
    {
        public class Startup
        {
            // ...
    
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddScoped<IRoomOrderDetailsService, RoomOrderDetailsService>();
                // ...
     
     
    تشکیل سرویس ابتدایی کار با RoomOrderDetails در پروژه‌ی WASM

    در ادامه، تعاریف خالی سرویس سمت کلاینت کار با RoomOrderDetails  را به پروژه‌ی WASM اضافه می‌کنیم. تکمیل این سرویس را به قسمت بعدی واگذار خواهیم کرد:
    namespace BlazorWasm.Client.Services
    {
        public interface IClientRoomOrderDetailsService
        {
            Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(RoomOrderDetailsDTO details);
            Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details);
        }
    }
    با این پیاده سازی ابتدایی:
    namespace BlazorWasm.Client.Services
    {
        public class ClientRoomOrderDetailsService : IClientRoomOrderDetailsService
        {
            private readonly HttpClient _httpClient;
    
            public ClientRoomOrderDetailsService(HttpClient httpClient)
            {
                _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
            }
    
            public Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(RoomOrderDetailsDTO details)
            {
                throw new NotImplementedException();
            }
    
            public Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details)
            {
                throw new NotImplementedException();
            }
        }
    }
    که این مورد نیز باید به نحو زیر به سیستم تزریق وابستگی‌های برنامه‌ی سمت کلاینت در فایل Program.cs آن اضافه شود:
    namespace BlazorWasm.Client
    {
        public class Program
        {
            public static async Task Main(string[] args)
            {
                var builder = WebAssemblyHostBuilder.CreateDefault(args);
                // ...
                builder.Services.AddScoped<IClientRoomOrderDetailsService, ClientRoomOrderDetailsService>();
                // ...
            }
        }
    }


    تعریف مدل فرم ثبت اطلاعات سفارش

    پس از تدارک مقدمات فوق، اکنون می‌توانیم کار تکمیل فرم ثبت اطلاعات سفارش را شروع کنیم. به همین جهت مدل مخصوص آن‌را در برنامه‌ی سمت کلاینت به صورت زیر تشکیل می‌دهیم:
    using BlazorServer.Models;
    
    namespace BlazorWasm.Client.Models.ViewModels
    {
        public class HotelRoomBookingVM
        {
            public RoomOrderDetailsDTO OrderDetails { get; set; }
        }
    }


    تعریف کامپوننت جدید RoomDetails و مقدار دهی اولیه‌ی مدل آن

    در ادامه فایل جدید BlazorWasm.Client\Pages\HotelRooms\RoomDetails.razor را ایجاد کرده و به صورت زیر مقدار دهی اولیه می‌کنیم:
    @page "/hotel/room-details/{Id:int}"
    
    @inject IJSRuntime JsRuntime
    @inject ILocalStorageService LocalStorage
    @inject IClientHotelRoomService HotelRoomService
    
    @if (HotelBooking?.OrderDetails?.HotelRoomDTO?.HotelRoomImages == null)
    {
        <div class="spinner"></div>
    }
    else
    {
    
    }
    
    @code {
        [Parameter]
        public int? Id { get; set; }
    
        HotelRoomBookingVM HotelBooking  = new HotelRoomBookingVM();
        int NoOfNights = 1;
    
        protected override async Task OnInitializedAsync()
        {
            try
            {
                HotelBooking.OrderDetails = new RoomOrderDetailsDTO();
                if (Id != null)
                {
                    if (await LocalStorage.GetItemAsync<HomeVM>(ConstantKeys.LocalInitialBooking) != null)
                    {
                        var roomInitialInfo = await LocalStorage.GetItemAsync<HomeVM>(ConstantKeys.LocalInitialBooking);
                        HotelBooking.OrderDetails.HotelRoomDTO = await HotelRoomService.GetHotelRoomDetailsAsync(
                            Id.Value, roomInitialInfo.StartDate, roomInitialInfo.EndDate);
                        NoOfNights = roomInitialInfo.NoOfNights;
                        HotelBooking.OrderDetails.CheckInDate = roomInitialInfo.StartDate;
                        HotelBooking.OrderDetails.CheckOutDate = roomInitialInfo.EndDate;
                        HotelBooking.OrderDetails.HotelRoomDTO.TotalDays = roomInitialInfo.NoOfNights;
                        HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount =
                            roomInitialInfo.NoOfNights * HotelBooking.OrderDetails.HotelRoomDTO.RegularRate;
                    }
                    else
                    {
                        HotelBooking.OrderDetails.HotelRoomDTO = await HotelRoomService.GetHotelRoomDetailsAsync(
                            Id.Value, DateTime.Now, DateTime.Now.AddDays(1));
                        NoOfNights = 1;
                        HotelBooking.OrderDetails.CheckInDate = DateTime.Now;
                        HotelBooking.OrderDetails.CheckOutDate = DateTime.Now.AddDays(1);
                        HotelBooking.OrderDetails.HotelRoomDTO.TotalDays = 1;
                        HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount =
                            HotelBooking.OrderDetails.HotelRoomDTO.RegularRate;
                    }
                }
            }
            catch (Exception e)
            {
                await JsRuntime.ToastrError(e.Message);
            }
        }
    }
    - در ابتدا مسیریابی کامپوننت جدید RoomDetails را مشخص کرد‌ه‌ایم که یک Id را می‌پذیرد که همان Id اتاق انتخاب شده‌ی توسط کاربر است. به همین جهت پارامتر عمومی متناظری با آن هم در قسمت کدهای کامپوننت تعریف شده‌است.
    - سپس سرویس توکار IJSRuntime به کامپوننت تزریق شده‌است تا توسط آن و Toastr، بتوان خطاهایی را به کاربر نمایش داد.
    - از سرویس ILocalStorageService برای دسترسی به اطلاعات شروع به رزرو شخص و تعداد روز مدنظر او استفاده می‌کنیم که در قسمت قبل آن‌را مقدار دهی کردیم.
    - همچنین از سرویس IClientHotelRoomService که آن‌را نیز در قسمت قبل افزودیم، برای فراخوانی متد GetHotelRoomDetailsAsync آن استفاده کرده‌ایم.

    در روال آغازین OnInitializedAsync، اگر Id تنظیم شده بود، یعنی کاربر به درستی وارد این صفحه شده‌است. سپس بررسی می‌کنیم که آیا اطلاعاتی از درخواست ابتدایی او در Local Storage مرورگر وجود دارد یا خیر؟ اگر این اطلاعات وجود داشته باشد، بر اساس آن، بازه‌ی تاریخی دقیقی را می‌توان تشکیل داد و اگر خیر، این بازه را از امروز، به مدت 1 روز درنظر می‌گیریم.
    پس از پایان کار متد OnInitializedAsync، چون اجزای HotelBooking مقدار دهی کامل شده‌اند، نمایش loading ابتدای کامپوننت، متوقف شده و قسمت else شرط نوشته شده اجرا می‌شود؛ یعنی اصل UI فرم نمایان خواهد شد.

    در قسمت قبل، متد GetHotelRoomDetailsAsync را تکمیل نکردیم؛ چون به آن نیازی نداشتیم و فقط قصد داشتیم تا لیست تمام اتاق‌ها را نمایش دهیم. اما در اینجا برای تکمیل کدهای آغازین کامپوننت RoomDetails، متد دریافت اطلاعات یک اتاق را نیز تکمیل می‌کنیم تا توسط آن بتوان در این کامپوننت نیز جزئیات اتاق انتخابی را نمایش داد:
    namespace BlazorWasm.Client.Services
    {
        public class ClientHotelRoomService : IClientHotelRoomService
        {
            private readonly HttpClient _httpClient;
    
            public ClientHotelRoomService(HttpClient httpClient)
            {
                _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
            }
    
            public Task<HotelRoomDTO> GetHotelRoomDetailsAsync(int roomId, DateTime checkInDate, DateTime checkOutDate)
            {
                // How to url-encode query-string parameters properly
                var uri = new UriBuilderExt(new Uri(_httpClient.BaseAddress, $"/api/hotelroom/{roomId}"))
                                .AddParameter("checkInDate", $"{checkInDate:yyyy'-'MM'-'dd}")
                                .AddParameter("checkOutDate", $"{checkOutDate:yyyy'-'MM'-'dd}")
                                .Uri;
                return _httpClient.GetFromJsonAsync<HotelRoomDTO>(uri);
            }
    
            public Task<IEnumerable<HotelRoomDTO>> GetHotelRoomsAsync(DateTime checkInDate, DateTime checkOutDate)
            {
               // ...
            }
        }
    }

    اتصال مدل کامپوننت RoomDetails به فرم ثبت سفارش آن

    تا اینجا مدل فرم را مقدار دهی اولیه کردیم. اکنون می‌توانیم قسمت else شرط نوشته شده را تکمیل کرده و در قسمتی از آن، مشخصات اتاق جاری را نمایش دهیم و در قسمتی دیگر، فرم ثبت سفارش را تکمیل کنیم.
    الف) نمایش مشخصات اتاق جاری
    در کامپوننت جاری با استفاده از خواص مقدار دهی اولیه شده‌ی شیء HotelBooking.OrderDetails.HotelRoomDTO، می‌توان جزئیات اتاق انتخابی را نمایش داد که نمونه‌ای از آن‌را در قسمت قبل هم مشاهده کردید:
    @if (HotelBooking?.OrderDetails?.HotelRoomDTO?.HotelRoomImages == null)
    {
        <div class="spinner"></div>
    }
    else
    {
        <div class="mt-4 mx-4 px-0 px-md-5 mx-md-5">
            <div class="row p-2 my-3 " style="border-radius:20px; ">
                <div class="col-12 col-lg-7 p-4" style="border: 1px solid gray">
                    <div class="row px-2 text-success border-bottom">
                        <div class="col-8 py-1"><p style="font-size:x-large;margin:0px;">Selected Room</p></div>
                        <div class="col-4 p-0"><a href="hotel/rooms" class="btn btn-secondary btn-block">Back to Room's</a></div>
                    </div>
                    <div class="row">
                        <div class="col-6">
                            <div id="" class="carousel slide mb-4 m-md-3 m-0 pt-3 pt-md-0" data-ride="carousel">
                                <div id="carouselExampleIndicators" class="carousel slide" data-ride="carousel">
                                    <ol class="carousel-indicators">
                                        <li data-target="#carouselExampleIndicators" data-slide-to="0" class="active"></li>
                                        <li data-target="#carouselExampleIndicators" data-slide-to="1"></li>
                                    </ol>
                                    <div class="carousel-inner">
                                        <div class="carousel-item active">
                                            <img class="d-block w-100" src="images/slide1.jpg" alt="First slide">
                                        </div>
                                    </div>
                                    <a class="carousel-control-prev" href="#carouselExampleIndicators" role="button" data-slide="prev">
                                        <span class="carousel-control-prev-icon" aria-hidden="true"></span>
                                        <span class="sr-only">Previous</span>
                                    </a>
                                    <a class="carousel-control-next" href="#carouselExampleIndicators" role="button" data-slide="next">
                                        <span class="carousel-control-next-icon" aria-hidden="true"></span>
                                        <span class="sr-only">Next</span>
                                    </a>
                                </div>
                            </div>
                        </div>
                        <div class="col-6">
                            <span class="float-right pt-4">
                                <span class="float-right">Occupancy : @HotelBooking.OrderDetails.HotelRoomDTO.Occupancy adults </span><br />
                                <span class="float-right pt-1">Size : @HotelBooking.OrderDetails.HotelRoomDTO.SqFt sqft</span><br />
                                <h4 class="text-warning font-weight-bold pt-5">
                                    <span style="border-bottom:1px solid #ff6a00">
                                        @HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount.ToString("#,#.00#;(#,#.00#)")
                                    </span>
                                </h4>
                                <span class="float-right">Cost for @HotelBooking.OrderDetails.HotelRoomDTO.TotalDays nights</span>
                            </span>
                        </div>
                    </div>
                    <div class="row p-2">
                        <div class="col-12">
                            <p class="card-title text-warning" style="font-size:xx-large">@HotelBooking.OrderDetails.HotelRoomDTO.Name</p>
                            <p class="card-text" style="font-size:large">
                                @((MarkupString)@HotelBooking.OrderDetails.HotelRoomDTO.Details)
                            </p>
                        </div>
    
                    </div>
                </div>
    }
    ب) نمایش فرم متصل به مدل کامپوننت
    قسمت دوم UI کامپوننت جاری، نمایش فرم زیر است که اجزای مختلف آن به فیلد HotelBooking متصل شده‌اند:
    @if (HotelBooking?.OrderDetails?.HotelRoomDTO?.HotelRoomImages == null)
    {
        <div class="spinner"></div>
    }
    else
    {
      // ...
                 
                <div class="col-12 col-lg-5 p-4 2 mt-4 mt-md-0" style="border: 1px solid gray;">
                    <EditForm Model="HotelBooking" class="container" OnValidSubmit="HandleCheckout">
                        <div class="row px-2 text-success border-bottom"><div class="col-7 py-1"><p style="font-size:x-large;margin:0px;">Enter Details</p></div></div>
    
                        <div class="form-group pt-2">
                            <label class="text-warning">Name</label>
                            <InputText @bind-Value="HotelBooking.OrderDetails.Name" type="text" class="form-control" />
                        </div>
                        <div class="form-group pt-2">
                            <label class="text-warning">Phone</label>
                            <InputText @bind-Value="HotelBooking.OrderDetails.Phone" type="text" class="form-control" />
                        </div>
                        <div class="form-group">
                            <label class="text-warning">Email</label>
                            <InputText @bind-Value="HotelBooking.OrderDetails.Email" type="text" class="form-control" />
                        </div>
                        <div class="form-group">
                            <label class="text-warning">Check in Date</label>
                            <InputDate @bind-Value="HotelBooking.OrderDetails.CheckInDate" type="date" disabled class="form-control" />
                        </div>
                        <div class="form-group">
                            <label class="text-warning">Check Out Date</label>
                            <InputDate @bind-Value="HotelBooking.OrderDetails.CheckOutDate" type="date" disabled class="form-control" />
                        </div>
                        <div class="form-group">
                            <label class="text-warning">No. of nights</label>
                            <select class="form-control" value="@NoOfNights" @onchange="HandleNoOfNightsChange">
                                @for (var i = 1; i <= 10; i++)
                                {
                                    if (i == NoOfNights)
                                    {
                                        <option value="@i" selected="selected">@i</option>
                                    }
                                    else
                                    {
                                        <option value="@i">@i</option>
                                    }
                                }
                            </select>
                        </div>
                        <div class="form-group">
                            <button type="submit" class="btn btn-success form-control">Checkout Now</button>
                        </div>
                    </EditForm>
                </div>
            </div>
        </div>
    }
    در این فرم دو روال رویدادگردان زیر نیز مورد استفاده هستند:
    @code {
        // ...
    
        private async Task HandleNoOfNightsChange(ChangeEventArgs e)
        {
            NoOfNights = Convert.ToInt32(e.Value.ToString());
            HotelBooking.OrderDetails.HotelRoomDTO = await HotelRoomService.GetHotelRoomDetailsAsync(
                Id.Value,
                HotelBooking.OrderDetails.CheckInDate,
                HotelBooking.OrderDetails.CheckInDate.AddDays(NoOfNights));
    
            HotelBooking.OrderDetails.CheckOutDate = HotelBooking.OrderDetails.CheckInDate.AddDays(NoOfNights);
            HotelBooking.OrderDetails.HotelRoomDTO.TotalDays = NoOfNights;
            HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount =
                    NoOfNights * HotelBooking.OrderDetails.HotelRoomDTO.RegularRate;
        }
    
        private async Task HandleCheckout()
        {
            if (!await HandleValidation())
            {
                return;
            }
        }
    
        private async Task<bool> HandleValidation()
        {
            if (string.IsNullOrEmpty(HotelBooking.OrderDetails.Name))
            {
                await JsRuntime.ToastrError("Name cannot be empty");
                return false;
            }
    
            if (string.IsNullOrEmpty(HotelBooking.OrderDetails.Phone))
            {
                await JsRuntime.ToastrError("Phone cannot be empty");
                return false;
            }
    
            if (string.IsNullOrEmpty(HotelBooking.OrderDetails.Email))
            {
                await JsRuntime.ToastrError("Email cannot be empty");
                return false;
            }
            return true;
        }
    }
    - کاربر اگر تعداد شب‌های اقامت را از طریق دارپ‌داون فرم تغییر داد، در روال رویدادگردان HandleNoOfNightsChange، محاسبات جدیدی را بر این اساس انجام می‌دهیم؛ چون هزینه و سایر مشخصات جزئیات اتاق نمایش داده شده، باید تغییر کنند.
    - همچنین کدهای ابتدایی HandleCheckout را که برای ثبت نهایی اطلاعات فرم است، تهیه کرده‌ایم. البته در این قسمت این مورد را فقط محدود به اعتبارسنجی دستی و سفارشی که در متد HandleValidation مشاهده می‌کنید، کرده‌ایم. این روش دستی را نیز می‌توان برای تعریف منطق اعتبارسنجی یک فرم بکار برد و آن‌را توسط کدهای #C تکمیل کرد. البته باید درنظر داشت که data annotation validator توکار، هنوز از اعتبارسنجی خواص تو در تو، پشتیبانی نمی‌کند. به همین جهت است که در اینجا خودمان این اعتبارسنجی را به صورت دستی تعریف کرده‌ایم.


    کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-29.zip
    مطالب
    امکان ساخت برنامه‌های دسکتاپ چندسکویی Blazor در دات نت 6
    در این مطلب، روش ساخت یک برنامه‌ی دسکتاپ چندسکویی Blazor 6x را که امکان به اشتراک گذاری کدهای خود را با یک برنامه‌ی WinForms دارد، بررسی خواهیم کرد.


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

    در ابتدا دو پوشه‌ی جدید BlazorServerApp و WinFormsApp را ایجاد می‌کنیم. سپس از طریق خط فرمان در اولی دستور dotnet new blazorserver و در دومی دستور dotnet new winforms را اجرا می‌کنیم تا دو برنامه‌ی خالی Blazor Server و همچنین Windows Forms، ایجاد شوند. برنامه‌ی WinForms ایجاد شده مبتنی بر NET Core. و یا همان NET 6x. است؛ بجای اینکه مبتنی بر دات نت فریم‌ورک 4x باشد.


    ایجاد یک پروژه‌ی کتابخانه‌ی Razor

    چون می‌خواهیم کدهای برنامه‌ی BlazorServerApp ما در برنامه‌ی WinForms قابل استفاده باشد، نیاز است فایل‌های اصلی آن‌را به یک پروژه‌ی razor class library منتقل کنیم. به همین جهت برای این پروژه‌، یک پوشه‌ی جدید را به نام BlazorClassLibrary ایجاد کرده و درون آن دستور dotnet new razorclasslib را اجرا می‌کنیم.


    انتقال فایل‌های پروژه‌ی Blazor به پروژه‌ی کتابخانه‌ی Razor

    در ادامه این فایل‌ها را از پروژه‌ی BlazorServerApp به پروژه‌ی BlazorClassLibrary منتقل می‌کنیم:
    - کل پوشه‌ی Data
    - کل پوشه‌ی Pages
    - کل پوشه‌ی Shared
    - فایل App.razor
    - فایل Imports.razor_
    - کل پوشه‌ی wwwroot

    پس از اینکار، نیاز است فایل csproj کتابخانه‌ی class lib را اندکی ویرایش کرد تا بتواند فایل‌های اضافه شده را کامپایل کند:
    <Project Sdk="Microsoft.NET.Sdk.Razor">
      <PropertyGroup>
        <AddRazorSupportForMvc>true</AddRazorSupportForMvc>
      </PropertyGroup>
    
      <ItemGroup>
        <FrameworkReference Include="Microsoft.AspNetCore.App" />
      </ItemGroup>
    </Project>
    - چون برنامه از نوع Blazor Server است، ارجاعی به AspNetCore را نیاز دارد و همچنین برای فایل‌های cshtml آن نیز باید AddRazorSupportForMvc را به true تنظیم کرد.
    - به علاوه فایل Error.cshtml.cs انتقالی، نیاز به افزودن فضای نام using Microsoft.Extensions.Logging را خواهد داشت.
    - در فایل Imports.razor_ انتقالی نیاز است دو using آخر آن‌را که به BlazorServerApp قبلی اشاره می‌کنند، به BlazorClassLibrary جدید ویرایش کنیم:
    @using BlazorClassLibrary
    @using BlazorClassLibrary.Shared
    - این تغییر فضای نام جدید، شامل ابتدای فایل BlazorClassLibrary\Pages\_Host.cshtml انتقالی هم می‌شود:
    @namespace BlazorClassLibrary.Pages
    - چون wwwroot را نیز به class library منتقل کرده‌ایم، جهت اصلاح مسیر فایل‌های css استفاده شده‌ی در برنامه، فایل BlazorClassLibrary\Pages\_Layout.cshtml را گشوده و تغییر زیر را اعمال می‌کنیم:
    <link rel="stylesheet" href="_content/BlazorClassLibrary/css/bootstrap/bootstrap.min.css" />
    <link href="_content/BlazorClassLibrary/css/site.css" rel="stylesheet" />
    در مورد این مسیر ویژه، در مطلب «روش ایجاد پروژه‌ها‌ی کتابخانه‌ای کامپوننت‌های Blazor» بیشتر بحث شده‌است.


    پس از این تغییرات، برای اینکه برنامه‌ی BlazorServerApp موجود، به کار خود ادامه دهد، نیاز است ارجاعی از پروژه‌ی class lib را به فایل csproj آن اضافه کنیم:
    <Project Sdk="Microsoft.NET.Sdk.Web">
      <ItemGroup>
        <ProjectReference Include="..\BlazorClassLibrary\BlazorClassLibrary.csproj" />
      </ItemGroup>
    </Project>
    اکنون جهت آزمایش برنامه‌ی Blazor Server، یکبار دستور dotnet run را در ریشه‌ی آن اجرا می‌کنیم تا مطمئن شویم انتقالات صورت گرفته، سبب کار افتادن آن نشده‌اند.


    ویرایش برنامه‌ی WinForms جهت اجرای کدهای Blazor

    تا اینجا برنامه‌ی Blazor Server ما تمام فایل‌های مورد نیاز خود را از BlazorClassLibrary دریافت می‌کند و بدون مشکل اجرا می‌شود. در ادامه می‌خواهیم کار هاست این class lib را در برنامه‌ی WinForms نیز انجام دهیم. به همین جهت در ابتدا ارجاعی را به class lib به آن اضافه می‌کنیم:
    <Project Sdk="Microsoft.NET.Sdk">
      <ItemGroup>
        <ProjectReference Include="..\BlazorClassLibrary\BlazorClassLibrary.csproj" />
      </ItemGroup>
    </Project>
    سپس کامپوننت جدید WebView را به پروژه‌ی WinForms اضافه می‌کنیم:
    <Project Sdk="Microsoft.NET.Sdk">
      <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.Components.WebView.WindowsForms" Version="6.0.101-preview.11.2349" />
      </ItemGroup>
    </Project>

    در ادامه نیاز است فایل Form1.Designer.cs را به صورت دستی جهت افزودن این WebView اضافه شده، تغییر داد:
    namespace WinFormsApp;
    
    partial class Form1
    {
        private void InitializeComponent()
        {
          this.blazorWebView1 = new Microsoft.AspNetCore.Components.WebView.WindowsForms.BlazorWebView();
    
          this.SuspendLayout();
    
          this.blazorWebView1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 
                | System.Windows.Forms.AnchorStyles.Left) 
                | System.Windows.Forms.AnchorStyles.Right)));
          this.blazorWebView1.Location = new System.Drawing.Point(13, 181);
          this.blazorWebView1.Name = "blazorWebView1";
          this.blazorWebView1.Size = new System.Drawing.Size(775, 257);
          this.blazorWebView1.TabIndex = 20;
          this.Controls.Add(this.blazorWebView1);
    
          this.components = new System.ComponentModel.Container();
          this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
          this.ClientSize = new System.Drawing.Size(800, 450);
          this.Text = "Form1";
    
          this.ResumeLayout(false);
        }
    
         private Microsoft.AspNetCore.Components.WebView.WindowsForms.BlazorWebView blazorWebView1;
    }
    کامپوننت WebView را نمی‌توان از طریق toolbox به فرم اضافه کرد؛ به همین جهت باید فایل فوق را به نحوی که مشاهده می‌کنید، اندکی ویرایش نمود.


    هاست برنامه‌ی Blazor در برنامه‌ی WinForm

    پس از تغییرات فوق، نیاز است فایل‌های wwwroot را از پروژه‌ی class lib به پروژه‌ی WinForms کپی کرد. از این جهت که این فایل‌ها از طریق index.html جدیدی خوانده خواهند شد. پس از کپی کردن این پوشه، نیاز است فایل csproj پروژه‌ی WinForm را به صورت زیر اصلاح کرد:
    <Project Sdk="Microsoft.NET.Sdk.Razor">
    
      <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.Components.WebView.WindowsForms" Version="6.0.101-preview.11.2349" />
      </ItemGroup>
    
      <ItemGroup>
        <ProjectReference Include="..\BlazorClassLibrary\BlazorClassLibrary.csproj" />
      </ItemGroup>
      
      <ItemGroup>
        <Content Update="wwwroot\**">
          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
        </Content>
      </ItemGroup>
      
    </Project>
    Sdk این فایل تغییر کرده‌است تا بتواند از wwwroot ذکر شده استفاده کند. همچنین به ازای هر Build، فایل‌های واقع در wwwroot به خروجی کپی خواهند شد.
    در ادامه داخل این پوشه‌ی wwwroot که از پروژه‌ی class lib کپی کردیم، نیاز است فایل index.html جدیدی را که قرار است blazor.webview.js را اجرا کند، به صورت زیر ایجاد کنیم:
    <!DOCTYPE html>
    <html>
    
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
        <title>Blazor WinForms app</title>
        <base href="/" />
        <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
        <link href="css/site.css" rel="stylesheet" />
        <link href="css/app.css" rel="stylesheet" />
        <link href="WinFormsApp.styles.css" rel="stylesheet" />
    </head>
    
    <body>
        <div id="app"></div>
    
        <div id="blazor-error-ui">
            An unhandled error has occurred.
            <a href="">Reload</a>
            <a>🗙</a>
        </div>
    
        <script src="_framework/blazor.webview.js"></script>
    </body>
    
    </html>
    - ساختار این فایل بسیار شبیه به ساختار فایل برنامه‌های Blazor WASM است؛ با این تفاوت که در انتهای آن از blazor.webview.js کامپوننت webview استفاده می‌شود.
    - همچنین در این فایل باید مداخل css.‌های مورد نیاز را هم مجددا ذکر کرد.

    مرحله‌ی آخر کار، استفاده از کامپوننت webview جهت نمایش فایل index.html فوق است:
    using System;
    using System.Windows.Forms;
    using Microsoft.AspNetCore.Components.Web;
    using Microsoft.AspNetCore.Components.WebView.WindowsForms;
    using Microsoft.Extensions.DependencyInjection;
    using BlazorServerApp.Data;
    using BlazorClassLibrary;
    
    namespace WinFormsApp;
    
    public partial class Form1 : Form
    {
    private readonly AppState _appState = new();
    
        public Form1()
        {
            var serviceCollection = new ServiceCollection();
            serviceCollection.AddBlazorWebView();
            serviceCollection.AddSingleton<AppState>(_appState);
            serviceCollection.AddSingleton<WeatherForecastService>();
    
            InitializeComponent();
    
            blazorWebView1.HostPage = @"wwwroot\index.html";
            blazorWebView1.Services = serviceCollection.BuildServiceProvider();
            blazorWebView1.RootComponents.Add<App>("#app");
    
            //blazorWebView1.Dock = DockStyle.Fill;
        }
    }


    نکته‌ی مهم! حتما نیاز است WebView2 Runtime را جداگانه دریافت و نصب کرد. در غیر اینصورت در حین اجرای برنامه، با خطای نامفهوم زیر مواجه خواهید شد:
    System.IO.FileNotFoundException: The system cannot find the file specified. (0x80070002)

    در اینجا یک ServiceCollection را ایجاد کرده و توسط آن سرویس‌های مورد نیاز کامپوننت WebView را تامین می‌کنیم. همچنین مسیر فایل index.html نیز توسط آن مشخص شده‌است. این تنظیمات شبیه به فایل Program.cs برنامه‌ی Blazor هستند.

    تا اینجا اگر برنامه را اجرا کنیم، چنین خروجی قابل مشاهده‌است:


    اکنون برنامه‌ی کامل Blazor Server ما توسط یک WinForms هاست شده‌است و کاربر برای کار با آن، نیاز به نصب IIS یا هیچ وب سرور خاصی ندارد.


    تعامل بین برنامه‌ی WinForm و برنامه‌ی Blazor


    می‌خواهیم یک دکمه را بر روی WinForm قرار داده و با کلیک بر روی آن، مقدار شمارشگر حاصل در برنامه‌ی Blazor را نمایش دهیم؛ مانند تصویر فوق.
    برای اینکار در کدهای فوق، ثبت سرویس جدید AppState را هم مشاهده می‌کنید:
    serviceCollection.AddSingleton<AppState>(_appState);
     که چنین محتوایی را دارد:
     namespace BlazorServerApp.Data;
    
    public class AppState
    {
       public int Counter { get; set; }
    }
    این سرویس را به نحو زیر نیز به فایل Program.cs پروژه‌ی Blazor Server اضافه می‌کنیم:
    builder.Services.AddSingleton<AppState>();
    سپس در فایل Counter.razor آن‌را تزریق کرده و به نحو زیر به ازای هر بار کلیک بر روی دکمه‌ی افزایش مقدار شمارشگر، مقدار آن‌را اضافه می‌کنیم:
    @inject BlazorServerApp.Data.AppState AppState
    
    // ...
    
    
    @code {
    
        private void IncrementCount()
        {
           // ...
           AppState.Counter++;
        }
    }
    با توجه به Singleton بودن آن و هاست برنامه‌ی Blazor توسط WinForms، یک وهله از این سرویس، هم در برنامه‌ی Blazor و هم در برنامه‌ی WinForms قابل دسترسی است. برای نمونه یک دکمه را به فرم برنامه‌ی WinForm اضافه کرده و در روال رویدادگردان کلیک آن، کد زیر را اضافه می‌کنیم:
    private void button1_Click(object sender, EventArgs e)
    {
       MessageBox.Show(
         owner: this,
         text: $"Current counter value is: {_appState.Counter}",
         caption: "Counter");
    }
    در اینجا می‌توان با استفاده از وهله‌ی سرویس به اشتراک گذاشته شده، به مقدار تنظیم شده‌ی در برنامه‌ی Blazor دسترسی یافت.

    کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: BlazorDesktopHybrid.zip
    مطالب
    React 16x - قسمت 26 - احراز هویت و اعتبارسنجی کاربران - بخش 1 - ثبت نام و ورود به سیستم
    می‌خواهیم به برنامه‌ی لیست فیلم‌هایی که تا این قسمت تکمیل کردیم، امکانات جدیدی را مانند ورود به سیستم، خروج از آن، کار با JWT، فراخوانی منابع محافظت شده‌ی سمت سرور، نمایش و یا مخفی کردن المان‌های صفحه بر اساس سطوح دسترسی کاربر و همچنین محافظت از مسیرهای مختلف تعریف شده‌ی در برنامه، اضافه کنیم.
    برای قسمت backend، از همان برنامه‌ی تکمیل شده‌ی قسمت قبل استفاده می‌کنیم که به آن تولید مقدماتی JWTها نیز اضافه شده‌است. البته این سری، مستقل از قسمت سمت سرور آن تهیه خواهد شد و صرفا در حد دریافت توکن از سرور و یا ارسال مشخصات کاربر جهت لاگین، نیاز بیشتری به قسمت سمت سرور آن ندارد و تاکید آن بر روی مباحث سمت کلاینت React است. بنابراین اینکه چگونه این توکن را تولید می‌کنید، در اینجا اهمیتی ندارد و کلیات آن با تمام روش‌های پیاده سازی سمت سرور سازگار است (و مختص به فناوری خاصی نیست). پیشنیاز درک کدهای سمت سرور قسمت JWT آن، مطالب زیر هستند:
    1. «معرفی JSON Web Token»
    2. «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» 
    3. «پیاده سازی JSON Web Token با ASP.NET Web API 2.x»
    4. « آزمایش Web APIs توسط Postman - قسمت ششم - اعتبارسنجی مبتنی بر JWT»  


    ثبت یک کاربر جدید

    فرم ثبت نام کاربران را در قسمت 21 این سری، در فایل src\components\registerForm.jsx، ایجاد و تکمیل کردیم. البته این فرم هنوز به backend server متصل نیست. برای کار با آن هم نیاز است شیءای را با ساختار زیر که ذکر سه خاصیت اول آن اجباری است، به endpoint ای با آدرس https://localhost:5001/api/Users به صورت یک HTTP Post ارسال کنیم:
    {
      "name": "string",
      "email": "string",
      "password": "string",
      "isAdmin": true,
      "id": 0
    }
    در سمت سرور هم در Services\UsersDataSource.cs که در انتهای بحث می‌توانید پروژه‌ی کامل آن‌را دریافت کنید، منحصربفرد بودن ایمیل وارد شده بررسی می‌شود و اگر یک رکورد دو بار ثبت شود، یک BadRequest را به همراه پیام خطایی، بازگشت می‌دهد.

    اکنون نوبت به اتصال کامپوننت registerForm.jsx، به سرویس backend است. تا اینجا دو سرویس src\services\genreService.js و src\services\movieService.js را در قسمت قبل، به برنامه جهت کار کردن با endpoint‌های backend server، اضافه کردیم. شبیه به همین روش را برای کار با سرویس سمت سرور api/Users نیز در پیش می‌گیریم. بنابراین فایل جدید src\services\userService.js را با محتوای زیر، به برنامه‌ی frontend اضافه می‌کنیم:
    import http from "./httpService";
    import { apiUrl } from "../config.json";
    
    const apiEndpoint = apiUrl + "/users";
    
    export function register(user) {
      return http.post(apiEndpoint, {
        email: user.username,
        password: user.password,
        name: user.name
      });
    }
    توسط متد register این سرویس می‌توانیم شیء user را با سه خاصیت مشخص شده، از طریق HTTP Post، به آدرس api/Users ارسال کنیم. خروجی این متد نیز یک Promise است. در این سرویس، تمام متدهایی که قرار است با این endpoint سمت سرور کار کنند، مانند ثبت، حذف، دریافت اطلاعات و غیره، تعریف خواهند شد.
    اطلاعات شیء user ای که در اینجا دریافت می‌شود، از خاصیت data کامپوننت RegisterForm تامین می‌گردد:
    class RegisterForm extends Form {
      state = {
        data: { username: "", password: "", name: "" },
        errors: {}
      };
    البته اگر دقت کرده باشید، در شیء منتسب به خاصیت data، خاصیتی به نام username تعریف شده‌است، اما در سمت سرور، نیاز است خاصیتی با نام Name را دریافت کنیم. یک چنین نگاشتی در داخل متد register سرویس کاربر، قابل مشاهده‌‌است. در غیراینصورت می‌شد در متد http.post، کل شیء user را به عنوان پارامتر دوم، درنظر گرفت و ارسال کرد.

    پس از تعریف userService.js، به registerForm.jsx بازگشته و ابتدا امکانات آن‌را import می‌کنیم:
    import * as userService from "../services/userService";
    می‌شد این سطر را به صورت زیر نیز نوشت، تا تنها یک متد از ماژول userService را دریافت کنیم:
    import { register } userService from "../services/userService";
    اما روش as userService * به معنای import تمام متدهای این ماژول است. به این ترتیب نام ذکر شده‌ی پس از as، به عنوان شیءای که می‌توان توسط آن به این متدها دسترسی یافت، قابل استفاده می‌شود؛ مانند فراخوانی متد userService.register. اکنون می‌توان متد doSubmit این فرم را به سرور متصل کرد:
      doSubmit = async () => {
        try {
          await userService.register(this.state.data);
        } catch (ex) {
          if (ex.response && ex.response.status === 400) {
            const errors = { ...this.state.errors }; // clone an object
            errors.username = ex.response.data;
            this.setState({ errors });
          }
        }
      };


    مدیریت و نمایش خطاهای دریافتی از سمت سرور

    در این حالت برای ارسال اطلاعات یک کاربر، در بار اول، یک چنین خروجی را از سمت سرور می‌توان شاهد بود که id جدیدی را به این رکورد نسبت داده‌است:


    اگر مجددا همین رکورد را به سمت سرور ارسال کنیم، اینبار خطای زیر را دریافت خواهیم کرد:


    که از نوع 400 یا همان BadRequest است:


    بنابراین نیاز است بدنه‌ی response را در یک چنین مواردی که خطایی از سمت سرور صادر می‌شود، دریافت کرده و با به روز رسانی خاصیت errors در state فرم (همان قسمت بدنه‌ی catch کدهای فوق)، سبب درج و نمایش خودکار این خطا شویم:


    پیشتر در قسمت بررسی «کار با فرم‌ها» آموختیم که برای مدیریت خطاهای پیش بینی شده‌ی دریافتی از سمت سرور، نیاز است قطعه کدهای مرتبط با سرویس http را در بدنه‌ی try/catch‌ها محصور کنیم. برای مثال در اینجا اگر ایمیل شخصی تکراری وارد شود، سرویس یک return BadRequest("Can't create the requested record.") را بازگشت می‌دهد که در اینجا status code معادل BadRequest، همان 400 است. بنابراین انتظار داریم که خطای 400 را از سمت سرور، تحت شرایط خاصی دریافت کنیم. به همین دلیل است که در اینجا تنها مدیریت status code=400 را در بدنه‌ی catch نوشته شده ملاحظه می‌کنید.
    سپس برای نمایش آن، نیاز است خاصیت متناظری را که این خطا به آن مرتبط می‌شود، با پیام دریافت شده‌ی از سمت سرور، مقدار دهی کنیم که در اینجا می‌دانیم مرتبط با username است. به همین جهت سطر errors.username = ex.response.data، کار انتساب بدنه‌ی response را به خاصیت جدید errors.username انجام می‌دهد. در این حالت اگر به کمک متد setState، کار به روز رسانی خاصیت errors موجود در state را انجام دهیم، رندر مجدد فرم، در صف انجام قرار گرفته و در رندر بعدی آن، پیام موجود در errors.username، نمایش داده می‌شود.


    پیاده سازی ورود به سیستم

    فرم ورود به سیستم را در قسمت 18 این سری، در فایل src\components\loginForm.jsx، ایجاد و تکمیل کردیم که این فرم نیز هنوز به backend server متصل نیست. برای کار با آن نیاز است شیءای را با ساختار زیر که ذکر هر دو خاصیت آن اجباری است، به endpoint ای با آدرس https://localhost:5001/api/Auth/Login به صورت یک HTTP Post ارسال کنیم:
    {
      "email": "string",
      "password": "string"
    }
    با ارسال این اطلاعات به سمت سرور، درخواست Login انجام می‌شود. سرور نیز در صورت تعیین اعتبار موفقیت آمیز کاربر، به صورت زیر، یک JSON Web token را بازگشت می‌دهد:
    var jwt = _tokenFactoryService.CreateAccessToken(user);
    return Ok(new { access_token = jwt });
    یعنی بدنه‌ی response رسیده‌ی از سمت سرور، دارای یک شیء JSON خواهد بود که خاصیت access_token آن، حاوی JSON Web token متعلق به کاربر جاری لاگین شده‌است. در آینده اگر این کاربر نیاز به دسترسی به یک api endpoint محافظت شده‌ای را در سمت سرور داشته باشد، باید این token را نیز به همراه درخواست خود ارسال کند تا پس از تعیین اعتبار آن توسط سرور، مجوز دسترسی به منبع درخواستی برای او صادر شود.

    در ادامه برای تعامل با منبع api/Auth/Login سمت سرور، ابتدا یک سرویس مختص آن‌را در فایل جدید src\services\authService.js، با محتوای زیر ایجاد می‌کنیم:
    import { apiUrl } from "../config.json";
    import http from "./httpService";
    
    const apiEndpoint = apiUrl + "/auth";
    
    export function login(email, password) {
      return http.post(apiEndpoint + "/login", { email, password });
    }
    متد login، کار ارسال ایمیل و کلمه‌ی عبور کاربر را به اکشن متد Login کنترلر Auth، انجام می‌دهد و خروجی آن یک Promise است. برای استفاده‌ی از آن به کامپوننت src\components\loginForm.jsx بازگشته و متد doSubmit آن‌را به صورت زیر تکمیل می‌کنیم:
    import * as auth from "../services/authService";
    
    class LoginForm extends Form {
      state = {
        data: { username: "", password: "" },
        errors: {}
      };
    
      // ...
    
      doSubmit = async () => {
        try {
          const { data } = this.state;
          const {
            data: { access_token }
          } = await auth.login(data.username, data.password);
          console.log("JWT", access_token);
          localStorage.setItem("token", access_token);
          this.props.history.push("/");
        } catch (ex) {
          if (ex.response && ex.response.status === 400) {
            const errors = { ...this.state.errors };
            errors.username = ex.response.data;
            this.setState({ errors });
          }
        }
      };
    توضیحات:
    - ابتدا تمام خروجی‌های ماژول authService را با نام شیء auth دریافت کرده‌ایم.
    - سپس در متد doSubmit، اطلاعات خاصیت data موجود در state را که معادل فیلدهای فرم لاگین هستند، به متد auth.login برای انجام لاگین سمت سرور، ارسال کرده‌ایم. این متد چون یک Promise را باز می‌گرداند، باید await شود و پس از آن متد جاری نیز باید به صورت async معرفی گردد.
    - همانطور که عنوان شد، خروجی نهایی متد auth.login، یک شیء JSON دارای خاصیت access_token است که در اینجا از خاصیت data خروجی نهایی دریافت شده‌است.
    - سپس نیاز است برای استفاده‌های آتی، این token دریافتی از سرور را در جایی ذخیره کرد. یکی از مکان‌های متداول اینکار، local storage مرورگرها است (اطلاعات بیشتر).
    - در آخر کاربر را توسط شیء history سیستم مسیریابی برنامه، به صفحه‌ی اصلی آن هدایت می‌کنیم.
    - در اینجا قسمت catch نیز ذکر شده‌است تا خطاهای حاصل از return BadRequestهای دریافتی از سمت سرور را بتوان ذیل فیلد نام کاربری نمایش داد. روش کار آن، دقیقا همانند روشی است که برای فرم ثبت یک کاربر جدید استفاده کردیم.

    اکنون اگر برنامه را ذخیره کرده و اجرا کنیم، توکن دریافتی، در کنسول توسعه دهندگان مرورگر لاگ شده و سپس کاربر به صفحه‌ی اصلی برنامه هدایت می‌شود. همچنین این token ذخیره شده را می‌توان در ذیل قسمت application->storage آن نیز مشاهده کرد:



    لاگین خودکار کاربر، پس از ثبت نام در سایت

    پس از ثبت نام یک کاربر در سایت، بدنه‌ی response بازگشت داده شده‌ی از سمت سرور، همان شیء user است که اکنون Id او مشخص شده‌است. بنابراین اینبار جهت ارائه‌ی token از سمت سرور، بجای response body، از یک هدر سفارشی در فایل Controllers\UsersController.cs استفاده می‌کنیم (کدهای کامل آن در انتهای بحث پیوست شده‌است):
    var jwt = _tokenFactoryService.CreateAccessToken(user);
    this.Response.Headers.Add("x-auth-token", jwt);



    در ادامه در کدهای سمت کلاینت src\components\registerForm.jsx، برای استخراج این هدر سفارشی، اگر شیء response دریافتی از سرور را لاگ کنیم:
    const response = await userService.register(this.state.data);
    console.log(response);
    یک چنین خروجی را به همراه دارد که در آن، هدر سفارشی ما درج نشده‌است و فقط هدر content-type در آن مشخص است:


    برای اینکه در کدهای سمت کلاینت بتوان این هدر سفارشی را خواند، نیاز است هدر مخصوص access-control-expose-headers را نیز به response اضافه کرد:
    var jwt = _tokenFactoryService.CreateAccessToken(data);
    this.Response.Headers.Add("x-auth-token", jwt);
    this.Response.Headers.Add("access-control-expose-headers", "x-auth-token");
    به این ترتیب وب سرور برنامه، هدر سفارشی را که قرار است برنامه‌ی کلاینت به آن دسترسی پیدا کند، مجاز اعلام می‌کند. اینبار اگر خروجی دریافتی از Axios را لاگ کنیم، در لیست هدرهای آن، هدر سفارشی x-auth-token نیز ظاهر می‌شود:


    اکنون می‌توان این هدر سفارشی را در متد doSubmit کامپوننت RegisterForm، از طریق شیء response.headers خواند و در localStorage ذخیره کرد. سپس کاربر را توسط شیء history سیستم مسیریابی، به ریشه‌ی سایت هدایت نمود:
    class RegisterForm extends Form {
      // ...
    
      doSubmit = async () => {
        try {
          const response = await userService.register(this.state.data);
          console.log(response);
          localStorage.setItem("token", response.headers["x-auth-token"]);
          this.props.history.push("/");
        } catch (ex) {
          if (ex.response && ex.response.status === 400) {
            const errors = { ...this.state.errors }; // clone an object
            errors.username = ex.response.data;
            this.setState({ errors });
          }
        }
      };

    کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-26-backend.zip و sample-26-frontend.zip
    مطالب
    معماری لایه بندی نرم افزار #3

    Service Layer

    نقش لایه‌ی سرویس این است که به عنوان یک مدخل ورودی به برنامه کاربردی عمل کند. در برخی مواقع این لایه را به عنوان لایه‌ی Facade نیز می‌شناسند. این لایه، داده‌ها را در قالب یک نوع داده ای قوی (Strongly Typed) به نام View Model، برای لایه‌ی Presentation فراهم می‌کند. کلاس View Model یک Strongly Typed محسوب می‌شود که نماهای خاصی از داده‌ها را که متفاوت از دید یا نمای تجاری آن است، بصورت بهینه ارائه می‌نماید. در مورد الگوی View Model در مباحث بعدی بیشتر صحبت خواهم کرد.

    الگوی Facade یک Interface ساده را به منظور کنترل دسترسی به مجموعه ای از Interface‌ها و زیر سیستم‌های پیچیده ارائه می‌کند. در مباحث بعدی در مورد آن بیشتر صحبت خواهم کرد.

    کلاسی با نام ProductViewModel را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:

    public class ProductViewModel
    {
        Public int ProductId {get; set;}
        public string Name { get; set; }
        public string Rrp { get; set; }
        public string SellingPrice { get; set; }
        public string Discount { get; set; }
        public string Savings { get; set; }
    }

    برای اینکه کلاینت با لایه‌ی سرویس در تعامل باشد باید از الگوی Request/Response Message استفاده کنیم. بخش Request توسط کلاینت تغذیه می‌شود و پارامترهای مورد نیاز را فراهم می‌کند. کلاسی با نام ProductListRequest را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:

    using SoCPatterns.Layered.Model;
    
    namespace SoCPatterns.Layered.Service
    {
        public class ProductListRequest
        {
            public CustomerType CustomerType { get; set; }
        }
    }

    در شی Response نیز بررسی می‌کنیم که درخواست به درستی انجام شده باشد، داده‌های مورد نیاز را برای کلاینت فراهم می‌کنیم و همچنین در صورت عدم اجرای صحیح درخواست، پیام مناسب را به کلاینت ارسال می‌نماییم. کلاسی با نام ProductListResponse را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:

    public class ProductListResponse
    {
        public bool Success { get; set; }
        public string Message { get; set; }
        public IList<ProductViewModel> Products { get; set; }
    }

    به منظور تبدیل موجودیت Product به ProductViewModel، به دو متد نیاز داریم، یکی برای تبدیل یک Product و دیگری برای تبدیل لیستی از Product. شما می‌توانید این دو متد را به کلاس Product موجود در Domain Model اضافه نمایید، اما این متدها نیاز واقعی منطق تجاری نمی‌باشند. بنابراین بهترین انتخاب، استفاده از Extension Method‌ها می‌باشد که باید برای کلاس Product و در لایه‌ی سرویس ایجاد نمایید. کلاسی با نام ProductMapperExtensionMethods را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:

    public static class ProductMapperExtensionMethods
    {
        public static ProductViewModel ConvertToProductViewModel(this Model.Product product)
        {
            ProductViewModel productViewModel = new ProductViewModel();
            productViewModel.ProductId = product.Id;
            productViewModel.Name = product.Name;
            productViewModel.RRP = String.Format(“{0:C}”, product.Price.RRP);
            productViewModel.SellingPrice = String.Format(“{0:C}”, product.Price.SellingPrice);
            if (product.Price.Discount > 0)
                productViewModel.Discount = String.Format(“{0:C}”, product.Price.Discount);
            if (product.Price.Savings < 1 && product.Price.Savings > 0)
                productViewModel.Savings = product.Price.Savings.ToString(“#%”);
            return productViewModel;
        }
        public static IList<ProductViewModel> ConvertToProductListViewModel(
            this IList<Model.Product> products)
        {
            IList<ProductViewModel> productViewModels = new List<ProductViewModel>();
            foreach(Model.Product p in products)
            {
                productViewModels.Add(p.ConvertToProductViewModel());
            }
            return productViewModels;
        }
    }

    حال کلاس ProductService را جهت تعامل با کلاس سرویس موجود در Domain Model و به منظور برگرداندن لیستی از محصولات و تبدیل آن به لیستی از ProductViewModel، ایجاد می‌نماییم. کلاسی با نام ProductService را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:

    public class ProductService
    {
        private Model.ProductService _productService;
        public ProductService(Model.ProductService ProductService)
        {
            _productService = ProductService;
        }
        public ProductListResponse GetAllProductsFor(
            ProductListRequest productListRequest)
        {
            ProductListResponse productListResponse = new ProductListResponse();
            try
            {
                IList<Model.Product> productEntities =
                    _productService.GetAllProductsFor(productListRequest.CustomerType);
                productListResponse.Products = productEntities.ConvertToProductListViewModel();
                productListResponse.Success = true;
            }
            catch (Exception ex)
            {
                // Log the exception…
                productListResponse.Success = false;
                // Return a friendly error message
                productListResponse.Message = ex.Message;
            }
            return productListResponse;
        }
    }

    کلاس Service تمامی خطاها را دریافت نموده و پس از مدیریت خطا، پیغامی مناسب را به کلاینت ارسال می‌کند. همچنین این لایه محل مناسبی برای Log کردن خطاها می‌باشد. در اینجا کد نویسی لایه سرویس به پایان رسید و در ادامه به کدنویسی Data Layer می‌پردازیم.

    Data Layer

    برای ذخیره سازی محصولات، یک بانک اطلاعاتی با نام Shop01 ایجاد کنید که شامل جدولی به نام Product با ساختار زیر باشد:

    برای اینکه کدهای بانک اطلاعاتی را سریعتر تولید کنیم از روش Linq to SQL در Data Layer استفاده می‌کنم. برای این منظور یک Data Context برای Linq to SQL به این لایه اضافه می‌کنیم. بر روی پروژه SoCPatterns.Layered.Repository کلیک راست نمایید و گزینه Add > New Item را انتخاب کنید. در پنجره ظاهر شده و از سمت چپ گزینه Data و سپس از سمت راست گزینه Linq to SQL Classes را انتخاب نموده و نام آن را Shop.dbml تعیین نمایید.

    از طریق پنجره Server Explorer به پایگاه داده مورد نظر متصل شوید و با عمل Drag & Drop جدول Product را به بخش Design کشیده و رها نمایید.

    اگر به یاد داشته باشید، در لایه Model برای برقراری ارتباط با پایگاه داده از یک Interface به نام IProductRepository استفاده نمودیم. حال باید این Interface را پیاده سازی نماییم. کلاسی با نام ProductRepository را با کد زیر به پروژه SoCPatterns.Layered.Repository اضافه کنید:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using SoCPatterns.Layered.Model;
    
    namespace SoCPatterns.Layered.Repository
    {
        public class ProductRepository : IProductRepository
        {
            public IList<Model.Product> FindAll()
            {
                var products = from p in new ShopDataContext().Products
                                    select new Model.Product
                                    {
                                        Id = p.ProductId,
                                        Name = p.ProductName,
                                        Price = new Model.Price(p.Rrp, p.SellingPrice)
                                    };
                return products.ToList();
            }
        }
    }

    در متد FindAll، با استفاده از دستورات Linq to SQL، لیست تمامی محصولات را برگرداندیم. کدنویسی لایه‌ی Data هم به پایان رسید و در ادامه به کدنویسی لایه‌ی Presentation و UI می‌پردازیم.

    Presentation Layer

    به منظور جداسازی منطق نمایش (Presentation) از رابط کاربری (User Interface)، از الگوی Model View Presenter یا همان MVP استفاده می‌کنیم که در مباحث بعدی با جزئیات بیشتری در مورد آن صحبت خواهم کرد. یک Interface با نام IProductListView را با کد زیر به پروژه SoCPatterns.Layered.Presentation اضافه کنید:

    using SoCPatterns.Layered.Service;
    
    public interface IProductListView
    {
        void Display(IList<ProductViewModel> Products);
        Model.CustomerType CustomerType { get; }
        string ErrorMessage { set; }
    }

    این Interface توسط Web Form‌های ASP.NET و یا Win Form‌ها باید پیاده سازی شوند. کار با Interface‌ها موجب می‌شود تا تست View‌ها به راحتی انجام شوند. کلاسی با نام ProductListPresenter را با کد زیر به پروژه SoCPatterns.Layered.Presentation اضافه کنید:

    using SoCPatterns.Layered.Service;
    
    namespace SoCPatterns.Layered.Presentation
    {
        public class ProductListPresenter
        {
            private IProductListView _productListView;
            private Service.ProductService _productService;
            public ProductListPresenter(IProductListView ProductListView,
                Service.ProductService ProductService)
            {
                _productService = ProductService;
                _productListView = ProductListView;
            }
            public void Display()
            {
                ProductListRequest productListRequest = new ProductListRequest();
                productListRequest.CustomerType = _productListView.CustomerType;
                ProductListResponse productResponse =
                    _productService.GetAllProductsFor(productListRequest);
                if (productResponse.Success)
                {
                    _productListView.Display(productResponse.Products);
                }
                else
                {
                    _productListView.ErrorMessage = productResponse.Message;
                }
            }
        }
    }

    کلاس Presenter وظیفه‌ی واکشی داده ها، مدیریت رویدادها و بروزرسانی UI را دارد. در اینجا کدنویسی لایه‌ی Presentation به پایان رسیده است. از مزایای وجود لایه‌ی Presentation این است که تست نویسی مربوط به نمایش داده‌ها و تعامل بین کاربر و سیستم به سهولت انجام می‌شود بدون آنکه نگران دشواری Unit Test نویسی Web Form‌ها باشید. حال می‌توانید کد نویسی مربوط به UI را انجام دهید که در ادامه به کد نویسی در Win Forms و Web Forms خواهیم پرداخت. 

    راهنماهای پروژه‌ها
    چگونه فایل‌های PDF را نمایش دهیم؟
    بهترین نمایشگر فایل‌های PDF بدون شک نرم افزار رایگان Adobe Reader است. پس از نصب این برنامه، Active-X همراه آن در انواع و اقسام برنامه‌ها و حتی صفحات وب نیز قابل استفاده است. برای نمونه به دو مطلب ذیل در سایت مراجعه نمائید:
    استفاده از کنترل‌های Active-X در WPF  
    نمایش یک فایل PDF در WinForms ، WPF و سیلورلایت

    همچنین روش‌های دیگری که عموما مبتنی بر تبدیل PDF به تصویر هستند نیز جهت نمایش فایل‌های PDF کاربرد دارند. در لینک ذیل، جمع آوری مطالب مرتبطی را خواهید یافت:
    iTextSharp

    به علاوه مرورگرهای مدرن نیز در پی ارائه پشتیبانی توکاری از نمایش فایل‌های PDF هستند. برای نمونه می‌توان به پروژه PDF.js فایرفاکس که از نگارش 15 آن جرئی از فایرفاکس شده است، اشاره کرد.
       

    مطالب
    ایجاد تایمرها در برنامه‌های Angular
    عموما در برنامه‌های جاوا اسکریپتی با استفاده از متدهای setTimeout و setInterval می‌توان یک تایمر را ایجاد کرد. اما در برنامه‌های Angular با توجه به استفاده‌ی از کتابخانه‌ی RxJS، امکان ایجاد تایمرهای reactive نیز وجود دارد که در این مطلب آن‌ها را مرور خواهیم کرد.


    ایجاد تایمرهای متوالی و بی‌وقفه

    با استفاده از عملگر Observable.interval می‌توان یک تایمر بی‌نهایت را ایجاد کرد. پارامتر ورودی آن بر حسب میلی ثانیه است و مشترکین به آن در بازه‌های زمانی مشخص شده‌ی توسط این پارامتر، عدد جاری این بازه را دریافت می‌کنند.
    یک مثال:


    در این مثال می‌خواهیم تایمری را ایجاد کنیم که هر ثانیه یکبار، کدی را اجرا کند:
    import { Observable } from "rxjs/Observable";
    import "rxjs/add/observable/interval";
    import { Subscription } from "rxjs/Subscription";
    
    @Component()
    export class UsingTimersComponent {
    
      private intervalSubscription: Subscription;
      interval = 0;
    
      startInterval() {
        const interval = Observable.interval(1000);
        this.intervalSubscription = interval.subscribe(i => this.interval += i);
      }
    
      stopInterval() {
        this.intervalSubscription.unsubscribe();
      }
    }
    با این قالب:
    <div class="panel panel-default">
      <div class="panel-heading">
        <h2 class="panel-title">Observable.interval(1000)</h2>
      </div>
      <div class="panel-body">
        <div>
          <label>interval: </label> {{interval}}
        </div>
        <div>
          <button (click)="startInterval()" class="btn btn-success">Start</button>
          <button (click)="stopInterval()" class="btn btn-danger">Stop</button>
        </div>
      </div>
    </div>
    عملگر interval باید از مسیر rxjs/add/observable/interval دریافت شود که در ابتدای تعاریف کامپوننت مشخص شده‌است.
     پس از آن فراخوانی Observable.interval(1000) یک Observable را ایجاد می‌کند که توانایی صدور رخ‌دادهایی را در بازه‌های زمانی متوالی 1000 میلی ثانیه‌ای، دارا است.
    اکنون مشترکین به آن، اعداد متوالی شروع شده‌ی از صفر را در هر ثانیه یکبار، دریافت می‌کنند:
    this.intervalSubscription = interval.subscribe(i => this.interval += i);
    این تایمر، به نحوی که تعریف شده‌است، تا ابد ادامه پیدا خواهد کرد. برای توقف آن نیاز است همانند روال معمول کار با Observableها، اشتراک به آن را لغو کرد:
    this.intervalSubscription.unsubscribe();


    مطلع شدن از پایان کار یک تایمر

    با استفاده از اپراتور finally که از مسیر rxjs/add/operator/finally قابل import است، می‌توان رخ‌داد لغو اشتراک به این Observable و یا همان خاتمه‌ی تایمر را در اینجا دریافت کرد:
    this.intervalSubscription = interval
          .finally(() => console.log("All done!"))
          .subscribe(i => this.interval += i);


    ایجاد تایمرهای خود متوقف شونده

    با استفاده از عملگر Observable.timer که در مسیر rxjs/add/observable/timer قرار دارد، می‌توان تایمری را ایجاد کرد که پس از یک تاخیر مشخص شده‌، اجرا شود و بلافاصله خاتمه یابد:
    const timer = Observable.timer(1000);
    timer.subscribe(data => console.log('ding!'));
    در اینجا تایمری ایجاد شده‌است که پس از یک ثانیه اجرا شده و کد نمایش ding را در کنسول مرورگر اجرا می‌کند. سپس به صورت خودکار خاتمه خواهد یافت. در اینجا data نیز مساوی صفر است (اولین بار اجرای تایمر).
    این تایمر امکان اجرای در بازه‌های زمانی مشخصی را نیز دارا است:
    const moreThanOne$ = Observable.timer(2000, 500);
    moreThanOne$.subscribe(data => console.log('timer with args', data));
    اولین پارامتر آن مشخص می‌کند که این تایمر باید پس از 2 ثانیه تاخیر، شروع به کار کند و دومین آرگومان آن مشخص می‌کند که این تایمر تا ابد، با فواصل زمانی هر 500 میلی‌ثانیه یکبار، اجرا خواهد شد.


    محدود کردن تعداد بار اجرای تایمر

    اگر Observable.timer با پارامتر دوم آن بکار رود، بی‌نهایت بار اجرا خواهد شد. اما می‌توان این تعداد بار اجرا را توسط اپراتور take که از مسیر rxjs/add/operator/take قابل import است، محدود کرد:
    let moreThanOne$ = Observable.timer(2000, 500).take(3);
    moreThanOne$.subscribe(data => console.log('timer with args', data));
    در اینجا تایمر تعریف شده، پس از یک وقفه‌ی آغازین 2 ثانیه‌ای شروع به کار می‌کند. سپس تنها دو بار دیگر در بازه‌های متوالی زمانی 500 میلی ثانیه یکبار، اجرا خواهد شد. یعنی جمعا سه بار با توجه به take(3) اجرا خواهد شد.


    اجرای با تاخیر بازه‌های زمانی

    با استفاده از اپراتور delay که از مسیر rxjs/add/operator/delay قابل import است، می‌توان هر بار اجرای callback تایمر را با یک تاخیر دریافت کرد:
    const start = new Date();
    const stream$ = Observable.interval(500).take(3);
    stream$.delay(300).subscribe(x => {
        console.log('val',x);
        console.log( new Date() - start );
    })
    در اینجا تایمر از نوع interval تعریف شده، با توجه به استفاده‌ی از عملگر take، تنها سه بار اجرا می‌شود. اما این اجراها با تاخیری 300 میلی‌ثانیه‌ای به مشترکین آن‌ها اطلاع رسانی می‌گردند. به این ترتیب خروجی لاگ شده‌ی این عملیات به صورت ذیل خواهد بود:
    val:0
    800ms
    val:1
    1300ms
    val:2
    1800ms


    ایجاد یک تایمر شمارش معکوس

    فرض کنید می‌خواهید تایمری را ایجاد کنید که در طی یک شمارش معکوس، از عدد 10000 شروع شود و هر ثانیه یکبار 1000 واحد از آن کاهش یابد و زمانیکه به صفر رسید، متوقف شود.
    این تایمر پس از import وابستگی‌های آن:
    import { Observable } from "rxjs/Observable";
    import "rxjs/add/observable/timer";
    import "rxjs/add/operator/finally";
    import "rxjs/add/operator/takeUntil";
    import "rxjs/add/operator/map";
    یک چنین تعریفی را پیدا می‌کند:
    const interval = 1000;
    const duration = 10 * 1000;
    const stream$ = Observable.timer(0, interval)
          .finally(() => console.log("All done!"))
          .takeUntil(Observable.timer(duration + interval))
          .map(value => duration - value * interval);
    stream$.subscribe(value => console.log(value));
    در اینجا تایمر تعریف شده با توجه به آرگومان صفر تاخیر آن، بلافاصله شروع به کار می‌کند. همچنین با توجه به عدد interval آن، هر یک ثانیه یکبار اعداد صفر، یک و ... را به مشترکین خود ارسال خواهد کرد. اکنون می‌خواهیم این تایمر دقیقا پس از 11 ثانیه متوقف شود. یکی از روش‌های پیاده سازی آن استفاده از takeUntil است که در اینجا یک تایمر خود متوقف شوند را دریافت کرده‌است. این تایمر دقیقا پس از 11 ثانیه از شروع عملیات، یکبار اجرا شده و بلافاصله خاتمه پیدا می‌کند. همین صدور رخ‌داد، کار takeUntil را به پایان می‌رساند که این مورد نیز سبب خاتمه‌ی تایمر اصلی می‌شود.
    در اینجا چون اعداد صادر شده‌ی از طرف تایمر، افزایشی هستند، نیاز است به روشی آن‌ها را تغییر داد. در یک چنین حالتی از اپراتور map استفاده می‌شود. در اینجا value، هربار مقدار افزایشی شروع شده‌ی از صفر را ارائه می‌دهد. توسط عملگر map، این خروجی افزایشی را به یک خروجی کاهشی تبدیل کرده‌ایم تا بتوان به یک تایمر شمارش معکوس رسید.


    دریافت مدت زمان بین اجرای بازه‌های زمانی

    Observable.timer با هر بار اجرا، اعداد شروع شده‌ی از صفر را به مشترکین ارسال می‌کند. اگر در این بین از اپراتور timeInterval قرار گرفته‌ی در مسیر rxjs/add/operator/timeInterval استفاده شود، این مقدار ارسالی از نوع مخصوص <TimeInterval<number خواهد بود که دارای خواص value و interval است:
    const source = Observable.timer(0, 1000)
          .timeInterval()
          .map(x => x.value + ":" + x.interval)
          .take(5);
    
    const subscription = source.subscribe(
          x => console.log("Next timeInterval: " + x),
          err => console.log("Error: " + err),
          () => console.log("Completed")
        );
    در اینجا value همان صفر، یک و ... است و interval بیانگر زمان سپری شده‌ی بین دو صدور رخ‌داد می‌باشد.
    در این مثال با استفاده از متد map، یک خروجی سفارشی تهیه شده‌است. اگر صرفا علاقمند به دریافت مقدار خاصیت interval باشید، می‌توان به صورت ذیل نیز عمل کرد:
    const source = Observable.timer(0, 1000)
          .timeInterval()
          .pluck("interval")
          .take(5);
    عملگر pluck که در مسیر rxjs/add/operator/pluck قرار دارد، خاصیت و یا خاصیت‌هایی از منبع را جهت بازگشت، انتخاب می‌کند. برای مثال در اینجا خاصیت interval یک شیء TimeInterval انتخاب شده‌است.


    تعلیق و از سرگیری مجدد تایمرها

    با قطع اشتراک از یک منبع تایمر، سبب توقف کامل آن خواهیم شد. اما اگر برای مدتی بخواهیم آن‌را در حالت تعلیق قرار دهیم، می‌توان به صورت ذیل عمل کرد:
    import { Observable } from "rxjs/Observable";
    import "rxjs/add/observable/never";
    import "rxjs/add/observable/timer";
    import { Subject } from "rxjs/Subject";
    
      tick: number;
      pauser = new Subject();
      tickerSource = new Subject();
      startTicker() {
        Observable.timer(0, 1000)
          .subscribe(this.tickerSource);
    
        this.pauser
          .switchMap(paused => paused ? Observable.never() : this.tickerSource).
          subscribe(t => this.tickerFunc(t));
    
        this.pauser.next(false); // resume
      }
    
      tickerFunc(tick) {
        this.tick = tick;
      }
    
      pauseTicker() {
        this.pauser.next(true);
      }
    
      resumeTicker() {
        this.pauser.next(false);
      }
    نکته‌ی اصلی این طراحی در switchMap و Observable.never آن نهفته‌است. در اینجا وجود Subject سبب صدور رخدادی به مشترکین آن می‌شود. اگر توسط متد next آن false ارسال شود، سبب از سرگیری مجدد منبع اصلی یا همان تایمر برنامه می‌شود و اگر true ارسال شود، عملیات فراخوانی tickerFunc را با فراخوانی Observable.never به حالت تعلیق می‌برد.


    کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
    مسیرراه‌ها
    ASP.NET MVC