Browse Source

Finish initial implementation of compiled bindings. Still some bugs to work out, but we have something that works now.

pull/3001/head
Jeremy Koritzinsky 7 years ago
parent
commit
e3bc5345ae
  1. 7
      src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs
  2. 3
      src/Avalonia.Base/Data/Core/IPropertyInfo.cs
  3. 2
      src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs
  4. 22
      src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs
  5. 37
      src/Avalonia.Base/Data/Core/StreamNode.cs
  6. 8
      src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
  7. 13
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  8. 75
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs
  9. 177
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs
  10. 255
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/NotifyingPropertyInfoHelpers.cs
  11. 28
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ObservableStreamPlugin.cs
  12. 112
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorPlugin.cs
  13. 53
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/TaskStreamPlugin.cs
  14. 14
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  15. 78
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs
  16. 33
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs
  17. 46
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs
  18. 5
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs
  19. 10
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  20. 459
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs
  21. 11
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs
  22. 263
      src/Markup/Avalonia.Markup/Data/Binding.cs
  23. 275
      src/Markup/Avalonia.Markup/Data/BindingBase.cs
  24. 5
      src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs
  25. 11
      src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs
  26. 2
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs
  27. 2
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs
  28. 2
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs
  29. 47
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs
  30. 15
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs

7
src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs

@ -9,14 +9,17 @@ namespace Avalonia.Data.Core
private readonly Func<object, object> _getter;
private readonly Action<object, object> _setter;
public ClrPropertyInfo(string name, Func<object, object> getter, Action<object, object> setter)
public ClrPropertyInfo(string name, Func<object, object> getter, Action<object, object> 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)
{
}

3
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; }
}
}

2
src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs

@ -57,7 +57,7 @@ namespace Avalonia.Data.Core.Plugins
return Observable.Empty<object>();
}
protected IObservable<object> HandleCompleted(Task task)
private IObservable<object> HandleCompleted(Task task)
{
var resultProperty = task.GetType().GetRuntimeProperty("Result");

22
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();

37
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;
}
}
}

8
src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj

@ -45,9 +45,15 @@
<Compile Include="../Avalonia.Base/Utilities/IdentifierParser.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
<Compile Include="..\Markup\Avalonia.Markup\Markup\Parsers\ArgumentListParser.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
<Compile Include="../Avalonia.Base/Utilities/KeywordParser.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
</Compile>
<Compile Include="..\Markup\Avalonia.Markup\Markup\Parsers\BindingExpressionGrammar.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
<Compile Remove="../Markup/Avalonia.Markup.Xaml/XamlIl\xamlil.github\**\obj\**\*.cs" />
<Compile Remove="../Markup/Avalonia.Markup.Xaml/XamlIl\xamlil.github\src\XamlIl\TypeSystem\SreTypeSystem.cs" />
<PackageReference Include="Avalonia.Unofficial.Cecil" Version="20190417.2.0" PrivateAssets="All" />

13
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@ -16,6 +16,12 @@
<Compile Include="Converters\TimeSpanTypeConverter.cs" />
<Compile Include="Extensions.cs" />
<Compile Include="MarkupExtension.cs" />
<Compile Include="MarkupExtensions\CompiledBindingExtension.cs" />
<Compile Include="MarkupExtensions\CompiledBindings\CompiledBindingPath.cs" />
<Compile Include="MarkupExtensions\CompiledBindings\NotifyingPropertyInfoHelpers.cs" />
<Compile Include="MarkupExtensions\CompiledBindings\ObservableStreamPlugin.cs" />
<Compile Include="MarkupExtensions\CompiledBindings\PropertyInfoAccessorPlugin.cs" />
<Compile Include="MarkupExtensions\CompiledBindings\TaskStreamPlugin.cs" />
<Compile Include="MarkupExtensions\DynamicResourceExtension.cs" />
<Compile Include="MarkupExtensions\ResourceInclude.cs" />
<Compile Include="MarkupExtensions\StaticResourceExtension.cs" />
@ -41,6 +47,7 @@
<Compile Include="XamlIl\CompilerExtensions\AvaloniaXamlIlCompilerConfiguration.cs" />
<Compile Include="XamlIl\CompilerExtensions\Transformers\AvaloniaBindingExtensionHackTransformer.cs" />
<Compile Include="XamlIl\CompilerExtensions\Transformers\AvaloniaXamlIlAvaloniaPropertyResolver.cs" />
<Compile Include="XamlIl\CompilerExtensions\Transformers\AvaloniaXamlIlBindingPathTransformer.cs" />
<Compile Include="XamlIl\CompilerExtensions\Transformers\AvaloniaXamlIlConstructorServiceProviderTransformer.cs" />
<Compile Include="XamlIl\CompilerExtensions\Transformers\AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs" />
<Compile Include="XamlIl\CompilerExtensions\Transformers\AvaloniaXamlIlDataContextTypeTransformer.cs" />
@ -58,6 +65,7 @@
<Compile Include="XamlIl\CompilerExtensions\Transformers\IgnoredDirectivesTransformer.cs" />
<Compile Include="XamlIl\CompilerExtensions\Transformers\AvaloniaXamlIlSelectorTransformer.cs" />
<Compile Include="XamlIl\CompilerExtensions\Transformers\XNameTransformer.cs" />
<Compile Include="XamlIl\CompilerExtensions\XamlIlBindingPathHelper.cs" />
<Compile Include="XamlIl\CompilerExtensions\XamlIlClrPropertyInfoHelper.cs" />
<Compile Include="XamlIl\Runtime\IAvaloniaXamlIlParentStackProvider.cs" />
<Compile Include="XamlIl\Runtime\IAvaloniaXamlIlXmlNamespaceInfoProviderV1.cs" />
@ -67,6 +75,7 @@
<Compile Condition="$(UseCecil) == true" Include="XamlIl\xamlil.github\src\XamlIl.Cecil\**\*.cs" />
<Compile Remove="XamlIl\xamlil.github\**\obj\**\*.cs" />
<Compile Include="..\Avalonia.Markup\Markup\Parsers\SelectorGrammar.cs" />
<Compile Include="..\Avalonia.Markup\Markup\Parsers\BindingExpressionGrammar.cs" />
<Compile Include="XamlTypes.cs" />
</ItemGroup>
<ItemGroup>
@ -78,7 +87,9 @@
<ProjectReference Include="..\..\Avalonia.Layout\Avalonia.Layout.csproj" />
<ProjectReference Include="..\..\Avalonia.Visuals\Avalonia.Visuals.csproj" />
<ProjectReference Include="..\..\Avalonia.Styling\Avalonia.Styling.csproj" />
<ProjectReference Include="..\Avalonia.Markup\Avalonia.Markup.csproj" />
<ProjectReference Include="..\Avalonia.Markup\Avalonia.Markup.csproj">
<Aliases>global,Markup,%(Aliases)</Aliases>
</ProjectReference>
<PackageReference Include="System.Reflection.Emit" Version="4.3.0" />
<PackageReference Condition="$(UseCecil) == true" Include="Mono.Cecil" Version="0.10.3" />
</ItemGroup>

75
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<IControl>();
// 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<IRootObjectProvider>()?.RootObject as IStyle ??
provider.GetLastParent<IStyle>();
}
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; }
}
}

177
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<ICompiledBindingPathElement> _elements = new List<ICompiledBindingPathElement>();
public CompiledBindingPath() { }
internal CompiledBindingPath(IEnumerable<ICompiledBindingPathElement> bindingPath)
{
_elements = new List<ICompiledBindingPathElement>(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<ICompiledBindingPathElement> _elements = new List<ICompiledBindingPathElement>();
public CompiledBindingPathBuilder Not()
{
_elements.Add(new NotExpressionPathElement());
return this;
}
public CompiledBindingPathBuilder Property(INotifyingPropertyInfo info)
{
_elements.Add(new PropertyElement(info));
return this;
}
public CompiledBindingPathBuilder StreamTask<T>()
{
_elements.Add(new TaskStreamPathElement<T>());
return this;
}
public CompiledBindingPathBuilder StreamObservable<T>()
{
_elements.Add(new ObservableStreamPathElement<T>());
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<T> : IStronglyTypedStreamElement
{
public static readonly TaskStreamPathElement<T> Instance = new TaskStreamPathElement<T>();
public IStreamPlugin CreatePlugin() => new TaskStreamPlugin<T>();
}
internal class ObservableStreamPathElement<T> : IStronglyTypedStreamElement
{
public static readonly ObservableStreamPathElement<T> Instance = new ObservableStreamPathElement<T>();
public IStreamPlugin CreatePlugin() => new ObservableStreamPlugin<T>();
}
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; }
}
}

255
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<object, EventHandler> _changedHandlers = new ConditionalWeakTable<object, EventHandler>();
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<INotifyPropertyChanged, PropertyChangedEventArgs, INPCPropertyInfo>(
inpc,
nameof(INotifyPropertyChanged.PropertyChanged),
OnNotifyPropertyChanged);
}
}
protected override void UnsubscribeToChangesForTarget(object target)
{
if (target is INotifyPropertyChanged)
{
WeakEventHandlerManager.Unsubscribe<PropertyChangedEventArgs, INPCPropertyInfo>(
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<INotifyCollectionChanged, NotifyCollectionChangedEventArgs, IndexerInfo>(
incc,
nameof(INotifyCollectionChanged.CollectionChanged),
OnNotifyCollectionChanged);
}
}
protected override void UnsubscribeToChangesForTarget(object target)
{
base.UnsubscribeToChangesForTarget(target);
if (target is INotifyCollectionChanged)
{
WeakEventHandlerManager.Unsubscribe<NotifyCollectionChangedEventArgs, IndexerInfo>(
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;
}
}

28
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<T> : IStreamPlugin
{
public bool Match(WeakReference reference)
{
return reference is IObservable<T>;
}
public IObservable<object> Start(WeakReference reference)
{
var target = reference.Target as IObservable<T>;
if (target is IObservable<object> obj)
{
return obj;
}
return target.Select(x => (object)x);
}
}
}

112
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);
}
}
}
}
}

53
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<T> : IStreamPlugin
{
public bool Match(WeakReference reference)
{
return reference.Target is Task<T>;
}
public IObservable<object> Start(WeakReference reference)
{
if (!(reference.Target is Task<T> task))
{
return Observable.Empty<object>();
}
switch (task.Status)
{
case TaskStatus.RanToCompletion:
case TaskStatus.Faulted:
return HandleCompleted(task);
default:
var subject = new Subject<object>();
task.ContinueWith(
x => HandleCompleted(task).Subscribe(subject),
TaskScheduler.FromCurrentSynchronizationContext())
.ConfigureAwait(false);
return subject;
}
}
private static IObservable<object> HandleCompleted(Task<T> 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.");
}
}
}
}

14
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<XamlIlNewObjectTransformer>(
new AddNameScopeRegistration(),
new AvaloniaXamlIlDataContextTypeTransformer(),
new AvaloniaXamlIlBindingPathTransformer()
);
Transformers.Add(new AvaloniaXamlIlMetadataRemover());
}

78
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<XamlIlAstObjectNode>().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);
}
}
}

33
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<AvaloniaXamlIlDataContextTypeMetadataNode>().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;
}
}
}

46
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<AvaloniaXamlIlDataContextTypeMetadataNode>().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;

5
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<IXamlIlType> typeArguments)
{
throw new System.InvalidOperationException();
}
}
}
}

10
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");
}
}

459
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<XamlIlAstXamlPropertyValueNode>()
.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<BindingExpressionGrammar.INode> bindingExpression)
{
bool appendNotNode = false;
List<IXamlIlBindingPathElementNode> nodes = new List<IXamlIlBindingPathElementNode>();
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<IXamlIlType> parameters = property.IndexerParameters;
List<IXamlIlAstValueNode> values = new List<IXamlIlAstValueNode>();
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<XamlIlClrPropertyInfoEmitter>()
.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<IXamlIlAstValueNode> _values;
private readonly bool _isNotifyingCollection;
public XamlIlClrIndexerPathElementNode(IXamlIlProperty property, List<IXamlIlAstValueNode> 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<XamlIlClrPropertyInfoEmitter>()
.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<IXamlIlBindingPathElementNode> _elements;
public XamlIlBindingPathNode(IXamlIlLineInfo lineInfo,
IXamlIlType bindingPathType,
List<IXamlIlBindingPathElementNode> 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; }
}
}

11
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<IXamlIlAstValueNode> indexerArguments = null)
{
indexerArguments = indexerArguments ?? Enumerable.Empty<IXamlIlAstValueNode>();
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)

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

@ -17,14 +17,14 @@ namespace Avalonia.Data
/// <summary>
/// A XAML binding.
/// </summary>
public class Binding : IBinding
public class Binding : BindingBase
{
/// <summary>
/// Initializes a new instance of the <see cref="Binding"/> class.
/// </summary>
public Binding()
:base()
{
FallbackValue = AvaloniaProperty.UnsetValue;
}
/// <summary>
@ -33,47 +33,16 @@ namespace Avalonia.Data
/// <param name="path">The binding path.</param>
/// <param name="mode">The binding mode.</param>
public Binding(string path, BindingMode mode = BindingMode.Default)
: this()
: base(mode)
{
Path = path;
Mode = mode;
}
/// <summary>
/// Gets or sets the <see cref="IValueConverter"/> to use.
/// </summary>
public IValueConverter Converter { get; set; }
/// <summary>
/// Gets or sets a parameter to pass to <see cref="Converter"/>.
/// </summary>
public object ConverterParameter { get; set; }
/// <summary>
/// Gets or sets the name of the element to use as the binding source.
/// </summary>
public string ElementName { get; set; }
/// <summary>
/// Gets or sets the value to use when the binding is unable to produce a value.
/// </summary>
public object FallbackValue { get; set; }
/// <summary>
/// Gets or sets the binding mode.
/// </summary>
public BindingMode Mode { get; set; }
/// <summary>
/// Gets or sets the binding path.
/// </summary>
public string Path { get; set; } = "";
/// <summary>
/// Gets or sets the binding priority.
/// </summary>
public BindingPriority Priority { get; set; }
/// <summary>
/// Gets or sets the relative source for the binding.
/// </summary>
@ -85,64 +54,50 @@ namespace Avalonia.Data
public object Source { get; set; }
/// <summary>
/// Gets or sets the string format.
/// Gets or sets the binding path.
/// </summary>
public string StringFormat { get; set; }
public WeakReference DefaultAnchor { get; set; }
public string Path { get; set; } = "";
/// <summary>
/// Gets or sets a function used to resolve types from names in the binding path.
/// </summary>
public Func<string, string, Type> TypeResolver { get; set; }
/// <inheritdoc/>
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<ArgumentNullException>(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<ArgumentNullException>(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<ArgumentNullException>(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<ArgumentNullException>(target != null);
IObservable<object> 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<ArgumentNullException>(source != null);
return new ExpressionObserver(source, node);
}
private ExpressionObserver CreateTemplatedParentObserver(
IAvaloniaObject target,
ExpressionNode node)
{
Contract.Requires<ArgumentNullException>(target != null);
var result = new ExpressionObserver(
() => target.GetValue(StyledElement.TemplatedParentProperty),
node,
new UpdateSignal(target, StyledElement.TemplatedParentProperty),
null);
return result;
}
private IObservable<object> 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<Unit>
{
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);
}
}
}
}
}

275
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
{
/// <summary>
/// Initializes a new instance of the <see cref="Binding"/> class.
/// </summary>
public BindingBase()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Binding"/> class.
/// </summary>
/// <param name="mode">The binding mode.</param>
public BindingBase(BindingMode mode = BindingMode.Default)
{
FallbackValue = AvaloniaProperty.UnsetValue;
Mode = mode;
}
/// <summary>
/// Gets or sets the <see cref="IValueConverter"/> to use.
/// </summary>
public IValueConverter Converter { get; set; }
/// <summary>
/// Gets or sets a parameter to pass to <see cref="Converter"/>.
/// </summary>
public object ConverterParameter { get; set; }
/// <summary>
/// Gets or sets the value to use when the binding is unable to produce a value.
/// </summary>
public object FallbackValue { get; set; }
/// <summary>
/// Gets or sets the binding mode.
/// </summary>
public BindingMode Mode { get; set; }
/// <summary>
/// Gets or sets the binding priority.
/// </summary>
public BindingPriority Priority { get; set; }
/// <summary>
/// Gets or sets the string format.
/// </summary>
public string StringFormat { get; set; }
public WeakReference DefaultAnchor { get; set; }
protected abstract ExpressionObserver CreateExpressionObserver(
IAvaloniaObject target,
AvaloniaProperty targetProperty,
object anchor,
bool enableDataValidation);
/// <inheritdoc/>
public InstancedBinding Initiate(
IAvaloniaObject target,
AvaloniaProperty targetProperty,
object anchor = null,
bool enableDataValidation = false)
{
Contract.Requires<ArgumentNullException>(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<ArgumentNullException>(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<ArgumentNullException>(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<ArgumentNullException>(target != null);
IObservable<object> 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<ArgumentNullException>(source != null);
return new ExpressionObserver(source, node);
}
protected ExpressionObserver CreateTemplatedParentObserver(
IAvaloniaObject target,
ExpressionNode node)
{
Contract.Requires<ArgumentNullException>(target != null);
var result = new ExpressionObserver(
() => target.GetValue(StyledElement.TemplatedParentProperty),
node,
new UpdateSignal(target, StyledElement.TemplatedParentProperty),
null);
return result;
}
protected IObservable<object> 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<Unit>
{
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);
}
}
}
}
}

5
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<string> ParseArguments(this ref CharacterReader r, char open, char close, char delimiter = ',')
{

11
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<INode> 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;

2
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;

2
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;

2
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";
}

47
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 = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
x:DataContextType='{x:Type local:TestDataContext}'>
<TextBlock Text='{CompiledBinding StringProperty}' Name='textBlock' />
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var textBlock = window.FindControl<TextBlock>("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; }
}
}

15
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<ChildTraversalPropertyPathElement>(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<UserControl>(@"
<UserControl
xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' x:DataContextType='{x:Type local:XamlIlBugTestsDataContext}' />");
}
}
}

Loading…
Cancel
Save