نظرات اشتراک‌ها
دوراهی انتخاب NHibernate و Entityframework
استفاده از NH در مقابل EF Code first سورس باز اشتباه است؛ به دلایل زیر:
- توهم پشتیبانی از بانک‌های اطلاعاتی مختلف توسط NH . به شخصه با حداقل یک مورد نیم بند آن سروکار داشتم: SQL-CE. تقریبا بیشتر از نیمی از توانایی‌های این بانک اطلاعاتی در NH لحاظ نشده و حتی یک کوئری ساده از تاریخ تا تاریخ را توسط آن نمی‌توانید تهیه کنید. این مورد برعکس EF Code first است. کامل و بی‌نقص کار می‌کند. کلا تمام محصولات مایکروسافت به همین نحو هستند. اگر عنوان می‌کنند این محصول دو قابلیت دارد، واقعا این دو قابلیت، درست کار می‌کنند. نه اینکه عنوان کنند 100 قابلیت را ارائه می‌دهیم و فقط 10 تای آن کامل پیاده سازی شده باشند.
- تیم مدیریتی به شدت مغرور و ناراحت NH. باز هم برای این تیم ناراحت، جهت تکمیل نقایص کار با SQL-CE بیشتر از یکسال قبل وصله‌ای رو ارسال کردم. تا به امروز حتی یک پاسخ که آیا خوبه، بده ... ارسال نشده. با اکثر همکاری‌ها هم به همین نحو رفتار می‌کنند.
خلاصه حال و هوای یک پروژه سالم سورس باز را ندارد.
- پس از ارائه EF Code first این پروژه تقریبا غیرفعال شده: (^)
- نیم بند بودن پشتیبانی از LINQ. باز هم اگر تصور می‌کنید که راحت می‌تونید مثل EF از کوئری‌های LINQ در اینجا استفاده کنید، سخت در اشتباه هستید.
- دیر به روز شدن کتابخانه‌های جانبی آن. این مساله هم به مدیریت بد پروژه NH بر می‌گردد. شاید بیشتر از 10 مورد افزونه برای NH هست، مانند کش و اعتبار سنجی و غیره. اما ... صاحبان آن مشخص نیستند! امروز NH3 ارائه می‌شود، سه ماه بعد کتابخانه اعتبارسنجی متناظر با آن. تیم NH هم حاضر نشده تمام این‌ها رو کنار هم قرار بده و یک کار یکپارچه رو ارائه کنه. NH اعتبار خودش رو از این افزونه‌های موجود در NH Contrib کسب می‌کنه، اما حاضر نیستند مدیریت و نگهداری یکپارچه آن‌ها را قبول کنند.
- ناسازگاری با اکوسیستم دات نت. اگر از NH استفاده کنید مدام در حال جنگ با دات نت هستید. مثلا سیستم اعتبار سنجی EF با سیستم اعتبار سنجی سمت کلاینت و سرور MVC یکپارچه است. با NH اینطور نیست و از این نوع مثال‌ها زیاد است. همین مساله حجم کاری شما را چندبرابر می‌کند.
- طراحی زمخت NH در مقابل طراحی روان EF. برای مثال در EF Code first شما به ندرت نیاز خواهید داشت که نگاشت‌ها را تعریف یا حتی سفارشی سازی کنید. یک سری پیش فرض بسیار عالی در آن به صورت توکار (و نه به شکل افزونه) وجود دارد که حجم کاری شما را به شدت کاهش می‌دهند. در NH کتابخانه fluent nh سعی کرده که اینکار را انجام دهد اما جالب اینجا است که این کتابخانه از طرف تیم اصلی NH اصلا تحویل گرفته نشده و ... دست آخر هم یک روش دیگر را برای نوشتن نگاشت‌ها با کد تهیه کردند.
- مستندات NH کامل نیست. برای مثال شاید یک سری بلاگ‌های متفرقه را پیدا کنید که در مورد روش تهیه نگاشت‌ها با کد مطلب نوشته باشند ... نه توسط کسانی که این کتابخانه را تهیه کرده‌اند! این روند رو مقایسه کنید با وبلاگ EF مثلا : (^) این بلاگ تا این حد کامل است که مرجع بسیاری از مطالب آموزشی و کتاب‌های مرتبط با EF می‌باشد.
- سیستم migration موجود در EF Code first نسبت به نمونه NH بسیار کاملتر است؛ با قابلیت سفارشی سازی، مقایسه هش مدل‌ها با جداول جهت جلوگیری از تداخلات و اشتباهات، تولید اسکریپت آپدیت بانک اطلاعاتی و غیره.

یک زمانی بود دات نت ORM قابل ملاحظه‌ای نداشت. زمان دات نت2. در آن موقع NH حرف برای گفتن داشت اما ... نه الان.
نظرات مطالب
طراحی افزونه پذیر با ASP.NET MVC 4.x/5.x - قسمت دوم
- کمی وقت بگذارید و این سه قسمت را به همراه نظرات آن‌ها « یکبار » مطالعه کنید. پروژه‌ی نهایی آن‌را « یکبار » اجرا کنید. به پاسخ سؤالات خودتان خواهید رسید.
- این افزونه‌ها هر کدام اساسا یک سیستم مستقل هستند؛ اما در قالب پروژه‌ی اصلی مفهوم پیدا می‌کنند و در کنار هم قرار می‌گیرند.
- در قسمت اول، در مورد منوی مشترک و نحوه‌ی ثبت منوی خاص یک افزونه در منوی اصلی برنامه بحث شده‌است.
- تمام افزونه‌ها از Layout پروژه‌ی اصلی استفاده می‌کنند و نهایتا سیستم، یک شکل و یک دست است.
- در قسمت سوم، نحوه‌ی استفاده‌ی از بانک اطلاعاتی اصلی برنامه به صورت یکسان و مشترکی در تمام افزونه‌ها بحث شده‌است. بنابراین زمانیکه یک افزونه دسترسی به کل Context دارد، دسترسی به نیازهای مشترک بین افزونه‌ها قابل مدیریت خواهد بود.
مطالب
آشنایی با ذخیره سازی در حافظه
آشنایی با Virtual Address spaces
فضای آدرس‌دهی مجازی: موقعی که یک پردازشگر در مکانی از حافظه عمل خواندن و نوشتن را آغاز می‌کند، از آدرس‌های مجازی بهره می‌برد. بخشی از عملیات خواندن و نوشتن، تبدیل آدرس‌های مجازی به آدرس‌های فیزیکی در حافظه است. این عمل سه مزیت دارد:

  1. آدرس‌های مجازی به صورت پیوسته و پشت سر هم هستند و آدرس دهی بسیار راحت است ولی داده‌ها بر روی یک حافظه به صورت متصل به هم یا پیوسته ذخیره یا خوانده نمی‌شوند و کار آدرس دهی مشکل است. پس یکی از مزایای داشتن آدرس دهی مجازی پشت سر هم قرار گرفتن آدرس هاست.
  2. برنامه از آدرس‌های مجازی برای دسترسی به بافر حافظه استفاده می‌کند که بزرگتر از حافظه فیزیکی موجود هست. موقعی که نیاز به حافظه بیشتر باشد و حافظه سیستم کوچکتر یا کمتر از تقاضا باشد، مدیر حافظه، صفحات حافظه فیزیکی را به صورت یک فایل (عموما 4 کیلیویی) بر روی دیسک سخت ذخیره می‌کند و صفحات داده‌ها در موقع نیاز بین حافظه فیزیکی و دیسک سخت جابجا می‌شود.
  3. هر پردازشی که بر روی آدرس‌های مجازی کار می‌کند ایزوله شده است. یعنی یک پروسه هیچ گاه نمیتواند به آدرس‌های یک پروسه دیگر دسترسی داشته باشد و باعث تخریب داده‌های آن شود.
به محدوده شروع آدرس‌های مجازی تا پایان آن محدوده، فضای آدرس‌دهی مجازی گویند. هر پروسه ای که در مد کاربر آغاز میشود از یک فضای آدرس خصوصی یا مختص به خود استفاده می‌کند. برای سیستم‌های 32 بیتی این فضا میتواند دو گیگ باشد که از آدرس   0x00000000 شروع می‌شود و تا 0x7FFFFFFF  ادامه پیدا می‌کند و برای یک سیستم 64 بیتی تا 8 ترابایت می‌باشد که از آدرس  0x000'00000000  تا آدرس  0x7FF'FFFFFFFF ادامه می‌یاید. گاهی اوقات به محدوده آدرس‌های مجازی، حافظه مجازی می‌گویند.
شکل زیر اصلی‌ترین خصوصیات فضای آدرس‌های مجازی را نشان می‌دهد:

در شکل بالا دو پروسه 64 بیتی به نام‌های notepad.exe و myapp.exe قرار دارند که هر کدام فضای آدرس‌های مجازی خودشان را دارند و از آدرس  0x000'0000000 شروع و تا آدرس 0x7FF'FFFFFFFF ادامه میابند. هر قسمت شامل یک صفحه 4 کیلویی از حافظه مجازی یا فیزیکی است. به برنامه نوت‌پد دقت کنید که از سه صفحه پشت سر هم یا پیوسته تشکیل شده که آدرس شروع آن 0x7F7'93950000 می باشد ولی در حافظه فیزیکی خبری از پیوسته بودن دیده نمی‌شود و حتما این نکته را متوجه شدید که هر دو پروسه از یک آدرس شروع استفاده کرده‌اند، ولی به آدرسی متفاوت از حافظه فیزیکی نگاشت شده اند.

تفاوت user mode و kernel mode
هر پردازش در سیستم بر اساس user mode مد کاربر یا kernel mode مد کرنل اجرا میشود. پردازش‌ها بر اساس هر نوع کد بین این دو بخش سوییچ می‌کنند. اپلیکیشن‌ها بر اساس مد کاربر و هسته سیستم عامل و اکثر درایورها بر اساس مد کرنل کار می‌کنند؛ ولی تعدادی از آن‌ها هم در مد کاربر.
هر برنامه یا اپلیکیشنی که اجرا می‌شود، در یک مد کاربری قرار می‌گیرد. ویندوز هم برای هر برنامه یک پروسه یا فرآیندی را ایجاد می‌کند. پروسه برای برنامه یک فضای آدرس‌دهی مجازی و یک جدول مدیریت به صورت خصوصی یا مختص همین برنامه تشکیل می‌دهد. به این ترتیب هیچ برنامه دیگری نمی‌تواند به داده‌های برنامه دیگر دسترسی داشته باشد و  هر برنامه در یک محیط ایزوله شده برای خودش قرار میگیرد و این برنامه اگر به هر ترتیبی کرش کند، برنامه‌های دیگر به کار خود ادامه می‌دهند و هیچ تاثیری بر برنامه‌های دیگر نمی‌گذارند.
البته استفاده از این آدرس‌های مجازی محدودیت هایی هم دارد، چرا که بعضی از آن‌ها توسط سیستم عامل رزرو شده اند و برنامه نمی‌تواند به آن قسمت‌ها دسترسی داشته باشد و این باعث می‌شود که داده‌های برنامه از خسارت و آسیب دیدن حفظ شوند.
تمام برنامه هایی در حالت کرنل ایجاد می‌شوند، از یک فضای آدرس مجازی استفاده می‌کنند. به این معنی که یک درایور مد کرنل نسبت به دیگر درایورها و خود سیستم عامل به هیچ عنوان در یک محیط ایزوله قرار ندارد. بنابراین ممکن است یک کرنل درایور تصادفا در یک آدرس مجازی اشتباه که میتواند متعلق به سیستم عامل یا یک درایور دیگر باشد بنویسد. یعنی اگر یک درایور کرنل کراش کند کل سیستم عامل کرش میکند.
تصویر زیر به خوبی ارتباط بین مد کاربری و مد کرنل را نشان می‌دهد:

 

فضای کاربری و فضای سیستمی User space and system space

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

در سیستم‌های 32 بیتی نهایتا تا 4 گیگ حافظه می‌توان به این‌ها تخصیص داد؛ 2 گیگ ابتدایی به user space و دو گیگ بعدی به system space :

در ویندوزهای 32 بیتی شما امکان تغییر این مقدار حافظه را در میان بوت دارید و می‌توانید حافظه کاربری را تا 3 گیگ مشخص کنید و یک گیگ را برای فضای سیستمی. برای اینکار می‌توانید از برنامه bcedit استفاده کنید.

در سیستم‌های 64 بیتی میزان حافظه‌های مجازی به صورت تئوری تا 16 اگزابایت مشخص شده است؛ ولی در عمل تنها بخش کوچکی از آن یعنی 8 ترابایت استفاده می‌شود.

 کدهایی که در user mode اجرا می‌شوند فقط به فضای کاربری دسترسی دارند و دسترسی آن‌ها به فضای سیستمی به منظور جلوگیری از تخریب داده ممکن نیست. ولی در حالت کرنل می‌توان به دو فضای سیستمی و کاربری دسترسی داشت. درایورهایی که در مدکرنل نوشته شده اند باید تمام دقت خود را در زمینه نوشتن و خواندن از فضای سیستمی در حافظه به کار گیرند. سناریوی زیر به شما نشان می‌دهد که چرا باید مراقب بود:
  1. برنامه جهت اجرا در مد کاربر یک درخواست را برای خواندن داده‌های یک device را آماده می‌کند. سپس برنامه آدرس شروع یک بافر را برای دریافت داده، مشخص می‌کند.
  2. وظیفه این درایور یک قطعه در مد کرنل این است که  عملیات خواندن را شروع کرده و کنترل را به درخواست کننده ارسال می‌کند.
  3. بعد device یک وقفه را به هر تردی thread که در حال اجراست ارسال می‌کند تا بگوید، عملیات خواندن پایان یافته است. این وقفه توسط ترد درایور مربوطه دریافت می‌شود.
  4. حالا دیگر درایور نباید داده‌ها را در همان جایی که گام اول برنامه مشخص کرده است ذخیره کند. چون این آدرس که برنامه در مد کاربری مشخص کرده است، با نمونه‌ای که این فرآیند محاسبه می‌کند متفاوت است.
Paged Pool and NonPaged Pool
در فضای کاربری تمام صفحات در صورت نیاز توانایی انتقال به دیسک سخت را دارند ولی در فضای سیستمی همه بدین صورت نیستند. فضای سیستمی دو ناحیه حافظه تخصیصی پویا دارد که به نام‌های paged pool و nonpaged pool شناخته می‌شوند.
در سیستم‌های 32 بیتی Pagedpool توانایی 128 گیگ فضای آدرس دهی مجازی را از آدرس 0xFFFFAC00'00000000 تا آدرس 0xFFFFAC1F'FFFFFFFF و در سیستم‌های 64 بیتی توانایی 128 گیگ فضای آدرس دهی مجازی را از 0xFFFFA800'00000000 تا 0xFFFFA81F'FFFFFFFF دارد. حافظه ای که به صورت paged pool تخصیص شده باشد می‌تواند صفحات حافظه را بر روی دیسک سخت ذخیره کند؛ ولی حافظه ای که به صورت nonpaged تخصیص یافته باشد، هرگز نمی‌تواند.

مطالب
مدیریت دانلود‌های همزمان از یک سایت و بحث تایم آوت
یک سرویس ویندوز ان تی با سی شارپ نوشته‌ام که کارش مراجعه به یک سری آدرس RSS و ذخیره سازی آنها به صورت آنالیز شده در یک دیتابیس SQL server است (این مورد ضعفی است که اکثر برنامه‌های فیدخوان دارند و پس از مدتی کار با آنها این احساس را دارید که اطلاعات گذشته را از دست داده‌اید).
در طی آزمایش اولیه این سرویس، به مشکل عجیب timeout پس از باز کردن برای مثال سومین یا چهارمین thread همزمان برای دانلود کردن اطلاعات بر خوردم. همه چیز درست بود، از کلاس‌ها، دریافت اطلاعات از وب و غیره، اما برنامه کار نمی‌کرد. این مشکل فقط هم با feedburner.com رخ می‌داد (همانطور که مطلع هستید feedburner.com سرویسی را جهت پیگیری آمار مشترکین فیدهای شما ارائه می‌دهد که بسیار جالب است. برای مثال چند نفر مشترک دارید، یا یک سری نمودار و غیره. به همین جهت رسم شده است که اکثر سایت‌ها فیدهای خودشان را در این سایت نیز ثبت می‌کنند).
پس از مدتی جستجو به نکته جالب زیر برخوردم که شاید برای شما هم در آینده مفید باشد:
مطابق RFC2068 - Hypertext Transfer Protocol -- HTTP/1.1 ، شما تنها مجازید 2 کانکشن فعال به یک سایت باز کنید. این علت تایم آوت در سومین thread ایجاد شده بود. برای مثال IE این مورد را محترم می‌شمارد. در دات نت نیز به صورت پیش فرض این محدودیت قرار داده شده است که به‌سادگی می‌توان آنرا تغییر داد. برای این منظور باید یک فایل app.config به پروژه اضافه کرد و سپس خطوط زیر را به آن افزود:

<configuration>
<system.net>
<connectionManagement>
<add address="*" maxconnection="100" />
</connectionManagement>
</system.net>
</configuration>


بعد از این تغییر مشکل timeout برنامه حل شد.

برای مدیریت چندین ترد همزمان دانلود کننده و در صف قرار دادن آنها در این پروژه، از کتابخانه سورس باز زیر استفاده کردم:
http://www.codeplex.com/smartthreadpool

مآخذ:
http://msdn.microsoft.com/en-us/library/fb6y0fyc.aspx
http://www.faqs.org/rfcs/rfc2068.html
http://vahidnasiri.blogspot.com
http://odetocode.com/Blogs/scott/archive/2004/06/08/272.aspx

پ.ن.
برای اینکه در بلاگر بتوانید متون حاوی xml را ارسال کنید باید از سرویس زیر استفاده کنید
http://www.elliotswan.com/postable/
مطالب
بررسی تغییرات Blazor 8x - قسمت هفتم - امکان تعریف جزیره‌های تعاملی Blazor WASM
در قسمت‌های قبل، نحوه‌ی تعریف جزیره‌های تعاملی Blazor Server را به همراه نکات مرتبط با آن‌ها بررسی کردیم. برای مثال مشاهده کردیم که چون Blazor Server و SSR هر دو بر روی سرور اجرا می‌شوند، از لحاظ دسترسی به اطلاعات و کار با سرویس‌ها، هماهنگی کاملی دارند و می‌توان کدهای یکسان و یکدستی را در اینجا بکار گرفت. در Blazor 8x، امکان تعریف جزیره‌های تعاملی Blazor WASM نیز وجود دارد که به همراه تعدادی نکته‌ی ویژه، در مورد نحوه‌ی مدیریت سرویس‌های مورد استفاده‌ی در این کامپوننت‌ها است.


معرفی برنامه‌ی Blazor WASM این مطلب

در این مطلب قصد داریم دقیقا قسمت جزیره‌ی تعاملی Blazor Server همان برنامه‌ی مطلب قبل را توسط یک جزیره‌ی تعاملی Blazor WASM بازنویسی کنیم و با نکات و تفاوت‌های ویژه‌ی آن آشنا شویم. یعنی زمانیکه صفحه‌ی SSR نمایش جزئیات یک محصول ظاهر می‌شود، نحوه‌ی رندر و پردازش کامپوننت نمایش محصولات مرتبط و مشابه، اینبار یک جزیره‌ی تعاملی Blazor WASM باشد. بنابراین قسمت عمده‌ای از کدهای این دو قسمت یکی است؛ فقط نحوه‌ی دسترسی به سرویس‌ها و محل قرارگیری تعدادی از فایل‌ها، متفاوت خواهد بود.


ایجاد یک پروژه‌ی جدید Blazor WASM تعاملی در دات نت 8

بنابراین در ادامه، در ابتدای کار نیاز است یک پوشه‌ی جدید را برای این پروژه، ایجاد کرده و بجای انتخاب interactivity از نوع Server:
dotnet new blazor --interactivity Server
اینبار برای اجرای در مرورگر توسط فناوری وب‌اسمبلی، نوع WebAssembly را انتخاب کنیم:
dotnet new blazor --interactivity WebAssembly
در این حالت، Solution ای که ایجاد می‌شود، به همراه دو پروژه‌‌است (برخلاف پروژه‌های Blazor Server تعاملی که فقط شامل یک پروژه‌ی سمت سرور هستند):
الف) یک پروژه‌ی سمت سرور (برای تامین backend و API و سرویس‌های مرتبط)
ب) یک پروژه‌ی سمت کلاینت (برای اجرای مستقیم درون مرورگر کاربر؛ بدون داشتن وابستگی مستقیمی به اجزای برنامه‌ی سمت سرور)

این ساختار، خیلی شبیه به ساختار پروژه‌های نگارش قبلی Blazor از نوع Hosted Blazor WASM است که در آن، یک پروژه‌ی ASP.NET Core هاست کننده‌ی پروژه‌ی Blazor WASM وجود دارد و یکی از کارهای اصلی آن، فراهم ساختن Web API مورد استفاده‌ی در پروژه‌ی WASM است.

در حالتیکه نوع تعاملی بودن پروژه را Server انتخاب کنیم (مانند مثال قسمت پنجم)، فایل Program.cs آن به همراه دو تعریف مهم زیر است که امکان تعریف کامپوننت‌های تعاملی سمت سرور را میسر می‌کنند:
// ...

builder.Services.AddRazorComponents()
       .AddInteractiveServerComponents();

// ...

app.MapRazorComponents<App>()
   .AddInteractiveServerRenderMode();
مهم‌ترین قسمت‌های آن، متدهای AddInteractiveServerComponents و AddInteractiveServerRenderMode هستند که server-side rendering را به همراه امکان داشتن کامپوننت‌های تعاملی، ممکن می‌کنند.

این تعاریف در فایل Program.cs (پروژه‌ی سمت سرور) قالب جدید Blazor WASM به صورت زیر تغییر می‌کنند تا امکان تعریف کامپوننت‌های تعاملی سمت کلاینت از نوع وب‌اسمبلی، میسر شود:
// ...

builder.Services.AddRazorComponents()
    .AddInteractiveWebAssemblyComponents();

// ...

app.MapRazorComponents<App>()
    .AddInteractiveWebAssemblyRenderMode()
    .AddAdditionalAssemblies(typeof(Counter).Assembly);

نیاز به تغییر معماری برنامه جهت کار با جزایر Blazor WASM

همانطور که در قسمت پنجم مشاهده کردیم، تبدیل کردن یک کامپوننت Blazor، به کامپوننتی تعاملی برای اجرای در سمت سرور، بسیار ساده‌است؛ فقط کافی است rendermode@ آن‌را به InteractiveServer تغییر دهیم تا ... کار کند. اما تبدیل همان کامپوننت نمایش محصولات مرتبط، به یک جزیره‌ی وب‌اسمبلی، نیاز به تغییرات قابل ملاحظه‌ای را دارد؛ از این لحاظ که اینبار این قسمت قرار است بر روی مرورگر کاربر اجرا شود و نه بر روی سرور. در این حالت دیگر کامپوننت ما دسترسی مستقیمی را به سرویس‌های سمت سرور ندارد و برای رسیدن به این مقصود باید از یک Web API در سمت سرور کمک بگیرد و برای کار کردن با آن API در سمت کلاینت، از سرویس HttpClient استفاده کند. به همین جهت، پیاده سازی معماری این روش، نیاز به کار بیشتری را دارد:


همانطور که ملاحظه می‌کنید، برای فعالسازی یک جزیره‌ی تعاملی وب‌اسمبلی، نمی‌توان کامپوننت RelatedProducts آن‌را مستقیما در پروژه‌ی سمت سرور قرار داد و باید آن‌را به پروژه‌ی سمت کلاینت منتقل کرد. در ادامه پیاده سازی کامل این پروژه را با توجه به این تغییرات بررسی می‌کنیم.


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

از این جهت که مدل برنامه (که در قسمت پنجم معرفی شد) در دو پروژه‌ی Client و سرور قابل استفاده‌است، به همین جهت مرسوم است یک پروژه‌ی سوم Shared را نیز به جمع دو پروژه‌ی جاری solution اضافه کرد و فایل این مدل را در آن قرار داد. بنابراین این فایل را از پوشه‌ی Models پروژه‌ی سرور به پوشه‌ی Models پروژه‌ی جدید BlazorDemoApp.Shared در مسیر جدید BlazorDemoApp.Shared\Models\Product.cs منتقل می‌کنیم. مابقی کدهای آن با قسمت پنجم تفاوتی ندارد.
سپس به فایل csproj. پروژه‌ی کلاینت مراجعه کرده و ارجاعی را به پروژه‌ی جدید BlazorDemoApp.Shared اضافه می‌کنیم:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\BlazorDemoApp.Shared\BlazorDemoApp.Shared.csproj" />
  </ItemGroup>

</Project>
نیازی نیست تا اینکار را برای پروژه‌ی سرور نیز تکرار کنیم؛ از این جهت که ارجاعی به پروژه‌ی کلاینت، در پروژه‌ی سرور وجود دارد که سبب دسترسی به این پروژه‌ی Shared هم می‌شود.


سرویس برنامه: سرویسی برای بازگشت لیست محصولات

چون Blazor Server و صفحات SSR آن هر دو بر روی سرور اجرا می‌شوند، از لحاظ دسترسی به اطلاعات و کار با سرویس‌ها، هماهنگی کاملی وجود داشته و می‌توان کدهای یکسان و یکدستی را در اینجا بکار گرفت. یعنی هنوز هم همان مسیر قبلی سرویس Services\ProductStore.cs در این پروژه‌ی سمت سرور نیز برقرار است و نیازی به تغییر مسیر آن نیست. البته بدیهی است چون این پروژه جدید است، باید این سرویس را در فایل Program.cs برنامه‌ی سمت سرور به صورت زیر معرفی کرد تا در فایل razor برنامه‌ی آن قابل دسترسی شود:
builder.Services.AddScoped<IProductStore, ProductStore>();


تکمیل فایل Imports.razor_ پروژه‌ی سمت سرور

جهت سهولت کار با برنامه، یک سری مسیر و using را نیاز است به فایل Imports.razor_ پروژه‌ی سمت سرور اضافه کرد:
@using static Microsoft.AspNetCore.Components.Web.RenderMode
// ...
@using BlazorDemoApp.Client.Components.Store
@using BlazorDemoApp.Client.Components
سطر اول سبب می‌شود تا بتوان به سادگی به اعضای کلاس استاتیک RenderMode، در برنامه‌ی سمت سرور دسترسی یافت. دو using جدید دیگر سبب سهولت دسترسی به کامپوننت‌های قرارگرفته‌ی در این مسیرها در صفحات SSR برنامه‌ی سمت سرور می‌شوند.


تکمیل صفحه‌ی نمایش لیست محصولات

کدها و مسیر کامپوننت ProductsList.razor، با قسمت پنجم دقیقا یکی است. این صفحه، یک صفحه‌ی SSR بوده و در همان سمت سرور اجرا می‌شود و دسترسی آن به سرویس‌های سمت سرور نیز ساده بوده و همانند قبل است.


تکمیل صفحه‌ی نمایش جزئیات یک محصول

کدها و مسیر کامپوننت ProductDetails.razor، با قسمت پنجم دقیقا یکی است. این صفحه، یک صفحه‌ی SSR بوده و در همان سمت سرور اجرا می‌شود و دسترسی آن به سرویس‌های سمت سرور نیز ساده بوده و همانند قبل است ... البته بجز یک تغییر کوچک:
<RelatedProducts ProductId="ProductId" @rendermode="@InteractiveWebAssembly"/>
در اینجا حالت رندر این کامپوننت، به InteractiveWebAssembly تغییر می‌کند. یعنی اینبار قرار است تبدیل به یک جزیره‌ی وب‌اسمبلی شود و نه یک جزیره‌ی Blazor Server که آن‌را در قسمت پنجم بررسی کردیم.


تکمیل کامپوننت نمایش لیست محصولات مشابه و مرتبط

پس از این توضیحات، به اصل موضوع این قسمت رسیدیم! کامپوننت سمت سرور RelatedProducts.razor قسمت پنجم ، از آنجا cut شده و به مسیر جدید BlazorDemoApp.Client\Components\Store\RelatedProducts.razor منتقل می‌شود. یعنی کاملا به پروژه‌ی وب‌اسمبلی منتقل می‌شود. بنابراین کدهای آن دیگر دسترسی مستقیمی به سرویس دریافت اطلاعات محصولات ندارند و برای اینکار نیاز است در سمت سرور، یک Web API Controller را تدارک ببینیم:
using BlazorDemoApp.Services;
using Microsoft.AspNetCore.Mvc;

namespace BlazorDemoApp.Controllers;

[ApiController]
[Route("/api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductStore _store;

    public ProductsController(IProductStore store) => _store = store;

    [HttpGet("[action]")]
    public IActionResult Related([FromQuery] int productId) => Ok(_store.GetRelatedProducts(productId));
}
این کلاس در مسیر Controllers\ProductsController.cs پروژه‌ی سمت سرور قرار می‌گیرد و کار آن، بازگشت اطلاعات محصولات مشابه یک محصول مشخص است.
برای اینکه مسیریابی این کنترلر کار کند، باید به فایل Program.cs برنامه، مراجعه و سطرهای زیر را اضافه کرد:
builder.Services.AddControllers();
// ...
app.MapControllers();

یک نکته: همانطور که مشاهده می‌کنید، در Blazor 8x، امکان استفاده از دو نوع مسیریابی یکپارچه، در یک پروژه وجود دارد؛ یعنی Blazor routing  و  ASP.NET Core endpoint routing. بنابراین در این پروژه‌ی سمت سرور، هم می‌توان صفحات SSR و یا Blazor Server ای داشت که مسیریابی آن‌ها با page@ مشخص می‌شوند و همزمان کنترلرهای Web API ای را داشت که بر اساس سیستم مسیریابی ASP.NET Core کار می‌کنند.

بر این اساس در پروژه‌ی سمت کلاینت، کامپوننت RelatedProducts.razor باید با استفاده از سرویس HttpClient، اطلاعات درخواستی را از Web API فوق دریافت و همانند قبل نمایش دهد که تغییرات آن به صورت زیر است:

@using BlazorDemoApp.Shared.Models
@inject HttpClient Http

<button class="btn btn-outline-secondary" @onclick="LoadRelatedProducts">Related products</button>

@if (_loadRelatedProducts)
{
    @if (_relatedProducts == null)
    {
        <p>Loading...</p>
    }
    else
    {
        <div class="mt-3">
            @foreach (var item in _relatedProducts)
            {
                <a href="/ProductDetails/@item.Id">
                    <div class="col-sm">
                        <h5 class="mt-0">@item.Title (@item.Price.ToString("C"))</h5>
                    </div>
                </a>
            }
        </div>
    }
}

@code{

    private IList<Product>? _relatedProducts;
    private bool _loadRelatedProducts;

    [Parameter]
    public int ProductId { get; set; }

    private async Task LoadRelatedProducts()
    {
        _loadRelatedProducts = true;
        var uri = $"/api/products/related?productId={ProductId}";
        _relatedProducts = await Http.GetFromJsonAsync<IList<Product>>(uri);
    }

}
و ... همین! اکنون برنامه قابل اجرا است و به محض نمایش صفحه‌ی جزئیات یک محصول انتخابی، کامپوننت RelatedProducts، در حالت وب‌اسمبلی جزیره‌ای اجرا شده و لیست این محصولات مرتبط را نمایش می‌دهد.
در ادامه یکبار برنامه را اجرا می‌کنیم و ... بلافاصله پس از انتخاب صفحه‌ی نمایش جزئیات یک محصول، با خطای زیر مواجه خواهیم شد!
System.InvalidOperationException: Cannot provide a value for property 'Http' on type 'RelatedProducts'.
There is no registered service of type 'System.Net.Http.HttpClient'.


اهمیت درنظر داشتن pre-rendering در حالت جزیره‌های وب‌اسمبلی

استثنائی را که مشاهده می‌کنید، به علت pre-rendering سمت سرور این کامپوننت، رخ‌داده‌است.
زمانیکه کامپوننتی را به این نحو رندر می‌کنیم:
<RelatedProducts ProductId="ProductId" @rendermode="@InteractiveWebAssembly"/>
به صورت پیش‌فرض در آن pre-rendering نیز فعال است؛ یعنی این کامپوننت دوبار رندر می‌شود:
الف) یکبار در سمت سرور تا HTML حداقل قالب آن، به همراه سایر قسمت‌های صفحه‌ی SSR جاری به سمت مرورگر کاربر ارسال شود.
ب) یکبار هم در سمت کلاینت، زمانیکه Blazor WASM بارگذاری شده و فعال می‌شود.

استثنائی را که مشاهده می‌کنیم، مربوط به حالت الف است. یعنی زمانیکه برنامه‌ی ASP.NET Core هاست برنامه، سعی می‌کند کامپوننت RelatedProducts را در سمت سرور رندر کند، اما ... ما سرویس HttpClient را در آن ثبت و فعالسازی نکرده‌ایم. به همین جهت است که عنوان می‌کند این سرویس را پیدا نکرده‌است. برای رفع این مشکل، چندین راه‌حل وجود دارند که در ادامه آن‌ها را بررسی می‌کنیم.


راه‌حل اول: ثبت سرویس HttpClient در سمت سرور

یک راه‌حل مواجه شدن با مشکل فوق، ثبت سرویس HttpClient در فایل Program.cs برنامه‌ی سمت سرور به صورت زیر است:
builder.Services.AddScoped(sp => new HttpClient 
{ 
    BaseAddress = new Uri("http://localhost/") 
});
پس از این تعریف، کامپوننت RelatedProducts، در حالت prerendering ابتدایی سمت سرور هم کار می‌کند و برنامه با استثنائی مواجه نخواهد شد.


راه‌حل دوم: استفاده از polymorphism یا چندریختی

برای اینکار اینترفیسی را طراحی می‌کنیم که قرارداد نحوه‌ی تامین اطلاعات مورد نیاز کامپوننت RelatedProducts را ارائه می‌کند. سپس یک پیاده سازی سمت سرور را از آن خواهیم داشت که مستقیما به بانک اطلاعاتی رجوع می‌کند و همچنین یک پیاده سازی سمت کلاینت را که از HttpClient جهت کار با Web API استفاده خواهد کرد.
از آنجائیکه این قرارداد نیاز است توسط هر دو پروژه‌ی سمت سرور و سمت کلاینت استفاده شود، باید آن‌را در پروژه‌ی Shared قرار داد تا بتوان ارجاعاتی از آن‌را به هر دو پروژه اضافه کرد؛ برای مثال در فایل BlazorDemoApp.Shared\Data\IProductStore.cs به صورت زیر:
using BlazorDemoApp.Shared.Models;

namespace BlazorDemoApp.Shared.Data;

public interface IProductStore
{
    IList<Product> GetAllProducts();
    Product GetProduct(int id);
    Task<IList<Product>?> GetRelatedProducts(int productId);
}
این همان اینترفیسی است که پیشتر در فایل ProductStore.cs سمت سرور تعریف کرده بودیم؛ با یک تفاوت: متد GetRelatedProducts آن async تعریف شده‌است که نمونه‌ی سمت کلاینت آن باید با متد GetFromJsonAsync کار کند که async است.
پیاده سازی سمت سرور این اینترفیس، کاملا مهیا است و فقط نیاز به تغییر زیر را دارد تا با خروجی Task دار هماهنگ شود:
public Task<IList<Product>?> GetRelatedProducts(int productId)
    {
        var product = ProductsDataSource.Single(x => x.Id == productId);
        return Task.FromResult<IList<Product>?>(ProductsDataSource.Where(p => product.Related.Contains(p.Id))
                                                                 .ToList());
    }
و اکشن متد متناظر هم باید به صورت زیر await دار شود تا خروجی صحیحی را ارائه دهد:
[HttpGet("[action]")]
public async Task<IActionResult> Related([FromQuery] int productId) =>
        Ok(await _store.GetRelatedProducts(productId));
همچنین پیشتر سرویس آن در فایل Program.cs برنامه‌ی سمت سرور، ثبت شده‌است و نیاز به نکته‌ی خاصی ندارد.

در ادامه نیاز است یک پیاده سازی سمت کلاینت را نیز از آن تهیه کنیم که در فایل BlazorDemoApp.Client\Data\ClientProductStore.cs درج خواهد شد:
public class ClientProductStore : IProductStore
{
    private readonly HttpClient _httpClient;

    public ClientProductStore(HttpClient httpClient) => _httpClient = httpClient;

    public IList<Product> GetAllProducts() => throw new NotImplementedException();

    public Product GetProduct(int id) => throw new NotImplementedException();

    public Task<IList<Product>?> GetRelatedProducts(int productId) =>
        _httpClient.GetFromJsonAsync<IList<Product>>($"/api/products/related?productId={productId}");
}
در این بین بر اساس نیاز کامپوننت نمایش لیست محصولات مشابه، فقط به متد GetRelatedProducts نیاز داریم؛ بنابراین فقط همین مورد در اینجا پیاده سازی شده‌است. پس از این تعریف، نیاز است سرویس فوق را در فایل Program.cs برنامه‌ی کلاینت هم ثبت کرد (به همراه سرویس HttpClient ای که در سازنده‌ی آن تزریق می‌شود):
builder.Services.AddScoped<IProductStore, ClientProductStore>();
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
به این ترتیب این سرویس در کامپوننت RelatedProducts قابل دسترسی می‌شود و جایگزین سرویس HttpClient تزریقی قبلی خواهد شد. به همین جهت به فایل کامپوننت ProductStore مراجعه کرده و فقط 2 سطر آن‌را تغییر می‌دهیم:
الف) معرفی سرویس IProductStore بجای HttpClient قبلی
@inject IProductStore ProductStore
ب) استفاده از متد GetRelatedProducts این سرویس:
private async Task LoadRelatedProducts()
{
   _loadRelatedProducts = true;
   _relatedProducts = await ProductStore.GetRelatedProducts(ProductId);
}
مابقی قسمت‌های این کامپوننت یکی است و تفاوتی با قبل ندارد.

اکنون اگر برنامه را اجرا کنیم، پس از مشاهده‌ی جزئیات یک محصول، بارگذاری کامپوننت Blazor WASM آن در developer tools مرورگر کاملا مشخص است:



راه‌حل سوم: استفاده از سرویس PersistentComponentState

با استفاده از سرویس PersistentComponentState می‌توان اطلاعات دریافتی از بانک‌اطلاعاتی را در حین pre-rendering در سمت سرور، به جزایر تعاملی انتقال داد و این روشی است که مایکروسافت برای پیاده سازی مباحث اعتبارسنجی و احراز هویت در Blazor 8x در پیش‌گرفته‌است. این راه‌حل را در قسمت بعد بررسی می‌کنیم.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: Blazor8x-WebAssembly-Normal.zip
مطالب
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
مطالب
چگونگی استفاده از افزونه Isotope در AngularJS

حتما تا به حال در وب سایت‌های زیادی قسمت هایی را دیده اید که چیدمان عناصر آن به شکل زیر است:

این گونه چیدمان را حتما در منوی Start ویندوز 8 بار‌ها دیده‌اید! عناصر تشکیل دهنده‌ی این شکل از چیدمان، می‌توانند یک سری عکس باشند که تشکیل یک گالری عکس را داده‌اند و یا یک سری div که محتوای پست‌های یک وبلاگ را در خود جای داده‌اند. چیزی که این شکل از چیدمان عناصر را نسبت به چیدمان‌های معمول متمایز می‌کند این است که طول و عرض هر یک از این عناصر با یکدیگر متفاوت است و هدف از این گونه چیدمان آن است که این عناصر در فضایی که به آن‌ها اختصاص داده شده است، به صورت بهینه قرار گیرند تا کمترین فضا هدر رود.

برای اعمال این شکل از چیدمان در دنیای وب افزونه‌های زیادی بر فراز کتاب خانه‌ی jQuery تدارک دیده شده است که از جمله مطرح‌ترین آن‌ها می‌توان به افزونه های Isotope ، Masonry و Gridster  اشاره کرد.

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

در این مقاله قصد من این است که نشان دهم چگونه از افزونه‌ی Isotope در AngularJS استفاده کنیم؛ چگونه چیدمان آن را راست به چپ کنیم و چگونه آن را با محیط‌های واکنش گرا (Responsive) سازگار کنیم.

فرض کنید در یک وب سایت قصد داریم اطلاعات یک سری مطلب خبری را از سرور، به فرمت JSON دریافت کرده و نمایش دهیم. در AngularJS شیوه‌ی کار بدین صورت است که اطلاعاتی که به فرمت JSON هستند را با استفاده از directive ایی به نام ng-repeat پیمایش کرده و آن‌ها را نمایش دهیم.  حال اگر بخواهیم چیدمان مطالب را با استفاده از Isotope تغییر دهیم، می‌بینیم که هیچ چیزی نمایش داده نمی‌شود. دلیل آن بر می‌گردد به مراحل کامپایل کردن AngularJS و نامشخص بودن زمان اعمال چیدمان Isotope به عناصر است.

در AngularJS هنگامیکه با دستکاری DOM سر و کار پیدا می‌کنیم، معمولا باید به سراغ Directive‌ها رفت و یک Directive سفارشی برای کار با Isotope تعریف کرد تا با مکانیزم‌های Angular سازگار باشد. خوشبختانه Directive Isotope برای Angular موجود می‌باشد. نکته‌ی مهم این است که این Directive برای نگارش 1 افزونه‌ی Isotope نوشته شده است. البته با نگارش 2 هم کار می‌کند که من برای انجام کار خود نسخه‌ی 1 را ترجیح دادم استفاده کنم.

نکته‌ی بعدی که باید رعایت شود این است که چیدمان عناصر باید از راست به چپ شوند. خوشبختانه این کار در نسخه‌ی 1 Isotope با تغییر کوچکی در سورس Isotope و تغییر یک تابع انجام میشود. گویا نسخه‌ی دوم امکان پیش فرضی را برای این کار دارد، اما نتوانستم آن را به خوبی پیاده سازی کنم و به همین دلیل ترجیح دادم از همان نسخه‌ی اول استفاده کنم.

برای اینکه در هنگام جابه جا شدن عناصر، انیمیشن‌ها نیز از راست به چپ انجام شوند، باید css‌های زیر را نیز اعمال نمود:

.isotope .isotope-item {
  -webkit-transition-property: right, top, -webkit-transform, opacity;
     -moz-transition-property: right, top, -moz-transform, opacity;
      -ms-transition-property: right, top, -ms-transform, opacity;
       -o-transition-property: right, top, -o-transform, opacity;
          transition-property: right, top, transform, opacity;
}

Responsive بودن این عناصر مسئله‌ی دیگری است که باید حل گردد. امروزه اکثر فریم ورک‌های مطرح css، واکنشگرا نیز هستند و برای پشتیبانی از سایز‌های متفاوت صفحه نمایش، تدابیری در نظر گرفته‌اند. اساس کار واکنش گرا بودن این فریم ورک‌ها در تعیین ابعاد عناصر، بیان ابعاد به صورت درصدی است. مثلا فلان عرض div برابر 50% باشد بدین معناست که همیشه عرض این div نصف عرض عنصر والد آن باشد.

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

  برای حل این مشکل می‌توان از امکانات CSS به مانند دستورات زیر استفاده کرد: 
@media (min-width: 768px) and (max-width: 980px) {
    .card {
        width: 320px;
    }
}

@media (min-width: 980px) and (max-width: 1200px) {
    .card {
        width: 260px;
    }
}

@media (min-width: 1200px) {
    .card {
        width: 340px;
    }
}
بدین صورت می‌توان در ابعاد مختلف نمایشگر تعیین کرد که عرض عناصر ما چقدر باشد.
اکنون یک گالری عکس را در نظر بگیرید که در زیر هر عکس توضیحی نیز نوشته شده است و ساختار HTML آن به این صورت است که داخل هر div عکسی نیز موجود است. اگر به شیوه‌ی ذکر شده عمل کنید با یک اشکال مواجه می‌شوید و عناصر روی هم قرار گرفته و اصطلاحا overlapping اتفاق می‌افتد. دلیل این امر این است که لود شدن عکس‌ها عملی زمان گیر است و Isotope قبل از این که عکس لود شود، سایز آن عنصر را محاسبه کرده که در حقیقت این سایز بدون احتساب سایز عکس است و ابعاد واقعی عنصر ما نیست؛ در نتیجه وقتی عکس لود می‌شود آن div فضای بیشتری احتیاج دارد و به همین دلیل به زیر div‌های دیگر می‌رود.
برای حل این مشکل باید به این صورت عمل کرد که وقتی عکس‌ها کامل لود شدند، Isotope وارد عمل شده و سایز عناصر را به دست آورده و آن‌ها را بچیند. برای این کار معمولا از افزونه‌ی  imagesLoaded استفاده می‌کنند که با کمک این افزونه می‌توان مشخص کرد که وقتی تمام عکس‌های موجود در فلان div کامل لود شدند، Isotope وارد عمل شده و عناصر را چیدمان کند.
البته بدون استفاده از افزونه‌ی imagesLoaded و به کمک امکانات AngularJS و تعریف یک Directive سفارشی می‌توان زمان لود شدن عکس‌ها را کنترل کرد.
app.directive('imageOnload', function () {
            return {
                restrict: 'A',
                link: function (scope, element, attrs) {
                    element.bind('load', function () {
                        scope.$emit('iso-method', { name: 'reLayout', params: null }); // call reLayout isotope methode prevent overlaaping the items
                    });
                }
            };
        });
کار این directive این است که به ازای بارگذاری هر عکس، متد reLayout را از Isotope، فراخوانی می‌کند. از این جهت فراخوانی reLayout به ازای لود شدن هر عکس بهتر است که لود شدن تمامی عکس‌ها ممکن است مدت زمان زیادی طول بکشد و کاربر برای مدتی با یک ساختار بهم ریخته مواجه شود.
    
اگر در نمونه کدی که قرار داده‌ام، به انتهای کدهای کنترلر ListController دقت کنید، برای رویداد resize شی window، تابعی تعریف شده است تا به هنگام تغییر سایز صفحه فراخوانی شود. در این رویداد هر بار که سایز پنجره تغییر کرد، پس از یک ثانیه تابع reLayout  افزونه‌ی Isotope را فراخوانی می‌کنیم تا مجددا المنت‌های صفحه چیده شوند. البته ضرورتی وجود نداشته ولی در بعضی مواقع عناصر خوب چیده نمی‌شدند که با فراخوانی reLayout از چیدمان صحیح عناصر مطابق با سایز جدید صفحه اطمینان حاصل پیدا می‌کنیم. دلیل یک ثانیه تاخیر این است که اگر به ساز و کار تعاریف متد‌ها در directive Isotope دقت کنید، از سرویس timeout$  به وفور استفاده شده است. ظاهرا اگر برای فراخوانی reLayout زودتر عمل کنیم با فراخوانی هایی این متد در ساختار خودش تداخل پیدا می‌کند.
$(window).resize(function () {
                $timeout(function myfunction() {
                    $scope.$broadcast('iso-method', { name: 'reLayout', params: null }); // call reLayout isotope methode prevent overlaaping the items
                },1000);
                
            });
   
در نهایت تمامی نکات گفته شده را به صورت یک نمونه کد آماده کردم:
   

مطالب
توسعه برنامه های Cross Platform با Xamarin Forms & Bit Framework - قسمت اول
یکی از دغدغه‌های جدی امروزه توسعه دهندگان نرم افزار در سمت Front end، توسعه برنامه‌های Cross Platform است. در این سری آموزشی به صورت قدم به قدم و پروژه محور می‌خواهیم برنامه‌ای را برای Android/iOS/Windows توسعه دهیم که روی کامپیوتر، تبلت و موبایل به خوبی کار کند.
انتخاب ابزار درست برای شروع به کار از اهمیت شایانی برخوردار است و بد نیست در ابتدا به بررسی دلایل انتخاب ابزارهایی بپردازیم که قرار است در این دوره از آنها استفاده شود.

۱- زبان برنامه نویسی: CSharp
CSharp با وجود امکاناتی مانند Generics‌، Lambda Expressions، Linq، Async و ... که تا حدودی در سایر زبان‌ها هم هستند، زبانی خوش ساختار و کاربردی است. همچنین اضافه شدن امکانات جدیدی مانند ref returns و ... نشان دهنده این است که این زبان رو به جلو در حرکت و در برخی موارد پیشرو است. اما در توسعه یک برنامه Cross Platform مواردی اهمیت پیدا می‌کنند که شاید توسعه دهنده نرم افزار مستقیما با آنها درگیر نشود، ولی از آن‌ها تاثیر می‌پذیرد. در زبان CSharp مواردی مانند P/Invoke ،Pointers، Extern و ... جزء این دست از موارد هستند که کمک می‌کنند CSharp به یکی از لذت بخش‌ترین زبان هایی تبدیل شود که قابلیت فراخوانی 100% امکانات زبان‌های دیگر را بدون اما و اگرهای فراوان داشته باشد.
در سایر زبان‌های Cross Platform اگر کتابخانه‌های توسعه داده شده و ترکیب زبان‌های برنامه نویسی استفاده شده در آنها را بررسی کنید، می‌بینید که اگر قرار است کتابخانه مربوطه مثلا در JavaScript استفاده شود، توسعه دهنده کد، درصدی از کد را با Java، درصدی را با Swift و درصدی را با JavaScript توسعه داده است! اگر معادل همان کتابخانه را برای CSharp پیدا کنید، می‌بینید که تمامی قسمت‌های مربوط به اندروید، iOS و ویندوز به زبان CSharp است.
برای مثال در ادامه کدهای مربوط به پروژه‌ای را می‌بینید که هدف آن، ارائه متدهایی ساده برای کار با امکانات مختلف دستگاه، به صورت Cross Platform هست. مثلا برای بررسی وضعیت باطری بنویسید:
var state = Battery.State; //  Charging, Full, Discharging, ...
که تماما با CSharp توسعه داده شده است.
اما معادل چنین پروژه‌ای در هیچ زبان دیگری به صورت 100% با خود آن زبان توسعه داده نشده‌است و بیشتر مواقع با چنین چیزی مواجه می‌شوید:

این مسئله وقتی حائز اهمیت می‌شود که در پروژه‌تان به سمت کارهایی حرکت کنید که کمی خاص باشند و نتوانید کتابخانه‌ای را پیدا کنید که نیازهای شما را پوشش دهد و یا از کیفیت خوبی برخوردار نباشد و ... و خلاصه بخواهید کمی بیشتر دست به کد شوید. در چنین مواقعی شما عملا درگیر چندین زبان و محیط توسعه و سیستم عامل و Debugger و ... می‌شوید. به هر میزان که برنامه شما خاص باشد، این هزینه افزایش پیدا می‌کند تا جایی که ممکن است ادامه توسعه نرم افزار را غیر ممکن کند.
در CSharp شما به صد در صد امکانات سیستم عامل‌ها (Android/iOS/Windows/Linux/Mac/Tizen) دسترسی دارید.

۲- اجرا کننده برنامه: NET.
انتخاب NET. و کتابخانه‌های آن مانند Task Parallel Library - Entity Framework(Sqlite) - Noda - JSON.NET که در هر زمینه‌ای بالاترین کیفیت ممکن را به شما ارائه می‌کنند به خودی خود منطقی به نظر می‌رسد. اما تمامی این‌ها در کنار سرعت اجرای NET. به صورت Native و همچنین قابلیت اجرای NET. در تمامی سیستم عامل‌ها و همچنین امکان اجرای آن در مرورگر به کمک استاندارد Web Assembly آن را به انتخابی فوق العاده بدل می‌کند. سرعت گسترش محبوبیت و استفاده از NET. در دنیا نیز دلیل دیگری است برای اطمینان خاطر از انتخاب درست.

۳- Xamarin forms
Xamarin forms همه آن چیزهای پایه‌ای است که برای نوشتن یک برنامه لازم داریم. کنترل هایی مانند ListView، Button و ...به همراه Binding - Navigation و ...
در عمل می‌توانید آن را معادل Angular & Angular Material بدانید. وقتی شما فرمی را با Xamarin Forms توسعه می‌دهید و درون آن دکمه‌ای است که از فرم اول، شما را به فرم دوم می‌برد، می‌توانید آن را در هر جایی که Xamarin forms پشتیبانی می‌کند، استفاده کنید. پشتیبانی Xamarin forms برای Android/iOS/Windows خوب و برای Linux/Mac/Tizen و Web در مراحل اولیه است.
در Xamarin forms شما UI کاملا Native خواهید داشت.

۴- Prism Patterns & practices
Prism همه آن چیزی است که برای نوشتن یک برنامه با کیفیت، با قابلیت نگهداری بالا و تست پذیر احتیاج داریم.

با نقش Bit و کمک‌های آن در طول مسیر آموزش بیشتر آشنا خواهیم شد.

در قسمت‌های بعدی به آموزش نصب و نحوه دیباگ کردن کد و ارائه پابلیش در Android-iOS-Windows خواهیم پرداخت و سپس وارد کدنویسی شده و پروژه اولیه را خواهیم ساخت و در قسمت‌های بعد از آن هم کار با دیتابیس کلاینت ساید، ارتباط با سرور و ... را آموزش می‌بینیم.
اگر قبلا Xamarin Forms را تست کرده‌اید و به علت مسائلی مانند حجم بالای خروجی برنامه و یا کندی در توسعه برنامه یا اجرای آن در دستگاه مشتری آن را کنار گذاشته‌اید، توصیه می‌کنم بار دیگر آن را با ما تست کنید و با رعایت چند نکته ساده از نوشتن برنامه Cross Platform به بهترین شکل لذت ببرید و خروجی خوبی را در نهایت به مشتریان سیستم ارائه کنید.
نظرات اشتراک‌ها
مقایسه‌ای بین امکانات Rider و Visual Studio
یک موضوعی در مورد IDE‌های jetbrain خیلی وقته وجود داره و اونم عدم امکان داخلی یا حتی افزونه برای minify کردن فایلهای اسکریپت و استایل هستش. شاید این موضوع خیلی خنده دار به نظر بیاد ولی موقعی که روی پروژه Spring داخل jetbrain Intellij کار می‌کردم یا با Rider کار می‌کردم خیلی دنبال این مورد گشتم ولی همه راه حل هایی که گفته بودن در حد Task runner‌ها و پکیج‌های npm بود که واقعا خیلی غیرعادیه. در صورتیکه حتی داخل vs code چند تا افزونه برای اینکار وجود داره که خیلی ساده minify رو انجام میدن. یا مشکل format کد javascript داخل فایل‌های razor مثل cshtml که از نسخه اولیه Rider تا الان وجود داشته و حل نشده باقی مونده. همه این موضوعات ریز و درشت رو در نظر بگیریم واقعا نمیشه در مورد اینکه کدوم IDE بهتره نظر دقیقی ارائه کرد.