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