Browse Source

Merge branch 'master' into pr/1729-call-addowner-on-slider-orientation-instead-of-declaring-a-new-property

pull/1729/head
Steven Kirk 8 years ago
parent
commit
65f3a93b4d
  1. 2
      readme.md
  2. 1
      samples/ControlCatalog/Pages/TextBoxPage.xaml
  3. 65
      src/Avalonia.Animation/Animation.cs
  4. 66
      src/Avalonia.Animation/AnimatorKeyFrame.cs
  5. 188
      src/Avalonia.Animation/AnimatorStateMachine`1.cs
  6. 75
      src/Avalonia.Animation/Animator`1.cs
  7. 2
      src/Avalonia.Animation/Cue.cs
  8. 8
      src/Avalonia.Animation/DoubleAnimator.cs
  9. 2
      src/Avalonia.Animation/IAnimationSetter.cs
  10. 16
      src/Avalonia.Animation/KeyFrame.cs
  11. 8
      src/Avalonia.Animation/KeyFramePair`1.cs
  12. 51
      src/Avalonia.Base/AvaloniaObject.cs
  13. 52
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  14. 60
      src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs
  15. 2
      src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs
  16. 2
      src/Avalonia.Base/Data/Core/ExpressionNode.cs
  17. 30
      src/Avalonia.Base/Data/Core/ExpressionNodeBuilder.cs
  18. 130
      src/Avalonia.Base/Data/Core/ExpressionObserver.cs
  19. 4
      src/Avalonia.Base/Data/Core/ExpressionParseException.cs
  20. 71
      src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs
  21. 92
      src/Avalonia.Base/Data/Core/IndexerNodeBase.cs
  22. 2
      src/Avalonia.Base/Data/Core/LogicalNotNode.cs
  23. 199
      src/Avalonia.Base/Data/Core/Parsers/ExpressionParser.cs
  24. 27
      src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs
  25. 219
      src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs
  26. 30
      src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs
  27. 2
      src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs
  28. 2
      src/Avalonia.Base/Data/Core/SettableNode.cs
  29. 27
      src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs
  30. 2
      src/Avalonia.Base/Data/Core/StreamNode.cs
  31. 25
      src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs
  32. 3
      src/Avalonia.Base/IPriorityValueOwner.cs
  33. 11
      src/Avalonia.Base/PriorityValue.cs
  34. 6
      src/Avalonia.Base/Utilities/CharacterReader.cs
  35. 53
      src/Avalonia.Base/Utilities/DeferredSetter.cs
  36. 6
      src/Avalonia.Base/Utilities/IdentifierParser.cs
  37. 58
      src/Avalonia.Base/Utilities/SingleOrQueue.cs
  38. 24
      src/Avalonia.Base/ValueStore.cs
  39. 20
      src/Avalonia.Controls/Grid.cs
  40. 6
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  41. 7
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  42. 90
      src/Avalonia.Controls/ProgressBar.cs
  43. 19
      src/Avalonia.Controls/StackPanel.cs
  44. 16
      src/Avalonia.Controls/TextBox.cs
  45. 1
      src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs
  46. 17
      src/Avalonia.Styling/Styling/Setter.cs
  47. 5
      src/Avalonia.Themes.Default/MenuItem.xaml
  48. 45
      src/Avalonia.Themes.Default/ProgressBar.xaml
  49. 1
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  50. 69
      src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs
  51. 2
      src/Markup/Avalonia.Markup.Xaml/Converters/SelectorTypeConverter.cs
  52. 161
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  53. 85
      src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs
  54. 4
      src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaRuntimeTypeProvider.cs
  55. 3
      src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs
  56. 3
      src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
  57. 91
      src/Markup/Avalonia.Markup/Data/Binding.cs
  58. 6
      src/Markup/Avalonia.Markup/Data/TemplateBinding.cs
  59. 14
      src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs
  60. 76
      src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs
  61. 330
      src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs
  62. 39
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs
  63. 54
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs
  64. 12
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs
  65. 88
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs
  66. 4
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
  67. 4
      src/OSX/Avalonia.MonoMac/SystemDialogsImpl.cs
  68. 10
      src/OSX/Avalonia.MonoMac/WindowImpl.cs
  69. 4
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  70. 2
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  71. 18
      src/Windows/Avalonia.Win32/SystemDialogImpl.cs
  72. 11
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs
  73. 43
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs
  74. 30
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AttachedProperty.cs
  75. 11
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AvaloniaProperty.cs
  76. 32
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs
  77. 224
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs
  78. 89
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs
  79. 17
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs
  80. 97
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Negation.cs
  81. 24
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs
  82. 120
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs
  83. 29
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_SetValue.cs
  84. 15
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Task.cs
  85. 52
      tests/Avalonia.Base.UnitTests/PriorityValueTests.cs
  86. 50
      tests/Avalonia.Base.UnitTests/Utilities/SingleOrQueueTests.cs
  87. 28
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  88. 39
      tests/Avalonia.Controls.UnitTests/StackPanelTests.cs
  89. 2
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  90. 8
      tests/Avalonia.LeakTests/ExpressionObserverTests.cs
  91. 124
      tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs
  92. 50
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs
  93. 23
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests_Errors.cs
  94. 165
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs
  95. 59
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AvaloniaProperty.cs
  96. 371
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs
  97. 13
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs
  98. 112
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs
  99. 42
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs
  100. 2
      tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs

2
readme.md

@ -20,6 +20,8 @@ Avalonia is a WPF-inspired cross-platform XAML-based UI framework providing a fl
Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started. After installing it, open "New Project" dialog in Visual Studio, choose "Avalonia" in "Visual C#" section, select "Avalonia .NET Core Application" and press OK (<a href="http://avaloniaui.net/docs/quickstart/images/new-project-dialog.png">screenshot</a>). Now you can write code and markup that will work on multiple platforms!
For those without Visual Studio, starter guide for .NET Core CLI can be found [here](http://avaloniaui.net/docs/quickstart/create-new-project#net-core).
Avalonia is delivered via <b>NuGet</b> package manager. You can find the packages here: ([stable(ish)](https://www.nuget.org/packages/Avalonia/), [nightly](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed))
Use these commands in Package Manager console to install Avalonia manually:

1
samples/ControlCatalog/Pages/TextBoxPage.xaml

@ -9,6 +9,7 @@
Gap="16">
<StackPanel Orientation="Vertical" Gap="8">
<TextBox Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit." Width="200" />
<TextBox Watermark="ReadOnly" IsReadOnly="True" Text="This is read only"/>
<TextBox Width="200" Watermark="Watermark" />
<TextBox Width="200"
Watermark="Floating Watermark"

65
src/Avalonia.Animation/Animation.cs

@ -68,7 +68,7 @@ namespace Avalonia.Animation
/// <summary>
/// The value fill mode for this animation.
/// </summary>
public FillMode FillMode { get; set; }
public FillMode FillMode { get; set; }
/// <summary>
/// Easing function to be used.
@ -80,10 +80,10 @@ namespace Avalonia.Animation
this.CollectionChanged += delegate { _isChildrenChanged = true; };
}
private void InterpretKeyframes()
private IList<IAnimator> InterpretKeyframes(Animatable control)
{
var handlerList = new List<(Type, AvaloniaProperty)>();
var kfList = new List<AnimatorKeyFrame>();
var handlerList = new List<(Type type, AvaloniaProperty property)>();
var animatorKeyFrames = new List<AnimatorKeyFrame>();
foreach (var keyframe in this)
{
@ -99,41 +99,38 @@ namespace Avalonia.Animation
if (!handlerList.Contains((handler, setter.Property)))
handlerList.Add((handler, setter.Property));
var newKF = new AnimatorKeyFrame()
var cue = keyframe.Cue;
if (keyframe.TimingMode == KeyFrameTimingMode.TimeSpan)
{
Handler = handler,
Property = setter.Property,
Cue = keyframe.Cue,
KeyTime = keyframe.KeyTime,
timeSpanSet = keyframe.timeSpanSet,
cueSet = keyframe.cueSet,
Value = setter.Value
};
kfList.Add(newKF);
cue = new Cue(keyframe.KeyTime.Ticks / Duration.Ticks);
}
var newKF = new AnimatorKeyFrame(handler, cue);
_subscription.Add(newKF.BindSetter(setter, control));
animatorKeyFrames.Add(newKF);
}
}
var newAnimatorInstances = new List<(Type handler, AvaloniaProperty prop, IAnimator inst)>();
var newAnimatorInstances = new List<IAnimator>();
foreach (var handler in handlerList)
foreach (var (handlerType, property) in handlerList)
{
var newInstance = (IAnimator)Activator.CreateInstance(handler.Item1);
newInstance.Property = handler.Item2;
newAnimatorInstances.Add((handler.Item1, handler.Item2, newInstance));
var newInstance = (IAnimator)Activator.CreateInstance(handlerType);
newInstance.Property = property;
newAnimatorInstances.Add(newInstance);
}
foreach (var kf in kfList)
foreach (var keyframe in animatorKeyFrames)
{
var parent = newAnimatorInstances.Where(p => p.handler == kf.Handler &&
p.prop == kf.Property)
.First();
parent.inst.Add(kf);
var animator = newAnimatorInstances.First(a => a.GetType() == keyframe.AnimatorType &&
a.Property == keyframe.Property);
animator.Add(keyframe);
}
foreach(var instance in newAnimatorInstances)
_animators.Add(instance.inst);
return newAnimatorInstances;
}
/// <summary>
@ -150,17 +147,11 @@ namespace Avalonia.Animation
/// <inheritdocs/>
public IDisposable Apply(Animatable control, IObservable<bool> matchObs)
{
if (_isChildrenChanged)
{
InterpretKeyframes();
_isChildrenChanged = false;
}
foreach (IAnimator keyframes in _animators)
foreach (IAnimator animator in InterpretKeyframes(control))
{
_subscription.Add(keyframes.Apply(this, control, matchObs));
_subscription.Add(animator.Apply(this, control, matchObs));
}
return this;
}
}
}
}

66
src/Avalonia.Animation/AnimatorKeyFrame.cs

@ -4,6 +4,8 @@ using System.Text;
using System.ComponentModel;
using Avalonia.Metadata;
using Avalonia.Collections;
using Avalonia.Data;
using Avalonia.Reactive;
namespace Avalonia.Animation
{
@ -11,13 +13,63 @@ namespace Avalonia.Animation
/// Defines a KeyFrame that is used for
/// <see cref="Animator{T}"/> objects.
/// </summary>
public class AnimatorKeyFrame
public class AnimatorKeyFrame : AvaloniaObject
{
public Type Handler;
public Cue Cue;
public TimeSpan KeyTime;
internal bool timeSpanSet, cueSet;
public AvaloniaProperty Property;
public object Value;
public static readonly DirectProperty<AnimatorKeyFrame, object> ValueProperty =
AvaloniaProperty.RegisterDirect<AnimatorKeyFrame, object>(nameof(Value), k => k._value, (k, v) => k._value = v);
public AnimatorKeyFrame()
{
}
public AnimatorKeyFrame(Type animatorType, Cue cue)
{
AnimatorType = animatorType;
Cue = cue;
}
public Type AnimatorType { get; }
public Cue Cue { get; }
public AvaloniaProperty Property { get; private set; }
private object _value;
public object Value
{
get => _value;
set => SetAndRaise(ValueProperty, ref _value, value);
}
public IDisposable BindSetter(IAnimationSetter setter, Animatable targetControl)
{
Property = setter.Property;
var value = setter.Value;
if (value is IBinding binding)
{
return this.Bind(ValueProperty, binding, targetControl);
}
else
{
return this.Bind(ValueProperty, ObservableEx.SingleValue(value).ToBinding(), targetControl);
}
}
public T GetTypedValue<T>()
{
var typeConv = TypeDescriptor.GetConverter(typeof(T));
if (Value == null)
{
throw new ArgumentNullException($"KeyFrame value can't be null.");
}
if (!typeConv.CanConvertTo(Value.GetType()))
{
throw new InvalidCastException($"KeyFrame value doesnt match property type.");
}
return (T)typeConv.ConvertTo(Value, typeof(T));
}
}
}

188
src/Avalonia.Animation/AnimatorStateMachine`1.cs

@ -51,9 +51,9 @@ namespace Avalonia.Animation
Disposed
}
public void Initialize(Animation animation, Animatable control, Animator<T> keyframes)
public void Initialize(Animation animation, Animatable control, Animator<T> animator)
{
_parent = keyframes;
_parent = animator;
_targetAnimation = animation;
_targetControl = control;
_neutralValue = (T)_targetControl.GetValue(_parent.Property);
@ -123,121 +123,133 @@ namespace Avalonia.Animation
double _tempDuration = 0d, _easedTime;
checkstate:
switch (_currentState)
bool handled = false;
while (!handled)
{
case KeyFramesStates.DoDelay:
switch (_currentState)
{
case KeyFramesStates.DoDelay:
if (_fillMode == FillMode.Backward
|| _fillMode == FillMode.Both)
{
if (_currentIteration == 0)
if (_fillMode == FillMode.Backward
|| _fillMode == FillMode.Both)
{
_targetObserver.OnNext(_firstKFValue);
if (_currentIteration == 0)
{
_targetObserver.OnNext(_firstKFValue);
}
else
{
_targetObserver.OnNext(_lastInterpValue);
}
}
if (_delayFrameCount > _delayTotalFrameCount)
{
_currentState = KeyFramesStates.DoRun;
}
else
{
_targetObserver.OnNext(_lastInterpValue);
handled = true;
_delayFrameCount++;
}
}
if (_delayFrameCount > _delayTotalFrameCount)
{
_currentState = KeyFramesStates.DoRun;
goto checkstate;
}
_delayFrameCount++;
break;
case KeyFramesStates.DoRun:
if (_isReversed)
_currentState = KeyFramesStates.RunBackwards;
else
_currentState = KeyFramesStates.RunForwards;
goto checkstate;
case KeyFramesStates.RunForwards:
if (_durationFrameCount > _durationTotalFrameCount)
{
_currentState = KeyFramesStates.RunComplete;
goto checkstate;
}
break;
_tempDuration = (double)_durationFrameCount / _durationTotalFrameCount;
_currentState = KeyFramesStates.RunApplyValue;
case KeyFramesStates.DoRun:
goto checkstate;
if (_isReversed)
_currentState = KeyFramesStates.RunBackwards;
else
_currentState = KeyFramesStates.RunForwards;
case KeyFramesStates.RunBackwards:
break;
if (_durationFrameCount > _durationTotalFrameCount)
{
_currentState = KeyFramesStates.RunComplete;
goto checkstate;
}
case KeyFramesStates.RunForwards:
_tempDuration = (double)(_durationTotalFrameCount - _durationFrameCount) / _durationTotalFrameCount;
_currentState = KeyFramesStates.RunApplyValue;
if (_durationFrameCount > _durationTotalFrameCount)
{
_currentState = KeyFramesStates.RunComplete;
}
else
{
_tempDuration = (double)_durationFrameCount / _durationTotalFrameCount;
_currentState = KeyFramesStates.RunApplyValue;
goto checkstate;
}
break;
case KeyFramesStates.RunApplyValue:
case KeyFramesStates.RunBackwards:
_easedTime = _targetAnimation.Easing.Ease(_tempDuration);
if (_durationFrameCount > _durationTotalFrameCount)
{
_currentState = KeyFramesStates.RunComplete;
}
else
{
_tempDuration = (double)(_durationTotalFrameCount - _durationFrameCount) / _durationTotalFrameCount;
_currentState = KeyFramesStates.RunApplyValue;
}
break;
_durationFrameCount++;
_lastInterpValue = Interpolator(_easedTime, _neutralValue);
_targetObserver.OnNext(_lastInterpValue);
_currentState = KeyFramesStates.DoRun;
case KeyFramesStates.RunApplyValue:
break;
_easedTime = _targetAnimation.Easing.Ease(_tempDuration);
case KeyFramesStates.RunComplete:
_durationFrameCount++;
_lastInterpValue = Interpolator(_easedTime, _neutralValue);
_targetObserver.OnNext(_lastInterpValue);
_currentState = KeyFramesStates.DoRun;
handled = true;
break;
if (_checkLoopAndRepeat)
{
_delayFrameCount = 0;
_durationFrameCount = 0;
case KeyFramesStates.RunComplete:
if (_isLooping)
{
_currentState = KeyFramesStates.DoRun;
}
else if (_isRepeating)
if (_checkLoopAndRepeat)
{
if (_currentIteration >= _repeatCount)
_delayFrameCount = 0;
_durationFrameCount = 0;
if (_isLooping)
{
_currentState = KeyFramesStates.Stop;
_currentState = KeyFramesStates.DoRun;
}
else
else if (_isRepeating)
{
_currentState = KeyFramesStates.DoRun;
if (_currentIteration >= _repeatCount)
{
_currentState = KeyFramesStates.Stop;
}
else
{
_currentState = KeyFramesStates.DoRun;
}
_currentIteration++;
}
_currentIteration++;
}
if (_animationDirection == PlaybackDirection.Alternate
|| _animationDirection == PlaybackDirection.AlternateReverse)
_isReversed = !_isReversed;
if (_animationDirection == PlaybackDirection.Alternate
|| _animationDirection == PlaybackDirection.AlternateReverse)
_isReversed = !_isReversed;
break;
}
break;
}
_currentState = KeyFramesStates.Stop;
goto checkstate;
_currentState = KeyFramesStates.Stop;
break;
case KeyFramesStates.Stop:
case KeyFramesStates.Stop:
if (_fillMode == FillMode.Forward
|| _fillMode == FillMode.Both)
{
_targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue);
}
_targetObserver.OnCompleted();
break;
if (_fillMode == FillMode.Forward
|| _fillMode == FillMode.Both)
{
_targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue);
}
_targetObserver.OnCompleted();
handled = true;
break;
default:
handled = true;
break;
}
}
}
@ -253,4 +265,4 @@ namespace Avalonia.Animation
_currentState = KeyFramesStates.Disposed;
}
}
}
}

75
src/Avalonia.Animation/Animator`1.cs

@ -19,7 +19,7 @@ namespace Avalonia.Animation
/// <summary>
/// List of type-converted keyframes.
/// </summary>
private Dictionary<double, (T, bool isNeutral)> _convertedKeyframes = new Dictionary<double, (T, bool)>();
private readonly SortedList<double, (AnimatorKeyFrame, bool isNeutral)> _convertedKeyframes = new SortedList<double, (AnimatorKeyFrame, bool)>();
private bool _isVerfifiedAndConverted;
@ -38,12 +38,11 @@ namespace Avalonia.Animation
public virtual IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch)
{
if (!_isVerfifiedAndConverted)
VerifyConvertKeyFrames(animation, typeof(T));
VerifyConvertKeyFrames();
return obsMatch
.Where(p => p == true)
// Ignore triggers when global timers are paused.
.Where(p => Timing.GetGlobalPlayState() != PlayState.Pause)
.Where(p => p && Timing.GetGlobalPlayState() != PlayState.Pause)
.Subscribe(_ =>
{
var timerObs = RunKeyFrames(animation, control);
@ -60,8 +59,8 @@ namespace Avalonia.Animation
/// <param name="t">The time parameter, relative to the total animation time</param>
protected (double IntraKFTime, KeyFramePair<T> KFPair) GetKFPairAndIntraKFTime(double t)
{
KeyValuePair<double, (T, bool)> firstCue, lastCue;
int kvCount = _convertedKeyframes.Count();
KeyValuePair<double, (AnimatorKeyFrame frame, bool isNeutral)> firstCue, lastCue;
int kvCount = _convertedKeyframes.Count;
if (kvCount > 2)
{
if (DoubleUtils.AboutEqual(t, 0.0) || t < 0.0)
@ -76,8 +75,8 @@ namespace Avalonia.Animation
}
else
{
firstCue = _convertedKeyframes.Where(j => j.Key <= t).Last();
lastCue = _convertedKeyframes.Where(j => j.Key >= t).First();
firstCue = _convertedKeyframes.Last(j => j.Key <= t);
lastCue = _convertedKeyframes.First(j => j.Key >= t);
}
}
else
@ -89,7 +88,9 @@ namespace Avalonia.Animation
double t0 = firstCue.Key;
double t1 = lastCue.Key;
var intraframeTime = (t - t0) / (t1 - t0);
return (intraframeTime, new KeyFramePair<T>(firstCue, lastCue));
var firstFrameData = (firstCue.Value.frame.GetTypedValue<T>(), firstCue.Value.isNeutral);
var lastFrameData = (lastCue.Value.frame.GetTypedValue<T>(), lastCue.Value.isNeutral);
return (intraframeTime, new KeyFramePair<T>(firstFrameData, lastFrameData));
}
@ -98,17 +99,14 @@ namespace Avalonia.Animation
/// </summary>
private IDisposable RunKeyFrames(Animation animation, Animatable control)
{
var _kfStateMach = new AnimatorStateMachine<T>();
_kfStateMach.Initialize(animation, control, this);
var stateMachine = new AnimatorStateMachine<T>();
stateMachine.Initialize(animation, control, this);
Timing.AnimationStateTimer
.TakeWhile(_ => !_kfStateMach._unsubscribe)
.Subscribe(p =>
{
_kfStateMach.Step(p, DoInterpolation);
});
.TakeWhile(_ => !stateMachine._unsubscribe)
.Subscribe(p => stateMachine.Step(p, DoInterpolation));
return control.Bind(Property, _kfStateMach, BindingPriority.Animation);
return control.Bind(Property, stateMachine, BindingPriority.Animation);
}
/// <summary>
@ -119,39 +117,19 @@ namespace Avalonia.Animation
/// <summary>
/// Verifies and converts keyframe values according to this class's target type.
/// </summary>
private void VerifyConvertKeyFrames(Animation animation, Type type)
private void VerifyConvertKeyFrames()
{
var typeConv = TypeDescriptor.GetConverter(type);
foreach (AnimatorKeyFrame k in this)
foreach (AnimatorKeyFrame keyframe in this)
{
if (k.Value == null)
{
throw new ArgumentNullException($"KeyFrame value can't be null.");
}
if (!typeConv.CanConvertTo(k.Value.GetType()))
{
throw new InvalidCastException($"KeyFrame value doesnt match property type.");
}
T convertedValue = (T)typeConv.ConvertTo(k.Value, type);
Cue _normalizedCue = k.Cue;
if (k.timeSpanSet)
{
_normalizedCue = new Cue(k.KeyTime.Ticks / animation.Duration.Ticks);
}
_convertedKeyframes.Add(_normalizedCue.CueValue, (convertedValue, false));
_convertedKeyframes.Add(keyframe.Cue.CueValue, (keyframe, false));
}
SortKeyFrameCues(_convertedKeyframes);
AddNeutralKeyFramesIfNeeded();
_isVerfifiedAndConverted = true;
}
private void SortKeyFrameCues(Dictionary<double, (T, bool)> convertedValues)
private void AddNeutralKeyFramesIfNeeded()
{
bool hasStartKey, hasEndKey;
hasStartKey = hasEndKey = false;
@ -170,23 +148,20 @@ namespace Avalonia.Animation
}
if (!hasStartKey || !hasEndKey)
AddNeutralKeyFrames(hasStartKey, hasEndKey, _convertedKeyframes);
_convertedKeyframes = _convertedKeyframes.OrderBy(p => p.Key)
.ToDictionary((k) => k.Key, (v) => v.Value);
AddNeutralKeyFrames(hasStartKey, hasEndKey);
}
private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey, Dictionary<double, (T, bool)> convertedKeyframes)
private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey)
{
if (!hasStartKey)
{
convertedKeyframes.Add(0.0d, (default(T), true));
_convertedKeyframes.Add(0.0d, (new AnimatorKeyFrame { Value = default(T) }, true));
}
if (!hasEndKey)
{
convertedKeyframes.Add(1.0d, (default(T), true));
_convertedKeyframes.Add(1.0d, (new AnimatorKeyFrame { Value = default(T) }, true));
}
}
}
}
}

2
src/Avalonia.Animation/Cue.cs

@ -10,7 +10,7 @@ namespace Avalonia.Animation
/// A Cue object for <see cref="KeyFrame"/>.
/// </summary>
[TypeConverter(typeof(CueTypeConverter))]
public struct Cue : IEquatable<Cue>, IEquatable<double>
public readonly struct Cue : IEquatable<Cue>, IEquatable<double>
{
/// <summary>
/// The normalized percent value, ranging from 0.0 to 1.0

8
src/Avalonia.Animation/DoubleAnimator.cs

@ -24,15 +24,15 @@ namespace Avalonia.Animation
var firstKF = pair.KFPair.FirstKeyFrame;
var secondKF = pair.KFPair.SecondKeyFrame;
if (firstKF.Value.isNeutral)
if (firstKF.isNeutral)
y0 = neutralValue;
else
y0 = firstKF.Value.TargetValue;
y0 = firstKF.TargetValue;
if (secondKF.Value.isNeutral)
if (secondKF.isNeutral)
y1 = neutralValue;
else
y1 = secondKF.Value.TargetValue;
y1 = secondKF.TargetValue;
// Do linear parametric interpolation
return y0 + (pair.IntraKFTime) * (y1 - y0);

2
src/Avalonia.Animation/IAnimationSetter.cs

@ -5,4 +5,4 @@ namespace Avalonia.Animation
AvaloniaProperty Property { get; set; }
object Value { get; set; }
}
}
}

16
src/Avalonia.Animation/KeyFrame.cs

@ -7,6 +7,11 @@ using Avalonia.Collections;
namespace Avalonia.Animation
{
internal enum KeyFrameTimingMode
{
TimeSpan = 1,
Cue
}
/// <summary>
/// Stores data regarding a specific key
@ -14,7 +19,6 @@ namespace Avalonia.Animation
/// </summary>
public class KeyFrame : AvaloniaList<IAnimationSetter>
{
internal bool timeSpanSet, cueSet;
private TimeSpan _ktimeSpan;
private Cue _kCue;
@ -30,6 +34,8 @@ namespace Avalonia.Animation
{
}
internal KeyFrameTimingMode TimingMode { get; private set; }
/// <summary>
/// Gets or sets the key time of this <see cref="KeyFrame"/>.
/// </summary>
@ -42,11 +48,11 @@ namespace Avalonia.Animation
}
set
{
if (cueSet)
if (TimingMode == KeyFrameTimingMode.Cue)
{
throw new InvalidOperationException($"You can only set either {nameof(KeyTime)} or {nameof(Cue)}.");
}
timeSpanSet = true;
TimingMode = KeyFrameTimingMode.TimeSpan;
_ktimeSpan = value;
}
}
@ -63,11 +69,11 @@ namespace Avalonia.Animation
}
set
{
if (timeSpanSet)
if (TimingMode == KeyFrameTimingMode.TimeSpan)
{
throw new InvalidOperationException($"You can only set either {nameof(KeyTime)} or {nameof(Cue)}.");
}
cueSet = true;
TimingMode = KeyFrameTimingMode.Cue;
_kCue = value;
}
}

8
src/Avalonia.Animation/KeyFramePair`1.cs

@ -22,7 +22,7 @@ namespace Avalonia.Animation
/// </summary>
/// <param name="FirstKeyFrame"></param>
/// <param name="LastKeyFrame"></param>
public KeyFramePair(KeyValuePair<double, (T, bool)> FirstKeyFrame, KeyValuePair<double, (T, bool)> LastKeyFrame) : this()
public KeyFramePair((T TargetValue, bool isNeutral) FirstKeyFrame, (T TargetValue, bool isNeutral) LastKeyFrame) : this()
{
this.FirstKeyFrame = FirstKeyFrame;
this.SecondKeyFrame = LastKeyFrame;
@ -31,11 +31,11 @@ namespace Avalonia.Animation
/// <summary>
/// First <see cref="KeyFrame"/> object.
/// </summary>
public KeyValuePair<double, (T TargetValue, bool isNeutral)> FirstKeyFrame { get; private set; }
public (T TargetValue, bool isNeutral) FirstKeyFrame { get; }
/// <summary>
/// Second <see cref="KeyFrame"/> object.
/// </summary>
public KeyValuePair<double, (T TargetValue, bool isNeutral)> SecondKeyFrame { get; private set; }
public (T TargetValue, bool isNeutral) SecondKeyFrame { get; }
}
}
}

51
src/Avalonia.Base/AvaloniaObject.cs

@ -22,7 +22,7 @@ namespace Avalonia
/// <remarks>
/// This class is analogous to DependencyObject in WPF.
/// </remarks>
public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged, IPriorityValueOwner
public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged
{
/// <summary>
/// The parent object that inherited values are inherited from.
@ -45,21 +45,8 @@ namespace Avalonia
/// </summary>
private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged;
private DeferredSetter<AvaloniaProperty, object> _directDeferredSetter;
private ValueStore _values;
/// <summary>
/// Delayed setter helper for direct properties. Used to fix #855.
/// </summary>
private DeferredSetter<AvaloniaProperty, object> DirectPropertyDeferredSetter
{
get
{
return _directDeferredSetter ??
(_directDeferredSetter = new DeferredSetter<AvaloniaProperty, object>());
}
}
private ValueStore Values => _values ?? (_values = new ValueStore(this));
/// <summary>
/// Initializes a new instance of the <see cref="AvaloniaObject"/> class.
@ -225,7 +212,7 @@ namespace Avalonia
}
else if (_values != null)
{
var result = _values.GetValue(property);
var result = Values.GetValue(property);
if (result == AvaloniaProperty.UnsetValue)
{
@ -376,12 +363,7 @@ namespace Avalonia
description,
priority);
if (_values == null)
{
_values = new ValueStore(this);
}
return _values.AddBinding(property, source, priority);
return Values.AddBinding(property, source, priority);
}
}
@ -414,9 +396,8 @@ namespace Avalonia
VerifyAccess();
_values?.Revalidate(property);
}
/// <inheritdoc/>
void IPriorityValueOwner.Changed(AvaloniaProperty property, int priority, object oldValue, object newValue)
internal void PriorityValueChanged(AvaloniaProperty property, int priority, object oldValue, object newValue)
{
oldValue = (oldValue == AvaloniaProperty.UnsetValue) ?
GetDefaultValue(property) :
@ -439,9 +420,8 @@ namespace Avalonia
(BindingPriority)priority);
}
}
/// <inheritdoc/>
void IPriorityValueOwner.BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
internal void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
{
UpdateDataValidation(property, notification);
}
@ -456,7 +436,7 @@ namespace Avalonia
/// Gets all priority values set on the object.
/// </summary>
/// <returns>A collection of property/value tuples.</returns>
internal IDictionary<AvaloniaProperty, PriorityValue> GetSetValues() => _values?.GetSetValues();
internal IDictionary<AvaloniaProperty, object> GetSetValues() => Values?.GetSetValues();
/// <summary>
/// Forces revalidation of properties when a property value changes.
@ -566,12 +546,12 @@ namespace Avalonia
T value)
{
Contract.Requires<ArgumentNullException>(setterCallback != null);
return DirectPropertyDeferredSetter.SetAndNotify(
return Values.Setter.SetAndNotify(
property,
ref field,
(object val, ref T backing, Action<Action> notify) =>
(object update, ref T backing, Action<Action> notify) =>
{
setterCallback((T)val, ref backing, notify);
setterCallback((T)update, ref backing, notify);
return true;
},
value);
@ -737,13 +717,8 @@ namespace Avalonia
originalValue?.GetType().FullName ?? "(null)"));
}
if (_values == null)
{
_values = new ValueStore(this);
}
LogPropertySet(property, value, priority);
_values.AddValue(property, value, (int)priority);
Values.AddValue(property, value, (int)priority);
}
/// <summary>

52
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@ -106,7 +106,7 @@ namespace Avalonia
}
/// <summary>
/// Finds a registered non-attached property on a type by name.
/// Finds a registered property on a type by name.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="name">The property name.</param>
@ -130,7 +130,7 @@ namespace Avalonia
}
/// <summary>
/// Finds a registered non-attached property on a type by name.
/// Finds a registered property on an object by name.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="name">The property name.</param>
@ -148,52 +148,6 @@ namespace Avalonia
return FindRegistered(o.GetType(), name);
}
/// <summary>
/// Finds a registered attached property on a type by name.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="ownerType">The owner type.</param>
/// <param name="name">The property name.</param>
/// <returns>
/// The registered property or null if no matching property found.
/// </returns>
/// <exception cref="InvalidOperationException">
/// The property name contains a '.'.
/// </exception>
public AvaloniaProperty FindRegisteredAttached(Type type, Type ownerType, string name)
{
Contract.Requires<ArgumentNullException>(type != null);
Contract.Requires<ArgumentNullException>(ownerType != null);
Contract.Requires<ArgumentNullException>(name != null);
if (name.Contains('.'))
{
throw new InvalidOperationException("Attached properties not supported.");
}
return GetRegisteredAttached(type).FirstOrDefault(x => x.Name == name);
}
/// <summary>
/// Finds a registered non-attached property on a type by name.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="ownerType">The owner type.</param>
/// <param name="name">The property name.</param>
/// <returns>
/// The registered property or null if no matching property found.
/// </returns>
/// <exception cref="InvalidOperationException">
/// The property name contains a '.'.
/// </exception>
public AvaloniaProperty FindRegisteredAttached(AvaloniaObject o, Type ownerType, string name)
{
Contract.Requires<ArgumentNullException>(o != null);
Contract.Requires<ArgumentNullException>(name != null);
return FindRegisteredAttached(o.GetType(), ownerType, name);
}
/// <summary>
/// Checks whether a <see cref="AvaloniaProperty"/> is registered on a type.
/// </summary>
@ -287,4 +241,4 @@ namespace Avalonia
_attachedCache.Clear();
}
}
}
}

60
src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs

@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Text;
using Avalonia.Reactive;
namespace Avalonia.Data.Core
{
public class AvaloniaPropertyAccessorNode : SettableNode
{
private IDisposable _subscription;
private readonly bool _enableValidation;
private readonly AvaloniaProperty _property;
public AvaloniaPropertyAccessorNode(AvaloniaProperty property, bool enableValidation)
{
_property = property;
_enableValidation = enableValidation;
}
public override string Description => PropertyName;
public string PropertyName { get; }
public override Type PropertyType => _property.PropertyType;
protected override bool SetTargetValueCore(object value, BindingPriority priority)
{
try
{
if (Target.IsAlive && Target.Target is IAvaloniaObject obj)
{
obj.SetValue(_property, value, priority);
return true;
}
return false;
}
catch
{
return false;
}
}
protected override void StartListeningCore(WeakReference reference)
{
if (reference.Target is IAvaloniaObject obj)
{
_subscription = new AvaloniaPropertyObservable<object>(obj, _property).Subscribe(ValueChanged);
}
else
{
_subscription = null;
}
}
protected override void StopListeningCore()
{
_subscription?.Dispose();
_subscription = null;
}
}
}

2
src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs

@ -6,7 +6,7 @@ using System.Reactive.Linq;
namespace Avalonia.Data.Core
{
internal class EmptyExpressionNode : ExpressionNode
public class EmptyExpressionNode : ExpressionNode
{
public override string Description => ".";
}

2
src/Avalonia.Base/Data/Core/ExpressionNode.cs

@ -5,7 +5,7 @@ using System;
namespace Avalonia.Data.Core
{
internal abstract class ExpressionNode
public abstract class ExpressionNode
{
private static readonly object CacheInvalid = new object();
protected static readonly WeakReference UnsetReference =

30
src/Avalonia.Base/Data/Core/ExpressionNodeBuilder.cs

@ -1,30 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Data.Core.Parsers;
namespace Avalonia.Data.Core
{
internal static class ExpressionNodeBuilder
{
public static ExpressionNode Build(string expression, bool enableValidation = false)
{
if (string.IsNullOrWhiteSpace(expression))
{
throw new ArgumentException("'expression' may not be empty.");
}
var reader = new Reader(expression);
var parser = new ExpressionParser(enableValidation);
var node = parser.Parse(reader);
if (!reader.End)
{
throw new ExpressionParseException(reader.Position, "Expected end of expression.");
}
return node;
}
}
}

130
src/Avalonia.Base/Data/Core/ExpressionObserver.cs

@ -3,9 +3,11 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reactive;
using System.Reactive.Linq;
using Avalonia.Data;
using Avalonia.Data.Core.Parsers;
using Avalonia.Data.Core.Plugins;
using Avalonia.Reactive;
@ -61,27 +63,22 @@ namespace Avalonia.Data.Core
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="root">The root object.</param>
/// <param name="expression">The expression.</param>
/// <param name="enableDataValidation">Whether data validation should be enabled.</param>
/// <param name="node">The expression.</param>
/// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/> will be used.
/// A description of the expression.
/// </param>
public ExpressionObserver(
object root,
string expression,
bool enableDataValidation = false,
ExpressionNode node,
string description = null)
{
Contract.Requires<ArgumentNullException>(expression != null);
if (root == AvaloniaProperty.UnsetValue)
{
root = null;
}
Expression = expression;
Description = description ?? expression;
_node = Parse(expression, enableDataValidation);
_node = node;
Description = description;
_root = new WeakReference(root);
}
@ -89,23 +86,19 @@ namespace Avalonia.Data.Core
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="rootObservable">An observable which provides the root object.</param>
/// <param name="expression">The expression.</param>
/// <param name="enableDataValidation">Whether data validation should be enabled.</param>
/// <param name="node">The expression.</param>
/// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/> will be used.
/// A description of the expression.
/// </param>
public ExpressionObserver(
IObservable<object> rootObservable,
string expression,
bool enableDataValidation = false,
string description = null)
ExpressionNode node,
string description)
{
Contract.Requires<ArgumentNullException>(rootObservable != null);
Contract.Requires<ArgumentNullException>(expression != null);
Expression = expression;
Description = description ?? expression;
_node = Parse(expression, enableDataValidation);
_node = node;
Description = description;
_root = rootObservable;
}
@ -113,30 +106,92 @@ namespace Avalonia.Data.Core
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="rootGetter">A function which gets the root object.</param>
/// <param name="expression">The expression.</param>
/// <param name="node">The expression.</param>
/// <param name="update">An observable which triggers a re-read of the getter.</param>
/// <param name="enableDataValidation">Whether data validation should be enabled.</param>
/// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/> will be used.
/// A description of the expression.
/// </param>
public ExpressionObserver(
Func<object> rootGetter,
string expression,
ExpressionNode node,
IObservable<Unit> update,
bool enableDataValidation = false,
string description = null)
string description)
{
Contract.Requires<ArgumentNullException>(rootGetter != null);
Contract.Requires<ArgumentNullException>(expression != null);
Contract.Requires<ArgumentNullException>(update != null);
Expression = expression;
Description = description ?? expression;
_node = Parse(expression, enableDataValidation);
Description = description;
_node = node;
_node.Target = new WeakReference(rootGetter());
_root = update.Select(x => rootGetter());
}
/// <summary>
/// Creates a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="root">The root object.</param>
/// <param name="expression">The expression.</param>
/// <param name="enableDataValidation">Whether or not to track data validation</param>
/// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/>'s string representation will be used.
/// </param>
public static ExpressionObserver Create<T, U>(
T root,
Expression<Func<T, U>> expression,
bool enableDataValidation = false,
string description = null)
{
return new ExpressionObserver(root, Parse(expression, enableDataValidation), description ?? expression.ToString());
}
/// <summary>
/// Creates a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="rootObservable">An observable which provides the root object.</param>
/// <param name="expression">The expression.</param>
/// <param name="enableDataValidation">Whether or not to track data validation</param>
/// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/>'s string representation will be used.
/// </param>
public static ExpressionObserver Create<T, U>(
IObservable<T> rootObservable,
Expression<Func<T, U>> expression,
bool enableDataValidation = false,
string description = null)
{
Contract.Requires<ArgumentNullException>(rootObservable != null);
return new ExpressionObserver(
rootObservable.Select(o => (object)o),
Parse(expression, enableDataValidation),
description ?? expression.ToString());
}
/// <summary>
/// Creates a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="rootGetter">A function which gets the root object.</param>
/// <param name="expression">The expression.</param>
/// <param name="update">An observable which triggers a re-read of the getter.</param>
/// <param name="enableDataValidation">Whether or not to track data validation</param>
/// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/>'s string representation will be used.
/// </param>
public static ExpressionObserver Create<T, U>(
Func<T> rootGetter,
Expression<Func<T, U>> expression,
IObservable<Unit> update,
bool enableDataValidation = false,
string description = null)
{
Contract.Requires<ArgumentNullException>(rootGetter != null);
return new ExpressionObserver(
() => rootGetter(),
Parse(expression, enableDataValidation),
update,
description ?? expression.ToString());
}
/// <summary>
/// Attempts to set the value of a property expression.
/// </summary>
@ -221,16 +276,9 @@ namespace Avalonia.Data.Core
}
}
private static ExpressionNode Parse(string expression, bool enableDataValidation)
private static ExpressionNode Parse(LambdaExpression expression, bool enableDataValidation)
{
if (!string.IsNullOrWhiteSpace(expression))
{
return ExpressionNodeBuilder.Build(expression, enableDataValidation);
}
else
{
return new EmptyExpressionNode();
}
return ExpressionTreeParser.Parse(expression, enableDataValidation);
}
private void StartRoot()

4
src/Avalonia.Base/Data/Core/ExpressionParseException.cs

@ -17,8 +17,8 @@ namespace Avalonia.Data.Core
/// </summary>
/// <param name="column">The column position of the error.</param>
/// <param name="message">The exception message.</param>
public ExpressionParseException(int column, string message)
: base(message)
public ExpressionParseException(int column, string message, Exception innerException = null)
: base(message, innerException)
{
Column = column;
}

71
src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs

@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using Avalonia.Data;
namespace Avalonia.Data.Core
{
class IndexerExpressionNode : IndexerNodeBase
{
private readonly ParameterExpression _parameter;
private readonly IndexExpression _expression;
private readonly Delegate _setDelegate;
private readonly Delegate _getDelegate;
private readonly Delegate _firstArgumentDelegate;
public IndexerExpressionNode(IndexExpression expression)
{
_parameter = Expression.Parameter(expression.Object.Type);
_expression = expression.Update(_parameter, expression.Arguments);
_getDelegate = Expression.Lambda(_expression, _parameter).Compile();
var valueParameter = Expression.Parameter(expression.Type);
_setDelegate = Expression.Lambda(Expression.Assign(_expression, valueParameter), _parameter, valueParameter).Compile();
_firstArgumentDelegate = Expression.Lambda(_expression.Arguments[0], _parameter).Compile();
}
public override Type PropertyType => _expression.Type;
public override string Description => _expression.ToString();
protected override bool SetTargetValueCore(object value, BindingPriority priority)
{
try
{
_setDelegate.DynamicInvoke(Target.Target, value);
return true;
}
catch (Exception)
{
return false;
}
}
protected override object GetValue(object target)
{
try
{
return _getDelegate.DynamicInvoke(target);
}
catch (TargetInvocationException e) when (e.InnerException is ArgumentOutOfRangeException
|| e.InnerException is IndexOutOfRangeException
|| e.InnerException is KeyNotFoundException)
{
return AvaloniaProperty.UnsetValue;
}
}
protected override bool ShouldUpdate(object sender, PropertyChangedEventArgs e)
{
return _expression.Indexer == null || _expression.Indexer.Name == e.PropertyName;
}
protected override int? TryGetFirstArgumentAsInt() => _firstArgumentDelegate.DynamicInvoke(Target.Target) as int?;
}
}

92
src/Avalonia.Base/Data/Core/IndexerNodeBase.cs

@ -0,0 +1,92 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reactive.Linq;
using System.Reflection;
using System.Text;
using Avalonia.Data;
using Avalonia.Utilities;
namespace Avalonia.Data.Core
{
public abstract class IndexerNodeBase : SettableNode
{
private IDisposable _subscription;
protected override void StartListeningCore(WeakReference reference)
{
var target = reference.Target;
var incc = target as INotifyCollectionChanged;
var inpc = target as INotifyPropertyChanged;
var inputs = new List<IObservable<object>>();
if (incc != null)
{
inputs.Add(WeakObservable.FromEventPattern<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>(
incc,
nameof(incc.CollectionChanged))
.Where(x => ShouldUpdate(x.Sender, x.EventArgs))
.Select(_ => GetValue(target)));
}
if (inpc != null)
{
inputs.Add(WeakObservable.FromEventPattern<INotifyPropertyChanged, PropertyChangedEventArgs>(
inpc,
nameof(inpc.PropertyChanged))
.Where(x => ShouldUpdate(x.Sender, x.EventArgs))
.Select(_ => GetValue(target)));
}
_subscription = Observable.Merge(inputs).StartWith(GetValue(target)).Subscribe(ValueChanged);
}
protected override void StopListeningCore()
{
_subscription.Dispose();
}
protected abstract object GetValue(object target);
protected abstract int? TryGetFirstArgumentAsInt();
private bool ShouldUpdate(object sender, NotifyCollectionChangedEventArgs e)
{
if (sender is IList)
{
var index = TryGetFirstArgumentAsInt();
if (index == null)
{
return false;
}
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 true; // Implementation defined meaning for the index, so just try to update anyway
}
protected abstract bool ShouldUpdate(object sender, PropertyChangedEventArgs e);
}
}

2
src/Avalonia.Base/Data/Core/LogicalNotNode.cs

@ -7,7 +7,7 @@ using Avalonia.Data;
namespace Avalonia.Data.Core
{
internal class LogicalNotNode : ExpressionNode, ITransformNode
public class LogicalNotNode : ExpressionNode, ITransformNode
{
public override string Description => "!";

199
src/Avalonia.Base/Data/Core/Parsers/ExpressionParser.cs

@ -1,199 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
namespace Avalonia.Data.Core.Parsers
{
internal class ExpressionParser
{
private bool _enableValidation;
public ExpressionParser(bool enableValidation)
{
_enableValidation = enableValidation;
}
public ExpressionNode Parse(Reader r)
{
var nodes = new List<ExpressionNode>();
var state = State.Start;
while (!r.End && state != State.End)
{
switch (state)
{
case State.Start:
state = ParseStart(r, nodes);
break;
case State.AfterMember:
state = ParseAfterMember(r, nodes);
break;
case State.BeforeMember:
state = ParseBeforeMember(r, nodes);
break;
case State.AttachedProperty:
state = ParseAttachedProperty(r, nodes);
break;
case State.Indexer:
state = ParseIndexer(r, nodes);
break;
}
}
if (state == State.BeforeMember)
{
throw new ExpressionParseException(r.Position, "Unexpected end of expression.");
}
for (int n = 0; n < nodes.Count - 1; ++n)
{
nodes[n].Next = nodes[n + 1];
}
return nodes.FirstOrDefault();
}
private State ParseStart(Reader r, IList<ExpressionNode> nodes)
{
if (ParseNot(r))
{
nodes.Add(new LogicalNotNode());
return State.Start;
}
else if (ParseOpenBrace(r))
{
return State.AttachedProperty;
}
else if (PeekOpenBracket(r))
{
return State.Indexer;
}
else
{
var identifier = IdentifierParser.Parse(r);
if (identifier != null)
{
nodes.Add(new PropertyAccessorNode(identifier, _enableValidation));
return State.AfterMember;
}
}
return State.End;
}
private static State ParseAfterMember(Reader r, IList<ExpressionNode> nodes)
{
if (ParseMemberAccessor(r))
{
return State.BeforeMember;
}
else if (ParseStreamOperator(r))
{
nodes.Add(new StreamNode());
return State.AfterMember;
}
else if (PeekOpenBracket(r))
{
return State.Indexer;
}
return State.End;
}
private State ParseBeforeMember(Reader r, IList<ExpressionNode> nodes)
{
if (ParseOpenBrace(r))
{
return State.AttachedProperty;
}
else
{
var identifier = IdentifierParser.Parse(r);
if (identifier != null)
{
nodes.Add(new PropertyAccessorNode(identifier, _enableValidation));
return State.AfterMember;
}
return State.End;
}
}
private State ParseAttachedProperty(Reader r, List<ExpressionNode> nodes)
{
var owner = IdentifierParser.Parse(r);
if (r.End || !r.TakeIf('.'))
{
throw new ExpressionParseException(r.Position, "Invalid attached property name.");
}
var name = IdentifierParser.Parse(r);
if (r.End || !r.TakeIf(')'))
{
throw new ExpressionParseException(r.Position, "Expected ')'.");
}
nodes.Add(new PropertyAccessorNode(owner + '.' + name, _enableValidation));
return State.AfterMember;
}
private State ParseIndexer(Reader r, List<ExpressionNode> nodes)
{
var args = ArgumentListParser.Parse(r, '[', ']');
if (args.Count == 0)
{
throw new ExpressionParseException(r.Position, "Indexer may not be empty.");
}
nodes.Add(new IndexerNode(args));
return State.AfterMember;
}
private static bool ParseNot(Reader r)
{
return !r.End && r.TakeIf('!');
}
private static bool ParseMemberAccessor(Reader r)
{
return !r.End && r.TakeIf('.');
}
private static bool ParseOpenBrace(Reader r)
{
return !r.End && r.TakeIf('(');
}
private static bool PeekOpenBracket(Reader r)
{
return !r.End && r.Peek == '[';
}
private static bool ParseStreamOperator(Reader r)
{
return !r.End && r.TakeIf('^');
}
private enum State
{
Start,
AfterMember,
BeforeMember,
AttachedProperty,
Indexer,
End,
}
}
}

27
src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
namespace Avalonia.Data.Core.Parsers
{
static class ExpressionTreeParser
{
public static ExpressionNode Parse(Expression expr, bool enableDataValidation)
{
var visitor = new ExpressionVisitorNodeBuilder(enableDataValidation);
visitor.Visit(expr);
var nodes = visitor.Nodes;
for (int n = 0; n < nodes.Count - 1; ++n)
{
nodes[n].Next = nodes[n + 1];
}
return nodes.FirstOrDefault() ?? new EmptyExpressionNode();
}
}
}

219
src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs

@ -0,0 +1,219 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
namespace Avalonia.Data.Core.Parsers
{
class ExpressionVisitorNodeBuilder : ExpressionVisitor
{
private const string MultiDimensionalArrayGetterMethodName = "Get";
private static PropertyInfo AvaloniaObjectIndexer;
private static MethodInfo CreateDelegateMethod;
private readonly bool _enableDataValidation;
static ExpressionVisitorNodeBuilder()
{
AvaloniaObjectIndexer = typeof(AvaloniaObject).GetProperty("Item", new[] { typeof(AvaloniaProperty) });
CreateDelegateMethod = typeof(MethodInfo).GetMethod("CreateDelegate", new[] { typeof(Type), typeof(object) });
}
public List<ExpressionNode> Nodes { get; }
public ExpressionVisitorNodeBuilder(bool enableDataValidation)
{
_enableDataValidation = enableDataValidation;
Nodes = new List<ExpressionNode>();
}
protected override Expression VisitUnary(UnaryExpression node)
{
if (node.NodeType == ExpressionType.Not && node.Type == typeof(bool))
{
Nodes.Add(new LogicalNotNode());
}
else if (node.NodeType == ExpressionType.Convert)
{
if (node.Operand.Type.IsAssignableFrom(node.Type))
{
// Ignore inheritance casts
}
else
{
throw new ExpressionParseException(0, $"Cannot parse non-inheritance casts in a binding expression.");
}
}
else if (node.NodeType == ExpressionType.TypeAs)
{
// Ignore as operator.
}
else
{
throw new ExpressionParseException(0, $"Unable to parse unary operator {node.NodeType} in a binding expression");
}
return base.VisitUnary(node);
}
protected override Expression VisitMember(MemberExpression node)
{
var visited = base.VisitMember(node);
Nodes.Add(new PropertyAccessorNode(node.Member.Name, _enableDataValidation));
return visited;
}
protected override Expression VisitIndex(IndexExpression node)
{
Visit(node.Object);
if (node.Indexer == AvaloniaObjectIndexer)
{
var property = GetArgumentExpressionValue<AvaloniaProperty>(node.Arguments[0]);
Nodes.Add(new AvaloniaPropertyAccessorNode(property, _enableDataValidation));
}
else
{
Nodes.Add(new IndexerExpressionNode(node));
}
return node;
}
private T GetArgumentExpressionValue<T>(Expression expr)
{
try
{
return Expression.Lambda<Func<T>>(expr).Compile(preferInterpretation: true)();
}
catch (InvalidOperationException ex)
{
throw new ExpressionParseException(0, "Unable to parse indexer value.", ex);
}
}
protected override Expression VisitBinary(BinaryExpression node)
{
if (node.NodeType == ExpressionType.ArrayIndex)
{
return Visit(Expression.MakeIndex(node.Left, null, new[] { node.Right }));
}
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitBlock(BlockExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override CatchBlock VisitCatchBlock(CatchBlock node)
{
throw new ExpressionParseException(0, $"Catch blocks are not allowed in binding expressions.");
}
protected override Expression VisitConditional(ConditionalExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitDynamic(DynamicExpression node)
{
throw new ExpressionParseException(0, $"Dynamic expressions are not allowed in binding expressions.");
}
protected override ElementInit VisitElementInit(ElementInit node)
{
throw new ExpressionParseException(0, $"Element init expressions are not valid in a binding expression.");
}
protected override Expression VisitGoto(GotoExpression node)
{
throw new ExpressionParseException(0, $"Goto expressions not supported in binding expressions.");
}
protected override Expression VisitInvocation(InvocationExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitLabel(LabelExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitListInit(ListInitExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitLoop(LoopExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override MemberAssignment VisitMemberAssignment(MemberAssignment node)
{
throw new ExpressionParseException(0, $"Member assignments not supported in binding expressions.");
}
protected override Expression VisitMethodCall(MethodCallExpression node)
{
if (node.Method == CreateDelegateMethod)
{
var visited = Visit(node.Arguments[1]);
Nodes.Add(new PropertyAccessorNode(GetArgumentExpressionValue<MethodInfo>(node.Object).Name, _enableDataValidation));
return node;
}
else if (node.Method.Name == StreamBindingExtensions.StreamBindingName || node.Method.Name.StartsWith(StreamBindingExtensions.StreamBindingName + '`'))
{
if (node.Method.IsStatic)
{
Visit(node.Arguments[0]);
}
else
{
Visit(node.Object);
}
Nodes.Add(new StreamNode());
return node;
}
var property = TryGetPropertyFromMethod(node.Method);
if (property != null)
{
return Visit(Expression.MakeIndex(node.Object, property, node.Arguments));
}
else if (node.Object.Type.IsArray && node.Method.Name == MultiDimensionalArrayGetterMethodName)
{
return Visit(Expression.MakeIndex(node.Object, null, node.Arguments));
}
throw new ExpressionParseException(0, $"Invalid method call in binding expression: '{node.Method.DeclaringType.AssemblyQualifiedName}.{node.Method.Name}'.");
}
private PropertyInfo TryGetPropertyFromMethod(MethodInfo method)
{
var type = method.DeclaringType;
return type.GetRuntimeProperties().FirstOrDefault(prop => prop.GetMethod == method);
}
protected override Expression VisitSwitch(SwitchExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitTry(TryExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitTypeBinary(TypeBinaryExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
}
}

30
src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs

@ -60,35 +60,7 @@ namespace Avalonia.Data.Core.Plugins
private static AvaloniaProperty LookupProperty(AvaloniaObject o, string propertyName)
{
if (!propertyName.Contains("."))
{
return AvaloniaPropertyRegistry.Instance.FindRegistered(o, propertyName);
}
else
{
var split = propertyName.Split('.');
if (split.Length == 2)
{
// HACK: We need a way to resolve types here using something like IXamlTypeResolver.
// We don't currently have that so we have to make our best guess.
var type = split[0];
var name = split[1];
var registry = AvaloniaPropertyRegistry.Instance;
var registered = registry.GetRegisteredAttached(o.GetType())
.Concat(registry.GetRegistered(o.GetType()));
foreach (var p in registered)
{
if (p.Name == name && IsOfType(p.OwnerType, type))
{
return p;
}
}
}
}
return null;
return AvaloniaPropertyRegistry.Instance.FindRegistered(o, propertyName);
}
private static bool IsOfType(Type type, string typeName)

2
src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs

@ -8,7 +8,7 @@ using Avalonia.Data.Core.Plugins;
namespace Avalonia.Data.Core
{
internal class PropertyAccessorNode : SettableNode
public class PropertyAccessorNode : SettableNode
{
private readonly bool _enableValidation;
private IPropertyAccessor _accessor;

2
src/Avalonia.Base/Data/Core/SettableNode.cs

@ -7,7 +7,7 @@ using System.Threading.Tasks;
namespace Avalonia.Data.Core
{
internal abstract class SettableNode : ExpressionNode
public abstract class SettableNode : ExpressionNode
{
public bool SetTargetValue(object value, BindingPriority priority)
{

27
src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace Avalonia
{
public static class StreamBindingExtensions
{
internal static string StreamBindingName = "StreamBinding";
public static T StreamBinding<T>(this Task<T> @this)
{
throw new InvalidOperationException("This should be used only in a binding expression");
}
public static object StreamBinding(this Task @this)
{
throw new InvalidOperationException("This should be used only in a binding expression");
}
public static T StreamBinding<T>(this IObservable<T> @this)
{
throw new InvalidOperationException("This should be used only in a binding expression");
}
}
}

2
src/Avalonia.Base/Data/Core/StreamNode.cs

@ -6,7 +6,7 @@ using System.Reactive.Linq;
namespace Avalonia.Data.Core
{
internal class StreamNode : ExpressionNode
public class StreamNode : ExpressionNode
{
private IDisposable _subscription;

25
src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs

@ -23,15 +23,24 @@ namespace Avalonia.Diagnostics
{
var set = o.GetSetValues();
PriorityValue value;
if (set.TryGetValue(property, out value))
if (set.TryGetValue(property, out var obj))
{
return new AvaloniaPropertyValue(
property,
o.GetValue(property),
(BindingPriority)value.ValuePriority,
value.GetDiagnostic());
if (obj is PriorityValue value)
{
return new AvaloniaPropertyValue(
property,
o.GetValue(property),
(BindingPriority)value.ValuePriority,
value.GetDiagnostic());
}
else
{
return new AvaloniaPropertyValue(
property,
obj,
BindingPriority.LocalValue,
"Local value");
}
}
else
{

3
src/Avalonia.Base/IPriorityValueOwner.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Data;
using Avalonia.Utilities;
namespace Avalonia
{
@ -31,5 +32,7 @@ namespace Avalonia
/// Ensures that the current thread is the UI thread.
/// </summary>
void VerifyAccess();
DeferredSetter<object> Setter { get; }
}
}

11
src/Avalonia.Base/PriorityValue.cs

@ -21,7 +21,7 @@ namespace Avalonia
/// priority binding that doesn't return <see cref="AvaloniaProperty.UnsetValue"/>. Where there
/// are multiple bindings registered with the same priority, the most recently added binding
/// has a higher priority. Each time the value changes, the
/// <see cref="IPriorityValueOwner.Changed(PriorityValue, object, object)"/> method on the
/// <see cref="IPriorityValueOwner.Changed"/> method on the
/// owner object is fired with the old and new values.
/// </remarks>
internal class PriorityValue
@ -30,7 +30,6 @@ namespace Avalonia
private readonly SingleOrDictionary<int, PriorityLevel> _levels = new SingleOrDictionary<int, PriorityLevel>();
private readonly Func<object, object> _validate;
private static readonly DeferredSetter<PriorityValue, (object value, int priority)> delayedSetter = new DeferredSetter<PriorityValue, (object, int)>();
private (object value, int priority) _value;
/// <summary>
@ -243,12 +242,18 @@ namespace Avalonia
/// <param name="priority">The priority level that the value came from.</param>
private void UpdateValue(object value, int priority)
{
delayedSetter.SetAndNotify(this,
Owner.Setter.SetAndNotify(Property,
ref _value,
UpdateCore,
(value, priority));
}
private bool UpdateCore(
object update,
ref (object value, int priority) backing,
Action<Action> notify)
=> UpdateCore(((object, int))update, ref backing, notify);
private bool UpdateCore(
(object value, int priority) update,
ref (object value, int priority) backing,

6
src/Avalonia.Base/Data/Core/Parsers/Reader.cs → src/Avalonia.Base/Utilities/CharacterReader.cs

@ -3,14 +3,14 @@
using System;
namespace Avalonia.Data.Core.Parsers
namespace Avalonia.Utilities
{
internal class Reader
public class CharacterReader
{
private readonly string _s;
private int _i;
public Reader(string s)
public CharacterReader(string s)
{
_s = s;
}

53
src/Avalonia.Base/Utilities/DeferredSetter.cs

@ -8,11 +8,10 @@ namespace Avalonia.Utilities
{
/// <summary>
/// A utility class to enable deferring assignment until after property-changed notifications are sent.
/// Used to fix #855.
/// </summary>
/// <typeparam name="TProperty">The type of the object that represents the property.</typeparam>
/// <typeparam name="TSetRecord">The type of value with which to track the delayed assignment.</typeparam>
class DeferredSetter<TProperty, TSetRecord>
where TProperty: class
class DeferredSetter<TSetRecord>
{
private struct NotifyDisposable : IDisposable
{
@ -37,29 +36,44 @@ namespace Avalonia.Utilities
{
public bool Notifying { get; set; }
private Queue<TSetRecord> pendingValues;
private SingleOrQueue<TSetRecord> pendingValues;
public Queue<TSetRecord> PendingValues
public SingleOrQueue<TSetRecord> PendingValues
{
get
{
return pendingValues ?? (pendingValues = new Queue<TSetRecord>());
return pendingValues ?? (pendingValues = new SingleOrQueue<TSetRecord>());
}
}
}
private readonly ConditionalWeakTable<TProperty, SettingStatus> setRecords = new ConditionalWeakTable<TProperty, SettingStatus>();
private Dictionary<AvaloniaProperty, SettingStatus> _setRecords;
private Dictionary<AvaloniaProperty, SettingStatus> SetRecords
=> _setRecords ?? (_setRecords = new Dictionary<AvaloniaProperty, SettingStatus>());
private SettingStatus GetOrCreateStatus(AvaloniaProperty property)
{
if (!SetRecords.TryGetValue(property, out var status))
{
status = new SettingStatus();
SetRecords.Add(property, status);
}
return status;
}
/// <summary>
/// Mark the property as currently notifying.
/// </summary>
/// <param name="property">The property to mark as notifying.</param>
/// <returns>Returns a disposable that when disposed, marks the property as done notifying.</returns>
private NotifyDisposable MarkNotifying(TProperty property)
private NotifyDisposable MarkNotifying(AvaloniaProperty property)
{
Contract.Requires<InvalidOperationException>(!IsNotifying(property));
return new NotifyDisposable(setRecords.GetOrCreateValue(property));
SettingStatus status = GetOrCreateStatus(property);
return new NotifyDisposable(status);
}
/// <summary>
@ -67,19 +81,19 @@ namespace Avalonia.Utilities
/// </summary>
/// <param name="property">The property.</param>
/// <returns>If the property is currently notifying listeners.</returns>
private bool IsNotifying(TProperty property)
=> setRecords.TryGetValue(property, out var value) && value.Notifying;
private bool IsNotifying(AvaloniaProperty property)
=> SetRecords.TryGetValue(property, out var value) && value.Notifying;
/// <summary>
/// Add a pending assignment for the property.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="value">The value to assign.</param>
private void AddPendingSet(TProperty property, TSetRecord value)
private void AddPendingSet(AvaloniaProperty property, TSetRecord value)
{
Contract.Requires<InvalidOperationException>(IsNotifying(property));
setRecords.GetOrCreateValue(property).PendingValues.Enqueue(value);
GetOrCreateStatus(property).PendingValues.Enqueue(value);
}
/// <summary>
@ -87,9 +101,9 @@ namespace Avalonia.Utilities
/// </summary>
/// <param name="property">The property to check.</param>
/// <returns>If the property has any pending assignments.</returns>
private bool HasPendingSet(TProperty property)
private bool HasPendingSet(AvaloniaProperty property)
{
return setRecords.TryGetValue(property, out var status) && status.PendingValues.Count != 0;
return SetRecords.TryGetValue(property, out var status) && !status.PendingValues.Empty;
}
/// <summary>
@ -97,9 +111,9 @@ namespace Avalonia.Utilities
/// </summary>
/// <param name="property">The property to check.</param>
/// <returns>The first pending assignment for the property.</returns>
private TSetRecord GetFirstPendingSet(TProperty property)
private TSetRecord GetFirstPendingSet(AvaloniaProperty property)
{
return setRecords.GetOrCreateValue(property).PendingValues.Dequeue();
return GetOrCreateStatus(property).PendingValues.Dequeue();
}
public delegate bool SetterDelegate<TValue>(TSetRecord record, ref TValue backing, Action<Action> notifyCallback);
@ -115,7 +129,7 @@ namespace Avalonia.Utilities
/// </param>
/// <param name="value">The value to try to set.</param>
public bool SetAndNotify<TValue>(
TProperty property,
AvaloniaProperty property,
ref TValue backing,
SetterDelegate<TValue> setterCallback,
TSetRecord value)
@ -144,6 +158,7 @@ namespace Avalonia.Utilities
}
});
}
return updated;
}
else if(!object.Equals(value, backing))

6
src/Avalonia.Base/Data/Core/Parsers/IdentifierParser.cs → src/Avalonia.Base/Utilities/IdentifierParser.cs

@ -4,11 +4,11 @@
using System.Globalization;
using System.Text;
namespace Avalonia.Data.Core.Parsers
namespace Avalonia.Utilities
{
internal static class IdentifierParser
public static class IdentifierParser
{
public static string Parse(Reader r)
public static string Parse(CharacterReader r)
{
if (IsValidIdentifierStart(r.Peek))
{

58
src/Avalonia.Base/Utilities/SingleOrQueue.cs

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Utilities
{
/// <summary>
/// FIFO Queue optimized for holding zero or one items.
/// </summary>
/// <typeparam name="T">The type of items held in the queue.</typeparam>
public class SingleOrQueue<T>
{
private T _head;
private Queue<T> _tail;
private Queue<T> Tail => _tail ?? (_tail = new Queue<T>());
private bool HasTail => _tail != null;
public bool Empty { get; private set; } = true;
public void Enqueue(T value)
{
if (Empty)
{
_head = value;
}
else
{
Tail.Enqueue(value);
}
Empty = false;
}
public T Dequeue()
{
if (Empty)
{
throw new InvalidOperationException("Cannot dequeue from an empty queue!");
}
var result = _head;
if (HasTail && Tail.Count != 0)
{
_head = Tail.Dequeue();
}
else
{
_head = default;
Empty = true;
}
return result;
}
}
}

24
src/Avalonia.Base/ValueStore.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using Avalonia.Data;
using Avalonia.Utilities;
namespace Avalonia
{
@ -91,15 +92,15 @@ namespace Avalonia
public void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
{
((IPriorityValueOwner)_owner).BindingNotificationReceived(property, notification);
_owner.BindingNotificationReceived(property, notification);
}
public void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue)
{
((IPriorityValueOwner)_owner).Changed(property, priority, oldValue, newValue);
_owner.PriorityValueChanged(property, priority, oldValue, newValue);
}
public IDictionary<AvaloniaProperty, PriorityValue> GetSetValues() => throw new NotImplementedException();
public IDictionary<AvaloniaProperty, object> GetSetValues() => _values;
public object GetValue(AvaloniaProperty property)
{
@ -115,7 +116,7 @@ namespace Avalonia
public bool IsAnimating(AvaloniaProperty property)
{
return _values.TryGetValue(property, out var value) ? (value as PriorityValue)?.IsAnimating ?? false : false;
return _values.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating;
}
public bool IsSet(AvaloniaProperty property)
@ -148,13 +149,11 @@ namespace Avalonia
validate2 = v => validate(_owner, v);
}
PriorityValue result = new PriorityValue(
return new PriorityValue(
this,
property,
property.PropertyType,
validate2);
return result;
}
private object Validate(AvaloniaProperty property, object value)
@ -168,5 +167,16 @@ namespace Avalonia
return value;
}
private DeferredSetter<object> _defferedSetter;
public DeferredSetter<object> Setter
{
get
{
return _defferedSetter ??
(_defferedSetter = new DeferredSetter<object>());
}
}
}
}

20
src/Avalonia.Controls/Grid.cs

@ -194,6 +194,16 @@ namespace Avalonia.Controls
/// </summary>
private GridLayout.MeasureResult _rowMeasureCache;
/// <summary>
/// Gets the row layout as of the last measure.
/// </summary>
private GridLayout _rowLayoutCache;
/// <summary>
/// Gets the column layout as of the last measure.
/// </summary>
private GridLayout _columnLayoutCache;
/// <summary>
/// Measures the grid.
/// </summary>
@ -253,6 +263,9 @@ namespace Avalonia.Controls
// Cache the measure result and return the desired size.
_columnMeasureCache = columnResult;
_rowMeasureCache = rowResult;
_rowLayoutCache = rowLayout;
_columnLayoutCache = columnLayout;
return new Size(columnResult.DesiredLength, rowResult.DesiredLength);
// Measure each child only once.
@ -299,13 +312,11 @@ namespace Avalonia.Controls
// arrow back to any statements and re-run them without any side-effect.
var (safeColumns, safeRows) = GetSafeColumnRows();
var columnLayout = new GridLayout(ColumnDefinitions);
var rowLayout = new GridLayout(RowDefinitions);
var columnLayout = _columnLayoutCache;
var rowLayout = _rowLayoutCache;
// Calculate for arrange result.
var columnResult = columnLayout.Arrange(finalSize.Width, _columnMeasureCache);
var rowResult = rowLayout.Arrange(finalSize.Height, _rowMeasureCache);
// Arrange the children.
foreach (var child in Children.OfType<Control>())
{
@ -315,7 +326,6 @@ namespace Avalonia.Controls
var y = Enumerable.Range(0, row).Sum(r => rowResult.LengthList[r]);
var width = Enumerable.Range(column, columnSpan).Sum(c => columnResult.LengthList[c]);
var height = Enumerable.Range(row, rowSpan).Sum(r => rowResult.LengthList[r]);
child.Arrange(new Rect(x, y, width, height));
}

6
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -189,6 +189,12 @@ namespace Avalonia.Controls.Presenters
_caretTimer.Start();
InvalidateVisual();
}
else
{
_caretTimer.Start();
InvalidateVisual();
_caretTimer.Stop();
}
if (IsMeasureValid)
{

7
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -360,7 +360,7 @@ namespace Avalonia.Controls.Primitives
{
if (!AlwaysSelected)
{
SelectedIndex = -1;
selectedIndex = SelectedIndex = -1;
}
else
{
@ -368,6 +368,11 @@ namespace Avalonia.Controls.Primitives
}
}
var items = Items?.Cast<object>();
if (selectedIndex >= items.Count())
{
selectedIndex = SelectedIndex = items.Count() - 1;
}
break;
case NotifyCollectionChangedAction.Reset:

90
src/Avalonia.Controls/ProgressBar.cs

@ -21,18 +21,27 @@ namespace Avalonia.Controls
public static readonly StyledProperty<Orientation> OrientationProperty =
AvaloniaProperty.Register<ProgressBar, Orientation>(nameof(Orientation), Orientation.Horizontal);
private static readonly DirectProperty<ProgressBar, double> IndeterminateStartingOffsetProperty =
AvaloniaProperty.RegisterDirect<ProgressBar, double>(
nameof(IndeterminateStartingOffset),
p => p.IndeterminateStartingOffset,
(p, o) => p.IndeterminateStartingOffset = o);
private static readonly DirectProperty<ProgressBar, double> IndeterminateEndingOffsetProperty =
AvaloniaProperty.RegisterDirect<ProgressBar, double>(
nameof(IndeterminateEndingOffset),
p => p.IndeterminateEndingOffset,
(p, o) => p.IndeterminateEndingOffset = o);
private Border _indicator;
private IndeterminateAnimation _indeterminateAnimation;
static ProgressBar()
{
PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical");
PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal");
PseudoClass(IsIndeterminateProperty, ":indeterminate");
ValueProperty.Changed.AddClassHandler<ProgressBar>(x => x.ValueChanged);
IsIndeterminateProperty.Changed.AddClassHandler<ProgressBar>(
(p, e) => { if (p._indicator != null) p.UpdateIsIndeterminate((bool)e.NewValue); });
}
public bool IsIndeterminate
@ -46,6 +55,19 @@ namespace Avalonia.Controls
get => GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
private double _indeterminateStartingOffset;
private double IndeterminateStartingOffset
{
get => _indeterminateStartingOffset;
set => SetAndRaise(IndeterminateStartingOffsetProperty, ref _indeterminateStartingOffset, value);
}
private double _indeterminateEndingOffset;
private double IndeterminateEndingOffset
{
get => _indeterminateEndingOffset;
set => SetAndRaise(IndeterminateEndingOffsetProperty, ref _indeterminateEndingOffset, value);
}
/// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize)
@ -60,7 +82,6 @@ namespace Avalonia.Controls
_indicator = e.NameScope.Get<Border>("PART_Indicator");
UpdateIndicator(Bounds.Size);
UpdateIsIndeterminate(IsIndeterminate);
}
private void UpdateIndicator(Size bounds)
@ -70,9 +91,20 @@ namespace Avalonia.Controls
if (IsIndeterminate)
{
if (Orientation == Orientation.Horizontal)
_indicator.Width = bounds.Width / 5.0;
{
var width = bounds.Width / 5.0;
IndeterminateStartingOffset = -width;
_indicator.Width = width;
IndeterminateEndingOffset = bounds.Width;
}
else
_indicator.Height = bounds.Height / 5.0;
{
var height = bounds.Height / 5.0;
IndeterminateStartingOffset = -bounds.Height;
_indicator.Height = height;
IndeterminateEndingOffset = height;
}
}
else
{
@ -86,53 +118,9 @@ namespace Avalonia.Controls
}
}
private void UpdateIsIndeterminate(bool isIndeterminate)
{
if (isIndeterminate)
{
if (_indeterminateAnimation == null || _indeterminateAnimation.Disposed)
_indeterminateAnimation = IndeterminateAnimation.StartAnimation(this);
}
else
_indeterminateAnimation?.Dispose();
}
private void ValueChanged(AvaloniaPropertyChangedEventArgs e)
{
UpdateIndicator(Bounds.Size);
}
// TODO: Implement Indeterminate Progress animation
// in xaml (most ideal) or if it's not possible
// then on this class.
private class IndeterminateAnimation : IDisposable
{
private WeakReference<ProgressBar> _progressBar;
private bool _disposed;
public bool Disposed => _disposed;
private IndeterminateAnimation(ProgressBar progressBar)
{
_progressBar = new WeakReference<ProgressBar>(progressBar);
}
public static IndeterminateAnimation StartAnimation(ProgressBar progressBar)
{
return new IndeterminateAnimation(progressBar);
}
private Rect GetAnimationRect(TimeSpan time)
{
return Rect.Empty;
}
public void Dispose()
{
_disposed = true;
}
}
}
}

19
src/Avalonia.Controls/StackPanel.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Linq;
using Avalonia.Input;
namespace Avalonia.Controls
@ -152,6 +153,7 @@ namespace Avalonia.Controls
double measuredWidth = 0;
double measuredHeight = 0;
double gap = Gap;
bool hasVisibleChild = Children.Any(c => c.IsVisible);
foreach (Control child in Children)
{
@ -160,23 +162,23 @@ namespace Avalonia.Controls
if (Orientation == Orientation.Vertical)
{
measuredHeight += size.Height + gap;
measuredHeight += size.Height + (child.IsVisible ? gap : 0);
measuredWidth = Math.Max(measuredWidth, size.Width);
}
else
{
measuredWidth += size.Width + gap;
measuredWidth += size.Width + (child.IsVisible ? gap : 0);
measuredHeight = Math.Max(measuredHeight, size.Height);
}
}
if (Orientation == Orientation.Vertical)
{
measuredHeight -= gap;
measuredHeight -= (hasVisibleChild ? gap : 0);
}
else
{
measuredWidth -= gap;
measuredWidth -= (hasVisibleChild ? gap : 0);
}
return new Size(measuredWidth, measuredHeight);
@ -193,6 +195,7 @@ namespace Avalonia.Controls
double arrangedWidth = finalSize.Width;
double arrangedHeight = finalSize.Height;
double gap = Gap;
bool hasVisibleChild = Children.Any(c => c.IsVisible);
if (Orientation == Orientation.Vertical)
{
@ -214,25 +217,25 @@ namespace Avalonia.Controls
Rect childFinal = new Rect(0, arrangedHeight, width, childHeight);
ArrangeChild(child, childFinal, finalSize, orientation);
arrangedWidth = Math.Max(arrangedWidth, childWidth);
arrangedHeight += childHeight + gap;
arrangedHeight += childHeight + (child.IsVisible ? gap : 0);
}
else
{
double height = Math.Max(childHeight, arrangedHeight);
Rect childFinal = new Rect(arrangedWidth, 0, childWidth, height);
ArrangeChild(child, childFinal, finalSize, orientation);
arrangedWidth += childWidth + gap;
arrangedWidth += childWidth + (child.IsVisible ? gap : 0);
arrangedHeight = Math.Max(arrangedHeight, childHeight);
}
}
if (orientation == Orientation.Vertical)
{
arrangedHeight = Math.Max(arrangedHeight - gap, finalSize.Height);
arrangedHeight = Math.Max(arrangedHeight - (hasVisibleChild ? gap : 0), finalSize.Height);
}
else
{
arrangedWidth = Math.Max(arrangedWidth - gap, finalSize.Width);
arrangedWidth = Math.Max(arrangedWidth - (hasVisibleChild ? gap : 0), finalSize.Width);
}
return new Size(arrangedWidth, arrangedHeight);

16
src/Avalonia.Controls/TextBox.cs

@ -124,7 +124,7 @@ namespace Avalonia.Controls
ScrollViewer.HorizontalScrollBarVisibilityProperty,
horizontalScrollBarVisibility,
BindingPriority.Style);
_undoRedoHelper = new UndoRedoHelper<UndoRedoState>(this);
_undoRedoHelper = new UndoRedoHelper<UndoRedoState>(this);
}
public bool AcceptsReturn
@ -262,7 +262,7 @@ namespace Avalonia.Controls
if (IsFocused)
{
_presenter.ShowCaret();
DecideCaretVisibility();
}
}
@ -282,12 +282,20 @@ namespace Avalonia.Controls
}
else
{
_presenter?.ShowCaret();
DecideCaretVisibility();
}
e.Handled = true;
}
private void DecideCaretVisibility()
{
if (!IsReadOnly)
_presenter?.ShowCaret();
else
_presenter?.HideCaret();
}
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
@ -557,7 +565,7 @@ namespace Avalonia.Controls
var index = CaretIndex = _presenter.GetCaretIndex(point);
var text = Text;
if (text != null)
if (text != null && e.MouseButton == MouseButton.Left)
{
switch (e.ClickCount)
{

1
src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs

@ -31,6 +31,7 @@ namespace Avalonia.Diagnostics.ViewModels
}
};
SelectedTab = 0;
root.GetObservable(TopLevel.PointerOverElementProperty)
.Subscribe(x => PointerOverElement = x?.GetType().Name);
}

17
src/Avalonia.Styling/Styling/Setter.cs

@ -126,7 +126,7 @@ namespace Avalonia.Styling
if (source != null)
{
var cloned = Clone(source, style, activator);
var cloned = Clone(source, source.Mode == BindingMode.Default ? Property.GetMetadata(control.GetType()).DefaultBindingMode : source.Mode, style, activator);
return BindingOperations.Apply(control, Property, cloned, null);
}
}
@ -134,13 +134,13 @@ namespace Avalonia.Styling
return Disposable.Empty;
}
private InstancedBinding Clone(InstancedBinding sourceInstance, IStyle style, IObservable<bool> activator)
private InstancedBinding Clone(InstancedBinding sourceInstance, BindingMode mode, IStyle style, IObservable<bool> activator)
{
if (activator != null)
{
var description = style?.ToString();
switch (sourceInstance.Mode)
switch (mode)
{
case BindingMode.OneTime:
if (sourceInstance.Observable != null)
@ -158,18 +158,11 @@ namespace Avalonia.Styling
var activated = new ActivatedObservable(activator, sourceInstance.Observable, description);
return InstancedBinding.OneWay(activated, BindingPriority.StyleTrigger);
}
case BindingMode.OneWayToSource:
{
var activated = new ActivatedSubject(activator, sourceInstance.Subject, description);
return InstancedBinding.OneWayToSource(activated, BindingPriority.StyleTrigger);
}
case BindingMode.TwoWay:
default:
{
var activated = new ActivatedSubject(activator, sourceInstance.Subject, description);
return InstancedBinding.TwoWay(activated, BindingPriority.StyleTrigger);
return new InstancedBinding(activated, sourceInstance.Mode, BindingPriority.StyleTrigger);
}
default:
throw new NotSupportedException("Unsupported BindingMode.");
}
}

5
src/Avalonia.Themes.Default/MenuItem.xaml

@ -122,11 +122,6 @@
</Setter>
</Style>
<Style Selector="MenuItem:selected /template/ Border#root">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentBrush}"/>
</Style>
<Style Selector="MenuItem:pointerover /template/ Border#root">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentBrush}"/>

45
src/Avalonia.Themes.Default/ProgressBar.xaml

@ -7,14 +7,9 @@
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Border Name="PART_Track"
BorderThickness="1"
BorderBrush="{TemplateBinding Background}"/>
<Border Name="PART_Indicator"
BorderThickness="1"
Background="{TemplateBinding Foreground}" />
</Grid>
<Border Name="PART_Indicator"
BorderThickness="1"
Background="{TemplateBinding Foreground}"/>
</Border>
</ControlTemplate>
</Setter>
@ -35,4 +30,36 @@
<Setter Property="MinWidth" Value="14"/>
<Setter Property="MinHeight" Value="200"/>
</Style>
</Styles>
<Style Selector="ProgressBar:horizontal:indeterminate /template/ Border#PART_Indicator">
<Style.Animations>
<Animation Duration="0:0:3"
RepeatCount="Loop"
Easing="LinearEasing">
<KeyFrame Cue="0%">
<Setter Property="TranslateTransform.X"
Value="{Binding IndeterminateStartingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="TranslateTransform.X"
Value="{Binding IndeterminateEndingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="ProgressBar:vertical:indeterminate /template/ Border#PART_Indicator">
<Style.Animations>
<Animation Duration="0:0:3"
RepeatCount="Loop"
Easing="LinearEasing">
<KeyFrame Cue="0%">
<Setter Property="TranslateTransform.Y"
Value="{Binding IndeterminateStartingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="TranslateTransform.Y"
Value="{Binding IndeterminateEndingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</Styles>

1
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@ -33,6 +33,7 @@
<Compile Include="MarkupExtensions\ResourceInclude.cs" />
<Compile Include="MarkupExtensions\StaticResourceExtension.cs" />
<Compile Include="MarkupExtensions\StyleIncludeExtension.cs" />
<Compile Include="Parsers\PropertyParser.cs" />
<Compile Include="PortableXaml\AvaloniaXamlContext.cs" />
<Compile Include="PortableXaml\AttributeExtensions.cs" />
<Compile Include="PortableXaml\AvaloniaMemberAttributeProvider.cs" />

69
src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs

@ -4,19 +4,19 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Text.RegularExpressions;
using Avalonia.Controls;
using Avalonia.Logging;
using Avalonia.Markup.Parsers;
using Avalonia.Markup.Xaml.Parsers;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.Styling;
using Portable.Xaml;
using Avalonia.Utilities;
using Portable.Xaml.ComponentModel;
using Portable.Xaml.Markup;
namespace Avalonia.Markup.Xaml.Converters
{
public class AvaloniaPropertyTypeConverter : TypeConverter
{
private static readonly Regex regex = new Regex(@"^\(?(\w*)\.(\w*)\)?|(.*)$");
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
@ -24,39 +24,48 @@ namespace Avalonia.Markup.Xaml.Converters
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
var (owner, propertyName) = ParseProperty((string)value);
var ownerType = TryResolveOwnerByName(context, owner) ??
context.GetFirstAmbientValue<ControlTemplate>()?.TargetType ??
context.GetFirstAmbientValue<Style>()?.Selector?.TargetType;
var registry = AvaloniaPropertyRegistry.Instance;
var parser = new PropertyParser();
var reader = new CharacterReader((string)value);
var (ns, owner, propertyName) = parser.Parse(reader);
var ownerType = TryResolveOwnerByName(context, ns, owner);
var targetType = context.GetFirstAmbientValue<ControlTemplate>()?.TargetType ??
context.GetFirstAmbientValue<Style>()?.Selector?.TargetType ??
typeof(Control);
var effectiveOwner = ownerType ?? targetType;
var property = registry.FindRegistered(effectiveOwner, propertyName);
if (ownerType == null)
if (property == null)
{
throw new XamlLoadException(
$"Could not determine the owner type for property '{propertyName}'. " +
"Please fully qualify the property name or specify a target type on " +
"the containing template.");
throw new XamlLoadException($"Could not find property '{effectiveOwner.Name}.{propertyName}'.");
}
var property = AvaloniaPropertyRegistry.Instance.FindRegistered(ownerType, propertyName);
if (property == null)
if (effectiveOwner != targetType &&
!property.IsAttached &&
!registry.IsRegistered(targetType, property))
{
throw new XamlLoadException($"Could not find AvaloniaProperty '{ownerType.Name}.{propertyName}'.");
Logger.Warning(
LogArea.Property,
this,
"Property '{Owner}.{Name}' is not registered on '{Type}'.",
effectiveOwner,
propertyName,
targetType);
}
return property;
}
private Type TryResolveOwnerByName(ITypeDescriptorContext context, string owner)
private Type TryResolveOwnerByName(ITypeDescriptorContext context, string ns, string owner)
{
if (owner != null)
{
var resolver = context.GetService<IXamlTypeResolver>();
var result = resolver.Resolve(owner);
var result = context.ResolveType(ns, owner);
if (result == null)
{
throw new XamlLoadException($"Could not find type '{owner}'.");
var name = string.IsNullOrEmpty(ns) ? owner : $"{ns}:{owner}";
throw new XamlLoadException($"Could not find type '{name}'.");
}
return result;
@ -64,19 +73,5 @@ namespace Avalonia.Markup.Xaml.Converters
return null;
}
private (string owner, string property) ParseProperty(string s)
{
var result = regex.Match(s);
if (result.Groups[1].Success)
{
return (result.Groups[1].Value, result.Groups[2].Value);
}
else
{
return (null, result.Groups[3].Value);
}
}
}
}
}

2
src/Markup/Avalonia.Markup.Xaml/Converters/SelectorTypeConverter.cs

@ -19,7 +19,7 @@ namespace Avalonia.Markup.Xaml.Converters
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
var parser = new SelectorParser((t, ns) => context.ResolveType(ns, t));
var parser = new SelectorParser(context.ResolveType);
return parser.Parse((string)value);
}

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

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

85
src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs

@ -0,0 +1,85 @@
using System;
using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using Avalonia.Utilities;
namespace Avalonia.Markup.Xaml.Parsers
{
internal class PropertyParser
{
public (string ns, string owner, string name) Parse(CharacterReader r)
{
if (r.End)
{
throw new ExpressionParseException(0, "Expected property name.");
}
var openParens = r.TakeIf('(');
bool closeParens = false;
string ns = null;
string owner = null;
string name = null;
do
{
var token = IdentifierParser.Parse(r);
if (token == null)
{
if (r.End)
{
break;
}
else
{
if (openParens && !r.End && (closeParens = r.TakeIf(')')))
{
break;
}
else if (openParens)
{
throw new ExpressionParseException(r.Position, $"Expected ')'.");
}
throw new ExpressionParseException(r.Position, $"Unexpected '{r.Peek}'.");
}
}
else if (!r.End && r.TakeIf(':'))
{
ns = ns == null ?
token :
throw new ExpressionParseException(r.Position, "Unexpected ':'.");
}
else if (!r.End && r.TakeIf('.'))
{
owner = owner == null ?
token :
throw new ExpressionParseException(r.Position, "Unexpected '.'.");
}
else
{
name = token;
}
} while (!r.End);
if (name == null)
{
throw new ExpressionParseException(0, "Expected property name.");
}
else if (openParens && owner == null)
{
throw new ExpressionParseException(1, "Expected property owner.");
}
else if (openParens && !closeParens)
{
throw new ExpressionParseException(r.Position, "Expected ')'.");
}
else if (!r.End)
{
throw new ExpressionParseException(r.Position, "Expected end of expression.");
}
return (ns, owner, name);
}
}
}

4
src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaRuntimeTypeProvider.cs

@ -7,7 +7,6 @@ using System.Linq;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Markup.Data;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media;
using Avalonia.Metadata;
@ -33,6 +32,7 @@ namespace Avalonia.Markup.Xaml.Context
private static readonly IEnumerable<Assembly> ForcedAssemblies = new[]
{
typeof(AvaloniaObject).GetTypeInfo().Assembly,
typeof(Animation.Animation).GetTypeInfo().Assembly,
typeof(Control).GetTypeInfo().Assembly,
typeof(Style).GetTypeInfo().Assembly,
typeof(DataTemplate).GetTypeInfo().Assembly,
@ -146,4 +146,4 @@ namespace Avalonia.Markup.Xaml.Context
return null;
}
}
}
}

3
src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs

@ -4,6 +4,7 @@
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using System;
using System.Reactive.Linq;
@ -37,7 +38,7 @@ namespace Avalonia.Markup.Xaml.Templates
return o;
}
var expression = new ExpressionObserver(o, MemberName);
var expression = ExpressionObserverBuilder.Build(o, MemberName);
object result = AvaloniaProperty.UnsetValue;
expression.Subscribe(x => result = x);

3
src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs

@ -8,6 +8,7 @@ using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Markup.Data;
using Avalonia.Markup.Parsers;
using Avalonia.Metadata;
namespace Avalonia.Markup.Xaml.Templates
@ -41,7 +42,7 @@ namespace Avalonia.Markup.Xaml.Templates
{
if (ItemsSource != null)
{
var obs = new ExpressionObserver(item, ItemsSource.Path);
var obs = ExpressionObserverBuilder.Build(item, ItemsSource.Path);
return InstancedBinding.OneWay(obs, BindingPriority.Style);
}

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

@ -8,6 +8,7 @@ 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;
@ -85,6 +86,11 @@ namespace Avalonia.Data
public WeakReference DefaultAnchor { get; set; }
/// <summary>
/// Gets or sets a function used to resolve types from names in the binding path.
/// </summary>
public Func<string, string, Type> TypeResolver { get; set; }
/// <inheritdoc/>
public InstancedBinding Initiate(
IAvaloniaObject target,
@ -99,34 +105,53 @@ namespace Avalonia.Data
ExpressionObserver observer;
var (node, mode) = ExpressionObserverBuilder.Parse(Path, enableDataValidation, TypeResolver);
if (ElementName != null)
{
observer = CreateElementObserver(
(target as IStyledElement) ?? (anchor as IStyledElement),
ElementName,
Path,
enableDataValidation);
node);
}
else if (Source != null)
{
observer = CreateSourceObserver(Source, Path, enableDataValidation);
observer = CreateSourceObserver(Source, node);
}
else if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext)
else if (RelativeSource == null)
{
if (mode == SourceMode.Data)
{
observer = CreateDataContextObserver(
target,
node,
targetProperty == StyledElement.DataContextProperty,
anchor);
}
else
{
observer = new ExpressionObserver(
(target as IStyledElement) ?? (anchor as IStyledElement),
node);
}
}
else if (RelativeSource.Mode == RelativeSourceMode.DataContext)
{
observer = CreateDataContextObserver(
target,
Path,
node,
targetProperty == StyledElement.DataContextProperty,
anchor,
enableDataValidation);
anchor);
}
else if (RelativeSource.Mode == RelativeSourceMode.Self)
{
observer = CreateSourceObserver(target, Path, enableDataValidation);
observer = CreateSourceObserver(target, node);
}
else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent)
{
observer = CreateTemplatedParentObserver(target, Path, enableDataValidation);
observer = CreateTemplatedParentObserver(
(target as IStyledElement) ?? (anchor as IStyledElement),
node);
}
else if (RelativeSource.Mode == RelativeSourceMode.FindAncestor)
{
@ -138,8 +163,7 @@ namespace Avalonia.Data
observer = CreateFindAncestorObserver(
(target as IStyledElement) ?? (anchor as IStyledElement),
RelativeSource,
Path,
enableDataValidation);
node);
}
else
{
@ -170,10 +194,9 @@ namespace Avalonia.Data
private ExpressionObserver CreateDataContextObserver(
IAvaloniaObject target,
string path,
ExpressionNode node,
bool targetIsDataContext,
object anchor,
bool enableDataValidation)
object anchor)
{
Contract.Requires<ArgumentNullException>(target != null);
@ -191,9 +214,9 @@ namespace Avalonia.Data
{
var result = new ExpressionObserver(
() => target.GetValue(StyledElement.DataContextProperty),
path,
node,
new UpdateSignal(target, StyledElement.DataContextProperty),
enableDataValidation);
null);
return result;
}
@ -201,33 +224,29 @@ namespace Avalonia.Data
{
return new ExpressionObserver(
GetParentDataContext(target),
path,
enableDataValidation);
node,
null);
}
}
private ExpressionObserver CreateElementObserver(
IStyledElement target,
string elementName,
string path,
bool enableDataValidation)
ExpressionNode node)
{
Contract.Requires<ArgumentNullException>(target != null);
var description = $"#{elementName}.{path}";
var result = new ExpressionObserver(
ControlLocator.Track(target, elementName),
path,
enableDataValidation,
description);
node,
null);
return result;
}
private ExpressionObserver CreateFindAncestorObserver(
IStyledElement target,
RelativeSource relativeSource,
string path,
bool enableDataValidation)
ExpressionNode node)
{
Contract.Requires<ArgumentNullException>(target != null);
@ -253,32 +272,30 @@ namespace Avalonia.Data
return new ExpressionObserver(
controlLocator,
path,
enableDataValidation);
node,
null);
}
private ExpressionObserver CreateSourceObserver(
object source,
string path,
bool enableDataValidation)
ExpressionNode node)
{
Contract.Requires<ArgumentNullException>(source != null);
return new ExpressionObserver(source, path, enableDataValidation);
return new ExpressionObserver(source, node);
}
private ExpressionObserver CreateTemplatedParentObserver(
IAvaloniaObject target,
string path,
bool enableDataValidation)
ExpressionNode node)
{
Contract.Requires<ArgumentNullException>(target != null);
var result = new ExpressionObserver(
() => target.GetValue(StyledElement.TemplatedParentProperty),
path,
node,
new UpdateSignal(target, StyledElement.TemplatedParentProperty),
enableDataValidation);
null);
return result;
}

6
src/Markup/Avalonia.Markup/Data/TemplateBinding.cs

@ -100,7 +100,9 @@ namespace Avalonia.Data
CultureInfo.CurrentCulture);
}
_target.TemplatedParent.SetValue(Property, value, BindingPriority.TemplatedParent);
// Use LocalValue priority here, as TemplatedParent doesn't make sense on controls
// that aren't template children.
_target.TemplatedParent.SetValue(Property, value, BindingPriority.LocalValue);
}
}
@ -171,7 +173,7 @@ namespace Avalonia.Data
{
if (e.Property == Property)
{
PublishNext(_target.TemplatedParent.GetValue(Property));
PublishValue();
}
}
}

14
src/Avalonia.Base/Data/Core/Parsers/ArgumentListParser.cs → src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs

@ -1,15 +1,17 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Data.Core;
using Avalonia.Utilities;
using System;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Data.Core.Parsers
namespace Avalonia.Markup.Parsers
{
internal static class ArgumentListParser
{
public static IList<string> Parse(Reader r, char open, char close)
public static IList<string> Parse(CharacterReader r, char open, char close, char delimiter = ',')
{
if (r.Peek == open)
{
@ -20,7 +22,7 @@ namespace Avalonia.Data.Core.Parsers
while (!r.End)
{
var builder = new StringBuilder();
while (!r.End && r.Peek != ',' && r.Peek != close && !char.IsWhiteSpace(r.Peek))
while (!r.End && r.Peek != delimiter && r.Peek != close && !char.IsWhiteSpace(r.Peek))
{
builder.Append(r.Take());
}
@ -34,7 +36,7 @@ namespace Avalonia.Data.Core.Parsers
if (r.End)
{
throw new ExpressionParseException(r.Position, "Expected ','.");
throw new ExpressionParseException(r.Position, $"Expected '{delimiter}'.");
}
else if (r.TakeIf(close))
{
@ -42,9 +44,9 @@ namespace Avalonia.Data.Core.Parsers
}
else
{
if (r.Take() != ',')
if (r.Take() != delimiter)
{
throw new ExpressionParseException(r.Position, "Expected ','.");
throw new ExpressionParseException(r.Position, $"Expected '{delimiter}'.");
}
r.SkipWhitespace();

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

@ -0,0 +1,76 @@
using Avalonia.Data.Core;
using Avalonia.Utilities;
using System;
using System.Collections.Generic;
using System.Reactive;
using System.Text;
namespace Avalonia.Markup.Parsers
{
public static class ExpressionObserverBuilder
{
internal static (ExpressionNode Node, SourceMode Mode) Parse(string expression, bool enableValidation = false, Func<string, string, Type> typeResolver = null)
{
if (string.IsNullOrWhiteSpace(expression))
{
return (new EmptyExpressionNode(), default);
}
var reader = new CharacterReader(expression);
var parser = new ExpressionParser(enableValidation, typeResolver);
var node = parser.Parse(reader);
if (!reader.End)
{
throw new ExpressionParseException(reader.Position, "Expected end of expression.");
}
return node;
}
public static ExpressionObserver Build(
object root,
string expression,
bool enableDataValidation = false,
string description = null,
Func<string, string, Type> typeResolver = null)
{
return new ExpressionObserver(
root,
Parse(expression, enableDataValidation, typeResolver).Node,
description ?? expression);
}
public static ExpressionObserver Build(
IObservable<object> rootObservable,
string expression,
bool enableDataValidation = false,
string description = null,
Func<string, string, Type> typeResolver = null)
{
Contract.Requires<ArgumentNullException>(rootObservable != null);
return new ExpressionObserver(
rootObservable,
Parse(expression, enableDataValidation, typeResolver).Node,
description ?? expression);
}
public static ExpressionObserver Build(
Func<object> rootGetter,
string expression,
IObservable<Unit> update,
bool enableDataValidation = false,
string description = null,
Func<string, string, Type> typeResolver = null)
{
Contract.Requires<ArgumentNullException>(rootGetter != null);
return new ExpressionObserver(
rootGetter,
Parse(expression, enableDataValidation, typeResolver).Node,
update,
description ?? expression);
}
}
}

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

@ -0,0 +1,330 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Data.Core;
using Avalonia.Markup.Parsers.Nodes;
using Avalonia.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Avalonia.Markup.Parsers
{
internal enum SourceMode
{
Data,
Control
}
internal class ExpressionParser
{
private readonly bool _enableValidation;
private readonly Func<string, string, Type> _typeResolver;
public ExpressionParser(bool enableValidation, Func<string, string, Type> typeResolver)
{
_typeResolver = typeResolver;
_enableValidation = enableValidation;
}
public (ExpressionNode Node, SourceMode Mode) Parse(CharacterReader r)
{
var nodes = new List<ExpressionNode>();
var state = State.Start;
var mode = SourceMode.Data;
while (!r.End && state != State.End)
{
switch (state)
{
case State.Start:
state = ParseStart(r, nodes);
break;
case State.AfterMember:
state = ParseAfterMember(r, nodes);
break;
case State.BeforeMember:
state = ParseBeforeMember(r, nodes);
break;
case State.AttachedProperty:
state = ParseAttachedProperty(r, nodes);
break;
case State.Indexer:
state = ParseIndexer(r, nodes);
break;
case State.ElementName:
state = ParseElementName(r, nodes);
mode = SourceMode.Control;
break;
case State.RelativeSource:
state = ParseRelativeSource(r, nodes);
mode = SourceMode.Control;
break;
}
}
if (state == State.BeforeMember)
{
throw new ExpressionParseException(r.Position, "Unexpected end of expression.");
}
for (int n = 0; n < nodes.Count - 1; ++n)
{
nodes[n].Next = nodes[n + 1];
}
return (nodes.FirstOrDefault(), mode);
}
private State ParseStart(CharacterReader r, IList<ExpressionNode> nodes)
{
if (ParseNot(r))
{
nodes.Add(new LogicalNotNode());
return State.Start;
}
else if (ParseSharp(r))
{
return State.ElementName;
}
else if (ParseDollarSign(r))
{
return State.RelativeSource;
}
else if (ParseOpenBrace(r))
{
return State.AttachedProperty;
}
else if (PeekOpenBracket(r))
{
return State.Indexer;
}
else
{
var identifier = IdentifierParser.Parse(r);
if (identifier != null)
{
nodes.Add(new PropertyAccessorNode(identifier, _enableValidation));
return State.AfterMember;
}
}
return State.End;
}
private static State ParseAfterMember(CharacterReader r, IList<ExpressionNode> nodes)
{
if (ParseMemberAccessor(r))
{
return State.BeforeMember;
}
else if (ParseStreamOperator(r))
{
nodes.Add(new StreamNode());
return State.AfterMember;
}
else if (PeekOpenBracket(r))
{
return State.Indexer;
}
return State.End;
}
private State ParseBeforeMember(CharacterReader r, IList<ExpressionNode> nodes)
{
if (ParseOpenBrace(r))
{
return State.AttachedProperty;
}
else
{
var identifier = IdentifierParser.Parse(r);
if (identifier != null)
{
nodes.Add(new PropertyAccessorNode(identifier, _enableValidation));
return State.AfterMember;
}
return State.End;
}
}
private State ParseAttachedProperty(CharacterReader r, List<ExpressionNode> nodes)
{
var (ns, owner) = ParseTypeName(r);
if (r.End || !r.TakeIf('.'))
{
throw new ExpressionParseException(r.Position, "Invalid attached property name.");
}
var name = IdentifierParser.Parse(r);
if (r.End || !r.TakeIf(')'))
{
throw new ExpressionParseException(r.Position, "Expected ')'.");
}
if (_typeResolver == null)
{
throw new InvalidOperationException("Cannot parse a binding path with an attached property without a type resolver. Maybe you can use a LINQ Expression binding path instead?");
}
var property = AvaloniaPropertyRegistry.Instance.FindRegistered(_typeResolver(ns, owner), name);
nodes.Add(new AvaloniaPropertyAccessorNode(property, _enableValidation));
return State.AfterMember;
}
private State ParseIndexer(CharacterReader r, List<ExpressionNode> nodes)
{
var args = ArgumentListParser.Parse(r, '[', ']');
if (args.Count == 0)
{
throw new ExpressionParseException(r.Position, "Indexer may not be empty.");
}
nodes.Add(new StringIndexerNode(args));
return State.AfterMember;
}
private State ParseElementName(CharacterReader r, List<ExpressionNode> nodes)
{
var name = IdentifierParser.Parse(r);
if (name == null)
{
throw new ExpressionParseException(r.Position, "Element name expected after '#'.");
}
nodes.Add(new ElementNameNode(name));
return State.AfterMember;
}
private State ParseRelativeSource(CharacterReader r, List<ExpressionNode> nodes)
{
var mode = IdentifierParser.Parse(r);
if (mode == "self")
{
nodes.Add(new SelfNode());
}
else if (mode == "parent")
{
Type ancestorType = null;
var ancestorLevel = 0;
if (PeekOpenBracket(r))
{
var args = ArgumentListParser.Parse(r, '[', ']', ';');
if (args.Count > 2 || args.Count == 0)
{
throw new ExpressionParseException(r.Position, "Too many arguments in RelativeSource syntax sugar");
}
else if (args.Count == 1)
{
if (int.TryParse(args[0], out int level))
{
ancestorType = null;
ancestorLevel = level;
}
else
{
var typeName = ParseTypeName(new CharacterReader(args[0]));
ancestorType = _typeResolver(typeName.ns, typeName.typeName);
}
}
else
{
var typeName = ParseTypeName(new CharacterReader(args[0]));
ancestorType = _typeResolver(typeName.ns, typeName.typeName);
ancestorLevel = int.Parse(args[1]);
}
}
nodes.Add(new FindAncestorNode(ancestorType, ancestorLevel));
}
else
{
throw new ExpressionParseException(r.Position, "Unknown RelativeSource mode.");
}
return State.AfterMember;
}
private static (string ns, string typeName) ParseTypeName(CharacterReader r)
{
string ns, typeName;
ns = string.Empty;
var typeNameOrNamespace = IdentifierParser.Parse(r);
if (!r.End && r.TakeIf(':'))
{
ns = typeNameOrNamespace;
typeName = IdentifierParser.Parse(r);
}
else
{
typeName = typeNameOrNamespace;
}
return (ns, typeName);
}
private static bool ParseNot(CharacterReader r)
{
return !r.End && r.TakeIf('!');
}
private static bool ParseMemberAccessor(CharacterReader r)
{
return !r.End && r.TakeIf('.');
}
private static bool ParseOpenBrace(CharacterReader r)
{
return !r.End && r.TakeIf('(');
}
private static bool PeekOpenBracket(CharacterReader r)
{
return !r.End && r.Peek == '[';
}
private static bool ParseStreamOperator(CharacterReader r)
{
return !r.End && r.TakeIf('^');
}
private static bool ParseDollarSign(CharacterReader r)
{
return !r.End && r.TakeIf('$');
}
private static bool ParseSharp(CharacterReader r)
{
return !r.End && r.TakeIf('#');
}
private enum State
{
Start,
RelativeSource,
ElementName,
AfterMember,
BeforeMember,
AttachedProperty,
Indexer,
End,
}
}
}

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

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

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

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

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

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

88
src/Avalonia.Base/Data/Core/IndexerNode.cs → src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs

@ -12,53 +12,19 @@ using System.Linq;
using System.Reflection;
using System.Reactive.Linq;
using Avalonia.Data;
using Avalonia.Data.Core;
namespace Avalonia.Data.Core
namespace Avalonia.Markup.Parsers.Nodes
{
internal class IndexerNode : SettableNode
internal class StringIndexerNode : IndexerNodeBase
{
private IDisposable _subscription;
public IndexerNode(IList<string> arguments)
public StringIndexerNode(IList<string> arguments)
{
Arguments = arguments;
}
public override string Description => "[" + string.Join(",", Arguments) + "]";
protected override void StartListeningCore(WeakReference reference)
{
var target = reference.Target;
var incc = target as INotifyCollectionChanged;
var inpc = target as INotifyPropertyChanged;
var inputs = new List<IObservable<object>>();
if (incc != null)
{
inputs.Add(WeakObservable.FromEventPattern<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>(
incc,
nameof(incc.CollectionChanged))
.Where(x => ShouldUpdate(x.Sender, x.EventArgs))
.Select(_ => GetValue(target)));
}
if (inpc != null)
{
inputs.Add(WeakObservable.FromEventPattern<INotifyPropertyChanged, PropertyChangedEventArgs>(
inpc,
nameof(inpc.PropertyChanged))
.Where(x => ShouldUpdate(x.Sender, x.EventArgs))
.Select(_ => GetValue(target)));
}
_subscription = Observable.Merge(inputs).StartWith(GetValue(target)).Subscribe(ValueChanged);
}
protected override void StopListeningCore()
{
_subscription.Dispose();
}
protected override bool SetTargetValueCore(object value, BindingPriority priority)
{
var typeInfo = Target.Target.GetType().GetTypeInfo();
@ -163,7 +129,7 @@ namespace Avalonia.Data.Core
public override Type PropertyType => GetIndexer(Target.Target.GetType().GetTypeInfo())?.PropertyType;
private object GetValue(object target)
protected override object GetValue(object target)
{
var typeInfo = target.GetType().GetTypeInfo();
var list = target as IList;
@ -316,45 +282,19 @@ namespace Avalonia.Data.Core
}
}
private bool ShouldUpdate(object sender, NotifyCollectionChangedEventArgs e)
protected override bool ShouldUpdate(object sender, PropertyChangedEventArgs e)
{
if (sender is IList)
{
object indexObject;
if (!TypeUtilities.TryConvert(typeof(int), Arguments[0], CultureInfo.InvariantCulture, out indexObject))
{
return false;
}
var index = (int)indexObject;
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 true; // Implementation defined meaning for the index, so just try to update anyway
var typeInfo = sender.GetType().GetTypeInfo();
return typeInfo.GetDeclaredProperty(e.PropertyName)?.GetIndexParameters().Any() ?? false;
}
private bool ShouldUpdate(object sender, PropertyChangedEventArgs e)
protected override int? TryGetFirstArgumentAsInt()
{
var typeInfo = sender.GetType().GetTypeInfo();
return typeInfo.GetDeclaredProperty(e.PropertyName)?.GetIndexParameters().Any() ?? false;
if (TypeUtilities.TryConvert(typeof(int), Arguments[0], CultureInfo.InvariantCulture, out var value))
{
return (int?)value;
}
return null;
}
}
}

4
src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs

@ -52,11 +52,11 @@ namespace Avalonia.Markup.Parsers
if (ofType != null)
{
result = result.OfType(_typeResolver(ofType.TypeName, ofType.Xmlns));
result = result.OfType(_typeResolver(ofType.Xmlns, ofType.TypeName));
}
if (@is != null)
{
result = result.Is(_typeResolver(@is.TypeName, @is.Xmlns));
result = result.Is(_typeResolver(@is.Xmlns, @is.TypeName));
}
else if (@class != null)
{

4
src/OSX/Avalonia.MonoMac/SystemDialogsImpl.cs

@ -25,9 +25,9 @@ namespace Avalonia.MonoMac
else
{
if (panel is NSOpenPanel openPanel)
tcs.SetResult(openPanel.Urls.Select(url => url.AbsoluteString).ToArray());
tcs.SetResult(openPanel.Urls.Select(url => url.AbsoluteString.Replace("file://", "")).ToArray());
else
tcs.SetResult(new[] { panel.Url.AbsoluteString });
tcs.SetResult(new[] { panel.Url.AbsoluteString.Replace("file://", "") });
}
panel.OrderOut(panel);
keyWindow?.MakeKeyAndOrderFront(keyWindow);

10
src/OSX/Avalonia.MonoMac/WindowImpl.cs

@ -3,6 +3,7 @@ using Avalonia.Controls;
using Avalonia.Platform;
using MonoMac.AppKit;
using MonoMac.CoreGraphics;
using Avalonia.Threading;
namespace Avalonia.MonoMac
{
@ -16,7 +17,14 @@ namespace Avalonia.MonoMac
public WindowImpl()
{
UpdateStyle();
// Post UpdateStyle to UIThread otherwise for as yet unknown reason.
// The window becomes transparent to mouse clicks except a 100x100 square
// at the top left. (danwalmsley)
Dispatcher.UIThread.Post(() =>
{
UpdateStyle();
});
Window.SetCanBecomeKeyAndMain();
Window.DidResize += delegate

4
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -491,6 +491,10 @@ namespace Avalonia.Skia
{
ConfigureTileBrush(ref paintWrapper, targetSize, tileBrush, tileBrushImage);
}
else
{
paint.Color = new SKColor(255, 255, 255, 0);
}
return paintWrapper;
}

2
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@ -394,7 +394,7 @@ namespace Avalonia.Direct2D1.Media
{
return new RadialGradientBrushImpl(radialGradientBrush, _renderTarget, destinationSize);
}
else if (imageBrush != null)
else if (imageBrush?.Source != null)
{
return new ImageBrushImpl(
imageBrush,

18
src/Windows/Avalonia.Win32/SystemDialogImpl.cs

@ -13,6 +13,9 @@ namespace Avalonia.Win32
class SystemDialogImpl : ISystemDialogImpl
{
private const UnmanagedMethods.FOS DefaultDialogOptions = UnmanagedMethods.FOS.FOS_FORCEFILESYSTEM | UnmanagedMethods.FOS.FOS_NOVALIDATE |
UnmanagedMethods.FOS.FOS_NOTESTFILECREATE | UnmanagedMethods.FOS.FOS_DONTADDTORECENT;
public unsafe Task<string[]> ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent)
{
var hWnd = parent?.Handle?.Handle ?? IntPtr.Zero;
@ -29,7 +32,7 @@ namespace Avalonia.Win32
uint options;
frm.GetOptions(out options);
options |= (uint)(UnmanagedMethods.FOS.FOS_NOVALIDATE | UnmanagedMethods.FOS.FOS_NOTESTFILECREATE | UnmanagedMethods.FOS.FOS_DONTADDTORECENT);
options |= (uint)(DefaultDialogOptions);
if (openDialog?.AllowMultiple == true)
options |= (uint)UnmanagedMethods.FOS.FOS_ALLOWMULTISELECT;
frm.SetOptions(options);
@ -37,13 +40,16 @@ namespace Avalonia.Win32
var defaultExtension = (dialog as SaveFileDialog)?.DefaultExtension ?? "";
frm.SetDefaultExtension(defaultExtension);
frm.SetFileName(dialog.InitialFileName ?? "");
frm.SetTitle(dialog.Title);
frm.SetTitle(dialog.Title ?? "");
var filters = new List<UnmanagedMethods.COMDLG_FILTERSPEC>();
foreach (var filter in dialog.Filters)
if (dialog.Filters != null)
{
var extMask = string.Join(";", filter.Extensions.Select(e => "*." + e));
filters.Add(new UnmanagedMethods.COMDLG_FILTERSPEC { pszName = filter.Name, pszSpec = extMask });
foreach (var filter in dialog.Filters)
{
var extMask = string.Join(";", filter.Extensions.Select(e => "*." + e));
filters.Add(new UnmanagedMethods.COMDLG_FILTERSPEC { pszName = filter.Name, pszSpec = extMask });
}
}
if (filters.Count == 0)
filters.Add(new UnmanagedMethods.COMDLG_FILTERSPEC { pszName = "All files", pszSpec = "*.*" });
@ -106,7 +112,7 @@ namespace Avalonia.Win32
var frm = (UnmanagedMethods.IFileDialog)unk;
uint options;
frm.GetOptions(out options);
options |= (uint)(UnmanagedMethods.FOS.FOS_PICKFOLDERS | UnmanagedMethods.FOS.FOS_FORCEFILESYSTEM | UnmanagedMethods.FOS.FOS_NOVALIDATE | UnmanagedMethods.FOS.FOS_NOTESTFILECREATE | UnmanagedMethods.FOS.FOS_DONTADDTORECENT);
options |= (uint)(UnmanagedMethods.FOS.FOS_PICKFOLDERS | DefaultDialogOptions);
frm.SetOptions(options);
if (dialog.InitialDirectory != null)

11
tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs

@ -99,17 +99,6 @@ namespace Avalonia.Base.UnitTests
Assert.Null(result);
}
[Fact]
public void FindRegisteredAttached_Finds_Property()
{
var result = AvaloniaPropertyRegistry.Instance.FindRegisteredAttached(
typeof(Class1),
typeof(AttachedOwner),
"Attached");
Assert.Equal(AttachedOwner.AttachedProperty, result);
}
private class Class1 : AvaloniaObject
{
public static readonly StyledProperty<string> FooProperty =

43
tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs

@ -10,6 +10,7 @@ using System.Threading.Tasks;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using Avalonia.UnitTests;
using Moq;
using Xunit;
@ -22,7 +23,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_Simple_Property_Value()
{
var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(string));
var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(string));
var result = await target.Take(1);
Assert.Equal("foo", result);
@ -34,7 +35,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Set_Simple_Property_Value()
{
var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(string));
var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(string));
target.OnNext("bar");
@ -47,7 +48,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Set_Indexed_Value()
{
var data = new { Foo = new[] { "foo" } };
var target = new BindingExpression(new ExpressionObserver(data, "Foo[0]"), typeof(string));
var target = new BindingExpression(ExpressionObserver.Create(data, o => o.Foo[0]), typeof(string));
target.OnNext("bar");
@ -60,7 +61,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Convert_Get_String_To_Double()
{
var data = new Class1 { StringValue = $"{5.6}" };
var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(double));
var result = await target.Take(1);
Assert.Equal(5.6, result);
@ -72,7 +73,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Getting_Invalid_Double_String_Should_Return_BindingError()
{
var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(double));
var result = await target.Take(1);
Assert.IsType<BindingNotification>(result);
@ -84,7 +85,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Coerce_Get_Null_Double_String_To_UnsetValue()
{
var data = new Class1 { StringValue = null };
var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(double));
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
@ -96,7 +97,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Convert_Set_String_To_Double()
{
var data = new Class1 { StringValue = $"{5.6}" };
var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(double));
target.OnNext(6.7);
@ -109,7 +110,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Convert_Get_Double_To_String()
{
var data = new Class1 { DoubleValue = 5.6 };
var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string));
var result = await target.Take(1);
Assert.Equal($"{5.6}", result);
@ -121,7 +122,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Convert_Set_Double_To_String()
{
var data = new Class1 { DoubleValue = 5.6 };
var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string));
target.OnNext($"{6.7}");
@ -135,7 +136,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(
new ExpressionObserver(data, "StringValue"),
ExpressionObserver.Create(data, o => o.StringValue),
typeof(int),
42,
DefaultValueConverter.Instance);
@ -156,7 +157,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(
new ExpressionObserver(data, "StringValue", true),
ExpressionObserver.Create(data, o => o.StringValue, true),
typeof(int),
42,
DefaultValueConverter.Instance);
@ -177,7 +178,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(
new ExpressionObserver(data, "StringValue"),
ExpressionObserver.Create(data, o => o.StringValue),
typeof(int),
"bar",
DefaultValueConverter.Instance);
@ -199,7 +200,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(
new ExpressionObserver(data, "StringValue", true),
ExpressionObserver.Create(data, o => o.StringValue, true),
typeof(int),
"bar",
DefaultValueConverter.Instance);
@ -220,7 +221,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Setting_Invalid_Double_String_Should_Not_Change_Target()
{
var data = new Class1 { DoubleValue = 5.6 };
var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string));
target.OnNext("foo");
@ -234,7 +235,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
var data = new Class1 { DoubleValue = 5.6 };
var target = new BindingExpression(
new ExpressionObserver(data, "DoubleValue"),
ExpressionObserver.Create(data, o => o.DoubleValue),
typeof(string),
"9.8",
DefaultValueConverter.Instance);
@ -250,7 +251,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Coerce_Setting_Null_Double_To_Default_Value()
{
var data = new Class1 { DoubleValue = 5.6 };
var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string));
target.OnNext(null);
@ -263,7 +264,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Coerce_Setting_UnsetValue_Double_To_Default_Value()
{
var data = new Class1 { DoubleValue = 5.6 };
var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string));
target.OnNext(AvaloniaProperty.UnsetValue);
@ -279,7 +280,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
var converter = new Mock<IValueConverter>();
var target = new BindingExpression(
new ExpressionObserver(data, "DoubleValue"),
ExpressionObserver.Create(data, o => o.DoubleValue),
typeof(string),
converter.Object,
converterParameter: "foo");
@ -297,7 +298,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
var data = new Class1 { DoubleValue = 5.6 };
var converter = new Mock<IValueConverter>();
var target = new BindingExpression(
new ExpressionObserver(data, "DoubleValue"),
ExpressionObserver.Create(data, o => o.DoubleValue),
typeof(string),
converter.Object,
converterParameter: "foo");
@ -314,7 +315,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
var data = new Class1 { DoubleValue = 5.6 };
var converter = new Mock<IValueConverter>();
var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue", true), typeof(string));
var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue, true), typeof(string));
var result = new List<object>();
target.Subscribe(x => result.Add(x));
@ -341,7 +342,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Second_Subscription_Should_Fire_Immediately()
{
var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(string));
var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(string));
object result = null;
target.Subscribe();

30
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AttachedProperty.cs

@ -13,16 +13,12 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
public class ExpressionObserverTests_AttachedProperty
{
public ExpressionObserverTests_AttachedProperty()
{
var foo = Owner.FooProperty;
}
[Fact]
public async Task Should_Get_Attached_Property_Value()
{
var data = new Class1();
var target = new ExpressionObserver(data, "(Owner.Foo)");
var target = ExpressionObserver.Create(data, o => o[Owner.FooProperty]);
var result = await target.Take(1);
Assert.Equal("foo", result);
@ -41,7 +37,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
}
};
var target = new ExpressionObserver(data, "Next.(Owner.Foo)");
var target = ExpressionObserver.Create(data, o => o.Next[Owner.FooProperty]);
var result = await target.Take(1);
Assert.Equal("bar", result);
@ -53,7 +49,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Track_Simple_Attached_Value()
{
var data = new Class1();
var target = new ExpressionObserver(data, "(Owner.Foo)");
var target = ExpressionObserver.Create(data, o => o[Owner.FooProperty]);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -77,7 +73,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
}
};
var target = new ExpressionObserver(data, "Next.(Owner.Foo)");
var target = ExpressionObserver.Create(data, o => o.Next[Owner.FooProperty]);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -96,7 +92,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
Func<Tuple<ExpressionObserver, WeakReference>> run = () =>
{
var source = new Class1();
var target = new ExpressionObserver(source, "(Owner.Foo)");
var target = ExpressionObserver.Create(source, o => o.Next[Owner.FooProperty]);
return Tuple.Create(target, new WeakReference(source));
};
@ -108,22 +104,6 @@ namespace Avalonia.Base.UnitTests.Data.Core
Assert.Null(result.Item2.Target);
}
[Fact]
public void Should_Fail_With_Attached_Property_With_Only_1_Part()
{
var data = new Class1();
Assert.Throws<ExpressionParseException>(() => new ExpressionObserver(data, "(Owner)"));
}
[Fact]
public void Should_Fail_With_Attached_Property_With_More_Than_2_Parts()
{
var data = new Class1();
Assert.Throws<ExpressionParseException>(() => new ExpressionObserver(data, "(Owner.Foo.Bar)"));
}
private static class Owner
{
public static readonly AttachedProperty<string> FooProperty =

11
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AvaloniaProperty.cs

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Avalonia.Diagnostics;
using Avalonia.Data.Core;
using Xunit;
using Avalonia.Markup.Parsers;
namespace Avalonia.Base.UnitTests.Data.Core
{
@ -22,7 +23,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_Simple_Property_Value()
{
var data = new Class1();
var target = new ExpressionObserver(data, "Foo");
var target = ExpressionObserver.Create(data, o => o.Foo);
var result = await target.Take(1);
Assert.Equal("foo", result);
@ -34,7 +35,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_Simple_ClrProperty_Value()
{
var data = new Class1();
var target = new ExpressionObserver(data, "ClrProperty");
var target = ExpressionObserver.Create(data, o => o.ClrProperty);
var result = await target.Take(1);
Assert.Equal("clr-property", result);
@ -44,7 +45,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Track_Simple_Property_Value()
{
var data = new Class1();
var target = new ExpressionObserver(data, "Foo");
var target = ExpressionObserver.Create(data, o => o.Foo);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -63,7 +64,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
Func<Tuple<ExpressionObserver, WeakReference>> run = () =>
{
var source = new Class1();
var target = new ExpressionObserver(source, "Foo");
var target = ExpressionObserver.Create(source, o => o.Foo);
return Tuple.Create(target, new WeakReference(source));
};
@ -80,6 +81,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
public static readonly StyledProperty<string> FooProperty =
AvaloniaProperty.Register<Class1, string>("Foo", defaultValue: "foo");
public string Foo { get => GetValue(FooProperty); set => SetValue(FooProperty, value); }
public string ClrProperty { get; } = "clr-property";
}
}

32
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs

@ -8,6 +8,7 @@ using System.Linq;
using System.Reactive.Linq;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using Avalonia.UnitTests;
using Xunit;
@ -19,7 +20,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Doesnt_Send_DataValidationError_When_DataValidatation_Not_Enabled()
{
var data = new ExceptionTest { MustBePositive = 5 };
var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false);
var observer = ExpressionObserver.Create(data, o => o.MustBePositive, false);
var validationMessageFound = false;
observer.OfType<BindingNotification>()
@ -36,7 +37,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Exception_Validation_Sends_DataValidationError()
{
var data = new ExceptionTest { MustBePositive = 5 };
var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true);
var observer = ExpressionObserver.Create(data, o => o.MustBePositive, true);
var validationMessageFound = false;
observer.OfType<BindingNotification>()
@ -53,7 +54,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Indei_Validation_Does_Not_Subscribe_When_DataValidatation_Not_Enabled()
{
var data = new IndeiTest { MustBePositive = 5 };
var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false);
var observer = ExpressionObserver.Create(data, o => o.MustBePositive, false);
observer.Subscribe(_ => { });
@ -64,7 +65,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Enabled_Indei_Validation_Subscribes()
{
var data = new IndeiTest { MustBePositive = 5 };
var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true);
var observer = ExpressionObserver.Create(data, o => o.MustBePositive, true);
var sub = observer.Subscribe(_ => { });
Assert.Equal(1, data.ErrorsChangedSubscriptionCount);
@ -76,7 +77,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Validation_Plugins_Send_Correct_Notifications()
{
var data = new IndeiTest();
var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true);
var observer = ExpressionObserver.Create(data, o => o.MustBePositive, true);
var result = new List<object>();
var errmsg = string.Empty;
@ -122,10 +123,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
Inner = new IndeiTest()
};
var observer = new ExpressionObserver(
data,
$"{nameof(Container.Inner)}.{nameof(IndeiTest.MustBePositive)}",
true);
var observer = ExpressionObserver.Create(data, o => o.Inner.MustBePositive, true);
observer.Subscribe(_ => { });
@ -133,19 +131,16 @@ namespace Avalonia.Base.UnitTests.Data.Core
// intermediate object in a chain so for the moment I'm not sure what the result of
// validating such a thing should look like.
Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
Assert.Equal(1, ((IndeiTest)data.Inner).ErrorsChangedSubscriptionCount);
Assert.Equal(1, data.Inner.ErrorsChangedSubscriptionCount);
}
[Fact]
public void Sends_Correct_Notifications_With_Property_Chain()
{
var container = new Container();
var inner = new IndeiTest();
var observer = new ExpressionObserver(
container,
$"{nameof(Container.Inner)}.{nameof(IndeiTest.MustBePositive)}",
true);
var observer = ExpressionObserver.Create(container, o => o.Inner.MustBePositive, true);
var result = new List<object>();
observer.Subscribe(x => result.Add(x));
@ -153,13 +148,12 @@ namespace Avalonia.Base.UnitTests.Data.Core
Assert.Equal(new[]
{
new BindingNotification(
new MarkupBindingChainException("Null value", "Inner.MustBePositive", "Inner"),
new MarkupBindingChainException("Null value", "o => o.Inner.MustBePositive", "Inner"),
BindingErrorType.Error,
AvaloniaProperty.UnsetValue),
}, result);
GC.KeepAlive(container);
GC.KeepAlive(inner);
}
public class ExceptionTest : NotifyingBase
@ -220,9 +214,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
private class Container : IndeiBase
{
private object _inner;
private IndeiTest _inner;
public object Inner
public IndeiTest Inner
{
get { return _inner; }
set { _inner = value; RaisePropertyChanged(); }

224
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs

@ -0,0 +1,224 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Data.Core;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Base.UnitTests.Data.Core
{
public class ExpressionObserverTests_ExpressionTree
{
[Fact]
public async Task IdentityExpression_Creates_IdentityObserver()
{
var target = new object();
var observer = ExpressionObserver.Create(target, o => o);
Assert.Equal(target, await observer.Take(1));
GC.KeepAlive(target);
}
[Fact]
public async Task Property_Access_Expression_Observes_Property()
{
var target = new Class1();
var observer = ExpressionObserver.Create(target, o => o.Foo);
Assert.Null(await observer.Take(1));
using (observer.Subscribe(_ => {}))
{
target.Foo = "Test";
}
Assert.Equal("Test", await observer.Take(1));
GC.KeepAlive(target);
}
[Fact]
public void Property_Acccess_Expression_Can_Set_Property()
{
var data = new Class1();
var target = ExpressionObserver.Create(data, o => o.Foo);
using (target.Subscribe(_ => { }))
{
Assert.True(target.SetValue("baz"));
}
GC.KeepAlive(data);
}
[Fact]
public async Task Indexer_Accessor_Can_Read_Value()
{
var data = new[] { 1, 2, 3, 4 };
var target = ExpressionObserver.Create(data, o => o[0]);
Assert.Equal(data[0], await target.Take(1));
GC.KeepAlive(data);
}
[Fact]
public async Task Indexer_List_Accessor_Can_Read_Value()
{
var data = new List<int> { 1, 2, 3, 4 };
var target = ExpressionObserver.Create(data, o => o[0]);
Assert.Equal(data[0], await target.Take(1));
GC.KeepAlive(data);
}
[Fact]
public async Task Indexer_Accessor_Can_Read_Complex_Index()
{
var data = new Dictionary<object, object>();
var key = new object();
data.Add(key, new object());
var target = ExpressionObserver.Create(data, o => o[key]);
Assert.Equal(data[key], await target.Take(1));
GC.KeepAlive(data);
}
[Fact]
public void Indexer_Can_Set_Value()
{
var data = new[] { 1, 2, 3, 4 };
var target = ExpressionObserver.Create(data, o => o[0]);
using (target.Subscribe(_ => { }))
{
Assert.True(target.SetValue(2));
}
GC.KeepAlive(data);
}
[Fact]
public async Task Inheritance_Casts_Should_Be_Ignored()
{
NotifyingBase test = new Class1 { Foo = "Test" };
var target = ExpressionObserver.Create(test, o => ((Class1)o).Foo);
Assert.Equal("Test", await target.Take(1));
GC.KeepAlive(test);
}
[Fact]
public void Convert_Casts_Should_Error()
{
var test = 1;
Assert.Throws<ExpressionParseException>(() => ExpressionObserver.Create(test, o => (double)o));
}
[Fact]
public async Task As_Operator_Should_Be_Ignored()
{
NotifyingBase test = new Class1 { Foo = "Test" };
var target = ExpressionObserver.Create(test, o => (o as Class1).Foo);
Assert.Equal("Test", await target.Take(1));
GC.KeepAlive(test);
}
[Fact]
public async Task Avalonia_Property_Indexer_Reads_Avalonia_Property_Value()
{
var test = new Class2();
var target = ExpressionObserver.Create(test, o => o[Class2.FooProperty]);
Assert.Equal("foo", await target.Take(1));
GC.KeepAlive(test);
}
[Fact]
public async Task Complex_Expression_Correctly_Parsed()
{
var test = new Class1 { Foo = "Test" };
var target = ExpressionObserver.Create(test, o => o.Foo.Length);
Assert.Equal(test.Foo.Length, await target.Take(1));
GC.KeepAlive(test);
}
[Fact]
public void Should_Get_Completed_Task_Value()
{
using (var sync = UnitTestSynchronizationContext.Begin())
{
var data = new { Foo = Task.FromResult("foo") };
var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding());
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
Assert.Equal(new[] { "foo" }, result);
GC.KeepAlive(data);
}
}
[Fact]
public async Task Should_Create_Method_Binding()
{
var data = new Class3();
var target = ExpressionObserver.Create(data, o => (Action)o.Method);
var value = await target.Take(1);
Assert.IsAssignableFrom<Delegate>(value);
GC.KeepAlive(data);
}
private class Class1 : NotifyingBase
{
private string _foo;
public string Foo
{
get { return _foo; }
set
{
_foo = value;
RaisePropertyChanged(nameof(Foo));
}
}
}
private class Class2 : AvaloniaObject
{
public static readonly StyledProperty<string> FooProperty =
AvaloniaProperty.Register<Class2, string>("Foo", defaultValue: "foo");
public string ClrProperty { get; } = "clr-property";
}
private class Class3
{
public void Method() { }
}
}
}

89
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs

@ -11,6 +11,7 @@ using Avalonia.Diagnostics;
using Avalonia.Data.Core;
using Avalonia.UnitTests;
using Xunit;
using Avalonia.Markup.Parsers;
namespace Avalonia.Base.UnitTests.Data.Core
{
@ -20,7 +21,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_Array_Value()
{
var data = new { Foo = new [] { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1]");
var target = ExpressionObserver.Create(data, x => x.Foo[1]);
var result = await target.Take(1);
Assert.Equal("bar", result);
@ -28,47 +29,11 @@ namespace Avalonia.Base.UnitTests.Data.Core
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Get_UnsetValue_For_Invalid_Array_Index()
{
var data = new { Foo = new[] { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[invalid]");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Get_UnsetValue_For_Invalid_Dictionary_Index()
{
var data = new { Foo = new Dictionary<int, string> { { 1, "foo" } } };
var target = new ExpressionObserver(data, "Foo[invalid]");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Get_UnsetValue_For_Object_Without_Indexer()
{
var data = new { Foo = 5 };
var target = new ExpressionObserver(data, "Foo[noindexer]");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Get_MultiDimensional_Array_Value()
{
var data = new { Foo = new[,] { { "foo", "bar" }, { "baz", "qux" } } };
var target = new ExpressionObserver(data, "Foo[1, 1]");
var target = ExpressionObserver.Create(data, o => o.Foo[1, 1]);
var result = await target.Take(1);
Assert.Equal("qux", result);
@ -80,7 +45,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_Value_For_String_Indexer()
{
var data = new { Foo = new Dictionary<string, string> { { "foo", "bar" }, { "baz", "qux" } } };
var target = new ExpressionObserver(data, "Foo[foo]");
var target = ExpressionObserver.Create(data, o => o.Foo["foo"]);
var result = await target.Take(1);
Assert.Equal("bar", result);
@ -92,7 +57,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_Value_For_Non_String_Indexer()
{
var data = new { Foo = new Dictionary<double, string> { { 1.0, "bar" }, { 2.0, "qux" } } };
var target = new ExpressionObserver(data, "Foo[1.0]");
var target = ExpressionObserver.Create(data, o => o.Foo[1.0]);
var result = await target.Take(1);
Assert.Equal("bar", result);
@ -104,19 +69,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Array_Out_Of_Bounds_Should_Return_UnsetValue()
{
var data = new { Foo = new[] { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[2]");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async Task Array_With_Wrong_Dimensions_Should_Return_UnsetValue()
{
var data = new { Foo = new[] { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1,2]");
var target = ExpressionObserver.Create(data, o => o.Foo[2]);
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
@ -128,7 +81,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task List_Out_Of_Bounds_Should_Return_UnsetValue()
{
var data = new { Foo = new List<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[2]");
var target = ExpressionObserver.Create(data, o => o.Foo[2]);
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
@ -140,7 +93,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_List_Value()
{
var data = new { Foo = new List<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1]");
var target = ExpressionObserver.Create(data, o => o.Foo[1]);
var result = await target.Take(1);
Assert.Equal("bar", result);
@ -152,7 +105,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Track_INCC_Add()
{
var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[2]");
var target = ExpressionObserver.Create(data, o => o.Foo[2]);
var result = new List<object>();
using (var sub = target.Subscribe(x => result.Add(x)))
@ -170,7 +123,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Track_INCC_Remove()
{
var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[0]");
var target = ExpressionObserver.Create(data, o => o.Foo[0]);
var result = new List<object>();
using (var sub = target.Subscribe(x => result.Add(x)))
@ -188,7 +141,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Track_INCC_Replace()
{
var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1]");
var target = ExpressionObserver.Create(data, o => o.Foo[1]);
var result = new List<object>();
using (var sub = target.Subscribe(x => result.Add(x)))
@ -209,7 +162,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
// method, but even if it did we need to test with ObservableCollection as well
// as AvaloniaList as it implements PropertyChanged as an explicit interface event.
var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1]");
var target = ExpressionObserver.Create(data, o => o.Foo[1]);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -225,7 +178,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Track_INCC_Reset()
{
var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1]");
var target = ExpressionObserver.Create(data, o => o.Foo[1]);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -244,7 +197,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
data.Foo["foo"] = "bar";
data.Foo["baz"] = "qux";
var target = new ExpressionObserver(data, "Foo[foo]");
var target = ExpressionObserver.Create(data, o => o.Foo["foo"]);
var result = new List<object>();
using (var sub = target.Subscribe(x => result.Add(x)))
@ -263,7 +216,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_SetArrayIndex()
{
var data = new { Foo = new[] { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1]");
var target = ExpressionObserver.Create(data, o => o.Foo[1]);
using (target.Subscribe(_ => { }))
{
@ -285,8 +238,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
{"foo", 1 }
}
};
var target = new ExpressionObserver(data, "Foo[foo]");
var target = ExpressionObserver.Create(data, o => o.Foo["foo"]);
using (target.Subscribe(_ => { }))
{
Assert.True(target.SetValue(4));
@ -307,8 +260,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
{"foo", 1 }
}
};
var target = new ExpressionObserver(data, "Foo[bar]");
var target = ExpressionObserver.Create(data, o => o.Foo["bar"]);
using (target.Subscribe(_ => { }))
{
Assert.True(target.SetValue(4));
@ -326,7 +279,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
data.Foo["foo"] = "bar";
data.Foo["baz"] = "qux";
var target = new ExpressionObserver(data, "Foo[foo]");
var target = ExpressionObserver.Create(data, o => o.Foo["foo"]);
using (target.Subscribe(_ => { }))
{
@ -343,7 +296,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
var data = new[] { 1, 2, 3 };
var target = new ExpressionObserver(data, "[1]");
var target = ExpressionObserver.Create(data, o => o[1]);
var value = await target.Take(1);

17
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs

@ -9,6 +9,7 @@ using System.Reactive.Subjects;
using Microsoft.Reactive.Testing;
using Avalonia.Data.Core;
using Xunit;
using Avalonia.Markup.Parsers;
namespace Avalonia.Base.UnitTests.Data.Core
{
@ -18,7 +19,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Complete_When_Source_Observable_Completes()
{
var source = new BehaviorSubject<object>(1);
var target = new ExpressionObserver(source, "Foo");
var target = ExpressionObserver.Create<object, object>(source, o => o);
var completed = false;
target.Subscribe(_ => { }, () => completed = true);
@ -31,7 +32,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Complete_When_Source_Observable_Errors()
{
var source = new BehaviorSubject<object>(1);
var target = new ExpressionObserver(source, "Foo");
var target = ExpressionObserver.Create<object, object>(source, o => o);
var completed = false;
target.Subscribe(_ => { }, () => completed = true);
@ -44,7 +45,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Complete_When_Update_Observable_Completes()
{
var update = new Subject<Unit>();
var target = new ExpressionObserver(() => 1, "Foo", update);
var target = ExpressionObserver.Create(() => 1, o => o, update);
var completed = false;
target.Subscribe(_ => { }, () => completed = true);
@ -57,7 +58,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Complete_When_Update_Observable_Errors()
{
var update = new Subject<Unit>();
var target = new ExpressionObserver(() => 1, "Foo", update);
var target = ExpressionObserver.Create(() => 1, o => o, update);
var completed = false;
target.Subscribe(_ => { }, () => completed = true);
@ -72,7 +73,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
var scheduler = new TestScheduler();
var source = scheduler.CreateColdObservable(
OnNext(1, new { Foo = "foo" }));
var target = new ExpressionObserver(source, "Foo");
var target = ExpressionObserver.Create(source, o => o.Foo);
var result = new List<object>();
using (target.Subscribe(x => result.Add(x)))
@ -91,7 +92,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
var scheduler = new TestScheduler();
var update = scheduler.CreateColdObservable<Unit>();
var data = new { Foo = "foo" };
var target = new ExpressionObserver(() => data, "Foo", update);
var target = ExpressionObserver.Create(() => data, o => o.Foo, update);
var result = new List<object>();
using (target.Subscribe(x => result.Add(x)))
@ -106,9 +107,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
GC.KeepAlive(data);
}
private Recorded<Notification<object>> OnNext(long time, object value)
private Recorded<Notification<T>> OnNext<T>(long time, T value)
{
return new Recorded<Notification<object>>(time, Notification.CreateOnNext<object>(value));
return new Recorded<Notification<T>>(time, Notification.CreateOnNext<T>(value));
}
}
}

97
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Negation.cs

@ -6,6 +6,7 @@ using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using Xunit;
namespace Avalonia.Base.UnitTests.Data.Core
@ -16,7 +17,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Negate_Boolean_Value()
{
var data = new { Foo = true };
var target = new ExpressionObserver(data, "!Foo");
var target = ExpressionObserver.Create(data, o => !o.Foo);
var result = await target.Take(1);
Assert.False((bool)result);
@ -24,103 +25,11 @@ namespace Avalonia.Base.UnitTests.Data.Core
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Negate_0()
{
var data = new { Foo = 0 };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
Assert.True((bool)result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Negate_1()
{
var data = new { Foo = 1 };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
Assert.False((bool)result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Negate_False_String()
{
var data = new { Foo = "false" };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
Assert.True((bool)result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Negate_True_String()
{
var data = new { Foo = "True" };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
Assert.False((bool)result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Return_BindingNotification_For_String_Not_Convertible_To_Boolean()
{
var data = new { Foo = "foo" };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
Assert.Equal(
new BindingNotification(
new InvalidCastException($"Unable to convert 'foo' to bool."),
BindingErrorType.Error),
result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Return_BindingNotification_For_Value_Not_Convertible_To_Boolean()
{
var data = new { Foo = new object() };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
Assert.Equal(
new BindingNotification(
new InvalidCastException($"Unable to convert 'System.Object' to bool."),
BindingErrorType.Error),
result);
GC.KeepAlive(data);
}
[Fact]
public void SetValue_Should_Return_False_For_Invalid_Value()
{
var data = new { Foo = "foo" };
var target = new ExpressionObserver(data, "!Foo");
target.Subscribe(_ => { });
Assert.False(target.SetValue("bar"));
GC.KeepAlive(data);
}
[Fact]
public void Can_SetValue_For_Valid_Value()
{
var data = new Test { Foo = true };
var target = new ExpressionObserver(data, "!Foo");
var target = ExpressionObserver.Create(data, o => !o.Foo);
target.Subscribe(_ => { });
Assert.True(target.SetValue(true));

24
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs

@ -7,6 +7,7 @@ using System.Reactive.Linq;
using System.Reactive.Subjects;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using Avalonia.UnitTests;
using Xunit;
@ -15,13 +16,13 @@ namespace Avalonia.Base.UnitTests.Data.Core
public class ExpressionObserverTests_Observable
{
[Fact]
public void Should_Not_Get_Observable_Value_Without_Modifier_Char()
public void Should_Not_Get_Observable_Value_Without_Streaming()
{
using (var sync = UnitTestSynchronizationContext.Begin())
{
var source = new BehaviorSubject<string>("foo");
var data = new { Foo = source };
var target = new ExpressionObserver(data, "Foo");
var target = ExpressionObserver.Create(data, o => o.Foo);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -41,7 +42,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
var source = new BehaviorSubject<string>("foo");
var data = new { Foo = source };
var target = new ExpressionObserver(data, "Foo^");
var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding());
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -60,7 +61,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
using (var sync = UnitTestSynchronizationContext.Begin())
{
var data = new Class1();
var target = new ExpressionObserver(data, "Next^.Foo");
var target = ExpressionObserver.Create(data, o => o.Next.StreamBinding().Foo);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -83,7 +84,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
var source = new BehaviorSubject<string>("foo");
var data = new { Foo = source };
var target = new ExpressionObserver(data, "Foo^", true);
var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding(), true);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -105,7 +106,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
var data1 = new Class1();
var data2 = new Class2("foo");
var target = new ExpressionObserver(data1, "Next^.Foo", true);
var target = ExpressionObserver.Create(data1, o => o.Next.StreamBinding().Foo, true);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -127,8 +128,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
using (var sync = UnitTestSynchronizationContext.Begin())
{
var data = new Class2("foo");
var target = new ExpressionObserver(data, "Foo^", true);
var data = new NotStreamable();
var target = ExpressionObserver.Create(data, o => o.StreamBinding());
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -138,7 +139,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
new[]
{
new BindingNotification(
new MarkupBindingChainException("Stream operator applied to unsupported type", "Foo^", "Foo^"),
new MarkupBindingChainException("Stream operator applied to unsupported type", "o => o.StreamBinding()", "^"),
BindingErrorType.Error)
},
result);
@ -163,5 +164,10 @@ namespace Avalonia.Base.UnitTests.Data.Core
public string Foo { get; }
}
private class NotStreamable
{
public object StreamBinding() { throw new InvalidOperationException(); }
}
}
}

120
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs

@ -12,6 +12,7 @@ using Avalonia.Data.Core;
using Avalonia.UnitTests;
using Xunit;
using System.Threading.Tasks;
using Avalonia.Markup.Parsers;
namespace Avalonia.Base.UnitTests.Data.Core
{
@ -21,7 +22,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_Simple_Property_Value()
{
var data = new { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
var target = ExpressionObserver.Create(data, o => o.Foo);
var result = await target.Take(1);
Assert.Equal("foo", result);
@ -33,7 +34,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Get_Simple_Property_Value_Type()
{
var data = new { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
var target = ExpressionObserver.Create(data, o => o.Foo);
target.Subscribe(_ => { });
@ -46,7 +47,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_Simple_Property_Value_Null()
{
var data = new { Foo = (string)null };
var target = new ExpressionObserver(data, "Foo");
var target = ExpressionObserver.Create(data, o => o.Foo);
var result = await target.Take(1);
Assert.Null(result);
@ -58,7 +59,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_Simple_Property_From_Base_Class()
{
var data = new Class3 { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
var target = ExpressionObserver.Create(data, o => o.Foo);
var result = await target.Take(1);
Assert.Equal("foo", result);
@ -69,76 +70,65 @@ namespace Avalonia.Base.UnitTests.Data.Core
[Fact]
public async Task Should_Return_BindingNotification_Error_For_Root_Null()
{
var data = new Class3 { Foo = "foo" };
var target = new ExpressionObserver(default(object), "Foo");
var target = ExpressionObserver.Create(default(Class3), o => o.Foo);
var result = await target.Take(1);
Assert.Equal(
new BindingNotification(
new MarkupBindingChainException("Null value", "Foo", string.Empty),
new MarkupBindingChainException("Null value", "o => o.Foo", string.Empty),
BindingErrorType.Error,
AvaloniaProperty.UnsetValue),
result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Return_BindingNotification_Error_For_Root_UnsetValue()
{
var data = new Class3 { Foo = "foo" };
var target = new ExpressionObserver(AvaloniaProperty.UnsetValue, "Foo");
var target = ExpressionObserver.Create(AvaloniaProperty.UnsetValue, o => (o as Class3).Foo);
var result = await target.Take(1);
Assert.Equal(
new BindingNotification(
new MarkupBindingChainException("Null value", "Foo", string.Empty),
new MarkupBindingChainException("Null value", "o => (o As Class3).Foo", string.Empty),
BindingErrorType.Error,
AvaloniaProperty.UnsetValue),
result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Return_BindingNotification_Error_For_Observable_Root_Null()
{
var data = new Class3 { Foo = "foo" };
var target = new ExpressionObserver(Observable.Return(default(object)), "Foo");
var target = ExpressionObserver.Create(Observable.Return(default(Class3)), o => o.Foo);
var result = await target.Take(1);
Assert.Equal(
new BindingNotification(
new MarkupBindingChainException("Null value", "Foo", string.Empty),
new MarkupBindingChainException("Null value", "o => o.Foo", string.Empty),
BindingErrorType.Error,
AvaloniaProperty.UnsetValue),
result);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Return_BindingNotification_Error_For_Observable_Root_UnsetValue()
{
var data = new Class3 { Foo = "foo" };
var target = new ExpressionObserver(Observable.Return(AvaloniaProperty.UnsetValue), "Foo");
var target = ExpressionObserver.Create<object, string>(Observable.Return(AvaloniaProperty.UnsetValue), o => (o as Class3).Foo);
var result = await target.Take(1);
Assert.Equal(
new BindingNotification(
new MarkupBindingChainException("Null value", "Foo", string.Empty),
new MarkupBindingChainException("Null value", "o => (o As Class3).Foo", string.Empty),
BindingErrorType.Error,
AvaloniaProperty.UnsetValue),
result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Get_Simple_Property_Chain()
{
var data = new { Foo = new { Bar = new { Baz = "baz" } } };
var target = new ExpressionObserver(data, "Foo.Bar.Baz");
var target = ExpressionObserver.Create(data, o => o.Foo.Bar.Baz);
var result = await target.Take(1);
Assert.Equal("baz", result);
@ -150,7 +140,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Get_Simple_Property_Chain_Type()
{
var data = new { Foo = new { Bar = new { Baz = "baz" } } };
var target = new ExpressionObserver(data, "Foo.Bar.Baz");
var target = ExpressionObserver.Create(data, o => o.Foo.Bar.Baz);
target.Subscribe(_ => { });
@ -159,28 +149,11 @@ namespace Avalonia.Base.UnitTests.Data.Core
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Return_BindingNotification_Error_For_Broken_Chain()
{
var data = new { Foo = new { Bar = 1 } };
var target = new ExpressionObserver(data, "Foo.Bar.Baz");
var result = await target.Take(1);
Assert.IsType<BindingNotification>(result);
Assert.Equal(
new BindingNotification(
new MissingMemberException("Could not find CLR property 'Baz' on '1'"), BindingErrorType.Error),
result);
GC.KeepAlive(data);
}
[Fact]
public void Should_Return_BindingNotification_Error_For_Chain_With_Null_Value()
{
var data = new { Foo = default(object) };
var target = new ExpressionObserver(data, "Foo.Bar.Baz");
var data = new { Foo = default(Class1) };
var target = ExpressionObserver.Create(data, o => o.Foo.Foo.Length);
var result = new List<object>();
target.Subscribe(x => result.Add(x));
@ -189,7 +162,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
new[]
{
new BindingNotification(
new MarkupBindingChainException("Null value", "Foo.Bar.Baz", "Foo"),
new MarkupBindingChainException("Null value", "o => o.Foo.Foo.Length", "Foo"),
BindingErrorType.Error,
AvaloniaProperty.UnsetValue),
},
@ -198,22 +171,11 @@ namespace Avalonia.Base.UnitTests.Data.Core
GC.KeepAlive(data);
}
[Fact]
public void Should_Have_Null_ResultType_For_Broken_Chain()
{
var data = new { Foo = new { Bar = 1 } };
var target = new ExpressionObserver(data, "Foo.Bar.Baz");
Assert.Null(target.ResultType);
GC.KeepAlive(data);
}
[Fact]
public void Should_Track_Simple_Property_Value()
{
var data = new Class1 { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
var target = ExpressionObserver.Create(data, o => o.Foo);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -232,7 +194,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Trigger_PropertyChanged_On_Null_Or_Empty_String()
{
var data = new Class1 { Bar = "foo" };
var target = new ExpressionObserver(data, "Bar");
var target = ExpressionObserver.Create(data, o => o.Bar);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -262,7 +224,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Track_End_Of_Property_Chain_Changing()
{
var data = new Class1 { Next = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Next.Bar");
var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -283,7 +245,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Track_Property_Chain_Changing()
{
var data = new Class1 { Next = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Next.Bar");
var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -316,7 +278,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
}
};
var target = new ExpressionObserver(data, "Next.Next.Bar");
var target = ExpressionObserver.Create(data, o => ((o.Next as Class2).Next as Class2).Bar);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -329,7 +291,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
"bar",
new BindingNotification(
new MarkupBindingChainException("Null value", "Next.Next.Bar", "Next.Next"),
new MarkupBindingChainException("Null value", "o => ((o.Next As Class2).Next As Class2).Bar", "Next.Next"),
BindingErrorType.Error,
AvaloniaProperty.UnsetValue),
"bar"
@ -349,7 +311,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Track_Property_Chain_Breaking_With_Missing_Member_Then_Mending()
{
var data = new Class1 { Next = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Next.Bar");
var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -384,7 +346,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
var data = new Class1 { Foo = "foo" };
var update = new Subject<Unit>();
var target = new ExpressionObserver(() => data.Foo, "", update);
var target = ExpressionObserver.Create(() => data.Foo, o => o, update);
var result = new List<object>();
target.Subscribe(x => result.Add(x));
@ -404,7 +366,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
var source = scheduler.CreateColdObservable(
OnNext(1, new Class1 { Foo = "foo" }),
OnNext(2, new Class1 { Foo = "bar" }));
var target = new ExpressionObserver(source, "Foo");
var target = ExpressionObserver.Create(source, o => o.Foo);
var result = new List<object>();
using (target.Subscribe(x => result.Add(x)))
@ -420,7 +382,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Subscribing_Multiple_Times_Should_Return_Values_To_All()
{
var data = new Class1 { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
var target = ExpressionObserver.Create(data, o => o.Foo);
var result1 = new List<object>();
var result2 = new List<object>();
var result3 = new List<object>();
@ -443,7 +405,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Subscribing_Multiple_Times_Should_Only_Add_PropertyChanged_Handlers_Once()
{
var data = new Class1 { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
var target = ExpressionObserver.Create(data, o => o.Foo);
var sub1 = target.Subscribe(x => { });
var sub2 = target.Subscribe(x => { });
@ -462,7 +424,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void SetValue_Should_Set_Simple_Property_Value()
{
var data = new Class1 { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
var target = ExpressionObserver.Create(data, o => o.Foo);
using (target.Subscribe(_ => { }))
{
@ -478,7 +440,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void SetValue_Should_Set_Property_At_The_End_Of_Chain()
{
var data = new Class1 { Next = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Next.Bar");
var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
using (target.Subscribe(_ => { }))
{
@ -494,7 +456,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void SetValue_Should_Return_False_For_Missing_Property()
{
var data = new Class1 { Next = new WithoutBar() };
var target = new ExpressionObserver(data, "Next.Bar");
var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
using (target.Subscribe(_ => { }))
{
@ -508,7 +470,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void SetValue_Should_Notify_New_Value_With_Inpc()
{
var data = new Class1();
var target = new ExpressionObserver(data, "Foo");
var target = ExpressionObserver.Create(data, o => o.Foo);
var result = new List<object>();
target.Subscribe(x => result.Add(x));
@ -523,7 +485,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void SetValue_Should_Notify_New_Value_Without_Inpc()
{
var data = new Class1();
var target = new ExpressionObserver(data, "Bar");
var target = ExpressionObserver.Create(data, o => o.Bar);
var result = new List<object>();
target.Subscribe(x => result.Add(x));
@ -538,7 +500,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void SetValue_Should_Return_False_For_Missing_Object()
{
var data = new Class1();
var target = new ExpressionObserver(data, "Next.Bar");
var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
using (target.Subscribe(_ => { }))
{
@ -555,7 +517,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
var second = new Class1 { Foo = "bar" };
var root = first;
var update = new Subject<Unit>();
var target = new ExpressionObserver(() => root, "Foo", update);
var target = ExpressionObserver.Create(() => root, o => o.Foo, update);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -570,7 +532,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
"foo",
"bar",
new BindingNotification(
new MarkupBindingChainException("Null value", "Foo", string.Empty),
new MarkupBindingChainException("Null value", "o => o.Foo", string.Empty),
BindingErrorType.Error,
AvaloniaProperty.UnsetValue)
},
@ -589,7 +551,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
Func<Tuple<ExpressionObserver, WeakReference>> run = () =>
{
var source = new Class1 { Foo = "foo" };
var target = new ExpressionObserver(source, "Foo");
var target = ExpressionObserver.Create(source, o => o.Foo);
return Tuple.Create(target, new WeakReference(source));
};
@ -673,9 +635,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
}
private Recorded<Notification<object>> OnNext(long time, object value)
private Recorded<Notification<T>> OnNext<T>(long time, T value)
{
return new Recorded<Notification<object>>(time, Notification.CreateOnNext<object>(value));
return new Recorded<Notification<T>>(time, Notification.CreateOnNext<T>(value));
}
}
}

29
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_SetValue.cs

@ -5,6 +5,7 @@ using System;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using Avalonia.UnitTests;
using Xunit;
@ -16,7 +17,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Set_Simple_Property_Value()
{
var data = new { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
var target = ExpressionObserver.Create(data, o => o.Foo);
using (target.Subscribe(_ => { }))
{
@ -30,7 +31,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Set_Value_On_Simple_Property_Chain()
{
var data = new Class1 { Foo = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Foo.Bar");
var target = ExpressionObserver.Create(data, o => o.Foo.Bar);
using (target.Subscribe(_ => { }))
{
@ -44,14 +46,15 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Not_Try_To_Set_Value_On_Broken_Chain()
{
var data = new Class1 { Foo = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Foo.Bar");
var target = ExpressionObserver.Create(data, o => o.Foo.Bar);
// Ensure the ExpressionObserver's subscriptions are kept active.
target.OfType<string>().Subscribe(x => { });
data.Foo = null;
using (target.OfType<string>().Subscribe(x => { }))
{
data.Foo = null;
Assert.False(target.SetValue("foo"));
}
Assert.False(target.SetValue("foo"));
}
/// <summary>
@ -67,13 +70,15 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
var data = new Class1 { Foo = new Class2 { Bar = "bar" } };
var rootObservable = new BehaviorSubject<Class1>(data);
var target = new ExpressionObserver(rootObservable, "Foo.Bar");
var target = ExpressionObserver.Create(rootObservable, o => o.Foo.Bar);
target.Subscribe(_ => { });
rootObservable.OnNext(null);
target.SetValue("baz");
using (target.Subscribe(_ => { }))
{
rootObservable.OnNext(null);
target.SetValue("baz");
Assert.Equal("bar", data.Foo.Bar);
}
Assert.Equal("bar", data.Foo.Bar);
}
private class Class1 : NotifyingBase

15
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Task.cs

@ -7,6 +7,7 @@ using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using Avalonia.UnitTests;
using Xunit;
@ -15,13 +16,13 @@ namespace Avalonia.Base.UnitTests.Data.Core
public class ExpressionObserverTests_Task
{
[Fact]
public void Should_Not_Get_Task_Result_Without_Modifier_Char()
public void Should_Not_Get_Task_Result_Without_StreamBinding()
{
using (var sync = UnitTestSynchronizationContext.Begin())
{
var tcs = new TaskCompletionSource<string>();
var data = new { Foo = tcs.Task };
var target = new ExpressionObserver(data, "Foo");
var target = ExpressionObserver.Create(data, o => o.Foo);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -41,7 +42,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
using (var sync = UnitTestSynchronizationContext.Begin())
{
var data = new { Foo = Task.FromResult("foo") };
var target = new ExpressionObserver(data, "Foo^");
var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding());
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -59,7 +60,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
var tcs = new TaskCompletionSource<Class2>();
var data = new Class1(tcs.Task);
var target = new ExpressionObserver(data, "Next^.Foo");
var target = ExpressionObserver.Create(data, o => o.Next.StreamBinding().Foo);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -79,7 +80,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
var tcs = new TaskCompletionSource<string>();
var data = new { Foo = tcs.Task };
var target = new ExpressionObserver(data, "Foo^");
var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding());
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -105,7 +106,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
using (var sync = UnitTestSynchronizationContext.Begin())
{
var data = new { Foo = TaskFromException(new NotSupportedException()) };
var target = new ExpressionObserver(data, "Foo^");
var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding());
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -130,7 +131,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
var tcs = new TaskCompletionSource<string>();
var data = new { Foo = tcs.Task };
var target = new ExpressionObserver(data, "Foo^", true);
var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding(), true);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));

52
tests/Avalonia.Base.UnitTests/PriorityValueTests.cs

@ -1,11 +1,12 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Utilities;
using Moq;
using System;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Moq;
using Xunit;
namespace Avalonia.Base.UnitTests
@ -21,7 +22,7 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void Initial_Value_Should_Be_UnsetValue()
{
var target = new PriorityValue(null, TestProperty, typeof(string));
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
Assert.Same(AvaloniaProperty.UnsetValue, target.Value);
}
@ -29,7 +30,7 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void First_Binding_Sets_Value()
{
var target = new PriorityValue(null, TestProperty, typeof(string));
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
target.Add(Single("foo"), 0);
@ -39,7 +40,7 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void Changing_Binding_Should_Set_Value()
{
var target = new PriorityValue(null, TestProperty, typeof(string));
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var subject = new BehaviorSubject<string>("foo");
target.Add(subject, 0);
@ -51,7 +52,7 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void Setting_Direct_Value_Should_Override_Binding()
{
var target = new PriorityValue(null, TestProperty, typeof(string));
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
target.Add(Single("foo"), 0);
target.SetValue("bar", 0);
@ -62,7 +63,7 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void Binding_Firing_Should_Override_Direct_Value()
{
var target = new PriorityValue(null, TestProperty, typeof(string));
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var source = new BehaviorSubject<object>("initial");
target.Add(source, 0);
@ -76,7 +77,7 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void Earlier_Binding_Firing_Should_Not_Override_Later()
{
var target = new PriorityValue(null, TestProperty, typeof(string));
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var nonActive = new BehaviorSubject<object>("na");
var source = new BehaviorSubject<object>("initial");
@ -92,7 +93,7 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void Binding_Completing_Should_Revert_To_Direct_Value()
{
var target = new PriorityValue(null, TestProperty, typeof(string));
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var source = new BehaviorSubject<object>("initial");
target.Add(source, 0);
@ -108,7 +109,7 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void Binding_With_Lower_Priority_Has_Precedence()
{
var target = new PriorityValue(null, TestProperty, typeof(string));
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
target.Add(Single("foo"), 1);
target.Add(Single("bar"), 0);
@ -120,7 +121,7 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void Later_Binding_With_Same_Priority_Should_Take_Precedence()
{
var target = new PriorityValue(null, TestProperty, typeof(string));
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
target.Add(Single("foo"), 1);
target.Add(Single("bar"), 0);
@ -133,7 +134,7 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void Changing_Binding_With_Lower_Priority_Should_Set_Not_Value()
{
var target = new PriorityValue(null, TestProperty, typeof(string));
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var subject = new BehaviorSubject<string>("bar");
target.Add(Single("foo"), 0);
@ -146,7 +147,7 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void UnsetValue_Should_Fall_Back_To_Next_Binding()
{
var target = new PriorityValue(null, TestProperty, typeof(string));
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var subject = new BehaviorSubject<object>("bar");
target.Add(subject, 0);
@ -162,7 +163,7 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void Adding_Value_Should_Call_OnNext()
{
var owner = new Mock<IPriorityValueOwner>();
var owner = GetMockOwner();
var target = new PriorityValue(owner.Object, TestProperty, typeof(string));
target.Add(Single("foo"), 0);
@ -173,7 +174,7 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void Changing_Value_Should_Call_OnNext()
{
var owner = new Mock<IPriorityValueOwner>();
var owner = GetMockOwner();
var target = new PriorityValue(owner.Object, TestProperty, typeof(string));
var subject = new BehaviorSubject<object>("foo");
@ -186,7 +187,7 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void Disposing_A_Binding_Should_Revert_To_Next_Value()
{
var target = new PriorityValue(null, TestProperty, typeof(string));
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
target.Add(Single("foo"), 0);
var disposable = target.Add(Single("bar"), 0);
@ -199,7 +200,7 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void Disposing_A_Binding_Should_Remove_BindingEntry()
{
var target = new PriorityValue(null, TestProperty, typeof(string));
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
target.Add(Single("foo"), 0);
var disposable = target.Add(Single("bar"), 0);
@ -212,7 +213,7 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void Completing_A_Binding_Should_Revert_To_Previous_Binding()
{
var target = new PriorityValue(null, TestProperty, typeof(string));
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var source = new BehaviorSubject<object>("bar");
target.Add(Single("foo"), 0);
@ -226,7 +227,7 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void Completing_A_Binding_Should_Revert_To_Lower_Priority()
{
var target = new PriorityValue(null, TestProperty, typeof(string));
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var source = new BehaviorSubject<object>("bar");
target.Add(Single("foo"), 1);
@ -240,7 +241,7 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void Completing_A_Binding_Should_Remove_BindingEntry()
{
var target = new PriorityValue(null, TestProperty, typeof(string));
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var subject = new BehaviorSubject<object>("bar");
target.Add(Single("foo"), 0);
@ -254,7 +255,7 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void Direct_Value_Should_Be_Coerced()
{
var target = new PriorityValue(null, TestProperty, typeof(int), x => Math.Min((int)x, 10));
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(int), x => Math.Min((int)x, 10));
target.SetValue(5, 0);
Assert.Equal(5, target.Value);
@ -265,7 +266,7 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void Bound_Value_Should_Be_Coerced()
{
var target = new PriorityValue(null, TestProperty, typeof(int), x => Math.Min((int)x, 10));
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(int), x => Math.Min((int)x, 10));
var source = new Subject<object>();
target.Add(source, 0);
@ -279,7 +280,7 @@ namespace Avalonia.Base.UnitTests
public void Revalidate_Should_ReCoerce_Value()
{
var max = 10;
var target = new PriorityValue(null, TestProperty, typeof(int), x => Math.Min((int)x, max));
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(int), x => Math.Min((int)x, max));
var source = new Subject<object>();
target.Add(source, 0);
@ -302,5 +303,12 @@ namespace Avalonia.Base.UnitTests
{
return Observable.Never<T>().StartWith(value);
}
private static Mock<IPriorityValueOwner> GetMockOwner()
{
var owner = new Mock<IPriorityValueOwner>();
owner.SetupGet(o => o.Setter).Returns(new DeferredSetter<object>());
return owner;
}
}
}

50
tests/Avalonia.Base.UnitTests/Utilities/SingleOrQueueTests.cs

@ -0,0 +1,50 @@
using Avalonia.Utilities;
using System;
using System.Collections.Generic;
using System.Text;
using Xunit;
namespace Avalonia.Base.UnitTests.Utilities
{
public class SingleOrQueueTests
{
[Fact]
public void New_SingleOrQueue_Is_Empty()
{
Assert.True(new SingleOrQueue<object>().Empty);
}
[Fact]
public void Dequeue_Throws_When_Empty()
{
var queue = new SingleOrQueue<object>();
Assert.Throws<InvalidOperationException>(() => queue.Dequeue());
}
[Fact]
public void Enqueue_Adds_Element()
{
var queue = new SingleOrQueue<int>();
queue.Enqueue(1);
Assert.False(queue.Empty);
Assert.Equal(1, queue.Dequeue());
}
[Fact]
public void Multiple_Elements_Dequeued_In_Correct_Order()
{
var queue = new SingleOrQueue<int>();
queue.Enqueue(1);
queue.Enqueue(2);
queue.Enqueue(3);
Assert.Equal(1, queue.Dequeue());
Assert.Equal(2, queue.Dequeue());
Assert.Equal(3, queue.Dequeue());
}
}
}

28
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@ -149,6 +149,34 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(1, target.SelectedIndex);
}
[Fact]
public void SelectedIndex_Item_Is_Updated_As_Items_Removed_When_Last_Item_Is_Selected()
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"FooBar"
};
var target = new SelectingItemsControl
{
Items = items,
Template = Template(),
};
target.ApplyTemplate();
target.SelectedItem = items[2];
Assert.Equal(items[2], target.SelectedItem);
Assert.Equal(2, target.SelectedIndex);
items.RemoveAt(0);
Assert.Equal(items[1], target.SelectedItem);
Assert.Equal(1, target.SelectedIndex);
}
[Fact]
public void Setting_SelectedItem_To_Not_Present_Item_Should_Clear_Selection()
{

39
tests/Avalonia.Controls.UnitTests/StackPanelTests.cs

@ -146,5 +146,44 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new Rect(20, 0, 30, 120), target.Children[1].Bounds);
Assert.Equal(new Rect(50, 0, 50, 120), target.Children[2].Bounds);
}
[Theory]
[InlineData(Orientation.Horizontal)]
[InlineData(Orientation.Vertical)]
public void Gap_Not_Added_For_Invisible_Children(Orientation orientation)
{
var targetThreeChildrenOneInvisble = new StackPanel
{
Gap = 40,
Orientation = orientation,
Children =
{
new StackPanel { Width = 10, Height= 10, IsVisible = false },
new StackPanel { Width = 10, Height= 10 },
new StackPanel { Width = 10, Height= 10 },
}
};
var targetTwoChildrenNoneInvisible = new StackPanel
{
Gap = 40,
Orientation = orientation,
Children =
{
new StackPanel { Width = 10, Height= 10 },
new StackPanel { Width = 10, Height= 10 }
}
};
targetThreeChildrenOneInvisble.Measure(Size.Infinity);
targetThreeChildrenOneInvisble.Arrange(new Rect(targetThreeChildrenOneInvisble.DesiredSize));
targetTwoChildrenNoneInvisible.Measure(Size.Infinity);
targetTwoChildrenNoneInvisible.Arrange(new Rect(targetTwoChildrenNoneInvisible.DesiredSize));
Size sizeWithTwoChildren = targetTwoChildrenNoneInvisible.Bounds.Size;
Size sizeWithThreeChildren = targetThreeChildrenOneInvisble.Bounds.Size;
Assert.Equal(sizeWithTwoChildren, sizeWithThreeChildren);
}
}
}

2
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@ -515,7 +515,7 @@ namespace Avalonia.Controls.UnitTests
public InstancedBinding ItemsSelector(object item)
{
var obs = new ExpressionObserver(item, nameof(Node.Children));
var obs = ExpressionObserver.Create(item, o => (o as Node).Children);
return InstancedBinding.OneWay(obs);
}

8
tests/Avalonia.LeakTests/ExpressionObserverTests.cs

@ -24,7 +24,7 @@ namespace Avalonia.LeakTests
Func<ExpressionObserver> run = () =>
{
var source = new { Foo = new AvaloniaList<string> {"foo", "bar"} };
var target = new ExpressionObserver(source, "Foo");
var target = ExpressionObserver.Create(source, o => o.Foo);
target.Subscribe(_ => { });
return target;
@ -42,7 +42,7 @@ namespace Avalonia.LeakTests
Func<ExpressionObserver> run = () =>
{
var source = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = new ExpressionObserver(source, "Foo", true);
var target = ExpressionObserver.Create(source, o => o.Foo, true);
target.Subscribe(_ => { });
return target;
@ -60,7 +60,7 @@ namespace Avalonia.LeakTests
Func<ExpressionObserver> run = () =>
{
var source = new { Foo = new NonIntegerIndexer() };
var target = new ExpressionObserver(source, "Foo");
var target = ExpressionObserver.Create(source, o => o.Foo);
target.Subscribe(_ => { });
return target;
@ -78,7 +78,7 @@ namespace Avalonia.LeakTests
Func<ExpressionObserver> run = () =>
{
var source = new { Foo = new MethodBound() };
var target = new ExpressionObserver(source, "Foo.A");
var target = ExpressionObserver.Create(source, o => (Action)o.Foo.A);
target.Subscribe(_ => { });
return target;
};

124
tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs

@ -0,0 +1,124 @@
using System;
using System.Globalization;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.VisualTree;
using Xunit;
namespace Avalonia.Markup.UnitTests.Data
{
public class TemplateBindingTests
{
[Fact]
public void OneWay_Binding_Should_Be_Set_Up()
{
var source = new Button
{
Template = new FuncControlTemplate<Button>(parent =>
new ContentPresenter
{
[~ContentPresenter.ContentProperty] = new TemplateBinding(ContentControl.ContentProperty)
}),
};
source.ApplyTemplate();
var target = (ContentPresenter)source.GetVisualChildren().Single();
Assert.Null(target.Content);
source.Content = "foo";
Assert.Equal("foo", target.Content);
source.Content = "bar";
Assert.Equal("bar", target.Content);
}
[Fact]
public void TwoWay_Binding_Should_Be_Set_Up()
{
var source = new Button
{
Template = new FuncControlTemplate<Button>(parent =>
new ContentPresenter
{
[~ContentPresenter.ContentProperty] = new TemplateBinding(ContentControl.ContentProperty)
{
Mode = BindingMode.TwoWay,
}
}),
};
source.ApplyTemplate();
var target = (ContentPresenter)source.GetVisualChildren().Single();
Assert.Null(target.Content);
source.Content = "foo";
Assert.Equal("foo", target.Content);
target.Content = "bar";
Assert.Equal("bar", source.Content);
}
[Fact]
public void Converter_Should_Be_Used()
{
var source = new Button
{
Template = new FuncControlTemplate<Button>(parent =>
new ContentPresenter
{
[~ContentPresenter.ContentProperty] = new TemplateBinding(ContentControl.ContentProperty)
{
Mode = BindingMode.TwoWay,
Converter = new PrefixConverter(),
ConverterParameter = "Hello ",
}
}),
};
source.ApplyTemplate();
var target = (ContentPresenter)source.GetVisualChildren().Single();
Assert.Null(target.Content);
source.Content = "foo";
Assert.Equal("Hello foo", target.Content);
target.Content = "Hello bar";
Assert.Equal("bar", source.Content);
}
private class PrefixConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value != null && parameter != null)
{
return parameter.ToString() + value;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value != null && parameter != null)
{
var s = value.ToString();
var prefix = parameter.ToString();
if (s.StartsWith(prefix) == true)
{
return s.Substring(prefix.Length);
}
return s;
}
return null;
}
}
}
}

50
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionNodeBuilderTests.cs → tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs

@ -4,16 +4,18 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using Avalonia.Markup.Parsers.Nodes;
using Xunit;
namespace Avalonia.Base.UnitTests.Data.Core
namespace Avalonia.Markup.UnitTests.Parsers
{
public class ExpressionNodeBuilderTests
public class ExpressionObserverBuilderTests
{
[Fact]
public void Should_Build_Single_Property()
{
var result = ToList(ExpressionNodeBuilder.Build("Foo"));
var result = ToList(ExpressionObserverBuilder.Parse("Foo"));
AssertIsProperty(result[0], "Foo");
}
@ -21,7 +23,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
[Fact]
public void Should_Build_Underscored_Property()
{
var result = ToList(ExpressionNodeBuilder.Build("_Foo"));
var result = ToList(ExpressionObserverBuilder.Parse("_Foo"));
AssertIsProperty(result[0], "_Foo");
}
@ -29,7 +31,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
[Fact]
public void Should_Build_Property_With_Digits()
{
var result = ToList(ExpressionNodeBuilder.Build("F0o"));
var result = ToList(ExpressionObserverBuilder.Parse("F0o"));
AssertIsProperty(result[0], "F0o");
}
@ -37,7 +39,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
[Fact]
public void Should_Build_Property_Chain()
{
var result = ToList(ExpressionNodeBuilder.Build("Foo.Bar.Baz"));
var result = ToList(ExpressionObserverBuilder.Parse("Foo.Bar.Baz"));
Assert.Equal(3, result.Count);
AssertIsProperty(result[0], "Foo");
@ -48,7 +50,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
[Fact]
public void Should_Build_Negated_Property_Chain()
{
var result = ToList(ExpressionNodeBuilder.Build("!Foo.Bar.Baz"));
var result = ToList(ExpressionObserverBuilder.Parse("!Foo.Bar.Baz"));
Assert.Equal(4, result.Count);
Assert.IsType<LogicalNotNode>(result[0]);
@ -60,7 +62,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
[Fact]
public void Should_Build_Double_Negated_Property_Chain()
{
var result = ToList(ExpressionNodeBuilder.Build("!!Foo.Bar.Baz"));
var result = ToList(ExpressionObserverBuilder.Parse("!!Foo.Bar.Baz"));
Assert.Equal(5, result.Count);
Assert.IsType<LogicalNotNode>(result[0]);
@ -73,29 +75,29 @@ namespace Avalonia.Base.UnitTests.Data.Core
[Fact]
public void Should_Build_Indexed_Property()
{
var result = ToList(ExpressionNodeBuilder.Build("Foo[15]"));
var result = ToList(ExpressionObserverBuilder.Parse("Foo[15]"));
Assert.Equal(2, result.Count);
AssertIsProperty(result[0], "Foo");
AssertIsIndexer(result[1], "15");
Assert.IsType<IndexerNode>(result[1]);
Assert.IsType<StringIndexerNode>(result[1]);
}
[Fact]
public void Should_Build_Indexed_Property_StringIndex()
{
var result = ToList(ExpressionNodeBuilder.Build("Foo[Key]"));
var result = ToList(ExpressionObserverBuilder.Parse("Foo[Key]"));
Assert.Equal(2, result.Count);
AssertIsProperty(result[0], "Foo");
AssertIsIndexer(result[1], "Key");
Assert.IsType<IndexerNode>(result[1]);
Assert.IsType<StringIndexerNode>(result[1]);
}
[Fact]
public void Should_Build_Multiple_Indexed_Property()
{
var result = ToList(ExpressionNodeBuilder.Build("Foo[15,6]"));
var result = ToList(ExpressionObserverBuilder.Parse("Foo[15,6]"));
Assert.Equal(2, result.Count);
AssertIsProperty(result[0], "Foo");
@ -105,7 +107,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
[Fact]
public void Should_Build_Multiple_Indexed_Property_With_Space()
{
var result = ToList(ExpressionNodeBuilder.Build("Foo[5, 16]"));
var result = ToList(ExpressionObserverBuilder.Parse("Foo[5, 16]"));
Assert.Equal(2, result.Count);
AssertIsProperty(result[0], "Foo");
@ -115,7 +117,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
[Fact]
public void Should_Build_Consecutive_Indexers()
{
var result = ToList(ExpressionNodeBuilder.Build("Foo[15][16]"));
var result = ToList(ExpressionObserverBuilder.Parse("Foo[15][16]"));
Assert.Equal(3, result.Count);
AssertIsProperty(result[0], "Foo");
@ -126,7 +128,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
[Fact]
public void Should_Build_Indexed_Property_In_Chain()
{
var result = ToList(ExpressionNodeBuilder.Build("Foo.Bar[5, 6].Baz"));
var result = ToList(ExpressionObserverBuilder.Parse("Foo.Bar[5, 6].Baz"));
Assert.Equal(4, result.Count);
AssertIsProperty(result[0], "Foo");
@ -135,6 +137,15 @@ namespace Avalonia.Base.UnitTests.Data.Core
AssertIsProperty(result[3], "Baz");
}
[Fact]
public void Should_Build_Stream_Node()
{
var result = ToList(ExpressionObserverBuilder.Parse("Foo^"));
Assert.Equal(2, result.Count);
Assert.IsType<StreamNode>(result[1]);
}
private void AssertIsProperty(ExpressionNode node, string name)
{
Assert.IsType<PropertyAccessorNode>(node);
@ -145,14 +156,15 @@ namespace Avalonia.Base.UnitTests.Data.Core
private void AssertIsIndexer(ExpressionNode node, params string[] args)
{
Assert.IsType<IndexerNode>(node);
Assert.IsType<StringIndexerNode>(node);
var e = (IndexerNode)node;
var e = (StringIndexerNode)node;
Assert.Equal(e.Arguments.ToArray(), args);
}
private List<ExpressionNode> ToList(ExpressionNode node)
private List<ExpressionNode> ToList((ExpressionNode node, SourceMode mode) parsed)
{
var (node, _) = parsed;
var result = new List<ExpressionNode>();
while (node != null)

23
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionNodeBuilderTests_Errors.cs → tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests_Errors.cs

@ -2,73 +2,74 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using Xunit;
namespace Avalonia.Base.UnitTests.Data.Core
namespace Avalonia.Markup.UnitTests.Parsers
{
public class ExpressionNodeBuilderTests_Errors
public class ExpressionObserverBuilderTests_Errors
{
[Fact]
public void Identifier_Cannot_Start_With_Digit()
{
Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("1Foo"));
() => ExpressionObserverBuilder.Parse("1Foo"));
}
[Fact]
public void Identifier_Cannot_Start_With_Symbol()
{
Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.%Bar"));
() => ExpressionObserverBuilder.Parse("Foo.%Bar"));
}
[Fact]
public void Expression_Cannot_End_With_Period()
{
Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.Bar."));
() => ExpressionObserverBuilder.Parse("Foo.Bar."));
}
[Fact]
public void Expression_Cannot_Have_Empty_Indexer()
{
Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.Bar[]"));
() => ExpressionObserverBuilder.Parse("Foo.Bar[]"));
}
[Fact]
public void Expression_Cannot_Have_Extra_Comma_At_Start_Of_Indexer()
{
Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.Bar[,3,4]"));
() => ExpressionObserverBuilder.Parse("Foo.Bar[,3,4]"));
}
[Fact]
public void Expression_Cannot_Have_Extra_Comma_In_Indexer()
{
Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.Bar[3,,4]"));
() => ExpressionObserverBuilder.Parse("Foo.Bar[3,,4]"));
}
[Fact]
public void Expression_Cannot_Have_Extra_Comma_At_End_Of_Indexer()
{
Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.Bar[3,4,]"));
() => ExpressionObserverBuilder.Parse("Foo.Bar[3,4,]"));
}
[Fact]
public void Expression_Cannot_Have_Digit_After_Indexer()
{
Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.Bar[3,4]5"));
() => ExpressionObserverBuilder.Parse("Foo.Bar[3,4]5"));
}
[Fact]
public void Expression_Cannot_Have_Letter_After_Indexer()
{
Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.Bar[3,4]A"));
() => ExpressionObserverBuilder.Parse("Foo.Bar[3,4]A"));
}
}
}

165
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs

@ -0,0 +1,165 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Diagnostics;
using Avalonia.Data.Core;
using Xunit;
using Avalonia.Markup.Parsers;
namespace Avalonia.Markup.UnitTests.Parsers
{
public class ExpressionObserverBuilderTests_AttachedProperty
{
private readonly Func<string, string, Type> _typeResolver;
public ExpressionObserverBuilderTests_AttachedProperty()
{
var foo = Owner.FooProperty;
_typeResolver = (_, name) => name == "Owner" ? typeof(Owner) : null;
}
[Fact]
public async Task Should_Get_Attached_Property_Value()
{
var data = new Class1();
var target = ExpressionObserverBuilder.Build(data, "(Owner.Foo)", typeResolver: _typeResolver);
var result = await target.Take(1);
Assert.Equal("foo", result);
Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers());
}
[Fact]
public async Task Should_Get_Attached_Property_Value_With_Namespace()
{
var data = new Class1();
var target = ExpressionObserverBuilder.Build(
data,
"(NS:Owner.Foo)",
typeResolver: (ns, name) => ns == "NS" && name == "Owner" ? typeof(Owner) : null);
var result = await target.Take(1);
Assert.Equal("foo", result);
Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers());
}
[Fact]
public async Task Should_Get_Chained_Attached_Property_Value()
{
var data = new Class1
{
Next = new Class1
{
[Owner.FooProperty] = "bar",
}
};
var target = ExpressionObserverBuilder.Build(data, "Next.(Owner.Foo)", typeResolver: _typeResolver);
var result = await target.Take(1);
Assert.Equal("bar", result);
Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers());
}
[Fact]
public void Should_Track_Simple_Attached_Value()
{
var data = new Class1();
var target = ExpressionObserverBuilder.Build(data, "(Owner.Foo)", typeResolver: _typeResolver);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
data.SetValue(Owner.FooProperty, "bar");
Assert.Equal(new[] { "foo", "bar" }, result);
sub.Dispose();
Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers());
}
[Fact]
public void Should_Track_Chained_Attached_Value()
{
var data = new Class1
{
Next = new Class1
{
[Owner.FooProperty] = "foo",
}
};
var target = ExpressionObserverBuilder.Build(data, "Next.(Owner.Foo)", typeResolver: _typeResolver);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
data.Next.SetValue(Owner.FooProperty, "bar");
Assert.Equal(new[] { "foo", "bar" }, result);
sub.Dispose();
Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers());
}
[Fact]
public void Should_Not_Keep_Source_Alive()
{
Func<Tuple<ExpressionObserver, WeakReference>> run = () =>
{
var source = new Class1();
var target = ExpressionObserverBuilder.Build(source, "(Owner.Foo)", typeResolver: _typeResolver);
return Tuple.Create(target, new WeakReference(source));
};
var result = run();
result.Item1.Subscribe(x => { });
GC.Collect();
Assert.Null(result.Item2.Target);
}
[Fact]
public void Should_Fail_With_Attached_Property_With_Only_1_Part()
{
var data = new Class1();
Assert.Throws<ExpressionParseException>(() => ExpressionObserverBuilder.Build(data, "(Owner)", typeResolver: _typeResolver));
}
[Fact]
public void Should_Fail_With_Attached_Property_With_More_Than_2_Parts()
{
var data = new Class1();
Assert.Throws<ExpressionParseException>(() => ExpressionObserverBuilder.Build(data, "(Owner.Foo.Bar)", typeResolver: _typeResolver));
}
private static class Owner
{
public static readonly AttachedProperty<string> FooProperty =
AvaloniaProperty.RegisterAttached<Class1, string>(
"Foo",
typeof(Owner),
defaultValue: "foo");
}
private class Class1 : AvaloniaObject
{
public static readonly StyledProperty<Class1> NextProperty =
AvaloniaProperty.Register<Class1, Class1>(nameof(Next));
public Class1 Next
{
get { return GetValue(NextProperty); }
set { SetValue(NextProperty, value); }
}
}
}
}

59
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AvaloniaProperty.cs

@ -0,0 +1,59 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Diagnostics;
using Avalonia.Data.Core;
using Xunit;
using Avalonia.Markup.Parsers;
namespace Avalonia.Markup.UnitTests.Parsers
{
public class ExpressionObserverBuilderTests_AvaloniaProperty
{
public ExpressionObserverBuilderTests_AvaloniaProperty()
{
var foo = Class1.FooProperty;
}
[Fact]
public async Task Should_Get_AvaloniaProperty_By_Name()
{
var data = new Class1();
var target = ExpressionObserverBuilder.Build(data, "Foo");
var result = await target.Take(1);
Assert.Equal("foo", result);
Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers());
}
[Fact]
public void Should_Track_AvaloniaProperty_By_Name()
{
var data = new Class1();
var target = ExpressionObserverBuilder.Build(data, "Foo");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
data.SetValue(Class1.FooProperty, "bar");
Assert.Equal(new[] { "foo", "bar" }, result);
sub.Dispose();
Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers());
}
private class Class1 : AvaloniaObject
{
public static readonly StyledProperty<string> FooProperty =
AvaloniaProperty.Register<Class1, string>("Foo", defaultValue: "foo");
public string ClrProperty { get; } = "clr-property";
}
}
}

371
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs

@ -0,0 +1,371 @@
using Avalonia.Collections;
using Avalonia.Data.Core;
using Avalonia.Diagnostics;
using Avalonia.Markup.Parsers;
using Avalonia.Markup.Parsers.Nodes;
using Avalonia.UnitTests;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
namespace Avalonia.Markup.UnitTests.Parsers
{
public class ExpressionObserverBuilderTests_Indexer
{
[Fact]
public async Task Should_Get_Array_Value()
{
var data = new { Foo = new[] { "foo", "bar" } };
var target = ExpressionObserverBuilder.Build(data, "Foo[1]");
var result = await target.Take(1);
Assert.Equal("bar", result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Get_UnsetValue_For_Invalid_Array_Index()
{
var data = new { Foo = new[] { "foo", "bar" } };
var target = ExpressionObserverBuilder.Build(data, "Foo[invalid]");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Get_UnsetValue_For_Invalid_Dictionary_Index()
{
var data = new { Foo = new Dictionary<int, string> { { 1, "foo" } } };
var target = ExpressionObserverBuilder.Build(data, "Foo[invalid]");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Get_UnsetValue_For_Object_Without_Indexer()
{
var data = new { Foo = 5 };
var target = ExpressionObserverBuilder.Build(data, "Foo[noindexer]");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Get_MultiDimensional_Array_Value()
{
var data = new { Foo = new[,] { { "foo", "bar" }, { "baz", "qux" } } };
var target = ExpressionObserverBuilder.Build(data, "Foo[1, 1]");
var result = await target.Take(1);
Assert.Equal("qux", result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Get_Value_For_String_Indexer()
{
var data = new { Foo = new Dictionary<string, string> { { "foo", "bar" }, { "baz", "qux" } } };
var target = ExpressionObserverBuilder.Build(data, "Foo[foo]");
var result = await target.Take(1);
Assert.Equal("bar", result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Get_Value_For_Non_String_Indexer()
{
var data = new { Foo = new Dictionary<double, string> { { 1.0, "bar" }, { 2.0, "qux" } } };
var target = ExpressionObserverBuilder.Build(data, "Foo[1.0]");
var result = await target.Take(1);
Assert.Equal("bar", result);
GC.KeepAlive(data);
}
[Fact]
public async Task Array_Out_Of_Bounds_Should_Return_UnsetValue()
{
var data = new { Foo = new[] { "foo", "bar" } };
var target = ExpressionObserverBuilder.Build(data, "Foo[2]");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async Task Array_With_Wrong_Dimensions_Should_Return_UnsetValue()
{
var data = new { Foo = new[] { "foo", "bar" } };
var target = ExpressionObserverBuilder.Build(data, "Foo[1,2]");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async Task List_Out_Of_Bounds_Should_Return_UnsetValue()
{
var data = new { Foo = new List<string> { "foo", "bar" } };
var target = ExpressionObserverBuilder.Build(data, "Foo[2]");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Get_List_Value()
{
var data = new { Foo = new List<string> { "foo", "bar" } };
var target = ExpressionObserverBuilder.Build(data, "Foo[1]");
var result = await target.Take(1);
Assert.Equal("bar", result);
GC.KeepAlive(data);
}
[Fact]
public void Should_Track_INCC_Add()
{
var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = ExpressionObserverBuilder.Build(data, "Foo[2]");
var result = new List<object>();
using (var sub = target.Subscribe(x => result.Add(x)))
{
data.Foo.Add("baz");
}
Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "baz" }, result);
Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers());
GC.KeepAlive(data);
}
[Fact]
public void Should_Track_INCC_Remove()
{
var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = ExpressionObserverBuilder.Build(data, "Foo[0]");
var result = new List<object>();
using (var sub = target.Subscribe(x => result.Add(x)))
{
data.Foo.RemoveAt(0);
}
Assert.Equal(new[] { "foo", "bar" }, result);
Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers());
GC.KeepAlive(data);
}
[Fact]
public void Should_Track_INCC_Replace()
{
var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = ExpressionObserverBuilder.Build(data, "Foo[1]");
var result = new List<object>();
using (var sub = target.Subscribe(x => result.Add(x)))
{
data.Foo[1] = "baz";
}
Assert.Equal(new[] { "bar", "baz" }, result);
Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers());
GC.KeepAlive(data);
}
[Fact]
public void Should_Track_INCC_Move()
{
// Using ObservableCollection here because AvaloniaList does not yet have a Move
// method, but even if it did we need to test with ObservableCollection as well
// as AvaloniaList as it implements PropertyChanged as an explicit interface event.
var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } };
var target = ExpressionObserverBuilder.Build(data, "Foo[1]");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
data.Foo.Move(0, 1);
Assert.Equal(new[] { "bar", "foo" }, result);
GC.KeepAlive(sub);
GC.KeepAlive(data);
}
[Fact]
public void Should_Track_INCC_Reset()
{
var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = ExpressionObserverBuilder.Build(data, "Foo[1]");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
data.Foo.Clear();
Assert.Equal(new[] { "bar", AvaloniaProperty.UnsetValue }, result);
GC.KeepAlive(sub);
GC.KeepAlive(data);
}
[Fact]
public void Should_Track_NonIntegerIndexer()
{
var data = new { Foo = new NonIntegerIndexer() };
data.Foo["foo"] = "bar";
data.Foo["baz"] = "qux";
var target = ExpressionObserverBuilder.Build(data, "Foo[foo]");
var result = new List<object>();
using (var sub = target.Subscribe(x => result.Add(x)))
{
data.Foo["foo"] = "bar2";
}
var expected = new[] { "bar", "bar2" };
Assert.Equal(expected, result);
Assert.Equal(0, data.Foo.PropertyChangedSubscriptionCount);
GC.KeepAlive(data);
}
[Fact]
public void Should_SetArrayIndex()
{
var data = new { Foo = new[] { "foo", "bar" } };
var target = ExpressionObserverBuilder.Build(data, "Foo[1]");
using (target.Subscribe(_ => { }))
{
Assert.True(target.SetValue("baz"));
}
Assert.Equal("baz", data.Foo[1]);
GC.KeepAlive(data);
}
[Fact]
public void Should_Set_ExistingDictionaryEntry()
{
var data = new
{
Foo = new Dictionary<string, int>
{
{"foo", 1 }
}
};
var target = ExpressionObserverBuilder.Build(data, "Foo[foo]");
using (target.Subscribe(_ => { }))
{
Assert.True(target.SetValue(4));
}
Assert.Equal(4, data.Foo["foo"]);
GC.KeepAlive(data);
}
[Fact]
public void Should_Add_NewDictionaryEntry()
{
var data = new
{
Foo = new Dictionary<string, int>
{
{"foo", 1 }
}
};
var target = ExpressionObserverBuilder.Build(data, "Foo[bar]");
using (target.Subscribe(_ => { }))
{
Assert.True(target.SetValue(4));
}
Assert.Equal(4, data.Foo["bar"]);
GC.KeepAlive(data);
}
[Fact]
public void Should_Set_NonIntegerIndexer()
{
var data = new { Foo = new NonIntegerIndexer() };
data.Foo["foo"] = "bar";
data.Foo["baz"] = "qux";
var target = ExpressionObserverBuilder.Build(data, "Foo[foo]");
using (target.Subscribe(_ => { }))
{
Assert.True(target.SetValue("bar2"));
}
Assert.Equal("bar2", data.Foo["foo"]);
GC.KeepAlive(data);
}
[Fact]
public async Task Indexer_Only_Binding_Works()
{
var data = new[] { 1, 2, 3 };
var target = ExpressionObserverBuilder.Build(data, "[1]");
var value = await target.Take(1);
Assert.Equal(data[1], value);
}
private class NonIntegerIndexer : NotifyingBase
{
private readonly Dictionary<string, string> _storage = new Dictionary<string, string>();
public string this[string key]
{
get
{
return _storage[key];
}
set
{
_storage[key] = value;
RaisePropertyChanged(CommonPropertyNames.IndexerName);
}
}
}
}
}

13
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Method.cs → tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs

@ -1,5 +1,6 @@
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using System;
using System.Collections.Generic;
using System.Linq;
@ -8,9 +9,9 @@ using System.Text;
using System.Threading.Tasks;
using Xunit;
namespace Avalonia.Base.UnitTests.Data.Core
namespace Avalonia.Markup.UnitTests.Parsers
{
public class ExpressionObserverTests_Method
public class ExpressionObserverBuilderTests_Method
{
private class TestObject
{
@ -30,7 +31,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_Method()
{
var data = new TestObject();
var observer = new ExpressionObserver(data, nameof(TestObject.MethodWithoutReturn));
var observer = ExpressionObserverBuilder.Build(data, nameof(TestObject.MethodWithoutReturn));
var result = await observer.Take(1);
Assert.NotNull(result);
@ -46,7 +47,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_Method_WithCorrectDelegateType(string methodName, Type expectedType)
{
var data = new TestObject();
var observer = new ExpressionObserver(data, methodName);
var observer = ExpressionObserverBuilder.Build(data, methodName);
var result = await observer.Take(1);
Assert.IsType(expectedType, result);
@ -58,7 +59,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Can_Call_Method_Returned_From_Observer()
{
var data = new TestObject();
var observer = new ExpressionObserver(data, nameof(TestObject.MethodWithReturnAndParameters));
var observer = ExpressionObserverBuilder.Build(data, nameof(TestObject.MethodWithReturnAndParameters));
var result = await observer.Take(1);
var callback = (Func<int, int>)result;
@ -74,7 +75,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Return_Error_Notification_If_Too_Many_Parameters(string methodName)
{
var data = new TestObject();
var observer = new ExpressionObserver(data, methodName);
var observer = ExpressionObserverBuilder.Build(data, methodName);
var result = await observer.Take(1);
Assert.IsType<BindingNotification>(result);

112
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs

@ -0,0 +1,112 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Data;
using Avalonia.Markup.Parsers;
using Xunit;
namespace Avalonia.Markup.UnitTests.Parsers
{
public class ExpressionObserverBuilderTests_Negation
{
[Fact]
public async Task Should_Negate_0()
{
var data = new { Foo = 0 };
var target = ExpressionObserverBuilder.Build(data, "!Foo");
var result = await target.Take(1);
Assert.True((bool)result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Negate_1()
{
var data = new { Foo = 1 };
var target = ExpressionObserverBuilder.Build(data, "!Foo");
var result = await target.Take(1);
Assert.False((bool)result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Negate_False_String()
{
var data = new { Foo = "false" };
var target = ExpressionObserverBuilder.Build(data, "!Foo");
var result = await target.Take(1);
Assert.True((bool)result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Negate_True_String()
{
var data = new { Foo = "True" };
var target = ExpressionObserverBuilder.Build(data, "!Foo");
var result = await target.Take(1);
Assert.False((bool)result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Return_BindingNotification_For_String_Not_Convertible_To_Boolean()
{
var data = new { Foo = "foo" };
var target = ExpressionObserverBuilder.Build(data, "!Foo");
var result = await target.Take(1);
Assert.Equal(
new BindingNotification(
new InvalidCastException($"Unable to convert 'foo' to bool."),
BindingErrorType.Error),
result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Return_BindingNotification_For_Value_Not_Convertible_To_Boolean()
{
var data = new { Foo = new object() };
var target = ExpressionObserverBuilder.Build(data, "!Foo");
var result = await target.Take(1);
Assert.Equal(
new BindingNotification(
new InvalidCastException($"Unable to convert 'System.Object' to bool."),
BindingErrorType.Error),
result);
GC.KeepAlive(data);
}
[Fact]
public void SetValue_Should_Return_False_For_Invalid_Value()
{
var data = new { Foo = "foo" };
var target = ExpressionObserverBuilder.Build(data, "!Foo");
target.Subscribe(_ => { });
Assert.False(target.SetValue("bar"));
GC.KeepAlive(data);
}
private class Test
{
public bool Foo { get; set; }
}
}
}

42
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs

@ -0,0 +1,42 @@
using Avalonia.Data;
using Avalonia.Markup.Parsers;
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
namespace Avalonia.Markup.UnitTests.Parsers
{
public class ExpressionObserverBuilderTests_Property
{
[Fact]
public async Task Should_Return_BindingNotification_Error_For_Broken_Chain()
{
var data = new { Foo = new { Bar = 1 } };
var target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz");
var result = await target.Take(1);
Assert.IsType<BindingNotification>(result);
Assert.Equal(
new BindingNotification(
new MissingMemberException("Could not find CLR property 'Baz' on '1'"), BindingErrorType.Error),
result);
GC.KeepAlive(data);
}
[Fact]
public void Should_Have_Null_ResultType_For_Broken_Chain()
{
var data = new { Foo = new { Bar = 1 } };
var target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz");
Assert.Null(target.ResultType);
GC.KeepAlive(data);
}
}
}

2
tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs

@ -6,7 +6,7 @@ using Avalonia.Markup.Parsers;
using Sprache;
using Xunit;
namespace Avalonia.Markup.UnitTest.Parsers
namespace Avalonia.Markup.UnitTests.Parsers
{
public class SelectorGrammarTests
{

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save