You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Blazor子组件自定义验证需求:拦截表单非法提交

Blazor子组件表单验证实现需求

我有一个嵌入表单的Blazor组件,使用方式如下:

<NumberWithNAAndAmberRanges2 @bind-BindNumber="LocVM.ChartDayStartMinute" 
    NaName="Not Applicable"  NaAllowed="true" 
    AmberRange_Min="10" AmberRange_Max="45" 
    RedRange_Max="59" RedRange_Min="0" />

组件现有逻辑

  • 包含绑定数字输入框,勾选N/A复选框时返回值为-1
  • 红色范围为绝对允许范围,输入值必须处于该范围内
  • 琥珀色范围为常规预期范围:若输入值在红色范围内但超出琥珀色范围(例如输入50),会显示确认复选框,要求用户确认该异常值

当前问题与需求

组件已能正常显示确认复选框,但需要实现一个属于子组件自身的验证器,阻止父表单提交,直到满足以下条件之一:

  1. 输入值处于琥珀色范围内
  2. 用户已勾选异常值确认复选框

要求验证逻辑不依赖父模型验证,但能将验证状态反馈给父表单,阻止非法提交。


现有组件代码

<table>
    <tr>
        <td>
          <InputNumber id="tbNumber" class="form-control"  Value="@DisplayNumber" ValueExpression="() => DisplayNumber"   @oninput="OnValueChanged"/>
        </td>
@if (NaAllowed){ 
        <td>&nbsp;</td>
        <td>
                        <InputCheckbox  id="cb" Value="@vNA" ValueExpression="() => vNA"  @oninput="OnCheckedChanged"  />
         </td>
        <td>&nbsp;</td>
        <td>@NaName</td>
 }
    </tr>
</table> 


@if (AmberRange_Outside){    
 <div>
     <strong>
         @DisplayNumber is out side of expected range of @AmberRange_Min to @AmberRange_Max please confirm below
     </strong>
  </div>
 <div>
            <InputCheckbox  id="cbOutsideAmber" Value="@AmberRange_ConfrimChk" ValueExpression="() => AmberRange_ConfrimChk"    /> Confrim unusual value
 </div>

}else{
 <div>
     Within amber
 </div>
}
@if (RedRange_Outside){    
 <div>
     <strong>
         @DisplayNumber is out side of acceptible range of @RedRange_Min to @RedRange_Max 
     </strong>
  </div> 
}else{
 <div>
     Within red
 </div>
}

@code {
    private Boolean initalSet  = false;
    private Boolean vNA;
    private Boolean RedRange_Outside = false;
    private Boolean AmberRange_Outside = false;
    private Boolean AmberRange_ConfrimChk = false;
    private int? _BindNumber;
    
    [Parameter]
    public int? RedRange_Max { get; set; }
    [Parameter]
    public int? RedRange_Min { get; set; }
    [Parameter]
    public int? AmberRange_Max { get; set; }
    [Parameter]
    public int? AmberRange_Min { get; set; }
    [Parameter]
    public String NaName { get; set; }
    [Parameter]
    public Boolean Required { get; set; }
    [Parameter]
    public Boolean NaAllowed { get; set; }
    [Parameter]
    public EventCallback<int?> BindNumberChanged { get; set; }
    [Parameter]
    public int? BindNumber {
        get { return _BindNumber; }
        set
        {
            if (_BindNumber != value || !initalSet )
            {        
                _BindNumber = value;
                if (_BindNumber == null)
                {
                    vNA = false;
                    DisplayNumber = null;
                }
                else if (_BindNumber == -1)
                {
                    vNA = true;
                    DisplayNumber = null;
                }
                else
                {
                    vNA = false;
                    DisplayNumber = _BindNumber;
                }
                if (initalSet)
                {
                    AmberRange_ConfrimChk = false;
                    BindNumberChanged.InvokeAsync(_BindNumber);  
                }
                else { initalSet = true;
                    AmberRange_ConfrimChk = true;}
            }
            
            AmberRange_Outside = AmberOutOfRange(_BindNumber);
            RedRange_Outside = RedOutOfRange(_BindNumber);

        }
    }
    


     protected Boolean RedOutOfRange(int? newNumber)
    {
        if (newNumber != null && (newNumber != -1 || !NaAllowed))
        {
            if ( RedRange_Min != null )
            {
                if (RedRange_Min > newNumber )
                {return true;}
            }
            if ( RedRange_Max != null )
            {
                if (RedRange_Max < newNumber )
                {return true;}
            }
        }
        return false;
    }  
    protected Boolean AmberOutOfRange(int? newNumber)
    {
        if (newNumber != null && newNumber != -1 && !RedOutOfRange(newNumber))
        {
            if (  AmberRange_Min != null )
            {
                if (AmberRange_Min > newNumber )
                {return true;}
            }
            if ( AmberRange_Max != null )
            {
                if (AmberRange_Max < newNumber )
                {return true;}
            }
        }
        return false;
    }

    public int? DisplayNumber { get; set; }

    protected async Task OnValueChanged(ChangeEventArgs e)
    {
        if (e.Value != null)
        {
            string stVal = e.Value.ToString();
            int newNumber;
            if (int.TryParse(stVal ,out newNumber))
            {                
                BindNumber = newNumber;
                vNA = false;
            }else{             
                BindNumber = null;
            }
            
        }
    }
    protected async Task OnCheckedChanged(ChangeEventArgs e)
    {
        if (e.Value != null)
        {
            string stVal = e.Value.ToString();
            Boolean newChecked;
            if (Boolean.TryParse(stVal ,out newChecked))
            {
                if (newChecked)
                {
                    BindNumber = -1;
                }
            }
        }
    }


}

解决方案

要实现子组件自带验证并反馈给父表单,需利用Blazor的EditContext和ValidationMessageStore,具体步骤如下:

修改后的完整组件代码

@inject EditContext EditContext
@implements IDisposable

<table>
    <tr>
        <td>
          <InputNumber id="tbNumber" class="form-control"  Value="@DisplayNumber" ValueExpression="() => DisplayNumber"   @oninput="OnValueChanged"/>
        </td>
@if (NaAllowed){ 
        <td>&nbsp;</td>
        <td>
                        <InputCheckbox  id="cb" Value="@vNA" ValueExpression="() => vNA"  @oninput="OnCheckedChanged"  />
         </td>
        <td>&nbsp;</td>
        <td>@NaName</td>
 }
    </tr>
</table> 


@if (AmberRange_Outside){    
 <div class="text-warning">
     <strong>
         @DisplayNumber 超出预期范围 @AmberRange_Min 至 @AmberRange_Max,请确认下方选项
     </strong>
  </div>
 <div>
            <InputCheckbox  id="cbOutsideAmber" Value="@AmberRange_ConfrimChk" ValueExpression="() => AmberRange_ConfrimChk" @oninput="OnAmberConfirmChanged"    /> 确认该异常值
 </div>

}else{
 <div class="text-success">
     处于琥珀色预期范围内
 </div>
}
@if (RedRange_Outside){    
 <div class="text-danger">
     <strong>
         @DisplayNumber 超出允许范围 @RedRange_Min 至 @RedRange_Max 
     </strong>
  </div> 
}else{
 <div class="text-success">
     处于红色允许范围内
 </div>
}

<!-- 显示验证错误信息 -->
<ValidationMessage For="() => BindNumber" />

@code {
    private Boolean initalSet = false;
    private Boolean vNA;
    private Boolean RedRange_Outside = false;
    private Boolean AmberRange_Outside = false;
    private Boolean AmberRange_ConfrimChk = false;
    private int? _BindNumber;
    private ValidationMessageStore _validationMessageStore;
    private FieldIdentifier _bindNumberField;

    [Parameter]
    public int? RedRange_Max { get; set; }
    [Parameter]
    public int? RedRange_Min { get; set; }
    [Parameter]
    public int? AmberRange_Max { get; set; }
    [Parameter]
    public int? AmberRange_Min { get; set; }
    [Parameter]
    public String NaName { get; set; }
    [Parameter]
    public Boolean Required { get; set; }
    [Parameter]
    public Boolean NaAllowed { get; set; }
    [Parameter]
    public EventCallback<int?> BindNumberChanged { get; set; }
    [Parameter]
    public int? BindNumber {
        get { return _BindNumber; }
        set
        {
            if (_BindNumber != value || !initalSet )
            {        
                _BindNumber = value;
                if (_BindNumber == null)
                {
                    vNA = false;
                    DisplayNumber = null;
                }
                else if (_BindNumber == -1)
                {
                    vNA = true;
                    DisplayNumber = null;
                }
                else
                {
                    vNA = false;
                    DisplayNumber = _BindNumber;
                }
                if (initalSet)
                {
                    AmberRange_ConfrimChk = false;
                    await BindNumberChanged.InvokeAsync(_BindNumber);  
                }
                else 
                { 
                    initalSet = true;
                    AmberRange_ConfrimChk = true;
                }
            }
            
            AmberRange_Outside = AmberOutOfRange(_BindNumber);
            RedRange_Outside = RedOutOfRange(_BindNumber);
            // 触发验证
            Validate();
        }
    }

    protected override void OnInitialized()
    {
        _validationMessageStore = new ValidationMessageStore(EditContext);
        _bindNumberField = FieldIdentifier.Create(() => BindNumber);
        // 监听表单验证请求
        EditContext.OnValidationRequested += (s, e) => Validate();
        base.OnInitialized();
    }

    private void Validate()
    {
        _validationMessageStore.Clear(_bindNumberField);

        // 红色范围验证:超出则添加错误
        if (RedRange_Outside)
        {
            _validationMessageStore.Add(_bindNumberField, $"值必须在{RedRange_Min}至{RedRange_Max}范围内");
        }
        // 琥珀色范围验证:超出且未确认则添加错误
        else if (AmberRange_Outside && !AmberRange_ConfrimChk)
        {
            _validationMessageStore.Add(_bindNumberField, $"值超出预期范围{AmberRange_Min}至{AmberRange_Max},请确认该异常值");
        }

        // 通知EditContext验证状态变更
        EditContext.NotifyValidationStateChanged();
    }

    protected Boolean RedOutOfRange(int? newNumber)
    {
        if (newNumber != null && (newNumber != -1 || !NaAllowed))
        {
            if (RedRange_Min != null && RedRange_Min > newNumber)
            {
                return true;
            }
            if (RedRange_Max != null && RedRange_Max < newNumber)
            {
                return true;
            }
        }
        return false;
    }  

    protected Boolean AmberOutOfRange(int? newNumber)
    {
        if (newNumber != null && newNumber != -1 && !RedOutOfRange(newNumber))
        {
            if (AmberRange_Min != null && AmberRange_Min > newNumber)
            {
                return true;
            }
            if (AmberRange_Max != null && AmberRange_Max < newNumber)
            {
                return true;
            }
        }
        return false;
    }

    public int? DisplayNumber { get; set; }

    protected async Task OnValueChanged(ChangeEventArgs e)
    {
        if (e.Value != null)
        {
            string stVal = e.Value.ToString();
            int newNumber;
            if (int.TryParse(stVal, out newNumber))
            {                
                BindNumber = newNumber;
                vNA = false;
            }
            else
            {             
                BindNumber = null;
            }
        }
        Validate();
    }

    protected async Task OnCheckedChanged(ChangeEventArgs e)
    {
        if (e.Value != null)
        {
            string stVal = e.Value.ToString();
            Boolean newChecked;
            if (Boolean.TryParse(stVal, out newChecked))
            {
                if (newChecked)
                {
                    BindNumber = -1;
                }
                else
                {
                    BindNumber = DisplayNumber;
                }
            }
        }
        Validate();
    }

    protected void OnAmberConfirmChanged(ChangeEventArgs e)
    {
        if (e.Value != null && Boolean.TryParse(e.Value.ToString(), out bool isChecked))
        {
            AmberRange_ConfrimChk = isChecked;
            Validate();
        }
    }

    public void Dispose()
    {
        EditContext.OnValidationRequested -= (s, e) => Validate();
    }
}

关键实现说明

  • EditContext与ValidationMessageStore:通过注入EditContext,子组件可直接操作表单验证状态,ValidationMessageStore用于存储自定义验证消息
  • Validate方法:集中处理所有验证逻辑,包括红色范围校验、琥珀色范围+确认复选框校验,验证后通知EditContext更新状态
  • 事件监听:监听表单OnValidationRequested事件,确保表单提交时触发子组件验证;同时在值变更、复选框操作后手动触发验证,保证实时性
  • ValidationMessage组件:显示子组件的验证错误信息,与父表单验证提示风格保持一致

实现后,当子组件验证不通过时,父表单的EditContext会标记为无效,从而阻止表单提交,完全符合需求。


内容的提问来源于stack exchange,提问作者Cain H

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 12:05:54