diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 83aa88a7b6..41e9e7c111 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -67,6 +67,9 @@ namespace Avalonia.Controls public static readonly StyledProperty DisplayMemberBindingProperty = AvaloniaProperty.Register(nameof(DisplayMemberBinding)); + private static readonly AttachedProperty AppliedItemContainerTheme = + AvaloniaProperty.RegisterAttached("AppliedItemContainerTheme"); + /// /// Gets or sets the to use for binding to the display member of each item. /// @@ -663,13 +666,26 @@ namespace Avalonia.Controls internal void PrepareItemContainer(Control container, object? item, int index) { - var itemContainerTheme = ItemContainerTheme; - - if (itemContainerTheme is not null && - !container.IsSet(ThemeProperty) && - StyledElement.GetStyleKey(container) == itemContainerTheme.TargetType) + // If the container has no theme set, or we've already applied our ItemContainerTheme + // (and it hasn't changed since) then we're in control of the container's Theme and may + // need to update it. + if (!container.IsSet(ThemeProperty) || container.GetValue(AppliedItemContainerTheme) == container.Theme) { - container.Theme = itemContainerTheme; + var itemContainerTheme = ItemContainerTheme; + + if (itemContainerTheme?.TargetType?.IsAssignableFrom(GetStyleKey(container)) == true) + { + // We have an ItemContainerTheme and it matches the container. Set the Theme + // property, and mark the container as having had ItemContainerTheme applied. + container.SetCurrentValue(ThemeProperty, itemContainerTheme); + container.SetValue(AppliedItemContainerTheme, itemContainerTheme); + } + else + { + // Otherwise clear the theme and the AppliedItemContainerTheme property. + container.ClearValue(ThemeProperty); + container.ClearValue(AppliedItemContainerTheme); + } } if (item is not Control) diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 58e4ec1b75..7d3c1fe4f4 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -14,6 +14,7 @@ using Avalonia.Input; using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Markup.Xaml.Templates; +using Avalonia.Media; using Avalonia.Styling; using Avalonia.UnitTests; using Avalonia.VisualTree; @@ -128,6 +129,155 @@ namespace Avalonia.Controls.UnitTests Assert.Same(container.Theme, theme); } + [Fact] + public void Container_Should_Have_Theme_Set_To_ItemContainerTheme_With_Base_TargetType() + { + using var app = Start(); + var theme = new ControlTheme { TargetType = typeof(Control) }; + var target = CreateTarget( + itemsSource: new[] { "Foo" }, + itemContainerTheme: theme); + + var container = GetContainer(target); + + Assert.Same(container.Theme, theme); + } + + [Fact] + public void ItemContainerTheme_Can_Be_Changed() + { + using var app = Start(); + + var theme1 = new ControlTheme + { + TargetType = typeof(ContentPresenter), + Setters = { new Setter(ContentPresenter.BackgroundProperty, Brushes.Red) } + }; + + var theme2 = new ControlTheme + { + TargetType = typeof(ContentPresenter), + Setters = { new Setter(ContentPresenter.BackgroundProperty, Brushes.Green) } + }; + + var target = CreateTarget( + itemsSource: new[] { "Foo" }, + itemContainerTheme: theme1); + + var container = GetContainer(target); + + Assert.Same(container.Theme, theme1); + Assert.Equal(container.Background, Brushes.Red); + + target.ItemContainerTheme = theme2; + + container = GetContainer(target); + Assert.Same(container.Theme, theme2); + Assert.Equal(container.Background, Brushes.Green); + } + + [Fact] + public void ItemContainerTheme_Can_Be_Changed_Virtualizing() + { + using var app = Start(); + + var theme1 = new ControlTheme + { + TargetType = typeof(ContentPresenter), + Setters = { new Setter(ContentPresenter.BackgroundProperty, Brushes.Red) } + }; + + var theme2 = new ControlTheme + { + TargetType = typeof(ContentPresenter), + Setters = { new Setter(ContentPresenter.BackgroundProperty, Brushes.Green) } + }; + + var itemsPanel = new FuncTemplate(() => new VirtualizingStackPanel()); + var target = CreateTarget( + itemsSource: new[] { "Foo" }, + itemContainerTheme: theme1, + itemsPanel: itemsPanel); + + var container = GetContainer(target); + + Assert.Same(container.Theme, theme1); + Assert.Equal(container.Background, Brushes.Red); + + target.ItemContainerTheme = theme2; + Layout(target); + + container = GetContainer(target); + Assert.Same(container.Theme, theme2); + Assert.Equal(container.Background, Brushes.Green); + } + + [Fact] + public void ItemContainerTheme_Can_Be_Cleared() + { + using var app = Start(); + + var theme = new ControlTheme + { + TargetType = typeof(ContentPresenter), + Setters = { new Setter(ContentPresenter.BackgroundProperty, Brushes.Red) } + }; + + var target = CreateTarget( + itemsSource: new[] { "Foo" }, + itemContainerTheme: theme); + + var container = GetContainer(target); + + Assert.Same(container.Theme, theme); + Assert.Equal(container.Background, Brushes.Red); + + target.ItemContainerTheme = null; + + container = GetContainer(target); + Assert.Null(container.Theme); + Assert.Null(container.Background); + } + + [Fact] + public void ItemContainerTheme_Should_Not_Override_LocalValue_Theme() + { + using var app = Start(); + + var theme1 = new ControlTheme + { + TargetType = typeof(ContentPresenter), + Setters = { new Setter(ContentPresenter.BackgroundProperty, Brushes.Red) } + }; + + var theme2 = new ControlTheme + { + TargetType = typeof(Control), + Setters = { new Setter(ContentPresenter.BackgroundProperty, Brushes.Green) } + }; + + var items = new object[] + { + new ContentPresenter(), + new ContentPresenter + { + Theme = theme2 + }, + }; + + var target = CreateTarget( + itemsSource: items, + itemContainerTheme: theme1); + + Assert.Same(theme1, GetContainer(target, 0).Theme); + Assert.Same(theme2, GetContainer(target, 1).Theme); + + target.ItemContainerTheme = null; + + Assert.Null(GetContainer(target, 0).Theme); + Assert.Same(theme2, GetContainer(target, 1).Theme); + } + [Fact] public void Container_Should_Have_LogicalParent_Set_To_ItemsControl() { @@ -851,6 +1001,7 @@ namespace Avalonia.Controls.UnitTests IList? itemsSource = null, ControlTheme? itemContainerTheme = null, IDataTemplate? itemTemplate = null, + ITemplate? itemsPanel = null, IEnumerable? dataTemplates = null, bool performLayout = true) { @@ -861,6 +1012,7 @@ namespace Avalonia.Controls.UnitTests itemsSource: itemsSource, itemContainerTheme: itemContainerTheme, itemTemplate: itemTemplate, + itemsPanel: itemsPanel, dataTemplates: dataTemplates, performLayout: performLayout); }