diff --git a/src/Avalonia.Base/Metadata/ControlTemplateScopeAttribute.cs b/src/Avalonia.Base/Metadata/ControlTemplateScopeAttribute.cs new file mode 100644 index 00000000000..8a69f400459 --- /dev/null +++ b/src/Avalonia.Base/Metadata/ControlTemplateScopeAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace Avalonia.Metadata; + +/// +/// 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. +/// +[AttributeUsage( + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct, + AllowMultiple = false, + Inherited = true)] +public sealed class ControlTemplateScopeAttribute : Attribute; diff --git a/src/Avalonia.Controls/Templates/IControlTemplate.cs b/src/Avalonia.Controls/Templates/IControlTemplate.cs index c3f9c9e8aab..0527861e7d8 100644 --- a/src/Avalonia.Controls/Templates/IControlTemplate.cs +++ b/src/Avalonia.Controls/Templates/IControlTemplate.cs @@ -1,10 +1,12 @@ using Avalonia.Controls.Primitives; +using Avalonia.Metadata; namespace Avalonia.Controls.Templates { /// /// Interface representing a template used to build a . /// + [ControlTemplateScope] public interface IControlTemplate : ITemplate?> { } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs index e08a665785f..e13e8550877 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using XamlX.Ast; using XamlX.Transform; @@ -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().FirstOrDefault(ch => ch.Property.GetClrProperty().Name == "TargetType"); @@ -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 _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 diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index a959de20ba9..25c01354921 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -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; } @@ -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; } @@ -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"); @@ -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); diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs index f31a693e725..d2a79404aab 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs @@ -4,6 +4,7 @@ namespace Avalonia.Markup.Xaml.Templates { + [ControlTemplateScope] public class ItemsPanelTemplate : ITemplate { [Content] diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ItemsPanelTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ItemsPanelTemplateTests.cs new file mode 100644 index 00000000000..d3e29b302ae --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ItemsPanelTemplateTests.cs @@ -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( + """ + + + + + + + """); + var listBox = Assert.IsType(window.Content); + var items = new[] { "foo", "bar" }; + listBox.ItemsSource = items; + + window.ApplyTemplate(); + listBox.ApplyTemplate(); + + var itemsPresenter = listBox.FindDescendantOfType(); + 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( + """ + + + + + + + + + + + + + + + """); + var listBox = Assert.IsType(window.Content); + var items = new[] { "foo", "bar" }; + listBox.ItemsSource = items; + + window.ApplyTemplate(); + listBox.ApplyTemplate(); + + var itemsPresenter = listBox.FindDescendantOfType(); + Assert.NotNull(itemsPresenter); + itemsPresenter.ApplyTemplate(); + + var panel = itemsPresenter.Panel; + Assert.NotNull(panel); + Assert.Equal(Brushes.DodgerBlue, panel.Background); + Assert.Same(items, panel.Tag); + } + } +}