-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
IsEditable combox box #18094
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
IsEditable combox box #18094
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,20 @@ public bool WrapSelection | |
set => this.RaiseAndSetIfChanged(ref _wrapSelection, value); | ||
} | ||
|
||
private string _textValue = string.Empty; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In C#, variables generally go near the top of the class and are grouped together. |
||
public string TextValue | ||
{ | ||
get => _textValue; | ||
set => this.RaiseAndSetIfChanged(ref _textValue, value); | ||
} | ||
|
||
private IdAndName? _selectedItem = null; | ||
public IdAndName? SelectedItem | ||
{ | ||
get => _selectedItem; | ||
set => this.RaiseAndSetIfChanged(ref _selectedItem, value); | ||
} | ||
|
||
public ObservableCollection<IdAndName> Values { get; set; } = new ObservableCollection<IdAndName> | ||
{ | ||
new IdAndName(){ Id = "Id 1", Name = "Name 1", SearchText = "A" }, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,9 +5,9 @@ | |
using Avalonia.Controls.Primitives; | ||
using Avalonia.Controls.Shapes; | ||
using Avalonia.Controls.Templates; | ||
using Avalonia.Controls.Utils; | ||
using Avalonia.Data; | ||
using Avalonia.Input; | ||
using Avalonia.Interactivity; | ||
using Avalonia.Layout; | ||
using Avalonia.Media; | ||
using Avalonia.Metadata; | ||
|
@@ -38,6 +38,14 @@ public class ComboBox : SelectingItemsControl | |
public static readonly StyledProperty<bool> IsDropDownOpenProperty = | ||
AvaloniaProperty.Register<ComboBox, bool>(nameof(IsDropDownOpen)); | ||
|
||
/// <summary> | ||
/// Defines the <see cref="IsEditable"/> property. | ||
/// </summary> | ||
public static readonly DirectProperty<ComboBox, bool> IsEditableProperty = | ||
AvaloniaProperty.RegisterDirect<ComboBox, bool>(nameof(IsEditable), | ||
o => o.IsEditable, | ||
(o, v) => o.IsEditable = v); | ||
|
||
/// <summary> | ||
/// Defines the <see cref="MaxDropDownHeight"/> property. | ||
/// </summary> | ||
|
@@ -73,7 +81,13 @@ public class ComboBox : SelectingItemsControl | |
/// </summary> | ||
public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty = | ||
ContentControl.VerticalContentAlignmentProperty.AddOwner<ComboBox>(); | ||
|
||
|
||
/// <summary> | ||
/// Defines the <see cref="Text"/> property | ||
/// </summary> | ||
public static readonly StyledProperty<string?> TextProperty = | ||
TextBlock.TextProperty.AddOwner<ComboBox>(new(string.Empty, BindingMode.TwoWay)); | ||
|
||
/// <summary> | ||
/// Defines the <see cref="SelectionBoxItemTemplate"/> property. | ||
/// </summary> | ||
|
@@ -95,6 +109,10 @@ public class ComboBox : SelectingItemsControl | |
private object? _selectionBoxItem; | ||
private readonly CompositeDisposable _subscriptionsOnOpen = new CompositeDisposable(); | ||
|
||
private bool _isEditable; | ||
private TextBox? _inputText; | ||
private BindingEvaluator<string?>? _textValueBindingEvaluator = null; | ||
|
||
/// <summary> | ||
/// Initializes static members of the <see cref="ComboBox"/> class. | ||
/// </summary> | ||
|
@@ -103,6 +121,12 @@ static ComboBox() | |
ItemsPanelProperty.OverrideDefaultValue<ComboBox>(DefaultPanel); | ||
FocusableProperty.OverrideDefaultValue<ComboBox>(true); | ||
IsTextSearchEnabledProperty.OverrideDefaultValue<ComboBox>(true); | ||
TextProperty.Changed.AddClassHandler<ComboBox>((x, e) => x.TextChanged(e)); | ||
DisplayMemberBindingProperty.Changed.AddClassHandler<ComboBox>((x, e) => x.DisplayMemberBindingChanged(e)); | ||
TextSearch.TextBindingProperty.Changed.AddClassHandler<ComboBox>((x, e) => x.ItemTextBindingChanged(e)); | ||
//when the items change we need to simulate a text change to validate the text being an item or not and selecting it | ||
ItemsSourceProperty.Changed.AddClassHandler<ComboBox>((x, e) => x.TextChanged( | ||
new AvaloniaPropertyChangedEventArgs<string?>(e.Sender, TextProperty, x.Text, x.Text, e.Priority))); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This might be an issue if something is listening for text change events? It might not. I have not tested it. Personally though, I would expect a text change event to only ever be fired when the text actually changes. |
||
} | ||
|
||
/// <summary> | ||
|
@@ -124,6 +148,15 @@ public bool IsDropDownOpen | |
set => SetValue(IsDropDownOpenProperty, value); | ||
} | ||
|
||
/// <summary> | ||
/// Gets or sets a value indicating whether the control is editable | ||
/// </summary> | ||
public bool IsEditable | ||
{ | ||
get => _isEditable; | ||
set => SetAndRaise(IsEditableProperty, ref _isEditable, value); | ||
} | ||
|
||
/// <summary> | ||
/// Gets or sets the maximum height for the dropdown list. | ||
/// </summary> | ||
|
@@ -188,6 +221,21 @@ public IDataTemplate? SelectionBoxItemTemplate | |
set => SetValue(SelectionBoxItemTemplateProperty, value); | ||
} | ||
|
||
/// <summary> | ||
/// Gets or sets the text used when <see cref="IsEditable"/> is true. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should document what this does if |
||
/// </summary> | ||
public string? Text | ||
{ | ||
get => GetValue(TextProperty); | ||
set => SetValue(TextProperty, value); | ||
} | ||
|
||
protected override void OnInitialized() | ||
{ | ||
EnsureTextValueBinderOrThrow(); | ||
base.OnInitialized(); | ||
} | ||
|
||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) | ||
{ | ||
base.OnAttachedToVisualTree(e); | ||
|
@@ -229,7 +277,7 @@ protected override void OnKeyDown(KeyEventArgs e) | |
SetCurrentValue(IsDropDownOpenProperty, false); | ||
e.Handled = true; | ||
} | ||
else if (!IsDropDownOpen && (e.Key == Key.Enter || e.Key == Key.Space)) | ||
else if (!IsDropDownOpen && !IsEditable && (e.Key == Key.Enter || e.Key == Key.Space)) | ||
{ | ||
SetCurrentValue(IsDropDownOpenProperty, true); | ||
e.Handled = true; | ||
|
@@ -315,6 +363,15 @@ protected override void OnPointerPressed(PointerPressedEventArgs e) | |
/// <inheritdoc/> | ||
protected override void OnPointerReleased(PointerReleasedEventArgs e) | ||
{ | ||
//if the user clicked in the input text we don't want to open the dropdown | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How does WPF/winforms do this? I seem to vaguely recall that it actually opens the drop down and filters the list as you type. But it has been a long time, so I could be misremembering. |
||
if (_inputText != null | ||
&& !e.Handled | ||
&& e.Source is StyledElement styledSource | ||
&& styledSource.TemplatedParent == _inputText) | ||
{ | ||
return; | ||
} | ||
|
||
if (!e.Handled && e.Source is Visual source) | ||
{ | ||
if (_popup?.IsInsidePopup(source) == true) | ||
|
@@ -348,6 +405,8 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) | |
_popup = e.NameScope.Get<Popup>("PART_Popup"); | ||
_popup.Opened += PopupOpened; | ||
_popup.Closed += PopupClosed; | ||
|
||
_inputText = e.NameScope.Get<TextBox>("PART_InputText"); | ||
} | ||
|
||
/// <inheritdoc/> | ||
|
@@ -357,6 +416,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang | |
{ | ||
UpdateSelectionBoxItem(change.NewValue); | ||
TryFocusSelectedItem(); | ||
UpdateInputTextFromSelection(change.NewValue); | ||
} | ||
else if (change.Property == IsDropDownOpenProperty) | ||
{ | ||
|
@@ -366,6 +426,10 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang | |
{ | ||
CoerceValue(SelectionBoxItemTemplateProperty); | ||
} | ||
else if (change.Property == IsEditableProperty && change.GetNewValue<bool>()) | ||
{ | ||
UpdateInputTextFromSelection(SelectedItem); | ||
} | ||
base.OnPropertyChanged(change); | ||
} | ||
|
||
|
@@ -386,6 +450,11 @@ private void PopupClosed(object? sender, EventArgs e) | |
{ | ||
_subscriptionsOnOpen.Clear(); | ||
|
||
if(IsEditable && CanFocus(this)) | ||
{ | ||
Focus(); | ||
} | ||
|
||
DropDownClosed?.Invoke(this, EventArgs.Empty); | ||
} | ||
|
||
|
@@ -502,6 +571,14 @@ private void UpdateFlowDirection() | |
} | ||
} | ||
|
||
private void UpdateInputTextFromSelection(object? item) | ||
{ | ||
//if we are modifying the text box which has deselected a value we don't want to update the textbox value | ||
if (_skipNextTextChanged) | ||
return; | ||
SetCurrentValue(TextProperty, GetItemTextValue(item)); | ||
} | ||
|
||
private void SelectFocusedItem() | ||
{ | ||
foreach (var dropdownItem in GetRealizedContainers()) | ||
|
@@ -561,5 +638,80 @@ public void Clear() | |
SelectedItem = null; | ||
SelectedIndex = -1; | ||
} | ||
|
||
private void ItemTextBindingChanged(AvaloniaPropertyChangedEventArgs e) | ||
=> HandleTextValueBindingValueChanged(e, null); | ||
|
||
private void DisplayMemberBindingChanged(AvaloniaPropertyChangedEventArgs e) | ||
=> HandleTextValueBindingValueChanged(null, e); | ||
|
||
private void HandleTextValueBindingValueChanged(AvaloniaPropertyChangedEventArgs? textSearchPropChange, | ||
AvaloniaPropertyChangedEventArgs? displayMemberPropChange) | ||
{ | ||
IBinding? textValueBinding; | ||
//prioritise using the TextSearch.TextBindingProperty if possible | ||
if (textSearchPropChange == null && TextSearch.GetTextBinding(this) is IBinding textSearchBinding) | ||
textValueBinding = textSearchBinding; | ||
|
||
else if (textSearchPropChange != null && textSearchPropChange.NewValue is IBinding eventTextSearchBinding) | ||
textValueBinding = eventTextSearchBinding; | ||
|
||
else if (displayMemberPropChange != null && displayMemberPropChange.NewValue is IBinding eventDisplayMemberBinding) | ||
textValueBinding = eventDisplayMemberBinding; | ||
|
||
else | ||
textValueBinding = null; | ||
|
||
_textValueBindingEvaluator = BindingEvaluator<string?>.TryCreate(textValueBinding); | ||
|
||
if (IsInitialized) | ||
EnsureTextValueBinderOrThrow(); | ||
|
||
//if the binding is set we want to set the initial value for the selected item so the text box has the correct value | ||
if (_textValueBindingEvaluator != null) | ||
_textValueBindingEvaluator.Value = GetItemTextValue(SelectedValue); | ||
} | ||
|
||
private void EnsureTextValueBinderOrThrow() | ||
{ | ||
if (IsEditable && _textValueBindingEvaluator == null) | ||
throw new InvalidOperationException($"When {nameof(ComboBox)}.{nameof(IsEditable)} is true you must either set {nameof(ComboBox)}.{nameof(DisplayMemberBinding)} or set the text value binding using attached property {nameof(TextSearch)}.{nameof(TextSearch.TextBindingProperty)}"); | ||
} | ||
|
||
private bool _skipNextTextChanged = false; | ||
private void TextChanged(AvaloniaPropertyChangedEventArgs e) | ||
{ | ||
if (Items == null || !IsEditable || _skipNextTextChanged) | ||
return; | ||
|
||
string newVal = e.GetNewValue<string>(); | ||
int selectedIdx = -1; | ||
object? selectedItem = null; | ||
int i = -1; | ||
foreach (object? item in Items) | ||
{ | ||
i++; | ||
string itemText = GetItemTextValue(item); | ||
if (string.Equals(newVal, itemText, StringComparison.CurrentCultureIgnoreCase)) | ||
{ | ||
selectedIdx = i; | ||
selectedItem = item; | ||
break; | ||
} | ||
} | ||
|
||
_skipNextTextChanged = true; | ||
SelectedIndex = selectedIdx; | ||
SelectedItem = selectedItem; | ||
_skipNextTextChanged = false; | ||
} | ||
|
||
private string GetItemTextValue(object? item) | ||
{ | ||
if (_textValueBindingEvaluator == null) | ||
return string.Empty; | ||
|
||
return TextSearch.GetEffectiveText(item, _textValueBindingEvaluator); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should use full property names to make this clearer. Not everyone will go straight to XAML when running the catalogue.