Клиентская валидация в ASP.Net Core без jQuery

Клиентская валидация в ASP.Net Core без jQuery
Photo by Scott Webb / Unsplash

На ASP.NET Core сильно повлиял старый .NET Framework System.Web, основанный на ASP.NET Framework, но Net Core принципиально быстрее и современнее. Тем не менее, одна функция попала в ASP.NET Core практически без изменений: валидация на стороне клиента.

В этой статье я рассматриваю альтернативу стандартной (поддерживаемой) клиентской валидации на основе jQuery. В этом альтернативном подходе мы полагаемся на небольшую библиотеку JavaScript под названием aspnet-client-validation.

Валидация на стороне клиента в ASP.NET Core.

Net меняется из года в год, но есть в нём и неизменные вещи, одна из которых — это способ выполнения валидации на стороне клиента в ASP.NET Core. Рекомендуемый/поддерживаемый подход основан на jQuery. В этой главе я покажу, как он работает. Если вы хорошо знаете этот подход, то можете пропустить главу!

Чтобы у нас был конкретный пример для работы, я создам очень простую страницу Razor Page, всё так же будет работать и в "традиционном" MVC. Следующая PageModel содержит простую модель Person, которую мы привяжем к форме.

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel.DataAnnotations;

namespace MvcClientValidation.Pages;

public class TestModel : PageModel
{
    [BindProperty]
    public Person Input { get; set; }

    public ActionResult OnPost()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        TempData["Message"] = $"Hello {Input.Name} ({Input.Email})!";
        return RedirectToPage("Index");
    }

    public class Person
    {
        [Required]
        public string Name { get; set; }

        [Required]
        [EmailAddress]
        public string Email { get; set; }
    }
}

После отправки обработчик OnPost() проверяет, успешно ли прошла валидация модели Person. Если да, то в данном примере мы устанавливаем некоторые TempData с сообщением и перенаправляем на другую страницу (используя паттерн Post-Redirect-Get).

Если модель не была валидной, мы повторно отображаем форму (которая показывает сообщения об ошибках валидации).

Для полноты картины приведу код Razor страницы с основной формой:

@page
@model MvcClientValidation.Pages.TestModel

<div class="col-md-6 offset-md-3">
  <form method="post">
    <div class="form-floating mb-3">
      <input class="form-control" asp-for="Input.Name">
      <label asp-for="Input.Name"></label>
      <span class="text-danger field-validation-valid" asp-validation-for="Input.Name"></span>
    </div>
    <div class="form-floating mb-3">
      <input class="form-control" asp-for="Input.Email">
      <label asp-for="Input.Email"></label>
      <span class="text-danger field-validation-valid" asp-validation-for="Input.Email"></span>
    </div>
    <button type="submit" class="w-100 btn btn-lg btn-primary">Отправить</button>
  </form>
</div>

Одна вещь, которую следует отметить в приведенной выше странице, заключается в том, что в настоящее время она не использует валидацию на стороне клиента. После заполнения полей клиент нажимает кнопку "Отправить", и только после проверки формы на сервере и отправки ответа получает сообщение о том, что что-то пошло не так.

Использование проверки на стороне клиента улучшает этот процесс, предоставляя немедленную обратную связь. Вместо того чтобы ждать, пока форма отправится на сервер, клиент сразу же получает уведомление об отсутствующих полях или ошибках. В наши дни для любого приложения в Интернете это поистине важный момент.

Обратите внимание, что вы всегда должны проводить проверку на сервере, независимо от того, есть ли у вас проверка на стороне клиента. Проверка на стороне клиента не обеспечивает целостность данных или безопасность, это может сделать только проверка на стороне сервера. Валидация на стороне клиента — это исключительно для удобства пользователя.

Все шаблоны Razor Pages по умолчанию включают общий частичный файл _ValidationScriptsPartial.cshtml. Эта часть содержит два тега script:

<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>

Они работают в сочетании со скриптом jQuery (обычно добавляемым на все страницы в _Layout.cshtml, я добавил тег в примере выше для демонстрации) для добавления client-side валидации. Вы так же можете добавить тег в секцию "Scripts", разместив следующее в нижней части вашей страницы Razor:

@section Scripts{
    <partial name="_ValidationScriptsPartial" />
}

Теперь у вас есть валидация на стороне клиента! Если пользователь не сможет правильно заполнить форму, он получит мгновенное предупреждение. Если пользователь все равно нажмет кнопку "Отправить", форма не будет отправлена на сервер, пока он не исправит ошибки.

Под капотом это работает потому, что ASP.NET Core передает целый ряд метаданных элементам формы, которые он генерирует. Например, форма, показанная выше, выглядит следующим образом:

<div class="col-md-6 offset-md-3">
  <form method="post">
    <div class="form-floating mb-3">
      <input class="form-control" type="text" data-val="true" 
        data-val-required="The Name field is required." id="Input_Name" 
        name="Input.Name" value="">
      <label for="Input_Name">Name</label>
      <span class="text-danger field-validation-valid field-validation-valid"
        data-valmsg-for="Input.Name" data-valmsg-replace="true"></span>
    </div>
    <div class="form-floating mb-3">
      <input class="form-control" type="email" data-val="true" 
        data-val-email="The Email field is not a valid e-mail address." 
        data-val-required="The Email field is required." id="Input_Email" 
        name="Input.Email" value="">
      <label for="Input_Email">Email</label>
      <span class="text-danger field-validation-valid field-validation-valid" 
        data-valmsg-for="Input.Email" data-valmsg-replace="true"></span>
    </div>
    <button type="submit" class="w-100 btn btn-lg btn-primary">Submit</button>
    <input name="__RequestVerificationToken" type="hidden" value="CfDJ8FJx1R5_kANFmBjoULQ...">
  </form>
</div>

В HTML коде выше вы можете увидеть все атрибуты data-*, добавленные ASP.NET Core. Библиотека jQuery для валидации ищет HTML-элементы с этими атрибутами и прикрепляет обработчики событий. Эти обработчики проверяют вводимые значения и используют данные атрибутов для создания соответствующих предупреждений и сообщений. Таким образом, на стороне клиента вы получаете ту же валидацию, что и на стороне сервера.

Все это работает со встроенными атрибутами [DataAnnotations]. Если вы используете FluentValidation или другой фреймворк валидации, то вам могут понадобиться дополнительные действия для включения валидации на стороне клиента. Например, для FluentValidation необходимо установить FluentValidation.AspNetCore.

Итак, если все это работает, в чем проблема? Имеет ли значение то, что в этом решении используется jQuery?

Что не так с jQuery?

В ASP.NET Core по умолчанию для валидации на стороне клиента используется скрипт от Microsoft под названием jQuery Unobtrusive Validation, который, в свою очередь, использует плагин валидации jQuery. Все это (очевидно) требует jQuery.

Если сложить все это, то получится более 100 КБ (после GZIP сжатия) JavaScript. Это... довольно много. Он не обязательно загружается в браузер каждый раз благодаря кэшированию, но, тем не менее, это весь JavaScript, который браузеру нужно разобрать и запустить для каждой отдельной страницы.

jQuery - исторически важная библиотека, которая отлично справлялась со своей задачей сглаживания различий между браузерами. Однако в наши дни браузеры гораздо лучше следуют одним и тем же стандартам. Это, в сочетании с монополией хрома, означает, что главное преимущество jQuery в наши дни уже не так актуально. "Вам может не понадобиться jQuery" — это отличный сайт, который показывает эквивалент API современных браузеров большей части функциональности jQuery.

Итак, если jQuery не так необходим в наши дни и занимает много трафика, разве не было бы здорово, если бы вы могли его удалить?

Вы справедливо можете возразить: "Разве Bootstrap не требует jQuery?". Ну, это зависит от обстоятельств. Текущие шаблоны ASP.NET Core используют Bootstrap 5, который явно разработан для того, чтобы не использовать jQuery. Если вы все еще используете Bootstrap 4, то jQuery вам понадобится в любом случае, но если вы используете Bootstrap 5 или Tailwind (например), то jQuery — это просто ненужные лишние байты.

Но, тем не менее, стандартная клиентская валидация ASP.NET Core требует jQuery. Так что мы с ним застряли, верно?

Но какова же альтернатива?

Как ни странно, стандартные способы для выполнения клиентской валидации в браузерах существуют уже много лет. В HTML 5 появился "Constraints Validation API". Он добавляет проверку элементов формы на основе типа элемента, а также других стандартных свойств. Например, вы могли видеть следующее стандартное всплывающее окно "Недействительный email" при повседневном просмотре веб-страниц:

Это сообщение о проверке было сгенерировано API Constraints Validation. В данном случае оно было вызвано тем, что элемент ввода имеет атрибут type="email".

Если вам интересно, почему в приложениях Razor Pages, использующих jQuery, не видно скрытой валидации, то это потому, что этот плагин добавляет атрибут novalidate к тегу.

В отличие от некоторых API браузеров, API проверки ограничений имеет отличную поддержку. За исключением Opera Mini, ограничения поддерживаются практически во всех браузерах. Даже IE 10 имеет (частичную) поддержку! 😮 Если отбросить IE и Opera Mini, то если вы используете браузер, который обновлялся в течение последних 5 лет, то все будет в порядке:

Итак, если браузер уже имеет встроенную поддержку валидации, можем ли мы использовать ее в ASP.NET Core и избавиться от jQuery?

К сожалению, ответ: не так просто. Проверка ограничений зависит от наличия определенных атрибутов, а не атрибутов data-*, которые ASP.NET Core добавляет по умолчанию. Также, как правило, невозможно стилизовать ошибки, выдаваемые браузером. Поэтому использование Constraints API, к сожалению, не является простым вариантом.

Но не все потеряно. Что если бы мы могли обойтись без API Constraints, а вместо этого просто заменить код незаметной валидации jQuery.

Совершенно другой подход к клиентской валидации использует AJAX для выполнения всех правил на стороне сервера, используя библиотеку, такую как FormHelper. Это позволяет избежать необходимости написания пользовательских правил на JavaScript и в некоторых случаях может обеспечить более последовательный опыт для пользователей.

Представляем aspnet-client-validation.

Эта статья была написана под впечатлением от твита легендарного Фила Хаака:

If you use aspnetcore or aspnetmvc without jquery, you should check out https://t.co/EMB8LZZWgg for client validation.

Библиотека aspnet-client-validation, на которую он указывает, представляет собой крошечную (4 кБ GZIP) библиотеку, которая по сути является простой реализацией скрипта unobtrusive validation от jQuery, упрощенной и без каких-либо зависимостей.

Обратите внимание, что aspnet-client-validation не поддерживает IE.

Вы можете легко опробовать библиотеку, заменив следующие теги <script>:

  • ~/lib/jquery/dist/jquery.min.js_Layout.cshtml)
  • ~/lib/jquery-validation/dist/jquery.validate.min.js_ValidationScriptsPartial.cshtml)
  • ~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js_ValidationScriptsPartial.cshtml)
Помните, что jQuery необходим, если вы используете Bootstrap 4 (или более раннюю версию), но если вы используете Bootstrap 5 (как в стандартных шаблонах .NET 7 ASP.NET Core), то jQuery совершенно необязателен и требуется только для добавления клиентской валидации.

После удаления этих файлов вы можете добавить ссылку на aspnet-validation.min.js в файл _ValidationScriptsPartial.cshtml. Приведенный ниже пример также включает необходимый вызов метода bootstrap().

<script src="https://www.unpkg.com/aspnet-client-validation/dist/aspnet-validation.min.js" crossorigin="anonymous" integrity="sha384-4E0R5+D480pDlcgfpDw2NZDUAX0YsK6J4Zk6o4raasSXMaAlatx9tyNxkFBESu6C"></script>
<script>
    const v  = new aspnetValidation.ValidationService()
    v.bootstrap()
</script>
Приведенный выше пример ссылается на файл JavaScript непосредственно из CDN, но вы можете сослаться на файл локально в теге <script> или включить его в свои пакеты с помощью ES Modules или Common JS, в зависимости от того, какой подход вы предпочитаете.

В приведенном примере загружается скрипт aspnet-client-validation, а затем запускается код bootstrap. ValidationService.bootstrap() загружает провайдеры проверки по умолчанию и устанавливает DOM-слушатель, который сканирует HTML-источник в поисках атрибутов проверки.

В итоге все работает. Вы получаете клиентскую валидацию, которая выполняется автоматически и, по сути, дает тот же результат, что и при использовании jQuery.

Таким образом, простым изменением _ValidationScriptsPartial.cshtml вы можете отказаться от jQuery и значительно уменьшить размер полезной нагрузки вашего приложения. В следующем разделе мы расширим библиотеку aspnet-client-validation, чтобы показать, что она так же гибка, как и jQuery-эквивалент.

Создание пользовательского провайдера валидации с помощью aspnet-client-validation.

ASP.NET Core поставляется с поддержкой встроенных по умолчанию атрибутов валидации в библиотеке .NET DataAnnotations, которая обеспечивает поддержку таких вещей, как Email, обязательные поля, минимальная длина, диапазоны и Regex. Эти атрибуты охватывают множество распространенных случаев, но часто возникает необходимость в написании дополнительных пользовательских правил валидации. Существует несколько способов сделать это, один из них - создать собственный атрибут валидации.

Создание собственного атрибута ValidationAttribute

Чтобы рассмотреть конкретный пример, представим, что вы хотите ограничить список разрешенных электронных адресов только теми, которые заканчиваются на @mycompany.com. Встроенного атрибута проверки "endswith" не существует, однако мы можем его сделать. Упрощенная реализация может выглядеть следующим образом:

public class EndsWithValidationAttribute : ValidationAttribute
{
    private readonly string _endsWith;
    public EndsWithValidationAttribute(string endsWith)
    {
        _endsWith = endsWith;
    }

    public override string FormatErrorMessage(string name)
        => $"The field {name} must end with '{_endsWith}'";

    public override bool IsValid(object? value)
    {
        if(value is null)
        {
            return true; // Допускает нулевые значения, поэтому работает с атрибутом Required
        }

        return value is string s&& s.EndsWith(_endsWith);
    }
}

И будет добавлен в модель как

public class Person
{
    [Required]
    public string Name { get; set; }

    [Required]
    [EmailAddress]
    [EndsWithValidation("@mycompany.com")] // 👈 Добавить
    public string Email { get; set; }
}

Когда вы отправляете форму, проверка выполняется на сервере и отмечает неправильные адреса электронной почты:

По умолчанию эта проверка выполняется только на сервере после публикации данных, даже если вы включили проверку на стороне клиента, поэтому обратная связь с пользователем происходит очень медленно. Чтобы включить проверку на стороне клиента, необходимо:

  1. Передавать атрибуты data-*, к которым может подключаться библиотека на стороне клиента.
  2. Добавить пользовательский провайдер в библиотеку на стороне клиента.

Шаг 1. одинаков независимо от того, используете ли вы стандартную jQuery unobtrusive validation или библиотеку aspnet-client-validation. Есть несколько способов сделать это, но для целей этой статьи я буду реализовывать IClientModelValidator на атрибуте валидации.

Добавление данных-атрибутов с помощью IClientModelValidator

Реализация IClientModelValidator требует реализации единственного метода AddValidation(), который отвечает за добавление необходимых атрибутов data-*. Для нашего атрибута валидации нам нужно добавить три атрибута:

  • data-val="true". Библиотеки на стороне клиента используют этот атрибут, чтобы понять, что поле имеет требования к валидации.
  • data-val-endswith. Этот атрибут содержит сообщение об ошибке, которое должно отображаться при неудачной проверке.
  • data-val-endswith-value. Этот атрибут содержит строку, которую должна искать библиотека на стороне клиента.

Часть -endswith уникальна для нашей конкретной реализации валидатора, но, по идее, она может быть любой. Это просто должно быть известное значение, к которому может подключиться библиотека на стороне клиента. Вы можете создать пользовательскую библиотеку для библиотеки jQuery unobtrusive validation, как описано в документации. Мы создадим аналогичный хук для библиотеки aspnet-client-validation.

public class EndsWithValidationAttribute : ValidationAttribute, IClientModelValidator
{
    private readonly string _endsWith;
    public EndsWithValidationAttribute(string endsWith)
    {
        _endsWith = endsWith;
    }

    // 👇 Добавьте этот метод для добавления атрибутов
    public void AddValidation(ClientModelValidationContext context)
    {
        var errorMessage = FormatErrorMessage(context.ModelMetadata.DisplayName ?? context.ModelMetadata.Name);
        context.Attributes.TryAdd("data-val", "true");
        context.Attributes.TryAdd("data-val-endswith", errorMessage);
        context.Attributes.TryAdd("data-val-endswith-value", _endsWith);
    }

    public override string FormatErrorMessage(string name)
        => $"The field {name} must end with '{_endsWith}'";

    public override bool IsValid(object? value)
    {
        if(value is null)
        {
            return true;
        }

        return value is string s&& s.EndsWith(_endsWith);
    }
}

Вы можете увидеть результат вызова AddValidation в сгенерированном HTML, содержащем два дополнительных атрибута data-val-endswith*:

<input class="form-control input-validation-error" type="email" 
    id="Input_Email" name="Input.Email" value="[email protected]"
    data-val="true" data-val-required="The Email field is required." 
    data-val-email="The Email field is not a valid e-mail address." 
    data-val-endswith="The field Email must end with '@mycompany.com'" 
    data-val-endswith-value="@mycompany.com">

Теперь у нас есть атрибуты, нам нужно подключить провайдер aspnet-client-validation для их использования.

Создание собственного провайдера для aspnet-client-validation.

В aspnet-client-validation можно обрабатывать пользовательские требования к валидации, создав новый провайдер. Чтобы добавить нового провайдера, вызовите ValidationService.addProvider(), передав имя провайдера (в нашем случае endswith) и передав функцию обратного вызова, которая выполняет проверку. Этот обратный вызов имеет три параметра:

  • value: значение элемента ввода
  • elemet: проверяемый элемент DOM
  • params: дополнительные значения, передаваемые через дополнительные атрибуты data-*. Таким образом, вы можете получить доступ к data-val-endswith-value, вызвав params.value. В качестве альтернативы вы можете получить доступ к data-val-endswith-somethingelse, вызвав params.somethingelse.

Для нашей простой функции валидации мы можем добавить новый провайдер следующим образом:

<script src="https://www.unpkg.com/aspnet-client-validation/dist/aspnet-validation.min.js"></script>
<script>
  const v  = new aspnetValidation.ValidationService()
  // 👇 Вызвать addProvider() _перед_ вызовом bootstrap()
  v.addProvider('endswith', (value, element, params) => {
    if (!value) {
        // Пусть [Required] обрабатывает ошибку валидации для пустого ввода...
        return true;
    }

    return value.endsWith(params.value);
  });
  v.bootstrap();
</script>

И вот, у нас есть пользовательская валидация на стороне клиента без jQuery!🎉

Будущее валидации в ASP.NET Core.

Я не использовал aspnet-client-validation в реальном проекте, но исходя из того, что я видел, я не вижу причин придерживаться jQuery unobtrusive validation scripts. Подход aspnet-client-validation, кажется, заполняет ту же самую дыру, но с меньшей сложностью и с гораздо меньшим размером загрузки.

У меня возник вопрос: почему это не используется по умолчанию? Если убрать зависимость от jQuery, то не будет веских причин держать его в шаблонах по умолчанию. Кажется, что aspnet-client-validation был бы более простой и легкой рекомендацией для пользователей.

Хорошая новость заключается в том, что в репозитории ASP.NET Core есть выпуск, посвященный именно этому. Плохая новость в том, что он был создан 4 года назад и, похоже, застрял в неопределенности 😢.

Так в чем же дело? Почему ничего не происходит?

В комментарии от 2021 года вы можете найти то, что, по-видимому, является корнем проблемы:

Первоначальная мысль была в том, что мы можем сделать что-то тактическое, опираясь либо на возможности браузера, такие как валидация ограничений, либо на ручное создание имитации jQuery.validation. Но при дальнейшем рассмотрении мы поняли, что альтернатива, которая просто убирает jquery, не достаточно эффективна для нас.

В комментарии есть еще много деталей, но суть проблемы заключается в том, что они просто не хотят поставлять новую библиотеку JavaScript. То, что начиналось как простой запрос "удалить jQuery", переросло в более грандиозный "переделать работу валидации в MVC" и впоследствии заглохло.

Помните, в настоящее время де-факто, "благословенный" способ сделать валидацию на стороне клиента — это полагаться на jQuery. В 2023 году. ASP.NET Core — это быстрый, современный веб-фреймворк. Ожидаете ли вы, что он будет требовать jQuery? 😳 Неловко, правда?

Конечно, я понимаю. Blazor сейчас в центре внимания команды, и у него нет тех же проблем. Но MVC/Razor Pages все еще очень поддерживаемые способы создания приложений.

Так что мы можем, наконец, просто заменить jQuery на aspnet-client-validation и покончить с этим😅.

Подводя итог.

В этой статье я описал, как в настоящее время работает клиентская валидация в ASP.NET Core, используя jQuery и плагин unobtrusive validation. Затем я описал альтернативную библиотеку Фила Хаака, которая выполняет ту же функцию, под названием aspnet-client-validation. Эта библиотека занимает всего 4 кБ в GZipped и не имеет зависимостей.

После демонстрации использования библиотеки я показал ее совместимость с существующей библиотекой unobtrusive validation, создав пользовательский атрибут валидации, и показал, как его можно подключить к библиотеке aspnet-client-validation. Наконец, я описал свой взгляд на будущее клиентской валидации в ASP.NET Core.

С оригиналом статьи вы можете ознакомиться по ссылке.