Browse Source

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

Make data templates work again with MenuItem.
pull/10858/head
Max Katz 3 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;
}
// 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);
}
}

16
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;
/// <summary>
/// Defines the <see cref="Header"/> 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 &&

63
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
/// </summary>
public class HeaderedSelectingItemsControl : SelectingItemsControl, IContentPresenterHost
{
private IDisposable? _itemsBinding;
private ItemsControl? _prepareItemContainerOnAttach;
/// <summary>
/// Defines the <see cref="Header"/> property.
/// </summary>
public static readonly StyledProperty<object?> HeaderProperty =
HeaderedContentControl.HeaderProperty.AddOwner<HeaderedSelectingItemsControl>();
/// <summary>
/// Defines the <see cref="HeaderTemplate"/> property.
/// </summary>
public static readonly StyledProperty<IDataTemplate?> HeaderTemplateProperty =
HeaderedItemsControl.HeaderTemplateProperty.AddOwner<HeaderedSelectingItemsControl>();
/// <summary>
/// Initializes static members of the <see cref="ContentControl"/> class.
/// </summary>
@ -32,6 +44,15 @@ namespace Avalonia.Controls.Primitives
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>
/// Gets the header presenter from the control's template.
/// </summary>
@ -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;
}
}
/// <summary>
/// Called when an <see cref="IContentPresenter"/> is registered with the control.
/// </summary>
@ -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)

1
src/Avalonia.Themes.Fluent/Controls/Menu.xaml

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

2
src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml

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

3
src/Avalonia.Themes.Simple/Controls/Menu.xaml

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

2
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}">
<ContentPresenter.DataTemplates>
<DataTemplate DataType="sys:String">
<AccessText Text="{Binding}" />

45
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,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()
{
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);
}
private record MenuViewModel(string Header);
}
}

Loading…
Cancel
Save