CommunityToolkit / CommunityToolkit/Windows

TokenizingTextBox should always create a new AutoSuggestTextBox

Open
#260 0 comments 0 reactions 0 assignees View on GitHub
bug components::controls::tokenizingtextbox
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.