بررسی تغییرات Blazor 8x - قسمت چهارم - معرفی فرمهای جدید تعاملی
نویسنده: وحید نصیری
تاریخ: ۱۴۰۲/۰۸/۲۴ ۸:۳۵
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
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!;
} @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> @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",
},
};
} <form method="post"> <input type="hidden" name="_handler" value="checkout" /> <input type="hidden" name="__RequestVerificationToken" value="CfDxxx" /> . . . <button type="submit">Submit</button> </form>
<EditForm Model="PlaceModel" method="post" OnValidSubmit="SubmitOrder" FormName="checkout">
[SupplyParameterFromForm]
public OrderPlace? PlaceModel { get; set; } @page "/checkout"
<FormMappingScope Name="store-checkout">
<CheckoutForm />
</FormMappingScope> <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>
<form method="post" @onsubmit="SaveData" @formname="MyFormName">
<AntiforgeryToken />
<InputText @bind-Value="Name" />
<button>Submit</button>
</form> @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
}
} <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");
}
} <EditForm Model="PlaceModel" method="post" OnValidSubmit="SubmitOrder" FormName="checkout" Enhance>
یک نکتهی تکمیلی: نحوهی نامگذاری ویژهی عناصر در فرمهای جدید Blazor SSR
اگر با نگارشهای دیگر Blazor کار کرده باشید، عموما یک EditForm را به صفحه اضافه کرده و چند المان را به آن اضافه میکنیم و ... کار میکند. حتی اگر کامپوننتهای سفارشی را هم بر این مبنا تهیه کنیم ... بازهم بدون نکتهی خاصی کار میکنند. اما ... در برنامههای Blazor SSR اینطور نیست! زمانیکه برای مثال مدل فرم را به این صورت تعریف میکنیم:
[SupplyParameterFromForm]
public OrderPlace? MyModel { get; set; }و آنرا به نحو متداولی در صفحه نمایش میدهیم:
<InputText @bind-Value="MyModel.City"/>
اگر به المان رندر شدهی در مرورگر مراجعه کنیم، ویژگی name حاصل، با MyModel.City مقدار دهی شدهاست و ... این موضوع درج نام خاصیت مدل (و یا اصطلاحا Html Field Prefix)، برای Blazor SSR بسیار مهم است! تاحدی که اگر از آن آگاه نباشید، ممکن است ساعتی را مشغول دیباگ برنامه شوید که چرا، مقدار نالی را دریافت کردهاید و یا عناصر تعریف شدهی در کامپوننتهای سفارشی، کار نمیکنند و مقدار نمیگیرند!
متاسفانه API بازگشت نام کامل عناصری که توسط Blazor SSR تولید میشود، عمومی نیست و internal است. اگر از کامپوننتهای استاندارد خود Blazor استفاده میکنید، نیازی نیست تا به این موضوع فکر کنید و مدیریت آن خودکار است؛ اما همینکه قصد تولید کامپوننتهای سفارشی مخصوص SSR را داشته باشید، اولین مشکلی را که با آن مواجه خواهید شد، دقیقا همین مسالهی تولید صحیح HtmlFieldPrefixها است.
برای رفع این مشکل و دسترسی به API پشت صحنهی تولید نام فیلدها در Blazor SSR، میتوان از کامپوننت پایهی InputBase خود Blazor ارثبری کرد و به این ترتیب به خاصیت جدید NameAttributeValue آن دسترسی یافت (این خاصیت به داتنت 8 و مخصوص Blazor SSR، اضافه شدهاست) که اینکار در کلاس BlazorHtmlField انجام شدهاست. روش استفادهی از آن هم به صورت زیر است:
private BlazorHtmlField<T?> ValueField
=> new(ValueExpression ?? throw new InvalidOperationException(message: "Please use @bind-Value here."));
[Parameter] public T? Value { set; get; }
[Parameter] public EventCallback<T?> ValueChanged { get; set; }
[Parameter] public Expression<Func<T?>> ValueExpression { get; set; } = default!;زمانیکه میخواهیم در یک کامپوننت سفارشی، خاصیتی bind پذیر را طراحی کنیم، روش کار آن، مانند مثال فوق است که به همراه یک خاصیت، یک EventCallback و یک Expression است تا اعتبارسنجی و انقیاد دوطرفه را فعال کند. اما ... اگر همین Value را مستقیما در فیلدهای کامپوننت استفاده کنیم ... مقدار نمیگیرد؛ چون به همراه نام کامل خاصیت بایند شدهی به آن نیست. برای مثال بجای MyModel.City فقط City درج میشود (که به علت نداشتن .MyModel، سیستم binding از مقدار آن صرفنظر میکند). اکنون با استفاده از BlazorHtmlField فوق، میتوان به نام کامل تولیدی توسط Blazor SSR دسترسی یافت و از آن استفاده کرد:
<input type="text" dir="ltr"
name="@ValueField.HtmlFieldName"
id="@ValueField.HtmlFieldName" />HtmlFieldName ای که در اینجا درج میشود، توسط خود Blazor محاسبه شده و با انتظارات موتور binding آن تطابق دارد و دیگر به خواص بایند شدهای که مقدار نمیگیرند، نخواهیم رسید.
یک نکتهی تکمیلی: روش طراحی binding دو طرفه در Blazor SSR
در نکتهی قبل عنوان شد که مقدمات طراحی binding دو طرفه، داشتن حداقل سه خاصیت زیر در یک کامپوننت سفارشی است:
[Parameter] public T? Value { set; get; }
[Parameter] public EventCallback<T?> ValueChanged { get; set; }
[Parameter] public Expression<Func<T?>> ValueExpression { get; set; } = default!;اگر این خواص را به کامپوننتهای توکار خود Blazor متصل کنیم (مانند InputBox آن و مابقی آنها)، نیازی به کدنویسی بیشتری ندارند و کار میکنند. اما اگر قرار است از یک input سادهی Html ای استفاده کنیم، نیاز است ValueChanged آنرا اینبار در متد OnInitialized فراخوانی کنیم؛ چون در زمان post-back به سرور است که مقدار آن در اختیار مصرف کنندهی کامپوننت قرار میگیرد. این مورد، مهمترین تفاوت نحوهی طراحی binding دوطرفه در Blazor SSR با مابقی حالات و نگارشهای Blazor است.
بررسی وقوع post-back به سرور به دو روش زیر میسر است:
الف) بررسی کنیم که آیا HttpPost ای رخدادهاست؟ سپس در همین لحظه، متد ValueChanged.InvokeAsync را فراخوانی کنیم:
[CascadingParameter] internal HttpContext HttpContext { set; get; } = null!;
protected override void OnInitialized()
{
base.OnInitialized();
if (HttpContext.IsPostRequest())
{
SetValueCallback(Value);
}
}
private void SetValueCallback(string value)
{
if (!ValueChanged.HasDelegate)
{
return;
}
_ = ValueChanged.InvokeAsync(value);
}در این مثال نحوهی فعالسازی ارسال اطلاعات از یک کامپوننت سفارشی را به مصرف کنندهی آن ملاحظه میکنید. اینکار در قسمت OnInitialized و فقط در صورت ارسال اطلاعات به سمت سرور، فعال خواهد شد.
ب) میتوان در قسمت OnInitialized، بررسی کرد که آیا درخواست جاری به همراه اطلاعات یک فرم ارسال شدهی به سمت سرور است یا خیر؟ روش کار به صورت زیر است:
protected override void OnInitialized()
{
base.OnInitialized();
if (HttpContext.Request.HasFormContentType &&
HttpContext.Request.Form.TryGetValue(ValueField.HtmlFieldName, out var data))
{
SetValueCallback(data.ToString());
}
}در اینجا از ValueField.HtmlFieldName که در نکتهی قبلی معرفی BlazorHtmlField به آن اشاره شد، جهت یافتن نام واقعی فیلد ورودی، استفاده شدهاست.