<compilation xdt:Transform="RemoveAttributes(debug)" />
<customErrors mode="Off" defaultRedirect="~/GenericErrorPage.aspx"> <error statusCode="404" redirect="~/GenericErrorPage.aspx" /> </customErrors>
<customErrors mode="RemoteOnly" xdt:Transform="Replace" defaultRedirect="~/GenericErrorPage.aspx"> <error statusCode="404" redirect="~/GenericErrorPage.aspx" /> </customErrors>
<connectionStrings> <add name="TestContext" connectionString="Data Source=Server1;Password=****;User ID=sa; Initial Catalog=Test;Integrated Security=True" xdt:Transform="SetAttributes" xdt:Locator="Match(name)"/> </connectionStrings>
در این قسمت میتوانید هر کدام از موارد را که نیاز دارید، نصب کنید. هر کدام از عنوانها در آینده آموزش داده خواهند شد و پیشنهاد میشود آنها را نصب کنید؛ در غیر این صورت فقط گزینهی اول کافی میباشد.
در صورت کلیک بر روی Options، با صفحهی زیر روبرو میشوید که در آن با انتخاب گزینهی Administrative، میتوانید تنظیمات بیشتری را در زمان نصب، انجام دهید و همچنین با انتخاب All users ، نرم افزار برای تمام کاربران سیستم در دسترس خواهد بود.
بعد از اتمام نصب، فایلهای نرم افزار در آدرس زیر در دسترس میباشند:
%LOCALAPPDATA%\JetBrains\Installations
اکنون اگر Visual studio را باز کنید، Resharper به قسمت Extensionsها اضافه شدهاست که در ادامهی آموزش به بررسی آن میپردازیم.
زبانهای پشتیبانی شده
ریشارپر از زبان های C# , VB.NET , TypeScript , JavaScript , C++ , CSS پشتیبانی میکند.
افزایش سرعت Reshaper
یکی از مشکلاتی که بیشتر افراد بعد از نصب این افزونه دارند، مخصوصا اگر سیستم آنها خیلی قوی نباشد، کند شدن Visual Studio میباشد. برای سریعتر کردن این افزونه راه هایی موجود میباشد که به آنها میپردازیم.
- خود ریشارپر پیشنهادهایی را برای بهبود سرعت میکند که آنها در مسیر زیر در دسترس میباشند:
ReSharper | Options | Environment | Performance Guide
- یکی از امکانات ریشارپر، نشان دادن تمام خطاهای موجود در برنامه به صورت یک لیست میباشد که نام این ویژگی، solution-wide analysis است و البته این امکان باعث سنگینی زیاد Visual studio میشود. برای غیر فعال کردن آن میتوانید به مسیر زیر بروید:
ReSharper | Options | Code Inspection | Settings
- راه دیگر انجام اینکار از قسمت تنظیمات خود Visual studio است. برای این کار به مسیر زیر بروید و گزینههای گفته شده را غیر فعال کنید.
Environment | General
Automatically adjust visual experience based on client performance Enable rich client visual experience
- همچنین این گزینه را نیز فعال کنید تا جلوی لگ در UI گرفته شود.
Use hardware graphics acceleration if available
- اگر پروژهی بزرگی را دارید، میتوانید گزینهی زیر را نیز غیر فعال کنید. البته با غیر فعال کردن این گزینه در صورت کرش نرم افزار، کدهای ذخیره نشده ازدست میروند.
Environment | AutoRecover
Save AutoRecover information
- اگر با تعداد فایلهای زیادی کار میکنید، امکان Track changes باعث کندی برنامه میشود. برای غیر فعال کردن این گزینه، به مسیر زیر بروید.
Text Editor | General
Track changes
- خود Visual studio گزینههایی را مانند خطاها در Scroll Bar نشان میدهد که این امکانات در ریشارپر هم موجود میباشد. برای غیر فعال کردن این امکان در Visual Studio برای جلوگیری از دو بار نشان دادن اطلاعات، به مسیر زیر بروید و گزینهی گفته شده را غیر فعال کنید.
Text Editor | All Languages | Scroll Bars
Show annotations over vertical scroll bar
- یکی دیگر از امکانات Visual Studio گزینهای به اسم CodeLens می باشد که یکی از کارهای آن، نشان دادن تمام رفرنسهای توابع یک فایل، در بالای تابع میباشد. این امکان نیز باعث کندی بسیار زیاد برنامه میشود. برای غیر فعال کردن آن میتوانید به مسیر زیر بروید.
Text Editor | All Languages | CodeLens
- هم Visual Studio و هم Reshaper کدهای شما را Format میکنند. پس برای جلوگیری از دوبار انجام شدن این کار، به مسیر زیر بروید و گزینهی گفته شده را غیر فعال کنید.
Text Editor | [Language] | Formatting
auto-formatting
- اگر از تمام امکانات Reshaper نمیخواهید استفاده کنید، میتوانید آنها را از آدرس زیر غیر فعال کنید.
Environment | Products & Features
- اگر در زمان تایپ کردن، برنامه کند میباشد، میتوانید بعضی از امکانات Resharper را از آدرس زیر غیر فعال کنید.
Environment | IntelliSense
Completion Appearance ReSharper's IntelliSense for specific languages
ادامه آشنایی با NUnit
اگر قسمت سوم را دنبال کرده باشید احتمالا از تعداد مراحلی که باید در خارج از IDE صورت گیرد گلایه خواهید کرد (کامپایل کن، اجرا کن، اتچ کن، باز کن، ذخیره کن، اجرا کن و ...). خوشبختانه افزونه ReSharper این مراحل را بسیار ساده و مجتمع کرده است.
این افزونه به صورت خودکار متدهای آزمایش واحد یک پروژه را تشخیص داده و آنها را با آیکونهایی ( Gutter icons ) متمایز مشخص میسازد (شکل زیر). پس از کلیک بر روی آنها ، امکان اجرای آزمایش یا حتی دیباگ کردن سطر به سطر یک متد آزمایش واحد درون IDE ویژوال استودیو وجود خواهد داشت.
برای نمونه پس از اجرای آزمایش واحد قسمت قبل، نتیجه حاصل مانند شکل زیر خواهد بود:
راه دیگر، استفاده از افزونه TestDriven.NET است که نحوه استفاده از آنرا اینجا میتوانید ملاحظه نمائید. به منوی جهنده کلیک راست بر روی یک صفحه، گزینه run tests را اضافه میکند و نتیجه حاصل را در پنجره output ویژوال استودیو نمایش میدهد.
ساختار کلی یک کلاس آزمایش واحد مبتنی بر NUnit framework :
using NUnit.Framework;
using NUnit.Framework.SyntaxHelpers;
namespace TestLibrary
{
[TestFixture]
public class Test2
{
[SetUp]
public void MyInit()
{
//کدی که در این قسمت قرار میگیرد پیش از اجرای هر متد تستی اجرا خواهد شد
}
[TearDown]
public void MyClean()
{
//کدی که در این قسمت قرار میگیرد پس از اجرای هر متد تستی اجرا خواهد شد
}
[TestFixtureSetUp]
public void MyTestFixtureSetUp()
{
// کدی که در اینجا قرار میگیرد در ابتدای بررسی آزمایش واحد و فقط یکبار اجرا میشود
}
[TestFixtureTearDown]
public void MyTestFixtureTearDown()
{
// کدهای این قسمت در پایان کار یک کلاس آزمایش واحد اجرا خواهند شد
}
[Test]
public void Test1()
{
//بدنه آزمایش واحد در اینجا قرار میگیرد
Assert.That(2, Is.EqualTo(2));
}
}
}
همانطور که در قسمت قبل نیز ذکر شد، به امضاهای متدها و کلاس فوق دقت نمائید (عمومی ، void و بدون آرگومان ورودی).
بهتر است از ویژگیهای SetUp و TearDown با دقت استفاده نمود. عموما هدف از این روالها ایجاد یک شیء و تخریب و پاک سازی آن است. حال اینکه این روالها قبل و پس از اجرای هر متد آزمایش واحدی فراخوانی میشوند. بنابراین به این موضوع دقت داشته باشید.
همچنین توصیه میشود که کلاسهای آزمایش واحد را در اسمبلی دیگری مجزا از پروژه اصلی پیاده سازی کنید (برای مثال یک پروژه جدید از نوع class library)، زیرا این موارد مرتبط با بررسی کیفیت کدهای شما هستند که موضوع جداگانهای نسبت به پروژه اصلی محسوب میگردد (نحوه پیاده سازی آنرا در قسمت قبل ملاحظه نمودید). همچنین در یک پروژه تیمی این جدا سازی، مدیریت آزمایشات را سادهتر میسازد و بعلاوه سبب حجیم شدن بیمورد اسمبلیهای اصلی محصول شما نیز نمیگردند.
ادامه دارد...
اهمیت code review
یکی کردن اسمبلیهای یک پروژهی WPF
- یک try/catch در قسمت بارگذاری اسمبلی قرار دهید تا بهتر منبع مشکل را شناسایی کنید. یک مثال
- شخص دیگری در اینجا گزارش داده اگر Generate serialization assembly در قسمت تنظیمات پروژه، ذیل Build > Output فعال است، باید خاموش شود تا پروژه کرش نکند.
- اگر نوع اسمبلی، PCL است (Portable Class Library)، باز هم روش Assembly.Load به نحوی که در مطلب ذکر شده کار نمیکند و باید به صورت ذیل اصلاح شود:
private static Assembly loadEmbeddedAssembly(string name) { if (name.EndsWith("Retargetable=Yes")) { return Assembly.Load(new AssemblyName(name)); } // Rest of your code //... }
[MethodImpl(MethodImplOptions.NoOptimization)]
افزودن متغیرهای محیطی
در برنامهی نمایش لیست فیلمهایی که تا قسمت 29 آنرا بررسی کردیم، از فایل src\config.json برای ذخیره سازی اطلاعات تنظیمات برنامه استفاده شد. هرچند این روش کار میکند اما بر اساس محیطهای مختلف توسعه، متغیر نیست. اغلب برنامهها باید بتوانند حداقل در سه محیط توسعه، آزمایش و تولید، بر اساس متغیرها و تنظیمات خاص هر کدام، کار کنند. برای مثال بر روی سیستمی که کار توسعه در آن انجام میشود، میخواهیم apiUrl متفاوتی را نسبت به حالتیکه برنامه توزیع میشود، داشته باشیم.
برای رفع این مشکل، برنامههایی که توسط create-react-app تولید میشوند، دارای پشتیبانی توکاری از متغیرهای محیطی هستند. برای این منظور نیاز است در ریشهی پروژه (جائیکه فایل package.json قرار دارد) فایل جدید env. را ایجاد کرد. در ویندوز برای ایجاد یک چنین فایلهایی که فقط از یک پسوند تشکیل میشوند، باید نام فایل را به صورت .env. وارد کرد؛ سپس خود ویندوز نقطهی نهایی را حذف میکند. البته اگر از ادیتور VSCode برای ایجاد این فایل استفاده میکنید، نیازی به درج نقطهی انتهایی نیست. در این فایل environment ایجاد شده میتوان تمام متغیرهای محیطی مورد نیاز را با مقادیر پیشفرض آنها درج کرد. همچنین میتوان این مقادیر پیشفرض را بر اساس محیطهای مختلف کاری، بازنویسی کرد. برای مثال میتوان فایل env.development. را اضافه کرد؛ به همراه فایلهای env.test. و env.production.
متغیرهای محیطی به صورت key=value درج میشوند. این کلیدها نیر باید با REACT_APP_ شروع شوند؛ در غیر اینصورت، کار نخواهند کرد. برای مثال در فایل env.، دو متغیر پیشفرض زیر را تعریف میکنیم:
REACT_APP_NAME=My App REACT_APP_VERSION=1
console.log(process.env);
در این خروجی، متغیر "NODE_ENV: "development به صورت خودکار با تولید بستههای مخصوص ارائهی نهایی، به production تنظیم میشود. سایر متغیرهای محیطی تعریف شده را نیز در اینجا ملاحظه میکنید. با توجه به خواص شیء env، برای مثال جهت دسترسی به نام برنامه میتوان از مقدار process.env.REACT_APP_NAME استفاده کرد.
یک نکته: با هر تغییری در مقادیر متغیرهای محیطی، نیاز است یکبار دیگر برنامه را از ابتدا توسط دستور npm start، راه اندازی مجدد کرد؛ چون این فایلها به صورت خودکار ردیابی نمیشوند.
نحوهی پردازش متغیرهای محیطی درج شدهی در برنامه
اگر همان سطر لاگ کردن خروجی process.env را به صورت زیر تغییر دهیم:
console.log("My App Name", process.env.REACT_APP_NAME);
همانطور که مشاهده میکنید، فراخوانی console.log ما، دیگر به همراه متغیر process.env.REACT_APP_NAME نیست؛ بلکه مقدار اصلی این متغیر در اینجا درج شدهاست. بنابراین اگر در در حین توسعهی برنامه، از متغیرهای محیطی استفاده شود، این متغیرها با مقادیر اصلی آنها در حین پروسهی Build نهایی، جایگزین میشوند.
Build برنامههای React برای محیط تولید
اجرای دستور npm start، سبب ایجاد یک Build مخصوص محیط توسعه میشود که بهینه سازی نشدهاست و به همراه اطلاعات اضافی قابل توجهی جهت دیباگ سادهتر برنامهاست. برای رسیدن به یک خروجی بهینه سازی شدهی مخصوص محیط تولید و ارائهی نهایی باید دستور npm run build را در خط فرمان اجرا کرد. خروجی نهایی این دستور، در پوشهی جدید build واقع در ریشهی پروژه، قرار میگیرد. اکنون میتوان کل محتویات این پوشه را جهت ارائهی نهایی در وب سرور خود، مورد استفاده قرار داد.
پس از پایان اجرای دستور npm run build، پیام «امکان ارائهی آن توسط static server زیر نیز وجود دارد» ظاهر میشود:
> npm install -g serve > serve -s build
البته با توجه به اینکه backend سرور برنامههای ما نیز در همین آدرس قرار دارد و در صورت ورود این آدرس، به صورت خودکار به https://localhost:5001/index.html هدایت خواهید شد، میتوان این پورت پیشفرض را با اجرای دستور serve -s build -l 1234 تغییر داد. اکنون میتوان آدرس جدید http://localhost:1234 را در مرورگر آزمایش کرد که ... با خطای زیر کار نمیکند:
Access to XMLHttpRequest at 'https://localhost:5001/api/genres' from origin 'http://localhost:1234' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
WithOrigins("http://localhost:3000", "http://localhost:1234")
یک نکته: زمانیکه از دستور npm start استفاده میشود، متغیرهای محیطی از فایل env.development. خوانده خواهند شد و زمانیکه از دستور npm run build استفاده میشود، این متغیرها از فایل env.production. تامین میشوند. در این حالتها اگر متغیری در این دو فایل درج نشده بود، از مقدار پیشفرض موجود در فایل env. استفاده میگردد. از فایل env.test. با اجرای دستور npm test، به صورت خودکار استفاده میشود.
آماده سازی برنامهی React، برای توزیع نهایی
تا اینجا برنامهی React تهیه شده، اطلاعات apiUrl خودش را از فایل config.json دریافت میکند. اکنون میخواهیم بر اساس حالات مختلف توسعه و تولید، از apiUrlهای متفاوتی استفاده شود. به همین جهت به فایل env.production. مراجعه کرده و تنظیمات ذیل را به آن اضافه میکنیم:
REACT_APP_API_URL=https://localhost:5001/api REACT_APP_ADMIN_ROLE_NAME=Admin
اکنون به برنامه مراجعه کرده و در هرجائی که ارجاعی به فایل config.json وجود دارد، سطر import آنرا حذف میکنیم. با این تغییر، تمام آدرسهایی مانند:
const apiEndpoint = apiUrl + "/users";
const apiEndpoint = "/users";
axios.defaults.baseURL = process.env.REACT_APP_API_URL;
همچنین adminRoleName مورد نیاز در فایل src\services\authService.js را نیز از همان فایل env جاری تامین میکنیم:
const adminRoleName = process.env.REACT_APP_ADMIN_ROLE_NAME;
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-34-frontend.zip و sample-34-backend.zip
افزودن فیلد آپلود تصاویر، به فرم ثبت اطلاعات یک اتاق
در ادامه به کامپوننت Pages\HotelRoom\HotelRoomUpsert.razor که تا این قسمت آنرا تکمیل کردهایم مراجعه کرده و فیلد جدید InputFile را ذیل قسمت ثبت توضیحات، اضافه میکنیم:
<div class="form-group"> <InputFile OnChange="HandleImageUpload" multiple></InputFile> </div> @code { private async Task HandleImageUpload(InputFileChangeEventArgs args) { } }
- در این کامپوننت، رویداد OnChange، پس از تغییر مجموعهی فایلهای اضافه شدهی به آن، فراخوانی میشود و آرگومانی از نوع InputFileChangeEventArgs را دریافت میکند.
افزودن لیست فایلهای انتخابی به HotelRoomDTO
تا اینجا اگر به BlazorServer.Models\HotelRoomDTO.cs مراجعه کنیم (کلاسی که مدل UI فرم ثبت اطلاعات اتاق را فراهم میکند)، امکان افزودن لیست تصاویر انتخابی به آن وجود ندارد. به همین جهت در این کلاس، تغییر زیر را اعمال میکنیم:
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace BlazorServer.Models { public class HotelRoomDTO { // ... public virtual ICollection<HotelRoomImageDTO> HotelRoomImages { get; set; } = new List<HotelRoomImageDTO>(); } }
تکمیل متد رویدادگردان HandleImageUpload
در ادامه، لیست فایلهای انتخاب شدهی توسط کاربر را دریافت کرده و آنها را آپلود میکنیم:
@inject IHotelRoomService HotelRoomService @inject NavigationManager NavigationManager @inject IJSRuntime JsRuntime @inject IFileUploadService FileUploadService @inject IWebHostEnvironment WebHostEnvironment @code { // ... private async Task HandleImageUpload(InputFileChangeEventArgs args) { var files = args.GetMultipleFiles(maximumFileCount: 5); if (args.FileCount == 0 || files.Count == 0) { return; } var allowedExtensions = new List<string> { ".jpg", ".png", ".jpeg" }; if(!files.Any(file => allowedExtensions.Contains(Path.GetExtension(file.Name), StringComparer.OrdinalIgnoreCase))) { await JsRuntime.ToastrError("Please select .jpg/.jpeg/.png files only."); return; } foreach (var file in files) { var uploadedImageUrl = await FileUploadService.UploadFileAsync(file, WebHostEnvironment.WebRootPath, "Uploads"); HotelRoomModel.HotelRoomImages.Add(new HotelRoomImageDTO { RoomImageUrl = uploadedImageUrl }); } } }
@using Microsoft.AspNetCore.Hosting @using System.Linq @using System.IO
- متد ()args.GetMultipleFiles، امکان دسترسی به فایلهای انتخابی توسط کاربر را میسر میکند که خروجی آن از نوع <IReadOnlyList<IBrowserFile است. در قسمت قبل، سرویس آپلود فایلهایی را که تکمیل کردیم، امکان آپلود یک IBrowserFile را به سرور میسر میکند. اگر متد ()GetMultipleFiles را بدون پارامتری فراخوانی کنیم، حداکثر 10 فایل را قبول میکند و اگر تعداد بیشتری انتخاب شده باشد، یک استثناء را صادر خواهد کرد.
- سپس بر اساس پسوند فایلهای دریافتی، آنها را صرفا به فایلهای تصویری محدود کردهایم.
- در آخر، لیست فایلهای دریافتی را یکی یکی به سرور آپلود کرده و Url دسترسی به آنها را به لیست HotelRoomImages اضافه میکنیم. فایلهای آپلود شده در پوشهی BlazorServer.App\wwwroot\Uploads قابل مشاهده هستند.
نمایش فایلهای انتخاب شدهی توسط کاربر
در ادامه میخواهیم پس از آپلود فایلها، آنها را در ذیل کامپوننت InputFile نمایش دهیم. برای اینکار در ابتدا به فایل wwwroot\css\site.css مراجعه کرده و شیوه نامهی نمایش تصاویر و عناوین آنها را اضافه میکنیم:
.room-image { display: block; width: 100%; height: 150px; background-size: cover !important; border: 3px solid green; position: relative; } .room-image-title { position: absolute; top: 0; right: 0; background-color: green; color: white; padding: 0px 6px; display: inline-block; }
<div class="form-group"> <InputFile OnChange="HandleImageUpload" multiple></InputFile> <div class="row"> @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" class="btn btn-outline-danger btn-block mt-4">Delete</button> </div> serial++; } } </div> </div>
ذخیره سازی اطلاعات تصاویر آپلودی یک اتاق در بانک اطلاعاتی
تا اینجا موفق شدیم تصاویر انتخابی کاربر را آپلود کرده و همچنین لیست آنها را نیز نمایش دهیم. در ادامه نیاز است تا این اطلاعات را در بانک اطلاعاتی ثبت کنیم. به همین جهت ابتدا سرویس IHotelRoomImageService را که در قسمت قبل تکمیل کردیم، به کامپوننت جاری تزریق میکنیم و سپس با استفاده از متد CreateHotelRoomImageAsync، رکوردهای تصویر متناظر با اتاق ثبت شده را اضافه میکنیم:
// ... @inject IHotelRoomImageService HotelRoomImageService @code { // ... private async Task AddHotelRoomImageAsync(HotelRoomDTO roomDto) { foreach (var imageDto in HotelRoomModel.HotelRoomImages) { imageDto.RoomId = roomDto.Id; await HotelRoomImageService.CreateHotelRoomImageAsync(imageDto); } } }
HotelRoomModel.HotelRoomImages.Add(new HotelRoomImageDTO { RoomImageUrl = uploadedImageUrl });
محل فراخوانی AddHotelRoomImageAsync فوق، در متد HandleHotelRoomUpsert است که در قسمتهای قبل تکمیل کردیم. در اینجا پس از ثبت اطلاعات اتاق در بانک اطلاعاتی است که به Id آن دسترسی پیدا میکنیم:
private async Task HandleHotelRoomUpsert() { // ... // Create Mode var createdRoomDto = await HotelRoomService.CreateHotelRoomAsync(HotelRoomModel); await AddHotelRoomImageAsync(createdRoomDto); await JsRuntime.ToastrSuccess($"The `{HotelRoomModel.Name}` created successfully."); // ... }
یک نکته: در انتهای بحث خواهیم دید که اینکار غیرضروری است و با وجود رابطهی one-to-many تعریف شدهی توسط EF-Core، اگر لیست HotelRoomImages موجودیت اتاق تعریف شده و در حال ثبت نیز مقدار دهی شده باشد، به صورت خودکار جزئی از این رابطه و تنها در یک رفت و برگشت، ثبت میشود. یعنی همان متد CreateHotelRoomAsync، قابلیت ثبت خودکار اطلاعات خاصیت HotelRoomImages موجودیت اتاق را نیز دارا است.
نمایش تصاویر یک اتاق، در حالت ویرایش رکورد آن
تا اینجا فقط حالت ثبت یک رکورد جدید را پوشش دادیم. در این حالت اگر به لیست اتاقهای ثبت شده مراجعه کرده و بر روی دکمهی edit یکی از آنها کلیک کنیم، به صفحهی ویرایش رکورد منتقل خواهیم شد؛ اما این صفحه، فاقد اطلاعات تصاویر منتسب به آن رکورد است.
علت اینجا است که در حین ویرایش اطلاعات، در متد OnInitializedAsync، هرچند اطلاعات یک اتاق را از بانک اطلاعاتی دریافت کرده و آنرا تبدیل به Dto آن میکنیم که سبب نمایش جزئیات هر خاصیت در فیلد متصل به آن در فرم جاری میشود:
protected override async Task OnInitializedAsync() { if (Id.HasValue) { // Update Mode Title = "Update"; HotelRoomModel = await HotelRoomService.GetHotelRoomAsync(Id.Value); } // ... }
بنابراین به فایل BlazorServer\BlazorServer.Services\HotelRoomService.cs مراجعه کرده و تغییرات زیر را اعمال میکنیم:
namespace BlazorServer.Services { public class HotelRoomService : IHotelRoomService { // ... public IAsyncEnumerable<HotelRoomDTO> GetAllHotelRoomsAsync() { return _dbContext.HotelRooms .Include(x => x.HotelRoomImages) .ProjectTo<HotelRoomDTO>(_mapperConfiguration) .AsAsyncEnumerable(); } public Task<HotelRoomDTO> GetHotelRoomAsync(int roomId) { return _dbContext.HotelRooms .Include(x => x.HotelRoomImages) .ProjectTo<HotelRoomDTO>(_mapperConfiguration) .FirstOrDefaultAsync(x => x.Id == roomId); } } }
افزودن تصاویر جدید، در حین ویرایش یک رکورد
پس از نمایش لیست تصاویر منتسب به یک اتاق در حال ویرایش، اکنون میخواهیم در همین حالت اگر کاربر تصویر جدیدی را انتخاب کرد، این تصویر را نیز به لیست تصاویر ثبت شدهی در بانک اطلاعاتی اضافه کنیم. برای اینکار نیز به متد HandleHotelRoomUpsert مراجعه کرده و از متد AddHotelRoomImageAsync در قسمت به روز رسانی آن استفاده میکنیم:
private async Task HandleHotelRoomUpsert() { //... // Update Mode var updatedRoomDto = await HotelRoomService.UpdateHotelRoomAsync(HotelRoomModel.Id, HotelRoomModel); await AddHotelRoomImageAsync(updatedRoomDto); await JsRuntime.ToastrSuccess($"The `{HotelRoomModel.Name}` updated successfully."); //... }
برای رفع این مشکل میتوان در متد AddHotelRoomImageAsync، جدید بودن یک تصویر را بر اساس RoomId آن بررسی کرد. اگر این RoomId مساوی صفر بود، یعنی تازه به لیست اضافه شدهاست و حاصل بارگذاری اولیهی فرم ویرایش اطلاعات نیست:
private async Task AddHotelRoomImageAsync(HotelRoomDTO roomDto) { foreach (var imageDto in HotelRoomModel.HotelRoomImages.Where(x => x.RoomId == 0)) { imageDto.RoomId = roomDto.Id; await HotelRoomImageService.CreateHotelRoomImageAsync(imageDto); } }
یک نکته: متد AddHotelRoomImageAsync اضافی است!
چون از AutoMapper استفاده میکنیم، در ابتدای متد ثبت یک اتاق، کار نگاشت DTO، به موجودیت متناظر با آن انجام میشود:
public async Task<HotelRoomDTO> CreateHotelRoomAsync(HotelRoomDTO hotelRoomDTO) { var hotelRoom = _mapper.Map<HotelRoom>(hotelRoomDTO);
مقدار دهی RoomId یک تصویر، در اینجا غیرضروری است؛ چون RoomId و Room، به عنوان کلید خارجی این رابطه تعریف شدهاند که در اینجا Room یک تصویر، دقیقا همین اتاق در حال ثبت است و EF Core در حین ثبت نهایی، آنرا به صورت خودکار در تمام تصاویر مرتبط نیز مقدار دهی میکند.
یعنی نیازی به چندین بار رفت و برگشت تعریف شدهی در متد AddHotelRoomImageAsync نیست و اساسا نیازی به آن نیست؛ نه برای ثبت و نه برای ویرایش اطلاعات!
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-17.zip
- Layout Rounding :
این مثال تاثیر استفاده از UseLayoutRounding را شرح میدهد. اگر شما در این مثال به آرامی اندازه پنجره را تغییر دهید، تفاوت دو شیء رسم شده را کاملا درک خواهید کرد.
<Page x:Class="LayoutRounding.Lines" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Lines" Name="linesPage" > <StackPanel Width="150" Margin="7" Orientation="Horizontal"> <!-- Single pixel line with layout rounding turned OFF.--> <Rectangle UseLayoutRounding="False" Width="45.5" Margin="10" Height="1" Fill="Red"/> <!-- Single pixel line with layout rounding turned ON.--> <Rectangle UseLayoutRounding="True" Width="45.5" Margin="10" Height="1" Fill="Red"/> </StackPanel> <!-- Background Grid --> <Page.Background> <DrawingBrush Viewport="0,0,10,10" ViewportUnits="Absolute" TileMode="Tile"> <DrawingBrush.Drawing> <DrawingGroup> <GeometryDrawing Brush="White"> <GeometryDrawing.Geometry> <RectangleGeometry Rect="0,0,1,1" /> </GeometryDrawing.Geometry> </GeometryDrawing> <GeometryDrawing Geometry="M0,0 L1,0 1,0.1, 0,0.1Z " Brush="#CCCCFF" /> <GeometryDrawing Geometry="M0,0 L0,1 0.1,1, 0.1,0Z" Brush="#CCCCFF" /> </DrawingGroup> </DrawingBrush.Drawing> </DrawingBrush> </Page.Background> </Page>
برای توضیحات بیشتر در مورد Layout Rounding میتوانید به لینک درج شده مراجعه کنید.
- Cached Composition :
<Window x:Class="BitmapCacheBrushDemo.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Height="300" Width="300" > <Window.Resources> <RichTextBox x:Key="cachedRichTextBox" > <RichTextBox.CacheMode> <BitmapCache EnableClearType="True" RenderAtScale="1" SnapsToDevicePixels="True" /> </RichTextBox.CacheMode> </RichTextBox> <BitmapCacheBrush x:Key="cachedRichTextBoxBrush" Target="{StaticResource cachedRichTextBox}"> <BitmapCacheBrush.BitmapCache> <BitmapCache EnableClearType="False" RenderAtScale="0.4" SnapsToDevicePixels="False" /> </BitmapCacheBrush.BitmapCache> </BitmapCacheBrush> </Window.Resources> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="*" /> <RowDefinition Height="*" /> <RowDefinition Height="*" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Name="button1" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Name="button2" Grid.Column="1" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Name="button3" Grid.Column="2" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Name="button4" Grid.Column="3" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Name="button5" Grid.Column="4" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Name="button6" Grid.Row="1" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="1" Name="button7" Grid.Row="1" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="2" Name="button8" Grid.Row="1" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="3" Name="button9" Grid.Row="1" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="4" Name="button10" Grid.Row="1" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Name="button11" Grid.Row="2" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="1" Name="button12" Grid.Row="2" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="2" Name="button13" Grid.Row="2" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="3" Name="button14" Grid.Row="2" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="4" Name="button15" Grid.Row="2" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Name="button16" Grid.Row="3" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="1" Name="button17" Grid.Row="3" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="2" Name="button18" Grid.Row="3" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="3" Name="button19" Grid.Row="3" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="4" Name="button20" Grid.Row="3" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Name="button21" Grid.Row="4" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="1" Name="button22" Grid.Row="4" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="2" Name="button23" Grid.Row="4" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="3" Name="button24" Grid.Row="4" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="4" Name="button25" Grid.Row="4" FontWeight="Bold" /> </Grid> </Window>
- Pixel Shader 3 Support :
- Easing Functions :
عنوان بخش دوم Graphics and Rendering میباشد که در ادامه آشنا خواهیم شد.
پروژه بنده به صورتی هست که در آن قرار است یک سری اطلاعات توسط DataTable به Report ارسال و ریپورت هم Fieldها را که از قبل طراحی شده، روی فرم داشته باشد و کاربر فقط این امکان را داشته باشد که مکان فیلدها ( یعنی مختصات ) روی صفحه A4 را با توجه به سلیقه خودش تنظیم و ذخیره نماید.
اهداف :
1.غیر فعال کردن بعضی از امکانات صفحه Design
2.اعمال یکسری تنظیمات از طریق کدنویسی (Code Behind) بر روی ریپورت
مانند : اینکه ShowGrid فعال باشه یا نه -- یا Toolbox.visible=false باشه
3.فارسی سازی محیط طراحی برای کاربر
4.ذخیره و ...
اسکرین شات : ( حالت پیشفرض بدون اعمال تغییرات Runtime )
رفرنسهای مورد نیاز:
using Stimulsoft.Base.Services; using Stimulsoft.Report; using Stimulsoft.Report.Components; using Stimulsoft.Report.Design; using Stimulsoft.Report.Design.Toolbars; using Stimulsoft.Report.Render; using Stimulsoft.Report.Units;
//ایجاد یک شی از ریپورت StiReport report = new StiReport(); //ریست کردن تنظیمات به حالت پیشفرض report.Reset(); //ریست کردن تنظیمات سرویسهای StiConfig.Reset(); //ریست کردن تنظیمات چاپ StiSettings.Clear(); //تنظیم عنوان ریپورت به صورت دلخواه StiOptions.Designer.DesignerTitle = title + " طراحی فرم "; StiOptions.Designer.DesignerTitleText = title + " طراحی فرم "; //غیرفعال شدن نمایش تب کدنویسی StiOptions.Designer.CodeTabVisible = false; //فعال کردن امکان RightToLeft StiOptions.Designer.UseRightToLeftGlobalizationEditor = true; //غیرفعال شدن قابلیت تغییر نام ریپورت توسط کاربر StiOptions.Designer.CanDesignerChangeReportFileName = false; //غیر فعال کردن تشخیص اتوماتیک زبان پیش فرض UI طراحی StiOptions.Designer.UseSimpleGlobalizationEditor = false; //تنظیم تم ریپورت از حالت استاندارد به ریبون StiOptions.Windows.GlobalGuiStyle = StiGlobalGuiStyle.Office2010Blue; //فعال سازی تم ریبون StiOptions.Designer.IsRibbonGuiEnabled = true;
یک شی از StiOptions ایجاد کنید. سپس Designer و فعال و غیرفعال کردن نمایش هر بخش را و حتی تغییر آیتمهای موجود در منوی راست کلیک هر شی، قابل تغییر خواهند بود.
پنل Dictionary از سه شاخه اصلی تشکیل میشود : که با دستورات زیر میتوان نمایش این بخشها را در صورت خالی بودن از داده غیر فعال نمود
BusinessObjectsCategory - DataSourcesCategory -VariablesCategory
StiOptions.Designer.Panels.Dictionary.ShowEmptyBusinessObjectsCategory = false; StiOptions.Designer.Panels.Dictionary.ShowEmptyDataSourcesCategory = false; StiOptions.Designer.Panels.Dictionary.ShowEmptyVariablesCategory = false;
1- Dictionary
2- Properties
3- Report Tree
که Properties نسبت به هر شیء ایی که از صفحه ریپورت انتخاب میکنید، تنظیمات مربوط به آن را برای ویرایش در اختیار کاربر قرار میدهد.
پنل Report Tree نیز از دادههای موجود از دیتاسورس بخش Dictionary که به صورت شیء در صفحه ریپورت قرار داده شدهاند نمایش درخت وارهای را در اختیار کاربر قرار میدهد و میتواند از اشیاء این درخت واره در ریپورت به صورت متعدد استفاده نماید.
میتوان هر کدام از این پنلها را ( که به صورت سرویس در Stimulsoft تعریف شده) از دید کاربر مخفی نمود یا به صورت محدود یکسری از قابلیتها را در اختیار کاربر قرار داد:
//غیر فعال کردن سرویسهای پنل Stimulsoft.Report.Design.Panels.StiPropertiesPanelService propPanel = Stimulsoft.Report.Design.Panels.StiPropertiesPanelService.GetService(); propPanel.ServiceEnabled = false; Stimulsoft.Report.Design.Panels.StiDictionaryPanelService dictPanel = Stimulsoft.Report.Design.Panels.StiDictionaryPanelService.GetService(); dictPanel.ServiceEnabled = true; Stimulsoft.Report.Design.Panels.StiReportTreePanelService treePanel = Stimulsoft.Report.Design.Panels.StiReportTreePanelService.GetService(); treePanel.ServiceEnabled = false; Stimulsoft.Report.Design.Toolbars.StiToolsToolbarService cpanel = Stimulsoft.Report.Design.Toolbars.StiToolsToolbarService.GetService(); cpanel.ServiceEnabled = false; StiOptions.Dictionary.BusinessObjects.AddBusinessObjectAssemblyToReferencedAssembliesAutomatically = false; StiOptions.Dictionary.BusinessObjects.AllowProcessNullItemsInEnumerables = false; StiOptions.Dictionary.BusinessObjects.AllowUseDataColumn = false; StiOptions.Dictionary.BusinessObjects.AllowUseFields = false; StiOptions.Dictionary.BusinessObjects.AllowUseProperties = false; StiOptions.Dictionary.BusinessObjects.CheckTableDuplication = false; StiOptions.Dictionary.ShowOnlyAliasForDataSource = true; StiOptions.Dictionary.ShowOnlyAliasForDataColumn = true; StiOptions.Dictionary.ShowOnlyAliasForTotal = true; dictPanel.ShowNewButton = false; dictPanel.ShowActionsButton = false; dictPanel.ShowBusinessObjectNewMenuItem = false; dictPanel.ShowCalcColumnNewMenuItem = false; dictPanel.ShowCategoryNewMenuItem = false; dictPanel.ShowCollapseAllMenuItem = true; dictPanel.ShowColumnNewMenuItem = false; dictPanel.ShowConnectionNewMenuItem = false; dictPanel.ShowContextMenu = false; dictPanel.ShowCreateFieldOnDoubleClick = false; dictPanel.ShowCreateLabel = false; dictPanel.ShowDataParameterNewMenuItem = false; dictPanel.ShowDataSourceNewMenuItem = false; dictPanel.ShowDataSourcesNewMenuItem = false; dictPanel.ShowDeleteButton = false; dictPanel.ShowDeleteForBusinessObject = false; dictPanel.ShowDeleteForDataColumn = false; dictPanel.ShowDeleteForDataConnection = false; dictPanel.ShowDeleteForDataParameter = false; dictPanel.ShowDeleteForDataRelation = false; dictPanel.ShowDeleteForDataSource = false; dictPanel.ShowDeleteForVariable = false; dictPanel.ShowDeleteMenuItem = false; dictPanel.ShowDictMergeMenuItem = false; dictPanel.ShowDictNewMenuItem = false; dictPanel.ShowDictOpenMenuItem = false; dictPanel.ShowDictSaveMenuItem = false; dictPanel.ShowDictXmlExportMenuItem = false; dictPanel.ShowDictXmlImportMenuItem = false; dictPanel.ShowDictXmlMergeMenuItem = false; dictPanel.ShowDownButton = false; dictPanel.ShowEditButton = false; dictPanel.ShowEditForBusinessObject = false; dictPanel.ShowEditForDataColumn = false; dictPanel.ShowEditForDataConnection = false; dictPanel.ShowEditForDataParameter = false; dictPanel.ShowEditForDataRelation = false; dictPanel.ShowEditForDataSource = false; dictPanel.ShowEditForVariable = false; dictPanel.ShowEditMenuItem = false; dictPanel.ShowExpandAllMenuItem = true; dictPanel.ShowMarkUsedMenuItem = false; dictPanel.ShowNewButton = false; dictPanel.ShowPropertiesForBusinessObject = false; dictPanel.ShowPropertiesForDataColumn = false; dictPanel.ShowPropertiesForDataConnection = false; dictPanel.ShowPropertiesForDataParameter = false; dictPanel.ShowPropertiesForDataRelation = false; dictPanel.ShowPropertiesForDataSource = false; dictPanel.ShowPropertiesForVariable = false; dictPanel.ShowPropertiesMenuItem = false; dictPanel.ShowRelationNewMenuItem = false; dictPanel.ShowRelationsImportMenuItem = false; dictPanel.ShowRemoveUnusedMenuItem = false; dictPanel.ShowSortItemsButton = false; dictPanel.ShowSynchronizeMenuItem = false; dictPanel.ShowUpButton = false; dictPanel.ShowUseAliases = true; dictPanel.ShowVariableNewMenuItem = false; dictPanel.ShowViewDataMenuItem = false;
نام فایل fa.xml میباشد. آنرا در مسیر نرم افزار قرار دهید و سپس کد زیر را اضافه نمایید:
//تنظیم زبان به فارسی StiConfig.LoadLocalization("fa.xml");