From c222070d84626495db9bca870afb113ac67ec35f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 14 Jun 2019 11:31:08 +0300 Subject: [PATCH 001/711] Added IPropertyInfo and use it in IProvideValueTarget when possible --- src/Avalonia.Base/AvaloniaProperty.cs | 8 +- .../Data/Core/ClrPropertyInfo.cs | 70 +++++++++ src/Avalonia.Base/Data/Core/IPropertyInfo.cs | 11 ++ .../XamlCompilerTaskExecutor.cs | 9 +- .../Avalonia.Markup.Xaml.csproj | 2 + .../XamlIl/AvaloniaXamlIlRuntimeCompiler.cs | 11 +- .../AvaloniaXamlIlCompiler.cs | 6 +- .../AvaloniaXamlIlCompilerConfiguration.cs | 21 +++ .../AvaloniaXamlIlLanguage.cs | 2 +- .../AvaloniaXamlIlWellKnownTypes.cs | 42 +++-- .../XamlIlAvaloniaPropertyHelper.cs | 14 ++ .../XamlIlClrPropertyInfoHelper.cs | 147 ++++++++++++++++++ .../Avalonia.Markup.Xaml/XamlIl/xamlil.github | 2 +- .../Xaml/XamlIlTests.cs | 30 ++++ 14 files changed, 348 insertions(+), 27 deletions(-) create mode 100644 src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs create mode 100644 src/Avalonia.Base/Data/Core/IPropertyInfo.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 1de5cb06c6..3e70f549f3 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Reactive.Subjects; using System.Reflection; using Avalonia.Data; +using Avalonia.Data.Core; using Avalonia.Utilities; namespace Avalonia @@ -14,7 +15,7 @@ namespace Avalonia /// /// Base class for avalonia properties. /// - public class AvaloniaProperty : IEquatable + public class AvaloniaProperty : IEquatable, IPropertyInfo { /// /// Represents an unset property value. @@ -546,7 +547,10 @@ namespace Avalonia } } - + bool IPropertyInfo.CanGet => true; + bool IPropertyInfo.CanSet => true; + object IPropertyInfo.Get(object target) => ((AvaloniaObject)target).GetValue(this); + void IPropertyInfo.Set(object target, object value) => ((AvaloniaObject)target).SetValue(this, value); } /// /// Class representing the . diff --git a/src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs b/src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs new file mode 100644 index 0000000000..05059b6617 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs @@ -0,0 +1,70 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; + +namespace Avalonia.Data.Core +{ + public class ClrPropertyInfo : IPropertyInfo + { + private readonly Func _getter; + private readonly Action _setter; + + public ClrPropertyInfo(string name, Func getter, Action setter) + { + _getter = getter; + _setter = setter; + Name = name; + } + + public string Name { get; } + public object Get(object target) + { + if (_getter == null) + throw new NotSupportedException("Property " + Name + " doesn't have a getter"); + return _getter(target); + } + + public void Set(object target, object value) + { + if (_setter == null) + throw new NotSupportedException("Property " + Name + " doesn't have a setter"); + _setter(target, value); + } + + public bool CanSet => _setter != null; + public bool CanGet => _getter != null; + } + + public class ReflectionClrPropertyInfo : ClrPropertyInfo + { + static Action CreateSetter(PropertyInfo info) + { + if (info.SetMethod == null) + return null; + var target = Expression.Parameter(typeof(object), "target"); + var value = Expression.Parameter(typeof(object), "value"); + return Expression.Lambda>( + Expression.Call(Expression.Convert(target, info.DeclaringType), info.SetMethod, + Expression.Convert(value, info.SetMethod.GetParameters()[0].ParameterType)), + target, value) + .Compile(); + } + + static Func CreateGetter(PropertyInfo info) + { + if (info.GetMethod == null) + return null; + var target = Expression.Parameter(typeof(object), "target"); + return Expression.Lambda>( + Expression.Convert(Expression.Call(Expression.Convert(target, info.DeclaringType), info.GetMethod), + typeof(object))) + .Compile(); + } + + public ReflectionClrPropertyInfo(PropertyInfo info) : base(info.Name, + CreateGetter(info), CreateSetter(info)) + { + + } + } +} diff --git a/src/Avalonia.Base/Data/Core/IPropertyInfo.cs b/src/Avalonia.Base/Data/Core/IPropertyInfo.cs new file mode 100644 index 0000000000..530b98d08b --- /dev/null +++ b/src/Avalonia.Base/Data/Core/IPropertyInfo.cs @@ -0,0 +1,11 @@ +namespace Avalonia.Data.Core +{ + public interface IPropertyInfo + { + string Name { get; } + object Get(object target); + void Set(object target, object value); + bool CanSet { get; } + bool CanGet { get; } + } +} diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index c54d8e19a1..4981b27959 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -49,13 +49,18 @@ namespace Avalonia.Build.Tasks if (avares.Resources.Count(CheckXamlName) == 0 && emres.Resources.Count(CheckXamlName) == 0) // Nothing to do return new CompileResult(true); + + var clrPropertiesDef = new TypeDefinition("CompiledAvaloniaXaml", "XamlIlHelpers", + TypeAttributes.Class, asm.MainModule.TypeSystem.Object); + asm.MainModule.Types.Add(clrPropertiesDef); var xamlLanguage = AvaloniaXamlIlLanguage.Configure(typeSystem); - var compilerConfig = new XamlIlTransformerConfiguration(typeSystem, + var compilerConfig = new AvaloniaXamlIlCompilerConfiguration(typeSystem, typeSystem.TargetAssembly, xamlLanguage, XamlIlXmlnsMappings.Resolve(typeSystem, xamlLanguage), - AvaloniaXamlIlLanguage.CustomValueConverter); + AvaloniaXamlIlLanguage.CustomValueConverter, + new XamlIlClrPropertyInfoEmitter(typeSystem.CreateTypeBuilder(clrPropertiesDef))); var contextDef = new TypeDefinition("CompiledAvaloniaXaml", "XamlIlContext", diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 6f3dabd568..91d0feedfb 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -38,6 +38,7 @@ + @@ -55,6 +56,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs index 167b75603f..5f95ae55a9 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs @@ -114,11 +114,15 @@ namespace Avalonia.Markup.Xaml.XamlIl InitializeSre(); var asm = localAssembly == null ? null : _sreTypeSystem.GetAssembly(localAssembly); + + var tb = _sreBuilder.DefineType("Builder_" + Guid.NewGuid().ToString("N") + "_" + uri); + var clrPropertyBuilder = tb.DefineNestedType("ClrProperties_" + Guid.NewGuid().ToString("N")); - var compiler = new AvaloniaXamlIlCompiler(new XamlIlTransformerConfiguration(_sreTypeSystem, asm, - _sreMappings, _sreXmlns, AvaloniaXamlIlLanguage.CustomValueConverter), + var compiler = new AvaloniaXamlIlCompiler(new AvaloniaXamlIlCompilerConfiguration(_sreTypeSystem, asm, + _sreMappings, _sreXmlns, AvaloniaXamlIlLanguage.CustomValueConverter, + new XamlIlClrPropertyInfoEmitter(_sreTypeSystem.CreateTypeBuilder(clrPropertyBuilder))), _sreContextType); - var tb = _sreBuilder.DefineType("Builder_" + Guid.NewGuid().ToString("N") + "_" + uri); + IXamlIlType overrideType = null; if (rootInstance != null) @@ -129,6 +133,7 @@ namespace Avalonia.Markup.Xaml.XamlIl compiler.IsDesignMode = isDesignMode; compiler.ParseAndCompile(xaml, uri?.ToString(), null, _sreTypeSystem.CreateTypeBuilder(tb), overrideType); var created = tb.CreateTypeInfo(); + clrPropertyBuilder.CreateTypeInfo(); return LoadOrPopulate(created, rootInstance); } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs index b84f50fa8d..0d0b0e6967 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -16,7 +16,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions private readonly IXamlIlType _contextType; private readonly AvaloniaXamlIlDesignPropertiesTransformer _designTransformer; - private AvaloniaXamlIlCompiler(XamlIlTransformerConfiguration configuration) : base(configuration, true) + private AvaloniaXamlIlCompiler(AvaloniaXamlIlCompilerConfiguration configuration) : base(configuration, true) { _configuration = configuration; @@ -57,14 +57,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } - public AvaloniaXamlIlCompiler(XamlIlTransformerConfiguration configuration, + public AvaloniaXamlIlCompiler(AvaloniaXamlIlCompilerConfiguration configuration, IXamlIlTypeBuilder contextTypeBuilder) : this(configuration) { _contextType = CreateContextType(contextTypeBuilder); } - public AvaloniaXamlIlCompiler(XamlIlTransformerConfiguration configuration, + public AvaloniaXamlIlCompiler(AvaloniaXamlIlCompilerConfiguration configuration, IXamlIlType contextType) : this(configuration) { _contextType = contextType; diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs new file mode 100644 index 0000000000..26965ffea4 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs @@ -0,0 +1,21 @@ +using XamlIl.Transform; +using XamlIl.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions +{ + class AvaloniaXamlIlCompilerConfiguration : XamlIlTransformerConfiguration + { + public XamlIlClrPropertyInfoEmitter ClrPropertyEmitter { get; } + + public AvaloniaXamlIlCompilerConfiguration(IXamlIlTypeSystem typeSystem, + IXamlIlAssembly defaultAssembly, + XamlIlLanguageTypeMappings typeMappings, + XamlIlXmlnsMappings xmlnsMappings, + XamlIlValueConverter customValueConverter, + XamlIlClrPropertyInfoEmitter clrPropertyEmitter) : base(typeSystem, defaultAssembly, typeMappings, xmlnsMappings, customValueConverter) + { + ClrPropertyEmitter = clrPropertyEmitter; + AddExtra(ClrPropertyEmitter); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs index c25e1186d0..dad69da3be 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -50,7 +50,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions }, InnerServiceProviderFactoryMethod = runtimeHelpers.FindMethod(m => m.Name == "CreateInnerServiceProviderV1"), - ProvideValueTargetPropertyEmitter = XamlIlAvaloniaPropertyHelper.Emit, + ProvideValueTargetPropertyEmitter = XamlIlAvaloniaPropertyHelper.EmitProvideValueTarget, }; rv.CustomAttributeResolver = new AttributeResolver(typeSystem, rv); return rv; diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index c054e57380..4c118899dc 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -19,26 +19,30 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlIlType Transitions { get; } public IXamlIlType AssignBindingAttribute { get; } public IXamlIlType UnsetValueType { get; } + public IXamlIlType IPropertyInfo { get; } + public IXamlIlType ClrPropertyInfo { get; } - public AvaloniaXamlIlWellKnownTypes(XamlIlAstTransformationContext ctx) + public AvaloniaXamlIlWellKnownTypes(XamlIlTransformerConfiguration cfg) { - XamlIlTypes = ctx.Configuration.WellKnownTypes; - AvaloniaObject = ctx.Configuration.TypeSystem.GetType("Avalonia.AvaloniaObject"); - IAvaloniaObject = ctx.Configuration.TypeSystem.GetType("Avalonia.IAvaloniaObject"); - AvaloniaObjectExtensions = ctx.Configuration.TypeSystem.GetType("Avalonia.AvaloniaObjectExtensions"); - AvaloniaProperty = ctx.Configuration.TypeSystem.GetType("Avalonia.AvaloniaProperty"); - AvaloniaPropertyT = ctx.Configuration.TypeSystem.GetType("Avalonia.AvaloniaProperty`1"); - BindingPriority = ctx.Configuration.TypeSystem.GetType("Avalonia.Data.BindingPriority"); - IBinding = ctx.Configuration.TypeSystem.GetType("Avalonia.Data.IBinding"); - IDisposable = ctx.Configuration.TypeSystem.GetType("System.IDisposable"); - Transitions = ctx.Configuration.TypeSystem.GetType("Avalonia.Animation.Transitions"); - AssignBindingAttribute = ctx.Configuration.TypeSystem.GetType("Avalonia.Data.AssignBindingAttribute"); + XamlIlTypes = cfg.WellKnownTypes; + AvaloniaObject = cfg.TypeSystem.GetType("Avalonia.AvaloniaObject"); + IAvaloniaObject = cfg.TypeSystem.GetType("Avalonia.IAvaloniaObject"); + AvaloniaObjectExtensions = cfg.TypeSystem.GetType("Avalonia.AvaloniaObjectExtensions"); + AvaloniaProperty = cfg.TypeSystem.GetType("Avalonia.AvaloniaProperty"); + AvaloniaPropertyT = cfg.TypeSystem.GetType("Avalonia.AvaloniaProperty`1"); + BindingPriority = cfg.TypeSystem.GetType("Avalonia.Data.BindingPriority"); + IBinding = cfg.TypeSystem.GetType("Avalonia.Data.IBinding"); + IDisposable = cfg.TypeSystem.GetType("System.IDisposable"); + Transitions = cfg.TypeSystem.GetType("Avalonia.Animation.Transitions"); + AssignBindingAttribute = cfg.TypeSystem.GetType("Avalonia.Data.AssignBindingAttribute"); AvaloniaObjectBindMethod = AvaloniaObjectExtensions.FindMethod("Bind", IDisposable, false, IAvaloniaObject, AvaloniaProperty, - IBinding, ctx.Configuration.WellKnownTypes.Object); - UnsetValueType = ctx.Configuration.TypeSystem.GetType("Avalonia.UnsetValueType"); + IBinding, cfg.WellKnownTypes.Object); + UnsetValueType = cfg.TypeSystem.GetType("Avalonia.UnsetValueType"); AvaloniaObjectSetValueMethod = AvaloniaObject.FindMethod("SetValue", XamlIlTypes.Void, false, AvaloniaProperty, XamlIlTypes.Object, BindingPriority); + IPropertyInfo = cfg.TypeSystem.GetType("Avalonia.Data.Core.IPropertyInfo"); + ClrPropertyInfo = cfg.TypeSystem.GetType("Avalonia.Data.Core.ClrPropertyInfo"); } } @@ -48,7 +52,15 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { if (ctx.TryGetItem(out var rv)) return rv; - ctx.SetItem(rv = new AvaloniaXamlIlWellKnownTypes(ctx)); + ctx.SetItem(rv = new AvaloniaXamlIlWellKnownTypes(ctx.Configuration)); + return rv; + } + + public static AvaloniaXamlIlWellKnownTypes GetAvaloniaTypes(this XamlIlEmitContext ctx) + { + if (ctx.TryGetItem(out var rv)) + return rv; + ctx.SetItem(rv = new AvaloniaXamlIlWellKnownTypes(ctx.Configuration)); return rv; } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs index 6fc83cb5a5..e0783364db 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs @@ -15,6 +15,20 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { class XamlIlAvaloniaPropertyHelper { + public static bool EmitProvideValueTarget(XamlIlEmitContext context, IXamlIlEmitter emitter, + XamlIlAstClrProperty property) + { + if (Emit(context, emitter, property)) + return true; + var foundClr = property.DeclaringType.Properties.FirstOrDefault(p => p.Name == property.Name); + if (foundClr == null) + return false; + context + .Configuration.GetExtra() + .Emit(context, emitter, foundClr); + return true; + } + public static bool Emit(XamlIlEmitContext context, IXamlIlEmitter emitter, XamlIlAstClrProperty property) { if (property is IXamlIlAvaloniaProperty ap) diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs new file mode 100644 index 0000000000..5b681e94a9 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; +using XamlIl.Ast; +using XamlIl.Transform; +using XamlIl.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions +{ + class XamlIlClrPropertyInfoHelper + { + + class ClrPropertyInfoNode : XamlIlAstNode, IXamlIlAstValueNode + { + public ClrPropertyInfoNode(AvaloniaXamlIlWellKnownTypes types, IXamlIlLineInfo lineInfo) : + base(lineInfo) + { + Type = new XamlIlAstClrTypeReference(this, types.XamlIlTypes.Object, false); + } + + public IXamlIlAstTypeReference Type { get; } + } + } + + class XamlIlClrPropertyInfoEmitter + { + private readonly IXamlIlTypeBuilder _builder; + + private Dictionary> _fields + = new Dictionary>(); + + public XamlIlClrPropertyInfoEmitter(IXamlIlTypeBuilder builder) + { + _builder = builder; + } + + static string GetKey(IXamlIlProperty property) + => property.Getter.DeclaringType.GetFullName() + "." + property.Name; + + public IXamlIlType Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen, IXamlIlProperty property) + { + var types = context.GetAvaloniaTypes(); + IXamlIlMethod Get() + { + var key = GetKey(property); + if (!_fields.TryGetValue(key, out var lst)) + _fields[key] = lst = new List<(IXamlIlProperty prop, IXamlIlMethod get)>(); + + foreach (var cached in lst) + { + if ( + ((cached.prop.Getter == null && property.Getter == null) || + cached.prop.Getter?.Equals(property.Getter) == true) && + ((cached.prop.Setter == null && property.Setter == null) || + cached.prop.Setter?.Equals(property.Setter) == true) + ) + return cached.get; + } + + var name = lst.Count == 0 ? key : key + "_" + Guid.NewGuid().ToString("N"); + + var field = _builder.DefineField(types.IPropertyInfo, name + "!Field", false, true); + + var getter = property.Getter == null ? + null : + _builder.DefineMethod(types.XamlIlTypes.Object, + new[] {types.XamlIlTypes.Object}, name + "!Getter", false, true, false); + if (getter != null) + { + getter.Generator + .Ldarg_0() + .Castclass(property.Getter.DeclaringType) + .EmitCall(property.Getter); + if (property.Getter.ReturnType.IsValueType) + getter.Generator.Box(property.Getter.ReturnType); + getter.Generator.Ret(); + } + + var setter = property.Setter == null ? + null : + _builder.DefineMethod(types.XamlIlTypes.Void, + new[] {types.XamlIlTypes.Object, types.XamlIlTypes.Object}, + name + "!Setter", false, true, false); + if (setter != null) + { + setter.Generator + .Ldarg_0() + .Castclass(property.Setter.DeclaringType) + .Ldarg(1); + if (property.Setter.Parameters[0].IsValueType) + setter.Generator.Unbox_Any(property.Setter.Parameters[0]); + else + setter.Generator.Castclass(property.Setter.Parameters[0]); + setter.Generator + .EmitCall(property.Setter, true) + .Ret(); + } + + var get = _builder.DefineMethod(types.IPropertyInfo, Array.Empty(), + name + "!Property", true, true, false); + + + var ctor = types.ClrPropertyInfo.Constructors.First(c => + c.Parameters.Count == 3 && c.IsStatic == false); + + var cacheMiss = get.Generator.DefineLabel(); + get.Generator + .Ldsfld(field) + .Brfalse(cacheMiss) + .Ldsfld(field) + .Ret() + .MarkLabel(cacheMiss) + .Ldstr(property.Name); + + void EmitFunc(IXamlIlEmitter emitter, IXamlIlMethod method, IXamlIlType del) + { + if (method == null) + emitter.Ldnull(); + else + { + emitter + .Ldnull() + .Ldftn(method) + .Newobj(del.Constructors.First(c => + c.Parameters.Count == 2 && + c.Parameters[0].Equals(context.Configuration.WellKnownTypes.Object))); + } + } + + EmitFunc(get.Generator, getter, ctor.Parameters[1]); + EmitFunc(get.Generator, setter, ctor.Parameters[2]); + get.Generator + .Newobj(ctor) + .Stsfld(field) + .Ldsfld(field) + .Ret(); + + lst.Add((property, get)); + return get; + } + + codeGen.EmitCall(Get()); + return types.IPropertyInfo; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index 610cda30c6..3ea474b672 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit 610cda30c69e32e83c8235060606480904c937bc +Subproject commit 3ea474b672ff63629300c36a9644caac2b74a1f2 diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs index 5398e76f63..913dd754f1 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs @@ -7,6 +7,7 @@ using System.Runtime.CompilerServices; using Avalonia.Controls; using Avalonia.Controls.Presenters; using Avalonia.Data.Converters; +using Avalonia.Data.Core; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; @@ -234,6 +235,17 @@ namespace Avalonia.Markup.Xaml.UnitTests Assert.Equal(100, XamlIlBugTestsStaticClassWithAttachedProperty.GetTestInt(tb)); } } + + [Fact] + public void Provide_Value_Target_Should_Provide_Clr_Property_Info() + { + var parsed = AvaloniaXamlLoader.Parse(@" +", typeof(XamlIlClassWithClrPropertyWithValue).Assembly); + Assert.Equal(6, parsed.Count); + } } public class XamlIlBugTestsEventHandlerCodeBehind : Window @@ -314,4 +326,22 @@ namespace Avalonia.Markup.Xaml.UnitTests return (int)control.GetValue(TestIntProperty); } } + + public class XamlIlCheckClrPropertyInfoExtension + { + public string ExpectedPropertyName { get; set; } + + public object ProvideValue(IServiceProvider prov) + { + var pvt = prov.GetService(); + var info = (ClrPropertyInfo)pvt.TargetProperty; + var v = (int)info.Get(pvt.TargetObject); + return v + 1; + } + } + + public class XamlIlClassWithClrPropertyWithValue + { + public int Count { get; set; }= 5; + } } From 048660ed8bacd61910375ed1f5236a8c4ad9c00f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 14 Jun 2019 11:59:04 +0300 Subject: [PATCH 002/711] Implemented PropertyPathGrammar --- .../Utilities/CharacterReader.cs | 24 +- .../Utilities/IdentifierParser.cs | 2 +- src/Avalonia.Base/Utilities/KeywordParser.cs | 34 +++ .../Parsers/PropertyParser.cs | 2 +- .../Markup/Parsers/ArgumentListParser.cs | 2 +- .../Markup/Parsers/ExpressionParser.cs | 2 +- .../Markup/Parsers/PropertyPathGrammar.cs | 217 ++++++++++++++++++ .../Markup/Parsers/SelectorGrammar.cs | 10 +- .../Data/Core/PropertyPathGrammarTests.cs | 103 +++++++++ 9 files changed, 384 insertions(+), 12 deletions(-) create mode 100644 src/Avalonia.Base/Utilities/KeywordParser.cs create mode 100644 src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs create mode 100644 tests/Avalonia.Base.UnitTests/Data/Core/PropertyPathGrammarTests.cs diff --git a/src/Avalonia.Base/Utilities/CharacterReader.cs b/src/Avalonia.Base/Utilities/CharacterReader.cs index 9be2d230d4..67a77f1c5c 100644 --- a/src/Avalonia.Base/Utilities/CharacterReader.cs +++ b/src/Avalonia.Base/Utilities/CharacterReader.cs @@ -19,7 +19,7 @@ namespace Avalonia.Utilities } public bool End => _s.IsEmpty; - public char Peek => _s[0]; + public char PeekOneOrThrow => _s[0]; public int Position { get; private set; } public char Take() { @@ -38,7 +38,7 @@ namespace Avalonia.Utilities public bool TakeIf(char c) { - if (Peek == c) + if (PeekOneOrThrow == c) { Take(); return true; @@ -51,7 +51,7 @@ namespace Avalonia.Utilities public bool TakeIf(Func condition) { - if (condition(Peek)) + if (condition(PeekOneOrThrow)) { Take(); return true; @@ -82,5 +82,23 @@ namespace Avalonia.Utilities Position += len; return span; } + + public ReadOnlySpan TryPeek(int count) + { + return _s.Slice(0, count); + } + + public ReadOnlySpan PeekWhitespace() + { + var trimmed = _s.TrimStart(); + return _s.Slice(0, _s.Length - trimmed.Length); + } + + public void Skip(int count) + { + if (_s.Length < count) + throw new IndexOutOfRangeException(); + _s = _s.Slice(count); + } } } diff --git a/src/Avalonia.Base/Utilities/IdentifierParser.cs b/src/Avalonia.Base/Utilities/IdentifierParser.cs index a57a2b7ba5..ab14ea2c58 100644 --- a/src/Avalonia.Base/Utilities/IdentifierParser.cs +++ b/src/Avalonia.Base/Utilities/IdentifierParser.cs @@ -13,7 +13,7 @@ namespace Avalonia.Utilities { public static ReadOnlySpan ParseIdentifier(this ref CharacterReader r) { - if (IsValidIdentifierStart(r.Peek)) + if (IsValidIdentifierStart(r.PeekOneOrThrow)) { return r.TakeWhile(IsValidIdentifierChar); } diff --git a/src/Avalonia.Base/Utilities/KeywordParser.cs b/src/Avalonia.Base/Utilities/KeywordParser.cs new file mode 100644 index 0000000000..5c7d415820 --- /dev/null +++ b/src/Avalonia.Base/Utilities/KeywordParser.cs @@ -0,0 +1,34 @@ +using System; + +namespace Avalonia.Utilities +{ +#if !BUILDTASK + public +#endif + static class KeywordParser + { + public static bool CheckKeyword(this ref CharacterReader r, string keyword) + { + return (CheckKeywordInternal(ref r, keyword) >= 0); + } + + static int CheckKeywordInternal(this ref CharacterReader r, string keyword) + { + var ws = r.PeekWhitespace(); + + var chars = r.TryPeek(ws.Length + keyword.Length); + if (chars.Slice(ws.Length).Equals(keyword.AsSpan(), StringComparison.Ordinal)) + return chars.Length; + return -1; + } + + public static bool TakeIfKeyword(this ref CharacterReader r, string keyword) + { + var l = CheckKeywordInternal(ref r, keyword); + if (l < 0) + return false; + r.Skip(l); + return true; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs b/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs index ea94012096..ebd6347a86 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs @@ -41,7 +41,7 @@ namespace Avalonia.Markup.Xaml.Parsers throw new ExpressionParseException(r.Position, $"Expected ')'."); } - throw new ExpressionParseException(r.Position, $"Unexpected '{r.Peek}'."); + throw new ExpressionParseException(r.Position, $"Unexpected '{r.PeekOneOrThrow}'."); } } else if (!r.End && r.TakeIf(':')) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs index cb9c81caa1..fd6f7b20bd 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs @@ -11,7 +11,7 @@ namespace Avalonia.Markup.Parsers { public static IList ParseArguments(this ref CharacterReader r, char open, char close, char delimiter = ',') { - if (r.Peek == open) + if (r.PeekOneOrThrow == open) { var result = new List(); diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index a1350a8393..2a737d36e9 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs @@ -304,7 +304,7 @@ namespace Avalonia.Markup.Parsers private static bool PeekOpenBracket(ref CharacterReader r) { - return !r.End && r.Peek == '['; + return !r.End && r.PeekOneOrThrow == '['; } private static bool ParseStreamOperator(ref CharacterReader r) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs new file mode 100644 index 0000000000..89490db2aa --- /dev/null +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs @@ -0,0 +1,217 @@ +using System; +using System.Collections.Generic; +using Avalonia.Data.Core; +using Avalonia.Utilities; + +namespace Avalonia.Markup.Parsers +{ +#if !BUILDTASK + public +#endif + class PropertyPathGrammar + { + private enum State + { + Start, + Next, + AfterProperty, + End + } + + public static IEnumerable Parse(string s) + { + var r = new CharacterReader(s.AsSpan()); + return Parse(ref r); + } + + private static IEnumerable Parse(ref CharacterReader r) + { + var state = State.Start; + var parsed = new List(); + while (state != State.End) + { + ISyntax syntax = null; + if (state == State.Start) + (state, syntax) = ParseStart(ref r); + else if (state == State.Next) + (state, syntax) = ParseNext(ref r); + else if (state == State.AfterProperty) + (state, syntax) = ParseAfterProperty(ref r); + + + if (syntax != null) + { + parsed.Add(syntax); + } + } + + if (state != State.End && r.End) + { + throw new ExpressionParseException(r.Position, "Unexpected end of property path"); + } + + return parsed; + } + + private static (State, ISyntax) ParseNext(ref CharacterReader r) + { + r.SkipWhitespace(); + if (r.End) + return (State.End, null); + + return ParseStart(ref r); + } + + private static (State, ISyntax) ParseStart(ref CharacterReader r) + { + r.SkipWhitespace(); + + if (r.TakeIf('(')) + return ParseTypeQualifiedProperty(ref r); + + return ParseProperty(ref r); + } + + private static (State, ISyntax) ParseTypeQualifiedProperty(ref CharacterReader r) + { + r.SkipWhitespace(); + const string error = + "Unable to parse qualified property name, expected `(ns:TypeName.PropertyName)` or `(TypeName.PropertyName)` after `(`"; + + var typeName = ParseXamlIdentifier(ref r); + + + if (!r.TakeIf('.')) + throw new ExpressionParseException(r.Position, error); + + var propertyName = r.ParseIdentifier(); + if (propertyName.IsEmpty) + throw new ExpressionParseException(r.Position, error); + + r.SkipWhitespace(); + if (!r.TakeIf(')')) + throw new ExpressionParseException(r.Position, + "Expected ')' after qualified property name " + + typeName.ns + ':' + typeName.name + + "." + propertyName.ToString()); + + return (State.AfterProperty, + new TypeQualifiedPropertySyntax + { + Name = propertyName.ToString(), + TypeName = typeName.name, + TypeNamespace = typeName.ns + }); + } + + static (string ns, string name) ParseXamlIdentifier(ref CharacterReader r) + { + var ident = r.ParseIdentifier(); + if (ident.IsEmpty) + throw new ExpressionParseException(r.Position, "Expected identifier"); + if (r.TakeIf(':')) + { + var part2 = r.ParseIdentifier(); + if (part2.IsEmpty) + throw new ExpressionParseException(r.Position, + "Expected the rest of the identifier after " + ident.ToString() + ":"); + return (ident.ToString(), part2.ToString()); + } + + return (null, ident.ToString()); + } + + private static (State, ISyntax) ParseProperty(ref CharacterReader r) + { + r.SkipWhitespace(); + var prop = r.ParseIdentifier(); + if (prop.IsEmpty) + throw new ExpressionParseException(r.Position, "Unable to parse property name"); + return (State.AfterProperty, new PropertySyntax {Name = prop.ToString()}); + } + + + private static (State, ISyntax) ParseAfterProperty(ref CharacterReader r) + { + r.SkipWhitespace(); + if (r.End) + return (State.End, null); + if (r.TakeIf('.')) + return (State.Next, ChildTraversalSyntax.Instance); + if (r.TakeIfKeyword(":=")) + return ParseEnsureType(ref r); + + if (r.TakeIfKeyword(":>") || r.TakeIfKeyword("as ")) + return ParseCastType(ref r); + + throw new ExpressionParseException(r.Position, "Unexpected character " + r.PeekOneOrThrow + " after property name"); + } + + private static (State, ISyntax) ParseEnsureType(ref CharacterReader r) + { + r.SkipWhitespace(); + var type = ParseXamlIdentifier(ref r); + return (State.AfterProperty, new EnsureTypeSyntax {TypeName = type.name, TypeNamespace = type.ns}); + } + + private static (State, ISyntax) ParseCastType(ref CharacterReader r) + { + r.SkipWhitespace(); + var type = ParseXamlIdentifier(ref r); + return (State.AfterProperty, new CastTypeSyntax {TypeName = type.name, TypeNamespace = type.ns}); + } + + public interface ISyntax + { + + } + + public class PropertySyntax : ISyntax + { + public string Name { get; set; } + + public override bool Equals(object obj) + => obj is PropertySyntax other + && other.Name == Name; + } + + public class TypeQualifiedPropertySyntax : ISyntax + { + public string Name { get; set; } + public string TypeName { get; set; } + public string TypeNamespace { get; set; } + + public override bool Equals(object obj) + => obj is TypeQualifiedPropertySyntax other + && other.Name == Name + && other.TypeName == TypeName + && other.TypeNamespace == TypeNamespace; + } + + public class ChildTraversalSyntax : ISyntax + { + public static ChildTraversalSyntax Instance { get; } = new ChildTraversalSyntax(); + public override bool Equals(object obj) => obj is ChildTraversalSyntax; + } + + public class EnsureTypeSyntax : ISyntax + { + public string TypeName { get; set; } + public string TypeNamespace { get; set; } + public override bool Equals(object obj) + => obj is EnsureTypeSyntax other + && other.TypeName == TypeName + && other.TypeNamespace == TypeNamespace; + } + + public class CastTypeSyntax : ISyntax + { + public string TypeName { get; set; } + public string TypeNamespace { get; set; } + public override bool Equals(object obj) + => obj is CastTypeSyntax other + && other.TypeName == TypeName + && other.TypeNamespace == TypeNamespace; + } + } +} diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index c57b8c4cf6..2de66d5501 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -123,7 +123,7 @@ namespace Avalonia.Markup.Parsers { return (State.Class, null); } - else if (r.TakeIf(char.IsWhiteSpace) || r.Peek == '>') + else if (r.TakeIf(char.IsWhiteSpace) || r.PeekOneOrThrow == '>') { return (State.Traversal, null); } @@ -139,7 +139,7 @@ namespace Avalonia.Markup.Parsers { return (State.Start, new CommaSyntax()); } - else if (end.HasValue && !r.End && r.Peek == end.Value) + else if (end.HasValue && !r.End && r.PeekOneOrThrow == end.Value) { return (State.End, null); } @@ -262,7 +262,7 @@ namespace Avalonia.Markup.Parsers if (!r.TakeIf('=')) { - throw new ExpressionParseException(r.Position, $"Expected '=', got '{r.Peek}'"); + throw new ExpressionParseException(r.Position, $"Expected '=', got '{r.PeekOneOrThrow}'"); } var value = r.TakeUntil(']'); @@ -281,7 +281,7 @@ namespace Avalonia.Markup.Parsers if (namespaceOrTypeName.IsEmpty) { - throw new ExpressionParseException(r.Position, $"Expected an identifier, got '{r.Peek}"); + throw new ExpressionParseException(r.Position, $"Expected an identifier, got '{r.PeekOneOrThrow}"); } if (!r.End && r.TakeIf('|')) @@ -311,7 +311,7 @@ namespace Avalonia.Markup.Parsers } else if (!r.TakeIf(')')) { - throw new ExpressionParseException(r.Position, $"Expected '{c}', got '{r.Peek}'."); + throw new ExpressionParseException(r.Position, $"Expected '{c}', got '{r.PeekOneOrThrow}'."); } } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/PropertyPathGrammarTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/PropertyPathGrammarTests.cs new file mode 100644 index 0000000000..99144b7381 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Data/Core/PropertyPathGrammarTests.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Markup.Parsers; +using Xunit; + +namespace Avalonia.Base.UnitTests.Data.Core +{ + public class PropertyPathGrammarTests + { + static void Check(string s, params PropertyPathGrammar.ISyntax[] expected) + { + var parsed = PropertyPathGrammar.Parse(s).ToList(); + Assert.Equal(expected.Length, parsed.Count); + for (var c = 0; c < parsed.Count; c++) + Assert.Equal(expected[c], parsed[c]); + } + + [Fact] + public void PropertyPath_Should_Support_Simple_Properties() + { + Check("SomeProperty", new PropertyPathGrammar.PropertySyntax {Name = "SomeProperty"}); + } + + [Fact] + public void PropertyPath_Should_Ignore_Trailing_Whitespace() + { + Check(" SomeProperty ", new PropertyPathGrammar.PropertySyntax {Name = "SomeProperty"}); + } + + [Fact] + public void PropertyPath_Should_Support_Qualified_Properties() + { + Check(" ( somens:SomeType.SomeProperty ) ", + new PropertyPathGrammar.TypeQualifiedPropertySyntax() + { + Name = "SomeProperty", TypeName = "SomeType", TypeNamespace = "somens" + }); + } + + [Fact] + public void PropertyPath_Should_Support_Property_Paths() + { + Check(" ( somens:SomeType.SomeProperty ).Child . SubChild ", + new PropertyPathGrammar.TypeQualifiedPropertySyntax() + { + Name = "SomeProperty", TypeName = "SomeType", TypeNamespace = "somens" + }, + PropertyPathGrammar.ChildTraversalSyntax.Instance, + new PropertyPathGrammar.PropertySyntax {Name = "Child"}, + PropertyPathGrammar.ChildTraversalSyntax.Instance, + new PropertyPathGrammar.PropertySyntax {Name = "SubChild"} + ); + } + + [Fact] + public void PropertyPath_Should_Support_Casts() + { + Check(" ( somens:SomeType.SomeProperty ) :> SomeType.Child as somens:SomeType . SubChild ", + new PropertyPathGrammar.TypeQualifiedPropertySyntax() + { + Name = "SomeProperty", TypeName = "SomeType", TypeNamespace = "somens" + }, + new PropertyPathGrammar.CastTypeSyntax + { + TypeName = "SomeType" + }, + PropertyPathGrammar.ChildTraversalSyntax.Instance, + new PropertyPathGrammar.PropertySyntax {Name = "Child"}, + new PropertyPathGrammar.CastTypeSyntax + { + TypeName = "SomeType", + TypeNamespace = "somens" + }, + PropertyPathGrammar.ChildTraversalSyntax.Instance, + new PropertyPathGrammar.PropertySyntax {Name = "SubChild"} + ); + } + + [Fact] + public void PropertyPath_Should_Support_Ensure_Type() + { + Check(" ( somens:SomeType.SomeProperty ) := SomeType.Child := somens:SomeType . SubChild ", + new PropertyPathGrammar.TypeQualifiedPropertySyntax() + { + Name = "SomeProperty", TypeName = "SomeType", TypeNamespace = "somens" + }, + new PropertyPathGrammar.EnsureTypeSyntax + { + TypeName = "SomeType" + }, + PropertyPathGrammar.ChildTraversalSyntax.Instance, + new PropertyPathGrammar.PropertySyntax {Name = "Child"}, + new PropertyPathGrammar.EnsureTypeSyntax + { + TypeName = "SomeType", + TypeNamespace = "somens" + }, + PropertyPathGrammar.ChildTraversalSyntax.Instance, + new PropertyPathGrammar.PropertySyntax {Name = "SubChild"} + ); + } + } +} From 8ad9a0f675a54f1a14bf00d5517d751abe65762b Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 14 Jun 2019 14:26:34 +0300 Subject: [PATCH 003/711] Compiled property paths --- src/Avalonia.Base/Data/Core/PropertyPath.cs | 91 +++++++ .../Utilities/CharacterReader.cs | 2 + src/Avalonia.Base/Utilities/KeywordParser.cs | 12 +- src/Avalonia.Styling/Styling/Setter.cs | 6 + .../Avalonia.Markup.Xaml.csproj | 1 + .../AvaloniaXamlIlCompiler.cs | 1 + .../AvaloniaXamlIlPropertyPathTransformer.cs | 250 ++++++++++++++++++ .../AvaloniaXamlIlSetterTransformer.cs | 35 ++- .../AvaloniaXamlIlWellKnownTypes.cs | 4 + .../XamlIlAvaloniaPropertyHelper.cs | 38 +-- .../XamlIlClrPropertyInfoHelper.cs | 39 ++- .../Avalonia.Markup.Xaml/XamlIl/xamlil.github | 2 +- .../Markup/Parsers/PropertyPathGrammar.cs | 24 +- .../Xaml/XamlIlTests.cs | 48 ++++ 14 files changed, 496 insertions(+), 57 deletions(-) create mode 100644 src/Avalonia.Base/Data/Core/PropertyPath.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlPropertyPathTransformer.cs diff --git a/src/Avalonia.Base/Data/Core/PropertyPath.cs b/src/Avalonia.Base/Data/Core/PropertyPath.cs new file mode 100644 index 0000000000..665953c4a1 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/PropertyPath.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Avalonia.Data.Core +{ + public class PropertyPath + { + public IReadOnlyList Elements { get; } + + public PropertyPath(IEnumerable elements) + { + Elements = elements.ToList(); + } + } + + public class PropertyPathBuilder + { + readonly List _elements = new List(); + + public PropertyPathBuilder Property(IPropertyInfo property) + { + _elements.Add(new PropertyPropertyPathElement(property)); + return this; + } + + + public PropertyPathBuilder ChildTraversal() + { + _elements.Add(new ChildTraversalPropertyPathElement()); + return this; + } + + public PropertyPathBuilder EnsureType(Type type) + { + _elements.Add(new EnsureTypePropertyPathElement(type)); + return this; + } + + public PropertyPathBuilder Cast(Type type) + { + _elements.Add(new CastTypePropertyPathElement(type)); + return this; + } + + public PropertyPath Build() + { + return new PropertyPath(_elements); + } + } + + public interface IPropertyPathElement + { + + } + + public class PropertyPropertyPathElement : IPropertyPathElement + { + public IPropertyInfo Property { get; } + + public PropertyPropertyPathElement(IPropertyInfo property) + { + Property = property; + } + } + + public class ChildTraversalPropertyPathElement : IPropertyPathElement + { + + } + + public class EnsureTypePropertyPathElement : IPropertyPathElement + { + public Type Type { get; } + + public EnsureTypePropertyPathElement(Type type) + { + Type = type; + } + } + + public class CastTypePropertyPathElement : IPropertyPathElement + { + public CastTypePropertyPathElement(Type type) + { + Type = type; + } + + public Type Type { get; } + } +} diff --git a/src/Avalonia.Base/Utilities/CharacterReader.cs b/src/Avalonia.Base/Utilities/CharacterReader.cs index 67a77f1c5c..3ba79c6866 100644 --- a/src/Avalonia.Base/Utilities/CharacterReader.cs +++ b/src/Avalonia.Base/Utilities/CharacterReader.cs @@ -85,6 +85,8 @@ namespace Avalonia.Utilities public ReadOnlySpan TryPeek(int count) { + if (_s.Length < count) + return ReadOnlySpan.Empty; return _s.Slice(0, count); } diff --git a/src/Avalonia.Base/Utilities/KeywordParser.cs b/src/Avalonia.Base/Utilities/KeywordParser.cs index 5c7d415820..34e1ea6f8e 100644 --- a/src/Avalonia.Base/Utilities/KeywordParser.cs +++ b/src/Avalonia.Base/Utilities/KeywordParser.cs @@ -17,11 +17,21 @@ namespace Avalonia.Utilities var ws = r.PeekWhitespace(); var chars = r.TryPeek(ws.Length + keyword.Length); - if (chars.Slice(ws.Length).Equals(keyword.AsSpan(), StringComparison.Ordinal)) + if (SpanEquals(chars.Slice(ws.Length), keyword.AsSpan())) return chars.Length; return -1; } + static bool SpanEquals(ReadOnlySpan left, ReadOnlySpan right) + { + if (left.Length != right.Length) + return false; + for(var c=0; c + /// Gets or sets the property path + /// + public PropertyPath PropertyPath { get; set; } /// /// Gets or sets the property value. diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 91d0feedfb..040ed57383 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -45,6 +45,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs index 0d0b0e6967..eadad60cde 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -44,6 +44,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions InsertBefore( new AvaloniaXamlIlSelectorTransformer(), + new AvaloniaXamlIlPropertyPathTransformer(), new AvaloniaXamlIlSetterTransformer(), new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), new AvaloniaXamlIlConstructorServiceProviderTransformer(), diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlPropertyPathTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlPropertyPathTransformer.cs new file mode 100644 index 0000000000..99ba264214 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlPropertyPathTransformer.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Markup.Parsers; +using XamlIl; +using XamlIl.Ast; +using XamlIl.Transform; +using XamlIl.Transform.Transformers; +using XamlIl.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlPropertyPathTransformer : IXamlIlAstTransformer + { + public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + { + if (node is XamlIlAstXamlPropertyValueNode pv + && pv.Values.Count == 1 + && pv.Values[0] is XamlIlAstTextNode text + && pv.Property.GetClrProperty().Getter.ReturnType.Equals(context.GetAvaloniaTypes().PropertyPath) + ) + { + var parentScope = context.ParentNodes().OfType() + .FirstOrDefault(); + if(parentScope == null) + throw new XamlIlParseException("No target type scope found for property path", text); + if (parentScope.ScopeType != AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style) + throw new XamlIlParseException("PropertyPath is currently only valid for styles", pv); + + + IEnumerable parsed; + try + { + parsed = PropertyPathGrammar.Parse(text.Text); + } + catch (Exception e) + { + throw new XamlIlParseException("Unable to parse PropertyPath: " + e.Message, text); + } + + var elements = new List(); + IXamlIlType currentType = parentScope.TargetType.GetClrType(); + + + var expectProperty = true; + var expectCast = true; + var expectTraversal = false; + var types = context.GetAvaloniaTypes(); + + IXamlIlType GetType(string ns, string name) + { + return XamlIlTypeReferenceResolver.ResolveType(context, $"{ns}:{name}", false, + text, true).GetClrType(); + } + + void HandleProperty(string name, string typeNamespace, string typeName) + { + if(!expectProperty || currentType == null) + throw new XamlIlParseException("Unexpected property node", text); + + var propertySearchType = + typeName != null ? GetType(typeNamespace, typeName) : currentType; + + IXamlIlPropertyPathElementNode prop = null; + var avaloniaPropertyFieldName = name + "Property"; + var avaloniaPropertyField = propertySearchType.GetAllFields().FirstOrDefault(f => + f.IsStatic && f.IsPublic && f.Name == avaloniaPropertyFieldName); + if (avaloniaPropertyField != null) + { + prop = new XamlIlAvaloniaPropertyPropertyPathElementNode(avaloniaPropertyField, + XamlIlAvaloniaPropertyHelper.GetAvaloniaPropertyType(avaloniaPropertyField, types, text)); + } + else + { + var clrProperty = propertySearchType.GetAllProperties().FirstOrDefault(p => p.Name == name); + prop = new XamlIClrPropertyPathElementNode(clrProperty); + } + + if (prop == null) + throw new XamlIlParseException( + $"Unable to resolve property {name} on type {propertySearchType.GetFqn()}", + text); + + currentType = prop.Type; + elements.Add(prop); + expectProperty = false; + expectTraversal = expectCast = true; + } + + foreach (var ge in parsed) + { + if (ge is PropertyPathGrammar.ChildTraversalSyntax) + { + if (!expectTraversal) + throw new XamlIlParseException("Unexpected child traversal .", text); + elements.Add(new XamlIlChildTraversalPropertyPathElementNode()); + expectTraversal = expectCast = false; + expectProperty = true; + } + else if (ge is PropertyPathGrammar.EnsureTypeSyntax ets) + { + if(!expectCast) + throw new XamlIlParseException("Unexpected cast node", text); + currentType = GetType(ets.TypeNamespace, ets.TypeName); + elements.Add(new XamlIlCastPropertyPathElementNode(currentType, true)); + expectProperty = false; + expectCast = expectTraversal = true; + } + else if (ge is PropertyPathGrammar.CastTypeSyntax cts) + { + if(!expectCast) + throw new XamlIlParseException("Unexpected cast node", text); + //TODO: Check if cast can be done + currentType = GetType(cts.TypeNamespace, cts.TypeName); + elements.Add(new XamlIlCastPropertyPathElementNode(currentType, false)); + expectProperty = false; + expectCast = expectTraversal = true; + } + else if (ge is PropertyPathGrammar.PropertySyntax ps) + { + HandleProperty(ps.Name, null, null); + } + else if (ge is PropertyPathGrammar.TypeQualifiedPropertySyntax tqps) + { + HandleProperty(tqps.Name, tqps.TypeNamespace, tqps.TypeName); + } + else + throw new XamlIlParseException("Unexpected node " + ge, text); + + } + var propertyPathNode = new XamlIlPropertyPathNode(text, elements, types); + if (propertyPathNode.Type == null) + throw new XamlIlParseException("Unexpected end of the property path", text); + pv.Values[0] = propertyPathNode; + } + + return node; + } + + interface IXamlIlPropertyPathElementNode + { + void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen); + IXamlIlType Type { get; } + } + + class XamlIlChildTraversalPropertyPathElementNode : IXamlIlPropertyPathElementNode + { + public void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + => codeGen.EmitCall( + context.GetAvaloniaTypes() + .PropertyPathBuilder.FindMethod(m => m.Name == "ChildTraversal")); + + public IXamlIlType Type => null; + } + + class XamlIlAvaloniaPropertyPropertyPathElementNode : IXamlIlPropertyPathElementNode + { + private readonly IXamlIlField _field; + + public XamlIlAvaloniaPropertyPropertyPathElementNode(IXamlIlField field, IXamlIlType propertyType) + { + _field = field; + Type = propertyType; + } + + public void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + => codeGen + .Ldsfld(_field) + .EmitCall(context.GetAvaloniaTypes() + .PropertyPathBuilder.FindMethod(m => m.Name == "Property")); + + public IXamlIlType Type { get; } + } + + class XamlIClrPropertyPathElementNode : IXamlIlPropertyPathElementNode + { + private readonly IXamlIlProperty _property; + + public XamlIClrPropertyPathElementNode(IXamlIlProperty property) + { + _property = property; + } + + public void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + context.Configuration.GetExtra() + .Emit(context, codeGen, _property); + + codeGen.EmitCall(context.GetAvaloniaTypes() + .PropertyPathBuilder.FindMethod(m => m.Name == "Property")); + } + + public IXamlIlType Type => _property.Getter?.ReturnType ?? _property.Setter?.Parameters[0]; + } + + class XamlIlCastPropertyPathElementNode : IXamlIlPropertyPathElementNode + { + private readonly IXamlIlType _type; + private readonly bool _ensureType; + + public XamlIlCastPropertyPathElementNode(IXamlIlType type, bool ensureType) + { + _type = type; + _ensureType = ensureType; + } + + public void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + codeGen + .Ldtype(_type) + .EmitCall(context.GetAvaloniaTypes() + .PropertyPathBuilder.FindMethod(m => m.Name == (_ensureType ? "EnsureType" : "Cast"))); + } + + public IXamlIlType Type => _type; + } + + class XamlIlPropertyPathNode : XamlIlAstNode, IXamlIlPropertyPathNode, IXamlIlAstEmitableNode + { + private readonly List _elements; + private readonly AvaloniaXamlIlWellKnownTypes _types; + + public XamlIlPropertyPathNode(IXamlIlLineInfo lineInfo, + List elements, + AvaloniaXamlIlWellKnownTypes types) : base(lineInfo) + { + _elements = elements; + _types = types; + Type = new XamlIlAstClrTypeReference(this, types.PropertyPath, false); + } + + public IXamlIlAstTypeReference Type { get; } + public IXamlIlType PropertyType => _elements.LastOrDefault()?.Type; + public XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + codeGen + .Newobj(_types.PropertyPathBuilder.FindConstructor()); + foreach(var e in _elements) + e.Emit(context, codeGen); + codeGen.EmitCall(_types.PropertyPathBuilder.FindMethod(m => m.Name == "Build")); + return XamlIlNodeEmitResult.Type(0, _types.PropertyPath); + } + } + } + + interface IXamlIlPropertyPathNode : IXamlIlAstValueNode + { + IXamlIlType PropertyType { get; } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs index 629e2562d3..468717f281 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Avalonia.Data.Core; using XamlIl; using XamlIl.Ast; using XamlIl.Transform; @@ -33,29 +34,39 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers throw new XamlIlParseException( "Can not resolve parent Style Selector type", node); - + IXamlIlType propType = null; var property = @on.Children.OfType() .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Property"); - if (property == null) - throw new XamlIlParseException("Setter without a property is not valid", node); + if (property != null) + { - var propertyName = property.Values.OfType().FirstOrDefault()?.Text; - if (propertyName == null) - throw new XamlIlParseException("Setter.Property must be a string", node); + var propertyName = property.Values.OfType().FirstOrDefault()?.Text; + if (propertyName == null) + throw new XamlIlParseException("Setter.Property must be a string", node); - var avaloniaPropertyNode = XamlIlAvaloniaPropertyHelper.CreateNode(context, propertyName, - new XamlIlAstClrTypeReference(selector, selector.TargetType, false), property.Values[0]); - property.Values = new List + var avaloniaPropertyNode = XamlIlAvaloniaPropertyHelper.CreateNode(context, propertyName, + new XamlIlAstClrTypeReference(selector, selector.TargetType, false), property.Values[0]); + property.Values = new List {avaloniaPropertyNode}; + propType = avaloniaPropertyNode.AvaloniaPropertyType; + } + else { - avaloniaPropertyNode - }; + var propertyPath = on.Children.OfType() + .FirstOrDefault(x => x.Property.GetClrProperty().Name == "PropertyPath"); + if (propertyPath == null) + throw new XamlIlParseException("Setter without a property or property path is not valid", node); + if (propertyPath.Values[0] is IXamlIlPropertyPathNode ppn + && ppn.PropertyType != null) + propType = ppn.PropertyType; + else + throw new XamlIlParseException("Unable to get the property path property type", node); + } var valueProperty = on.Children .OfType().FirstOrDefault(p => p.Property.GetClrProperty().Name == "Value"); if (valueProperty?.Values?.Count == 1 && valueProperty.Values[0] is XamlIlAstTextNode) { - var propType = avaloniaPropertyNode.AvaloniaPropertyType; if (!XamlIlTransformHelpers.TryGetCorrectlyTypedValue(context, valueProperty.Values[0], propType, out var converted)) throw new XamlIlParseException( diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 4c118899dc..f9f59dfb87 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -21,6 +21,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlIlType UnsetValueType { get; } public IXamlIlType IPropertyInfo { get; } public IXamlIlType ClrPropertyInfo { get; } + public IXamlIlType PropertyPath { get; } + public IXamlIlType PropertyPathBuilder { get; } public AvaloniaXamlIlWellKnownTypes(XamlIlTransformerConfiguration cfg) { @@ -43,6 +45,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers false, AvaloniaProperty, XamlIlTypes.Object, BindingPriority); IPropertyInfo = cfg.TypeSystem.GetType("Avalonia.Data.Core.IPropertyInfo"); ClrPropertyInfo = cfg.TypeSystem.GetType("Avalonia.Data.Core.ClrPropertyInfo"); + PropertyPath = cfg.TypeSystem.GetType("Avalonia.Data.Core.PropertyPath"); + PropertyPathBuilder = cfg.TypeSystem.GetType("Avalonia.Data.Core.PropertyPathBuilder"); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs index e0783364db..bc7c58bbb4 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs @@ -94,6 +94,26 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions context.Configuration.TypeSystem.GetType("Avalonia.AvaloniaProperty"), clrProperty); } + + public static IXamlIlType GetAvaloniaPropertyType(IXamlIlField field, + AvaloniaXamlIlWellKnownTypes types, IXamlIlLineInfo lineInfo) + { + var avaloniaPropertyType = field.FieldType; + while (avaloniaPropertyType != null) + { + if (avaloniaPropertyType.GenericTypeDefinition?.Equals(types.AvaloniaPropertyT) == true) + { + return avaloniaPropertyType.GenericArguments[0]; + } + + avaloniaPropertyType = avaloniaPropertyType.BaseType; + } + + throw new XamlIlParseException( + $"{field.Name}'s type {field.FieldType} doesn't inherit from AvaloniaProperty, make sure to use typed properties", + lineInfo); + + } } interface IXamlIlAvaloniaPropertyNode : IXamlIlAstValueNode @@ -132,22 +152,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions IXamlIlLineInfo lineInfo, IXamlIlField field) : base(lineInfo) { _field = field; - var avaloniaPropertyType = field.FieldType; - while (avaloniaPropertyType != null) - { - if (avaloniaPropertyType.GenericTypeDefinition?.Equals(types.AvaloniaPropertyT) == true) - { - AvaloniaPropertyType = avaloniaPropertyType.GenericArguments[0]; - return; - } - - avaloniaPropertyType = avaloniaPropertyType.BaseType; - } - - throw new XamlIlParseException( - $"{field.Name}'s type {field.FieldType} doesn't inherit from AvaloniaProperty, make sure to use typed properties", - lineInfo); - + AvaloniaPropertyType = XamlIlAvaloniaPropertyHelper.GetAvaloniaPropertyType(field, + types, lineInfo); } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs index 5b681e94a9..95ec8536fc 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs @@ -8,21 +8,6 @@ using XamlIl.TypeSystem; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { - class XamlIlClrPropertyInfoHelper - { - - class ClrPropertyInfoNode : XamlIlAstNode, IXamlIlAstValueNode - { - public ClrPropertyInfoNode(AvaloniaXamlIlWellKnownTypes types, IXamlIlLineInfo lineInfo) : - base(lineInfo) - { - Type = new XamlIlAstClrTypeReference(this, types.XamlIlTypes.Object, false); - } - - public IXamlIlAstTypeReference Type { get; } - } - } - class XamlIlClrPropertyInfoEmitter { private readonly IXamlIlTypeBuilder _builder; @@ -62,16 +47,25 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions var field = _builder.DefineField(types.IPropertyInfo, name + "!Field", false, true); + void Load(IXamlIlMethod m, IXamlIlEmitter cg) + { + cg + .Ldarg_0(); + if (m.DeclaringType.IsValueType) + cg.Unbox(m.DeclaringType); + else + cg.Castclass(m.DeclaringType); + } + var getter = property.Getter == null ? null : _builder.DefineMethod(types.XamlIlTypes.Object, new[] {types.XamlIlTypes.Object}, name + "!Getter", false, true, false); if (getter != null) { - getter.Generator - .Ldarg_0() - .Castclass(property.Getter.DeclaringType) - .EmitCall(property.Getter); + Load(property.Getter, getter.Generator); + + getter.Generator.EmitCall(property.Getter); if (property.Getter.ReturnType.IsValueType) getter.Generator.Box(property.Getter.ReturnType); getter.Generator.Ret(); @@ -84,10 +78,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions name + "!Setter", false, true, false); if (setter != null) { - setter.Generator - .Ldarg_0() - .Castclass(property.Setter.DeclaringType) - .Ldarg(1); + Load(property.Setter, setter.Generator); + + setter.Generator.Ldarg(1); if (property.Setter.Parameters[0].IsValueType) setter.Generator.Unbox_Any(property.Setter.Parameters[0]); else diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index 3ea474b672..9ae023a988 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit 3ea474b672ff63629300c36a9644caac2b74a1f2 +Subproject commit 9ae023a988d1b419e19386cfd3448d8e604bbc7e diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs index 89490db2aa..1f0f421233 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs @@ -64,6 +64,8 @@ namespace Avalonia.Markup.Parsers private static (State, ISyntax) ParseStart(ref CharacterReader r) { + if (TryParseCasts(ref r, out var rv)) + return rv; r.SkipWhitespace(); if (r.TakeIf('(')) @@ -130,19 +132,33 @@ namespace Avalonia.Markup.Parsers return (State.AfterProperty, new PropertySyntax {Name = prop.ToString()}); } + private static bool TryParseCasts(ref CharacterReader r, out (State, ISyntax) rv) + { + if (r.TakeIfKeyword(":=")) + rv = ParseEnsureType(ref r); + else if (r.TakeIfKeyword(":>") || r.TakeIfKeyword("as ")) + rv = ParseCastType(ref r); + else + { + rv = default; + return false; + } + return true; + } + private static (State, ISyntax) ParseAfterProperty(ref CharacterReader r) { + if (TryParseCasts(ref r, out var rv)) + return rv; + r.SkipWhitespace(); if (r.End) return (State.End, null); if (r.TakeIf('.')) return (State.Next, ChildTraversalSyntax.Instance); - if (r.TakeIfKeyword(":=")) - return ParseEnsureType(ref r); + - if (r.TakeIfKeyword(":>") || r.TakeIfKeyword("as ")) - return ParseCastType(ref r); throw new ExpressionParseException(r.Position, "Unexpected character " + r.PeekOneOrThrow + " after property name"); } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs index 913dd754f1..6bc7f33039 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs @@ -11,6 +11,7 @@ using Avalonia.Data.Core; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; +using Avalonia.Styling; using Avalonia.Threading; using Avalonia.UnitTests; using Avalonia.VisualTree; @@ -246,6 +247,53 @@ namespace Avalonia.Markup.Xaml.UnitTests />", typeof(XamlIlClassWithClrPropertyWithValue).Assembly); Assert.Equal(6, parsed.Count); } + + [Fact] + public void Should_Provide_PropertyPath_For_Setters() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parsed = AvaloniaXamlLoader.Parse"); + var s1e = ((Setter)parsed.Setters[0]).PropertyPath.Elements; + var s2e = ((Setter)parsed.Setters[1]).PropertyPath.Elements; + var s3e = ((Setter)parsed.Setters[2]).PropertyPath.Elements; + + Assert.Equal(typeof(Visual), ((CastTypePropertyPathElement)s1e[0]).Type); + Assert.IsType(s1e[1]); + Assert.Equal("Bounds", ((AvaloniaProperty)((PropertyPropertyPathElement)s1e[2]).Property).Name); + Assert.IsType(s1e[3]); + var bottomRight = ((PropertyPropertyPathElement)s1e[4]).Property; + Assert.IsType(s1e[5]); + var pointX = ((PropertyPropertyPathElement)s1e[6]).Property; + + var orect = (object)(new Rect(100, 100, 200, 200)); + var point = bottomRight.Get(orect); + var x = pointX.Get(point); + Assert.Equal(300, (double)x); + + Assert.Equal(typeof(Visual), ((CastTypePropertyPathElement)s2e[0]).Type); + Assert.IsType(s2e[1]); + Assert.Equal("RenderTransform", ((AvaloniaProperty)((PropertyPropertyPathElement)s2e[2]).Property).Name); + Assert.Equal(typeof(ScaleTransform), ((EnsureTypePropertyPathElement)s2e[3]).Type); + Assert.IsType(s2e[4]); + Assert.Equal("ScaleX", ((AvaloniaProperty)((PropertyPropertyPathElement)s2e[5]).Property).Name); + + + var s3fqp = (AvaloniaProperty)((PropertyPropertyPathElement)s3e[0]).Property; + Assert.Equal("RenderTransform", s3fqp.Name); + Assert.Equal(typeof(Visual), s3fqp.OwnerType); + Assert.Equal(typeof(ScaleTransform), ((EnsureTypePropertyPathElement)s3e[1]).Type); + Assert.IsType(s3e[2]); + Assert.Equal("ScaleX", ((AvaloniaProperty)((PropertyPropertyPathElement)s3e[3]).Property).Name); + + + } + } } public class XamlIlBugTestsEventHandlerCodeBehind : Window From bc6502c0cd67415486339273bb738b1cebfb02ab Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 14 Jun 2019 14:34:32 +0300 Subject: [PATCH 004/711] Missing file --- src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index 582e4499c5..1b4424a050 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -30,6 +30,9 @@ Markup/%(RecursiveDir)%(FileName)%(Extension) + + Markup/%(RecursiveDir)%(FileName)%(Extension) + Markup/%(RecursiveDir)%(FileName)%(Extension) @@ -38,11 +41,13 @@ Markup/%(RecursiveDir)%(FileName)%(Extension) - + Markup/%(RecursiveDir)%(FileName)%(Extension) - + + Markup/%(RecursiveDir)%(FileName)%(Extension) + From 6f158f463459c72731e6e382b5f9a043353c6002 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 14 Jun 2019 14:51:53 +0300 Subject: [PATCH 005/711] Do not crash --- src/Avalonia.Base/Utilities/KeywordParser.cs | 2 ++ .../Transformers/AvaloniaXamlIlPropertyPathTransformer.cs | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Utilities/KeywordParser.cs b/src/Avalonia.Base/Utilities/KeywordParser.cs index 34e1ea6f8e..16ef95f5f4 100644 --- a/src/Avalonia.Base/Utilities/KeywordParser.cs +++ b/src/Avalonia.Base/Utilities/KeywordParser.cs @@ -17,6 +17,8 @@ namespace Avalonia.Utilities var ws = r.PeekWhitespace(); var chars = r.TryPeek(ws.Length + keyword.Length); + if (chars.IsEmpty) + return -1; if (SpanEquals(chars.Slice(ws.Length), keyword.AsSpan())) return chars.Length; return -1; diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlPropertyPathTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlPropertyPathTransformer.cs index 99ba264214..1d2a4ea50b 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlPropertyPathTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlPropertyPathTransformer.cs @@ -17,7 +17,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers if (node is XamlIlAstXamlPropertyValueNode pv && pv.Values.Count == 1 && pv.Values[0] is XamlIlAstTextNode text - && pv.Property.GetClrProperty().Getter.ReturnType.Equals(context.GetAvaloniaTypes().PropertyPath) + && pv.Property.GetClrProperty().Getter?.ReturnType + .Equals(context.GetAvaloniaTypes().PropertyPath) == true ) { var parentScope = context.ParentNodes().OfType() From 30066c5e7093207e95022ae7c5620ea1a0ed0ec7 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sun, 23 Jun 2019 17:17:03 -0500 Subject: [PATCH 006/711] Convert current binding parsing to create an AST. --- .../Avalonia.Markup/Avalonia.Markup.csproj | 6 + .../Parsers/BindingExpressionGrammar.cs | 386 ++++++++++++++++++ .../Markup/Parsers/ExpressionParser.cs | 341 ++-------------- 3 files changed, 435 insertions(+), 298 deletions(-) create mode 100644 src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index 673eb51901..aca39dd8b0 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -3,6 +3,12 @@ netstandard2.0 Avalonia + + + + + + diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs new file mode 100644 index 0000000000..b8308155fe --- /dev/null +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs @@ -0,0 +1,386 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Data.Core; +using Avalonia.Markup.Parsers.Nodes; +using Avalonia.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Avalonia.Markup.Parsers +{ + internal enum SourceMode + { + Data, + Control + } + internal static class BindingExpressionGrammar + { + public static (IList Nodes, SourceMode Mode) Parse(ref CharacterReader r) + { + var nodes = new List(); + var state = State.Start; + var mode = SourceMode.Data; + + while (!r.End && state != State.End) + { + switch (state) + { + case State.Start: + state = ParseStart(ref r, nodes); + break; + + case State.AfterMember: + state = ParseAfterMember(ref r, nodes); + break; + + case State.BeforeMember: + state = ParseBeforeMember(ref r, nodes); + break; + + case State.AttachedProperty: + state = ParseAttachedProperty(ref r, nodes); + break; + + case State.Indexer: + state = ParseIndexer(ref r, nodes); + break; + + case State.ElementName: + state = ParseElementName(ref r, nodes); + mode = SourceMode.Control; + break; + + case State.RelativeSource: + state = ParseRelativeSource(ref r, nodes); + mode = SourceMode.Control; + break; + } + } + + if (state == State.BeforeMember) + { + throw new ExpressionParseException(r.Position, "Unexpected end of expression."); + } + + return (nodes, mode); + } + + private static State ParseStart(ref CharacterReader r, IList nodes) + { + if (ParseNot(ref r)) + { + nodes.Add(new NotNode()); + return State.Start; + } + + else if (ParseSharp(ref r)) + { + return State.ElementName; + } + else if (ParseDollarSign(ref r)) + { + return State.RelativeSource; + } + else if (ParseOpenBrace(ref r)) + { + return State.AttachedProperty; + } + else if (PeekOpenBracket(ref r)) + { + return State.Indexer; + } + else if (ParseDot(ref r)) + { + nodes.Add(new EmptyExpressionNode()); + return State.End; + } + else + { + var identifier = r.ParseIdentifier(); + + if (!identifier.IsEmpty) + { + nodes.Add(new PropertyNameNode { PropertyName = identifier.ToString() }); + return State.AfterMember; + } + } + + return State.End; + } + + private static State ParseAfterMember(ref CharacterReader r, IList nodes) + { + if (ParseMemberAccessor(ref r)) + { + return State.BeforeMember; + } + else if (ParseStreamOperator(ref r)) + { + nodes.Add(new StreamNode()); + return State.AfterMember; + } + else if (PeekOpenBracket(ref r)) + { + return State.Indexer; + } + + return State.End; + } + + private static State ParseBeforeMember(ref CharacterReader r, IList nodes) + { + if (ParseOpenBrace(ref r)) + { + return State.AttachedProperty; + } + else + { + var identifier = r.ParseIdentifier(); + + if (!identifier.IsEmpty) + { + nodes.Add(new PropertyNameNode { PropertyName = identifier.ToString() }); + return State.AfterMember; + } + + return State.End; + } + } + + private static State ParseAttachedProperty(ref CharacterReader r, List nodes) + { + var (ns, owner) = ParseTypeName(ref r); + + if (r.End || !r.TakeIf('.')) + { + throw new ExpressionParseException(r.Position, "Invalid attached property name."); + } + + var name = r.ParseIdentifier(); + + if (r.End || !r.TakeIf(')')) + { + throw new ExpressionParseException(r.Position, "Expected ')'."); + } + + nodes.Add(new AttachedPropertyNameNode + { + Namespace = ns.ToString(), + TypeName = owner.ToString(), + PropertyName = name.ToString() + }); + return State.AfterMember; + } + + private static State ParseIndexer(ref CharacterReader r, List nodes) + { + var args = r.ParseArguments('[', ']'); + + if (args.Count == 0) + { + throw new ExpressionParseException(r.Position, "Indexer may not be empty."); + } + + nodes.Add(new IndexerNode { Arguments = args }); + return State.AfterMember; + } + + private static State ParseElementName(ref CharacterReader r, List nodes) + { + var name = r.ParseIdentifier(); + + if (name == null) + { + throw new ExpressionParseException(r.Position, "Element name expected after '#'."); + } + + nodes.Add(new NameNode { Name = name.ToString() }); + return State.AfterMember; + } + + private static State ParseRelativeSource(ref CharacterReader r, List nodes) + { + var mode = r.ParseIdentifier(); + + if (mode.Equals("self".AsSpan(), StringComparison.InvariantCulture)) + { + nodes.Add(new SelfNode()); + } + else if (mode.Equals("parent".AsSpan(), StringComparison.InvariantCulture)) + { + string ancestorNamespace = null; + string ancestorType = null; + var ancestorLevel = 0; + if (PeekOpenBracket(ref r)) + { + var args = r.ParseArguments('[', ']', ';'); + if (args.Count > 2 || args.Count == 0) + { + throw new ExpressionParseException(r.Position, "Too many arguments in RelativeSource syntax sugar"); + } + else if (args.Count == 1) + { + if (int.TryParse(args[0], out int level)) + { + ancestorType = null; + ancestorLevel = level; + } + else + { + var reader = new CharacterReader(args[0].AsSpan()); + (ancestorNamespace, ancestorType) = ParseTypeName(ref reader); + } + } + else + { + var reader = new CharacterReader(args[0].AsSpan()); + (ancestorNamespace, ancestorType) = ParseTypeName(ref reader); + ancestorLevel = int.Parse(args[1]); + } + } + nodes.Add(new AncestorNode + { + Namespace = ancestorNamespace, + TypeName = ancestorType, + Level = ancestorLevel + }); + } + else + { + throw new ExpressionParseException(r.Position, "Unknown RelativeSource mode."); + } + + return State.AfterMember; + } + + private static TypeName ParseTypeName(ref CharacterReader r) + { + ReadOnlySpan ns, typeName; + ns = ReadOnlySpan.Empty; + var typeNameOrNamespace = r.ParseIdentifier(); + + if (!r.End && r.TakeIf(':')) + { + ns = typeNameOrNamespace; + typeName = r.ParseIdentifier(); + } + else + { + typeName = typeNameOrNamespace; + } + + return new TypeName(ns, typeName); + } + + private static bool ParseNot(ref CharacterReader r) + { + return !r.End && r.TakeIf('!'); + } + + private static bool ParseMemberAccessor(ref CharacterReader r) + { + return !r.End && r.TakeIf('.'); + } + + private static bool ParseOpenBrace(ref CharacterReader r) + { + return !r.End && r.TakeIf('('); + } + + private static bool PeekOpenBracket(ref CharacterReader r) + { + return !r.End && r.PeekOneOrThrow == '['; + } + + private static bool ParseStreamOperator(ref CharacterReader r) + { + return !r.End && r.TakeIf('^'); + } + + private static bool ParseDollarSign(ref CharacterReader r) + { + return !r.End && r.TakeIf('$'); + } + + private static bool ParseSharp(ref CharacterReader r) + { + return !r.End && r.TakeIf('#'); + } + + private static bool ParseDot(ref CharacterReader r) + { + return !r.End && r.TakeIf('.'); + } + + private enum State + { + Start, + RelativeSource, + ElementName, + AfterMember, + BeforeMember, + AttachedProperty, + Indexer, + End, + } + + private readonly ref struct TypeName + { + public TypeName(ReadOnlySpan ns, ReadOnlySpan typeName) + { + Namespace = ns; + Type = typeName; + } + + public readonly ReadOnlySpan Namespace; + public readonly ReadOnlySpan Type; + + public void Deconstruct(out string ns, out string typeName) + { + ns = Namespace.ToString(); + typeName = Type.ToString(); + } + } + + public interface INode {} + + public class EmptyExpressionNode : INode { } + + public class PropertyNameNode : INode + { + public string PropertyName { get; set; } + } + + public class AttachedPropertyNameNode : INode + { + public string Namespace { get; set; } + public string TypeName { get; set; } + public string PropertyName { get; set; } + } + + public class IndexerNode : INode + { + public IList Arguments { get; set; } + } + + public class NotNode : INode {} + + public class StreamNode : INode {} + + public class SelfNode : INode {} + + public class NameNode : INode + { + public string Name { get; set; } + } + + public class AncestorNode : INode + { + public string Namespace { get; set; } + public string TypeName { get; set; } + public int Level { get; set; } + } + } +} diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index 2a737d36e9..fe58ac139e 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs @@ -10,12 +10,6 @@ using System.Linq; namespace Avalonia.Markup.Parsers { - internal enum SourceMode - { - Data, - Control - } - internal class ExpressionParser { private readonly bool _enableValidation; @@ -29,332 +23,83 @@ namespace Avalonia.Markup.Parsers public (ExpressionNode Node, SourceMode Mode) Parse(ref CharacterReader r) { - var nodes = new List(); - var state = State.Start; - var mode = SourceMode.Data; + ExpressionNode node = null; + var (astNodes, mode) = BindingExpressionGrammar.Parse(ref r); - while (!r.End && state != State.End) + foreach (var astNode in astNodes) { - switch (state) + ExpressionNode nextNode = null; + switch (astNode) { - case State.Start: - state = ParseStart(ref r, nodes); + case BindingExpressionGrammar.EmptyExpressionNode _: + nextNode = new EmptyExpressionNode(); break; - - case State.AfterMember: - state = ParseAfterMember(ref r, nodes); + case BindingExpressionGrammar.NotNode _: + nextNode = new LogicalNotNode(); break; - - case State.BeforeMember: - state = ParseBeforeMember(ref r, nodes); + case BindingExpressionGrammar.StreamNode _: + nextNode = new StreamNode(); break; - - case State.AttachedProperty: - state = ParseAttachedProperty(ref r, nodes); + case BindingExpressionGrammar.PropertyNameNode propName: + nextNode = new PropertyAccessorNode(propName.PropertyName, _enableValidation); break; - - case State.Indexer: - state = ParseIndexer(ref r, nodes); + case BindingExpressionGrammar.IndexerNode indexer: + nextNode = new StringIndexerNode(indexer.Arguments); break; - - case State.ElementName: - state = ParseElementName(ref r, nodes); - mode = SourceMode.Control; + case BindingExpressionGrammar.AttachedPropertyNameNode attachedProp: + nextNode = ParseAttachedProperty(attachedProp); break; - - case State.RelativeSource: - state = ParseRelativeSource(ref r, nodes); - mode = SourceMode.Control; + case BindingExpressionGrammar.SelfNode _: + nextNode = new SelfNode(); + break; + case BindingExpressionGrammar.AncestorNode ancestor: + nextNode = ParseFindAncestor(ancestor); + break; + case BindingExpressionGrammar.NameNode elementName: + nextNode = new ElementNameNode(elementName.Name); break; } - } - - if (state == State.BeforeMember) - { - throw new ExpressionParseException(r.Position, "Unexpected end of expression."); - } - - for (int n = 0; n < nodes.Count - 1; ++n) - { - nodes[n].Next = nodes[n + 1]; - } - - return (nodes.FirstOrDefault(), mode); - } - - private State ParseStart(ref CharacterReader r, IList nodes) - { - if (ParseNot(ref r)) - { - nodes.Add(new LogicalNotNode()); - return State.Start; - } - - else if (ParseSharp(ref r)) - { - return State.ElementName; - } - else if (ParseDollarSign(ref r)) - { - return State.RelativeSource; - } - else if (ParseOpenBrace(ref r)) - { - return State.AttachedProperty; - } - else if (PeekOpenBracket(ref r)) - { - return State.Indexer; - } - else if (ParseDot(ref r)) - { - nodes.Add(new EmptyExpressionNode()); - return State.End; - } - else - { - var identifier = r.ParseIdentifier(); - - if (!identifier.IsEmpty) + if (node is null) { - nodes.Add(new PropertyAccessorNode(identifier.ToString(), _enableValidation)); - return State.AfterMember; + node = nextNode; } - } - - return State.End; - } - - private static State ParseAfterMember(ref CharacterReader r, IList nodes) - { - if (ParseMemberAccessor(ref r)) - { - return State.BeforeMember; - } - else if (ParseStreamOperator(ref r)) - { - nodes.Add(new StreamNode()); - return State.AfterMember; - } - else if (PeekOpenBracket(ref r)) - { - return State.Indexer; - } - - return State.End; - } - - private State ParseBeforeMember(ref CharacterReader r, IList nodes) - { - if (ParseOpenBrace(ref r)) - { - return State.AttachedProperty; - } - else - { - var identifier = r.ParseIdentifier(); - - if (!identifier.IsEmpty) + else { - nodes.Add(new PropertyAccessorNode(identifier.ToString(), _enableValidation)); - return State.AfterMember; + node.Next = nextNode; } - - return State.End; - } - } - - private State ParseAttachedProperty(ref CharacterReader r, List nodes) - { - var (ns, owner) = ParseTypeName(ref r); - - if (r.End || !r.TakeIf('.')) - { - throw new ExpressionParseException(r.Position, "Invalid attached property name."); - } - - var name = r.ParseIdentifier(); - - if (r.End || !r.TakeIf(')')) - { - throw new ExpressionParseException(r.Position, "Expected ')'."); - } - - if (_typeResolver == null) - { - throw new InvalidOperationException("Cannot parse a binding path with an attached property without a type resolver. Maybe you can use a LINQ Expression binding path instead?"); - } - - var property = AvaloniaPropertyRegistry.Instance.FindRegistered(_typeResolver(ns.ToString(), owner.ToString()), name.ToString()); - - nodes.Add(new AvaloniaPropertyAccessorNode(property, _enableValidation)); - return State.AfterMember; - } - - private State ParseIndexer(ref CharacterReader r, List nodes) - { - var args = r.ParseArguments('[', ']'); - - if (args.Count == 0) - { - throw new ExpressionParseException(r.Position, "Indexer may not be empty."); } - nodes.Add(new StringIndexerNode(args)); - return State.AfterMember; + return (node, mode); } - private State ParseElementName(ref CharacterReader r, List nodes) + private FindAncestorNode ParseFindAncestor(BindingExpressionGrammar.AncestorNode node) { - var name = r.ParseIdentifier(); + Type ancestorType = null; + var ancestorLevel = node.Level; - if (name == null) + if (!(node.Namespace is null) && !(node.TypeName is null)) { - throw new ExpressionParseException(r.Position, "Element name expected after '#'."); - } - - nodes.Add(new ElementNameNode(name.ToString())); - return State.AfterMember; - } - - private State ParseRelativeSource(ref CharacterReader r, List nodes) - { - var mode = r.ParseIdentifier(); - - if (mode.Equals("self".AsSpan(), StringComparison.InvariantCulture)) - { - nodes.Add(new SelfNode()); - } - else if (mode.Equals("parent".AsSpan(), StringComparison.InvariantCulture)) - { - Type ancestorType = null; - var ancestorLevel = 0; - if (PeekOpenBracket(ref r)) + if (_typeResolver == null) { - var args = r.ParseArguments('[', ']', ';'); - if (args.Count > 2 || args.Count == 0) - { - throw new ExpressionParseException(r.Position, "Too many arguments in RelativeSource syntax sugar"); - } - else if (args.Count == 1) - { - if (int.TryParse(args[0], out int level)) - { - ancestorType = null; - ancestorLevel = level; - } - else - { - var reader = new CharacterReader(args[0].AsSpan()); - var typeName = ParseTypeName(ref reader); - ancestorType = _typeResolver(typeName.Namespace.ToString(), typeName.Type.ToString()); - } - } - else - { - var reader = new CharacterReader(args[0].AsSpan()); - var typeName = ParseTypeName(ref reader); - ancestorType = _typeResolver(typeName.Namespace.ToString(), typeName.Type.ToString()); - ancestorLevel = int.Parse(args[1]); - } + throw new InvalidOperationException("Cannot parse a binding path with a typed FindAncestor without a type resolver. Maybe you can use a LINQ Expression binding path instead?"); } - nodes.Add(new FindAncestorNode(ancestorType, ancestorLevel)); - } - else - { - throw new ExpressionParseException(r.Position, "Unknown RelativeSource mode."); - } - return State.AfterMember; - } - - private static TypeName ParseTypeName(ref CharacterReader r) - { - ReadOnlySpan ns, typeName; - ns = ReadOnlySpan.Empty; - var typeNameOrNamespace = r.ParseIdentifier(); - - if (!r.End && r.TakeIf(':')) - { - ns = typeNameOrNamespace; - typeName = r.ParseIdentifier(); + ancestorType = _typeResolver(node.Namespace, node.TypeName); } - else - { - typeName = typeNameOrNamespace; - } - - return new TypeName(ns, typeName); - } - - private static bool ParseNot(ref CharacterReader r) - { - return !r.End && r.TakeIf('!'); - } - - private static bool ParseMemberAccessor(ref CharacterReader r) - { - return !r.End && r.TakeIf('.'); - } - - private static bool ParseOpenBrace(ref CharacterReader r) - { - return !r.End && r.TakeIf('('); - } - - private static bool PeekOpenBracket(ref CharacterReader r) - { - return !r.End && r.PeekOneOrThrow == '['; - } - - private static bool ParseStreamOperator(ref CharacterReader r) - { - return !r.End && r.TakeIf('^'); - } - - private static bool ParseDollarSign(ref CharacterReader r) - { - return !r.End && r.TakeIf('$'); - } - - private static bool ParseSharp(ref CharacterReader r) - { - return !r.End && r.TakeIf('#'); - } - private static bool ParseDot(ref CharacterReader r) - { - return !r.End && r.TakeIf('.'); - } - - private enum State - { - Start, - RelativeSource, - ElementName, - AfterMember, - BeforeMember, - AttachedProperty, - Indexer, - End, + return new FindAncestorNode(ancestorType, ancestorLevel); } - private readonly ref struct TypeName + private AvaloniaPropertyAccessorNode ParseAttachedProperty(BindingExpressionGrammar.AttachedPropertyNameNode node) { - public TypeName(ReadOnlySpan ns, ReadOnlySpan typeName) + if (_typeResolver == null) { - Namespace = ns; - Type = typeName; + throw new InvalidOperationException("Cannot parse a binding path with an attached property without a type resolver. Maybe you can use a LINQ Expression binding path instead?"); } - public readonly ReadOnlySpan Namespace; - public readonly ReadOnlySpan Type; + var property = AvaloniaPropertyRegistry.Instance.FindRegistered(_typeResolver(node.Namespace, node.TypeName), node.PropertyName); - public void Deconstruct(out ReadOnlySpan ns, out ReadOnlySpan typeName) - { - ns = Namespace; - typeName = Type; - } + return new AvaloniaPropertyAccessorNode(property, _enableValidation); } } } From 1c380dc375a903806dced234f966f0ccfa2d8df4 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sun, 23 Jun 2019 23:14:52 -0500 Subject: [PATCH 007/711] Add new AST node generated from x:DataContextType. To be used as part of compiled bindings. --- .../Avalonia.Markup.Xaml.csproj | 3 +- .../AvaloniaXamlIlCompiler.cs | 3 +- ...valoniaXamlIlDataContextTypeTransformer.cs | 46 +++++++++++++++++++ .../AvaloniaXamlIlMetadataRemover.cs | 7 ++- 4 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 040ed57383..dfa036b1f2 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -43,9 +43,10 @@ + - + diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs index eadad60cde..933d39bad8 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -48,7 +48,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions new AvaloniaXamlIlSetterTransformer(), new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), new AvaloniaXamlIlConstructorServiceProviderTransformer(), - new AvaloniaXamlIlTransitionsTypeMetadataTransformer() + new AvaloniaXamlIlTransitionsTypeMetadataTransformer(), + new AvaloniaXamlIlDataContextTypeTransformer() ); // After everything else diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs new file mode 100644 index 0000000000..5d68a72514 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Text; +using XamlIl; +using XamlIl.Ast; +using XamlIl.Transform; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlDataContextTypeTransformer : IXamlIlAstTransformer + { + public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + { + if (node is XamlIlAstObjectNode on) + { + foreach (var child in on.Children) + { + if (child is XamlIlAstXmlDirective directive) + { + if (directive.Namespace == XamlNamespaces.Xaml2006 + && directive.Name == "DataContextType" + && directive.Values.Count == 1 + && directive.Values[0] is XamlIlTypeExtensionNode dataContextType) + { + on.Children.Remove(child); + return new AvaloniaXamlIlDataContextTypeMetadataNode(on, dataContextType.Value); + } + } + } + } + + return node; + } + } + + class AvaloniaXamlIlDataContextTypeMetadataNode : XamlIlValueWithSideEffectNodeBase + { + public IXamlIlAstTypeReference DataContextType { get; set; } + + public AvaloniaXamlIlDataContextTypeMetadataNode(IXamlIlAstValueNode value, IXamlIlAstTypeReference targetType) + : base(value, value) + { + DataContextType = targetType; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlMetadataRemover.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlMetadataRemover.cs index 994928b7fe..608e68bf10 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlMetadataRemover.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlMetadataRemover.cs @@ -8,8 +8,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) { - if (node is AvaloniaXamlIlTargetTypeMetadataNode md) - return md.Value; + if (node is AvaloniaXamlIlTargetTypeMetadataNode targetTypeMetadata) + return targetTypeMetadata.Value; + + if (node is AvaloniaXamlIlDataContextTypeMetadataNode dataContextTypeMetadata) + return dataContextTypeMetadata.Value; return node; } From bb2c563416af1132039ec8c1da09e4228eb81935 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 28 Jun 2019 01:24:01 +0300 Subject: [PATCH 008/711] Headless platform WIP --- Avalonia.sln | 52 +++ .../ControlCatalog.NetCore.csproj | 3 +- samples/ControlCatalog.NetCore/Program.cs | 5 + .../Avalonia.Headless.Vnc.csproj | 12 + .../HeadlessVncFramebufferSource.cs | 96 +++++ .../HeadlessVncPlatformExtensions.cs | 48 +++ .../Avalonia.Headless.csproj | 8 + .../AvaloniaHeadlessPlatform.cs | 93 +++++ .../HeadlessPlatformRenderInterface.cs | 385 ++++++++++++++++++ .../HeadlessPlatformStubs.cs | 96 +++++ .../HeadlessPlatformThreadingInterface.cs | 86 ++++ src/Avalonia.Headless/HeadlessWindowImpl.cs | 251 ++++++++++++ src/Avalonia.Headless/IHeadlessWindow.cs | 17 + 13 files changed, 1151 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj create mode 100644 src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs create mode 100644 src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs create mode 100644 src/Avalonia.Headless/Avalonia.Headless.csproj create mode 100644 src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs create mode 100644 src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs create mode 100644 src/Avalonia.Headless/HeadlessPlatformStubs.cs create mode 100644 src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs create mode 100644 src/Avalonia.Headless/HeadlessWindowImpl.cs create mode 100644 src/Avalonia.Headless/IHeadlessWindow.cs diff --git a/Avalonia.sln b/Avalonia.sln index ac678ba9ba..16fc4d0121 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -199,6 +199,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.UnitTes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Controls.DataGrid", "src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj", "{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless", "src\Avalonia.Headless\Avalonia.Headless.csproj", "{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.Vnc", "src\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj", "{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 @@ -1842,6 +1846,54 @@ Global {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhone.Build.0 = Release|Any CPU {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhone.Build.0 = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhone.Build.0 = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|Any CPU.Build.0 = Release|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhone.ActiveCfg = Release|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhone.Build.0 = Release|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhone.Build.0 = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhone.Build.0 = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|Any CPU.Build.0 = Release|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhone.ActiveCfg = Release|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhone.Build.0 = Release|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index 6a40f7187d..d9ce776ffd 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -7,11 +7,12 @@ + - + diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 40321496c0..91f05ca719 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -6,6 +6,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Skia; using Avalonia.ReactiveUI; +using Avalonia.Threading; namespace ControlCatalog.NetCore { @@ -32,6 +33,10 @@ namespace ControlCatalog.NetCore System.Threading.ThreadPool.QueueUserWorkItem(_ => ConsoleSilencer()); return builder.StartLinuxFramebuffer(args); } + else if (args.Contains("--vnc")) + { + return builder.StartWithHeadlessVncPlatform(null, 5901, args, ShutdownMode.OnMainWindowClose); + } else return builder.StartWithClassicDesktopLifetime(args); } diff --git a/src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj b/src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj new file mode 100644 index 0000000000..4aac862ae8 --- /dev/null +++ b/src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.0 + + + + + + + + diff --git a/src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs b/src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs new file mode 100644 index 0000000000..df68807195 --- /dev/null +++ b/src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs @@ -0,0 +1,96 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Threading; +using RemoteViewing.Vnc; +using RemoteViewing.Vnc.Server; + +namespace Avalonia.Headless.Vnc +{ + public class HeadlessVncFramebufferSource : IVncFramebufferSource + { + public IHeadlessWindow Window { get; set; } + private object _lock = new object(); + public VncFramebuffer _framebuffer = new VncFramebuffer("Avalonia", 1, 1, VncPixelFormat.RGB32); + + private VncButton _previousButtons; + public HeadlessVncFramebufferSource(VncServerSession session, Window window) + { + Window = (IHeadlessWindow)window.PlatformImpl; + session.PointerChanged += (_, args) => + { + var pt = new Point(args.X, args.Y); + + var buttons = (VncButton)args.PressedButtons; + + int TranslateButton(VncButton vncButton) => + vncButton == VncButton.Left ? 0 : vncButton == VncButton.Right ? 1 : 2; + + var modifiers = (InputModifiers)(((int)buttons & 7) << 4); + + Dispatcher.UIThread.Post(() => + { + Window?.MouseMove(pt); + foreach (var btn in CheckedButtons) + if (_previousButtons.HasFlag(btn) && !buttons.HasFlag(btn)) + Window?.MouseUp(pt, TranslateButton(btn), modifiers); + + foreach (var btn in CheckedButtons) + if (!_previousButtons.HasFlag(btn) && buttons.HasFlag(btn)) + Window?.MouseDown(pt, TranslateButton(btn), modifiers); + _previousButtons = buttons; + }, DispatcherPriority.Input); + }; + + } + + [Flags] + enum VncButton + { + Left = 1, + Middle = 2, + Right = 4, + ScrollUp = 8, + ScrollDown = 16 + } + + + private static VncButton[] CheckedButtons = new[] {VncButton.Left, VncButton.Middle, VncButton.Right}; + + public VncFramebuffer Capture() + { + lock (_lock) + { + using (var bmpRef = Window.GetLastRenderedFrame()) + { + if (bmpRef?.Item == null) + return _framebuffer; + var bmp = bmpRef.Item; + if (bmp.PixelSize.Width != _framebuffer.Width || bmp.PixelSize.Height != _framebuffer.Height) + { + _framebuffer = new VncFramebuffer("Avalonia", bmp.PixelSize.Width, bmp.PixelSize.Height, + VncPixelFormat.RGB32); + } + + using (var fb = bmp.Lock()) + { + var buf = _framebuffer.GetBuffer(); + if (_framebuffer.Stride == fb.RowBytes) + Marshal.Copy(fb.Address, buf, 0, buf.Length); + else + for (var y = 0; y < fb.Size.Height; y++) + { + var sourceStart = fb.RowBytes * y; + var dstStart = _framebuffer.Stride * y; + var row = fb.Size.Width * 4; + Marshal.Copy(new IntPtr(sourceStart + fb.Address.ToInt64()), buf, dstStart, row); + } + } + } + } + + return _framebuffer; + } + } +} diff --git a/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs b/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs new file mode 100644 index 0000000000..bd215296c8 --- /dev/null +++ b/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs @@ -0,0 +1,48 @@ +using System.Net; +using System.Net.Sockets; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Headless; +using Avalonia.Headless.Vnc; +using RemoteViewing.Vnc; +using RemoteViewing.Vnc.Server; + +namespace Avalonia +{ + public static class HeadlessVncPlatformExtensions + { + public static int StartWithHeadlessVncPlatform( + this T builder, + string host, int port, + string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) + where T : AppBuilderBase, new() + { + var tcpServer = new TcpListener(host == null ? IPAddress.Loopback : IPAddress.Parse(host), port); + tcpServer.Start(); + return builder + .UseHeadless(false) + .AfterSetup(_ => + { + var lt = ((IClassicDesktopStyleApplicationLifetime)builder.Instance.ApplicationLifetime); + lt.Startup += async delegate + { + while (true) + { + var client = await tcpServer.AcceptTcpClientAsync(); + var options = new VncServerSessionOptions + { + AuthenticationMethod = AuthenticationMethod.None + }; + var session = new VncServerSession(); + + session.SetFramebufferSource(new HeadlessVncFramebufferSource( + session, lt.MainWindow)); + session.Connect(client.GetStream(), options); + } + + }; + }) + .StartWithClassicDesktopLifetime(args, shutdownMode); + } + } +} diff --git a/src/Avalonia.Headless/Avalonia.Headless.csproj b/src/Avalonia.Headless/Avalonia.Headless.csproj new file mode 100644 index 0000000000..b871316a2c --- /dev/null +++ b/src/Avalonia.Headless/Avalonia.Headless.csproj @@ -0,0 +1,8 @@ + + + netstandard2.0 + + + + + diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs new file mode 100644 index 0000000000..5857d94182 --- /dev/null +++ b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -0,0 +1,93 @@ +using System; +using System.Diagnostics; +using System.Reactive.Disposables; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Threading; + +namespace Avalonia.Headless +{ + public static class AvaloniaHeadlessPlatform + { + class RenderTimer : DefaultRenderTimer + { + private readonly int _framesPerSecond; + private Action _forceTick; + protected override IDisposable StartCore(Action tick) + { + bool cancelled = false; + var st = Stopwatch.StartNew(); + _forceTick = () => tick(st.Elapsed); + DispatcherTimer.Run(() => + { + if (cancelled) + return false; + tick(st.Elapsed); + return !cancelled; + }, TimeSpan.FromSeconds(1.0 / _framesPerSecond), DispatcherPriority.Render); + return Disposable.Create(() => + { + _forceTick = null; + cancelled = true; + }); + } + + public RenderTimer(int framesPerSecond) : base(framesPerSecond) + { + _framesPerSecond = framesPerSecond; + } + + public void ForceTick() => _forceTick?.Invoke(); + } + + class HeadlessWindowingPlatform : IWindowingPlatform + { + public IWindowImpl CreateWindow() => new HeadlessWindowImpl(false); + + public IEmbeddableWindowImpl CreateEmbeddableWindow() => throw new PlatformNotSupportedException(); + + public IPopupImpl CreatePopup() => new HeadlessWindowImpl(true); + } + + internal static void Initialize() + { + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(new HeadlessPlatformThreadingInterface()) + .Bind().ToSingleton() + .Bind().ToSingleton() + .Bind().ToConstant(new HeadlessPlatformSettingsStub()) + .Bind().ToSingleton() + .Bind().ToSingleton() + .Bind().ToConstant(new KeyboardDevice()) + .Bind().ToConstant(new RenderLoop()) + .Bind().ToConstant(new RenderTimer(60)) + + .Bind().ToConstant(new HeadlessWindowingPlatform()) + .Bind().ToSingleton(); + } + + + public static void ForceRenderTimerTick(int count = 1) + { + var timer = AvaloniaLocator.Current.GetService() as RenderTimer; + for (var c = 0; c < count; c++) + timer?.ForceTick(); + + } + } + + public static class AvaloniaHeadlessPlatformExtensions + { + public static T UseHeadless(this T builder, bool headlessDrawing = true) + where T : AppBuilderBase, new() + { + if (headlessDrawing) + builder.UseRenderingSubsystem(HeadlessPlatformRenderInterface.Initialize, "Headless"); + return builder.UseWindowingSubsystem(AvaloniaHeadlessPlatform.Initialize, "Headless"); + } + } +} diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs new file mode 100644 index 0000000000..381eccef08 --- /dev/null +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -0,0 +1,385 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; +using Avalonia.Visuals.Media.Imaging; + +namespace Avalonia.Headless +{ + internal class HeadlessPlatformRenderInterface : IPlatformRenderInterface + { + public static void Initialize() + { + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(new HeadlessPlatformRenderInterface()); + } + + public IEnumerable InstalledFontNames { get; } = new[] {"Tahoma"}; + + public IFormattedTextImpl CreateFormattedText(string text, Typeface typeface, TextAlignment textAlignment, + TextWrapping wrapping, Size constraint, IReadOnlyList spans) + { + return new HeadlessFormattedTextStub(text, constraint); + } + + public IGeometryImpl CreateEllipseGeometry(Rect rect) => new HeadlessGeometryStub(rect); + + public IGeometryImpl CreateLineGeometry(Point p1, Point p2) + { + var tl = new Point(Math.Min(p1.X, p2.X), Math.Min(p1.Y, p2.Y)); + var br = new Point(Math.Max(p1.X, p2.X), Math.Max(p1.Y, p2.Y)); + return new HeadlessGeometryStub(new Rect(tl, br)); + } + + public IGeometryImpl CreateRectangleGeometry(Rect rect) + { + return new HeadlessGeometryStub(rect); + } + + public IStreamGeometryImpl CreateStreamGeometry() => new HeadlessStreamingGeometryStub(); + + public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => new HeadlessRenderTarget(); + + public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) + { + return new HeadlessBitmapStub(size, dpi); + } + + public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat? format = null) + { + return new HeadlessBitmapStub(size, dpi); + } + + public IBitmapImpl LoadBitmap(string fileName) + { + return new HeadlessBitmapStub(new Size(1, 1), new Vector(96, 96)); + } + + public IBitmapImpl LoadBitmap(Stream stream) + { + return new HeadlessBitmapStub(new Size(1, 1), new Vector(96, 96)); + } + + public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride) + { + return new HeadlessBitmapStub(new Size(1, 1), new Vector(96, 96)); + } + + class HeadlessGeometryStub : IGeometryImpl + { + public HeadlessGeometryStub(Rect bounds) + { + Bounds = bounds; + } + public Rect Bounds { get; set; } + public virtual Rect GetRenderBounds(Pen pen) => Bounds.Inflate(pen.Thickness / 2); + public virtual bool FillContains(Point point) => Bounds.Contains(point); + + public IGeometryImpl Intersect(IGeometryImpl geometry) + => new HeadlessGeometryStub(geometry.Bounds.Intersect(Bounds)); + + public bool StrokeContains(Pen pen, Point point) => false; + + public ITransformedGeometryImpl WithTransform(Matrix transform) => + new HeadlessTransformedGeometryStub(this, transform); + } + + class HeadlessTransformedGeometryStub : HeadlessGeometryStub, ITransformedGeometryImpl + { + public HeadlessTransformedGeometryStub(IGeometryImpl b, Matrix transform) : this(Fix(b, transform)) + { + + } + + static (IGeometryImpl, Matrix, Rect) Fix(IGeometryImpl b, Matrix transform) + { + if (b is HeadlessTransformedGeometryStub transformed) + { + b = transformed.SourceGeometry; + transform = transformed.Transform * transform; + } + + return (b, transform, b.Bounds.TransformToAABB(transform)); + } + + private HeadlessTransformedGeometryStub((IGeometryImpl b, Matrix transform, Rect bounds) fix) : base(fix.bounds) + { + SourceGeometry = fix.b; + Transform = fix.transform; + } + + + public IGeometryImpl SourceGeometry { get; } + public Matrix Transform { get; } + } + + class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl + { + public HeadlessStreamingGeometryStub() : base(Rect.Empty) + { + } + + public IStreamGeometryImpl Clone() + { + return this; + } + + public IStreamGeometryContextImpl Open() + { + return new HeadlessStreamingGeometryContextStub(this); + } + + class HeadlessStreamingGeometryContextStub : IStreamGeometryContextImpl + { + private readonly HeadlessStreamingGeometryStub _parent; + private double _x1, _y1, _x2, _y2; + public HeadlessStreamingGeometryContextStub(HeadlessStreamingGeometryStub parent) + { + _parent = parent; + } + + void Track(Point pt) + { + if (_x1 > pt.X) + _x1 = pt.X; + if (_x2 < pt.X) + _x2 = pt.X; + if (_y1 > pt.Y) + _y1 = pt.Y; + if (_y2 < pt.Y) + _y2 = pt.Y; + } + + public void Dispose() + { + _parent.Bounds = new Rect(_x1, _y1, _x2 - _x1, _y2 - _y1); + } + + public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) + => Track(point); + + public void BeginFigure(Point startPoint, bool isFilled = true) => Track(startPoint); + + public void CubicBezierTo(Point point1, Point point2, Point point3) + { + Track(point1); + Track(point2); + Track(point3); + } + + public void QuadraticBezierTo(Point control, Point endPoint) + { + Track(control); + Track(endPoint); + } + + public void LineTo(Point point) => Track(point); + + public void EndFigure(bool isClosed) + { + Dispose(); + } + + public void SetFillRule(FillRule fillRule) + { + + } + } + } + + class HeadlessBitmapStub : IBitmapImpl, IRenderTargetBitmapImpl, IWriteableBitmapImpl + { + public Size Size { get; } + + public HeadlessBitmapStub(Size size, Vector dpi) + { + Size = size; + Dpi = dpi; + var pixel = Size * (Dpi / 96); + PixelSize = new PixelSize(Math.Max(1, (int)pixel.Width), Math.Max(1, (int)pixel.Height)); + } + + public HeadlessBitmapStub(PixelSize size, Vector dpi) + { + PixelSize = size; + Dpi = dpi; + Size = PixelSize.ToSizeWithDpi(dpi); + } + + public void Dispose() + { + + } + + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + { + return new HeadlessDrawingContextStub(); + } + + public Vector Dpi { get; } + public PixelSize PixelSize { get; } + public int Version { get; set; } + public void Save(string fileName) + { + + } + + public void Save(Stream stream) + { + + } + + public ILockedFramebuffer Lock() + { + Version++; + var mem = Marshal.AllocHGlobal(PixelSize.Width * PixelSize.Height * 4); + return new LockedFramebuffer(mem, PixelSize, PixelSize.Width * 4, Dpi, PixelFormat.Rgba8888, + () => Marshal.FreeHGlobal(mem)); + } + } + + class HeadlessDrawingContextStub : IDrawingContextImpl + { + public void Dispose() + { + + } + + public Matrix Transform { get; set; } + public void Clear(Color color) + { + + } + + public void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) + { + + } + + public void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + { + + } + + public void DrawLine(Pen pen, Point p1, Point p2) + { + + } + + public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry) + { + + } + + public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0) + { + + } + + public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) + { + + } + + public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0) + { + + } + + public IRenderTargetBitmapImpl CreateLayer(Size size) + { + return new HeadlessBitmapStub(size, new Vector(96, 96)); + } + + public void PushClip(Rect clip) + { + + } + + public void PopClip() + { + + } + + public void PushOpacity(double opacity) + { + + } + + public void PopOpacity() + { + + } + + public void PushOpacityMask(IBrush mask, Rect bounds) + { + + } + + public void PopOpacityMask() + { + + } + + public void PushGeometryClip(IGeometryImpl clip) + { + + } + + public void PopGeometryClip() + { + + } + + public void Custom(ICustomDrawOperation custom) + { + + } + } + + class HeadlessRenderTarget : IRenderTarget + { + public void Dispose() + { + + } + + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + { + return new HeadlessDrawingContextStub(); + } + } + + class HeadlessFormattedTextStub : IFormattedTextImpl + { + public HeadlessFormattedTextStub(string text, Size constraint) + { + Text = text; + Constraint = constraint; + Bounds = new Rect(Constraint.Constrain(new Size(50, 50))); + } + + public Size Constraint { get; } + public Rect Bounds { get; } + public string Text { get; } + + + public IEnumerable GetLines() + { + return new[] {new FormattedTextLine(Text.Length, 10)}; + } + + public TextHitTestResult HitTestPoint(Point point) => new TextHitTestResult(); + + public Rect HitTestTextPosition(int index) => new Rect(); + + public IEnumerable HitTestTextRange(int index, int length) => new Rect[length]; + } + } +} diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs new file mode 100644 index 0000000000..debe77c1b6 --- /dev/null +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Platform; + +namespace Avalonia.Headless +{ + class HeadlessClipboardStub : IClipboard + { + private string _text; + public Task GetTextAsync() + { + return Task.Run(() => _text); + } + + public Task SetTextAsync(string text) + { + return Task.Run(() => _text = text); + } + + public Task ClearAsync() + { + return Task.Run(() => _text = null); + } + } + + class HeadlessCursorFactoryStub : IStandardCursorFactory + { + + public IPlatformHandle GetCursor(StandardCursorType cursorType) + { + return new PlatformHandle(new IntPtr((int)cursorType), "STUB"); + } + } + + class HeadlessPlatformSettingsStub : IPlatformSettings + { + public Size DoubleClickSize { get; } = new Size(2, 2); + public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500); + } + + class HeadlessSystemDialogsStub : ISystemDialogImpl + { + public Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + { + return Task.Run(() => (string[])null); + } + + public Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + { + return Task.Run(() => (string)null); + } + } + + class HeadlessIconLoaderStub : IPlatformIconLoader + { + + class IconStub : IWindowIconImpl + { + public void Save(Stream outputStream) + { + + } + } + public IWindowIconImpl LoadIcon(string fileName) + { + return new IconStub(); + } + + public IWindowIconImpl LoadIcon(Stream stream) + { + return new IconStub(); + } + + public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) + { + return new IconStub(); + } + } + + class HeadlessScreensStub : IScreenImpl + { + public int ScreenCount { get; } = 1; + + public IReadOnlyList AllScreens { get; } = new[] + { + new Screen(new PixelRect(0, 0, 1920, 1280), + new PixelRect(0, 0, 1920, 1280), true), + }; + } +} diff --git a/src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs b/src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs new file mode 100644 index 0000000000..e42a7b1a71 --- /dev/null +++ b/src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs @@ -0,0 +1,86 @@ +using System; +using System.Reactive.Disposables; +using System.Threading; +using Avalonia.Platform; +using Avalonia.Threading; + +namespace Avalonia.Headless +{ + class HeadlessPlatformThreadingInterface : IPlatformThreadingInterface + { + public HeadlessPlatformThreadingInterface() + { + _thread = Thread.CurrentThread; + } + + private AutoResetEvent _event = new AutoResetEvent(false); + private Thread _thread; + private object _lock = new object(); + private DispatcherPriority? _signaledPriority; + + public void RunLoop(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + DispatcherPriority? signaled = null; + lock (_lock) + { + signaled = _signaledPriority; + _signaledPriority = null; + } + if(signaled.HasValue) + Signaled?.Invoke(signaled); + WaitHandle.WaitAny(new[] {cancellationToken.WaitHandle, _event}, TimeSpan.FromMilliseconds(20)); + } + } + + public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) + { + var cancelled = false; + var enqueued = false; + var l = new object(); + var timer = new Timer(_ => + { + lock (l) + { + if (cancelled || enqueued) + return; + enqueued = true; + Dispatcher.UIThread.Post(() => + { + lock (l) + { + enqueued = false; + if (cancelled) + return; + tick(); + } + }, priority); + } + }, null, interval, interval); + return Disposable.Create(() => + { + lock (l) + { + timer.Dispose(); + cancelled = true; + } + }); + } + + public void Signal(DispatcherPriority priority) + { + lock (_lock) + { + if (_signaledPriority == null || _signaledPriority.Value > priority) + { + _signaledPriority = priority; + } + _event.Set(); + } + } + + public bool CurrentThreadIsLoopThread => _thread == Thread.CurrentThread; + public event Action Signaled; + } +} diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs new file mode 100644 index 0000000000..550defa815 --- /dev/null +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Avalonia.Controls; +using Avalonia.Controls.Platform.Surfaces; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Threading; +using Avalonia.Utilities; + +namespace Avalonia.Headless +{ + class HeadlessWindowImpl : IWindowImpl, IPopupImpl, IFramebufferPlatformSurface, IHeadlessWindow + { + private IKeyboardDevice _keyboard; + private Stopwatch _st = Stopwatch.StartNew(); + private Pointer _mousePointer; + private WriteableBitmap _lastRenderedFrame; + private object _sync = new object(); + public bool IsPopup { get; } + + public HeadlessWindowImpl(bool isPopup) + { + IsPopup = isPopup; + Surfaces = new object[] {this}; + _keyboard = AvaloniaLocator.Current.GetService(); + _mousePointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); + MouseDevice = new MouseDevice(_mousePointer); + ClientSize = new Size(1024, 768); + } + + public void Dispose() + { + Closed?.Invoke(); + _lastRenderedFrame?.Dispose(); + _lastRenderedFrame = null; + } + + public Size ClientSize { get; set; } + public double Scaling { get; } = 1; + public IEnumerable Surfaces { get; } + public Action Input { get; set; } + public Action Paint { get; set; } + public Action Resized { get; set; } + public Action ScalingChanged { get; set; } + + public IRenderer CreateRenderer(IRenderRoot root) + => new DeferredRenderer(root, AvaloniaLocator.Current.GetService()); + + public void Invalidate(Rect rect) + { + } + + public void SetInputRoot(IInputRoot inputRoot) + { + InputRoot = inputRoot; + } + + public IInputRoot InputRoot { get; set; } + + public Point PointToClient(PixelPoint point) => point.ToPoint(Scaling); + + public PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, Scaling); + + public void SetCursor(IPlatformHandle cursor) + { + + } + + public Action Closed { get; set; } + public IMouseDevice MouseDevice { get; } + + public void Show() + { + Dispatcher.UIThread.Post(() => Activated?.Invoke(), DispatcherPriority.Input); + } + + public void Hide() + { + Dispatcher.UIThread.Post(() => Deactivated?.Invoke(), DispatcherPriority.Input); + } + + public void BeginMoveDrag() + { + + } + + public void BeginResizeDrag(WindowEdge edge) + { + + } + + public PixelPoint Position { get; set; } + public Action PositionChanged { get; set; } + public void Activate() + { + Dispatcher.UIThread.Post(() => Activated?.Invoke(), DispatcherPriority.Input); + } + + public Action Deactivated { get; set; } + public Action Activated { get; set; } + public IPlatformHandle Handle { get; } = new PlatformHandle(IntPtr.Zero, "STUB"); + public Size MaxClientSize { get; } = new Size(1920, 1280); + public void Resize(Size clientSize) + { + // Emulate X11 behavior here + if (IsPopup) + DoResize(clientSize); + else + Dispatcher.UIThread.Post(() => + { + DoResize(clientSize); + }); + } + + void DoResize(Size clientSize) + { + ClientSize = clientSize; + Resized?.Invoke(clientSize); + } + + public void SetMinMaxSize(Size minSize, Size maxSize) + { + + } + + public void SetTopmost(bool value) + { + + } + + public IScreenImpl Screen { get; } = new HeadlessScreensStub(); + public WindowState WindowState { get; set; } + public Action WindowStateChanged { get; set; } + public void SetTitle(string title) + { + + } + + public void ShowDialog(IWindowImpl parent) + { + Show(); + } + + public void SetSystemDecorations(bool enabled) + { + + } + + public void SetIcon(IWindowIconImpl icon) + { + + } + + public void ShowTaskbarIcon(bool value) + { + + } + + public void CanResize(bool value) + { + + } + + public Func Closing { get; set; } + + class FramebufferProxy : ILockedFramebuffer + { + private readonly ILockedFramebuffer _fb; + private readonly Action _onDispose; + private bool _disposed; + + public FramebufferProxy(ILockedFramebuffer fb, Action onDispose) + { + _fb = fb; + _onDispose = onDispose; + } + public void Dispose() + { + if(_disposed) + return; + _disposed = true; + _fb.Dispose(); + _onDispose(); + } + + public IntPtr Address => _fb.Address; + public PixelSize Size => _fb.Size; + public int RowBytes => _fb.RowBytes; + public Vector Dpi => _fb.Dpi; + public PixelFormat Format => _fb.Format; + } + + public ILockedFramebuffer Lock() + { + var bmp = new WriteableBitmap(PixelSize.FromSize(ClientSize, Scaling), new Vector(96, 96) * Scaling); + var fb = bmp.Lock(); + return new FramebufferProxy(fb, () => + { + lock (_sync) + { + _lastRenderedFrame?.Dispose(); + _lastRenderedFrame = bmp; + } + }); + } + + public IRef GetLastRenderedFrame() + { + lock (_sync) + return _lastRenderedFrame?.PlatformImpl?.CloneAs(); + } + + private ulong Timestamp => (ulong)_st.ElapsedMilliseconds; + + void IHeadlessWindow.KeyPress(Key key, InputModifiers modifiers) + { + Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, RawKeyEventType.KeyDown, key, modifiers)); + } + + void IHeadlessWindow.KeyRelease(Key key, InputModifiers modifiers) + { + Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, RawKeyEventType.KeyUp, key, modifiers)); + } + + void IHeadlessWindow.MouseDown(Point point, int button, InputModifiers modifiers) + { + Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot, + button == 0 ? RawPointerEventType.LeftButtonDown : + button == 1 ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.RightButtonDown, + point, modifiers)); + } + + void IHeadlessWindow.MouseMove(Point point, InputModifiers modifiers) + { + Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot, + RawPointerEventType.Move, point, modifiers)); + } + + void IHeadlessWindow.MouseUp(Point point, int button, InputModifiers modifiers) + { + Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot, + button == 0 ? RawPointerEventType.LeftButtonUp : + button == 1 ? RawPointerEventType.MiddleButtonUp : RawPointerEventType.RightButtonUp, + point, modifiers)); + } + } +} diff --git a/src/Avalonia.Headless/IHeadlessWindow.cs b/src/Avalonia.Headless/IHeadlessWindow.cs new file mode 100644 index 0000000000..aafad2e500 --- /dev/null +++ b/src/Avalonia.Headless/IHeadlessWindow.cs @@ -0,0 +1,17 @@ +using Avalonia.Input; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Headless +{ + public interface IHeadlessWindow + { + IRef GetLastRenderedFrame(); + void KeyPress(Key key, InputModifiers modifiers); + void KeyRelease(Key key, InputModifiers modifiers); + void MouseDown(Point point, int button, InputModifiers modifiers = InputModifiers.None); + void MouseMove(Point point, InputModifiers modifiers = InputModifiers.None); + void MouseUp(Point point, int button, InputModifiers modifiers = InputModifiers.None); + } +} From a0ffb8ec9890d7217835403a79c42cd9aae567a0 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 28 Jun 2019 10:40:09 +0300 Subject: [PATCH 009/711] Added a "script" to load all control catalog pages in headless mode --- samples/ControlCatalog.NetCore/Program.cs | 46 +++++++++++++++++++++ src/Avalonia.Headless/HeadlessWindowImpl.cs | 8 +++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 91f05ca719..766f3c5afa 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -2,8 +2,12 @@ using System.Diagnostics; using System.Linq; using System.Threading; +using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Headless; +using Avalonia.LogicalTree; using Avalonia.Skia; using Avalonia.ReactiveUI; using Avalonia.Threading; @@ -37,6 +41,48 @@ namespace ControlCatalog.NetCore { return builder.StartWithHeadlessVncPlatform(null, 5901, args, ShutdownMode.OnMainWindowClose); } + else if (args.Contains("--full-headless")) + { + return builder + .UseHeadless(true) + .AfterSetup(_ => + { + DispatcherTimer.RunOnce(async () => + { + var window = ((IClassicDesktopStyleApplicationLifetime)Application.Current.ApplicationLifetime) + .MainWindow; + var tc = window.GetLogicalDescendants().OfType().First(); + foreach (var page in tc.Items.Cast().ToList()) + { + // Skip DatePicker because of some layout bug in grid + if (page.Header.ToString() == "DatePicker") + continue; + Console.WriteLine("Selecting " + page.Header); + tc.SelectedItem = page; + await Task.Delay(500); + } + Console.WriteLine("Selecting the first page"); + tc.SelectedItem = tc.Items.OfType().First(); + await Task.Delay(500); + Console.WriteLine("Clicked through all pages, triggering GC"); + for (var c = 0; c < 3; c++) + { + GC.Collect(2, GCCollectionMode.Forced); + await Task.Delay(500); + } + + void FormatMem(string metric, long bytes) + { + Console.WriteLine(metric + ": " + bytes / 1024 / 1024 + "MB"); + } + + FormatMem("GC allocated bytes", GC.GetTotalMemory(true)); + FormatMem("WorkingSet64", Process.GetCurrentProcess().WorkingSet64); + + }, TimeSpan.FromSeconds(1)); + }) + .StartWithClassicDesktopLifetime(args); + } else return builder.StartWithClassicDesktopLifetime(args); } diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index 550defa815..b19abffc01 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -118,8 +118,12 @@ namespace Avalonia.Headless void DoResize(Size clientSize) { - ClientSize = clientSize; - Resized?.Invoke(clientSize); + // Uncomment this check and experience a weird bug in layout engine + if (ClientSize != clientSize) + { + ClientSize = clientSize; + Resized?.Invoke(clientSize); + } } public void SetMinMaxSize(Size minSize, Size maxSize) From e3bc5345aeb0b1e71f39b2100290d73bc9db033f Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 6 Jul 2019 12:32:57 -0700 Subject: [PATCH 010/711] Finish initial implementation of compiled bindings. Still some bugs to work out, but we have something that works now. --- .../Data/Core/ClrPropertyInfo.cs | 7 +- src/Avalonia.Base/Data/Core/IPropertyInfo.cs | 3 + .../Data/Core/Plugins/TaskStreamPlugin.cs | 2 +- .../Data/Core/PropertyAccessorNode.cs | 22 +- src/Avalonia.Base/Data/Core/StreamNode.cs | 37 +- .../Avalonia.Build.Tasks.csproj | 8 +- .../Avalonia.Markup.Xaml.csproj | 13 +- .../CompiledBindingExtension.cs | 75 +++ .../CompiledBindings/CompiledBindingPath.cs | 177 +++++++ .../NotifyingPropertyInfoHelpers.cs | 255 ++++++++++ .../ObservableStreamPlugin.cs | 28 ++ .../PropertyInfoAccessorPlugin.cs | 112 +++++ .../CompiledBindings/TaskStreamPlugin.cs | 53 ++ .../AvaloniaXamlIlCompiler.cs | 14 +- .../Transformers/AddNameScopeRegistration.cs | 78 +-- .../AvaloniaXamlIlBindingPathTransformer.cs | 33 ++ ...valoniaXamlIlDataContextTypeTransformer.cs | 46 +- ...mlIlTransformInstanceAttachedProperties.cs | 5 + .../AvaloniaXamlIlWellKnownTypes.cs | 10 +- .../XamlIlBindingPathHelper.cs | 459 ++++++++++++++++++ .../XamlIlClrPropertyInfoHelper.cs | 11 +- src/Markup/Avalonia.Markup/Data/Binding.cs | 263 +--------- .../Avalonia.Markup/Data/BindingBase.cs | 275 +++++++++++ .../Markup/Parsers/ArgumentListParser.cs | 5 +- .../Parsers/BindingExpressionGrammar.cs | 11 +- .../Markup/Parsers/Nodes/ElementNameNode.cs | 2 +- .../Markup/Parsers/Nodes/FindAncestorNode.cs | 2 +- .../Markup/Parsers/Nodes/SelfNode.cs | 2 +- .../CompiledBindingExtensionTests.cs | 47 ++ .../Xaml/XamlIlTests.cs | 15 +- 30 files changed, 1743 insertions(+), 327 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/NotifyingPropertyInfoHelpers.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ObservableStreamPlugin.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorPlugin.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/TaskStreamPlugin.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs create mode 100644 src/Markup/Avalonia.Markup/Data/BindingBase.cs create mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs diff --git a/src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs b/src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs index 05059b6617..f66411c2c2 100644 --- a/src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs +++ b/src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs @@ -9,14 +9,17 @@ namespace Avalonia.Data.Core private readonly Func _getter; private readonly Action _setter; - public ClrPropertyInfo(string name, Func getter, Action setter) + public ClrPropertyInfo(string name, Func getter, Action setter, Type propertyType) { _getter = getter; _setter = setter; + PropertyType = propertyType; Name = name; } public string Name { get; } + public Type PropertyType { get; } + public object Get(object target) { if (_getter == null) @@ -62,7 +65,7 @@ namespace Avalonia.Data.Core } public ReflectionClrPropertyInfo(PropertyInfo info) : base(info.Name, - CreateGetter(info), CreateSetter(info)) + CreateGetter(info), CreateSetter(info), info.PropertyType) { } diff --git a/src/Avalonia.Base/Data/Core/IPropertyInfo.cs b/src/Avalonia.Base/Data/Core/IPropertyInfo.cs index 530b98d08b..2417d0ffc4 100644 --- a/src/Avalonia.Base/Data/Core/IPropertyInfo.cs +++ b/src/Avalonia.Base/Data/Core/IPropertyInfo.cs @@ -1,3 +1,5 @@ +using System; + namespace Avalonia.Data.Core { public interface IPropertyInfo @@ -7,5 +9,6 @@ namespace Avalonia.Data.Core void Set(object target, object value); bool CanSet { get; } bool CanGet { get; } + Type PropertyType { get; } } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs index 16862f576d..35917226ef 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs @@ -57,7 +57,7 @@ namespace Avalonia.Data.Core.Plugins return Observable.Empty(); } - protected IObservable HandleCompleted(Task task) + private IObservable HandleCompleted(Task task) { var resultProperty = task.GetType().GetRuntimeProperty("Result"); diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index df8f46a7d7..6cc7889eb6 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -11,6 +11,7 @@ namespace Avalonia.Data.Core public class PropertyAccessorNode : SettableNode { private readonly bool _enableValidation; + private IPropertyAccessorPlugin _customPlugin; private IPropertyAccessor _accessor; public PropertyAccessorNode(string propertyName, bool enableValidation) @@ -19,6 +20,13 @@ namespace Avalonia.Data.Core _enableValidation = enableValidation; } + public PropertyAccessorNode(string propertyName, bool enableValidation, IPropertyAccessorPlugin customPlugin) + { + PropertyName = propertyName; + _enableValidation = enableValidation; + _customPlugin = customPlugin; + } + public override string Description => PropertyName; public string PropertyName { get; } public override Type PropertyType => _accessor?.PropertyType; @@ -39,7 +47,7 @@ namespace Avalonia.Data.Core protected override void StartListeningCore(WeakReference reference) { - var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference.Target, PropertyName)); + var plugin = _customPlugin ?? GetPropertyAccessorPluginForObject(reference.Target); var accessor = plugin?.Start(reference, PropertyName); if (_enableValidation && Next == null) @@ -53,16 +61,16 @@ namespace Avalonia.Data.Core } } - if (accessor == null) - { - throw new NotSupportedException( + _accessor = accessor ?? throw new NotSupportedException( $"Could not find a matching property accessor for {PropertyName}."); - } - - _accessor = accessor; accessor.Subscribe(ValueChanged); } + private IPropertyAccessorPlugin GetPropertyAccessorPluginForObject(object target) + { + return ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(target, PropertyName)); + } + protected override void StopListeningCore() { _accessor.Dispose(); diff --git a/src/Avalonia.Base/Data/Core/StreamNode.cs b/src/Avalonia.Base/Data/Core/StreamNode.cs index 6fc178e7f8..17b5951ed8 100644 --- a/src/Avalonia.Base/Data/Core/StreamNode.cs +++ b/src/Avalonia.Base/Data/Core/StreamNode.cs @@ -3,36 +3,55 @@ using System; using System.Reactive.Linq; +using Avalonia.Data.Core.Plugins; namespace Avalonia.Data.Core { public class StreamNode : ExpressionNode { + private IStreamPlugin _customPlugin = null; private IDisposable _subscription; public override string Description => "^"; + public StreamNode() { } + + public StreamNode(IStreamPlugin customPlugin) + { + _customPlugin = customPlugin; + } + protected override void StartListeningCore(WeakReference reference) { + GetPlugin(reference)?.Start(reference).Subscribe(ValueChanged); + } + + protected override void StopListeningCore() + { + _subscription?.Dispose(); + _subscription = null; + } + + private IStreamPlugin GetPlugin(WeakReference reference) + { + if (_customPlugin != null) + { + return _customPlugin; + } + foreach (var plugin in ExpressionObserver.StreamHandlers) { if (plugin.Match(reference)) { - _subscription = plugin.Start(reference).Subscribe(ValueChanged); - return; + return plugin; } } - // TODO: Improve error. + // TODO: Improve error ValueChanged(new BindingNotification( new MarkupBindingChainException("Stream operator applied to unsupported type", Description), BindingErrorType.Error)); - } - - protected override void StopListeningCore() - { - _subscription?.Dispose(); - _subscription = null; + return null; } } } diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index 1b4424a050..eb9b1c2fb5 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -45,9 +45,15 @@ Markup/%(RecursiveDir)%(FileName)%(Extension) + + Markup/%(RecursiveDir)%(FileName)%(Extension) + Markup/%(RecursiveDir)%(FileName)%(Extension) - + + + Markup/%(RecursiveDir)%(FileName)%(Extension) + diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index dfa036b1f2..472afab0ae 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -16,6 +16,12 @@ + + + + + + @@ -41,6 +47,7 @@ + @@ -58,6 +65,7 @@ + @@ -67,6 +75,7 @@ + @@ -78,7 +87,9 @@ - + + global,Markup,%(Aliases) + diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs new file mode 100644 index 0000000000..bc1e2cdd36 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs @@ -0,0 +1,75 @@ +extern alias Markup; +using System; +using Avalonia.Data; +using Avalonia.Controls; +using Avalonia.Styling; +using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings; +using Avalonia.Data.Core; + +using SourceMode = Markup::Avalonia.Markup.Parsers.SourceMode; +using Avalonia.Data.Converters; + +namespace Avalonia.Markup.Xaml.MarkupExtensions +{ + public class CompiledBindingExtension : BindingBase + { + public CompiledBindingExtension() + { + Path = new CompiledBindingPath(); + } + + public CompiledBindingExtension(CompiledBindingPath path) + { + Path = path; + } + + public CompiledBindingExtension ProvideValue(IServiceProvider provider) + { + return new CompiledBindingExtension + { + Path = Path, + Converter = Converter, + FallbackValue = FallbackValue, + Mode = Mode, + Priority = Priority, + StringFormat = StringFormat, + DefaultAnchor = new WeakReference(GetDefaultAnchor(provider)) + }; + } + + private static object GetDefaultAnchor(IServiceProvider provider) + { + // If the target is not a control, so we need to find an anchor that will let us look + // up named controls and style resources. First look for the closest IControl in + // the context. + object anchor = provider.GetFirstParent(); + + // If a control was not found, then try to find the highest-level style as the XAML + // file could be a XAML file containing only styles. + return anchor ?? + provider.GetService()?.RootObject as IStyle ?? + provider.GetLastParent(); + } + + protected override ExpressionObserver CreateExpressionObserver(IAvaloniaObject target, AvaloniaProperty targetProperty, object anchor, bool enableDataValidation) + { + if (Path.SourceMode == SourceMode.Data) + { + return CreateDataContextObserver( + target, + Path.BuildExpression(enableDataValidation), + targetProperty == StyledElement.DataContextProperty, + anchor); + } + else + { + return CreateSourceObserver( + (target as IStyledElement) ?? (anchor as IStyledElement), + Path.BuildExpression(enableDataValidation)); + } + } + + [ConstructorArgument("path")] + public CompiledBindingPath Path { get; set; } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs new file mode 100644 index 0000000000..81b9bd29b9 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs @@ -0,0 +1,177 @@ +extern alias Markup; +using System; +using System.Collections.Generic; +using Avalonia.Data.Core; +using Avalonia.Data.Core.Plugins; +using Avalonia.Markup.Parsers.Nodes; +using SourceMode = Markup::Avalonia.Markup.Parsers.SourceMode; + +namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings +{ + public class CompiledBindingPath + { + private readonly List _elements = new List(); + + public CompiledBindingPath() { } + + internal CompiledBindingPath(IEnumerable bindingPath) + { + _elements = new List(bindingPath); + } + + public ExpressionNode BuildExpression(bool enableValidation) + { + ExpressionNode pathRoot = null; + ExpressionNode path = null; + foreach (var element in _elements) + { + ExpressionNode node = null; + switch (element) + { + case NotExpressionPathElement _: + node = new LogicalNotNode(); + break; + case PropertyElement prop: + node = new PropertyAccessorNode(prop.Property.Name, enableValidation, new PropertyInfoAccessorPlugin(prop.Property)); + break; + case AncestorPathElement ancestor: + node = new FindAncestorNode(ancestor.AncestorType, ancestor.Level); + break; + case SelfPathElement _: + node = new SelfNode(); + break; + case ElementNameElement name: + node = new ElementNameNode(name.Name); + break; + case IStronglyTypedStreamElement stream: + node = new StreamNode(stream.CreatePlugin()); + break; + default: + throw new InvalidOperationException($"Unknown binding path element type {element.GetType().FullName}"); + } + + path = pathRoot is null ? (pathRoot = node) : path.Next = node; + } + + return pathRoot ?? new EmptyExpressionNode(); + } + + public SourceMode SourceMode => _elements.Count > 0 && _elements[0] is IControlSourceBindingPathElement ? SourceMode.Control : SourceMode.Data; + } + + public class CompiledBindingPathBuilder + { + private List _elements = new List(); + + public CompiledBindingPathBuilder Not() + { + _elements.Add(new NotExpressionPathElement()); + return this; + } + + public CompiledBindingPathBuilder Property(INotifyingPropertyInfo info) + { + _elements.Add(new PropertyElement(info)); + return this; + } + + public CompiledBindingPathBuilder StreamTask() + { + _elements.Add(new TaskStreamPathElement()); + return this; + } + + public CompiledBindingPathBuilder StreamObservable() + { + _elements.Add(new ObservableStreamPathElement()); + return this; + } + + public CompiledBindingPathBuilder Self() + { + _elements.Add(new SelfPathElement()); + return this; + } + + public CompiledBindingPathBuilder Ancestor(Type ancestorType, int level) + { + _elements.Add(new AncestorPathElement(ancestorType, level)); + return this; + } + + public CompiledBindingPathBuilder ElementName(string name) + { + _elements.Add(new ElementNameElement(name)); + return this; + } + + public CompiledBindingPath Build() => new CompiledBindingPath(_elements); + } + + public interface ICompiledBindingPathElement + { + } + + internal interface IControlSourceBindingPathElement { } + + internal class NotExpressionPathElement : ICompiledBindingPathElement + { + public static readonly NotExpressionPathElement Instance = new NotExpressionPathElement(); + } + + internal class PropertyElement : ICompiledBindingPathElement + { + public PropertyElement(INotifyingPropertyInfo property) + { + Property = property; + } + + public INotifyingPropertyInfo Property { get; } + } + + internal interface IStronglyTypedStreamElement : ICompiledBindingPathElement + { + IStreamPlugin CreatePlugin(); + } + + internal class TaskStreamPathElement : IStronglyTypedStreamElement + { + public static readonly TaskStreamPathElement Instance = new TaskStreamPathElement(); + + public IStreamPlugin CreatePlugin() => new TaskStreamPlugin(); + } + + internal class ObservableStreamPathElement : IStronglyTypedStreamElement + { + public static readonly ObservableStreamPathElement Instance = new ObservableStreamPathElement(); + + public IStreamPlugin CreatePlugin() => new ObservableStreamPlugin(); + } + + internal class SelfPathElement : ICompiledBindingPathElement, IControlSourceBindingPathElement + { + public static readonly SelfPathElement Instance = new SelfPathElement(); + } + + internal class AncestorPathElement : ICompiledBindingPathElement, IControlSourceBindingPathElement + { + public AncestorPathElement(Type ancestorType, int level) + { + AncestorType = ancestorType; + Level = level; + } + + public Type AncestorType { get; } + public int Level { get; } + } + + internal class ElementNameElement : ICompiledBindingPathElement, IControlSourceBindingPathElement + { + public ElementNameElement(string name) + { + Name = name; + } + + public string Name { get; } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/NotifyingPropertyInfoHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/NotifyingPropertyInfoHelpers.cs new file mode 100644 index 0000000000..0af201ade8 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/NotifyingPropertyInfoHelpers.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Text; +using Avalonia.Data.Core; +using Avalonia.Reactive; +using Avalonia.Utilities; + +namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings +{ + public static class NotifyingPropertyInfoHelpers + { + public static INotifyingPropertyInfo CreateINPCPropertyInfo(IPropertyInfo basePropertyInfo) + => new INPCPropertyInfo(basePropertyInfo); + + public static INotifyingPropertyInfo CreateAvaloniaPropertyInfo(AvaloniaProperty property) + => new AvaloniaPropertyInfo(property); + + public static INotifyingPropertyInfo CreateIndexerPropertyInfo(IPropertyInfo basePropertyInfo, int argument) + => new IndexerInfo(basePropertyInfo, argument); + } + + public interface INotifyingPropertyInfo : IPropertyInfo + { + void OnPropertyChanged(object target, EventHandler handler); + void RemoveListener(object target, EventHandler handler); + } + + internal abstract class NotifyingPropertyInfoBase : INotifyingPropertyInfo + { + private readonly IPropertyInfo _base; + protected readonly ConditionalWeakTable _changedHandlers = new ConditionalWeakTable(); + + public NotifyingPropertyInfoBase(IPropertyInfo baseProperty) + { + _base = baseProperty; + } + + public string Name => _base.Name; + + public bool CanSet => _base.CanSet; + + public bool CanGet => _base.CanGet; + + public Type PropertyType => _base.PropertyType; + + public object Get(object target) + { + return _base.Get(target); + } + + public void Set(object target, object value) + { + _base.Set(target, value); + } + + public void OnPropertyChanged(object target, EventHandler handler) + { + if (ValidateTargetType(target)) + { + return; + } + + if (_changedHandlers.TryGetValue(target, out var value)) + { + _changedHandlers.Remove(target); + _changedHandlers.Add(target, (EventHandler)Delegate.Combine(value, handler)); + } + else + { + _changedHandlers.Add(target, handler); + SubscribeToChangesForNewTarget(target); + } + } + + protected abstract bool ValidateTargetType(object target); + + protected abstract void SubscribeToChangesForNewTarget(object target); + + protected abstract void UnsubscribeToChangesForTarget(object target); + + protected bool TryGetHandlersForTarget(object target, out EventHandler handlers) + => _changedHandlers.TryGetValue(target, out handlers); + + public void RemoveListener(object target, EventHandler handler) + { + if (!ValidateTargetType(target)) + { + return; + } + + if (_changedHandlers.TryGetValue(target, out var value)) + { + _changedHandlers.Remove(target); + EventHandler modified = (EventHandler)Delegate.Remove(value, handler); + if (modified != null) + { + _changedHandlers.Add(target, modified); + } + else + { + UnsubscribeToChangesForTarget(target); + } + } + } + } + + internal class INPCPropertyInfo : NotifyingPropertyInfoBase + { + public INPCPropertyInfo(IPropertyInfo baseProperty) + :base(baseProperty) + { + } + + void OnNotifyPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (Name == e.PropertyName && TryGetHandlersForTarget(sender, out var handlers)) + { + handlers(sender, EventArgs.Empty); + } + } + + protected override bool ValidateTargetType(object target) + { + return target is INotifyPropertyChanged; + } + + protected override void SubscribeToChangesForNewTarget(object target) + { + if (target is INotifyPropertyChanged inpc) + { + WeakEventHandlerManager.Subscribe( + inpc, + nameof(INotifyPropertyChanged.PropertyChanged), + OnNotifyPropertyChanged); + } + } + + protected override void UnsubscribeToChangesForTarget(object target) + { + if (target is INotifyPropertyChanged) + { + WeakEventHandlerManager.Unsubscribe( + target, + nameof(INotifyPropertyChanged.PropertyChanged), + OnNotifyPropertyChanged); + } + } + } + + internal class AvaloniaPropertyInfo : NotifyingPropertyInfoBase + { + private readonly AvaloniaProperty _base; + + public AvaloniaPropertyInfo(AvaloniaProperty baseProperty) + :base(baseProperty) + { + _base = baseProperty; + } + + protected override void SubscribeToChangesForNewTarget(object target) + { + IAvaloniaObject obj = (IAvaloniaObject)target; + obj.PropertyChanged += OnPropertyChanged; + } + + private void OnPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (_base == e.Property && TryGetHandlersForTarget(sender, out var handlers)) + { + handlers(sender, EventArgs.Empty); + } + } + + protected override void UnsubscribeToChangesForTarget(object target) + { + ((IAvaloniaObject)target).PropertyChanged -= OnPropertyChanged; + } + + protected override bool ValidateTargetType(object target) + { + return target is IAvaloniaObject; + } + } + + internal class IndexerInfo : INPCPropertyInfo + { + private int _index; + + public IndexerInfo(IPropertyInfo baseProperty, int indexerArgument) : base(baseProperty) + { + _index = indexerArgument; + } + + protected override void SubscribeToChangesForNewTarget(object target) + { + base.SubscribeToChangesForNewTarget(target); + if (target is INotifyCollectionChanged incc) + { + WeakEventHandlerManager.Subscribe( + incc, + nameof(INotifyCollectionChanged.CollectionChanged), + OnNotifyCollectionChanged); + } + } + + protected override void UnsubscribeToChangesForTarget(object target) + { + base.UnsubscribeToChangesForTarget(target); + if (target is INotifyCollectionChanged) + { + WeakEventHandlerManager.Unsubscribe( + target, + nameof(INotifyCollectionChanged.CollectionChanged), + OnNotifyCollectionChanged); + } + } + + void OnNotifyCollectionChanged(object sender, NotifyCollectionChangedEventArgs args) + { + if (ShouldNotifyListeners(args) && TryGetHandlersForTarget(sender, out var handlers)) + { + handlers(sender, EventArgs.Empty); + } + } + + bool ShouldNotifyListeners(NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + return _index >= e.NewStartingIndex; + case NotifyCollectionChangedAction.Remove: + return _index >= e.OldStartingIndex; + case NotifyCollectionChangedAction.Replace: + return _index >= e.NewStartingIndex && + _index < e.NewStartingIndex + e.NewItems.Count; + case NotifyCollectionChangedAction.Move: + return (_index >= e.NewStartingIndex && + _index < e.NewStartingIndex + e.NewItems.Count) || + (_index >= e.OldStartingIndex && + _index < e.OldStartingIndex + e.OldItems.Count); + case NotifyCollectionChangedAction.Reset: + return true; + } + return false; + } + + protected override bool ValidateTargetType(object target) + => base.ValidateTargetType(target) || target is INotifyCollectionChanged; + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ObservableStreamPlugin.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ObservableStreamPlugin.cs new file mode 100644 index 0000000000..361cbef740 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ObservableStreamPlugin.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Text; +using Avalonia.Data.Core.Plugins; + +namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings +{ + class ObservableStreamPlugin : IStreamPlugin + { + public bool Match(WeakReference reference) + { + return reference is IObservable; + } + + public IObservable Start(WeakReference reference) + { + var target = reference.Target as IObservable; + + if (target is IObservable obj) + { + return obj; + } + + return target.Select(x => (object)x); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorPlugin.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorPlugin.cs new file mode 100644 index 0000000000..ac3108e847 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorPlugin.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using Avalonia.Data; +using Avalonia.Data.Core.Plugins; + +namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings +{ + class PropertyInfoAccessorPlugin : IPropertyAccessorPlugin + { + private readonly INotifyingPropertyInfo _propertyInfo; + + public PropertyInfoAccessorPlugin(INotifyingPropertyInfo propertyInfo) + { + _propertyInfo = propertyInfo; + } + + public bool Match(object obj, string propertyName) + { + throw new InvalidOperationException("The PropertyInfoAccessorPlugin does not support dynamic matching"); + } + + public IPropertyAccessor Start(WeakReference reference, string propertyName) + { + Debug.Assert(_propertyInfo.Name == propertyName); + return new Accessor(reference, _propertyInfo); + } + + class Accessor : PropertyAccessorBase + { + private WeakReference _reference; + private INotifyingPropertyInfo _propertyInfo; + private bool _eventRaised; + + public Accessor(WeakReference reference, INotifyingPropertyInfo propertyInfo) + { + _reference = reference; + _propertyInfo = propertyInfo; + } + + public override Type PropertyType => _propertyInfo.PropertyType; + + public override object Value + { + get + { + var o = _reference.Target; + return (o != null) ? _propertyInfo.Get(o) : null; + } + } + + public override bool SetValue(object value, BindingPriority priority) + { + if (_propertyInfo.CanSet) + { + _eventRaised = false; + _propertyInfo.Set(_reference.Target, value); + + if (!_eventRaised) + { + SendCurrentValue(); + } + + return true; + } + + return false; + } + + void OnChanged(object sender, EventArgs e) + { + _eventRaised = true; + SendCurrentValue(); + } + + protected override void SubscribeCore() + { + SendCurrentValue(); + SubscribeToChanges(); + } + + protected override void UnsubscribeCore() + { + var target = _reference.Target; + if (target != null) + { + _propertyInfo.RemoveListener(target, OnChanged); + } + } + + private void SendCurrentValue() + { + try + { + var value = Value; + PublishValue(value); + } + catch { } + } + + private void SubscribeToChanges() + { + var target = _reference.Target; + if (target != null) + { + _propertyInfo.OnPropertyChanged(target, OnChanged); + } + } + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/TaskStreamPlugin.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/TaskStreamPlugin.cs new file mode 100644 index 0000000000..30fd1fdaa2 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/TaskStreamPlugin.cs @@ -0,0 +1,53 @@ +using System; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Threading.Tasks; +using Avalonia.Data; +using Avalonia.Data.Core.Plugins; + +namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings +{ + class TaskStreamPlugin : IStreamPlugin + { + public bool Match(WeakReference reference) + { + return reference.Target is Task; + } + + public IObservable Start(WeakReference reference) + { + if (!(reference.Target is Task task)) + { + return Observable.Empty(); + } + + switch (task.Status) + { + case TaskStatus.RanToCompletion: + case TaskStatus.Faulted: + return HandleCompleted(task); + default: + var subject = new Subject(); + task.ContinueWith( + x => HandleCompleted(task).Subscribe(subject), + TaskScheduler.FromCurrentSynchronizationContext()) + .ConfigureAwait(false); + return subject; + } + } + + + private static IObservable HandleCompleted(Task task) + { + switch (task.Status) + { + case TaskStatus.RanToCompletion: + return Observable.Return((object)task.Result); + case TaskStatus.Faulted: + return Observable.Return(new BindingNotification(task.Exception, BindingErrorType.Error)); + default: + throw new AvaloniaInternalException("HandleCompleted called for non-completed Task."); + } + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs index 933d39bad8..9be4c0aa03 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -48,13 +48,17 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions new AvaloniaXamlIlSetterTransformer(), new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), new AvaloniaXamlIlConstructorServiceProviderTransformer(), - new AvaloniaXamlIlTransitionsTypeMetadataTransformer(), - new AvaloniaXamlIlDataContextTypeTransformer() + new AvaloniaXamlIlTransitionsTypeMetadataTransformer() ); - + // After everything else - - Transformers.Add(new AddNameScopeRegistration()); + + InsertBefore( + new AddNameScopeRegistration(), + new AvaloniaXamlIlDataContextTypeTransformer(), + new AvaloniaXamlIlBindingPathTransformer() + ); + Transformers.Add(new AvaloniaXamlIlMetadataRemover()); } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs index 33056fa3e8..87b5ec72fe 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs @@ -36,59 +36,65 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } if (value != null) + { + var objectType = context.ParentNodes().OfType().FirstOrDefault()?.Type.GetClrType(); return new XamlIlManipulationGroupNode(pa) { Children = { pa, - new ScopeRegistrationNode(value) + new ScopeRegistrationNode(value, objectType) } }; + } } return node; } + } - class ScopeRegistrationNode : XamlIlAstNode, IXamlIlAstManipulationNode, IXamlIlAstEmitableNode + class ScopeRegistrationNode : XamlIlAstNode, IXamlIlAstManipulationNode, IXamlIlAstEmitableNode + { + public IXamlIlAstValueNode Value { get; set; } + public IXamlIlType ControlType { get; } + + public ScopeRegistrationNode(IXamlIlAstValueNode value, IXamlIlType controlType) : base(value) { - public IXamlIlAstValueNode Value { get; set; } - public ScopeRegistrationNode(IXamlIlAstValueNode value) : base(value) - { - Value = value; - } + Value = value; + ControlType = controlType; + } - public override void VisitChildren(IXamlIlAstVisitor visitor) - => Value = (IXamlIlAstValueNode)Value.Visit(visitor); + public override void VisitChildren(IXamlIlAstVisitor visitor) + => Value = (IXamlIlAstValueNode)Value.Visit(visitor); - public XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + public XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + var exts = context.Configuration.TypeSystem.GetType("Avalonia.Controls.NameScopeExtensions"); + var findNameScope = exts.FindMethod(m => m.Name == "FindNameScope"); + var registerMethod = findNameScope.ReturnType.FindMethod(m => m.Name == "Register"); + using (var targetLoc = context.GetLocal(context.Configuration.WellKnownTypes.Object)) + using (var nameScopeLoc = context.GetLocal(findNameScope.ReturnType)) { - var exts = context.Configuration.TypeSystem.GetType("Avalonia.Controls.NameScopeExtensions"); - var findNameScope = exts.FindMethod(m => m.Name == "FindNameScope"); - var registerMethod = findNameScope.ReturnType.FindMethod(m => m.Name == "Register"); - using (var targetLoc = context.GetLocal(context.Configuration.WellKnownTypes.Object)) - using (var nameScopeLoc = context.GetLocal(findNameScope.ReturnType)) - { - var exit = codeGen.DefineLabel(); - codeGen - // var target = {pop} - .Stloc(targetLoc.Local) - // var scope = target.FindNameScope() - .Ldloc(targetLoc.Local) - .Castclass(findNameScope.Parameters[0]) - .EmitCall(findNameScope) - .Stloc(nameScopeLoc.Local) - // if({scope} != null) goto call; - .Ldloc(nameScopeLoc.Local) - .Brfalse(exit) - .Ldloc(nameScopeLoc.Local); - context.Emit(Value, codeGen, Value.Type.GetClrType()); - codeGen - .Ldloc(targetLoc.Local) - .EmitCall(registerMethod) - .MarkLabel(exit); - } - return XamlIlNodeEmitResult.Void(1); + var exit = codeGen.DefineLabel(); + codeGen + // var target = {pop} + .Stloc(targetLoc.Local) + // var scope = target.FindNameScope() + .Ldloc(targetLoc.Local) + .Castclass(findNameScope.Parameters[0]) + .EmitCall(findNameScope) + .Stloc(nameScopeLoc.Local) + // if({scope} != null) goto call; + .Ldloc(nameScopeLoc.Local) + .Brfalse(exit) + .Ldloc(nameScopeLoc.Local); + context.Emit(Value, codeGen, Value.Type.GetClrType()); + codeGen + .Ldloc(targetLoc.Local) + .EmitCall(registerMethod) + .MarkLabel(exit); } + return XamlIlNodeEmitResult.Void(1); } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs new file mode 100644 index 0000000000..cd7d9f5d33 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using XamlIl; +using XamlIl.Ast; +using XamlIl.Transform; +using XamlIl.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlBindingPathTransformer : IXamlIlAstTransformer + { + public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + { + if (node is XamlIlAstObjectNode binding && binding.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension)) + { + IXamlIlType startType; + var parentDataContextNode = context.ParentNodes().OfType().FirstOrDefault(); + if (parentDataContextNode is null) + { + throw new XamlIlParseException("Cannot parse a compiled binding without an explicit x:DataContextType directive to give a starting data type for bindings.", binding); + } + + startType = parentDataContextNode.DataContextType; + + XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, binding, startType); + } + + return node; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs index 5d68a72514..7e13b8a18a 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs @@ -1,18 +1,25 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text; +using Avalonia.Markup.Parsers; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; +using Avalonia.Utilities; using XamlIl; using XamlIl.Ast; using XamlIl.Transform; +using XamlIl.TypeSystem; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { class AvaloniaXamlIlDataContextTypeTransformer : IXamlIlAstTransformer { + private const string AvaloniaNs = "https://github.com/avaloniaui"; public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) { if (node is XamlIlAstObjectNode on) { + AvaloniaXamlIlDataContextTypeMetadataNode calculatedDataContextTypeNode = null; foreach (var child in on.Children) { if (child is XamlIlAstXmlDirective directive) @@ -23,11 +30,44 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers && directive.Values[0] is XamlIlTypeExtensionNode dataContextType) { on.Children.Remove(child); - return new AvaloniaXamlIlDataContextTypeMetadataNode(on, dataContextType.Value); + return new AvaloniaXamlIlDataContextTypeMetadataNode(on, dataContextType.Value.GetClrType()); } } + else if (child is XamlIlAstXamlPropertyValueNode pv + && pv.Property is XamlIlAstNamePropertyReference pref + && pref.Name == "DataContext" + && pref.DeclaringType is XamlIlAstXmlTypeReference tref + && tref.Name == "StyledElement" + && tref.XmlNamespace == AvaloniaNs) + { + var bindingType = context.GetAvaloniaTypes().IBinding; + if (!pv.Values[0].Type.GetClrType().GetAllInterfaces().Contains(bindingType)) + { + calculatedDataContextTypeNode = new AvaloniaXamlIlDataContextTypeMetadataNode(on, pv.Values[0].Type.GetClrType()); + } + else if(pv.Values[0].Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension) + && pv.Values[0] is XamlIlAstObjectNode binding) + { + IXamlIlType startType; + var parentDataContextNode = context.ParentNodes().OfType().FirstOrDefault(); + if (parentDataContextNode is null) + { + throw new XamlIlParseException("Cannot parse a compiled binding without an explicit x:DataContextType directive to give a starting data type for bindings.", binding); + } + + startType = parentDataContextNode.DataContextType; + + var bindingResultType = XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, binding, startType); + calculatedDataContextTypeNode = new AvaloniaXamlIlDataContextTypeMetadataNode(on, bindingResultType); + } + } + } + if (!(calculatedDataContextTypeNode is null)) + { + return calculatedDataContextTypeNode; } } + // TODO: Add node for DataTemplate scope. return node; } @@ -35,9 +75,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers class AvaloniaXamlIlDataContextTypeMetadataNode : XamlIlValueWithSideEffectNodeBase { - public IXamlIlAstTypeReference DataContextType { get; set; } + public IXamlIlType DataContextType { get; set; } - public AvaloniaXamlIlDataContextTypeMetadataNode(IXamlIlAstValueNode value, IXamlIlAstTypeReference targetType) + public AvaloniaXamlIlDataContextTypeMetadataNode(IXamlIlAstValueNode value, IXamlIlType targetType) : base(value, value) { DataContextType = targetType; diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs index bbacef43dd..0adbba72b1 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs @@ -187,6 +187,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers emitter.Unbox_Any(Parent.PropertyType); } + + public IXamlIlMethod MakeGenericMethod(IReadOnlyList typeArguments) + { + throw new System.InvalidOperationException(); + } } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index f9f59dfb87..740d62e85e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -23,7 +23,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlIlType ClrPropertyInfo { get; } public IXamlIlType PropertyPath { get; } public IXamlIlType PropertyPathBuilder { get; } - + public IXamlIlType NotifyingPropertyInfoHelpers { get; } + public IXamlIlType CompiledBindingPathBuilder { get; } + public IXamlIlType CompiledBindingPath { get; } + public IXamlIlType CompiledBindingExtension { get; } + public AvaloniaXamlIlWellKnownTypes(XamlIlTransformerConfiguration cfg) { XamlIlTypes = cfg.WellKnownTypes; @@ -47,6 +51,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers ClrPropertyInfo = cfg.TypeSystem.GetType("Avalonia.Data.Core.ClrPropertyInfo"); PropertyPath = cfg.TypeSystem.GetType("Avalonia.Data.Core.PropertyPath"); PropertyPathBuilder = cfg.TypeSystem.GetType("Avalonia.Data.Core.PropertyPathBuilder"); + NotifyingPropertyInfoHelpers = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.NotifyingPropertyInfoHelpers"); + CompiledBindingPathBuilder = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CompiledBindingPathBuilder"); + CompiledBindingPath = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CompiledBindingPath"); + CompiledBindingExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindingExtension"); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs new file mode 100644 index 0000000000..c98e2029c4 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -0,0 +1,459 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Linq; +using Avalonia.Markup.Parsers; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; +using XamlIl.Ast; +using XamlIl.Transform; +using XamlIl.Transform.Transformers; +using XamlIl.TypeSystem; +using XamlIl; +using Avalonia.Utilities; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions +{ + static class XamlIlBindingPathHelper + { + public static IXamlIlType UpdateCompiledBindingExtension(XamlIlAstTransformationContext context, XamlIlAstObjectNode binding, IXamlIlType startType) + { + IXamlIlType bindingResultType = null; + if (binding.Arguments.Count > 0 && binding.Arguments[0] is XamlIlAstTextNode bindingPathText) + { + var reader = new CharacterReader(bindingPathText.Text.AsSpan()); + var grammar = BindingExpressionGrammar.Parse(ref reader); + + var transformed = TransformBindingPath( + context, + bindingPathText, + startType, + grammar.Nodes); + + bindingResultType = transformed.BindingResultType; + binding.Arguments[0] = transformed; + } + else + { + var bindingPathAssignment = binding.Children.OfType() + .FirstOrDefault(v => v.Property.GetClrProperty().Name == "Path"); + + if (bindingPathAssignment is null) + { + return startType; + } + + if (bindingPathAssignment.Values[0] is XamlIlAstTextNode pathValue) + { + var reader = new CharacterReader(pathValue.Text.AsSpan()); + var grammar = BindingExpressionGrammar.Parse(ref reader); + + var transformed = TransformBindingPath( + context, + pathValue, + startType, + grammar.Nodes); + + bindingResultType = transformed.BindingResultType; + bindingPathAssignment.Values[0] = transformed; + } + else + { + throw new InvalidOperationException(); + } + } + + return bindingResultType; + } + + private static IXamlIlBindingPathNode TransformBindingPath(XamlIlAstTransformationContext context, IXamlIlLineInfo lineInfo, IXamlIlType startType, IEnumerable bindingExpression) + { + bool appendNotNode = false; + List nodes = new List(); + foreach (var astNode in bindingExpression) + { + var targetType = nodes.Count == 0 ? startType : nodes[nodes.Count - 1].Type; + switch (astNode) + { + case BindingExpressionGrammar.EmptyExpressionNode _: + break; + case BindingExpressionGrammar.NotNode _: + appendNotNode = !appendNotNode; + break; + case BindingExpressionGrammar.StreamNode _: + var observableType = targetType.GetAllInterfaces().FirstOrDefault(i => i.GenericTypeDefinition.Equals(context.Configuration.TypeSystem.FindType("System.IObservable`1"))); + if (observableType != null) + { + nodes.Add(new XamlIlStreamObservablePathElementNode(observableType.GenericArguments[0])); + break; + } + bool foundTask = false; + for (var currentType = targetType; currentType != null; currentType = currentType.BaseType) + { + if (currentType.GenericTypeDefinition.Equals(context.Configuration.TypeSystem.GetType("System.Threading.Tasks.Task`1"))) + { + foundTask = true; + nodes.Add(new XamlIlStreamTaskPathElementNode(currentType.GenericArguments[0])); + break; + } + } + if (foundTask) + { + break; + } + throw new XamlIlParseException($"Compiled bindings do not support stream bindings for objects of type {targetType.FullName}.", lineInfo); + case BindingExpressionGrammar.PropertyNameNode propName: + var avaloniaPropertyFieldNameMaybe = propName.PropertyName + "Property"; + var avaloniaPropertyFieldMaybe = targetType.GetAllFields().FirstOrDefault(f => + f.IsStatic && f.IsPublic && f.Name == avaloniaPropertyFieldNameMaybe); + + if (avaloniaPropertyFieldMaybe != null) + { + nodes.Add(new XamlIlAvaloniaPropertyPropertyPathElementNode(avaloniaPropertyFieldMaybe, + XamlIlAvaloniaPropertyHelper.GetAvaloniaPropertyType(avaloniaPropertyFieldMaybe, context.GetAvaloniaTypes(), lineInfo))); + } + else + { + var clrProperty = targetType.GetAllProperties().FirstOrDefault(p => p.Name == propName.PropertyName); + nodes.Add(new XamlIlClrPropertyPathElementNode(clrProperty)); + } + break; + case BindingExpressionGrammar.IndexerNode indexer: + { + IXamlIlProperty property = null; + for (var currentType = targetType; currentType != null; currentType = currentType.BaseType) + { + var defaultMemberAttribute = currentType.CustomAttributes.FirstOrDefault(x => x.Type.GetFullName() == "System.Reflection.DefaultMemberAttribute"); + if (defaultMemberAttribute != null) + { + property = targetType.GetAllProperties().FirstOrDefault(x => x.Name == (string)defaultMemberAttribute.Parameters[0]); + break; + } + + }; + if (property is null) + { + throw new XamlIlParseException($"The type '${targetType}' does not have an indexer.", lineInfo); + } + + IEnumerable parameters = property.IndexerParameters; + + List values = new List(); + int currentParamIndex = 0; + foreach (var param in parameters) + { + var textNode = new XamlIlAstTextNode(lineInfo, indexer.Arguments[currentParamIndex]); + if (!XamlIlTransformHelpers.TryGetCorrectlyTypedValue(context, textNode, + param, out var converted)) + throw new XamlIlParseException( + $"Unable to convert indexer parameter value of '{indexer.Arguments[currentParamIndex]}' to {param.GetFqn()}", + textNode); + + values.Add(converted); + currentParamIndex++; + } + + bool isNotifyingCollection = targetType.GetAllInterfaces().Any(i => i.FullName == "System.Collections.Specialized.INotifyCollectionChanged"); + + nodes.Add(new XamlIlClrIndexerPathElementNode(property, values, isNotifyingCollection)); + break; + } + case BindingExpressionGrammar.AttachedPropertyNameNode attachedProp: + var avaloniaPropertyFieldName = attachedProp.PropertyName + "Property"; + var avaloniaPropertyField = GetType(attachedProp.Namespace, attachedProp.TypeName).GetAllFields().FirstOrDefault(f => + f.IsStatic && f.IsPublic && f.Name == avaloniaPropertyFieldName); + nodes.Add(new XamlIlAvaloniaPropertyPropertyPathElementNode(avaloniaPropertyField, + XamlIlAvaloniaPropertyHelper.GetAvaloniaPropertyType(avaloniaPropertyField, context.GetAvaloniaTypes(), lineInfo))); + break; + case BindingExpressionGrammar.SelfNode _: + nodes.Add(new SelfPathElementNode(targetType)); + break; + case BindingExpressionGrammar.AncestorNode ancestor: + nodes.Add(new FindAncestorPathElementNode(GetType(ancestor.Namespace, ancestor.TypeName), ancestor.Level)); + break; + case BindingExpressionGrammar.NameNode elementName: + var elementType = ScopeRegistrationFinder.GetControlType(context, context.RootObject, elementName.Name); + if (elementType is null) + { + throw new XamlIlParseException($"Unable to find element '{elementName.Name}' in the current namescope. Unable to use a compiled binding with a name binding if the name cannot be found at compile time.", lineInfo); + } + nodes.Add(new ElementNamePathElementNode(elementName.Name, elementType)); + break; + } + + } + + if (appendNotNode) + { + // TODO: Fix Not behavior + nodes.Add(new XamlIlNotPathElementNode(context.Configuration.WellKnownTypes.Boolean)); + } + + return new XamlIlBindingPathNode(lineInfo, context.GetAvaloniaTypes().CompiledBindingPath, nodes); + + IXamlIlType GetType(string ns, string name) + { + return XamlIlTypeReferenceResolver.ResolveType(context, $"{ns}:{name}", false, + lineInfo, true).GetClrType(); + } + } + + class ScopeRegistrationFinder : IXamlIlAstTransformer + { + private ScopeRegistrationFinder(string name) + { + Name = name; + } + + string Name { get; } + + IXamlIlType ControlType { get; set; } + + public static IXamlIlType GetControlType(XamlIlAstTransformationContext context, IXamlIlAstNode namescopeRoot, string name) + { + var finder = new ScopeRegistrationFinder(name); + context.Visit(namescopeRoot, finder); + return finder.ControlType; + } + + IXamlIlAstNode IXamlIlAstTransformer.Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + { + if (node is ScopeRegistrationNode registration) + { + if (registration.Value is XamlIlAstTextNode text && text.Text == Name) + { + ControlType = registration.ControlType; + } + } + return node; + } + } + + interface IXamlIlBindingPathElementNode + { + IXamlIlType Type { get; } + + void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen); + } + + class XamlIlNotPathElementNode : IXamlIlBindingPathElementNode + { + public XamlIlNotPathElementNode(IXamlIlType boolType) + { + Type = boolType; + } + + public IXamlIlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "Not")); + } + } + + class XamlIlStreamObservablePathElementNode : IXamlIlBindingPathElementNode + { + public XamlIlStreamObservablePathElementNode(IXamlIlType type) + { + Type = type; + } + + public IXamlIlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "StreamObservable").MakeGenericMethod(new[] { Type })); + } + } + + class XamlIlStreamTaskPathElementNode : IXamlIlBindingPathElementNode + { + public XamlIlStreamTaskPathElementNode(IXamlIlType type) + { + Type = type; + } + + public IXamlIlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "StreamTask").MakeGenericMethod(new[] { Type })); + } + } + + class SelfPathElementNode : IXamlIlBindingPathElementNode + { + public SelfPathElementNode(IXamlIlType type) + { + Type = type; + } + + public IXamlIlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "Self")); + } + } + + class FindAncestorPathElementNode : IXamlIlBindingPathElementNode + { + private readonly int _level; + + public FindAncestorPathElementNode(IXamlIlType ancestorType, int level) + { + Type = ancestorType; + _level = level; + } + + public IXamlIlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + codeGen.Ldtype(Type) + .Ldc_I4(_level) + .EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "FindAncestor")); + } + } + + class ElementNamePathElementNode : IXamlIlBindingPathElementNode + { + private readonly string _name; + + public ElementNamePathElementNode(string name, IXamlIlType elementType) + { + _name = name; + Type = elementType; + } + + public IXamlIlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + codeGen.Ldstr(_name) + .EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "ElementName")); + } + } + + class XamlIlAvaloniaPropertyPropertyPathElementNode : IXamlIlBindingPathElementNode + { + private readonly IXamlIlField _field; + + public XamlIlAvaloniaPropertyPropertyPathElementNode(IXamlIlField field, IXamlIlType propertyType) + { + _field = field; + Type = propertyType; + } + + public void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + => codeGen + .Ldsfld(_field) + .EmitCall(context.GetAvaloniaTypes() + .NotifyingPropertyInfoHelpers.FindMethod(m => m.Name == "CreateAvaloniaPropertyInfo")) + .EmitCall(context.GetAvaloniaTypes() + .CompiledBindingPathBuilder.FindMethod(m => m.Name == "Property")); + + public IXamlIlType Type { get; } + } + + class XamlIlClrPropertyPathElementNode : IXamlIlBindingPathElementNode + { + private readonly IXamlIlProperty _property; + + public XamlIlClrPropertyPathElementNode(IXamlIlProperty property) + { + _property = property; + } + + public void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + context.Configuration.GetExtra() + .Emit(context, codeGen, _property); + + codeGen + .EmitCall(context.GetAvaloniaTypes() + .NotifyingPropertyInfoHelpers.FindMethod(m => m.Name == "CreateINPCPropertyInfo")) + .EmitCall(context.GetAvaloniaTypes() + .CompiledBindingPathBuilder.FindMethod(m => m.Name == "Property")); + } + + public IXamlIlType Type => _property.Getter?.ReturnType ?? _property.Setter?.Parameters[0]; + } + + class XamlIlClrIndexerPathElementNode : IXamlIlBindingPathElementNode + { + private readonly IXamlIlProperty _property; + private readonly List _values; + private readonly bool _isNotifyingCollection; + + public XamlIlClrIndexerPathElementNode(IXamlIlProperty property, List values, bool isNotifyingCollection) + { + _property = property; + _values = values; + _isNotifyingCollection = isNotifyingCollection; + } + + public void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + var intType = context.Configuration.TypeSystem.GetType("System.Int32"); + context.Configuration.GetExtra() + .Emit(context, codeGen, _property, _values); + + if (_isNotifyingCollection + && + _values.Count == 1 + && _values[0].Type.GetClrType().Equals(intType)) + { + context.Emit(_values[0], codeGen, intType); + codeGen.EmitCall(context.GetAvaloniaTypes() + .NotifyingPropertyInfoHelpers.FindMethod(m => m.Name == "CreateIndexerPropertyInfo")); + } + else + { + codeGen.EmitCall(context.GetAvaloniaTypes() + .NotifyingPropertyInfoHelpers.FindMethod(m => m.Name == "CreateINPCPropertyInfo")); + } + + codeGen.EmitCall(context.GetAvaloniaTypes() + .CompiledBindingPathBuilder.FindMethod(m => m.Name == "Property")); + } + + public IXamlIlType Type => _property.Getter?.ReturnType ?? _property.Setter?.Parameters[0]; + } + + class XamlIlBindingPathNode : XamlIlAstNode, IXamlIlBindingPathNode, IXamlIlAstEmitableNode + { + private readonly List _elements; + + public XamlIlBindingPathNode(IXamlIlLineInfo lineInfo, + IXamlIlType bindingPathType, + List elements) : base(lineInfo) + { + Type = new XamlIlAstClrTypeReference(lineInfo, bindingPathType, false); + _elements = elements; + } + + public IXamlIlType BindingResultType => _elements[_elements.Count - 1].Type; + + public IXamlIlAstTypeReference Type { get; } + + public XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + var types = context.GetAvaloniaTypes(); + codeGen.Newobj(types.CompiledBindingPathBuilder.FindConstructor()); + + foreach (var element in _elements) + { + element.Emit(context, codeGen); + } + + codeGen.EmitCall(types.CompiledBindingPathBuilder.FindMethod(m => m.Name == "Build")); + return XamlIlNodeEmitResult.Type(0, types.CompiledBindingPath); + } + } + } + + interface IXamlIlBindingPathNode : IXamlIlAstValueNode + { + IXamlIlType BindingResultType { get; } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs index 95ec8536fc..43d5a4eba1 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs @@ -23,8 +23,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions static string GetKey(IXamlIlProperty property) => property.Getter.DeclaringType.GetFullName() + "." + property.Name; - public IXamlIlType Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen, IXamlIlProperty property) + public IXamlIlType Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen, IXamlIlProperty property, IEnumerable indexerArguments = null) { + indexerArguments = indexerArguments ?? Enumerable.Empty(); var types = context.GetAvaloniaTypes(); IXamlIlMethod Get() { @@ -55,6 +56,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions cg.Unbox(m.DeclaringType); else cg.Castclass(m.DeclaringType); + + foreach (var indexerArg in indexerArguments) + { + context.Emit(indexerArg, cg, indexerArg.Type.GetClrType()); + } } var getter = property.Getter == null ? @@ -95,7 +101,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions var ctor = types.ClrPropertyInfo.Constructors.First(c => - c.Parameters.Count == 3 && c.IsStatic == false); + c.Parameters.Count == 4 && c.IsStatic == false); var cacheMiss = get.Generator.DefineLabel(); get.Generator @@ -124,6 +130,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions EmitFunc(get.Generator, getter, ctor.Parameters[1]); EmitFunc(get.Generator, setter, ctor.Parameters[2]); get.Generator + .Ldtype(property.PropertyType) .Newobj(ctor) .Stsfld(field) .Ldsfld(field) diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index 0b85e3224d..4b62b87199 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -17,14 +17,14 @@ namespace Avalonia.Data /// /// A XAML binding. /// - public class Binding : IBinding + public class Binding : BindingBase { /// /// Initializes a new instance of the class. /// public Binding() + :base() { - FallbackValue = AvaloniaProperty.UnsetValue; } /// @@ -33,47 +33,16 @@ namespace Avalonia.Data /// The binding path. /// The binding mode. public Binding(string path, BindingMode mode = BindingMode.Default) - : this() + : base(mode) { Path = path; - Mode = mode; } - /// - /// Gets or sets the to use. - /// - public IValueConverter Converter { get; set; } - - /// - /// Gets or sets a parameter to pass to . - /// - public object ConverterParameter { get; set; } - /// /// Gets or sets the name of the element to use as the binding source. /// public string ElementName { get; set; } - /// - /// Gets or sets the value to use when the binding is unable to produce a value. - /// - public object FallbackValue { get; set; } - - /// - /// Gets or sets the binding mode. - /// - public BindingMode Mode { get; set; } - - /// - /// Gets or sets the binding path. - /// - public string Path { get; set; } = ""; - - /// - /// Gets or sets the binding priority. - /// - public BindingPriority Priority { get; set; } - /// /// Gets or sets the relative source for the binding. /// @@ -85,64 +54,50 @@ namespace Avalonia.Data public object Source { get; set; } /// - /// Gets or sets the string format. + /// Gets or sets the binding path. /// - public string StringFormat { get; set; } - - public WeakReference DefaultAnchor { get; set; } + public string Path { get; set; } = ""; /// /// Gets or sets a function used to resolve types from names in the binding path. /// public Func TypeResolver { get; set; } - /// - public InstancedBinding Initiate( - IAvaloniaObject target, - AvaloniaProperty targetProperty, - object anchor = null, - bool enableDataValidation = false) + protected override ExpressionObserver CreateExpressionObserver(IAvaloniaObject target, AvaloniaProperty targetProperty, object anchor, bool enableDataValidation) { - Contract.Requires(target != null); - anchor = anchor ?? DefaultAnchor?.Target; - - enableDataValidation = enableDataValidation && Priority == BindingPriority.LocalValue; - - ExpressionObserver observer; - - var (node, mode) = ExpressionObserverBuilder.Parse(Path, enableDataValidation, TypeResolver); + var (node, mode) = ExpressionObserverBuilder.Parse(Path, enableDataValidation, TypeResolver); if (ElementName != null) { - observer = CreateElementObserver( + return CreateElementObserver( (target as IStyledElement) ?? (anchor as IStyledElement), ElementName, node); } else if (Source != null) { - observer = CreateSourceObserver(Source, node); + return CreateSourceObserver(Source, node); } else if (RelativeSource == null) { if (mode == SourceMode.Data) { - observer = CreateDataContextObserver( + return CreateDataContextObserver( target, node, targetProperty == StyledElement.DataContextProperty, - anchor); + anchor); } else { - observer = new ExpressionObserver( + return CreateSourceObserver( (target as IStyledElement) ?? (anchor as IStyledElement), node); } } else if (RelativeSource.Mode == RelativeSourceMode.DataContext) { - observer = CreateDataContextObserver( + return CreateDataContextObserver( target, node, targetProperty == StyledElement.DataContextProperty, @@ -150,13 +105,13 @@ namespace Avalonia.Data } else if (RelativeSource.Mode == RelativeSourceMode.Self) { - observer = CreateSourceObserver( + return CreateSourceObserver( (target as IStyledElement) ?? (anchor as IStyledElement), node); } else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent) { - observer = CreateTemplatedParentObserver( + return CreateTemplatedParentObserver( (target as IStyledElement) ?? (anchor as IStyledElement), node); } @@ -167,7 +122,7 @@ namespace Avalonia.Data throw new InvalidOperationException("AncestorType must be set for RelativeSourceMode.FindAncestor when searching the visual tree."); } - observer = CreateFindAncestorObserver( + return CreateFindAncestorObserver( (target as IStyledElement) ?? (anchor as IStyledElement), RelativeSource, node); @@ -177,192 +132,6 @@ namespace Avalonia.Data throw new NotSupportedException(); } - var fallback = FallbackValue; - - // If we're binding to DataContext and our fallback is UnsetValue then override - // the fallback value to null, as broken bindings to DataContext must reset the - // DataContext in order to not propagate incorrect DataContexts to child controls. - // See Avalonia.Markup.UnitTests.Data.DataContext_Binding_Should_Produce_Correct_Results. - if (targetProperty == StyledElement.DataContextProperty && fallback == AvaloniaProperty.UnsetValue) - { - fallback = null; - } - - var converter = Converter; - var targetType = targetProperty?.PropertyType ?? typeof(object); - - // We only respect `StringFormat` if the type of the property we're assigning to will - // accept a string. Note that this is slightly different to WPF in that WPF only applies - // `StringFormat` for target type `string` (not `object`). - if (!string.IsNullOrWhiteSpace(StringFormat) && - (targetType == typeof(string) || targetType == typeof(object))) - { - converter = new StringFormatValueConverter(StringFormat, converter); - } - - var subject = new BindingExpression( - observer, - targetType, - fallback, - converter ?? DefaultValueConverter.Instance, - ConverterParameter, - Priority); - - return new InstancedBinding(subject, Mode, Priority); - } - - private ExpressionObserver CreateDataContextObserver( - IAvaloniaObject target, - ExpressionNode node, - bool targetIsDataContext, - object anchor) - { - Contract.Requires(target != null); - - if (!(target is IStyledElement)) - { - target = anchor as IStyledElement; - - if (target == null) - { - throw new InvalidOperationException("Cannot find a DataContext to bind to."); - } - } - - if (!targetIsDataContext) - { - var result = new ExpressionObserver( - () => target.GetValue(StyledElement.DataContextProperty), - node, - new UpdateSignal(target, StyledElement.DataContextProperty), - null); - - return result; - } - else - { - return new ExpressionObserver( - GetParentDataContext(target), - node, - null); - } - } - - private ExpressionObserver CreateElementObserver( - IStyledElement target, - string elementName, - ExpressionNode node) - { - Contract.Requires(target != null); - - var result = new ExpressionObserver( - ControlLocator.Track(target, elementName), - node, - null); - return result; - } - - private ExpressionObserver CreateFindAncestorObserver( - IStyledElement target, - RelativeSource relativeSource, - ExpressionNode node) - { - Contract.Requires(target != null); - - IObservable controlLocator; - - switch (relativeSource.Tree) - { - case TreeType.Logical: - controlLocator = ControlLocator.Track( - (ILogical)target, - relativeSource.AncestorLevel - 1, - relativeSource.AncestorType); - break; - case TreeType.Visual: - controlLocator = VisualLocator.Track( - (IVisual)target, - relativeSource.AncestorLevel - 1, - relativeSource.AncestorType); - break; - default: - throw new InvalidOperationException("Invalid tree to traverse."); - } - - return new ExpressionObserver( - controlLocator, - node, - null); - } - - private ExpressionObserver CreateSourceObserver( - object source, - ExpressionNode node) - { - Contract.Requires(source != null); - - return new ExpressionObserver(source, node); - } - - private ExpressionObserver CreateTemplatedParentObserver( - IAvaloniaObject target, - ExpressionNode node) - { - Contract.Requires(target != null); - - var result = new ExpressionObserver( - () => target.GetValue(StyledElement.TemplatedParentProperty), - node, - new UpdateSignal(target, StyledElement.TemplatedParentProperty), - null); - - return result; - } - - private IObservable GetParentDataContext(IAvaloniaObject target) - { - // The DataContext is based on the visual parent and not the logical parent: this may - // seem counter intuitive considering the fact that property inheritance works on the logical - // tree, but consider a ContentControl with a ContentPresenter. The ContentControl's - // Content property is bound to a value which becomes the ContentPresenter's - // DataContext - it is from this that the child hosted by the ContentPresenter needs to - // inherit its DataContext. - return target.GetObservable(Visual.VisualParentProperty) - .Select(x => - { - return (x as IAvaloniaObject)?.GetObservable(StyledElement.DataContextProperty) ?? - Observable.Return((object)null); - }).Switch(); - } - - private class UpdateSignal : SingleSubscriberObservableBase - { - private readonly IAvaloniaObject _target; - private readonly AvaloniaProperty _property; - - public UpdateSignal(IAvaloniaObject target, AvaloniaProperty property) - { - _target = target; - _property = property; - } - - protected override void Subscribed() - { - _target.PropertyChanged += PropertyChanged; - } - - protected override void Unsubscribed() - { - _target.PropertyChanged -= PropertyChanged; - } - - private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) - { - if (e.Property == _property) - { - PublishNext(Unit.Default); - } - } } } } diff --git a/src/Markup/Avalonia.Markup/Data/BindingBase.cs b/src/Markup/Avalonia.Markup/Data/BindingBase.cs new file mode 100644 index 0000000000..9c4ea27fda --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/BindingBase.cs @@ -0,0 +1,275 @@ + +using System; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using Avalonia.Data.Converters; +using Avalonia.Data.Core; +using Avalonia.LogicalTree; +using Avalonia.Markup.Parsers; +using Avalonia.Reactive; +using Avalonia.VisualTree; + + +namespace Avalonia.Data +{ + public abstract class BindingBase + { + /// + /// Initializes a new instance of the class. + /// + public BindingBase() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The binding mode. + public BindingBase(BindingMode mode = BindingMode.Default) + { + FallbackValue = AvaloniaProperty.UnsetValue; + Mode = mode; + } + + /// + /// Gets or sets the to use. + /// + public IValueConverter Converter { get; set; } + + /// + /// Gets or sets a parameter to pass to . + /// + public object ConverterParameter { get; set; } + + /// + /// Gets or sets the value to use when the binding is unable to produce a value. + /// + public object FallbackValue { get; set; } + + /// + /// Gets or sets the binding mode. + /// + public BindingMode Mode { get; set; } + + /// + /// Gets or sets the binding priority. + /// + public BindingPriority Priority { get; set; } + + /// + /// Gets or sets the string format. + /// + public string StringFormat { get; set; } + + public WeakReference DefaultAnchor { get; set; } + + protected abstract ExpressionObserver CreateExpressionObserver( + IAvaloniaObject target, + AvaloniaProperty targetProperty, + object anchor, + bool enableDataValidation); + + /// + public InstancedBinding Initiate( + IAvaloniaObject target, + AvaloniaProperty targetProperty, + object anchor = null, + bool enableDataValidation = false) + { + Contract.Requires(target != null); + anchor = anchor ?? DefaultAnchor?.Target; + + enableDataValidation = enableDataValidation && Priority == BindingPriority.LocalValue; + + var observer = CreateExpressionObserver(target, targetProperty, anchor, enableDataValidation); + + var fallback = FallbackValue; + + // If we're binding to DataContext and our fallback is UnsetValue then override + // the fallback value to null, as broken bindings to DataContext must reset the + // DataContext in order to not propagate incorrect DataContexts to child controls. + // See Avalonia.Markup.UnitTests.Data.DataContext_Binding_Should_Produce_Correct_Results. + if (targetProperty == StyledElement.DataContextProperty && fallback == AvaloniaProperty.UnsetValue) + { + fallback = null; + } + + var converter = Converter; + var targetType = targetProperty?.PropertyType ?? typeof(object); + + // We only respect `StringFormat` if the type of the property we're assigning to will + // accept a string. Note that this is slightly different to WPF in that WPF only applies + // `StringFormat` for target type `string` (not `object`). + if (!string.IsNullOrWhiteSpace(StringFormat) && + (targetType == typeof(string) || targetType == typeof(object))) + { + converter = new StringFormatValueConverter(StringFormat, converter); + } + + var subject = new BindingExpression( + observer, + targetType, + fallback, + converter ?? DefaultValueConverter.Instance, + ConverterParameter, + Priority); + + return new InstancedBinding(subject, Mode, Priority); + } + + protected ExpressionObserver CreateDataContextObserver( + IAvaloniaObject target, + ExpressionNode node, + bool targetIsDataContext, + object anchor) + { + Contract.Requires(target != null); + + if (!(target is IStyledElement)) + { + target = anchor as IStyledElement; + + if (target == null) + { + throw new InvalidOperationException("Cannot find a DataContext to bind to."); + } + } + + if (!targetIsDataContext) + { + var result = new ExpressionObserver( + () => target.GetValue(StyledElement.DataContextProperty), + node, + new UpdateSignal(target, StyledElement.DataContextProperty), + null); + + return result; + } + else + { + return new ExpressionObserver( + GetParentDataContext(target), + node, + null); + } + } + + protected ExpressionObserver CreateElementObserver( + IStyledElement target, + string elementName, + ExpressionNode node) + { + Contract.Requires(target != null); + + var result = new ExpressionObserver( + ControlLocator.Track(target, elementName), + node, + null); + return result; + } + + protected ExpressionObserver CreateFindAncestorObserver( + IStyledElement target, + RelativeSource relativeSource, + ExpressionNode node) + { + Contract.Requires(target != null); + + IObservable controlLocator; + + switch (relativeSource.Tree) + { + case TreeType.Logical: + controlLocator = ControlLocator.Track( + (ILogical)target, + relativeSource.AncestorLevel - 1, + relativeSource.AncestorType); + break; + case TreeType.Visual: + controlLocator = VisualLocator.Track( + (IVisual)target, + relativeSource.AncestorLevel - 1, + relativeSource.AncestorType); + break; + default: + throw new InvalidOperationException("Invalid tree to traverse."); + } + + return new ExpressionObserver( + controlLocator, + node, + null); + } + + protected ExpressionObserver CreateSourceObserver( + object source, + ExpressionNode node) + { + Contract.Requires(source != null); + + return new ExpressionObserver(source, node); + } + + protected ExpressionObserver CreateTemplatedParentObserver( + IAvaloniaObject target, + ExpressionNode node) + { + Contract.Requires(target != null); + + var result = new ExpressionObserver( + () => target.GetValue(StyledElement.TemplatedParentProperty), + node, + new UpdateSignal(target, StyledElement.TemplatedParentProperty), + null); + + return result; + } + + protected IObservable GetParentDataContext(IAvaloniaObject target) + { + // The DataContext is based on the visual parent and not the logical parent: this may + // seem counter intuitive considering the fact that property inheritance works on the logical + // tree, but consider a ContentControl with a ContentPresenter. The ContentControl's + // Content property is bound to a value which becomes the ContentPresenter's + // DataContext - it is from this that the child hosted by the ContentPresenter needs to + // inherit its DataContext. + return target.GetObservable(Visual.VisualParentProperty) + .Select(x => + { + return (x as IAvaloniaObject)?.GetObservable(StyledElement.DataContextProperty) ?? + Observable.Return((object)null); + }).Switch(); + } + + private class UpdateSignal : SingleSubscriberObservableBase + { + private readonly IAvaloniaObject _target; + private readonly AvaloniaProperty _property; + + public UpdateSignal(IAvaloniaObject target, AvaloniaProperty property) + { + _target = target; + _property = property; + } + + protected override void Subscribed() + { + _target.PropertyChanged += PropertyChanged; + } + + protected override void Unsubscribed() + { + _target.PropertyChanged -= PropertyChanged; + } + + private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _property) + { + PublishNext(Unit.Default); + } + } + } + } +} diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs index fd6f7b20bd..4d88b34659 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs @@ -7,7 +7,10 @@ using Avalonia.Utilities; namespace Avalonia.Markup.Parsers { - internal static class ArgumentListParser +#if !BUILDTASK + public +#endif + static class ArgumentListParser { public static IList ParseArguments(this ref CharacterReader r, char open, char close, char delimiter = ',') { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs index b8308155fe..7181bbaa69 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs @@ -2,19 +2,18 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using Avalonia.Data.Core; -using Avalonia.Markup.Parsers.Nodes; using Avalonia.Utilities; using System; using System.Collections.Generic; -using System.Linq; namespace Avalonia.Markup.Parsers { - internal enum SourceMode + public enum SourceMode { Data, Control } + internal static class BindingExpressionGrammar { public static (IList Nodes, SourceMode Mode) Parse(ref CharacterReader r) @@ -191,7 +190,7 @@ namespace Avalonia.Markup.Parsers { var name = r.ParseIdentifier(); - if (name == null) + if (name.IsEmpty) { throw new ExpressionParseException(r.Position, "Element name expected after '#'."); } @@ -204,11 +203,11 @@ namespace Avalonia.Markup.Parsers { var mode = r.ParseIdentifier(); - if (mode.Equals("self".AsSpan(), StringComparison.InvariantCulture)) + if (mode.SequenceEqual("self".AsSpan())) { nodes.Add(new SelfNode()); } - else if (mode.Equals("parent".AsSpan(), StringComparison.InvariantCulture)) + else if (mode.SequenceEqual("parent".AsSpan())) { string ancestorNamespace = null; string ancestorType = null; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs index f09efca7d0..7803a7227d 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs @@ -4,7 +4,7 @@ using Avalonia.LogicalTree; namespace Avalonia.Markup.Parsers.Nodes { - internal class ElementNameNode : ExpressionNode + public class ElementNameNode : ExpressionNode { private readonly string _name; private IDisposable _subscription; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs index 221df44327..9f441ad360 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs @@ -4,7 +4,7 @@ using Avalonia.LogicalTree; namespace Avalonia.Markup.Parsers.Nodes { - internal class FindAncestorNode : ExpressionNode + public class FindAncestorNode : ExpressionNode { private readonly int _level; private readonly Type _ancestorType; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs index 2cb87efa65..1cd233c68a 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs @@ -2,7 +2,7 @@ namespace Avalonia.Markup.Parsers.Nodes { - internal class SelfNode : ExpressionNode + public class SelfNode : ExpressionNode { public override string Description => "$self"; } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs new file mode 100644 index 0000000000..59daf95637 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Controls; +using Avalonia.Markup.Data; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions +{ + public class CompiledBindingExtensionTests + { + [Fact] + public void ResolvesClrPropertyBasedOnDataContextType() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + DelayedBinding.ApplyBindings(textBlock); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, textBlock.Text); + } + } + } + + public class TestDataContext + { + public string StringProperty { get; set; } + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs index 6bc7f33039..8eaa391a5f 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs @@ -290,8 +290,19 @@ namespace Avalonia.Markup.Xaml.UnitTests Assert.Equal(typeof(ScaleTransform), ((EnsureTypePropertyPathElement)s3e[1]).Type); Assert.IsType(s3e[2]); Assert.Equal("ScaleX", ((AvaloniaProperty)((PropertyPropertyPathElement)s3e[3]).Property).Name); - - + } + } + + [Fact] + public void DataContextType_Resolution() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parsed = AvaloniaXamlLoader.Parse(@" +"); } } } From b1a2ea21c586b0334a89f0ddd9c252075c9c9a4e Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 6 Jul 2019 12:42:54 -0700 Subject: [PATCH 011/711] Fix stream bindings. --- .../XamlIlBindingPathHelper.cs | 2 +- .../Avalonia.Markup/Data/BindingBase.cs | 2 +- .../CompiledBindingExtensionTests.cs | 32 +++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs index c98e2029c4..dfa90d1989 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -80,7 +80,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions appendNotNode = !appendNotNode; break; case BindingExpressionGrammar.StreamNode _: - var observableType = targetType.GetAllInterfaces().FirstOrDefault(i => i.GenericTypeDefinition.Equals(context.Configuration.TypeSystem.FindType("System.IObservable`1"))); + var observableType = targetType.GetAllInterfaces().FirstOrDefault(i => i.GenericTypeDefinition?.Equals(context.Configuration.TypeSystem.FindType("System.IObservable`1")) ?? false); if (observableType != null) { nodes.Add(new XamlIlStreamObservablePathElementNode(observableType.GenericArguments[0])); diff --git a/src/Markup/Avalonia.Markup/Data/BindingBase.cs b/src/Markup/Avalonia.Markup/Data/BindingBase.cs index 9c4ea27fda..16ada21f56 100644 --- a/src/Markup/Avalonia.Markup/Data/BindingBase.cs +++ b/src/Markup/Avalonia.Markup/Data/BindingBase.cs @@ -13,7 +13,7 @@ using Avalonia.VisualTree; namespace Avalonia.Data { - public abstract class BindingBase + public abstract class BindingBase : IBinding { /// /// Initializes a new instance of the class. diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 59daf95637..3d1a5cb566 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text; +using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Markup.Data; using Avalonia.UnitTests; @@ -38,10 +39,41 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions Assert.Equal(dataContext.StringProperty, textBlock.Text); } } + + [Fact] + public void ResolvesStreamBindingCorrectly() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + DelayedBinding.ApplyBindings(textBlock); + + var dataContext = new TestDataContext + { + TaskProperty = Task.FromResult("foobar") + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.TaskProperty.Result, textBlock.Text); + } + } } public class TestDataContext { public string StringProperty { get; set; } + + public Task TaskProperty { get; set; } } } From 590f5923a85d9ae72e649517282f190f233dc68a Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 6 Jul 2019 21:13:42 -0700 Subject: [PATCH 012/711] Traverse transform nodes in the correct order when setting a binding source value. --- .../Data/Core/ExpressionObserver.cs | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 65f26df011..9b62686ad5 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -57,6 +57,7 @@ namespace Avalonia.Data.Core private object _root; private IDisposable _rootSubscription; private WeakReference _value; + private IReadOnlyList _transformNodes; /// /// Initializes a new instance of the class. @@ -191,6 +192,24 @@ namespace Avalonia.Data.Core description ?? expression.ToString()); } + private IReadOnlyList GetTransformNodesFromChain() + { + LinkedList transforms = new LinkedList(); + var node = _node; + while (node != null) + { + if (node is ITransformNode transform) + { + transforms.AddFirst(transform); + } + node = node.Next; + } + + return new List(transforms); + } + + private IReadOnlyList TransformNodes => (_transformNodes ?? (_transformNodes = GetTransformNodesFromChain())); + /// /// Attempts to set the value of a property expression. /// @@ -206,18 +225,13 @@ namespace Avalonia.Data.Core { if (Leaf is SettableNode settable) { - var node = _node; - while (node != null) + foreach (var transform in TransformNodes) { - if (node is ITransformNode transform) + value = transform.Transform(value); + if (value is BindingNotification) { - value = transform.Transform(value); - if (value is BindingNotification) - { - return false; - } + return false; } - node = node.Next; } return settable.SetTargetValue(value, priority); } From ac3be6c662d56a3bb467227b6b3f29bfd94d66cf Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 6 Jul 2019 21:18:08 -0700 Subject: [PATCH 013/711] Fix handling of Not nodes. --- .../XamlIlBindingPathHelper.cs | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs index dfa90d1989..598f9175e8 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -67,7 +67,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions private static IXamlIlBindingPathNode TransformBindingPath(XamlIlAstTransformationContext context, IXamlIlLineInfo lineInfo, IXamlIlType startType, IEnumerable bindingExpression) { - bool appendNotNode = false; + List transformNodes = new List(); List nodes = new List(); foreach (var astNode in bindingExpression) { @@ -77,10 +77,19 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions case BindingExpressionGrammar.EmptyExpressionNode _: break; case BindingExpressionGrammar.NotNode _: - appendNotNode = !appendNotNode; + transformNodes.Add(new XamlIlNotPathElementNode(context.Configuration.WellKnownTypes.Boolean)); break; case BindingExpressionGrammar.StreamNode _: - var observableType = targetType.GetAllInterfaces().FirstOrDefault(i => i.GenericTypeDefinition?.Equals(context.Configuration.TypeSystem.FindType("System.IObservable`1")) ?? false); + IXamlIlType observableType; + if (targetType.GenericTypeDefinition?.Equals(context.Configuration.TypeSystem.FindType("System.IObservable`1")) == true) + { + observableType = targetType; + } + else + { + observableType = targetType.GetAllInterfaces().FirstOrDefault(i => i.GenericTypeDefinition?.Equals(context.Configuration.TypeSystem.FindType("System.IObservable`1")) ?? false); + } + if (observableType != null) { nodes.Add(new XamlIlStreamObservablePathElementNode(observableType.GenericArguments[0])); @@ -179,16 +188,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions nodes.Add(new ElementNamePathElementNode(elementName.Name, elementType)); break; } - } - if (appendNotNode) - { - // TODO: Fix Not behavior - nodes.Add(new XamlIlNotPathElementNode(context.Configuration.WellKnownTypes.Boolean)); - } - - return new XamlIlBindingPathNode(lineInfo, context.GetAvaloniaTypes().CompiledBindingPath, nodes); + return new XamlIlBindingPathNode(lineInfo, context.GetAvaloniaTypes().CompiledBindingPath, transformNodes, nodes); IXamlIlType GetType(string ns, string name) { @@ -422,17 +424,23 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions class XamlIlBindingPathNode : XamlIlAstNode, IXamlIlBindingPathNode, IXamlIlAstEmitableNode { + private readonly List _transformElements; private readonly List _elements; public XamlIlBindingPathNode(IXamlIlLineInfo lineInfo, IXamlIlType bindingPathType, + List transformElements, List elements) : base(lineInfo) { Type = new XamlIlAstClrTypeReference(lineInfo, bindingPathType, false); + _transformElements = transformElements; _elements = elements; } - public IXamlIlType BindingResultType => _elements[_elements.Count - 1].Type; + public IXamlIlType BindingResultType + => _transformElements.Count > 0 + ? _transformElements[0].Type + : _elements[_elements.Count - 1].Type; public IXamlIlAstTypeReference Type { get; } @@ -441,6 +449,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions var types = context.GetAvaloniaTypes(); codeGen.Newobj(types.CompiledBindingPathBuilder.FindConstructor()); + foreach (var transform in _transformElements) + { + transform.Emit(context, codeGen); + } + foreach (var element in _elements) { element.Emit(context, codeGen); From c836c7700220e786ece045a868003eefdae49932 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 6 Jul 2019 21:18:45 -0700 Subject: [PATCH 014/711] Make SourceMode internal since it doesn't need to be public. --- .../MarkupExtensions/CompiledBindingExtension.cs | 7 ++----- .../CompiledBindings/CompiledBindingPath.cs | 7 +++---- .../Markup/Parsers/BindingExpressionGrammar.cs | 2 +- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs index bc1e2cdd36..5c3628fd22 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs @@ -1,13 +1,10 @@ -extern alias Markup; -using System; +using System; using Avalonia.Data; using Avalonia.Controls; using Avalonia.Styling; using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings; using Avalonia.Data.Core; - -using SourceMode = Markup::Avalonia.Markup.Parsers.SourceMode; -using Avalonia.Data.Converters; +using Avalonia.Markup.Parsers; namespace Avalonia.Markup.Xaml.MarkupExtensions { diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs index 81b9bd29b9..7daa90ec44 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs @@ -1,10 +1,9 @@ -extern alias Markup; -using System; +using System; using System.Collections.Generic; using Avalonia.Data.Core; using Avalonia.Data.Core.Plugins; +using Avalonia.Markup.Parsers; using Avalonia.Markup.Parsers.Nodes; -using SourceMode = Markup::Avalonia.Markup.Parsers.SourceMode; namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { @@ -56,7 +55,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings return pathRoot ?? new EmptyExpressionNode(); } - public SourceMode SourceMode => _elements.Count > 0 && _elements[0] is IControlSourceBindingPathElement ? SourceMode.Control : SourceMode.Data; + internal SourceMode SourceMode => _elements.Count > 0 && _elements[0] is IControlSourceBindingPathElement ? SourceMode.Control : SourceMode.Data; } public class CompiledBindingPathBuilder diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs index 7181bbaa69..a58068cc5b 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs @@ -8,7 +8,7 @@ using System.Collections.Generic; namespace Avalonia.Markup.Parsers { - public enum SourceMode + internal enum SourceMode { Data, Control From ee46a69cefe644b0d507501d2004f51c76ec895b Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 6 Jul 2019 21:19:49 -0700 Subject: [PATCH 015/711] Add additional test for IObservable compiled bindings. --- .../CompiledBindingExtensionTests.cs | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 3d1a5cb566..34bb071aef 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Reactive.Subjects; using System.Text; using System.Threading.Tasks; using Avalonia.Controls; @@ -41,7 +42,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } [Fact] - public void ResolvesStreamBindingCorrectly() + public void ResolvesStreamTaskBindingCorrectly() { using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -68,6 +69,38 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions Assert.Equal(dataContext.TaskProperty.Result, textBlock.Text); } } + + [Fact] + public void ResolvesStreamObservableBindingCorrectly() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + DelayedBinding.ApplyBindings(textBlock); + + var subject = new Subject(); + var dataContext = new TestDataContext + { + ObservableProperty = subject + }; + + window.DataContext = dataContext; + + subject.OnNext("foobar"); + + Assert.Equal("foobar", textBlock.Text); + } + } } public class TestDataContext @@ -75,5 +108,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions public string StringProperty { get; set; } public Task TaskProperty { get; set; } + + public IObservable ObservableProperty { get; set; } } } From b173d5b5fb3e4d7346c8ec7248ef9d75bec75fbf Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 6 Jul 2019 21:20:12 -0700 Subject: [PATCH 016/711] Change the observable stream binding in BindingDemo to be a compiled binding. --- samples/BindingDemo/MainWindow.xaml | 5 +++-- samples/BindingDemo/ViewModels/MainWindowViewModel.cs | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/samples/BindingDemo/MainWindow.xaml b/samples/BindingDemo/MainWindow.xaml index a232a06383..6c4a071214 100644 --- a/samples/BindingDemo/MainWindow.xaml +++ b/samples/BindingDemo/MainWindow.xaml @@ -5,7 +5,8 @@ xmlns:local="clr-namespace:BindingDemo" Title="AvaloniaUI Bindings Test" Width="800" - Height="600"> + Height="600" + x:DataContextType="{x:Type vm:MainWindowViewModel}"> "; + + using (StartWithResources(("test:style.xaml", styleXaml))) + { + var xaml = @" + + + + +"; + + var loader = new AvaloniaXamlLoader(); + var app = Application.Current; + + loader.Load(xaml, null, app); + } + } + + private IDisposable StartWithResources(params (string, string)[] assets) + { + var assetLoader = new MockAssetLoader(assets); + var services = new TestServices(assetLoader: assetLoader); + return UnitTestApplication.Start(services); + } + } +} From 8f396d0570165066194b5f13fb3e42e705fa6c4d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 27 May 2020 18:49:33 -0300 Subject: [PATCH 076/711] add fix for StackOverflow in resource loading. --- .../MarkupExtensions/ResourceInclude.cs | 15 ++++++++++++--- .../Avalonia.Markup.Xaml/Styling/StyleInclude.cs | 7 +++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs index 2e8fd49702..ce9e3fd4b1 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs @@ -12,7 +12,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public class ResourceInclude : IResourceProvider { private Uri? _baseUri; - private IResourceDictionary? _loaded; + private IResourceDictionary? _loaded; + private bool _isLoading; /// /// Gets the loaded resource dictionary. @@ -22,9 +23,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions get { if (_loaded is null) - { + { + _isLoading = true; var loader = new AvaloniaXamlLoader(); _loaded = (IResourceDictionary)loader.Load(Source, _baseUri); + _isLoading = false; } return _loaded; @@ -48,7 +51,13 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions bool IResourceNode.TryGetResource(object key, out object? value) { - return Loaded.TryGetResource(key, out value); + if(!_isLoading) + { + return Loaded.TryGetResource(key, out value); + } + + value = null; + return false; } void IResourceProvider.AddOwner(IResourceHost owner) => Loaded.AddOwner(owner); diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index 00320ce4e9..9d5b56ebb4 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -13,7 +13,8 @@ namespace Avalonia.Markup.Xaml.Styling public class StyleInclude : IStyle, IResourceProvider { private readonly Uri _baseUri; - private IStyle[]? _loaded; + private IStyle[]? _loaded; + private bool _isLoading; /// /// Initializes a new instance of the class. @@ -49,9 +50,11 @@ namespace Avalonia.Markup.Xaml.Styling { if (_loaded is null) { + _isLoading = true; var loader = new AvaloniaXamlLoader(); var loaded = (IStyle)loader.Load(Source, _baseUri); _loaded = new[] { loaded }; + _isLoading = false; } return _loaded?[0]!; @@ -84,7 +87,7 @@ namespace Avalonia.Markup.Xaml.Styling public bool TryGetResource(object key, out object? value) { - if (Loaded is IResourceProvider p) + if (!_isLoading && Loaded is IResourceProvider p) { return p.TryGetResource(key, out value); } From ed375b578008586617ec5380e9fc5dd03d6f4b79 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 27 May 2020 18:53:49 -0300 Subject: [PATCH 077/711] Allow tests to pass as we expect a KeyNotFound exceptions. --- .../MarkupExtensions/ResourceIncludeTests.cs | 10 +++++++++- .../StyleIncludeTests.cs | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs index e827627938..1514d5bd95 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Media; using Avalonia.UnitTests; @@ -70,7 +71,14 @@ namespace Avalonia.Markup.Xaml.UnitTests.MakrupExtensions var loader = new AvaloniaXamlLoader(); var app = Application.Current; - loader.Load(xaml, null, app); + try + { + loader.Load(xaml, null, app); + } + catch (KeyNotFoundException) + { + + } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/StyleIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/StyleIncludeTests.cs index f4e96942a3..dca936d229 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/StyleIncludeTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/StyleIncludeTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Avalonia.UnitTests; using Xunit; @@ -30,7 +31,14 @@ namespace Avalonia.Markup.Xaml.UnitTests var loader = new AvaloniaXamlLoader(); var app = Application.Current; - loader.Load(xaml, null, app); + try + { + loader.Load(xaml, null, app); + } + catch (KeyNotFoundException) + { + + } } } From 46151fd35bd36860f4d2d0176ffea576e3caaf7c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 27 May 2020 18:55:25 -0300 Subject: [PATCH 078/711] remove unintended change. --- .../Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs | 2 +- src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs index ce9e3fd4b1..1627053e67 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs @@ -22,7 +22,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions { get { - if (_loaded is null) + if (_loaded == null) { _isLoading = true; var loader = new AvaloniaXamlLoader(); diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index 9d5b56ebb4..ea9042f779 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -48,7 +48,7 @@ namespace Avalonia.Markup.Xaml.Styling { get { - if (_loaded is null) + if (_loaded == null) { _isLoading = true; var loader = new AvaloniaXamlLoader(); From ab6ff87a73b9d707176de8f15d5cd81cf2f5a70c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 28 May 2020 12:16:09 +0200 Subject: [PATCH 079/711] Fix spacing. --- .../Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs index 1627053e67..0cedf4f364 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs @@ -51,7 +51,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions bool IResourceNode.TryGetResource(object key, out object? value) { - if(!_isLoading) + if (!_isLoading) { return Loaded.TryGetResource(key, out value); } From ff7e724f73e8220550930f5b5d1bff52bba45934 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 28 May 2020 12:59:20 +0200 Subject: [PATCH 080/711] Make ItemsRepeater.GetOrCreateElement public As it's public in the WinUI version: https://docs.microsoft.com/en-us/uwp/api/microsoft.ui.xaml.controls.itemsrepeater.getorcreateelement?view=winui-2.4 --- src/Avalonia.Controls/Repeater/ItemsRepeater.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 069da6e9ac..2c870b4efd 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -219,12 +219,21 @@ namespace Avalonia.Controls /// public IControl TryGetElement(int index) => GetElementFromIndexImpl(index); + /// + /// Retrieves the UIElement that corresponds to the item at the specified index in the + /// data source. + /// + /// The index of the item. + /// + /// An that corresponds to the item at the specified index. If the + /// item is not realized, a new UIElement is created. + /// + public IControl GetOrCreateElement(int index) => GetOrCreateElementImpl(index); + internal void PinElement(IControl element) => _viewManager.UpdatePin(element, true); internal void UnpinElement(IControl element) => _viewManager.UpdatePin(element, false); - internal IControl GetOrCreateElement(int index) => GetOrCreateElementImpl(index); - internal static VirtualizationInfo TryGetVirtualizationInfo(IControl element) { var value = element.GetValue(VirtualizationInfoProperty); From f46c62801ef473af174640d99537c9fc02f84681 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 28 May 2020 15:04:46 +0200 Subject: [PATCH 081/711] Convert Color resources to brushes. Make `StaticResource` and `DynamicResource` automatically convert `Color`s to `ImmutableSolidColorBrush`es and add a `ColorToBrushConverter` for use by bindings. To do this I made `GetResourceObservable` accept an optional conversion function. --- .../Controls/ResourceNodeExtensions.cs | 35 +++++--- .../Avalonia.Markup.Xaml.csproj | 1 + .../Converters/ColorToBrushConverter.cs | 88 +++++++++++++++++++ .../DynamicResourceExtension.cs | 18 +++- .../StaticResourceExtension.cs | 23 +++-- .../DynamicResourceExtensionTests.cs | 21 +++++ .../StaticResourceExtensionTests.cs | 21 +++++ 7 files changed, 186 insertions(+), 21 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml/Converters/ColorToBrushConverter.cs diff --git a/src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs index 7771629e0d..250274c39b 100644 --- a/src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs +++ b/src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Data.Converters; using Avalonia.LogicalTree; using Avalonia.Reactive; @@ -58,31 +59,39 @@ namespace Avalonia.Controls return false; } - public static IObservable GetResourceObservable(this IStyledElement control, object key) + public static IObservable GetResourceObservable( + this IStyledElement control, + object key, + Func? converter = null) { control = control ?? throw new ArgumentNullException(nameof(control)); key = key ?? throw new ArgumentNullException(nameof(key)); - return new ResourceObservable(control, key); + return new ResourceObservable(control, key, converter); } - public static IObservable GetResourceObservable(this IResourceProvider resourceProvider, object key) + public static IObservable GetResourceObservable( + this IResourceProvider resourceProvider, + object key, + Func? converter = null) { resourceProvider = resourceProvider ?? throw new ArgumentNullException(nameof(resourceProvider)); key = key ?? throw new ArgumentNullException(nameof(key)); - return new FloatingResourceObservable(resourceProvider, key); + return new FloatingResourceObservable(resourceProvider, key, converter); } private class ResourceObservable : LightweightObservableBase { private readonly IStyledElement _target; private readonly object _key; + private readonly Func? _converter; - public ResourceObservable(IStyledElement target, object key) + public ResourceObservable(IStyledElement target, object key, Func? converter) { _target = target; _key = key; + _converter = converter; } protected override void Initialize() @@ -97,25 +106,29 @@ namespace Avalonia.Controls protected override void Subscribed(IObserver observer, bool first) { - observer.OnNext(_target.FindResource(_key)); + observer.OnNext(Convert(_target.FindResource(_key))); } private void ResourcesChanged(object sender, ResourcesChangedEventArgs e) { - PublishNext(_target.FindResource(_key)); + PublishNext(Convert(_target.FindResource(_key))); } + + private object? Convert(object? value) => _converter?.Invoke(value) ?? value; } private class FloatingResourceObservable : LightweightObservableBase { private readonly IResourceProvider _target; private readonly object _key; + private readonly Func? _converter; private IResourceHost? _owner; - public FloatingResourceObservable(IResourceProvider target, object key) + public FloatingResourceObservable(IResourceProvider target, object key, Func? converter) { _target = target; _key = key; + _converter = converter; } protected override void Initialize() @@ -134,13 +147,13 @@ namespace Avalonia.Controls { if (_target.Owner is object) { - observer.OnNext(_target.Owner?.FindResource(_key)); + observer.OnNext(Convert(_target.Owner?.FindResource(_key))); } } private void PublishNext() { - PublishNext(_target.Owner?.FindResource(_key)); + PublishNext(Convert(_target.Owner?.FindResource(_key))); } private void OwnerChanged(object sender, EventArgs e) @@ -164,6 +177,8 @@ namespace Avalonia.Controls { PublishNext(); } + + private object? Convert(object? value) => _converter?.Invoke(value) ?? value; } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 06c5375520..985a6b9f5a 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/ColorToBrushConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/ColorToBrushConverter.cs new file mode 100644 index 0000000000..5b44e9eb50 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/ColorToBrushConverter.cs @@ -0,0 +1,88 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; +using Avalonia.Media.Immutable; + +namespace Avalonia.Markup.Xaml.Converters +{ + /// + /// Converts a to an . + /// + public class ColorToBrushConverter : IValueConverter + { + /// + /// Converts a to an if the arguments are of the + /// correct type. + /// + /// The value. + /// The target type. + /// Not used. + /// Not used. + /// + /// If is a and + /// is then converts the color to a solid color brush. + /// + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return Convert(value, targetType); + } + + /// + /// Converts an to a if the arguments are of the + /// correct type. + /// + /// The value. + /// The target type. + /// Not used. + /// Not used. + /// + /// If is an and + /// is then converts the solid color brush to a color. + /// + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return ConvertBack(value, targetType); + } + + /// + /// Converts a to an if the arguments are of the + /// correct type. + /// + /// The value. + /// The target type. + /// + /// If is a and + /// is then converts the color to a solid color brush. + /// + public static object Convert(object value, Type targetType) + { + if (targetType == typeof(IBrush) && value is Color c) + { + return new ImmutableSolidColorBrush(c); + } + + return value; + } + + /// + /// Converts an to a if the arguments are of the + /// correct type. + /// + /// The value. + /// The target type. + /// + /// If is an and + /// is then converts the solid color brush to a color. + /// + public static object ConvertBack(object value, Type targetType) + { + if (targetType == typeof(Color) && value is ISolidColorBrush brush) + { + return brush.Color; + } + + return value; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs index d0d4aae9b8..03fd2e60dd 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs @@ -1,6 +1,8 @@ using System; using Avalonia.Controls; using Avalonia.Data; +using Avalonia.Markup.Xaml.Converters; +using Avalonia.Media; #nullable enable @@ -54,11 +56,23 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions if (control != null) { - return InstancedBinding.OneWay(control.GetResourceObservable(ResourceKey)); + var source = control.GetResourceObservable(ResourceKey, GetConverter(targetProperty)); + return InstancedBinding.OneWay(source); } else if (_resourceProvider is object) { - return InstancedBinding.OneWay(_resourceProvider.GetResourceObservable(ResourceKey)); + var source = _resourceProvider.GetResourceObservable(ResourceKey, GetConverter(targetProperty)); + return InstancedBinding.OneWay(source); + } + + return null; + } + + private Func? GetConverter(AvaloniaProperty targetProperty) + { + if (targetProperty?.PropertyType == typeof(IBrush)) + { + return x => ColorToBrushConverter.Convert(x, typeof(IBrush)); } return null; diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index 6d9fac638c..4ad68fc63e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Reflection; using Avalonia.Controls; using Avalonia.Markup.Data; +using Avalonia.Markup.Xaml.Converters; using Avalonia.Markup.Xaml.XamlIl.Runtime; namespace Avalonia.Markup.Xaml.MarkupExtensions @@ -24,6 +24,14 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public object ProvideValue(IServiceProvider serviceProvider) { var stack = serviceProvider.GetService(); + var provideTarget = serviceProvider.GetService(); + + var targetType = provideTarget.TargetProperty switch + { + AvaloniaProperty ap => ap.PropertyType, + PropertyInfo pi => pi.PropertyType, + _ => null, + }; // Look upwards though the ambient context for IResourceHosts and IResourceProviders // which might be able to give us the resource. @@ -33,30 +41,27 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions if (e is IResourceHost host && host.TryGetResource(ResourceKey, out value)) { - return value; + return ColorToBrushConverter.Convert(value, targetType); } else if (e is IResourceProvider provider && provider.TryGetResource(ResourceKey, out value)) { - return value; + return ColorToBrushConverter.Convert(value, targetType); } } - // The resource still hasn't been found, so add a delayed one-time binding. - var provideTarget = serviceProvider.GetService(); - if (provideTarget.TargetObject is IControl target && provideTarget.TargetProperty is PropertyInfo property) { - DelayedBinding.Add(target, property, GetValue); + DelayedBinding.Add(target, property, x => GetValue(x, targetType)); return AvaloniaProperty.UnsetValue; } throw new KeyNotFoundException($"Static resource '{ResourceKey}' not found."); } - private object GetValue(IStyledElement control) + private object GetValue(IStyledElement control, Type targetType) { - return control.FindResource(ResourceKey); + return ColorToBrushConverter.Convert(control.FindResource(ResourceKey), targetType); } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs index ed3e4c986b..e651c5f29c 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs @@ -797,6 +797,27 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void Automatically_Converts_Color_To_SolidColorBrush() + { + var xaml = @" + + + #ff506070 + + + +"; + + var loader = new AvaloniaXamlLoader(); + var userControl = (UserControl)loader.Load(xaml); + var border = userControl.FindControl("border"); + + var brush = (ISolidColorBrush)border.Background; + Assert.Equal(0xff506070, brush.Color.ToUint32()); + } + private IDisposable StyledWindow(params (string, string)[] assets) { var services = TestServices.StyledWindow.With( diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs index ddfa96c094..efcda47f0f 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs @@ -511,6 +511,27 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions Assert.Equal(0xff506070, brush.Color.ToUint32()); } + [Fact] + public void Automatically_Converts_Color_To_SolidColorBrush() + { + var xaml = @" + + + #ff506070 + + + +"; + + var loader = new AvaloniaXamlLoader(); + var userControl = (UserControl)loader.Load(xaml); + var border = userControl.FindControl("border"); + + var brush = (ISolidColorBrush)border.Background; + Assert.Equal(0xff506070, brush.Color.ToUint32()); + } + private IDisposable StyledWindow(params (string, string)[] assets) { var services = TestServices.StyledWindow.With( From f8a95fa2a124b977d957071e0fb0ad84952cfd8d Mon Sep 17 00:00:00 2001 From: aguahombre Date: Thu, 28 May 2020 15:29:56 +0100 Subject: [PATCH 082/711] Fix tooltip memory leak --- src/Avalonia.Controls/ToolTip.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index 3e57ba529f..2f11746891 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -246,7 +246,7 @@ namespace Avalonia.Controls if (_popup != null) { _popup.SetChild(null); - _popup.Hide(); + _popup.Dispose(); _popup = null; } } From 59172a3f7906c2e970a3fb3662d6cfadb7e8059f Mon Sep 17 00:00:00 2001 From: danwalmsley Date: Thu, 28 May 2020 14:26:52 -0300 Subject: [PATCH 083/711] Add a Fluent theme (identical to Default for now) to allow new theme work to progress without affecting default theme and templates (#4004) * Define the base colours and brushes. * import base / system colours. * test textbox using alias for resource. * add a fluent theme * revert default theme * revert original theme. * restore default theme. * remove old accents from fluent theme. * rename namespaces, etc for new theme. * make control catalog run again. * undo textbox work. * Revert "undo textbox work." This reverts commit 477665c306cad1c82fc655e7f22cf90e1855a015. * add missing fontweight. * add the UWP base resources. * Organise the Fluent themes to separate UWP base resources from Fluent resources. * Add fluentBase dark and light overrides. * Add xaml files to contain control resources until we get equivalent of ThemeDictionaries. * undo textbox work. --- Avalonia.sln | 26 ++ build/CoreLibraries.props | 1 + samples/ControlCatalog/App.xaml | 6 +- src/Avalonia.Themes.Fluent/Accents/Base.xaml | 18 + .../Accents/BaseDark.xaml | 350 ++++++++++++++++++ .../Accents/BaseLight.xaml | 349 +++++++++++++++++ .../Accents/FluentBaseDark.xaml | 74 ++++ .../Accents/FluentBaseLight.xaml | 75 ++++ .../Accents/FluentControlResourcesDark.xaml | 7 + .../Accents/FluentControlResourcesLight.xaml | 7 + .../Accents/FluentDark.xaml | 8 + .../Accents/FluentLight.xaml | 8 + .../AutoCompleteBox.xaml | 38 ++ .../Avalonia.Themes.Fluent.csproj | 22 ++ src/Avalonia.Themes.Fluent/Button.xaml | 34 ++ src/Avalonia.Themes.Fluent/ButtonSpinner.xaml | 104 ++++++ src/Avalonia.Themes.Fluent/Calendar.xaml | 31 ++ .../CalendarButton.xaml | 81 ++++ .../CalendarDayButton.xaml | 116 ++++++ src/Avalonia.Themes.Fluent/CalendarItem.xaml | 183 +++++++++ src/Avalonia.Themes.Fluent/Carousel.xaml | 16 + src/Avalonia.Themes.Fluent/CheckBox.xaml | 69 ++++ src/Avalonia.Themes.Fluent/ComboBox.xaml | 64 ++++ src/Avalonia.Themes.Fluent/ComboBoxItem.xaml | 41 ++ .../ContentControl.xaml | 15 + src/Avalonia.Themes.Fluent/ContextMenu.xaml | 23 ++ .../DataValidationErrors.xaml | 38 ++ src/Avalonia.Themes.Fluent/DatePicker.xaml | 126 +++++++ .../EmbeddableControlRoot.xaml | 19 + src/Avalonia.Themes.Fluent/Expander.xaml | 138 +++++++ src/Avalonia.Themes.Fluent/FluentTheme.xaml | 55 +++ .../FluentTheme.xaml.cs | 12 + src/Avalonia.Themes.Fluent/FocusAdorner.xaml | 10 + src/Avalonia.Themes.Fluent/GridSplitter.xaml | 23 ++ .../InverseBooleanValueConverter.cs | 20 + src/Avalonia.Themes.Fluent/ItemsControl.xaml | 15 + src/Avalonia.Themes.Fluent/ListBox.xaml | 31 ++ src/Avalonia.Themes.Fluent/ListBoxItem.xaml | 41 ++ src/Avalonia.Themes.Fluent/Menu.xaml | 16 + src/Avalonia.Themes.Fluent/MenuItem.xaml | 147 ++++++++ src/Avalonia.Themes.Fluent/NativeMenuBar.xaml | 26 ++ .../NotificationCard.xaml | 92 +++++ src/Avalonia.Themes.Fluent/NumericUpDown.xaml | 38 ++ .../OverlayPopupHost.xaml | 17 + src/Avalonia.Themes.Fluent/PopupRoot.xaml | 17 + src/Avalonia.Themes.Fluent/ProgressBar.xaml | 81 ++++ src/Avalonia.Themes.Fluent/RadioButton.xaml | 60 +++ src/Avalonia.Themes.Fluent/RepeatButton.xaml | 40 ++ src/Avalonia.Themes.Fluent/ScrollBar.xaml | 142 +++++++ src/Avalonia.Themes.Fluent/ScrollViewer.xaml | 47 +++ src/Avalonia.Themes.Fluent/Separator.xaml | 20 + src/Avalonia.Themes.Fluent/Slider.xaml | 93 +++++ src/Avalonia.Themes.Fluent/TabControl.xaml | 62 ++++ src/Avalonia.Themes.Fluent/TabItem.xaml | 45 +++ src/Avalonia.Themes.Fluent/TabStrip.xaml | 20 + src/Avalonia.Themes.Fluent/TabStripItem.xaml | 23 ++ src/Avalonia.Themes.Fluent/TextBox.xaml | 78 ++++ src/Avalonia.Themes.Fluent/ToggleButton.xaml | 38 ++ src/Avalonia.Themes.Fluent/ToolTip.xaml | 17 + src/Avalonia.Themes.Fluent/TreeView.xaml | 23 ++ src/Avalonia.Themes.Fluent/TreeViewItem.xaml | 92 +++++ src/Avalonia.Themes.Fluent/UserControl.xaml | 15 + src/Avalonia.Themes.Fluent/Window.xaml | 22 ++ .../WindowNotificationManager.xaml | 45 +++ src/Avalonia.Visuals/Media/FontWeight.cs | 5 + 65 files changed, 3612 insertions(+), 3 deletions(-) create mode 100644 src/Avalonia.Themes.Fluent/Accents/Base.xaml create mode 100644 src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml create mode 100644 src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml create mode 100644 src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml create mode 100644 src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml create mode 100644 src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml create mode 100644 src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml create mode 100644 src/Avalonia.Themes.Fluent/Accents/FluentDark.xaml create mode 100644 src/Avalonia.Themes.Fluent/Accents/FluentLight.xaml create mode 100644 src/Avalonia.Themes.Fluent/AutoCompleteBox.xaml create mode 100644 src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj create mode 100644 src/Avalonia.Themes.Fluent/Button.xaml create mode 100644 src/Avalonia.Themes.Fluent/ButtonSpinner.xaml create mode 100644 src/Avalonia.Themes.Fluent/Calendar.xaml create mode 100644 src/Avalonia.Themes.Fluent/CalendarButton.xaml create mode 100644 src/Avalonia.Themes.Fluent/CalendarDayButton.xaml create mode 100644 src/Avalonia.Themes.Fluent/CalendarItem.xaml create mode 100644 src/Avalonia.Themes.Fluent/Carousel.xaml create mode 100644 src/Avalonia.Themes.Fluent/CheckBox.xaml create mode 100644 src/Avalonia.Themes.Fluent/ComboBox.xaml create mode 100644 src/Avalonia.Themes.Fluent/ComboBoxItem.xaml create mode 100644 src/Avalonia.Themes.Fluent/ContentControl.xaml create mode 100644 src/Avalonia.Themes.Fluent/ContextMenu.xaml create mode 100644 src/Avalonia.Themes.Fluent/DataValidationErrors.xaml create mode 100644 src/Avalonia.Themes.Fluent/DatePicker.xaml create mode 100644 src/Avalonia.Themes.Fluent/EmbeddableControlRoot.xaml create mode 100644 src/Avalonia.Themes.Fluent/Expander.xaml create mode 100644 src/Avalonia.Themes.Fluent/FluentTheme.xaml create mode 100644 src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs create mode 100644 src/Avalonia.Themes.Fluent/FocusAdorner.xaml create mode 100644 src/Avalonia.Themes.Fluent/GridSplitter.xaml create mode 100644 src/Avalonia.Themes.Fluent/InverseBooleanValueConverter.cs create mode 100644 src/Avalonia.Themes.Fluent/ItemsControl.xaml create mode 100644 src/Avalonia.Themes.Fluent/ListBox.xaml create mode 100644 src/Avalonia.Themes.Fluent/ListBoxItem.xaml create mode 100644 src/Avalonia.Themes.Fluent/Menu.xaml create mode 100644 src/Avalonia.Themes.Fluent/MenuItem.xaml create mode 100644 src/Avalonia.Themes.Fluent/NativeMenuBar.xaml create mode 100644 src/Avalonia.Themes.Fluent/NotificationCard.xaml create mode 100644 src/Avalonia.Themes.Fluent/NumericUpDown.xaml create mode 100644 src/Avalonia.Themes.Fluent/OverlayPopupHost.xaml create mode 100644 src/Avalonia.Themes.Fluent/PopupRoot.xaml create mode 100644 src/Avalonia.Themes.Fluent/ProgressBar.xaml create mode 100644 src/Avalonia.Themes.Fluent/RadioButton.xaml create mode 100644 src/Avalonia.Themes.Fluent/RepeatButton.xaml create mode 100644 src/Avalonia.Themes.Fluent/ScrollBar.xaml create mode 100644 src/Avalonia.Themes.Fluent/ScrollViewer.xaml create mode 100644 src/Avalonia.Themes.Fluent/Separator.xaml create mode 100644 src/Avalonia.Themes.Fluent/Slider.xaml create mode 100644 src/Avalonia.Themes.Fluent/TabControl.xaml create mode 100644 src/Avalonia.Themes.Fluent/TabItem.xaml create mode 100644 src/Avalonia.Themes.Fluent/TabStrip.xaml create mode 100644 src/Avalonia.Themes.Fluent/TabStripItem.xaml create mode 100644 src/Avalonia.Themes.Fluent/TextBox.xaml create mode 100644 src/Avalonia.Themes.Fluent/ToggleButton.xaml create mode 100644 src/Avalonia.Themes.Fluent/ToolTip.xaml create mode 100644 src/Avalonia.Themes.Fluent/TreeView.xaml create mode 100644 src/Avalonia.Themes.Fluent/TreeViewItem.xaml create mode 100644 src/Avalonia.Themes.Fluent/UserControl.xaml create mode 100644 src/Avalonia.Themes.Fluent/Window.xaml create mode 100644 src/Avalonia.Themes.Fluent/WindowNotificationManager.xaml diff --git a/Avalonia.sln b/Avalonia.sln index 38f8e5f720..e8d5034fb0 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -206,6 +206,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.FreeDesktop", "src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid.UnitTests", "tests\Avalonia.Controls.DataGrid.UnitTests\Avalonia.Controls.DataGrid.UnitTests.csproj", "{351337F5-D66F-461B-A957-4EF60BDB4BA6}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Fluent", "src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj", "{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 @@ -1921,6 +1923,30 @@ Global {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhone.Build.0 = Release|Any CPU {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.AppStore|iPhone.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|iPhone.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|Any CPU.Build.0 = Release|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|iPhone.ActiveCfg = Release|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|iPhone.Build.0 = Release|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/build/CoreLibraries.props b/build/CoreLibraries.props index d61268173d..2b54ee3f56 100644 --- a/build/CoreLibraries.props +++ b/build/CoreLibraries.props @@ -11,6 +11,7 @@ + diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index e40509dfda..c9441782a2 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -1,9 +1,9 @@ - - - + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml new file mode 100644 index 0000000000..2b13fb0d3b --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml @@ -0,0 +1,350 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml new file mode 100644 index 0000000000..3db9915334 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml @@ -0,0 +1,349 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml new file mode 100644 index 0000000000..9623204e2c --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml @@ -0,0 +1,74 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml new file mode 100644 index 0000000000..2d8e457457 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml @@ -0,0 +1,75 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml new file mode 100644 index 0000000000..70599aa51e --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -0,0 +1,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml new file mode 100644 index 0000000000..70599aa51e --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -0,0 +1,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentDark.xaml new file mode 100644 index 0000000000..80ad15ee15 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/FluentDark.xaml @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentLight.xaml new file mode 100644 index 0000000000..38601a2f2d --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/FluentLight.xaml @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/Avalonia.Themes.Fluent/AutoCompleteBox.xaml b/src/Avalonia.Themes.Fluent/AutoCompleteBox.xaml new file mode 100644 index 0000000000..788b60892b --- /dev/null +++ b/src/Avalonia.Themes.Fluent/AutoCompleteBox.xaml @@ -0,0 +1,38 @@ + + + diff --git a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj new file mode 100644 index 0000000000..3c3e14010d --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj @@ -0,0 +1,22 @@ + + + netstandard2.0 + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Button.xaml b/src/Avalonia.Themes.Fluent/Button.xaml new file mode 100644 index 0000000000..698ddec2a8 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Button.xaml @@ -0,0 +1,34 @@ + + + + + + \ No newline at end of file diff --git a/src/Avalonia.Themes.Fluent/ButtonSpinner.xaml b/src/Avalonia.Themes.Fluent/ButtonSpinner.xaml new file mode 100644 index 0000000000..89fbb9d64d --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ButtonSpinner.xaml @@ -0,0 +1,104 @@ + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Calendar.xaml b/src/Avalonia.Themes.Fluent/Calendar.xaml new file mode 100644 index 0000000000..6bbee4ef17 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Calendar.xaml @@ -0,0 +1,31 @@ + + + + + \ No newline at end of file diff --git a/src/Avalonia.Themes.Fluent/CalendarButton.xaml b/src/Avalonia.Themes.Fluent/CalendarButton.xaml new file mode 100644 index 0000000000..3314534b3b --- /dev/null +++ b/src/Avalonia.Themes.Fluent/CalendarButton.xaml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/CalendarDayButton.xaml b/src/Avalonia.Themes.Fluent/CalendarDayButton.xaml new file mode 100644 index 0000000000..2d79e62a75 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/CalendarDayButton.xaml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/CalendarItem.xaml b/src/Avalonia.Themes.Fluent/CalendarItem.xaml new file mode 100644 index 0000000000..dfd89ed82f --- /dev/null +++ b/src/Avalonia.Themes.Fluent/CalendarItem.xaml @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Carousel.xaml b/src/Avalonia.Themes.Fluent/Carousel.xaml new file mode 100644 index 0000000000..955a49a974 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Carousel.xaml @@ -0,0 +1,16 @@ + diff --git a/src/Avalonia.Themes.Fluent/CheckBox.xaml b/src/Avalonia.Themes.Fluent/CheckBox.xaml new file mode 100644 index 0000000000..f87ae2674f --- /dev/null +++ b/src/Avalonia.Themes.Fluent/CheckBox.xaml @@ -0,0 +1,69 @@ + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/ComboBox.xaml b/src/Avalonia.Themes.Fluent/ComboBox.xaml new file mode 100644 index 0000000000..95bd9550a5 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ComboBox.xaml @@ -0,0 +1,64 @@ + + + + + diff --git a/src/Avalonia.Themes.Fluent/ComboBoxItem.xaml b/src/Avalonia.Themes.Fluent/ComboBoxItem.xaml new file mode 100644 index 0000000000..23456ec744 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ComboBoxItem.xaml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/ContentControl.xaml b/src/Avalonia.Themes.Fluent/ContentControl.xaml new file mode 100644 index 0000000000..d76eecee84 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ContentControl.xaml @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/src/Avalonia.Themes.Fluent/ContextMenu.xaml b/src/Avalonia.Themes.Fluent/ContextMenu.xaml new file mode 100644 index 0000000000..75f8f7c23d --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ContextMenu.xaml @@ -0,0 +1,23 @@ + diff --git a/src/Avalonia.Themes.Fluent/DataValidationErrors.xaml b/src/Avalonia.Themes.Fluent/DataValidationErrors.xaml new file mode 100644 index 0000000000..f4145a51f5 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/DataValidationErrors.xaml @@ -0,0 +1,38 @@ + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/DatePicker.xaml b/src/Avalonia.Themes.Fluent/DatePicker.xaml new file mode 100644 index 0000000000..7adb1c2d5f --- /dev/null +++ b/src/Avalonia.Themes.Fluent/DatePicker.xaml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + Light = 300, + /// + /// Specifies a "semi light" font weight. + /// + SemiLight = 350, + /// /// Specifies a "normal" font weight. /// From f68d81941401cef9c1a7bd1184093a18e4a024c9 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 27 May 2020 18:10:22 -0300 Subject: [PATCH 084/711] Add TextBox control resources. --- .../Accents/FluentControlResourcesDark.xaml | 25 ++++++++++++++++++- .../Accents/FluentControlResourcesLight.xaml | 25 ++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index 70599aa51e..38e4778e24 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -2,6 +2,29 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=netstandard"> - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml index 70599aa51e..276828c587 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -2,6 +2,29 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=netstandard"> - + + + + + + + + + + + + + + + + + + + + + + + + From a596e0bfb8867c49b37e6bf3b5be3993db9f8e98 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 27 May 2020 19:40:16 -0300 Subject: [PATCH 085/711] Almost Working fluent textbox. --- samples/ControlCatalog/App.xaml | 3 +- samples/ControlCatalog/MainView.xaml.cs | 44 +++++------ samples/ControlCatalog/MainWindow.xaml | 2 +- src/Avalonia.Themes.Fluent/Accents/Base.xaml | 6 ++ .../Accents/FluentBaseDark.xaml | 32 ++++++++ src/Avalonia.Themes.Fluent/TextBox.xaml | 73 ++++++++++++++++--- 6 files changed, 124 insertions(+), 36 deletions(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index c9441782a2..d59c15bb6a 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -1,7 +1,8 @@ - + + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 8c4818fb07..fceca426f4 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -32,30 +32,30 @@ namespace ControlCatalog } - var light = new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) - { - Source = new Uri("resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default") - }; - var dark = new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) - { - Source = new Uri("resm:Avalonia.Themes.Default.Accents.BaseDark.xaml?assembly=Avalonia.Themes.Default") - }; + //var light = new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) + //{ + // Source = new Uri("resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default") + //}; + //var dark = new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) + //{ + // Source = new Uri("resm:Avalonia.Themes.Default.Accents.BaseDark.xaml?assembly=Avalonia.Themes.Default") + //}; - var themes = this.Find("Themes"); - themes.SelectionChanged += (sender, e) => - { - switch (themes.SelectedIndex) - { - case 0: - Styles[0] = light; - break; - case 1: - Styles[0] = dark; - break; - } - }; - Styles.Add(light); + //var themes = this.Find("Themes"); + //themes.SelectionChanged += (sender, e) => + //{ + // switch (themes.SelectedIndex) + // { + // case 0: + // Styles[0] = light; + // break; + // case 1: + // Styles[0] = dark; + // break; + // } + //}; + //Styles.Add(light); var decorations = this.Find("Decorations"); decorations.SelectionChanged += (sender, e) => diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index a0bb956425..670e30725b 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -7,7 +7,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:ControlCatalog.ViewModels" xmlns:v="clr-namespace:ControlCatalog.Views" - x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}" Background="Transparent"> + x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}"> diff --git a/src/Avalonia.Themes.Fluent/Accents/Base.xaml b/src/Avalonia.Themes.Fluent/Accents/Base.xaml index b94c0ecf5f..6dc59bed21 100644 --- a/src/Avalonia.Themes.Fluent/Accents/Base.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/Base.xaml @@ -14,5 +14,11 @@ #FF000000 Segoe UI 14 + + + True + 1 + 2 + 10,6,6,5 diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml index 9623204e2c..d2a8cac601 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml @@ -70,5 +70,37 @@ #FF00459D #FF000000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/TextBox.xaml b/src/Avalonia.Themes.Fluent/TextBox.xaml index 4fb3653e89..4e4cdf5231 100644 --- a/src/Avalonia.Themes.Fluent/TextBox.xaml +++ b/src/Avalonia.Themes.Fluent/TextBox.xaml @@ -1,17 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + - + From 55995c258b8cf1ff91242ba4a29cf1240e41e004 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 27 May 2020 19:48:51 -0300 Subject: [PATCH 086/711] fix fluent textbox background. --- src/Avalonia.Themes.Fluent/TextBox.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/TextBox.xaml b/src/Avalonia.Themes.Fluent/TextBox.xaml index 4e4cdf5231..fc88212fde 100644 --- a/src/Avalonia.Themes.Fluent/TextBox.xaml +++ b/src/Avalonia.Themes.Fluent/TextBox.xaml @@ -2,6 +2,7 @@ From df06ad1b11e4d72bad3d8f34b927ad209fb50540 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 27 May 2020 20:45:41 -0300 Subject: [PATCH 088/711] add missing light resources for textbox. --- .../Accents/FluentBaseLight.xaml | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml index 2d8e457457..a77fbd80fe 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml @@ -71,5 +71,37 @@ #FFFFFFFF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From fb0cced162fcc7a4e26971caa1df04642752afa4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 27 May 2020 22:27:08 -0300 Subject: [PATCH 089/711] Fix Textbox layout. --- src/Avalonia.Themes.Fluent/Accents/FluentDark.xaml | 4 ++-- src/Avalonia.Themes.Fluent/Accents/FluentLight.xaml | 2 +- src/Avalonia.Themes.Fluent/TextBox.xaml | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentDark.xaml index 80ad15ee15..5b6da70233 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentDark.xaml @@ -1,8 +1,8 @@ - + xmlns:sys="clr-namespace:System;assembly=netstandard"> + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentLight.xaml index 38601a2f2d..5825a2bf34 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentLight.xaml @@ -1,8 +1,8 @@ - + diff --git a/src/Avalonia.Themes.Fluent/TextBox.xaml b/src/Avalonia.Themes.Fluent/TextBox.xaml index 1b766376b5..e89cf2b49c 100644 --- a/src/Avalonia.Themes.Fluent/TextBox.xaml +++ b/src/Avalonia.Themes.Fluent/TextBox.xaml @@ -46,9 +46,7 @@ + Margin="{TemplateBinding BorderThickness}"> - + Date: Thu, 28 May 2020 14:38:10 -0300 Subject: [PATCH 090/711] keep control catalog on old theme for now. --- samples/ControlCatalog/App.xaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index d59c15bb6a..20ca291910 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -2,10 +2,9 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.App"> - - - - + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml index a77fbd80fe..5ef54abeaf 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml @@ -103,5 +103,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From a8d598d9c87b721ee9d3ddad53b4f1f7d686164f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 27 May 2020 23:19:20 -0300 Subject: [PATCH 093/711] add fluent resources for checkbox. --- .../Accents/FluentControlResourcesDark.xaml | 90 +++++++++++++++++++ .../Accents/FluentControlResourcesLight.xaml | 90 +++++++++++++++++++ 2 files changed, 180 insertions(+) diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index 38e4778e24..edfc487647 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -26,5 +26,95 @@ + + + 1 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml index 276828c587..70bee7074e 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -26,5 +26,95 @@ + + + 1 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 91caa451320c63eaab9fd800153e3c8dfbcbc015 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 27 May 2020 23:53:35 -0300 Subject: [PATCH 094/711] add initial port of checkbox template. --- src/Avalonia.Themes.Fluent/CheckBox.xaml | 89 ++++++++++++------------ 1 file changed, 46 insertions(+), 43 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/CheckBox.xaml b/src/Avalonia.Themes.Fluent/CheckBox.xaml index f87ae2674f..1e6300c3fb 100644 --- a/src/Avalonia.Themes.Fluent/CheckBox.xaml +++ b/src/Avalonia.Themes.Fluent/CheckBox.xaml @@ -1,50 +1,53 @@ From 50fe90f8b5f0ef55184714a1a320bb24ed42364e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 28 May 2020 01:15:12 -0300 Subject: [PATCH 095/711] almost fully working checkbox. --- .../Accents/BaseDark.xaml | 4 +- .../Accents/BaseLight.xaml | 4 +- .../Accents/FluentControlResourcesDark.xaml | 4 +- src/Avalonia.Themes.Fluent/CheckBox.xaml | 99 ++++++++++++++++++- 4 files changed, 101 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml index 2b13fb0d3b..7ee211f249 100644 --- a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml @@ -161,8 +161,8 @@ 3 374 0 - 2 - 0 + 2 + 0 21 64 80 diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml index 3db9915334..309759705c 100644 --- a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml @@ -160,8 +160,8 @@ 3 374 0 - 2 - 0 + 2 + 0 21 64 80 diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index edfc487647..56c6de1144 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -28,8 +28,8 @@ - 1 - 0 + 1 + 0 diff --git a/src/Avalonia.Themes.Fluent/CheckBox.xaml b/src/Avalonia.Themes.Fluent/CheckBox.xaml index 1e6300c3fb..beadc28d13 100644 --- a/src/Avalonia.Themes.Fluent/CheckBox.xaml +++ b/src/Avalonia.Themes.Fluent/CheckBox.xaml @@ -1,4 +1,9 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 7411c3ae7b48f0bde6cd00daa47f73b4c63982b1 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 28 May 2020 01:46:57 -0300 Subject: [PATCH 096/711] further progress on checkbox. --- src/Avalonia.Themes.Fluent/CheckBox.xaml | 43 +++++++++++++++++++----- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/CheckBox.xaml b/src/Avalonia.Themes.Fluent/CheckBox.xaml index beadc28d13..7f93c2842d 100644 --- a/src/Avalonia.Themes.Fluent/CheckBox.xaml +++ b/src/Avalonia.Themes.Fluent/CheckBox.xaml @@ -35,15 +35,8 @@ CornerRadius="{DynamicResource ControlCornerRadius}" /> - + - + + + + + + + @@ -109,6 +119,23 @@ + + + + + + + From b3040728fe0aa93809babad3dc7cad4d963d9bce Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 28 May 2020 12:22:43 -0300 Subject: [PATCH 097/711] add missing state styles. --- src/Avalonia.Themes.Fluent/CheckBox.xaml | 192 +++++++++++++++++------ 1 file changed, 146 insertions(+), 46 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/CheckBox.xaml b/src/Avalonia.Themes.Fluent/CheckBox.xaml index 7f93c2842d..fa6b044cf6 100644 --- a/src/Avalonia.Themes.Fluent/CheckBox.xaml +++ b/src/Avalonia.Themes.Fluent/CheckBox.xaml @@ -21,34 +21,34 @@ --> - - - - - - - - - - - - + + + + + + + + + + - + + + @@ -84,8 +84,8 @@ + + + + + + + + + + + + + + @@ -108,7 +142,7 @@ @@ -129,8 +163,8 @@ + + + + + + + + + + + + + + + - + - + + + - - - - - - + + + + From fb3900aea3758b5fda19ba65488e432a285aef4e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 28 May 2020 13:22:54 -0300 Subject: [PATCH 098/711] fix accents. --- src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml | 14 +++++++------- .../Accents/BaseLight.xaml | 14 +++++++------- .../Accents/FluentBaseDark.xaml | 14 +++++++------- .../Accents/FluentBaseLight.xaml | 16 ++++++++-------- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml index 7ee211f249..7ce6c5a19f 100644 --- a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml @@ -4,13 +4,13 @@ - #FF0073CF - #FF1D85D7 - #FF3B97DF - #FF58A8E8 - #FF0064BE - #FF0055AD - #FF00459D + #FF0078D7 + #FF005A9E + #FF004275 + #FF002642 + #FF429CE3 + #FF76B9ED + #FFA6D8FF #FF000000 diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml index 309759705c..026edca4f9 100644 --- a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml @@ -4,13 +4,13 @@ - #FF0073CF - #FF1D85D7 - #FF3B97DF - #FF58A8E8 - #FF0064BE - #FF0055AD - #FF00459D + #FF0078D7 + #FF005A9E + #FF004275 + #FF002642 + #FF429CE3 + #FF76B9ED + #FFA6D8FF #FFFFFFFF diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml index 0bd95eb108..c8761541e8 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml @@ -2,7 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=netstandard"> - #FF0073CF + #FF0078D7 #FF000000 #FF000000 #FF000000 @@ -62,12 +62,12 @@ 1,1,1,1 1 - #FF1D85D7 - #FF3B97DF - #FF58A8E8 - #FF0064BE - #FF0055AD - #FF00459D + #FF005A9E + #FF004275 + #FF002642 + #FF429CE3 + #FF76B9ED + #FFA6D8FF #FF000000 diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml index 5ef54abeaf..65f4023318 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml @@ -2,7 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=netstandard"> - #FF0073CF + #FF0078D7 #FFFFFFFF #FFFFFFFF #FFFFFFFF @@ -61,13 +61,13 @@ 1,1,1,1 1,1,1,1 1 - - #FF1D85D7 - #FF3B97DF - #FF58A8E8 - #FF0064BE - #FF0055AD - #FF00459D + + #FF005A9E + #FF004275 + #FF002642 + #FF429CE3 + #FF76B9ED + #FFA6D8FF #FFFFFFFF From 6e7656dfb6d95b9bcdaa1fb32cbb482357dd8011 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 28 May 2020 14:12:22 -0300 Subject: [PATCH 099/711] fix checkbox glyph size. --- src/Avalonia.Themes.Fluent/CheckBox.xaml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/CheckBox.xaml b/src/Avalonia.Themes.Fluent/CheckBox.xaml index fa6b044cf6..d1d7f36478 100644 --- a/src/Avalonia.Themes.Fluent/CheckBox.xaml +++ b/src/Avalonia.Themes.Fluent/CheckBox.xaml @@ -35,10 +35,12 @@ Width="20" CornerRadius="{DynamicResource ControlCornerRadius}" /> - - + + + + + - + @@ -208,7 +211,7 @@ - + @@ -286,5 +290,5 @@ + From d80403aca91821356870a2ac3ee7a6ce61c52352 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 28 May 2020 15:02:39 -0300 Subject: [PATCH 100/711] add button resources --- .../Accents/FluentBaseDark.xaml | 14 +++++++ .../Accents/FluentBaseLight.xaml | 16 +++++++- .../Accents/FluentControlResourcesDark.xaml | 37 +++++++++++++++++++ .../Accents/FluentControlResourcesLight.xaml | 37 +++++++++++++++++++ 4 files changed, 103 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml index c8761541e8..ed6f56b211 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml @@ -71,6 +71,20 @@ #FF000000 + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml index 65f4023318..ac7b792a9a 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml @@ -61,7 +61,7 @@ 1,1,1,1 1,1,1,1 1 - + #FF005A9E #FF004275 #FF002642 @@ -72,6 +72,20 @@ #FFFFFFFF + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index 56c6de1144..a0f3ef0d62 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -2,6 +2,43 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=netstandard"> + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml index 70bee7074e..ec4f35664c 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -2,6 +2,43 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=netstandard"> + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + From 966519e23d844b58b03f05332361396f9d85ad5f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 28 May 2020 15:21:15 -0300 Subject: [PATCH 101/711] new button template. --- src/Avalonia.Themes.Fluent/Button.xaml | 98 ++++++++++++++++++++------ 1 file changed, 76 insertions(+), 22 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Button.xaml b/src/Avalonia.Themes.Fluent/Button.xaml index 698ddec2a8..42fcee6cd5 100644 --- a/src/Avalonia.Themes.Fluent/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Button.xaml @@ -1,34 +1,88 @@ - + + + + +