ایجاد یک برنامهی خالی React برای آزمایش توابع Redux
در اینجا برای بررسی توابع Redux، یک پروژهی جدید React را ایجاد میکنیم:
> npm i -g create-react-app > create-react-app state-management-redux-mobx > cd state-management-redux-mobx > npm start
> npm install --save redux
معرفی سریع توابع Redux
Redux، کتابخانهی کوچکی است و تنها از 5 تابع تشکیل شدهاست:
applyMiddleware: function() bindActionCreators: function() combineReducers: function() compose: function() createStore: function()
بررسی تابع compose با یک مثال
پس از ایجاد پروژهی React و افزودن کتابخانهی Redux به آن، به فایل src\index.js این پروژه مراجعه کرده و محتویات آنرا با قطعه کد ذیل، تعویض میکنیم:
import { compose } from "redux"; const makeLouder = text => text.toUpperCase(); const repeatThreeTimes = text => text.repeat(3); const embolden = text => text.bold(); const makeLouderAndRepeatThreeTimesAndEmbolden = compose( embolden, repeatThreeTimes, makeLouder ); console.log(makeLouderAndRepeatThreeTimesAndEmbolden("Hello"));
- سپس سه تابع ساده را برای ضخیم کردن، تکرار و با حروف بزرگ نمایش دادن یک متن ورودی، تعریف کردهایم.
- اکنون با استفاده از متد compose کتابخانهی redux، این سه متد را به صورت ترکیبی، بر روی متن ورودی Hello، اعمال کردهایم.
- در آخر اگر برای مشاهدهی نتیجهی اجرای console.log بر روی آن، به کنسول توسعه دهندگان مرورگر مراجعه کنیم، به خروجی زیر خواهیم رسید:
<b>HELLOHELLOHELLO</b>
بررسی تابع createStore با یک مثال
Store در Redux، جائی است که در آن state برنامه ذخیره میشود. تابع createStore که کار ایجاد store را انجام میدهد، یک پارامتر را دریافت میکند و آنهم تابع reducer است که در قسمت قبل معرفی شد و در سادهترین حالت آن، به نحو زیر با یک متد خالی، قابل فراخوانی است:
import { createStore } from "redux"; createStore(() => {});
import { createStore } from "redux"; const reducer = (state, action) => { return state; }; const store = createStore(reducer); console.log(store);
در شیء store، چهار متد dispatch, subscribe, getState, replaceReducer مشخص هستند:
- کارکرد متد replaceReducer مشخص است؛ یک reducer جدید را به آن میدهیم و reducer قبلی را جایگزین میکند.
- متد dispatch آن مرتبط است به مبحث dispatch actions که در قسمت قبل به آن پرداختیم. هدف آن تغییر state، بر اساس یک action رسیدهاست.
- متد getState، وضعیت فعلی state را باز میگرداند.
- متد subscribe با هر تغییری در state، سبب بروز رخدادی میشود. یکی از کاربردهای آن میتواند به روز رسانی UI، بر اساس تغییرات state باشد. برای مثال کتابخانهی دیگری به نام react redux، دقیقا همین کار را به کمک متد subscribe، انجام میدهد. در این قسمت، هدف ما بررسی پشت صحنهی کتابخانههایی مانند react redux است که چه متدهایی را محصور کردهاند و دقیقا چگونه کار میکنند.
در مثال زیر، مقدار ابتدایی پارامتر state متد reducer را به یک شیء که دارای خاصیت value و مقدار 1 است، تنظیم کردهایم:
import { createStore } from "redux"; const reducer = (state = { value: 1 }, action) => { return state; }; const store = createStore(reducer); console.log(store.getState());
در کامپوننتهای React، این نوع خواص به صورت props ارسال میشوند. کل state در Redux ذخیره شده و سپس قابل دسترسی و خواندن خواهد شد.
بررسی متد dispatch یک store با مثال
در اینجا مثالی را از کاربرد متد dispatch ملاحظه میکنید که یک شیء را میپذیرد:
import { compose, createStore } from "redux"; const reducer = (state = { value: 1 }, action) => { console.log("Something happened!", action); return state; }; const store = createStore(reducer); console.log(store.getState()); store.dispatch({ type: "ADD" });
فرمت شیءای که به متد dispatch ارسال میشود، حتما باید به همراه خاصیت type باشد؛ در غیر اینصورت با صدور استثنائی، این نکته را گوشزد میکند. مقدار آن نیز یک رشتهاست که بیانگر وقوع رخدادی در برنامه میباشد؛ برای مثال کاربر درخواست دریافت اطلاعاتی را کردهاست و یا کاربر از سیستم خارج شدهاست و امثال آن.
خروجی قطعه کد فوق، به صورت زیر است:
با اجرای برنامه، یک رخداد درونی از نوع redux/INITo.2.g.b.y.i@@ صادر شدهاست که هدف آن آغاز و مقدار دهی اولیهی store است. پس از آن زمانیکه متد store.dispatch فراخوانی شدهاست، مجددا console.log داخل متد reducer فراخوانی شدهاست که اینبار به همراه type ای است که ما مشخص کردهایم.
در حالت کلی، اینکه شیء ارسالی توسط dispatch را چگونه طراحی میکنید، اختیاری است؛ اما الگوی پیشنهادی در این زمینه، redux standard actions نام دارد و به صورت زیر است که هدف از آن، یکدست شدن طراحی و پیاده سازی برنامه است:
store.dispatch({ type: "ADD", payload: { amount: 2 }, meta: {} });
- سپس به خاصیت payload، تمام دادههای مرتبط با آن اکشن، مانند نتیجهی بازگشتی از یک API، به صورت یک شیء، انتساب داده میشود.
- خاصیت meta، مرتبط با متادیتا و اطلاعات اضافی است که عموما استفاده نمیشود.
اکنون که طراحی شیء ارسالی به پارامتر action متد reducer مشخص شد، میتوان بر اساس خاصیت type آن، یک switch را طراحی کرد و نسبت به نوعهای مختلف رسیده، واکنش نشان داد:
import { createStore } from "redux"; const reducer = (state = { value: 1 }, action) => { console.log("Something happened!", action); if (action.type === "ADD") { const value = state.value; const amount = action.payload.amount; state.value = value + amount; } return state; }; const store = createStore(reducer); const firstState = store.getState(); console.log(firstState); store.dispatch({ type: "ADD", payload: { amount: 2 }, meta: {} }); const secondState = store.getState(); console.log(secondState); console.log("secondState === firstState", secondState === firstState);
یک روش حل این مشکل، حذف سطر state.value = value + amount و جایگزینی آن با یک return است که شیء state جدیدی را بازگشت میدهد:
return { value: value + amount };
بررسی متد subscribe یک store با مثال
در ادامه به store همان مثال فوق، متد subscribe را نیز اضافه میکنیم و سپس دوبار، کار store.dispatch را انجام خواهیم داد:
const store = createStore(reducer); const unsubscribe = store.subscribe(() => console.log(store.getState())); store.dispatch({ type: "ADD", payload: { amount: 2 }, meta: {} }); store.dispatch({ type: "ADD", payload: { amount: 2 }, meta: {} }); unsubscribe();
- خروجی مستقیم متد store.subscribe نیز یک متد است که unsubscribe نام دارد و میتوان در پایان کار، برای جلوگیری از نشتیهای حافظه، آنرا فراخوانی کرد.
خروجی حاصل از console.log منتسب به متد store.subscribe، با دوبار فراخوانی متد store.dispatch پس از آن، به صورت زیر است:
{value: 3} {value: 5}
بررسی تابع combineReducers با یک مثال
اگر قرار باشد در متد reducer، صدها if action.type را تعریف کرد ... پس از مدتی از کنترل خارج میشود و غیرقابل نگهداری خواهد شد. تابع combineReducers به همین جهت طراحی شدهاست تا چندین متد مجزای reducer را با هم ترکیب کند. بنابراین میتوان در برنامه صدها متد کوچک reducer را که قابلیت توسعه و نگهداری بالایی را دارند، پیاده سازی کرد و سپس با استفاده از تابع combineReducers، آنها را یکی کرده و به متد createStore ارسال کرد. نکتهی مهم اینجا است که هرچند اینبار میتوان تعداد زیادی reducer را طراحی کرد، اما توسط تابع combineReducers، مقدار action ارسالی، از تمام این reducerها عبور داده خواهد شد. به این ترتیب اگر نیاز بود میتوان به یک action، در چندین متد مختلف reducer گوش فرا داد و بر اساس آن تصمیم گیری کرد. بنابراین بهتر است هر reducer تعریف شده در صورتیکه action.type آن با action رسیده تطابق نداشته باشد، همان state قبلی را بازگشت دهد تا بتوان زنجیرهی reducerها را تشکیل داد و بهتر مدیریت کرد.
برای نمونه اگر متد reducer فعلی را به calculatorReducer تغییر نام دهیم، روش معرفی آن توسط متد combineReducers به متد createStore به صورت زیر است:
import { combineReducers, createStore } from "redux"; // ... const calculatorReducer = (state = { value: 1 }, action) => { // ... }; const store = createStore( combineReducers({ calculator: calculatorReducer }) );
بررسی تابع bindActionCreators با یک مثال
فرض کنید میخواهیم متد dispatch را چندین بار فراخوانی کنیم:
store.dispatch({ type: "ADD", payload: { amount: 2 }, meta: {} }); store.dispatch({ type: "ADD", payload: { amount: 2 }, meta: {} });
const createAddAction = amount => { return { type: "ADD", payload: { amount // = amount: amount }, meta: {} }; };
store.dispatch(createAddAction(2)); store.dispatch(createAddAction(2));
import { bindActionCreators, combineReducers, compose, createStore } from "redux"; // ... const dispatchAdd = bindActionCreators(createAddAction, store.dispatch); dispatchAdd(2); dispatchAdd(2);
میانافزارها (Middlewares) در Redux
پس از اینکه یک اکشن، به سمت reducer ارسال شد و پیش از رسیدن آن به reducer، میتوان کدهای دیگری را نیز اجرا کرد. برای مثال چون این توابع خالص هستند، نمیتوان اعمالی را داخل آنها اجرا کرد که به همراه اثرات جانبی مانند کار با یک API خارجی باشند. با استفاده از میانافزارها در این بین میتوان کدهایی را که با دنیای خارج تعامل دارند نیز اجرا کرد.
یک میانافزار در Redux، همانند سایر قسمتهای آن، تنها یک تابع سادهی جاوا اسکریپتی است:
const logger = ({ getState }) => { return next => action => { console.log( 'MIDDLEWARE', getState(), action ); const value = next(action); console.log({value}); return value; } }
در پایان کار میانافزار باید متد next آنرا فراخوانی کرد تا بتوان میانافزارهای متعددی را زنجیروار اجرا کرد.
در آخر برای معرفی آن به یک store میتوان از تابع applyMiddleware استفاده کرد:
import { applyMiddleware, bindActionCreators, combineReducers, compose, createStore } from "redux"; // ... const store = createStore( combineReducers({ calculator: calculatorReducer }), applyMiddleware(logger) );
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: state-management-redux-mobx-part02.zip
مدیریت سراسری خطاها در یک برنامهی Angular
- علت عمل نکردن فیلتری که به آن لینک دادید (که من با آن موافق نیستم)، این است که دیگر نباید از میانافزار مدیریت استثناهای مخصوص توسعه دهندههای ASP.NET Core در این حالت استفاده کنید، چون با آن تداخل میکند و پیش از آن وارد عمل میشود. علت دریافت صفحهی HTML ایی که مشاهده میکنید، همین مورد است. این صفحه برای برنامههای ASP.NET Core دارای Viewهای Razor طراحی شدهاست و نه مخصوص حالت کار صرفا Web API آن.
- یکی از مشکلات آن فیلتر هم این است که به هیچ عنوان نباید اصل خطای رخدادهی در سمت سرور را به سمت کلاینت ارسال کرد و به کاربر نمایش داد. این مورد امکان دیباگ از راه دور برنامهی شما را توسط یک مهاجم سهولت میبخشد و از دیدگاه امنیتی اشتباه است. این موارد را فقط باید توسط امکانات Logging توکار ASP.NET Core ثبت و در سمت سرور با «دسترسی ادمین» بررسی کنید. کاربر هم فقط باید جملهی کلی «خطایی رخ دادهاست» را مشاهده کند و نه جزئیات آنرا.
در قسمت قبل که لیست اتاقهای دریافتی از 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
کار با Kendo UI DataSource
- Filter : یک پراپرتی از نوع string است که درصورت مقداردهی آن، بر روی لیست خروجی ما عملیات فیلترینگ اعمال میشود. مثال :
Filter = "Name==Ali,Age>>10";
- SortBy : یک پراپرتی از نوع string است که درصورت مقداردهی آن، بر روی لیست خروجی ما عملیات چیدمان یا سورتینگ با استفاده از نام فیلد انجام میشود. مثال :
SortBy = "Age";
- IsSortAsc: یک پراپرتی از نوع bool است که مشخص کننده چیدمان به صورت نزولی و یا صعودی است.
- Page : یک پراپرتی از نوع عددی short است. از این پراپرتی برای عملیات Pagination یا صفحه بندی استفاده میشود که مشخص کننده شماره صفحه درخواستی است.
- PageSize : یک پراپرتی از نوع عددی int است که برای مشخص کردن تعداد رکورد در هر صفحه استفاده میشود.
ApplyFiltering | از این متد برای اعمال فیلترینگ روی یک IQueryable استفاده میشود. این متد یک رشته متنی (string) ویا یک GridifyQuery دریافت کرده و پس از اعمال فیلترینگ یک IQueryable بازمیگرداند. |
ApplyOrdering | از این متد برای اعمال چیدمان یا Sorting روی یک IQueryable استفاده میشود. پس از اعمال چیدمان، یک IQueryable را بازمیگرداند. |
ApplyPaging | از این متد برای اعمال صفحه بندی (Pagination) استفاده میشود. پس از اعمال صفحه بندی یک IQueryable را بازمیگرداند. |
ApplyOrderingAndPaging | از این متد برای اعمال همزمان چیدمان و صفحه بندی استفاده میشود که یک IQueryable را باز میگرداند. |
ApplyFilterAndOrdering | از این متد برای اعمال همزمان فیلترینگ و چیدمان استفاده میشود که یک IQueryalbe را باز میگرداند. |
ApplyEverything | از این متد برای اعمال عملیات صفحه بندی، چیدمان و فیلترینگ استفاده میشود که یک IQueryable را باز میگرداند. |
GridifyQueryable | این متد مشابه ApplyEverything است که مقدار یک <QueryablePaging<T را برمیگرداند و دارای یک خصیصه اضافی TotalItems است که در عملیات صفحه بندی عموما نیاز داریم. (تعداد کل رکوردهای موجود در پایگاه داده، با توجه به فیلتر اعمال شده) |
Gridify | متدهای قبلی فقط به query موجود ما یکسری شرط را اضافه میکردند. ولی مسئولیت اجرای query به عهده ما بود. (مثلا با استفاده از ToList.). متد Gridify تمامی شرطها را باتوجه به GridifyQuery دریافتی اعمال کرده، سپس اطلاعات را بارگذاری کرده و یک <Paging<T را بازمیگرداند که کاملا قابل استفاده و بهینه شده برای دیتاگریدها میباشد. |
| |
GetFilteringExpression | این متد expression معادل فیلتر string نوشته شده شما را برمیگرداند که میتوانید از آن به طور مثال در متد Where در Linq استفاده نمایید. |
GetOrderingExpression | این متد expression انتخاب فیلد برای Orderby و OrderByDescending را باتوجه به مقدار وارد شده در فیلد SortBy بازمیگرداند. |
Filtering Operators:
همانطور که در تصویر بالا مشاهده میکنید، برای اعمال فیلترینگهای پیچیده میتوانیم از چهار اپراتور , | ( ) استفاده کنیم. به همین جهت اگر نیاز داشتید که در مقدار جستجوی خود از این علائم استفاده کنید، باید قبل از هرکدام از آنها، علامت \ را اضافه کنید.
مثال رجاکس escape character در JavaScript :
let esc = (v) => v.replace(/([(),|])/g, '\\$1')
مثال #C :
var value = "(test,test2)"; var esc = Regex.Replace(value, "([(),|])", "\\$1" ); // esc = \(test\,test2\)
در بخش بعد با امکانات Mapper توکار Gridify و شخصی سازی آن آشنا خواهیم شد.
- یعنی کدهای بعدی، بلافاصله اجرا میشوند.
- نتیجه درخواست HTTP در متد subscribe در آیندهای نزدیک بازگشت داده میشود و هیچ ترتیب خاصی را نمیتوان برای آن قائل شد.
- بنابراین فراخوانی loadMenu و بلافاصله دسترسی به نتیجهی آن، حاصلی ندارد؛ چون هنوز subscribe آن نتیجهای را از طرف سرور دریافت نکرده و سطر بعدی، بلافاصله اجرا شدهاست.
- سطر loadMenu، یک سطر blocking نیست و async است.
یک. قبل از آپلود امکان برش وجود داشته باشد.
دو. سیستم برش قابلیت پشتیبانی از تناسب بین پهنا و ارتفاع را داشته باشد.
سه. قابلیت استفاده خارج از قواعد Ajax را داشته باشد و بتوان ارسال آن را به طور دستی کنترل کرد.
چهار. پشتیبانی برش تصاویر به صورت تاچ برای گوشیهای همراه را نیز دارا باشد.
کتابخانه jcrop کتابخانهای است که این امکانات را برای شما فراهم میکند. این کتابخانه بدین صورت است که در حین برش به شما 4 عدد x1,x2,y1,y2 را داده و شما با ارسال آن به سمت سرور میتوانید بر اساس این اعداد، عکس اصلی را برش بزنید. بدین صورت شما هم عکس اصلی را دارید و هم مختصات برش را دارید و اگر دوست دارید در جاهای مختلف از عکس اصلی برش داشته باشید، بسیار مفید خواهد بود.
مرحله اول:
ابتدا فایل jcrop را دانلود نمایید.
مرحله دوم:
کد Html زیر را به صفحه اضافه کنید:
<div> <!-- upload form --> <!-- hidden crop params --> <input type="hidden" id="x1" name="x1" /> <input type="hidden" id="y1" name="y1" /> <input type="hidden" id="x2" name="x2" /> <input type="hidden" id="y2" name="y2" /> <h2>ابتدا تصویر خود را انتخاب کنید</h2> <div><input type="file" name="postedFileBase" data-buttonText="انتخاب تصویر" id="image_file" onchange="fileSelectHandler()" /></div> <div></div> <div> <h2>قسمتی از تصویر را انتخاب نمایید</h2> <img id="preview" /> <div> <label>حجم فایل </label> <input type="text" id="filesize" name="filesize" /> <label>نوع فایل</label> <input type="text" id="filetype" name="filetype" /> <label>ابعاد فایل</label> <input style="direction: ltr;" type="text" id="filedim" name="filedim" /> </div> </div> </div>
تگ step2 نیز بعد از نمایش موفقیت آمیز تصویر نشان داده میشود که کاربر میتواند در آن تصویر را برش دهد و شامل بخش info نیز میباشد تا بتوان اندازه اصلی تصویر، نوع فایل تصویر Content Type و حجم آن را نمایش داد.
مرحله سوم:
سپس برای استایل دهی کدهای بالا از کد Css زیر استفاده میکنیم:
.bheader { background-color: #DDDDDD; border-radius: 10px 10px 0 0; padding: 10px 0; text-align: center; } .bbody { color: #000; overflow: hidden; padding-bottom: 20px; text-align: center; background: -moz-linear-gradient(#ffffff, #f2f2f2); background: -ms-linear-gradient(#ffffff, #f2f2f2); background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ffffff), color-stop(100%, #f2f2f2)); background: -webkit-linear-gradient(#ffffff, #f2f2f2); background: -o-linear-gradient(#ffffff, #f2f2f2); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f2f2f2'); -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f2f2f2')"; background: linear-gradient(#ffffff, #f2f2f2); } .bbody h2, .info, .error { margin: 10px 0; } .step2, .error { display: none; } .error { color: red; } .info { } label { margin: 0 5px; } .roundinput { border: 1px solid #CCCCCC; border-radius: 10px; padding: 4px 8px; text-align: center; width: 150px; } .jcrop-holder { display: inline-block; } input[type=submit] { background: #e3e3e3; border: 1px solid #bbb; border-radius: 3px; -webkit-box-shadow: inset 0 0 1px 1px #f6f6f6; box-shadow: inset 0 0 1px 1px #f6f6f6; color: #333; padding: 8px 0 9px; text-align: center; text-shadow: 0 1px 0 #fff; width: 150px; } input[type=submit]:hover { background: #d9d9d9; -webkit-box-shadow: inset 0 0 1px 1px #eaeaea; box-shadow: inset 0 0 1px 1px #eaeaea; color: #222; cursor: pointer; } input[type=submit]:active { background: #d0d0d0; -webkit-box-shadow: inset 0 0 1px 1px #e3e3e3; box-shadow: inset 0 0 1px 1px #e3e3e3; color: #000; }
مرحله چهارم:
افزودن کد جاوااسکریپتی زیر برای کار کردن با کتابخانه Jcrop میباشد:
function bytesToSize(bytes) { var sizes = ['بایت', 'کیلو بایت', 'مگابایت']; if (bytes == 0) return 'n/a'; var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + sizes[i]; }; function updateInfo(e) { $('#x1').val(e.x); $('#y1').val(e.y); $('#x2').val(e.x2); $('#y2').val(e.y2); }; var jcrop_api, boundx, boundy; function fileSelectHandler() { var oFile = $('#image_file')[0].files[0]; $('.error').hide(); var rFilter = /^(image\/jpeg|image\/png)$/i; if (!rFilter.test(oFile.type)) { $('.error').html('فقط تصویر معتبر انتخاب نمایید').show(); return; } var oImage = document.getElementById('preview'); var oReader = new FileReader(); oReader.onload = function (e) { oImage.src = e.target.result; oImage.onload = function () { $('.step2').fadeIn(500); var sResultFileSize = bytesToSize(oFile.size); $('#filesize').val(sResultFileSize); $('#filetype').val(oFile.type); $('#filedim').val(oImage.naturalWidth + ' x ' + oImage.naturalHeight); if (typeof jcrop_api != 'undefined') { jcrop_api.destroy(); jcrop_api = null; $('#preview').width(oImage.naturalWidth); $('#preview').height(oImage.naturalHeight); } $('#preview').Jcrop({ aspectRatio: 2, bgFade: true, bgOpacity: .3, onChange: updateInfo, onSelect: updateInfo }, function () { //var bounds = this.getBounds(); //var boundx = bounds[0]; //var boundy = bounds[1]; // Store the Jcrop API in the jcrop_api variable jcrop_api = this; }); }; }; oReader.readAsDataURL(oFile); }
تابع fileSelectHandler
function fileSelectHandler() { var oFile = $('#image_file')[0].files[0]; $('.error').hide(); var rFilter = /^(image\/jpeg|image\/png)$/i; if (!rFilter.test(oFile.type)) { $('.error').html('فقط تصویر معتبر انتخاب نمایید').show(); return; }
در ادامه همین تابع بالا، کدهای زیر را اضافه میکنیم:
var oImage = document.getElementById('preview'); var oReader = new FileReader(); oReader.onload = function (e) { oImage.src = e.target.result; oImage.onload = function () { $('.step2').fadeIn(500); var sResultFileSize = bytesToSize(oFile.size); $('#filesize').val(sResultFileSize); $('#filetype').val(oFile.type); $('#filedim').val(oImage.naturalWidth + ' x ' + oImage.naturalHeight); if (typeof jcrop_api != 'undefined') { jcrop_api.destroy(); jcrop_api = null; $('#preview').width(oImage.naturalWidth); $('#preview').height(oImage.naturalHeight); } $('#preview').Jcrop({ aspectRatio: 2, bgFade: true, bgOpacity: .3, onChange: updateInfo, onSelect: updateInfo, onRelease: clearInfo }, function () { //var bounds = this.getBounds(); //var boundx = bounds[0]; //var boundy = bounds[1]; jcrop_api = this; }); }; }; oReader.readAsDataURL(oFile);
FileReader یکی از توابع موجود در HTML است که مستندات آن در سایت موزیلا موجود است و قابلیت خواندن غیرهمزمان فایلها و اشیا Blob را دارد. در خط آخر به عنوان پارامتر ما فایلی را که در آپلودر خوانده ایم و در مرحله قبل نوع فایل آن را بررسی کردیم، پاس میکنیم و باعث میشود که رویداد Load شیء FileReader صدا زده شود.
در این رویداد ابتدا اطلاعات این فایل را از قبیل سایز و ابعاد و نوع فایل، خوانده و در همان تگ Div که با کلاس info تعیین شده بود، نمایش میدهیم. سپس متغیر jcrop_api را که به صورت global در بالای تابع صدا زدیم، بررسی میکنم که آیا از قبل پر شدهاست یا خیر؟ اگر از قبل پرشدهاست باید شیء Jcrop را که به آن اعمال شده است، نابود و آن را نال کنیم تا برای تصویر جدید آماده شود. این کد زمانی کاربرد دارد که کاربر از تصویر قبلی انصراف دادهاست و تصویر جدیدی را انتخاب نموده است یا اینکه عملیات دارد به صورت ایجکسی پیاده میشود. اگر عملیات نابودی روی این پلاگین صورت نگیرد، برای مرتبه دوم کار نخواهد کرد.
سپس پلاگین جیکوئری Jcrop را بر روی آن اعمال میکنیم. در پرامتر اول یک سری تنظیمات اولیه را انجام میدهیم که در ادامه با آن آشنا میشویم و در پارامتر دوم یک callback را به آن پاس میکنیم تا بعد از آماده شدن پلاگین اجرا شود که در آن شیء جدید ایجاد شده یعنی this را در متغیری به اسم jcrop_api دخیره میکنیم تا در بررسیهای آتی که در بند بالا توضیح داده شد، در دسترس داشته باشیم. همچنین در این تابع شما میتوانید اندازه تصویر انتخابی را نیز داشته باشید.
این پلاگین شامل optionهای متفاوتی در پارامتر اول است که آنها را بررسی میکنیم:
MinSize : شما میتوانید حداقل پهنا و ارتفاعی را برای برش زدن تصویر در نظر بگیرید.
minSize:[40,20]
aspectRatio:1.5
bgOpacity از 0 تا یک مقدار میگیرد و میزان opacity محلهای تاریک را تعیین میکند. همچنین شامل سه رویداد onSelect,onChange,onrelease هم میباشد که به ترتیب در موارد زیر رخ میدهند:
ناحیه مورد نظر انتخاب شد.
ناحیه مورد نظر در حالت انتخاب است و ماوس در حال درگ شدن است و با هر حرکتی ماوس اجرا میگردد.
ناحیه انتخابی از حالت انتخاب خارج شد.
دو رویداد اول یعنی onchange و onSelect را برای به روزسانی فیلدهای مخفی و مختصات استفاده میکنیم:
function updateInfo(e) { $('#x1').val(e.x); $('#y1').val(e.y); $('#x2').val(e.x2); $('#y2').val(e.y2); };
این مختصات از طریق یک پارامتر به آنها پاس میشود. به غیر از این چهار عدد مختصات میتوانید با استفاده از متغیرهای w و h هم اندازه پهنا و ارتفاع محل برش خورده را نیز به دست آورید. هر چند که این اعداد، از تفریق خود مختصات هم به دست میآیند.
یک تابع جزئی دیگر هم در این فایل وجود دارد که حین نمایش اندازه تصویر، واحد نمایش مناسب آن را برای ما انتخاب میکند:
function bytesToSize(bytes) { var sizes = ['بایت', 'کیلو بایت', 'مگابایت']; if (bytes == 0) return 'n/a'; var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + sizes[i]; };
بعد از اینکه کدهای سمت کلاینت را تمام کردیم لازم است با نحوه برش تصویر در سمت سرور هم آشنا شویم:
public static byte[] Resize(this byte[] byteImageIn, int x1,int y1,int x2,int y2) { ImageConverter ic = new ImageConverter(); Image src = (Image)(ic.ConvertFrom(byteImageIn)); Bitmap target = new Bitmap(x2 - x1, y2 - y1); using (Graphics graphics = Graphics.FromImage(target)) graphics.DrawImage(src, new Rectangle(0, 0, target.Width, target.Height), new Rectangle(x1,y1,x2-x1,y2-y1), GraphicsUnit.Pixel); src = target; using (var ms = new MemoryStream()) { src.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg); return ms.ToArray(); } }
از آنجا که ما تصاویر را در دیتابیس به صورت آرایهای از بایتها ذخیره میکنیم، extension method ذکر شده در بالا تصویر را در حالت آرایهای از بایتها برش میدهد. بدیهی که بسته به نیاز شما کد بالا دست خوش تغییراتی خواهد شد. ابتدا تصویر باینری را به شی Image تبدیل میکنیم و یک شیء Bitmap جدید را به عنوان بوم خالی و به اندازه کادر برش ایجاد میکنیم تا تصویر برش خورده در آن قرار بگیرد و سپس توسط متد DrawImage میخواهیم که تصویر مبدا را با مختصات شیء Rectangle از نقطه 0 و 0 بوم آغاز کرده و تا انتهای آن شروع به ترسیم کند. سپس آن را ذخیره و مجددا در قالب همان آرایهای از بایتها بر میگردانیم.
تنها یک نکته را به خاطر داشته باشید که مقادیر مختصاتی که پلاگین جی کوئری ارسال میکند در قالب اعداد اعشاری هستند و برای ارسال و دریافت آنها در سرور این نکته را به خاطر داشته باشید.
IdentityServer قسمت اول
برای تولید چنین نموداری از میتوانیم Mermaid استفاده کنیم. یکی از انواع دیاگرامهایی را که پشتیبانی میکند، Gitgraph میباشد. دیاگرامها در Mermaid با کمک یک DSL ساخته میشوند. سینکس آن نیز خیلی ساده است؛ ابتدا نوع دیاگرامی را که میخواهیم ترسیم کنیم، تعیین میکنیم و سپس محتویات را براساس نوع دیاگرام، تعیین میکنیم. به عنوان مثال برای Gitgraph سینتکس آن به این صورت است:
gitGraph commit commit branch develop checkout develop commit commit checkout main merge develop commit commit
در ساختار فوق ابتدا دو کامیت بر روی برنچ اصلی (main) انجام شدهاست؛ سپس یک برنچ جدید را با نام develop، ایجاده کردهایم و بلافاصله به آن checkout کردهایم. در ادامه تعدادی کامیت را بر روی این برنچ انجام داده و در نهایت برنچ موردنظر را بر روی main، مرج کردهایم. در نهایت نیز دو کامیت دیگر را بر روی main ایجاد کردهایم. تعریف فوق، منجر به ساخت چنین نموداری خواهد شد:
از ادیتور آنلاین Mermaid نیز میتوانید برای تست سینکس استفاده کنید. در ادامه میخواهیم با کمک PowerShell، از روی یک پروژهی گیت، DSL موردنیاز برای ساخت دیاگرام را ایجاد کنیم. برای اینکار ابتدا توسط تابع زیر یک پروژهی گیت را با تعدادی فایل نمونه ایجاد خواهیم کرد:
Function New-RandomRepo { Function RandomFiles($branch = "main") { 1..3 | ForEach-Object { New-Item -ItemType File -Name "file_$($_).txt" Set-Content -Path "file_$($_).txt" -Value "This is file $($_) on branch $branch" git add . git commit -m "Commit $($_) on branch $branch" } } Set-Location ~/Desktop New-Item -ItemType Directory "random_git_repo" Set-Location "random_git_repo" git init -b main Write-Output "This is the main branch" | Set-Content -Path "main.txt" git add . git commit -m "Initial commit" 1..3 | ForEach-Object { git checkout -b "branch_$($_)" RandomFiles "branch_$($_)" git checkout main } }
در ادامه تابع New-GitRepoDiagram را برای تولید ساختار مورد نیاز نوشتهایم:
Function New-GitRepoDiagram { $commitIds = @() $branches = git branch | ForEach-Object { $default = $false $activeBranch = git symbolic-ref --short HEAD $currentBranch = ($_.Replace("* ", " ")).Trim() if ($currentBranch -eq $activeBranch) { $default = $true } @{ name = $currentBranch isMainBranch = $default } | ConvertTo-Json } | ConvertFrom-Json $defaultBranch = $branches | Where-Object { $_.isMainBranch -eq $true } | Select-Object -ExpandProperty name $mermaidFile = "%%{init: { 'gitGraph': {'mainBranchName': '$defaultBranch' } } }%%" + [Environment]::NewLine $mermaidFile += 'gitGraph' + [Environment]::NewLine foreach ($branch in ($branches | Sort-Object -Property isMainBranch -Descending)) { $name = $branch.name $notIncludeTheMainCommits = $name -ne $defaultBranch ? "--not $(git merge-base $defaultBranch $name)" : "" $notIncludeTheMainCommits $logs = git log --pretty=format:'{"commit": "%h", "author": "%an", "message": "%s"}' --reverse $name | ConvertFrom-Json if ($name -ne $defaultBranch) { $mermaidFile += ' branch "$name"'.Replace('$name', $name) + [Environment]::NewLine } foreach ($log in $logs) { $commit = $log.commit if ($commitIds -contains $commit) { continue } $commitToAdd = ' commit id: "$commit"'.Replace('$commit', $commit) + [Environment]::NewLine $mermaidFile += $commitToAdd $commitIds += $commit } if ($name -ne $defaultBranch) { $mermaidFile += ' checkout main' + [Environment]::NewLine } } Write-Host $mermaidFile }
توضیحات:
- در تابع فوق، ابتدا یک آرایهی خالی برای ذخیرهی کامیت آیدیها، اضافه شده؛ از این آرایه برای جلوگیری از اضافه شدن کامیت تکراری در هر برنچ استفاده شدهاست.
- سپس با کمک دستور git branch، لیست تمام برنچها، دریافت شدهاست (همانطور که در قسمت قبل بررسی شد +).
- برای هر برنچ، تابع تعیین میکند که آیا برنچ جاری است یا خیر. اینکار با کمک دستور git symbolic-ref انجام شدهاست.
- در ادامه متغیر mermaidFile$ برای ایجاد یک گراف جدید Git مقداردهی میشود. در اینجا نام برنچ اصلی برابر با نام برنچ پیشفرض، تنظیم میشود.
- سپس لیست کامیتهای هر برنچ را با کمک دستور git log (همان سینتکسی که در قسمتهای قبل بررسی شد +) استخراج میکنیم.
- و در نهایت به ازای هر کامیت، یک commit id تولید کردهایم.
با فراخوانی تابع فوق، اینچنین ساختاری برایمان تولید خواهد شد:
%%{init: { 'gitGraph': {'mainBranchName': 'main' } } }%% gitGraph commit id: "765100f" branch "branch_1" commit id: "c88c441" commit id: "44149d9" commit id: "a660fe3" checkout main branch "branch_2" commit id: "2dcb572" commit id: "b043ad1" commit id: "92cafc0" checkout main branch "branch_3" commit id: "559e381" commit id: "f72957f" commit id: "c066e72" checkout main
خروجی فوق دقیقاً دیاگرامی است که در ابتدای مطلب نشان داده شد: