diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj
index 37f9da0c43..f6d5627555 100644
--- a/samples/ControlCatalog/ControlCatalog.csproj
+++ b/samples/ControlCatalog/ControlCatalog.csproj
@@ -38,6 +38,9 @@
Designer
+
+ Designer
+
Designer
@@ -110,6 +113,9 @@
BorderPage.xaml
+
+ AutoCompleteBoxPage.xaml
+
ButtonPage.xaml
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index 060369e404..e8b87e99b0 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -5,9 +5,10 @@
+
-
+
@@ -25,4 +26,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..491f41ecbf
--- /dev/null
+++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml
@@ -0,0 +1,55 @@
+
+
+ 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..9f181d44f2
--- /dev/null
+++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs
@@ -0,0 +1,125 @@
+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;
+
+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;
+ }
+ private IEnumerable GetAllAutoCompleteBox()
+ {
+ return
+ this.GetLogicalDescendants()
+ .OfType();
+ }
+
+ 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..0e8b93146c
--- /dev/null
+++ b/src/Avalonia.Controls/AutoCompleteBox.cs
@@ -0,0 +1,2669 @@
+// (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 Avalonia.Controls.Primitives;
+using Avalonia.Controls.Utils;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+using Avalonia.Collections;
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.Collections;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using Avalonia.Threading;
+using Avalonia.Controls.Templates;
+using Avalonia.VisualTree;
+using Avalonia.Utilities;
+using System.Globalization;
+using System.Collections.Specialized;
+using System.Reactive.Disposables;
+
+namespace Avalonia.Controls
+{
+ public class CancelableEventArgs : EventArgs
+ {
+ public bool Cancel { get; set; }
+
+ public CancelableEventArgs()
+ : base()
+ { }
+ }
+
+ ///
+ /// 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 : CancelableEventArgs
+ {
+ ///
+ /// 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 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);
+
+ 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); }
+ }
+
+ ///
+ /// 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(CancelableEventArgs 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(CancelableEventArgs 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 CancelableEventArgs();
+ 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 CancelableEventArgs();
+
+ // 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;
+
+ // 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 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/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.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 5b6f4b78fd..7d3090de66 100644
--- a/src/Avalonia.Themes.Default/DefaultTheme.xaml
+++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml
@@ -23,7 +23,7 @@
-
+
@@ -42,4 +42,5 @@
+