عنوان:

‫پیاده سازی Remote Validation در Blazor


نویسنده: وحید نصیری
تاریخ: ۱۴۰۱/۰۱/۱۵ ۱۳:۴۵
آدرس: www.dntips.ir
فرم‌های Blazor به همراه پشتیبانی از ویژگی Remote که به همراه ASP.NET Core ارائه می‌شود، نیستند. هرچند می‌توان در حین ارسال فرم به سرور، نتیجه‌ی اعتبارسنجی از راه دور و سمت سرور را به کاربر نمایش داد، اما تجربه‌ی کاربری آن در حد Remote validation نیست. یعنی می‌خواهیم در حین ورود اطلاعات و یا انتقال focus به کنترل دیگری، اعتبارسنجی سمت سرور صورت گیرد و نه فقط در زمان ارسال کل اطلاعات به سرور، در پایان کار. در این مطلب روشی را جهت پیاده سازی این عملیات بررسی خواهیم کرد.


ایجاد یک پروژه‌ی ابتدایی 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 زیر را برای آن تدارک خواهیم دید.


کنترلر 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>
 تنها رخ‌دادی که در اختیار ما قرار می‌گیرد، رخ‌داد submit (در حالت موفقیت اعتبارسنجی سمت کلاینت و یا تنها submit معمولی) است. بنابراین برای دسترسی به رخ‌دادهای بیشتر EditForm، نیاز است با EditContext آن کار کنیم:
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();
به همین جهت EditContext را در سطح کامپوننت جاری تعریف کرده و همچنین سرویس HttpClient را جهت ارسال اطلاعات Name و دریافت پاسخ true/false از سرور و یک ValidationMessageStore را برای نگهداری تعاریف خطاهای سفارشی که قرار است در فرم نمایش داده شوند، معرفی می‌کنیم.
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();
    }
در اینجا ابتدا یک نمونه‌ی جدید از EditContext، بر اساس Model فرم، تشکیل می‌شود. سپس ValidationMessageStore سفارشی که قرار است خطاهای اعتبارسنجی remote ما را نمایش دهد نیز نمونه سازی می‌شود. در ادامه امکان دسترسی به رخ‌دادهای OnFieldChanged ، OnValidationStateChanged و OnValidationRequested را خواهیم داشت که در حالت معمولی کار با EditForm میسر نیستند.
برای مثال اگر فیلدی تغییر کند، رویداد 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;
    }
در اینجا درخواستی به سمت آدرس api/Register/IsUserNameUnique ارسال شده و سپس بر اساس مقدار true و یا false آن، پیامی را بازگشت می‌دهد. اگر نال را بازگشت دهد یعنی مشکلی نبوده.

یک نکته: InputText استاندارد در حالت معمول آن، پس از تغییر focus به یک کنترل دیگر، سبب بروز رویداد OnFieldChanged می‌شود و نه در حالت فشرده شدن کلیدها. به همین جهت اگر برنامه پیوستی را می‌خواهید آزمایش کنید، نیاز است فقط focus را تغییر دهید و یا یک کنترل سفارشی را برای اینکار توسعه دهید.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: BlazorAsyncValidation.zip

نظرات

  • وحید نصیری در ۱۴۰۱/۱۰/۲۸ ۱۴:۶
    یک نکته‌ی تکمیلی: امکان اجرای ساده‌تر اعمال async پس از رخ‌داد onchange در Blazor 7x

    پیشنیاز: برای اجرای نکات زیر، نیاز به حداقل NET SDK 7.0.101. است و اگر از ویژوال استودیو استفاده می‌کنید، باید شماره نگارش آن حداقل 17.4.3 باشد؛ در غیراینصورت با خطای «'cannot convert from 'method group' to 'Action» مواجه خواهید شد.


    همانطور که در مطلب فوق هم مشاهده کردید، در جهت انجام اعتبارسنجی از راه دور async پس از ورود اطلاعات، تنها رخ‌دادی که در اینجا در اختیار ما قرار می‌گیرد، رخ‌داد submit (در حالت موفقیت اعتبارسنجی سمت کلاینت و یا تنها submit معمولی) است. بنابراین برای دسترسی به رخ‌دادهای بیشتر EditForm، نیاز است با EditContext آن کار کنیم تا بتوانیم برای مثال به کمک رویداد OnFieldChanged آن، این عملیات async را انجام دهیم. در دات نت 7.0.1، این وضعیت با معرفی modifier جدیدی به نام bind:after@ تغییر کرده‌است که در ادامه توضیحات آن‌را ملاحظه خواهید کرد.


    تعاریف زیر را جهت پیاده سازی یک انقیاد دوطرفه (two-way data-binding) درنظر بگیرید:
    <input @bind="username" />
    
    <InputText @bind-Value="Model.Name" />
    که در اولی با درج bind@ بر روی یک المان استاندارد HTML و در دومی با ذکر bind-Value@ میسر شده‌است. در این حالت هر تغییری در مقدار کنترل قرار گرفته‌ی بر روی صفحه، به خاصیت متصل به آن منعکس می‌شود (با پیاده سازی خودکار یک رویدادگردان onchange توسط Blazor در پشت صحنه) و برعکس.
    مشکل! اگر در اینجا نیاز باشد تا در حین ورود اطلاعات، کدی نیز اجرا شود چه باید کرد؟
    متاسفانه در این حالت نمی‌توانیم رویدادگردان onchange را به صورت دستی، به تعاریف فوق اضافه کنیم و اگر چنین کاری را انجام دهیم، با خطای زیر مواجه خواهیم شد:
    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.
    عنوان می‌کند که چون ما خودمان onchange را راسا پیاده سازی کرده‌ایم، شما دیگر نمی‌توانید اینکار را مجددا انجام دهید!

    راه حل‌های ممکن انجام اعمال async پس از بروز تغییرات تا پیش از دات نت 7

    الف) username متصل را تبدیل به یک خاصیت get و set دار کرده و اکنون در قسمت set آن می‌توان عملیات synchronous ای را انجام داد که متاسفانه در این حالت، امکان انجام اعمال async میسر نیست.
    ب) چون می‌خواهیم عملیات async ای را پس از تغییرات انجام دهیم، باید از انقیاد دوطرفه صرفنظر کنیم و مدیریت رویداد onchange را خودمان به‌دست بگیریم؛ برای نمونه در مثال زیر می‌توان با پیاده سازی async متد CheckUsername به هدف خود رسید؛ اما همانطور که مشاهده می‌کنید، این عملیات اکنون one-way binding است:
    <input value="@username" @onchange="CheckUsername" />
    ج) اگر از EditForm و کنترل‌های آن استفاده می‌کنیم، می‌توان همانند مثال مطلب جاری از رویداد OnFieldChanged استفاده کرد یا راه دیگر آن شکستن bind-Value@ به اجزای تشکیل دهنده‌ی آن است که سه جزء Value ،ValueExpression و ValueChanged را تشکیل می‌دهد و اینبار می‌توان رویداد ValueChanged آن‌را دستی پیاده سازی کرد:
    <InputText
      Value="@Model.Name"
      ValueExpression="()=>Model.Name"
      ValueChanged="(string s)=>CheckUsername(s)" />  
    <ValidationMessage For="() => Model.Name" />

    راه حل جدید انجام اعمال async پس از بروز تغییرات در دات نت 7

    Blazor در دات نت 7، به همراه یک bind:after modifier@ است که امکان اجرای متدی را (چه همزمان یا غیرهمزمان) پس از بروز تغییرات، میسر می‌کند و مزیت آن عدم نیاز به بازنویسی متد onchange و از دست دادن انقیاد دوطرفه است:
    <input @bind="username" @bind:after="CheckUsername" />
    همانطور که مشاهده می‌کنید هنوز در این حالت bind@ وجود دارد (یعنی two-way data-binding هنوز هم برقرار است) و توسط bind:after@، متدی را که قرار است پس از تغییرات اجرا شود، مشخص کرده‌ایم.

    این modifier را حتی می‌توان به کنترل‌های EditForm نیز اعمال کرد؛ بدون اینکه نیازی به استفاده از راه‌حل‌های پیشین (حالت ج عنوان شده) باشد:
    <InputText
      @bind-Value="Model.Name"
      @bind-Value:after="CheckUsername" />   
    <ValidationMessage For="() => Model.Name" />
    در اینجا نیز هنوز از مزایای two-way data-binding برخورداریم و همچنین می‌توانیم پس از تغییری، یک متد sync و یا async را فراخوانی کنیم. برای نمونه پیاده سازی اعتبارسنجی از راه دور مطلب جاری، اینبار به صورت زیر ساده می‌شود:
    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();
        }
    }
  • وحید نصیری در ۱۴۰۵/۰۳/۲۸ ۰۹:۰۵
    اضافه شدن قابلیت اعتبارسنجی Asynchronous فرم‌ها در Blazor 11x

    یکی از چالش‌های همیشگی در توسعه برنامه‌های تحت وب، اعتبارسنجی داده‌های ورودی کاربر (Form Validation) به صورت ناهمگام (Asynchronous) است. فرآیندهایی مانند بررسی تکراری نبودن نام کاربری در پایگاه داده یا استعلام یک کد پستی از طریق API‌های خارجی، نیازمند ارتباط با سرور هستند و نمی‌توان آن‌ها را به صورت آنی و همگام (Synchronous) پردازش کرد.
    در نسخه‌های پیشین فریم‌ورک Blazor، پیاده‌سازی این سناریوها نیازمند راه‌کارهای سفارشی و گاه پیچیده بود. با این حال، در به‌روزرسانی‌های جدید (طی Pull Request شماره 66526 در مخزن dotnet/aspnetcore)، بلیزور پشتیبانی بومی و همه‌جانبه‌ای را از قوانین اعتبارسنجی ناهمگام معرفی کرده‌است. این مطلب به بررسی این قابلیت جدید، معماری پشت پرده آن و نحوه استفاده از آن در پروژه‌های واقعی می‌پردازد.

    توضیح فنی و نحوه عملکرد
    پشتیبانی جدید Blazor از اعتبارسنجی ناهمگام، تغییرات مهمی را در هر دو حالت رندرینگ (Rendering Modes) یعنی تعاملی (Interactive) و غیرتعاملی ایجاد کرده‌است. در تمامی حالت‌ها، متد ارسال فرم (EditForm Submit) اکنون به صورت سرتاسری (End-to-End) منتظر به پایان رسیدن فرآیند اعتبارسنجی‌های ناهمگام می‌ماند.
    در حالت‌های تعاملی (مانند Blazor WebAssembly یا Blazor Server)، کامپوننت‌های اعتبارسنجی می‌توانند وظایف ناهمگام خود را به ازای هر فیلد (Per-field) از طریق متد جدید EditContext.AddValidationTask ثبت کنند. فریم‌ورک Blazor به صورت هوشمندانه این وظایف را زیر نظر می‌گیرد و مدیریت می‌کند.

    ویژگی‌های کلیدی این ابزار ساختاری:
    • مدیریت تسک‌های منقضی شده (Cancellation of Superseded Tasks): اگر کاربر در حال تایپ سریع باشد، درخواست‌های قبلی که هنوز کامل نشده‌اند به صورت خودکار لغو (Cancel) می‌شوند تا از هدررفت منابع سرور و تداخل داده‌ها جلوگیری شود.
    • ردیابی وضعیت پیشرفت: فریم‌ورک دو متد کلیدی IsValidationPending(field) (برای بررسی اینکه آیا اعتبارسنجی در حال انجام است) و IsValidationFaulted(field) (برای تشخیص خطاهای سیستمی حین اعتبارسنجی) را در اختیار توسعه‌دهنده قرار می‌دهد.

    سازگاری با DataAnnotations
    قابلیت‌های فعلی، بلوک‌های ساختاری پایه‌ای (Building Blocks) را فراهم می‌کنند. تجربه کامل و بدون کدنویسی اضافی (Built-in) زمانی تکمیل خواهد شد که APIهای ناهمگام جدیدِ 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();
    }

    نکات تکمیلی و تحلیل عمیق‌تر
    برای استفاده بهینه از این قابلیت در محیط‌های عملیاتی، توجه به دو نکته زیر اهمیت بالایی دارد:
    - اهمیت استفاده از CancellationToken: در متد OnFieldChanged با هر بار فشردن کلید توسط کاربر، یک رویداد متد اجرا می‌شود. اگر کاربر کلمه "Admin" را تایپ کند، ۵ درخواست به سمت سرور روانه می‌شود. ارسال CancellationToken به متد AddValidationTask این امکان را به Blazor می‌دهد که ۴ درخواست اول را به محض تغییر ورودی ابطال (Cancel) کند و فقط منتظر پاسخ درخواست آخر بماند. این کار بار پردازشی سرور را به شدت کاهش می‌دهد.
    - بهبود تجربه کاربری (UX): با کمک متد editContext.IsValidationPending به راحتی می‌توان دکمه ثبت فرم را تا زمان اتمام اعتبارسنجی غیرفعال (Disable) کرد یا یک انیمیشن بارگذاری (Loading Spinner) نشان داد تا کاربر متوجه پردازش اطلاعات در پشت صحنه بشود.