30 changed files with 1743 additions and 327 deletions
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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."); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue