diff --git a/src/Avalonia.Base/Metadata/TemplateContent.cs b/src/Avalonia.Base/Metadata/TemplateContent.cs index fcd7d69e7b..7f9e878419 100644 --- a/src/Avalonia.Base/Metadata/TemplateContent.cs +++ b/src/Avalonia.Base/Metadata/TemplateContent.cs @@ -8,5 +8,6 @@ namespace Avalonia.Metadata [AttributeUsage(AttributeTargets.Property)] public class TemplateContentAttribute : Attribute { + public Type TemplateResultType { get; set; } } } diff --git a/src/Avalonia.Controls/Templates/IControlTemplate.cs b/src/Avalonia.Controls/Templates/IControlTemplate.cs index 7414f438a1..ab46884402 100644 --- a/src/Avalonia.Controls/Templates/IControlTemplate.cs +++ b/src/Avalonia.Controls/Templates/IControlTemplate.cs @@ -1,3 +1,4 @@ +using System; using Avalonia.Controls.Primitives; using Avalonia.Styling; @@ -10,18 +11,16 @@ namespace Avalonia.Controls.Templates { } - public class ControlTemplateResult + public class ControlTemplateResult : TemplateResult { public IControl Control { get; } - public INameScope NameScope { get; } - public ControlTemplateResult(IControl control, INameScope nameScope) + public ControlTemplateResult(IControl control, INameScope nameScope) : base(control, nameScope) { Control = control; - NameScope = nameScope; } - public void Deconstruct(out IControl control, out INameScope scope) + public new void Deconstruct(out IControl control, out INameScope scope) { control = Control; scope = NameScope; diff --git a/src/Avalonia.Controls/Templates/TemplateResult.cs b/src/Avalonia.Controls/Templates/TemplateResult.cs new file mode 100644 index 0000000000..770aecc329 --- /dev/null +++ b/src/Avalonia.Controls/Templates/TemplateResult.cs @@ -0,0 +1,20 @@ +namespace Avalonia.Controls.Templates +{ + public class TemplateResult + { + public T Result { get; } + public INameScope NameScope { get; } + + public TemplateResult(T result, INameScope nameScope) + { + Result = result; + NameScope = nameScope; + } + + public void Deconstruct(out T result, out INameScope scope) + { + result = Result; + scope = NameScope; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs index a82f5b9e60..e9bf20a626 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -49,8 +49,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions XmlNamespaceInfoProvider = typeSystem.GetType("Avalonia.Markup.Xaml.XamlIl.Runtime.IAvaloniaXamlIlXmlNamespaceInfoProvider"), DeferredContentPropertyAttributes = {typeSystem.GetType("Avalonia.Metadata.TemplateContentAttribute")}, + DeferredContentExecutorCustomizationTypeParameterDeferredContentAttributePropertyNames = new List + { + "TemplateResultType" + }, DeferredContentExecutorCustomization = - runtimeHelpers.FindMethod(m => m.Name == "DeferredTransformationFactoryV1"), + runtimeHelpers.FindMethod(m => m.Name == "DeferredTransformationFactoryV2"), UsableDuringInitializationAttributes = { typeSystem.GetType("Avalonia.Metadata.UsableDuringInitializationAttribute"), diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github index f4ac681b91..22766f0922 160000 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github @@ -1 +1 @@ -Subproject commit f4ac681b91a9dc7a7a095d1050a683de23d86b72 +Subproject commit 22766f092201ea634356b5f1ef2193b0f0d6695c diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs index 483a1a5d06..07c79d7077 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs @@ -7,6 +7,7 @@ namespace Avalonia.Markup.Xaml.Templates public static class TemplateContent { public static ControlTemplateResult Load(object templateContent) + { if (templateContent is Func direct) { @@ -20,5 +21,16 @@ namespace Avalonia.Markup.Xaml.Templates throw new ArgumentException(nameof(templateContent)); } + + public static TemplateResult Load(object templateContent) + { + if (templateContent is Func direct) + return (TemplateResult)direct(null); + + if (templateContent is null) + return null; + + throw new ArgumentException(nameof(templateContent)); + } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index 83d70122b3..c48f386ffd 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -15,6 +15,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime { public static Func DeferredTransformationFactoryV1(Func builder, IServiceProvider provider) + { + return DeferredTransformationFactoryV2(builder, provider); + } + + public static Func DeferredTransformationFactoryV2(Func builder, + IServiceProvider provider) { var resourceNodes = provider.GetService().Parents .OfType().ToList(); @@ -25,7 +31,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime var scope = parentScope != null ? new ChildNameScope(parentScope) : (INameScope)new NameScope(); var obj = builder(new DeferredParentServiceProvider(sp, resourceNodes, rootObject, scope)); scope.Complete(); - return new ControlTemplateResult((IControl)obj, scope); + + if(typeof(T) == typeof(IControl)) + return new ControlTemplateResult((IControl)obj, scope); + + return new TemplateResult((T)obj, scope); }; } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/GenericTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/GenericTemplateTests.cs new file mode 100644 index 0000000000..9fee5285aa --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/GenericTemplateTests.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Markup.Xaml.Templates; +using Avalonia.Metadata; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml +{ + public class SampleTemplatedObject : StyledElement + { + [Content] public List Content { get; set; } = new List(); + public string Foo { get; set; } + } + + public class SampleTemplatedObjectTemplate + { + [Content] + [TemplateContent(TemplateResultType = typeof(SampleTemplatedObject))] + public object Content { get; set; } + } + + public class SampleTemplatedObjectContainer + { + public SampleTemplatedObjectTemplate Template { get; set; } + } + + public class GenericTemplateTests + { + [Fact] + public void DataTemplate_Can_Be_Empty() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + + +"; + var container = + (SampleTemplatedObjectContainer)AvaloniaRuntimeXamlLoader.Load(xaml, + typeof(GenericTemplateTests).Assembly); + var res = TemplateContent.Load(container.Template.Content); + Assert.Equal(res.Result, res.NameScope.Find("root")); + Assert.Equal(res.Result.Content[0], res.NameScope.Find("child1")); + Assert.Equal(res.Result.Content[1], res.NameScope.Find("child2")); + Assert.Equal("foo", res.Result.Content[0].Foo); + Assert.Equal("bar", res.Result.Content[1].Foo); + } + } + } +}