diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj
index 5d8f661990..862de9d320 100644
--- a/samples/ControlCatalog/ControlCatalog.csproj
+++ b/samples/ControlCatalog/ControlCatalog.csproj
@@ -41,6 +41,9 @@
Designer
+
+ Designer
+
Designer
@@ -116,6 +119,9 @@
BorderPage.xaml
+
+ AutoCompleteBoxPage.xaml
+
ButtonPage.xaml
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index a2e0980d6a..a0e0df450b 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -5,10 +5,11 @@
+
-
+
@@ -27,4 +28,4 @@
-
\ No newline at end of file
+
diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml
new file mode 100644
index 0000000000..943fadf100
--- /dev/null
+++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml
@@ -0,0 +1,59 @@
+
+
+ AutoCompleteBox
+ A control into which the user can input text
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs
new file mode 100644
index 0000000000..6f3b8361cd
--- /dev/null
+++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs
@@ -0,0 +1,143 @@
+using Avalonia.Controls;
+using Avalonia.LogicalTree;
+using Avalonia.Markup;
+using Avalonia.Markup.Xaml;
+using Avalonia.Markup.Xaml.Data;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace ControlCatalog.Pages
+{
+ public class AutoCompleteBoxPage : UserControl
+ {
+ public class StateData
+ {
+ public string Name { get; private set; }
+ public string Abbreviation { get; private set; }
+ public string Capital { get; private set; }
+
+ public StateData(string name, string abbreviatoin, string capital)
+ {
+ Name = name;
+ Abbreviation = abbreviatoin;
+ Capital = capital;
+ }
+
+ public override string ToString()
+ {
+ return Name;
+ }
+ }
+
+ private StateData[] BuildAllStates()
+ {
+ return new StateData[]
+ {
+ new StateData("Alabama","AL","Montgomery"),
+ new StateData("Alaska","AK","Juneau"),
+ new StateData("Arizona","AZ","Phoenix"),
+ new StateData("Arkansas","AR","Little Rock"),
+ new StateData("California","CA","Sacramento"),
+ new StateData("Colorado","CO","Denver"),
+ new StateData("Connecticut","CT","Hartford"),
+ new StateData("Delaware","DE","Dover"),
+ new StateData("Florida","FL","Tallahassee"),
+ new StateData("Georgia","GA","Atlanta"),
+ new StateData("Hawaii","HI","Honolulu"),
+ new StateData("Idaho","ID","Boise"),
+ new StateData("Illinois","IL","Springfield"),
+ new StateData("Indiana","IN","Indianapolis"),
+ new StateData("Iowa","IA","Des Moines"),
+ new StateData("Kansas","KS","Topeka"),
+ new StateData("Kentucky","KY","Frankfort"),
+ new StateData("Louisiana","LA","Baton Rouge"),
+ new StateData("Maine","ME","Augusta"),
+ new StateData("Maryland","MD","Annapolis"),
+ new StateData("Massachusetts","MA","Boston"),
+ new StateData("Michigan","MI","Lansing"),
+ new StateData("Minnesota","MN","St. Paul"),
+ new StateData("Mississippi","MS","Jackson"),
+ new StateData("Missouri","MO","Jefferson City"),
+ new StateData("Montana","MT","Helena"),
+ new StateData("Nebraska","NE","Lincoln"),
+ new StateData("Nevada","NV","Carson City"),
+ new StateData("New Hampshire","NH","Concord"),
+ new StateData("New Jersey","NJ","Trenton"),
+ new StateData("New Mexico","NM","Santa Fe"),
+ new StateData("New York","NY","Albany"),
+ new StateData("North Carolina","NC","Raleigh"),
+ new StateData("North Dakota","ND","Bismarck"),
+ new StateData("Ohio","OH","Columbus"),
+ new StateData("Oklahoma","OK","Oklahoma City"),
+ new StateData("Oregon","OR","Salem"),
+ new StateData("Pennsylvania","PA","Harrisburg"),
+ new StateData("Rhode Island","RI","Providence"),
+ new StateData("South Carolina","SC","Columbia"),
+ new StateData("South Dakota","SD","Pierre"),
+ new StateData("Tennessee","TN","Nashville"),
+ new StateData("Texas","TX","Austin"),
+ new StateData("Utah","UT","Salt Lake City"),
+ new StateData("Vermont","VT","Montpelier"),
+ new StateData("Virginia","VA","Richmond"),
+ new StateData("Washington","WA","Olympia"),
+ new StateData("West Virginia","WV","Charleston"),
+ new StateData("Wisconsin","WI","Madison"),
+ new StateData("Wyoming","WY","Cheyenne"),
+ };
+ }
+ public StateData[] States { get; private set; }
+
+ public AutoCompleteBoxPage()
+ {
+ this.InitializeComponent();
+
+ States = BuildAllStates();
+
+ foreach (AutoCompleteBox box in GetAllAutoCompleteBox())
+ {
+ box.Items = States;
+ }
+
+ var converter = new FuncMultiValueConverter(parts =>
+ {
+ return String.Format("{0} ({1})", parts.ToArray());
+ });
+ var binding = new MultiBinding { Converter = converter };
+ binding.Bindings.Add(new Binding("Name"));
+ binding.Bindings.Add(new Binding("Abbreviation"));
+
+ var multibindingBox = this.FindControl("MultiBindingBox");
+ multibindingBox.ValueMemberBinding = binding;
+
+ var asyncBox = this.FindControl("AsyncBox");
+ asyncBox.AsyncPopulator = PopulateAsync;
+ }
+ private IEnumerable GetAllAutoCompleteBox()
+ {
+ return
+ this.GetLogicalDescendants()
+ .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
new file mode 100644
index 0000000000..8e801d606b
--- /dev/null
+++ b/src/Avalonia.Controls/AutoCompleteBox.cs
@@ -0,0 +1,2726 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Avalonia.Collections;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Templates;
+using Avalonia.Controls.Utils;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Provides data for the
+ ///
+ /// event.
+ ///
+ public class PopulatedEventArgs : EventArgs
+ {
+ ///
+ /// Gets the list of possible matches added to the drop-down portion of
+ /// the
+ /// control.
+ ///
+ /// The list of possible matches added to the
+ /// .
+ public IEnumerable Data { get; private set; }
+
+ ///
+ /// Initializes a new instance of the
+ /// .
+ ///
+ /// The list of possible matches added to the
+ /// drop-down portion of the
+ /// control.
+ public PopulatedEventArgs(IEnumerable data)
+ {
+ Data = data;
+ }
+ }
+
+ ///
+ /// Provides data for the
+ ///
+ /// event.
+ ///
+ /// Stable
+ public class PopulatingEventArgs : CancelEventArgs
+ {
+ ///
+ /// Gets the text that is used to determine which items to display in
+ /// the
+ /// control.
+ ///
+ /// The text that is used to determine which items to display in
+ /// the .
+ public string Parameter { get; private set; }
+
+ ///
+ /// Initializes a new instance of the
+ /// .
+ ///
+ /// The value of the
+ ///
+ /// property, which is used to filter items for the
+ /// control.
+ public PopulatingEventArgs(string parameter)
+ {
+ Parameter = parameter;
+ }
+ }
+
+ ///
+ /// Represents the filter used by the
+ /// control to
+ /// determine whether an item is a possible match for the specified text.
+ ///
+ /// true to indicate is a possible match
+ /// for ; otherwise false.
+ /// The string used as the basis for filtering.
+ /// The item that is compared with the
+ /// parameter.
+ /// The type used for filtering the
+ /// . This type can
+ /// be either a string or an object.
+ /// Stable
+ public delegate bool AutoCompleteFilterPredicate(string search, T item);
+
+ ///
+ /// Specifies how text in the text box portion of the
+ /// control is used
+ /// to filter items specified by the
+ ///
+ /// property for display in the drop-down.
+ ///
+ /// Stable
+ public enum AutoCompleteFilterMode
+ {
+ ///
+ /// Specifies that no filter is used. All items are returned.
+ ///
+ None = 0,
+
+ ///
+ /// Specifies a culture-sensitive, case-insensitive filter where the
+ /// returned items start with the specified text. The filter uses the
+ ///
+ /// method, specifying
+ /// as
+ /// the string comparison criteria.
+ ///
+ StartsWith = 1,
+
+ ///
+ /// Specifies a culture-sensitive, case-sensitive filter where the
+ /// returned items start with the specified text. The filter uses the
+ ///
+ /// method, specifying
+ /// as the string
+ /// comparison criteria.
+ ///
+ StartsWithCaseSensitive = 2,
+
+ ///
+ /// Specifies an ordinal, case-insensitive filter where the returned
+ /// items start with the specified text. The filter uses the
+ ///
+ /// method, specifying
+ /// as the
+ /// string comparison criteria.
+ ///
+ StartsWithOrdinal = 3,
+
+ ///
+ /// Specifies an ordinal, case-sensitive filter where the returned items
+ /// start with the specified text. The filter uses the
+ ///
+ /// method, specifying as
+ /// the string comparison criteria.
+ ///
+ StartsWithOrdinalCaseSensitive = 4,
+
+ ///
+ /// Specifies a culture-sensitive, case-insensitive filter where the
+ /// returned items contain the specified text.
+ ///
+ Contains = 5,
+
+ ///
+ /// Specifies a culture-sensitive, case-sensitive filter where the
+ /// returned items contain the specified text.
+ ///
+ ContainsCaseSensitive = 6,
+
+ ///
+ /// Specifies an ordinal, case-insensitive filter where the returned
+ /// items contain the specified text.
+ ///
+ ContainsOrdinal = 7,
+
+ ///
+ /// Specifies an ordinal, case-sensitive filter where the returned items
+ /// contain the specified text.
+ ///
+ ContainsOrdinalCaseSensitive = 8,
+
+ ///
+ /// Specifies a culture-sensitive, case-insensitive filter where the
+ /// returned items equal the specified text. The filter uses the
+ ///
+ /// method, specifying
+ /// as
+ /// the search comparison criteria.
+ ///
+ Equals = 9,
+
+ ///
+ /// Specifies a culture-sensitive, case-sensitive filter where the
+ /// returned items equal the specified text. The filter uses the
+ ///
+ /// method, specifying
+ /// as the string
+ /// comparison criteria.
+ ///
+ EqualsCaseSensitive = 10,
+
+ ///
+ /// Specifies an ordinal, case-insensitive filter where the returned
+ /// items equal the specified text. The filter uses the
+ ///
+ /// method, specifying
+ /// as the
+ /// string comparison criteria.
+ ///
+ EqualsOrdinal = 11,
+
+ ///
+ /// Specifies an ordinal, case-sensitive filter where the returned items
+ /// equal the specified text. The filter uses the
+ ///
+ /// method, specifying as
+ /// the string comparison criteria.
+ ///
+ EqualsOrdinalCaseSensitive = 12,
+
+ ///
+ /// Specifies that a custom filter is used. This mode is used when the
+ ///
+ /// or
+ ///
+ /// properties are set.
+ ///
+ Custom = 13,
+ }
+
+ ///
+ /// Represents a control that provides a text box for user input and a
+ /// drop-down that contains possible matches based on the input in the text
+ /// box.
+ ///
+ public class AutoCompleteBox : TemplatedControl
+ {
+ ///
+ /// Specifies the name of the selection adapter TemplatePart.
+ ///
+ private const string ElementSelectionAdapter = "PART_SelectionAdapter";
+
+ ///
+ /// Specifies the name of the Selector TemplatePart.
+ ///
+ private const string ElementSelector = "PART_SelectingItemsControl";
+
+ ///
+ /// Specifies the name of the Popup TemplatePart.
+ ///
+ private const string ElementPopup = "PART_Popup";
+
+ ///
+ /// The name for the text box part.
+ ///
+ private const string ElementTextBox = "PART_TextBox";
+
+ private IEnumerable _itemsEnumerable;
+
+ ///
+ /// Gets or sets a local cached copy of the items data.
+ ///
+ private List _items;
+
+ ///
+ /// Gets or sets the observable collection that contains references to
+ /// all of the items in the generated view of data that is provided to
+ /// the selection-style control adapter.
+ ///
+ private AvaloniaList _view;
+
+ ///
+ /// Gets or sets a value to ignore a number of pending change handlers.
+ /// The value is decremented after each use. This is used to reset the
+ /// value of properties without performing any of the actions in their
+ /// change handlers.
+ ///
+ /// The int is important as a value because the TextBox
+ /// TextChanged event does not immediately fire, and this will allow for
+ /// nested property changes to be ignored.
+ private int _ignoreTextPropertyChange;
+
+ ///
+ /// Gets or sets a value indicating whether to ignore calling a pending
+ /// change handlers.
+ ///
+ private bool _ignorePropertyChange;
+
+ ///
+ /// Gets or sets a value indicating whether to ignore the selection
+ /// changed event.
+ ///
+ private bool _ignoreTextSelectionChange;
+
+ ///
+ /// Gets or sets a value indicating whether to skip the text update
+ /// processing when the selected item is updated.
+ ///
+ private bool _skipSelectedItemTextUpdate;
+
+ ///
+ /// Gets or sets the last observed text box selection start location.
+ ///
+ private int _textSelectionStart;
+
+ ///
+ /// Gets or sets a value indicating whether the user initiated the
+ /// current populate call.
+ ///
+ private bool _userCalledPopulate;
+
+ ///
+ /// A value indicating whether the popup has been opened at least once.
+ ///
+ private bool _popupHasOpened;
+
+ ///
+ /// Gets or sets the DispatcherTimer used for the MinimumPopulateDelay
+ /// condition for auto completion.
+ ///
+ private DispatcherTimer _delayTimer;
+
+ ///
+ /// Gets or sets a value indicating whether a read-only dependency
+ /// property change handler should allow the value to be set. This is
+ /// used to ensure that read-only properties cannot be changed via
+ /// SetValue, etc.
+ ///
+ private bool _allowWrite;
+
+ ///
+ /// The TextBox template part.
+ ///
+ private TextBox _textBox;
+ private IDisposable _textBoxSubscriptions;
+
+ ///
+ /// The SelectionAdapter.
+ ///
+ private ISelectionAdapter _adapter;
+
+ ///
+ /// A control that can provide updated string values from a binding.
+ ///
+ private BindingEvaluator _valueBindingEvaluator;
+
+ ///
+ /// A weak subscription for the collection changed event.
+ ///
+ private IDisposable _collectionChangeSubscription;
+
+ private IMemberSelector _valueMemberSelector;
+ private Func>> _asyncPopulator;
+ private CancellationTokenSource _populationCancellationTokenSource;
+
+ private bool _itemTemplateIsFromValueMemeberBinding = true;
+ private bool _settingItemTemplateFromValueMemeberBinding;
+
+ private object _selectedItem;
+ private bool _isDropDownOpen;
+ private bool _isFocused = false;
+
+ private string _text = string.Empty;
+ private string _searchText = string.Empty;
+
+ private AutoCompleteFilterPredicate _itemFilter;
+ private AutoCompleteFilterPredicate _textFilter = AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith);
+
+ public static readonly RoutedEvent SelectionChangedEvent =
+ RoutedEvent.Register(nameof(SelectionChanged), RoutingStrategies.Bubble, typeof(AutoCompleteBox));
+
+ public static readonly StyledProperty WatermarkProperty =
+ TextBox.WatermarkProperty.AddOwner();
+
+ ///
+ /// Identifies the
+ ///
+ /// dependency property.
+ ///
+ /// The identifier for the
+ ///
+ /// dependency property.
+ public static readonly StyledProperty MinimumPrefixLengthProperty =
+ AvaloniaProperty.Register(
+ nameof(MinimumPrefixLength), 1,
+ validate: ValidateMinimumPrefixLength);
+
+ ///
+ /// Identifies the
+ ///
+ /// dependency property.
+ ///
+ /// The identifier for the
+ ///
+ /// dependency property.
+ public static readonly StyledProperty MinimumPopulateDelayProperty =
+ AvaloniaProperty.Register(
+ nameof(MinimumPopulateDelay),
+ TimeSpan.Zero,
+ validate: ValidateMinimumPopulateDelay);
+
+ ///
+ /// Identifies the
+ ///
+ /// dependency property.
+ ///
+ /// The identifier for the
+ ///
+ /// dependency property.
+ public static readonly StyledProperty MaxDropDownHeightProperty =
+ AvaloniaProperty.Register(
+ nameof(MaxDropDownHeight),
+ double.PositiveInfinity,
+ validate: ValidateMaxDropDownHeight);
+
+ ///
+ /// Identifies the
+ ///
+ /// dependency property.
+ ///
+ /// The identifier for the
+ ///
+ /// dependency property.
+ public static readonly StyledProperty IsTextCompletionEnabledProperty =
+ AvaloniaProperty.Register(nameof(IsTextCompletionEnabled));
+
+ ///
+ /// Identifies the
+ ///
+ /// dependency property.
+ ///
+ /// The identifier for the
+ ///
+ /// dependency property.
+ public static readonly StyledProperty ItemTemplateProperty =
+ AvaloniaProperty.Register(nameof(ItemTemplate));
+
+ ///
+ /// Identifies the
+ ///
+ /// dependency property.
+ ///
+ /// The identifier for the
+ ///
+ /// dependency property.
+ public static readonly DirectProperty IsDropDownOpenProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(IsDropDownOpen),
+ o => o.IsDropDownOpen,
+ (o, v) => o.IsDropDownOpen = v);
+
+ ///
+ /// Identifies the
+ ///
+ /// dependency property.
+ ///
+ /// The identifier the
+ ///
+ /// dependency property.
+ public static readonly DirectProperty SelectedItemProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(SelectedItem),
+ o => o.SelectedItem,
+ (o, v) => o.SelectedItem = v);
+
+ ///
+ /// Identifies the
+ ///
+ /// dependency property.
+ ///
+ /// The identifier for the
+ ///
+ /// dependency property.
+ public static readonly DirectProperty TextProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(Text),
+ o => o.Text,
+ (o, v) => o.Text = v);
+
+ ///
+ /// Identifies the
+ ///
+ /// dependency property.
+ ///
+ /// The identifier for the
+ ///
+ /// dependency property.
+ public static readonly DirectProperty SearchTextProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(SearchText),
+ o => o.SearchText,
+ unsetValue: string.Empty);
+
+ ///
+ /// Gets the identifier for the
+ ///
+ /// dependency property.
+ ///
+ public static readonly StyledProperty FilterModeProperty =
+ AvaloniaProperty.Register(
+ nameof(FilterMode),
+ defaultValue: AutoCompleteFilterMode.StartsWith,
+ validate: ValidateFilterMode);
+
+ ///
+ /// Identifies the
+ ///
+ /// dependency property.
+ ///
+ /// The identifier for the
+ ///
+ /// dependency property.
+ public static readonly DirectProperty> ItemFilterProperty =
+ AvaloniaProperty.RegisterDirect>(
+ nameof(ItemFilter),
+ o => o.ItemFilter,
+ (o, v) => o.ItemFilter = v);
+
+ ///
+ /// Identifies the
+ ///
+ /// dependency property.
+ ///
+ /// The identifier for the
+ ///
+ /// dependency property.
+ public static readonly DirectProperty> TextFilterProperty =
+ AvaloniaProperty.RegisterDirect>(
+ nameof(TextFilter),
+ o => o.TextFilter,
+ (o, v) => o.TextFilter = v,
+ unsetValue: AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith));
+
+ ///
+ /// Identifies the
+ ///
+ /// dependency property.
+ ///
+ /// The identifier for the
+ ///
+ /// dependency property.
+ public static readonly DirectProperty ItemsProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(Items),
+ o => o.Items,
+ (o, v) => o.Items = v);
+
+ public static readonly DirectProperty ValueMemberSelectorProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(ValueMemberSelector),
+ 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);
+
+ return value;
+ }
+
+ private static TimeSpan ValidateMinimumPopulateDelay(AutoCompleteBox control, TimeSpan value)
+ {
+ Contract.Requires(value.TotalMilliseconds >= 0.0);
+
+ return value;
+ }
+
+ private static double ValidateMaxDropDownHeight(AutoCompleteBox control, double value)
+ {
+ Contract.Requires(value >= 0.0);
+
+ return value;
+ }
+
+ private static bool IsValidFilterMode(AutoCompleteFilterMode mode)
+ {
+ switch (mode)
+ {
+ case AutoCompleteFilterMode.None:
+ case AutoCompleteFilterMode.StartsWith:
+ case AutoCompleteFilterMode.StartsWithCaseSensitive:
+ case AutoCompleteFilterMode.StartsWithOrdinal:
+ case AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive:
+ case AutoCompleteFilterMode.Contains:
+ case AutoCompleteFilterMode.ContainsCaseSensitive:
+ case AutoCompleteFilterMode.ContainsOrdinal:
+ case AutoCompleteFilterMode.ContainsOrdinalCaseSensitive:
+ case AutoCompleteFilterMode.Equals:
+ case AutoCompleteFilterMode.EqualsCaseSensitive:
+ case AutoCompleteFilterMode.EqualsOrdinal:
+ case AutoCompleteFilterMode.EqualsOrdinalCaseSensitive:
+ case AutoCompleteFilterMode.Custom:
+ return true;
+ default:
+ return false;
+ }
+ }
+ private static AutoCompleteFilterMode ValidateFilterMode(AutoCompleteBox control, AutoCompleteFilterMode value)
+ {
+ Contract.Requires(IsValidFilterMode(value));
+
+ return value;
+ }
+
+ ///
+ /// Handle the change of the IsEnabled property.
+ ///
+ /// The event data.
+ private void OnControlIsEnabledChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ bool isEnabled = (bool)e.NewValue;
+ if (!isEnabled)
+ {
+ IsDropDownOpen = false;
+ }
+ }
+
+ ///
+ /// MinimumPopulateDelayProperty property changed handler. Any current
+ /// dispatcher timer will be stopped. The timer will not be restarted
+ /// until the next TextUpdate call by the user.
+ ///
+ /// Event arguments.
+ private void OnMinimumPopulateDelayChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ var newValue = (TimeSpan)e.NewValue;
+
+ // Stop any existing timer
+ if (_delayTimer != null)
+ {
+ _delayTimer.Stop();
+
+ if (newValue == TimeSpan.Zero)
+ {
+ _delayTimer = null;
+ }
+ }
+
+ if (newValue > TimeSpan.Zero)
+ {
+ // Create or clear a dispatcher timer instance
+ if (_delayTimer == null)
+ {
+ _delayTimer = new DispatcherTimer();
+ _delayTimer.Tick += PopulateDropDown;
+ }
+
+ // Set the new tick interval
+ _delayTimer.Interval = newValue;
+ }
+ }
+
+ ///
+ /// IsDropDownOpenProperty property changed handler.
+ ///
+ /// Event arguments.
+ private void OnIsDropDownOpenChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ // Ignore the change if requested
+ if (_ignorePropertyChange)
+ {
+ _ignorePropertyChange = false;
+ return;
+ }
+
+ bool oldValue = (bool)e.OldValue;
+ bool newValue = (bool)e.NewValue;
+
+ if (newValue)
+ {
+ TextUpdated(Text, true);
+ }
+ else
+ {
+ ClosingDropDown(oldValue);
+ }
+
+ UpdatePseudoClasses();
+ }
+
+ private void OnSelectedItemPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ if (_ignorePropertyChange)
+ {
+ _ignorePropertyChange = false;
+ return;
+ }
+
+ // Update the text display
+ if (_skipSelectedItemTextUpdate)
+ {
+ _skipSelectedItemTextUpdate = false;
+ }
+ else
+ {
+ OnSelectedItemChanged(e.NewValue);
+ }
+
+ // Fire the SelectionChanged event
+ List removed = new List();
+ if (e.OldValue != null)
+ {
+ removed.Add(e.OldValue);
+ }
+
+ List added = new List();
+ if (e.NewValue != null)
+ {
+ added.Add(e.NewValue);
+ }
+
+ OnSelectionChanged(new SelectionChangedEventArgs(SelectionChangedEvent, removed, added));
+ }
+
+ ///
+ /// TextProperty property changed handler.
+ ///
+ /// Event arguments.
+ private void OnTextPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ TextUpdated((string)e.NewValue, false);
+ }
+
+ private void OnSearchTextPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ if (_ignorePropertyChange)
+ {
+ _ignorePropertyChange = false;
+ return;
+ }
+
+ // Ensure the property is only written when expected
+ if (!_allowWrite)
+ {
+ // Reset the old value before it was incorrectly written
+ _ignorePropertyChange = true;
+ SetValue(e.Property, e.OldValue);
+
+ throw new InvalidOperationException("Cannot set read-only property SearchText.");
+ }
+ }
+
+ ///
+ /// FilterModeProperty property changed handler.
+ ///
+ /// Event arguments.
+ private void OnFilterModePropertyChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ AutoCompleteFilterMode mode = (AutoCompleteFilterMode)e.NewValue;
+
+ // Sets the filter predicate for the new value
+ TextFilter = AutoCompleteSearch.GetFilter(mode);
+ }
+
+ ///
+ /// ItemFilterProperty property changed handler.
+ ///
+ /// Event arguments.
+ private void OnItemFilterPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ AutoCompleteFilterPredicate value = e.NewValue as AutoCompleteFilterPredicate;
+
+ // If null, revert to the "None" predicate
+ if (value == null)
+ {
+ FilterMode = AutoCompleteFilterMode.None;
+ }
+ else
+ {
+ FilterMode = AutoCompleteFilterMode.Custom;
+ TextFilter = null;
+ }
+ }
+
+ ///
+ /// ItemsSourceProperty property changed handler.
+ ///
+ /// Event arguments.
+ private void OnItemsPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ OnItemsChanged((IEnumerable)e.NewValue);
+ }
+
+ private void OnItemTemplatePropertyChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ if (!_settingItemTemplateFromValueMemeberBinding)
+ _itemTemplateIsFromValueMemeberBinding = false;
+ }
+ private void OnValueMemberBindingChanged(IBinding value)
+ {
+ if(_itemTemplateIsFromValueMemeberBinding)
+ {
+ var template =
+ new FuncDataTemplate(
+ typeof(object),
+ o =>
+ {
+ var control = new ContentControl();
+ control.Bind(ContentControl.ContentProperty, value);
+ return control;
+ });
+
+ _settingItemTemplateFromValueMemeberBinding = true;
+ ItemTemplate = template;
+ _settingItemTemplateFromValueMemeberBinding = false;
+ }
+ }
+
+ static AutoCompleteBox()
+ {
+ FocusableProperty.OverrideDefaultValue(true);
+
+ MinimumPopulateDelayProperty.Changed.AddClassHandler(x => x.OnMinimumPopulateDelayChanged);
+ IsDropDownOpenProperty.Changed.AddClassHandler(x => x.OnIsDropDownOpenChanged);
+ SelectedItemProperty.Changed.AddClassHandler(x => x.OnSelectedItemPropertyChanged);
+ TextProperty.Changed.AddClassHandler(x => x.OnTextPropertyChanged);
+ SearchTextProperty.Changed.AddClassHandler(x => x.OnSearchTextPropertyChanged);
+ FilterModeProperty.Changed.AddClassHandler(x => x.OnFilterModePropertyChanged);
+ ItemFilterProperty.Changed.AddClassHandler(x => x.OnItemFilterPropertyChanged);
+ ItemsProperty.Changed.AddClassHandler(x => x.OnItemsPropertyChanged);
+ IsEnabledProperty.Changed.AddClassHandler(x => x.OnControlIsEnabledChanged);
+ }
+
+ ///
+ /// Initializes a new instance of the
+ /// class.
+ ///
+ public AutoCompleteBox()
+ {
+ ClearView();
+ }
+
+ ///
+ /// Gets or sets the minimum number of characters required to be entered
+ /// in the text box before the
+ /// displays
+ /// possible matches.
+ /// matches.
+ ///
+ ///
+ /// The minimum number of characters to be entered in the text box
+ /// before the
+ /// displays possible matches. The default is 1.
+ ///
+ ///
+ /// If you set MinimumPrefixLength to -1, the AutoCompleteBox will
+ /// not provide possible matches. There is no maximum value, but
+ /// setting MinimumPrefixLength to value that is too large will
+ /// prevent the AutoCompleteBox from providing possible matches as well.
+ ///
+ public int MinimumPrefixLength
+ {
+ get { return GetValue(MinimumPrefixLengthProperty); }
+ set { SetValue(MinimumPrefixLengthProperty, value); }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the first possible match
+ /// found during the filtering process will be displayed automatically
+ /// in the text box.
+ ///
+ ///
+ /// True if the first possible match found will be displayed
+ /// automatically in the text box; otherwise, false. The default is
+ /// false.
+ ///
+ public bool IsTextCompletionEnabled
+ {
+ get { return GetValue(IsTextCompletionEnabledProperty); }
+ set { SetValue(IsTextCompletionEnabledProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the used
+ /// to display each item in the drop-down portion of the control.
+ ///
+ /// The used to
+ /// display each item in the drop-down. The default is null.
+ ///
+ /// You use the ItemTemplate property to specify the visualization
+ /// of the data objects in the drop-down portion of the AutoCompleteBox
+ /// control. If your AutoCompleteBox is bound to a collection and you
+ /// do not provide specific display instructions by using a
+ /// DataTemplate, the resulting UI of each item is a string
+ /// representation of each object in the underlying collection.
+ ///
+ public IDataTemplate ItemTemplate
+ {
+ get { return GetValue(ItemTemplateProperty); }
+ set { SetValue(ItemTemplateProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the minimum delay, after text is typed
+ /// in the text box before the
+ /// control
+ /// populates the list of possible matches in the drop-down.
+ ///
+ /// The minimum delay, after text is typed in
+ /// the text box, but before the
+ /// populates
+ /// the list of possible matches in the drop-down. The default is 0.
+ public TimeSpan MinimumPopulateDelay
+ {
+ get { return GetValue(MinimumPopulateDelayProperty); }
+ set { SetValue(MinimumPopulateDelayProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the maximum height of the drop-down portion of the
+ /// control.
+ ///
+ /// The maximum height of the drop-down portion of the
+ /// control.
+ /// The default is .
+ /// The specified value is less than 0.
+ public double MaxDropDownHeight
+ {
+ get { return GetValue(MaxDropDownHeightProperty); }
+ set { SetValue(MaxDropDownHeightProperty, value); }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the drop-down portion of
+ /// the control is open.
+ ///
+ ///
+ /// True if the drop-down is open; otherwise, false. The default is
+ /// false.
+ ///
+ public bool IsDropDownOpen
+ {
+ get { return _isDropDownOpen; }
+ set { SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value); }
+ }
+
+ ///
+ /// Gets or sets the that
+ /// is used to get the values for display in the text portion of
+ /// the
+ /// control.
+ ///
+ /// The object used
+ /// when binding to a collection property.
+ [AssignBinding]
+ public IBinding ValueMemberBinding
+ {
+ get { return _valueBindingEvaluator?.ValueBinding; }
+ set
+ {
+ if (ValueMemberBinding != value)
+ {
+ _valueBindingEvaluator = new BindingEvaluator(value);
+ OnValueMemberBindingChanged(value);
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the MemberSelector that is used to get values for
+ /// display in the text portion of the
+ /// control.
+ ///
+ /// The MemberSelector that is used to get values for display in
+ /// the text portion of the
+ /// control.
+ public IMemberSelector ValueMemberSelector
+ {
+ get { return _valueMemberSelector; }
+ set { SetAndRaise(ValueMemberSelectorProperty, ref _valueMemberSelector, value); }
+ }
+
+ ///
+ /// Gets or sets the selected item in the drop-down.
+ ///
+ /// The selected item in the drop-down.
+ ///
+ /// If the IsTextCompletionEnabled property is true and text typed by
+ /// the user matches an item in the ItemsSource collection, which is
+ /// then displayed in the text box, the SelectedItem property will be
+ /// a null reference.
+ ///
+ public object SelectedItem
+ {
+ get { return _selectedItem; }
+ set { SetAndRaise(SelectedItemProperty, ref _selectedItem, value); }
+ }
+
+ ///
+ /// Gets or sets the text in the text box portion of the
+ /// control.
+ ///
+ /// The text in the text box portion of the
+ /// control.
+ public string Text
+ {
+ get { return _text; }
+ set { SetAndRaise(TextProperty, ref _text, value); }
+ }
+
+ ///
+ /// Gets the text that is used to filter items in the
+ ///
+ /// item collection.
+ ///
+ /// The text that is used to filter items in the
+ ///
+ /// item collection.
+ ///
+ /// The SearchText value is typically the same as the
+ /// Text property, but is set after the TextChanged event occurs
+ /// and before the Populating event.
+ ///
+ public string SearchText
+ {
+ get { return _searchText; }
+ private set
+ {
+ try
+ {
+ _allowWrite = true;
+ SetAndRaise(SearchTextProperty, ref _searchText, value);
+ }
+ finally
+ {
+ _allowWrite = false;
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets how the text in the text box is used to filter items
+ /// specified by the
+ ///
+ /// property for display in the drop-down.
+ ///
+ /// One of the
+ ///
+ /// values The default is
+ /// .
+ /// The specified value is
+ /// not a valid
+ /// .
+ ///
+ /// Use the FilterMode property to specify how possible matches are
+ /// filtered. For example, possible matches can be filtered in a
+ /// predefined or custom way. The search mode is automatically set to
+ /// Custom if you set the ItemFilter property.
+ ///
+ public AutoCompleteFilterMode FilterMode
+ {
+ get { return GetValue(FilterModeProperty); }
+ set { SetValue(FilterModeProperty, value); }
+ }
+
+ public string Watermark
+ {
+ get { return GetValue(WatermarkProperty); }
+ set { SetValue(WatermarkProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the custom method that uses user-entered text to filter
+ /// the items specified by the
+ ///
+ /// property for display in the drop-down.
+ ///
+ /// The custom method that uses the user-entered text to filter
+ /// the items specified by the
+ ///
+ /// property. The default is null.
+ ///
+ /// The filter mode is automatically set to Custom if you set the
+ /// ItemFilter property.
+ ///
+ public AutoCompleteFilterPredicate ItemFilter
+ {
+ get { return _itemFilter; }
+ set { SetAndRaise(ItemFilterProperty, ref _itemFilter, value); }
+ }
+
+ ///
+ /// Gets or sets the custom method that uses the user-entered text to
+ /// filter items specified by the
+ ///
+ /// property in a text-based way for display in the drop-down.
+ ///
+ /// The custom method that uses the user-entered text to filter
+ /// items specified by the
+ ///
+ /// property in a text-based way for display in the drop-down.
+ ///
+ /// The search mode is automatically set to Custom if you set the
+ /// TextFilter property.
+ ///
+ public AutoCompleteFilterPredicate TextFilter
+ {
+ get { return _textFilter; }
+ 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
+ /// control.
+ ///
+ /// The collection that is used to generate the items of the
+ /// drop-down portion of the
+ /// control.
+ public IEnumerable Items
+ {
+ get { return _itemsEnumerable; }
+ set { SetAndRaise(ItemsProperty, ref _itemsEnumerable, value); }
+ }
+
+ ///
+ /// Gets or sets the drop down popup control.
+ ///
+ private Popup DropDownPopup { get; set; }
+
+ ///
+ /// Gets or sets the Text template part.
+ ///
+ private TextBox TextBox
+ {
+ get { return _textBox; }
+ set
+ {
+ _textBoxSubscriptions?.Dispose();
+ _textBox = value;
+
+ // Attach handlers
+ if (_textBox != null)
+ {
+ _textBoxSubscriptions =
+ _textBox.GetObservable(TextBox.TextProperty)
+ .Subscribe(_ => OnTextBoxTextChanged());
+
+ if (Text != null)
+ {
+ UpdateTextValue(Text);
+ }
+ }
+ }
+ }
+
+ private int TextBoxSelectionStart
+ {
+ get
+ {
+ if (TextBox != null)
+ {
+ return Math.Min(TextBox.SelectionStart, TextBox.SelectionEnd);
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ }
+ private int TextBoxSelectionLength
+ {
+ get
+ {
+ if (TextBox != null)
+ {
+ return Math.Abs(TextBox.SelectionEnd - TextBox.SelectionStart);
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the selection adapter used to populate the drop-down
+ /// with a list of selectable items.
+ ///
+ /// The selection adapter used to populate the drop-down with a
+ /// list of selectable items.
+ ///
+ /// You can use this property when you create an automation peer to
+ /// use with AutoCompleteBox or deriving from AutoCompleteBox to
+ /// create a custom control.
+ ///
+ protected ISelectionAdapter SelectionAdapter
+ {
+ get { return _adapter; }
+ set
+ {
+ if (_adapter != null)
+ {
+ _adapter.SelectionChanged -= OnAdapterSelectionChanged;
+ _adapter.Commit -= OnAdapterSelectionComplete;
+ _adapter.Cancel -= OnAdapterSelectionCanceled;
+ _adapter.Cancel -= OnAdapterSelectionComplete;
+ _adapter.Items = null;
+ }
+
+ _adapter = value;
+
+ if (_adapter != null)
+ {
+ _adapter.SelectionChanged += OnAdapterSelectionChanged;
+ _adapter.Commit += OnAdapterSelectionComplete;
+ _adapter.Cancel += OnAdapterSelectionCanceled;
+ _adapter.Cancel += OnAdapterSelectionComplete;
+ _adapter.Items = _view;
+ }
+ }
+ }
+
+ ///
+ /// Returns the
+ /// part, if
+ /// possible.
+ ///
+ ///
+ /// A object,
+ /// if possible. Otherwise, null.
+ ///
+ protected virtual ISelectionAdapter GetSelectionAdapterPart(INameScope nameScope)
+ {
+ ISelectionAdapter adapter = null;
+ SelectingItemsControl selector = nameScope.Find(ElementSelector);
+ if (selector != null)
+ {
+ // Check if it is already an IItemsSelector
+ adapter = selector as ISelectionAdapter;
+ if (adapter == null)
+ {
+ // Built in support for wrapping a Selector control
+ adapter = new SelectingItemsControlSelectionAdapter(selector);
+ }
+ }
+ if (adapter == null)
+ {
+ adapter = nameScope.Find(ElementSelectionAdapter);
+ }
+ return adapter;
+ }
+
+ ///
+ /// Builds the visual tree for the
+ /// control
+ /// when a new template is applied.
+ ///
+ protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
+ {
+
+ if (DropDownPopup != null)
+ {
+ DropDownPopup.Closed -= DropDownPopup_Closed;
+ DropDownPopup = null;
+ }
+
+ // Set the template parts. Individual part setters remove and add
+ // any event handlers.
+ Popup popup = e.NameScope.Find(ElementPopup);
+ if (popup != null)
+ {
+ DropDownPopup = popup;
+ DropDownPopup.Closed += DropDownPopup_Closed;
+ }
+
+ SelectionAdapter = GetSelectionAdapterPart(e.NameScope);
+ TextBox = e.NameScope.Find(ElementTextBox);
+
+ // If the drop down property indicates that the popup is open,
+ // flip its value to invoke the changed handler.
+ if (IsDropDownOpen && DropDownPopup != null && !DropDownPopup.IsOpen)
+ {
+ OpeningDropDown(false);
+ }
+
+ base.OnTemplateApplied(e);
+ }
+
+ ///
+ /// Provides handling for the
+ /// event.
+ ///
+ /// A
+ /// that contains the event data.
+ protected override void OnKeyDown(KeyEventArgs e)
+ {
+ Contract.Requires(e != null);
+
+ base.OnKeyDown(e);
+
+ if (e.Handled || !IsEnabled)
+ {
+ return;
+ }
+
+ // The drop down is open, pass along the key event arguments to the
+ // selection adapter. If it isn't handled by the adapter's logic,
+ // then we handle some simple navigation scenarios for controlling
+ // the drop down.
+ if (IsDropDownOpen)
+ {
+ if (SelectionAdapter != null)
+ {
+ SelectionAdapter.HandleKeyDown(e);
+ if (e.Handled)
+ {
+ return;
+ }
+ }
+
+ if (e.Key == Key.Escape)
+ {
+ OnAdapterSelectionCanceled(this, new RoutedEventArgs());
+ e.Handled = true;
+ }
+ }
+ else
+ {
+ // The drop down is not open, the Down key will toggle it open.
+ if (e.Key == Key.Down)
+ {
+ IsDropDownOpen = true;
+ e.Handled = true;
+ }
+ }
+
+ // Standard drop down navigation
+ switch (e.Key)
+ {
+ case Key.F4:
+ IsDropDownOpen = !IsDropDownOpen;
+ e.Handled = true;
+ break;
+
+ case Key.Enter:
+ OnAdapterSelectionComplete(this, new RoutedEventArgs());
+ e.Handled = true;
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ ///
+ /// Provides handling for the
+ /// event.
+ ///
+ /// A
+ /// that contains the event data.
+ protected override void OnGotFocus(GotFocusEventArgs e)
+ {
+ base.OnGotFocus(e);
+ FocusChanged(HasFocus());
+ }
+
+ ///
+ /// Provides handling for the
+ /// event.
+ ///
+ /// A
+ /// that contains the event data.
+ protected override void OnLostFocus(RoutedEventArgs e)
+ {
+ base.OnLostFocus(e);
+ FocusChanged(HasFocus());
+ }
+
+ ///
+ /// Determines whether the text box or drop-down portion of the
+ /// control has
+ /// focus.
+ ///
+ /// true to indicate the
+ /// has focus;
+ /// otherwise, false.
+ protected bool HasFocus()
+ {
+ IVisual focused = FocusManager.Instance.Current;
+
+ while (focused != null)
+ {
+ if (object.ReferenceEquals(focused, this))
+ {
+ return true;
+ }
+
+ // This helps deal with popups that may not be in the same
+ // visual tree
+ IVisual parent = focused.GetVisualParent();
+ if (parent == null)
+ {
+ // Try the logical parent.
+ IControl element = focused as IControl;
+ if (element != null)
+ {
+ parent = element.Parent;
+ }
+ }
+ focused = parent;
+ }
+ return false;
+ }
+
+ ///
+ /// Handles the FocusChanged event.
+ ///
+ /// A value indicating whether the control
+ /// currently has the focus.
+ private void FocusChanged(bool hasFocus)
+ {
+ // The OnGotFocus & OnLostFocus are asynchronously and cannot
+ // reliably tell you that have the focus. All they do is let you
+ // know that the focus changed sometime in the past. To determine
+ // if you currently have the focus you need to do consult the
+ // FocusManager (see HasFocus()).
+
+ bool wasFocused = _isFocused;
+ _isFocused = hasFocus;
+
+ if (hasFocus)
+ {
+
+ if (!wasFocused && TextBox != null && TextBoxSelectionLength <= 0)
+ {
+ TextBox.Focus();
+ TextBox.SelectionStart = 0;
+ TextBox.SelectionEnd = TextBox.Text?.Length ?? 0;
+ }
+ }
+ else
+ {
+ IsDropDownOpen = false;
+ _userCalledPopulate = false;
+ ClearTextBoxSelection();
+ }
+
+ _isFocused = hasFocus;
+ }
+
+ ///
+ /// Occurs when the text in the text box portion of the
+ /// changes.
+ ///
+ public event EventHandler TextChanged;
+
+ ///
+ /// Occurs when the
+ /// is
+ /// populating the drop-down with possible matches based on the
+ ///
+ /// property.
+ ///
+ ///
+ /// If the event is canceled, by setting the PopulatingEventArgs.Cancel
+ /// property to true, the AutoCompleteBox will not automatically
+ /// populate the selection adapter contained in the drop-down.
+ /// In this case, if you want possible matches to appear, you must
+ /// provide the logic for populating the selection adapter.
+ ///
+ public event EventHandler Populating;
+
+ ///
+ /// Occurs when the
+ /// has
+ /// populated the drop-down with possible matches based on the
+ ///
+ /// property.
+ ///
+ public event EventHandler Populated;
+
+ ///
+ /// Occurs when the value of the
+ ///
+ /// property is changing from false to true.
+ ///
+ public event EventHandler DropDownOpening;
+
+ ///
+ /// Occurs when the value of the
+ ///
+ /// property has changed from false to true and the drop-down is open.
+ ///
+ public event EventHandler DropDownOpened;
+
+ ///
+ /// Occurs when the
+ ///
+ /// property is changing from true to false.
+ ///
+ public event EventHandler DropDownClosing;
+
+ ///
+ /// Occurs when the
+ ///
+ /// property was changed from true to false and the drop-down is open.
+ ///
+ public event EventHandler DropDownClosed;
+
+ ///
+ /// Occurs when the selected item in the drop-down portion of the
+ /// has
+ /// changed.
+ ///
+ public event EventHandler SelectionChanged
+ {
+ add { AddHandler(SelectionChangedEvent, value); }
+ remove { RemoveHandler(SelectionChangedEvent, value); }
+ }
+
+ ///
+ /// Raises the
+ ///
+ /// event.
+ ///
+ /// A
+ /// that
+ /// contains the event data.
+ protected virtual void OnPopulating(PopulatingEventArgs e)
+ {
+ Populating?.Invoke(this, e);
+ }
+
+ ///
+ /// Raises the
+ ///
+ /// event.
+ ///
+ /// A
+ ///
+ /// that contains the event data.
+ protected virtual void OnPopulated(PopulatedEventArgs e)
+ {
+ Populated?.Invoke(this, e);
+ }
+
+ ///
+ /// Raises the
+ ///
+ /// event.
+ ///
+ /// A
+ ///
+ /// that contains the event data.
+ protected virtual void OnSelectionChanged(SelectionChangedEventArgs e)
+ {
+ RaiseEvent(e);
+ }
+
+ ///
+ /// Raises the
+ ///
+ /// event.
+ ///
+ /// A
+ ///
+ /// that contains the event data.
+ protected virtual void OnDropDownOpening(CancelEventArgs e)
+ {
+ DropDownOpening?.Invoke(this, e);
+ }
+
+ ///
+ /// Raises the
+ ///
+ /// event.
+ ///
+ /// A
+ ///
+ /// that contains the event data.
+ protected virtual void OnDropDownOpened(EventArgs e)
+ {
+ DropDownOpened?.Invoke(this, e);
+ }
+
+ ///
+ /// Raises the
+ ///
+ /// event.
+ ///
+ /// A
+ ///
+ /// that contains the event data.
+ protected virtual void OnDropDownClosing(CancelEventArgs e)
+ {
+ DropDownClosing?.Invoke(this, e);
+ }
+
+ ///
+ /// Raises the
+ ///
+ /// event.
+ ///
+ /// A
+ ///
+ /// which contains the event data.
+ protected virtual void OnDropDownClosed(EventArgs e)
+ {
+ DropDownClosed?.Invoke(this, e);
+ }
+
+ ///
+ /// Raises the
+ ///
+ /// event.
+ ///
+ /// A
+ /// that contains the event data.
+ protected virtual void OnTextChanged(RoutedEventArgs e)
+ {
+ TextChanged?.Invoke(this, e);
+ }
+
+ ///
+ /// Begin closing the drop-down.
+ ///
+ /// The original value.
+ private void ClosingDropDown(bool oldValue)
+ {
+ var args = new CancelEventArgs();
+ OnDropDownClosing(args);
+
+ if (args.Cancel)
+ {
+ _ignorePropertyChange = true;
+ SetValue(IsDropDownOpenProperty, oldValue);
+ }
+ else
+ {
+ CloseDropDown();
+ }
+
+ UpdatePseudoClasses();
+ }
+
+ ///
+ /// Begin opening the drop down by firing cancelable events, opening the
+ /// drop-down or reverting, depending on the event argument values.
+ ///
+ /// The original value, if needed for a revert.
+ private void OpeningDropDown(bool oldValue)
+ {
+ var args = new CancelEventArgs();
+
+ // Opening
+ OnDropDownOpening(args);
+
+ if (args.Cancel)
+ {
+ _ignorePropertyChange = true;
+ SetValue(IsDropDownOpenProperty, oldValue);
+ }
+ else
+ {
+ OpenDropDown();
+ }
+
+ UpdatePseudoClasses();
+ }
+
+ ///
+ /// Connects to the DropDownPopup Closed event.
+ ///
+ /// The source object.
+ /// The event data.
+ private void DropDownPopup_Closed(object sender, EventArgs e)
+ {
+ // Force the drop down dependency property to be false.
+ if (IsDropDownOpen)
+ {
+ IsDropDownOpen = false;
+ }
+
+ // Fire the DropDownClosed event
+ if (_popupHasOpened)
+ {
+ OnDropDownClosed(EventArgs.Empty);
+ }
+ }
+
+ ///
+ /// Handles the timer tick when using a populate delay.
+ ///
+ /// The source object.
+ /// The event arguments.
+ private void PopulateDropDown(object sender, EventArgs e)
+ {
+ if (_delayTimer != null)
+ {
+ _delayTimer.Stop();
+ }
+
+ // 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
+ // display process if Cancel is set to true.
+ PopulatingEventArgs populating = new PopulatingEventArgs(SearchText);
+ OnPopulating(populating);
+ if (!populating.Cancel)
+ {
+ 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
+ /// button, and then fires the Opened event.
+ ///
+ private void OpenDropDown()
+ {
+ if (DropDownPopup != null)
+ {
+ DropDownPopup.IsOpen = true;
+ }
+ _popupHasOpened = true;
+ OnDropDownOpened(EventArgs.Empty);
+ }
+
+ ///
+ /// Private method that directly closes the popup, flips the Checked
+ /// value, and then fires the Closed event.
+ ///
+ private void CloseDropDown()
+ {
+ if (_popupHasOpened)
+ {
+ if (SelectionAdapter != null)
+ {
+ SelectionAdapter.SelectedItem = null;
+ }
+ if (DropDownPopup != null)
+ {
+ DropDownPopup.IsOpen = false;
+ }
+ OnDropDownClosed(EventArgs.Empty);
+ }
+ }
+
+ ///
+ /// Formats an Item for text comparisons based on Converter
+ /// and ConverterCulture properties.
+ ///
+ /// The object to format.
+ /// A value indicating whether to clear
+ /// the data context after the lookup is performed.
+ /// Formatted Value.
+ private string FormatValue(object value, bool clearDataContext)
+ {
+ string result = FormatValue(value);
+ if(clearDataContext && _valueBindingEvaluator != null)
+ {
+ _valueBindingEvaluator.ClearDataContext();
+ }
+
+ return result;
+ }
+
+ ///
+ /// Converts the specified object to a string by using the
+ /// and
+ /// values
+ /// of the binding object specified by the
+ ///
+ /// property.
+ ///
+ /// The object to format as a string.
+ /// The string representation of the specified object.
+ ///
+ /// Override this method to provide a custom string conversion.
+ ///
+ protected virtual string FormatValue(object value)
+ {
+ if (_valueBindingEvaluator != null)
+ {
+ return _valueBindingEvaluator.GetDynamicValue(value) ?? String.Empty;
+ }
+
+ if (_valueMemberSelector != null)
+ {
+ value = _valueMemberSelector.Select(value);
+ }
+
+ return value == null ? String.Empty : value.ToString();
+ }
+
+ ///
+ /// Handle the TextChanged event that is directly attached to the
+ /// TextBox part. This ensures that only user initiated actions will
+ /// result in an AutoCompleteBox suggestion and operation.
+ ///
+ private void OnTextBoxTextChanged()
+ {
+ //Uses Dispatcher.Post to allow the TextBox selection to update before processing
+ Dispatcher.UIThread.Post(() =>
+ {
+ // Call the central updated text method as a user-initiated action
+ TextUpdated(_textBox.Text, true);
+ });
+ }
+
+ ///
+ /// Updates both the text box value and underlying text dependency
+ /// property value if and when they change. Automatically fires the
+ /// text changed events when there is a change.
+ ///
+ /// The new string value.
+ private void UpdateTextValue(string value)
+ {
+ UpdateTextValue(value, null);
+ }
+
+ ///
+ /// Updates both the text box value and underlying text dependency
+ /// property value if and when they change. Automatically fires the
+ /// text changed events when there is a change.
+ ///
+ /// The new string value.
+ /// A nullable bool value indicating whether
+ /// the action was user initiated. In a user initiated mode, the
+ /// underlying text dependency property is updated. In a non-user
+ /// interaction, the text box value is updated. When user initiated is
+ /// null, all values are updated.
+ private void UpdateTextValue(string value, bool? userInitiated)
+ {
+ bool callTextChanged = false;
+ // Update the Text dependency property
+ if ((userInitiated == null || userInitiated == true) && Text != value)
+ {
+ _ignoreTextPropertyChange++;
+ Text = value;
+ callTextChanged = true;
+ }
+
+ // Update the TextBox's Text dependency property
+ if ((userInitiated == null || userInitiated == false) && TextBox != null && TextBox.Text != value)
+ {
+ _ignoreTextPropertyChange++;
+ TextBox.Text = value ?? string.Empty;
+
+ // Text dependency property value was set, fire event
+ if (!callTextChanged && (Text == value || Text == null))
+ {
+ callTextChanged = true;
+ }
+ }
+
+ if (callTextChanged)
+ {
+ OnTextChanged(new RoutedEventArgs());
+ }
+ }
+
+ ///
+ /// Handle the update of the text for the control from any source,
+ /// including the TextBox part and the Text dependency property.
+ ///
+ /// The new text.
+ /// A value indicating whether the update
+ /// is a user-initiated action. This should be a True value when the
+ /// TextUpdated method is called from a TextBox event handler.
+ private void TextUpdated(string newText, bool userInitiated)
+ {
+ // Only process this event if it is coming from someone outside
+ // setting the Text dependency property directly.
+ if (_ignoreTextPropertyChange > 0)
+ {
+ _ignoreTextPropertyChange--;
+ return;
+ }
+
+ if (newText == null)
+ {
+ newText = string.Empty;
+ }
+
+ // The TextBox.TextChanged event was not firing immediately and
+ // was causing an immediate update, even with wrapping. If there is
+ // a selection currently, no update should happen.
+ if (IsTextCompletionEnabled && TextBox != null && TextBoxSelectionLength > 0 && TextBoxSelectionStart != TextBox.Text.Length)
+ {
+ return;
+ }
+
+ // Evaluate the conditions needed for completion.
+ // 1. Minimum prefix length
+ // 2. If a delay timer is in use, use it
+ bool populateReady = newText.Length >= MinimumPrefixLength && MinimumPrefixLength >= 0;
+ _userCalledPopulate = populateReady ? userInitiated : false;
+
+ // Update the interface and values only as necessary
+ UpdateTextValue(newText, userInitiated);
+
+ if (populateReady)
+ {
+ _ignoreTextSelectionChange = true;
+
+ if (_delayTimer != null)
+ {
+ _delayTimer.Start();
+ }
+ else
+ {
+ PopulateDropDown(this, EventArgs.Empty);
+ }
+ }
+ else
+ {
+ SearchText = string.Empty;
+ if (SelectedItem != null)
+ {
+ _skipSelectedItemTextUpdate = true;
+ }
+ SelectedItem = null;
+ if (IsDropDownOpen)
+ {
+ IsDropDownOpen = false;
+ }
+ }
+ }
+
+ ///
+ /// A simple helper method to clear the view and ensure that a view
+ /// object is always present and not null.
+ ///
+ private void ClearView()
+ {
+ if (_view == null)
+ {
+ _view = new AvaloniaList();
+ }
+ else
+ {
+ _view.Clear();
+ }
+ }
+
+ ///
+ /// Walks through the items enumeration. Performance is not going to be
+ /// perfect with the current implementation.
+ ///
+ private void RefreshView()
+ {
+ if (_items == null)
+ {
+ ClearView();
+ return;
+ }
+
+ // Cache the current text value
+ string text = Text ?? string.Empty;
+
+ // Determine if any filtering mode is on
+ bool stringFiltering = TextFilter != null;
+ bool objectFiltering = FilterMode == AutoCompleteFilterMode.Custom && TextFilter == null;
+
+ int view_index = 0;
+ int view_count = _view.Count;
+ List items = _items;
+ foreach (object item in items)
+ {
+ bool inResults = !(stringFiltering || objectFiltering);
+ if (!inResults)
+ {
+ inResults = stringFiltering ? TextFilter(text, FormatValue(item)) : ItemFilter(text, item);
+ }
+
+ if (view_count > view_index && inResults && _view[view_index] == item)
+ {
+ // Item is still in the view
+ view_index++;
+ }
+ else if (inResults)
+ {
+ // Insert the item
+ if (view_count > view_index && _view[view_index] != item)
+ {
+ // Replace item
+ // Unfortunately replacing via index throws a fatal
+ // exception: View[view_index] = item;
+ // Cost: O(n) vs O(1)
+ _view.RemoveAt(view_index);
+ _view.Insert(view_index, item);
+ view_index++;
+ }
+ else
+ {
+ // Add the item
+ if (view_index == view_count)
+ {
+ // Constant time is preferred (Add).
+ _view.Add(item);
+ }
+ else
+ {
+ _view.Insert(view_index, item);
+ }
+ view_index++;
+ view_count++;
+ }
+ }
+ else if (view_count > view_index && _view[view_index] == item)
+ {
+ // Remove the item
+ _view.RemoveAt(view_index);
+ view_count--;
+ }
+ }
+
+ // Clear the evaluator to discard a reference to the last item
+ if (_valueBindingEvaluator != null)
+ {
+ _valueBindingEvaluator.ClearDataContext();
+ }
+ }
+
+ ///
+ /// Handle any change to the ItemsSource dependency property, update
+ /// the underlying ObservableCollection view, and set the selection
+ /// adapter's ItemsSource to the view if appropriate.
+ ///
+ /// The new enumerable reference.
+ private void OnItemsChanged(IEnumerable newValue)
+ {
+ // Remove handler for oldValue.CollectionChanged (if present)
+ _collectionChangeSubscription?.Dispose();
+ _collectionChangeSubscription = null;
+
+ // Add handler for newValue.CollectionChanged (if possible)
+ if (newValue is INotifyCollectionChanged newValueINotifyCollectionChanged)
+ {
+ _collectionChangeSubscription = newValueINotifyCollectionChanged.WeakSubscribe(ItemsCollectionChanged);
+ }
+
+ // Store a local cached copy of the data
+ _items = newValue == null ? null : new List(newValue.Cast().ToList());
+
+ // Clear and set the view on the selection adapter
+ ClearView();
+ if (SelectionAdapter != null && SelectionAdapter.Items != _view)
+ {
+ SelectionAdapter.Items = _view;
+ }
+ if (IsDropDownOpen)
+ {
+ RefreshView();
+ }
+ }
+
+ ///
+ /// Method that handles the ObservableCollection.CollectionChanged event for the ItemsSource property.
+ ///
+ /// The object that raised the event.
+ /// The event data.
+ private void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ // Update the cache
+ if (e.Action == NotifyCollectionChangedAction.Remove && e.OldItems != null)
+ {
+ for (int index = 0; index < e.OldItems.Count; index++)
+ {
+ _items.RemoveAt(e.OldStartingIndex);
+ }
+ }
+ if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems != null && _items.Count >= e.NewStartingIndex)
+ {
+ for (int index = 0; index < e.NewItems.Count; index++)
+ {
+ _items.Insert(e.NewStartingIndex + index, e.NewItems[index]);
+ }
+ }
+ if (e.Action == NotifyCollectionChangedAction.Replace && e.NewItems != null && e.OldItems != null)
+ {
+ for (int index = 0; index < e.NewItems.Count; index++)
+ {
+ _items[e.NewStartingIndex] = e.NewItems[index];
+ }
+ }
+
+ // Update the view
+ if (e.Action == NotifyCollectionChangedAction.Remove || e.Action == NotifyCollectionChangedAction.Replace)
+ {
+ for (int index = 0; index < e.OldItems.Count; index++)
+ {
+ _view.Remove(e.OldItems[index]);
+ }
+ }
+
+ if (e.Action == NotifyCollectionChangedAction.Reset)
+ {
+ // Significant changes to the underlying data.
+ ClearView();
+ if (Items != null)
+ {
+ _items = new List(Items.Cast().ToList());
+ }
+ }
+
+ // Refresh the observable collection used in the selection adapter.
+ RefreshView();
+ }
+
+ ///
+ /// Notifies the
+ /// that the
+ ///
+ /// property has been set and the data can be filtered to provide
+ /// possible matches in the drop-down.
+ ///
+ ///
+ /// Call this method when you are providing custom population of
+ /// the drop-down portion of the AutoCompleteBox, to signal the control
+ /// that you are done with the population process.
+ /// Typically, you use PopulateComplete when the population process
+ /// is a long-running process and you want to cancel built-in filtering
+ /// of the ItemsSource items. In this case, you can handle the
+ /// Populated event and set PopulatingEventArgs.Cancel to true.
+ /// When the long-running process has completed you call
+ /// PopulateComplete to indicate the drop-down is populated.
+ ///
+ public void PopulateComplete()
+ {
+ // Apply the search filter
+ RefreshView();
+
+ // Fire the Populated event containing the read-only view data.
+ PopulatedEventArgs populated = new PopulatedEventArgs(new ReadOnlyCollection(_view));
+ OnPopulated(populated);
+
+ if (SelectionAdapter != null && SelectionAdapter.Items != _view)
+ {
+ SelectionAdapter.Items = _view;
+ }
+
+ bool isDropDownOpen = _userCalledPopulate && (_view.Count > 0);
+ if (isDropDownOpen != IsDropDownOpen)
+ {
+ _ignorePropertyChange = true;
+ IsDropDownOpen = isDropDownOpen;
+ }
+ if (IsDropDownOpen)
+ {
+ OpeningDropDown(false);
+ }
+ else
+ {
+ ClosingDropDown(true);
+ }
+
+ UpdateTextCompletion(_userCalledPopulate);
+ }
+
+ ///
+ /// Performs text completion, if enabled, and a lookup on the underlying
+ /// item values for an exact match. Will update the SelectedItem value.
+ ///
+ /// A value indicating whether the operation
+ /// was user initiated. Text completion will not be performed when not
+ /// directly initiated by the user.
+ private void UpdateTextCompletion(bool userInitiated)
+ {
+ // By default this method will clear the selected value
+ object newSelectedItem = null;
+ string text = Text;
+
+ // Text search is StartsWith explicit and only when enabled, in
+ // line with WPF's ComboBox lookup. When in use it will associate
+ // a Value with the Text if it is found in ItemsSource. This is
+ // only valid when there is data and the user initiated the action.
+ if (_view.Count > 0)
+ {
+ if (IsTextCompletionEnabled && TextBox != null && userInitiated)
+ {
+ int currentLength = TextBox.Text.Length;
+ int selectionStart = TextBoxSelectionStart;
+ if (selectionStart == text.Length && selectionStart > _textSelectionStart)
+ {
+ // When the FilterMode dependency property is set to
+ // either StartsWith or StartsWithCaseSensitive, the
+ // first item in the view is used. This will improve
+ // performance on the lookup. It assumes that the
+ // FilterMode the user has selected is an acceptable
+ // case sensitive matching function for their scenario.
+ object top = FilterMode == AutoCompleteFilterMode.StartsWith || FilterMode == AutoCompleteFilterMode.StartsWithCaseSensitive
+ ? _view[0]
+ : TryGetMatch(text, _view, AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith));
+
+ // If the search was successful, update SelectedItem
+ if (top != null)
+ {
+ newSelectedItem = top;
+ string topString = FormatValue(top, true);
+
+ // Only replace partially when the two words being the same
+ int minLength = Math.Min(topString.Length, Text.Length);
+ if (AutoCompleteSearch.Equals(Text.Substring(0, minLength), topString.Substring(0, minLength)))
+ {
+ // Update the text
+ UpdateTextValue(topString);
+
+ // Select the text past the user's caret
+ TextBox.SelectionStart = currentLength;
+ TextBox.SelectionEnd = topString.Length;
+ }
+ }
+ }
+ }
+ else
+ {
+ // Perform an exact string lookup for the text. This is a
+ // design change from the original Toolkit release when the
+ // IsTextCompletionEnabled property behaved just like the
+ // WPF ComboBox's IsTextSearchEnabled property.
+ //
+ // This change provides the behavior that most people expect
+ // to find: a lookup for the value is always performed.
+ newSelectedItem = TryGetMatch(text, _view, AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.EqualsCaseSensitive));
+ }
+ }
+
+ // Update the selected item property
+
+ if (SelectedItem != newSelectedItem)
+ {
+ _skipSelectedItemTextUpdate = true;
+ }
+ SelectedItem = newSelectedItem;
+
+ // Restore updates for TextSelection
+ if (_ignoreTextSelectionChange)
+ {
+ _ignoreTextSelectionChange = false;
+ if (TextBox != null)
+ {
+ _textSelectionStart = TextBoxSelectionStart;
+ }
+ }
+ }
+
+ ///
+ /// Attempts to look through the view and locate the specific exact
+ /// text match.
+ ///
+ /// The search text.
+ /// The view reference.
+ /// The predicate to use for the partial or
+ /// exact match.
+ /// Returns the object or null.
+ private object TryGetMatch(string searchText, AvaloniaList view, AutoCompleteFilterPredicate predicate)
+ {
+ if (view != null && view.Count > 0)
+ {
+ foreach (object o in view)
+ {
+ if (predicate(searchText, FormatValue(o)))
+ {
+ return o;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private void UpdatePseudoClasses()
+ {
+ PseudoClasses.Set(":dropdownopen", IsDropDownOpen);
+ }
+
+ private void ClearTextBoxSelection()
+ {
+ if (TextBox != null)
+ {
+ int length = TextBox.Text?.Length ?? 0;
+ TextBox.SelectionStart = length;
+ TextBox.SelectionEnd = length;
+ }
+ }
+
+ ///
+ /// Called when the selected item is changed, updates the text value
+ /// that is displayed in the text box part.
+ ///
+ /// The new item.
+ private void OnSelectedItemChanged(object newItem)
+ {
+ string text;
+
+ if (newItem == null)
+ {
+ text = SearchText;
+ }
+ else
+ {
+ text = FormatValue(newItem, true);
+ }
+
+ // Update the Text property and the TextBox values
+ UpdateTextValue(text);
+
+ // Move the caret to the end of the text box
+ ClearTextBoxSelection();
+ }
+
+ ///
+ /// Handles the SelectionChanged event of the selection adapter.
+ ///
+ /// The source object.
+ /// The selection changed event data.
+ private void OnAdapterSelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ SelectedItem = _adapter.SelectedItem;
+ }
+
+ //TODO Check UpdateTextCompletion
+ ///
+ /// Handles the Commit event on the selection adapter.
+ ///
+ /// The source object.
+ /// The event data.
+ private void OnAdapterSelectionComplete(object sender, RoutedEventArgs e)
+ {
+ IsDropDownOpen = false;
+
+ // Completion will update the selected value
+ //UpdateTextCompletion(false);
+
+ // Text should not be selected
+ ClearTextBoxSelection();
+
+ TextBox.Focus();
+ }
+
+ ///
+ /// Handles the Cancel event on the selection adapter.
+ ///
+ /// The source object.
+ /// The event data.
+ private void OnAdapterSelectionCanceled(object sender, RoutedEventArgs e)
+ {
+ UpdateTextValue(SearchText);
+
+ // Completion will update the selected value
+ UpdateTextCompletion(false);
+ }
+
+ ///
+ /// A predefined set of filter functions for the known, built-in
+ /// AutoCompleteFilterMode enumeration values.
+ ///
+ private static class AutoCompleteSearch
+ {
+ ///
+ /// Index function that retrieves the filter for the provided
+ /// AutoCompleteFilterMode.
+ ///
+ /// The built-in search mode.
+ /// Returns the string-based comparison function.
+ public static AutoCompleteFilterPredicate GetFilter(AutoCompleteFilterMode FilterMode)
+ {
+ switch (FilterMode)
+ {
+ case AutoCompleteFilterMode.Contains:
+ return Contains;
+
+ case AutoCompleteFilterMode.ContainsCaseSensitive:
+ return ContainsCaseSensitive;
+
+ case AutoCompleteFilterMode.ContainsOrdinal:
+ return ContainsOrdinal;
+
+ case AutoCompleteFilterMode.ContainsOrdinalCaseSensitive:
+ return ContainsOrdinalCaseSensitive;
+
+ case AutoCompleteFilterMode.Equals:
+ return Equals;
+
+ case AutoCompleteFilterMode.EqualsCaseSensitive:
+ return EqualsCaseSensitive;
+
+ case AutoCompleteFilterMode.EqualsOrdinal:
+ return EqualsOrdinal;
+
+ case AutoCompleteFilterMode.EqualsOrdinalCaseSensitive:
+ return EqualsOrdinalCaseSensitive;
+
+ case AutoCompleteFilterMode.StartsWith:
+ return StartsWith;
+
+ case AutoCompleteFilterMode.StartsWithCaseSensitive:
+ return StartsWithCaseSensitive;
+
+ case AutoCompleteFilterMode.StartsWithOrdinal:
+ return StartsWithOrdinal;
+
+ case AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive:
+ return StartsWithOrdinalCaseSensitive;
+
+ case AutoCompleteFilterMode.None:
+ case AutoCompleteFilterMode.Custom:
+ default:
+ return null;
+ }
+ }
+
+ ///
+ /// An implementation of the Contains member of string that takes in a
+ /// string comparison. The traditional .NET string Contains member uses
+ /// StringComparison.Ordinal.
+ ///
+ /// The string.
+ /// The string value to search for.
+ /// The string comparison type.
+ /// Returns true when the substring is found.
+ private static bool Contains(string s, string value, StringComparison comparison)
+ {
+ return s.IndexOf(value, comparison) >= 0;
+ }
+
+ ///
+ /// Check if the string value begins with the text.
+ ///
+ /// The AutoCompleteBox prefix text.
+ /// The item's string value.
+ /// Returns true if the condition is met.
+ public static bool StartsWith(string text, string value)
+ {
+ return value.StartsWith(text, StringComparison.CurrentCultureIgnoreCase);
+ }
+
+ ///
+ /// Check if the string value begins with the text.
+ ///
+ /// The AutoCompleteBox prefix text.
+ /// The item's string value.
+ /// Returns true if the condition is met.
+ public static bool StartsWithCaseSensitive(string text, string value)
+ {
+ return value.StartsWith(text, StringComparison.CurrentCulture);
+ }
+
+ ///
+ /// Check if the string value begins with the text.
+ ///
+ /// The AutoCompleteBox prefix text.
+ /// The item's string value.
+ /// Returns true if the condition is met.
+ public static bool StartsWithOrdinal(string text, string value)
+ {
+ return value.StartsWith(text, StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Check if the string value begins with the text.
+ ///
+ /// The AutoCompleteBox prefix text.
+ /// The item's string value.
+ /// Returns true if the condition is met.
+ public static bool StartsWithOrdinalCaseSensitive(string text, string value)
+ {
+ return value.StartsWith(text, StringComparison.Ordinal);
+ }
+
+ ///
+ /// Check if the prefix is contained in the string value. The current
+ /// culture's case insensitive string comparison operator is used.
+ ///
+ /// The AutoCompleteBox prefix text.
+ /// The item's string value.
+ /// Returns true if the condition is met.
+ public static bool Contains(string text, string value)
+ {
+ return Contains(value, text, StringComparison.CurrentCultureIgnoreCase);
+ }
+
+ ///
+ /// Check if the prefix is contained in the string value.
+ ///
+ /// The AutoCompleteBox prefix text.
+ /// The item's string value.
+ /// Returns true if the condition is met.
+ public static bool ContainsCaseSensitive(string text, string value)
+ {
+ return Contains(value, text, StringComparison.CurrentCulture);
+ }
+
+ ///
+ /// Check if the prefix is contained in the string value.
+ ///
+ /// The AutoCompleteBox prefix text.
+ /// The item's string value.
+ /// Returns true if the condition is met.
+ public static bool ContainsOrdinal(string text, string value)
+ {
+ return Contains(value, text, StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Check if the prefix is contained in the string value.
+ ///
+ /// The AutoCompleteBox prefix text.
+ /// The item's string value.
+ /// Returns true if the condition is met.
+ public static bool ContainsOrdinalCaseSensitive(string text, string value)
+ {
+ return Contains(value, text, StringComparison.Ordinal);
+ }
+
+ ///
+ /// Check if the string values are equal.
+ ///
+ /// The AutoCompleteBox prefix text.
+ /// The item's string value.
+ /// Returns true if the condition is met.
+ public static bool Equals(string text, string value)
+ {
+ return value.Equals(text, StringComparison.CurrentCultureIgnoreCase);
+ }
+
+ ///
+ /// Check if the string values are equal.
+ ///
+ /// The AutoCompleteBox prefix text.
+ /// The item's string value.
+ /// Returns true if the condition is met.
+ public static bool EqualsCaseSensitive(string text, string value)
+ {
+ return value.Equals(text, StringComparison.CurrentCulture);
+ }
+
+ ///
+ /// Check if the string values are equal.
+ ///
+ /// The AutoCompleteBox prefix text.
+ /// The item's string value.
+ /// Returns true if the condition is met.
+ public static bool EqualsOrdinal(string text, string value)
+ {
+ return value.Equals(text, StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Check if the string values are equal.
+ ///
+ /// The AutoCompleteBox prefix text.
+ /// The item's string value.
+ /// Returns true if the condition is met.
+ public static bool EqualsOrdinalCaseSensitive(string text, string value)
+ {
+ return value.Equals(text, StringComparison.Ordinal);
+ }
+ }
+
+ ///
+ /// A framework element that permits a binding to be evaluated in a new data
+ /// context leaf node.
+ ///
+ /// The type of dynamic binding to return.
+ public class BindingEvaluator : Control
+ {
+ ///
+ /// Gets or sets the string value binding used by the control.
+ ///
+ private IBinding _binding;
+
+ #region public T Value
+
+ ///
+ /// Identifies the Value dependency property.
+ ///
+ public static readonly StyledProperty ValueProperty =
+ AvaloniaProperty.Register, T>(nameof(Value));
+
+ ///
+ /// Gets or sets the data item value.
+ ///
+ public T Value
+ {
+ get { return GetValue(ValueProperty); }
+ set { SetValue(ValueProperty, value); }
+ }
+
+ #endregion public string Value
+
+ ///
+ /// Gets or sets the value binding.
+ ///
+ public IBinding ValueBinding
+ {
+ get { return _binding; }
+ set
+ {
+ _binding = value;
+ AvaloniaObjectExtensions.Bind(this, ValueProperty, value);
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the BindingEvaluator class.
+ ///
+ public BindingEvaluator()
+ { }
+
+ ///
+ /// Initializes a new instance of the BindingEvaluator class,
+ /// setting the initial binding to the provided parameter.
+ ///
+ /// The initial string value binding.
+ public BindingEvaluator(IBinding binding)
+ : this()
+ {
+ ValueBinding = binding;
+ }
+
+ ///
+ /// Clears the data context so that the control does not keep a
+ /// reference to the last-looked up item.
+ ///
+ public void ClearDataContext()
+ {
+ DataContext = null;
+ }
+
+ ///
+ /// Updates the data context of the framework element and returns the
+ /// updated binding value.
+ ///
+ /// The object to use as the data context.
+ /// If set to true, this parameter will
+ /// clear the data context immediately after retrieving the value.
+ /// Returns the evaluated T value of the bound dependency
+ /// property.
+ public T GetDynamicValue(object o, bool clearDataContext)
+ {
+ DataContext = o;
+ T value = Value;
+ if (clearDataContext)
+ {
+ DataContext = null;
+ }
+ return value;
+ }
+
+ ///
+ /// Updates the data context of the framework element and returns the
+ /// updated binding value.
+ ///
+ /// The object to use as the data context.
+ /// Returns the evaluated T value of the bound dependency
+ /// property.
+ public T GetDynamicValue(object o)
+ {
+ DataContext = o;
+ return Value;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index a17a072f19..34c6b1cfd6 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/src/Avalonia.Controls/TextBox.cs
@@ -812,7 +812,7 @@ namespace Avalonia.Controls
int pos = 0;
int i;
- for (i = 0; i < lines.Count; ++i)
+ for (i = 0; i < lines.Count - 1; ++i)
{
var line = lines[i];
pos += line.Length;
diff --git a/src/Avalonia.Controls/Utils/ISelectionAdapter.cs b/src/Avalonia.Controls/Utils/ISelectionAdapter.cs
new file mode 100644
index 0000000000..3c1006a12e
--- /dev/null
+++ b/src/Avalonia.Controls/Utils/ISelectionAdapter.cs
@@ -0,0 +1,64 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System;
+using System.Collections;
+using Avalonia.Interactivity;
+using Avalonia.Input;
+
+namespace Avalonia.Controls.Utils
+{
+ ///
+ /// Defines an item collection, selection members, and key handling for the
+ /// selection adapter contained in the drop-down portion of an
+ /// control.
+ ///
+ public interface ISelectionAdapter
+ {
+ ///
+ /// Gets or sets the selected item.
+ ///
+ /// The currently selected item.
+ object SelectedItem { get; set; }
+
+ ///
+ /// Occurs when the
+ ///
+ /// property value changes.
+ ///
+ event EventHandler SelectionChanged;
+
+ ///
+ /// Gets or sets a collection that is used to generate content for the
+ /// selection adapter.
+ ///
+ /// The collection that is used to generate content for the
+ /// selection adapter.
+ IEnumerable Items { get; set; }
+
+ ///
+ /// Occurs when a selected item is not cancelled and is committed as the
+ /// selected item.
+ ///
+ event EventHandler Commit;
+
+ ///
+ /// Occurs when a selection has been canceled.
+ ///
+ event EventHandler Cancel;
+
+ ///
+ /// Provides handling for the
+ /// event that occurs
+ /// when a key is pressed while the drop-down portion of the
+ /// has focus.
+ ///
+ /// A
+ /// that contains data about the
+ /// event.
+ void HandleKeyDown(KeyEventArgs e);
+ }
+
+}
diff --git a/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs b/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs
new file mode 100644
index 0000000000..43c8a5aa6c
--- /dev/null
+++ b/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs
@@ -0,0 +1,342 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.Text;
+using Avalonia.Controls.Primitives;
+using Avalonia.Interactivity;
+using Avalonia.Input;
+using Avalonia.LogicalTree;
+using System.Collections;
+using System.Diagnostics;
+
+namespace Avalonia.Controls.Utils
+{
+ ///
+ /// Represents the selection adapter contained in the drop-down portion of
+ /// an control.
+ ///
+ public class SelectingItemsControlSelectionAdapter : ISelectionAdapter
+ {
+ ///
+ /// The SelectingItemsControl instance.
+ ///
+ private SelectingItemsControl _selector;
+
+ ///
+ /// Gets or sets a value indicating whether the selection change event
+ /// should not be fired.
+ ///
+ private bool IgnoringSelectionChanged { get; set; }
+
+ ///
+ /// Gets or sets the underlying
+ ///
+ /// control.
+ ///
+ /// The underlying
+ ///
+ /// control.
+ public SelectingItemsControl SelectorControl
+ {
+ get { return _selector; }
+
+ set
+ {
+ if (_selector != null)
+ {
+ _selector.SelectionChanged -= OnSelectionChanged;
+ _selector.PointerReleased -= OnSelectorPointerReleased;
+ }
+
+ _selector = value;
+
+ if (_selector != null)
+ {
+ _selector.SelectionChanged += OnSelectionChanged;
+ _selector.PointerReleased += OnSelectorPointerReleased;
+ }
+ }
+ }
+
+ ///
+ /// Occurs when the
+ ///
+ /// property value changes.
+ ///
+ public event EventHandler SelectionChanged;
+
+ ///
+ /// Occurs when an item is selected and is committed to the underlying
+ ///
+ /// control.
+ ///
+ public event EventHandler Commit;
+
+ ///
+ /// Occurs when a selection is canceled before it is committed.
+ ///
+ public event EventHandler Cancel;
+
+ ///
+ /// Initializes a new instance of the
+ ///
+ /// class.
+ ///
+ public SelectingItemsControlSelectionAdapter()
+ {
+
+ }
+
+ ///
+ /// Initializes a new instance of the
+ ///
+ /// class with the specified
+ ///
+ /// control.
+ ///
+ /// The
+ /// control
+ /// to wrap as a
+ /// .
+ public SelectingItemsControlSelectionAdapter(SelectingItemsControl selector)
+ {
+ SelectorControl = selector;
+ }
+
+ ///
+ /// Gets or sets the selected item of the selection adapter.
+ ///
+ /// The selected item of the underlying selection adapter.
+ public object SelectedItem
+ {
+ get
+ {
+ return SelectorControl?.SelectedItem;
+ }
+
+ set
+ {
+ IgnoringSelectionChanged = true;
+ if (SelectorControl != null)
+ {
+ SelectorControl.SelectedItem = value;
+ }
+
+ // Attempt to reset the scroll viewer's position
+ if (value == null)
+ {
+ ResetScrollViewer();
+ }
+
+ IgnoringSelectionChanged = false;
+ }
+ }
+
+ ///
+ /// Gets or sets a collection that is used to generate the content of
+ /// the selection adapter.
+ ///
+ /// The collection used to generate content for the selection
+ /// adapter.
+ public IEnumerable Items
+ {
+ get
+ {
+ return SelectorControl?.Items;
+ }
+ set
+ {
+ if (SelectorControl != null)
+ {
+ SelectorControl.Items = value;
+ }
+ }
+ }
+
+ ///
+ /// If the control contains a ScrollViewer, this will reset the viewer
+ /// to be scrolled to the top.
+ ///
+ private void ResetScrollViewer()
+ {
+ if (SelectorControl != null)
+ {
+ ScrollViewer sv = SelectorControl.GetLogicalDescendants().OfType().FirstOrDefault();
+ if (sv != null)
+ {
+ sv.Offset = new Vector(0, 0);
+ }
+ }
+ }
+
+ ///
+ /// Handles the mouse left button up event on the selector control.
+ ///
+ /// The source object.
+ /// The event data.
+ private void OnSelectorPointerReleased(object sender, PointerReleasedEventArgs e)
+ {
+ if (e.MouseButton == MouseButton.Left)
+ {
+ OnCommit();
+ }
+ }
+
+ ///
+ /// Handles the SelectionChanged event on the SelectingItemsControl control.
+ ///
+ /// The source object.
+ /// The selection changed event data.
+ private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (IgnoringSelectionChanged)
+ {
+ return;
+ }
+
+ SelectionChanged?.Invoke(sender, e);
+ }
+
+ ///
+ /// Increments the
+ ///
+ /// property of the underlying
+ ///
+ /// control.
+ ///
+ protected void SelectedIndexIncrement()
+ {
+ if (SelectorControl != null)
+ {
+ SelectorControl.SelectedIndex = SelectorControl.SelectedIndex + 1 >= SelectorControl.ItemCount ? -1 : SelectorControl.SelectedIndex + 1;
+ }
+ }
+
+ ///
+ /// Decrements the
+ ///
+ /// property of the underlying
+ ///
+ /// control.
+ ///
+ protected void SelectedIndexDecrement()
+ {
+ if (SelectorControl != null)
+ {
+ int index = SelectorControl.SelectedIndex;
+ if (index >= 0)
+ {
+ SelectorControl.SelectedIndex--;
+ }
+ else if (index == -1)
+ {
+ SelectorControl.SelectedIndex = SelectorControl.ItemCount - 1;
+ }
+ }
+ }
+
+ ///
+ /// Provides handling for the
+ /// event that occurs
+ /// when a key is pressed while the drop-down portion of the
+ /// has focus.
+ ///
+ /// A
+ /// that contains data about the
+ /// event.
+ public void HandleKeyDown(KeyEventArgs e)
+ {
+ switch (e.Key)
+ {
+ case Key.Enter:
+ OnCommit();
+ e.Handled = true;
+ break;
+
+ case Key.Up:
+ SelectedIndexDecrement();
+ e.Handled = true;
+ break;
+
+ case Key.Down:
+ if ((e.Modifiers & InputModifiers.Alt) == InputModifiers.None)
+ {
+ SelectedIndexIncrement();
+ e.Handled = true;
+ }
+ break;
+
+ case Key.Escape:
+ OnCancel();
+ e.Handled = true;
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ ///
+ /// Raises the
+ ///
+ /// event.
+ ///
+ protected virtual void OnCommit()
+ {
+ OnCommit(this, new RoutedEventArgs());
+ }
+
+ ///
+ /// Fires the Commit event.
+ ///
+ /// The source object.
+ /// The event data.
+ private void OnCommit(object sender, RoutedEventArgs e)
+ {
+ Commit?.Invoke(sender, e);
+
+ AfterAdapterAction();
+ }
+
+ ///
+ /// Raises the
+ ///
+ /// event.
+ ///
+ protected virtual void OnCancel()
+ {
+ OnCancel(this, new RoutedEventArgs());
+ }
+
+ ///
+ /// Fires the Cancel event.
+ ///
+ /// The source object.
+ /// The event data.
+ private void OnCancel(object sender, RoutedEventArgs e)
+ {
+ Cancel?.Invoke(sender, e);
+
+ AfterAdapterAction();
+ }
+
+ ///
+ /// Change the selection after the actions are complete.
+ ///
+ private void AfterAdapterAction()
+ {
+ IgnoringSelectionChanged = true;
+ if (SelectorControl != null)
+ {
+ SelectorControl.SelectedItem = null;
+ SelectorControl.SelectedIndex = -1;
+ }
+ IgnoringSelectionChanged = false;
+ }
+ }
+}
diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs
index d815f8082b..2a1cdec1c0 100644
--- a/src/Avalonia.Input/KeyboardDevice.cs
+++ b/src/Avalonia.Input/KeyboardDevice.cs
@@ -46,13 +46,13 @@ namespace Avalonia.Input
if (element != FocusedElement)
{
var interactive = FocusedElement as IInteractive;
+ FocusedElement = element;
interactive?.RaiseEvent(new RoutedEventArgs
{
RoutedEvent = InputElement.LostFocusEvent,
});
- FocusedElement = element;
interactive = element as IInteractive;
interactive?.RaiseEvent(new GotFocusEventArgs
diff --git a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml
new file mode 100644
index 0000000000..82dbf6064b
--- /dev/null
+++ b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml
@@ -0,0 +1,43 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml
index aa1a3c6385..2b9132ee56 100644
--- a/src/Avalonia.Themes.Default/DefaultTheme.xaml
+++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml
@@ -23,7 +23,7 @@
-
+
@@ -44,4 +44,5 @@
+
diff --git a/src/Avalonia.Visuals/Media/GradientBrush.cs b/src/Avalonia.Visuals/Media/GradientBrush.cs
index 8c2c9a2c01..fb29f68373 100644
--- a/src/Avalonia.Visuals/Media/GradientBrush.cs
+++ b/src/Avalonia.Visuals/Media/GradientBrush.cs
@@ -22,7 +22,7 @@ namespace Avalonia.Media
/// Defines the property.
///
public static readonly StyledProperty> GradientStopsProperty =
- AvaloniaProperty.Register>(nameof(Opacity));
+ AvaloniaProperty.Register>(nameof(GradientStops));
///
/// Initializes a new instance of the class.
diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
new file mode 100644
index 0000000000..f9da2ab6f3
--- /dev/null
+++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
@@ -0,0 +1,1042 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Templates;
+using Avalonia.Data;
+using Avalonia.Markup.Xaml.Data;
+using Avalonia.Platform;
+using Avalonia.Threading;
+using Avalonia.UnitTests;
+using Moq;
+using Xunit;
+using System.Collections.ObjectModel;
+
+namespace Avalonia.Controls.UnitTests
+{
+ public class AutoCompleteBoxTests
+ {
+ [Fact]
+ public void Search_Filters()
+ {
+ Assert.True(GetFilter(AutoCompleteFilterMode.Contains)("am", "name"));
+ Assert.True(GetFilter(AutoCompleteFilterMode.Contains)("AME", "name"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.Contains)("hello", "name"));
+
+ Assert.True(GetFilter(AutoCompleteFilterMode.ContainsCaseSensitive)("na", "name"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.ContainsCaseSensitive)("AME", "name"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.ContainsCaseSensitive)("hello", "name"));
+
+ Assert.Null(GetFilter(AutoCompleteFilterMode.Custom));
+ Assert.Null(GetFilter(AutoCompleteFilterMode.None));
+
+ Assert.True(GetFilter(AutoCompleteFilterMode.Equals)("na", "na"));
+ Assert.True(GetFilter(AutoCompleteFilterMode.Equals)("na", "NA"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.Equals)("hello", "name"));
+
+ Assert.True(GetFilter(AutoCompleteFilterMode.EqualsCaseSensitive)("na", "na"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.EqualsCaseSensitive)("na", "NA"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.EqualsCaseSensitive)("hello", "name"));
+
+ Assert.True(GetFilter(AutoCompleteFilterMode.StartsWith)("na", "name"));
+ Assert.True(GetFilter(AutoCompleteFilterMode.StartsWith)("NAM", "name"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.StartsWith)("hello", "name"));
+
+ Assert.True(GetFilter(AutoCompleteFilterMode.StartsWithCaseSensitive)("na", "name"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithCaseSensitive)("NAM", "name"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithCaseSensitive)("hello", "name"));
+ }
+
+ [Fact]
+ public void Ordinal_Search_Filters()
+ {
+ Assert.True(GetFilter(AutoCompleteFilterMode.ContainsOrdinal)("am", "name"));
+ Assert.True(GetFilter(AutoCompleteFilterMode.ContainsOrdinal)("AME", "name"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.ContainsOrdinal)("hello", "name"));
+
+ Assert.True(GetFilter(AutoCompleteFilterMode.ContainsOrdinalCaseSensitive)("na", "name"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.ContainsOrdinalCaseSensitive)("AME", "name"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.ContainsOrdinalCaseSensitive)("hello", "name"));
+
+ Assert.True(GetFilter(AutoCompleteFilterMode.EqualsOrdinal)("na", "na"));
+ Assert.True(GetFilter(AutoCompleteFilterMode.EqualsOrdinal)("na", "NA"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.EqualsOrdinal)("hello", "name"));
+
+ Assert.True(GetFilter(AutoCompleteFilterMode.EqualsOrdinalCaseSensitive)("na", "na"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.EqualsOrdinalCaseSensitive)("na", "NA"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.EqualsOrdinalCaseSensitive)("hello", "name"));
+
+ Assert.True(GetFilter(AutoCompleteFilterMode.StartsWithOrdinal)("na", "name"));
+ Assert.True(GetFilter(AutoCompleteFilterMode.StartsWithOrdinal)("NAM", "name"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithOrdinal)("hello", "name"));
+
+ Assert.True(GetFilter(AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive)("na", "name"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive)("NAM", "name"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive)("hello", "name"));
+ }
+
+ [Fact]
+ public void Fires_DropDown_Events()
+ {
+ RunTest((control, textbox) =>
+ {
+ bool openEvent = false;
+ bool closeEvent = false;
+ control.DropDownOpened += (s, e) => openEvent = true;
+ control.DropDownClosed += (s, e) => closeEvent = true;
+ control.Items = CreateSimpleStringArray();
+
+ textbox.Text = "a";
+ Dispatcher.UIThread.RunJobs();
+ Assert.True(control.SearchText == "a");
+ Assert.True(control.IsDropDownOpen);
+ Assert.True(openEvent);
+
+ textbox.Text = String.Empty;
+ Dispatcher.UIThread.RunJobs();
+ Assert.True(control.SearchText == String.Empty);
+ Assert.False(control.IsDropDownOpen);
+ Assert.True(closeEvent);
+ });
+ }
+
+ [Fact]
+ public void Text_Completion_Via_Text_Property()
+ {
+ RunTest((control, textbox) =>
+ {
+ control.IsTextCompletionEnabled = true;
+
+ Assert.Equal(String.Empty, control.Text);
+ control.Text = "close";
+ Assert.NotNull(control.SelectedItem);
+ });
+ }
+
+ [Fact]
+ public void Text_Completion_Selects_Text()
+ {
+ RunTest((control, textbox) =>
+ {
+ control.IsTextCompletionEnabled = true;
+
+ textbox.Text = "ac";
+ textbox.SelectionEnd = textbox.SelectionStart = 2;
+ Dispatcher.UIThread.RunJobs();
+
+ Assert.True(control.IsDropDownOpen);
+ Assert.True(Math.Abs(textbox.SelectionEnd - textbox.SelectionStart) > 2);
+ });
+ }
+
+ [Fact]
+ public void TextChanged_Event_Fires()
+ {
+ RunTest((control, textbox) =>
+ {
+ bool textChanged = false;
+ control.TextChanged += (s, e) => textChanged = true;
+
+ textbox.Text = "a";
+ Dispatcher.UIThread.RunJobs();
+ Assert.True(textChanged);
+
+ textChanged = false;
+ control.Text = "conversati";
+ Dispatcher.UIThread.RunJobs();
+ Assert.True(textChanged);
+
+ textChanged = false;
+ control.Text = null;
+ Dispatcher.UIThread.RunJobs();
+ Assert.True(textChanged);
+ });
+ }
+
+ [Fact]
+ public void MinimumPrefixLength_Works()
+ {
+ RunTest((control, textbox) =>
+ {
+ textbox.Text = "a";
+ Dispatcher.UIThread.RunJobs();
+ Assert.True(control.IsDropDownOpen);
+
+
+ textbox.Text = String.Empty;
+ Dispatcher.UIThread.RunJobs();
+ Assert.False(control.IsDropDownOpen);
+
+ control.MinimumPrefixLength = 3;
+
+ textbox.Text = "a";
+ Dispatcher.UIThread.RunJobs();
+ Assert.False(control.IsDropDownOpen);
+
+ textbox.Text = "acc";
+ Dispatcher.UIThread.RunJobs();
+ Assert.True(control.IsDropDownOpen);
+ });
+ }
+
+ [Fact]
+ public void Can_Cancel_DropDown_Opening()
+ {
+ RunTest((control, textbox) =>
+ {
+ control.DropDownOpening += (s, e) => e.Cancel = true;
+
+ textbox.Text = "a";
+ Dispatcher.UIThread.RunJobs();
+ Assert.False(control.IsDropDownOpen);
+ });
+ }
+
+ [Fact]
+ public void Can_Cancel_DropDown_Closing()
+ {
+ RunTest((control, textbox) =>
+ {
+ control.DropDownClosing += (s, e) => e.Cancel = true;
+
+ textbox.Text = "a";
+ Dispatcher.UIThread.RunJobs();
+ Assert.True(control.IsDropDownOpen);
+
+ control.IsDropDownOpen = false;
+ Assert.True(control.IsDropDownOpen);
+ });
+ }
+
+ [Fact]
+ public void Can_Cancel_Population()
+ {
+ RunTest((control, textbox) =>
+ {
+ bool populating = false;
+ bool populated = false;
+ control.FilterMode = AutoCompleteFilterMode.None;
+ control.Populating += (s, e) =>
+ {
+ e.Cancel = true;
+ populating = true;
+ };
+ control.Populated += (s, e) => populated = true;
+
+ textbox.Text = "accounti";
+ Dispatcher.UIThread.RunJobs();
+
+ Assert.True(populating);
+ Assert.False(populated);
+ });
+ }
+
+ [Fact]
+ public void Custom_Population_Supported()
+ {
+ RunTest((control, textbox) =>
+ {
+ string custom = "Custom!";
+ string search = "accounti";
+ bool populated = false;
+ bool populatedOk = false;
+ control.FilterMode = AutoCompleteFilterMode.None;
+ control.Populating += (s, e) =>
+ {
+ control.Items = new string[] { custom };
+ Assert.Equal(search, e.Parameter);
+ };
+ control.Populated += (s, e) =>
+ {
+ populated = true;
+ ReadOnlyCollection collection = e.Data as ReadOnlyCollection;
+ populatedOk = collection != null && collection.Count == 1;
+ };
+
+ textbox.Text = search;
+ Dispatcher.UIThread.RunJobs();
+
+ Assert.True(populated);
+ Assert.True(populatedOk);
+ });
+ }
+
+ [Fact]
+ public void Text_Completion()
+ {
+ RunTest((control, textbox) =>
+ {
+ control.IsTextCompletionEnabled = true;
+ textbox.Text = "accounti";
+ textbox.SelectionStart = textbox.SelectionEnd = textbox.Text.Length;
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal("accounti", control.SearchText);
+ Assert.Equal("accounting", textbox.Text);
+ });
+ }
+
+ [Fact]
+ public void String_Search()
+ {
+ RunTest((control, textbox) =>
+ {
+ textbox.Text = "a";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+
+ textbox.Text = "acc";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+
+ textbox.Text = "a";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+
+ textbox.Text = "";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+
+ textbox.Text = "cook";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+
+ textbox.Text = "accept";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+
+ textbox.Text = "cook";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+ });
+ }
+
+ [Fact]
+ public void Item_Search()
+ {
+ RunTest((control, textbox) =>
+ {
+ control.FilterMode = AutoCompleteFilterMode.Custom;
+ control.ItemFilter = (search, item) =>
+ {
+ string s = item as string;
+ return s == null ? false : true;
+ };
+
+ // Just set to null briefly to exercise that code path
+ AutoCompleteFilterPredicate filter = control.ItemFilter;
+ Assert.NotNull(filter);
+ control.ItemFilter = null;
+ Assert.Null(control.ItemFilter);
+ control.ItemFilter = filter;
+ Assert.NotNull(control.ItemFilter);
+
+ textbox.Text = "a";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+
+ textbox.Text = "acc";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+
+ textbox.Text = "a";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+
+ textbox.Text = "";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+
+ textbox.Text = "cook";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+
+ textbox.Text = "accept";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+
+ textbox.Text = "cook";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+ });
+ }
+
+ ///
+ /// Retrieves a defined predicate filter through a new AutoCompleteBox
+ /// control instance.
+ ///
+ /// The FilterMode of interest.
+ /// Returns the predicate instance.
+ private static AutoCompleteFilterPredicate GetFilter(AutoCompleteFilterMode mode)
+ {
+ return new AutoCompleteBox { FilterMode = mode }
+ .TextFilter;
+ }
+
+ ///
+ /// Creates a large list of strings for AutoCompleteBox testing.
+ ///
+ /// Returns a new List of string values.
+ private IList CreateSimpleStringArray()
+ {
+ return new List
+ {
+ "a",
+ "abide",
+ "able",
+ "about",
+ "above",
+ "absence",
+ "absurd",
+ "accept",
+ "acceptance",
+ "accepted",
+ "accepting",
+ "access",
+ "accessed",
+ "accessible",
+ "accident",
+ "accidentally",
+ "accordance",
+ "account",
+ "accounting",
+ "accounts",
+ "accusation",
+ "accustomed",
+ "ache",
+ "across",
+ "act",
+ "active",
+ "actual",
+ "actually",
+ "ada",
+ "added",
+ "adding",
+ "addition",
+ "additional",
+ "additions",
+ "address",
+ "addressed",
+ "addresses",
+ "addressing",
+ "adjourn",
+ "adoption",
+ "advance",
+ "advantage",
+ "adventures",
+ "advice",
+ "advisable",
+ "advise",
+ "affair",
+ "affectionately",
+ "afford",
+ "afore",
+ "afraid",
+ "after",
+ "afterwards",
+ "again",
+ "against",
+ "age",
+ "aged",
+ "agent",
+ "ago",
+ "agony",
+ "agree",
+ "agreed",
+ "agreement",
+ "ah",
+ "ahem",
+ "air",
+ "airs",
+ "ak",
+ "alarm",
+ "alarmed",
+ "alas",
+ "alice",
+ "alive",
+ "all",
+ "allow",
+ "almost",
+ "alone",
+ "along",
+ "aloud",
+ "already",
+ "also",
+ "alteration",
+ "altered",
+ "alternate",
+ "alternately",
+ "altogether",
+ "always",
+ "am",
+ "ambition",
+ "among",
+ "an",
+ "ancient",
+ "and",
+ "anger",
+ "angrily",
+ "angry",
+ "animal",
+ "animals",
+ "ann",
+ "annoy",
+ "annoyed",
+ "another",
+ "answer",
+ "answered",
+ "answers",
+ "antipathies",
+ "anxious",
+ "anxiously",
+ "any",
+ "anyone",
+ "anything",
+ "anywhere",
+ "appealed",
+ "appear",
+ "appearance",
+ "appeared",
+ "appearing",
+ "appears",
+ "applause",
+ "apple",
+ "apples",
+ "applicable",
+ "apply",
+ "approach",
+ "arch",
+ "archbishop",
+ "arches",
+ "archive",
+ "are",
+ "argue",
+ "argued",
+ "argument",
+ "arguments",
+ "arise",
+ "arithmetic",
+ "arm",
+ "arms",
+ "around",
+ "arranged",
+ "array",
+ "arrived",
+ "arrow",
+ "arrum",
+ "as",
+ "ascii",
+ "ashamed",
+ "ask",
+ "askance",
+ "asked",
+ "asking",
+ "asleep",
+ "assembled",
+ "assistance",
+ "associated",
+ "at",
+ "ate",
+ "atheling",
+ "atom",
+ "attached",
+ "attempt",
+ "attempted",
+ "attempts",
+ "attended",
+ "attending",
+ "attends",
+ "audibly",
+ "australia",
+ "author",
+ "authority",
+ "available",
+ "avoid",
+ "away",
+ "awfully",
+ "axes",
+ "axis",
+ "b",
+ "baby",
+ "back",
+ "backs",
+ "bad",
+ "bag",
+ "baked",
+ "balanced",
+ "bank",
+ "banks",
+ "banquet",
+ "bark",
+ "barking",
+ "barley",
+ "barrowful",
+ "based",
+ "bat",
+ "bathing",
+ "bats",
+ "bawled",
+ "be",
+ "beak",
+ "bear",
+ "beast",
+ "beasts",
+ "beat",
+ "beating",
+ "beau",
+ "beauti",
+ "beautiful",
+ "beautifully",
+ "beautify",
+ "became",
+ "because",
+ "become",
+ "becoming",
+ "bed",
+ "beds",
+ "bee",
+ "been",
+ "before",
+ "beg",
+ "began",
+ "begged",
+ "begin",
+ "beginning",
+ "begins",
+ "begun",
+ "behead",
+ "beheaded",
+ "beheading",
+ "behind",
+ "being",
+ "believe",
+ "believed",
+ "bells",
+ "belong",
+ "belongs",
+ "beloved",
+ "below",
+ "belt",
+ "bend",
+ "bent",
+ "besides",
+ "best",
+ "better",
+ "between",
+ "bill",
+ "binary",
+ "bird",
+ "birds",
+ "birthday",
+ "bit",
+ "bite",
+ "bitter",
+ "blacking",
+ "blades",
+ "blame",
+ "blasts",
+ "bleeds",
+ "blew",
+ "blow",
+ "blown",
+ "blows",
+ "body",
+ "boldly",
+ "bone",
+ "bones",
+ "book",
+ "books",
+ "boon",
+ "boots",
+ "bore",
+ "both",
+ "bother",
+ "bottle",
+ "bottom",
+ "bough",
+ "bound",
+ "bowed",
+ "bowing",
+ "box",
+ "boxed",
+ "boy",
+ "brain",
+ "branch",
+ "branches",
+ "brandy",
+ "brass",
+ "brave",
+ "breach",
+ "bread",
+ "break",
+ "breath",
+ "breathe",
+ "breeze",
+ "bright",
+ "brightened",
+ "bring",
+ "bringing",
+ "bristling",
+ "broke",
+ "broken",
+ "brother",
+ "brought",
+ "brown",
+ "brush",
+ "brushing",
+ "burn",
+ "burning",
+ "burnt",
+ "burst",
+ "bursting",
+ "busily",
+ "business",
+ "business@pglaf",
+ "busy",
+ "but",
+ "butter",
+ "buttercup",
+ "buttered",
+ "butterfly",
+ "buttons",
+ "by",
+ "bye",
+ "c",
+ "cackled",
+ "cake",
+ "cakes",
+ "calculate",
+ "calculated",
+ "call",
+ "called",
+ "calling",
+ "calmly",
+ "came",
+ "camomile",
+ "can",
+ "canary",
+ "candle",
+ "cannot",
+ "canterbury",
+ "canvas",
+ "capering",
+ "capital",
+ "card",
+ "cardboard",
+ "cards",
+ "care",
+ "carefully",
+ "cares",
+ "carried",
+ "carrier",
+ "carroll",
+ "carry",
+ "carrying",
+ "cart",
+ "cartwheels",
+ "case",
+ "cat",
+ "catch",
+ "catching",
+ "caterpillar",
+ "cats",
+ "cattle",
+ "caucus",
+ "caught",
+ "cauldron",
+ "cause",
+ "caused",
+ "cautiously",
+ "cease",
+ "ceiling",
+ "centre",
+ "certain",
+ "certainly",
+ "chain",
+ "chains",
+ "chair",
+ "chance",
+ "chanced",
+ "change",
+ "changed",
+ "changes",
+ "changing",
+ "chapter",
+ "character",
+ "charge",
+ "charges",
+ "charitable",
+ "charities",
+ "chatte",
+ "cheap",
+ "cheated",
+ "check",
+ "checked",
+ "checks",
+ "cheeks",
+ "cheered",
+ "cheerfully",
+ "cherry",
+ "cheshire",
+ "chief",
+ "child",
+ "childhood",
+ "children",
+ "chimney",
+ "chimneys",
+ "chin",
+ "choice",
+ "choke",
+ "choked",
+ "choking",
+ "choose",
+ "choosing",
+ "chop",
+ "chorus",
+ "chose",
+ "christmas",
+ "chrysalis",
+ "chuckled",
+ "circle",
+ "circumstances",
+ "city",
+ "civil",
+ "claim",
+ "clamour",
+ "clapping",
+ "clasped",
+ "classics",
+ "claws",
+ "clean",
+ "clear",
+ "cleared",
+ "clearer",
+ "clearly",
+ "clever",
+ "climb",
+ "clinging",
+ "clock",
+ "close",
+ "closed",
+ "closely",
+ "closer",
+ "clubs",
+ "coast",
+ "coaxing",
+ "codes",
+ "coils",
+ "cold",
+ "collar",
+ "collected",
+ "collection",
+ "come",
+ "comes",
+ "comfits",
+ "comfort",
+ "comfortable",
+ "comfortably",
+ "coming",
+ "commercial",
+ "committed",
+ "common",
+ "commotion",
+ "company",
+ "compilation",
+ "complained",
+ "complaining",
+ "completely",
+ "compliance",
+ "comply",
+ "complying",
+ "compressed",
+ "computer",
+ "computers",
+ "concept",
+ "concerning",
+ "concert",
+ "concluded",
+ "conclusion",
+ "condemn",
+ "conduct",
+ "confirmation",
+ "confirmed",
+ "confused",
+ "confusing",
+ "confusion",
+ "conger",
+ "conqueror",
+ "conquest",
+ "consented",
+ "consequential",
+ "consider",
+ "considerable",
+ "considered",
+ "considering",
+ "constant",
+ "consultation",
+ "contact",
+ "contain",
+ "containing",
+ "contempt",
+ "contemptuous",
+ "contemptuously",
+ "content",
+ "continued",
+ "contract",
+ "contradicted",
+ "contributions",
+ "conversation",
+ "conversations",
+ "convert",
+ "cook",
+ "cool",
+ "copied",
+ "copies",
+ "copy",
+ "copying",
+ "copyright",
+ "corner",
+ "corners",
+ "corporation",
+ "corrupt",
+ "cost",
+ "costs",
+ "could",
+ "couldn",
+ "counting",
+ "countries",
+ "country",
+ "couple",
+ "couples",
+ "courage",
+ "course",
+ "court",
+ "courtiers",
+ "coward",
+ "crab",
+ "crash",
+ "crashed",
+ "crawled",
+ "crawling",
+ "crazy",
+ "created",
+ "creating",
+ "creation",
+ "creature",
+ "creatures",
+ "credit",
+ "creep",
+ "crept",
+ "cried",
+ "cries",
+ "crimson",
+ "critical",
+ "crocodile",
+ "croquet",
+ "croqueted",
+ "croqueting",
+ "cross",
+ "crossed",
+ "crossly",
+ "crouched",
+ "crowd",
+ "crowded",
+ "crown",
+ "crumbs",
+ "crust",
+ "cry",
+ "crying",
+ "cucumber",
+ "cunning",
+ "cup",
+ "cupboards",
+ "cur",
+ "curiosity",
+ "curious",
+ "curiouser",
+ "curled",
+ "curls",
+ "curly",
+ "currants",
+ "current",
+ "curtain",
+ "curtsey",
+ "curtseying",
+ "curving",
+ "cushion",
+ "custard",
+ "custody",
+ "cut",
+ "cutting",
+ };
+ }
+ private void RunTest(Action test)
+ {
+ using (UnitTestApplication.Start(Services))
+ {
+ AutoCompleteBox control = CreateControl();
+ control.Items = CreateSimpleStringArray();
+ TextBox textBox = GetTextBox(control);
+ Dispatcher.UIThread.RunJobs();
+ test.Invoke(control, textBox);
+ }
+ }
+
+ private static TestServices Services => TestServices.StyledWindow;
+
+ /*private static TestServices Services => TestServices.MockThreadingInterface.With(
+ standardCursorFactory: Mock.Of(),
+ windowingPlatform: new MockWindowingPlatform());*/
+
+ private AutoCompleteBox CreateControl()
+ {
+ var datePicker =
+ new AutoCompleteBox
+ {
+ Template = CreateTemplate()
+ };
+
+ datePicker.ApplyTemplate();
+ return datePicker;
+ }
+ private TextBox GetTextBox(AutoCompleteBox control)
+ {
+ return control.GetTemplateChildren()
+ .OfType()
+ .First();
+ }
+ private IControlTemplate CreateTemplate()
+ {
+ return new FuncControlTemplate(control =>
+ {
+ var textBox =
+ new TextBox
+ {
+ Name = "PART_TextBox"
+ };
+ var listbox =
+ new ListBox
+ {
+ Name = "PART_SelectingItemsControl"
+ };
+ var popup =
+ new Popup
+ {
+ Name = "PART_Popup"
+ };
+
+ var panel = new Panel();
+ panel.Children.Add(textBox);
+ panel.Children.Add(popup);
+ panel.Children.Add(listbox);
+
+ return panel;
+ });
+ }
+ }
+}