using Microsoft.AspNetCore.Components; using Timer = System.Timers.Timer; namespace AIStudio.Components; public partial class DebouncedTextField : MudComponentBase, IDisposable { [Parameter] public string Label { get; set; } = string.Empty; [Parameter] public string Text { get; set; } = string.Empty; [Parameter] public EventCallback TextChanged { get; set; } [Parameter] public Func WhenTextChangedAsync { get; set; } = _ => Task.CompletedTask; [Parameter] public Action WhenTextCanged { get; set; } = _ => { }; [Parameter] public int Lines { get; set; } = 1; [Parameter] public int MaxLines { get; set; } = 1; [Parameter] public Dictionary Attributes { get; set; } = []; [Parameter] public Func ValidationFunc { get; set; } = _ => null; [Parameter] public string HelpText { get; set; } = string.Empty; [Parameter] public string Placeholder { get; set; } = string.Empty; [Parameter] public string Icon { get; set; } = string.Empty; [Parameter] public TimeSpan DebounceTime { get; set; } = TimeSpan.FromMilliseconds(800); [Parameter] public bool Disabled { get; set; } private readonly Timer debounceTimer = new(); private string text = string.Empty; private string lastParameterText = string.Empty; private bool isInitialized; #region Overrides of ComponentBase protected override async Task OnInitializedAsync() { this.text = this.Text; this.lastParameterText = this.Text; this.debounceTimer.AutoReset = false; this.debounceTimer.Interval = this.DebounceTime.TotalMilliseconds; this.debounceTimer.Elapsed += (_, _) => { this.debounceTimer.Stop(); this.InvokeAsync(async () => { // The parent hands this text back with its next render, which is no change of its own, cf. OnParametersSetAsync: this.lastParameterText = this.text; await this.TextChanged.InvokeAsync(this.text); }).Observe($"{nameof(DebouncedTextField)}: notifying about changed text"); this.InvokeAsync(async () => await this.WhenTextChangedAsync(this.text)).Observe($"{nameof(DebouncedTextField)}: handling changed text asynchronously"); this.InvokeAsync(() => this.WhenTextCanged(this.text)).Observe($"{nameof(DebouncedTextField)}: handling changed text"); }; this.isInitialized = true; // A text the field starts with, e.g., one sent from another assistant, is handled like a typed one: if (!string.IsNullOrWhiteSpace(this.text)) this.debounceTimer.Start(); await base.OnInitializedAsync(); } protected override async Task OnParametersSetAsync() { // Ensure the timer uses the latest debouncing interval: if (!this.isInitialized) return; if(Math.Abs(this.debounceTimer.Interval - this.DebounceTime.TotalMilliseconds) > 1) this.debounceTimer.Interval = this.DebounceTime.TotalMilliseconds; // // Only a text which the parent changed itself, e.g., by a reset, is synced and handled like a typed one. Every // other render of the parent leaves the timer alone: restarting it there let its notification render the parent // again, which restarted the timer, and so on, as long as the assistant stayed open. // if (this.Text != this.lastParameterText) { this.text = this.Text; this.lastParameterText = this.Text; this.debounceTimer.Stop(); this.debounceTimer.Start(); } await base.OnParametersSetAsync(); } #endregion private void OnTextChanged(string? value) { this.text = value ?? string.Empty; this.debounceTimer.Stop(); this.debounceTimer.Start(); } #region IDisposable public void Dispose() { try { this.debounceTimer.Stop(); this.debounceTimer.Dispose(); } catch { // ignore } } #endregion }