کتابخانه bootstrap-photo-gallery
<EditForm Model="@selectedCar" OnValidSubmit="@SaveObject"> <DataAnnotationsValidator /> <ValidationSummary /> ....My <InputText>'s for all values I have in my object <button type="submit" value="Save">Save</button> </EditForm> @code { [Parameter] public string Id { get; set; } CarModel selectedCar; protected override async Task OnInitializedAsync() { selectedCar = await _CarService.GetCar(int.Parse(Id)); } protected async Task SaveObject() { selectedCar.Id = await _CarService.SaveCar(selectedCar); } }
<EditForm Model="@selectedCar" Context="formContext"> <DataAnnotationsValidator /> <ValidationSummary /> ....My <InputText>'s for all values I have in my object <button type="submit" @onclick="@(() => SaveCar(formContext))">Save</button> <button type="submit" @onclick="@(() => UpdateStockQuantity(formContext))">Update stock quantity</button> <button type="submit" @onclick="@(() => DeleteCar(formContext))">Delete</button> </EditForm> @code { [Parameter] public string Id { get; set; } CarModel selectedCar; protected override async Task OnInitializedAsync() { selectedCar = await _CarService.GetCar(int.Parse(Id)); } protected async Task SaveCar(EditContext formContext) { bool formIsValid = formContext.Validate(); if (formIsValid == false) return; selectedCar.Id = await _CarService.SaveCar(selectedCar); } protected async Task DeleteObject(EditContext formContext) { selectedCar.Id = await _CarService.DeleteCar(selectedCar); } // ... plus same approach with UpdateStockQuantity. }
امکان تعریف HTML Forms استاندارد در Blazor 8x
فرمهای استاندارد HTML، پیش از ظهور جاوااسکریپت و SPAها وجود داشتند (دقیقا همان زمانیکه که فقط مفهوم SSR وجود خارجی داشت) و هنوز هم جزء مهمی از اغلب برنامههای وب را تشکیل میدهند. با ارائهی دات نت 8 و قابلیت server side rendering آن، کامپوننتهای برنامه، فقط یکبار در سمت سرور رندر شده و HTML سادهی آنها به سمت مرورگر کاربر بازگشت داده میشود. در این حالت، فرمهای استاندارد HTML، امکان دریافت ورودیهای کاربر و ارسال دادههای آنها را به سمت سرور میسر میکنند (چون دیگر خبری از اتصال دائم SignalR نیست و باید اطلاعات را به همان نحو استاندارد پروتکل HTTP، به سمت سرور Post کرد). در دات نت 8، دو راهحل برای کار با فرمها در برنامههای Blazor وجود دارد: استفاده از EditForm خود Blazor و یا استفاده از HTML forms استاندارد و ساده، به همان نحوی که بوده و هست.
روش کار با EditForm در برنامههای Blazor SSR
البته ما قصد استفاده از فرمهای سادهی HTML را در اینجا نداریم و ترجیح میدهیم که از همان EditForm استفاده کنیم. EditForms در Blazor بسیار مفید بوده و امکان بایند خواص یک مدل را به اجزای مختلف ورودیهای تعریف شدهی در آن میسر میکند و همچنین قابلیتهایی مانند اعتبارسنجی و امثال آنرا نیز به همراه دارد (اطلاعات بیشتر). اما چگونه میتوان از این امکان در برنامههای Blazor SSR نیز استفاده کرد؟
برای این منظور، ابتدا مثالی را به صورت زیر تکمیل میکنیم (که بر اساس قالب dotnet new blazor --interactivity Server تهیه شده) و سپس توضیحات آن ارائه خواهد شد:
الف) تهیه یک مدل برای تعریف محلهای مرتبط با یک سفارش در فایل Models/OrderPlace.cs
using System.ComponentModel.DataAnnotations; namespace Models; public record OrderPlace { public Address BillingAddress { get; set; } = new(); public Address ShippingAddress { get; set; } = new(); } public class Address { [Required] public string Name { get; set; } = default!; public string? AddressLine1 { get; set; } public string? AddressLine2 { get; set; } public string? City { get; set; } [Required] public string PostCode { get; set; } = default!; }
ب) تهیهی یک کامپوننت Editor برای دریافت اطلاعات آدرس فوق در فایل Components\Pages\Chekout\AddressEntry.razor
@inherits Editor<Models.Address> <div> <label>Name</label> <InputText @bind-Value="Value.Name"/> </div> <div> <label>Address 1</label> <InputText @bind-Value="Value.AddressLine1"/> </div> <div> <label>Address 2</label> <InputText @bind-Value="Value.AddressLine2"/> </div> <div> <label>City</label> <InputText @bind-Value="Value.City"/> </div> <div> <label>Post Code</label> <InputText @bind-Value="Value.PostCode"/> </div>
ج) استفاده از مدل و ادیتور فوق در یک EditForm تغییر یافته برای کار با برنامههای Blazor SSR در فایل Components\Pages\Chekout\Checkout.razor
@page "/checkout" @using Models @if (!_submitted && PlaceModel != null) { <EditForm Model="PlaceModel" method="post" OnValidSubmit="SubmitOrder" FormName="checkout"> <DataAnnotationsValidator/> <h4>Bill To:</h4> <AddressEntry @bind-Value="PlaceModel.BillingAddress"/> <h4>Ship To:</h4> <AddressEntry @bind-Value="PlaceModel.ShippingAddress"/> <button type="submit">Submit</button> <ValidationSummary/> </EditForm> } @if (_submitted && PlaceModel != null) { <div> <h2>Order Summary</h2> <h3>Shipping To:</h3> <dl> <dt>Name</dt> <dd>@PlaceModel.BillingAddress.Name</dd> <dt>Address 1</dt> <dd>@PlaceModel.BillingAddress.AddressLine1</dd> <dt>Address 2</dt> <dd>@PlaceModel.BillingAddress.AddressLine2</dd> <dt>City</dt> <dd>@PlaceModel.BillingAddress.City</dd> <dt>Post Code</dt> <dd>@PlaceModel.BillingAddress.PostCode</dd> </dl> </div> } @code { bool _submitted; [SupplyParameterFromForm] public OrderPlace? PlaceModel { get; set; } protected override void OnInitialized() { PlaceModel ??= GetOrderPlace(); } private void SubmitOrder() { _submitted = true; } private static OrderPlace GetOrderPlace() => new() { BillingAddress = new Address { PostCode = "12345", Name = "Test 1", }, ShippingAddress = new Address { PostCode = "67890", Name = "Test 2", }, }; }
باید بخاطر داشت که این فرم بر اساس حالت Server Side Rendering در اختیار مرورگر کاربر قرار میگیرد. یعنی برای بار اول، یک HTML خالص، در سمت سرور بر اساس اطلاعات آن تهیه شده و بازگشت داده میشود و زمانیکه به کاربر نمایش داده شد، دیگر برخلاف Blazor Server پیشین، اتصال SignalR ای وجود ندارد تا قابلیتهای تعاملی آنرا مدیریت کند. در این حالت اگر به view source صفحهی جاری رجوع کنیم، چنین خروجی قابل مشاهدهاست:
<form method="post"> <input type="hidden" name="_handler" value="checkout" /> <input type="hidden" name="__RequestVerificationToken" value="CfDxxx" /> . . . <button type="submit">Submit</button> </form>
این EditForm تعریف شده، دو قسمت اضافهتر را نسبت به EditFormهای نگارشهای قبلی Blazor دارد:
<EditForm Model="PlaceModel" method="post" OnValidSubmit="SubmitOrder" FormName="checkout">
همانطور که در نحوهی تعریف فرم HTML ای فوق مشخص است، فیلد مخفی handler_، کار متمایز ساختن این فرم را به عهده داشته و از مقدار آن در سمت سرور جهت یافتن کامپوننت متناظر، استفاده خواهد شد.
همچنین برای دریافت و پردازش این اطلاعات در سمت سرور، تنها کافی است خاصیت مرتبط با آنرا با ویژگی SupplyParameterFromForm مزین کنیم:
[SupplyParameterFromForm] public OrderPlace? PlaceModel { get; set; }
جریان کاری این فرم به صورت خلاصه به نحو زیر است (که در آن متد OnInitialized دوبار فراخوانی میشود و باید به آن دقت داشت):
- در بار اول نمایش این صفحه (با فراخوانی مسیر /checkout در مرورگر)، متد OnInitialized فراخوانی شده و در آن، مقدار شیء PlaceModel نال است.
- بنابراین به متد GetOrderPlace مراجعه کرده و اطلاعاتی را دریافت میکند؛ برای مثال، این اطلاعات را از سرویسی میخواند.
- پس از پایان هر روال رخدادگردانی در Blazor، در پشت صحنه به صورت خودکار، متد تغییر حالت جاری کامپوننت (متد StateHasChanged) هم فراخوانی میشود. این فراخوانی خودکار، باعث رندر مجدد UI آن بر اساس اطلاعات جدید خواهد شد. یعنی قسمتهای نمایش فرم و نمایش اطلاعات ارسالی، یکبار ارزیابی شده و در صورت برقراری شرطها، نمایش داده میشوند.
- در ادامه، کاربر فرم را پر کرده و به سمت سرور POST میکند.
- پیش از هر رخدادی، خواص شیء PlaceModel به علت مزین بودن به ویژگی SupplyParameterFromForm، بر اساس اطلاعات ارسالی به سرور، مقدار دهی میشوند.
- سپس متد OnInitialized فراخوانی شده و چون اینبار مقدار PlaceModel نال نیست، به متد GetOrderPlace جهت دریافت مقادیر ابتدایی خود مراجعه نمیکند. سطر تعریف شدهی در متد OnInitialized فقط زمانی سبب مقدار دهی شیء PlaceModel میشود که مقدار این شیء، نال باشد (یعنی فقط در اولین بار نمایش صفحه)؛ اما اگر این مقدار توسط پارامتر مزین شدهی به SupplyParameterFromForm به علت ارسال دادههای فرم به سرور، مقدار دهی شده باشد، دیگر به منبع دادهی ابتدایی رجوع نمیکند.
- چون متد رخدادگردان OnInitialized فراخوانی شده، پس از پایان آن (و فراخوانی خودکار متد StateHasChanged در انتهای آن)، یکبار دیگر کار رندر UI فرم جاری بر اساس اطلاعات جدید، انجام خواهد شد.
- اکنون است که پس از طی این رخدادها، متد رویدادگردان SubmitOrder فراخوانی میشود. یعنی زمانیکه این متد فراخوانی میشود، شیء PlaceModel بر اساس اطلاعات رسیدهی از طرف کاربر، مقدار دهی شده و آمادهی استفاده است (برای مثال آمادهی ذخیره سازی در بانک اطلاعاتی؛ با فراخوانی سرویسی در اینجا).
- پس از پایان فراخوانی متد رویدادگردان SubmitOrder، به علت تغییر حالت کامپوننت (و فراخوانی خودکار متد StateHasChanged در انتهای آن)، یکبار دیگر نیز کار رندر UI فرم جاری بر اساس اطلاعات جدید انجام خواهد شد. یعنی اینبار قسمت Order Summary نمایش داده میشود.
مدیریت تداخل نامهای HTML Forms در Blazor 8x SSR
تمام فرمهایی که به این صورت در برنامههای Blazor SSR مدیریت میشوند، باید دارای نام منحصربفردی که توسط خاصیت FormName مشخص میشود، باشند. برای جلوگیری از این تداخل نامها، کامپوننت جدیدی به نام FormMappingScope معرفی شدهاست که نمونهای از آنرا در فایل فرضی Components\Pages\Chekout\CheckoutForm.razor تعریف شدهی به صورت زیر مشاهده میکنید:
@page "/checkout" <FormMappingScope Name="store-checkout"> <CheckoutForm /> </FormMappingScope>
اکنون اگر برنامه را اجرا کرده و خروجی HTML آنرا بررسی کنیم، به فرم زیر خواهیم رسید:
<form method="post"> <input type="hidden" name="_handler" value="[store-checkout]checkout" /> <input type="hidden" name="__RequestVerificationToken" value="CfDxxxxx" /> . . . <button type="submit">Submit</button> </form>
یک نکته: اگر به تگهای فرم HTML ای فوق دقت کنید، به همراه یک anti-forgery token نیز هست که کار تولید و مدیریت آن، به صورت خودکار صورت میگیرد و میانافزاری نیز برای آن طراحی شده که در فایل Program.cs برنامه، به صورت app.UseAntiforgery بکارگرفته شدهاست.
یک نکته: در Blazor 8x SSR میتوان بجای EditForm، از همان HTML form متداول هم استفاده کرد
اگر بخواهیم بجای استفاده از EditForm، از فرمهای استاندارد HTML هم در حالت SSR استفاده کنیم، این کار میسر بوده و روش کار به صورت زیر است:
<form method="post" @onsubmit="SaveData" @formname="MyFormName"> <AntiforgeryToken /> <InputText @bind-Value="Name" /> <button>Submit</button> </form>
پردازش فرمهای GET در Blazor 8x
در حالتیکه از فرمهای استاندارد HTML ای استفاده میشود، ممکن است method فرم، بجای post، حالت get باشد که نتایج آن به صورت کوئری استرینگ در نوار آدرس مرورگر ظاهر میشوند؛ مانند جستجوی گوگل که اشخاص میتوانند کوئری استرینگ و لینک نهایی را به اشتراک بگذارند. روش پردازش یک چنین فرمهایی به صورت زیر است:
@page "/" <form method="GET"> <input type="text" name="q"/> <button type="submit">Search</button> </form> @code { [SupplyParameterFromQuery(Name="q")] public string SearchTerm { get; set; } protected override async Task OnInitializedAsync() { // do something with the search term } }
یک ابتکار! تعاملی کردن قسمتی از صفحه بدون فعالسازی کامل Blazor Server و یا Blazor WASM کامل
این دکمهی قرار گرفتهی در یک صفحهی SSR را ملاحظه کنید:
<button class="nav-link border-0" @onclick="BeginSignOut">Log out</button>
<EditForm Context="ctx" FormName="LogoutForm" method="post" Model="@Foo" OnValidSubmit="BeginSignOut"> <button type="submit" class="nav-link border-0">Log out</button> </EditForm> @code{ [SupplyParameterFromForm(Name = "LogoutForm")] public string? Foo { get; set; } protected override void OnInitialized() => Foo = ""; async Task BeginSignOut() { // TODO: SignOutAsync(); // TODO: NavigateTo("/authentication/logout"); } }
یک نکته: میتوان حالت post-back مانند فرمهای تعاملی Blazor 8x را تغییر داد.
به همراه ویژگیهای جدید مرتبط با صفحات SSR، ویژگی هدایت بهبودیافته هم وجود دارد که جزئیات بیشتر آنرا در قسمتهای بعدی این سری بررسی میکنیم. برای نمونه اگر مثال این قسمت را اجرا کنید، فرم آن به همراه یک post-back مانند به سمت سرور است که کاملا قابل احساس است؛ این رفتار هرچند استاندارد است، اما بیشباهت به برنامههای MVC ، Razor pages و یا وبفرمها نیست و با فرمهای بیصدا و سریع نگارشهای قبلی Blazor متفاوت است. در Blazor8x میتوان این نوع ارسال اطلاعات را Ajax ای هم کرد که به آن enhanced navigation میگویند. برای اینکار فقط کافی است ویژگی Enhance را به تگ EditForm اضافه کرد و یا ویژگی جدید data-enhance را به تگهای فرمهای استاندارد HTML ای افزود. پس از آن اگر برنامه را اجرا کنیم، دیگر یک post-back استاندارد وبفرمها مشاهده نمیشود و رفتار این صفحه بسیار سریع، نرم و روان خواهد بود.
<EditForm Model="PlaceModel" method="post" OnValidSubmit="SubmitOrder" FormName="checkout" Enhance>
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: Blazor8x-Server-Normal.zip
مقدمهای بر Docker
دو مفهوم اساسی در محیط Docker وجود دارند که دانستن آنها ضروری است: Image و Container
image عملا چیزی است که از آن برای Build یک Container استفاده میشود. image دارای یک سری فایلهای لازم و اساسی است که باعث میشود بر روی یک Operation System اجرا شود؛ مثل Ubuntu یا Windows. بنابراین شما Application Framework خود را خواهید داشت و همچنین Databaseی که با آن کار میکند. بنابراین قابلیت استفاده از زبانها و فریم ورکهای مختلف چون Asp.net Core, Nodejs, Python و غیره را خواهد داشت. یک image به خودی خود غیر قابل استفاده است تا زمانیکه بر روی یک Container توزیع شده باشد، تا قابلیت اجرا پیدا کند. بنابراین نقطهی شروع اصلی اجرایی یک برنامه با Container مربوط به آن میباشد.
به صورت خلاصه Image یک template از نوع Readonly است که ترکیبی از لایههای File System میباشد، به همراه فایلهای share شدهی دیگر (از قبیل فریم ورکها و ...) که میتوانند یک Docker Container Instance را تولید نمایند.
Container یک محیط امن و ایزوله است که به وسیلهی image ساخته شده است و میتواند اجرا، متوقف، منتقل و یا حذف شود (بطور قابل ملاحظهای اجرا کردن و متوقف کردن آن سریع میباشد).
تفاوت Docker Containers و Virtual Machines
Virtual Machines همیشه بر روی Host Operation System اجرا میشوند (که میتواند بر روی ویندوز یا لینوکس باشد) و بعد از آن اجرای Guest OS بر روی سطحی به نام Hypervisor. پس میتوان گفت یک کپی کامل از سیستم عامل است که که بر روی hypervisor اجرا میشود و خودش نیز بر روی سخت افزار اجرا میشود. بنابراین میتوان مثل شکل زیر، یک App داشت که عملا یک سری باینری و کتابخانه است و اگر قرار باشد بر روی سیستم عاملهای مختلفی کار کند، احتیاج به کپی کردن کل آن میباشد و بطور واضحی زمان و هزینهی بیشتری برای بالا آوردن آن لازم است.
اما بر خلاف آن، داکر با استفاده از ابزاری به نام Docker Engine کار میکند که میتواند Containerهای مختلفی از OSهای مختلف را اجرا نماید و نیازی به کپی گرفتن از کل سیستم عامل برای اجرای هر container نخواهد بود.
بنابراین با استفاده از ابزارهای مجازی سازی چون Vmware، نسخهی کاملی را از سیستم عامل مطبوع خود میتوان نصب و اجرا نمود؛ اما برخلاف آن با استفاده از داکر، یک نسخهی کوچک از سیستم عامل، بدون وابستگیها و پیچیدگیهای نسخهی اصلی در اختیار خواهد بود.
با این وجود، بوسیله داکر به راحتی میتوان تعداد زیادی از Containerها را به راحتی و با سرعت بالا اجرا نموده و مورد تست و ارزیابی قرار داد.
چطور Docker میتواند سریعتر از Virtual Machineها عمل کند ؟
داکر از چیزی به نام Copy On Write استفاده میکند؛ به معنای کپی کردن همزمان با نوشتن. همانطور که گفته شد هر Container از یک Image ساخته میشود و عملا Imageها همان FileSystemهای از قبل تولید شده هستند و هر کدام از لایهای از کتابخانهها استفاده میکنند که برای اجرای برنامههای کاربردی مورد استفاده قرار میگیرند. سرور آپاچی را در نظر بگیرید، به عنوان یک فایل image که FileSystem بر روی آن ذخیره شدهاست. با نصب Php یک لایه بر روی لایه دیگر ایجاد شده و فقط تغییرات جدید به آن اضافه خواهند شد و حال اگر بخواهید تغییری را بر روی source code خود بدهید، عملا فقط آن تغییر به Image و FileSystem اضافه خواهد شد. این معماری لایه لایه باعث تولید یک FileSystem بصورت read-only میشود که شامل لایههای متفاوتی است و سبب کم حجم شدن آن، بالا رفتن سرعت آن میشود و همچنین با استفاده از Caching، قدرت زیادی را بدان میبخشد.
پس همانطور که در شکل فوق مشاهده میکنید، هر image از لایههای مختلفی تشکیل شده است و توانایی به اشتراک گذاشتن این لایههای متمایز از یکدیگر در Containerها وجود دارد.
بنابراین طبق شکل فوق، بحث را اینگونه خلاصه میکنیم که هر Image از ترکیبی از لایههایی از نوع read-only تشکیل شده است و با اضافه شدن Container، عملا یک لایهی دیگری که قابلیت read/write را دارد بر روی آن اضافه میشود و درون آن source code میتواند قرار گیرد و اینکه بر مبنای شکل زیر میبینید که قابلیت به اشتراک گذاری Image layerها به Containerهای مختلف تعبیه شده است که باعث میشود لایهی نصب شده بر روی سیستم، بصورت اشتراکی قابل استفادهی مجدد باشد و فضای دیسک کمتری، به علاوه سرعت اجرای بالاتری را داشته باشد. هر لایه یک مقدار هش شدهی یکتایی را در اختیار دارد تا از لایههای دیگر تمیز داده شود و قابل شناسایی باشد.
داکر در شبکه چگونه کار میکند؟
ضمنا نکتهی قابل توجه که در مقالههای بعدی به صورت عملی به آن میپردازیم این است که با استفاده از داکر میتوانیم وب سرورهایی را بر روی Containerهای مختلفی داشته باشیم که همگی بر روی پورت بطور مثال 80 هستند؛ طوری که درون هر Container بدلیل ایزوله بودن پروسسهای مخصوص Container مربوط به خود، به پورتهای باز داخل آن شبکه دسترسی دارند و میتوانند پورت در نظر گرفته شدهی درون Container را با پورت دیگری بیرون Container به اصطلاح Expose نمایند.
ضمن اینکه نکتهی دیگری که وجود دارد، ارتباط Containerها با یکدیگر است. برای مثال یک Container برای Database و دیگری برای WebApp میباشد که باید به همدیگر link شده تا قابل استفاده گردند و عملا نیازی به نوشتن ip یکدیگر در این حالت وجود ندارد. البته راههای دیگری از قبیل Compose کردن نیز وجود دارد که در ادامه بیشتر با آنها آشنا خواهیم شد.
Docker Volume چیست؟
بحث دیگری که وجود دارد، Volumeها هستند که قسمتی از FileSystemها میباشند و بصورت ساده، مثال کاربردیاش میتواند قسمتی از یک سیستم و دایرکتوری خاصی را بر روی Container خاصی Map کردن باشد و عملا داخل آن دایرکتوری میتواند source code بوده باشد (یکی از راههای ممکن برای map کردن source code به container) و بر روی Container ایجاد شود.
فوایدی که با استفاده از Volumeها میتوان به آن رسید از قبیل موارد زیر میباشند:
قابلیت به اشتراک گذاری یک Volume بین Containerهای مختلف که به شدت میتواند قابل استفاده باشد.
Data Volumeها ماندگار هستند. یعنی حتی بعد از اینکه Container مربوطه را حذف نمایید، volume مربوط به آن بطور اتوماتیک حذف نمیشود (مگر اینکه خودتان دستور حذف کردن آن را وارد نمایید). پس عملا قابلیت استفادهی مجدد را نیز خواهد داشت.
طبق شکل فوق ما میتوانیم درون یک container یک volume داشته باشیم. وقتی ما چیزی را درون آن مینویسیم عملا داریم در قسمت خاصی به نام Docker Host عمل write کردن را انجام میدهیم که باعث میشود داکر متوجه آن شود. وقتی اسمی را به یک Volume انتساب میدهیم همانند /var/www، در واقع یک اسم مستعار (alias) میباشد که اشاره میکند به این Docker host موجود. در ادامه بیشتر با Volumeها آشنا خواهیم شد.
DockerFile و ساخت imageها چگونه است؟
روش دیگر برای اجرای source code در داکر، ساخت یک image اختصاصی از آن و اجرا کردن آن بر روی یک container مجزا است. با استفاده از DockerFile میتوانید imageهای خود را build کرده که عملا هر image در آخر باید به یک سیستم عامل برسد و همانطور که گفته شد به صورت لایهای کار میکنند و مراتب اجرای آن از قبیل working directory و expose کردن بر روی پورتی خاص، همچنین استفاده از Environment Variableها میباشد و همچنین با استفاده از DockerHub (که نسخهی enterprise نیز دارد) میتوان imageهای ساخته شده را بر روی cloud نگه داشت و همهی اعضای تیم از یک image بخصوص استفاده کنند؛ برای مثال همهی اعضای تیم از یک نسخهی Nodejs استفاده کنند و اشتباها بر روی ماشینهای توسعهی مختلف برنامه نویسان، از نسخههای مختلفی استفاده نشود و همچنین روند بهروز رسانی به سادگی انجام گیرد.
مزایای Docker برای برنامه نویسان
فرض کنید که یک App Service از Azure تهیه کرده باشید. تستهای unit, integration, acceptance را انجام داده و با خیال راحت Container خود را از طریق برای مثال Visual studio team service بر روی App service به صورت انتشار از طریق مدل Continuous Integration و Continuous Deployment داشته باشید. پس عملا داکر به Devops بودن محیط و چابک بودن تیم توسعه کمک شایانی کرده و فرآیندهای سخت و زمانبر انتقال Codeها از محیط توسعه به محیط انتشار را تسریع میبخشد.
بنابراین از داکر به راحتی میتوان در محیط Production نیز استفاده کرد و مزایای فوق العاده ای را برای برنامه نویسان ارائه کرده است. بطور مثال فرض کنید در تولید نرمافزار یک Web server ، تعدادی Database و یک Caching server که کانفیگ کردن، اجرا و ... به صورت عادی بسیار صعب و مشکل ساز بوده را به راحتی میتوان اجرا نمود. ضمن اینکه ممکن است هر کدام از ابزارهایی که استفاده شده، فقط مخصوص سیستم عاملی خاص باشد که قاعدتا احتیاج به بالا آوردن Virtual Machine خواهید بود و در سناریوهای خاصی مثل سیستم هایی با معماری Microservice که هر کدام از این ریز سرویسها ممکن است زبان، فریم ورک، دیتابیس و ... مخصوص به خود را داشته باشند، عملا کار بسیار سخت و پر هزینه خواهد بود (ضمن اینکه استفادهی همزمان از چند Virtual Machine در کنار هم در محیط توسعه، حجم زیادی از memory و disk سیستم شما را خواهد گرفت و شما را مجبور به ارتقای سیستم خود خواهد کرد!).
مشکل دیگری که Docker آن را حل کرده، Conflictهای ورژنهای مختلف ابزارهای مورد استفاده است. به راحتی میتوان Containerی از Imageها را به صورت ایزوله با ورژنهای مختلفی ایجاد کرد تا بطور کامل برنامه نویسان را از مشکل همیشگی بهروزرسانیها و Role-back کردنها آسوده خاطر نماید.
از آنجایی که داکر قابلیت اجرای در محیط production را نیز دارد، عملا محیط Development با محیط Production تفاوتی ندارد و این جملهی معروف که «در سیستم من کار میکند اما در نسخهی انتشار داده شده خیر» دیگر اتفاق نخواهد افتاد.
به راحتی میتوانید از یک Image خاص، Containerهای ایزولهی متفاوتی را ساخته و همگی آنها را در کنار هم اجرا نمود و مورد تست و ارزیابی قرار داد.
Dokcer hub
مخرنی است از هزاران Image آماده از قبیل سیستم عامل، فریم ورک و... که قابلیت استفادهی مجدد خواهد داشت. همچنین شما میتوانید Imageهای خود را نیز بدان اضافه نموده تا دیگران از آن استفاده نمایند. استفاده از مخزنهای public آن رایگان میباشد. از آنجایی که Docker یک محصول متن باز و رایگان است، یک بخش از درآمدهای آن از فروش اختصاصی مخزنها در DokcerHub میباشد (چیزی شبیه به Private Repository در Github).
بیشتر از این به مفاهیم نمیپردازیم. برای مطالعهی بیشتر، کتاب فوق العادهی Mastering Docker را پیشنهاد میکنم.
شروع به کار با Docker
بعد از نصب کردن نسخهی رسمی Docker و باز کردن ترمینال مربوطه، اولین دستوراتی را که باید با آن آشنا باشیم، شامل موارد زیر میباشد:
لیست Imageهای کش شدهی بر روی سیستم:
docker images
docker ps
برای آزمایش کردن و نصب اولین image، دستور زیر را وارد میکنیم (میتوانید اطلاعات بیشتری از imageها را در dockerHub پیدا کنید). من در اینجا kitematic/hello-world-nginx را به عنوان image از مخزن dokcerhub، بر روی سیستم خود pull کردهام (این یک نسخهی بسیار سبک از کانتینر nginx میباشد).
docker pull kitematic/hello-world-nginx
حال وقت اجرای این image و توزیع آن بر روی container میباشد که با استفاده از دستور زیر است:
docker run -p 80:80 kitematic/hello-world-nginx
بعد از اجرای این دستور میتوانید با وارد کردن ip مربوط به virtual machine ساخته شده بر روی سیستم خود (اگر از مک یا ویندوز استفاده میکنید احتمالا 192.168.99.100 خواهد بود) که البته با دستور docker-machine ip میتوانید آن را پیدا کنید و وارد کردن آن بر روی مرورگر خود، تصویری مثل زیر را مشاهده کنید:
بدین معناست که container شما اجرا شده و قابلیت مورد استفاده قرار گرفتن را خواهد داشت. حال اگر دستور docker ps را مجددا وارد نمایید، اطلاعات این container را از نوع id, status port و غیره، مشاهده خواهید کرد.
در اکثر اوقات نتیجه کار مایوس کننده، بسیار سخت و نگهداری آن در طول زمان بسیار مشکل خواهد بود؛ به علاوه سازگاری با مرورگرهای مختلف و نکات ریز هر کدام را نیز لحاظ کنید. به همین جهت تعدادی فریم ورک CSS برای شبیه سازی گرید و جدول تهیه شدهاند که کار طراحی table less را بسیار ساده و لذت بخش کردهاند. یکی از این موارد، فریم ورک Blueprint CSS نام دارد و در ادامه نحوه استفاده از آنرا مرور خواهیم کرد. این مرور هم مستقل است از فناوری سمت سرور مورد استفاده و صرفا مباحث html و CSS آن بررسی خواهند شد.
دریافت Blueprint CSS
این فریم ورک سورس باز را از مخزن کدهای آن در GitHub میتوانید دریافت کنید: (^)
البته نگران حجم نزدیک به 4 مگابایتی بسته دریافتی آن نباشید؛ زیرا نهایتا با سه فایل CSS از آن بیشتر کاری نداریم و مابقی مثالهای آن هستند.
پس از دریافت آن، یک پوشه را به نام blueprint ایجاد کرده و سه فایل ie.css ،print.css و screen.css را در آن قرار دهید.
به علاوه داخل این پوشه، یک پوشه جدید دیگر را به نام src ایجاد کرده و فایل grid.png موجود در این بسته را نیز در آن کپی کنید.
ساختار ابتدایی یک صفحه مبتنی بر Blueprint CSS
پس از ایجاد پوشه blueprint و src به نحوی که توضیح داده شد، ابتداییترین ساختار یک صفحه تشکیل شده با blueprint css به نحو زیر است:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Blueprint test page</title> <!-- Framework CSS --> <link rel="stylesheet" href="blueprint/screen.css" type="text/css" media="screen, projection"> <link rel="stylesheet" href="blueprint/print.css" type="text/css" media="print"> <!--[if lt IE 8]><link rel="stylesheet" href="blueprint/ie.css" type="text/css" media="screen, projection"><![endif]--> </head> <body> <div class="container showgrid"> test <hr class="space" /> <hr class="space" /> <hr class="space" /> <hr class="space" /> <hr class="space" /> test </div> </body> </html>
پس از مشخص سازی DocType (مهم)، سه فایل CSS یاد شده به header صفحه اضافه خواهند شد. همانطور که ملاحظه میکنید، سازگاری با IE نیز مدنظر آن بوده است.
کار با blueprint css همواره داخل div زیر انجام میشود:
<div class="container"> page </div>
اگر علاقمند باشید که این گرید را مشاهده نمائید و همچنین بتوانید ستونهای آنرا نیز شمارش کنید، تنها کافی است showgrid را به این class تعریف شده اضافه نمائید (همانند ساختار صفحه فوق). به این ترتیب شکل زیر نمایان خواهد شد:
مطابق شکل فوق، در این عرض مشخص، 24 ستون آن در اختیار ما خواهند بود.
به علاوه ذکر hr با class=space سبب خواهد شد تا مطابق تنظیمات و فاصله بندی منظم این فریم ورک، یک سطر خالی برای ما ایجاد شود.
طراحی بدون جدول با Blueprint CSS
در ادامه قصد داریم در این صفحه ابتدایی، یک جدول با دو ستون و دو ردیف را ایجاد کنیم:
<body> <div class="container showgrid"> <div class="span-12"> row1-col1 </div> <div class="span-12 last"> row1-col2 </div> <div class="span-12"> row2-col1 </div> <div class="span-12 last"> row2-col2 </div> </div> </body>
توضیحات:
ستونهای گرید نهایی با رنگ آبی مشخص هستند (class=container showgrid). اگر نیاز به 12 ستون داریم، مینویسیم span-12 و ... همین! به این ترتیب یک سلول جدول، با 12 ستون در اختیار ما خواهد بود. سلول بعدی هم در اینجا 12 ستونه است. اما یک last را اضافهتر دارد. در span-12 last این last به معنای انتهای ردیف جاری است و ذکر آن الزامی است.
تا اینجا یک ردیف تمام شد. اکنون در ادامه ردیف دوم را نیز به همین ترتیب با دو div و classهایی که ملاحظه میکنید، مشخص خواهیم کرد.
نحوه کار کلی با Blueprint css به همین سادگی است که ملاحظه میکنید. تعداد ستونهای مورد نیاز را با ذکر container showgrid به سادگی میتوان شمارش کرد. سپس این اعداد شمارش شده و مد نظر را پس از span ذکر کنید. مثلا اگر یک طرح سه ستونه نیاز دارید به صورت زیر خواهد بود:
<body> <div class="container showgrid"> <div class="span-8"> row1-col1 </div> <div class="span-8"> row1-col2 </div> <div class="span-8 last"> row1-col3 </div> </div> </body>
طراحی سلولهای تو در تو
سؤال: ما پیشتر در یک html table به سادگی میتوانستیم داخل یک سلول آن حتی یک جدول جدید نیز قرار دهیم، اینجا چطور؟
پاسخ: در اینجا هم بجای td و tr و table، از divهای تو در تو استفاده کنید. بستن ستون آخر را با last یاد شده فراموش نکنید. مثلا:
<body> <div class="container showgrid"> <div class="span-8"> <div class="span-4"> row-1, col1 : cell-1 </div> <div class="span-4 last"> row-1, col1 : cell-2 </div> </div> <div class="span-8"> row1-col2 </div> <div class="span-8 last"> row1-col3 </div> </div> </body>
سایر امکانات Blueprint CSS
تا اینجا با کلیات نحوه طراحی یک جدول به کمک CSS و فریم ورک Blueprint CSS آشنا شدیم (به کمک container و span-n آن). در ادامه مرور سریعی خواهیم داشت بر سایر امکانات این فریم ورک CSS و منظور از این امکانات، کلمات و عبارات مجازی است که میتوانید داخل classهای divهای تعریف شده اضافه نمائید (CSS selectors تعریف شده در آن):
prepend-n و border:
فرض کنید در divهای تو در توی قسمت قبل، قصد داریم عرض ستون اول را بجای 4 ستون به 3 ستون تبدیل کنیم، اما این div را یک ستون به سمت راست حرکت دهیم:
<body> <div class="container showgrid"> <div class="span-8"> <div class="prepend-1 span-3 border"> row-1, col1 : cell-1 </div> <div class="span-4 last"> row-1, col1 : cell-2 </div> </div> <div class="span-8"> row1-col2 </div> <div class="span-8 last"> row1-col3 </div> </div> </body>
شبیه به همین قابلیت، با append-x (افزودن تعدادی ستون به سمت راست)، prepend-top (فاصلهای به اندازه 1.5em را به بالای div اضافه میکند) و append-bottom (فاصلهای به اندازه 1.5em را به پایین div اضافه میکند) نیز وجود دارد.
در مقابل اینها، push-n و pull-n هم وجود دارند. کار append و prepend اضافه کردن چند ستون به بعد و قبل از یک div است. push یک div را به تعداد واحدی که مشخص میکنیم به سمت راست حرکت میدهد. pull یک div را n ستون به سمت چپ حرکت خواهد داد (بدون تغییری در تعداد ستونها).
دریافت مرجع سریع Blueprint CSS
ایجاد یک پروژهی ابتدایی Blazor WASM
پروژهای را که در این مطلب تکمیل خواهیم کرد، از نوع Blazor WASM هاست شدهاست. بنابراین در پوشهی فرضی BlazorAsyncValidation، دستور «dotnet new blazorwasm --hosted» را صادر میکنیم تا ساختار ابتدایی پروژه که به همراه یک کلاینت Blazor WASM و یک سرور ASP.NET Core Web API است، تشکیل شود. از قسمت Web API، برای پیاده سازی اعتبارسنجی سمت سرور استفاده خواهیم کرد.
مدل ثبت نام برنامه
مدل ثبت نام برنامه تنها از یک خاصیت نام تشکیل شده و در پروژهی Shared قرار میگیرد تا هم در کلاینت و هم در سرور قابل استفاده باشد:
using System.ComponentModel.DataAnnotations; namespace BlazorAsyncValidation.Shared; public class UserDto { [Required] public string Name { set; get; } = default!; }
کنترلر API ثبت نام برنامه
کنترلر زیر که در پوشهی BlazorAsyncValidation\Server\Controllers قرار میگیرد، منطق بررسی تکراری نبودن نام دریافتی از برنامهی کلاینت را شبیه به منطق remote validation استاندارد MVC، پیاده سازی میکند که در نهایت یک true و یا false را باز میگرداند.
در اینجا خروجی بازگشت داده کاملا در اختیار شما است و نیازی نیست تا حتما ارتباطی با MVC داشته باشد؛ چون مدیریت سمت کلاینت بررسی آنرا خودمان انجام خواهیم داد و نه یک کتابخانهی از پیش نوشته شده و مشخص.
using BlazorAsyncValidation.Shared; using Microsoft.AspNetCore.Mvc; namespace BlazorAsyncValidation.Server.Controllers; [ApiController, Route("api/[controller]/[action]")] public class RegisterController : ControllerBase { [HttpPost] public IActionResult IsUserNameUnique([FromBody] UserDto userModel) { if (string.Equals(userModel?.Name, "Vahid", StringComparison.OrdinalIgnoreCase)) { return Ok(false); } return Ok(true); } }
غنی سازی فرم استاندارد Blazor جهت انجام Remote validation
اگر بخواهیم از EditForm استاندارد Blazor در حالت متداول آن و بدون هیچ تغییری استفاده کنیم، مانند مثال زیر که InputText متصل به خاصیت Name مربوط به Dto برنامه را نمایش میدهد:
@page "/" <PageTitle>Index</PageTitle> <h2>Register</h2> <EditForm EditContext="@EditContext" OnValidSubmit="DoSubmitAsync"> <DataAnnotationsValidator/> <div class="row mb-2"> <label class="col-form-label col-lg-2">Name:</label> <div class="col-lg-10"> <InputText @bind-Value="Model.Name" class="form-control"/> <ValidationMessage For="@(() => Model.Name)"/> </div> </div> <button class="btn btn-secondary" type="submit">Submit</button> </EditForm>
public partial class Index { private const string UserValidationUrl = "/api/Register/IsUserNameUnique"; private ValidationMessageStore? _messageStore; [Inject] private HttpClient HttpClient { set; get; } = default!; private EditContext? EditContext { set; get; } private UserDto Model { get; } = new();
ValidationMessageStore به همراه متد Add است و اگر به آن نام فیلد مدنظر را به همراه پیامی، اضافه کنیم، این اطلاعات را به صورت خطای اعتبارسنجی توسط کامپوننت ValidationMessage نمایش میدهد.
محل مقدار دهی اولیهی این اشیاء نیز در روال رویدادگردان OnInitialized به صورت زیر است:
protected override void OnInitialized() { EditContext = new EditContext(Model); _messageStore = new ValidationMessageStore(EditContext); EditContext.OnFieldChanged += (sender, eventArgs) => { var fieldIdentifier = eventArgs.FieldIdentifier; _messageStore?.Clear(fieldIdentifier); _ = InvokeAsync(async () => { var errors = await OnValidateFieldAsync(fieldIdentifier.FieldName); if (errors?.Any() != true) { return; } foreach (var error in errors) { _messageStore?.Add(fieldIdentifier, error); } EditContext.NotifyValidationStateChanged(); }); StateHasChanged(); }; EditContext.OnValidationStateChanged += (sender, eventArgs) => StateHasChanged(); EditContext.OnValidationRequested += (sender, eventArgs) => _messageStore?.Clear(); }
برای مثال اگر فیلدی تغییر کند، رویداد OnFieldChanged صادر میشود. در همینجا است که کار فراخوانی متد OnValidateFieldAsync که در ادامه معرفی میشود را انجام میدهیم تا کار اعتبارسنجی Async سمت سرور را انجام دهد. اگر نتیجهای به همراه آن بود، توسط messageStore به صورت یک خطای اعتبارسنجی نمایش داده خواهد شد و همچنین EditContext نیز با فراخوانی متد NotifyValidationStateChanged، وادار به بهروز رسانی وضعیت اعتبارسنجی خود میگردد.
متد سفارشی OnValidateFieldAsync که کار اعتبارسنجی سمت سرور را انجام میدهد، به صورت زیر تعریف شدهاست:
private async Task<IList<string>?> OnValidateFieldAsync(string fieldName) { switch (fieldName) { case nameof(UserDto.Name): var response = await HttpClient.PostAsJsonAsync( UserValidationUrl, new UserDto { Name = Model.Name }); var responseContent = await response.Content.ReadAsStringAsync(); if (string.Equals(responseContent, "false", StringComparison.OrdinalIgnoreCase)) { return new List<string> { $"`{Model.Name}` is in use. Please choose another name." }; } // TIP: It's better to use the `DntDebounceInputText` component for this case to reduce the network round-trips. break; } return null; }
یک نکته: InputText استاندارد در حالت معمول آن، پس از تغییر focus به یک کنترل دیگر، سبب بروز رویداد OnFieldChanged میشود و نه در حالت فشرده شدن کلیدها. به همین جهت اگر برنامه پیوستی را میخواهید آزمایش کنید، نیاز است فقط focus را تغییر دهید و یا یک کنترل سفارشی را برای اینکار توسعه دهید.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorAsyncValidation.zip
دوره 3 ساعته NET MAUI.
.NET MAUI Course for Beginners – Create Cross-Platform Apps with C#
Learn how to use .NET MAUI for native cross-platform desktop and mobile development! You will learn the essentials of building mobile applications with .NET MAUI and C# while creating a Contacts app.
⭐️ Contents ⭐️
⌨️ (0:00:00) Introduction
⌨️ (0:03:42) What is .Net Maui - .Net Maui vs Xamarin Forms
⌨️ (0:06:52) Prepare Development Environment _ Create first project
⌨️ (0:12:29) Project Structure
⌨️ (0:20:28) Three elements of stateful .Net Maui
⌨️ (0:23:51) Page, Layout _ View, Namespaces
⌨️ (0:33:02) URL based navigation
⌨️ (0:51:10) Basics of ListView and Data Binding
⌨️ (1:05:58) Events Handling of ListView
⌨️ (1:16:54) Parameters in URL based Navigation _ Static Repository
⌨️ (1:35:35) Stacklayout for Edit Contact page
⌨️ (1:52:47) View Contact Details _ Update Contact
⌨️ (2:06:40) Observable Collection
⌨️ (2:14:58) Field Validation with .Net Maui CommunityToolkit
⌨️ (2:27:08) Reusable Control
⌨️ (2:40:37) Grid Layout and Use reusable control
⌨️ (2:53:23) ContextActions _ MenuItems in ListView
⌨️ (3:03:44) SearchBar in .NetMaui