Caching can help improve the performance of an ASP.NET Core application. Distributed caching is helpful when working with an ASP.NET application that’s deployed to a server farm or scalable cloud environment. Microsoft documentation contains examples of doing this with SQL Server or Redis, but in this post,I’ll show you an alternative.
مجموعهای از Open Source SVG Icons
MVC Scaffolding #1
کل سری ASP.NET MVC
به همراه کل سری EF Code First
MVC Scaffolding چیست؟
MVC Scaffolding ابزاری است برای تولید خودکار کدهای «اولیه» برنامه، جهت بالا بردن سرعت تولید برنامههای ASP.NET MVC مبتنی بر EF Code First.
بررسی مقدماتی MVC Scaffolding
امکان اجرای ابزار MVC Scaffolding از دو طریق دستورات خط فرمان Powershell و یا صفحه دیالوگ افزودن یک کنترلر در پروژههای ASP.NET MVC وجود دارد. در ابتدا حالت ساده و ابتدایی استفاده از صفحه دیالوگ افزودن یک کنترلر را بررسی خواهیم کرد تا با کلیات این فرآیند آشنا شویم. سپس در ادامه به خط فرمان Powershell که اصل توانمندیها و قابلیتهای سفارشی MVC Scaffolding در آن قرار دارد، خواهیم پرداخت.
برای این منظور یک پروژه جدید MVC را آغاز کنید؛ ابزارهای مقدماتی MVC Scaffolding از اولین به روز رسانی ASP.NET MVC3 به بعد با VS.NET یکپارچه هستند.
ابتدا کلاس زیر را به پوشه مدلهای برنامه اضافه کنید:
using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace MvcApplication1.Models { public class Task { public int Id { set; get; } [Required] public string Name { set; get; } [DisplayName("Due Date")] public DateTime? DueDate { set; get; } [DisplayName("Is Complete")] public bool IsComplete { set; get; } [StringLength(450)] public string Description { set; get; } } }
همانطور که ملاحظه میکنید در قسمت قالبها، تولید کنترلرهایی با اکشن متدهای ثبت و نمایش اطلاعات مبتنی بر EF Code First انتخاب شده است. کلاس مدل نیز به کلاس Task فوق تنظیم گردیده و در زمان انتخاب DbContext مرتبط، گزینه new data context را انتخاب کرده و نام پیش فرض آنرا پذیرفتهایم. زمانیکه بر روی دکمه Add کلیک کنیم، اتفاقات ذیل رخ خواهند داد:
الف) کنترلر جدید TasksController.cs به همراه تمام کدهای Insert/Update/Delete/Display مرتبط تولید خواهد شد.
ب) کلاس DbContext خودکاری به نام MvcApplication1Context.cs در پوشه مدلهای برنامه ایجاد میگردد تا کلاس Task را در معرض دید EF Code first قرار دهد. (همانطور که عنوان شد یکی از پیشنیازهای بحث Scaffolding آشنایی با EF Code first است)
ج) در پوشه Views\Tasks، پنج View جدید را جهت مدیریت فرآیندهای نمایش صفحات Insert، حذف، ویرایش، نمایش و غیره تهیه میکند.
د) فایل وب کانفیگ برنامه جهت درج رشته اتصالی به بانک اطلاعاتی تغییر کرده است. حالت پیش فرض آن استفاده از SQL CE است و برای استفاده از آن نیاز است قسمت 15 سری EF سایت جاری را پیشتر مطالعه کرده باشید (به چه اسمبلیهای دیگری مانند System.Data.SqlServerCe.dll برای اجرا نیاز است و چطور باید اتصال به بانک اطلاعاتی را تنظیم کرد)
معایب:
کیفیت کد تولیدی پیش فرض قابل قبول نیست:
- DbContext در سطح یک کنترلر وهله سازی شده و الگوی Context Per Request در اینجا بکارگرفته نشده است. واقعیت یک برنامه ASP.NET MVC کامل، داشتن چندین Partial View تغدیه شونده از کنترلرهای مختلف در یک صفحه واحد است. اگر قرار باشد به ازای هر کدام یکبار DbContext وهله سازی شود یعنی به ازای هر صفحه چندین بار اتصال به بانک اطلاعاتی باید برقرار شود که سربار زیادی را به همراه دارد. (قسمت 12 سری EF سایت جاری)
- اکشن متدها حاوی منطق پیاده سازی اعمال CRUD یا همان Create/Update/Delete هستند. به عبارتی از یک لایه سرویس برای خلوت کردن اکشن متدها استفاده نشده است.
- از ViewModel تعریف شدهای به نام Task هم به عنوان Domain model و هم ViewModel استفاده شده است. یک کلاس متناظر با جداول بانک اطلاعاتی میتواند شامل فیلدهای بیشتری باشد و نباید آنرا مستقیما در معرض دید یک View قرار داد (خصوصا از لحاظ مسایل امنیتی).
مزیتها:
قسمت عمدهای از کارهای «اولیه» تهیه یک کنترلر و همچنین Viewهای مرتبط به صورت خودکار انجام شدهاند. کارهای اولیهای که با هر روش و الگوی شناخته شدهای قصد پیاده سازی آنها را داشته باشید، وقت زیادی را به خود اختصاص داده و نهایتا آنچنان تفاوت عمدهای هم با کدهای تولیدی در اینجا نخواهند داشت. حداکثر فرمهای آنرا بخواهید با jQuery Ajax پیاده سازی کنید یا کنترلهای پیش فرض را با افزونههای jQuery غنی سازی نمائید. اما شروع کار و کدهای اولیه چیزی بیشتر از این نیست.
نصب بسته اصلی MVC Scaffolding توسط NuGet
بسته اصلی MVC Scaffolding را با استفاده از دستور خط فرمان Powershell ذیل، از طریق منوی Tools، گزینه Library package manager و انتخاب Package manager console میتوان به پروژه خود اضافه کرد:
Install-Package MvcScaffolding
Attempting to resolve dependency 'T4Scaffolding'. Attempting to resolve dependency 'T4Scaffolding.Core'. Attempting to resolve dependency 'EntityFramework'. Successfully installed 'T4Scaffolding.Core 1.0.0'. Successfully installed 'T4Scaffolding 1.0.8'. Successfully installed 'MvcScaffolding 1.0.9'. Successfully added 'T4Scaffolding.Core 1.0.0' to MvcApplication1. Successfully added 'T4Scaffolding 1.0.8' to MvcApplication1. Successfully added 'MvcScaffolding 1.0.9' to MvcApplication1.
پس از اینکه بسته MvcScaffolding به پروژه جاری اضافه شد، همان مراحل قبل را که توسط صفحه دیالوگ افزودن یک کنترلر انجام دادیم، اینبار به کمک دستور ذیل نیز میتوان پیاده سازی کرد:
Scaffold Controller Task
نکته و مزیت مهم دیگری که در اینجا در دسترس میباشد، سوئیچهای خط فرمانی است که به همراه صفحه دیالوگ افزودن یک کنترلر وجود ندارند. برای مثال دستور Scaffold Controller را تایپ کرده و سپس یک خط تیره را اضافه کنید. اکنون دکمه tab را مجددا بفشارید. منویی ظاهر خواهد شد که بیانگر سوئیچهای قابل استفاده است.
برای مثال اگر بخواهیم دستور Scaffold Controller Task را با جزئیات اولیه کاملتری ذکر کنیم، مانند تعیین نام دقیق کلاس مدل و کنترلر تولیدی به همراه نام دیگری برای DbContext مرتبط، خواهیم داشت:
Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext
بهبود مقدماتی کیفیت کد تولیدی MVC Scaffolding
در همان کنسول پاروشل NuGet، کلید up arrow را فشار دهید تا مجددا دستور قبلی اجرا شده ظاهر شود. اینبار دستور قبلی را با سوئیچ جدید Repository (استفاده از الگوی مخزن) اجرا کنید:
Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext -Repository
Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext -Repository -Force
public class TasksController : Controller { private readonly ITaskRepository taskRepository; // If you are using Dependency Injection, you can delete the following constructor public TasksController() : this(new TaskRepository()) { } public TasksController(ITaskRepository taskRepository) { this.taskRepository = taskRepository; }
public interface ITaskRepository : IDisposable { IQueryable<Task> All { get; } IQueryable<Task> AllIncluding(params Expression<Func<Task, object>>[] includeProperties); Task Find(int id); void InsertOrUpdate(Task task); void Delete(int id); void Save(); }
پیاده سازی این اینترفیس در حالت متد Save آن شامل فراخوانی context.SaveChanges است. این مورد باید به الگوی واحد کار (که در اینجا تعریف نشده) منتقل شود. زیرا در یک دنیای واقعی حاصل کار بر روی چندین موجودیت باید در یک تراکنش ذخیره شوند و قرارگیری متد Save داخل کلاس مخزن یا سرویس برنامه، مخزنهای تعریف شده را تک موجودیتی میکند.
اما در کل با توجه به اینکه پیاده سازی منطق کار با موجودیتها به کلاسهای مخزن واگذار شدهاند و کنترلرها به این نحو خلوتتر گردیدهاند، یک مرحله پیشرفت محسوب میشود.
امکانات جدید در فایرفاکس کوانتم
Firefox Quantum is over twice as fast as Firefox from 6 months ago, built on a completely overhauled core engine with brand new technology stolen from our advanced research group, and graced with a beautiful new look designed to get out of the way and let you do what you do best: surf a ton of pages, open a zillion tabs, all guilt free because Firefox Quantum uses less memory than the competition
مشکل حذف تصاویر آپلود شده
در قسمت قبل، این امکان را مهیا کردیم که کاربران بتوانند پیش از ثبت اطلاعات یک اتاق، تصاویر آنرا به سرور آپلود کنند. یعنی تصاویری که در ابتدای کار آپلود میشوند، هنوز در بانک اطلاعاتی ثبت نشدهاند و هیچ رکوردی از آنها موجود نیست. در این حالت اگر کاربری تصاویری را آپلود کرده و سپس بر روی دکمهی back کلیک کند، با تعدادی تصویر آپلود شدهی غیرمنتسب به اتاقهای موجود، مواجه خواهیم شد. همچنین اگر شخصی به قسمت ویرایش تصاویر مراجعه کند و با کلیک بر روی دکمهی حذف یک تصویر، آنرا حذف کند، این حذف باید در بانک اطلاعاتی هم منعکس شود؛ در غیر اینصورت باز هم کاربر میتواند تصویری را حذف کند، اما در آخر بر روی دکمهی به روز رسانی اطلاعات رکورد کلیک نکند. در این حالت در دفعات بعدی مراجعهی به اطلاعات یک چنین اتاقی، با نقص اطلاعات تصاویری مواجه میشویم که در لیست تصاویر منتسب به یک اتاق وجود دارند، اما اصل فایل تصویری متناظر با آنها از سرور حذف شدهاست.
حذف اطلاعات تصاویر، در حالت ثبت اطلاعات
زمانیکه قرار است اطلاعات اتاقی برای اولین بار ثبت شود، حذف تصاویر آپلود شدهی مرتبط با آن سادهاست؛ چون هنوز اصل رکورد اتاق ثبت نشدهاست و این تصاویر در این لحظه، به رکوردی تعلق ندارند. بنابراین ابتدا متد رویدادگردان DeletePhoto را به دکمهی حذف اطلاعات هر تصویر نمایش داده شده، انتساب میدهیم:
@if (HotelRoomModel.HotelRoomImages.Count > 0) { var serial = 1; foreach (var roomImage in HotelRoomModel.HotelRoomImages) { <div class="col-md-2 mt-3"> <div class="room-image" style="background: url('@roomImage.RoomImageUrl') 50% 50%; "> <span class="room-image-title">@serial</span> </div> <button type="button" @onclick="()=>DeletePhoto(roomImage)" class="btn btn-outline-danger btn-block mt-4">Delete</button> </div> serial++; } }
@code { private const string UploadFolder = "Uploads"; private void DeletePhoto(HotelRoomImageDTO imageDto) { var imageFileName = imageDto.RoomImageUrl.Replace($"{UploadFolder}/", "", StringComparison.OrdinalIgnoreCase); if (HotelRoomModel.Id == 0 && Title == "Create") { FileUploadService.DeleteFile(imageFileName, WebHostEnvironment.WebRootPath, UploadFolder); HotelRoomModel.HotelRoomImages.Remove(imageDto); } } }
- در این شیء، مقدار خاصیت RoomImageUrl، همواره با نام پوشهای که فایلهای تصویری در آن آپلود شدهاند، شروع میشود. به همین جهت نام پوشه را از آن حذف کرده و بر این اساس، متد FileUploadService.DeleteFile را فراخوانی میکنیم تا تصویر جاری را از سرور حذف کند.
- سپس با فراخوانی متد Remove بر روی لیست تصاویر موجود، سبب به روز رسانی UI نیز خواهیم شد و به این ترتیب، تصویری که فایل آن از سرور حذف شده، از UI نیز حذف خواهد شد.
حذف تصاویر، در زمان ویرایش اطلاعات یک اتاق تعریف شده
همانطور که در ابتدای بحث نیز عنوان شد، نمیخواهیم در حالت ویرایش یک رکورد، با کلیک بر روی حذف یک تصویر، بلافاصله آنرا از سرور نیز حذف کنیم. چون ممکن است کاربری تصویری را حذف کند، اما بجای ذخیره سازی اطلاعات رکورد، بر روی دکمهی back کلیک کند. بنابراین در اینجا حذف تصاویر را صرفا به حذف آنها از UI محدود میکنیم و حذف نهایی را به زمان کلیک بر روی دکمهی ذخیره سازی اطلاعات در حال ویرایش، موکول خواهیم کرد.
به همین جهت در ابتدا با کلیک بر روی دکمهی حذف، ابتدا با حذف آن تصویر از HotelRoomImages، سبب به روز رسانی UI خواهیم شد، اما این تصویر را واقعا حذف نمیکنیم. در اینجا فقط نام آنرا در یک لیست، برای حذف نهایی، ذخیره سازی خواهیم کرد:
@code { private List<string> DeletedImageFileNames = new List<string>(); private void DeletePhoto(HotelRoomImageDTO imageDto) { var imageFileName = imageDto.RoomImageUrl.Replace($"{UploadFolder}/", "", StringComparison.OrdinalIgnoreCase); if (HotelRoomModel.Id == 0 && Title == "Create") { // ... } else { // Edit Mode DeletedImageFileNames.Add(imageFileName); HotelRoomModel.HotelRoomImages.Remove(imageDto); // Update UI } }
سپس در جائیکه کار مدیریت ثبت اطلاعات صورت میگیرد، پس از به روز رسانی رکورد متناظر با یک اتاق، بر اساس لیست DeletedImageFileNames، فایلهای علامتگذاری شدهی برای حذف را نیز واقعا از سرور حذف میکنیم:
private async Task HandleHotelRoomUpsert() { // ... if (HotelRoomModel.Id != 0 && Title == "Update") { // Update Mode var updatedRoomDto = await HotelRoomService.UpdateHotelRoomAsync(HotelRoomModel.Id, HotelRoomModel); foreach(var imageFileName in DeletedImageFileNames) { FileUploadService.DeleteFile(imageFileName, WebHostEnvironment.WebRootPath, UploadFolder); } // await AddHotelRoomImageAsync(updatedRoomDto); await JsRuntime.ToastrSuccess($"The `{HotelRoomModel.Name}` updated successfully."); } else { // ... } } }
نمایش «لطفا منتظر بمانید» در حین آپلود تصاویر
در ادامه میخواهیم تا پایان نمایش آپلود تصاویر، پیام «لطفا منتظر بمانید» را به همراه یک spinner نمایش دهیم. بنابراین در ابتدا کلاسهای جدید زیر را به فایل wwwroot\css\site.css اضافه میکنیم:
.spinner { border: 16px solid silver !important; border-top: 16px solid #337ab7 !important; border-radius: 50% !important; width: 80px !important; height: 80px !important; animation: spin 700ms linear infinite !important; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%); position: absolute !important; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
@code { private bool IsImageUploadProcessStarted; private async Task HandleImageUpload(InputFileChangeEventArgs args) { try { IsImageUploadProcessStarted = true; // ... } finally { IsImageUploadProcessStarted = false; } } }
<InputFile OnChange="HandleImageUpload" multiple></InputFile> <div class="row"> @if (IsImageUploadProcessStarted) { <div class="col-md-12"> <span><i class="spinner"></i> Please wait.. Images are uploading...</span> </div> }
دریافت تائیدیهی حذف، پس از کلیک بر روی دکمههای حذف تصاویر
در قسمت 12 این سری، کامپوننت Confirmation.razor را توسعه دادیم. در اینجا میخواهیم با کلیک بر روی دکمههای حذف تصاویر، ابتدا توسط این کامپوننت، تائیدیهای دریافت شود و در صورت تائید، آن تصویر انتخابی را حذف کنیم.
به همین جهت در ابتدا فایل Confirmation.razor را به پوشهی جدید Pages\Components کپی میکنیم. سپس فضای نام آنرا به فایل BlazorServer\BlazorServer.App\_Imports.razor اضافه میکنیم تا در تمام کامپوننتهای برنامه قابل استفاده شود:
@using BlazorServer.App.Pages.Components
<Confirmation @ref="Confirmation1" OnCancel="OnCancelDeleteImageClicked" OnConfirm="@(()=>OnConfirmDeleteImageClicked(ImageToBeDeleted))"> <div> Do you want to delete @ImageToBeDeleted?.RoomImageUrl image? </div> </Confirmation>
- سپس روالهای رویدادگردان OnCancel و OnConfirm به متدهایی در کامپوننت جاری متصل شدهاند.
- در آخر پیامی تعریف شدهاست.
برای اینکه کامپوننت فوق عمل کند، نیاز است تغییرات زیر را به قسمت کدها اعمال کنیم:
private Confirmation Confirmation1; private HotelRoomImageDTO ImageToBeDeleted; private void OnCancelDeleteImageClicked() { // Confirmation1.Hide(); } private void DeletePhoto(HotelRoomImageDTO imageDto) { ImageToBeDeleted = imageDto; Confirmation1.Show(); } private void OnConfirmDeleteImageClicked(HotelRoomImageDTO imageDto) {
- در اینجا محتوای متد DeletePhoto اصلی را (متدی را که تا پیش از این مرحله تکمیل کردیم) به متد جدید OnConfirmDeleteImageClicked منتقل کردهایم. یعنی در ابتدا فقط یک modal نمایش داده میشود. پس از اینکه کاربر عملیات حذف را تائید کرد، رویداد OnConfirm، سبب فراخوانی متد OnConfirmDeleteImageClicked خواهد شد (که همان DeletePhoto قبل از این تغییرات است).
حذف کامل یک اتاق به همراه تمام تصاویر منتسب به آن
مرحلهی آخر این قسمت، اضافه کردن دکمهی حذف، به ردیفهای کامپوننت نمایش لیست اتاقها است که این مورد نیز باید به همراه دریافت تائیدیهی حذف و همچنین حذف تمام وابستگیهای اتاق ثبت شده باشد:
<td> <NavLink href="@($"hotel-room/edit/{room.Id}")" class="btn btn-primary">Edit</NavLink> <button class="btn btn-danger" @onclick="()=>HandleDeleteRoom(room)">Delete</button> </td>
اکنون برای مدیریت دریافت تائیدیهی حذف از کاربر، کامپوننت Confirmation را اضافه کرده:
<Confirmation @ref="Confirmation1" OnCancel="OnCancelDeleteRoomClicked" OnConfirm="OnConfirmDeleteRoomClicked"> <div> Do you want to delete @RoomToBeDeleted?.Name? </div> </Confirmation>
@code { private List<HotelRoomDTO> HotelRooms = new List<HotelRoomDTO>(); private HotelRoomDTO RoomToBeDeleted; private Confirmation Confirmation1; private void OnCancelDeleteRoomClicked() { // Confirmation1.Hide(); } private void HandleDeleteRoom(HotelRoomDTO roomDto) { RoomToBeDeleted = roomDto; Confirmation1.Show(); } private async Task OnConfirmDeleteRoomClicked() { if(RoomToBeDeleted is null) { return; } await HotelRoomService.DeleteHotelRoomAsync(RoomToBeDeleted.Id); HotelRooms.Remove(RoomToBeDeleted); // Update UI }
مشکل! این روش استفادهی از DbContext کار نمیکند!
اگر برنامه را اجرا کرده و سعی در حذف یک ردیف کنیم، به خطای زیر میرسیم:
An exception occurred while iterating over the results of a query for context type 'BlazorServer.DataAccess.ApplicationDbContext'. System.InvalidOperationException: A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-18.zip
An extremely simple and responsive jQuery plugin that allows you to
create multi-level drop down menu with subtle effects (sliding, fading,
scale up, or scale down). Each sub-menu in this menu will be shown in
its own context, making the “parent” level disappear, good for saving
space for menus which have a lot of content