From 2b606e1f0b1645c1a1b529675aabf4258f587244 Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Mon, 17 Oct 2022 11:36:05 -0700 Subject: [PATCH 1/9] Fix issues with building in debug Any CPU (again?) (Don't set Release as that effects CI) --- Windows Community Toolkit.sln | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Windows Community Toolkit.sln b/Windows Community Toolkit.sln index a0c4a505487..af1b38fc7b1 100644 --- a/Windows Community Toolkit.sln +++ b/Windows Community Toolkit.sln @@ -173,6 +173,8 @@ Global EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {719C43C6-8753-4395-ADAA-2FCC70F76BF3}.Debug|Any CPU.ActiveCfg = Debug|x86 + {719C43C6-8753-4395-ADAA-2FCC70F76BF3}.Debug|Any CPU.Build.0 = Debug|x86 + {719C43C6-8753-4395-ADAA-2FCC70F76BF3}.Debug|Any CPU.Deploy.0 = Debug|x86 {719C43C6-8753-4395-ADAA-2FCC70F76BF3}.Debug|ARM.ActiveCfg = Debug|ARM {719C43C6-8753-4395-ADAA-2FCC70F76BF3}.Debug|ARM.Build.0 = Debug|ARM {719C43C6-8753-4395-ADAA-2FCC70F76BF3}.Debug|ARM.Deploy.0 = Debug|ARM @@ -355,6 +357,7 @@ Global {BAB1CAF4-C400-4A7F-A987-C576DE63CFFD}.Release|x86.Build.0 = Release|x86 {BAB1CAF4-C400-4A7F-A987-C576DE63CFFD}.Release|x86.Deploy.0 = Release|x86 {1AE2CB5C-58A0-4F12-8E6F-2CD4AAADB34C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1AE2CB5C-58A0-4F12-8E6F-2CD4AAADB34C}.Debug|Any CPU.Build.0 = Debug|Any CPU {1AE2CB5C-58A0-4F12-8E6F-2CD4AAADB34C}.Debug|ARM.ActiveCfg = Debug|ARM {1AE2CB5C-58A0-4F12-8E6F-2CD4AAADB34C}.Debug|ARM.Build.0 = Debug|ARM {1AE2CB5C-58A0-4F12-8E6F-2CD4AAADB34C}.Debug|ARM64.ActiveCfg = Debug|ARM64 From d039aa88a8f412b77512e900548e5d4f94fd2554 Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Mon, 17 Oct 2022 11:47:52 -0700 Subject: [PATCH 2/9] Fix issue building release mode tests locally See https://devblogs.microsoft.com/nuget/announcing-nuget-6-3-transitive-dependencies-floating-versions-and-re-enabling-signed-package-verification/#new-warnings-when-duplicate-packagereference-packageversion-or-packagedownload-are-found for info about increased warning level. Also see https://github.com/dotnet/sdk/issues/24747 Not sure where the duplicate reference is coming from though... --- UITests/UITests.Tests.MSTest/UITests.Tests.MSTest.csproj | 2 +- .../UnitTests.Notifications.NetCore.csproj | 2 +- .../UnitTests.Notifications.UWP.csproj | 2 +- UnitTests/UnitTests.UWP/UnitTests.UWP.csproj | 2 +- .../UnitTests.XamlIslands.UWPApp.csproj | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/UITests/UITests.Tests.MSTest/UITests.Tests.MSTest.csproj b/UITests/UITests.Tests.MSTest/UITests.Tests.MSTest.csproj index 733e4c0e7a7..429adb007c5 100644 --- a/UITests/UITests.Tests.MSTest/UITests.Tests.MSTest.csproj +++ b/UITests/UITests.Tests.MSTest/UITests.Tests.MSTest.csproj @@ -16,7 +16,7 @@ - + diff --git a/UnitTests/UnitTests.Notifications.NetCore/UnitTests.Notifications.NetCore.csproj b/UnitTests/UnitTests.Notifications.NetCore/UnitTests.Notifications.NetCore.csproj index 129cb762893..3a218e5ac9d 100644 --- a/UnitTests/UnitTests.Notifications.NetCore/UnitTests.Notifications.NetCore.csproj +++ b/UnitTests/UnitTests.Notifications.NetCore/UnitTests.Notifications.NetCore.csproj @@ -11,7 +11,7 @@ - + diff --git a/UnitTests/UnitTests.Notifications.UWP/UnitTests.Notifications.UWP.csproj b/UnitTests/UnitTests.Notifications.UWP/UnitTests.Notifications.UWP.csproj index 35fa9601fd3..6c06c8484c3 100644 --- a/UnitTests/UnitTests.Notifications.UWP/UnitTests.Notifications.UWP.csproj +++ b/UnitTests/UnitTests.Notifications.UWP/UnitTests.Notifications.UWP.csproj @@ -122,7 +122,7 @@ 2.1.2 - + 4.5.0 diff --git a/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj b/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj index d50b438032e..9faa59332eb 100644 --- a/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj +++ b/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj @@ -143,7 +143,7 @@ 2.2.5 - + 10.0.3 diff --git a/UnitTests/UnitTests.XamlIslands.UWPApp/UnitTests.XamlIslands.UWPApp.csproj b/UnitTests/UnitTests.XamlIslands.UWPApp/UnitTests.XamlIslands.UWPApp.csproj index 966a88255d2..4d28462a7bf 100644 --- a/UnitTests/UnitTests.XamlIslands.UWPApp/UnitTests.XamlIslands.UWPApp.csproj +++ b/UnitTests/UnitTests.XamlIslands.UWPApp/UnitTests.XamlIslands.UWPApp.csproj @@ -163,7 +163,7 @@ 6.1.2 From 39925b9982fadacc7368feedeb7d8c382d0655be Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Mon, 17 Oct 2022 12:01:43 -0700 Subject: [PATCH 3/9] Switch to using VisualUITestBase for general TokenizingTextBox tests, use proper async tests Also splits out Clear test into two separate tests. --- .../Test_TokenizingTextBox_General.cs | 157 ++++++++++++------ 1 file changed, 103 insertions(+), 54 deletions(-) diff --git a/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs b/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs index 000133a3d6a..c4b2043b0bb 100644 --- a/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs +++ b/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs @@ -2,23 +2,28 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.Toolkit.Uwp; using Microsoft.Toolkit.Uwp.UI; using Microsoft.Toolkit.Uwp.UI.Controls; using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer; +using System.Threading.Tasks; using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Markup; namespace UnitTests.UWP.UI.Controls { [TestClass] - public class Test_TokenizingTextBox_General + public class Test_TokenizingTextBox_General : VisualUITestBase { [TestCategory("Test_TokenizingTextBox_General")] - [UITestMethod] - public void Test_Clear() + [TestMethod] + public async Task Test_ClearTokens() { - var treeRoot = XamlReader.Load( + await App.DispatcherQueue.EnqueueAsync(async () => + { + var treeRoot = XamlReader.Load( @"") as FrameworkElement; - Assert.IsNotNull(treeRoot, "Could not load XAML tree."); + Assert.IsNotNull(treeRoot, "Could not load XAML tree."); - var tokenBox = treeRoot.FindChild("tokenboxname") as TokenizingTextBox; + await SetTestContentAsync(treeRoot); - Assert.IsNotNull(tokenBox, "Could not find TokenizingTextBox in tree."); + var tokenBox = treeRoot.FindChild("tokenboxname") as TokenizingTextBox; - Assert.AreEqual(tokenBox.Items.Count, 1, "Token default items failed"); + Assert.IsNotNull(tokenBox, "Could not find TokenizingTextBox in tree."); + Assert.AreEqual(1, tokenBox.Items.Count, "Token default items failed"); - tokenBox.AddTokenItem("TokenItem1"); - tokenBox.AddTokenItem("TokenItem2"); - tokenBox.AddTokenItem("TokenItem3"); - tokenBox.AddTokenItem("TokenItem4"); + // Add 4 items + tokenBox.AddTokenItem("TokenItem1"); + tokenBox.AddTokenItem("TokenItem2"); + tokenBox.AddTokenItem("TokenItem3"); + tokenBox.AddTokenItem("TokenItem4"); - Assert.AreEqual(tokenBox.Items.Count, 5, "Token Add count failed"); + Assert.AreEqual(5, tokenBox.Items.Count, "Token Add count failed"); // 5th item is the textbox - // now test clear - Assert.IsTrue(tokenBox.ClearAsync().Wait(200), "Failed to wait for Clear() to finish"); + var count = 0; - Assert.AreEqual(tokenBox.Items.Count, 1, "Clear Failed to clear"); + tokenBox.TokenItemRemoving += (sender, args) => { count++; }; - // test cancelled clear - tokenBox.AddTokenItem("TokenItem1"); - tokenBox.AddTokenItem("TokenItem2"); - tokenBox.AddTokenItem("TokenItem3"); - tokenBox.AddTokenItem("TokenItem4"); + // now test clear + await tokenBox.ClearAsync(); - Assert.AreEqual(tokenBox.Items.Count, 5, "Token Add count failed"); + Assert.AreEqual(1, tokenBox.Items.Count, "Clear Failed to clear"); // Still expect textbox to remain + Assert.AreEqual(4, count, "Did not receive 4 removal events."); + }); + } - tokenBox.TokenItemRemoving += (sender, args) => { args.Cancel = true; }; + [TestCategory("Test_TokenizingTextBox_General")] + [TestMethod] + public async Task Test_ClearTokenCancel() + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + var treeRoot = XamlReader.Load( +@" - Assert.IsTrue(tokenBox.ClearAsync().Wait(200), "Failed to wait for Clear() to finish"); + + + +") as FrameworkElement; - Assert.AreEqual(tokenBox.Items.Count, 5, "Cancelled Clear Failed "); + Assert.IsNotNull(treeRoot, "Could not load XAML tree."); + + await SetTestContentAsync(treeRoot); + + var tokenBox = treeRoot.FindChild("tokenboxname") as TokenizingTextBox; + + Assert.IsNotNull(tokenBox, "Could not find TokenizingTextBox in tree."); + Assert.AreEqual(1, tokenBox.Items.Count, "Token default items failed"); + + // test cancelled clear + tokenBox.AddTokenItem("TokenItem1"); + tokenBox.AddTokenItem("TokenItem2"); + tokenBox.AddTokenItem("TokenItem3"); + tokenBox.AddTokenItem("TokenItem4"); + + Assert.AreEqual(5, tokenBox.Items.Count, "Token Add count failed"); + + tokenBox.TokenItemRemoving += (sender, args) => { args.Cancel = true; }; + + await tokenBox.ClearAsync(); + + // Should have the same number of items left + Assert.AreEqual(5, tokenBox.Items.Count, "Cancelled Clear Failed "); + + // TODO: We should have test for individual removal as well. + }); } [TestCategory("Test_TokenizingTextBox_General")] - [UITestMethod] - public void Test_MaximumTokens() + [TestMethod] + public async Task Test_MaximumTokens() { - var maxTokens = 2; + await App.DispatcherQueue.EnqueueAsync(async () => + { + var maxTokens = 2; - var treeRoot = XamlReader.Load( -$@" @@ -81,39 +127,42 @@ public void Test_MaximumTokens() ") as FrameworkElement; - Assert.IsNotNull(treeRoot, "Could not load XAML tree."); + Assert.IsNotNull(treeRoot, "Could not load XAML tree."); + + await SetTestContentAsync(treeRoot); - var tokenBox = treeRoot.FindChild("tokenboxname") as TokenizingTextBox; + var tokenBox = treeRoot.FindChild("tokenboxname") as TokenizingTextBox; - Assert.IsNotNull(tokenBox, "Could not find TokenizingTextBox in tree."); + Assert.IsNotNull(tokenBox, "Could not find TokenizingTextBox in tree."); - // Items includes the text fields as well, so we can expect at least one item to exist initially, the input box. - // Use the starting count as an offset. - var startingItemsCount = tokenBox.Items.Count; + // Items includes the text fields as well, so we can expect at least one item to exist initially, the input box. + // Use the starting count as an offset. + var startingItemsCount = tokenBox.Items.Count; - // Add two items. - tokenBox.AddTokenItem("TokenItem1"); - tokenBox.AddTokenItem("TokenItem2"); + // Add two items. + tokenBox.AddTokenItem("TokenItem1"); + tokenBox.AddTokenItem("TokenItem2"); - // Make sure we have the appropriate amount of items and that they are in the appropriate order. - Assert.AreEqual(startingItemsCount + maxTokens, tokenBox.Items.Count, "Token Add failed"); - Assert.AreEqual("TokenItem1", tokenBox.Items[0]); - Assert.AreEqual("TokenItem2", tokenBox.Items[1]); + // Make sure we have the appropriate amount of items and that they are in the appropriate order. + Assert.AreEqual(startingItemsCount + maxTokens, tokenBox.Items.Count, "Token Add failed"); + Assert.AreEqual("TokenItem1", tokenBox.Items[0]); + Assert.AreEqual("TokenItem2", tokenBox.Items[1]); - // Attempt to add an additional item, beyond the maximum. - tokenBox.AddTokenItem("TokenItem3"); + // Attempt to add an additional item, beyond the maximum. + tokenBox.AddTokenItem("TokenItem3"); - // Check that the number of items did not change, because the maximum number of items are already present. - Assert.AreEqual(startingItemsCount + maxTokens, tokenBox.Items.Count, "Token Add succeeded, where it should have failed."); - Assert.AreEqual("TokenItem1", tokenBox.Items[0]); - Assert.AreEqual("TokenItem2", tokenBox.Items[1]); + // Check that the number of items did not change, because the maximum number of items are already present. + Assert.AreEqual(startingItemsCount + maxTokens, tokenBox.Items.Count, "Token Add succeeded, where it should have failed."); + Assert.AreEqual("TokenItem1", tokenBox.Items[0]); + Assert.AreEqual("TokenItem2", tokenBox.Items[1]); - // Reduce the maximum number of tokens. - tokenBox.MaximumTokens = 1; + // Reduce the maximum number of tokens. + tokenBox.MaximumTokens = 1; - // The last token should be removed to account for the reduced maximum. - Assert.AreEqual(startingItemsCount + 1, tokenBox.Items.Count); - Assert.AreEqual("TokenItem1", tokenBox.Items[0]); + // The last token should be removed to account for the reduced maximum. + Assert.AreEqual(startingItemsCount + 1, tokenBox.Items.Count); + Assert.AreEqual("TokenItem1", tokenBox.Items[0]); + }); } } } \ No newline at end of file From 9f018956ead5e8e7b30dc5390745f79e04be7d80 Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Mon, 17 Oct 2022 14:36:42 -0700 Subject: [PATCH 4/9] Remove invalid binding - add failing tests to show problems for #4749 Think we maybe used to bind to the text directly, but there's no Text property directly on the TokenizingTextBoxItem, so this binding is meaningless. It doesn't effect the behavior of the textbox in practical usage, but somehow was doing something which was masking the problem for us to be able to detect within a test case. Removing it to reduce confusion. The text sync between the parent TokenizingTextBox collection of items (and the current edit) and the item is maintained through code-behind with text changing events. Though work is being done to resolve issues in this sync process. See issue https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/4749 --- .../TokenizingTextBoxItem.AutoSuggestBox.xaml | 1 - .../Test_TokenizingTextBox_General.cs | 79 +++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) 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 26dd6f69844..9729ba3a716 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBoxItem.AutoSuggestBox.xaml +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBoxItem.AutoSuggestBox.xaml @@ -314,7 +314,6 @@ ItemsSource="{Binding Path=Owner.SuggestedItemsSource, RelativeSource={RelativeSource Mode=TemplatedParent}}" PlaceholderText="{Binding Path=Owner.PlaceholderText, RelativeSource={RelativeSource Mode=TemplatedParent}}" Style="{StaticResource SystemAutoSuggestBoxStyle}" - Text="{Binding Text, Mode=TwoWay}" TextBoxStyle="{StaticResource TokenizingTextBoxTextBoxStyle}"/> diff --git a/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs b/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs index c4b2043b0bb..a45aec6f06d 100644 --- a/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs +++ b/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs @@ -164,5 +164,84 @@ await App.DispatcherQueue.EnqueueAsync(async () => Assert.AreEqual("TokenItem1", tokenBox.Items[0]); }); } + + [TestCategory("Test_TokenizingTextBox_General")] + [TestMethod] + public async Task Test_SetInitialText() + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + var treeRoot = XamlReader.Load( +@" + + + +") as FrameworkElement; + + Assert.IsNotNull(treeRoot, "Could not load XAML tree."); + + await SetTestContentAsync(treeRoot); + + var tokenBox = treeRoot.FindChild("tokenboxname") as TokenizingTextBox; + + Assert.IsNotNull(tokenBox, "Could not find TokenizingTextBox in tree."); + Assert.AreEqual(1, tokenBox.Items.Count, "Token default items failed"); // AutoSuggestBox + + // Test initial value of property + Assert.AreEqual("Some Text", tokenBox.Text, "Token text not equal to starting value."); + + // Reach into AutoSuggestBox's text to check it was set properly + var autoSuggestBox = tokenBox.FindDescendant(); + + Assert.IsNotNull(autoSuggestBox, "Could not find inner autosuggestbox"); + Assert.AreEqual("Some Text", autoSuggestBox.Text, "Inner text not set based on initial value of TokenizingTextBox"); + }); + } + + [TestCategory("Test_TokenizingTextBox_General")] + [TestMethod] + public async Task Test_ClearText() + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + var treeRoot = XamlReader.Load( +@" + + + +") as FrameworkElement; + + Assert.IsNotNull(treeRoot, "Could not load XAML tree."); + + await SetTestContentAsync(treeRoot); + + var tokenBox = treeRoot.FindChild("tokenboxname") as TokenizingTextBox; + + Assert.IsNotNull(tokenBox, "Could not find TokenizingTextBox in tree."); + Assert.AreEqual(1, tokenBox.Items.Count, "Token default items failed"); // AutoSuggestBox + + // TODO: When in Labs, we should inject text via keyboard here vs. setting an initial value (more independent of SetInitialText test). + + // Test initial value of property + Assert.AreEqual("Some Text", tokenBox.Text, "Token text not equal to starting value."); + + // Reach into AutoSuggestBox's text to check it was set properly + var autoSuggestBox = tokenBox.FindDescendant(); + + Assert.IsNotNull(autoSuggestBox, "Could not find inner autosuggestbox"); + Assert.AreEqual("Some Text", autoSuggestBox.Text, "Inner text not set based on initial value of TokenizingTextBox"); + + await tokenBox.ClearAsync(); + + Assert.AreEqual(string.Empty, autoSuggestBox.Text, "Inner text was not cleared."); + Assert.AreEqual(string.Empty, tokenBox.Text, "TokenizingTextBox text was not cleared."); + }); + } } } \ No newline at end of file From c0ada84646528da2d90d9b09ac0654bf53f90042 Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Mon, 17 Oct 2022 16:18:42 -0700 Subject: [PATCH 5/9] Fix issue #4749 Hook up initialization and change detection of parent Text property to update corresponding inner text of TokenizingTextBoxItem Failing tests from previous commit now pass. --- .../TokenizingTextBox.Properties.cs | 4 ++ .../TokenizingTextBox/TokenizingTextBox.cs | 3 +- .../TokenizingTextBoxItem.AutoSuggestBox.cs | 37 ++++++++++++++++++- 3 files changed, 42 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 1a5a1e36b89..00a826cf243 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs @@ -109,6 +109,10 @@ private static void TextPropertyChanged(DependencyObject d, DependencyPropertyCh if (d is TokenizingTextBox ttb && ttb._currentTextEdit != null) { ttb._currentTextEdit.Text = e.NewValue as string; + + // Notify inner container of text change, see issue #4749 + var item = ttb.ContainerFromItem(ttb._currentTextEdit) as TokenizingTextBoxItem; + item?.UpdateText(ttb._currentTextEdit.Text); } } diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs index 3ddc778355e..2d9d7f4b47d 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs @@ -89,7 +89,8 @@ private void ItemsSource_PropertyChanged(DependencyObject sender, DependencyProp } } - _currentTextEdit = _lastTextEdit = new PretokenStringContainer(true); + // Add our text box at the end of items and set it's default value to our initial text, fix for #4749 + _currentTextEdit = _lastTextEdit = new PretokenStringContainer(true) { Text = Text }; _innerItemsSource.Insert(_innerItemsSource.Count, _currentTextEdit); ItemsSource = _innerItemsSource; } 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 5a95f5b4b45..7489d3a34b8 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBoxItem.AutoSuggestBox.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBoxItem.AutoSuggestBox.cs @@ -2,6 +2,7 @@ // 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.Collections.Generic; using Windows.Foundation; using Windows.System; using Windows.UI; @@ -123,6 +124,8 @@ private void OnApplyTemplateAutoSuggestBox(AutoSuggestBox auto) iconSourceElement.SetBinding(IconSourceElement.IconSourceProperty, iconBinding); _autoSuggestBox.QueryIcon = iconSourceElement; + + _autoSuggestBox.Text = str.Text; } } } @@ -156,8 +159,40 @@ private void AutoSuggestBox_SuggestionChosen(AutoSuggestBox sender, AutoSuggestB Owner.RaiseSuggestionChosen(sender, args); } + // Called to update text by link:TokenizingTextBox.Properties.cs:TextPropertyChanged + internal void UpdateText(string text) + { + if (_autoSuggestBox != null) + { + _autoSuggestBox.Text = text; + } + else + { + void WaitForLoad(object s, RoutedEventArgs eargs) + { + if (_autoSuggestTextBox != null) + { + _autoSuggestTextBox.Text = text; + } + + AutoSuggestTextBoxLoaded -= WaitForLoad; + } + + AutoSuggestTextBoxLoaded += WaitForLoad; + } + } + private void AutoSuggestBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) { + var hasDelimiter = !string.IsNullOrEmpty(Owner.TokenDelimiter) && sender.Text?.Contains(Owner.TokenDelimiter) == true; + + // Ignore in the case we've been set from the parent and already equal the owning text, + // unless we contain our delimiter. + if (!hasDelimiter && EqualityComparer.Default.Equals(sender.Text, Owner.Text)) + { + return; + } + var t = sender.Text.Trim(); Owner.Text = sender.Text; // Update parent text property @@ -173,7 +208,7 @@ private void AutoSuggestBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTex Owner.RaiseTextChanged(sender, args); // Look for Token Delimiters to create new tokens when text changes. - if (!string.IsNullOrEmpty(Owner.TokenDelimiter) && t.Contains(Owner.TokenDelimiter)) + if (hasDelimiter) { bool lastDelimited = t[t.Length - 1] == Owner.TokenDelimiter[0]; From 6e9441635d73a19f146d3ef9e96e99afb72284db Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Mon, 17 Oct 2022 16:21:21 -0700 Subject: [PATCH 6/9] Add tests and fix issues with setting Text to delimited items. --- .../TokenizingTextBoxItem.AutoSuggestBox.cs | 2 +- .../Test_TokenizingTextBox_General.cs | 79 +++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) 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 7489d3a34b8..76a360ee582 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBoxItem.AutoSuggestBox.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBoxItem.AutoSuggestBox.cs @@ -230,7 +230,7 @@ private void AutoSuggestBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTex } else { - sender.Text = tokens[tokens.Length - 1]; + sender.Text = tokens[tokens.Length - 1].Trim(); } } } diff --git a/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs b/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs index a45aec6f06d..aa908ac5384 100644 --- a/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs +++ b/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs @@ -243,5 +243,84 @@ await App.DispatcherQueue.EnqueueAsync(async () => Assert.AreEqual(string.Empty, tokenBox.Text, "TokenizingTextBox text was not cleared."); }); } + + [TestCategory("Test_TokenizingTextBox_General")] + [TestMethod] + public async Task Test_SetInitialTextWithDelimiter() + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + var treeRoot = XamlReader.Load( +@" + + + +") as FrameworkElement; + + Assert.IsNotNull(treeRoot, "Could not load XAML tree."); + + await SetTestContentAsync(treeRoot); + + var tokenBox = treeRoot.FindChild("tokenboxname") as TokenizingTextBox; + + Assert.IsNotNull(tokenBox, "Could not find TokenizingTextBox in tree."); + Assert.AreEqual(1, tokenBox.Items.Count, "Tokens not created"); // AutoSuggestBox + + Assert.AreEqual("Token 1, Token 2, Token 3", tokenBox.Text, "Token text not equal to starting value."); + + await Task.Delay(500); // TODO: Wait for a loaded event? + + Assert.AreEqual(1 + 2, tokenBox.Items.Count, "Tokens not created"); + + // Test initial value of property + Assert.AreEqual("Token 3", tokenBox.Text, "Token text should be last value now."); + + Assert.AreEqual("Token 1", tokenBox.Items[0]); + Assert.AreEqual("Token 2", tokenBox.Items[1]); + }); + } + + [TestCategory("Test_TokenizingTextBox_General")] + [TestMethod] + public async Task Test_SetInitialTextWithDelimiterAll() + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + var treeRoot = XamlReader.Load( +@" + + + +") as FrameworkElement; + + Assert.IsNotNull(treeRoot, "Could not load XAML tree."); + + await SetTestContentAsync(treeRoot); + + var tokenBox = treeRoot.FindChild("tokenboxname") as TokenizingTextBox; + + Assert.IsNotNull(tokenBox, "Could not find TokenizingTextBox in tree."); + Assert.AreEqual(1, tokenBox.Items.Count, "Tokens not created"); // AutoSuggestBox + + Assert.AreEqual("Token 1, Token 2, Token 3, ", tokenBox.Text, "Token text not equal to starting value."); + + await Task.Delay(500); // TODO: Wait for a loaded event? + + Assert.AreEqual(1 + 3, tokenBox.Items.Count, "Tokens not created"); + + // Test initial value of property + Assert.AreEqual(string.Empty, tokenBox.Text, "Token text should be blank now."); + + Assert.AreEqual("Token 1", tokenBox.Items[0]); + Assert.AreEqual("Token 2", tokenBox.Items[1]); + Assert.AreEqual("Token 3", tokenBox.Items[2]); + }); + } } } \ No newline at end of file From ac1723d1fb94a8eb34139ca5702200a100850404 Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Mon, 17 Oct 2022 16:41:50 -0700 Subject: [PATCH 7/9] Fix manually tested issue where initial character press of overwriting token wasn't being set to box (aggressive if) Also realized we would no longer raise the text changed event in that scenario, so changed logic for text changed event. We'll need to create integration tests for these keyboard driven scenarios in the new test setup from Labs (when it's finished) in 8.0. These would make good keyboard driver tests. --- .../TokenizingTextBoxItem.AutoSuggestBox.cs | 57 +++++++++---------- 1 file changed, 28 insertions(+), 29 deletions(-) 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 76a360ee582..6b58b24bd1b 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBoxItem.AutoSuggestBox.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBoxItem.AutoSuggestBox.cs @@ -101,31 +101,36 @@ private void OnApplyTemplateAutoSuggestBox(AutoSuggestBox auto) _autoSuggestBox.LostFocus += AutoSuggestBox_LostFocus; // Setup a binding to the QueryIcon of the Parent if we're the last box. - if (Content is ITokenStringContainer str && str.IsLast) + if (Content is ITokenStringContainer str) { - // Workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/2568 - if (Owner.QueryIcon is FontIconSource fis && - fis.ReadLocalValue(FontIconSource.FontSizeProperty) == DependencyProperty.UnsetValue) - { - // This can be expensive, could we optimize? - // Also, this is changing the FontSize on the IconSource (which could be shared?) - fis.FontSize = Owner.TryFindResource("TokenizingTextBoxIconFontSize") as double? ?? 16; - } + // We need to set our initial text in all cases. + _autoSuggestBox.Text = str.Text; - var iconBinding = new Binding() + // We only set/bind some properties on the last textbox to mimic the autosuggestbox look + if (str.IsLast) { - Source = Owner, - Path = new PropertyPath(nameof(Owner.QueryIcon)), - RelativeSource = new RelativeSource() { Mode = RelativeSourceMode.TemplatedParent } - }; + // Workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/2568 + if (Owner.QueryIcon is FontIconSource fis && + fis.ReadLocalValue(FontIconSource.FontSizeProperty) == DependencyProperty.UnsetValue) + { + // This can be expensive, could we optimize? + // Also, this is changing the FontSize on the IconSource (which could be shared?) + fis.FontSize = Owner.TryFindResource("TokenizingTextBoxIconFontSize") as double? ?? 16; + } - var iconSourceElement = new IconSourceElement(); + var iconBinding = new Binding() + { + Source = Owner, + Path = new PropertyPath(nameof(Owner.QueryIcon)), + RelativeSource = new RelativeSource() { Mode = RelativeSourceMode.TemplatedParent } + }; - iconSourceElement.SetBinding(IconSourceElement.IconSourceProperty, iconBinding); + var iconSourceElement = new IconSourceElement(); - _autoSuggestBox.QueryIcon = iconSourceElement; + iconSourceElement.SetBinding(IconSourceElement.IconSourceProperty, iconBinding); - _autoSuggestBox.Text = str.Text; + _autoSuggestBox.QueryIcon = iconSourceElement; + } } } } @@ -184,19 +189,11 @@ void WaitForLoad(object s, RoutedEventArgs eargs) private void AutoSuggestBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) { - var hasDelimiter = !string.IsNullOrEmpty(Owner.TokenDelimiter) && sender.Text?.Contains(Owner.TokenDelimiter) == true; - - // Ignore in the case we've been set from the parent and already equal the owning text, - // unless we contain our delimiter. - if (!hasDelimiter && EqualityComparer.Default.Equals(sender.Text, Owner.Text)) + if (!EqualityComparer.Default.Equals(sender.Text, Owner.Text)) { - return; + Owner.Text = sender.Text; // Update parent text property, if different } - var t = sender.Text.Trim(); - - Owner.Text = sender.Text; // Update parent text property - // Override our programmatic manipulation as we're redirecting input for the user if (UseCharacterAsUser) { @@ -207,8 +204,10 @@ private void AutoSuggestBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTex Owner.RaiseTextChanged(sender, args); + var t = sender.Text?.Trim() ?? string.Empty; + // Look for Token Delimiters to create new tokens when text changes. - if (hasDelimiter) + if (!string.IsNullOrEmpty(Owner.TokenDelimiter) && t.Contains(Owner.TokenDelimiter)) { bool lastDelimited = t[t.Length - 1] == Owner.TokenDelimiter[0]; From 21e5a35a0762a704ae25bd3d3576f40bcb8763ce Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Mon, 17 Oct 2022 16:51:26 -0700 Subject: [PATCH 8/9] Add test to check changing the Text of a TokenizingTextBox --- .../Test_TokenizingTextBox_General.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs b/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs index aa908ac5384..2fef71b8b57 100644 --- a/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs +++ b/UnitTests/UnitTests.UWP/UI/Controls/Test_TokenizingTextBox_General.cs @@ -5,6 +5,7 @@ using Microsoft.Toolkit.Uwp; using Microsoft.Toolkit.Uwp.UI; using Microsoft.Toolkit.Uwp.UI.Controls; +using Microsoft.Toolkit.Uwp.UI.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer; using System.Threading.Tasks; @@ -201,6 +202,51 @@ await App.DispatcherQueue.EnqueueAsync(async () => }); } + [TestCategory("Test_TokenizingTextBox_General")] + [TestMethod] + public async Task Test_ChangeText() + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + var treeRoot = XamlReader.Load( +@" + + + +") as FrameworkElement; + + Assert.IsNotNull(treeRoot, "Could not load XAML tree."); + + await SetTestContentAsync(treeRoot); + + var tokenBox = treeRoot.FindChild("tokenboxname") as TokenizingTextBox; + + Assert.IsNotNull(tokenBox, "Could not find TokenizingTextBox in tree."); + Assert.AreEqual(1, tokenBox.Items.Count, "Token default items failed"); // AutoSuggestBox + + // Test initial value of property + Assert.AreEqual(string.Empty, tokenBox.Text, "Text should start as empty."); + + // Reach into AutoSuggestBox's text to check it was set properly + var autoSuggestBox = tokenBox.FindDescendant(); + + Assert.IsNotNull(autoSuggestBox, "Could not find inner autosuggestbox"); + Assert.AreEqual(string.Empty, autoSuggestBox.Text, "Inner text not set based on initial value of TokenizingTextBox"); + + // Change Text + tokenBox.Text = "New Text"; + + // Wait for update + await CompositionTargetHelper.ExecuteAfterCompositionRenderingAsync(() => { }); + + Assert.AreEqual("New Text", tokenBox.Text, "Text should be changed now."); + Assert.AreEqual("New Text", autoSuggestBox.Text, "Inner text not set based on value of TokenizingTextBox"); + }); + } + [TestCategory("Test_TokenizingTextBox_General")] [TestMethod] public async Task Test_ClearText() From cd1afd006212a44994d42392aa82ee4ba516b1d8 Mon Sep 17 00:00:00 2001 From: "Michael Hawker MSFT (XAML Llama)" <24302614+michael-hawker@users.noreply.github.com> Date: Tue, 18 Oct 2022 09:27:32 -0700 Subject: [PATCH 9/9] fix comment in typo based on PR --- .../TokenizingTextBox/TokenizingTextBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs index 2d9d7f4b47d..c6e55c73b23 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs @@ -89,7 +89,7 @@ private void ItemsSource_PropertyChanged(DependencyObject sender, DependencyProp } } - // Add our text box at the end of items and set it's default value to our initial text, fix for #4749 + // Add our text box at the end of items and set its default value to our initial text, fix for #4749 _currentTextEdit = _lastTextEdit = new PretokenStringContainer(true) { Text = Text }; _innerItemsSource.Insert(_innerItemsSource.Count, _currentTextEdit); ItemsSource = _innerItemsSource;