diff --git a/.ncrunch/SafeAreaDemo.Android.v3.ncrunchproject b/.ncrunch/SafeAreaDemo.Android.v3.ncrunchproject new file mode 100644 index 00000000000..319cd523cec --- /dev/null +++ b/.ncrunch/SafeAreaDemo.Android.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/SafeAreaDemo.Desktop.v3.ncrunchproject b/.ncrunch/SafeAreaDemo.Desktop.v3.ncrunchproject new file mode 100644 index 00000000000..319cd523cec --- /dev/null +++ b/.ncrunch/SafeAreaDemo.Desktop.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/SafeAreaDemo.iOS.v3.ncrunchproject b/.ncrunch/SafeAreaDemo.iOS.v3.ncrunchproject new file mode 100644 index 00000000000..319cd523cec --- /dev/null +++ b/.ncrunch/SafeAreaDemo.iOS.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/SafeAreaDemo.v3.ncrunchproject b/.ncrunch/SafeAreaDemo.v3.ncrunchproject new file mode 100644 index 00000000000..319cd523cec --- /dev/null +++ b/.ncrunch/SafeAreaDemo.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 60b0f8b193c..1c62de9bed6 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -378,48 +378,48 @@ protected internal virtual void PrepareContainerForItemOverride(Control containe if (container is HeaderedContentControl hcc) { - hcc.Content = item; + SetIfUnset(hcc, HeaderedContentControl.ContentProperty, item); if (item is IHeadered headered) - hcc.Header = headered.Header; + SetIfUnset(hcc, HeaderedContentControl.HeaderProperty, headered.Header); else if (item is not Visual) - hcc.Header = item; + SetIfUnset(hcc, HeaderedContentControl.HeaderProperty, item); if (itemTemplate is not null) - hcc.HeaderTemplate = itemTemplate; + SetIfUnset(hcc, HeaderedContentControl.HeaderTemplateProperty, itemTemplate); } else if (container is ContentControl cc) { - cc.Content = item; + SetIfUnset(cc, ContentControl.ContentProperty, item); if (itemTemplate is not null) - cc.ContentTemplate = itemTemplate; + SetIfUnset(cc, ContentControl.ContentTemplateProperty, itemTemplate); } else if (container is ContentPresenter p) { - p.Content = item; + SetIfUnset(p, ContentPresenter.ContentProperty, item); if (itemTemplate is not null) - p.ContentTemplate = itemTemplate; + SetIfUnset(p, ContentPresenter.ContentTemplateProperty, itemTemplate); } else if (container is ItemsControl ic) { if (itemTemplate is not null) - ic.ItemTemplate = itemTemplate; - if (ItemContainerTheme is { } ict && !ict.IsSet(ItemContainerThemeProperty)) - ic.ItemContainerTheme = ict; + SetIfUnset(ic, ItemTemplateProperty, itemTemplate); + if (ItemContainerTheme is { } ict) + SetIfUnset(ic, ItemContainerThemeProperty, ict); } // These conditions are separate because HeaderedItemsControl and // HeaderedSelectingItemsControl also need to run the ItemsControl preparation. if (container is HeaderedItemsControl hic) { - hic.Header = item; - hic.HeaderTemplate = itemTemplate; + SetIfUnset(hic, HeaderedItemsControl.HeaderProperty, item); + SetIfUnset(hic, HeaderedItemsControl.HeaderTemplateProperty, itemTemplate); hic.PrepareItemContainer(this); } else if (container is HeaderedSelectingItemsControl hsic) { - hsic.Header = item; - hsic.HeaderTemplate = itemTemplate; + SetIfUnset(hsic, HeaderedSelectingItemsControl.HeaderProperty, item); + SetIfUnset(hsic, HeaderedSelectingItemsControl.HeaderTemplateProperty, itemTemplate); hsic.PrepareItemContainer(this); } } @@ -458,30 +458,35 @@ protected internal virtual void ClearContainerForItemOverride(Control container) { if (container is HeaderedContentControl hcc) { - if (hcc.Content is Control) - hcc.Content = null; - if (hcc.Header is Control) - hcc.Header = null; + hcc.ClearValue(HeaderedContentControl.ContentProperty); + hcc.ClearValue(HeaderedContentControl.HeaderProperty); + hcc.ClearValue(HeaderedContentControl.HeaderTemplateProperty); } else if (container is ContentControl cc) { - if (cc.Content is Control) - cc.Content = null; + cc.ClearValue(ContentControl.ContentProperty); + cc.ClearValue(ContentControl.ContentTemplateProperty); } else if (container is ContentPresenter p) { - if (p.Content is Control) - p.Content = null; + p.ClearValue(ContentPresenter.ContentProperty); + p.ClearValue(ContentPresenter.ContentTemplateProperty); } - else if (container is HeaderedItemsControl hic) + else if (container is ItemsControl ic) + { + ic.ClearValue(ItemTemplateProperty); + ic.ClearValue(ItemContainerThemeProperty); + } + + if (container is HeaderedItemsControl hic) { - if (hic.Header is Control) - hic.Header = null; + hic.ClearValue(HeaderedItemsControl.HeaderProperty); + hic.ClearValue(HeaderedItemsControl.HeaderTemplateProperty); } else if (container is HeaderedSelectingItemsControl hsic) { - if (hsic.Header is Control) - hsic.Header = null; + hsic.ClearValue(HeaderedSelectingItemsControl.HeaderProperty); + hsic.ClearValue(HeaderedSelectingItemsControl.HeaderTemplateProperty); } // Feels like we should be clearing the HeaderedItemsControl.Items binding here, but looking at @@ -707,6 +712,12 @@ private void AddControlItemsToLogicalChildren(IEnumerable? items) LogicalChildren.AddRange(toAdd); } + private void SetIfUnset(AvaloniaObject target, StyledProperty property, T value) + { + if (!target.IsSet(property)) + target.SetCurrentValue(property, value); + } + private void RemoveControlItemsFromLogicalChildren(IEnumerable? items) { if (items is null) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 7a227a48ab1..84eed5ec82b 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -554,6 +554,36 @@ public void Initial_Binding_Of_SelectedItems_Should_Not_Cause_Write_To_SelectedI Assert.Equal(new[] { "Bar" }, target.Selection.SelectedItems); } + [Fact] + public void Content_Can_Be_Bound_In_ItemContainerTheme() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var items = new[] { new ItemViewModel("Foo"), new ItemViewModel("Bar") }; + var theme = new ControlTheme(typeof(ListBoxItem)) + { + Setters = + { + new Setter(ListBoxItem.ContentProperty, new Binding("Caption")), + } + }; + + var target = new ListBox + { + Template = ListBoxTemplate(), + ItemsSource = items, + ItemContainerTheme = theme, + }; + + Prepare(target); + + var containers = target.GetRealizedContainers().Cast().ToList(); + Assert.Equal(2, containers.Count); + Assert.Equal("Foo", containers[0].Content); + Assert.Equal("Bar", containers[1].Content); + } + } + private static FuncControlTemplate ListBoxTemplate() { return new FuncControlTemplate((parent, scope) => @@ -918,6 +948,8 @@ public void ContainerClearing_Is_Raised_When_Item_Removed() Assert.Equal(1, raised); } + private record ItemViewModel(string Caption); + private class ResettingCollection : List, INotifyCollectionChanged { public ResettingCollection(int itemCount) diff --git a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs index bd59183e926..08aedceac35 100644 --- a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs @@ -1,16 +1,16 @@ using System; +using System.Collections; using System.Collections.Generic; -using System.Text; +using System.Linq; using System.Windows.Input; -using Avalonia.Collections; using Avalonia.Controls.Presenters; -using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Controls.Utils; using Avalonia.Data; using Avalonia.Input; using Avalonia.Platform; +using Avalonia.Styling; using Avalonia.UnitTests; -using Avalonia.VisualTree; using Moq; using Xunit; @@ -36,7 +36,6 @@ public void Separator_Item_Should_Set_Focusable_False() Assert.False(target.Focusable); } - [Fact] public void MenuItem_Is_Disabled_When_Command_Is_Enabled_But_IsEnabled_Is_False() { @@ -393,6 +392,87 @@ public void Menu_ItemTemplate_Should_Be_Applied_To_TopLevel_MenuItem_Header() } } + [Fact] + public void Header_And_ItemsSource_Can_Be_Bound_In_Style() + { + using var app = Application(); + var items = new[] + { + new MenuViewModel("Foo") + { + Children = new[] + { + new MenuViewModel("FooChild"), + }, + }, + new MenuViewModel("Bar"), + }; + + var target = new Menu + { + ItemsSource = items, + Styles = + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(MenuItem.HeaderProperty, new Binding("Header")), + new Setter(MenuItem.ItemsSourceProperty, new Binding("Children")), + } + } + } + }; + + var root = new TestRoot(true, target); + root.LayoutManager.ExecuteInitialLayoutPass(); + + var children = target.GetRealizedContainers().Cast().ToList(); + Assert.Equal(2, children.Count); + Assert.Equal("Foo", children[0].Header); + Assert.Equal("Bar", children[1].Header); + Assert.Same(items[0].Children, children[0].ItemsSource); + } + + [Fact] + public void Header_And_ItemsSource_Can_Be_Bound_In_ItemContainerTheme() + { + using var app = Application(); + var items = new[] + { + new MenuViewModel("Foo") + { + Children = new[] + { + new MenuViewModel("FooChild"), + }, + }, + new MenuViewModel("Bar"), + }; + + var target = new Menu + { + ItemsSource = items, + ItemContainerTheme = new ControlTheme(typeof(MenuItem)) + { + Setters = + { + new Setter(MenuItem.HeaderProperty, new Binding("Header")), + new Setter(MenuItem.ItemsSourceProperty, new Binding("Children")), + } + } + }; + + var root = new TestRoot(true, target); + root.LayoutManager.ExecuteInitialLayoutPass(); + + var children = target.GetRealizedContainers().Cast().ToList(); + Assert.Equal(2, children.Count); + Assert.Equal("Foo", children[0].Header); + Assert.Equal("Bar", children[1].Header); + Assert.Same(items[0].Children, children[0].ItemsSource); + } + private IDisposable Application() { var screen = new PixelRect(new PixelPoint(), new PixelSize(100, 100)); @@ -447,6 +527,9 @@ public event EventHandler CanExecuteChanged public void RaiseCanExecuteChanged() => _canExecuteChanged?.Invoke(this, EventArgs.Empty); } - private record MenuViewModel(string Header); + private record MenuViewModel(string Header) + { + public IList Children { get; set;} + } } } diff --git a/tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs b/tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs index 20594a97744..eb7740aa43e 100644 --- a/tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs @@ -18,7 +18,7 @@ public NativeMenuTests(DefaultAppFixture fixture) } [PlatformFact(TestPlatforms.MacOS)] - public void View_Menu_Select_Button_Tab() + public void MacOS_View_Menu_Select_Button_Tab() { var tabs = _session.FindElementByAccessibilityId("MainTabs"); var buttonTab = tabs.FindElementByName("Button"); @@ -33,5 +33,21 @@ public void View_Menu_Select_Button_Tab() Assert.True(buttonTab.Selected); } + + [PlatformFact(TestPlatforms.Windows)] + public void Win32_View_Menu_Select_Button_Tab() + { + var tabs = _session.FindElementByAccessibilityId("MainTabs"); + var buttonTab = tabs.FindElementByName("Button"); + var viewMenu = _session.FindElementByXPath("//MenuItem[@Name='View']"); + + Assert.False(buttonTab.Selected); + + viewMenu.Click(); + var buttonMenu = viewMenu.FindElementByName("Button"); + buttonMenu.Click(); + + Assert.True(buttonTab.Selected); + } } }