Blazor模态表单验证:删除表单字段时,需要点击两次取消按钮才能关闭模态窗口

9
在Blazor中,我使用模态对话框进行CRUD操作。当我打开模态对话框以编辑记录时,如果我删除(例如)用户名,然后直接点击“取消”按钮,表单验证仍然会触发,并且模态对话框不会关闭。 Blazor Cancel Validation 我需要再次点击“取消”按钮才能关闭模态对话框。
我知道我可以将取消按钮放在EditForm之外,但这样在对话框关闭之前您会看到一个闪烁的验证消息。我想要我的取消按钮位于模态对话框页脚中我的提交按钮旁边。
是否有任何方法可以在我按下“取消”按钮时覆盖表单验证?我宁愿不使用JavaScript Interop,只是使用纯Blazor。
工作代码示例:
@page "/cancel"
@using System.ComponentModel.DataAnnotations;

<h3>Cancel Validation</h3>

<button type="submit" class="btn btn-primary" @onclick="Login">Login</button>
<hr />
<p>Status: @status</p>

@if (showModal)
{
    <div class="modal" tabindex="-1" role="dialog" style="display:block" id="taskModal">
        <div class="modal-dialog shadow-lg bg-white rounded" role="document">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">Login</h5>
                </div>
                <div class="modal-body">
                    <EditForm Model="user" OnValidSubmit="HandleValidSubmit">
                        <DataAnnotationsValidator />
                        <ValidationSummary />
                        <div class="form-group row">
                            <label class="col-3 col-form-label">Email: </label>
                            <InputText class="col-8 form-control" @bind-Value="user.Email" />
                        </div>
                        <div class="form-group row">
                            <label class="col-3 col-form-label">Password: </label>
                            <InputText type="password" class="col-8 form-control" @bind-Value="user.Password" />
                        </div>
                        <div class="modal-footer">
                            <button type="submit" class="btn btn-primary">Submit</button>
                            <button type="button" class="btn btn-secondary" @onclick="CancelSubmit">Cancel</button>
                        </div>
                    </EditForm>
                </div>
            </div>
        </div>
    </div>
}

@code {

    public class UserLogin
    {
        [Required(ErrorMessage = "Email is required")]
        public string Email { get; set; }

        [Required(ErrorMessage = "Password is required")]
        public string Password { get; set; }
    }

    UserLogin user = new UserLogin();

    bool showModal;
    string status;

    protected override void OnInitialized()
    {
        showModal = false;
        status = "Init";
        // for demo purposes, if you delete user.Email in the login dialog, you'll need to press 'Cancel' 2 times.
        user.Email = "user@example.com";
        user.Password = "12345";
    }

    private void Login()
    {
        status = "Show Login Modal";
        showModal = true;
    }

    private void HandleValidSubmit()
    {
        status = "Valid Submit";
        showModal = false;
    }

    private void CancelSubmit()
    {
        status = "Cancelled"; 
        showModal = false;
    }
}

什么意思是“当我按下取消按钮时覆盖表单验证”?你是在谈论清除验证错误吗?为此,我在editform上使用自定义上下文(而不是EditForm Model="user",我使用EditForm EditContext="ctx",其中ctx是new EditContext(user);,并且我在取消时创建一个新的上下文)。更多信息请参见ASP.NET Core Blazor表单和验证 - dani herrera
我回答了自己的问题,但是我犯了个错误。一开始没注意到,但是 @enet 发了个消息让这件事清楚了。所以我删除了我的回答。我需要进一步调查一下这个问题。 - Jaap
1
@Jaap,当您清除电子邮件文本框并按下取消按钮时,会在触发取消按钮的事件处理程序之前进行验证。这是因为验证发生在电子邮件文本框失去焦点时,而您不能在导致文本框失去焦点之前按下取消按钮。我认为这不是一个错误,而是一个限制,您可以通过使用自定义验证来克服它。 - enet
@enet 是的,这不是Blazor的错误。你说的有道理。如果我在删除字段内容后使用TAB,点击取消就可以直接工作了。但是我想直接使用取消,并且在我点击取消时没有任何验证。我需要花更多时间来解决这个问题。我也在研究流畅的验证,也许那可以解决我的问题,但我不知道。还没确定 :) 热爱编程! - Jaap
@Jaap,我有个坏消息...我尝试使用流畅验证,但问题仍然存在。我会继续调查并让你知道... - enet
显示剩余4条评论
4个回答

6
今天我发现了一个更简单的解决方案...只需使用
而不是

在类似的情况下,只要type=button,你可以将它保留为一个<button>: <button class="btn btn-lg btn-secondary" @onclick="CancelSubmit" type="button">取消</button> - psnx

4

@Jaap,这是一个基于表单验证内部机制的解决方案。我希望这个解决方案能够满足您的需求,直到更好的解决方案被提供。

在Blazor中添加到EditContext对象的表单验证支持分为两个级别:对象级和字段级。当您点击提交按钮时,整个模型将被验证。我们已经看到,提交按钮非常有效,只有在模型字段的值有效时才允许您提交。当您点击取消按钮时,且模型字段的值有效时,对话框会顺利关闭。但是,当一个或多个字段具有无效值(例如,在清除电子邮件字段后),并且您点击取消按钮时,字段级验证立即开始,而取消按钮事件处理程序中的代码没有任何机会做任何事情。这种行为是设计上的,即使我使用Fluent Validation而不是DataAnnotations验证也重复发生。结论:这是我们的限制,而不是系统问题。我们需要投入更多的时间学习Blazor。 我提出的解决方案是在单击“取消”按钮时禁用字段级验证,因此立即关闭对话框而不进行任何验证。

注意:我的代码正在使用Fluent Validation,因为我正在尝试使用Fluent Validation,但同样也可以使用DataAnnotations验证来实现相同的功能。 在两种情况下的代码几乎相同,与Fluent Validation实际无关。请注意,我通过Chris Sainty的Fluent Validation示例代码进行了调整。

UserLogin.cs

public class UserLogin
{
    public string Email { get; set; }
    public string Password { get; set; }
}

UserLoginValidator.cs

public class UserLoginValidator : AbstractValidator<UserLogin>
    {
       public UserLoginValidator()
     {

         RuleFor(user => user.Email).NotEmpty().WithMessage("You must enter an email address");
         RuleFor(user => user.Email).EmailAddress().WithMessage("You must provide a valid email address");
         RuleFor(user => user.Password).NotEmpty().WithMessage("You must enter a password");
         RuleFor(user => user.Password).MaximumLength(50).WithMessage("Password cannot be longer than 50 characters");
    }
 }

FluentValidationValidator.cs

public class FluentValidationValidator : ComponentBase
{
    [CascadingParameter] EditContext CurrentEditContext { get; set; }
    [Parameter] public bool ShouldValidate { get; set; }

    protected override void OnInitialized()
    {
        if (CurrentEditContext == null)
        {
            throw new InvalidOperationException($"{nameof(FluentValidationValidator)} requires a cascading " +
                $"parameter of type {nameof(EditContext)}. For example, you can use {nameof(FluentValidationValidator)} " +
                $"inside an {nameof(EditForm)}.");
        }

        CurrentEditContext.AddFluentValidation(ShouldValidate);
    }
}

EditContextFluentValidationExtensions.cs

 public static class EditContextFluentValidationExtensions
{
    public static EditContext AddFluentValidation(this EditContext editContext, bool shouldValidate)
    {
        if (editContext == null)
        {
            throw new ArgumentNullException(nameof(editContext));
        }

        var messages = new ValidationMessageStore(editContext);

        editContext.OnValidationRequested +=
            (sender, eventArgs) => ValidateModel((EditContext)sender, messages);

        editContext.OnFieldChanged +=
            (sender, eventArgs) => ValidateField(editContext, messages, eventArgs.FieldIdentifier, shouldValidate);

        return editContext;
    }

    private static void ValidateModel(EditContext editContext, ValidationMessageStore messages)
    {
        var validator = GetValidatorForModel(editContext.Model);
        var validationResults = validator.Validate(editContext.Model);

        messages.Clear();
        foreach (var validationResult in validationResults.Errors)
        {
            messages.Add(editContext.Field(validationResult.PropertyName), validationResult.ErrorMessage);
        }

        editContext.NotifyValidationStateChanged();
    }

    private static void ValidateField(EditContext editContext, ValidationMessageStore messages, in FieldIdentifier fieldIdentifier, bool shouldValidate)
    {
        Console.WriteLine(fieldIdentifier.FieldName.ToString());

        if (shouldValidate)
        {
            var properties = new[] { fieldIdentifier.FieldName };
            var context = new FluentValidation.ValidationContext(fieldIdentifier.Model, new PropertyChain(), new MemberNameValidatorSelector(properties));

            var validator = GetValidatorForModel(fieldIdentifier.Model);
            var validationResults = validator.Validate(context);

            messages.Clear(fieldIdentifier);

            foreach (var validationResult in validationResults.Errors)
            {
                messages.Add(editContext.Field(validationResult.PropertyName), validationResult.ErrorMessage);
            }

            editContext.NotifyValidationStateChanged();
        }
    }

    private static IValidator GetValidatorForModel(object model)
    {
        var abstractValidatorType = typeof(AbstractValidator<>).MakeGenericType(model.GetType());
        var modelValidatorType = Assembly.GetExecutingAssembly().GetTypes().FirstOrDefault(t => t.IsSubclassOf(abstractValidatorType));
        var modelValidatorInstance = (IValidator)Activator.CreateInstance(modelValidatorType);

        return modelValidatorInstance;
    }
}

Cancel.razor

@page "/cancel"
@using System.ComponentModel.DataAnnotations;

<h3>Cancel Validation</h3>

<button type="submit" class="btn btn-primary" @onclick="Login">Login</button>
<hr />
<p>Status: @status</p>

@if (showModal)
{
    <div class="modal" tabindex="-1" role="dialog" style="display:block" id="taskModal">
        <div class="modal-dialog shadow-lg bg-white rounded" role="document">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">Login</h5>
                </div>
                <div class="modal-body">
                    <EditForm Model="user" OnValidSubmit="HandleValidSubmit">
                        @*<DataAnnotationsValidator />*@
                        <FluentValidationValidator ShouldValidate="false" />
                        @*<ValidationSummary />*@
                        <div class="form-group row">
                            <label class="col-3 col-form-label">Email: </label>
                            <InputText class="col-8 form-control" @bind-Value="user.Email" />
                            <ValidationMessage For="@(() => user.Email)" />
                        </div>
                        <div class="form-group row">
                            <label class="col-3 col-form-label">Password: </label>
                            <InputText type="password" class="col-8 form-control" @bind-Value="user.Password" />
                            <ValidationMessage For="@(() => user.Password)" />
                        </div>
                        <div class="modal-footer">
                            <button type="submit" class="btn btn-primary">Submit</button>
                            <button type="button" class="btn btn-secondary" @onclick="CancelSubmit">Cancel</button>
                        </div>
                    </EditForm>
                </div>
            </div>
        </div>
    </div>
}

@code {
    UserLogin user = new UserLogin();

    bool showModal;
    string status;

    protected override void OnInitialized()
    {
        showModal = false;
        status = "Init";
        // for demo purposes, if you delete user.Email in the login dialog, you'll need to press 'Cancel' 2 times.
        user.Email = "user@example.com";
        user.Password = "12345";
    }

    private void Login()
    {
        status = "Show Login Modal";

        showModal = true;
    }

    private void HandleValidSubmit()
    {
        status = "Valid Submit";
        showModal = false;
    }

    private void CancelSubmit()
    {
        Console.WriteLine("CancelSubmit");

        status = "Cancelled";
        showModal = false;
    }
}

请注意,FluentValidationValidator组件有一个名为ShouldValidate的属性,我们将其设置为false以删除字段级验证。请遵循执行流程,它非常简单。我几乎没有做什么来解决这个问题,这让我想到可能有一种更短更好的方法来解决它。 您可能需要安装Fluent Validation软件包... 祝你好运...

感谢您提供详细的答案和示例代码。还有,感谢您所投入的时间。这肯定是一个改进,我同意您所说的一切。 - Jaap
@enet 我注意到在 FluidValidator v9.3.0 中,以下代码行在 ValidateModel 扩展方法中引起了错误:var validationResults = validator.Validate(editContext.Model);。有什么解决办法吗? - DoomerDGR8
@enet,CurrentEditContext.AddFluentValidation(ShouldValidate); 这一行代码也是在 FluentValidationValidator -> OnInitialized() 中的一个错误。 - DoomerDGR8
@Hassan Gulzar,是的,你说得对。我已经更新了我的代码示例,使用v9.3.0,并且得到了相同的错误。这需要对代码进行重大更改,而作者不是我。这里是同一作者的链接和代码示例,我的答案依赖于它。是的,他的代码也包含了重大更改,并考虑了FluentValidation的新版本:https://github.com/Blazored/FluentValidation/tree/main/src/Blazored.FluentValidation 我会尽快适应这些变化并更新我的答案。 - enet

2
所以我在这里发现了一些新东西:ASP.NET Core Blazor表单和验证 默认情况下,<ValidationSummary style="@displaySummary" />是禁用的,当存在InvalidSubmit时才启用。
这不是一个理想的解决方案,因为当用户(1)删除字段内容然后(2)点击“提交”时,验证会启动,但如果用户在同一字段中输入有效内容(3),则用户必须点击两次“提交”。但是,目前我可以接受这种情况,因为这种情况并不经常发生。
新的工作代码:
@page "/cancel"
@using System.ComponentModel.DataAnnotations;

<h3>Cancel Validation</h3>

<button type="submit" class="btn btn-primary" @onclick="OpenDialog">Open Dialog</button>
<hr />
<div>@((MarkupString)status)</div>

@if (showModal)
{
    <div class="modal" tabindex="-1" role="dialog" style="display:block" id="taskModal">
        <div class="modal-dialog shadow-lg bg-white rounded" role="document">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">Cancel Test</h5>
                    <button type="button" class="close" @onclick="CancelSubmit"><span aria-hidden="true">&times;</span></button>
                </div>
                <div class="modal-body">
                    <div>
                        <EditForm Model="@user" OnValidSubmit="HandleValidSubmit" OnInvalidSubmit="HandleInValidSubmit">
                            <DataAnnotationsValidator />
                            <ValidationSummary style="@displaySummary" />
                            <div class="form-group row">
                                <label class="col-3 col-form-label">Email</label>
                                <InputText class="col-8 form-control" @bind-Value="user.Email" />
                            </div>
                            <div class="form-group row">
                                <label class="col-3 col-form-label">Password</label>
                                <InputText type="password" class="col-8 form-control" @bind-Value="user.Password" />
                            </div>
                            <div class="modal-footer">
                                <button type="submit" class="btn btn-primary">Submit</button>
                                <button type="button" class="btn btn-secondary" @onclick="CancelSubmit">Cancel</button>
                            </div>
                        </EditForm>
                    </div>
                </div>
            </div>
        </div>
    </div>
}

@code {

    public class UserLogin
    {
        [Required(ErrorMessage = "Email is required")]
        public string Email { get; set; }

        [Required(ErrorMessage = "Password is required")]
        public string Password { get; set; }
    }

    private UserLogin user = new UserLogin();

    bool showModal = false;
    private string displaySummary = "display:none";
    string status = "";

    protected override void OnInitialized()
    {
        InitUser();
    }

    private void InitUser()
    {
        // for demo purposes, think of it as an 'edit dialog' in CRUD operation.
        status += "Init User";
        user.Email = "user@example.com";
        user.Password = "12345";
        displaySummary = "display:none";
    }

    private void OpenDialog()
    {
        status += "<br />Open User Dialog";
        showModal = true;
    }

    private void HandleValidSubmit()
    {
        status += "<br />Valid Submit";
        displaySummary = "display:none";
        showModal = false;
    }

    private void HandleInValidSubmit()
    {
        displaySummary = "display:block";
        status += "<br />Invalid Submit";
    }

    private void CancelSubmit()
    {
        status += "<br />Cancelled<br /><br />";
        InitUser(); // for demo purposes so you can test it multiple times
        showModal = false;
    }
}

1
抱歉,@Jaap,您的解决方案存在问题,因为它没有应对这个“故障”的原因,而是将问题从取消按钮转移到提交按钮。我认为反过来更好。您的解决方案或解决方法,无论多么糟糕,至少必须在不让用户注意到任何故障的情况下解决问题。无论如何,在学习和调查表单验证的内部后,我找到了一个有效的解决方案,至少在外部表现出色...稍后我会发布一些说明... - enet
@enet 它并没有将问题从“取消”转移到“提交”,该行为仍然存在。这个问题之前已经存在,但我之前没有提到过。但是,是的,这不是一个理想的解决方案。我同意,这就是为什么我在我的回答中提到了它。但我期待着看到更好的解决方案。我正在考虑将其作为 Github 问题,因为我认为它应该可以直接使用,而无需任何额外的代码。 - Jaap
将问题从“取消”转移到“提交”上,表面上解决了问题。在Github上,他们会将您重定向回Stackoverflow。它不能直接使用...为什么?这是软件,不是魔法。 - enet
你说得对,我宁愿不使用Github。我已经在Twitter上向一些开发人员提到了这个问题。虽然你说得没错,它并不是魔法,但自从我开始编码以来,它对我来说就像魔法一样 :) - Jaap
"对我来说,这感觉就像魔法 :) ",你的意思是你喜欢它。我也很喜欢... - enet

1

我在我的EditForm中放置了2个处理程序:一个用于OnValidSubmit(即使表单有效,也应该处理Cancel),另一个用于OnInvalidSubmit(用于在表单无效时处理取消,因此有些重复,但它能工作!)。 (示例显示使用Radzen卡片进行确认对话框,并且似乎多余的Close和Confirm方法,我仍在努力弄清楚....)。

<EditForm Model="@selectedUnit" OnValidSubmit="@Update" OnInvalidSubmit="@AreYouSure">
    <Microsoft.AspNetCore.Components.Forms.DataAnnotationsValidator />
    <Microsoft.AspNetCore.Components.Forms.ValidationSummary />

  <button type="submit" @onclick="@(() => selectedUnit.Action = "Save")" class="btn btn-primary">Save</button>
    <button @onclick="@(() => selectedUnit.Action = "Cancel")" class="btn btn-primary" >Cancel</button>

@code{
  public async void AreYouSure()
    {
// should use if to determine if they clicked Save or Cancel... this is the code for if Cancel //

        await dialogService.OpenAsync("Are you sure?", ds =>@<RadzenCard Style="padding: 20px;">
        <p Style="margin-bottom: 10px;">Changes will not be saved!</p>
        <div class="row">
            <div class="col-md-12">
                <RadzenButton Text="Confirm Cancel Changes" Click="() => Confirm(true)" Style="margin-bottom: 10px; width: auto" />
                <RadzenButton Text="Oops! Continue Input" Click="() => ds.Close(false)" ButtonStyle="ButtonStyle.Secondary" Style="margin-bottom: 10px; width: auto" />
            </div>
        </div>
    </RadzenCard>);
        return;
    }

    private async Task Update()
    {
        if (selectedAppl.Action == "Cancel")
        {
            await dialogService.OpenAsync("Are you sure?", ds =>@<RadzenCard Style="padding: 20px;">
    <p Style="margin-bottom: 10px;">Changes will not be saved!</p>
    <div class="row">
        <div class="col-md-12">
            <RadzenButton Text="Confirm Cancel Changes" Click="()=> Confirm(true)" Style="margin-bottom: 10px; width: auto" />
            <RadzenButton Text="Oops! Continue Input" Click="()=> ds.Close(false)" ButtonStyle="ButtonStyle.Secondary" Style="margin-bottom: 10px; width: auto" />
        </div>
    </div>
</RadzenCard>);
return;
};

   public void Confirm(bool confirmed)
    {
        if (confirmed)
        { ReturnToList(); }
        else
        {
            return;
        }
    }


    void Close(bool confirmed)
    {
        if (confirmed)
        { ReturnToList(); }
     
    }

  public void ReturnToList()
    {

        NavigationManager.NavigateTo("/Data/Counter");

    }






网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接