Skip to content

Commit 2f1a66f

Browse files
committed
IsEditable combox box with text bindings
1 parent 874c99f commit 2f1a66f

File tree

5 files changed

+247
-9
lines changed

5 files changed

+247
-9
lines changed

samples/ControlCatalog/Pages/ComboBoxPage.xaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,16 @@
135135
</DataTemplate>
136136
</ComboBox.ItemTemplate>
137137
</ComboBox>
138+
139+
<StackPanel Spacing="10">
140+
<ComboBox WrapSelection="{Binding WrapSelection}" PlaceholderText="Editable"
141+
ItemsSource="{Binding Values}" DisplayMemberBinding="{Binding Name}"
142+
IsEditable="True" Text="{Binding TextValue}" ItemTextBinding="{Binding Name}"
143+
SelectedItem="{Binding SelectedItem}" />
144+
145+
<TextBlock Text="{Binding TextValue, StringFormat=Text Value: {0}}" />
146+
<TextBlock Text="{Binding SelectedItem.Name, StringFormat=Selected Item: {0}}" />
147+
</StackPanel>
138148
</WrapPanel>
139149
</StackPanel>
140150
</StackPanel>

samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,20 @@ public bool WrapSelection
1717
set => this.RaiseAndSetIfChanged(ref _wrapSelection, value);
1818
}
1919

20+
private string _textValue = string.Empty;
21+
public string TextValue
22+
{
23+
get => _textValue;
24+
set => this.RaiseAndSetIfChanged(ref _textValue, value);
25+
}
26+
27+
private IdAndName? _selectedItem = null;
28+
public IdAndName? SelectedItem
29+
{
30+
get => _selectedItem;
31+
set => this.RaiseAndSetIfChanged(ref _selectedItem, value);
32+
}
33+
2034
public ObservableCollection<IdAndName> Values { get; set; } = new ObservableCollection<IdAndName>
2135
{
2236
new IdAndName(){ Id = "Id 1", Name = "Name 1" },

src/Avalonia.Controls/ComboBox.cs

Lines changed: 149 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Avalonia.Metadata;
1414
using Avalonia.Reactive;
1515
using Avalonia.VisualTree;
16+
using static Avalonia.Controls.AutoCompleteBox;
1617

1718
namespace Avalonia.Controls
1819
{
@@ -38,6 +39,14 @@ public class ComboBox : SelectingItemsControl
3839
public static readonly StyledProperty<bool> IsDropDownOpenProperty =
3940
AvaloniaProperty.Register<ComboBox, bool>(nameof(IsDropDownOpen));
4041

42+
/// <summary>
43+
/// Defines the <see cref="IsEditable"/> property.
44+
/// </summary>
45+
public static readonly DirectProperty<ComboBox, bool> IsEditableProperty =
46+
AvaloniaProperty.RegisterDirect<ComboBox, bool>(nameof(IsEditable),
47+
o => o.IsEditable,
48+
(o, v) => o.IsEditable = v);
49+
4150
/// <summary>
4251
/// Defines the <see cref="MaxDropDownHeight"/> property.
4352
/// </summary>
@@ -73,7 +82,19 @@ public class ComboBox : SelectingItemsControl
7382
/// </summary>
7483
public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty =
7584
ContentControl.VerticalContentAlignmentProperty.AddOwner<ComboBox>();
76-
85+
86+
/// <summary>
87+
/// Defines the <see cref="Text"/> property
88+
/// </summary>
89+
public static readonly StyledProperty<string?> TextProperty =
90+
TextBlock.TextProperty.AddOwner<ComboBox>(new(string.Empty, BindingMode.TwoWay));
91+
92+
/// <summary>
93+
/// Defines the <see cref="ItemTextBinding"/> property.
94+
/// </summary>
95+
public static readonly StyledProperty<IBinding?> ItemTextBindingProperty =
96+
AvaloniaProperty.Register<ComboBox, IBinding?>(nameof(ItemTextBinding));
97+
7798
/// <summary>
7899
/// Defines the <see cref="SelectionBoxItemTemplate"/> property.
79100
/// </summary>
@@ -95,6 +116,10 @@ public class ComboBox : SelectingItemsControl
95116
private object? _selectionBoxItem;
96117
private readonly CompositeDisposable _subscriptionsOnOpen = new CompositeDisposable();
97118

119+
private bool _isEditable;
120+
private TextBox? _inputText;
121+
private BindingEvaluator<string>? _textValueBindingEvaluator = null;
122+
98123
/// <summary>
99124
/// Initializes static members of the <see cref="ComboBox"/> class.
100125
/// </summary>
@@ -103,6 +128,11 @@ static ComboBox()
103128
ItemsPanelProperty.OverrideDefaultValue<ComboBox>(DefaultPanel);
104129
FocusableProperty.OverrideDefaultValue<ComboBox>(true);
105130
IsTextSearchEnabledProperty.OverrideDefaultValue<ComboBox>(true);
131+
TextProperty.Changed.AddClassHandler<ComboBox>((x, e) => x.TextChanged(e));
132+
ItemTextBindingProperty.Changed.AddClassHandler<ComboBox>((x, e) => x.ItemTextBindingChanged(e));
133+
//when the items change we need to simulate a text change to validate the text being an item or not and selecting it
134+
ItemsSourceProperty.Changed.AddClassHandler<ComboBox>((x, e) => x.TextChanged(
135+
new AvaloniaPropertyChangedEventArgs<string?>(e.Sender, TextProperty, x.Text, x.Text, e.Priority)));
106136
}
107137

108138
/// <summary>
@@ -124,6 +154,15 @@ public bool IsDropDownOpen
124154
set => SetValue(IsDropDownOpenProperty, value);
125155
}
126156

157+
/// <summary>
158+
/// Gets or sets a value indicating whether the control is editable
159+
/// </summary>
160+
public bool IsEditable
161+
{
162+
get => _isEditable;
163+
set => SetAndRaise(IsEditableProperty, ref _isEditable, value);
164+
}
165+
127166
/// <summary>
128167
/// Gets or sets the maximum height for the dropdown list.
129168
/// </summary>
@@ -188,6 +227,34 @@ public IDataTemplate? SelectionBoxItemTemplate
188227
set => SetValue(SelectionBoxItemTemplateProperty, value);
189228
}
190229

230+
/// <summary>
231+
/// Gets or sets the text used when <see cref="IsEditable"/> is true.
232+
/// </summary>
233+
public string? Text
234+
{
235+
get => GetValue(TextProperty);
236+
set => SetValue(TextProperty, value);
237+
}
238+
239+
/// <summary>
240+
/// Gets or sets the <see cref="T:Avalonia.Data.Binding" /> that
241+
/// is used to get the text for editing of an item.
242+
/// </summary>
243+
/// <value>The <see cref="T:Avalonia.Data.IBinding" /> object used
244+
/// when binding to a collection property.</value>
245+
[AssignBinding, InheritDataTypeFromItems(nameof(ItemsSource), AncestorType = typeof(ComboBox))]
246+
public IBinding? ItemTextBinding
247+
{
248+
get => GetValue(ItemTextBindingProperty);
249+
set => SetValue(ItemTextBindingProperty, value);
250+
}
251+
252+
protected override void OnInitialized()
253+
{
254+
EnsureTextValueBinderOrThrow();
255+
base.OnInitialized();
256+
}
257+
191258
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
192259
{
193260
base.OnAttachedToVisualTree(e);
@@ -229,7 +296,7 @@ protected override void OnKeyDown(KeyEventArgs e)
229296
SetCurrentValue(IsDropDownOpenProperty, false);
230297
e.Handled = true;
231298
}
232-
else if (!IsDropDownOpen && (e.Key == Key.Enter || e.Key == Key.Space))
299+
else if (!IsDropDownOpen && !IsEditable && (e.Key == Key.Enter || e.Key == Key.Space))
233300
{
234301
SetCurrentValue(IsDropDownOpenProperty, true);
235302
e.Handled = true;
@@ -315,6 +382,15 @@ protected override void OnPointerPressed(PointerPressedEventArgs e)
315382
/// <inheritdoc/>
316383
protected override void OnPointerReleased(PointerReleasedEventArgs e)
317384
{
385+
//if the user clicked in the input text we don't want to open the dropdown
386+
if (_inputText != null
387+
&& !e.Handled
388+
&& e.Source is StyledElement styledSource
389+
&& styledSource.TemplatedParent == _inputText)
390+
{
391+
return;
392+
}
393+
318394
if (!e.Handled && e.Source is Visual source)
319395
{
320396
if (_popup?.IsInsidePopup(source) == true)
@@ -348,6 +424,8 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
348424
_popup = e.NameScope.Get<Popup>("PART_Popup");
349425
_popup.Opened += PopupOpened;
350426
_popup.Closed += PopupClosed;
427+
428+
_inputText = e.NameScope.Get<TextBox>("PART_InputText");
351429
}
352430

353431
/// <inheritdoc/>
@@ -357,6 +435,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
357435
{
358436
UpdateSelectionBoxItem(change.NewValue);
359437
TryFocusSelectedItem();
438+
UpdateInputTextFromSelection(change.NewValue);
360439
}
361440
else if (change.Property == IsDropDownOpenProperty)
362441
{
@@ -366,6 +445,10 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
366445
{
367446
CoerceValue(SelectionBoxItemTemplateProperty);
368447
}
448+
else if (change.Property == IsEditableProperty && change.GetNewValue<bool>())
449+
{
450+
UpdateInputTextFromSelection(SelectedItem);
451+
}
369452
base.OnPropertyChanged(change);
370453
}
371454

@@ -386,6 +469,11 @@ private void PopupClosed(object? sender, EventArgs e)
386469
{
387470
_subscriptionsOnOpen.Clear();
388471

472+
if(IsEditable && CanFocus(this))
473+
{
474+
Focus();
475+
}
476+
389477
DropDownClosed?.Invoke(this, EventArgs.Empty);
390478
}
391479

@@ -502,6 +590,11 @@ private void UpdateFlowDirection()
502590
}
503591
}
504592

593+
private void UpdateInputTextFromSelection(object? item)
594+
{
595+
SetCurrentValue(TextProperty, GetItemTextValue(item));
596+
}
597+
505598
private void SelectFocusedItem()
506599
{
507600
foreach (var dropdownItem in GetRealizedContainers())
@@ -561,5 +654,59 @@ public void Clear()
561654
SelectedItem = null;
562655
SelectedIndex = -1;
563656
}
657+
658+
private void ItemTextBindingChanged(AvaloniaPropertyChangedEventArgs e)
659+
{
660+
_textValueBindingEvaluator = e.NewValue is IBinding binding
661+
? new(binding) : null;
662+
663+
if(IsInitialized)
664+
EnsureTextValueBinderOrThrow();
665+
666+
if(_textValueBindingEvaluator != null)
667+
_textValueBindingEvaluator.Value = GetItemTextValue(SelectedValue);
668+
}
669+
670+
private void EnsureTextValueBinderOrThrow()
671+
{
672+
if (IsEditable && _textValueBindingEvaluator == null)
673+
throw new InvalidOperationException($"When {nameof(ComboBox)}.{nameof(IsEditable)} is true you must set the text value binding using {nameof(ItemTextBinding)}");
674+
}
675+
676+
private bool _skipNextTextChanged = false;
677+
private void TextChanged(AvaloniaPropertyChangedEventArgs e)
678+
{
679+
if (Items == null || !IsEditable || _skipNextTextChanged)
680+
return;
681+
682+
string newVal = e.GetNewValue<string>();
683+
int selectedIdx = -1;
684+
object? selectedItem = null;
685+
int i = -1;
686+
foreach (object? item in Items)
687+
{
688+
i++;
689+
string itemText = GetItemTextValue(item);
690+
if (string.Equals(newVal, itemText, StringComparison.CurrentCultureIgnoreCase))
691+
{
692+
selectedIdx = i;
693+
selectedItem = item;
694+
break;
695+
}
696+
}
697+
698+
_skipNextTextChanged = true;
699+
SelectedIndex = selectedIdx;
700+
SelectedItem = selectedItem;
701+
_skipNextTextChanged = false;
702+
}
703+
704+
private string GetItemTextValue(object? item)
705+
{
706+
if (_textValueBindingEvaluator == null)
707+
return string.Empty;
708+
709+
return _textValueBindingEvaluator.GetDynamicValue(item) ?? item?.ToString() ?? string.Empty;
710+
}
564711
}
565712
}

src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<ComboBoxItem>Item 1</ComboBoxItem>
1717
<ComboBoxItem>Item 2</ComboBoxItem>
1818
</ComboBox>
19+
1920
<ComboBox PlaceholderText="Error">
2021
<DataValidationErrors.Error>
2122
<sys:Exception>
@@ -25,6 +26,25 @@
2526
</sys:Exception>
2627
</DataValidationErrors.Error>
2728
</ComboBox>
29+
30+
<ComboBox SelectedIndex="1" IsEditable="True">
31+
<ComboBoxItem>Item A</ComboBoxItem>
32+
<ComboBoxItem>Item b</ComboBoxItem>
33+
<ComboBoxItem>Item c</ComboBoxItem>
34+
</ComboBox>
35+
36+
<ComboBox SelectedIndex="0">
37+
<ComboBox.SelectionBoxItemTemplate>
38+
<DataTemplate>
39+
<Border Padding="20" BorderBrush="Red" BorderThickness="1">
40+
<TextBlock Text="{ReflectionBinding}"/>
41+
</Border>
42+
</DataTemplate>
43+
</ComboBox.SelectionBoxItemTemplate>
44+
<ComboBoxItem>Item A</ComboBoxItem>
45+
<ComboBoxItem>Item b</ComboBoxItem>
46+
<ComboBoxItem>Item c</ComboBoxItem>
47+
</ComboBox>
2848
</StackPanel>
2949
</Border>
3050
</Design.PreviewWith>
@@ -80,17 +100,42 @@
80100
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
81101
Margin="{TemplateBinding Padding}"
82102
Text="{TemplateBinding PlaceholderText}"
83-
Foreground="{TemplateBinding PlaceholderForeground}"
84-
IsVisible="{TemplateBinding SelectionBoxItem, Converter={x:Static ObjectConverters.IsNull}}" />
103+
Foreground="{TemplateBinding PlaceholderForeground}">
104+
<TextBlock.IsVisible>
105+
<MultiBinding Converter="{x:Static BoolConverters.And}">
106+
<Binding Path="SelectionBoxItem" RelativeSource="{RelativeSource TemplatedParent}" Converter="{x:Static ObjectConverters.IsNull}" />
107+
<Binding Path="!IsEditable" RelativeSource="{RelativeSource TemplatedParent}" />
108+
</MultiBinding>
109+
</TextBlock.IsVisible>
110+
</TextBlock>
85111
<ContentControl x:Name="ContentPresenter"
86112
Content="{TemplateBinding SelectionBoxItem}"
87113
Grid.Column="0"
88114
Margin="{TemplateBinding Padding}"
89115
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
90116
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
91-
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}">
117+
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
118+
IsVisible="{TemplateBinding IsEditable, Converter={x:Static BoolConverters.Not}}">
92119
</ContentControl>
93120

121+
<TextBox Name="PART_InputText"
122+
Grid.Column="0"
123+
Padding="{TemplateBinding Padding}"
124+
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
125+
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
126+
Foreground="{TemplateBinding Foreground}"
127+
Background="Transparent"
128+
Text="{TemplateBinding Text, Mode=TwoWay}"
129+
Watermark="{TemplateBinding PlaceholderText}"
130+
BorderThickness="0"
131+
IsVisible="{TemplateBinding IsEditable}">
132+
<TextBox.Resources>
133+
<SolidColorBrush x:Key="TextControlBackgroundFocused">Transparent</SolidColorBrush>
134+
<SolidColorBrush x:Key="TextControlBackgroundPointerOver">Transparent</SolidColorBrush>
135+
<Thickness x:Key="TextControlBorderThemeThicknessFocused">0</Thickness>
136+
</TextBox.Resources>
137+
</TextBox>
138+
94139
<Border x:Name="DropDownOverlay"
95140
Grid.Column="1"
96141
Background="Transparent"

0 commit comments

Comments
 (0)