Skip to content

Commit b09e0d5

Browse files
authored
Implement TextSearch.TextBinding (#18405)
* Implement TextSearch.TextBinding * Move AssignBinding to TextSearch.GetTextBinding
1 parent af7c3c7 commit b09e0d5

File tree

9 files changed

+286
-123
lines changed

9 files changed

+286
-123
lines changed

samples/ControlCatalog/Pages/ComboBoxPage.xaml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,8 @@
102102
<ComboBox
103103
WrapSelection="{Binding WrapSelection}"
104104
ItemsSource="{Binding Values}"
105-
DisplayMemberBinding="{Binding Name}">
106-
107-
</ComboBox>
105+
DisplayMemberBinding="{Binding Name}"
106+
TextSearch.TextBinding="{Binding SearchText, DataType=viewModels:IdAndName}" />
108107

109108
<ComboBox
110109
WrapSelection="{Binding WrapSelection}"

samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,18 @@ public bool WrapSelection
1919

2020
public ObservableCollection<IdAndName> Values { get; set; } = new ObservableCollection<IdAndName>
2121
{
22-
new IdAndName(){ Id = "Id 1", Name = "Name 1" },
23-
new IdAndName(){ Id = "Id 2", Name = "Name 2" },
24-
new IdAndName(){ Id = "Id 3", Name = "Name 3" },
25-
new IdAndName(){ Id = "Id 4", Name = "Name 4" },
26-
new IdAndName(){ Id = "Id 5", Name = "Name 5" },
22+
new IdAndName(){ Id = "Id 1", Name = "Name 1", SearchText = "A" },
23+
new IdAndName(){ Id = "Id 2", Name = "Name 2", SearchText = "B" },
24+
new IdAndName(){ Id = "Id 3", Name = "Name 3", SearchText = "C" },
25+
new IdAndName(){ Id = "Id 4", Name = "Name 4", SearchText = "D" },
26+
new IdAndName(){ Id = "Id 5", Name = "Name 5", SearchText = "E" },
2727
};
2828
}
2929

3030
public class IdAndName
3131
{
3232
public string? Id { get; set; }
3333
public string? Name { get; set; }
34+
public string? SearchText { get; set; }
3435
}
3536
}

src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2036,6 +2036,7 @@ public static bool EqualsOrdinalCaseSensitive(string? text, string? value)
20362036
}
20372037
}
20382038

2039+
// TODO12: Remove, this shouldn't be part of the public API. Use our internal BindingEvaluator instead.
20392040
/// <summary>
20402041
/// A framework element that permits a binding to be evaluated in a new data
20412042
/// context leaf node.

src/Avalonia.Controls/Presenters/ItemsPresenter.cs

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -197,44 +197,6 @@ private void CreateSimplePanelGenerator()
197197
return Panel?.Children;
198198
}
199199

200-
internal static bool ControlMatchesTextSearch(Control control, string textSearchTerm)
201-
{
202-
if (control is AvaloniaObject ao && ao.IsSet(TextSearch.TextProperty))
203-
{
204-
var searchText = ao.GetValue(TextSearch.TextProperty);
205-
206-
if (searchText?.StartsWith(textSearchTerm, StringComparison.OrdinalIgnoreCase) == true)
207-
{
208-
return true;
209-
}
210-
}
211-
return control is IContentControl cc &&
212-
cc.Content?.ToString()?.StartsWith(textSearchTerm, StringComparison.OrdinalIgnoreCase) == true;
213-
}
214-
215-
internal int GetIndexFromTextSearch(string textSearch)
216-
{
217-
if (Panel is VirtualizingPanel v)
218-
return v.GetIndexFromTextSearch(textSearch);
219-
return GetIndexFromTextSearch(ItemsControl?.Items, textSearch);
220-
}
221-
222-
internal static int GetIndexFromTextSearch(IReadOnlyList<object?>? items, string textSearchTerm)
223-
{
224-
if (items is null)
225-
return -1;
226-
227-
for (var i = 0; i < items.Count; i++)
228-
{
229-
if (items[i] is Control c && ControlMatchesTextSearch(c, textSearchTerm)
230-
|| items[i]?.ToString()?.StartsWith(textSearchTerm, StringComparison.OrdinalIgnoreCase) == true)
231-
{
232-
return i;
233-
}
234-
}
235-
return -1;
236-
}
237-
238200
internal int IndexFromContainer(Control container)
239201
{
240202
if (Panel is VirtualizingPanel v)

src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

Lines changed: 44 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Diagnostics.CodeAnalysis;
66
using System.Linq;
77
using Avalonia.Controls.Selection;
8+
using Avalonia.Controls.Utils;
89
using Avalonia.Data;
910
using Avalonia.Input;
1011
using Avalonia.Interactivity;
@@ -146,7 +147,7 @@ public class SelectingItemsControl : ItemsControl
146147
private bool _ignoreContainerSelectionChanged;
147148
private UpdateState? _updateState;
148149
private bool _hasScrolledToSelectedItem;
149-
private BindingHelper? _bindingHelper;
150+
private BindingEvaluator<object?>? _selectedValueBindingEvaluator;
150151
private bool _isSelectionChangeActive;
151152

152153
public SelectingItemsControl()
@@ -609,10 +610,10 @@ protected override void OnTextInput(TextInputEventArgs e)
609610

610611
_textSearchTerm += e.Text;
611612

612-
var newIndex = Presenter?.GetIndexFromTextSearch(_textSearchTerm);
613+
var newIndex = GetIndexFromTextSearch(_textSearchTerm);
613614
if (newIndex >= 0)
614615
{
615-
SelectedIndex = (int)newIndex;
616+
SelectedIndex = newIndex;
616617
}
617618

618619
StartTextSearchTimer();
@@ -678,17 +679,10 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
678679
{
679680
_isSelectionChangeActive = true;
680681

681-
if (_bindingHelper is null)
682-
{
683-
_bindingHelper = new BindingHelper(value);
684-
}
685-
else
686-
{
687-
_bindingHelper.UpdateBinding(value);
688-
}
682+
var bindingEvaluator = GetSelectedValueBindingEvaluator(value);
689683

690684
// Re-evaluate SelectedValue with the new binding
691-
SetCurrentValue(SelectedValueProperty, _bindingHelper.Evaluate(selectedItem));
685+
SetCurrentValue(SelectedValueProperty, bindingEvaluator.Evaluate(selectedItem));
692686
}
693687
finally
694688
{
@@ -1067,20 +1061,23 @@ private void SelectItemWithValue(object? value)
10671061
}
10681062
}
10691063

1070-
_bindingHelper ??= new BindingHelper(binding);
1064+
var bindingEvaluator = GetSelectedValueBindingEvaluator(binding);
10711065

10721066
// Matching UWP behavior, if duplicates are present, return the first item matching
10731067
// the SelectedValue provided
10741068
foreach (var item in items!)
10751069
{
1076-
var itemValue = _bindingHelper.Evaluate(item);
1070+
var itemValue = bindingEvaluator.Evaluate(item);
10771071

10781072
if (Equals(itemValue, value))
10791073
{
1074+
bindingEvaluator.ClearDataContext();
10801075
return item;
10811076
}
10821077
}
10831078

1079+
bindingEvaluator.ClearDataContext();
1080+
10841081
return AvaloniaProperty.UnsetValue;
10851082
}
10861083

@@ -1107,12 +1104,12 @@ private void UpdateSelectedValueFromItem()
11071104
return;
11081105
}
11091106

1110-
_bindingHelper ??= new BindingHelper(binding);
1107+
var bindingEvaluator = GetSelectedValueBindingEvaluator(binding);
11111108

11121109
try
11131110
{
11141111
_isSelectionChangeActive = true;
1115-
SetCurrentValue(SelectedValueProperty, _bindingHelper.Evaluate(item));
1112+
SetCurrentValue(SelectedValueProperty, bindingEvaluator.Evaluate(item));
11161113
}
11171114
finally
11181115
{
@@ -1338,6 +1335,37 @@ private void TextSearchTimer_Tick(object? sender, EventArgs e)
13381335
StopTextSearchTimer();
13391336
}
13401337

1338+
private int GetIndexFromTextSearch(string textSearchTerm)
1339+
{
1340+
if (string.IsNullOrEmpty(textSearchTerm))
1341+
return -1;
1342+
1343+
var count = Items.Count;
1344+
if (count == 0)
1345+
return -1;
1346+
1347+
var textBinding = TextSearch.GetTextBinding(this) ?? DisplayMemberBinding;
1348+
using var textBindingEvaluator = BindingEvaluator<string?>.TryCreate(textBinding);
1349+
1350+
for (var i = 0; i < count; i++)
1351+
{
1352+
var text = TextSearch.GetEffectiveText(Items[i], textBindingEvaluator);
1353+
if (text.StartsWith(textSearchTerm, StringComparison.OrdinalIgnoreCase))
1354+
{
1355+
return i;
1356+
}
1357+
}
1358+
1359+
return -1;
1360+
}
1361+
1362+
private BindingEvaluator<object?> GetSelectedValueBindingEvaluator(IBinding binding)
1363+
{
1364+
_selectedValueBindingEvaluator ??= new();
1365+
_selectedValueBindingEvaluator.UpdateBinding(binding);
1366+
return _selectedValueBindingEvaluator;
1367+
}
1368+
13411369
// When in a BeginInit..EndInit block, or when the DataContext is updating, we need to
13421370
// defer changes to the selection model because we have no idea in which order properties
13431371
// will be set. Consider:
@@ -1367,41 +1395,5 @@ private class UpdateState
13671395
public Optional<object?> SelectedItem { get; set; }
13681396
public Optional<object?> SelectedValue { get; set; }
13691397
}
1370-
1371-
/// <summary>
1372-
/// Helper class for evaluating a binding from an Item and IBinding instance
1373-
/// </summary>
1374-
private class BindingHelper : StyledElement
1375-
{
1376-
private BindingExpressionBase? _expression;
1377-
private IBinding? _lastBinding;
1378-
1379-
public BindingHelper(IBinding binding)
1380-
{
1381-
UpdateBinding(binding);
1382-
}
1383-
1384-
public static readonly StyledProperty<object> ValueProperty =
1385-
AvaloniaProperty.Register<BindingHelper, object>("Value");
1386-
1387-
public object? Evaluate(object? dataContext)
1388-
{
1389-
// Only update the DataContext if necessary
1390-
if (!Equals(dataContext, DataContext))
1391-
DataContext = dataContext;
1392-
1393-
return GetValue(ValueProperty);
1394-
}
1395-
1396-
public void UpdateBinding(IBinding binding)
1397-
{
1398-
if (binding == _lastBinding)
1399-
return;
1400-
1401-
_expression?.Dispose();
1402-
_expression = Bind(ValueProperty, binding);
1403-
_lastBinding = binding;
1404-
}
1405-
}
14061398
}
14071399
}
Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using Avalonia.Controls.Utils;
2+
using Avalonia.Data;
13
using Avalonia.Interactivity;
24

35
namespace Avalonia.Controls.Primitives
@@ -9,29 +11,95 @@ public static class TextSearch
911
{
1012
/// <summary>
1113
/// Defines the Text attached property.
12-
/// This text will be considered during text search in <see cref="SelectingItemsControl"/> (such as <see cref="ComboBox"/>)
14+
/// This text will be considered during text search in <see cref="SelectingItemsControl"/> (such as <see cref="ComboBox"/>).
15+
/// This property is usually applied to an item container directly.
1316
/// </summary>
1417
public static readonly AttachedProperty<string?> TextProperty
1518
= AvaloniaProperty.RegisterAttached<Interactive, string?>("Text", typeof(TextSearch));
1619

1720
/// <summary>
18-
/// Sets the <see cref="TextProperty"/> for a control.
21+
/// Defines the TextBinding attached property.
22+
/// The binding will be applied to each item during text search in <see cref="SelectingItemsControl"/> (such as <see cref="ComboBox"/>).
1923
/// </summary>
20-
/// <param name="control">The control</param>
21-
/// <param name="text">The search text to set</param>
24+
public static readonly AttachedProperty<IBinding?> TextBindingProperty
25+
= AvaloniaProperty.RegisterAttached<Interactive, IBinding?>("TextBinding", typeof(TextSearch));
26+
27+
// TODO12: Control should be Interactive to match the property definition.
28+
/// <summary>
29+
/// Sets the value of the <see cref="TextProperty"/> attached property to a given <see cref="Control"/>.
30+
/// </summary>
31+
/// <param name="control">The control.</param>
32+
/// <param name="text">The search text to set.</param>
2233
public static void SetText(Control control, string? text)
23-
{
24-
control.SetValue(TextProperty, text);
25-
}
34+
=> control.SetValue(TextProperty, text);
2635

36+
// TODO12: Control should be Interactive to match the property definition.
2737
/// <summary>
28-
/// Gets the <see cref="TextProperty"/> of a control.
38+
/// Gets the value of the <see cref="TextProperty"/> attached property from a given <see cref="Control"/>.
2939
/// </summary>
30-
/// <param name="control">The control</param>
31-
/// <returns>The property value</returns>
40+
/// <param name="control">The control.</param>
41+
/// <returns>The search text.</returns>
3242
public static string? GetText(Control control)
43+
=> control.GetValue(TextProperty);
44+
45+
/// <summary>
46+
/// Sets the value of the <see cref="TextBindingProperty"/> attached property to a given <see cref="Interactive"/>.
47+
/// </summary>
48+
/// <param name="interactive">The interactive element.</param>
49+
/// <param name="value">The search text binding to set.</param>
50+
public static void SetTextBinding(Interactive interactive, IBinding? value)
51+
=> interactive.SetValue(TextBindingProperty, value);
52+
53+
/// <summary>
54+
/// Gets the value of the <see cref="TextBindingProperty"/> attached property from a given <see cref="Interactive"/>.
55+
/// </summary>
56+
/// <param name="interactive">The interactive element.</param>
57+
/// <returns>The search text binding.</returns>
58+
[AssignBinding]
59+
public static IBinding? GetTextBinding(Interactive interactive)
60+
=> interactive.GetValue(TextBindingProperty);
61+
62+
/// <summary>
63+
/// <para>Gets the effective text of a given item.</para>
64+
/// <para>
65+
/// This method uses the first non-empty text from the following list:
66+
/// <list>
67+
/// <item><see cref="TextSearch.TextProperty"/> (if the item is a control)</item>
68+
/// <item><see cref="TextSearch.TextBindingProperty"/></item>
69+
/// <item><see cref="ItemsControl.DisplayMemberBinding"/></item>
70+
/// <item><see cref="IContentControl.Content"/>.<see cref="object.ToString"/> (if the item is a <see cref="IContentControl"/>)</item>
71+
/// <item><see cref="object.ToString"/></item>
72+
/// </list>
73+
/// </para>
74+
/// </summary>
75+
/// <param name="item">The item.</param>
76+
/// <param name="textBindingEvaluator">A <see cref="BindingEvaluator{T}"/> used to get the item's text from a binding.</param>
77+
/// <returns>The item's text.</returns>
78+
internal static string GetEffectiveText(object? item, BindingEvaluator<string?>? textBindingEvaluator)
3379
{
34-
return control.GetValue(TextProperty);
80+
if (item is null)
81+
return string.Empty;
82+
83+
string? text;
84+
85+
if (item is Interactive interactive)
86+
{
87+
text = interactive.GetValue(TextProperty);
88+
if (!string.IsNullOrEmpty(text))
89+
return text;
90+
}
91+
92+
if (textBindingEvaluator is not null)
93+
{
94+
text = textBindingEvaluator.Evaluate(item);
95+
if (!string.IsNullOrEmpty(text))
96+
return text;
97+
}
98+
99+
if (item is IContentControl contentControl)
100+
return contentControl.Content?.ToString() ?? string.Empty;
101+
102+
return item.ToString() ?? string.Empty;
35103
}
36104
}
37105
}

0 commit comments

Comments
 (0)