| 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.Dashboard.Model.Interaction; using Aspire.Dashboard.Model.Markdown; using Aspire.Dashboard.Resources; using Aspire.Dashboard.Utils; using Aspire.DashboardService.Proto.V1; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Forms; using Microsoft.Extensions.Localization; using Microsoft.FluentUI.AspNetCore.Components; using Microsoft.JSInterop; using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons; namespace Aspire.Dashboard.Components.Dialogs; public partial class InteractionsInputDialog : IAsyncDisposable { [Parameter] public InteractionsInputsDialogViewModel Content { get; set; } = default!; [CascadingParameter] public FluentDialog Dialog { get; set; } = default!; [Inject] public required IStringLocalizer<ControlsStrings> ControlsStringsLoc { get; init; } [Inject] public required IStringLocalizer<Resources.Dialogs> Loc { get; init; } [Inject] public required IJSRuntime JS { get; init; } private InteractionsInputsDialogViewModel? _content; private EditContext _editContext = default!; private ValidationMessageStore _validationMessages = default!; private List<InputViewModel> _inputDialogInputViewModels = default!; private Dictionary<InputViewModel, FluentComponentBase?> _elementRefs = default!; private MarkdownProcessor _markdownProcessor = default!; private IJSObjectReference? _jsModule; protected override void OnInitialized() { _editContext = new EditContext(Content); _validationMessages = new ValidationMessageStore(_editContext); _editContext.OnValidationRequested += (s, e) => ValidateModel(); _editContext.OnFieldChanged += (s, e) => InputValueChanged(e.FieldIdentifier); _elementRefs = new(); _markdownProcessor = InteractionMarkdownHelper.CreateProcessor(ControlsStringsLoc); } 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 async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { _jsModule = await JS.InvokeAsync<IJSObjectReference>("import", "./Components/Dialogs/InteractionsInputDialog.razor.js"); // 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(); } else if (firstInputElement is FluentInputBase<SelectViewModel<string>> selectInput) { selectInput.FocusAsync(); } } } } 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 InputValueChanged(FieldIdentifier field) { _validationMessages.Clear(field); if (field.Model is InputViewModel inputModel) { if (IsMissingRequiredValue(inputModel)) { _validationMessages.Add(field, $"{inputModel.Input.Label} is required."); } if (inputModel.Input.UpdateStateOnChange) { _ = Content.OnSubmitCallback(Content.Interaction, true); } } _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, false); } } private async Task CancelAsync() { await Dialog.CancelAsync(); } private async Task ToggleSecretTextVisibilityAsync(InputViewModel inputModel) { inputModel.IsSecretTextVisible = !inputModel.IsSecretTextVisible; if (_jsModule != null && _elementRefs.TryGetValue(inputModel, out var element) && element != null) { await _jsModule.InvokeVoidAsync("togglePasswordVisibility", element.Id); } } private static Icon GetSecretTextIcon(InputViewModel inputModel) { return inputModel.IsSecretTextVisible ? new Icons.Regular.Size16.EyeOff() : new Icons.Regular.Size16.Eye(); } public async ValueTask DisposeAsync() { await JSInteropHelpers.SafeDisposeAsync(_jsModule); } }