Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 71 additions & 7 deletions src/Avalonia.Base/Data/CompiledBindingPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ internal void BuildExpression(List<ExpressionNode> result, out bool isRooted)
isRooted = true;
break;
case IStronglyTypedStreamElement stream:
node = new StreamNode(stream.CreatePlugin());
node = new StreamNode(stream.CreatePlugin(), stream.AcceptsNull);
break;
case ITypeCastElement typeCast:
node = new FuncTransformNode(typeCast.Cast);
Expand Down Expand Up @@ -152,7 +152,13 @@ public CompiledBindingPathBuilder Command(string methodName, Action<object, obje

public CompiledBindingPathBuilder StreamTask<T>()
{
_elements.Add(new TaskStreamPathElement<T>());
_elements.Add(new TaskStreamPathElement<T>(acceptsNull: false));
return this;
}

public CompiledBindingPathBuilder StreamTask<T>(bool acceptsNull)
{
_elements.Add(new TaskStreamPathElement<T>(acceptsNull));
return this;
}

Expand All @@ -163,9 +169,22 @@ public CompiledBindingPathBuilder StreamTask()
return this;
}

[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
public CompiledBindingPathBuilder StreamTask(bool acceptsNull)
{
_elements.Add(new TaskStreamPathElement(acceptsNull));
return this;
}

public CompiledBindingPathBuilder StreamObservable<T>()
{
_elements.Add(new ObservableStreamPathElement<T>());
_elements.Add(new ObservableStreamPathElement<T>(acceptsNull: false));
return this;
}

public CompiledBindingPathBuilder StreamObservable<T>(bool acceptsNull)
{
_elements.Add(new ObservableStreamPathElement<T>(acceptsNull));
return this;
}

Expand All @@ -176,6 +195,13 @@ public CompiledBindingPathBuilder StreamObservable()
return this;
}

[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
public CompiledBindingPathBuilder StreamObservable(bool acceptsNull)
{
_elements.Add(new ObservableStreamPathElement(acceptsNull));
return this;
}

public CompiledBindingPathBuilder Self()
{
_elements.Add(new SelfPathElement());
Expand Down Expand Up @@ -303,6 +329,8 @@ public MethodAsCommandElement(string methodName, Action<object, object?> execute

internal interface IStronglyTypedStreamElement : ICompiledBindingPathElement
{
bool AcceptsNull { get; }

IStreamPlugin CreatePlugin();
}

Expand All @@ -315,30 +343,66 @@ internal interface ITypeCastElement : ICompiledBindingPathElement

internal class TaskStreamPathElement<T> : IStronglyTypedStreamElement
{
public static readonly TaskStreamPathElement<T> Instance = new TaskStreamPathElement<T>();
public static readonly TaskStreamPathElement<T> Instance = new TaskStreamPathElement<T>(acceptsNull: false);

public TaskStreamPathElement(bool acceptsNull)
{
AcceptsNull = acceptsNull;
}

public bool AcceptsNull { get; }

public IStreamPlugin CreatePlugin() => new TaskStreamPlugin<T>();
}

[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
internal class TaskStreamPathElement : IStronglyTypedStreamElement
{
public static readonly TaskStreamPathElement Instance = new TaskStreamPathElement();
public static readonly TaskStreamPathElement Instance = new TaskStreamPathElement(acceptsNull: false);

public TaskStreamPathElement() : this(acceptsNull: false)
{
}

public TaskStreamPathElement(bool acceptsNull)
{
AcceptsNull = acceptsNull;
}

public bool AcceptsNull { get; }

public IStreamPlugin CreatePlugin() => new TaskStreamPlugin();
}

internal class ObservableStreamPathElement<T> : IStronglyTypedStreamElement
{
public static readonly ObservableStreamPathElement<T> Instance = new ObservableStreamPathElement<T>();
public static readonly ObservableStreamPathElement<T> Instance = new ObservableStreamPathElement<T>(acceptsNull: false);

public ObservableStreamPathElement(bool acceptsNull)
{
AcceptsNull = acceptsNull;
}

public bool AcceptsNull { get; }

public IStreamPlugin CreatePlugin() => new ObservableStreamPlugin<T>();
}

[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
internal class ObservableStreamPathElement : IStronglyTypedStreamElement
{
public static readonly ObservableStreamPathElement Instance = new ObservableStreamPathElement();
public static readonly ObservableStreamPathElement Instance = new ObservableStreamPathElement(acceptsNull: false);

public ObservableStreamPathElement() : this(acceptsNull: false)
{
}

public ObservableStreamPathElement(bool acceptsNull)
{
AcceptsNull = acceptsNull;
}

public bool AcceptsNull { get; }

public IStreamPlugin CreatePlugin() => new ObservableStreamPlugin();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,29 @@ namespace Avalonia.Data.Core.ExpressionNodes.Reflection;
[RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)]
internal sealed class DynamicPluginStreamNode : ExpressionNode
{
private readonly bool _acceptsNull;
private IDisposable? _subscription;

public DynamicPluginStreamNode(bool acceptsNull)
{
_acceptsNull = acceptsNull;
}

override public void BuildString(StringBuilder builder)
{
builder.Append('^');
}

protected override void OnSourceChanged(object? source, Exception? dataValidationError)
{
if (!ValidateNonNullSource(source))
if (source is null)
{
if (_acceptsNull)
SetValue(null);
else
ValidateNonNullSource(source);
return;
}

var reference = new WeakReference<object?>(source);

Expand Down
12 changes: 10 additions & 2 deletions src/Avalonia.Base/Data/Core/ExpressionNodes/StreamNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ namespace Avalonia.Data.Core.ExpressionNodes;

internal sealed class StreamNode : ExpressionNode, IObserver<object?>
{
private readonly bool _acceptsNull;
private IStreamPlugin _plugin;
private IDisposable? _subscription;

public StreamNode(IStreamPlugin plugin)
public StreamNode(IStreamPlugin plugin, bool acceptsNull)
{
_plugin = plugin;
_acceptsNull = acceptsNull;
}

public override void BuildString(StringBuilder builder)
Expand All @@ -25,8 +27,14 @@ public override void BuildString(StringBuilder builder)

protected override void OnSourceChanged(object? source, Exception? dataValidationError)
{
if (!ValidateNonNullSource(source))
if (source is null)
{
if (_acceptsNull)
SetValue(null);
else
ValidateNonNullSource(source);
return;
}

if (_plugin.Start(new(source)) is { } accessor)
{
Expand Down
34 changes: 27 additions & 7 deletions src/Avalonia.Base/Data/Core/Parsers/BindingExpressionGrammar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,10 @@ private static State ParseStart(ref CharacterReader r, IList<INode> nodes)
nodes.Add(new EmptyExpressionNode());
return State.AfterMember;
}
else if (ParseStreamOperator(ref r))
else if (ParseStreamOperator(ref r, out var acceptsNull))
{
nodes.Add(new EmptyExpressionNode());
nodes.Add(new StreamNode());
nodes.Add(new StreamNode { AcceptsNull = acceptsNull });
return State.AfterMember;
}
else
Expand All @@ -166,9 +166,9 @@ private static State ParseAfterMember(ref CharacterReader r, IList<INode> nodes)
{
return acceptsNull ? State.BeforeMemberNullable : State.BeforeMember;
}
else if (ParseStreamOperator(ref r))
else if (ParseStreamOperator(ref r, out acceptsNull))
{
nodes.Add(new StreamNode());
nodes.Add(new StreamNode { AcceptsNull = acceptsNull });
return State.AfterMember;
}
else if (PeekOpenBracket(ref r))
Expand Down Expand Up @@ -441,9 +441,26 @@ private static bool PeekOpenBrace(ref CharacterReader r)
return !r.End && r.Peek == '(';
}

private static bool ParseStreamOperator(ref CharacterReader r)
private static bool ParseStreamOperator(ref CharacterReader r, out bool acceptsNull)
{
return !r.End && r.TakeIf('^');
acceptsNull = false;

if (r.End)
return false;

if (r.Peek == '^')
{
r.Take();
return true;
}

if (r.TakeIf("?^"))
{
acceptsNull = true;
return true;
}

return false;
}

private static bool ParseDollarSign(ref CharacterReader r)
Expand Down Expand Up @@ -521,7 +538,10 @@ public class IndexerNode : INode

public class NotNode : INode, ITransformNode { }

public class StreamNode : INode { }
public class StreamNode : INode
{
public bool AcceptsNull { get; set; }
}

public class SelfNode : INode { }

Expand Down
4 changes: 2 additions & 2 deletions src/Avalonia.Base/Data/Core/Parsers/ExpressionNodeFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ internal static class ExpressionNodeFactory
node = null;
isRooted = true;
break;
case BindingExpressionGrammar.StreamNode:
node = new DynamicPluginStreamNode();
case BindingExpressionGrammar.StreamNode stream:
node = new DynamicPluginStreamNode(stream.AcceptsNull);
break;
case BindingExpressionGrammar.TypeCastNode typeCast:
node = new ReflectionTypeCastNode(LookupType(typeResolver, typeCast.Namespace, typeCast.TypeName));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ private static XamlIlBindingPathNode TransformBindingPath(AstTransformationConte
case BindingExpressionGrammar.NotNode _:
transformNodes.Add(new XamlIlNotPathElementNode(context.Configuration.WellKnownTypes.Boolean));
break;
case BindingExpressionGrammar.StreamNode _:
case BindingExpressionGrammar.StreamNode streamNode:
{
IXamlType targetType = targetTypeResolver();
IXamlType? observableType;
Expand All @@ -168,7 +168,7 @@ private static XamlIlBindingPathNode TransformBindingPath(AstTransformationConte

if (observableType != null)
{
nodes.Add(new XamlIlStreamObservablePathElementNode(observableType.GenericArguments[0]));
nodes.Add(new XamlIlStreamObservablePathElementNode(observableType.GenericArguments[0], streamNode.AcceptsNull));
break;
}

Expand All @@ -180,7 +180,7 @@ private static XamlIlBindingPathNode TransformBindingPath(AstTransformationConte
if (currentType.GenericTypeDefinition?.Equals(taskType) == true)
{
foundTask = true;
nodes.Add(new XamlIlStreamTaskPathElementNode(currentType.GenericArguments[0]));
nodes.Add(new XamlIlStreamTaskPathElementNode(currentType.GenericArguments[0], streamNode.AcceptsNull));
break;
}
}
Expand Down Expand Up @@ -620,31 +620,57 @@ public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen)

class XamlIlStreamObservablePathElementNode : IXamlIlBindingPathElementNode
{
public XamlIlStreamObservablePathElementNode(IXamlType type)
private readonly bool _acceptsNull;

public XamlIlStreamObservablePathElementNode(IXamlType type, bool acceptsNull)
{
Type = type;
_acceptsNull = acceptsNull;
}

public IXamlType Type { get; }

public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen)
{
codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.GetMethod(m => m is { Name: "StreamObservable", IsGenericMethod: true }).MakeGenericMethod(new[] { Type }));
var parameterCount = 0;

if (_acceptsNull)
{
parameterCount = 1;
codeGen.Ldc_I4(1);
}

codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.GetMethod(m =>
m is { Name: "StreamObservable", IsGenericMethod: true } &&
m.Parameters.Count == parameterCount).MakeGenericMethod(new[] { Type }));
}
}

class XamlIlStreamTaskPathElementNode : IXamlIlBindingPathElementNode
{
public XamlIlStreamTaskPathElementNode(IXamlType type)
private readonly bool _acceptsNull;

public XamlIlStreamTaskPathElementNode(IXamlType type, bool acceptsNull)
{
Type = type;
_acceptsNull = acceptsNull;
}

public IXamlType Type { get; }

public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen)
{
codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.GetMethod(m => m is { Name: "StreamTask", IsGenericMethod: true }).MakeGenericMethod(new[] { Type }));
var parameterCount = 0;

if (_acceptsNull)
{
parameterCount = 1;
codeGen.Ldc_I4(1);
}

codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.GetMethod(m =>
m is { Name: "StreamTask", IsGenericMethod: true } &&
m.Parameters.Count == parameterCount).MakeGenericMethod(new[] { Type }));
}
}

Expand Down
Loading