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
state1istrue, andstate2is currentlyfalse.If I proceed, either the
state1function should be executed and thestate2function 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.
ValidationMessage- see github.com/dotnet/aspnetcore/blob/main/src/Components/Web/src/…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.