Windows icon indicating copy to clipboard operation
Windows copied to clipboard

TokenizingTextBox should always create a new AutoSuggestTextBox

Open minesworld opened this issue 2 years ago • 0 comments

Describe the bug

I'm using the proposed IsSuggestionListOpen .

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

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.

minesworld avatar Oct 17 '23 07:10 minesworld