Skip to content

Commit

Permalink
Add ControlTemplateScope and enable it on ItemsPanelTemplate (#17483)
Browse files Browse the repository at this point in the history
  • Loading branch information
MrJul authored Nov 14, 2024
1 parent 9fb3a13 commit 8ec3b6b
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 3 deletions.
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);
}
}
}

0 comments on commit 8ec3b6b

Please sign in to comment.