【问题标题】:Validating forms only on submit with Blazor仅在使用 Blazor 提交时验证表单
【发布时间】:2022-01-21 23:31:52
【问题描述】:

我最近开始使用 Blazor。有没有办法只在提交时触发表单模型验证,而不是在每次更改时触发?

为了澄清,假设我有这样的事情:

<EditForm Model="this" OnValidSubmit="SubmitForm">
    <DataAnnotationsValidator />
    <ValidationSummary />
            <Label For="Name">Name</Label>
            <InputText id="Name" name="Name" class="form-control" @bind-Value="Name"/>
    <button type="submit">Save</button>
</EditForm>

@code {
    [StringLength(10, ErrorMessage="Name too long")]
    public string Name { get; set; }

    private async Task SubmitForm()
    {
        // ...
        // send a POST request
    }
}

默认情况下,似乎字段的有效性和 ValidationSummary 中显示的错误消息会在文本输入的每次更改时重新评估(例如,一旦我从输入中删除第 11 个字符,“太长”消息消失)。

我希望显示的消息在单击提交按钮之前保持冻结状态。

我想可以通过删除 ValidationSummary 组件并实现自定义解决方案来实现它(例如,显示仅在提交时刷新的错误消息列表),但我想知道是否有一些惯用的解决方案我'我不知道。

【问题讨论】:

    标签: asp.net validation blazor


    【解决方案1】:

    @enet 的回答引发了另一个答案。构建您自己的 DataAnnotationsValidator。

    这是 EditContext 扩展代码。它是原始 MS 代码的修改版本,带有一些额外的控制参数。

    using Microsoft.AspNetCore.Components.Forms;
    using System.Collections.Concurrent;
    using System.ComponentModel.DataAnnotations;
    using System.Diagnostics.CodeAnalysis;
    using System.Reflection;
    using System.Reflection.Metadata;
    using System.Runtime.InteropServices;
    
    namespace StackOverflowAnswers;
    
    public static class EditContextCustomValidationExtensions
    {
        public static IDisposable EnableCustomValidation(this EditContext editContext, bool doFieldValidation, bool clearMessageStore)
            =>  new DataAnnotationsEventSubscriptions(editContext, doFieldValidation, clearMessageStore);
    
        private static event Action? OnClearCache;
    
        private static void ClearCache(Type[]? _)
            =>  OnClearCache?.Invoke();
    
        private sealed class DataAnnotationsEventSubscriptions : IDisposable
        {
            private static readonly ConcurrentDictionary<(Type ModelType, string FieldName), PropertyInfo?> _propertyInfoCache = new();
    
            private readonly EditContext _editContext;
            private readonly ValidationMessageStore _messages;
            private bool _doFieldValidation;
            private bool _clearMessageStore;
    
            public DataAnnotationsEventSubscriptions(EditContext editContext, bool doFieldValidation, bool clearMessageStore)
            {
                _doFieldValidation = doFieldValidation;
                _clearMessageStore = clearMessageStore;
                _editContext = editContext ?? throw new ArgumentNullException(nameof(editContext));
                _messages = new ValidationMessageStore(_editContext);
    
                if (doFieldValidation)
                    _editContext.OnFieldChanged += OnFieldChanged;
                _editContext.OnValidationRequested += OnValidationRequested;
    
                if (MetadataUpdater.IsSupported)
                {
                    OnClearCache += ClearCache;
                }
            }
            private void OnFieldChanged(object? sender, FieldChangedEventArgs eventArgs)
            {
                var fieldIdentifier = eventArgs.FieldIdentifier;
                if (TryGetValidatableProperty(fieldIdentifier, out var propertyInfo))
                {
                    var propertyValue = propertyInfo.GetValue(fieldIdentifier.Model);
                    var validationContext = new ValidationContext(fieldIdentifier.Model)
                    {
                        MemberName = propertyInfo.Name
                    };
                    var results = new List<ValidationResult>();
    
                    Validator.TryValidateProperty(propertyValue, validationContext, results);
                    _messages.Clear(fieldIdentifier);
                    foreach (var result in CollectionsMarshal.AsSpan(results))
                    {
                        _messages.Add(fieldIdentifier, result.ErrorMessage!);
                    }
    
                    // We have to notify even if there were no messages before and are still no messages now,
                    // because the "state" that changed might be the completion of some async validation task
                    _editContext.NotifyValidationStateChanged();
                }
            }
    
            private void OnValidationRequested(object? sender, ValidationRequestedEventArgs e)
            {
                var validationContext = new ValidationContext(_editContext.Model);
                var validationResults = new List<ValidationResult>();
                Validator.TryValidateObject(_editContext.Model, validationContext, validationResults, true);
    
                // Transfer results to the ValidationMessageStore
                _messages.Clear();
                foreach (var validationResult in validationResults)
                {
                    if (validationResult == null)
                    {
                        continue;
                    }
    
                    var hasMemberNames = false;
                    foreach (var memberName in validationResult.MemberNames)
                    {
                        hasMemberNames = true;
                        _messages.Add(_editContext.Field(memberName), validationResult.ErrorMessage!);
                    }
    
                    if (!hasMemberNames)
                    {
                        _messages.Add(new FieldIdentifier(_editContext.Model, fieldName: string.Empty), validationResult.ErrorMessage!);
                    }
                }
    
                _editContext.NotifyValidationStateChanged();
            }
    
            public void Dispose()
            {
                if (_clearMessageStore)
                    _messages.Clear();
                if (_doFieldValidation)
                    _editContext.OnFieldChanged -= OnFieldChanged;
                _editContext.OnValidationRequested -= OnValidationRequested;
                _editContext.NotifyValidationStateChanged();
    
                if (MetadataUpdater.IsSupported)
                {
                    OnClearCache -= ClearCache;
                }
            }
    
            private static bool TryGetValidatableProperty(in FieldIdentifier fieldIdentifier, [NotNullWhen(true)] out PropertyInfo? propertyInfo)
            {
                var cacheKey = (ModelType: fieldIdentifier.Model.GetType(), fieldIdentifier.FieldName);
                if (!_propertyInfoCache.TryGetValue(cacheKey, out propertyInfo))
                {
                    // DataAnnotations only validates public properties, so that's all we'll look for
                    // If we can't find it, cache 'null' so we don't have to try again next time
                    propertyInfo = cacheKey.ModelType.GetProperty(cacheKey.FieldName);
    
                    // No need to lock, because it doesn't matter if we write the same value twice
                    _propertyInfoCache[cacheKey] = propertyInfo;
                }
    
                return propertyInfo != null;
            }
    
            internal void ClearCache()
                => _propertyInfoCache.Clear();
        }
    }
    

    还有CustomValidation 组件:

    using Microsoft.AspNetCore.Components;
    using Microsoft.AspNetCore.Components.Forms;
    
    namespace StackOverflowAnswers;
    
    public class CustomValidation : ComponentBase, IDisposable
    {
        private IDisposable? _subscriptions;
        private EditContext? _originalEditContext;
    
        [CascadingParameter] EditContext? CurrentEditContext { get; set; }
    
        [Parameter] public bool DoEditValidation { get; set; } = false;
    
        /// <inheritdoc />
        protected override void OnInitialized()
        {
            if (CurrentEditContext == null)
            {
                throw new InvalidOperationException($"{nameof(DataAnnotationsValidator)} requires a cascading " +
                    $"parameter of type {nameof(EditContext)}. For example, you can use {nameof(DataAnnotationsValidator)} " +
                    $"inside an EditForm.");
            }
    
            _subscriptions = CurrentEditContext.EnableCustomValidation(DoEditValidation, true);
            _originalEditContext = CurrentEditContext;
        }
    
        /// <inheritdoc />
        protected override void OnParametersSet()
        {
            if (CurrentEditContext != _originalEditContext)
            {
                // While we could support this, there's no known use case presently. Since InputBase doesn't support it,
                // it's more understandable to have the same restriction.
                throw new InvalidOperationException($"{GetType()} does not support changing the " +
                    $"{nameof(EditContext)} dynamically.");
            }
        }
    
        /// <inheritdoc/>
        protected virtual void Dispose(bool disposing)
        {
        }
    
        void IDisposable.Dispose()
        {
            _subscriptions?.Dispose();
            _subscriptions = null;
    
            Dispose(disposing: true);
        }
    }
    

    你可以这样使用它:

    <EditForm EditContext=this.editContext OnValidSubmit=OnValidSubmit>
        <CustomValidation DoEditValidation=false/>
        @*<DataAnnotationsValidator/>*@
        <div class="row">
            <div class="col-2">
                Date:
            </div>
            <div class="col-10">
                <InputDate @bind-Value=this.Record.Date></InputDate>
            </div>
        </div>
    .......
    

    【讨论】:

      【解决方案2】:

      您可以跳过使用DataAnnotationsValidator 组件,而是在SubmitForm 方法中执行类似的操作:

      private void SubmitForm()
      {
          EditContext.AddDataAnnotationsValidation();
          
          EditContext.Validate(); 
      }
      

      这是完整的代码...请注意,我没有使用 EditForm 的 Model 属性。相反,我使用的是 EditForm 的 EditContext 属性

      <EditForm EditContext="@EditContext" OnValidSubmit="SubmitForm">
           <ValidationSummary />
      
             <div class="form-group">
              <label for="name">Name: </label>
              <InputText Id="name" Class="form-control" @bind-Value="@Model.Name"></InputText>
              <ValidationMessage For="@(() => Model.Name)" />
      
          </div>
          <div class="form-group">
              <label for="body">Text: </label>
              <InputTextArea Id="body" Class="form-control" @bind-Value="@Model.Text"></InputTextArea>
              <ValidationMessage For="@(() => Model.Text)" />
          </div>
          <button type="submit" class="btn btn-success">Submit</button>
      </EditForm>
      
      @code {
         
          private EditContext EditContext;
          private Comment Model = new Comment();
      
           protected override void OnInitialized()
          {
              EditContext = new EditContext(Model);
              base.OnInitialized();
          }
         
           private void SubmitForm()
          {
             // Adds DataAnnotations validation support to the EditContext. 
             // instead of the removed DataAnnotationsValidator component
             EditContext.AddDataAnnotationsValidation();
      
             // Validates the EditContext.  
             EditContext.Validate();
          }
      
           public class Comment
          {
              [Required]
              [MaxLength(10,ErrorMessage="Name too long")]
              public string Name { get; set; }
      
              [Required]
              public string Text { get; set; }
             
          }
         
      }
      

      惯用的解决方案

      我的回答是惯用的解决方案吗?我不知道。是否完整?我不知道...如果您正在寻找一个惯用的解决方案,那么可以提供它的合适人选是史蒂夫桑德森。现在谈论 Blazor 中的惯用解决方案和良好实践还为时过早。我这样做是因为我有这种感觉,没有别的。

      我正在使用 ValidationMessage 组件,以便您可以看到验证何时生效。

      【讨论】:

      • 有趣。我看到方法了。一点。 AddDataAnnotationsValidation() 是一个扩展方法,它返回一个 DataAnnotationsEventSubscriptions 对象。这实现了IDisposable,因为它需要“取消挂钩”它在创建时添加的各种事件处理程序挂钩 - EditContext.OnFieldChanged,....。但是,当 DataAnnotationsEventSubscriptions 实例在 SubmitForm 末尾超出范围时调用 Dispose,也会在 ValidationMessageStore 上调用 _messages.Clear(),我认为这将清除您要显示的所有验证方法。
      • 感谢您的支持...据我所知,AddDataAnnotationsValidation 产生的结果与使用 DataAnnotationsValidator 组件产生的结果相同。没时间验证你的描述...
      • DataAnnotationsValidator 是一个组件,所以我认为渲染器正确地“处理”了。它是 Dispose_subscriptions 上调用 Dispose,它创建了 DataAnnotationsEventSubscriptions 的实例。
      • @MrCakaShaunCurtis,好的,知道了...他们已经更改了DataAnnotationsValidator 组件,但没有让我知道;} 我想创建类似于您第二个答案中的解决方案,但后来决定不花太多时间在我认为不好的做法上......输入数据的验证应该立即执行。
      【解决方案3】:

      何时发生验证由您使用的验证器控制。

      您可以从 EditContext 接收两个事件:

      OnValidationRequested 在调用EditContext.Validate 时或作为表单提交过程的一部分被调用。

      每次更改字段值时都会调用OnFieldChanged

      验证器使用这些事件来触发它的验证过程,并将结果输出到 EditContext 的 ValidationMessageStore。

      DataAnnotationsValidator 连接两个事件,并在任何一个被调用时触发验证。

      还有其他验证器,编写自己的验证器并不太难。除了来自通常的控制供应商的那些之外,还有 Blazored 或我的。我的记录在这里 - https://shauncurtis.github.io/articles/Blazor-Form-Validation.html。它有一个DoValidationOnFieldChange 设置!

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-05-26
        • 1970-01-01
        • 2019-11-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多