Custom validator works in components but not on pages

I have a custom validator that checks for input value that conflicts with existing ones. The validator works from components but fails to catch conflicts when used from pages - that's the only difference I can tell. However RadzenRequiredValidator works correctly in either case, so my custom validator must be missing something.

I include the code of the validator, and usages from component and page. The Radzen.Blazor version is 4.5.0.

Custom validator:

    public class ExistenceValidator : ValidatorBase
    {
        [Parameter]
        public override string Text { get; set; }

        [Parameter]
        public string CurrentName { get; set; }

        [Parameter]
        public IEnumerable<string> ExistingNames { get; set; }

        /// <inheritdoc />
        protected override bool Validate(IRadzenFormComponent component)
        {
            if (ExistingNames.IsNullOrEmpty())
                return true;

            var newName = (string)component.GetValue();
            return !CurrentName.IsNullOrEmpty() && CurrentName == newName || !ExistingNames.Contains(newName);
        }
    }

Usage from a component:

@using CSG.ACPxOpsHub.Common.DataModel

@namespace CSG.ACPxOpsHub.Portal.Shared

@inject DialogService DialogService
@inject NavigationManager NavigationManager

<RadzenTemplateForm TItem="Group" Data="@_group" Submit="@Submit">
    <RadzenFieldset Text="@Title">
        <table class="form-basic" style="width: 100%">
            <tr>
                <th>
                    <RadzenLabel Text="Name" />
                </th>
                <td class="editable">
                    <RadzenTextBox Name="GroupName" @bind-Value="@_group.Name" style="width: 100%"/>
                    <RadzenRequiredValidator Component="GroupName" Text="Enter a group name."/>
                    <ExistenceValidator Component="GroupName" CurrentName="@Name" ExistingNames="@ExistingNames" Text="The group name already exists."/>
                </td>
            </tr>
            <tr>
                <th>
                    <RadzenLabel Text="Description" />
                </th>
                <td class="editable">
                    <RadzenTextBox @bind-Value="@_group.Description" style="width: 100%" />
                </td>
            </tr>
        </table>
    </RadzenFieldset>

    <div class="col-md-12 d-flex align-items-end justify-content-center" style="margin-top: 16px;">
        <RadzenButton ButtonStyle="ButtonStyle.Light" Icon="cancel" style="display: inline-block; margin-left: 10px;" Text="Cancel" Click="@Cancel"/>
        <RadzenButton ButtonType="ButtonType.Submit" Text="Save" Icon="save" />
    </div>
</RadzenTemplateForm>

@code
{
    [Parameter]
    public string Title { get; set; }

    [Parameter]
    public string Name { get; set; }

    [Parameter]
    public string Description { get; set; }

    [Parameter]
    public string[] ExistingNames { get; set; }

    [Parameter]
    public string PagePath { get; set; }

    [Parameter]
    public Action<string, string> Save { get; set; }

    class Group
    {
        public string Name { get; set; }
        public string Description { get; set; }
    }

    private Group _group;

    protected override void OnParametersSet()
    {
        _group = new Group { Name = Name, Description = Description };

        if (PagePath.IsNullOrEmpty())
            PagePath = Constants.BlankPath;
    }

    void Cancel()
    {
        NavigationManager.NavigateTo(PagePath);
    }

    void Submit()
    {
        Save(_group.Name, _group.Description);
        NavigationManager.NavigateTo(PagePath);
    }
}

Usage from a page:

@attribute [Route("/" + Constants.AuthorizationAdminPath + "/{authorizationId:int?}")]

@using CSG.ACPxOpsHub.Portal.Services
@using CSG.ACPxOpsHub.Common.DataModel
@using CSG.Framework.Objects

@inject DialogService DialogService
@inject NavigationManager NavigationManager
@inject AuthorizationService AuthorizationService

<RadzenTemplateForm TItem="Authorization" Data="@_authorization" Submit="@Submit">
    <RadzenFieldset>
        <table class="form-basic" style="width: 100%">
            <tr>
                <th>
                    <RadzenLabel Text="Name" />
                </th>
                <td class="editable">
                    <RadzenTextBox Name="AuthName" @bind-Value="@_authorization.Name" style="width: 100%" />
                    <RadzenRequiredValidator Component="AuthName" Text="Enter a domain-qualified user or group name, e.g. CSGICORP\g_team_legend." />
                    <ExistenceValidator Component="AuthName" CurrentName="@_authorization.Name" ExistingNames="@_existingNames" Text="The user/group name already exists." />
                </td>
            </tr>
            <tr>
                <th>
                    <RadzenLabel Text="Type" />
                </th>
                <td class="editable">
                    <RadzenRadioButtonList @bind-Value=@_authorization.IsGroup TValue="bool">
                        <Items>
                            <RadzenRadioButtonListItem Text="Group" Value="true" />
                            <RadzenRadioButtonListItem Text="User" Value="false" />
                        </Items>
                    </RadzenRadioButtonList>
                </td>
            </tr>
        </table>
    </RadzenFieldset>

    <div class="col-md-12 d-flex align-items-end justify-content-center" style="margin-top: 16px;">
        <RadzenButton ButtonStyle="ButtonStyle.Light" Icon="cancel" style="display: inline-block; margin-left: 10px;" Text="Cancel" Click="@Cancel" />
        <RadzenButton ButtonType="ButtonType.Submit" Text="Save" Icon="save" />
    </div>
</RadzenTemplateForm>

@code
{
    [Parameter]
    public int? AuthorizationId { get; set; }

    private string _pagePath;
    private Authorization _authorization = new() {Id = -1};
    private HashSet<string> _existingNames;

    protected override void OnParametersSet()
    {
        _existingNames = new HashSet<string>(AuthorizationService.GetAllAuthorizations().Select(x => x.Name), StringComparer.CurrentCultureIgnoreCase);

        if (!AuthorizationId.HasValue)
            return;

        _pagePath = $"{Constants.AuthorizationPath}/{AuthorizationId.Value}";
        _authorization = (Authorization)AuthorizationService.GetAuthorization(AuthorizationId.Value).DeepClone();
    }

    void Submit()
    {
        // For some reason the ExistenceValidator fails to stop duplicated value, so we need to check here.
        if (_existingNames.Contains(_authorization.Name))
            DialogService.Alert($"{_authorization.Name} already exists.", "warning");

        // Deep clone so that temporarily edits do not affect cached data objects.
        var entry = (Authorization)_authorization.DeepClone();
        AuthorizationService.SaveAuthorization(entry);

        _pagePath = $"{Constants.AuthorizationPath}/{entry.Id}";
        NavigationManager.NavigateTo(_pagePath);
    }

    void Cancel()
    {
        NavigationManager.NavigateTo(_pagePath);
    }
}

Pages are nothing but routable components so this should not be the problem. What happens when you debug the Validate method? Check if all parameters have the expected values set.

The Validate method does get called, not just once but twice. The difference between my component vs page scenario is how "CurrentName" parameter is bound, which affect the value returned by Validate() ( the first call returns the correct value, but the second call returns a wrong value).

Is there any reason why Validate() is invoked twice when Submit button is clicked?

The Validate() method can be called multiple times - when you submit the form, when you blur the input etc. It should always return correct results.

Thanks for the clarification @korchev! I'll fix my app.

On a separate note - can validator's warning message be configured to display in a different font and/or location (to make it look better on the UI)?

It is up to you to position and style the validator however you want. You can use its Style property to set various CSS attributes.

Thanks for the hint. Setting popup to true give me much better looking UI.