Browse Source

Add ControlTemplateScope and enable it on ItemsPanelTemplate (#17483)

release/11.2.1
Julien Lebosquain 1 year ago
committed by Max Katz
parent
commit
5c1492d9e9
  1. 13
      src/Avalonia.Base/Metadata/ControlTemplateScopeAttribute.cs
  2. 2
      src/Avalonia.Controls/Templates/IControlTemplate.cs
  3. 55
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs
  4. 4
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  5. 1
      src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs
  6. 100
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ItemsPanelTemplateTests.cs

13
src/Avalonia.Base/Metadata/ControlTemplateScopeAttribute.cs

@ -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
src/Avalonia.Controls/Templates/IControlTemplate.cs

@ -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>?>
{
}

55
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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
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");
@ -40,6 +42,57 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
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

4
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

@ -36,6 +36,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType WindowTransparencyLevel { get; }
public IXamlType IReadOnlyListOfT { get; }
public IXamlType ControlTemplate { get; }
public IXamlType IControlTemplate { get; }
public IXamlType EventHandlerT { get; }
sealed internal class InteractivityWellKnownTypes
@ -203,6 +203,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
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");
@ -325,7 +326,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
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);

1
src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs

@ -4,6 +4,7 @@ using Avalonia.Styling;
namespace Avalonia.Markup.Xaml.Templates
{
[ControlTemplateScope]
public class ItemsPanelTemplate : ITemplate<Panel?>
{
[Content]

100
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(
"""
<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);
}
}
}
Loading…
Cancel
Save