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),会显示确认复选框,要求用户确认该异常值
当前问题与需求
组件已能正常显示确认复选框,但需要实现一个属于子组件自身的验证器,阻止父表单提交,直到满足以下条件之一:
- 输入值处于琥珀色范围内
- 用户已勾选异常值确认复选框
要求验证逻辑不依赖父模型验证,但能将验证状态反馈给父表单,阻止非法提交。
现有组件代码
<table> <tr> <td> <InputNumber id="tbNumber" class="form-control" Value="@DisplayNumber" ValueExpression="() => DisplayNumber" @oninput="OnValueChanged"/> </td> @if (NaAllowed){ <td> </td> <td> <InputCheckbox id="cb" Value="@vNA" ValueExpression="() => vNA" @oninput="OnCheckedChanged" /> </td> <td> </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> </td> <td> <InputCheckbox id="cb" Value="@vNA" ValueExpression="() => vNA" @oninput="OnCheckedChanged" /> </td> <td> </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
相关产品推荐
相关产品推荐

