اشتراک‌ها
وضعیت پشتیبانی از ASP.NET Web Forms چگونه است؟

طول عمر آن به طول عمر ویندوز 10 و NET 4.8. که تا سال 2025 پشتیبانی می‌شوند، گره خورده است؛ اما ... نگارش بعدی دات نت یا نگارش 5، دیگر به همراه Web Forms نیست.

وضعیت پشتیبانی از ASP.NET Web Forms چگونه است؟
اشتراک‌ها
تمرکز اصلی در امکانات پیش روی Visual Studio بر DotNET Core میباشد

.NET Core, the reinvention of the Microsoft .NET Framework as an open source, cross-platform development choice, is a key focus of the upcoming features planned for the Visual Studio IDE.

In conjunction with .NET Standard -- a spec detailing what .NET APIs should be available on all .NET implementations -- .NET Core retains compatibility with .NET Framework, Xamarin and Mono while letting developers use Windows, macOS or Linux machines to code for mobile, cloud and embedded/IoT projects.

While still primarily driven by Microsoft dev teams, .NET Core is hosted on a GitHub repository and is supported by the .NET Foundation, an independent organization created by Microsoft three years ago to improve its open source development and collaboration.

Peeking at the Visual Studio Roadmap (updated last week) reveals .NET Core figures prominently in upcoming functionality for the IDE, both in the near-term and long:  

تمرکز اصلی در امکانات پیش روی Visual Studio بر DotNET Core میباشد
اشتراک‌ها
سری مقدماتی NET Core.

Day 1 - Installing and Running .NET Core on a Windows Box
Day 2 - Taking a Look at the Visual Studio Templates for .NET Core
Day 3 - Running a .NET Core app on a Mac
Day 4 - Creating a NuGet Package from .NET Core app
Day 5 - Creating a Test Project from .NET Core
Day 6 - Migrating an existing .NET Core to csproj
Day 7 - Creating an ASP.NET Core Web Application 

سری مقدماتی NET Core.
مسیرراه‌ها
ASP.NET MVC
              مطالب
              برنامه نویسی موازی بخش دوم (محافظت از مقادیر مشترک)
               در بخش قبلی، مروری کلی بر مفاهیم اصلی برنامه نویسی موازی، از جمله شرایط و نکات استفاده از آن را بررسی کردیم. در انتهای بخش اول عنوان کردیم که در روند برنامه نویسی موازی، اگر دو یا چند Thread به طور مشترک به داده‌ای دسترسی داشته باشند، امکان بروز Race condition وجود خواهد داشت. پس باید کد خود را Thread Safe کنیم. می‌توان برای کنترل رفتارهای عجیب اشیاء در محیط‌های Multi Thread، عنوان Thread Safety را بکار برد.

              به طور کلی ۴ روش در #C برای ایجاد Thread Safety وجود دارند:


              1- Lock/Monitor
              این دو روش یکسان هستند و مانند هم عمل می‌کنند. در واقع در ابتدا روش Monitor وجود داشته و بعد روش lock برای کوتاهی syntax، به صورت بلاکی به #C افزوده شده‌است. این روش تنهای بر روی Thread‌های داخلی App Domain کنترل دارد (اجازه ورود یک Thread) و نمی‌تواند بر روی Thread‌های خارج از این حوزه در محیط‌های Multi Thread محدودیتی اعمال نماید. منظور از Thread‌های داخلی، Thread هایی هستند که داخل Application ما ایجاد شده‌اند.

              به تکه کد زیر توجه کنید:
              using System;
              using System.Collections.Generic;
              using System.Linq;
              using System.Text;
              using System.Threading.Tasks;
              using System.Threading;
              
               class Program
                  {
                      static int a = 0;
                      static int b = 0;
                      static Random random = new Random();
                      
                      static void Main(string[] args)
                      {
              
                          Thread obj = new Thread(Division);
                          obj.Start();
              
                          Division();
                      }
              
                      static void Division()
                      {
              
                          for (int i = 0; i <= 500; i++)
                          {
              
                              try
                              {
                                 
                                      //Choosing random numbers between 1 to 5
                                      a = random.Next(1, 10);
                                      b = random.Next(1, 10);
              
              
                                      //Dividing
                                      double ans = a / b;
              
              
                                      //Reset Variables
                                      a = 0;
                                      b = 0;
              
                                      Console.WriteLine("Answer : {0} --> {1}", i, ans);
                                  
                              }
                              catch (Exception ex)
                              {
                                  Console.WriteLine(ex.ToString());
                              }
                          }
                      }
                  }

              همانطور که در کد بالا ملاحظه می‌کنید، متد Division به صورت Thread Safe پیاده سازی نشده‌است! اما مشکل کجاست!؟

              با برسی این متد و عملکرد آن متوجه می‌شویم که این متد در یک چرخه‌ی تکرار ۵۰۰ مرتبه‌ای، دو عدد تصادفی را در بازه‌ی ۱ تا ۱۰، انتخاب کرده و آن‌ها را بر هم تقسیم و متغیر‌های تصادفی را با مقدار ۰ پر می‌کند. همین عمل Reset Variable در این متد، باعث بروز خطا در محیط Multi Thread خواهد شد. بدین صورت که اگر این متد مانند مثال بالا توسط دو Thread مجزا فراخوانی شود، یکبار توسط New Thread و بلافاصله در Thread اصلی Application، احتمال این وجود خواهد داشت که در Thread دوم، بعد از انتخاب دو مقدار تصادفی و درست قبل از عملیات تقسیم، به طور همزمان Thread اول عملیات Reset Variable را انجام دهد که باعث بروز خطای تقسیم بر ۰ در Thread دوم می‌شود. این همان مشکلی است که گاها یافتن آن از طریق Debug بسیار دشوار خواهد بود.
              اما با تغییر کد به شکل زیر
              class Program
                  {
                      static int a = 0;
                      static int b = 0;
                      static Random random = new Random();
                      static readonly object _object = new object();
                      static void Main(string[] args)
                      {
              
                          Thread obj = new Thread(Division);
                          obj.Start();
              
                          Division();
                      }
              
                      static void Division()
                      {
              
                          for (int i = 0; i <= 500; i++)
                          {
              
                              try
                              {
                                  Monitor.Enter(_object);
                                 
                                      //Choosing random numbers between 1 to 5
                                      a = random.Next(1, 10);
                                      b = random.Next(1, 10);
              
              
                                      //Dividing
                                      double ans = a / b;
              
              
                                      //Reset Variables
                                      a = 0;
                                      b = 0;
              
                                      Console.WriteLine("Answer : {0} --> {1}", i, ans);
                                  Monitor.Exit(_object);
              
                              }
                              catch (Exception ex)
                              {
                                  Console.WriteLine(ex.ToString());
                              }
                          }
                      }
                  }

              مادامی که یک Thread در حالت انتخاب اعداد تصادفی تا تقسیم و اعلام نتیجه می‌باشد، به Thread‌های داخلی دیگر، اجازه‌ی ورود به این بخش که تحت کنترل Monitor می‌باشد داده نخواهد شد. همانطور که گفته شده، بازه‌ی تحت کنترل مانیتور میتواند با بلاک Lock(object) جایگزین شود. شیء object یک شیء مشترک (static) میان تمام اشیاء است برای کنترل ورود Thread‌ها و قفل گزاری مشترک بین این اشیاء.

              2- Mutex:
              این نوع قفل گزاری به منظور محافظت منابع مشترک برای جلوگیری از ورود Thread‌های بیرونی استفاده می‌شود. منظور از Thread‌های بیرونی Thread‌های یک کامپیوتر است. همچنین می‌توان از Mutex بجای lock نیز استفاده کرد؛ اما به دلیل هدف کاری Mutex، باید هزینه‌ی بیشتری (تقریبا 50 برابر کندتر از Lock) پرداخت کرد.
               static void Main()
                { 
                  using (var mutex = new Mutex (false, "dotnettips.info Demo"))
                  {
                   
                    if (!mutex.WaitOne (TimeSpan.FromSeconds (3), false))
                    {
                      Console.WriteLine ("Another app instance is running. Bye!");
                      return;
                    }
                    RunProgram();
                  }
                }
               
                static void RunProgram()
                {
                  Console.WriteLine ("Running. Press Enter to exit");
                  Console.ReadLine();
                }
              در مثال بالا از یک Mutex نام دار استفاده شده است که به ما این امکان را می‌دهد تا به صورت Computer-Wide روی Thread‌ها ایجاد محدودیت نماییم. اگر متد بالا را در دو ترمینال اجرا کنید، نسخه‌ی دوم اجرا نخواهد شد. البته این نکته را در نظر داشته باشید که این امکان در سیتم عامل‌های مبتنی بر Linux غیرفعال است .
              Mutex دارای دو متد مهم است :

              ۱- WaiteOne : شروع Blocking با این متد خواهد بود و اگر بتواند عملیات blocking را انجام دهد مقدار True را باز می‌گرداند. این متد دارای دو ورودی دیگر نیز هست که در مقالات بعدی به طور مفصل به آن‌ها اشاره خواهد شد. اما بطور خلاصه می‌توان اینگونه عنوان نمود که یک پارامتر زمان وجود دارد که مدت زمان انتظار برای Blocking را مشخص می‌کند و پارامتر Boolean دیگری که در حالت synchronization مورد استفاده قرار می‌گیرد و خروج و یا عدم خروج از دامنه synchronization را مشخص می‌کند.

              ۲- ReleaseMutex : شروع آزاد سازی انحصار، با این متد انجام می‌شود.

              هیچگاه نباید یک Mutex را در کد رها کرد؛ زیرا باعث به‌وجود آمدن خطاهایی در کد خواهد شد. روش‌هایی برای رها سازی وجود دارد مانند Dispose کردن Mutex و یا استفاده از متد ReleaseMutex. قبل از خروج از کد باید دقت داشت در بخش هایی از کد که از این نوع قفل گزاری استفاده شده‌است، حتما باید مکانیسم‌های Exception Handling و یا Disposing را برای مدیریت Mutex ایجاد شده اعمال کرد.

              3 -Semaphore 
              یک نسخه پیشرفته‌تر از Mutex است که می‌تواند برای Thread‌های داخلی و یا خارجی استفاده شود و روی آنها اعمال محدودیت کند. همچنین می‌تواند اجازه‌ی ورود یک تا چند Thread را به بخشی از کد، برای محافظت از منابع بدهد. Semaphore نیز مانند Mutex دارای متد‌های Wait و Release است. یک Semaphore با ظرفیت ورود یک Thread در لحظه همان Mutex است. همچنین از Semaphore‌‌ها می‌توان در متدهای Async نیز استفاده کرد.

              4- SemaphoreSlim
              در واقع یک نسخه‌ی پیشرفته از Monitor و یک نسخه‌ی سبک وزن از Semaphore است و به همان شکل به شما اجازه‌ی محدودیت گزاری فقط بر روی Thread‌های داخلی را می‌دهد. اما بجای اجازه‌ی ورود فقط یک Thread، به شما این امکان را می‌دهد که اجازه‌ی ورود همزمان یک یا چند Thread را به انتخاب خود بدهید.

              هزینه‌ی اعمال محدودیت (قفل گزاری) روی Thread ها
              به طور کل هزینه‌ی قفل گزاری بر روی Thread‌ها بالاست. اما در صورت نیاز باید انتخاب درستی از بین موارد عنوان شده را انتخاب نمود. lock/Monitor و SemaphoreSlim دارای کمترین هزینه و Mutex و Semaphore دارای بیشترین هزینه و سربار هستند. اگر در Application‌های بزرگ از Mutex و Semaphore به درستی استفاده نشود، به جد باعث کندی خواهد شد.

              در بخش بعدی مقاله، Double-checked locking را مورد بررسی قرار خواهیم داد.
              اشتراک‌ها
              ریلیز شد Orleans 7.0

              The .NET 7 release marks an exciting milestone in many ways, but one in particular that’s exciting for ASP.NET developers building distributed apps or apps designed to be cloud native and ready for dynamic horizontal scale out is the addition of the Orleans team to the broader .NET team. Bringing Orleans and ASP.NET Core closer together has led to some exciting ideas for the future of how we blend Orleans into the ASP.NET toolchain, and coupled with the huge advances in performance throughout .NET 7 are improvements to Orleans 7 that bring over 150% improvements to some areas of the Orleans toolchain. This post will introduce you to some of the new features in Orleans 7. 

              ریلیز شد Orleans 7.0
              اشتراک‌ها
              قسمت‌های مختلف Xamarin با NET 6. یکی خواهند شد

              As part of our .NET unification, Xamarin.iOS and Xamarin.Android will become part of .NET 6 as .NET for iOS and .NET for Android. Because these bindings are projections of the SDKs shipped from Apple and Google, nothing changes there, however build tooling, target framework monikers, and runtime framework monikers will be updated to match all other .NET 6 workloads. Our commitment to keeping .NET developers up-to-date with the latest mobile SDKs is foundational to .NET MAUI and remains firm. When .NET 6 ships, we expect to ship a final release of Xamarin SDKs in their current form that will be serviced for a year. All modern work will at that time shift to .NET 6. 

              قسمت‌های مختلف Xamarin با NET 6. یکی خواهند شد
              مطالب
              آشنایی با WPF قسمت چهارم: کنترل ها
              WPF همانند Windows Form شامل ابزارها یا کنترل‌های داخلی است که می‌توانند در تهیه‌ی یک برنامه بسیار کارآمد باشند. در این بخش به بررسی تعدادی از این کنترل‌ها می‌پردازیم و مابقی آن‌ها را در قسمت‌های آینده بررسی خواهیم کرد. در این نوشتار سعی بر این است که یک فرم ساده را با آن ایجاد کرده و مورد استفاده قرار دهیم.
              این فرم دارای اطلاعاتی شامل : نام، جنسیت ، زمینه‌های کاری، کشور، تاریخ تولد و تصویر می‌باشد.

              TextBlock

              همان Label قدیمی خودمان است که برای نمایش متون کاربر دارد. متن داخل آن بین دو تگ قرار می‌گیرد و یا از خاصیت Text آن کمک گرفته خواهد شد. حتما از خاصیت Width و height آن برای مقداردهی کمک بگیرید، زیرا در غیر آن صورت کل Container خود را خواهد پوشاند. در صورتی که متنی در مکان خود جا نشود می‌توان از دو ویژگی استفاده کرد. آن را برش داد یا به خطوط بعدی شکست. برای حذف یا برش باقی مانده متن می‌توان از خصوصیت TextTrimming استفاده کرد که سه مقدار می‌گیرد:

              None مقدار پیش فرض

              CharacterEllipsis با نزدیک شدن به آخر پهنای کار از ... استفاده می‌نماید. در صورتی که لیستی یا مورد مشابهی دارید میتواند بسیار کاربردی باشد.

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

              گزینه TextWrapping جهت شکستن یک خط به خطوط است؛ موقعی که متن شما به انتهای صفحه می‌رسد، این ویژگی باعث می‌شود متن به بیرون از پنجره نرفته و یک خط به سمت پایین حرکت کند. این گزینه سه مقدار را دارد:

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

              None: مقدار پیش فرض که خصوصیت Wrap را به همراه ندارد.

              Wrap: فعال سازی ویژگی TextWrapping

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

              خصوصیت LineStackingStrategy:

              این خصوصیت فاصله‌ی بین خطوط را با استفاده از یک واحد منطقی dp مشخص می‌کند. هر چند دو گزینه دیگر هم دارد که دو تصویر زیر را در این صفحه به شما نمایش می‌دهد:

              برای ساخت فرم از یک گرید با سه ستون و 6 سطر استفاده می‌کنم.

              <Grid Margin="5">
                  <Grid.ColumnDefinitions>
                      <ColumnDefinition Width="Auto"></ColumnDefinition>
                      <ColumnDefinition Width="*"></ColumnDefinition>
                          <ColumnDefinition Width="Auto"></ColumnDefinition>
                  </Grid.ColumnDefinitions>
                  <Grid.RowDefinitions>
                          <RowDefinition Height="Auto"></RowDefinition>
                          <RowDefinition Height="Auto"></RowDefinition>
                          <RowDefinition Height="Auto"></RowDefinition>
                          <RowDefinition Height="Auto"></RowDefinition>
                          <RowDefinition Height="Auto"></RowDefinition>
                          <RowDefinition Height="Auto"></RowDefinition>
                      
                      </Grid.RowDefinitions>
              </Grid>
              در ستون اول نام فیلدهای مورد نظر را می‌نویسیم و در ستون دوم هم کنترل‌های مد نظر هر فیلد را قرار خواهیم داد. در صورتی که دوست دارید کار از راست به چپ پشتیبانی کند از گزینه OverflowDirection در تگ پنجره Window استفاده نمایید.
              در داخل گرید بعد از تعریف سطر و ستون، همانطور که قبلا توضیح دادیم کنترل‌های TextBlock را اضافه می‌کنیم:
              <TextBlock Grid.Column="0"  Grid.Row="0" VerticalAlignment="Center" HorizontalAlignment="Left" >Name</TextBlock>
                      <TextBlock Grid.Column="0"  Grid.Row="1" VerticalAlignment="Center" HorizontalAlignment="Left" >Gender</TextBlock>
                      <TextBlock Grid.Column="0"  Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Left" >Field Of Work</TextBlock>
                      <TextBlock Grid.Column="0"  Grid.Row="3" VerticalAlignment="Center" HorizontalAlignment="Left" >Country</TextBlock>
                      <TextBlock Grid.Column="0"  Grid.Row="4" VerticalAlignment="Center" HorizontalAlignment="Left" >Birth Date</TextBlock>
              
              <TextBox Grid.Row="0" Grid.Column="1" Name="Txtname" HorizontalAlignment="Left" Margin="5" Width="200" ></TextBox>
              برای فیلد نام، از کنترل TextBox استفاده کردم که با محدود کردن Width آن اندازه ثابت به آن دادم. در صورتی که width ذکر نشود یا به Auto ذکر شود، در صورتی که متنی که کاربر تایپ می‌کند، بیش از اندازه تعیین شده کنترل Textbox باشد، کنترل هم همراه متن بزرگتر خواهد شد و تا پایان محدوده سلولی اش در گرید کش خواهد آمد.

              Buttons 
              برای فیلد جنسیت Gender هم از RadioButton کمک گرفتم که با استفاده از خاصیت GroupName می‌توان دسته‌ای از این کنترل‌ها را با هم مرتبط ساخت تا با انتخاب یک آیتم جدید از همان گروه، آیتم قبلی که انتخاب شده بود از حالت انتخاب خارج شده و آیتم جدیدی انتخاب شود. از خاصیت IsChecked می‌توان برای انتخاب یک آیتم بهره برد.

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

              • Button
              • ToggleButton
              • CheckBox
              • RadioButton

              که همگی این عناصر از کلاسی به نام ButtonBase مشتق شده اند. کد زیر RadioButton‌ها را به صورت عمودی چینش کرده است:

              <StackPanel Orientation="Vertical" Grid.Row="1" Grid.Column="1" Margin="10">
                          <RadioButton GroupName="Gender" Name="RdoMale" IsChecked="True" >Male</RadioButton>
                          <RadioButton GroupName="Gender" Name="RdoFemale" Margin="0 5 0 0" >Female</RadioButton>
                      </StackPanel>
              برای فیلد زمینه کاری ، لیست کشورها و تاریخ تولد از کدهای زیر کمک گرفتم:
              <StackPanel Orientation="Horizontal" Grid.Row="2" Grid.Column="1" Margin="10">
                          <CheckBox Name="ChkActor" >Actor/Actress</CheckBox>
                          <CheckBox Name="ChkDirector" >Director</CheckBox>
                          <CheckBox Name="ChkProducer" >Producer</CheckBox>
                      </StackPanel>
              
                      <ListBox Grid.Row="3" Grid.Column="1" Margin="10"  Height="80">
                      <ListBoxItem>
                              <TextBlock>UnitedStates</TextBlock>
                          </ListBoxItem>
                          <ListBoxItem>
                              <TextBlock >UK</TextBlock>
                          </ListBoxItem>
                          <ListBoxItem>
                              <TextBlock >France</TextBlock>
                          </ListBoxItem>
                          <ListBoxItem>
                              <TextBlock >Japan</TextBlock>
                          </ListBoxItem>
                      </ListBox>
              
                      <Calendar Grid.Row="4" Grid.Column="1" HorizontalAlignment="Left" Margin="10"></Calendar>
              برای لیست کشورها می‌توان از یک ListBox یا ComboBox استفاده کرده که هر آیتم داخل آنها در یک تگ ListBoxItem یا ComboBoxItem قرار می‌گیرد. اگر از حالت ListBox استفاده می‌کنید، در صورتی که آیتم‌ها از ارتفاع لیست بیشتر شود به طور خودکار یک Scrollbar برای آن‌ها در نظر گرفته خواهد شد و نیازی نیست که آن را دستی اضافه کنید.
              برای تصویر شخص، قصد دارم آن را در گوشه‌ی سمت راست و بالا قرار دهم. برای همین محل ستون آن را ستون سوم یا اندیس دوم انتخاب کرده و از آنجا که این عکس حالت پرسنلی دارد، می‌تواند چند سطر را به خود اختصاص دهد که با کمک خاصیت Rowspan، چهار سطر، کنترل را ادامه دادم. برای ستون‌ها هم می‌توان از خاصیت ColumnSpan استفاده کرد. همچنین دوست دارم یک دکمه هم روی تصویر در گوشه‌ی سمت چپ و پایین قرار داده که کاربر با انتخاب آن به انتخاب عکس یا تغییر آن بپردازد. برای همین از یک پنل گرید استفاده کردم و کنترل دکمه را روی تصویر قرار دادم. همپوشانی کنترل‌ها در اینجا صورت گرفته است.

               <Grid Grid.Row="0" Grid.Column="2" Grid.RowSpan="4">
                          <Grid.RowDefinitions>
                              <RowDefinition Height="*"></RowDefinition>
                          </Grid.RowDefinitions>
                          <Grid.ColumnDefinitions>
                              <ColumnDefinition Width="*"></ColumnDefinition>
                          </Grid.ColumnDefinitions>
                          <Image HorizontalAlignment="Right" Source="man.jpg" Stretch="UniformToFill" VerticalAlignment="Top" Width="100" Height="150"></Image>
                          <Button Width="25" Height="15"    Padding="0"  HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="0,0,0,83">
                               <TextBlock VerticalAlignment="Center" Margin="0 -7 0 0">...</TextBlock>
                          </Button>
                      </Grid>
              خاصیت Stretch کنترل Image در بالا، نحوه‌ی نمایش تصویر را نشان می‌دهد که چهار مقدار دارد:
              None: تصویر، اندازه‌ی اصلی خود را حفظ کرده و هر مقدار آن که در کنترل جا شود، نمایش می‌یابد و بسته به سایز تصویر ممکن است گوشه هایی از تصویر نمایش نیابد.
              Fill: تصویر را داخل کنترل به زور جا داده تا پهنا و ارتفاع عکس، هم اندازه کنترل می‌شود.
              Uniform: تصویر بزرگ را با در نظر گرفتن نسبت پهنا و ارتفاع تصویر، با یکدیگر در کنترل جا می‌دهد.
              UniformToFill: تصویر، کل کنترل را می‌گیرد ولی نسبت پهنا و عرض را حفظ کرده ولی قسمت هایی از تصویر در کنترل دیده نمی‌شود.

              همانطور که قبلا هم گفتیم، خود کنترل دکمه شامل زیر کنترل‌هایی می‌شود که یکی از آن‌ها TextBlock است و از طریق خصوصیت *.TextBlock دیگر خصوصیات آن قابل تنظیم است و البته برای خصوصی سازی بیشتر هم می‌توان یک TextBlock را به صورت Nested یعنی داخل تگ Button تعریف کنید که ما همین کار را کرده ایم.
              فرم نهایی ما به صورت زیر است:


              در صورتی که دوست دارید جهت ListBox را از عمودی به افقی تغییر دهید می‌توانید از پنل‌های Stack یا Wrap استفاده کنید که تعریف آن به شکل زیر است:
              <ListBox>
                  <ListBox.ItemsPanel>
                      <ItemsPanelTemplate>
                          <VirtualizingStackPanel Orientation="Horizontal" />
                      </ItemsPanelTemplate>
                  </ListBox.ItemsPanel>
              </ListBox>

              بدین صورت ListBox به شکل زیر تغییر می‌یابد:



              و در صورتی که می‌خواهید Scroll حذف شود و از Wrap استفاده کنید، کد را به شکل زیر تعریف کنید. فراموش نکنید که اسکرول افقی را غیرفعال کنید؛ وگرنه نتیجه کار به شکل بالا خواهد بود.
              <ListBox ScrollViewer.HorizontalScrollBarVisibility="Disabled">
                  <ListBox.ItemsPanel>
                      <ItemsPanelTemplate>
                          <WrapPanel />
                      </ItemsPanelTemplate>
                  </ListBox.ItemsPanel>
              </ListBox>
              نتیجه:

              Calendar
              تقویم یکی دیگر از کنترل‌های موجود است که شامل خصوصیات زیر است:
              DisplayDate: تاریخ پیش فرض و اولیه تقویم را مشخص می‌کند؛ در صورتی که ذکر نشود تاریخ جاری درج می‌شود.
              <Calendar DisplayDate="01.01.2010" />
              از خصوصیات دیگر در این زمینه می‌توان به DisplayDateStart و DisplayDateEnd اشاره کرد که محدوه‌ی نمایش تاریخ تقویم را مشخص می‌کند. کد زیر تنها تاریخ‌های روز اول ماه ابتدای سال 2015، تا روز اول ماه پنجم 2015 را نمایش می‌دهد:
              <Calendar DisplayDateStart="01.01.2015" DisplayDateEnd="05.01.2015" />


              SelectionMode: نحوه‌ی انتخاب تاریخ را مشخص می‌کند:
              SingleDate: فقط یک تاریخ قابل انتخاب است.
              SingleRange: می‌توانید از یک تاریخ تا تاریخ دیگر را انتخاب کنید. ولی نمی‌توانید مجددا چند انتخاب دیگر را در جای جای تقویم داشته باشید. مثلا از تاریخ 5 آپریل تا 10 آپریل را انتخاب کرده‌اید؛ ولی دیگر نمی‌توانید تاریخ 15 آپریل یا محدوده‌ی 15 آپریل تا 20 آپریل را انتخاب کنید. چون تنها قادر به انتخاب یک رنج یا محدوده تاریخی هستید.
              MultipleRanges: بر خلاف گزینه‌ی بالایی هر محدوده تاریخی قابل انتخاب است.
              <Calendar SelectionMode="MultipleRange" />

              نکته بعدی در مورد غیرفعال کردن بعضی از تاریخ هاست که شما قصد ندارید به کاربر اجازه دهید آن‌ها را انتخاب کند. برای مثال تاریخ‌های 1 آپریل تا 10 آپریل را از دسترس خارج کنید. برای همین از خصوصیت BlackoutDates استفاده می‌کنیم که نحوه‌ی تعریف آن به شرح زیر است که در این کد دو محدوده‌ی تاریخی غیر فعال شده اند:
              <Calendar>
                  <Calendar.BlackoutDates>
                      <CalendarDateRange Start="01/01/2010" End="01/06/2010" />
                      <CalendarDateRange Start="05/01/2010" End="05/03/2010" />
                  </Calendar.BlackoutDates>
              </Calendar>



              DisplayMode
              : به طور پیش فرض، تقویم ماه‌ها را نشان می‌دهد ولی میتوانید آن را توسط این خصوصیت روی سال یا دهه و ماه هم تنظیم کنید.
              با انتخاب سال Year، تقویم ماه‌های یک سال را نمایش می‌دهد.
              با انتخاب دهه Decades سال‌های یک دهه‌ی تعیین شده را نشان می‌دهد و با انتخاب ماه Month روز‌های هر ماه در آن نمایش داده می‌شود.
              در هنگام انتخاب این گزینه، به داخل تقویم نگاه نکنید، بلکه به سر تیتر آن نگاه کنید.
              <Calendar DisplayMode="Year" />



              مطالب
              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
              مطالب
              C# 12.0 - Interceptors
              به C# 12 و دات‌نت 8، ویژگی «آزمایشی» جدیدی به نام Interceptors اضافه شده‌است که به آن «monkey patching» هم می‌گویند. هدف از آن، جایگزین کردن یک پیاده سازی، با پیاده سازی دیگری است. به این ترتیب توسعه دهندگان دات‌نتی می‌توانند فراخوانی متدهایی خاص را ره‌گیری کرده (interception) و سپس آن‌را به فراخوانی یک پیاده سازی جدید، هدایت کنند.


              Interceptor چیست؟

              از زمان ارائه‌ی NET 8 preview 6 SDK. به بعد، امکان ره‌گیری هر متدی از کدهای برنامه، به دات‌نت اضافه شده‌است؛ به همین جهت از واژه‌ی Interceptor/ره‌گیر در اینجا استفاده می‌شود. خود تیم دات‌نت از این قابلیت در جهت بازنویسی پویای قسمت‌هایی از کدهای زیرساخت دات‌نت که از Reflection استفاده می‌کنند، با نگارش‌های کامپایل شده‌ی مختص به برنامه‌ی شما، کمک می‌گیرند. به این ترتیب سرعت و کارآیی برنامه‌های دات‌نت 8، بهبود قابل ملاحظه‌ای را پیدا کرده‌اند. برای مثال ahead-of-time compilation (AOT) در دات‌نت 8 و ASP.NET Core 8x بر اساس این ویژگی پیاده سازی شده‌است. این ویژگی جدید، مکمل source generators است که در نگارش‌های پیشین دات‌نت ارائه شده بود.


              بررسی  Interceptors با تهیه‌ی یک مثال ساده

              فرض کنید می‌خواهیم فراخوانی متد GetText زیر را ره‌گیری کرده و سپس آن‌را با نمونه‌ی دیگری جایگزین کنیم:
              namespace CS8Tests;
              
              public class InterceptorsSample
              {
                  public string GetText(string text)
                  {
                      return $"{text}, World!";
                  }
              }
              برای اینکار ابتدا نیاز است یک فایل جدید را به نام InterceptsLocationAttribute.cs با محتوای زیر به پروژه اضافه کرد:
              namespace System.Runtime.CompilerServices;
              
              [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
              public sealed class InterceptsLocationAttribute : Attribute
              {
                  public InterceptsLocationAttribute(string filePath, int line, int character)
                  {
                  }
              }
              همانطور که در مقدمه‌ی بحث هم عنوان شد، این ویژگی هنوز آزمایشی است و نهایی نشده و ویژگی فوق نیز هنوز به دات‌نت اضافه نشده‌است. به همین جهت فعلا باید آن‌را به صورت دستی به پروژه اضافه کرد و احتمالا در نگارش‌های بعدی دات‌نت، امضای آن تغییر خواهد کرد ... یا حتی ممکن است بطور کامل حذف شود!

              سپس فرض کنید فراخوانی متد GetText در فایل Program.cs برنامه به صورت زیر انجام شده‌است:
              using CS8Tests;
              
              var example = new InterceptorsSample();
              var text = example.GetText("Hello");
              Console.WriteLine(text); //Hello, World!
              یعنی متد GetText، در سطر چهارم و کاراکتر 20 ام آن فراخوانی شده‌است. این اعداد مهم هستند!

              در ادامه از این اطلاعات در ره‌گیر سفارشی زیر استفاده خواهیم کرد:
              using System.Runtime.CompilerServices;
              
              namespace CS8Tests;
              
              public static class MyInterceptor
              {
                  [InterceptsLocation("C:\\Path\\To\\CS8Tests\\Program.cs", 4, 20)] 
                  public static string InterceptorMethod(this InterceptorsSample example, string text)
                  {
                      return $"{text}, DNT!";
                  }
              }
              این ره‌گیر که به صورت متدی الحاقی برای کلاس InterceptorsSample دربرگیرنده‌ی متد GetText تهیه می‌شود، کار جایگزینی فراخوانی آن‌را در سطر چهارم و کاراکتر 20 ام فایل Program.cs انجام می‌دهد. امضای پارامترهای این متد، باید با امضای پارامترهای متد ره‌گیری شده، یکی باشد.

              اکنون اگر برنامه را اجرا کنیم ... با خطای زیر مواجه می‌شویم:
               error CS9137: The 'interceptors' experimental feature is not enabled in this namespace. Add
              '<InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);CS8Tests</InterceptorsPreviewNamespaces>'
              to your project.
              عنوان می‌کند که این ویژگی آزمایشی است و باید فایل csproj. را به صورت زیر تغییر داد تا بتوان از آن استفاده نمود:
              <Project Sdk="Microsoft.NET.Sdk">
                <PropertyGroup>
                  <OutputType>Exe</OutputType>
                  <TargetFramework>net8.0</TargetFramework>
                  <ImplicitUsings>enable</ImplicitUsings>
                  <Nullable>enable</Nullable>
                  <!--<NoWarn>Test001</NoWarn>-->
                  <InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);CS8Tests</InterceptorsPreviewNamespaces>
                </PropertyGroup>
              </Project>
              اینبار برنامه کامپایل شده و اجرا می‌شود. در این حالت خروجی جدید برنامه، خروجی تامین شده‌ی توسط ره‌گیر سفارشی ما است:
              Hello, DNT!


              سؤال: آیا ره‌گیری انجام شده، در زمان کامپایل انجام می‌شود یا در زمان اجرا؟

              برای این مورد می‌توان به Low-Level C# code تولیدی مراجعه کرد. برای مشاهده‌ی یک چنین کدهایی می‌توانید از منوی Tools->IL Viewer برنامه‌ی Rider استفاده کرده و در برگه‌ی ظاهر شده، گزینه‌ی Low-Level C# آن‌را انتخاب نمائید:
              using CS8Tests;
              using System;
              using System.Runtime.CompilerServices;
              
              [CompilerGenerated]
              internal class Program
              {
                private static void <Main>$(string[] args)
                {
                  Console.WriteLine(new InterceptorsSample().InterceptorMethod("Hello"));
                }
              
                public Program()
                {
                  base..ctor();
                }
              }
              همانطور که مشاهده می‌کنید، این ره‌گیری و جایگزینی، در زمان کامپایل انجام شده و کامپایلر، به‌طور کامل نحوه‌ی فراخوانی متد GetText اصلی را به متد ره‌گیر ما تغییر داده و بازنویسی کرده‌است.


              سؤال: آیا این قابلیت واقعا کاربردی است؟!

              اکنون شاید این سؤال مطرح شود که ... واقعا چه کسی قرار است مسیر کامل یک فایل، شماره سطر و شماره ستون فراخوانی متدی را به اینگونه در اختیار سیستم ره‌گیری قرار دهد؟! آیا واقعا این قابلیت، یک قابلیت کاربردی و مناسب است؟!
              اینجا است که اهمیت source generators مشخص می‌شود. توسط source generators دسترسی کاملی به syntax trees وجود دارد و همچنین یکسری اطلاعات تکمیلی مانند FilePath و سپس CSharpSyntaxNodeها که دسترسی به داده‌های متد ()GetLocation را دارند که مکان دقیق سطر و ستون‌های فراخوانی‌ها را مشخص می‌کند.


              کاربردهای فعلی ره‌گیرها در دات نت 8

              در دات نت 8، این موارد با استفاده از ره‌گیرها بهینه سازی شده و سرعت آن‌ها افزایش یافته‌اند:
              - فراخوانی‌هایی که تمام اطلاعات آن‌ها در زمان کامپایل فراهم است، مانند Regex.IsMatch(@"a+b+") که از یک الگوی ثابت و مشخص استفاده می‌کند، ره‌گیری شده و پیاده سازی آن با کدی استاتیک، جایگزین می‌شود.
              - در ASP.NET Minimal API، استفاده از lambda expressions جهت ارائه‌ی تعاریفی مانند:
              app.MapGet("/products", handler: (int? page, int? pageLength, MyDb db) => { ... })
              مرسوم است. این نوع فراخوانی‌ها نیز توسط ره‌گیرها برای جایگزینی handler آن‌ها با کدهای استاتیک، جهت بالابردن کارآیی و کاهش تخصیص‌های حافظه انجام می‌شود.
              - بهبود کارآیی foreach loops جهت استفاده از ریاضیات برداری و SIMD در صورت امکان.
              - بهبود کارآیی تزریق وابستگی‌ها، زمانیکه به تعاریف مشخصی مانند ()<provider.Register<MyService ختم می‌شود.
              - بجای استفاده از expression trees در زمان اجرای برنامه، اکنون می‌توان کدهای SQL معادل را در زمان کامپایل برنامه تولید کرد.
              - بهبود کارآیی Serializers، زمانیکه از یک نوع مشخص مانند ()<Serialize<MyType استفاده می‌شود و کامپایلر می‌تواند آن‌را با کدهای زمان کامپایل، جایگزین کند.


              محدودیت‌های ره‌گیرها در دات‌نت 8

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