Skip to content

Commit

Permalink
[FluentInputBase] Use Debouncer instead of PeriodicTimer for debo…
Browse files Browse the repository at this point in the history
…uncing `ValueChanged` handler with `ImmediateDelay`. (#2042)

* fix(#2030):  debounce using ImmediateDelay can throw an exception because of a race condition with PeriodicTimer.

* feat: add `FluentSearch` demo for immediate use with debounce

* chore: minor code clean for consistency
  • Loading branch information
ApacheTech authored and vnbaaij committed May 15, 2024
1 parent 106a711 commit c2a91f5
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 43 deletions.
108 changes: 108 additions & 0 deletions examples/Demo/Shared/Pages/Search/Examples/SearchImmediate.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<FluentStack Orientation="Orientation.Vertical" VerticalGap="10">

@* Immediate Delay *@
<FluentNumberField @bind-Value="_immediateDelay"
TValue="int"
Label="Immediate Delay"
Placeholder="Delay"
Min="0"
Max="2000"
Step="100" />

@* Search Box *@
<FluentSearch @bind-Value="_searchValue"
@bind-Value:after="OnSearch"
Immediate="true"
ImmediateDelay="_immediateDelay"
Placeholder="Search for name" />

@* Search Results *@
<p>You searched for: @_searchValue</p>
<FluentListbox aria-label="search results"
TOption="string"
Items=@_searchResults
SelectedOptionChanged="@(e => _searchValue = (e != _defaultResultsText ? e : string.Empty))" />
</FluentStack>

@code {
private string? _searchValue;
private int _immediateDelay;

private const string _defaultResultsText = "No results";
private List<string> _searchResults = DefaultResults();

private static List<string> DefaultResults() => new() { _defaultResultsText };

private void OnSearch()
{
if (!string.IsNullOrWhiteSpace(_searchValue))
{
// You can also call an API here if the list is not local.
var results = searchData
.Where(str => str.Contains(_searchValue, StringComparison.OrdinalIgnoreCase))
.Select(str => str)
.ToList();

_searchResults = results.Any() ? results : DefaultResults();
}
else
{
_searchResults = DefaultResults();
}
}

//This component is made for a lot of data. You can copy and paste a list with 6000 entries here https://sharetext.me/vfknowohwl"
private List<string> searchData = new()
{
"Alabama",
"Alaska",
"Arizona",
"Arkansas",
"California",
"Colorado",
"Connecticut",
"Delaware",
"Florida",
"Georgia",
"Hawaii",
"Idaho",
"Illinois",
"Indiana",
"Iowa",
"Kansas",
"Kentucky",
"Louisiana",
"Maine",
"Maryland",
"Massachussets",
"Michigain",
"Minnesota",
"Mississippi",
"Missouri",
"Montana",
"Nebraska",
"Nevada",
"New Hampshire",
"New Jersey",
"New Mexico",
"New York",
"North Carolina",
"North Dakota",
"Ohio",
"Oklahoma",
"Oregon",
"Pennsylvania",
"Rhode Island",
"South Carolina",
"South Dakota",
"Texas",
"Tennessee",
"Utah",
"Vermont",
"Virginia",
"Washington",
"Wisconsin",
"West Virginia",
"Wyoming"
};
}
2 changes: 2 additions & 0 deletions examples/Demo/Shared/Pages/Search/SearchPage.razor
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@

<DemoSection Title="Interactive with debounce" Component="@typeof(SearchInteractiveWithDebounce)"></DemoSection>

<DemoSection Title="Immediate (with and without debounce)" Component="@typeof(SearchImmediate)"></DemoSection>

<DemoSection Title="States" Component="@typeof(SearchStates)"></DemoSection>

<DemoSection Title="Icons" Component="@typeof(SearchIcons)"></DemoSection>
Expand Down
5 changes: 3 additions & 2 deletions src/Core/Components/Base/FluentInputBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ protected async Task SetCurrentValueAsync(TValue? value)
Value = value;
if (ValueChanged.HasDelegate)
{
await ValueChanged.InvokeAsync(value);
// Thread Safety: Force `ValueChanged` to be re-associated with the Dispatcher, prior to invokation.
await InvokeAsync(async () => await ValueChanged.InvokeAsync(value));
}
if (FieldBound)
{
Expand Down Expand Up @@ -459,7 +460,7 @@ void IDisposable.Dispose()
EditContext.OnValidationStateChanged -= _validationStateChangedHandler;
}

_timerCancellationTokenSource.Dispose();
_debouncer.Dispose();

Dispose(disposing: true);
}
Expand Down
51 changes: 10 additions & 41 deletions src/Core/Components/Base/FluentInputBaseHandlers.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.FluentUI.AspNetCore.Components.Utilities;

namespace Microsoft.Fast.Components.FluentUI;

public partial class FluentInputBase<TValue>
{
private PeriodicTimer? _timerForImmediate;
private CancellationTokenSource _timerCancellationTokenSource = new();
private readonly Debouncer _debouncer = new();

/// <summary>
/// Change the content of this input field when the user write text (based on 'OnInput' HTML event).
Expand Down Expand Up @@ -58,48 +58,17 @@ protected virtual async Task ChangeHandlerAsync(ChangeEventArgs e)
/// <returns></returns>
protected virtual async Task InputHandlerAsync(ChangeEventArgs e) // TODO: To update in all Input fields
{
if (this.Immediate)
if (!Immediate)
{
// Raise ChangeHandler after a delay
if (ImmediateDelay > 0)
{
_timerForImmediate = GetNewPeriodicTimer(ImmediateDelay);

while (await _timerForImmediate.WaitForNextTickAsync(_timerCancellationTokenSource.Token))
{
await ChangeHandlerAsync(e);
_timerCancellationTokenSource.Cancel();
}
}
// Raise ChangeHandler immediately
else
{
// Cancel a potential existing object
_timerForImmediate?.Dispose();
_timerForImmediate = null;

await ChangeHandlerAsync(e);
}
return;
}

// Cancel the previous Timer (if existing)
// And create a new Timer with a new CancellationToken
PeriodicTimer GetNewPeriodicTimer(int delay)
if (ImmediateDelay > 0)
{
_timerCancellationTokenSource.Cancel();

if (_timerForImmediate is not null)
{
_timerForImmediate.Dispose();
_timerForImmediate = null;
}

_timerForImmediate = new PeriodicTimer(TimeSpan.FromMilliseconds(delay));

_timerCancellationTokenSource.Dispose();
_timerCancellationTokenSource = new CancellationTokenSource();

return _timerForImmediate;
await _debouncer.DebounceAsync(ImmediateDelay, async () => await ChangeHandlerAsync(e));
}
else
{
await ChangeHandlerAsync(e);
}
}
}

0 comments on commit c2a91f5

Please sign in to comment.