diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ComboBox.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ComboBox.cs index 58b28f7502cf..121ccf70fd50 100644 --- a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ComboBox.cs +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ComboBox.cs @@ -545,6 +545,72 @@ public async Task When_Full_Collection_Reset() } #endif +#if HAS_UNO + [TestMethod] + public async Task When_Recycling_Explicit_Items() + { + var SUT = new ComboBox(); + SUT.ItemTemplate = new DataTemplate(() => + { + + var tb = new TextBlock(); + tb.SetBinding(TextBlock.TextProperty, new Windows.UI.Xaml.Data.Binding { Path = "Text" }); + tb.SetBinding(TextBlock.NameProperty, new Windows.UI.Xaml.Data.Binding { Path = "Text" }); + + return tb; + }); + + try + { + WindowHelper.WindowContent = SUT; + + var c = new MyObservableCollection(); + c.Add(new ComboBoxItem { Content = "One" }); + c.Add(new ComboBoxItem { Content = "Two" }); + c.Add(new ComboBoxItem { Content = "Three" }); + + SUT.ItemsSource = c; + + await WindowHelper.WaitForIdle(); + SUT.IsDropDownOpen = true; + + await WindowHelper.WaitForIdle(); + + Assert.AreEqual(3, SUT.Items.Count); + Assert.IsNotNull(SUT.ContainerFromItem(c[0])); + Assert.IsNotNull(SUT.ContainerFromItem(c[1])); + Assert.IsNotNull(SUT.ContainerFromItem(c[2])); + Assert.AreEqual("One", c[0].Content); + Assert.AreEqual("Two", c[1].Content); + Assert.AreEqual("Three", c[2].Content); + + await WindowHelper.WaitForIdle(); + + SUT.IsDropDownOpen = false; + + await WindowHelper.WaitForIdle(); + + // Open again to ensure that the items are recycled + SUT.IsDropDownOpen = true; + + await WindowHelper.WaitForIdle(); + + Assert.AreEqual(3, SUT.Items.Count); + Assert.IsNotNull(SUT.ContainerFromItem(c[0])); + Assert.IsNotNull(SUT.ContainerFromItem(c[1])); + Assert.IsNotNull(SUT.ContainerFromItem(c[2])); + Assert.AreEqual("One", c[0].Content); + Assert.AreEqual("Two", c[1].Content); + Assert.AreEqual("Three", c[2].Content); + + } + finally + { + SUT.IsDropDownOpen = false; + } + } +#endif + public class ItemModel { public string Text { get; set; } 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 884abbb0d9c5..63e1f4d35612 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 @@ -358,6 +358,46 @@ public async Task Check_ItemContainerStyle_ContentControl() Assert.AreEqual(third.Style, containerStyle); } + [TestMethod] + [RunsOnUIThread] + public async Task When_ContainerRecycled_And_Explicit_Item() + { + var source = new[] + { + new ContentControl { Content = "First"}, + new ContentControl { Content = "Second"}, + new ContentControl { Content = "Third"}, + }; + + var SUT = new ItemsControl() + { + ItemsSource = source, + }; + + WindowHelper.WindowContent = SUT; + + await WindowHelper.WaitForIdle(); + + ContentControl first = null; + await WindowHelper.WaitFor(() => (first = SUT.ContainerFromItem(source[0]) as ContentControl) != null); + + ContentControl second = null; + await WindowHelper.WaitFor(() => (second = SUT.ContainerFromItem(source[1]) as ContentControl) != null); + + ContentControl third = null; + await WindowHelper.WaitFor(() => (third = SUT.ContainerFromItem(source[2]) as ContentControl) != null); + + Assert.IsNotNull(first); + Assert.IsNotNull(second); + Assert.IsNotNull(third); + + SUT.ItemsSource = null; + + Assert.AreEqual("First", source[0].Content); + Assert.AreEqual("Second", source[1].Content); + Assert.AreEqual("Third", source[2].Content); + } + [TestMethod] [RunsOnUIThread] #if __MACOS__ diff --git a/src/Uno.UI/UI/Xaml/Controls/ItemsControl/ItemsControl.cs b/src/Uno.UI/UI/Xaml/Controls/ItemsControl/ItemsControl.cs index fd3f5ee69c9b..a86a49fb003e 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ItemsControl/ItemsControl.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ItemsControl/ItemsControl.cs @@ -1107,23 +1107,25 @@ internal virtual void ContainerClearedForItem(object item, SelectorItem itemCont /// internal void CleanUpContainer(global::Windows.UI.Xaml.DependencyObject element) { + // Determine the item characteristics manually, as the item + // does not exist anymore in the Items or ItemsSource. object item; + bool isOwnContainer = false; switch (element) { case ContentPresenter cp when cp is ContentPresenter: item = cp.Content; + isOwnContainer = cp.IsOwnContainer; break; case ContentControl cc when cc is ContentControl: item = cc.Content; + isOwnContainer = cc.IsOwnContainer; break; default: item = element; break; - } - var isOwnContainer = ReferenceEquals(element, item); - ClearContainerForItemOverride(element, item); ContainerClearedForItem(item, element as SelectorItem); @@ -1213,6 +1215,8 @@ void SetContent(UIElement container, DependencyProperty contentProperty) //Prepare ContentPresenter if (element is ContentPresenter containerAsContentPresenter) { + containerAsContentPresenter.IsOwnContainer = isOwnContainer; + containerAsContentPresenter.ContentTemplate = ItemTemplate; containerAsContentPresenter.ContentTemplateSelector = ItemTemplateSelector; @@ -1223,6 +1227,8 @@ void SetContent(UIElement container, DependencyProperty contentProperty) } else if (element is ContentControl containerAsContentControl) { + containerAsContentControl.IsOwnContainer = isOwnContainer; + if (!containerAsContentControl.IsContainerFromTemplateRoot) { containerAsContentControl.ContentTemplate = ItemTemplate; diff --git a/src/Uno.UI/UI/Xaml/UIElement.cs b/src/Uno.UI/UI/Xaml/UIElement.cs index 8f9f499d07a1..69c9c30361bc 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.cs @@ -284,6 +284,16 @@ internal bool IsContainerFromTemplateRoot get => _virtualizationInformation?.IsContainerFromTemplateRoot ?? false; set => GetVirtualizationInformation().IsContainerFromTemplateRoot = value; } + + /// + /// Marks this as a container defined in the root of an ItemTemplate, so that it can be handled appropriately when cleared. + /// + internal bool IsOwnContainer + { + get => _virtualizationInformation?.IsOwnContainer ?? false; + set => GetVirtualizationInformation().IsOwnContainer = value; + } + #endregion #region Clip DependencyProperty diff --git a/src/Uno.UI/UI/Xaml/VirtualizationInformation.cs b/src/Uno.UI/UI/Xaml/VirtualizationInformation.cs index 341ec6ced14d..0a1da4ce03d7 100644 --- a/src/Uno.UI/UI/Xaml/VirtualizationInformation.cs +++ b/src/Uno.UI/UI/Xaml/VirtualizationInformation.cs @@ -12,19 +12,59 @@ namespace Windows.UI.Xaml /// internal sealed class VirtualizationInformation { + private Flags _flags; + /// /// Marks this as a container generated by, eg, a , rather than an element explicitly /// defined in xaml. /// - public bool IsGeneratedContainer { get; set; } + public bool IsGeneratedContainer + { + get => _flags.HasFlag(Flags.IsGeneratedContainer); + set => SetFlag(Flags.IsGeneratedContainer, value); + } /// /// Marks this as a container defined in the root of an ItemTemplate, so that it can be handled appropriately when recycled. /// - public bool IsContainerFromTemplateRoot { get; set; } + public bool IsContainerFromTemplateRoot + { + get => _flags.HasFlag(Flags.IsContainerFromTemplateRoot); + set => SetFlag(Flags.IsContainerFromTemplateRoot, value); + } + + /// + /// Marks this as an item being its own container + /// + public bool IsOwnContainer + { + get => _flags.HasFlag(Flags.IsOwnContainer); + set => SetFlag(Flags.IsOwnContainer, value); + } public Rect Bounds { get; set; } public Size MeasureSize { get; set; } + + private void SetFlag(Flags flag, bool value) + { + if (value) + { + _flags |= flag; + } + else + { + _flags &= ~flag; + } + } + + [Flags] + private enum Flags + { + None, + IsGeneratedContainer, + IsContainerFromTemplateRoot, + IsOwnContainer + } } }