diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml index 491f41ecbf..943fadf100 100644 --- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml @@ -8,7 +8,6 @@ HorizontalAlignment="Center" Gap="8"> - + + + diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs index 9f181d44f2..6f3b8361cd 100644 --- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs @@ -6,6 +6,8 @@ using Avalonia.Markup.Xaml.Data; using System; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace ControlCatalog.Pages { @@ -109,6 +111,9 @@ namespace ControlCatalog.Pages var multibindingBox = this.FindControl("MultiBindingBox"); multibindingBox.ValueMemberBinding = binding; + + var asyncBox = this.FindControl("AsyncBox"); + asyncBox.AsyncPopulator = PopulateAsync; } private IEnumerable GetAllAutoCompleteBox() { @@ -117,6 +122,19 @@ namespace ControlCatalog.Pages .OfType(); } + private bool StringContains(string str, string query) + { + return str.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0; + } + private async Task> PopulateAsync(string searchText, CancellationToken cancellationToken) + { + await Task.Delay(TimeSpan.FromSeconds(1.5), cancellationToken); + + return + States.Where(data => StringContains(data.Name, searchText) || StringContains(data.Capital, searchText)) + .ToList(); + } + private void InitializeComponent() { AvaloniaXamlLoader.Load(this); diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 0e8b93146c..e4962dc069 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -23,6 +23,8 @@ using Avalonia.Utilities; using System.Globalization; using System.Collections.Specialized; using System.Reactive.Disposables; +using System.Threading; +using System.Threading.Tasks; namespace Avalonia.Controls { @@ -360,6 +362,8 @@ namespace Avalonia.Controls private IDisposable _collectionChangeSubscription; private IMemberSelector _valueMemberSelector; + private Func>> _asyncPopulator; + private CancellationTokenSource _populationCancellationTokenSource; private bool _itemTemplateIsFromValueMemeberBinding = true; private bool _settingItemTemplateFromValueMemeberBinding; @@ -559,6 +563,12 @@ namespace Avalonia.Controls o => o.ValueMemberSelector, (o, v) => o.ValueMemberSelector = v); + public static readonly DirectProperty>>> AsyncPopulatorProperty = + AvaloniaProperty.RegisterDirect>>>( + nameof(AsyncPopulator), + o => o.AsyncPopulator, + (o, v) => o.AsyncPopulator = v); + private static int ValidateMinimumPrefixLength(AutoCompleteBox control, int value) { Contract.Requires(value >= -1); @@ -1107,6 +1117,12 @@ namespace Avalonia.Controls set { SetAndRaise(TextFilterProperty, ref _textFilter, value); } } + public Func>> AsyncPopulator + { + get { return _asyncPopulator; } + set { SetAndRaise(AsyncPopulatorProperty, ref _asyncPopulator, value); } + } + /// /// Gets or sets a collection that is used to generate the items for the /// drop-down portion of the @@ -1702,6 +1718,11 @@ namespace Avalonia.Controls // Update the prefix/search text. SearchText = Text; + if(TryPopulateAsync(SearchText)) + { + return; + } + // The Populated event enables advanced, custom filtering. The // client needs to directly update the ItemsSource collection or // call the Populate method on the control to continue the @@ -1713,6 +1734,55 @@ namespace Avalonia.Controls PopulateComplete(); } } + private bool TryPopulateAsync(string searchText) + { + _populationCancellationTokenSource?.Cancel(false); + _populationCancellationTokenSource?.Dispose(); + _populationCancellationTokenSource = null; + + if(_asyncPopulator == null) + { + return false; + } + + _populationCancellationTokenSource = new CancellationTokenSource(); + var task = PopulateAsync(searchText, _populationCancellationTokenSource.Token); + if (task.Status == TaskStatus.Created) + task.Start(); + + return true; + } + private async Task PopulateAsync(string searchText, CancellationToken cancellationToken) + { + + try + { + IEnumerable result = await _asyncPopulator.Invoke(searchText, cancellationToken); + var resultList = result.ToList(); + + if (cancellationToken.IsCancellationRequested) + { + return; + } + + await Dispatcher.UIThread.InvokeAsync(() => + { + if (!cancellationToken.IsCancellationRequested) + { + Items = resultList; + PopulateComplete(); + } + }); + } + catch (TaskCanceledException) + { } + finally + { + _populationCancellationTokenSource?.Dispose(); + _populationCancellationTokenSource = null; + } + + } /// /// Private method that directly opens the popup, checks the expander