Переглянути джерело

Merge pull request #10836 from AvaloniaUI/fixes/10626-menuitem-headertemplate

Make data templates work again with MenuItem.
Max Katz 2 роки тому
батько
коміт
660033c770

+ 9 - 3
src/Avalonia.Controls/ItemsControl.cs

@@ -460,13 +460,19 @@ namespace Avalonia.Controls
                     ic.ItemContainerTheme = ict;
                     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)
             if (container is HeaderedItemsControl hic)
             {
             {
                 hic.Header = item;
                 hic.Header = item;
                 hic.HeaderTemplate = itemTemplate;
                 hic.HeaderTemplate = itemTemplate;
-                hic.PrepareItemContainer();
+                hic.PrepareItemContainer(this);
+            }
+            else if (container is HeaderedSelectingItemsControl hsic)
+            {
+                hsic.Header = item;
+                hsic.HeaderTemplate = itemTemplate;
+                hsic.PrepareItemContainer(this);
             }
             }
         }
         }
 
 

+ 8 - 8
src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs

@@ -13,7 +13,7 @@ namespace Avalonia.Controls.Primitives
     public class HeaderedItemsControl : ItemsControl, IContentPresenterHost
     public class HeaderedItemsControl : ItemsControl, IContentPresenterHost
     {
     {
         private IDisposable? _itemsBinding;
         private IDisposable? _itemsBinding;
-        private bool _prepareItemContainerOnAttach;
+        private ItemsControl? _prepareItemContainerOnAttach;
 
 
         /// <summary>
         /// <summary>
         /// Defines the <see cref="Header"/> property.
         /// Defines the <see cref="Header"/> property.
@@ -69,10 +69,10 @@ namespace Avalonia.Controls.Primitives
         {
         {
             base.OnAttachedToLogicalTree(e);
             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;
             return false;
         }
         }
 
 
-        internal void PrepareItemContainer()
+        internal void PrepareItemContainer(ItemsControl parent)
         {
         {
             _itemsBinding?.Dispose();
             _itemsBinding?.Dispose();
             _itemsBinding = null;
             _itemsBinding = null;
@@ -106,18 +106,18 @@ namespace Avalonia.Controls.Primitives
 
 
             if (item is null)
             if (item is null)
             {
             {
-                _prepareItemContainerOnAttach = false;
+                _prepareItemContainerOnAttach = null;
                 return;
                 return;
             }
             }
 
 
-            var headerTemplate = HeaderTemplate;
+            var headerTemplate = HeaderTemplate ?? parent.ItemTemplate;
 
 
             if (headerTemplate is null)
             if (headerTemplate is null)
             {
             {
                 if (((ILogical)this).IsAttachedToLogicalTree)
                 if (((ILogical)this).IsAttachedToLogicalTree)
                     headerTemplate = this.FindDataTemplate(item);
                     headerTemplate = this.FindDataTemplate(item);
                 else
                 else
-                    _prepareItemContainerOnAttach = true;
+                    _prepareItemContainerOnAttach = parent;
             }
             }
 
 
             if (headerTemplate is ITreeDataTemplate treeTemplate &&
             if (headerTemplate is ITreeDataTemplate treeTemplate &&

+ 63 - 0
src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs

@@ -1,5 +1,8 @@
+using System;
 using Avalonia.Collections;
 using Avalonia.Collections;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Templates;
+using Avalonia.Data;
 using Avalonia.LogicalTree;
 using Avalonia.LogicalTree;
 
 
 namespace Avalonia.Controls.Primitives
 namespace Avalonia.Controls.Primitives
@@ -9,12 +12,21 @@ namespace Avalonia.Controls.Primitives
     /// </summary>
     /// </summary>
     public class HeaderedSelectingItemsControl : SelectingItemsControl, IContentPresenterHost
     public class HeaderedSelectingItemsControl : SelectingItemsControl, IContentPresenterHost
     {
     {
+        private IDisposable? _itemsBinding;
+        private ItemsControl? _prepareItemContainerOnAttach;
+
         /// <summary>
         /// <summary>
         /// Defines the <see cref="Header"/> property.
         /// Defines the <see cref="Header"/> property.
         /// </summary>
         /// </summary>
         public static readonly StyledProperty<object?> HeaderProperty =
         public static readonly StyledProperty<object?> HeaderProperty =
             HeaderedContentControl.HeaderProperty.AddOwner<HeaderedSelectingItemsControl>();
             HeaderedContentControl.HeaderProperty.AddOwner<HeaderedSelectingItemsControl>();
 
 
+        /// <summary>
+        /// Defines the <see cref="HeaderTemplate"/> property.
+        /// </summary>
+        public static readonly StyledProperty<IDataTemplate?> HeaderTemplateProperty =
+            HeaderedItemsControl.HeaderTemplateProperty.AddOwner<HeaderedSelectingItemsControl>();
+
         /// <summary>
         /// <summary>
         /// Initializes static members of the <see cref="ContentControl"/> class.
         /// Initializes static members of the <see cref="ContentControl"/> class.
         /// </summary>
         /// </summary>
@@ -32,6 +44,15 @@ namespace Avalonia.Controls.Primitives
             set { SetValue(HeaderProperty, value); }
             set { SetValue(HeaderProperty, value); }
         }
         }
 
 
+        /// <summary>
+        /// Gets or sets the data template used to display the header content of the control.
+        /// </summary>
+        public IDataTemplate? HeaderTemplate
+        {
+            get => GetValue(HeaderTemplateProperty);
+            set => SetValue(HeaderTemplateProperty, value);
+        }
+
         /// <summary>
         /// <summary>
         /// Gets the header presenter from the control's template.
         /// Gets the header presenter from the control's template.
         /// </summary>
         /// </summary>
@@ -50,6 +71,17 @@ namespace Avalonia.Controls.Primitives
             return RegisterContentPresenter(presenter);
             return RegisterContentPresenter(presenter);
         }
         }
 
 
+        protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
+        {
+            base.OnAttachedToLogicalTree(e);
+
+            if (_prepareItemContainerOnAttach is not null)
+            {
+                PrepareItemContainer(_prepareItemContainerOnAttach);
+                _prepareItemContainerOnAttach = null;
+            }
+        }
+
         /// <summary>
         /// <summary>
         /// Called when an <see cref="IContentPresenter"/> is registered with the control.
         /// Called when an <see cref="IContentPresenter"/> is registered with the control.
         /// </summary>
         /// </summary>
@@ -65,6 +97,37 @@ namespace Avalonia.Controls.Primitives
             return false;
             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)
         private void HeaderChanged(AvaloniaPropertyChangedEventArgs e)
         {
         {
             if (e.OldValue is ILogical oldChild)
             if (e.OldValue is ILogical oldChild)

+ 1 - 0
src/Avalonia.Themes.Fluent/Controls/Menu.xaml

@@ -28,6 +28,7 @@
           <Panel>
           <Panel>
             <ContentPresenter Name="PART_HeaderPresenter"
             <ContentPresenter Name="PART_HeaderPresenter"
                               Content="{TemplateBinding Header}"
                               Content="{TemplateBinding Header}"
+                              ContentTemplate="{TemplateBinding HeaderTemplate}"
                               VerticalAlignment="Center"
                               VerticalAlignment="Center"
                               HorizontalAlignment="Stretch"
                               HorizontalAlignment="Stretch"
                               RecognizesAccessKey="True"
                               RecognizesAccessKey="True"

+ 1 - 1
src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml

@@ -92,7 +92,7 @@
 
 
               <ContentPresenter Name="PART_HeaderPresenter"
               <ContentPresenter Name="PART_HeaderPresenter"
                                 Content="{TemplateBinding Header}"
                                 Content="{TemplateBinding Header}"
-                                ContentTemplate="{TemplateBinding ItemTemplate}"
+                                ContentTemplate="{TemplateBinding HeaderTemplate}"
                                 VerticalAlignment="Center"
                                 VerticalAlignment="Center"
                                 HorizontalAlignment="Stretch"
                                 HorizontalAlignment="Stretch"
                                 RecognizesAccessKey="True"
                                 RecognizesAccessKey="True"

+ 2 - 1
src/Avalonia.Themes.Simple/Controls/Menu.xaml

@@ -17,7 +17,8 @@
           <Panel>
           <Panel>
             <ContentPresenter Name="PART_HeaderPresenter"
             <ContentPresenter Name="PART_HeaderPresenter"
                               Margin="{TemplateBinding Padding}"
                               Margin="{TemplateBinding Padding}"
-                              Content="{TemplateBinding Header}">
+                              Content="{TemplateBinding Header}"
+                              ContentTemplate="{TemplateBinding HeaderTemplate}">
               <ContentPresenter.DataTemplates>
               <ContentPresenter.DataTemplates>
                 <DataTemplate DataType="sys:String">
                 <DataTemplate DataType="sys:String">
                   <AccessText Text="{Binding}" />
                   <AccessText Text="{Binding}" />

+ 1 - 1
src/Avalonia.Themes.Simple/Controls/MenuItem.xaml

@@ -43,7 +43,7 @@
                               Margin="{TemplateBinding Padding}"
                               Margin="{TemplateBinding Padding}"
                               VerticalAlignment="Center"
                               VerticalAlignment="Center"
                               Content="{TemplateBinding Header}"
                               Content="{TemplateBinding Header}"
-                              ContentTemplate="{TemplateBinding ItemTemplate}">
+                              ContentTemplate="{TemplateBinding HeaderTemplate}">
               <ContentPresenter.DataTemplates>
               <ContentPresenter.DataTemplates>
                 <DataTemplate DataType="sys:String">
                 <DataTemplate DataType="sys:String">
                   <AccessText Text="{Binding}" />
                   <AccessText Text="{Binding}" />

+ 45 - 0
tests/Avalonia.Controls.UnitTests/MenuItemTests.cs

@@ -3,7 +3,9 @@ using System.Collections.Generic;
 using System.Text;
 using System.Text;
 using System.Windows.Input;
 using System.Windows.Input;
 using Avalonia.Collections;
 using Avalonia.Collections;
+using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Templates;
 using Avalonia.Data;
 using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Input;
 using Avalonia.Platform;
 using Avalonia.Platform;
@@ -348,6 +350,47 @@ 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<MenuViewModel>((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<StackPanel>(menu.Presenter.Panel);
+            Assert.Equal(2, panel.Children.Count);
+
+            for (var i = 0; i <  panel.Children.Count; i++)
+            {
+                var menuItem = Assert.IsType<MenuItem>(panel.Children[i]);
+
+                Assert.Equal(items[i], menuItem.Header);
+                Assert.Same(itemTemplate, menuItem.HeaderTemplate);
+
+                var headerPresenter = Assert.IsType<ContentPresenter>(menuItem.HeaderPresenter);
+                Assert.Same(itemTemplate, headerPresenter.ContentTemplate);
+
+                var headerControl = Assert.IsType<TextBlock>(headerPresenter.Child);
+                Assert.Equal(items[i].Header, headerControl.Text);
+            }
+        }
+
         private IDisposable Application()
         private IDisposable Application()
         {
         {
             var screen = new PixelRect(new PixelPoint(), new PixelSize(100, 100));
             var screen = new PixelRect(new PixelPoint(), new PixelSize(100, 100));
@@ -401,5 +444,7 @@ namespace Avalonia.Controls.UnitTests
 
 
             public void RaiseCanExecuteChanged() => _canExecuteChanged?.Invoke(this, EventArgs.Empty);
             public void RaiseCanExecuteChanged() => _canExecuteChanged?.Invoke(this, EventArgs.Empty);
         }
         }
+
+        private record MenuViewModel(string Header);
     }
     }
 }
 }