Browse Source

Merge branch 'master' into FixReadOnlyCaretVisibility

pull/1725/head
Rayyan Tahir 8 years ago
committed by GitHub
parent
commit
0e8093de4f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 65
      src/Avalonia.Animation/Animation.cs
  2. 66
      src/Avalonia.Animation/AnimatorKeyFrame.cs
  3. 188
      src/Avalonia.Animation/AnimatorStateMachine`1.cs
  4. 75
      src/Avalonia.Animation/Animator`1.cs
  5. 2
      src/Avalonia.Animation/Cue.cs
  6. 8
      src/Avalonia.Animation/DoubleAnimator.cs
  7. 2
      src/Avalonia.Animation/IAnimationSetter.cs
  8. 16
      src/Avalonia.Animation/KeyFrame.cs
  9. 8
      src/Avalonia.Animation/KeyFramePair`1.cs
  10. 51
      src/Avalonia.Base/AvaloniaObject.cs
  11. 52
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  12. 25
      src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs
  13. 3
      src/Avalonia.Base/IPriorityValueOwner.cs
  14. 11
      src/Avalonia.Base/PriorityValue.cs
  15. 6
      src/Avalonia.Base/Utilities/CharacterReader.cs
  16. 53
      src/Avalonia.Base/Utilities/DeferredSetter.cs
  17. 6
      src/Avalonia.Base/Utilities/IdentifierParser.cs
  18. 58
      src/Avalonia.Base/Utilities/SingleOrQueue.cs
  19. 24
      src/Avalonia.Base/ValueStore.cs
  20. 90
      src/Avalonia.Controls/ProgressBar.cs
  21. 1
      src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs
  22. 6
      src/Avalonia.Styling/Styling/Setter.cs
  23. 45
      src/Avalonia.Themes.Default/ProgressBar.xaml
  24. 1
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  25. 69
      src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs
  26. 160
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  27. 85
      src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs
  28. 4
      src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaRuntimeTypeProvider.cs
  29. 98
      src/Markup/Avalonia.Markup/Data/Binding.cs
  30. 11
      src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs
  31. 15
      src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs
  32. 158
      src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs
  33. 39
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs
  34. 54
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs
  35. 12
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs
  36. 4
      src/OSX/Avalonia.MonoMac/SystemDialogsImpl.cs
  37. 11
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs
  38. 52
      tests/Avalonia.Base.UnitTests/PriorityValueTests.cs
  39. 50
      tests/Avalonia.Base.UnitTests/Utilities/SingleOrQueueTests.cs
  40. 3
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs
  41. 36
      tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs
  42. 226
      tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs
  43. 29
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
  44. 55
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs
  45. 54
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs

65
src/Avalonia.Animation/Animation.cs

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

66
src/Avalonia.Animation/AnimatorKeyFrame.cs

@ -4,6 +4,8 @@ using System.Text;
using System.ComponentModel; using System.ComponentModel;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Data;
using Avalonia.Reactive;
namespace Avalonia.Animation namespace Avalonia.Animation
{ {
@ -11,13 +13,63 @@ namespace Avalonia.Animation
/// Defines a KeyFrame that is used for /// Defines a KeyFrame that is used for
/// <see cref="Animator{T}"/> objects. /// <see cref="Animator{T}"/> objects.
/// </summary> /// </summary>
public class AnimatorKeyFrame public class AnimatorKeyFrame : AvaloniaObject
{ {
public Type Handler; public static readonly DirectProperty<AnimatorKeyFrame, object> ValueProperty =
public Cue Cue; AvaloniaProperty.RegisterDirect<AnimatorKeyFrame, object>(nameof(Value), k => k._value, (k, v) => k._value = v);
public TimeSpan KeyTime;
internal bool timeSpanSet, cueSet; public AnimatorKeyFrame()
public AvaloniaProperty Property; {
public object Value;
}
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 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; _targetAnimation = animation;
_targetControl = control; _targetControl = control;
_neutralValue = (T)_targetControl.GetValue(_parent.Property); _neutralValue = (T)_targetControl.GetValue(_parent.Property);
@ -123,121 +123,133 @@ namespace Avalonia.Animation
double _tempDuration = 0d, _easedTime; double _tempDuration = 0d, _easedTime;
checkstate: bool handled = false;
switch (_currentState)
while (!handled)
{ {
case KeyFramesStates.DoDelay: switch (_currentState)
{
case KeyFramesStates.DoDelay:
if (_fillMode == FillMode.Backward if (_fillMode == FillMode.Backward
|| _fillMode == FillMode.Both) || _fillMode == FillMode.Both)
{
if (_currentIteration == 0)
{ {
_targetObserver.OnNext(_firstKFValue); if (_currentIteration == 0)
{
_targetObserver.OnNext(_firstKFValue);
}
else
{
_targetObserver.OnNext(_lastInterpValue);
}
}
if (_delayFrameCount > _delayTotalFrameCount)
{
_currentState = KeyFramesStates.DoRun;
} }
else else
{ {
_targetObserver.OnNext(_lastInterpValue); handled = true;
_delayFrameCount++;
} }
} break;
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;
}
_tempDuration = (double)_durationFrameCount / _durationTotalFrameCount; case KeyFramesStates.DoRun:
_currentState = KeyFramesStates.RunApplyValue;
goto checkstate; if (_isReversed)
_currentState = KeyFramesStates.RunBackwards;
else
_currentState = KeyFramesStates.RunForwards;
case KeyFramesStates.RunBackwards: break;
if (_durationFrameCount > _durationTotalFrameCount) case KeyFramesStates.RunForwards:
{
_currentState = KeyFramesStates.RunComplete;
goto checkstate;
}
_tempDuration = (double)(_durationTotalFrameCount - _durationFrameCount) / _durationTotalFrameCount; if (_durationFrameCount > _durationTotalFrameCount)
_currentState = KeyFramesStates.RunApplyValue; {
_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++; case KeyFramesStates.RunApplyValue:
_lastInterpValue = Interpolator(_easedTime, _neutralValue);
_targetObserver.OnNext(_lastInterpValue);
_currentState = KeyFramesStates.DoRun;
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) case KeyFramesStates.RunComplete:
{
_delayFrameCount = 0;
_durationFrameCount = 0;
if (_isLooping) if (_checkLoopAndRepeat)
{
_currentState = KeyFramesStates.DoRun;
}
else if (_isRepeating)
{ {
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 if (_animationDirection == PlaybackDirection.Alternate
|| _animationDirection == PlaybackDirection.AlternateReverse) || _animationDirection == PlaybackDirection.AlternateReverse)
_isReversed = !_isReversed; _isReversed = !_isReversed;
break; break;
} }
_currentState = KeyFramesStates.Stop; _currentState = KeyFramesStates.Stop;
goto checkstate; break;
case KeyFramesStates.Stop: case KeyFramesStates.Stop:
if (_fillMode == FillMode.Forward if (_fillMode == FillMode.Forward
|| _fillMode == FillMode.Both) || _fillMode == FillMode.Both)
{ {
_targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue); _targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue);
} }
_targetObserver.OnCompleted(); _targetObserver.OnCompleted();
break; handled = true;
break;
default:
handled = true;
break;
}
} }
} }
@ -253,4 +265,4 @@ namespace Avalonia.Animation
_currentState = KeyFramesStates.Disposed; _currentState = KeyFramesStates.Disposed;
} }
} }
} }

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

@ -19,7 +19,7 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// List of type-converted keyframes. /// List of type-converted keyframes.
/// </summary> /// </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; private bool _isVerfifiedAndConverted;
@ -38,12 +38,11 @@ namespace Avalonia.Animation
public virtual IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch) public virtual IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch)
{ {
if (!_isVerfifiedAndConverted) if (!_isVerfifiedAndConverted)
VerifyConvertKeyFrames(animation, typeof(T)); VerifyConvertKeyFrames();
return obsMatch return obsMatch
.Where(p => p == true)
// Ignore triggers when global timers are paused. // Ignore triggers when global timers are paused.
.Where(p => Timing.GetGlobalPlayState() != PlayState.Pause) .Where(p => p && Timing.GetGlobalPlayState() != PlayState.Pause)
.Subscribe(_ => .Subscribe(_ =>
{ {
var timerObs = RunKeyFrames(animation, control); 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> /// <param name="t">The time parameter, relative to the total animation time</param>
protected (double IntraKFTime, KeyFramePair<T> KFPair) GetKFPairAndIntraKFTime(double t) protected (double IntraKFTime, KeyFramePair<T> KFPair) GetKFPairAndIntraKFTime(double t)
{ {
KeyValuePair<double, (T, bool)> firstCue, lastCue; KeyValuePair<double, (AnimatorKeyFrame frame, bool isNeutral)> firstCue, lastCue;
int kvCount = _convertedKeyframes.Count(); int kvCount = _convertedKeyframes.Count;
if (kvCount > 2) if (kvCount > 2)
{ {
if (DoubleUtils.AboutEqual(t, 0.0) || t < 0.0) if (DoubleUtils.AboutEqual(t, 0.0) || t < 0.0)
@ -76,8 +75,8 @@ namespace Avalonia.Animation
} }
else else
{ {
firstCue = _convertedKeyframes.Where(j => j.Key <= t).Last(); firstCue = _convertedKeyframes.Last(j => j.Key <= t);
lastCue = _convertedKeyframes.Where(j => j.Key >= t).First(); lastCue = _convertedKeyframes.First(j => j.Key >= t);
} }
} }
else else
@ -89,7 +88,9 @@ namespace Avalonia.Animation
double t0 = firstCue.Key; double t0 = firstCue.Key;
double t1 = lastCue.Key; double t1 = lastCue.Key;
var intraframeTime = (t - t0) / (t1 - t0); 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> /// </summary>
private IDisposable RunKeyFrames(Animation animation, Animatable control) private IDisposable RunKeyFrames(Animation animation, Animatable control)
{ {
var _kfStateMach = new AnimatorStateMachine<T>(); var stateMachine = new AnimatorStateMachine<T>();
_kfStateMach.Initialize(animation, control, this); stateMachine.Initialize(animation, control, this);
Timing.AnimationStateTimer Timing.AnimationStateTimer
.TakeWhile(_ => !_kfStateMach._unsubscribe) .TakeWhile(_ => !stateMachine._unsubscribe)
.Subscribe(p => .Subscribe(p => stateMachine.Step(p, DoInterpolation));
{
_kfStateMach.Step(p, DoInterpolation);
});
return control.Bind(Property, _kfStateMach, BindingPriority.Animation); return control.Bind(Property, stateMachine, BindingPriority.Animation);
} }
/// <summary> /// <summary>
@ -119,39 +117,19 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// Verifies and converts keyframe values according to this class's target type. /// Verifies and converts keyframe values according to this class's target type.
/// </summary> /// </summary>
private void VerifyConvertKeyFrames(Animation animation, Type type) private void VerifyConvertKeyFrames()
{ {
var typeConv = TypeDescriptor.GetConverter(type); foreach (AnimatorKeyFrame keyframe in this)
foreach (AnimatorKeyFrame k in this)
{ {
if (k.Value == null) _convertedKeyframes.Add(keyframe.Cue.CueValue, (keyframe, false));
{
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));
} }
SortKeyFrameCues(_convertedKeyframes); AddNeutralKeyFramesIfNeeded();
_isVerfifiedAndConverted = true; _isVerfifiedAndConverted = true;
} }
private void SortKeyFrameCues(Dictionary<double, (T, bool)> convertedValues) private void AddNeutralKeyFramesIfNeeded()
{ {
bool hasStartKey, hasEndKey; bool hasStartKey, hasEndKey;
hasStartKey = hasEndKey = false; hasStartKey = hasEndKey = false;
@ -170,23 +148,20 @@ namespace Avalonia.Animation
} }
if (!hasStartKey || !hasEndKey) if (!hasStartKey || !hasEndKey)
AddNeutralKeyFrames(hasStartKey, hasEndKey, _convertedKeyframes); AddNeutralKeyFrames(hasStartKey, hasEndKey);
_convertedKeyframes = _convertedKeyframes.OrderBy(p => p.Key)
.ToDictionary((k) => k.Key, (v) => v.Value);
} }
private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey, Dictionary<double, (T, bool)> convertedKeyframes) private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey)
{ {
if (!hasStartKey) if (!hasStartKey)
{ {
convertedKeyframes.Add(0.0d, (default(T), true)); _convertedKeyframes.Add(0.0d, (new AnimatorKeyFrame { Value = default(T) }, true));
} }
if (!hasEndKey) 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"/>. /// A Cue object for <see cref="KeyFrame"/>.
/// </summary> /// </summary>
[TypeConverter(typeof(CueTypeConverter))] [TypeConverter(typeof(CueTypeConverter))]
public struct Cue : IEquatable<Cue>, IEquatable<double> public readonly struct Cue : IEquatable<Cue>, IEquatable<double>
{ {
/// <summary> /// <summary>
/// The normalized percent value, ranging from 0.0 to 1.0 /// 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 firstKF = pair.KFPair.FirstKeyFrame;
var secondKF = pair.KFPair.SecondKeyFrame; var secondKF = pair.KFPair.SecondKeyFrame;
if (firstKF.Value.isNeutral) if (firstKF.isNeutral)
y0 = neutralValue; y0 = neutralValue;
else else
y0 = firstKF.Value.TargetValue; y0 = firstKF.TargetValue;
if (secondKF.Value.isNeutral) if (secondKF.isNeutral)
y1 = neutralValue; y1 = neutralValue;
else else
y1 = secondKF.Value.TargetValue; y1 = secondKF.TargetValue;
// Do linear parametric interpolation // Do linear parametric interpolation
return y0 + (pair.IntraKFTime) * (y1 - y0); return y0 + (pair.IntraKFTime) * (y1 - y0);

2
src/Avalonia.Animation/IAnimationSetter.cs

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

16
src/Avalonia.Animation/KeyFrame.cs

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

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

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

52
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@ -106,7 +106,7 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Finds a registered non-attached property on a type by name. /// Finds a registered property on a type by name.
/// </summary> /// </summary>
/// <param name="type">The type.</param> /// <param name="type">The type.</param>
/// <param name="name">The property name.</param> /// <param name="name">The property name.</param>
@ -130,7 +130,7 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Finds a registered non-attached property on a type by name. /// Finds a registered property on an object by name.
/// </summary> /// </summary>
/// <param name="o">The object.</param> /// <param name="o">The object.</param>
/// <param name="name">The property name.</param> /// <param name="name">The property name.</param>
@ -148,52 +148,6 @@ namespace Avalonia
return FindRegistered(o.GetType(), name); 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> /// <summary>
/// Checks whether a <see cref="AvaloniaProperty"/> is registered on a type. /// Checks whether a <see cref="AvaloniaProperty"/> is registered on a type.
/// </summary> /// </summary>
@ -287,4 +241,4 @@ namespace Avalonia
_attachedCache.Clear(); _attachedCache.Clear();
} }
} }
} }

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

@ -23,15 +23,24 @@ namespace Avalonia.Diagnostics
{ {
var set = o.GetSetValues(); var set = o.GetSetValues();
PriorityValue value; if (set.TryGetValue(property, out var obj))
if (set.TryGetValue(property, out value))
{ {
return new AvaloniaPropertyValue( if (obj is PriorityValue value)
property, {
o.GetValue(property), return new AvaloniaPropertyValue(
(BindingPriority)value.ValuePriority, property,
value.GetDiagnostic()); o.GetValue(property),
(BindingPriority)value.ValuePriority,
value.GetDiagnostic());
}
else
{
return new AvaloniaPropertyValue(
property,
obj,
BindingPriority.LocalValue,
"Local value");
}
} }
else 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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Utilities;
namespace Avalonia namespace Avalonia
{ {
@ -31,5 +32,7 @@ namespace Avalonia
/// Ensures that the current thread is the UI thread. /// Ensures that the current thread is the UI thread.
/// </summary> /// </summary>
void VerifyAccess(); 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 /// 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 /// are multiple bindings registered with the same priority, the most recently added binding
/// has a higher priority. Each time the value changes, the /// 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. /// owner object is fired with the old and new values.
/// </remarks> /// </remarks>
internal class PriorityValue internal class PriorityValue
@ -30,7 +30,6 @@ namespace Avalonia
private readonly SingleOrDictionary<int, PriorityLevel> _levels = new SingleOrDictionary<int, PriorityLevel>(); private readonly SingleOrDictionary<int, PriorityLevel> _levels = new SingleOrDictionary<int, PriorityLevel>();
private readonly Func<object, object> _validate; 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; private (object value, int priority) _value;
/// <summary> /// <summary>
@ -243,12 +242,18 @@ namespace Avalonia
/// <param name="priority">The priority level that the value came from.</param> /// <param name="priority">The priority level that the value came from.</param>
private void UpdateValue(object value, int priority) private void UpdateValue(object value, int priority)
{ {
delayedSetter.SetAndNotify(this, Owner.Setter.SetAndNotify(Property,
ref _value, ref _value,
UpdateCore, UpdateCore,
(value, priority)); (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( private bool UpdateCore(
(object value, int priority) update, (object value, int priority) update,
ref (object value, int priority) backing, ref (object value, int priority) backing,

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

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

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

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

6
src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs → src/Avalonia.Base/Utilities/IdentifierParser.cs

@ -4,11 +4,11 @@
using System.Globalization; using System.Globalization;
using System.Text; using System.Text;
namespace Avalonia.Markup.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)) 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;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Utilities;
namespace Avalonia namespace Avalonia
{ {
@ -91,15 +92,15 @@ namespace Avalonia
public void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification) 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) 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) public object GetValue(AvaloniaProperty property)
{ {
@ -115,7 +116,7 @@ namespace Avalonia
public bool IsAnimating(AvaloniaProperty property) 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) public bool IsSet(AvaloniaProperty property)
@ -148,13 +149,11 @@ namespace Avalonia
validate2 = v => validate(_owner, v); validate2 = v => validate(_owner, v);
} }
PriorityValue result = new PriorityValue( return new PriorityValue(
this, this,
property, property,
property.PropertyType, property.PropertyType,
validate2); validate2);
return result;
} }
private object Validate(AvaloniaProperty property, object value) private object Validate(AvaloniaProperty property, object value)
@ -168,5 +167,16 @@ namespace Avalonia
return value; return value;
} }
private DeferredSetter<object> _defferedSetter;
public DeferredSetter<object> Setter
{
get
{
return _defferedSetter ??
(_defferedSetter = new DeferredSetter<object>());
}
}
} }
} }

90
src/Avalonia.Controls/ProgressBar.cs

@ -21,18 +21,27 @@ namespace Avalonia.Controls
public static readonly StyledProperty<Orientation> OrientationProperty = public static readonly StyledProperty<Orientation> OrientationProperty =
AvaloniaProperty.Register<ProgressBar, Orientation>(nameof(Orientation), Orientation.Horizontal); 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 Border _indicator;
private IndeterminateAnimation _indeterminateAnimation;
static ProgressBar() static ProgressBar()
{ {
PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical"); PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical");
PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal"); PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal");
PseudoClass(IsIndeterminateProperty, ":indeterminate");
ValueProperty.Changed.AddClassHandler<ProgressBar>(x => x.ValueChanged); 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 public bool IsIndeterminate
@ -46,6 +55,19 @@ namespace Avalonia.Controls
get => GetValue(OrientationProperty); get => GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value); 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/> /// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize) protected override Size ArrangeOverride(Size finalSize)
@ -60,7 +82,6 @@ namespace Avalonia.Controls
_indicator = e.NameScope.Get<Border>("PART_Indicator"); _indicator = e.NameScope.Get<Border>("PART_Indicator");
UpdateIndicator(Bounds.Size); UpdateIndicator(Bounds.Size);
UpdateIsIndeterminate(IsIndeterminate);
} }
private void UpdateIndicator(Size bounds) private void UpdateIndicator(Size bounds)
@ -70,9 +91,20 @@ namespace Avalonia.Controls
if (IsIndeterminate) if (IsIndeterminate)
{ {
if (Orientation == Orientation.Horizontal) 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 else
_indicator.Height = bounds.Height / 5.0; {
var height = bounds.Height / 5.0;
IndeterminateStartingOffset = -bounds.Height;
_indicator.Height = height;
IndeterminateEndingOffset = height;
}
} }
else 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) private void ValueChanged(AvaloniaPropertyChangedEventArgs e)
{ {
UpdateIndicator(Bounds.Size); 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;
}
}
} }
} }

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

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

6
src/Avalonia.Styling/Styling/Setter.cs

@ -126,7 +126,7 @@ namespace Avalonia.Styling
if (source != null) 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); return BindingOperations.Apply(control, Property, cloned, null);
} }
} }
@ -134,13 +134,13 @@ namespace Avalonia.Styling
return Disposable.Empty; 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) if (activator != null)
{ {
var description = style?.ToString(); var description = style?.ToString();
switch (sourceInstance.Mode) switch (mode)
{ {
case BindingMode.OneTime: case BindingMode.OneTime:
if (sourceInstance.Observable != null) if (sourceInstance.Observable != null)

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

@ -7,14 +7,9 @@
<Border Background="{TemplateBinding Background}" <Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"> BorderThickness="{TemplateBinding BorderThickness}">
<Grid> <Border Name="PART_Indicator"
<Border Name="PART_Track" BorderThickness="1"
BorderThickness="1" Background="{TemplateBinding Foreground}"/>
BorderBrush="{TemplateBinding Background}"/>
<Border Name="PART_Indicator"
BorderThickness="1"
Background="{TemplateBinding Foreground}" />
</Grid>
</Border> </Border>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
@ -35,4 +30,36 @@
<Setter Property="MinWidth" Value="14"/> <Setter Property="MinWidth" Value="14"/>
<Setter Property="MinHeight" Value="200"/> <Setter Property="MinHeight" Value="200"/>
</Style> </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\ResourceInclude.cs" />
<Compile Include="MarkupExtensions\StaticResourceExtension.cs" /> <Compile Include="MarkupExtensions\StaticResourceExtension.cs" />
<Compile Include="MarkupExtensions\StyleIncludeExtension.cs" /> <Compile Include="MarkupExtensions\StyleIncludeExtension.cs" />
<Compile Include="Parsers\PropertyParser.cs" />
<Compile Include="PortableXaml\AvaloniaXamlContext.cs" /> <Compile Include="PortableXaml\AvaloniaXamlContext.cs" />
<Compile Include="PortableXaml\AttributeExtensions.cs" /> <Compile Include="PortableXaml\AttributeExtensions.cs" />
<Compile Include="PortableXaml\AvaloniaMemberAttributeProvider.cs" /> <Compile Include="PortableXaml\AvaloniaMemberAttributeProvider.cs" />

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

@ -4,19 +4,19 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Globalization; 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.Markup.Xaml.Templates;
using Avalonia.Styling; using Avalonia.Styling;
using Portable.Xaml; using Avalonia.Utilities;
using Portable.Xaml.ComponentModel; using Portable.Xaml.ComponentModel;
using Portable.Xaml.Markup;
namespace Avalonia.Markup.Xaml.Converters namespace Avalonia.Markup.Xaml.Converters
{ {
public class AvaloniaPropertyTypeConverter : TypeConverter public class AvaloniaPropertyTypeConverter : TypeConverter
{ {
private static readonly Regex regex = new Regex(@"^\(?(\w*)\.(\w*)\)?|(.*)$");
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{ {
return sourceType == typeof(string); return sourceType == typeof(string);
@ -24,39 +24,48 @@ namespace Avalonia.Markup.Xaml.Converters
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{ {
var (owner, propertyName) = ParseProperty((string)value); var registry = AvaloniaPropertyRegistry.Instance;
var ownerType = TryResolveOwnerByName(context, owner) ?? var parser = new PropertyParser();
context.GetFirstAmbientValue<ControlTemplate>()?.TargetType ?? var reader = new CharacterReader((string)value);
context.GetFirstAmbientValue<Style>()?.Selector?.TargetType; 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( throw new XamlLoadException($"Could not find property '{effectiveOwner.Name}.{propertyName}'.");
$"Could not determine the owner type for property '{propertyName}'. " +
"Please fully qualify the property name or specify a target type on " +
"the containing template.");
} }
var property = AvaloniaPropertyRegistry.Instance.FindRegistered(ownerType, propertyName); if (effectiveOwner != targetType &&
!property.IsAttached &&
if (property == null) !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; return property;
} }
private Type TryResolveOwnerByName(ITypeDescriptorContext context, string owner) private Type TryResolveOwnerByName(ITypeDescriptorContext context, string ns, string owner)
{ {
if (owner != null) if (owner != null)
{ {
var resolver = context.GetService<IXamlTypeResolver>(); var result = context.ResolveType(ns, owner);
var result = resolver.Resolve(owner);
if (result == null) 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; return result;
@ -64,19 +73,5 @@ namespace Avalonia.Markup.Xaml.Converters
return null; 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);
}
}
} }
} }

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

@ -32,176 +32,28 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
{ {
var descriptorContext = (ITypeDescriptorContext)serviceProvider; var descriptorContext = (ITypeDescriptorContext)serviceProvider;
var pathInfo = ParsePath(Path, descriptorContext);
ValidateState(pathInfo);
return new Binding return new Binding
{ {
TypeResolver = descriptorContext.ResolveType, TypeResolver = descriptorContext.ResolveType,
Converter = Converter, Converter = Converter,
ConverterParameter = ConverterParameter, ConverterParameter = ConverterParameter,
ElementName = pathInfo.ElementName ?? ElementName, ElementName = ElementName,
FallbackValue = FallbackValue, FallbackValue = FallbackValue,
Mode = Mode, Mode = Mode,
Path = pathInfo.Path, Path = Path,
Priority = Priority, Priority = Priority,
Source = Source, Source = Source,
RelativeSource = pathInfo.RelativeSource ?? RelativeSource, RelativeSource = RelativeSource,
DefaultAnchor = new WeakReference(GetDefaultAnchor((ITypeDescriptorContext)serviceProvider)) DefaultAnchor = new WeakReference(GetDefaultAnchor((ITypeDescriptorContext)serviceProvider))
}; };
} }
private class PathInfo
{
public string Path { get; set; }
public string ElementName { get; set; }
public RelativeSource RelativeSource { get; set; }
}
private void ValidateState(PathInfo pathInfo)
{
if (pathInfo.ElementName != null && ElementName != null)
{
throw new InvalidOperationException(
"ElementName property cannot be set when an #elementName path is provided.");
}
if (pathInfo.RelativeSource != null && RelativeSource != null)
{
throw new InvalidOperationException(
"ElementName property cannot be set when a $self or $parent path is provided.");
}
if ((pathInfo.ElementName != null || ElementName != null) &&
(pathInfo.RelativeSource != null || RelativeSource != null))
{
throw new InvalidOperationException(
"ElementName property cannot be set with a RelativeSource.");
}
}
private static PathInfo ParsePath(string path, ITypeDescriptorContext context)
{
var result = new PathInfo();
if (string.IsNullOrWhiteSpace(path) || path == ".")
{
result.Path = string.Empty;
}
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) private static object GetDefaultAnchor(ITypeDescriptorContext context)
{ {
object anchor = null; // If the target is not a control, so we need to find an anchor that will let us look
// The target is not a control, so we need to find an anchor that will let us look
// up named controls and style resources. First look for the closest IControl in // up named controls and style resources. First look for the closest IControl in
// the context. // the context.
anchor = context.GetFirstAmbientValue<IControl>(); object anchor = context.GetFirstAmbientValue<IControl>();
// If a control was not found, then try to find the highest-level style as the XAML // If a control was not found, then try to find the highest-level style as the XAML
// file could be a XAML file containing only styles. // file could be a XAML file containing only styles.
@ -229,4 +81,4 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
public RelativeSource RelativeSource { get; set; } 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 System.Reflection;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Markup.Data;
using Avalonia.Markup.Xaml.Templates; using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Metadata; using Avalonia.Metadata;
@ -33,6 +32,7 @@ namespace Avalonia.Markup.Xaml.Context
private static readonly IEnumerable<Assembly> ForcedAssemblies = new[] private static readonly IEnumerable<Assembly> ForcedAssemblies = new[]
{ {
typeof(AvaloniaObject).GetTypeInfo().Assembly, typeof(AvaloniaObject).GetTypeInfo().Assembly,
typeof(Animation.Animation).GetTypeInfo().Assembly,
typeof(Control).GetTypeInfo().Assembly, typeof(Control).GetTypeInfo().Assembly,
typeof(Style).GetTypeInfo().Assembly, typeof(Style).GetTypeInfo().Assembly,
typeof(DataTemplate).GetTypeInfo().Assembly, typeof(DataTemplate).GetTypeInfo().Assembly,
@ -146,4 +146,4 @@ namespace Avalonia.Markup.Xaml.Context
return null; return null;
} }
} }
} }

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

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

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

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

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

@ -1,4 +1,5 @@
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.Utilities;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reactive; using System.Reactive;
@ -8,14 +9,14 @@ namespace Avalonia.Markup.Parsers
{ {
public static class ExpressionObserverBuilder public static class ExpressionObserverBuilder
{ {
internal static ExpressionNode Parse(string expression, bool enableValidation = false, Func<string, string, Type> typeResolver = null) internal static (ExpressionNode Node, SourceMode Mode) Parse(string expression, bool enableValidation = false, Func<string, string, Type> typeResolver = null)
{ {
if (string.IsNullOrWhiteSpace(expression)) if (string.IsNullOrWhiteSpace(expression))
{ {
return new EmptyExpressionNode(); return (new EmptyExpressionNode(), default);
} }
var reader = new Reader(expression); var reader = new CharacterReader(expression);
var parser = new ExpressionParser(enableValidation, typeResolver); var parser = new ExpressionParser(enableValidation, typeResolver);
var node = parser.Parse(reader); var node = parser.Parse(reader);
@ -36,7 +37,7 @@ namespace Avalonia.Markup.Parsers
{ {
return new ExpressionObserver( return new ExpressionObserver(
root, root,
Parse(expression, enableDataValidation, typeResolver), Parse(expression, enableDataValidation, typeResolver).Node,
description ?? expression); description ?? expression);
} }
@ -50,7 +51,7 @@ namespace Avalonia.Markup.Parsers
Contract.Requires<ArgumentNullException>(rootObservable != null); Contract.Requires<ArgumentNullException>(rootObservable != null);
return new ExpressionObserver( return new ExpressionObserver(
rootObservable, rootObservable,
Parse(expression, enableDataValidation, typeResolver), Parse(expression, enableDataValidation, typeResolver).Node,
description ?? expression); description ?? expression);
} }
@ -66,8 +67,8 @@ namespace Avalonia.Markup.Parsers
Contract.Requires<ArgumentNullException>(rootGetter != null); Contract.Requires<ArgumentNullException>(rootGetter != null);
return new ExpressionObserver( return new ExpressionObserver(
() => rootGetter(), rootGetter,
Parse(expression, enableDataValidation, typeResolver), Parse(expression, enableDataValidation, typeResolver).Node,
update, update,
description ?? expression); description ?? expression);
} }

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

@ -3,12 +3,19 @@
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.Markup.Parsers.Nodes; using Avalonia.Markup.Parsers.Nodes;
using Avalonia.Utilities;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace Avalonia.Markup.Parsers namespace Avalonia.Markup.Parsers
{ {
internal enum SourceMode
{
Data,
Control
}
internal class ExpressionParser internal class ExpressionParser
{ {
private readonly bool _enableValidation; private readonly bool _enableValidation;
@ -20,10 +27,11 @@ namespace Avalonia.Markup.Parsers
_enableValidation = enableValidation; _enableValidation = enableValidation;
} }
public ExpressionNode Parse(Reader r) public (ExpressionNode Node, SourceMode Mode) Parse(CharacterReader r)
{ {
var nodes = new List<ExpressionNode>(); var nodes = new List<ExpressionNode>();
var state = State.Start; var state = State.Start;
var mode = SourceMode.Data;
while (!r.End && state != State.End) while (!r.End && state != State.End)
{ {
@ -48,6 +56,16 @@ namespace Avalonia.Markup.Parsers
case State.Indexer: case State.Indexer:
state = ParseIndexer(r, nodes); state = ParseIndexer(r, nodes);
break; break;
case State.ElementName:
state = ParseElementName(r, nodes);
mode = SourceMode.Control;
break;
case State.RelativeSource:
state = ParseRelativeSource(r, nodes);
mode = SourceMode.Control;
break;
} }
} }
@ -61,16 +79,24 @@ namespace Avalonia.Markup.Parsers
nodes[n].Next = nodes[n + 1]; nodes[n].Next = nodes[n + 1];
} }
return nodes.FirstOrDefault(); return (nodes.FirstOrDefault(), mode);
} }
private State ParseStart(Reader r, IList<ExpressionNode> nodes) private State ParseStart(CharacterReader r, IList<ExpressionNode> nodes)
{ {
if (ParseNot(r)) if (ParseNot(r))
{ {
nodes.Add(new LogicalNotNode()); nodes.Add(new LogicalNotNode());
return State.Start; return State.Start;
} }
else if (ParseSharp(r))
{
return State.ElementName;
}
else if (ParseDollarSign(r))
{
return State.RelativeSource;
}
else if (ParseOpenBrace(r)) else if (ParseOpenBrace(r))
{ {
return State.AttachedProperty; return State.AttachedProperty;
@ -93,7 +119,7 @@ namespace Avalonia.Markup.Parsers
return State.End; return State.End;
} }
private static State ParseAfterMember(Reader r, IList<ExpressionNode> nodes) private static State ParseAfterMember(CharacterReader r, IList<ExpressionNode> nodes)
{ {
if (ParseMemberAccessor(r)) if (ParseMemberAccessor(r))
{ {
@ -112,7 +138,7 @@ namespace Avalonia.Markup.Parsers
return State.End; return State.End;
} }
private State ParseBeforeMember(Reader r, IList<ExpressionNode> nodes) private State ParseBeforeMember(CharacterReader r, IList<ExpressionNode> nodes)
{ {
if (ParseOpenBrace(r)) if (ParseOpenBrace(r))
{ {
@ -132,21 +158,9 @@ namespace Avalonia.Markup.Parsers
} }
} }
private State ParseAttachedProperty(Reader r, List<ExpressionNode> nodes) private State ParseAttachedProperty(CharacterReader r, List<ExpressionNode> nodes)
{ {
string ns = string.Empty; var (ns, owner) = ParseTypeName(r);
string owner;
var ownerOrNamespace = IdentifierParser.Parse(r);
if (r.TakeIf(':'))
{
ns = ownerOrNamespace;
owner = IdentifierParser.Parse(r);
}
else
{
owner = ownerOrNamespace;
}
if (r.End || !r.TakeIf('.')) if (r.End || !r.TakeIf('.'))
{ {
@ -171,7 +185,7 @@ namespace Avalonia.Markup.Parsers
return State.AfterMember; return State.AfterMember;
} }
private State ParseIndexer(Reader r, List<ExpressionNode> nodes) private State ParseIndexer(CharacterReader r, List<ExpressionNode> nodes)
{ {
var args = ArgumentListParser.Parse(r, '[', ']'); var args = ArgumentListParser.Parse(r, '[', ']');
@ -184,34 +198,128 @@ namespace Avalonia.Markup.Parsers
return State.AfterMember; return State.AfterMember;
} }
private static bool ParseNot(Reader r) 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('!'); return !r.End && r.TakeIf('!');
} }
private static bool ParseMemberAccessor(Reader r) private static bool ParseMemberAccessor(CharacterReader r)
{ {
return !r.End && r.TakeIf('.'); return !r.End && r.TakeIf('.');
} }
private static bool ParseOpenBrace(Reader r) private static bool ParseOpenBrace(CharacterReader r)
{ {
return !r.End && r.TakeIf('('); return !r.End && r.TakeIf('(');
} }
private static bool PeekOpenBracket(Reader r) private static bool PeekOpenBracket(CharacterReader r)
{ {
return !r.End && r.Peek == '['; return !r.End && r.Peek == '[';
} }
private static bool ParseStreamOperator(Reader r) private static bool ParseStreamOperator(CharacterReader r)
{ {
return !r.End && r.TakeIf('^'); 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 private enum State
{ {
Start, Start,
RelativeSource,
ElementName,
AfterMember, AfterMember,
BeforeMember, BeforeMember,
AttachedProperty, AttachedProperty,

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

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

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

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

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

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

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

@ -25,9 +25,9 @@ namespace Avalonia.MonoMac
else else
{ {
if (panel is NSOpenPanel openPanel) 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 else
tcs.SetResult(new[] { panel.Url.AbsoluteString }); tcs.SetResult(new[] { panel.Url.AbsoluteString.Replace("file://", "") });
} }
panel.OrderOut(panel); panel.OrderOut(panel);
keyWindow?.MakeKeyAndOrderFront(keyWindow); keyWindow?.MakeKeyAndOrderFront(keyWindow);

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

@ -99,17 +99,6 @@ namespace Avalonia.Base.UnitTests
Assert.Null(result); 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 private class Class1 : AvaloniaObject
{ {
public static readonly StyledProperty<string> FooProperty = public static readonly StyledProperty<string> FooProperty =

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

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

3
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs

@ -162,8 +162,9 @@ namespace Avalonia.Markup.UnitTests.Parsers
Assert.Equal(e.Arguments.ToArray(), args); 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>(); var result = new List<ExpressionNode>();
while (node != null) while (node != null)

36
tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs

@ -10,6 +10,7 @@ using Xunit;
using System.ComponentModel; using System.ComponentModel;
using Portable.Xaml; using Portable.Xaml;
using Portable.Xaml.Markup; using Portable.Xaml.Markup;
using Avalonia.Controls;
namespace Avalonia.Markup.Xaml.UnitTests.Converters namespace Avalonia.Markup.Xaml.UnitTests.Converters
{ {
@ -26,7 +27,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters
public void ConvertFrom_Finds_Fully_Qualified_Property() public void ConvertFrom_Finds_Fully_Qualified_Property()
{ {
var target = new AvaloniaPropertyTypeConverter(); var target = new AvaloniaPropertyTypeConverter();
var context = CreateContext(); var style = new Style(x => x.OfType<Class1>());
var context = CreateContext(style);
var result = target.ConvertFrom(context, null, "Class1.Foo"); var result = target.ConvertFrom(context, null, "Class1.Foo");
Assert.Equal(Class1.FooProperty, result); Assert.Equal(Class1.FooProperty, result);
@ -47,7 +49,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters
public void ConvertFrom_Finds_Attached_Property() public void ConvertFrom_Finds_Attached_Property()
{ {
var target = new AvaloniaPropertyTypeConverter(); var target = new AvaloniaPropertyTypeConverter();
var context = CreateContext(); var style = new Style(x => x.OfType<Class1>());
var context = CreateContext(style);
var result = target.ConvertFrom(context, null, "AttachedOwner.Attached"); var result = target.ConvertFrom(context, null, "AttachedOwner.Attached");
Assert.Equal(AttachedOwner.AttachedProperty, result); Assert.Equal(AttachedOwner.AttachedProperty, result);
@ -57,12 +60,37 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters
public void ConvertFrom_Finds_Attached_Property_With_Parentheses() public void ConvertFrom_Finds_Attached_Property_With_Parentheses()
{ {
var target = new AvaloniaPropertyTypeConverter(); var target = new AvaloniaPropertyTypeConverter();
var context = CreateContext(); var style = new Style(x => x.OfType<Class1>());
var context = CreateContext(style);
var result = target.ConvertFrom(context, null, "(AttachedOwner.Attached)"); var result = target.ConvertFrom(context, null, "(AttachedOwner.Attached)");
Assert.Equal(AttachedOwner.AttachedProperty, result); Assert.Equal(AttachedOwner.AttachedProperty, result);
} }
[Fact]
public void ConvertFrom_Throws_For_Nonexistent_Property()
{
var target = new AvaloniaPropertyTypeConverter();
var style = new Style(x => x.OfType<Class1>());
var context = CreateContext(style);
var ex = Assert.Throws<XamlLoadException>(() => target.ConvertFrom(context, null, "Nonexistent"));
Assert.Equal("Could not find property 'Class1.Nonexistent'.", ex.Message);
}
[Fact]
public void ConvertFrom_Throws_For_Nonexistent_Attached_Property()
{
var target = new AvaloniaPropertyTypeConverter();
var style = new Style(x => x.OfType<Class1>());
var context = CreateContext(style);
var ex = Assert.Throws<XamlLoadException>(() => target.ConvertFrom(context, null, "AttachedOwner.NonExistent"));
Assert.Equal("Could not find property 'AttachedOwner.NonExistent'.", ex.Message);
}
private ITypeDescriptorContext CreateContext(Style style = null) private ITypeDescriptorContext CreateContext(Style style = null)
{ {
var tdMock = new Mock<ITypeDescriptorContext>(); var tdMock = new Mock<ITypeDescriptorContext>();
@ -126,4 +154,4 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters
AvaloniaProperty.RegisterAttached<AttachedOwner, Class1, string>("Attached"); AvaloniaProperty.RegisterAttached<AttachedOwner, Class1, string>("Attached");
} }
} }
} }

226
tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs

@ -0,0 +1,226 @@
using System;
using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using Avalonia.Markup.Xaml.Parsers;
using Avalonia.Utilities;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Parsers
{
public class PropertyParserTests
{
[Fact]
public void Parses_Name()
{
var target = new PropertyParser();
var reader = new CharacterReader("Foo");
var (ns, owner, name) = target.Parse(reader);
Assert.Null(ns);
Assert.Null(owner);
Assert.Equal("Foo", name);
}
[Fact]
public void Parses_Owner_And_Name()
{
var target = new PropertyParser();
var reader = new CharacterReader("Foo.Bar");
var (ns, owner, name) = target.Parse(reader);
Assert.Null(ns);
Assert.Equal("Foo", owner);
Assert.Equal("Bar", name);
}
[Fact]
public void Parses_Namespace_Owner_And_Name()
{
var target = new PropertyParser();
var reader = new CharacterReader("foo:Bar.Baz");
var (ns, owner, name) = target.Parse(reader);
Assert.Equal("foo", ns);
Assert.Equal("Bar", owner);
Assert.Equal("Baz", name);
}
[Fact]
public void Parses_Owner_And_Name_With_Parentheses()
{
var target = new PropertyParser();
var reader = new CharacterReader("(Foo.Bar)");
var (ns, owner, name) = target.Parse(reader);
Assert.Null(ns);
Assert.Equal("Foo", owner);
Assert.Equal("Bar", name);
}
[Fact]
public void Parses_Namespace_Owner_And_Name_With_Parentheses()
{
var target = new PropertyParser();
var reader = new CharacterReader("(foo:Bar.Baz)");
var (ns, owner, name) = target.Parse(reader);
Assert.Equal("foo", ns);
Assert.Equal("Bar", owner);
Assert.Equal("Baz", name);
}
[Fact]
public void Fails_With_Empty_String()
{
var target = new PropertyParser();
var reader = new CharacterReader("");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(0, ex.Column);
Assert.Equal("Expected property name.", ex.Message);
}
[Fact]
public void Fails_With_Only_Whitespace()
{
var target = new PropertyParser();
var reader = new CharacterReader(" ");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(0, ex.Column);
Assert.Equal("Unexpected ' '.", ex.Message);
}
[Fact]
public void Fails_With_Leading_Whitespace()
{
var target = new PropertyParser();
var reader = new CharacterReader(" Foo");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(0, ex.Column);
Assert.Equal("Unexpected ' '.", ex.Message);
}
[Fact]
public void Fails_With_Trailing_Whitespace()
{
var target = new PropertyParser();
var reader = new CharacterReader("Foo ");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(3, ex.Column);
Assert.Equal("Unexpected ' '.", ex.Message);
}
[Fact]
public void Fails_With_Invalid_Property_Name()
{
var target = new PropertyParser();
var reader = new CharacterReader("123");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(0, ex.Column);
Assert.Equal("Unexpected '1'.", ex.Message);
}
[Fact]
public void Fails_With_Trailing_Junk()
{
var target = new PropertyParser();
var reader = new CharacterReader("Foo%");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(3, ex.Column);
Assert.Equal("Unexpected '%'.", ex.Message);
}
[Fact]
public void Fails_With_Invalid_Property_Name_After_Owner()
{
var target = new PropertyParser();
var reader = new CharacterReader("Foo.123");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(4, ex.Column);
Assert.Equal("Unexpected '1'.", ex.Message);
}
[Fact]
public void Fails_With_Whitespace_Between_Owner_And_Name()
{
var target = new PropertyParser();
var reader = new CharacterReader("Foo. Bar");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(4, ex.Column);
Assert.Equal("Unexpected ' '.", ex.Message);
}
[Fact]
public void Fails_With_Too_Many_Segments()
{
var target = new PropertyParser();
var reader = new CharacterReader("Foo.Bar.Baz");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(8, ex.Column);
Assert.Equal("Unexpected '.'.", ex.Message);
}
[Fact]
public void Fails_With_Too_Many_Namespaces()
{
var target = new PropertyParser();
var reader = new CharacterReader("foo:bar:Baz");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(8, ex.Column);
Assert.Equal("Unexpected ':'.", ex.Message);
}
[Fact]
public void Fails_With_Parens_But_No_Owner()
{
var target = new PropertyParser();
var reader = new CharacterReader("(Foo)");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(1, ex.Column);
Assert.Equal("Expected property owner.", ex.Message);
}
[Fact]
public void Fails_With_Parens_And_Namespace_But_No_Owner()
{
var target = new PropertyParser();
var reader = new CharacterReader("(foo:Bar)");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(1, ex.Column);
Assert.Equal("Expected property owner.", ex.Message);
}
[Fact]
public void Fails_With_Missing_Close_Parens()
{
var target = new PropertyParser();
var reader = new CharacterReader("(Foo.Bar");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(8, ex.Column);
Assert.Equal("Expected ')'.", ex.Message);
}
[Fact]
public void Fails_With_Unexpected_Close_Parens()
{
var target = new PropertyParser();
var reader = new CharacterReader("Foo.Bar)");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(7, ex.Column);
Assert.Equal("Unexpected ')'.", ex.Message);
}
}
}

29
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs

@ -281,5 +281,32 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Equal(5.6, AttachedPropertyOwner.GetDouble(textBlock)); Assert.Equal(5.6, AttachedPropertyOwner.GetDouble(textBlock));
} }
} }
[Fact]
public void Binding_To_Attached_Property_In_Style_Works()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Window.Styles>
<Style Selector='TextBlock'>
<Setter Property='local:TestControl.Double' Value='{Binding}'/>
</Style>
</Window.Styles>
<TextBlock/>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var textBlock = (TextBlock)window.Content;
window.DataContext = 5.6;
window.ApplyTemplate();
Assert.Equal(5.6, AttachedPropertyOwner.GetDouble(textBlock));
}
}
} }
} }

55
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs

@ -297,7 +297,60 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Equal("title", button.Content); Assert.Equal("title", button.Content);
} }
} }
[Fact]
public void Shorthand_Binding_With_Negation_Works()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Border Name='border1'>
<Border Name='border2'>
<Button Name='button' Content='{Binding !$self.IsDefault}'/>
</Border>
</Border>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var button = window.FindControl<Button>("button");
window.ApplyTemplate();
#pragma warning disable xUnit2004 // Diagnostic mis-firing since button.Content isn't guaranteed to be a bool.
Assert.Equal(true, button.Content);
#pragma warning restore xUnit2004
}
}
[Fact]
public void Shorthand_Binding_With_Multiple_Negation_Works()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Border Name='border1'>
<Border Name='border2'>
<Button Name='button' Content='{Binding !!$self.IsDefault}'/>
</Border>
</Border>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var button = window.FindControl<Button>("button");
window.ApplyTemplate();
#pragma warning disable xUnit2004 // Diagnostic mis-firing since button.Content isn't guaranteed to be a bool.
Assert.Equal(false, button.Content);
#pragma warning restore xUnit2004
}
}
} }
public class TestWindow : Window { } public class TestWindow : Window { }
} }

54
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs

@ -7,6 +7,7 @@ using Avalonia.Markup.Xaml.Styling;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Portable.Xaml;
using Xunit; using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Xaml namespace Avalonia.Markup.Xaml.UnitTests.Xaml
@ -146,5 +147,56 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.NotNull(target.FocusAdorner); Assert.NotNull(target.FocusAdorner);
} }
} }
[Fact]
public void Setter_Can_Set_Attached_Property()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Window.Styles>
<Style Selector='TextBlock'>
<Setter Property='DockPanel.Dock' Value='Right'/>
</Style>
</Window.Styles>
<TextBlock/>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var textBlock = (TextBlock)window.Content;
window.ApplyTemplate();
Assert.Equal(Dock.Right, DockPanel.GetDock(textBlock));
}
}
[Fact(Skip = "The animation system currently needs to be able to set any property on any object")]
public void Disallows_Setting_Non_Registered_Property()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Window.Styles>
<Style Selector='TextBlock'>
<Setter Property='Button.IsDefault' Value='True'/>
</Style>
</Window.Styles>
<TextBlock/>
</Window>";
var loader = new AvaloniaXamlLoader();
var ex = Assert.Throws<XamlObjectWriterException>(() => loader.Load(xaml));
Assert.Equal(
"Property 'Button.IsDefault' is not registered on 'Avalonia.Controls.TextBlock'.",
ex.InnerException.Message);
}
}
} }
} }

Loading…
Cancel
Save