File: Components\Dialogs\InteractionsInputDialog.razor.cs
Web Access
Project: src\src\Aspire.Dashboard\Aspire.Dashboard.csproj (Aspire.Dashboard)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Dashboard.Model;
using Aspire.DashboardService.Proto.V1;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.FluentUI.AspNetCore.Components;
 
namespace Aspire.Dashboard.Components.Dialogs;
 
public partial class InteractionsInputDialog
{
    [Parameter]
    public InteractionsInputsDialogViewModel Content { get; set; } = default!;
 
    [CascadingParameter]
    public FluentDialog Dialog { get; set; } = default!;
 
    private InteractionsInputsDialogViewModel? _content;
    private EditContext _editContext = default!;
    private ValidationMessageStore _validationMessages = default!;
    private List<InputViewModel> _inputDialogInputViewModels = default!;
    private Dictionary<InputViewModel, FluentComponentBase?> _elementRefs = default!;
 
    protected override void OnInitialized()
    {
        _editContext = new EditContext(Content);
        _validationMessages = new ValidationMessageStore(_editContext);
 
        _editContext.OnValidationRequested += (s, e) => ValidateModel();
        _editContext.OnFieldChanged += (s, e) => ValidateField(e.FieldIdentifier);
 
        _elementRefs = new();
    }
 
    protected override void OnParametersSet()
    {
        if (_content != Content)
        {
            _content = Content;
            _inputDialogInputViewModels = Content.Inputs.Select(input => new InputViewModel(input)).ToList();
 
            // Initialize keys for @ref binding.
            // Do this in case Blazor tries to get the element from the dictionary.
            // If the input view model isn't in the dictionary then it will throw a KeyNotFoundException.
            _elementRefs.Clear();
            foreach (var inputVM in _inputDialogInputViewModels)
            {
                _elementRefs[inputVM] = null;
            }
 
            AddValidationErrorsFromModel();
 
            Content.OnInteractionUpdated = async () =>
            {
                AddValidationErrorsFromModel();
 
                await InvokeAsync(StateHasChanged);
            };
        }
    }
 
    protected override Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            // Focus the first input when the dialog loads.
            if (_inputDialogInputViewModels.Count > 0 && _elementRefs.TryGetValue(_inputDialogInputViewModels[0], out var firstInputElement))
            {
                if (firstInputElement is FluentInputBase<string> textInput)
                {
                    textInput.FocusAsync();
                }
                else if (firstInputElement is FluentInputBase<bool> boolInput)
                {
                    boolInput.FocusAsync();
                }
                else if (firstInputElement is FluentInputBase<int?> numberInput)
                {
                    numberInput.FocusAsync();
                }
            }
        }
 
        return Task.CompletedTask;
    }
 
    private void AddValidationErrorsFromModel()
    {
        for (var i = 0; i < Content.Inputs.Count; i++)
        {
            var inputModel = Content.Inputs[i];
            var inputViewModel = _inputDialogInputViewModels[i];
 
            inputViewModel.SetInput(inputModel);
 
            var field = GetFieldIdentifier(inputViewModel);
            foreach (var validationError in inputModel.ValidationErrors)
            {
                _validationMessages.Add(field, validationError);
            }
        }
    }
 
    private void ValidateModel()
    {
        _validationMessages.Clear();
 
        foreach (var inputModel in _inputDialogInputViewModels)
        {
            var field = GetFieldIdentifier(inputModel);
            if (IsMissingRequiredValue(inputModel))
            {
                _validationMessages.Add(field, $"{inputModel.Input.Label} is required.");
            }
        }
 
        _editContext.NotifyValidationStateChanged();
    }
 
    private void ValidateField(FieldIdentifier field)
    {
        _validationMessages.Clear(field);
 
        if (field.Model is InputViewModel inputModel)
        {
            if (IsMissingRequiredValue(inputModel))
            {
                _validationMessages.Add(field, $"{inputModel.Input.Label} is required.");
            }
        }
 
        _editContext.NotifyValidationStateChanged();
    }
 
    private static FieldIdentifier GetFieldIdentifier(InputViewModel inputModel)
    {
        var fieldName = inputModel.Input.InputType switch
        {
            InputType.Boolean => nameof(inputModel.IsChecked),
            InputType.Number => nameof(inputModel.NumberValue),
            _ => nameof(inputModel.Value)
        };
        return new FieldIdentifier(inputModel, fieldName);
    }
 
    private static bool IsMissingRequiredValue(InputViewModel inputModel)
    {
        return inputModel.Input.Required &&
            inputModel.Input.InputType != InputType.Boolean &&
            string.IsNullOrWhiteSpace(inputModel.Value);
    }
 
    private async Task SubmitAsync()
    {
        // The workflow is:
        // 1. Validate the model that required fields are present.
        // 2. Run submit callback. Sends input values to the server.
        // 3. If validation on the server passes, a completion dialog is send back to the client which closes the dialog.
        // 4. If validation fails, the server sends back validation errors which are displayed in the dialog.
        if (_editContext.Validate())
        {
            await Content.OnSubmitCallback(Content.Interaction);
        }
    }
 
    private async Task CancelAsync()
    {
        await Dialog.CancelAsync();
    }
}