diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index a801d338c3..dc9064b262 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -95,6 +95,9 @@ Markup/%(RecursiveDir)%(FileName)%(Extension) + + Markup/%(RecursiveDir)%(FileName)%(Extension) + Markup/%(RecursiveDir)%(FileName)%(Extension) diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index b39ceab1b6..39a83c00c4 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -251,7 +251,8 @@ namespace Avalonia.Build.Tasks var populateBuilder = classTypeDefinition == null ? builder : typeSystem.CreateTypeBuilder(classTypeDefinition); - compiler.Compile(parsed, contextClass, + compiler.Compile(parsed, + contextClass, compiler.DefinePopulateMethod(populateBuilder, parsed, populateName, classTypeDefinition == null), buildName == null ? null : compiler.DefineBuildMethod(builder, parsed, buildName, true), diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index 0d41ec93b4..094cd59025 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -68,6 +68,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions new AvaloniaXamlIlCompiledBindingsMetadataRemover() ); + Transformers.Add(new AvaloniaXamlIlControlTemplatePriorityTransformer()); Transformers.Add(new AvaloniaXamlIlMetadataRemover()); Transformers.Add(new AvaloniaXamlIlRootObjectScope()); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplatePriorityTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplatePriorityTransformer.cs new file mode 100644 index 0000000000..6cab68e756 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplatePriorityTransformer.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Data; +using XamlX.Ast; +using XamlX.Transform; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + /// + /// Transforms property assignments within ControlTemplates to use Style priority where possible. + /// + class AvaloniaXamlIlControlTemplatePriorityTransformer : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + var bindingPriorityType = context.GetAvaloniaTypes().BindingPriority; + + // The node is a candidate for transformation if: + // - It's a property assignment to an Avalonia property + // - There's a ControlTemplate ancestor + // - The property has a single value + if (node is XamlPropertyAssignmentNode prop && + prop.Property is XamlIlAvaloniaProperty avaloniaProperty && + context.ParentNodes().Any(IsControlTemplate) && + prop.Values.Count == 1) + { + var priorityValueSetters = new List(); + + // Iterate through the possible setters, trying to find a setter on the property + // which has a BindingPriority parameter followed by the parameter of the existing + // setter. + foreach (var setter in prop.PossibleSetters) + { + var s = avaloniaProperty.Setters.FirstOrDefault(x => + x.Parameters[0] == bindingPriorityType && + x.Parameters[1] == setter.Parameters[0]); + if (s != null) + priorityValueSetters.Add(s); + } + + // If any BindingPriority setters were found, use those. + if (priorityValueSetters.Count > 0) + { + prop.PossibleSetters = priorityValueSetters; + prop.Values.Insert(0, new XamlConstantNode(node, bindingPriorityType, (int)BindingPriority.TemplatedParent)); + } + } + + return node; + } + + private static bool IsControlTemplate(IXamlAstNode node) + { + return node is AvaloniaXamlIlTargetTypeMetadataNode tt && + tt.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.ControlTemplate; + } + } +} 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 28787d9b84..9d9f6a08ab 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -15,6 +15,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType AvaloniaObjectExtensions { get; } public IXamlType AvaloniaProperty { get; } public IXamlType AvaloniaPropertyT { get; } + public IXamlType StyledPropertyT { get; } + public IXamlMethod AvaloniaObjectSetStyledPropertyValue { get; } public IXamlType AvaloniaAttachedPropertyT { get; } public IXamlType IBinding { get; } public IXamlMethod AvaloniaObjectBindMethod { get; } @@ -105,8 +107,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers AvaloniaObjectExtensions = cfg.TypeSystem.GetType("Avalonia.AvaloniaObjectExtensions"); AvaloniaProperty = cfg.TypeSystem.GetType("Avalonia.AvaloniaProperty"); AvaloniaPropertyT = cfg.TypeSystem.GetType("Avalonia.AvaloniaProperty`1"); + StyledPropertyT = cfg.TypeSystem.GetType("Avalonia.StyledProperty`1"); AvaloniaAttachedPropertyT = cfg.TypeSystem.GetType("Avalonia.AttachedProperty`1"); BindingPriority = cfg.TypeSystem.GetType("Avalonia.Data.BindingPriority"); + AvaloniaObjectSetStyledPropertyValue = AvaloniaObject + .FindMethod(m => m.IsPublic && !m.IsStatic && m.Name == "SetValue" + && m.Parameters.Count == 3 + && m.Parameters[0].Name == "StyledPropertyBase`1" + && m.Parameters[2].Equals(BindingPriority)); IBinding = cfg.TypeSystem.GetType("Avalonia.Data.IBinding"); IDisposable = cfg.TypeSystem.GetType("System.IDisposable"); ICommand = cfg.TypeSystem.GetType("System.Windows.Input.ICommand"); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs index 7f1b8caf0d..5c7a80e680 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs @@ -185,11 +185,24 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions AvaloniaXamlIlWellKnownTypes types) :base(original, original.Name, original.DeclaringType, original.Getter, original.Setters) { + var assignBinding = original.CustomAttributes.Any(ca => ca.Type.Equals(types.AssignBindingAttribute)); + AvaloniaProperty = field; CustomAttributes = original.CustomAttributes; - if (!original.CustomAttributes.Any(ca => ca.Type.Equals(types.AssignBindingAttribute))) + if (!assignBinding) Setters.Insert(0, new BindingSetter(types, original.DeclaringType, field)); - + + // Styled and attached properties can be set with a BindingPriority when they're + // assigned in a ControlTemplate. + if (field.FieldType.GenericTypeDefinition == types.StyledPropertyT || + field.FieldType.GenericTypeDefinition == types.AvaloniaAttachedPropertyT) + { + var propertyType = field.FieldType.GenericArguments[0]; + Setters.Insert(0, new SetValueWithPrioritySetter(types, original.DeclaringType, field, propertyType)); + if (!assignBinding) + Setters.Insert(1, new BindingWithPrioritySetter(types, original.DeclaringType, field)); + } + Setters.Insert(0, new UnsetValueSetter(types, original.DeclaringType, field)); } @@ -240,6 +253,63 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } } + class BindingWithPrioritySetter : AvaloniaPropertyCustomSetter + { + public BindingWithPrioritySetter(AvaloniaXamlIlWellKnownTypes types, + IXamlType declaringType, + IXamlField avaloniaProperty) : base(types, declaringType, avaloniaProperty) + { + Parameters = new[] { types.BindingPriority, types.IBinding }; + } + + public override void Emit(IXamlILEmitter emitter) + { + using (var bloc = emitter.LocalsPool.GetLocal(Types.IBinding)) + using (var priorityLocal = emitter.LocalsPool.GetLocal(Types.Int)) + emitter + .Stloc(bloc.Local) + .Stloc(priorityLocal.Local) + .Ldsfld(AvaloniaProperty) + .Ldloc(bloc.Local) + // TODO: provide anchor? + .Ldnull(); + emitter.EmitCall(Types.AvaloniaObjectBindMethod, true); + } + } + + class SetValueWithPrioritySetter : AvaloniaPropertyCustomSetter + { + public SetValueWithPrioritySetter(AvaloniaXamlIlWellKnownTypes types, IXamlType declaringType, IXamlField avaloniaProperty, + IXamlType propertyType) + : base(types, declaringType, avaloniaProperty) + { + Parameters = new[] { types.BindingPriority, propertyType }; + } + + public override void Emit(IXamlILEmitter emitter) + { + /* + Current stack: + - object + - binding priority + - value + */ + + var method = Types.AvaloniaObjectSetStyledPropertyValue + .MakeGenericMethod(new[] { Parameters[1] }); + + using (var valueLocal = emitter.LocalsPool.GetLocal(Parameters[1])) + using (var priorityLocal = emitter.LocalsPool.GetLocal(Types.Int)) + emitter + .Stloc(valueLocal.Local) + .Stloc(priorityLocal.Local) + .Ldsfld(AvaloniaProperty) + .Ldloc(valueLocal.Local) + .Ldloc(priorityLocal.Local) + .EmitCall(method, true); + } + } + class UnsetValueSetter : AvaloniaPropertyCustomSetter { public UnsetValueSetter(AvaloniaXamlIlWellKnownTypes types, IXamlType declaringType, IXamlField avaloniaProperty) diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 30d321426f..d0c39f0289 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -48,6 +48,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/Extensions.cs b/src/Markup/Avalonia.Markup.Xaml/Extensions.cs index 263750c316..d937a83010 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Extensions.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Extensions.cs @@ -24,10 +24,10 @@ namespace Avalonia.Markup.Xaml public static IEnumerable GetParents(this IServiceProvider sp) { return sp.GetService().Parents.OfType(); - - } + public static bool IsInControlTemplate(this IServiceProvider sp) => sp.GetService() != null; + public static Type ResolveType(this IServiceProvider ctx, string namespacePrefix, string type) { var tr = ctx.GetService(); diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs index 087611bd59..f13ecab4e1 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs @@ -11,6 +11,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public class DynamicResourceExtension : IBinding { private object? _anchor; + private BindingPriority _priority; public DynamicResourceExtension() { @@ -25,6 +26,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public IBinding ProvideValue(IServiceProvider serviceProvider) { + if (serviceProvider.IsInControlTemplate()) + _priority = BindingPriority.TemplatedParent; + var provideTarget = serviceProvider.GetService(); if (!(provideTarget.TargetObject is IStyledElement)) @@ -53,12 +57,12 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions if (control != null) { var source = control.GetResourceObservable(ResourceKey, GetConverter(targetProperty)); - return InstancedBinding.OneWay(source); + return InstancedBinding.OneWay(source, _priority); } else if (_anchor is IResourceProvider resourceProvider) { var source = resourceProvider.GetResourceObservable(ResourceKey, GetConverter(targetProperty)); - return InstancedBinding.OneWay(source); + return InstancedBinding.OneWay(source, _priority); } return null; diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/IAvaloniaXamlIlControlTemplateProvider.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/IAvaloniaXamlIlControlTemplateProvider.cs new file mode 100644 index 0000000000..ed3f5bfdff --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/IAvaloniaXamlIlControlTemplateProvider.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Avalonia.Markup.Xaml.XamlIl.Runtime +{ + public interface IAvaloniaXamlIlControlTemplateProvider + { + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index c48f386ffd..a0e7fd7dcf 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -42,7 +42,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime class DeferredParentServiceProvider : IAvaloniaXamlIlParentStackProvider, IServiceProvider, - IRootObjectProvider + IRootObjectProvider, + IAvaloniaXamlIlControlTemplateProvider { private readonly IServiceProvider _parentProvider; private readonly List _parentResourceNodes; @@ -75,6 +76,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime return this; if (serviceType == typeof(IRootObjectProvider)) return this; + if (serviceType == typeof(IAvaloniaXamlIlControlTemplateProvider)) + return this; return _parentProvider?.GetService(serviceType); } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index 9635ab285d..0bb6c01041 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -283,70 +283,6 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal(expected4, grid.RowDefinitions[3].Height); } - [Fact] - public void ControlTemplate_With_Nested_Child_Is_Operational() - { - var xaml = @" - - - - - -"; - var template = AvaloniaRuntimeXamlLoader.Parse(xaml); - - var parent = (ContentControl)template.Build(new ContentControl()).Control; - - Assert.Equal("parent", parent.Name); - - var child = parent.Content as ContentControl; - - Assert.NotNull(child); - - Assert.Equal("child", child.Name); - } - - [Fact] - public void ControlTemplate_With_TargetType_Is_Operational() - { - var xaml = @" - - - -"; - var template = AvaloniaRuntimeXamlLoader.Parse(xaml); - - Assert.Equal(typeof(ContentControl), template.TargetType); - - Assert.IsType(typeof(ContentPresenter), template.Build(new ContentControl()).Control); - } - - [Fact] - public void ControlTemplate_With_Panel_Children_Are_Added() - { - var xaml = @" - - - - - - -"; - var template = AvaloniaRuntimeXamlLoader.Parse(xaml); - - var panel = (Panel)template.Build(new ContentControl()).Control; - - Assert.Equal(2, panel.Children.Count); - - var foo = panel.Children[0]; - var bar = panel.Children[1]; - - Assert.Equal("Foo", foo.Name); - Assert.Equal("Bar", bar.Name); - } - [Fact] public void Named_x_Control_Is_Added_To_NameScope_Simple() { @@ -363,7 +299,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } [Fact] - public void Standart_TypeConverter_Is_Used() + public void Standard_TypeConverter_Is_Used() { var xaml = @""; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs new file mode 100644 index 0000000000..2d6ed1dc62 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs @@ -0,0 +1,273 @@ +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Data; +using Avalonia.Diagnostics; +using Avalonia.Markup.Xaml.Templates; +using Avalonia.Media; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml +{ + public class ControlTemplateTests : XamlTestBase + { + [Fact] + public void Inline_ControlTemplate_Styled_Values_Are_Set_With_Style_Priority() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var button = (Button)window.Content; + + window.ApplyTemplate(); + button.ApplyTemplate(); + + var presenter = (ContentPresenter)button.Presenter; + Assert.Equal(Brushes.Red, presenter.Background); + + var diagnostic = presenter.GetDiagnostic(Button.BackgroundProperty); + Assert.Equal(BindingPriority.TemplatedParent, diagnostic.Priority); + } + } + + [Fact] + public void Style_ControlTemplate_Styled_Values_Are_Set_With_Style_Priority() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var button = (Button)window.Content; + + window.ApplyTemplate(); + button.ApplyTemplate(); + + var presenter = (ContentPresenter)button.Presenter; + Assert.Equal(Dock.Top, DockPanel.GetDock(presenter)); + + var diagnostic = presenter.GetDiagnostic(DockPanel.DockProperty); + Assert.Equal(BindingPriority.TemplatedParent, diagnostic.Priority); + } + } + + [Fact] + public void ControlTemplate_StaticResources_Are_Set_With_Style_Priority() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + Red + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var button = (Button)window.Content; + + window.ApplyTemplate(); + button.ApplyTemplate(); + + var presenter = (ContentPresenter)button.Presenter; + Assert.Equal(Brushes.Red, presenter.Background); + + var diagnostic = presenter.GetDiagnostic(Button.BackgroundProperty); + Assert.Equal(BindingPriority.TemplatedParent, diagnostic.Priority); + } + } + + [Fact] + public void ControlTemplate_DynamicResources_Are_Set_With_Style_Priority() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + Red + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var button = (Button)window.Content; + + window.ApplyTemplate(); + button.ApplyTemplate(); + + var presenter = (ContentPresenter)button.Presenter; + Assert.Equal(Brushes.Red, presenter.Background); + + var diagnostic = presenter.GetDiagnostic(Button.BackgroundProperty); + Assert.Equal(BindingPriority.TemplatedParent, diagnostic.Priority); + } + } + + [Fact] + public void ControlTemplate_TemplateBindings_Are_Set_With_TemplatedParent_Priority() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var button = (Button)window.Content; + + window.ApplyTemplate(); + button.ApplyTemplate(); + + var presenter = (ContentPresenter)button.Presenter; + Assert.Equal("Foo", presenter.Content); + + var diagnostic = presenter.GetDiagnostic(ContentPresenter.ContentProperty); + Assert.Equal(BindingPriority.TemplatedParent, diagnostic.Priority); + } + } + + [Fact] + public void ControlTemplate_With_Nested_Child_Is_Operational() + { + var xaml = @" + + + + + +"; + var template = AvaloniaRuntimeXamlLoader.Parse(xaml); + + var parent = (ContentControl)template.Build(new ContentControl()).Control; + + Assert.Equal("parent", parent.Name); + + var child = parent.Content as ContentControl; + + Assert.NotNull(child); + + Assert.Equal("child", child.Name); + } + + [Fact] + public void ControlTemplate_With_TargetType_Is_Operational() + { + var xaml = @" + + + +"; + var template = AvaloniaRuntimeXamlLoader.Parse(xaml); + + Assert.Equal(typeof(ContentControl), template.TargetType); + + Assert.IsType(typeof(ContentPresenter), template.Build(new ContentControl()).Control); + } + + [Fact] + public void ControlTemplate_With_Panel_Children_Are_Added() + { + var xaml = @" + + + + + + +"; + var template = AvaloniaRuntimeXamlLoader.Parse(xaml); + + var panel = (Panel)template.Build(new ContentControl()).Control; + + Assert.Equal(2, panel.Children.Count); + + var foo = panel.Children[0]; + var bar = panel.Children[1]; + + Assert.Equal("Foo", foo.Name); + Assert.Equal("Bar", bar.Name); + } + } +}