در قسمت قبل که لیست اتاقهای دریافتی از 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; } } }
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"; } }
namespace BlazorServer.DataAccess { public class ApplicationDbContext : IdentityDbContext<ApplicationUser> { public DbSet<RoomOrderDetail> RoomOrderDetails { get; set; } // ... } }
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
پس از تعریف یک موجودیت، یک 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; } } }
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(); } } }
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); } } }
- سپس سرویس توکار 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; } }
- همچنین کدهای ابتدایی HandleCheckout را که برای ثبت نهایی اطلاعات فرم است، تهیه کردهایم. البته در این قسمت این مورد را فقط محدود به اعتبارسنجی دستی و سفارشی که در متد HandleValidation مشاهده میکنید، کردهایم. این روش دستی را نیز میتوان برای تعریف منطق اعتبارسنجی یک فرم بکار برد و آنرا توسط کدهای #C تکمیل کرد. البته باید درنظر داشت که data annotation validator توکار، هنوز از اعتبارسنجی خواص تو در تو، پشتیبانی نمیکند. به همین جهت است که در اینجا خودمان این اعتبارسنجی را به صورت دستی تعریف کردهایم.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-29.zip