Skip to content
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

IsEditable combox box #18094

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions samples/ControlCatalog/Pages/ComboBoxPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,16 @@
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>

<StackPanel Spacing="10">
<ComboBox WrapSelection="{Binding WrapSelection}" PlaceholderText="Editable"
ItemsSource="{Binding Values}" DisplayMemberBinding="{Binding Name}"
IsEditable="True" Text="{Binding TextValue}" ItemTextBinding="{Binding Name}"
SelectedItem="{Binding SelectedItem}" />

<TextBlock Text="{Binding TextValue, StringFormat=Text Value: {0}}" />
<TextBlock Text="{Binding SelectedItem.Name, StringFormat=Selected Item: {0}}" />
</StackPanel>
</WrapPanel>
</StackPanel>
</StackPanel>
Expand Down
14 changes: 14 additions & 0 deletions samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ public bool WrapSelection
set => this.RaiseAndSetIfChanged(ref _wrapSelection, value);
}

private string _textValue = string.Empty;
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" },
Expand Down
151 changes: 149 additions & 2 deletions src/Avalonia.Controls/ComboBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Avalonia.Metadata;
using Avalonia.Reactive;
using Avalonia.VisualTree;
using static Avalonia.Controls.AutoCompleteBox;

namespace Avalonia.Controls
{
Expand All @@ -38,6 +39,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>
Expand Down Expand Up @@ -73,7 +82,19 @@ 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="ItemTextBinding"/> property.
/// </summary>
public static readonly StyledProperty<IBinding?> ItemTextBindingProperty =
AvaloniaProperty.Register<ComboBox, IBinding?>(nameof(ItemTextBinding));

/// <summary>
/// Defines the <see cref="SelectionBoxItemTemplate"/> property.
/// </summary>
Expand All @@ -95,6 +116,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>
Expand All @@ -103,6 +128,11 @@ static ComboBox()
ItemsPanelProperty.OverrideDefaultValue<ComboBox>(DefaultPanel);
FocusableProperty.OverrideDefaultValue<ComboBox>(true);
IsTextSearchEnabledProperty.OverrideDefaultValue<ComboBox>(true);
TextProperty.Changed.AddClassHandler<ComboBox>((x, e) => x.TextChanged(e));
ItemTextBindingProperty.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)));
}

/// <summary>
Expand All @@ -124,6 +154,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>
Expand Down Expand Up @@ -188,6 +227,34 @@ public IDataTemplate? SelectionBoxItemTemplate
set => SetValue(SelectionBoxItemTemplateProperty, value);
}

/// <summary>
/// Gets or sets the text used when <see cref="IsEditable"/> is true.
/// </summary>
public string? Text
{
get => GetValue(TextProperty);
set => SetValue(TextProperty, value);
}

/// <summary>
/// Gets or sets the <see cref="T:Avalonia.Data.Binding" /> that
/// is used to get the text for editing of an item.
/// </summary>
/// <value>The <see cref="T:Avalonia.Data.IBinding" /> object used
/// when binding to a collection property.</value>
[AssignBinding, InheritDataTypeFromItems(nameof(ItemsSource), AncestorType = typeof(ComboBox))]
public IBinding? ItemTextBinding
{
get => GetValue(ItemTextBindingProperty);
set => SetValue(ItemTextBindingProperty, value);
}

protected override void OnInitialized()
{
EnsureTextValueBinderOrThrow();
base.OnInitialized();
}

protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
Expand Down Expand Up @@ -229,7 +296,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;
Expand Down Expand Up @@ -315,6 +382,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
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)
Expand Down Expand Up @@ -348,6 +424,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/>
Expand All @@ -357,6 +435,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
{
UpdateSelectionBoxItem(change.NewValue);
TryFocusSelectedItem();
UpdateInputTextFromSelection(change.NewValue);
}
else if (change.Property == IsDropDownOpenProperty)
{
Expand All @@ -366,6 +445,10 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
{
CoerceValue(SelectionBoxItemTemplateProperty);
}
else if (change.Property == IsEditableProperty && change.GetNewValue<bool>())
{
UpdateInputTextFromSelection(SelectedItem);
}
base.OnPropertyChanged(change);
}

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

if(IsEditable && CanFocus(this))
{
Focus();
}

DropDownClosed?.Invoke(this, EventArgs.Empty);
}

Expand Down Expand Up @@ -502,6 +590,11 @@ private void UpdateFlowDirection()
}
}

private void UpdateInputTextFromSelection(object? item)
{
SetCurrentValue(TextProperty, GetItemTextValue(item));
}

private void SelectFocusedItem()
{
foreach (var dropdownItem in GetRealizedContainers())
Expand Down Expand Up @@ -561,5 +654,59 @@ public void Clear()
SelectedItem = null;
SelectedIndex = -1;
}

private void ItemTextBindingChanged(AvaloniaPropertyChangedEventArgs e)
{
_textValueBindingEvaluator = e.NewValue is IBinding binding
? new(binding) : null;

if(IsInitialized)
EnsureTextValueBinderOrThrow();

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 set the text value binding using {nameof(ItemTextBinding)}");
}

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 _textValueBindingEvaluator.GetDynamicValue(item) ?? item?.ToString() ?? string.Empty;
}
}
}
51 changes: 48 additions & 3 deletions src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<ComboBoxItem>Item 1</ComboBoxItem>
<ComboBoxItem>Item 2</ComboBoxItem>
</ComboBox>

<ComboBox PlaceholderText="Error">
<DataValidationErrors.Error>
<sys:Exception>
Expand All @@ -25,6 +26,25 @@
</sys:Exception>
</DataValidationErrors.Error>
</ComboBox>

<ComboBox SelectedIndex="1" IsEditable="True">
<ComboBoxItem>Item A</ComboBoxItem>
<ComboBoxItem>Item b</ComboBoxItem>
<ComboBoxItem>Item c</ComboBoxItem>
</ComboBox>

<ComboBox SelectedIndex="0">
<ComboBox.SelectionBoxItemTemplate>
<DataTemplate>
<Border Padding="20" BorderBrush="Red" BorderThickness="1">
<TextBlock Text="{ReflectionBinding}"/>
</Border>
</DataTemplate>
</ComboBox.SelectionBoxItemTemplate>
<ComboBoxItem>Item A</ComboBoxItem>
<ComboBoxItem>Item b</ComboBoxItem>
<ComboBoxItem>Item c</ComboBoxItem>
</ComboBox>
</StackPanel>
</Border>
</Design.PreviewWith>
Expand Down Expand Up @@ -80,17 +100,42 @@
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Margin="{TemplateBinding Padding}"
Text="{TemplateBinding PlaceholderText}"
Foreground="{TemplateBinding PlaceholderForeground}"
IsVisible="{TemplateBinding SelectionBoxItem, Converter={x:Static ObjectConverters.IsNull}}" />
Foreground="{TemplateBinding PlaceholderForeground}">
<TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="SelectionBoxItem" RelativeSource="{RelativeSource TemplatedParent}" Converter="{x:Static ObjectConverters.IsNull}" />
<Binding Path="!IsEditable" RelativeSource="{RelativeSource TemplatedParent}" />
</MultiBinding>
</TextBlock.IsVisible>
</TextBlock>
<ContentControl x:Name="ContentPresenter"
Content="{TemplateBinding SelectionBoxItem}"
Grid.Column="0"
Margin="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}">
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
IsVisible="{TemplateBinding IsEditable, Converter={x:Static BoolConverters.Not}}">
</ContentControl>

<TextBox Name="PART_InputText"
Grid.Column="0"
Padding="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Foreground="{TemplateBinding Foreground}"
Background="Transparent"
Text="{TemplateBinding Text, Mode=TwoWay}"
Watermark="{TemplateBinding PlaceholderText}"
BorderThickness="0"
IsVisible="{TemplateBinding IsEditable}">
<TextBox.Resources>
<SolidColorBrush x:Key="TextControlBackgroundFocused">Transparent</SolidColorBrush>
<SolidColorBrush x:Key="TextControlBackgroundPointerOver">Transparent</SolidColorBrush>
<Thickness x:Key="TextControlBorderThemeThicknessFocused">0</Thickness>
</TextBox.Resources>
</TextBox>

<Border x:Name="DropDownOverlay"
Grid.Column="1"
Background="Transparent"
Expand Down
Loading
Loading