diff --git a/src/Avalonia.Base/Data/CompiledBinding.cs b/src/Avalonia.Base/Data/CompiledBinding.cs index e243246b4f..ca8ecc62ff 100644 --- a/src/Avalonia.Base/Data/CompiledBinding.cs +++ b/src/Avalonia.Base/Data/CompiledBinding.cs @@ -28,7 +28,7 @@ public class CompiledBinding : BindingBase /// /// The binding path. public CompiledBinding(CompiledBindingPath path) => Path = path; - + /// /// Creates a from a lambda expression. /// @@ -81,12 +81,10 @@ public class CompiledBinding : BindingBase /// Indexers: x => x.Items[0] /// Type casts: x => ((DerivedType)x).Property /// Logical NOT: x => !x.BoolProperty - /// Stream bindings: x => x.TaskProperty (Task/Observable) /// AvaloniaProperty access: x => x[MyProperty] /// /// - [RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)] - [RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Expression statically preserves members used in binding expressions.")] public static CompiledBinding Create( Expression> expression, object? source = null, diff --git a/src/Avalonia.Base/Data/CompiledBindingPath.cs b/src/Avalonia.Base/Data/CompiledBindingPath.cs index 886d89df43..2b8b914122 100644 --- a/src/Avalonia.Base/Data/CompiledBindingPath.cs +++ b/src/Avalonia.Base/Data/CompiledBindingPath.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using Avalonia.Controls; using Avalonia.Data.Core; @@ -155,12 +156,26 @@ namespace Avalonia.Data return this; } + [RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)] + public CompiledBindingPathBuilder StreamTask() + { + _elements.Add(new TaskStreamPathElement()); + return this; + } + public CompiledBindingPathBuilder StreamObservable() { _elements.Add(new ObservableStreamPathElement()); return this; } + [RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)] + public CompiledBindingPathBuilder StreamObservable() + { + _elements.Add(new ObservableStreamPathElement()); + return this; + } + public CompiledBindingPathBuilder Self() { _elements.Add(new SelfPathElement()); @@ -197,6 +212,12 @@ namespace Avalonia.Data return this; } + public CompiledBindingPathBuilder TypeCast(Type targetType) + { + _elements.Add(new TypeCastPathElement(targetType)); + return this; + } + public CompiledBindingPathBuilder TemplatedParent() { _elements.Add(new TemplatedParentPathElement()); @@ -299,6 +320,14 @@ namespace Avalonia.Data public IStreamPlugin CreatePlugin() => new TaskStreamPlugin(); } + [RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)] + 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(); @@ -306,6 +335,14 @@ namespace Avalonia.Data public IStreamPlugin CreatePlugin() => new ObservableStreamPlugin(); } + [RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)] + 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(); @@ -387,7 +424,28 @@ namespace Avalonia.Data public Type Type => typeof(T); - public Func Cast => TryCast; + public Func Cast { get; } = TryCast; + + public override string ToString() + => $"({Type.FullName})"; + } + + internal class TypeCastPathElement : ITypeCastElement + { + public TypeCastPathElement(Type type) + { + Type = type; + Cast = obj => + { + if (obj is { } result && type.IsInstanceOfType(result)) + return result; + return null; + }; + } + + public Type Type { get; } + + public Func Cast { get; } public override string ToString() => $"({Type.FullName})"; diff --git a/src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitor.cs b/src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitor.cs index c354bcc15a..8525dd8493 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitor.cs +++ b/src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitor.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; @@ -8,9 +7,7 @@ using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; using Avalonia.Data.Core.ExpressionNodes; -using Avalonia.Data.Core.ExpressionNodes.Reflection; using Avalonia.Data.Core.Plugins; -using Avalonia.Reactive; using Avalonia.Utilities; namespace Avalonia.Data.Core.Parsers; @@ -25,7 +22,6 @@ namespace Avalonia.Data.Core.Parsers; /// can then be converted into instances. It supports property access, /// indexers, AvaloniaProperty access, stream bindings, type casts, and logical operators. /// -[RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)] [RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)] internal class BindingExpressionVisitor(LambdaExpression expression) : ExpressionVisitor { @@ -149,17 +145,11 @@ internal class BindingExpressionVisitor(LambdaExpression expression) : Expr instanceType.GetGenericTypeDefinition() == typeof(Task<>) && genericArg.IsAssignableFrom(instanceType.GetGenericArguments()[0]))) { - var builderMethod = typeof(CompiledBindingPathBuilder) - .GetMethod(nameof(CompiledBindingPathBuilder.StreamTask))! - .MakeGenericMethod(genericArg); - return Add(instance, node, x => builderMethod.Invoke(x, null)); + return Add(instance, node, x => x.StreamTask()); } - else if (typeof(IObservable<>).MakeGenericType(genericArg).IsAssignableFrom(instance?.Type)) + else if (instanceType is not null && ObservableStreamPlugin.MatchesType(instanceType)) { - var builderMethod = typeof(CompiledBindingPathBuilder) - .GetMethod(nameof(CompiledBindingPathBuilder.StreamObservable))! - .MakeGenericMethod(genericArg); - return Add(instance, node, x => builderMethod.Invoke(x, null)); + return Add(instance, node, x => x.StreamObservable()); } } else if (method == BindingExpressionVisitorMembers.CreateDelegateMethod) @@ -194,18 +184,12 @@ internal class BindingExpressionVisitor(LambdaExpression expression) : Expr if (!node.Type.IsValueType && !node.Operand.Type.IsValueType && (node.Type.IsAssignableFrom(node.Operand.Type) || node.Operand.Type.IsAssignableFrom(node.Type))) { - var castMethod = typeof(CompiledBindingPathBuilder) - .GetMethod(nameof(CompiledBindingPathBuilder.TypeCast))! - .MakeGenericMethod(node.Type); - return Add(node.Operand, node, x => castMethod.Invoke(x, null)); + return Add(node.Operand, node, x => x.TypeCast(node.Type)); } } else if (node.NodeType == ExpressionType.TypeAs) { - var castMethod = typeof(CompiledBindingPathBuilder) - .GetMethod(nameof(CompiledBindingPathBuilder.TypeCast))! - .MakeGenericMethod(node.Type); - return Add(node.Operand, node, x => castMethod.Invoke(x, null)); + return Add(node.Operand, node, x => x.TypeCast(node.Type)); } throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); diff --git a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs index 523c0bcf39..b747542ee5 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs @@ -31,7 +31,18 @@ namespace Avalonia.Data.Core.Plugins { reference.TryGetTarget(out var target); - return target != null && target.GetType().GetInterfaces().Any(x => + return target != null && MatchesType(target.GetType()); + } + + public static bool MatchesType(Type type) + { + var interfaces = type.GetInterfaces().AsEnumerable(); + if (type.IsInterface) + { + interfaces = interfaces.Concat([type]); + } + + return interfaces.Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IObservable<>)); } diff --git a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs index 56a7fcefe4..5d395b8b5d 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs @@ -61,10 +61,9 @@ namespace Avalonia.Data.Core.Plugins return Observable.Empty(); } - [RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)] private static IObservable HandleCompleted(Task task) { - var resultProperty = task.GetType().GetRuntimeProperty("Result"); + var resultProperty = GetTaskResult(task); if (resultProperty != null) { @@ -80,6 +79,14 @@ namespace Avalonia.Data.Core.Plugins } return Observable.Empty(); + + [DynamicDependency("Result", typeof(Task<>))] + [UnconditionalSuppressMessage("Trimming", "IL2070")] + [UnconditionalSuppressMessage("Trimming", "IL2075")] + PropertyInfo? GetTaskResult(Task obj) + { + return obj.GetType().GetProperty("Result"); + } } } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs index 96af9a3a18..b366e98856 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -561,7 +561,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) { - codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.GetMethod(m => m.Name == "StreamObservable").MakeGenericMethod(new[] { Type })); + codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.GetMethod(m => m is { Name: "StreamObservable", IsGenericMethod: true }).MakeGenericMethod(new[] { Type })); } } @@ -576,7 +576,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) { - codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.GetMethod(m => m.Name == "StreamTask").MakeGenericMethod(new[] { Type })); + codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.GetMethod(m => m is { Name: "StreamTask", IsGenericMethod: true }).MakeGenericMethod(new[] { Type })); } } @@ -980,7 +980,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) { - codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.GetMethod(m => m.Name == "TypeCast").MakeGenericMethod(new[] { Type })); + codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.GetMethod(m => m is { Name: "TypeCast", IsGenericMethod: true }).MakeGenericMethod(new[] { Type })); } }