diff --git a/readme.md b/readme.md index 4fe76a2faf..345ad7fe9b 100644 --- a/readme.md +++ b/readme.md @@ -20,6 +20,8 @@ Avalonia is a WPF-inspired cross-platform XAML-based UI framework providing a fl Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started. After installing it, open "New Project" dialog in Visual Studio, choose "Avalonia" in "Visual C#" section, select "Avalonia .NET Core Application" and press OK (screenshot). Now you can write code and markup that will work on multiple platforms! +For those without Visual Studio, starter guide for .NET Core CLI can be found [here](http://avaloniaui.net/docs/quickstart/create-new-project#net-core). + Avalonia is delivered via NuGet package manager. You can find the packages here: ([stable(ish)](https://www.nuget.org/packages/Avalonia/), [nightly](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed)) Use these commands in Package Manager console to install Avalonia manually: diff --git a/samples/ControlCatalog/Pages/TextBoxPage.xaml b/samples/ControlCatalog/Pages/TextBoxPage.xaml index fb0d8fb7b1..a717518655 100644 --- a/samples/ControlCatalog/Pages/TextBoxPage.xaml +++ b/samples/ControlCatalog/Pages/TextBoxPage.xaml @@ -9,6 +9,7 @@ Gap="16"> + /// The value fill mode for this animation. /// - public FillMode FillMode { get; set; } + public FillMode FillMode { get; set; } /// /// Easing function to be used. @@ -80,10 +80,10 @@ namespace Avalonia.Animation this.CollectionChanged += delegate { _isChildrenChanged = true; }; } - private void InterpretKeyframes() + private IList InterpretKeyframes(Animatable control) { - var handlerList = new List<(Type, AvaloniaProperty)>(); - var kfList = new List(); + var handlerList = new List<(Type type, AvaloniaProperty property)>(); + var animatorKeyFrames = new List(); foreach (var keyframe in this) { @@ -99,41 +99,38 @@ namespace Avalonia.Animation if (!handlerList.Contains((handler, setter.Property))) handlerList.Add((handler, setter.Property)); - var newKF = new AnimatorKeyFrame() + var cue = keyframe.Cue; + + if (keyframe.TimingMode == KeyFrameTimingMode.TimeSpan) { - Handler = handler, - Property = setter.Property, - Cue = keyframe.Cue, - KeyTime = keyframe.KeyTime, - timeSpanSet = keyframe.timeSpanSet, - cueSet = keyframe.cueSet, - Value = setter.Value - }; - - kfList.Add(newKF); + cue = new Cue(keyframe.KeyTime.Ticks / Duration.Ticks); + } + + var newKF = new AnimatorKeyFrame(handler, cue); + + _subscription.Add(newKF.BindSetter(setter, control)); + + animatorKeyFrames.Add(newKF); } } - var newAnimatorInstances = new List<(Type handler, AvaloniaProperty prop, IAnimator inst)>(); + var newAnimatorInstances = new List(); - foreach (var handler in handlerList) + foreach (var (handlerType, property) in handlerList) { - var newInstance = (IAnimator)Activator.CreateInstance(handler.Item1); - newInstance.Property = handler.Item2; - newAnimatorInstances.Add((handler.Item1, handler.Item2, newInstance)); + var newInstance = (IAnimator)Activator.CreateInstance(handlerType); + newInstance.Property = property; + newAnimatorInstances.Add(newInstance); } - foreach (var kf in kfList) + foreach (var keyframe in animatorKeyFrames) { - var parent = newAnimatorInstances.Where(p => p.handler == kf.Handler && - p.prop == kf.Property) - .First(); - parent.inst.Add(kf); + var animator = newAnimatorInstances.First(a => a.GetType() == keyframe.AnimatorType && + a.Property == keyframe.Property); + animator.Add(keyframe); } - foreach(var instance in newAnimatorInstances) - _animators.Add(instance.inst); - + return newAnimatorInstances; } /// @@ -150,17 +147,11 @@ namespace Avalonia.Animation /// public IDisposable Apply(Animatable control, IObservable matchObs) { - if (_isChildrenChanged) - { - InterpretKeyframes(); - _isChildrenChanged = false; - } - - foreach (IAnimator keyframes in _animators) + foreach (IAnimator animator in InterpretKeyframes(control)) { - _subscription.Add(keyframes.Apply(this, control, matchObs)); + _subscription.Add(animator.Apply(this, control, matchObs)); } return this; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Animation/AnimatorKeyFrame.cs b/src/Avalonia.Animation/AnimatorKeyFrame.cs index 02457cb9aa..bd9c7a0184 100644 --- a/src/Avalonia.Animation/AnimatorKeyFrame.cs +++ b/src/Avalonia.Animation/AnimatorKeyFrame.cs @@ -4,6 +4,8 @@ using System.Text; using System.ComponentModel; using Avalonia.Metadata; using Avalonia.Collections; +using Avalonia.Data; +using Avalonia.Reactive; namespace Avalonia.Animation { @@ -11,13 +13,63 @@ namespace Avalonia.Animation /// Defines a KeyFrame that is used for /// objects. /// - public class AnimatorKeyFrame + public class AnimatorKeyFrame : AvaloniaObject { - public Type Handler; - public Cue Cue; - public TimeSpan KeyTime; - internal bool timeSpanSet, cueSet; - public AvaloniaProperty Property; - public object Value; + public static readonly DirectProperty ValueProperty = + AvaloniaProperty.RegisterDirect(nameof(Value), k => k._value, (k, v) => k._value = v); + + public AnimatorKeyFrame() + { + + } + + public AnimatorKeyFrame(Type animatorType, Cue cue) + { + AnimatorType = animatorType; + Cue = cue; + } + + public Type AnimatorType { get; } + public Cue Cue { get; } + public AvaloniaProperty Property { get; private set; } + + private object _value; + + public object Value + { + get => _value; + set => SetAndRaise(ValueProperty, ref _value, value); + } + + public IDisposable BindSetter(IAnimationSetter setter, Animatable targetControl) + { + Property = setter.Property; + var value = setter.Value; + + if (value is IBinding binding) + { + return this.Bind(ValueProperty, binding, targetControl); + } + else + { + return this.Bind(ValueProperty, ObservableEx.SingleValue(value).ToBinding(), targetControl); + } + } + + public T GetTypedValue() + { + 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)); + } } } diff --git a/src/Avalonia.Animation/AnimatorStateMachine`1.cs b/src/Avalonia.Animation/AnimatorStateMachine`1.cs index e37b0e592a..1a51b897c0 100644 --- a/src/Avalonia.Animation/AnimatorStateMachine`1.cs +++ b/src/Avalonia.Animation/AnimatorStateMachine`1.cs @@ -51,9 +51,9 @@ namespace Avalonia.Animation Disposed } - public void Initialize(Animation animation, Animatable control, Animator keyframes) + public void Initialize(Animation animation, Animatable control, Animator animator) { - _parent = keyframes; + _parent = animator; _targetAnimation = animation; _targetControl = control; _neutralValue = (T)_targetControl.GetValue(_parent.Property); @@ -123,121 +123,133 @@ namespace Avalonia.Animation double _tempDuration = 0d, _easedTime; - checkstate: - switch (_currentState) + bool handled = false; + + while (!handled) { - case KeyFramesStates.DoDelay: + switch (_currentState) + { + case KeyFramesStates.DoDelay: - if (_fillMode == FillMode.Backward - || _fillMode == FillMode.Both) - { - if (_currentIteration == 0) + if (_fillMode == FillMode.Backward + || _fillMode == FillMode.Both) { - _targetObserver.OnNext(_firstKFValue); + if (_currentIteration == 0) + { + _targetObserver.OnNext(_firstKFValue); + } + else + { + _targetObserver.OnNext(_lastInterpValue); + } + } + + if (_delayFrameCount > _delayTotalFrameCount) + { + _currentState = KeyFramesStates.DoRun; } else { - _targetObserver.OnNext(_lastInterpValue); + handled = true; + _delayFrameCount++; } - } - - if (_delayFrameCount > _delayTotalFrameCount) - { - _currentState = KeyFramesStates.DoRun; - goto checkstate; - } - _delayFrameCount++; - break; - - case KeyFramesStates.DoRun: - - if (_isReversed) - _currentState = KeyFramesStates.RunBackwards; - else - _currentState = KeyFramesStates.RunForwards; - - goto checkstate; - - case KeyFramesStates.RunForwards: - - if (_durationFrameCount > _durationTotalFrameCount) - { - _currentState = KeyFramesStates.RunComplete; - goto checkstate; - } + break; - _tempDuration = (double)_durationFrameCount / _durationTotalFrameCount; - _currentState = KeyFramesStates.RunApplyValue; + case KeyFramesStates.DoRun: - goto checkstate; + if (_isReversed) + _currentState = KeyFramesStates.RunBackwards; + else + _currentState = KeyFramesStates.RunForwards; - case KeyFramesStates.RunBackwards: + break; - if (_durationFrameCount > _durationTotalFrameCount) - { - _currentState = KeyFramesStates.RunComplete; - goto checkstate; - } + case KeyFramesStates.RunForwards: - _tempDuration = (double)(_durationTotalFrameCount - _durationFrameCount) / _durationTotalFrameCount; - _currentState = KeyFramesStates.RunApplyValue; + if (_durationFrameCount > _durationTotalFrameCount) + { + _currentState = KeyFramesStates.RunComplete; + } + else + { + _tempDuration = (double)_durationFrameCount / _durationTotalFrameCount; + _currentState = KeyFramesStates.RunApplyValue; - goto checkstate; + } + break; - case KeyFramesStates.RunApplyValue: + case KeyFramesStates.RunBackwards: - _easedTime = _targetAnimation.Easing.Ease(_tempDuration); + if (_durationFrameCount > _durationTotalFrameCount) + { + _currentState = KeyFramesStates.RunComplete; + } + else + { + _tempDuration = (double)(_durationTotalFrameCount - _durationFrameCount) / _durationTotalFrameCount; + _currentState = KeyFramesStates.RunApplyValue; + } + break; - _durationFrameCount++; - _lastInterpValue = Interpolator(_easedTime, _neutralValue); - _targetObserver.OnNext(_lastInterpValue); - _currentState = KeyFramesStates.DoRun; + case KeyFramesStates.RunApplyValue: - break; + _easedTime = _targetAnimation.Easing.Ease(_tempDuration); - case KeyFramesStates.RunComplete: + _durationFrameCount++; + _lastInterpValue = Interpolator(_easedTime, _neutralValue); + _targetObserver.OnNext(_lastInterpValue); + _currentState = KeyFramesStates.DoRun; + handled = true; + break; - if (_checkLoopAndRepeat) - { - _delayFrameCount = 0; - _durationFrameCount = 0; + case KeyFramesStates.RunComplete: - if (_isLooping) - { - _currentState = KeyFramesStates.DoRun; - } - else if (_isRepeating) + if (_checkLoopAndRepeat) { - if (_currentIteration >= _repeatCount) + _delayFrameCount = 0; + _durationFrameCount = 0; + + if (_isLooping) { - _currentState = KeyFramesStates.Stop; + _currentState = KeyFramesStates.DoRun; } - else + else if (_isRepeating) { - _currentState = KeyFramesStates.DoRun; + if (_currentIteration >= _repeatCount) + { + _currentState = KeyFramesStates.Stop; + } + else + { + _currentState = KeyFramesStates.DoRun; + } + _currentIteration++; } - _currentIteration++; - } - if (_animationDirection == PlaybackDirection.Alternate - || _animationDirection == PlaybackDirection.AlternateReverse) - _isReversed = !_isReversed; + if (_animationDirection == PlaybackDirection.Alternate + || _animationDirection == PlaybackDirection.AlternateReverse) + _isReversed = !_isReversed; - break; - } + break; + } - _currentState = KeyFramesStates.Stop; - goto checkstate; + _currentState = KeyFramesStates.Stop; + break; - case KeyFramesStates.Stop: + case KeyFramesStates.Stop: - if (_fillMode == FillMode.Forward - || _fillMode == FillMode.Both) - { - _targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue); - } - _targetObserver.OnCompleted(); - break; + if (_fillMode == FillMode.Forward + || _fillMode == FillMode.Both) + { + _targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue); + } + _targetObserver.OnCompleted(); + handled = true; + break; + default: + handled = true; + break; + } } } @@ -253,4 +265,4 @@ namespace Avalonia.Animation _currentState = KeyFramesStates.Disposed; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index 6d4ae7d8e2..a1eef87e1e 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -19,7 +19,7 @@ namespace Avalonia.Animation /// /// List of type-converted keyframes. /// - private Dictionary _convertedKeyframes = new Dictionary(); + private readonly SortedList _convertedKeyframes = new SortedList(); private bool _isVerfifiedAndConverted; @@ -38,12 +38,11 @@ namespace Avalonia.Animation public virtual IDisposable Apply(Animation animation, Animatable control, IObservable obsMatch) { if (!_isVerfifiedAndConverted) - VerifyConvertKeyFrames(animation, typeof(T)); + VerifyConvertKeyFrames(); return obsMatch - .Where(p => p == true) // Ignore triggers when global timers are paused. - .Where(p => Timing.GetGlobalPlayState() != PlayState.Pause) + .Where(p => p && Timing.GetGlobalPlayState() != PlayState.Pause) .Subscribe(_ => { var timerObs = RunKeyFrames(animation, control); @@ -60,8 +59,8 @@ namespace Avalonia.Animation /// The time parameter, relative to the total animation time protected (double IntraKFTime, KeyFramePair KFPair) GetKFPairAndIntraKFTime(double t) { - KeyValuePair firstCue, lastCue; - int kvCount = _convertedKeyframes.Count(); + KeyValuePair firstCue, lastCue; + int kvCount = _convertedKeyframes.Count; if (kvCount > 2) { if (DoubleUtils.AboutEqual(t, 0.0) || t < 0.0) @@ -76,8 +75,8 @@ namespace Avalonia.Animation } else { - firstCue = _convertedKeyframes.Where(j => j.Key <= t).Last(); - lastCue = _convertedKeyframes.Where(j => j.Key >= t).First(); + firstCue = _convertedKeyframes.Last(j => j.Key <= t); + lastCue = _convertedKeyframes.First(j => j.Key >= t); } } else @@ -89,7 +88,9 @@ namespace Avalonia.Animation double t0 = firstCue.Key; double t1 = lastCue.Key; var intraframeTime = (t - t0) / (t1 - t0); - return (intraframeTime, new KeyFramePair(firstCue, lastCue)); + var firstFrameData = (firstCue.Value.frame.GetTypedValue(), firstCue.Value.isNeutral); + var lastFrameData = (lastCue.Value.frame.GetTypedValue(), lastCue.Value.isNeutral); + return (intraframeTime, new KeyFramePair(firstFrameData, lastFrameData)); } @@ -98,17 +99,14 @@ namespace Avalonia.Animation /// private IDisposable RunKeyFrames(Animation animation, Animatable control) { - var _kfStateMach = new AnimatorStateMachine(); - _kfStateMach.Initialize(animation, control, this); + var stateMachine = new AnimatorStateMachine(); + stateMachine.Initialize(animation, control, this); Timing.AnimationStateTimer - .TakeWhile(_ => !_kfStateMach._unsubscribe) - .Subscribe(p => - { - _kfStateMach.Step(p, DoInterpolation); - }); + .TakeWhile(_ => !stateMachine._unsubscribe) + .Subscribe(p => stateMachine.Step(p, DoInterpolation)); - return control.Bind(Property, _kfStateMach, BindingPriority.Animation); + return control.Bind(Property, stateMachine, BindingPriority.Animation); } /// @@ -119,39 +117,19 @@ namespace Avalonia.Animation /// /// Verifies and converts keyframe values according to this class's target type. /// - private void VerifyConvertKeyFrames(Animation animation, Type type) + private void VerifyConvertKeyFrames() { - var typeConv = TypeDescriptor.GetConverter(type); - - foreach (AnimatorKeyFrame k in this) + foreach (AnimatorKeyFrame keyframe in this) { - if (k.Value == null) - { - throw new ArgumentNullException($"KeyFrame value can't be null."); - } - if (!typeConv.CanConvertTo(k.Value.GetType())) - { - throw new InvalidCastException($"KeyFrame value doesnt match property type."); - } - - T convertedValue = (T)typeConv.ConvertTo(k.Value, type); - - Cue _normalizedCue = k.Cue; - - if (k.timeSpanSet) - { - _normalizedCue = new Cue(k.KeyTime.Ticks / animation.Duration.Ticks); - } - - _convertedKeyframes.Add(_normalizedCue.CueValue, (convertedValue, false)); + _convertedKeyframes.Add(keyframe.Cue.CueValue, (keyframe, false)); } - SortKeyFrameCues(_convertedKeyframes); + AddNeutralKeyFramesIfNeeded(); _isVerfifiedAndConverted = true; } - private void SortKeyFrameCues(Dictionary convertedValues) + private void AddNeutralKeyFramesIfNeeded() { bool hasStartKey, hasEndKey; hasStartKey = hasEndKey = false; @@ -170,23 +148,20 @@ namespace Avalonia.Animation } if (!hasStartKey || !hasEndKey) - AddNeutralKeyFrames(hasStartKey, hasEndKey, _convertedKeyframes); - - _convertedKeyframes = _convertedKeyframes.OrderBy(p => p.Key) - .ToDictionary((k) => k.Key, (v) => v.Value); + AddNeutralKeyFrames(hasStartKey, hasEndKey); } - private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey, Dictionary convertedKeyframes) + private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey) { if (!hasStartKey) { - convertedKeyframes.Add(0.0d, (default(T), true)); + _convertedKeyframes.Add(0.0d, (new AnimatorKeyFrame { Value = default(T) }, true)); } if (!hasEndKey) { - convertedKeyframes.Add(1.0d, (default(T), true)); + _convertedKeyframes.Add(1.0d, (new AnimatorKeyFrame { Value = default(T) }, true)); } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Animation/Cue.cs b/src/Avalonia.Animation/Cue.cs index fe36b13495..5a95c108e3 100644 --- a/src/Avalonia.Animation/Cue.cs +++ b/src/Avalonia.Animation/Cue.cs @@ -10,7 +10,7 @@ namespace Avalonia.Animation /// A Cue object for . /// [TypeConverter(typeof(CueTypeConverter))] - public struct Cue : IEquatable, IEquatable + public readonly struct Cue : IEquatable, IEquatable { /// /// The normalized percent value, ranging from 0.0 to 1.0 diff --git a/src/Avalonia.Animation/DoubleAnimator.cs b/src/Avalonia.Animation/DoubleAnimator.cs index 5b994377f1..154f37360c 100644 --- a/src/Avalonia.Animation/DoubleAnimator.cs +++ b/src/Avalonia.Animation/DoubleAnimator.cs @@ -24,15 +24,15 @@ namespace Avalonia.Animation var firstKF = pair.KFPair.FirstKeyFrame; var secondKF = pair.KFPair.SecondKeyFrame; - if (firstKF.Value.isNeutral) + if (firstKF.isNeutral) y0 = neutralValue; else - y0 = firstKF.Value.TargetValue; + y0 = firstKF.TargetValue; - if (secondKF.Value.isNeutral) + if (secondKF.isNeutral) y1 = neutralValue; else - y1 = secondKF.Value.TargetValue; + y1 = secondKF.TargetValue; // Do linear parametric interpolation return y0 + (pair.IntraKFTime) * (y1 - y0); diff --git a/src/Avalonia.Animation/IAnimationSetter.cs b/src/Avalonia.Animation/IAnimationSetter.cs index f2a94c9ed6..2d22377286 100644 --- a/src/Avalonia.Animation/IAnimationSetter.cs +++ b/src/Avalonia.Animation/IAnimationSetter.cs @@ -5,4 +5,4 @@ namespace Avalonia.Animation AvaloniaProperty Property { get; set; } object Value { get; set; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Animation/KeyFrame.cs b/src/Avalonia.Animation/KeyFrame.cs index 46be119c36..ea04aa0aab 100644 --- a/src/Avalonia.Animation/KeyFrame.cs +++ b/src/Avalonia.Animation/KeyFrame.cs @@ -7,6 +7,11 @@ using Avalonia.Collections; namespace Avalonia.Animation { + internal enum KeyFrameTimingMode + { + TimeSpan = 1, + Cue + } /// /// Stores data regarding a specific key @@ -14,7 +19,6 @@ namespace Avalonia.Animation /// public class KeyFrame : AvaloniaList { - internal bool timeSpanSet, cueSet; private TimeSpan _ktimeSpan; private Cue _kCue; @@ -30,6 +34,8 @@ namespace Avalonia.Animation { } + internal KeyFrameTimingMode TimingMode { get; private set; } + /// /// Gets or sets the key time of this . /// @@ -42,11 +48,11 @@ namespace Avalonia.Animation } set { - if (cueSet) + if (TimingMode == KeyFrameTimingMode.Cue) { throw new InvalidOperationException($"You can only set either {nameof(KeyTime)} or {nameof(Cue)}."); } - timeSpanSet = true; + TimingMode = KeyFrameTimingMode.TimeSpan; _ktimeSpan = value; } } @@ -63,11 +69,11 @@ namespace Avalonia.Animation } set { - if (timeSpanSet) + if (TimingMode == KeyFrameTimingMode.TimeSpan) { throw new InvalidOperationException($"You can only set either {nameof(KeyTime)} or {nameof(Cue)}."); } - cueSet = true; + TimingMode = KeyFrameTimingMode.Cue; _kCue = value; } } diff --git a/src/Avalonia.Animation/KeyFramePair`1.cs b/src/Avalonia.Animation/KeyFramePair`1.cs index c192479a1d..408b13e0d8 100644 --- a/src/Avalonia.Animation/KeyFramePair`1.cs +++ b/src/Avalonia.Animation/KeyFramePair`1.cs @@ -22,7 +22,7 @@ namespace Avalonia.Animation /// /// /// - public KeyFramePair(KeyValuePair FirstKeyFrame, KeyValuePair LastKeyFrame) : this() + public KeyFramePair((T TargetValue, bool isNeutral) FirstKeyFrame, (T TargetValue, bool isNeutral) LastKeyFrame) : this() { this.FirstKeyFrame = FirstKeyFrame; this.SecondKeyFrame = LastKeyFrame; @@ -31,11 +31,11 @@ namespace Avalonia.Animation /// /// First object. /// - public KeyValuePair FirstKeyFrame { get; private set; } + public (T TargetValue, bool isNeutral) FirstKeyFrame { get; } /// /// Second object. /// - public KeyValuePair SecondKeyFrame { get; private set; } + public (T TargetValue, bool isNeutral) SecondKeyFrame { get; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 761c0618da..35e189e6a4 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -22,7 +22,7 @@ namespace Avalonia /// /// This class is analogous to DependencyObject in WPF. /// - public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged, IPriorityValueOwner + public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged { /// /// The parent object that inherited values are inherited from. @@ -45,21 +45,8 @@ namespace Avalonia /// private EventHandler _propertyChanged; - private DeferredSetter _directDeferredSetter; private ValueStore _values; - - /// - /// Delayed setter helper for direct properties. Used to fix #855. - /// - private DeferredSetter DirectPropertyDeferredSetter - { - get - { - return _directDeferredSetter ?? - (_directDeferredSetter = new DeferredSetter()); - } - } - + private ValueStore Values => _values ?? (_values = new ValueStore(this)); /// /// Initializes a new instance of the class. @@ -225,7 +212,7 @@ namespace Avalonia } else if (_values != null) { - var result = _values.GetValue(property); + var result = Values.GetValue(property); if (result == AvaloniaProperty.UnsetValue) { @@ -376,12 +363,7 @@ namespace Avalonia description, priority); - if (_values == null) - { - _values = new ValueStore(this); - } - - return _values.AddBinding(property, source, priority); + return Values.AddBinding(property, source, priority); } } @@ -414,9 +396,8 @@ namespace Avalonia VerifyAccess(); _values?.Revalidate(property); } - - /// - void IPriorityValueOwner.Changed(AvaloniaProperty property, int priority, object oldValue, object newValue) + + internal void PriorityValueChanged(AvaloniaProperty property, int priority, object oldValue, object newValue) { oldValue = (oldValue == AvaloniaProperty.UnsetValue) ? GetDefaultValue(property) : @@ -439,9 +420,8 @@ namespace Avalonia (BindingPriority)priority); } } - - /// - void IPriorityValueOwner.BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification) + + internal void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification) { UpdateDataValidation(property, notification); } @@ -456,7 +436,7 @@ namespace Avalonia /// Gets all priority values set on the object. /// /// A collection of property/value tuples. - internal IDictionary GetSetValues() => _values?.GetSetValues(); + internal IDictionary GetSetValues() => Values?.GetSetValues(); /// /// Forces revalidation of properties when a property value changes. @@ -566,12 +546,12 @@ namespace Avalonia T value) { Contract.Requires(setterCallback != null); - return DirectPropertyDeferredSetter.SetAndNotify( + return Values.Setter.SetAndNotify( property, ref field, - (object val, ref T backing, Action notify) => + (object update, ref T backing, Action notify) => { - setterCallback((T)val, ref backing, notify); + setterCallback((T)update, ref backing, notify); return true; }, value); @@ -737,13 +717,8 @@ namespace Avalonia originalValue?.GetType().FullName ?? "(null)")); } - if (_values == null) - { - _values = new ValueStore(this); - } - LogPropertySet(property, value, priority); - _values.AddValue(property, value, (int)priority); + Values.AddValue(property, value, (int)priority); } /// diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index c0a4ace6ed..e29e7339ae 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -106,7 +106,7 @@ namespace Avalonia } /// - /// Finds a registered non-attached property on a type by name. + /// Finds a registered property on a type by name. /// /// The type. /// The property name. @@ -130,7 +130,7 @@ namespace Avalonia } /// - /// Finds a registered non-attached property on a type by name. + /// Finds a registered property on an object by name. /// /// The object. /// The property name. @@ -148,52 +148,6 @@ namespace Avalonia return FindRegistered(o.GetType(), name); } - /// - /// Finds a registered attached property on a type by name. - /// - /// The type. - /// The owner type. - /// The property name. - /// - /// The registered property or null if no matching property found. - /// - /// - /// The property name contains a '.'. - /// - public AvaloniaProperty FindRegisteredAttached(Type type, Type ownerType, string name) - { - Contract.Requires(type != null); - Contract.Requires(ownerType != null); - Contract.Requires(name != null); - - if (name.Contains('.')) - { - throw new InvalidOperationException("Attached properties not supported."); - } - - return GetRegisteredAttached(type).FirstOrDefault(x => x.Name == name); - } - - /// - /// Finds a registered non-attached property on a type by name. - /// - /// The object. - /// The owner type. - /// The property name. - /// - /// The registered property or null if no matching property found. - /// - /// - /// The property name contains a '.'. - /// - public AvaloniaProperty FindRegisteredAttached(AvaloniaObject o, Type ownerType, string name) - { - Contract.Requires(o != null); - Contract.Requires(name != null); - - return FindRegisteredAttached(o.GetType(), ownerType, name); - } - /// /// Checks whether a is registered on a type. /// @@ -287,4 +241,4 @@ namespace Avalonia _attachedCache.Clear(); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs new file mode 100644 index 0000000000..f7e5de2fe2 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Text; +using Avalonia.Reactive; + +namespace Avalonia.Data.Core +{ + public class AvaloniaPropertyAccessorNode : SettableNode + { + private IDisposable _subscription; + private readonly bool _enableValidation; + private readonly AvaloniaProperty _property; + + public AvaloniaPropertyAccessorNode(AvaloniaProperty property, bool enableValidation) + { + _property = property; + _enableValidation = enableValidation; + } + + public override string Description => PropertyName; + public string PropertyName { get; } + public override Type PropertyType => _property.PropertyType; + + protected override bool SetTargetValueCore(object value, BindingPriority priority) + { + try + { + if (Target.IsAlive && Target.Target is IAvaloniaObject obj) + { + obj.SetValue(_property, value, priority); + return true; + } + return false; + } + catch + { + return false; + } + } + + protected override void StartListeningCore(WeakReference reference) + { + if (reference.Target is IAvaloniaObject obj) + { + _subscription = new AvaloniaPropertyObservable(obj, _property).Subscribe(ValueChanged); + } + else + { + _subscription = null; + } + } + + protected override void StopListeningCore() + { + _subscription?.Dispose(); + _subscription = null; + } + } +} diff --git a/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs b/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs index c4166b44e5..0e2c3c035c 100644 --- a/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs @@ -6,7 +6,7 @@ using System.Reactive.Linq; namespace Avalonia.Data.Core { - internal class EmptyExpressionNode : ExpressionNode + public class EmptyExpressionNode : ExpressionNode { public override string Description => "."; } diff --git a/src/Avalonia.Base/Data/Core/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNode.cs index 600cd68d60..9ee4787e47 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNode.cs @@ -5,7 +5,7 @@ using System; namespace Avalonia.Data.Core { - internal abstract class ExpressionNode + public abstract class ExpressionNode { private static readonly object CacheInvalid = new object(); protected static readonly WeakReference UnsetReference = diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodeBuilder.cs b/src/Avalonia.Base/Data/Core/ExpressionNodeBuilder.cs deleted file mode 100644 index 8e9e9fc3c1..0000000000 --- a/src/Avalonia.Base/Data/Core/ExpressionNodeBuilder.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using Avalonia.Data.Core.Parsers; - -namespace Avalonia.Data.Core -{ - internal static class ExpressionNodeBuilder - { - public static ExpressionNode Build(string expression, bool enableValidation = false) - { - if (string.IsNullOrWhiteSpace(expression)) - { - throw new ArgumentException("'expression' may not be empty."); - } - - var reader = new Reader(expression); - var parser = new ExpressionParser(enableValidation); - var node = parser.Parse(reader); - - if (!reader.End) - { - throw new ExpressionParseException(reader.Position, "Expected end of expression."); - } - - return node; - } - } -} diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 3a061206bf..773049d3a5 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -3,9 +3,11 @@ using System; using System.Collections.Generic; +using System.Linq.Expressions; using System.Reactive; using System.Reactive.Linq; using Avalonia.Data; +using Avalonia.Data.Core.Parsers; using Avalonia.Data.Core.Plugins; using Avalonia.Reactive; @@ -61,27 +63,22 @@ namespace Avalonia.Data.Core /// Initializes a new instance of the class. /// /// The root object. - /// The expression. - /// Whether data validation should be enabled. + /// The expression. /// - /// A description of the expression. If null, will be used. + /// A description of the expression. /// public ExpressionObserver( object root, - string expression, - bool enableDataValidation = false, + ExpressionNode node, string description = null) { - Contract.Requires(expression != null); - if (root == AvaloniaProperty.UnsetValue) { root = null; } - Expression = expression; - Description = description ?? expression; - _node = Parse(expression, enableDataValidation); + _node = node; + Description = description; _root = new WeakReference(root); } @@ -89,23 +86,19 @@ namespace Avalonia.Data.Core /// Initializes a new instance of the class. /// /// An observable which provides the root object. - /// The expression. - /// Whether data validation should be enabled. + /// The expression. /// - /// A description of the expression. If null, will be used. + /// A description of the expression. /// public ExpressionObserver( IObservable rootObservable, - string expression, - bool enableDataValidation = false, - string description = null) + ExpressionNode node, + string description) { Contract.Requires(rootObservable != null); - Contract.Requires(expression != null); - - Expression = expression; - Description = description ?? expression; - _node = Parse(expression, enableDataValidation); + + _node = node; + Description = description; _root = rootObservable; } @@ -113,30 +106,92 @@ namespace Avalonia.Data.Core /// Initializes a new instance of the class. /// /// A function which gets the root object. - /// The expression. + /// The expression. /// An observable which triggers a re-read of the getter. - /// Whether data validation should be enabled. /// - /// A description of the expression. If null, will be used. + /// A description of the expression. /// public ExpressionObserver( Func rootGetter, - string expression, + ExpressionNode node, IObservable update, - bool enableDataValidation = false, - string description = null) + string description) { Contract.Requires(rootGetter != null); - Contract.Requires(expression != null); Contract.Requires(update != null); - - Expression = expression; - Description = description ?? expression; - _node = Parse(expression, enableDataValidation); + Description = description; + _node = node; _node.Target = new WeakReference(rootGetter()); _root = update.Select(x => rootGetter()); } + + /// + /// Creates a new instance of the class. + /// + /// The root object. + /// The expression. + /// Whether or not to track data validation + /// + /// A description of the expression. If null, 's string representation will be used. + /// + public static ExpressionObserver Create( + T root, + Expression> expression, + bool enableDataValidation = false, + string description = null) + { + return new ExpressionObserver(root, Parse(expression, enableDataValidation), description ?? expression.ToString()); + } + + /// + /// Creates a new instance of the class. + /// + /// An observable which provides the root object. + /// The expression. + /// Whether or not to track data validation + /// + /// A description of the expression. If null, 's string representation will be used. + /// + public static ExpressionObserver Create( + IObservable rootObservable, + Expression> expression, + bool enableDataValidation = false, + string description = null) + { + Contract.Requires(rootObservable != null); + return new ExpressionObserver( + rootObservable.Select(o => (object)o), + Parse(expression, enableDataValidation), + description ?? expression.ToString()); + } + + /// + /// Creates a new instance of the class. + /// + /// A function which gets the root object. + /// The expression. + /// An observable which triggers a re-read of the getter. + /// Whether or not to track data validation + /// + /// A description of the expression. If null, 's string representation will be used. + /// + public static ExpressionObserver Create( + Func rootGetter, + Expression> expression, + IObservable update, + bool enableDataValidation = false, + string description = null) + { + Contract.Requires(rootGetter != null); + + return new ExpressionObserver( + () => rootGetter(), + Parse(expression, enableDataValidation), + update, + description ?? expression.ToString()); + } + /// /// Attempts to set the value of a property expression. /// @@ -221,16 +276,9 @@ namespace Avalonia.Data.Core } } - private static ExpressionNode Parse(string expression, bool enableDataValidation) + private static ExpressionNode Parse(LambdaExpression expression, bool enableDataValidation) { - if (!string.IsNullOrWhiteSpace(expression)) - { - return ExpressionNodeBuilder.Build(expression, enableDataValidation); - } - else - { - return new EmptyExpressionNode(); - } + return ExpressionTreeParser.Parse(expression, enableDataValidation); } private void StartRoot() diff --git a/src/Avalonia.Base/Data/Core/ExpressionParseException.cs b/src/Avalonia.Base/Data/Core/ExpressionParseException.cs index 3d7bce4080..1845b1b52a 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionParseException.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionParseException.cs @@ -17,8 +17,8 @@ namespace Avalonia.Data.Core /// /// The column position of the error. /// The exception message. - public ExpressionParseException(int column, string message) - : base(message) + public ExpressionParseException(int column, string message, Exception innerException = null) + : base(message, innerException) { Column = column; } diff --git a/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs b/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs new file mode 100644 index 0000000000..04412b61ef --- /dev/null +++ b/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using Avalonia.Data; + +namespace Avalonia.Data.Core +{ + class IndexerExpressionNode : IndexerNodeBase + { + private readonly ParameterExpression _parameter; + private readonly IndexExpression _expression; + private readonly Delegate _setDelegate; + private readonly Delegate _getDelegate; + private readonly Delegate _firstArgumentDelegate; + + public IndexerExpressionNode(IndexExpression expression) + { + _parameter = Expression.Parameter(expression.Object.Type); + _expression = expression.Update(_parameter, expression.Arguments); + + _getDelegate = Expression.Lambda(_expression, _parameter).Compile(); + + var valueParameter = Expression.Parameter(expression.Type); + + _setDelegate = Expression.Lambda(Expression.Assign(_expression, valueParameter), _parameter, valueParameter).Compile(); + + _firstArgumentDelegate = Expression.Lambda(_expression.Arguments[0], _parameter).Compile(); + } + + public override Type PropertyType => _expression.Type; + + public override string Description => _expression.ToString(); + + protected override bool SetTargetValueCore(object value, BindingPriority priority) + { + try + { + _setDelegate.DynamicInvoke(Target.Target, value); + return true; + } + catch (Exception) + { + return false; + } + } + + protected override object GetValue(object target) + { + try + { + return _getDelegate.DynamicInvoke(target); + } + catch (TargetInvocationException e) when (e.InnerException is ArgumentOutOfRangeException + || e.InnerException is IndexOutOfRangeException + || e.InnerException is KeyNotFoundException) + { + return AvaloniaProperty.UnsetValue; + } + } + + protected override bool ShouldUpdate(object sender, PropertyChangedEventArgs e) + { + return _expression.Indexer == null || _expression.Indexer.Name == e.PropertyName; + } + + protected override int? TryGetFirstArgumentAsInt() => _firstArgumentDelegate.DynamicInvoke(Target.Target) as int?; + } +} diff --git a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs new file mode 100644 index 0000000000..5c3295a9d8 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Reactive.Linq; +using System.Reflection; +using System.Text; +using Avalonia.Data; +using Avalonia.Utilities; + +namespace Avalonia.Data.Core +{ + public abstract class IndexerNodeBase : SettableNode + { + private IDisposable _subscription; + + protected override void StartListeningCore(WeakReference reference) + { + var target = reference.Target; + var incc = target as INotifyCollectionChanged; + var inpc = target as INotifyPropertyChanged; + var inputs = new List>(); + + if (incc != null) + { + inputs.Add(WeakObservable.FromEventPattern( + incc, + nameof(incc.CollectionChanged)) + .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) + .Select(_ => GetValue(target))); + } + + if (inpc != null) + { + inputs.Add(WeakObservable.FromEventPattern( + inpc, + nameof(inpc.PropertyChanged)) + .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) + .Select(_ => GetValue(target))); + } + + _subscription = Observable.Merge(inputs).StartWith(GetValue(target)).Subscribe(ValueChanged); + } + + protected override void StopListeningCore() + { + _subscription.Dispose(); + } + + protected abstract object GetValue(object target); + + protected abstract int? TryGetFirstArgumentAsInt(); + + private bool ShouldUpdate(object sender, NotifyCollectionChangedEventArgs e) + { + if (sender is IList) + { + var index = TryGetFirstArgumentAsInt(); + + if (index == null) + { + return false; + } + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + return index >= e.NewStartingIndex; + case NotifyCollectionChangedAction.Remove: + return index >= e.OldStartingIndex; + case NotifyCollectionChangedAction.Replace: + return index >= e.NewStartingIndex && + index < e.NewStartingIndex + e.NewItems.Count; + case NotifyCollectionChangedAction.Move: + return (index >= e.NewStartingIndex && + index < e.NewStartingIndex + e.NewItems.Count) || + (index >= e.OldStartingIndex && + index < e.OldStartingIndex + e.OldItems.Count); + case NotifyCollectionChangedAction.Reset: + return true; + } + } + + return true; // Implementation defined meaning for the index, so just try to update anyway + } + + protected abstract bool ShouldUpdate(object sender, PropertyChangedEventArgs e); + } +} diff --git a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs index f277005cec..20f1bcd21e 100644 --- a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs +++ b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs @@ -7,7 +7,7 @@ using Avalonia.Data; namespace Avalonia.Data.Core { - internal class LogicalNotNode : ExpressionNode, ITransformNode + public class LogicalNotNode : ExpressionNode, ITransformNode { public override string Description => "!"; diff --git a/src/Avalonia.Base/Data/Core/Parsers/ExpressionParser.cs b/src/Avalonia.Base/Data/Core/Parsers/ExpressionParser.cs deleted file mode 100644 index 5c74c5cd13..0000000000 --- a/src/Avalonia.Base/Data/Core/Parsers/ExpressionParser.cs +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Avalonia.Data.Core.Parsers -{ - internal class ExpressionParser - { - private bool _enableValidation; - - public ExpressionParser(bool enableValidation) - { - _enableValidation = enableValidation; - } - - public ExpressionNode Parse(Reader r) - { - var nodes = new List(); - var state = State.Start; - - while (!r.End && state != State.End) - { - switch (state) - { - case State.Start: - state = ParseStart(r, nodes); - break; - - case State.AfterMember: - state = ParseAfterMember(r, nodes); - break; - - case State.BeforeMember: - state = ParseBeforeMember(r, nodes); - break; - - case State.AttachedProperty: - state = ParseAttachedProperty(r, nodes); - break; - - case State.Indexer: - state = ParseIndexer(r, nodes); - break; - } - } - - if (state == State.BeforeMember) - { - throw new ExpressionParseException(r.Position, "Unexpected end of expression."); - } - - for (int n = 0; n < nodes.Count - 1; ++n) - { - nodes[n].Next = nodes[n + 1]; - } - - return nodes.FirstOrDefault(); - } - - private State ParseStart(Reader r, IList nodes) - { - if (ParseNot(r)) - { - nodes.Add(new LogicalNotNode()); - return State.Start; - } - else if (ParseOpenBrace(r)) - { - return State.AttachedProperty; - } - else if (PeekOpenBracket(r)) - { - return State.Indexer; - } - else - { - var identifier = IdentifierParser.Parse(r); - - if (identifier != null) - { - nodes.Add(new PropertyAccessorNode(identifier, _enableValidation)); - return State.AfterMember; - } - } - - return State.End; - } - - private static State ParseAfterMember(Reader r, IList nodes) - { - if (ParseMemberAccessor(r)) - { - return State.BeforeMember; - } - else if (ParseStreamOperator(r)) - { - nodes.Add(new StreamNode()); - return State.AfterMember; - } - else if (PeekOpenBracket(r)) - { - return State.Indexer; - } - - return State.End; - } - - private State ParseBeforeMember(Reader r, IList nodes) - { - if (ParseOpenBrace(r)) - { - return State.AttachedProperty; - } - else - { - var identifier = IdentifierParser.Parse(r); - - if (identifier != null) - { - nodes.Add(new PropertyAccessorNode(identifier, _enableValidation)); - return State.AfterMember; - } - - return State.End; - } - } - - private State ParseAttachedProperty(Reader r, List nodes) - { - var owner = IdentifierParser.Parse(r); - - if (r.End || !r.TakeIf('.')) - { - throw new ExpressionParseException(r.Position, "Invalid attached property name."); - } - - var name = IdentifierParser.Parse(r); - - if (r.End || !r.TakeIf(')')) - { - throw new ExpressionParseException(r.Position, "Expected ')'."); - } - - nodes.Add(new PropertyAccessorNode(owner + '.' + name, _enableValidation)); - return State.AfterMember; - } - - private State ParseIndexer(Reader r, List nodes) - { - var args = ArgumentListParser.Parse(r, '[', ']'); - - if (args.Count == 0) - { - throw new ExpressionParseException(r.Position, "Indexer may not be empty."); - } - - nodes.Add(new IndexerNode(args)); - return State.AfterMember; - } - - private static bool ParseNot(Reader r) - { - return !r.End && r.TakeIf('!'); - } - - private static bool ParseMemberAccessor(Reader r) - { - return !r.End && r.TakeIf('.'); - } - - private static bool ParseOpenBrace(Reader r) - { - return !r.End && r.TakeIf('('); - } - - private static bool PeekOpenBracket(Reader r) - { - return !r.End && r.Peek == '['; - } - - private static bool ParseStreamOperator(Reader r) - { - return !r.End && r.TakeIf('^'); - } - - private enum State - { - Start, - AfterMember, - BeforeMember, - AttachedProperty, - Indexer, - End, - } - } -} diff --git a/src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs b/src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs new file mode 100644 index 0000000000..db5d117687 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; + +namespace Avalonia.Data.Core.Parsers +{ + static class ExpressionTreeParser + { + public static ExpressionNode Parse(Expression expr, bool enableDataValidation) + { + var visitor = new ExpressionVisitorNodeBuilder(enableDataValidation); + + visitor.Visit(expr); + + var nodes = visitor.Nodes; + + for (int n = 0; n < nodes.Count - 1; ++n) + { + nodes[n].Next = nodes[n + 1]; + } + + return nodes.FirstOrDefault() ?? new EmptyExpressionNode(); + } + } +} diff --git a/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs new file mode 100644 index 0000000000..1b4d1c200d --- /dev/null +++ b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs @@ -0,0 +1,219 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; + +namespace Avalonia.Data.Core.Parsers +{ + class ExpressionVisitorNodeBuilder : ExpressionVisitor + { + private const string MultiDimensionalArrayGetterMethodName = "Get"; + private static PropertyInfo AvaloniaObjectIndexer; + private static MethodInfo CreateDelegateMethod; + + private readonly bool _enableDataValidation; + + static ExpressionVisitorNodeBuilder() + { + AvaloniaObjectIndexer = typeof(AvaloniaObject).GetProperty("Item", new[] { typeof(AvaloniaProperty) }); + CreateDelegateMethod = typeof(MethodInfo).GetMethod("CreateDelegate", new[] { typeof(Type), typeof(object) }); + } + + public List Nodes { get; } + + public ExpressionVisitorNodeBuilder(bool enableDataValidation) + { + _enableDataValidation = enableDataValidation; + Nodes = new List(); + } + + protected override Expression VisitUnary(UnaryExpression node) + { + if (node.NodeType == ExpressionType.Not && node.Type == typeof(bool)) + { + Nodes.Add(new LogicalNotNode()); + } + else if (node.NodeType == ExpressionType.Convert) + { + if (node.Operand.Type.IsAssignableFrom(node.Type)) + { + // Ignore inheritance casts + } + else + { + throw new ExpressionParseException(0, $"Cannot parse non-inheritance casts in a binding expression."); + } + } + else if (node.NodeType == ExpressionType.TypeAs) + { + // Ignore as operator. + } + else + { + throw new ExpressionParseException(0, $"Unable to parse unary operator {node.NodeType} in a binding expression"); + } + + return base.VisitUnary(node); + } + + protected override Expression VisitMember(MemberExpression node) + { + var visited = base.VisitMember(node); + Nodes.Add(new PropertyAccessorNode(node.Member.Name, _enableDataValidation)); + return visited; + } + + protected override Expression VisitIndex(IndexExpression node) + { + Visit(node.Object); + + if (node.Indexer == AvaloniaObjectIndexer) + { + var property = GetArgumentExpressionValue(node.Arguments[0]); + Nodes.Add(new AvaloniaPropertyAccessorNode(property, _enableDataValidation)); + } + else + { + Nodes.Add(new IndexerExpressionNode(node)); + } + + return node; + } + + private T GetArgumentExpressionValue(Expression expr) + { + try + { + return Expression.Lambda>(expr).Compile(preferInterpretation: true)(); + } + catch (InvalidOperationException ex) + { + throw new ExpressionParseException(0, "Unable to parse indexer value.", ex); + } + } + + protected override Expression VisitBinary(BinaryExpression node) + { + if (node.NodeType == ExpressionType.ArrayIndex) + { + return Visit(Expression.MakeIndex(node.Left, null, new[] { node.Right })); + } + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitBlock(BlockExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override CatchBlock VisitCatchBlock(CatchBlock node) + { + throw new ExpressionParseException(0, $"Catch blocks are not allowed in binding expressions."); + } + + protected override Expression VisitConditional(ConditionalExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitDynamic(DynamicExpression node) + { + throw new ExpressionParseException(0, $"Dynamic expressions are not allowed in binding expressions."); + } + + protected override ElementInit VisitElementInit(ElementInit node) + { + throw new ExpressionParseException(0, $"Element init expressions are not valid in a binding expression."); + } + + protected override Expression VisitGoto(GotoExpression node) + { + throw new ExpressionParseException(0, $"Goto expressions not supported in binding expressions."); + } + + protected override Expression VisitInvocation(InvocationExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitLabel(LabelExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitListInit(ListInitExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitLoop(LoopExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override MemberAssignment VisitMemberAssignment(MemberAssignment node) + { + throw new ExpressionParseException(0, $"Member assignments not supported in binding expressions."); + } + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method == CreateDelegateMethod) + { + var visited = Visit(node.Arguments[1]); + Nodes.Add(new PropertyAccessorNode(GetArgumentExpressionValue(node.Object).Name, _enableDataValidation)); + return node; + } + else if (node.Method.Name == StreamBindingExtensions.StreamBindingName || node.Method.Name.StartsWith(StreamBindingExtensions.StreamBindingName + '`')) + { + if (node.Method.IsStatic) + { + Visit(node.Arguments[0]); + } + else + { + Visit(node.Object); + } + Nodes.Add(new StreamNode()); + return node; + } + + var property = TryGetPropertyFromMethod(node.Method); + + if (property != null) + { + return Visit(Expression.MakeIndex(node.Object, property, node.Arguments)); + } + else if (node.Object.Type.IsArray && node.Method.Name == MultiDimensionalArrayGetterMethodName) + { + return Visit(Expression.MakeIndex(node.Object, null, node.Arguments)); + } + + throw new ExpressionParseException(0, $"Invalid method call in binding expression: '{node.Method.DeclaringType.AssemblyQualifiedName}.{node.Method.Name}'."); + } + + private PropertyInfo TryGetPropertyFromMethod(MethodInfo method) + { + var type = method.DeclaringType; + return type.GetRuntimeProperties().FirstOrDefault(prop => prop.GetMethod == method); + } + + protected override Expression VisitSwitch(SwitchExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitTry(TryExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitTypeBinary(TypeBinaryExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + } +} diff --git a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs index a163c07f87..ee91f964ff 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs @@ -60,35 +60,7 @@ namespace Avalonia.Data.Core.Plugins private static AvaloniaProperty LookupProperty(AvaloniaObject o, string propertyName) { - if (!propertyName.Contains(".")) - { - return AvaloniaPropertyRegistry.Instance.FindRegistered(o, propertyName); - } - else - { - var split = propertyName.Split('.'); - - if (split.Length == 2) - { - // HACK: We need a way to resolve types here using something like IXamlTypeResolver. - // We don't currently have that so we have to make our best guess. - var type = split[0]; - var name = split[1]; - var registry = AvaloniaPropertyRegistry.Instance; - var registered = registry.GetRegisteredAttached(o.GetType()) - .Concat(registry.GetRegistered(o.GetType())); - - foreach (var p in registered) - { - if (p.Name == name && IsOfType(p.OwnerType, type)) - { - return p; - } - } - } - } - - return null; + return AvaloniaPropertyRegistry.Instance.FindRegistered(o, propertyName); } private static bool IsOfType(Type type, string typeName) diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index e9831eb047..a916142675 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -8,7 +8,7 @@ using Avalonia.Data.Core.Plugins; namespace Avalonia.Data.Core { - internal class PropertyAccessorNode : SettableNode + public class PropertyAccessorNode : SettableNode { private readonly bool _enableValidation; private IPropertyAccessor _accessor; diff --git a/src/Avalonia.Base/Data/Core/SettableNode.cs b/src/Avalonia.Base/Data/Core/SettableNode.cs index 092cdbe48f..e7c6ab766f 100644 --- a/src/Avalonia.Base/Data/Core/SettableNode.cs +++ b/src/Avalonia.Base/Data/Core/SettableNode.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; namespace Avalonia.Data.Core { - internal abstract class SettableNode : ExpressionNode + public abstract class SettableNode : ExpressionNode { public bool SetTargetValue(object value, BindingPriority priority) { diff --git a/src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs b/src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs new file mode 100644 index 0000000000..fa8b56765c --- /dev/null +++ b/src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia +{ + public static class StreamBindingExtensions + { + internal static string StreamBindingName = "StreamBinding"; + + public static T StreamBinding(this Task @this) + { + throw new InvalidOperationException("This should be used only in a binding expression"); + } + + public static object StreamBinding(this Task @this) + { + throw new InvalidOperationException("This should be used only in a binding expression"); + } + + public static T StreamBinding(this IObservable @this) + { + throw new InvalidOperationException("This should be used only in a binding expression"); + } + } +} diff --git a/src/Avalonia.Base/Data/Core/StreamNode.cs b/src/Avalonia.Base/Data/Core/StreamNode.cs index 415def4d30..6fc178e7f8 100644 --- a/src/Avalonia.Base/Data/Core/StreamNode.cs +++ b/src/Avalonia.Base/Data/Core/StreamNode.cs @@ -6,7 +6,7 @@ using System.Reactive.Linq; namespace Avalonia.Data.Core { - internal class StreamNode : ExpressionNode + public class StreamNode : ExpressionNode { private IDisposable _subscription; diff --git a/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs index f44f9043f0..7afbcabd2a 100644 --- a/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs @@ -23,15 +23,24 @@ namespace Avalonia.Diagnostics { var set = o.GetSetValues(); - PriorityValue value; - - if (set.TryGetValue(property, out value)) + if (set.TryGetValue(property, out var obj)) { - return new AvaloniaPropertyValue( - property, - o.GetValue(property), - (BindingPriority)value.ValuePriority, - value.GetDiagnostic()); + if (obj is PriorityValue value) + { + return new AvaloniaPropertyValue( + property, + o.GetValue(property), + (BindingPriority)value.ValuePriority, + value.GetDiagnostic()); + } + else + { + return new AvaloniaPropertyValue( + property, + obj, + BindingPriority.LocalValue, + "Local value"); + } } else { diff --git a/src/Avalonia.Base/IPriorityValueOwner.cs b/src/Avalonia.Base/IPriorityValueOwner.cs index 5f63f6ef91..8cbf212381 100644 --- a/src/Avalonia.Base/IPriorityValueOwner.cs +++ b/src/Avalonia.Base/IPriorityValueOwner.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using Avalonia.Data; +using Avalonia.Utilities; namespace Avalonia { @@ -31,5 +32,7 @@ namespace Avalonia /// Ensures that the current thread is the UI thread. /// void VerifyAccess(); + + DeferredSetter Setter { get; } } } diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs index c474f9098e..03094e2236 100644 --- a/src/Avalonia.Base/PriorityValue.cs +++ b/src/Avalonia.Base/PriorityValue.cs @@ -21,7 +21,7 @@ namespace Avalonia /// priority binding that doesn't return . Where there /// are multiple bindings registered with the same priority, the most recently added binding /// has a higher priority. Each time the value changes, the - /// method on the + /// method on the /// owner object is fired with the old and new values. /// internal class PriorityValue @@ -30,7 +30,6 @@ namespace Avalonia private readonly SingleOrDictionary _levels = new SingleOrDictionary(); private readonly Func _validate; - private static readonly DeferredSetter delayedSetter = new DeferredSetter(); private (object value, int priority) _value; /// @@ -243,12 +242,18 @@ namespace Avalonia /// The priority level that the value came from. private void UpdateValue(object value, int priority) { - delayedSetter.SetAndNotify(this, + Owner.Setter.SetAndNotify(Property, ref _value, UpdateCore, (value, priority)); } + private bool UpdateCore( + object update, + ref (object value, int priority) backing, + Action notify) + => UpdateCore(((object, int))update, ref backing, notify); + private bool UpdateCore( (object value, int priority) update, ref (object value, int priority) backing, diff --git a/src/Avalonia.Base/Data/Core/Parsers/Reader.cs b/src/Avalonia.Base/Utilities/CharacterReader.cs similarity index 89% rename from src/Avalonia.Base/Data/Core/Parsers/Reader.cs rename to src/Avalonia.Base/Utilities/CharacterReader.cs index 14187c769a..0910d5b969 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/Reader.cs +++ b/src/Avalonia.Base/Utilities/CharacterReader.cs @@ -3,14 +3,14 @@ using System; -namespace Avalonia.Data.Core.Parsers +namespace Avalonia.Utilities { - internal class Reader + public class CharacterReader { private readonly string _s; private int _i; - public Reader(string s) + public CharacterReader(string s) { _s = s; } diff --git a/src/Avalonia.Base/Utilities/DeferredSetter.cs b/src/Avalonia.Base/Utilities/DeferredSetter.cs index fdfa160134..ae6f599005 100644 --- a/src/Avalonia.Base/Utilities/DeferredSetter.cs +++ b/src/Avalonia.Base/Utilities/DeferredSetter.cs @@ -8,11 +8,10 @@ namespace Avalonia.Utilities { /// /// A utility class to enable deferring assignment until after property-changed notifications are sent. + /// Used to fix #855. /// - /// The type of the object that represents the property. /// The type of value with which to track the delayed assignment. - class DeferredSetter - where TProperty: class + class DeferredSetter { private struct NotifyDisposable : IDisposable { @@ -37,29 +36,44 @@ namespace Avalonia.Utilities { public bool Notifying { get; set; } - private Queue pendingValues; + private SingleOrQueue pendingValues; - public Queue PendingValues + public SingleOrQueue PendingValues { get { - return pendingValues ?? (pendingValues = new Queue()); + return pendingValues ?? (pendingValues = new SingleOrQueue()); } } } - private readonly ConditionalWeakTable setRecords = new ConditionalWeakTable(); + private Dictionary _setRecords; + private Dictionary SetRecords + => _setRecords ?? (_setRecords = new Dictionary()); + + private SettingStatus GetOrCreateStatus(AvaloniaProperty property) + { + if (!SetRecords.TryGetValue(property, out var status)) + { + status = new SettingStatus(); + SetRecords.Add(property, status); + } + + return status; + } /// /// Mark the property as currently notifying. /// /// The property to mark as notifying. /// Returns a disposable that when disposed, marks the property as done notifying. - private NotifyDisposable MarkNotifying(TProperty property) + private NotifyDisposable MarkNotifying(AvaloniaProperty property) { Contract.Requires(!IsNotifying(property)); - - return new NotifyDisposable(setRecords.GetOrCreateValue(property)); + + SettingStatus status = GetOrCreateStatus(property); + + return new NotifyDisposable(status); } /// @@ -67,19 +81,19 @@ namespace Avalonia.Utilities /// /// The property. /// If the property is currently notifying listeners. - private bool IsNotifying(TProperty property) - => setRecords.TryGetValue(property, out var value) && value.Notifying; + private bool IsNotifying(AvaloniaProperty property) + => SetRecords.TryGetValue(property, out var value) && value.Notifying; /// /// Add a pending assignment for the property. /// /// The property. /// The value to assign. - private void AddPendingSet(TProperty property, TSetRecord value) + private void AddPendingSet(AvaloniaProperty property, TSetRecord value) { Contract.Requires(IsNotifying(property)); - setRecords.GetOrCreateValue(property).PendingValues.Enqueue(value); + GetOrCreateStatus(property).PendingValues.Enqueue(value); } /// @@ -87,9 +101,9 @@ namespace Avalonia.Utilities /// /// The property to check. /// If the property has any pending assignments. - 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; } /// @@ -97,9 +111,9 @@ namespace Avalonia.Utilities /// /// The property to check. /// The first pending assignment for the property. - 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(TSetRecord record, ref TValue backing, Action notifyCallback); @@ -115,7 +129,7 @@ namespace Avalonia.Utilities /// /// The value to try to set. public bool SetAndNotify( - TProperty property, + AvaloniaProperty property, ref TValue backing, SetterDelegate setterCallback, TSetRecord value) @@ -144,6 +158,7 @@ namespace Avalonia.Utilities } }); } + return updated; } else if(!object.Equals(value, backing)) diff --git a/src/Avalonia.Base/Data/Core/Parsers/IdentifierParser.cs b/src/Avalonia.Base/Utilities/IdentifierParser.cs similarity index 91% rename from src/Avalonia.Base/Data/Core/Parsers/IdentifierParser.cs rename to src/Avalonia.Base/Utilities/IdentifierParser.cs index b0a9ff4df2..14b8affbdd 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/IdentifierParser.cs +++ b/src/Avalonia.Base/Utilities/IdentifierParser.cs @@ -4,11 +4,11 @@ using System.Globalization; using System.Text; -namespace Avalonia.Data.Core.Parsers +namespace Avalonia.Utilities { - internal static class IdentifierParser + public static class IdentifierParser { - public static string Parse(Reader r) + public static string Parse(CharacterReader r) { if (IsValidIdentifierStart(r.Peek)) { diff --git a/src/Avalonia.Base/Utilities/SingleOrQueue.cs b/src/Avalonia.Base/Utilities/SingleOrQueue.cs new file mode 100644 index 0000000000..4a66b72a56 --- /dev/null +++ b/src/Avalonia.Base/Utilities/SingleOrQueue.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Utilities +{ + /// + /// FIFO Queue optimized for holding zero or one items. + /// + /// The type of items held in the queue. + public class SingleOrQueue + { + private T _head; + private Queue _tail; + + private Queue Tail => _tail ?? (_tail = new Queue()); + + 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; + } + } +} diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 8283edab80..ab80e74923 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Avalonia.Data; +using Avalonia.Utilities; namespace Avalonia { @@ -91,15 +92,15 @@ namespace Avalonia public void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification) { - ((IPriorityValueOwner)_owner).BindingNotificationReceived(property, notification); + _owner.BindingNotificationReceived(property, notification); } public void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue) { - ((IPriorityValueOwner)_owner).Changed(property, priority, oldValue, newValue); + _owner.PriorityValueChanged(property, priority, oldValue, newValue); } - public IDictionary GetSetValues() => throw new NotImplementedException(); + public IDictionary GetSetValues() => _values; public object GetValue(AvaloniaProperty property) { @@ -115,7 +116,7 @@ namespace Avalonia public bool IsAnimating(AvaloniaProperty property) { - return _values.TryGetValue(property, out var value) ? (value as PriorityValue)?.IsAnimating ?? false : false; + return _values.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating; } public bool IsSet(AvaloniaProperty property) @@ -148,13 +149,11 @@ namespace Avalonia validate2 = v => validate(_owner, v); } - PriorityValue result = new PriorityValue( + return new PriorityValue( this, property, property.PropertyType, validate2); - - return result; } private object Validate(AvaloniaProperty property, object value) @@ -168,5 +167,16 @@ namespace Avalonia return value; } + + private DeferredSetter _defferedSetter; + + public DeferredSetter Setter + { + get + { + return _defferedSetter ?? + (_defferedSetter = new DeferredSetter()); + } + } } } diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 54fcefeb3f..5f194bdd71 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -194,6 +194,16 @@ namespace Avalonia.Controls /// private GridLayout.MeasureResult _rowMeasureCache; + /// + /// Gets the row layout as of the last measure. + /// + private GridLayout _rowLayoutCache; + + /// + /// Gets the column layout as of the last measure. + /// + private GridLayout _columnLayoutCache; + /// /// Measures the grid. /// @@ -253,6 +263,9 @@ namespace Avalonia.Controls // Cache the measure result and return the desired size. _columnMeasureCache = columnResult; _rowMeasureCache = rowResult; + _rowLayoutCache = rowLayout; + _columnLayoutCache = columnLayout; + return new Size(columnResult.DesiredLength, rowResult.DesiredLength); // Measure each child only once. @@ -299,13 +312,11 @@ namespace Avalonia.Controls // arrow back to any statements and re-run them without any side-effect. var (safeColumns, safeRows) = GetSafeColumnRows(); - var columnLayout = new GridLayout(ColumnDefinitions); - var rowLayout = new GridLayout(RowDefinitions); - + var columnLayout = _columnLayoutCache; + var rowLayout = _rowLayoutCache; // Calculate for arrange result. var columnResult = columnLayout.Arrange(finalSize.Width, _columnMeasureCache); var rowResult = rowLayout.Arrange(finalSize.Height, _rowMeasureCache); - // Arrange the children. foreach (var child in Children.OfType()) { @@ -315,7 +326,6 @@ namespace Avalonia.Controls var y = Enumerable.Range(0, row).Sum(r => rowResult.LengthList[r]); var width = Enumerable.Range(column, columnSpan).Sum(c => columnResult.LengthList[c]); var height = Enumerable.Range(row, rowSpan).Sum(r => rowResult.LengthList[r]); - child.Arrange(new Rect(x, y, width, height)); } diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index bda660be51..636c836da5 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -189,6 +189,12 @@ namespace Avalonia.Controls.Presenters _caretTimer.Start(); InvalidateVisual(); } + else + { + _caretTimer.Start(); + InvalidateVisual(); + _caretTimer.Stop(); + } if (IsMeasureValid) { diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index a7b8981583..eb3fbde8f2 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -360,7 +360,7 @@ namespace Avalonia.Controls.Primitives { if (!AlwaysSelected) { - SelectedIndex = -1; + selectedIndex = SelectedIndex = -1; } else { @@ -368,6 +368,11 @@ namespace Avalonia.Controls.Primitives } } + var items = Items?.Cast(); + if (selectedIndex >= items.Count()) + { + selectedIndex = SelectedIndex = items.Count() - 1; + } break; case NotifyCollectionChangedAction.Reset: diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 5e5a460368..b7db352c74 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -21,18 +21,27 @@ namespace Avalonia.Controls public static readonly StyledProperty OrientationProperty = AvaloniaProperty.Register(nameof(Orientation), Orientation.Horizontal); + private static readonly DirectProperty IndeterminateStartingOffsetProperty = + AvaloniaProperty.RegisterDirect( + nameof(IndeterminateStartingOffset), + p => p.IndeterminateStartingOffset, + (p, o) => p.IndeterminateStartingOffset = o); + + private static readonly DirectProperty IndeterminateEndingOffsetProperty = + AvaloniaProperty.RegisterDirect( + nameof(IndeterminateEndingOffset), + p => p.IndeterminateEndingOffset, + (p, o) => p.IndeterminateEndingOffset = o); + private Border _indicator; - private IndeterminateAnimation _indeterminateAnimation; static ProgressBar() { PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical"); PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal"); + PseudoClass(IsIndeterminateProperty, ":indeterminate"); ValueProperty.Changed.AddClassHandler(x => x.ValueChanged); - - IsIndeterminateProperty.Changed.AddClassHandler( - (p, e) => { if (p._indicator != null) p.UpdateIsIndeterminate((bool)e.NewValue); }); } public bool IsIndeterminate @@ -46,6 +55,19 @@ namespace Avalonia.Controls get => GetValue(OrientationProperty); set => SetValue(OrientationProperty, value); } + private double _indeterminateStartingOffset; + private double IndeterminateStartingOffset + { + get => _indeterminateStartingOffset; + set => SetAndRaise(IndeterminateStartingOffsetProperty, ref _indeterminateStartingOffset, value); + } + + private double _indeterminateEndingOffset; + private double IndeterminateEndingOffset + { + get => _indeterminateEndingOffset; + set => SetAndRaise(IndeterminateEndingOffsetProperty, ref _indeterminateEndingOffset, value); + } /// protected override Size ArrangeOverride(Size finalSize) @@ -60,7 +82,6 @@ namespace Avalonia.Controls _indicator = e.NameScope.Get("PART_Indicator"); UpdateIndicator(Bounds.Size); - UpdateIsIndeterminate(IsIndeterminate); } private void UpdateIndicator(Size bounds) @@ -70,9 +91,20 @@ namespace Avalonia.Controls if (IsIndeterminate) { if (Orientation == Orientation.Horizontal) - _indicator.Width = bounds.Width / 5.0; + { + var width = bounds.Width / 5.0; + IndeterminateStartingOffset = -width; + _indicator.Width = width; + IndeterminateEndingOffset = bounds.Width; + + } else - _indicator.Height = bounds.Height / 5.0; + { + var height = bounds.Height / 5.0; + IndeterminateStartingOffset = -bounds.Height; + _indicator.Height = height; + IndeterminateEndingOffset = height; + } } else { @@ -86,53 +118,9 @@ namespace Avalonia.Controls } } - private void UpdateIsIndeterminate(bool isIndeterminate) - { - if (isIndeterminate) - { - if (_indeterminateAnimation == null || _indeterminateAnimation.Disposed) - _indeterminateAnimation = IndeterminateAnimation.StartAnimation(this); - } - else - _indeterminateAnimation?.Dispose(); - } - private void ValueChanged(AvaloniaPropertyChangedEventArgs e) { UpdateIndicator(Bounds.Size); } - - // TODO: Implement Indeterminate Progress animation - // in xaml (most ideal) or if it's not possible - // then on this class. - private class IndeterminateAnimation : IDisposable - { - private WeakReference _progressBar; - - private bool _disposed; - - public bool Disposed => _disposed; - - private IndeterminateAnimation(ProgressBar progressBar) - { - _progressBar = new WeakReference(progressBar); - - } - - public static IndeterminateAnimation StartAnimation(ProgressBar progressBar) - { - return new IndeterminateAnimation(progressBar); - } - - private Rect GetAnimationRect(TimeSpan time) - { - return Rect.Empty; - } - - public void Dispose() - { - _disposed = true; - } - } } } diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index a6fe35d668..d404e6913b 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Linq; using Avalonia.Input; namespace Avalonia.Controls @@ -152,6 +153,7 @@ namespace Avalonia.Controls double measuredWidth = 0; double measuredHeight = 0; double gap = Gap; + bool hasVisibleChild = Children.Any(c => c.IsVisible); foreach (Control child in Children) { @@ -160,23 +162,23 @@ namespace Avalonia.Controls if (Orientation == Orientation.Vertical) { - measuredHeight += size.Height + gap; + measuredHeight += size.Height + (child.IsVisible ? gap : 0); measuredWidth = Math.Max(measuredWidth, size.Width); } else { - measuredWidth += size.Width + gap; + measuredWidth += size.Width + (child.IsVisible ? gap : 0); measuredHeight = Math.Max(measuredHeight, size.Height); } } if (Orientation == Orientation.Vertical) { - measuredHeight -= gap; + measuredHeight -= (hasVisibleChild ? gap : 0); } else { - measuredWidth -= gap; + measuredWidth -= (hasVisibleChild ? gap : 0); } return new Size(measuredWidth, measuredHeight); @@ -193,6 +195,7 @@ namespace Avalonia.Controls double arrangedWidth = finalSize.Width; double arrangedHeight = finalSize.Height; double gap = Gap; + bool hasVisibleChild = Children.Any(c => c.IsVisible); if (Orientation == Orientation.Vertical) { @@ -214,25 +217,25 @@ namespace Avalonia.Controls Rect childFinal = new Rect(0, arrangedHeight, width, childHeight); ArrangeChild(child, childFinal, finalSize, orientation); arrangedWidth = Math.Max(arrangedWidth, childWidth); - arrangedHeight += childHeight + gap; + arrangedHeight += childHeight + (child.IsVisible ? gap : 0); } else { double height = Math.Max(childHeight, arrangedHeight); Rect childFinal = new Rect(arrangedWidth, 0, childWidth, height); ArrangeChild(child, childFinal, finalSize, orientation); - arrangedWidth += childWidth + gap; + arrangedWidth += childWidth + (child.IsVisible ? gap : 0); arrangedHeight = Math.Max(arrangedHeight, childHeight); } } if (orientation == Orientation.Vertical) { - arrangedHeight = Math.Max(arrangedHeight - gap, finalSize.Height); + arrangedHeight = Math.Max(arrangedHeight - (hasVisibleChild ? gap : 0), finalSize.Height); } else { - arrangedWidth = Math.Max(arrangedWidth - gap, finalSize.Width); + arrangedWidth = Math.Max(arrangedWidth - (hasVisibleChild ? gap : 0), finalSize.Width); } return new Size(arrangedWidth, arrangedHeight); diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 2ea9319194..388f984b78 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -124,7 +124,7 @@ namespace Avalonia.Controls ScrollViewer.HorizontalScrollBarVisibilityProperty, horizontalScrollBarVisibility, BindingPriority.Style); - _undoRedoHelper = new UndoRedoHelper(this); + _undoRedoHelper = new UndoRedoHelper(this); } public bool AcceptsReturn @@ -262,7 +262,7 @@ namespace Avalonia.Controls if (IsFocused) { - _presenter.ShowCaret(); + DecideCaretVisibility(); } } @@ -282,12 +282,20 @@ namespace Avalonia.Controls } else { - _presenter?.ShowCaret(); + DecideCaretVisibility(); } e.Handled = true; } + private void DecideCaretVisibility() + { + if (!IsReadOnly) + _presenter?.ShowCaret(); + else + _presenter?.HideCaret(); + } + protected override void OnLostFocus(RoutedEventArgs e) { base.OnLostFocus(e); @@ -557,7 +565,7 @@ namespace Avalonia.Controls var index = CaretIndex = _presenter.GetCaretIndex(point); var text = Text; - if (text != null) + if (text != null && e.MouseButton == MouseButton.Left) { switch (e.ClickCount) { diff --git a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs index 555a0b2354..ce8ad36c17 100644 --- a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs @@ -31,6 +31,7 @@ namespace Avalonia.Diagnostics.ViewModels } }; + SelectedTab = 0; root.GetObservable(TopLevel.PointerOverElementProperty) .Subscribe(x => PointerOverElement = x?.GetType().Name); } diff --git a/src/Avalonia.Styling/Styling/Setter.cs b/src/Avalonia.Styling/Styling/Setter.cs index 1a78e0f4d7..c75bae4db8 100644 --- a/src/Avalonia.Styling/Styling/Setter.cs +++ b/src/Avalonia.Styling/Styling/Setter.cs @@ -126,7 +126,7 @@ namespace Avalonia.Styling if (source != null) { - var cloned = Clone(source, style, activator); + var cloned = Clone(source, source.Mode == BindingMode.Default ? Property.GetMetadata(control.GetType()).DefaultBindingMode : source.Mode, style, activator); return BindingOperations.Apply(control, Property, cloned, null); } } @@ -134,13 +134,13 @@ namespace Avalonia.Styling return Disposable.Empty; } - private InstancedBinding Clone(InstancedBinding sourceInstance, IStyle style, IObservable activator) + private InstancedBinding Clone(InstancedBinding sourceInstance, BindingMode mode, IStyle style, IObservable activator) { if (activator != null) { var description = style?.ToString(); - switch (sourceInstance.Mode) + switch (mode) { case BindingMode.OneTime: if (sourceInstance.Observable != null) @@ -158,18 +158,11 @@ namespace Avalonia.Styling var activated = new ActivatedObservable(activator, sourceInstance.Observable, description); return InstancedBinding.OneWay(activated, BindingPriority.StyleTrigger); } - case BindingMode.OneWayToSource: - { - var activated = new ActivatedSubject(activator, sourceInstance.Subject, description); - return InstancedBinding.OneWayToSource(activated, BindingPriority.StyleTrigger); - } - case BindingMode.TwoWay: + default: { var activated = new ActivatedSubject(activator, sourceInstance.Subject, description); - return InstancedBinding.TwoWay(activated, BindingPriority.StyleTrigger); + return new InstancedBinding(activated, sourceInstance.Mode, BindingPriority.StyleTrigger); } - default: - throw new NotSupportedException("Unsupported BindingMode."); } } diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index 53965db016..47b4528f88 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -122,11 +122,6 @@ - - - \ No newline at end of file + + + diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index cdc22f4102..8c843a4b49 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -33,6 +33,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs index 63b7811dbc..627a646bcf 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs @@ -4,19 +4,19 @@ using System; using System.ComponentModel; using System.Globalization; -using System.Text.RegularExpressions; +using Avalonia.Controls; +using Avalonia.Logging; +using Avalonia.Markup.Parsers; +using Avalonia.Markup.Xaml.Parsers; using Avalonia.Markup.Xaml.Templates; using Avalonia.Styling; -using Portable.Xaml; +using Avalonia.Utilities; using Portable.Xaml.ComponentModel; -using Portable.Xaml.Markup; namespace Avalonia.Markup.Xaml.Converters { public class AvaloniaPropertyTypeConverter : TypeConverter { - private static readonly Regex regex = new Regex(@"^\(?(\w*)\.(\w*)\)?|(.*)$"); - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { return sourceType == typeof(string); @@ -24,39 +24,48 @@ namespace Avalonia.Markup.Xaml.Converters public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { - var (owner, propertyName) = ParseProperty((string)value); - var ownerType = TryResolveOwnerByName(context, owner) ?? - context.GetFirstAmbientValue()?.TargetType ?? - context.GetFirstAmbientValue + + +"; + 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)); + } + } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs index 095aae7742..c6fe79bc0c 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs @@ -297,7 +297,60 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal("title", button.Content); } } + + [Fact] + public void Shorthand_Binding_With_Negation_Works() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + +