Skip to content

Commit

Permalink
fix(ItemsControl): Ensure Content property is cleared when is not own…
Browse files Browse the repository at this point in the history
… container
  • Loading branch information
jeromelaban committed Feb 28, 2023
1 parent 823a506 commit e9b3372
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<ComboBoxItem>();
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; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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__
Expand Down
12 changes: 9 additions & 3 deletions src/Uno.UI/UI/Xaml/Controls/ItemsControl/ItemsControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1107,23 +1107,25 @@ internal virtual void ContainerClearedForItem(object item, SelectorItem itemCont
/// </summary>
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);

Expand Down Expand Up @@ -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;

Expand All @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions src/Uno.UI/UI/Xaml/UIElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,16 @@ internal bool IsContainerFromTemplateRoot
get => _virtualizationInformation?.IsContainerFromTemplateRoot ?? false;
set => GetVirtualizationInformation().IsContainerFromTemplateRoot = value;
}

/// <summary>
/// Marks this as a container defined in the root of an ItemTemplate, so that it can be handled appropriately when cleared.
/// </summary>
internal bool IsOwnContainer
{
get => _virtualizationInformation?.IsOwnContainer ?? false;
set => GetVirtualizationInformation().IsOwnContainer = value;
}

#endregion

#region Clip DependencyProperty
Expand Down
44 changes: 42 additions & 2 deletions src/Uno.UI/UI/Xaml/VirtualizationInformation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,59 @@ namespace Windows.UI.Xaml
/// </summary>
internal sealed class VirtualizationInformation
{
private Flags _flags;

/// <summary>
/// Marks this as a container generated by, eg, a <see cref="Controls.Primitives.Selector"/>, rather than an element explicitly
/// defined in xaml.
/// </summary>
public bool IsGeneratedContainer { get; set; }
public bool IsGeneratedContainer
{
get => _flags.HasFlag(Flags.IsGeneratedContainer);
set => SetFlag(Flags.IsGeneratedContainer, value);
}

/// <summary>
/// Marks this as a container defined in the root of an ItemTemplate, so that it can be handled appropriately when recycled.
/// </summary>
public bool IsContainerFromTemplateRoot { get; set; }
public bool IsContainerFromTemplateRoot
{
get => _flags.HasFlag(Flags.IsContainerFromTemplateRoot);
set => SetFlag(Flags.IsContainerFromTemplateRoot, value);
}

/// <summary>
/// Marks this as an item being its own container
/// </summary>
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
}
}
}

0 comments on commit e9b3372

Please sign in to comment.