1

In a Blazor web app, I needed to conditionally handle multiple <ValidationMessages> because they're not bound to a single function.

At the moment, I have two instances of <ValidationMessages>.

This is the algorithm I should achieve:

  • The first one should be executed on a button click to trigger a state change for this function.

    For example, a button is clicked, and state1 is true, and state2 is currently false.

    If I proceed, either the state1 function should be executed and the state2 function skipped, or vice versa.

  • It should handle the validation messages correspondingly with their particular functions.

But this isn't achieved on my current setup.

Data class model:

[Keyless]
public class SomeModel
{
  [Required]
  [ValidateComplexType]
  public SomeOtherModel SomeOtherModel { get; set; }

  [Required]
  public string? SelectedRoleID { get; set; }

  [Required(ErrorMessage = "Required field.")]
  public string? SelectedRole { get; set; }

  [Required]
  public string? SelectedRemarkID { get; set; }

  [Required(ErrorMessage = "Required field.")]
  public string? SelectedRemark { get; set; }

  public SomeModel() // for demonstration purposes only XD, I observed that it is not currently in use.
  {
    SomeOtherModel = new SomeOtherModel();
  }
}

[Keyless]
public class SomeOtherModel
{
  [Required]
  public string? SomeField { get; set; }
  // other fields here...
}

Razor page

Client-side

@page "/users/review-user"
@page "/users/review/{Id}/user"
@rendermode InteractiveServer
@using Microsoft.EntityFrameworkCore
@using MyProject.Models.SQLServer
@using MyProject.Data.SQLServer
@implements IAsyncDisposable
@inject IDbContextFactory<MyProject.Data.SQLServer.SQLServerContext> DbFactory
@inject NavigationManager NavigationManager
@inject IJSRuntime JS

<PageTitle>Details</PageTitle>

<h3>Review User</h3>

<script src="lib/customjs/reviewuser.js"></script>

<div>
    <hr />
    @if (plainTextModel is null)
    {
        <div class="position-absolute top-50 start-50 translate-middle">
            <div class="spinner-grow text-danger spinner-grow-xxl" role="status">
                <span class="visually-hidden">Loading...</span>
            </div>
        </div>
    }
    else
    {
        <EditForm Model="SomeModel" OnValidSubmit="VerifyUser" FormName="reviewUser" Enhance>
            <ObjectGraphDataAnnotationsValidator />
            <div class="container-fluid overflow-x-auto mw-40">
                <div class="row py-1 border rounded border-secondary-subtle text-center my-1">

                    <!-- Left side: user info -->
                    <div class="col-6 d-flex flex-column justify-content-center align-items-center">
                        <dl class="row m-0">
                            <dt class="col-sm-3 text-start p-0">#</dt>
                            <dd class="col-sm-9">@rowNum</dd>

                            <dt class="col-sm-3 text-start p-0">Role</dt>
                            @if (isModifyUser)
                            {
                                <dd class="col-sm-9">

                                    @if (userRoles is null)
                                    {
                                        <div class="position-absolute top-50 start-50 translate-middle">
                                            <div class="spinner-grow text-danger spinner-grow-sm" role="status">
                                                <span class="visually-hidden">Loading...</span>
                                            </div>
                                        </div>
                                    }
                                    else
                                    {
                                        <Dropdown Color="DropdownColor.Light" Size="DropdownSize.Small">
                                            <DropdownToggleButton>@(SomeModel.SelectedRole ?? "Please select a role")
                                            </DropdownToggleButton>
                                            <DropdownMenu>
                                                @foreach (var role in userRoles)
                                                {
                                                    <DropdownItem @onclick="() => OnRoleItemSelected(role)" Active="@role.IsActive"
                                                        Disabled="@(role.RoleType == "Programmer")">@role.RoleType
                                                    </DropdownItem>
                                                }
                                            </DropdownMenu>
                                        </Dropdown>
                                        <ValidationMessage For="() => SomeModel.SelectedRole" class="text-danger" />
                                    }

                                </dd>
                            }
                            else
                            {
                                <dd class="col-sm-9">@plainTextModel.RoleType</dd>
                            }
                        </dl>

                        @if (isModifyUser || isReevaluateUser)
                        {
                            <div class="row py-1 text-center m-1 w-100">
                                <div class="col-6 d-flex flex-column justify-content-center align-items-center ps-0">
                                    <button type="submit" class="btn btn-primary w-100 text-wrap">Save Changes</button>
                                </div>
                                <div class="col-6 d-flex flex-column justify-content-center align-items-center pe-0">
                                    <button @onclick="Discard" type="button"
                                        class="btn btn-outline-danger w-100 text-wrap">Discard</button>
                                </div>
                            </div>
                        }

                    </div>

                    <!-- Right side: actions -->
                    <div class="col-6 d-flex flex-column justify-content-center align-items-center gap-3 py-3">
                        <dl class="row m-0">
                            <dt class="col-sm-3 text-start p-0">Remarks</dt>
                            @if (isReevaluateUser)
                            {
                                <dd class="col-sm-9">
                                    @if (userRemarks is null)
                                    {
                                        <div class="position-absolute top-50 start-50 translate-middle">
                                            <div class="spinner-grow text-danger spinner-grow-sm" role="status">
                                                <span class="visually-hidden">Loading...</span>
                                            </div>
                                        </div>
                                    }
                                    else
                                    {
                                        <Dropdown Color="DropdownColor.Light" Size="DropdownSize.Small" Class="w-100">
                                            <DropdownToggleButton Class="text-truncate">@(SomeModel.SelectedRemark ??
                                                                                    "Please select a remark")</DropdownToggleButton>
                                            <DropdownMenu data-bs-display="static">
                                                @foreach (var remark in userRemarks)
                                                {
                                                    <DropdownItem @onclick="() => OnRemarkItemSelected(remark)">@remark.Remarks
                                                    </DropdownItem>
                                                }
                                            </DropdownMenu>
                                        </Dropdown>
                                        <ValidationMessage For="() => SomeModel.SelectedRemark" class="text-danger" />
                                    }
                                </dd>
                            }
                            else
                            {
                                @if (plainTextModel.Remarks == null)
                                {
                                    <dd class="col-sm-9 fw-bold">-</dd>
                                }
                                else
                                {
                                    <dd class="col-sm-9">@plainTextModel.Remarks</dd>
                                }
                            }
                        </dl>

                        <button @onclick="ReevaluateUser" type="button" class="btn btn-danger w-100 text-wrap">Reevaluate
                            User</button>
                        <button @onclick="ModifyUser" type="button" class="btn btn-warning w-100 text-wrap">Modify User
                            Role</button>
                    </div>

                </div>

                @if (reviewUserListImageSingle is null)
                {
                    <div class="position-absolute top-50 start-50 translate-middle">
                        <div class="spinner-grow text-danger spinner-grow-xxl" role="status">
                            <span class="visually-hidden">Loading...</span>
                        </div>
                    </div>
                }
                else
                {

                    <div class="container-fluid p-0">
                        <div class="row gap-2">

                            <div class="col">
                                <div class="row py-1 border rounded border-secondary-subtle text-center my-2">
                                    @*other code here*@
                                </div>
                            </div>

                            <div class="col">
                                <div class="row py-1 border rounded border-secondary-subtle text-center my-2">
                                    @*other code here*@
                                </div>
                            </div>

                            <div class="col">
                                <div class="row py-1 border rounded border-secondary-subtle text-center my-2">
                                    @*other code here*@
                                </div>
                            </div>

                        </div>
                    </div>
                }
            </div>
        </EditForm>
    }
</div>

<div id="liveToast" class="toast align-items-center text-bg-primary border-0 position-fixed bottom-0 end-0 mb-3 me-3"
    role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="1500">
    <div class="d-flex">
        <div class="toast-body">
            Successfully modified!
        </div>
        <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"
            aria-label="Close"></button>
    </div>
</div>

<div id="liveToastError"
    class="toast align-items-center text-bg-danger border-0 position-fixed bottom-0 end-0 mb-3 me-3" role="alert"
    aria-live="assertive" aria-atomic="true" data-bs-delay="1500">
    <div class="d-flex">
        @if (errorMessage is not null)
        {
            <div class="toast-body">
                @errorMessage
            </div>
        }
        else
        {
            <div class="toast-body">
                Someting went wrong. Please try again later.
            </div>
        }
        <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"
            aria-label="Close"></button>
    </div>
</div>

<div class="modal fade" id="verifyModal" data-bs-keyboard="false" tabindex="-1" aria-labelledby="verifyModalLabel"
    aria-hidden="true">
    <div class="modal-dialog modal-sm">
        <div class="modal-content">
            <div class="modal-body">
                Are you sure you want to verify this user?
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
                <EditForm method="post" Model="SomeModel" OnValidSubmit="VerifyUser" FormName="reviewUser"
                    Enhance>
                    @* <input type="hidden" name="ReturnUrl" value="@currentUrl" /> *@
                    <button type="submit" class="btn btn-danger">Confirm</button>
                </EditForm>
            </div>
        </div>
    </div>
</div>

Server-side

@code {
    private SomeModel SomeModel = new SomeModel();
    private PlainTextModel? plainTextModel = new();
    private UserListImageSingleDM? reviewUserListImageSingle = new();
    private List<UserRoles>? userRoles = new();
    private List<UserRemarks>? userRemarks = new();
    private SQLServerContext context = default!;

    [SupplyParameterFromQuery(Name = "status")]
    private bool status { get; set; }

    [SupplyParameterFromQuery(Name = "row")]
    private long rowNum { get; set; }

    [Parameter]
    public string? Id { get; set; }

    private bool isModifyUser = false;
    private bool isReevaluateUser = false;
    private string? errorMessage;
    private string? output;

    protected override async Task OnInitializedAsync()
    {
        context = DbFactory.CreateDbContext();
        await LoadPlainText();
    }

    private async Task LoadPlainText()
    {
        var sQLServerHelper = new SQLServerHelper(context);
        var spParamPlainText = SQLServerInnerHelper.GlobalMethodRazorPageParam(id: Id, key:
        "REVIEW_USER_PLAINTEXT");
        plainTextModel = await sQLServerHelper.PlainTextAsync("myStoredProcedure", spParamPlainText);
        // TODO: Let this load first.

        if (plainTextModel is null)
        {
            NavigationManager.NavigateTo("notfound");
        }
        StateHasChanged();
    }

    private async Task ModifyUser()
    {
        SomeModel.SelectedRemark = null;
        isModifyUser = true;
        isReevaluateUser = false;
        // data fetch for userRoles
    }

    private async Task ReevaluateUser()
    {
        SomeModel.SelectedRoleID = null;
        SomeModel.SelectedRole = null;
        isReevaluateUser = true;
        isModifyUser = false;
        // data fetch for userRemarks
    }

    private void Discard()
    {
        isModifyUser = false;
        isReevaluateUser = false;
    }

    private void OnRoleItemSelected(UserRoles item)
    {
        SomeModel.SelectedRoleID = item.RoleID ?? string.Empty;
        SomeModel.SelectedRole = item.RoleType ?? string.Empty;
        SetActiveRole(SomeModel.SelectedRoleID);
    }

    private void OnRemarkItemSelected(UserRemarks item)
    {
        SomeModel.SelectedRemarkID = item.RemarkID ?? string.Empty;
        SomeModel.SelectedRemark = item.Remarks ?? string.Empty;
    }

    private void SetActiveRole(string id)
    {
        if (userRoles is null) return;

        foreach (var role in userRoles)
            role.IsActive = role.RoleID == id;
    }

    private async Task VerifyUser()
    {
        if (isModifyUser)
        {
            var sQLServerHelper = new SQLServerHelper(context);
            var spParam = SQLServerInnerHelper.ManageUsersDataWOutputPageParams(userID: Id, roleID:
            SomeModel.SelectedRoleID, functionKey:
            "TEST_KEY_1");
            output = await sQLServerHelper.StringOutputAsync("myStoredProcedure", spParam);
            if (output is not null && output == "SUCCESSFUL")
            {
                await JS.InvokeVoidAsync("showLiveToast");
            }
            else if (output is not null && output != "SUCCESSFUL")
            {
                errorMessage = null;
                await JS.InvokeVoidAsync("showLiveToastError");
            }
            else
            {
                errorMessage = null;
                await JS.InvokeVoidAsync("showLiveToastError");
            }
        }
        else if (isReevaluateUser)
        {
            var sQLServerHelper = new SQLServerHelper(context);
            var spParam = SQLServerInnerHelper.ManageUsersDataWOutputPageParams(userID: Id, remarkID:
            SomeModel.SelectedRemarkID, functionKey:
            "TEST_KEY_2");
            output = await sQLServerHelper.StringOutputAsync("myStoredProcedure", spParam);
            if (output is not null && output == "SUCCESSFUL")
            {
                await JS.InvokeVoidAsync("showLiveToast");
            }
            else if (output is not null && output != "SUCCESSFUL")
            {
                errorMessage = null;
                await JS.InvokeVoidAsync("showLiveToastError");
            }
            else
            {
                errorMessage = null;
                await JS.InvokeVoidAsync("showLiveToastError");
            }
        }
        else
        {
            var sQLServerHelper = new SQLServerHelper(context);
            var spParam = SQLServerInnerHelper.ManageUsersDataWOutputPageParams(userID: Id, userIDModifier: "DevQt",
            functionKey:
            "TEST_KEY_3");
            output = await sQLServerHelper.StringOutputAsync("myStoredProcedure", spParam);
            if (output is not null && output == "SUCCESSFUL")
            {
                await JS.InvokeVoidAsync("showLiveToast");
            }
            else if (output is not null && output != "SUCCESSFUL")
            {
                errorMessage = null;
                await JS.InvokeVoidAsync("showLiveToastError");
            }
            else
            {
                errorMessage = null;
                await JS.InvokeVoidAsync("showLiveToastError");
            }
        }
    }

    public async ValueTask DisposeAsync() // this is important
    {
        await context.DisposeAsync();
    }
}

I was expecting this to run properly since it is conditionally managed through states: isModifyUser and isReevaluateUser. But it's not.

It works only if I commented out these classes (for demonstration purposes only):

<ObjectGraphDataAnnotationsValidator />
<ValidationMessage For="() => SomeModel.SelectedRole" class="text-danger" />
<ValidationMessage For="() => SomeModel.SelectedRemark" class="text-danger" />

But I needed those classes to implement validation in the simplest way possible.

The concept of my project is inspired by this thread:

Moreover, this portion of my Blazor web app project will serve as the main reference for its other modules.

11
  • 1
    You should not still be using the Microsoft.AspNetCore.Components.DataAnnotations.Validation package. It was experimental and was last updated in 2020. Note this section from MsDocs : learn.microsoft.com/en-us/aspnet/core/blazor/forms/…. It's dead but not quite buried yet. Commented Aug 28 at 6:20
  • 1
    You can probably write you own version of ValidationMessage - see github.com/dotnet/aspnetcore/blob/main/src/Components/Web/src/… Commented Aug 28 at 6:28
  • @MrCakaShaunCurtis, could you please provide your answer so I can review and compare it? Commented Aug 28 at 6:53
  • @MrCakaShaunCurtis, regarding your first comment, I have no idea how to handle a complex model, aside from using that package. Commented Aug 28 at 6:56
  • 1
    On what to use instead of ObjectGraphDataAnnotationsValidator, look up Blazor Fluent Validation [you'll find plenty of information on various Blazor specific implementations]. Also consider flattening your edit model. Your edit model doesn't need to be the same as your data model. Mine never are. If you have a complex object [with domain level business rules to apply], then consider an aggregate. Commented Aug 28 at 9:21

1 Answer 1

0

To achieve the given goal: "Conditionally handle multiple ValidationMessages with a ValidateComplexType model in an EditForm". This required moderate refactoring and code adjustments.


Refactor data class model:

[Keyless]
public class SomeModel
{
  [Required]
  [ValidateComplexType]
  public SomeOtherModel SomeOtherModel { get; set; }

  public string? SelectedRoleID { get; set; }

  public string? SelectedRole { get; set; }

  public string? SelectedRemarkID { get; set; }

  public string? SelectedRemark { get; set; }

  public SomeModel() // for demonstration purposes only XD, I observed that it is not currently in use.
  {
    SomeOtherModel = new SomeOtherModel();
  }
}

[Keyless]
public class SomeOtherModel
{
  public string? SomeField { get; set; }
  // other fields here...
}

For this program refactoring to work, the particular [Required] annotations should be removed because they are handled programmatically, as will be shown below. If SomeOtherModel will be used, the [Required] annotation instances should be removed from its field as well.


Refactor Razor page:

<EditForm EditContext="editContext" OnValidSubmit="VerifyUser" FormName="reviewUser" Enhance>

The EditForm should be managed with EditContext instead of a direct binding with its data class model.


@code {
    private SomeModel SomeModel = new SomeModel();
    private EditContext? editContext;
    private ValidationMessageStore? messageStore;

    protected override async Task OnInitializedAsync()
    {
        editContext = new EditContext(SomeModel);
        messageStore = new ValidationMessageStore(editContext);

        editContext.OnValidationRequested += HandleValidationRequested;
        context = DbFactory.CreateDbContext();
        await LoadPlainText();
    }

    private void HandleValidationRequested(object? sender, ValidationRequestedEventArgs e)
    {
        messageStore!.Clear();

        if (isModifyUser)
        {
            if (string.IsNullOrWhiteSpace(SomeModel.SelectedRole))
            {
                messageStore.Add(() => SomeModel.SelectedRole, "Required field.");
            }
        }
        else if (isReevaluateUser)
        {
            // Only validate SomeModel.SelectedRemark if SomeModel.SelectedRole is valid
            if (string.IsNullOrWhiteSpace(SomeModel.SelectedRemark))
            {
                messageStore.Add(() => SomeModel.SelectedRemark, "Required field.");
            }
        }
        else
        {
            messageStore.Add(() => SomeModel.SelectedRole, "Required field.");
            messageStore.Add(() => SomeModel.SelectedRemark, "Required field.");
        }
    }
}

The above code snippet is the handler to toggle message validation based on a condition. That's the main reason why the [Required] annotations on some fields are not necessary anymore. This should work as expected based on my criteria (since this is a question-then-self-answer thread).

EDIT: This context is not primarily utilizing the SomeOtherModel data class model at the moment

For this particular context, the <ObjectGraphDataAnnotationsValidator /> should be replaced with <DataAnnotationsValidator /> in order for this program to work. This is because I tested it thoroughly and compared the behavior of the program based on these classes.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.