Skip to content
This repository was archived by the owner on Feb 25, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel>
<TextBlock FontSize="32" Text="Select Actions"
<TextBlock FontSize="32" Text="Select up to 3 Actions"
Comment thread
shweaver-MSFT marked this conversation as resolved.
Outdated
Margin="0,0,0,4"/>
<controls:TokenizingTextBox
x:Name="TokenBox"
Expand All @@ -40,7 +40,7 @@
HorizontalAlignment="Stretch"
TextMemberPath="Text"
TokenDelimiter=","
TokenSelectionMode="Single">
MaxTokens="3">
Comment thread
shweaver-MSFT marked this conversation as resolved.
Outdated
<controls:TokenizingTextBox.SuggestedItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
Expand Down Expand Up @@ -76,7 +76,6 @@
QueryIcon="{ui:SymbolIconSource Symbol=Find}"
TextMemberPath="Text"
TokenDelimiter=","
TokenSelectionMode="Multiple"
IsItemClickEnabled="True"
TokenItemTemplate="{StaticResource EmailTokenTemplate}">
</controls:TokenizingTextBox>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,27 +158,40 @@ private static void TextPropertyChanged(DependencyObject d, DependencyPropertyCh
new PropertyMetadata(false));

/// <summary>
/// Identifies the <see cref="TokenSelectionMode"/> property.
/// Identifies the <see cref="MaxTokens"/> property.
/// </summary>
public static readonly DependencyProperty TokenSelectionModeProperty = DependencyProperty.Register(
nameof(TokenSelectionMode),
typeof(TokenSelectionMode),
public static readonly DependencyProperty MaxTokensProperty = DependencyProperty.Register(
nameof(MaxTokens),
typeof(int?),
typeof(TokenizingTextBox),
new PropertyMetadata(TokenSelectionMode.Multiple, OnTokenSelectionModeChanged));
new PropertyMetadata(null, OnMaxTokensChanged));

private static void OnTokenSelectionModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
private static void OnMaxTokensChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is TokenizingTextBox ttb && e.NewValue is TokenSelectionMode newTokenSelectionMode && newTokenSelectionMode == TokenSelectionMode.Single)
if (d is TokenizingTextBox ttb && e.NewValue is int newMaxTokens)
{
// Start at the end, remove all but the first token.
for (var i = ttb._innerItemsSource.Count - 1; i >= 1; --i)
var tokenCount = ttb.Items.Count;
if (tokenCount > newMaxTokens)
Comment thread
shweaver-MSFT marked this conversation as resolved.
Outdated
{
var item = ttb._innerItemsSource[i];
if (item is not ITokenStringContainer)
int tokensToRemove = newMaxTokens - tokenCount;
var tokensRemoved = 0;

// Start at the end, remove any extra tokens.
for (var i = ttb._innerItemsSource.Count - 1; i >= 0; --i)
{
// Force remove the items. No warning and no option to cancel.
ttb._innerItemsSource.Remove(item);
ttb.TokenItemRemoved?.Invoke(ttb, item);
var item = ttb._innerItemsSource[i];
if (item is not ITokenStringContainer)
{
// Force remove the items. No warning and no option to cancel.
ttb._innerItemsSource.Remove(item);
ttb.TokenItemRemoved?.Invoke(ttb, item);

tokensRemoved++;
if (tokensRemoved == tokensToRemove)
{
break;
}
}
}
}
}
Expand Down Expand Up @@ -332,14 +345,12 @@ public string SelectedTokenText
}

/// <summary>
/// Gets or sets how the control should display tokens.
/// <see cref="TokenSelectionMode.Multiple"/> is the default. Multiple tokens can be selected at a time.
/// <see cref="TokenSelectionMode.Single"/> indicates that only one token can be present in the control at a time.
/// Gets or sets the maximum number of token results allowed at a time.
/// </summary>
public TokenSelectionMode TokenSelectionMode
public int? MaxTokens
Comment thread
shweaver-MSFT marked this conversation as resolved.
Outdated
{
get => (TokenSelectionMode)GetValue(TokenSelectionModeProperty);
set => SetValue(TokenSelectionModeProperty, value);
get => (int?)GetValue(MaxTokensProperty);
set => SetValue(MaxTokensProperty, value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ private void ItemsSource_PropertyChanged(DependencyObject sender, DependencyProp
if (ItemsSource != null && ItemsSource.GetType() != typeof(InterspersedObservableCollection))
{
_innerItemsSource = new InterspersedObservableCollection(ItemsSource);

if (MaxTokens.HasValue && _innerItemsSource.ItemsSource.Count > MaxTokens)
{
// Reduce down to the max as necessary.
for (var i = _innerItemsSource.ItemsSource.Count; i > MaxTokens; --i)
{
_innerItemsSource.Remove(_innerItemsSource[i]);
}
}

_currentTextEdit = _lastTextEdit = new PretokenStringContainer(true);
_innerItemsSource.Insert(_innerItemsSource.Count, _currentTextEdit);
ItemsSource = _innerItemsSource;
Expand Down Expand Up @@ -278,18 +288,16 @@ void WaitForLoad(object s, RoutedEventArgs eargs)
}
else
{
// TODO: It looks like we're setting selection and focus together on items? Not sure if that's what we want...
// If that's the case, don't think this code will ever be called?

//// TODO: Behavior question: if no items selected (just focus) does it just go to our last active textbox?
//// Community voted that typing in the end box made sense

// If no items are selected, send input to the last active string container.
// 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.
if (_innerItemsSource[_innerItemsSource.Count - 1] is ITokenStringContainer textToken)
{
var last = ContainerFromIndex(Items.Count - 1) as TokenizingTextBoxItem; // Should be our last text box
var position = last._autoSuggestTextBox.SelectionStart;
textToken.Text = last._autoSuggestTextBox.Text.Substring(0, position) + args.Character +
last._autoSuggestTextBox.Text.Substring(position);
var text = last._autoSuggestTextBox.Text;
var selectionStart = last._autoSuggestTextBox.SelectionStart;
var position = selectionStart > text.Length ? text.Length : selectionStart;
textToken.Text = text.Substring(0, position) + args.Character +
text.Substring(position);

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

Expand Down Expand Up @@ -432,6 +440,12 @@ public async Task ClearAsync()

internal async Task AddTokenAsync(object data, bool? atEnd = null)
{
if (MaxTokens == 0)
{
// No tokens for you
return;
}

if (data is string str && TokenItemAdding != null)
{
var tiaea = new TokenItemAddingEventArgs(str);
Expand All @@ -448,24 +462,29 @@ internal async Task AddTokenAsync(object data, bool? atEnd = null)
}
}

if (TokenSelectionMode == TokenSelectionMode.Single)
// If we've been typing in the last box, just add this to the end of our collection
if (atEnd == true || _currentTextEdit == _lastTextEdit)
{
// Start at the end, remove any existing tokens.
for (var i = _innerItemsSource.Count - 1; i >= 0; --i)
if (MaxTokens != null && _innerItemsSource.ItemsSource.Count >= MaxTokens)
{
var item = _innerItemsSource[i];
if (item is not ITokenStringContainer)
// Remove tokens from the end until below the max number.
for (var i = _innerItemsSource.Count - 2; i >= 0; --i)
{
// Force remove the items. No warning and no option to cancel.
_innerItemsSource.Remove(item);
TokenItemRemoved?.Invoke(this, item);
var item = _innerItemsSource[i];
if (item is not ITokenStringContainer)
{
_innerItemsSource.Remove(item);
TokenItemRemoved?.Invoke(this, item);

// Keep going until we are below the max.
if (_innerItemsSource.ItemsSource.Count < MaxTokens)
{
break;
}
}
}
}
}

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

if (MaxTokens != null && _innerItemsSource.ItemsSource.Count >= MaxTokens)
{
// Find the next token and remove it, until below the max number of tokens.
for (var i = index; i < _innerItemsSource.Count; i++)
{
var item = _innerItemsSource[i];
if (item is not ITokenStringContainer)
{
_innerItemsSource.Remove(item);
TokenItemRemoved?.Invoke(this, item);

// Keep going until we are below the max.
if (_innerItemsSource.ItemsSource.Count < MaxTokens)
{
break;
}
}
}
}

// Insert our new data item at the location of our textbox
_innerItemsSource.InsertAt(index, data);

Expand Down