Browse Source

Move binding syntax sugar parsing into the regular binding path parsing instead of being XAML-only.

pull/1772/head
Jeremy Koritzinsky 8 years ago
parent
commit
3b5a2ecb01
  1. 167
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  2. 96
      src/Markup/Avalonia.Markup/Data/Binding.cs
  3. 10
      src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs
  4. 12
      src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs
  5. 137
      src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs
  6. 39
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs
  7. 54
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs
  8. 12
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs

167
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs

@ -32,185 +32,28 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
{ {
var descriptorContext = (ITypeDescriptorContext)serviceProvider; var descriptorContext = (ITypeDescriptorContext)serviceProvider;
var pathInfo = ParsePath(Path, descriptorContext);
ValidateState(pathInfo);
return new Binding return new Binding
{ {
TypeResolver = descriptorContext.ResolveType, TypeResolver = descriptorContext.ResolveType,
Converter = Converter, Converter = Converter,
ConverterParameter = ConverterParameter, ConverterParameter = ConverterParameter,
ElementName = pathInfo.ElementName ?? ElementName, ElementName = ElementName,
FallbackValue = FallbackValue, FallbackValue = FallbackValue,
Mode = Mode, Mode = Mode,
Path = pathInfo.Path, Path = Path,
Priority = Priority, Priority = Priority,
Source = Source, Source = Source,
RelativeSource = pathInfo.RelativeSource ?? RelativeSource, RelativeSource = RelativeSource,
DefaultAnchor = new WeakReference(GetDefaultAnchor((ITypeDescriptorContext)serviceProvider)) DefaultAnchor = new WeakReference(GetDefaultAnchor((ITypeDescriptorContext)serviceProvider))
}; };
} }
private class PathInfo
{
public string Path { get; set; }
public string ElementName { get; set; }
public RelativeSource RelativeSource { get; set; }
}
private void ValidateState(PathInfo pathInfo)
{
if (pathInfo.ElementName != null && ElementName != null)
{
throw new InvalidOperationException(
"ElementName property cannot be set when an #elementName path is provided.");
}
if (pathInfo.RelativeSource != null && RelativeSource != null)
{
throw new InvalidOperationException(
"ElementName property cannot be set when a $self or $parent path is provided.");
}
if ((pathInfo.ElementName != null || ElementName != null) &&
(pathInfo.RelativeSource != null || RelativeSource != null))
{
throw new InvalidOperationException(
"ElementName property cannot be set with a RelativeSource.");
}
}
private static PathInfo ParsePath(string path, ITypeDescriptorContext context)
{
var result = new PathInfo();
if (string.IsNullOrWhiteSpace(path) || path == ".")
{
result.Path = string.Empty;
return result;
}
else if (path.StartsWith("!"))
{
int pathStart = 0;
for (; pathStart < path.Length && path[pathStart] == '!'; ++pathStart);
result.Path = path.Substring(0, pathStart);
path = path.Substring(pathStart);
}
if (path.StartsWith("#"))
{
var dot = path.IndexOf('.');
if (dot != -1)
{
result.Path += path.Substring(dot + 1);
result.ElementName = path.Substring(1, dot - 1);
}
else
{
result.Path += string.Empty;
result.ElementName = path.Substring(1);
}
}
else if (path.StartsWith("$"))
{
var relativeSource = new RelativeSource
{
Tree = TreeType.Logical
};
result.RelativeSource = relativeSource;
var dot = path.IndexOf('.');
string relativeSourceMode;
if (dot != -1)
{
result.Path += path.Substring(dot + 1);
relativeSourceMode = path.Substring(1, dot - 1);
}
else
{
result.Path += string.Empty;
relativeSourceMode = path.Substring(1);
}
if (relativeSourceMode == "self")
{
relativeSource.Mode = RelativeSourceMode.Self;
}
else if (relativeSourceMode == "parent")
{
relativeSource.Mode = RelativeSourceMode.FindAncestor;
relativeSource.AncestorLevel = 1;
}
else if (relativeSourceMode.StartsWith("parent["))
{
relativeSource.Mode = RelativeSourceMode.FindAncestor;
var parentConfigStart = relativeSourceMode.IndexOf('[');
if (!relativeSourceMode.EndsWith("]"))
{
throw new InvalidOperationException("Invalid RelativeSource binding syntax. Expected matching ']' for '['.");
}
var parentConfigParams = relativeSourceMode.Substring(parentConfigStart + 1).TrimEnd(']').Split(';');
if (parentConfigParams.Length > 2 || parentConfigParams.Length == 0)
{
throw new InvalidOperationException("Expected either 1 or 2 parameters for RelativeSource binding syntax");
}
else if (parentConfigParams.Length == 1)
{
if (int.TryParse(parentConfigParams[0], out int level))
{
relativeSource.AncestorType = null;
relativeSource.AncestorLevel = level + 1;
}
else
{
relativeSource.AncestorType = LookupAncestorType(parentConfigParams[0], context);
}
}
else
{
relativeSource.AncestorType = LookupAncestorType(parentConfigParams[0], context);
relativeSource.AncestorLevel = int.Parse(parentConfigParams[1]) + 1;
}
}
else
{
throw new InvalidOperationException($"Invalid RelativeSource binding syntax: {relativeSourceMode}");
}
}
else
{
result.Path += path;
}
return result;
}
private static Type LookupAncestorType(string ancestorTypeName, ITypeDescriptorContext context)
{
var parts = ancestorTypeName.Split(':');
if (parts.Length == 0 || parts.Length > 2)
{
throw new InvalidOperationException("Invalid type name");
}
if (parts.Length == 1)
{
return context.ResolveType(string.Empty, parts[0]);
}
else
{
return context.ResolveType(parts[0], parts[1]);
}
}
private static object GetDefaultAnchor(ITypeDescriptorContext context) private static object GetDefaultAnchor(ITypeDescriptorContext context)
{ {
object anchor = null; // If the target is not a control, so we need to find an anchor that will let us look
// 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 // up named controls and style resources. First look for the closest IControl in
// the context. // the context.
anchor = context.GetFirstAmbientValue<IControl>(); object anchor = context.GetFirstAmbientValue<IControl>();
// If a control was not found, then try to find the highest-level style as the XAML // 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. // file could be a XAML file containing only styles.

96
src/Markup/Avalonia.Markup/Data/Binding.cs

@ -105,34 +105,51 @@ namespace Avalonia.Data
ExpressionObserver observer; ExpressionObserver observer;
var (node, mode) = ExpressionObserverBuilder.Parse(Path, enableDataValidation, TypeResolver);
if (ElementName != null) if (ElementName != null)
{ {
observer = CreateElementObserver( observer = CreateElementObserver(
(target as IStyledElement) ?? (anchor as IStyledElement), (target as IStyledElement) ?? (anchor as IStyledElement),
ElementName, ElementName,
Path, node);
enableDataValidation);
} }
else if (Source != null) else if (Source != null)
{ {
observer = CreateSourceObserver(Source, Path, enableDataValidation); observer = CreateSourceObserver(Source, node);
} }
else if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext) else if (RelativeSource == null)
{
if (mode == SourceMode.Data)
{
observer = CreateDataContextObserver(
target,
node,
targetProperty == StyledElement.DataContextProperty,
anchor);
}
else
{
observer = new ExpressionObserver(
(target as IStyledElement) ?? (anchor as IStyledElement),
node);
}
}
else if (RelativeSource.Mode == RelativeSourceMode.DataContext)
{ {
observer = CreateDataContextObserver( observer = CreateDataContextObserver(
target, target,
Path, node,
targetProperty == StyledElement.DataContextProperty, targetProperty == StyledElement.DataContextProperty,
anchor, anchor);
enableDataValidation);
} }
else if (RelativeSource.Mode == RelativeSourceMode.Self) else if (RelativeSource.Mode == RelativeSourceMode.Self)
{ {
observer = CreateSourceObserver(target, Path, enableDataValidation); observer = CreateSourceObserver(target, node);
} }
else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent) else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent)
{ {
observer = CreateTemplatedParentObserver(target, Path, enableDataValidation); observer = CreateTemplatedParentObserver(target, node);
} }
else if (RelativeSource.Mode == RelativeSourceMode.FindAncestor) else if (RelativeSource.Mode == RelativeSourceMode.FindAncestor)
{ {
@ -144,8 +161,7 @@ namespace Avalonia.Data
observer = CreateFindAncestorObserver( observer = CreateFindAncestorObserver(
(target as IStyledElement) ?? (anchor as IStyledElement), (target as IStyledElement) ?? (anchor as IStyledElement),
RelativeSource, RelativeSource,
Path, node);
enableDataValidation);
} }
else else
{ {
@ -176,10 +192,9 @@ namespace Avalonia.Data
private ExpressionObserver CreateDataContextObserver( private ExpressionObserver CreateDataContextObserver(
IAvaloniaObject target, IAvaloniaObject target,
string path, ExpressionNode node,
bool targetIsDataContext, bool targetIsDataContext,
object anchor, object anchor)
bool enableDataValidation)
{ {
Contract.Requires<ArgumentNullException>(target != null); Contract.Requires<ArgumentNullException>(target != null);
@ -195,48 +210,41 @@ namespace Avalonia.Data
if (!targetIsDataContext) if (!targetIsDataContext)
{ {
var result = ExpressionObserverBuilder.Build( var result = new ExpressionObserver(
() => target.GetValue(StyledElement.DataContextProperty), () => target.GetValue(StyledElement.DataContextProperty),
path, node,
new UpdateSignal(target, StyledElement.DataContextProperty), new UpdateSignal(target, StyledElement.DataContextProperty),
enableDataValidation, null);
typeResolver: TypeResolver);
return result; return result;
} }
else else
{ {
return ExpressionObserverBuilder.Build( return new ExpressionObserver(
GetParentDataContext(target), GetParentDataContext(target),
path, node,
enableDataValidation, null);
typeResolver: TypeResolver);
} }
} }
private ExpressionObserver CreateElementObserver( private ExpressionObserver CreateElementObserver(
IStyledElement target, IStyledElement target,
string elementName, string elementName,
string path, ExpressionNode node)
bool enableDataValidation)
{ {
Contract.Requires<ArgumentNullException>(target != null); Contract.Requires<ArgumentNullException>(target != null);
var description = $"#{elementName}.{path}"; var result = new ExpressionObserver(
var result = ExpressionObserverBuilder.Build(
ControlLocator.Track(target, elementName), ControlLocator.Track(target, elementName),
path, node,
enableDataValidation, null);
description,
typeResolver: TypeResolver);
return result; return result;
} }
private ExpressionObserver CreateFindAncestorObserver( private ExpressionObserver CreateFindAncestorObserver(
IStyledElement target, IStyledElement target,
RelativeSource relativeSource, RelativeSource relativeSource,
string path, ExpressionNode node)
bool enableDataValidation)
{ {
Contract.Requires<ArgumentNullException>(target != null); Contract.Requires<ArgumentNullException>(target != null);
@ -260,36 +268,32 @@ namespace Avalonia.Data
throw new InvalidOperationException("Invalid tree to traverse."); throw new InvalidOperationException("Invalid tree to traverse.");
} }
return ExpressionObserverBuilder.Build( return new ExpressionObserver(
controlLocator, controlLocator,
path, node,
enableDataValidation, null);
typeResolver: TypeResolver);
} }
private ExpressionObserver CreateSourceObserver( private ExpressionObserver CreateSourceObserver(
object source, object source,
string path, ExpressionNode node)
bool enableDataValidation)
{ {
Contract.Requires<ArgumentNullException>(source != null); Contract.Requires<ArgumentNullException>(source != null);
return ExpressionObserverBuilder.Build(source, path, enableDataValidation, typeResolver: TypeResolver); return new ExpressionObserver(source, node);
} }
private ExpressionObserver CreateTemplatedParentObserver( private ExpressionObserver CreateTemplatedParentObserver(
IAvaloniaObject target, IAvaloniaObject target,
string path, ExpressionNode node)
bool enableDataValidation)
{ {
Contract.Requires<ArgumentNullException>(target != null); Contract.Requires<ArgumentNullException>(target != null);
var result = ExpressionObserverBuilder.Build( var result = new ExpressionObserver(
() => target.GetValue(StyledElement.TemplatedParentProperty), () => target.GetValue(StyledElement.TemplatedParentProperty),
path, node,
new UpdateSignal(target, StyledElement.TemplatedParentProperty), new UpdateSignal(target, StyledElement.TemplatedParentProperty),
enableDataValidation, null);
typeResolver: TypeResolver);
return result; return result;
} }

10
src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs

@ -10,7 +10,7 @@ namespace Avalonia.Markup.Parsers
{ {
internal static class ArgumentListParser internal static class ArgumentListParser
{ {
public static IList<string> Parse(Reader r, char open, char close) public static IList<string> Parse(Reader r, char open, char close, char delimiter = ',')
{ {
if (r.Peek == open) if (r.Peek == open)
{ {
@ -21,7 +21,7 @@ namespace Avalonia.Markup.Parsers
while (!r.End) while (!r.End)
{ {
var builder = new StringBuilder(); var builder = new StringBuilder();
while (!r.End && r.Peek != ',' && r.Peek != close && !char.IsWhiteSpace(r.Peek)) while (!r.End && r.Peek != delimiter && r.Peek != close && !char.IsWhiteSpace(r.Peek))
{ {
builder.Append(r.Take()); builder.Append(r.Take());
} }
@ -35,7 +35,7 @@ namespace Avalonia.Markup.Parsers
if (r.End) if (r.End)
{ {
throw new ExpressionParseException(r.Position, "Expected ','."); throw new ExpressionParseException(r.Position, $"Expected '{delimiter}'.");
} }
else if (r.TakeIf(close)) else if (r.TakeIf(close))
{ {
@ -43,9 +43,9 @@ namespace Avalonia.Markup.Parsers
} }
else else
{ {
if (r.Take() != ',') if (r.Take() != delimiter)
{ {
throw new ExpressionParseException(r.Position, "Expected ','."); throw new ExpressionParseException(r.Position, $"Expected '{delimiter}'.");
} }
r.SkipWhitespace(); r.SkipWhitespace();

12
src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs

@ -8,11 +8,11 @@ namespace Avalonia.Markup.Parsers
{ {
public static class ExpressionObserverBuilder public static class ExpressionObserverBuilder
{ {
internal static ExpressionNode Parse(string expression, bool enableValidation = false, Func<string, string, Type> typeResolver = null) internal static (ExpressionNode Node, SourceMode Mode) Parse(string expression, bool enableValidation = false, Func<string, string, Type> typeResolver = null)
{ {
if (string.IsNullOrWhiteSpace(expression)) if (string.IsNullOrWhiteSpace(expression))
{ {
return new EmptyExpressionNode(); return (new EmptyExpressionNode(), default);
} }
var reader = new Reader(expression); var reader = new Reader(expression);
@ -36,7 +36,7 @@ namespace Avalonia.Markup.Parsers
{ {
return new ExpressionObserver( return new ExpressionObserver(
root, root,
Parse(expression, enableDataValidation, typeResolver), Parse(expression, enableDataValidation, typeResolver).Node,
description ?? expression); description ?? expression);
} }
@ -50,7 +50,7 @@ namespace Avalonia.Markup.Parsers
Contract.Requires<ArgumentNullException>(rootObservable != null); Contract.Requires<ArgumentNullException>(rootObservable != null);
return new ExpressionObserver( return new ExpressionObserver(
rootObservable, rootObservable,
Parse(expression, enableDataValidation, typeResolver), Parse(expression, enableDataValidation, typeResolver).Node,
description ?? expression); description ?? expression);
} }
@ -66,8 +66,8 @@ namespace Avalonia.Markup.Parsers
Contract.Requires<ArgumentNullException>(rootGetter != null); Contract.Requires<ArgumentNullException>(rootGetter != null);
return new ExpressionObserver( return new ExpressionObserver(
() => rootGetter(), rootGetter,
Parse(expression, enableDataValidation, typeResolver), Parse(expression, enableDataValidation, typeResolver).Node,
update, update,
description ?? expression); description ?? expression);
} }

137
src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs

@ -9,6 +9,12 @@ using System.Linq;
namespace Avalonia.Markup.Parsers namespace Avalonia.Markup.Parsers
{ {
internal enum SourceMode
{
Data,
Control
}
internal class ExpressionParser internal class ExpressionParser
{ {
private readonly bool _enableValidation; private readonly bool _enableValidation;
@ -20,10 +26,11 @@ namespace Avalonia.Markup.Parsers
_enableValidation = enableValidation; _enableValidation = enableValidation;
} }
public ExpressionNode Parse(Reader r) public (ExpressionNode Node, SourceMode Mode) Parse(Reader r)
{ {
var nodes = new List<ExpressionNode>(); var nodes = new List<ExpressionNode>();
var state = State.Start; var state = State.Start;
var mode = SourceMode.Data;
while (!r.End && state != State.End) while (!r.End && state != State.End)
{ {
@ -48,6 +55,16 @@ namespace Avalonia.Markup.Parsers
case State.Indexer: case State.Indexer:
state = ParseIndexer(r, nodes); state = ParseIndexer(r, nodes);
break; break;
case State.ElementName:
state = ParseElementName(r, nodes);
mode = SourceMode.Control;
break;
case State.RelativeSource:
state = ParseRelativeSource(r, nodes);
mode = SourceMode.Control;
break;
} }
} }
@ -61,7 +78,7 @@ namespace Avalonia.Markup.Parsers
nodes[n].Next = nodes[n + 1]; nodes[n].Next = nodes[n + 1];
} }
return nodes.FirstOrDefault(); return (nodes.FirstOrDefault(), mode);
} }
private State ParseStart(Reader r, IList<ExpressionNode> nodes) private State ParseStart(Reader r, IList<ExpressionNode> nodes)
@ -71,6 +88,14 @@ namespace Avalonia.Markup.Parsers
nodes.Add(new LogicalNotNode()); nodes.Add(new LogicalNotNode());
return State.Start; return State.Start;
} }
else if (ParseSharp(r))
{
return State.ElementName;
}
else if (ParseDollarSign(r))
{
return State.RelativeSource;
}
else if (ParseOpenBrace(r)) else if (ParseOpenBrace(r))
{ {
return State.AttachedProperty; return State.AttachedProperty;
@ -134,19 +159,7 @@ namespace Avalonia.Markup.Parsers
private State ParseAttachedProperty(Reader r, List<ExpressionNode> nodes) private State ParseAttachedProperty(Reader r, List<ExpressionNode> nodes)
{ {
string ns = string.Empty; var (ns, owner) = ParseTypeName(r);
string owner;
var ownerOrNamespace = IdentifierParser.Parse(r);
if (r.TakeIf(':'))
{
ns = ownerOrNamespace;
owner = IdentifierParser.Parse(r);
}
else
{
owner = ownerOrNamespace;
}
if (r.End || !r.TakeIf('.')) if (r.End || !r.TakeIf('.'))
{ {
@ -184,6 +197,88 @@ namespace Avalonia.Markup.Parsers
return State.AfterMember; return State.AfterMember;
} }
private State ParseElementName(Reader r, List<ExpressionNode> nodes)
{
var name = IdentifierParser.Parse(r);
if (name == null)
{
throw new ExpressionParseException(r.Position, "Element name expected after '#'.");
}
nodes.Add(new ElementNameNode(name));
return State.AfterMember;
}
private State ParseRelativeSource(Reader r, List<ExpressionNode> nodes)
{
var mode = IdentifierParser.Parse(r);
if (mode == "self")
{
nodes.Add(new SelfNode());
}
else if (mode == "parent")
{
Type ancestorType = null;
var ancestorLevel = 0;
if (PeekOpenBracket(r))
{
var args = ArgumentListParser.Parse(r, '[', ']', ';');
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 typeName = ParseTypeName(new Reader(args[0]));
ancestorType = _typeResolver(typeName.ns, typeName.typeName);
}
}
else
{
var typeName = ParseTypeName(new Reader(args[0]));
ancestorType = _typeResolver(typeName.ns, typeName.typeName);
ancestorLevel = int.Parse(args[1]);
}
}
nodes.Add(new FindAncestorNode(ancestorType, ancestorLevel));
}
else
{
throw new ExpressionParseException(r.Position, "Unknown RelativeSource mode.");
}
return State.AfterMember;
}
private static (string ns, string typeName) ParseTypeName(Reader r)
{
string ns, typeName;
ns = string.Empty;
var typeNameOrNamespace = IdentifierParser.Parse(r);
if (!r.End && r.TakeIf(':'))
{
ns = typeNameOrNamespace;
typeName = IdentifierParser.Parse(r);
}
else
{
typeName = typeNameOrNamespace;
}
return (ns, typeName);
}
private static bool ParseNot(Reader r) private static bool ParseNot(Reader r)
{ {
return !r.End && r.TakeIf('!'); return !r.End && r.TakeIf('!');
@ -209,9 +304,21 @@ namespace Avalonia.Markup.Parsers
return !r.End && r.TakeIf('^'); return !r.End && r.TakeIf('^');
} }
private static bool ParseDollarSign(Reader r)
{
return !r.End && r.TakeIf('$');
}
private static bool ParseSharp(Reader r)
{
return !r.End && r.TakeIf('#');
}
private enum State private enum State
{ {
Start, Start,
RelativeSource,
ElementName,
AfterMember, AfterMember,
BeforeMember, BeforeMember,
AttachedProperty, AttachedProperty,

39
src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Data.Core;
using Avalonia.LogicalTree;
namespace Avalonia.Markup.Parsers.Nodes
{
internal class ElementNameNode : ExpressionNode
{
private readonly string _name;
private IDisposable _subscription;
public ElementNameNode(string name)
{
_name = name;
}
public override string Description => $"#{_name}";
protected override void StartListeningCore(WeakReference reference)
{
if (reference.Target is ILogical logical)
{
_subscription = ControlLocator.Track(logical, _name).Subscribe(ValueChanged);
}
else
{
_subscription = null;
}
}
protected override void StopListeningCore()
{
_subscription?.Dispose();
_subscription = null;
}
}
}

54
src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Data.Core;
using Avalonia.LogicalTree;
namespace Avalonia.Markup.Parsers.Nodes
{
internal class FindAncestorNode : ExpressionNode
{
private readonly int _level;
private readonly Type _ancestorType;
private IDisposable _subscription;
public FindAncestorNode(Type ancestorType, int level)
{
_level = level;
_ancestorType = ancestorType;
}
public override string Description
{
get
{
if (_ancestorType == null)
{
return $"$parent[{_level}]";
}
else
{
return $"$parent[{_ancestorType.Name}, {_level}]";
}
}
}
protected override void StartListeningCore(WeakReference reference)
{
if (reference.Target is ILogical logical)
{
_subscription = ControlLocator.Track(logical, _level, _ancestorType).Subscribe(ValueChanged);
}
else
{
_subscription = null;
}
}
protected override void StopListeningCore()
{
_subscription?.Dispose();
_subscription = null;
}
}
}

12
src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Data.Core;
namespace Avalonia.Markup.Parsers.Nodes
{
internal class SelfNode : ExpressionNode
{
public override string Description => "$self";
}
}
Loading…
Cancel
Save