Skip to content

Commit 93ca18a

Browse files
committed
Replaced TokenSelectionMode with MaxTokens property on TokenizingTextBox
1 parent fbeb2c4 commit 93ca18a

File tree

3 files changed

+93
-44
lines changed

3 files changed

+93
-44
lines changed

Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxXaml.bind

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
<RowDefinition/>
3131
</Grid.RowDefinitions>
3232
<StackPanel>
33-
<TextBlock FontSize="32" Text="Select Actions"
33+
<TextBlock FontSize="32" Text="Select up to 3 Actions"
3434
Margin="0,0,0,4"/>
3535
<controls:TokenizingTextBox
3636
x:Name="TokenBox"
@@ -40,7 +40,7 @@
4040
HorizontalAlignment="Stretch"
4141
TextMemberPath="Text"
4242
TokenDelimiter=","
43-
TokenSelectionMode="Single">
43+
MaxTokens="3">
4444
<controls:TokenizingTextBox.SuggestedItemTemplate>
4545
<DataTemplate>
4646
<StackPanel Orientation="Horizontal">
@@ -76,7 +76,6 @@
7676
QueryIcon="{ui:SymbolIconSource Symbol=Find}"
7777
TextMemberPath="Text"
7878
TokenDelimiter=","
79-
TokenSelectionMode="Multiple"
8079
IsItemClickEnabled="True"
8180
TokenItemTemplate="{StaticResource EmailTokenTemplate}">
8281
</controls:TokenizingTextBox>

Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -158,27 +158,40 @@ private static void TextPropertyChanged(DependencyObject d, DependencyPropertyCh
158158
new PropertyMetadata(false));
159159

160160
/// <summary>
161-
/// Identifies the <see cref="TokenSelectionMode"/> property.
161+
/// Identifies the <see cref="MaxTokens"/> property.
162162
/// </summary>
163-
public static readonly DependencyProperty TokenSelectionModeProperty = DependencyProperty.Register(
164-
nameof(TokenSelectionMode),
165-
typeof(TokenSelectionMode),
163+
public static readonly DependencyProperty MaxTokensProperty = DependencyProperty.Register(
164+
nameof(MaxTokens),
165+
typeof(int?),
166166
typeof(TokenizingTextBox),
167-
new PropertyMetadata(TokenSelectionMode.Multiple, OnTokenSelectionModeChanged));
167+
new PropertyMetadata(null, OnMaxTokensChanged));
168168

169-
private static void OnTokenSelectionModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
169+
private static void OnMaxTokensChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
170170
{
171-
if (d is TokenizingTextBox ttb && e.NewValue is TokenSelectionMode newTokenSelectionMode && newTokenSelectionMode == TokenSelectionMode.Single)
171+
if (d is TokenizingTextBox ttb && e.NewValue is int newMaxTokens)
172172
{
173-
// Start at the end, remove all but the first token.
174-
for (var i = ttb._innerItemsSource.Count - 1; i >= 1; --i)
173+
var tokenCount = ttb.Items.Count;
174+
if (tokenCount > newMaxTokens)
175175
{
176-
var item = ttb._innerItemsSource[i];
177-
if (item is not ITokenStringContainer)
176+
int tokensToRemove = newMaxTokens - tokenCount;
177+
var tokensRemoved = 0;
178+
179+
// Start at the end, remove any extra tokens.
180+
for (var i = ttb._innerItemsSource.Count - 1; i >= 0; --i)
178181
{
179-
// Force remove the items. No warning and no option to cancel.
180-
ttb._innerItemsSource.Remove(item);
181-
ttb.TokenItemRemoved?.Invoke(ttb, item);
182+
var item = ttb._innerItemsSource[i];
183+
if (item is not ITokenStringContainer)
184+
{
185+
// Force remove the items. No warning and no option to cancel.
186+
ttb._innerItemsSource.Remove(item);
187+
ttb.TokenItemRemoved?.Invoke(ttb, item);
188+
189+
tokensRemoved++;
190+
if (tokensRemoved == tokensToRemove)
191+
{
192+
break;
193+
}
194+
}
182195
}
183196
}
184197
}
@@ -332,14 +345,12 @@ public string SelectedTokenText
332345
}
333346

334347
/// <summary>
335-
/// Gets or sets how the control should display tokens.
336-
/// <see cref="TokenSelectionMode.Multiple"/> is the default. Multiple tokens can be selected at a time.
337-
/// <see cref="TokenSelectionMode.Single"/> indicates that only one token can be present in the control at a time.
348+
/// Gets or sets the maximum number of token results allowed at a time.
338349
/// </summary>
339-
public TokenSelectionMode TokenSelectionMode
350+
public int? MaxTokens
340351
{
341-
get => (TokenSelectionMode)GetValue(TokenSelectionModeProperty);
342-
set => SetValue(TokenSelectionModeProperty, value);
352+
get => (int?)GetValue(MaxTokensProperty);
353+
set => SetValue(MaxTokensProperty, value);
343354
}
344355
}
345356
}

Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ private void ItemsSource_PropertyChanged(DependencyObject sender, DependencyProp
7777
if (ItemsSource != null && ItemsSource.GetType() != typeof(InterspersedObservableCollection))
7878
{
7979
_innerItemsSource = new InterspersedObservableCollection(ItemsSource);
80+
81+
if (MaxTokens.HasValue && _innerItemsSource.ItemsSource.Count > MaxTokens)
82+
{
83+
// Reduce down to the max as necessary.
84+
for (var i = _innerItemsSource.ItemsSource.Count; i > MaxTokens; --i)
85+
{
86+
_innerItemsSource.Remove(_innerItemsSource[i]);
87+
}
88+
}
89+
8090
_currentTextEdit = _lastTextEdit = new PretokenStringContainer(true);
8191
_innerItemsSource.Insert(_innerItemsSource.Count, _currentTextEdit);
8292
ItemsSource = _innerItemsSource;
@@ -278,18 +288,16 @@ void WaitForLoad(object s, RoutedEventArgs eargs)
278288
}
279289
else
280290
{
281-
// TODO: It looks like we're setting selection and focus together on items? Not sure if that's what we want...
282-
// If that's the case, don't think this code will ever be called?
283-
284-
//// TODO: Behavior question: if no items selected (just focus) does it just go to our last active textbox?
285-
//// Community voted that typing in the end box made sense
286-
291+
// If no items are selected, send input to the last active string container.
292+
// This code is only fires during an edgecase where an item is in the process of being deleted and the user inputs a character before the focus has been redirected to a string container.
287293
if (_innerItemsSource[_innerItemsSource.Count - 1] is ITokenStringContainer textToken)
288294
{
289295
var last = ContainerFromIndex(Items.Count - 1) as TokenizingTextBoxItem; // Should be our last text box
290-
var position = last._autoSuggestTextBox.SelectionStart;
291-
textToken.Text = last._autoSuggestTextBox.Text.Substring(0, position) + args.Character +
292-
last._autoSuggestTextBox.Text.Substring(position);
296+
var text = last._autoSuggestTextBox.Text;
297+
var selectionStart = last._autoSuggestTextBox.SelectionStart;
298+
var position = selectionStart > text.Length ? text.Length : selectionStart;
299+
textToken.Text = text.Substring(0, position) + args.Character +
300+
text.Substring(position);
293301

294302
last._autoSuggestTextBox.SelectionStart = position + 1; // Set position to after our new character inserted
295303

@@ -432,6 +440,12 @@ public async Task ClearAsync()
432440

433441
internal async Task AddTokenAsync(object data, bool? atEnd = null)
434442
{
443+
if (MaxTokens == 0)
444+
{
445+
// No tokens for you
446+
return;
447+
}
448+
435449
if (data is string str && TokenItemAdding != null)
436450
{
437451
var tiaea = new TokenItemAddingEventArgs(str);
@@ -448,24 +462,29 @@ internal async Task AddTokenAsync(object data, bool? atEnd = null)
448462
}
449463
}
450464

451-
if (TokenSelectionMode == TokenSelectionMode.Single)
465+
// If we've been typing in the last box, just add this to the end of our collection
466+
if (atEnd == true || _currentTextEdit == _lastTextEdit)
452467
{
453-
// Start at the end, remove any existing tokens.
454-
for (var i = _innerItemsSource.Count - 1; i >= 0; --i)
468+
if (MaxTokens != null && _innerItemsSource.ItemsSource.Count >= MaxTokens)
455469
{
456-
var item = _innerItemsSource[i];
457-
if (item is not ITokenStringContainer)
470+
// Remove tokens from the end until below the max number.
471+
for (var i = _innerItemsSource.Count - 2; i >= 0; --i)
458472
{
459-
// Force remove the items. No warning and no option to cancel.
460-
_innerItemsSource.Remove(item);
461-
TokenItemRemoved?.Invoke(this, item);
473+
var item = _innerItemsSource[i];
474+
if (item is not ITokenStringContainer)
475+
{
476+
_innerItemsSource.Remove(item);
477+
TokenItemRemoved?.Invoke(this, item);
478+
479+
// Keep going until we are below the max.
480+
if (_innerItemsSource.ItemsSource.Count < MaxTokens)
481+
{
482+
break;
483+
}
484+
}
462485
}
463486
}
464-
}
465487

466-
// If we've been typing in the last box, just add this to the end of our collection
467-
if (atEnd == true || _currentTextEdit == _lastTextEdit)
468-
{
469488
_innerItemsSource.InsertAt(_innerItemsSource.Count - 1, data);
470489
}
471490
else
@@ -474,6 +493,26 @@ internal async Task AddTokenAsync(object data, bool? atEnd = null)
474493
var edit = _currentTextEdit;
475494
var index = _innerItemsSource.IndexOf(edit);
476495

496+
if (MaxTokens != null && _innerItemsSource.ItemsSource.Count >= MaxTokens)
497+
{
498+
// Find the next token and remove it, until below the max number of tokens.
499+
for (var i = index; i < _innerItemsSource.Count; i++)
500+
{
501+
var item = _innerItemsSource[i];
502+
if (item is not ITokenStringContainer)
503+
{
504+
_innerItemsSource.Remove(item);
505+
TokenItemRemoved?.Invoke(this, item);
506+
507+
// Keep going until we are below the max.
508+
if (_innerItemsSource.ItemsSource.Count < MaxTokens)
509+
{
510+
break;
511+
}
512+
}
513+
}
514+
}
515+
477516
// Insert our new data item at the location of our textbox
478517
_innerItemsSource.InsertAt(index, data);
479518

0 commit comments

Comments
 (0)