CommunityToolkit / CommunityToolkit/Windows
TokenizingTextBox should always create a new AutoSuggestTextBox
- Dominant language
- C#
- Stars
- 1.1k
- Forks
- 166
- PR merge metrics
- No merged PRs in 30d
Description
### Describe the bug
I'm using the proposed [`IsSuggestionListOpen`](https://github.com/CommunityToolkit/Windows/issues/256) .
But there is something strange happening: after creating a Token, doing a keystroke so that a new `SuggestedItems `will be shown and then pressing ESC to close that list the `AutoSuggestBox `will invoke a `TextChanged `Event out of nowhere - having the text of the last created Token.
My workaround is as follow
- move code to containerize a new `AutoSuggestTextBox `item of `TokenizingTextBox_CharacterReceived` into its own function (to avoid duplicate code)
- call that code from `TokenizingTextBox_CharacterReceived`
- in `AddTokenAsync `the currentTextBox is always removed and a new `AutoSuggestTextBox `gets created
**TokenizingTextBox.cs**
```
private void UpdateCurrentTextEditAndContainerize(string text, int index)
{
UpdateCurrentTextEdit(new PretokenStringContainer(text)); // Trim so that 'space' isn't inserted and can be used to insert a new box.
_innerItemsSource.Insert(index, _currentTextEdit);
_lastTextEdit = _currentTextEdit;
// Need to wait for containerization
#if WINAPPSDK
_ = DispatcherQueue.EnqueueAsync(
#else
_ = dispatcherQueue.EnqueueAsync(
#endif
() =>
{
if (ContainerFromIndex(index) is TokenizingTextBoxItem newContainer) // Should be our last text box
{
newContainer.UseCharacterAsUser = true; // Make sure we trigger a refresh of suggested items.
void WaitForLoad(object s, RoutedEventArgs eargs)
{
if (newContainer._autoSuggestTextBox != null)
{
newContainer._autoSuggestTextBox.SelectionStart = 1; // Set position to after our new character inserted
newContainer._autoSuggestTextBox.Focus(FocusState.Keyboard);
}
newContainer.Loaded -= WaitForLoad;
}
newContainer.AutoSuggestTextBoxLoaded += WaitForLoad;
}
}, DispatcherQueuePriority.Normal);
}
private async void TokenizingTextBox_CharacterReceived(UIElement sender, CharacterReceivedRoutedEventArgs args)
{
var container = ContainerFromItem(_currentTextEdit) as TokenizingTextBoxItem;
if (container != null && !(GetFocusedElement().Equals(container._autoSuggestTextBox) || char.IsControl(args.Character)))
{
if (SelectedItems.Count > 0)
{
var index = _innerItemsSource.IndexOf(SelectedItems.First());
await RemoveAllSelectedTokens();
// Wait for removal of old items
#if WINAPPSDK
_ = DispatcherQueue.EnqueueAsync(
#else
var dispatcherQueue = DispatcherQueue.GetForCurrentThread();
_ = dispatcherQueue.EnqueueAsync(
#endif
() =>
{
// If we're before the last textbox and it's empty, redirect focus to that one instead
if (index == _innerItemsSource.Count - 1 && string.IsNullOrWhiteSpace(_lastTextEdit.Text))
{
if (ContainerFromItem(_lastTextEdit) is TokenizingTextBoxItem lastContainer)
{
lastContainer.UseCharacterAsUser = true; // Make sure we trigger a refresh of suggested items.
_lastTextEdit.Text = string.Empty + args.Character;
UpdateCurrentTextEdit(_lastTextEdit);
lastContainer._autoSuggestTextBox.SelectionStart = 1; // Set position to after our new character inserted
lastContainer._autoSuggestTextBox.Focus(FocusState.Keyboard);
}
}
else
{
//// Otherwise, create a new textbox for this text.
///
UpdateCurrentTextEditAndContainerize((string.Empty + args.Character).Trim(), index);
}
}, DispatcherQueuePriority.Normal);
}
else
{
// 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)
{
if (ContainerFromIndex(Items.Count - 1) is TokenizingTextBoxItem last) // Should be our last text box
{
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
last._autoSuggestTextBox.Focus(FocusState.Keyboard);
}
}
}
}
}
internal async Task AddTokenAsync(object data, bool? atEnd = null)
{
if (ReadLocalValue(MaximumTokensProperty) != DependencyProperty.UnsetValue && (MaximumTokens <= 0 || MaximumTokens <= _innerItemsSource.ItemsSource.Count))
{
// No tokens for you
return;
}
if (TokenItemAdding != null)
{
TokenItemAddingEventArgs tiaea;
if (data is string str)
{
tiaea = new TokenItemAddingEventArgs(str);
}
else
{
tiaea = new TokenItemAddingEventArgs(string.Empty);
tiaea.Item = data;
}
await TokenItemAdding.InvokeAsync(this, tiaea);
if (tiaea.Cancel)
{
return;
}
if (tiaea.Item != null)
{
data = tiaea.Item; // Transformed by event implementor
}
}
// 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);
_innerItemsSource.Remove(_currentTextEdit);
}
else
{
// Otherwise, we'll insert before our current box
var edit = _currentTextEdit;
var index = _innerItemsSource.IndexOf(edit);
// Insert our new data item at the location of our textbox
_innerItemsSource.InsertAt(index, data);
// Remove our textbox
_innerItemsSource.Remove(edit);
}
UpdateCurrentTextEditAndContainerize(String.Empty, _innerItemsSource.Count);
TokenItemAdded?.Invoke(this, data);
}
```
But there is an unwanted side effect: the user can not delete the Token before the new `AutoSuggestTextBox `by pressing the Delete key directly - putting a character first and then pressing Delete twice works ....
I have no clue why this is the case as having to less understanding of the inner workings of the `TokenizingTextBox `....
### Steps to reproduce
```text
Does not happen with the current source code.
```
### Expected behavior
The `AutoSuggestBox `shouldn't fire a `TextChanged `Event.
### Screenshots
_No response_
### Code Platform
- [ ] UWP
- [ ] WinAppSDK / WinUI 3
- [ ] Web Assembly (WASM)
- [ ] Android
- [ ] iOS
- [ ] MacOS
- [ ] Linux / GTK
### Windows Build Number
- [ ] Windows 10 1809 (Build 17763)
- [ ] Windows 10 1903 (Build 18362)
- [ ] Windows 10 1909 (Build 18363)
- [ ] Windows 10 2004 (Build 19041)
- [ ] Windows 10 20H2 (Build 19042)
- [ ] Windows 10 21H1 (Build 19043)
- [ ] Windows 10 21H2 (Build 19044)
- [ ] Windows 10 22H2 (Build 19045)
- [X] Windows 11 21H2 (Build 22000)
- [ ] Other (specify)
### Other Windows Build number
_No response_
### App minimum and target SDK version
- [ ] Windows 10, version 1809 (Build 17763)
- [ ] Windows 10, version 1903 (Build 18362)
- [ ] Windows 10, version 1909 (Build 18363)
- [ ] Windows 10, version 2004 (Build 19041)
- [ ] Windows 10, version 2104 (Build 20348)
- [ ] Windows 11, version 22H2 (Build 22000)
- [ ] Other (specify)
### Other SDK version
_No response_
### Visual Studio Version
_No response_
### Visual Studio Build Number
_No response_
### Device form factor
_No response_
### Additional context
_No response_
### Help us help you
Yes, but only if others can assist.
Contributor guide
Assessment
This issue has not been assessed yet.