پیاده سازی Remote Validation در Blazor
نویسنده: وحید نصیری
تاریخ: ۱۴۰۱/۰۱/۱۵ ۱۳:۴۵
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
using System.ComponentModel.DataAnnotations;
namespace BlazorAsyncValidation.Shared;
public class UserDto
{
[Required] public string Name { set; get; } = default!;
} 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);
}
}
@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(); 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();
} 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;
} <input @bind="username" /> <InputText @bind-Value="Model.Name" />
RZ10008 The attribute 'onchange' is used two or more times for this element. Attributes must be unique (case-insensitive). The attribute 'onchange' is used by the '@bind' directive attribute.
<input value="@username" @onchange="CheckUsername" />
<InputText Value="@Model.Name" ValueExpression="()=>Model.Name" ValueChanged="(string s)=>CheckUsername(s)" /> <ValidationMessage For="() => Model.Name" />
<input @bind="username" @bind:after="CheckUsername" />
<InputText @bind-Value="Model.Name" @bind-Value:after="CheckUsername" /> <ValidationMessage For="() => Model.Name" />
async Task CheckUsername()
{
if (!string.IsNullOrWhiteSpace(Model.Name))
{
_messageStore?.Clear(EditContext.Field(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))
{
_messageStore?.Add(EditContext.Field(nameof(UserDto.Name)),
$"`{Model.Name}` is in use. Please choose another name.");
}
EditContext.NotifyValidationStateChanged();
}
} EditForm Submit) اکنون به صورت سرتاسری (End-to-End) منتظر به پایان رسیدن فرآیند اعتبارسنجیهای ناهمگام میماند.EditContext.AddValidationTask ثبت کنند. فریمورک Blazor به صورت هوشمندانه این وظایف را زیر نظر میگیرد و مدیریت میکند.IsValidationPending(field) (برای بررسی اینکه آیا اعتبارسنجی در حال انجام است) و IsValidationFaulted(field) (برای تشخیص خطاهای سیستمی حین اعتبارسنجی) را در اختیار توسعهدهنده قرار میدهد.DataAnnotations در پیشنمایشهای بعدی داتنت منتشر شوند. پس از انتشار، کامپوننت محبوب DataAnnotationsValidator به طور کامل و خودکار از این ویژگی پشتیبانی خواهد کرد.UserService) بررسی میشود:<EditForm EditContext="editContext" OnSubmit="HandleSubmit">
<div>
<label>نام کاربری:</label>
<InputText @bind-Value="model.Username" />
@* نمایش وضعیت در حال بررسی به کاربر *@
@if (editContext.IsValidationPending(() => model.Username))
{
<span class="text-muted">در حال بررسی در دسترس بودن نام کاربری...</span>
}
</div>
<ValidationMessage For="() => model.Username" />
<button type="submit">ثبت نام</button>
</EditForm>
@code {
[Inject] public UserService Users { get; set; } = default!;
private readonly RegistrationModel model = new();
private EditContext editContext = default!;
private ValidationMessageStore messages = default!;
protected override void OnInitialized()
{
editContext = new EditContext(model);
messages = new ValidationMessageStore(editContext);
// گوش دادن به تغییرات فیلدها برای اعتبارسنجی آنی
editContext.OnFieldChanged += (_, e) =>
{
if (e.FieldIdentifier.FieldName == nameof(model.Username))
{
// استفاده از CancellationTokenSource برای لغو درخواستهای منقضی شده
var cts = new CancellationTokenSource();
editContext.AddValidationTask(
e.FieldIdentifier,
CheckAsync(e.FieldIdentifier, model.Username, cts.Token),
cts);
}
};
}
private async Task CheckAsync(FieldIdentifier field, string value, CancellationToken ct)
{
// پاک کردن پیامهای خطای قبلی مربوط به این فیلد
messages.Clear(field);
try
{
// شبیهسازی یا اجرای درخواست واقعی به API/پایگاهداده
if (await Users.IsUsernameTakenAsync(value, ct))
{
messages.Add(field, "این نام کاربری قبلاً انتخاب شده است.");
}
}
catch (OperationCanceledException)
{
// درخواست توسط فریمورک لغو شده است؛ نیاز به اقدام خاصی نیست.
}
// مطلع کردن EditContext برای بهروزرسانی رابط کاربری (UI)
editContext.NotifyValidationStateChanged();
}
// اطمینان از اتمام اعتبارسنجیها در زمان ثبت نهایی فرم
private async Task HandleSubmit() => await editContext.ValidateAsync();
}OnFieldChanged با هر بار فشردن کلید توسط کاربر، یک رویداد متد اجرا میشود. اگر کاربر کلمه "Admin" را تایپ کند، ۵ درخواست به سمت سرور روانه میشود. ارسال CancellationToken به متد AddValidationTask این امکان را به Blazor میدهد که ۴ درخواست اول را به محض تغییر ورودی ابطال (Cancel) کند و فقط منتظر پاسخ درخواست آخر بماند. این کار بار پردازشی سرور را به شدت کاهش میدهد.editContext.IsValidationPending به راحتی میتوان دکمه ثبت فرم را تا زمان اتمام اعتبارسنجی غیرفعال (Disable) کرد یا یک انیمیشن بارگذاری (Loading Spinner) نشان داد تا کاربر متوجه پردازش اطلاعات در پشت صحنه بشود.