Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable TemplateBinding inside ItemsPanelTemplate #17483

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/Avalonia.Base/Metadata/ControlTemplateScopeAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;

namespace Avalonia.Metadata;

/// <summary>
/// Indicates that a type acts as a control template scope (for example, TemplateBindings are expected to work).
/// Types annotated with this attribute may provide a TargetType property.
/// </summary>
[AttributeUsage(
AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct,
AllowMultiple = false,
Inherited = true)]
public sealed class ControlTemplateScopeAttribute : Attribute;
2 changes: 2 additions & 0 deletions src/Avalonia.Controls/Templates/IControlTemplate.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using Avalonia.Controls.Primitives;
using Avalonia.Metadata;

namespace Avalonia.Controls.Templates
{
/// <summary>
/// Interface representing a template used to build a <see cref="TemplatedControl"/>.
/// </summary>
[ControlTemplateScope]
public interface IControlTemplate : ITemplate<TemplatedControl, TemplateResult<Control>?>
{
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using XamlX.Ast;
using XamlX.Transform;
Expand All @@ -11,8 +12,9 @@ class AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer : IXamlAstTrans
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
if (!(node is XamlAstObjectNode on
&& context.GetAvaloniaTypes().IControlTemplate.IsAssignableFrom(on.Type.GetClrType())))
&& ControlTemplateScopeCache.GetOrCreate(context).IsControlTemplateScope(on.Type.GetClrType())))
return node;

var tt = on.Children.OfType<XamlAstXamlPropertyValueNode>().FirstOrDefault(ch =>
ch.Property.GetClrProperty().Name == "TargetType");

Expand Down Expand Up @@ -40,6 +42,57 @@ _ when context.ParentNodes().Skip(1).FirstOrDefault() is XamlAstObjectNode direc
return new AvaloniaXamlIlTargetTypeMetadataNode(on, targetType,
AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.ControlTemplate);
}

private sealed class ControlTemplateScopeCache
{
private readonly IXamlType _controlTemplateScopeAttributeType;
private readonly Dictionary<IXamlType, bool> _isScopeByType = new();

private ControlTemplateScopeCache(IXamlType controlTemplateScopeAttributeType)
=> _controlTemplateScopeAttributeType = controlTemplateScopeAttributeType;

public static ControlTemplateScopeCache GetOrCreate(AstTransformationContext context)
{
if (!context.TryGetItem(out ControlTemplateScopeCache? cache))
{
cache = new ControlTemplateScopeCache(context.GetAvaloniaTypes().ControlTemplateScopeAttribute);
context.SetItem(cache);
}

return cache;
}

private bool HasScopeAttribute(IXamlType type)
=> type.CustomAttributes.Any(attr => attr.Type == _controlTemplateScopeAttributeType);

private bool IsControlTemplateScopeCore(IXamlType type)
{
for (var t = type; t is not null; t = t.BaseType)
{
if (HasScopeAttribute(t))
return true;
}

foreach (var iface in type.Interfaces)
{
if (HasScopeAttribute(iface))
return true;
}

return false;
}

public bool IsControlTemplateScope(IXamlType type)
{
if (!_isScopeByType.TryGetValue(type, out var isScope))
{
isScope = IsControlTemplateScopeCore(type);
_isScopeByType[type] = isScope;
}

return isScope;
}
}
}

class AvaloniaXamlIlTargetTypeMetadataNode : XamlValueWithSideEffectNodeBase
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ sealed class AvaloniaXamlIlWellKnownTypes
public IXamlType InheritDataTypeFromAttribute { get; }
public IXamlType MarkupExtensionOptionAttribute { get; }
public IXamlType MarkupExtensionDefaultOptionAttribute { get; }
public IXamlType ControlTemplateScopeAttribute { get; }
public IXamlType AvaloniaListAttribute { get; }
public IXamlType AvaloniaList { get; }
public IXamlType OnExtensionType { get; }
Expand Down Expand Up @@ -129,7 +130,6 @@ sealed class AvaloniaXamlIlWellKnownTypes
public IXamlType WindowTransparencyLevel { get; }
public IXamlType IReadOnlyListOfT { get; }
public IXamlType ControlTemplate { get; }
public IXamlType IControlTemplate { get; }
public IXamlType EventHandlerT { get; }
public IXamlMethod GetClassProperty { get; }

Expand Down Expand Up @@ -204,6 +204,7 @@ public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg)
InheritDataTypeFromAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.InheritDataTypeFromAttribute");
MarkupExtensionOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionOptionAttribute");
MarkupExtensionDefaultOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionDefaultOptionAttribute");
ControlTemplateScopeAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.ControlTemplateScopeAttribute");
AvaloniaListAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.AvaloniaListAttribute");
AvaloniaList = cfg.TypeSystem.GetType("Avalonia.Collections.AvaloniaList`1");
OnExtensionType = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.On");
Expand Down Expand Up @@ -326,7 +327,6 @@ public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg)
Style = cfg.TypeSystem.GetType("Avalonia.Styling.Style");
ControlTheme = cfg.TypeSystem.GetType("Avalonia.Styling.ControlTheme");
ControlTemplate = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Templates.ControlTemplate");
IControlTemplate = cfg.TypeSystem.GetType("Avalonia.Controls.Templates.IControlTemplate");
IReadOnlyListOfT = cfg.TypeSystem.GetType("System.Collections.Generic.IReadOnlyList`1");
EventHandlerT = cfg.TypeSystem.GetType("System.EventHandler`1");
Interactivity = new InteractivityWellKnownTypes(cfg);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Avalonia.Markup.Xaml.Templates
{
[ControlTemplateScope]
public class ItemsPanelTemplate : ITemplate<Panel?>
{
[Content]
Expand Down
100 changes: 100 additions & 0 deletions tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ItemsPanelTemplateTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Media;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;

namespace Avalonia.Markup.Xaml.UnitTests.Xaml;

public class ItemsPanelTemplateTests
{
[Fact]
public void ItemsPanelTemplate_In_Style_Allows_TemplateBinding()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = (Window)AvaloniaRuntimeXamlLoader.Load(
"""
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Window.Styles>
<Style Selector="ListBox">
<Setter Property="Template">
<ControlTemplate>
<ItemsPresenter Name="PART_ItemsPresenter"
ItemsPanel="{TemplateBinding ItemsPanel}" />
</ControlTemplate>
</Setter>
<Setter Property="ItemsPanel">
<ItemsPanelTemplate>
<Panel Background="{TemplateBinding Background}"
Tag="{TemplateBinding ItemsSource}" />
</ItemsPanelTemplate>
</Setter>
</Style>
</Window.Styles>
<ListBox Background="DodgerBlue" />
</Window>
""");
var listBox = Assert.IsType<ListBox>(window.Content);
var items = new[] { "foo", "bar" };
listBox.ItemsSource = items;

window.ApplyTemplate();
listBox.ApplyTemplate();

var itemsPresenter = listBox.FindDescendantOfType<ItemsPresenter>();
Assert.NotNull(itemsPresenter);
itemsPresenter.ApplyTemplate();

var panel = itemsPresenter.Panel;
Assert.NotNull(panel);
Assert.Equal(Brushes.DodgerBlue, panel.Background);
Assert.Same(items, panel.Tag);
}
}

[Fact]
public void ItemsPanelTemplate_In_Control_Allows_TemplateBinding()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = (Window)AvaloniaRuntimeXamlLoader.Load(
"""
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ListBox Background="DodgerBlue">
<ListBox.Template>
<ControlTemplate>
<ItemsPresenter Name="PART_ItemsPresenter"
ItemsPanel="{TemplateBinding ItemsPanel}" />
</ControlTemplate>
</ListBox.Template>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<Panel Background="{TemplateBinding Background}"
Tag="{TemplateBinding ItemsSource}" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</Window>
""");
var listBox = Assert.IsType<ListBox>(window.Content);
var items = new[] { "foo", "bar" };
listBox.ItemsSource = items;

window.ApplyTemplate();
listBox.ApplyTemplate();

var itemsPresenter = listBox.FindDescendantOfType<ItemsPresenter>();
Assert.NotNull(itemsPresenter);
itemsPresenter.ApplyTemplate();

var panel = itemsPresenter.Panel;
Assert.NotNull(panel);
Assert.Equal(Brushes.DodgerBlue, panel.Background);
Assert.Same(items, panel.Tag);
}
}
}