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