ComboBox.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725
  1. using System;
  2. using System.Linq;
  3. using Avalonia.Automation.Peers;
  4. using Avalonia.Controls.Metadata;
  5. using Avalonia.Controls.Primitives;
  6. using Avalonia.Controls.Shapes;
  7. using Avalonia.Controls.Templates;
  8. using Avalonia.Controls.Utils;
  9. using Avalonia.Data;
  10. using Avalonia.Input;
  11. using Avalonia.Interactivity;
  12. using Avalonia.Layout;
  13. using Avalonia.Media;
  14. using Avalonia.Metadata;
  15. using Avalonia.Reactive;
  16. using Avalonia.VisualTree;
  17. namespace Avalonia.Controls
  18. {
  19. /// <summary>
  20. /// A drop-down list control.
  21. /// </summary>
  22. [TemplatePart("PART_Popup", typeof(Popup), IsRequired = true)]
  23. [TemplatePart("PART_EditableTextBox", typeof(TextBox), IsRequired = false)]
  24. [PseudoClasses(pcDropdownOpen, pcPressed)]
  25. public class ComboBox : SelectingItemsControl
  26. {
  27. internal const string pcDropdownOpen = ":dropdownopen";
  28. internal const string pcPressed = ":pressed";
  29. /// <summary>
  30. /// The default value for the <see cref="ItemsControl.ItemsPanel"/> property.
  31. /// </summary>
  32. private static readonly FuncTemplate<Panel?> DefaultPanel =
  33. new(() => new VirtualizingStackPanel());
  34. /// <summary>
  35. /// Defines the <see cref="IsDropDownOpen"/> property.
  36. /// </summary>
  37. public static readonly StyledProperty<bool> IsDropDownOpenProperty =
  38. AvaloniaProperty.Register<ComboBox, bool>(nameof(IsDropDownOpen));
  39. /// <summary>
  40. /// Defines the <see cref="IsEditable"/> property.
  41. /// </summary>
  42. public static readonly StyledProperty<bool> IsEditableProperty =
  43. AvaloniaProperty.Register<ComboBox, bool>(nameof(IsEditable));
  44. /// <summary>
  45. /// Defines the <see cref="MaxDropDownHeight"/> property.
  46. /// </summary>
  47. public static readonly StyledProperty<double> MaxDropDownHeightProperty =
  48. AvaloniaProperty.Register<ComboBox, double>(nameof(MaxDropDownHeight), 200);
  49. /// <summary>
  50. /// Defines the <see cref="SelectionBoxItem"/> property.
  51. /// </summary>
  52. public static readonly DirectProperty<ComboBox, object?> SelectionBoxItemProperty =
  53. AvaloniaProperty.RegisterDirect<ComboBox, object?>(nameof(SelectionBoxItem), o => o.SelectionBoxItem);
  54. /// <summary>
  55. /// Defines the <see cref="PlaceholderText"/> property.
  56. /// </summary>
  57. public static readonly StyledProperty<string?> PlaceholderTextProperty =
  58. AvaloniaProperty.Register<ComboBox, string?>(nameof(PlaceholderText));
  59. /// <summary>
  60. /// Defines the <see cref="PlaceholderForeground"/> property.
  61. /// </summary>
  62. public static readonly StyledProperty<IBrush?> PlaceholderForegroundProperty =
  63. AvaloniaProperty.Register<ComboBox, IBrush?>(nameof(PlaceholderForeground));
  64. /// <summary>
  65. /// Defines the <see cref="HorizontalContentAlignment"/> property.
  66. /// </summary>
  67. public static readonly StyledProperty<HorizontalAlignment> HorizontalContentAlignmentProperty =
  68. ContentControl.HorizontalContentAlignmentProperty.AddOwner<ComboBox>();
  69. /// <summary>
  70. /// Defines the <see cref="VerticalContentAlignment"/> property.
  71. /// </summary>
  72. public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty =
  73. ContentControl.VerticalContentAlignmentProperty.AddOwner<ComboBox>();
  74. /// <summary>
  75. /// Defines the <see cref="Text"/> property
  76. /// </summary>
  77. public static readonly StyledProperty<string?> TextProperty =
  78. TextBlock.TextProperty.AddOwner<ComboBox>(new(string.Empty, BindingMode.TwoWay));
  79. /// <summary>
  80. /// Defines the <see cref="SelectionBoxItemTemplate"/> property.
  81. /// </summary>
  82. public static readonly StyledProperty<IDataTemplate?> SelectionBoxItemTemplateProperty =
  83. AvaloniaProperty.Register<ComboBox, IDataTemplate?>(
  84. nameof(SelectionBoxItemTemplate), defaultBindingMode: BindingMode.TwoWay, coerce: CoerceSelectionBoxItemTemplate);
  85. private static IDataTemplate? CoerceSelectionBoxItemTemplate(AvaloniaObject obj, IDataTemplate? template)
  86. {
  87. if (template is not null) return template;
  88. if(obj is ComboBox comboBox && template is null)
  89. {
  90. return comboBox.ItemTemplate;
  91. }
  92. return template;
  93. }
  94. private Popup? _popup;
  95. private object? _selectionBoxItem;
  96. private readonly CompositeDisposable _subscriptionsOnOpen = new CompositeDisposable();
  97. private TextBox? _inputTextBox;
  98. private BindingEvaluator<string?>? _textValueBindingEvaluator = null;
  99. private bool _skipNextTextChanged = false;
  100. /// <summary>
  101. /// Initializes static members of the <see cref="ComboBox"/> class.
  102. /// </summary>
  103. static ComboBox()
  104. {
  105. ItemsPanelProperty.OverrideDefaultValue<ComboBox>(DefaultPanel);
  106. FocusableProperty.OverrideDefaultValue<ComboBox>(true);
  107. IsTextSearchEnabledProperty.OverrideDefaultValue<ComboBox>(true);
  108. }
  109. /// <summary>
  110. /// Occurs after the drop-down (popup) list of the <see cref="ComboBox"/> closes.
  111. /// </summary>
  112. public event EventHandler? DropDownClosed;
  113. /// <summary>
  114. /// Occurs after the drop-down (popup) list of the <see cref="ComboBox"/> opens.
  115. /// </summary>
  116. public event EventHandler? DropDownOpened;
  117. /// <summary>
  118. /// Gets or sets a value indicating whether the dropdown is currently open.
  119. /// </summary>
  120. public bool IsDropDownOpen
  121. {
  122. get => GetValue(IsDropDownOpenProperty);
  123. set => SetValue(IsDropDownOpenProperty, value);
  124. }
  125. /// <summary>
  126. /// Gets or sets a value indicating whether the control is editable
  127. /// </summary>
  128. public bool IsEditable
  129. {
  130. get => GetValue(IsEditableProperty);
  131. set => SetValue(IsEditableProperty, value);
  132. }
  133. /// <summary>
  134. /// Gets or sets the maximum height for the dropdown list.
  135. /// </summary>
  136. public double MaxDropDownHeight
  137. {
  138. get => GetValue(MaxDropDownHeightProperty);
  139. set => SetValue(MaxDropDownHeightProperty, value);
  140. }
  141. /// <summary>
  142. /// Gets or sets the item to display as the control's content.
  143. /// </summary>
  144. public object? SelectionBoxItem
  145. {
  146. get => _selectionBoxItem;
  147. protected set => SetAndRaise(SelectionBoxItemProperty, ref _selectionBoxItem, value);
  148. }
  149. /// <summary>
  150. /// Gets or sets the PlaceHolder text.
  151. /// </summary>
  152. public string? PlaceholderText
  153. {
  154. get => GetValue(PlaceholderTextProperty);
  155. set => SetValue(PlaceholderTextProperty, value);
  156. }
  157. /// <summary>
  158. /// Gets or sets the Brush that renders the placeholder text.
  159. /// </summary>
  160. public IBrush? PlaceholderForeground
  161. {
  162. get => GetValue(PlaceholderForegroundProperty);
  163. set => SetValue(PlaceholderForegroundProperty, value);
  164. }
  165. /// <summary>
  166. /// Gets or sets the horizontal alignment of the content within the control.
  167. /// </summary>
  168. public HorizontalAlignment HorizontalContentAlignment
  169. {
  170. get => GetValue(HorizontalContentAlignmentProperty);
  171. set => SetValue(HorizontalContentAlignmentProperty, value);
  172. }
  173. /// <summary>
  174. /// Gets or sets the vertical alignment of the content within the control.
  175. /// </summary>
  176. public VerticalAlignment VerticalContentAlignment
  177. {
  178. get => GetValue(VerticalContentAlignmentProperty);
  179. set => SetValue(VerticalContentAlignmentProperty, value);
  180. }
  181. /// <summary>
  182. /// Gets or sets the DataTemplate used to display the selected item. This has a higher priority than <see cref="ItemsControl.ItemTemplate"/> if set.
  183. /// </summary>
  184. [InheritDataTypeFromItems(nameof(ItemsSource))]
  185. public IDataTemplate? SelectionBoxItemTemplate
  186. {
  187. get => GetValue(SelectionBoxItemTemplateProperty);
  188. set => SetValue(SelectionBoxItemTemplateProperty, value);
  189. }
  190. /// <summary>
  191. /// Gets or sets the text used when <see cref="IsEditable"/> is true.
  192. /// Does nothing if not <see cref="IsEditable"/>.
  193. /// </summary>
  194. public string? Text
  195. {
  196. get => GetValue(TextProperty);
  197. set => SetValue(TextProperty, value);
  198. }
  199. protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
  200. {
  201. base.OnAttachedToVisualTree(e);
  202. UpdateSelectionBoxItem(SelectedItem);
  203. }
  204. protected internal override void InvalidateMirrorTransform()
  205. {
  206. base.InvalidateMirrorTransform();
  207. UpdateFlowDirection();
  208. }
  209. protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
  210. {
  211. return new ComboBoxItem();
  212. }
  213. protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
  214. {
  215. return NeedsContainer<ComboBoxItem>(item, out recycleKey);
  216. }
  217. /// <inheritdoc/>
  218. protected override void OnKeyDown(KeyEventArgs e)
  219. {
  220. base.OnKeyDown(e);
  221. if (e.Handled)
  222. return;
  223. if ((e.Key == Key.F4 && e.KeyModifiers.HasAllFlags(KeyModifiers.Alt) == false) ||
  224. ((e.Key == Key.Down || e.Key == Key.Up) && e.KeyModifiers.HasAllFlags(KeyModifiers.Alt)))
  225. {
  226. SetCurrentValue(IsDropDownOpenProperty, !IsDropDownOpen);
  227. e.Handled = true;
  228. }
  229. else if (IsDropDownOpen && e.Key == Key.Escape)
  230. {
  231. SetCurrentValue(IsDropDownOpenProperty, false);
  232. e.Handled = true;
  233. }
  234. else if (!IsDropDownOpen && !IsEditable && (e.Key == Key.Enter || e.Key == Key.Space))
  235. {
  236. SetCurrentValue(IsDropDownOpenProperty, true);
  237. e.Handled = true;
  238. }
  239. else if (IsDropDownOpen && e.Key == Key.Tab)
  240. {
  241. SetCurrentValue(IsDropDownOpenProperty, false);
  242. }
  243. // Ignore key buttons, if they are used for XY focus.
  244. else if (!IsDropDownOpen
  245. && !XYFocusHelpers.IsAllowedXYNavigationMode(this, e.KeyDeviceType))
  246. {
  247. if (e.Key == Key.Down)
  248. {
  249. e.Handled = SelectNext();
  250. }
  251. else if (e.Key == Key.Up)
  252. {
  253. e.Handled = SelectPrevious();
  254. }
  255. }
  256. // This part of code is needed just to acquire initial focus, subsequent focus navigation will be done by ItemsControl.
  257. else if (IsDropDownOpen && SelectedIndex < 0 && ItemCount > 0 &&
  258. (e.Key == Key.Up || e.Key == Key.Down) && IsFocused == true)
  259. {
  260. var firstChild = Presenter?.Panel?.Children.FirstOrDefault(c => CanFocus(c));
  261. if (firstChild != null)
  262. {
  263. e.Handled = firstChild.Focus(NavigationMethod.Directional);
  264. }
  265. }
  266. }
  267. /// <inheritdoc/>
  268. protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
  269. {
  270. base.OnPointerWheelChanged(e);
  271. if (!e.Handled)
  272. {
  273. if (!IsDropDownOpen)
  274. {
  275. if (IsFocused)
  276. {
  277. e.Handled = e.Delta.Y < 0 ? SelectNext() : SelectPrevious();
  278. }
  279. }
  280. else
  281. {
  282. e.Handled = true;
  283. }
  284. }
  285. }
  286. /// <inheritdoc/>
  287. protected override void OnPointerPressed(PointerPressedEventArgs e)
  288. {
  289. base.OnPointerPressed(e);
  290. if(!e.Handled && e.Source is Visual source)
  291. {
  292. if (_popup?.IsInsidePopup(source) == true)
  293. {
  294. e.Handled = true;
  295. return;
  296. }
  297. }
  298. if (IsDropDownOpen)
  299. {
  300. // When a drop-down is open with OverlayDismissEventPassThrough enabled and the control
  301. // is pressed, close the drop-down
  302. SetCurrentValue(IsDropDownOpenProperty, false);
  303. e.Handled = true;
  304. }
  305. else
  306. {
  307. PseudoClasses.Set(pcPressed, true);
  308. }
  309. }
  310. /// <inheritdoc/>
  311. protected override void OnPointerReleased(PointerReleasedEventArgs e)
  312. {
  313. //if the user clicked in the input text we don't want to open the dropdown
  314. if (_inputTextBox != null
  315. && !e.Handled
  316. && e.Source is StyledElement styledSource
  317. && styledSource.TemplatedParent == _inputTextBox)
  318. {
  319. return;
  320. }
  321. if (!e.Handled && e.Source is Visual source)
  322. {
  323. if (_popup?.IsInsidePopup(source) != true && PseudoClasses.Contains(pcPressed))
  324. {
  325. SetCurrentValue(IsDropDownOpenProperty, !IsDropDownOpen);
  326. e.Handled = true;
  327. }
  328. }
  329. PseudoClasses.Set(pcPressed, false);
  330. base.OnPointerReleased(e);
  331. }
  332. public override bool UpdateSelectionFromEvent(Control container, RoutedEventArgs eventArgs)
  333. {
  334. if (base.UpdateSelectionFromEvent(container, eventArgs))
  335. {
  336. _popup?.Close();
  337. return true;
  338. }
  339. return false;
  340. }
  341. protected override bool ShouldTriggerSelection(Visual selectable, PointerEventArgs eventArgs) =>
  342. ItemSelectionEventTriggers.IsPointerEventWithinBounds(selectable, eventArgs) &&
  343. eventArgs is { Properties.PointerUpdateKind: PointerUpdateKind.LeftButtonReleased or PointerUpdateKind.RightButtonReleased } &&
  344. eventArgs.RoutedEvent == PointerReleasedEvent;
  345. /// <inheritdoc/>
  346. protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
  347. {
  348. if (_popup != null)
  349. {
  350. _popup.Opened -= PopupOpened;
  351. _popup.Closed -= PopupClosed;
  352. }
  353. _popup = e.NameScope.Get<Popup>("PART_Popup");
  354. _popup.Opened += PopupOpened;
  355. _popup.Closed += PopupClosed;
  356. _inputTextBox = e.NameScope.Find<TextBox>("PART_EditableTextBox");
  357. }
  358. /// <inheritdoc/>
  359. protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
  360. {
  361. if (change.Property == SelectedItemProperty)
  362. {
  363. UpdateSelectionBoxItem(change.NewValue);
  364. TryFocusSelectedItem();
  365. UpdateInputTextFromSelection(change.NewValue);
  366. }
  367. else if (change.Property == IsDropDownOpenProperty)
  368. {
  369. PseudoClasses.Set(pcDropdownOpen, change.GetNewValue<bool>());
  370. }
  371. else if (change.Property == ItemTemplateProperty)
  372. {
  373. CoerceValue(SelectionBoxItemTemplateProperty);
  374. }
  375. else if (change.Property == IsEditableProperty && change.GetNewValue<bool>())
  376. {
  377. UpdateInputTextFromSelection(SelectedItem);
  378. }
  379. else if (change.Property == TextProperty)
  380. {
  381. TextChanged(change.GetNewValue<string>());
  382. }
  383. else if (change.Property == ItemsSourceProperty)
  384. {
  385. //the base handler deselects the current item (and resets Text) so we want to run the base first, then try match by text
  386. string? text = Text;
  387. base.OnPropertyChanged(change);
  388. SetCurrentValue(TextProperty, text);
  389. return;
  390. }
  391. else if (change.Property == DisplayMemberBindingProperty)
  392. {
  393. HandleTextValueBindingValueChanged(null, change);
  394. }
  395. else if (change.Property == TextSearch.TextBindingProperty)
  396. {
  397. HandleTextValueBindingValueChanged(change, null);
  398. }
  399. base.OnPropertyChanged(change);
  400. }
  401. protected override AutomationPeer OnCreateAutomationPeer()
  402. {
  403. return new ComboBoxAutomationPeer(this);
  404. }
  405. protected override void OnGotFocus(GotFocusEventArgs e)
  406. {
  407. if (IsEditable && _inputTextBox != null)
  408. {
  409. _inputTextBox.Focus();
  410. _inputTextBox.SelectAll();
  411. }
  412. base.OnGotFocus(e);
  413. }
  414. internal void ItemFocused(ComboBoxItem dropDownItem)
  415. {
  416. if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid)
  417. {
  418. dropDownItem.BringIntoView();
  419. }
  420. }
  421. private void PopupClosed(object? sender, EventArgs e)
  422. {
  423. _subscriptionsOnOpen.Clear();
  424. if(IsEditable && CanFocus(this))
  425. {
  426. Focus();
  427. }
  428. DropDownClosed?.Invoke(this, EventArgs.Empty);
  429. }
  430. private void PopupOpened(object? sender, EventArgs e)
  431. {
  432. TryFocusSelectedItem();
  433. _subscriptionsOnOpen.Clear();
  434. this.GetObservable(IsVisibleProperty).Subscribe(IsVisibleChanged).DisposeWith(_subscriptionsOnOpen);
  435. foreach (var parent in this.GetVisualAncestors().OfType<Control>())
  436. {
  437. parent.GetObservable(IsVisibleProperty).Subscribe(IsVisibleChanged).DisposeWith(_subscriptionsOnOpen);
  438. }
  439. UpdateFlowDirection();
  440. DropDownOpened?.Invoke(this, EventArgs.Empty);
  441. }
  442. private void IsVisibleChanged(bool isVisible)
  443. {
  444. if (!isVisible && IsDropDownOpen)
  445. {
  446. SetCurrentValue(IsDropDownOpenProperty, false);
  447. }
  448. }
  449. private void TryFocusSelectedItem()
  450. {
  451. var selectedIndex = SelectedIndex;
  452. if (IsDropDownOpen && selectedIndex != -1)
  453. {
  454. var container = ContainerFromIndex(selectedIndex);
  455. if (container == null && SelectedIndex != -1)
  456. {
  457. ScrollIntoView(Selection.SelectedIndex);
  458. container = ContainerFromIndex(selectedIndex);
  459. }
  460. if (container != null && CanFocus(container))
  461. {
  462. container.Focus();
  463. }
  464. }
  465. }
  466. private bool CanFocus(Control control) => control.Focusable && control.IsEffectivelyEnabled && control.IsVisible;
  467. private void UpdateSelectionBoxItem(object? item)
  468. {
  469. var contentControl = item as IContentControl;
  470. if (contentControl != null)
  471. {
  472. item = contentControl.Content;
  473. }
  474. var control = item as Control;
  475. if (control != null)
  476. {
  477. if (VisualRoot is object)
  478. {
  479. control.Measure(Size.Infinity);
  480. SelectionBoxItem = new Rectangle
  481. {
  482. Width = control.DesiredSize.Width,
  483. Height = control.DesiredSize.Height,
  484. Fill = new VisualBrush
  485. {
  486. Visual = control,
  487. Stretch = Stretch.None,
  488. AlignmentX = AlignmentX.Left,
  489. }
  490. };
  491. }
  492. UpdateFlowDirection();
  493. }
  494. else
  495. {
  496. if (item is not null && ItemTemplate is null && SelectionBoxItemTemplate is null && DisplayMemberBinding is { } binding)
  497. {
  498. var template = new FuncDataTemplate<object?>((_, _) =>
  499. new TextBlock
  500. {
  501. [TextBlock.DataContextProperty] = item,
  502. [!TextBlock.TextProperty] = binding,
  503. });
  504. var text = template.Build(item);
  505. SelectionBoxItem = text;
  506. }
  507. else
  508. {
  509. SelectionBoxItem = item;
  510. }
  511. }
  512. }
  513. private void UpdateFlowDirection()
  514. {
  515. if (SelectionBoxItem is Rectangle rectangle)
  516. {
  517. if ((rectangle.Fill as VisualBrush)?.Visual is Visual content)
  518. {
  519. var flowDirection = content.VisualParent?.FlowDirection ?? FlowDirection.LeftToRight;
  520. rectangle.FlowDirection = flowDirection;
  521. }
  522. }
  523. }
  524. private void UpdateInputTextFromSelection(object? item)
  525. {
  526. //if we are modifying the text box which has deselected a value we don't want to update the textbox value
  527. if (_skipNextTextChanged)
  528. return;
  529. SetCurrentValue(TextProperty, GetItemTextValue(item));
  530. }
  531. private bool SelectNext() => MoveSelection(SelectedIndex, 1, WrapSelection);
  532. private bool SelectPrevious() => MoveSelection(SelectedIndex, -1, WrapSelection);
  533. private bool MoveSelection(int startIndex, int step, bool wrap)
  534. {
  535. static bool IsSelectable(object? o) => (o as AvaloniaObject)?.GetValue(IsEnabledProperty) ?? true;
  536. var count = ItemCount;
  537. for (int i = startIndex + step; i != startIndex; i += step)
  538. {
  539. if (i < 0 || i >= count)
  540. {
  541. if (wrap)
  542. {
  543. if (i < 0)
  544. i += count;
  545. else if (i >= count)
  546. i %= count;
  547. }
  548. else
  549. {
  550. return false;
  551. }
  552. }
  553. var item = ItemsView[i];
  554. var container = ContainerFromIndex(i);
  555. if (IsSelectable(item) && IsSelectable(container))
  556. {
  557. SelectedIndex = i;
  558. return true;
  559. }
  560. }
  561. return false;
  562. }
  563. /// <summary>
  564. /// Clears the selection
  565. /// </summary>
  566. public void Clear()
  567. {
  568. SelectedItem = null;
  569. SelectedIndex = -1;
  570. }
  571. private void HandleTextValueBindingValueChanged(AvaloniaPropertyChangedEventArgs? textSearchPropChange,
  572. AvaloniaPropertyChangedEventArgs? displayMemberPropChange)
  573. {
  574. BindingBase? textValueBinding;
  575. //prioritise using the TextSearch.TextBindingProperty if possible
  576. if (textSearchPropChange == null && TextSearch.GetTextBinding(this) is BindingBase textSearchBinding)
  577. textValueBinding = textSearchBinding;
  578. else if (textSearchPropChange != null && textSearchPropChange.NewValue is BindingBase eventTextSearchBinding)
  579. textValueBinding = eventTextSearchBinding;
  580. else if (displayMemberPropChange != null && displayMemberPropChange.NewValue is BindingBase eventDisplayMemberBinding)
  581. textValueBinding = eventDisplayMemberBinding;
  582. else
  583. textValueBinding = null;
  584. if (_textValueBindingEvaluator == null)
  585. _textValueBindingEvaluator = BindingEvaluator<string?>.TryCreate(textValueBinding);
  586. else if (textValueBinding == null)
  587. _textValueBindingEvaluator = null;
  588. else
  589. _textValueBindingEvaluator.UpdateBinding(textValueBinding);
  590. //if the binding is set we want to set the initial value for the selected item so the text box has the correct value
  591. if (_textValueBindingEvaluator != null)
  592. _textValueBindingEvaluator.Value = GetItemTextValue(SelectedValue);
  593. }
  594. private void TextChanged(string? newValue)
  595. {
  596. if (!IsEditable || _skipNextTextChanged)
  597. return;
  598. int selectedIdx = -1;
  599. object? selectedItem = null;
  600. int i = -1;
  601. foreach (object? item in Items)
  602. {
  603. i++;
  604. string itemText = GetItemTextValue(item);
  605. if (string.Equals(newValue, itemText, StringComparison.CurrentCultureIgnoreCase))
  606. {
  607. selectedIdx = i;
  608. selectedItem = item;
  609. break;
  610. }
  611. }
  612. _skipNextTextChanged = true;
  613. try
  614. {
  615. SelectedIndex = selectedIdx;
  616. SelectedItem = selectedItem;
  617. }
  618. finally
  619. {
  620. _skipNextTextChanged = false;
  621. }
  622. //when changing the SelectedIndex it will call: KeyboardNavigation.SetTabOnceActiveElement(this, [combo box item]);
  623. //this will then break tab navigation back into this combobox as it will try to focus the combo box item
  624. //rather than the combobox or editable text box, so we need to SetTabOnceActiveElement to null
  625. KeyboardNavigation.SetTabOnceActiveElement(this, null);
  626. }
  627. private string GetItemTextValue(object? item)
  628. => TextSearch.GetEffectiveText(item, _textValueBindingEvaluator);
  629. }
  630. }