From fafbf5b335af26012ef7d21a9c7547c7c3e89dbd Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Wed, 4 Aug 2021 15:19:31 -0700 Subject: [PATCH 01/16] Added support for TokenSelectionMode in TokenizingTextBox --- .../TokenizingTextBoxXaml.bind | 4 ++- .../TokenizingTextBox/TokenSelectionMode.cs | 28 +++++++++++++++++ .../TokenizingTextBox.Properties.cs | 31 +++++++++++++++++++ .../TokenizingTextBox/TokenizingTextBox.cs | 13 ++++++++ 4 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenSelectionMode.cs diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxXaml.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxXaml.bind index b38058a507d..672d2c72d1d 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxXaml.bind +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxXaml.bind @@ -39,7 +39,8 @@ MaxHeight="104" HorizontalAlignment="Stretch" TextMemberPath="Text" - TokenDelimiter=","> + TokenDelimiter="," + TokenSelectionMode="Single"> @@ -75,6 +76,7 @@ QueryIcon="{ui:SymbolIconSource Symbol=Find}" TextMemberPath="Text" TokenDelimiter="," + TokenSelectionMode="Multiple" IsItemClickEnabled="True" TokenItemTemplate="{StaticResource EmailTokenTemplate}"> diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenSelectionMode.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenSelectionMode.cs new file mode 100644 index 00000000000..77543fb6232 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenSelectionMode.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Indicates how tokens are selected in the . + /// + public enum TokenSelectionMode + { + /// + /// Only one token can be selected at a time. A new token should replace the active selection. + /// + Single, + + /// + /// Multiple tokens can be selected at a time. + /// + Multiple, + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs index d6099b8596e..f45c8acc049 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs @@ -157,6 +157,26 @@ private static void TextPropertyChanged(DependencyObject d, DependencyPropertyCh typeof(TokenizingTextBox), new PropertyMetadata(false)); + /// + /// Identifies the property. + /// + public static readonly DependencyProperty TokenSelectionModeProperty = DependencyProperty.Register( + nameof(TokenSelectionMode), + typeof(TokenSelectionMode), + typeof(TokenizingTextBox), + new PropertyMetadata(TokenSelectionMode.Multiple, OnTokenSelectionModeChanged)); + + private static void OnTokenSelectionModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is TokenizingTextBox ttb && e.NewValue is TokenSelectionMode newTokenSelectionMode && newTokenSelectionMode == TokenSelectionMode.Single) + { + while (ttb.Items.Count > 1) + { + ttb.Items.RemoveAt(ttb.Items.Count - 1); + } + } + } + /// /// Gets or sets the Style for the contained AutoSuggestBox template part. /// @@ -303,5 +323,16 @@ public string SelectedTokenText return PrepareSelectionForClipboard(); } } + + /// + /// Gets or sets how the control should display tokens. + /// is the default. Multiple tokens can be selected at a time. + /// indicates that only one token can be present in the control at a time. + /// + public TokenSelectionMode TokenSelectionMode + { + get => (TokenSelectionMode)GetValue(TokenSelectionModeProperty); + set => SetValue(TokenSelectionModeProperty, value); + } } } \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs index 89474a89fc6..849612b9c1d 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs @@ -448,6 +448,19 @@ internal async Task AddTokenAsync(object data, bool? atEnd = null) } } + if (TokenSelectionMode == TokenSelectionMode.Single) + { + // Remove any existing tokens. + for (var i = _innerItemsSource.Count - 1; i >= 0; --i) + { + var item = _innerItemsSource[i]; + if (item is not ITokenStringContainer) + { + _innerItemsSource.Remove(item); + } + } + } + // If we've been typing in the last box, just add this to the end of our collection if (atEnd == true || _currentTextEdit == _lastTextEdit) { From fbeb2c4cf6681a43c756b08b948abc1bfac4a195 Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Wed, 4 Aug 2021 15:49:50 -0700 Subject: [PATCH 02/16] Added TokenItemRemoved events --- .../TokenizingTextBox/TokenizingTextBox.Properties.cs | 11 +++++++++-- .../TokenizingTextBox/TokenizingTextBox.cs | 4 +++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs index f45c8acc049..e1409afa3a4 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs @@ -170,9 +170,16 @@ private static void OnTokenSelectionModeChanged(DependencyObject d, DependencyPr { if (d is TokenizingTextBox ttb && e.NewValue is TokenSelectionMode newTokenSelectionMode && newTokenSelectionMode == TokenSelectionMode.Single) { - while (ttb.Items.Count > 1) + // Start at the end, remove all but the first token. + for (var i = ttb._innerItemsSource.Count - 1; i >= 1; --i) { - ttb.Items.RemoveAt(ttb.Items.Count - 1); + 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); + } } } } diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs index 849612b9c1d..36771cc48e4 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs @@ -450,13 +450,15 @@ internal async Task AddTokenAsync(object data, bool? atEnd = null) if (TokenSelectionMode == TokenSelectionMode.Single) { - // Remove any existing tokens. + // Start at the end, remove any existing tokens. for (var i = _innerItemsSource.Count - 1; i >= 0; --i) { var item = _innerItemsSource[i]; if (item is not ITokenStringContainer) { + // Force remove the items. No warning and no option to cancel. _innerItemsSource.Remove(item); + TokenItemRemoved?.Invoke(this, item); } } } From 93ca18ad9dce07d1bb368f9dde210012074e4dbc Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Thu, 5 Aug 2021 16:32:49 -0700 Subject: [PATCH 03/16] Replaced TokenSelectionMode with MaxTokens property on TokenizingTextBox --- .../TokenizingTextBoxXaml.bind | 5 +- .../TokenizingTextBox.Properties.cs | 51 +++++++----- .../TokenizingTextBox/TokenizingTextBox.cs | 81 ++++++++++++++----- 3 files changed, 93 insertions(+), 44 deletions(-) diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxXaml.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxXaml.bind index 672d2c72d1d..46025c3b0ce 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxXaml.bind +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxXaml.bind @@ -30,7 +30,7 @@ - + MaxTokens="3"> @@ -76,7 +76,6 @@ QueryIcon="{ui:SymbolIconSource Symbol=Find}" TextMemberPath="Text" TokenDelimiter="," - TokenSelectionMode="Multiple" IsItemClickEnabled="True" TokenItemTemplate="{StaticResource EmailTokenTemplate}"> diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs index e1409afa3a4..b5d36d49517 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs @@ -158,27 +158,40 @@ private static void TextPropertyChanged(DependencyObject d, DependencyPropertyCh new PropertyMetadata(false)); /// - /// Identifies the property. + /// Identifies the property. /// - 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) { - 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; + } + } } } } @@ -332,14 +345,12 @@ public string SelectedTokenText } /// - /// Gets or sets how the control should display tokens. - /// is the default. Multiple tokens can be selected at a time. - /// 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. /// - public TokenSelectionMode TokenSelectionMode + public int? MaxTokens { - get => (TokenSelectionMode)GetValue(TokenSelectionModeProperty); - set => SetValue(TokenSelectionModeProperty, value); + get => (int?)GetValue(MaxTokensProperty); + set => SetValue(MaxTokensProperty, value); } } } \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs index 36771cc48e4..fe55e37f3f8 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs @@ -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; @@ -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 @@ -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); @@ -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 @@ -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); From 9ee8b96502629cddcf61ae19275f908641bad656 Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Thu, 5 Aug 2021 16:34:17 -0700 Subject: [PATCH 04/16] Removed TokenSelectionMode enum --- .../TokenizingTextBox/TokenSelectionMode.cs | 28 ------------------- 1 file changed, 28 deletions(-) delete mode 100644 Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenSelectionMode.cs diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenSelectionMode.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenSelectionMode.cs deleted file mode 100644 index 77543fb6232..00000000000 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenSelectionMode.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.Toolkit.Uwp.UI.Controls -{ - /// - /// Indicates how tokens are selected in the . - /// - public enum TokenSelectionMode - { - /// - /// Only one token can be selected at a time. A new token should replace the active selection. - /// - Single, - - /// - /// Multiple tokens can be selected at a time. - /// - Multiple, - } -} From b4fecf308e651e8ff37016d4b417bfa42c12400c Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Thu, 5 Aug 2021 16:49:27 -0700 Subject: [PATCH 05/16] Accounting for negative MaxToken value --- .../TokenizingTextBox/TokenizingTextBox.Properties.cs | 2 +- .../TokenizingTextBox/TokenizingTextBox.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs index b5d36d49517..1e13a2689dd 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs @@ -173,7 +173,7 @@ private static void OnMaxTokensChanged(DependencyObject d, DependencyPropertyCha var tokenCount = ttb.Items.Count; if (tokenCount > newMaxTokens) { - int tokensToRemove = newMaxTokens - tokenCount; + int tokensToRemove = tokenCount - Math.Max(newMaxTokens, 0); var tokensRemoved = 0; // Start at the end, remove any extra tokens. diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs index fe55e37f3f8..97bbcd4fc49 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs @@ -81,7 +81,7 @@ private void ItemsSource_PropertyChanged(DependencyObject sender, DependencyProp if (MaxTokens.HasValue && _innerItemsSource.ItemsSource.Count > MaxTokens) { // Reduce down to the max as necessary. - for (var i = _innerItemsSource.ItemsSource.Count; i > MaxTokens; --i) + for (var i = _innerItemsSource.ItemsSource.Count - 1; i >= Math.Max(MaxTokens.Value, 0); --i) { _innerItemsSource.Remove(_innerItemsSource[i]); } @@ -440,7 +440,7 @@ public async Task ClearAsync() internal async Task AddTokenAsync(object data, bool? atEnd = null) { - if (MaxTokens == 0) + if (MaxTokens <= 0) { // No tokens for you return; From 358ada42ebea9056dcaed0e2cf2a0fc51d97813c Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Thu, 5 Aug 2021 17:09:10 -0700 Subject: [PATCH 06/16] Added MaxTokens test --- .../Test_TokenizingTextBox_General.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs b/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs index 6ea65407d7e..39a1764bb7b 100644 --- a/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs +++ b/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs @@ -63,5 +63,44 @@ public void Test_Clear() Assert.AreEqual(tokenBox.Items.Count, 5, "Cancelled Clear Failed "); } + + [TestCategory("Test_TokenizingTextBox_General")] + [UITestMethod] + public void Test_MaxTokens() + { + var maxTokens = 2; + + var treeRoot = XamlReader.Load( +$@" + + + + +") as FrameworkElement; + + Assert.IsNotNull(treeRoot, "Could not load XAML tree."); + + var tokenBox = treeRoot.FindChild("tokenboxname") as TokenizingTextBox; + + Assert.IsNotNull(tokenBox, "Could not find TokenizingTextBox in tree."); + + var startingItemsCount = tokenBox.Items.Count; + + tokenBox.AddTokenItem("TokenItem1"); + tokenBox.AddTokenItem("TokenItem2"); + + Assert.AreEqual(startingItemsCount + maxTokens, tokenBox.Items.Count, "Token Add failed"); + Assert.AreEqual("TokenItem1", tokenBox.Items[0]); + Assert.AreEqual("TokenItem2", tokenBox.Items[1]); + + tokenBox.AddTokenItem("TokenItem3"); + + Assert.AreEqual(startingItemsCount + maxTokens, tokenBox.Items.Count, "Token Replace failed"); + Assert.AreEqual("TokenItem1", tokenBox.Items[0]); + Assert.AreEqual("TokenItem3", tokenBox.Items[1]); + } } } \ No newline at end of file From f0d8b2a6add4febe9cb51ea648bdf2f9094a34e1 Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Fri, 6 Aug 2021 10:27:36 -0700 Subject: [PATCH 07/16] Made MaxTokens non-nullable and added checks for DP.UnsetValue --- .../TokenizingTextBox/TokenizingTextBox.Properties.cs | 10 +++++----- .../TokenizingTextBox/TokenizingTextBox.cs | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs index 1e13a2689dd..21b8eb70a55 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs @@ -162,13 +162,13 @@ private static void TextPropertyChanged(DependencyObject d, DependencyPropertyCh /// public static readonly DependencyProperty MaxTokensProperty = DependencyProperty.Register( nameof(MaxTokens), - typeof(int?), + typeof(int), typeof(TokenizingTextBox), - new PropertyMetadata(null, OnMaxTokensChanged)); + new PropertyMetadata(DependencyProperty.UnsetValue, OnMaxTokensChanged)); private static void OnMaxTokensChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - if (d is TokenizingTextBox ttb && e.NewValue is int newMaxTokens) + if (d is TokenizingTextBox ttb && ttb.ReadLocalValue(MaxTokensProperty) != DependencyProperty.UnsetValue && e.NewValue is int newMaxTokens) { var tokenCount = ttb.Items.Count; if (tokenCount > newMaxTokens) @@ -347,9 +347,9 @@ public string SelectedTokenText /// /// Gets or sets the maximum number of token results allowed at a time. /// - public int? MaxTokens + public int MaxTokens { - get => (int?)GetValue(MaxTokensProperty); + get => (int)GetValue(MaxTokensProperty); set => SetValue(MaxTokensProperty, value); } } diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs index 97bbcd4fc49..9eaf54bd651 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs @@ -78,10 +78,10 @@ private void ItemsSource_PropertyChanged(DependencyObject sender, DependencyProp { _innerItemsSource = new InterspersedObservableCollection(ItemsSource); - if (MaxTokens.HasValue && _innerItemsSource.ItemsSource.Count > MaxTokens) + if (ReadLocalValue(MaxTokensProperty) != DependencyProperty.UnsetValue && _innerItemsSource.ItemsSource.Count > MaxTokens) { // Reduce down to the max as necessary. - for (var i = _innerItemsSource.ItemsSource.Count - 1; i >= Math.Max(MaxTokens.Value, 0); --i) + for (var i = _innerItemsSource.ItemsSource.Count - 1; i >= Math.Max(MaxTokens, 0); --i) { _innerItemsSource.Remove(_innerItemsSource[i]); } @@ -440,7 +440,7 @@ public async Task ClearAsync() internal async Task AddTokenAsync(object data, bool? atEnd = null) { - if (MaxTokens <= 0) + if (ReadLocalValue(MaxTokensProperty) != DependencyProperty.UnsetValue && MaxTokens <= 0) { // No tokens for you return; @@ -465,7 +465,7 @@ internal async Task AddTokenAsync(object data, bool? atEnd = null) // If we've been typing in the last box, just add this to the end of our collection if (atEnd == true || _currentTextEdit == _lastTextEdit) { - if (MaxTokens != null && _innerItemsSource.ItemsSource.Count >= MaxTokens) + if (ReadLocalValue(MaxTokensProperty) != DependencyProperty.UnsetValue && _innerItemsSource.ItemsSource.Count >= MaxTokens) { // Remove tokens from the end until below the max number. for (var i = _innerItemsSource.Count - 2; i >= 0; --i) @@ -493,7 +493,7 @@ internal async Task AddTokenAsync(object data, bool? atEnd = null) var edit = _currentTextEdit; var index = _innerItemsSource.IndexOf(edit); - if (MaxTokens != null && _innerItemsSource.ItemsSource.Count >= MaxTokens) + if (ReadLocalValue(MaxTokensProperty) != DependencyProperty.UnsetValue && _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++) From 1309ca1c60616789e06e7ba4e028a5666992ed75 Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Fri, 6 Aug 2021 13:53:42 -0700 Subject: [PATCH 08/16] Update Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs Co-authored-by: Michael Hawker MSFT (XAML Llama) <24302614+michael-hawker@users.noreply.github.com> --- .../TokenizingTextBox/TokenizingTextBox.Properties.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs index 21b8eb70a55..29521486d4e 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs @@ -164,7 +164,7 @@ private static void TextPropertyChanged(DependencyObject d, DependencyPropertyCh nameof(MaxTokens), typeof(int), typeof(TokenizingTextBox), - new PropertyMetadata(DependencyProperty.UnsetValue, OnMaxTokensChanged)); + new PropertyMetadata(null, OnMaxTokensChanged)); private static void OnMaxTokensChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { @@ -353,4 +353,4 @@ public int MaxTokens set => SetValue(MaxTokensProperty, value); } } -} \ No newline at end of file +} From c7b55c6f4011d796174faad5a6645f1f5c6e0a63 Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Mon, 9 Aug 2021 15:50:05 -0700 Subject: [PATCH 09/16] Updated OnMaxTokensChanged method and improved MaxTokens test --- .../TokenizingTextBox.Properties.cs | 25 ++++++------------- .../Test_TokenizingTextBox_General.cs | 4 +++ 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs index 29521486d4e..20af01ea948 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs @@ -170,28 +170,19 @@ private static void OnMaxTokensChanged(DependencyObject d, DependencyPropertyCha { if (d is TokenizingTextBox ttb && ttb.ReadLocalValue(MaxTokensProperty) != DependencyProperty.UnsetValue && e.NewValue is int newMaxTokens) { - var tokenCount = ttb.Items.Count; - if (tokenCount > newMaxTokens) + var tokenCount = ttb._innerItemsSource.ItemsSource.Count; + if (tokenCount > 0 && tokenCount > newMaxTokens) { int tokensToRemove = tokenCount - Math.Max(newMaxTokens, 0); - var tokensRemoved = 0; // Start at the end, remove any extra tokens. - for (var i = ttb._innerItemsSource.Count - 1; i >= 0; --i) + for (var i = tokenCount; i >= tokenCount - tokensToRemove; --i) { - 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; - } - } + var token = ttb._innerItemsSource.ItemsSource[i - 1]; + + // Force remove the items. No warning and no option to cancel. + ttb._innerItemsSource.Remove(token); + ttb.TokenItemRemoved?.Invoke(ttb, token); } } } diff --git a/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs b/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs index 39a1764bb7b..25cdefac5ad 100644 --- a/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs +++ b/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs @@ -101,6 +101,10 @@ public void Test_MaxTokens() Assert.AreEqual(startingItemsCount + maxTokens, tokenBox.Items.Count, "Token Replace failed"); Assert.AreEqual("TokenItem1", tokenBox.Items[0]); Assert.AreEqual("TokenItem3", tokenBox.Items[1]); + + tokenBox.MaxTokens = 1; + + Assert.AreEqual(1, tokenBox.Items.Count); } } } \ No newline at end of file From 07f19d57835d228248531bac1d2b2a5fce18f8d5 Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Mon, 9 Aug 2021 16:04:20 -0700 Subject: [PATCH 10/16] Bug fixes --- .../TokenizingTextBox/TokenizingTextBox.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs index 9eaf54bd651..827aa03e804 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs @@ -440,7 +440,7 @@ public async Task ClearAsync() internal async Task AddTokenAsync(object data, bool? atEnd = null) { - if (ReadLocalValue(MaxTokensProperty) != DependencyProperty.UnsetValue && MaxTokens <= 0) + if (ReadLocalValue(MaxTokensProperty) == DependencyProperty.UnsetValue || MaxTokens <= 0) { // No tokens for you return; @@ -465,7 +465,7 @@ internal async Task AddTokenAsync(object data, bool? atEnd = null) // If we've been typing in the last box, just add this to the end of our collection if (atEnd == true || _currentTextEdit == _lastTextEdit) { - if (ReadLocalValue(MaxTokensProperty) != DependencyProperty.UnsetValue && _innerItemsSource.ItemsSource.Count >= MaxTokens) + if (ReadLocalValue(MaxTokensProperty) != DependencyProperty.UnsetValue && _innerItemsSource.ItemsSource.Count > MaxTokens) { // Remove tokens from the end until below the max number. for (var i = _innerItemsSource.Count - 2; i >= 0; --i) @@ -493,7 +493,7 @@ internal async Task AddTokenAsync(object data, bool? atEnd = null) var edit = _currentTextEdit; var index = _innerItemsSource.IndexOf(edit); - if (ReadLocalValue(MaxTokensProperty) != DependencyProperty.UnsetValue && _innerItemsSource.ItemsSource.Count >= MaxTokens) + if (ReadLocalValue(MaxTokensProperty) != DependencyProperty.UnsetValue && _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++) From 01c413b62ae5e6050b5ffb880b584a9a25470eee Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Mon, 9 Aug 2021 17:03:15 -0700 Subject: [PATCH 11/16] bugfix bug fixes --- .../TokenizingTextBox/TokenizingTextBox.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs index 827aa03e804..9eaf54bd651 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs @@ -440,7 +440,7 @@ public async Task ClearAsync() internal async Task AddTokenAsync(object data, bool? atEnd = null) { - if (ReadLocalValue(MaxTokensProperty) == DependencyProperty.UnsetValue || MaxTokens <= 0) + if (ReadLocalValue(MaxTokensProperty) != DependencyProperty.UnsetValue && MaxTokens <= 0) { // No tokens for you return; @@ -465,7 +465,7 @@ internal async Task AddTokenAsync(object data, bool? atEnd = null) // If we've been typing in the last box, just add this to the end of our collection if (atEnd == true || _currentTextEdit == _lastTextEdit) { - if (ReadLocalValue(MaxTokensProperty) != DependencyProperty.UnsetValue && _innerItemsSource.ItemsSource.Count > MaxTokens) + if (ReadLocalValue(MaxTokensProperty) != DependencyProperty.UnsetValue && _innerItemsSource.ItemsSource.Count >= MaxTokens) { // Remove tokens from the end until below the max number. for (var i = _innerItemsSource.Count - 2; i >= 0; --i) @@ -493,7 +493,7 @@ internal async Task AddTokenAsync(object data, bool? atEnd = null) var edit = _currentTextEdit; var index = _innerItemsSource.IndexOf(edit); - if (ReadLocalValue(MaxTokensProperty) != DependencyProperty.UnsetValue && _innerItemsSource.ItemsSource.Count > MaxTokens) + if (ReadLocalValue(MaxTokensProperty) != DependencyProperty.UnsetValue && _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++) From a7bb51b31c2b07f725b15989f452267e8e3f1690 Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Mon, 9 Aug 2021 17:09:25 -0700 Subject: [PATCH 12/16] Added binding to MaxTokens value in sample --- .../TokenizingTextBox/TokenizingTextBoxXaml.bind | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxXaml.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxXaml.bind index 46025c3b0ce..4877768f0c4 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxXaml.bind +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxXaml.bind @@ -30,8 +30,11 @@ - + + + + + Date: Tue, 10 Aug 2021 16:34:56 -0700 Subject: [PATCH 13/16] Updated TokenizingTextBox.MaxTokens test --- .../TokenizingTextBox/TokenizingTextBox.Properties.cs | 2 +- .../UI/Controls/Test_TokenizingTextBox_General.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs index 20af01ea948..ea162758e04 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs @@ -176,7 +176,7 @@ private static void OnMaxTokensChanged(DependencyObject d, DependencyPropertyCha int tokensToRemove = tokenCount - Math.Max(newMaxTokens, 0); // Start at the end, remove any extra tokens. - for (var i = tokenCount; i >= tokenCount - tokensToRemove; --i) + for (var i = tokenCount; i > tokenCount - tokensToRemove; --i) { var token = ttb._innerItemsSource.ItemsSource[i - 1]; diff --git a/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs b/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs index 25cdefac5ad..ae55c31d885 100644 --- a/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs +++ b/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs @@ -104,7 +104,8 @@ public void Test_MaxTokens() tokenBox.MaxTokens = 1; - Assert.AreEqual(1, tokenBox.Items.Count); + Assert.AreEqual(startingItemsCount + 1, tokenBox.Items.Count); + Assert.AreEqual("TokenItem1", tokenBox.Items[0]); } } } \ No newline at end of file From 86cd4d6eb6d524583e9f254bdf56245cc1115baa Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Thu, 12 Aug 2021 15:59:02 -0700 Subject: [PATCH 14/16] Updated Max to Maximum and added token counter --- .../TokenizingTextBoxXaml.bind | 4 +- .../TokenizingTextBox.Properties.cs | 18 +++---- .../TokenizingTextBox/TokenizingTextBox.cs | 46 ++---------------- .../TokenizingTextBoxItem.AutoSuggestBox.cs | 47 +++++++++++++++++++ .../TokenizingTextBoxItem.AutoSuggestBox.xaml | 11 ++++- .../TokenizingTextBoxItem.cs | 1 - .../Test_TokenizingTextBox_General.cs | 18 +++++-- 7 files changed, 83 insertions(+), 62 deletions(-) diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxXaml.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxXaml.bind index 4877768f0c4..fca7b60e1de 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxXaml.bind +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxXaml.bind @@ -32,7 +32,7 @@ - + + MaximumTokens="3"> diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs index ea162758e04..1a5a1e36b89 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs @@ -158,17 +158,17 @@ private static void TextPropertyChanged(DependencyObject d, DependencyPropertyCh new PropertyMetadata(false)); /// - /// Identifies the property. + /// Identifies the property. /// - public static readonly DependencyProperty MaxTokensProperty = DependencyProperty.Register( - nameof(MaxTokens), + public static readonly DependencyProperty MaximumTokensProperty = DependencyProperty.Register( + nameof(MaximumTokens), typeof(int), typeof(TokenizingTextBox), - new PropertyMetadata(null, OnMaxTokensChanged)); + new PropertyMetadata(null, OnMaximumTokensChanged)); - private static void OnMaxTokensChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + private static void OnMaximumTokensChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - if (d is TokenizingTextBox ttb && ttb.ReadLocalValue(MaxTokensProperty) != DependencyProperty.UnsetValue && e.NewValue is int newMaxTokens) + if (d is TokenizingTextBox ttb && ttb.ReadLocalValue(MaximumTokensProperty) != DependencyProperty.UnsetValue && e.NewValue is int newMaxTokens) { var tokenCount = ttb._innerItemsSource.ItemsSource.Count; if (tokenCount > 0 && tokenCount > newMaxTokens) @@ -338,10 +338,10 @@ public string SelectedTokenText /// /// Gets or sets the maximum number of token results allowed at a time. /// - public int MaxTokens + public int MaximumTokens { - get => (int)GetValue(MaxTokensProperty); - set => SetValue(MaxTokensProperty, value); + get => (int)GetValue(MaximumTokensProperty); + set => SetValue(MaximumTokensProperty, value); } } } diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs index 9eaf54bd651..981c37584dd 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs @@ -78,10 +78,10 @@ private void ItemsSource_PropertyChanged(DependencyObject sender, DependencyProp { _innerItemsSource = new InterspersedObservableCollection(ItemsSource); - if (ReadLocalValue(MaxTokensProperty) != DependencyProperty.UnsetValue && _innerItemsSource.ItemsSource.Count > MaxTokens) + if (ReadLocalValue(MaximumTokensProperty) != DependencyProperty.UnsetValue && _innerItemsSource.ItemsSource.Count > MaximumTokens) { // Reduce down to the max as necessary. - for (var i = _innerItemsSource.ItemsSource.Count - 1; i >= Math.Max(MaxTokens, 0); --i) + for (var i = _innerItemsSource.ItemsSource.Count - 1; i >= Math.Max(MaximumTokens, 0); --i) { _innerItemsSource.Remove(_innerItemsSource[i]); } @@ -440,7 +440,7 @@ public async Task ClearAsync() internal async Task AddTokenAsync(object data, bool? atEnd = null) { - if (ReadLocalValue(MaxTokensProperty) != DependencyProperty.UnsetValue && MaxTokens <= 0) + if (ReadLocalValue(MaximumTokensProperty) != DependencyProperty.UnsetValue && (MaximumTokens <= 0 || MaximumTokens <= _innerItemsSource.ItemsSource.Count)) { // No tokens for you return; @@ -465,26 +465,6 @@ internal async Task AddTokenAsync(object data, bool? atEnd = null) // If we've been typing in the last box, just add this to the end of our collection if (atEnd == true || _currentTextEdit == _lastTextEdit) { - if (ReadLocalValue(MaxTokensProperty) != DependencyProperty.UnsetValue && _innerItemsSource.ItemsSource.Count >= MaxTokens) - { - // Remove tokens from the end until below the max number. - for (var i = _innerItemsSource.Count - 2; i >= 0; --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; - } - } - } - } - _innerItemsSource.InsertAt(_innerItemsSource.Count - 1, data); } else @@ -493,26 +473,6 @@ internal async Task AddTokenAsync(object data, bool? atEnd = null) var edit = _currentTextEdit; var index = _innerItemsSource.IndexOf(edit); - if (ReadLocalValue(MaxTokensProperty) != DependencyProperty.UnsetValue && _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); diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBoxItem.AutoSuggestBox.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBoxItem.AutoSuggestBox.cs index ebd64de2539..77722b7a75d 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBoxItem.AutoSuggestBox.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBoxItem.AutoSuggestBox.cs @@ -4,10 +4,12 @@ using Windows.Foundation; using Windows.System; +using Windows.UI; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Data; using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; namespace Microsoft.Toolkit.Uwp.UI.Controls { @@ -16,9 +18,11 @@ namespace Microsoft.Toolkit.Uwp.UI.Controls /// [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1124:Do not use regions", Justification = "Organization")] [TemplatePart(Name = PART_AutoSuggestBox, Type = typeof(AutoSuggestBox))] //// String case + [TemplatePart(Name = PART_TokensCounter, Type = typeof(TextBlock))] public partial class TokenizingTextBoxItem { private const string PART_AutoSuggestBox = "PART_AutoSuggestBox"; + private const string PART_TokensCounter = "PART_TokensCounter"; private AutoSuggestBox _autoSuggestBox; @@ -231,6 +235,8 @@ private void AutoSuggestBox_GotFocus(object sender, RoutedEventArgs e) #region Inner TextBox private void OnASBLoaded(object sender, RoutedEventArgs e) { + UpdateTokensCounter(this); + // Local function for Selection changed void AutoSuggestTextBox_SelectionChanged(object box, RoutedEventArgs args) { @@ -329,6 +335,47 @@ private void AutoSuggestTextBox_PreviewKeyDown(object sender, KeyRoutedEventArgs Owner.SelectAllTokensAndText(); } } + + private void UpdateTokensCounter(TokenizingTextBoxItem ttbi) + { + var maxTokensCounter = (TextBlock)_autoSuggestBox?.FindDescendant(PART_TokensCounter); + if (maxTokensCounter == null) + { + return; + } + + void OnTokenCountChanged(TokenizingTextBox ttb, object value = null) + { + var itemsSource = ttb.ItemsSource as InterspersedObservableCollection; + var currentTokens = itemsSource.ItemsSource.Count; + var maxTokens = ttb.MaximumTokens; + + maxTokensCounter.Text = $"{currentTokens}/{maxTokens}"; + maxTokensCounter.Visibility = Visibility.Visible; + + maxTokensCounter.Foreground = (currentTokens == maxTokens) + ? new SolidColorBrush(Colors.Red) + : _autoSuggestBox.Foreground; + } + + ttbi.Owner.TokenItemAdded -= OnTokenCountChanged; + ttbi.Owner.TokenItemRemoved -= OnTokenCountChanged; + + // I would have like to compared to DependencyProperty.UnsetValue, but MaximumTokensProperty value is returning 0 even though we didn't set it! + // This means that the token counter will not show up for a specified maximum value of 0. However, it's a pretty uncommon scenario to offer a picker + // with no ability to add items. If the case does arrive where the ttb should be unusable by design, developers should disable the control instead or setting the maximum to 0. + if (Content is ITokenStringContainer str && str.IsLast && ttbi?.Owner != null && (int)ttbi.Owner.GetValue(TokenizingTextBox.MaximumTokensProperty) > 0) + { + ttbi.Owner.TokenItemAdded += OnTokenCountChanged; + ttbi.Owner.TokenItemRemoved += OnTokenCountChanged; + OnTokenCountChanged(ttbi.Owner); + } + else + { + maxTokensCounter.Visibility = Visibility.Collapsed; + maxTokensCounter.Text = string.Empty; + } + } #endregion } } \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBoxItem.AutoSuggestBox.xaml b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBoxItem.AutoSuggestBox.xaml index e0b9725492f..26dd6f69844 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBoxItem.AutoSuggestBox.xaml +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBoxItem.AutoSuggestBox.xaml @@ -137,6 +137,7 @@ + @@ -176,7 +177,7 @@ ZoomMode="Disabled" /> + + +