Browse Source

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

Make data templates work again with MenuItem.
pull/10858/head
Max Katz 4 years ago
committed by GitHub
parent
commit
660033c770
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      src/Avalonia.Controls/ItemsControl.cs
  2. 16
      src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs
  3. 63
      src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs
  4. 1
      src/Avalonia.Themes.Fluent/Controls/Menu.xaml
  5. 2
      src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
  6. 3
      src/Avalonia.Themes.Simple/Controls/Menu.xaml
  7. 2
      src/Avalonia.Themes.Simple/Controls/MenuItem.xaml
  8. 45
      tests/Avalonia.Controls.UnitTests/MenuItemTests.cs

12
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 // These conditions are separate because HeaderedItemsControl and
// ItemsControl preparation. // 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);
} }
} }

16
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(); PrepareItemContainer(_prepareItemContainerOnAttach);
_prepareItemContainerOnAttach = false; _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
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
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"

2
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"

3
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}" />

2
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
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);
} }
} }

Loading…
Cancel
Save