From 6542c27e731073d1a56d3a01ed6ad6ea690a9f2d Mon Sep 17 00:00:00 2001 From: Jerome Laban Date: Mon, 6 Mar 2023 17:42:32 -0500 Subject: [PATCH] fix(ItemsControl): ContentControl at the DataTemplate root should keep bindings --- .../Given_ItemsControl.cs | 80 +++++++++++++++++++ .../Given_TreeView.cs | 73 +++++++++++++++++ .../TreeViewTests/When_Open_Close_Twice.xaml | 21 +++++ .../When_Open_Close_Twice.xaml.cs | 24 ++++++ .../Controls/ItemsControl/ItemsControl.cs | 17 +++- 5 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_TreeView.cs create mode 100644 src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/TreeViewTests/When_Open_Close_Twice.xaml create mode 100644 src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/TreeViewTests/When_Open_Close_Twice.xaml.cs diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ItemsControl.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ItemsControl.cs index d3006dc174aa..fcd8d6298f9b 100644 --- a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ItemsControl.cs +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ItemsControl.cs @@ -480,6 +480,86 @@ public async Task When_ContentPresenter_ContainerRecycled_And_Explicit_Item() Assert.IsNotNull(third.ContentTemplateSelector); } + [TestMethod] + [RunsOnUIThread] + public async Task When_ContentPresenter_ContainerRecycled_And_ContentControl_Template() + { + var dataTemplate = (DataTemplate)XamlReader.Load( + """ + + + + """); + + var selector = new TestTemplateSelector(); + + var source = new[] + { + "First", + "Second", + }; + + var SUT = new ItemsControl() + { + ItemsSource = source, + ItemTemplate = dataTemplate + }; + + WindowHelper.WindowContent = SUT; + + await WindowHelper.WaitForIdle(); + + { + ContentPresenter first = null; + await WindowHelper.WaitFor(() => (first = SUT.ContainerFromItem(source[0]) as ContentPresenter) != null); + + ContentPresenter second = null; + await WindowHelper.WaitFor(() => (second = SUT.ContainerFromItem(source[1]) as ContentPresenter) != null); + + Assert.IsNotNull(first); + Assert.IsNotNull(second); + + Assert.IsNotNull(first.Content); + Assert.IsNotNull(second.Content); + + var firstInnerContent = first.ContentTemplateRoot as ContentControl; + Assert.AreEqual(source[0], firstInnerContent?.Content); + Assert.IsNotNull(firstInnerContent.GetBindingExpression(ContentControl.ContentProperty)); + + var secondInnerContent = second.ContentTemplateRoot as ContentControl; + Assert.AreEqual(source[1], secondInnerContent.Content); + Assert.IsNotNull(secondInnerContent.GetBindingExpression(ContentControl.ContentProperty)); + } + + SUT.ItemsSource = null; + await WindowHelper.WaitForIdle(); + + SUT.ItemsSource = source; + await WindowHelper.WaitForIdle(); + + { + ContentPresenter first = null; + await WindowHelper.WaitFor(() => (first = SUT.ContainerFromItem(source[0]) as ContentPresenter) != null); + + ContentPresenter second = null; + await WindowHelper.WaitFor(() => (second = SUT.ContainerFromItem(source[1]) as ContentPresenter) != null); + + Assert.IsNotNull(first); + Assert.IsNotNull(second); + + Assert.IsNotNull(first.Content); + Assert.IsNotNull(second.Content); + + var firstInnerContent = first.ContentTemplateRoot as ContentControl; + Assert.AreEqual(source[0], firstInnerContent?.Content); + Assert.IsNotNull(firstInnerContent.GetBindingExpression(ContentControl.ContentProperty)); + + var secondInnerContent = second.ContentTemplateRoot as ContentControl; + Assert.AreEqual(source[1], secondInnerContent.Content); + Assert.IsNotNull(secondInnerContent.GetBindingExpression(ContentControl.ContentProperty)); + } + } + [TestMethod] [RunsOnUIThread] #if __MACOS__ diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_TreeView.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_TreeView.cs new file mode 100644 index 000000000000..f2348db46f0f --- /dev/null +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_TreeView.cs @@ -0,0 +1,73 @@ +using System; +using System.Linq; +using Windows.UI; +using System.Threading.Tasks; +using Private.Infrastructure; +using Uno.UI.RuntimeTests.Helpers; +using Windows.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using TreeView = Microsoft.UI.Xaml.Controls.TreeView; +using TreeViewItem = Microsoft.UI.Xaml.Controls.TreeViewItem; +using Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Controls.TreeViewTests; +using System.Collections.Generic; + +namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Controls; + +[TestClass] +[RunsOnUIThread] +public class Given_TreeView +{ + // Test method that create a tree of three nested items from a itemssource and that will open and close a single treeview item twice and validate that the container is still containing the same property values + [TestMethod] + public async Task When_Open_Close_Twice() + { + var SUT = new When_Open_Close_Twice(); + TestServices.WindowHelper.WindowContent = SUT; + + var root = new MyNode(); + root.Name = "root"; + root.Children = new List(); + var child1 = new MyNode { Name = "Child 1" }; + var child2 = new MyNode { Name = "Child 2" }; + root.Children.Add(child1); + root.Children.Add(child2); + + SUT.myTree.ItemsSource = new[] { root }; + await TestServices.WindowHelper.WaitForIdle(); + + var rootNode = (TreeViewItem)SUT.FindName("root"); + rootNode.IsExpanded = true; + await TestServices.WindowHelper.WaitForIdle(); + + var child1Node = (TreeViewItem)SUT.FindName("Child 1"); + Assert.IsNotNull(child1Node); + Assert.AreEqual("Child 1", child1Node.Content); + + rootNode.IsExpanded = false; + await TestServices.WindowHelper.WaitForIdle(); + + rootNode.IsExpanded = true; + await TestServices.WindowHelper.WaitForIdle(); + + var child1NodeAfter = (TreeViewItem)SUT.FindName("Child 1"); + Assert.IsNotNull(child1NodeAfter); + + Assert.AreEqual("Child 1", child1NodeAfter.Content); + + var child2NodeAfter = (TreeViewItem)SUT.FindName("Child 2"); + Assert.IsNotNull(child2NodeAfter); + + Assert.AreEqual("Child 2", child2NodeAfter.Content); + } + + private class MyNode + { + public string Name { get; set; } + public List Children { get; set; } + } +} diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/TreeViewTests/When_Open_Close_Twice.xaml b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/TreeViewTests/When_Open_Close_Twice.xaml new file mode 100644 index 000000000000..10c620f6eb71 --- /dev/null +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/TreeViewTests/When_Open_Close_Twice.xaml @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/TreeViewTests/When_Open_Close_Twice.xaml.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/TreeViewTests/When_Open_Close_Twice.xaml.cs new file mode 100644 index 000000000000..9f03d997a78d --- /dev/null +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/TreeViewTests/When_Open_Close_Twice.xaml.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Navigation; + +namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Controls.TreeViewTests; + +public sealed partial class When_Open_Close_Twice : Grid +{ + public When_Open_Close_Twice() + { + this.InitializeComponent(); + } +} diff --git a/src/Uno.UI/UI/Xaml/Controls/ItemsControl/ItemsControl.cs b/src/Uno.UI/UI/Xaml/Controls/ItemsControl/ItemsControl.cs index f95c79296aa6..4ef458bc0c0d 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ItemsControl/ItemsControl.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ItemsControl/ItemsControl.cs @@ -1152,16 +1152,27 @@ internal void CleanUpContainer(global::Windows.UI.Xaml.DependencyObject element) if (!isOwnContainer) { + static void ClearPropertyWhenNoExpression(ContentControl target, DependencyProperty property) + { + // We must not clear the properties for the container if a binding expession + // is defined. This is a use-case present for TreeView, which generally uses TreeViewItem + // at the root of hierarchical templates. + if (target.GetBindingExpression(property) == null) + { + target.ClearValue(property); + } + } + // Clears value set in PrepareContainerForItemOverride - element.ClearValue(ContentControl.ContentProperty); + ClearPropertyWhenNoExpression(contentControl, ContentControl.ContentProperty); if (contentControl.ContentTemplate is { } ct && ct == ItemTemplate) { - contentControl.ClearValue(ContentControl.ContentTemplateProperty); + ClearPropertyWhenNoExpression(contentControl, ContentControl.ContentTemplateProperty); } else if (contentControl.ContentTemplateSelector is { } cts && cts == ItemTemplateSelector) { - contentControl.ClearValue(ContentControl.ContentTemplateSelectorProperty); + ClearPropertyWhenNoExpression(contentControl, ContentControl.ContentTemplateSelectorProperty); } } }