From 7bb6d06ac50f393c667715eb3393a36b282b78e4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 29 Mar 2023 15:24:29 +0200 Subject: [PATCH 1/2] Added failing test for #10626 and #10718. --- .../MenuItemTests.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs index 1049ff2678..909b65853c 100644 --- a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs @@ -3,7 +3,9 @@ using System.Collections.Generic; using System.Text; using System.Windows.Input; using Avalonia.Collections; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; using Avalonia.Platform; @@ -348,6 +350,46 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Menu_ItemTemplate_Should_Be_Applied_To_TopLevel_MenuItem_Header() + { + using var app = Application(); + + var items = new[] + { + new MenuViewModel("Foo"), + new MenuViewModel("Bar"), + }; + + var itemTemplate = new FuncDataTemplate((x, _) => + new TextBlock { Text = x.Header }); + + var menu = new Menu + { + ItemTemplate = itemTemplate, + ItemsSource = items, + }; + + var window = new Window { Content = menu }; + window.LayoutManager.ExecuteInitialLayoutPass(); + + var panel = Assert.IsType(menu.Presenter.Panel); + Assert.Equal(2, panel.Children.Count); + + for (var i = 0; i < panel.Children.Count; i++) + { + var menuItem = Assert.IsType(panel.Children[i]); + + Assert.Equal(items[i], menuItem.Header); + + var headerPresenter = Assert.IsType(menuItem.HeaderPresenter); + Assert.Same(itemTemplate, headerPresenter.ContentTemplate); + + var headerControl = Assert.IsType(headerPresenter.Child); + Assert.Equal(items[i].Header, headerControl.Text); + } + } + private IDisposable Application() { var screen = new PixelRect(new PixelPoint(), new PixelSize(100, 100)); @@ -401,5 +443,7 @@ namespace Avalonia.Controls.UnitTests public void RaiseCanExecuteChanged() => _canExecuteChanged?.Invoke(this, EventArgs.Empty); } + + private record MenuViewModel(string Header); } } From b7a249107bb9475b1aba7a942418a6fcd2d0a30e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 29 Mar 2023 15:33:43 +0200 Subject: [PATCH 2/2] Make data templates work again with MenuItem. - `MenuItem` is a `HeaderedSelectingItemsControl` not a `HeaderedItemsControl` so need to separate logic for that case when preparing items - Added `HeaderTemplate` to `HeaderedSelectingItemsControl ` - Tweaked logic for selecting header templates: parent's `ItemTemplate` should be used if set (cross-checked with WPF) - Update menu templates to bind to menu item's `HeaderTemplate` Fixes #10626 Fixes #10718 --- src/Avalonia.Controls/ItemsControl.cs | 12 +++- .../Primitives/HeaderedItemsControl.cs | 16 ++--- .../HeaderedSelectingItemsControl.cs | 63 +++++++++++++++++++ src/Avalonia.Themes.Fluent/Controls/Menu.xaml | 1 + .../Controls/MenuItem.xaml | 2 +- src/Avalonia.Themes.Simple/Controls/Menu.xaml | 3 +- .../Controls/MenuItem.xaml | 2 +- .../MenuItemTests.cs | 1 + 8 files changed, 86 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 1123f42afa..06f0d54e4c 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -460,13 +460,19 @@ namespace Avalonia.Controls ic.ItemContainerTheme = ict; } - // This condition is separate because HeaderedItemsControl needs to also run the - // ItemsControl preparation. + // 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; - hic.PrepareItemContainer(); + hic.PrepareItemContainer(this); + } + else if (container is HeaderedSelectingItemsControl hsic) + { + hsic.Header = item; + hsic.HeaderTemplate = itemTemplate; + hsic.PrepareItemContainer(this); } } diff --git a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs index 55d2ec7506..273271d2ce 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs @@ -13,7 +13,7 @@ namespace Avalonia.Controls.Primitives public class HeaderedItemsControl : ItemsControl, IContentPresenterHost { private IDisposable? _itemsBinding; - private bool _prepareItemContainerOnAttach; + private ItemsControl? _prepareItemContainerOnAttach; /// /// Defines the property. @@ -69,10 +69,10 @@ namespace Avalonia.Controls.Primitives { base.OnAttachedToLogicalTree(e); - if (_prepareItemContainerOnAttach) + if (_prepareItemContainerOnAttach is not null) { - PrepareItemContainer(); - _prepareItemContainerOnAttach = false; + PrepareItemContainer(_prepareItemContainerOnAttach); + _prepareItemContainerOnAttach = null; } } @@ -97,7 +97,7 @@ namespace Avalonia.Controls.Primitives return false; } - internal void PrepareItemContainer() + internal void PrepareItemContainer(ItemsControl parent) { _itemsBinding?.Dispose(); _itemsBinding = null; @@ -106,18 +106,18 @@ namespace Avalonia.Controls.Primitives if (item is null) { - _prepareItemContainerOnAttach = false; + _prepareItemContainerOnAttach = null; return; } - var headerTemplate = HeaderTemplate; + var headerTemplate = HeaderTemplate ?? parent.ItemTemplate; if (headerTemplate is null) { if (((ILogical)this).IsAttachedToLogicalTree) headerTemplate = this.FindDataTemplate(item); else - _prepareItemContainerOnAttach = true; + _prepareItemContainerOnAttach = parent; } if (headerTemplate is ITreeDataTemplate treeTemplate && diff --git a/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs index 49fc58c8f5..88ca1f1fe1 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs @@ -1,5 +1,8 @@ +using System; using Avalonia.Collections; using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.LogicalTree; namespace Avalonia.Controls.Primitives @@ -9,12 +12,21 @@ namespace Avalonia.Controls.Primitives /// public class HeaderedSelectingItemsControl : SelectingItemsControl, IContentPresenterHost { + private IDisposable? _itemsBinding; + private ItemsControl? _prepareItemContainerOnAttach; + /// /// Defines the property. /// public static readonly StyledProperty HeaderProperty = HeaderedContentControl.HeaderProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty HeaderTemplateProperty = + HeaderedItemsControl.HeaderTemplateProperty.AddOwner(); + /// /// Initializes static members of the class. /// @@ -32,6 +44,15 @@ namespace Avalonia.Controls.Primitives set { SetValue(HeaderProperty, value); } } + /// + /// Gets or sets the data template used to display the header content of the control. + /// + public IDataTemplate? HeaderTemplate + { + get => GetValue(HeaderTemplateProperty); + set => SetValue(HeaderTemplateProperty, value); + } + /// /// Gets the header presenter from the control's template. /// @@ -50,6 +71,17 @@ namespace Avalonia.Controls.Primitives return RegisterContentPresenter(presenter); } + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnAttachedToLogicalTree(e); + + if (_prepareItemContainerOnAttach is not null) + { + PrepareItemContainer(_prepareItemContainerOnAttach); + _prepareItemContainerOnAttach = null; + } + } + /// /// Called when an is registered with the control. /// @@ -65,6 +97,37 @@ namespace Avalonia.Controls.Primitives return false; } + internal void PrepareItemContainer(ItemsControl parent) + { + _itemsBinding?.Dispose(); + _itemsBinding = null; + + var item = Header; + + if (item is null) + { + _prepareItemContainerOnAttach = null; + return; + } + + var headerTemplate = HeaderTemplate ?? parent.ItemTemplate; + + if (headerTemplate is null) + { + if (((ILogical)this).IsAttachedToLogicalTree) + headerTemplate = this.FindDataTemplate(item); + else + _prepareItemContainerOnAttach = parent; + } + + if (headerTemplate is ITreeDataTemplate treeTemplate && + treeTemplate.Match(item) && + treeTemplate.ItemsSelector(item) is { } itemsBinding) + { + _itemsBinding = BindingOperations.Apply(this, ItemsSourceProperty, itemsBinding, null); + } + } + private void HeaderChanged(AvaloniaPropertyChangedEventArgs e) { if (e.OldValue is ILogical oldChild) diff --git a/src/Avalonia.Themes.Fluent/Controls/Menu.xaml b/src/Avalonia.Themes.Fluent/Controls/Menu.xaml index c234cfd68e..e6bbbde632 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Menu.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Menu.xaml @@ -28,6 +28,7 @@ + Content="{TemplateBinding Header}" + ContentTemplate="{TemplateBinding HeaderTemplate}"> diff --git a/src/Avalonia.Themes.Simple/Controls/MenuItem.xaml b/src/Avalonia.Themes.Simple/Controls/MenuItem.xaml index 2f09b9dc40..59ddcdf325 100644 --- a/src/Avalonia.Themes.Simple/Controls/MenuItem.xaml +++ b/src/Avalonia.Themes.Simple/Controls/MenuItem.xaml @@ -43,7 +43,7 @@ Margin="{TemplateBinding Padding}" VerticalAlignment="Center" Content="{TemplateBinding Header}" - ContentTemplate="{TemplateBinding ItemTemplate}"> + ContentTemplate="{TemplateBinding HeaderTemplate}"> diff --git a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs index 909b65853c..6fda5209ad 100644 --- a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs @@ -381,6 +381,7 @@ namespace Avalonia.Controls.UnitTests var menuItem = Assert.IsType(panel.Children[i]); Assert.Equal(items[i], menuItem.Header); + Assert.Same(itemTemplate, menuItem.HeaderTemplate); var headerPresenter = Assert.IsType(menuItem.HeaderPresenter); Assert.Same(itemTemplate, headerPresenter.ContentTemplate);