From ae48a9a13b22df8bd2cecfff923e9a5374f31cdd Mon Sep 17 00:00:00 2001 From: Foaltin Dorin Date: Fri, 12 Nov 2021 15:37:59 +0200 Subject: [PATCH 01/44] Fix DataGrid selection broken after 3 right clicks --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 10c7c16488..95ee73be4e 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -5751,6 +5751,7 @@ namespace Avalonia.Controls return true; } // Unselect everything except the row that was clicked on + _noSelectionChangeCount++; try { UpdateSelectionAndCurrency(columnIndex, slot, DataGridSelectionAction.SelectCurrent, scrollIntoView: false); From 03d6ca9374f932a7d411dc3a9b2003fc0cb277eb Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 22 Dec 2021 00:33:26 -0500 Subject: [PATCH 02/44] Better fix for #6830 Previous numericupdown bugfix (see https://github.com/AvaloniaUI/Avalonia/pull/6830/files) introduced another bug (see https://github.com/AvaloniaUI/Avalonia/discussions/7230#discussioncomment-1855837). This PR solves both problems. --- src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml b/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml index 1e58259358..36ab07e3e3 100644 --- a/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml @@ -38,7 +38,7 @@ BorderBrush="{TemplateBinding BorderBrush}" CornerRadius="{TemplateBinding CornerRadius}" Padding="0" - MinWidth="{TemplateBinding MinWidth}" + MinWidth="0" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" AllowSpin="{TemplateBinding AllowSpin}" @@ -50,7 +50,7 @@ BorderBrush="Transparent" Margin="-1" Padding="{TemplateBinding Padding}" - MinWidth="{TemplateBinding MinWidth}" + MinWidth="0" Foreground="{TemplateBinding Foreground}" FontSize="{TemplateBinding FontSize}" Watermark="{TemplateBinding Watermark}" From ffac3eb0278ea1281aea3aa281d910663c6f010d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 22 Dec 2021 14:24:46 +0100 Subject: [PATCH 03/44] Added nullable annotations to Avalonia.Animation. --- src/Avalonia.Animation/Animatable.cs | 16 +++---- src/Avalonia.Animation/Animation.cs | 46 +++++++++++++------ src/Avalonia.Animation/AnimationInstance`1.cs | 33 +++++++------ src/Avalonia.Animation/AnimatorKeyFrame.cs | 20 ++++---- .../Animators/Animator`1.cs | 9 ++-- .../Avalonia.Animation.csproj | 4 ++ src/Avalonia.Animation/Clock.cs | 2 +- src/Avalonia.Animation/Cue.cs | 8 ++-- .../DisposeAnimationInstanceSubject.cs | 8 ++-- src/Avalonia.Animation/Easing/Easing.cs | 4 +- .../Easing/EasingTypeConverter.cs | 4 +- src/Avalonia.Animation/IAnimation.cs | 2 +- src/Avalonia.Animation/IAnimationSetter.cs | 4 +- src/Avalonia.Animation/IAnimator.cs | 4 +- src/Avalonia.Animation/ITransition.cs | 2 +- src/Avalonia.Animation/IterationCount.cs | 2 +- .../IterationCountTypeConverter.cs | 4 +- src/Avalonia.Animation/KeyFrame.cs | 4 +- .../KeySplineTypeConverter.cs | 4 +- src/Avalonia.Animation/Transition.cs | 21 +++++++-- src/Avalonia.Animation/TransitionInstance.cs | 6 +-- .../Animation/Animators/BaseBrushAnimator.cs | 7 ++- .../Animators/GradientBrushAnimator.cs | 5 ++ .../Animators/SolidColorBrushAnimator.cs | 5 ++ .../Animation/Animators/TransformAnimator.cs | 7 ++- 25 files changed, 142 insertions(+), 89 deletions(-) diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 4811028f85..50fc5ac73b 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -157,7 +157,7 @@ namespace Avalonia.Animation state.Instance?.Dispose(); state.Instance = transition.Apply( this, - Clock ?? AvaloniaLocator.Current.GetService(), + Clock ?? AvaloniaLocator.Current.GetRequiredService(), oldValue, newValue); return; @@ -169,7 +169,7 @@ namespace Avalonia.Animation base.OnPropertyChangedCore(change); } - private void TransitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + private void TransitionsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (!_transitionsEnabled) { @@ -179,14 +179,14 @@ namespace Avalonia.Animation switch (e.Action) { case NotifyCollectionChangedAction.Add: - AddTransitions(e.NewItems); + AddTransitions(e.NewItems!); break; case NotifyCollectionChangedAction.Remove: - RemoveTransitions(e.OldItems); + RemoveTransitions(e.OldItems!); break; case NotifyCollectionChangedAction.Replace: - RemoveTransitions(e.OldItems); - AddTransitions(e.NewItems); + RemoveTransitions(e.OldItems!); + AddTransitions(e.NewItems!); break; case NotifyCollectionChangedAction.Reset: throw new NotSupportedException("Transitions collection cannot be reset."); @@ -204,7 +204,7 @@ namespace Avalonia.Animation for (var i = 0; i < items.Count; ++i) { - var t = (ITransition)items[i]; + var t = (ITransition)items[i]!; _transitionState.Add(t, new TransitionState { @@ -222,7 +222,7 @@ namespace Avalonia.Animation for (var i = 0; i < items.Count; ++i) { - var t = (ITransition)items[i]; + var t = (ITransition)items[i]!; if (_transitionState.TryGetValue(t, out var state)) { diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index a4515db514..03b2d17e44 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -203,7 +203,7 @@ namespace Avalonia.Animation /// /// The animation setter. /// The property animator type. - public static Type GetAnimator(IAnimationSetter setter) + public static Type? GetAnimator(IAnimationSetter setter) { if (s_animators.TryGetValue(setter, out var type)) { @@ -254,7 +254,7 @@ namespace Avalonia.Animation Animators.Insert(0, (condition, typeof(TAnimator))); } - private static Type GetAnimatorType(AvaloniaProperty property) + private static Type? GetAnimatorType(AvaloniaProperty property) { foreach (var (condition, type) in Animators) { @@ -276,6 +276,11 @@ namespace Avalonia.Animation { foreach (var setter in keyframe.Setters) { + if (setter.Property is null) + { + throw new InvalidOperationException("No Setter property assigned."); + } + var handler = Animation.GetAnimator(setter) ?? GetAnimatorType(setter.Property); if (handler == null) @@ -305,7 +310,7 @@ namespace Avalonia.Animation foreach (var (handlerType, property) in handlerList) { - var newInstance = (IAnimator)Activator.CreateInstance(handlerType); + var newInstance = (IAnimator)Activator.CreateInstance(handlerType)!; newInstance.Property = property; newAnimatorInstances.Add(newInstance); } @@ -321,32 +326,43 @@ namespace Avalonia.Animation } /// - public IDisposable Apply(Animatable control, IClock clock, IObservable match, Action onComplete) + public IDisposable Apply(Animatable control, IClock? clock, IObservable match, Action? onComplete) { var (animators, subscriptions) = InterpretKeyframes(control); if (animators.Count == 1) { - subscriptions.Add(animators[0].Apply(this, control, clock, match, onComplete)); + var subscription = animators[0].Apply(this, control, clock, match, onComplete); + + if (subscription is not null) + { + subscriptions.Add(subscription); + } } else { var completionTasks = onComplete != null ? new List() : null; foreach (IAnimator animator in animators) { - Action animatorOnComplete = null; + Action? animatorOnComplete = null; if (onComplete != null) { - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(); animatorOnComplete = () => tcs.SetResult(null); - completionTasks.Add(tcs.Task); + completionTasks!.Add(tcs.Task); + } + + var subscription = animator.Apply(this, control, clock, match, animatorOnComplete); + + if (subscription is not null) + { + subscriptions.Add(subscription); } - subscriptions.Add(animator.Apply(this, control, clock, match, animatorOnComplete)); } if (onComplete != null) { - Task.WhenAll(completionTasks).ContinueWith( - (_, state) => ((Action)state).Invoke(), + Task.WhenAll(completionTasks!).ContinueWith( + (_, state) => ((Action)state!).Invoke(), onComplete); } } @@ -354,25 +370,25 @@ namespace Avalonia.Animation } /// - public Task RunAsync(Animatable control, IClock clock = null) + public Task RunAsync(Animatable control, IClock? clock = null) { return RunAsync(control, clock, default); } /// - public Task RunAsync(Animatable control, IClock clock = null, CancellationToken cancellationToken = default) + public Task RunAsync(Animatable control, IClock? clock = null, CancellationToken cancellationToken = default) { if (cancellationToken.IsCancellationRequested) { return Task.CompletedTask; } - var run = new TaskCompletionSource(); + var run = new TaskCompletionSource(); if (this.IterationCount == IterationCount.Infinite) run.SetException(new InvalidOperationException("Looping animations must not use the Run method.")); - IDisposable subscriptions = null, cancellation = null; + IDisposable? subscriptions = null, cancellation = null; subscriptions = this.Apply(control, clock, Observable.Return(true), () => { run.TrySetResult(null); diff --git a/src/Avalonia.Animation/AnimationInstance`1.cs b/src/Avalonia.Animation/AnimationInstance`1.cs index cf79640150..52cd4b324f 100644 --- a/src/Avalonia.Animation/AnimationInstance`1.cs +++ b/src/Avalonia.Animation/AnimationInstance`1.cs @@ -31,15 +31,15 @@ namespace Avalonia.Animation private TimeSpan _initialDelay; private TimeSpan _iterationDelay; private TimeSpan _duration; - private Easings.Easing _easeFunc; - private Action _onCompleteAction; + private Easings.Easing? _easeFunc; + private Action? _onCompleteAction; private Func _interpolator; - private IDisposable _timerSub; + private IDisposable? _timerSub; private readonly IClock _baseClock; - private IClock _clock; - private EventHandler _propertyChangedDelegate; + private IClock? _clock; + private EventHandler? _propertyChangedDelegate; - public AnimationInstance(Animation animation, Animatable control, Animator animator, IClock baseClock, Action OnComplete, Func Interpolator) + public AnimationInstance(Animation animation, Animatable control, Animator animator, IClock baseClock, Action? OnComplete, Func Interpolator) { _animator = animator; _animation = animation; @@ -47,6 +47,9 @@ namespace Avalonia.Animation _onCompleteAction = OnComplete; _interpolator = Interpolator; _baseClock = baseClock; + _lastInterpValue = default!; + _firstKFValue = default!; + _neutralValue = default!; FetchProperties(); } @@ -82,7 +85,7 @@ namespace Avalonia.Animation _targetControl.PropertyChanged -= _propertyChangedDelegate; _timerSub?.Dispose(); - _clock.PlayState = PlayState.Stop; + _clock!.PlayState = PlayState.Stop; } protected override void Subscribed() @@ -108,6 +111,8 @@ namespace Avalonia.Animation private void ApplyFinalFill() { + if (_animator.Property is null) + throw new InvalidOperationException("Animator has no property specified."); if (_fillMode == FillMode.Forward || _fillMode == FillMode.Both) _targetControl.SetValue(_animator.Property, _lastInterpValue, BindingPriority.LocalValue); } @@ -130,12 +135,12 @@ namespace Avalonia.Animation private void DoPlayStates() { - if (_clock.PlayState == PlayState.Stop || _baseClock.PlayState == PlayState.Stop) + if (_clock!.PlayState == PlayState.Stop || _baseClock.PlayState == PlayState.Stop) DoComplete(); if (!_gotFirstKFValue) { - _firstKFValue = (T)_animator.First().Value; + _firstKFValue = (T)_animator.First().Value!; _gotFirstKFValue = true; } } @@ -169,7 +174,7 @@ namespace Avalonia.Animation // and snap the last iteration value to exact values. if ((_currentIteration + 1) > _iterationCount) { - var easedTime = _easeFunc.Ease(_playbackReversed ? 0.0 : 1.0); + var easedTime = _easeFunc!.Ease(_playbackReversed ? 0.0 : 1.0); _lastInterpValue = _interpolator(easedTime, _neutralValue); DoComplete(); } @@ -203,7 +208,7 @@ namespace Avalonia.Animation normalizedTime = 1 - normalizedTime; // Ease and interpolate - var easedTime = _easeFunc.Ease(normalizedTime); + var easedTime = _easeFunc!.Ease(normalizedTime); _lastInterpValue = _interpolator(easedTime, _neutralValue); PublishNext(_lastInterpValue); @@ -223,14 +228,14 @@ namespace Avalonia.Animation private void UpdateNeutralValue() { - var property = _animator.Property; + var property = _animator.Property ?? throw new InvalidOperationException("Animator has no property specified."); var baseValue = _targetControl.GetBaseValue(property, BindingPriority.LocalValue); _neutralValue = baseValue != AvaloniaProperty.UnsetValue ? - (T)baseValue : (T)_targetControl.GetValue(property); + (T)baseValue! : (T)_targetControl.GetValue(property)!; } - private void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + private void ControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { if (e.Property == _animator.Property && e.Priority > BindingPriority.Animation) { diff --git a/src/Avalonia.Animation/AnimatorKeyFrame.cs b/src/Avalonia.Animation/AnimatorKeyFrame.cs index f6a0c12be4..8af31f2948 100644 --- a/src/Avalonia.Animation/AnimatorKeyFrame.cs +++ b/src/Avalonia.Animation/AnimatorKeyFrame.cs @@ -12,22 +12,22 @@ namespace Avalonia.Animation /// public class AnimatorKeyFrame : AvaloniaObject { - public static readonly DirectProperty ValueProperty = - AvaloniaProperty.RegisterDirect(nameof(Value), k => k.Value, (k, v) => k.Value = v); + 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) + public AnimatorKeyFrame(Type? animatorType, Cue cue) { AnimatorType = animatorType; Cue = cue; KeySpline = null; } - public AnimatorKeyFrame(Type animatorType, Cue cue, KeySpline keySpline) + public AnimatorKeyFrame(Type? animatorType, Cue cue, KeySpline? keySpline) { AnimatorType = animatorType; Cue = cue; @@ -35,14 +35,14 @@ namespace Avalonia.Animation } internal bool isNeutral; - public Type AnimatorType { get; } + public Type? AnimatorType { get; } public Cue Cue { get; } - public KeySpline KeySpline { get; } - public AvaloniaProperty Property { get; private set; } + public KeySpline? KeySpline { get; } + public AvaloniaProperty? Property { get; private set; } - private object _value; + private object? _value; - public object Value + public object? Value { get => _value; set => SetAndRaise(ValueProperty, ref _value, value); @@ -80,7 +80,7 @@ namespace Avalonia.Animation throw new InvalidCastException($"KeyFrame value doesnt match property type."); } - return (T)typeConv.ConvertTo(Value, typeof(T)); + return (T)typeConv.ConvertTo(Value, typeof(T))!; } } } diff --git a/src/Avalonia.Animation/Animators/Animator`1.cs b/src/Avalonia.Animation/Animators/Animator`1.cs index 23afa76bf6..248ca61c1d 100644 --- a/src/Avalonia.Animation/Animators/Animator`1.cs +++ b/src/Avalonia.Animation/Animators/Animator`1.cs @@ -24,7 +24,7 @@ namespace Avalonia.Animation.Animators /// /// Gets or sets the target property for the keyframe. /// - public AvaloniaProperty Property { get; set; } + public AvaloniaProperty? Property { get; set; } public Animator() { @@ -33,7 +33,7 @@ namespace Avalonia.Animation.Animators } /// - public virtual IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable match, Action onComplete) + public virtual IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable match, Action? onComplete) { if (!_isVerifiedAndConverted) VerifyConvertKeyFrames(); @@ -106,13 +106,16 @@ namespace Avalonia.Animation.Animators public virtual IDisposable BindAnimation(Animatable control, IObservable instance) { + if (Property is null) + throw new InvalidOperationException("Animator has no property specified."); + return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation); } /// /// Runs the KeyFrames Animation. /// - internal IDisposable Run(Animation animation, Animatable control, IClock clock, Action onComplete) + internal IDisposable Run(Animation animation, Animatable control, IClock? clock, Action? onComplete) { var instance = new AnimationInstance( animation, diff --git a/src/Avalonia.Animation/Avalonia.Animation.csproj b/src/Avalonia.Animation/Avalonia.Animation.csproj index 85938ad958..9e3758658c 100644 --- a/src/Avalonia.Animation/Avalonia.Animation.csproj +++ b/src/Avalonia.Animation/Avalonia.Animation.csproj @@ -2,9 +2,13 @@ netstandard2.0;net6.0 + + + + diff --git a/src/Avalonia.Animation/Clock.cs b/src/Avalonia.Animation/Clock.cs index 5c2b7ce0dd..5afd2ae705 100644 --- a/src/Avalonia.Animation/Clock.cs +++ b/src/Avalonia.Animation/Clock.cs @@ -4,7 +4,7 @@ namespace Avalonia.Animation { public class Clock : ClockBase { - public static IClock GlobalClock => AvaloniaLocator.Current.GetService(); + public static IClock GlobalClock => AvaloniaLocator.Current.GetRequiredService(); private readonly IDisposable _parentSubscription; diff --git a/src/Avalonia.Animation/Cue.cs b/src/Avalonia.Animation/Cue.cs index 7da7a9382b..6578148b07 100644 --- a/src/Avalonia.Animation/Cue.cs +++ b/src/Avalonia.Animation/Cue.cs @@ -30,7 +30,7 @@ namespace Avalonia.Animation /// /// Parses a string to a object. /// - public static Cue Parse(string value, CultureInfo culture) + public static Cue Parse(string value, CultureInfo? culture) { string v = value; @@ -72,14 +72,14 @@ namespace Avalonia.Animation public class CueTypeConverter : TypeConverter { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { return sourceType == typeof(string); } - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { return Cue.Parse((string)value, culture); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs b/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs index 696f43d006..7283eaeedf 100644 --- a/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs +++ b/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs @@ -8,15 +8,15 @@ namespace Avalonia.Animation /// internal class DisposeAnimationInstanceSubject : IObserver, IDisposable { - private IDisposable _lastInstance; + private IDisposable? _lastInstance; private bool _lastMatch; private Animator _animator; private Animation _animation; private Animatable _control; - private Action _onComplete; - private IClock _clock; + private Action? _onComplete; + private IClock? _clock; - public DisposeAnimationInstanceSubject(Animator animator, Animation animation, Animatable control, IClock clock, Action onComplete) + public DisposeAnimationInstanceSubject(Animator animator, Animation animation, Animatable control, IClock? clock, Action? onComplete) { this._animator = animator; this._animation = animation; diff --git a/src/Avalonia.Animation/Easing/Easing.cs b/src/Avalonia.Animation/Easing/Easing.cs index e006459652..2f4b93dab1 100644 --- a/src/Avalonia.Animation/Easing/Easing.cs +++ b/src/Avalonia.Animation/Easing/Easing.cs @@ -15,7 +15,7 @@ namespace Avalonia.Animation.Easings /// public abstract double Ease(double progress); - static Dictionary _easingTypes; + static Dictionary? _easingTypes; static readonly Type s_thisType = typeof(Easing); @@ -48,7 +48,7 @@ namespace Avalonia.Animation.Easings if (_easingTypes.ContainsKey(e)) { var type = _easingTypes[e]; - return (Easing)Activator.CreateInstance(type); + return (Easing)Activator.CreateInstance(type)!; } else { diff --git a/src/Avalonia.Animation/Easing/EasingTypeConverter.cs b/src/Avalonia.Animation/Easing/EasingTypeConverter.cs index 6613f6d393..3d67d54a6f 100644 --- a/src/Avalonia.Animation/Easing/EasingTypeConverter.cs +++ b/src/Avalonia.Animation/Easing/EasingTypeConverter.cs @@ -6,12 +6,12 @@ namespace Avalonia.Animation.Easings { public class EasingTypeConverter : TypeConverter { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { return sourceType == typeof(string); } - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { return Easing.Parse((string)value); } diff --git a/src/Avalonia.Animation/IAnimation.cs b/src/Avalonia.Animation/IAnimation.cs index d037834630..436a765d27 100644 --- a/src/Avalonia.Animation/IAnimation.cs +++ b/src/Avalonia.Animation/IAnimation.cs @@ -12,7 +12,7 @@ namespace Avalonia.Animation /// /// Apply the animation to the specified control and run it when produces true. /// - IDisposable Apply(Animatable control, IClock clock, IObservable match, Action onComplete = null); + IDisposable Apply(Animatable control, IClock? clock, IObservable match, Action? onComplete = null); /// /// Run the animation on the specified control. diff --git a/src/Avalonia.Animation/IAnimationSetter.cs b/src/Avalonia.Animation/IAnimationSetter.cs index 2d22377286..6a1d3539e2 100644 --- a/src/Avalonia.Animation/IAnimationSetter.cs +++ b/src/Avalonia.Animation/IAnimationSetter.cs @@ -2,7 +2,7 @@ namespace Avalonia.Animation { public interface IAnimationSetter { - AvaloniaProperty Property { get; set; } - object Value { get; set; } + AvaloniaProperty? Property { get; set; } + object? Value { get; set; } } } diff --git a/src/Avalonia.Animation/IAnimator.cs b/src/Avalonia.Animation/IAnimator.cs index d0fb173c54..f64ac9f913 100644 --- a/src/Avalonia.Animation/IAnimator.cs +++ b/src/Avalonia.Animation/IAnimator.cs @@ -11,11 +11,11 @@ namespace Avalonia.Animation /// /// The target property. /// - AvaloniaProperty Property {get; set;} + AvaloniaProperty? Property {get; set;} /// /// Applies the current KeyFrame group to the specified control. /// - IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable match, Action onComplete); + IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable match, Action? onComplete); } } diff --git a/src/Avalonia.Animation/ITransition.cs b/src/Avalonia.Animation/ITransition.cs index ade2ec8b9e..241ca208d1 100644 --- a/src/Avalonia.Animation/ITransition.cs +++ b/src/Avalonia.Animation/ITransition.cs @@ -10,7 +10,7 @@ namespace Avalonia.Animation /// /// Applies the transition to the specified . /// - IDisposable Apply(Animatable control, IClock clock, object oldValue, object newValue); + IDisposable Apply(Animatable control, IClock clock, object? oldValue, object? newValue); /// /// Gets the property to be animated. diff --git a/src/Avalonia.Animation/IterationCount.cs b/src/Avalonia.Animation/IterationCount.cs index 9463718608..3b52cdab49 100644 --- a/src/Avalonia.Animation/IterationCount.cs +++ b/src/Avalonia.Animation/IterationCount.cs @@ -97,7 +97,7 @@ namespace Avalonia.Animation /// /// The object with which to test equality. /// True if the objects are equal, otherwise false. - public override bool Equals(object o) + public override bool Equals(object? o) { if (o == null) { diff --git a/src/Avalonia.Animation/IterationCountTypeConverter.cs b/src/Avalonia.Animation/IterationCountTypeConverter.cs index 1c63f8cdf1..f64972ff5c 100644 --- a/src/Avalonia.Animation/IterationCountTypeConverter.cs +++ b/src/Avalonia.Animation/IterationCountTypeConverter.cs @@ -6,12 +6,12 @@ namespace Avalonia.Animation { public class IterationCountTypeConverter : TypeConverter { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { return sourceType == typeof(string); } - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { return IterationCount.Parse((string)value); } diff --git a/src/Avalonia.Animation/KeyFrame.cs b/src/Avalonia.Animation/KeyFrame.cs index c2cc1aa051..3ab7a70d90 100644 --- a/src/Avalonia.Animation/KeyFrame.cs +++ b/src/Avalonia.Animation/KeyFrame.cs @@ -19,7 +19,7 @@ namespace Avalonia.Animation { private TimeSpan _ktimeSpan; private Cue _kCue; - private KeySpline _kKeySpline; + private KeySpline? _kKeySpline; public KeyFrame() { @@ -79,7 +79,7 @@ namespace Avalonia.Animation /// Gets or sets the KeySpline of this . /// /// The key spline. - public KeySpline KeySpline + public KeySpline? KeySpline { get { diff --git a/src/Avalonia.Animation/KeySplineTypeConverter.cs b/src/Avalonia.Animation/KeySplineTypeConverter.cs index b026206e5f..eecad3c3ac 100644 --- a/src/Avalonia.Animation/KeySplineTypeConverter.cs +++ b/src/Avalonia.Animation/KeySplineTypeConverter.cs @@ -12,12 +12,12 @@ namespace Avalonia.Animation /// public class KeySplineTypeConverter : TypeConverter { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { return sourceType == typeof(string); } - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { return KeySpline.Parse((string)value, CultureInfo.InvariantCulture); } diff --git a/src/Avalonia.Animation/Transition.cs b/src/Avalonia.Animation/Transition.cs index 4115c95c0f..d307f348c4 100644 --- a/src/Avalonia.Animation/Transition.cs +++ b/src/Avalonia.Animation/Transition.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia.Animation.Easings; namespace Avalonia.Animation @@ -8,7 +9,7 @@ namespace Avalonia.Animation /// public abstract class Transition : AvaloniaObject, ITransition { - private AvaloniaProperty _prop; + private AvaloniaProperty? _prop; /// /// Gets or sets the duration of the transition. @@ -26,7 +27,8 @@ namespace Avalonia.Animation public Easing Easing { get; set; } = new LinearEasing(); /// - public AvaloniaProperty Property + [DisallowNull] + public AvaloniaProperty? Property { get { @@ -42,16 +44,25 @@ namespace Avalonia.Animation } } + AvaloniaProperty ITransition.Property + { + get => Property ?? throw new InvalidOperationException("Transition has no property specified."); + set => Property = value; + } + /// /// Apply interpolation to the property. /// public abstract IObservable DoTransition(IObservable progress, T oldValue, T newValue); /// - public virtual IDisposable Apply(Animatable control, IClock clock, object oldValue, object newValue) + public virtual IDisposable Apply(Animatable control, IClock clock, object? oldValue, object? newValue) { - var transition = DoTransition(new TransitionInstance(clock, Delay, Duration), (T)oldValue, (T)newValue); + if (Property is null) + throw new InvalidOperationException("Transition has no property specified."); + + var transition = DoTransition(new TransitionInstance(clock, Delay, Duration), (T)oldValue!, (T)newValue!); return control.Bind((AvaloniaProperty)Property, transition, Data.BindingPriority.Animation); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Animation/TransitionInstance.cs b/src/Avalonia.Animation/TransitionInstance.cs index b522d1961e..9c9494ff87 100644 --- a/src/Avalonia.Animation/TransitionInstance.cs +++ b/src/Avalonia.Animation/TransitionInstance.cs @@ -10,11 +10,11 @@ namespace Avalonia.Animation /// internal class TransitionInstance : SingleSubscriberObservableBase, IObserver { - private IDisposable _timerSubscription; + private IDisposable? _timerSubscription; private TimeSpan _delay; private TimeSpan _duration; private readonly IClock _baseClock; - private TransitionClock _clock; + private TransitionClock? _clock; public TransitionInstance(IClock clock, TimeSpan delay, TimeSpan duration) { @@ -67,7 +67,7 @@ namespace Avalonia.Animation protected override void Unsubscribed() { _timerSubscription?.Dispose(); - _clock.PlayState = PlayState.Stop; + _clock!.PlayState = PlayState.Stop; } protected override void Subscribed() diff --git a/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs index a2c4b0313b..5f22254fb5 100644 --- a/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs @@ -38,8 +38,8 @@ namespace Avalonia.Animation.Animators } /// - public override IDisposable Apply(Animation animation, Animatable control, IClock clock, - IObservable match, Action onComplete) + public override IDisposable? Apply(Animation animation, Animatable control, IClock? clock, + IObservable match, Action? onComplete) { if (TryCreateCustomRegisteredAnimator(out var animator) || TryCreateGradientAnimator(out animator) @@ -135,9 +135,8 @@ namespace Avalonia.Animation.Animators private bool TryCreateCustomRegisteredAnimator([NotNullWhen(true)] out IAnimator? animator) { - if (_brushAnimators.Count > 0) + if (_brushAnimators.Count > 0 && this[0].Value?.GetType() is Type firstKeyType) { - var firstKeyType = this[0].Value.GetType(); foreach (var (match, animatorType) in _brushAnimators) { if (!match(firstKeyType)) diff --git a/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs index 864e12413f..0979de16d0 100644 --- a/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs @@ -58,6 +58,11 @@ namespace Avalonia.Animation.Animators public override IDisposable BindAnimation(Animatable control, IObservable instance) { + if (Property is null) + { + throw new InvalidOperationException("Animator has no property specified."); + } + return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation); } diff --git a/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs index 7c6372aae2..57f9f3c1a5 100644 --- a/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs @@ -24,6 +24,11 @@ namespace Avalonia.Animation.Animators public override IDisposable BindAnimation(Animatable control, IObservable instance) { + if (Property is null) + { + throw new InvalidOperationException("Animator has no property specified."); + } + return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation); } } diff --git a/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs index 1d7bfd3748..34ec8ac503 100644 --- a/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs @@ -14,10 +14,15 @@ namespace Avalonia.Animation.Animators DoubleAnimator? _doubleAnimator; /// - public override IDisposable? Apply(Animation animation, Animatable control, IClock clock, IObservable obsMatch, Action onComplete) + public override IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable obsMatch, Action? onComplete) { var ctrl = (Visual)control; + if (Property is null) + { + throw new InvalidOperationException("Animator has no property specified."); + } + // Check if the Target Property is Transform derived. if (typeof(Transform).IsAssignableFrom(Property.OwnerType)) { From 98d061ec30b65b63ec98d9c45e52332d65a6db21 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 22 Dec 2021 15:53:12 +0100 Subject: [PATCH 04/44] Fix failing unit tests. --- .../AnimatableTests.cs | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs index 26d8059eec..1d5296bebd 100644 --- a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs +++ b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs @@ -36,7 +36,7 @@ namespace Avalonia.Animation.UnitTests [Fact] public void Transition_Is_Not_Applied_To_Initial_Style() { - using (UnitTestApplication.Start(TestServices.RealStyler)) + using (Start()) { var target = CreateTarget(); var control = new Control @@ -74,6 +74,7 @@ namespace Avalonia.Animation.UnitTests [Fact] public void Transition_Is_Applied_When_Local_Value_Changes() { + using var app = Start(); var target = CreateTarget(); var control = CreateControl(target.Object); @@ -170,6 +171,7 @@ namespace Avalonia.Animation.UnitTests [Fact] public void Transition_Is_Not_Applied_When_StyleTrigger_Changes_With_LocalValue_Present() { + using var app = Start(); var target = CreateTarget(); var control = CreateControl(target.Object); @@ -195,6 +197,7 @@ namespace Avalonia.Animation.UnitTests [Fact] public void Transition_Is_Disposed_When_Local_Value_Changes() { + using var app = Start(); var target = CreateTarget(); var control = CreateControl(target.Object); var sub = new Mock(); @@ -211,6 +214,7 @@ namespace Avalonia.Animation.UnitTests [Fact] public void New_Transition_Is_Applied_When_Local_Value_Changes() { + using var app = Start(); var target = CreateTarget(); var control = CreateControl(target.Object); @@ -239,6 +243,7 @@ namespace Avalonia.Animation.UnitTests [Fact] public void Transition_Is_Not_Applied_When_Removed_From_Visual_Tree() { + using var app = Start(); var target = CreateTarget(); var control = CreateControl(target.Object); @@ -266,6 +271,7 @@ namespace Avalonia.Animation.UnitTests [Fact] public void Animation_Is_Cancelled_When_Transition_Removed() { + using var app = Start(); var target = CreateTarget(); var control = CreateControl(target.Object); var sub = new Mock(); @@ -285,7 +291,7 @@ namespace Avalonia.Animation.UnitTests [Fact] public void Animation_Is_Cancelled_When_New_Style_Activates() { - using (UnitTestApplication.Start(TestServices.RealStyler)) + using (Start()) { var target = CreateTarget(); var control = CreateStyledControl(target.Object); @@ -301,7 +307,7 @@ namespace Avalonia.Animation.UnitTests target.Verify(x => x.Apply( control, - It.IsAny(), + It.IsAny(), 1.0, 0.5), Times.Once); @@ -315,7 +321,7 @@ namespace Avalonia.Animation.UnitTests [Fact] public void Transition_From_Style_Trigger_Is_Applied() { - using (UnitTestApplication.Start(TestServices.RealStyler)) + using (Start()) { var target = CreateTransition(Control.WidthProperty); var control = CreateStyledControl(transition2: target.Object); @@ -326,7 +332,7 @@ namespace Avalonia.Animation.UnitTests target.Verify(x => x.Apply( control, - It.IsAny(), + It.IsAny(), double.NaN, 100.0), Times.Once); @@ -337,7 +343,7 @@ namespace Avalonia.Animation.UnitTests public void Replacing_Transitions_During_Animation_Does_Not_Throw_KeyNotFound() { // Issue #4059 - using (UnitTestApplication.Start(TestServices.RealStyler)) + using (Start()) { Border target; var clock = new TestClock(); @@ -428,6 +434,13 @@ namespace Avalonia.Animation.UnitTests control.EndBatchUpdate(); } + private static IDisposable Start() + { + var clock = new MockGlobalClock(); + var services = TestServices.RealStyler.With(globalClock: clock); + return UnitTestApplication.Start(services); + } + private static Mock CreateTarget() { return CreateTransition(Visual.OpacityProperty); From c51086401454b08895164525fb3aad99f88ac6bb Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Sun, 2 Jan 2022 05:20:39 +0300 Subject: [PATCH 05/44] [WASM] Fix cursors in macOS, fix default cursor set logic --- src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs | 6 +++++- src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs | 8 ++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs index 4b0ada1f27..7644514687 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs @@ -259,7 +259,11 @@ namespace Avalonia.Web.Blazor _inputHelper.Hide(); _canvasHelper.SetCursor("default"); - _topLevelImpl.SetCssCursor = _canvasHelper.SetCursor; + _topLevelImpl.SetCssCursor = x => + { + _inputHelper.SetCursor(x);//macOS + _canvasHelper.SetCursor(x);//windows + }; Console.WriteLine("starting html canvas setup"); _interop = await SKHtmlCanvasInterop.ImportAsync(Js, _htmlCanvas, OnRenderFrame); diff --git a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs index ac5f5044d8..1d667c0f0c 100644 --- a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs +++ b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs @@ -127,15 +127,11 @@ namespace Avalonia.Web.Blazor public void SetCursor(ICursorImpl cursor) { - var cur = cursor as CssCursor; - var val = CssCursor.Default; - if (cur != null && cur.Value != null) - { - val = cur.Value; - } + var val = (cursor as CssCursor)?.Value ?? CssCursor.Default; if (_currentCursor != val) { SetCssCursor?.Invoke(val); + _currentCursor = val; } } From 509b9d8f09eb1928608fe75354a43b6a11e0bf2c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 3 Jan 2022 11:50:43 +0100 Subject: [PATCH 06/44] Add additional null checks to WindowBaseImpl. Fixes #7231. --- src/Avalonia.Native/WindowImplBase.cs | 28 +++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 4a3baa2788..87b7a7608e 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -28,18 +28,18 @@ namespace Avalonia.Native public string HandleDescriptor => "NSWindow"; - public IntPtr NSView => _native.ObtainNSViewHandle(); + public IntPtr NSView => _native?.ObtainNSViewHandle() ?? IntPtr.Zero; - public IntPtr NSWindow => _native.ObtainNSWindowHandle(); + public IntPtr NSWindow => _native?.ObtainNSWindowHandle() ?? IntPtr.Zero; public IntPtr GetNSViewRetained() { - return _native.ObtainNSViewHandleRetained(); + return _native?.ObtainNSViewHandleRetained() ?? IntPtr.Zero; } public IntPtr GetNSWindowRetained() { - return _native.ObtainNSWindowHandleRetained(); + return _native?.ObtainNSWindowHandleRetained() ?? IntPtr.Zero; } } @@ -260,7 +260,7 @@ namespace Avalonia.Native public void Activate() { - _native.Activate(); + _native?.Activate(); } public bool RawTextInputEvent(uint timeStamp, string text) @@ -322,7 +322,7 @@ namespace Avalonia.Native public void Resize(Size clientSize, PlatformResizeReason reason) { - _native.Resize(clientSize.Width, clientSize.Height, (AvnPlatformResizeReason)reason); + _native?.Resize(clientSize.Width, clientSize.Height, (AvnPlatformResizeReason)reason); } public IRenderer CreateRenderer(IRenderRoot root) @@ -367,14 +367,14 @@ namespace Avalonia.Native public virtual void Show(bool activate, bool isDialog) { - _native.Show(activate.AsComBool(), isDialog.AsComBool()); + _native?.Show(activate.AsComBool(), isDialog.AsComBool()); } public PixelPoint Position { - get => _native.Position.ToAvaloniaPixelPoint(); - set => _native.SetPosition(value.ToAvnPoint()); + get => _native?.Position.ToAvaloniaPixelPoint() ?? default; + set => _native?.SetPosition(value.ToAvnPoint()); } public Point PointToClient(PixelPoint point) @@ -389,12 +389,12 @@ namespace Avalonia.Native public void Hide() { - _native.Hide(); + _native?.Hide(); } public void BeginMoveDrag(PointerPressedEventArgs e) { - _native.BeginMoveDrag(); + _native?.BeginMoveDrag(); } public Size MaxAutoSizeHint => Screen.AllScreens.Select(s => s.Bounds.Size.ToSize(1)) @@ -402,7 +402,7 @@ namespace Avalonia.Native public void SetTopmost(bool value) { - _native.SetTopMost(value.AsComBool()); + _native?.SetTopMost(value.AsComBool()); } public double RenderScaling => _native?.Scaling ?? 1; @@ -438,7 +438,7 @@ namespace Avalonia.Native public void SetMinMaxSize(Size minSize, Size maxSize) { - _native.SetMinMaxSize(minSize.ToAvnSize(), maxSize.ToAvnSize()); + _native?.SetMinMaxSize(minSize.ToAvnSize(), maxSize.ToAvnSize()); } public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) @@ -449,7 +449,7 @@ namespace Avalonia.Native internal void BeginDraggingSession(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard clipboard, IAvnDndResultCallback callback, IntPtr sourceHandle) { - _native.BeginDragAndDropOperation(effects, point, clipboard, callback, sourceHandle); + _native?.BeginDragAndDropOperation(effects, point, clipboard, callback, sourceHandle); } public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) From e6c08a13d70f7173494b83489806e2e1f10b442d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 3 Jan 2022 19:04:06 +0300 Subject: [PATCH 07/44] Added reflection-free API for weak events --- .../NotifyCollectionChangedExtensions.cs | 21 +-- .../Data/Core/IndexerNodeBase.cs | 10 +- .../Core/Plugins/IndeiValidationPlugin.cs | 20 +- .../Plugins/InpcPropertyAccessorPlugin.cs | 24 +-- .../Utilities/IWeakEventSubscriber.cs | 12 ++ src/Avalonia.Base/Utilities/WeakEvent.cs | 175 ++++++++++++++++++ src/Avalonia.Base/Utilities/WeakEvents.cs | 40 ++++ src/Avalonia.Base/Utilities/WeakObservable.cs | 35 +++- .../Utilities/WeakSubscriptionManager.cs | 1 + src/Avalonia.Controls/NativeMenuItem.cs | 10 +- .../Repeater/ItemsRepeater.cs | 32 ++-- src/Avalonia.Controls/TopLevel.cs | 15 +- .../Utils/CollectionChangedEventManager.cs | 15 +- src/Avalonia.Layout/AttachedLayout.cs | 17 ++ .../PropertyInfoAccessorFactory.cs | 36 +--- .../Avalonia.Base.UnitTests/WeakEventTests.cs | 75 ++++++++ 16 files changed, 421 insertions(+), 117 deletions(-) create mode 100644 src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs create mode 100644 src/Avalonia.Base/Utilities/WeakEvent.cs create mode 100644 src/Avalonia.Base/Utilities/WeakEvents.cs create mode 100644 tests/Avalonia.Base.UnitTests/WeakEventTests.cs diff --git a/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs b/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs index dcd32ddd76..689fcc89a4 100644 --- a/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs +++ b/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs @@ -59,7 +59,7 @@ namespace Avalonia.Collections } private class WeakCollectionChangedObservable : LightweightObservableBase, - IWeakSubscriber + IWeakEventSubscriber { private WeakReference _sourceReference; @@ -68,31 +68,22 @@ namespace Avalonia.Collections _sourceReference = source; } - public void OnEvent(object? sender, NotifyCollectionChangedEventArgs e) + public void OnEvent(object? sender, + WeakEvent ev, + NotifyCollectionChangedEventArgs e) { PublishNext(e); } - protected override void Initialize() { if (_sourceReference.TryGetTarget(out var instance)) - { - WeakSubscriptionManager.Subscribe( - instance, - nameof(instance.CollectionChanged), - this); - } + WeakEvents.CollectionChanged.Subscribe(instance, this); } protected override void Deinitialize() { if (_sourceReference.TryGetTarget(out var instance)) - { - WeakSubscriptionManager.Unsubscribe( - instance, - nameof(instance.CollectionChanged), - this); - } + WeakEvents.CollectionChanged.Unsubscribe(instance, this); } } } diff --git a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs index e197e29103..a808827896 100644 --- a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs +++ b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs @@ -23,18 +23,16 @@ namespace Avalonia.Data.Core if (incc != null) { - inputs.Add(WeakObservable.FromEventPattern( - incc, - nameof(incc.CollectionChanged)) + inputs.Add(WeakObservable.FromEventPattern( + incc, WeakEvents.CollectionChanged) .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) .Select(_ => GetValue(target))); } if (inpc != null) { - inputs.Add(WeakObservable.FromEventPattern( - inpc, - nameof(inpc.PropertyChanged)) + inputs.Add(WeakObservable.FromEventPattern( + inpc, WeakEvents.PropertyChanged) .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) .Select(_ => GetValue(target))); } diff --git a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs index 9f827daf94..1e7a0d5c8f 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs @@ -11,6 +11,12 @@ namespace Avalonia.Data.Core.Plugins /// public class IndeiValidationPlugin : IDataValidationPlugin { + private static readonly WeakEvent + ErrorsChangedWeakEvent = WeakEvent.Register( + (s, h) => s.ErrorsChanged += h, + (s, h) => s.ErrorsChanged -= h + ); + /// public bool Match(WeakReference reference, string memberName) { @@ -25,7 +31,7 @@ namespace Avalonia.Data.Core.Plugins return new Validator(reference, name, accessor); } - private class Validator : DataValidationBase, IWeakSubscriber + private class Validator : DataValidationBase, IWeakEventSubscriber { private readonly WeakReference _reference; private readonly string _name; @@ -37,7 +43,7 @@ namespace Avalonia.Data.Core.Plugins _name = name; } - void IWeakSubscriber.OnEvent(object? sender, DataErrorsChangedEventArgs e) + void IWeakEventSubscriber.OnEvent(object? notifyDataErrorInfo, WeakEvent ev, DataErrorsChangedEventArgs e) { if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName)) { @@ -51,10 +57,7 @@ namespace Avalonia.Data.Core.Plugins if (target != null) { - WeakSubscriptionManager.Subscribe( - target, - nameof(target.ErrorsChanged), - this); + ErrorsChangedWeakEvent.Subscribe(target, this); } base.SubscribeCore(); @@ -66,10 +69,7 @@ namespace Avalonia.Data.Core.Plugins if (target != null) { - WeakSubscriptionManager.Unsubscribe( - target, - nameof(target.ErrorsChanged), - this); + ErrorsChangedWeakEvent.Unsubscribe(target, this); } base.UnsubscribeCore(); diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index fd532f3014..33cecd10a7 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.ComponentModel; using System.Reflection; using Avalonia.Utilities; @@ -85,7 +86,7 @@ namespace Avalonia.Data.Core.Plugins return found; } - private class Accessor : PropertyAccessorBase, IWeakSubscriber + private class Accessor : PropertyAccessorBase, IWeakEventSubscriber { private readonly WeakReference _reference; private readonly PropertyInfo _property; @@ -129,7 +130,8 @@ namespace Avalonia.Data.Core.Plugins return false; } - void IWeakSubscriber.OnEvent(object? sender, PropertyChangedEventArgs e) + void IWeakEventSubscriber. + OnEvent(object? notifyPropertyChanged, WeakEvent ev, PropertyChangedEventArgs e) { if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName)) { @@ -148,13 +150,8 @@ namespace Avalonia.Data.Core.Plugins { var inpc = GetReferenceTarget() as INotifyPropertyChanged; - if (inpc != null) - { - WeakSubscriptionManager.Unsubscribe( - inpc, - nameof(inpc.PropertyChanged), - this); - } + if (inpc != null) + WeakEvents.PropertyChanged.Unsubscribe(inpc, this); } private object? GetReferenceTarget() @@ -178,13 +175,8 @@ namespace Avalonia.Data.Core.Plugins { var inpc = GetReferenceTarget() as INotifyPropertyChanged; - if (inpc != null) - { - WeakSubscriptionManager.Subscribe( - inpc, - nameof(inpc.PropertyChanged), - this); - } + if (inpc != null) + WeakEvents.PropertyChanged.Subscribe(inpc, this); } } } diff --git a/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs b/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs new file mode 100644 index 0000000000..e48c0cb111 --- /dev/null +++ b/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs @@ -0,0 +1,12 @@ +using System; + +namespace Avalonia.Utilities; + +/// +/// Defines a listener to a event subscribed vis the . +/// +/// The type of the event arguments. +public interface IWeakEventSubscriber where TEventArgs : EventArgs +{ + void OnEvent(object? sender, WeakEvent ev, TEventArgs e); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs new file mode 100644 index 0000000000..430bc92838 --- /dev/null +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Avalonia.Utilities; + +/// +/// Manages subscriptions to events using weak listeners. +/// +public class WeakEvent : WeakEvent where TEventArgs : EventArgs where TSender : class +{ + private readonly Func, Action> _subscribe; + + readonly ConditionalWeakTable _subscriptions = new(); + + internal WeakEvent( + Action> subscribe, + Action> unsubscribe) + { + _subscribe = (t, s) => + { + subscribe(t, s); + return () => unsubscribe(t, s); + }; + } + + internal WeakEvent(Func, Action> subscribe) + { + _subscribe = subscribe; + } + + public void Subscribe(TSender target, IWeakEventSubscriber subscriber) + { + if (!_subscriptions.TryGetValue(target, out var subscription)) + _subscriptions.Add(target, subscription = new Subscription(this, target)); + subscription.Add(new WeakReference>(subscriber)); + } + + public void Unsubscribe(TSender target, IWeakEventSubscriber subscriber) + { + if (_subscriptions.TryGetValue(target, out var subscription)) + subscription.Remove(subscriber); + } + + private class Subscription + { + private readonly WeakEvent _ev; + private readonly TSender _target; + + private WeakReference>?[] _data = + new WeakReference>[16]; + private int _count; + private readonly Action _unsubscribe; + + public Subscription(WeakEvent ev, TSender target) + { + _ev = ev; + _target = target; + + _unsubscribe = ev._subscribe(target, OnEvent); + } + + void Destroy() + { + _unsubscribe(); + _ev._subscriptions.Remove(_target); + } + + public void Add(WeakReference> s) + { + if (_count == _data.Length) + { + //Extend capacity + var extendedData = new WeakReference>?[_data.Length * 2]; + Array.Copy(_data, extendedData, _data.Length); + _data = extendedData; + } + + _data[_count] = s; + _count++; + } + + public void Remove(IWeakEventSubscriber s) + { + var removed = false; + + for (int c = 0; c < _count; ++c) + { + var reference = _data[c]; + + if (reference != null && reference.TryGetTarget(out var instance) && instance == s) + { + _data[c] = null; + removed = true; + } + } + + if (removed) + { + Compact(); + } + } + + void Compact() + { + int empty = -1; + for (var c = 0; c < _count; c++) + { + var r = _data[c]; + //Mark current index as first empty + if (r == null && empty == -1) + empty = c; + //If current element isn't null and we have an empty one + if (r != null && empty != -1) + { + _data[c] = null; + _data[empty] = r; + empty++; + } + } + + if (empty != -1) + _count = empty; + if (_count == 0) + Destroy(); + } + + void OnEvent(object? sender, TEventArgs eventArgs) + { + var needCompact = false; + for (var c = 0; c < _count; c++) + { + var r = _data[c]; + if (r?.TryGetTarget(out var sub) == true) + sub!.OnEvent(_target, _ev, eventArgs); + else + needCompact = true; + } + + if (needCompact) + Compact(); + } + } + +} + +public class WeakEvent +{ + public static WeakEvent Register( + Action> subscribe, + Action> unsubscribe) where TSender : class where TEventArgs : EventArgs + { + return new WeakEvent(subscribe, unsubscribe); + } + + public static WeakEvent Register( + Func, Action> subscribe) where TSender : class where TEventArgs : EventArgs + { + return new WeakEvent(subscribe); + } + + public static WeakEvent Register( + Action subscribe, + Action unsubscribe) where TSender : class + { + return Register((s, h) => + { + EventHandler handler = (_, e) => h(s, e); + subscribe(s, handler); + return () => unsubscribe(s, handler); + }); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Utilities/WeakEvents.cs b/src/Avalonia.Base/Utilities/WeakEvents.cs new file mode 100644 index 0000000000..d1b5e7f12d --- /dev/null +++ b/src/Avalonia.Base/Utilities/WeakEvents.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Windows.Input; + +namespace Avalonia.Utilities; + +public class WeakEvents +{ + /// + /// Represents CollectionChanged event from + /// + public static readonly WeakEvent + CollectionChanged = WeakEvent.Register( + (c, s) => + { + NotifyCollectionChangedEventHandler handler = (_, e) => s(c, e); + c.CollectionChanged += handler; + return () => c.CollectionChanged -= handler; + }); + + /// + /// Represents PropertyChanged event from + /// + public static readonly WeakEvent + PropertyChanged = WeakEvent.Register( + (s, h) => + { + PropertyChangedEventHandler handler = (_, e) => h(s, e); + s.PropertyChanged += handler; + return () => s.PropertyChanged -= handler; + }); + + /// + /// Represents CanExecuteChanged event from + /// + public static readonly WeakEvent CommandCanExecuteChanged = + WeakEvent.Register((s, h) => s.CanExecuteChanged += h, + (s, h) => s.CanExecuteChanged -= h); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Utilities/WeakObservable.cs b/src/Avalonia.Base/Utilities/WeakObservable.cs index 52edc7ad1a..6bf1d4082f 100644 --- a/src/Avalonia.Base/Utilities/WeakObservable.cs +++ b/src/Avalonia.Base/Utilities/WeakObservable.cs @@ -18,6 +18,7 @@ namespace Avalonia.Utilities /// Object instance that exposes the event to convert. /// Name of the event to convert. /// + [Obsolete("Use WeakEvent-based overload")] public static IObservable> FromEventPattern( TTarget target, string eventName) @@ -34,7 +35,9 @@ namespace Avalonia.Utilities }).Publish().RefCount(); } - private class Handler : IWeakSubscriber where TEventArgs : EventArgs + private class Handler + : IWeakSubscriber, + IWeakEventSubscriber where TEventArgs : EventArgs { private IObserver> _observer; @@ -47,6 +50,36 @@ namespace Avalonia.Utilities { _observer.OnNext(new EventPattern(sender, e)); } + + public void OnEvent(object? sender, WeakEvent ev, TEventArgs e) + { + _observer.OnNext(new EventPattern(sender, e)); + } } + + /// + /// Converts a WeakEvent conforming to the standard .NET event pattern into an observable + /// sequence, subscribing weakly. + /// + /// The type of target. + /// The type of the event args. + /// Object instance that exposes the event to convert. + /// The weak event to convert. + /// + public static IObservable> FromEventPattern( + TTarget target, WeakEvent ev) + where TEventArgs : EventArgs where TTarget : class + { + _ = target ?? throw new ArgumentNullException(nameof(target)); + _ = ev ?? throw new ArgumentNullException(nameof(ev)); + + return Observable.Create>(observer => + { + var handler = new Handler(observer); + ev.Subscribe(target, handler); + return () => ev.Unsubscribe(target, handler); + }).Publish().RefCount(); + } + } } diff --git a/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs b/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs index 88b1e3c807..dc9e86cc32 100644 --- a/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs +++ b/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs @@ -19,6 +19,7 @@ namespace Avalonia.Utilities /// The event source. /// The name of the event. /// The subscriber. + [Obsolete("Use WeakEvent")] public static void Subscribe(TTarget target, string eventName, IWeakSubscriber subscriber) where TEventArgs : EventArgs { diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index 2ceaeb6dba..4d048f0fb0 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -33,7 +33,7 @@ namespace Avalonia.Controls } - class CanExecuteChangedSubscriber : IWeakSubscriber + class CanExecuteChangedSubscriber : IWeakEventSubscriber { private readonly NativeMenuItem _parent; @@ -42,7 +42,7 @@ namespace Avalonia.Controls _parent = parent; } - public void OnEvent(object sender, EventArgs e) + public void OnEvent(object? sender, WeakEvent ev, EventArgs e) { _parent.CanExecuteChanged(); } @@ -160,14 +160,12 @@ namespace Avalonia.Controls set { if (_command != null) - WeakSubscriptionManager.Unsubscribe(_command, - nameof(ICommand.CanExecuteChanged), _canExecuteChangedSubscriber); + WeakEvents.CommandCanExecuteChanged.Unsubscribe(_command, _canExecuteChangedSubscriber); SetAndRaise(CommandProperty, ref _command, value); if (_command != null) - WeakSubscriptionManager.Subscribe(_command, - nameof(ICommand.CanExecuteChanged), _canExecuteChangedSubscriber); + WeakEvents.CommandCanExecuteChanged.Subscribe(_command, _canExecuteChangedSubscriber); CanExecuteChanged(); } diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index ecc0fa3a48..b40cf26df5 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -20,7 +20,7 @@ namespace Avalonia.Controls /// Represents a data-driven collection control that incorporates a flexible layout system, /// custom views, and virtualization. /// - public class ItemsRepeater : Panel, IChildIndexProvider + public class ItemsRepeater : Panel, IChildIndexProvider, IWeakEventSubscriber { /// /// Defines the property. @@ -723,14 +723,8 @@ namespace Avalonia.Controls { oldValue.UninitializeForContext(LayoutContext); - WeakEventHandlerManager.Unsubscribe( - oldValue, - nameof(AttachedLayout.MeasureInvalidated), - InvalidateMeasureForLayout); - WeakEventHandlerManager.Unsubscribe( - oldValue, - nameof(AttachedLayout.ArrangeInvalidated), - InvalidateArrangeForLayout); + AttachedLayout.MeasureInvalidatedWeakEvent.Unsubscribe(oldValue, this); + AttachedLayout.ArrangeInvalidatedWeakEvent.Unsubscribe(oldValue, this); // Walk through all the elements and make sure they are cleared foreach (var element in Children) @@ -748,14 +742,8 @@ namespace Avalonia.Controls { newValue.InitializeForContext(LayoutContext); - WeakEventHandlerManager.Subscribe( - newValue, - nameof(AttachedLayout.MeasureInvalidated), - InvalidateMeasureForLayout); - WeakEventHandlerManager.Subscribe( - newValue, - nameof(AttachedLayout.ArrangeInvalidated), - InvalidateArrangeForLayout); + AttachedLayout.MeasureInvalidatedWeakEvent.Subscribe(newValue, this); + AttachedLayout.ArrangeInvalidatedWeakEvent.Subscribe(newValue, this); } bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout; @@ -806,9 +794,13 @@ namespace Avalonia.Controls _viewportManager.OnBringIntoViewRequested(e); } - private void InvalidateMeasureForLayout(object sender, EventArgs e) => InvalidateMeasure(); - - private void InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateArrange(); + void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, EventArgs e) + { + if(ev == AttachedLayout.ArrangeInvalidatedWeakEvent) + InvalidateArrange(); + else if (ev == AttachedLayout.MeasureInvalidatedWeakEvent) + InvalidateMeasure(); + } private VirtualizingLayoutContext GetLayoutContext() { diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 5d9a0c8eed..eaee5bdb50 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -34,7 +34,7 @@ namespace Avalonia.Controls IStyleHost, ILogicalRoot, ITextInputMethodRoot, - IWeakSubscriber + IWeakEventSubscriber { /// /// Defines the property. @@ -74,6 +74,12 @@ namespace Avalonia.Controls public static readonly StyledProperty TransparencyBackgroundFallbackProperty = AvaloniaProperty.Register(nameof(TransparencyBackgroundFallback), Brushes.White); + private static readonly WeakEvent + ResourcesChangedWeakEvent = WeakEvent.Register( + (s, h) => s.ResourcesChanged += h, + (s, h) => s.ResourcesChanged -= h + ); + private readonly IInputManager _inputManager; private readonly IAccessKeyHandler _accessKeyHandler; private readonly IKeyboardNavigationHandler _keyboardNavigationHandler; @@ -178,10 +184,7 @@ namespace Avalonia.Controls if (((IStyleHost)this).StylingParent is IResourceHost applicationResources) { - WeakSubscriptionManager.Subscribe( - applicationResources, - nameof(IResourceHost.ResourcesChanged), - this); + ResourcesChangedWeakEvent.Subscribe(applicationResources, this); } impl.LostFocus += PlatformImpl_LostFocus; @@ -286,7 +289,7 @@ namespace Avalonia.Controls /// IMouseDevice IInputRoot.MouseDevice => PlatformImpl?.MouseDevice; - void IWeakSubscriber.OnEvent(object sender, ResourcesChangedEventArgs e) + void IWeakEventSubscriber.OnEvent(object sender, WeakEvent ev, ResourcesChangedEventArgs e) { ((ILogical)this).NotifyResourcesChanged(e); } diff --git a/src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs b/src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs index 1a190391b7..74705a0262 100644 --- a/src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs +++ b/src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs @@ -83,7 +83,7 @@ namespace Avalonia.Controls.Utils "Collection listener not registered for this collection/listener combination."); } - private class Entry : IWeakSubscriber, IDisposable + private class Entry : IWeakEventSubscriber, IDisposable { private INotifyCollectionChanged _collection; @@ -91,23 +91,18 @@ namespace Avalonia.Controls.Utils { _collection = collection; Listeners = new List>(); - WeakSubscriptionManager.Subscribe( - _collection, - nameof(INotifyCollectionChanged.CollectionChanged), - this); + WeakEvents.CollectionChanged.Subscribe(_collection, this); } public List> Listeners { get; } public void Dispose() { - WeakSubscriptionManager.Unsubscribe( - _collection, - nameof(INotifyCollectionChanged.CollectionChanged), - this); + WeakEvents.CollectionChanged.Unsubscribe(_collection, this); } - void IWeakSubscriber.OnEvent(object? sender, NotifyCollectionChangedEventArgs e) + void IWeakEventSubscriber. + OnEvent(object? notifyCollectionChanged, WeakEvent ev, NotifyCollectionChangedEventArgs e) { static void Notify( INotifyCollectionChanged incc, diff --git a/src/Avalonia.Layout/AttachedLayout.cs b/src/Avalonia.Layout/AttachedLayout.cs index 6c884641f8..ece8bbe805 100644 --- a/src/Avalonia.Layout/AttachedLayout.cs +++ b/src/Avalonia.Layout/AttachedLayout.cs @@ -4,6 +4,7 @@ // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; +using Avalonia.Utilities; namespace Avalonia.Layout { @@ -19,10 +20,26 @@ namespace Avalonia.Layout /// public event EventHandler? MeasureInvalidated; + /// + /// Occurs when the measurement state (layout) has been invalidated. + /// + public static readonly WeakEvent MeasureInvalidatedWeakEvent = + WeakEvent.Register( + (s, h) => s.MeasureInvalidated += h, + (s, h) => s.MeasureInvalidated -= h); + /// /// Occurs when the arrange state (layout) has been invalidated. /// public event EventHandler? ArrangeInvalidated; + + /// + /// Occurs when the arrange state (layout) has been invalidated. + /// + public static readonly WeakEvent ArrangeInvalidatedWeakEvent = + WeakEvent.Register( + (s, h) => s.ArrangeInvalidated += h, + (s, h) => s.ArrangeInvalidated -= h); /// /// Initializes any per-container state the layout requires when it is attached to an diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs index b3f78bfbe3..c21a2d4299 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs @@ -72,7 +72,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings } } - internal class InpcPropertyAccessor : PropertyAccessorBase + internal class InpcPropertyAccessor : PropertyAccessorBase, IWeakEventSubscriber { protected readonly WeakReference _reference; private readonly IPropertyInfo _property; @@ -110,7 +110,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings return false; } - void OnNotifyPropertyChanged(object sender, PropertyChangedEventArgs e) + public void OnEvent(object sender, WeakEvent ev, PropertyChangedEventArgs e) { if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName)) { @@ -128,10 +128,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { if (_reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc) { - WeakEventHandlerManager.Unsubscribe( - inpc, - nameof(INotifyPropertyChanged.PropertyChanged), - OnNotifyPropertyChanged); + WeakEvents.PropertyChanged.Unsubscribe(inpc, this); } } @@ -148,16 +145,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings private void SubscribeToChanges() { if (_reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc) - { - WeakEventHandlerManager.Subscribe( - inpc, - nameof(INotifyPropertyChanged.PropertyChanged), - OnNotifyPropertyChanged); - } + WeakEvents.PropertyChanged.Subscribe(inpc, this); } } - internal class IndexerAccessor : InpcPropertyAccessor + internal class IndexerAccessor : InpcPropertyAccessor, IWeakEventSubscriber { private int _index; @@ -172,27 +164,17 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { base.SubscribeCore(); if (_reference.TryGetTarget(out var o) && o is INotifyCollectionChanged incc) - { - WeakEventHandlerManager.Subscribe( - incc, - nameof(INotifyCollectionChanged.CollectionChanged), - OnNotifyCollectionChanged); - } + WeakEvents.CollectionChanged.Subscribe(incc, this); } protected override void UnsubscribeCore() { base.UnsubscribeCore(); if (_reference.TryGetTarget(out var o) && o is INotifyCollectionChanged incc) - { - WeakEventHandlerManager.Unsubscribe( - incc, - nameof(INotifyCollectionChanged.CollectionChanged), - OnNotifyCollectionChanged); - } + WeakEvents.CollectionChanged.Unsubscribe(incc, this); } - - void OnNotifyCollectionChanged(object sender, NotifyCollectionChangedEventArgs args) + + public void OnEvent(object? sender, WeakEvent ev, NotifyCollectionChangedEventArgs args) { if (ShouldNotifyListeners(args)) { diff --git a/tests/Avalonia.Base.UnitTests/WeakEventTests.cs b/tests/Avalonia.Base.UnitTests/WeakEventTests.cs new file mode 100644 index 0000000000..81009f7c5f --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/WeakEventTests.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Utilities; +using Xunit; + +namespace Avalonia.Base.UnitTests +{ + public class WeakEventManagerTests + { + class EventSource + { + public event EventHandler Event; + + public void Fire() + { + Event?.Invoke(this, new EventArgs()); + } + + public static readonly WeakEvent WeakEvent = WeakEvent.Register( + (t, s) => t.Event += s, + (t, s) => t.Event -= s); + } + + class Subscriber : IWeakEventSubscriber + { + private readonly Action _onEvent; + + public Subscriber(Action onEvent) + { + _onEvent = onEvent; + } + + public void OnEvent(object sender, WeakEvent ev, EventArgs args) + { + _onEvent?.Invoke(); + } + } + + [Fact] + public void EventShouldBePassedToSubscriber() + { + bool handled = false; + var subscriber = new Subscriber(() => handled = true); + var source = new EventSource(); + EventSource.WeakEvent.Subscribe(source, subscriber); + + source.Fire(); + Assert.True(handled); + } + + + [Fact] + public void EventHandlerShouldNotBeKeptAlive() + { + bool handled = false; + var source = new EventSource(); + AddSubscriber(source, () => handled = true); + for (int c = 0; c < 10; c++) + { + GC.Collect(); + GC.Collect(3, GCCollectionMode.Forced, true); + } + source.Fire(); + Assert.False(handled); + } + + private void AddSubscriber(EventSource source, Action func) + { + EventSource.WeakEvent.Subscribe(source, new Subscriber(func)); + } + } +} From c2d627360cc256e503c2d78810dffd380af4b781 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Mon, 3 Jan 2022 19:15:45 +0300 Subject: [PATCH 08/44] [MacOS] fix quit menu item copy paste mistake --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index bda1c91750..5015c92e46 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -131,7 +131,7 @@ namespace Avalonia.Native }; quitItem.Click += (sender, args) => { - _applicationCommands.ShowAll(); + (Application.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.Shutdown(); }; result.Add(quitItem); } From 1221356df31623fd2cb036c8194ef18ee780012f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 3 Jan 2022 19:20:44 +0300 Subject: [PATCH 09/44] apicompat --- src/Avalonia.Controls/ApiCompatBaseline.txt | 7 ++++++- src/Avalonia.Dialogs/ApiCompatBaseline.txt | 3 +++ tests/Avalonia.Base.UnitTests/WeakEventTests.cs | 10 +++++----- 3 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 src/Avalonia.Dialogs/ApiCompatBaseline.txt diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index dd41c30e85..9b7d37e108 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -29,15 +29,20 @@ MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDownValueChang MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.NewValue.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.OldValue.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.ScrollViewer.AllowAutoHideProperty' does not exist in the implementation but it does exist in the contract. +CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.TopLevel' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Viewbox.StretchProperty' does not exist in the implementation but it does exist in the contract. +CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Window' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. +CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.WindowBase' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.EventHandler Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.ShutdownRequested' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.add_ShutdownRequested(System.EventHandler)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.remove_ShutdownRequested(System.EventHandler)' is present in the implementation but not in the contract. +CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Embedding.EmbeddableControlRoot' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. MembersMustExist : Member 'public System.Action Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.set(System.Action)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.Platform.ITopLevelNativeMenuExporter.SetNativeMenu(Avalonia.Controls.NativeMenu)' is present in the contract but not in the implementation. +CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Primitives.PopupRoot' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract. @@ -57,4 +62,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract. -Total Issues: 58 +Total Issues: 63 diff --git a/src/Avalonia.Dialogs/ApiCompatBaseline.txt b/src/Avalonia.Dialogs/ApiCompatBaseline.txt new file mode 100644 index 0000000000..9cb1b47015 --- /dev/null +++ b/src/Avalonia.Dialogs/ApiCompatBaseline.txt @@ -0,0 +1,3 @@ +Compat issues with assembly Avalonia.Dialogs: +CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Dialogs.AboutAvaloniaDialog' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. +Total Issues: 1 diff --git a/tests/Avalonia.Base.UnitTests/WeakEventTests.cs b/tests/Avalonia.Base.UnitTests/WeakEventTests.cs index 81009f7c5f..2663b4858f 100644 --- a/tests/Avalonia.Base.UnitTests/WeakEventTests.cs +++ b/tests/Avalonia.Base.UnitTests/WeakEventTests.cs @@ -8,18 +8,18 @@ using Xunit; namespace Avalonia.Base.UnitTests { - public class WeakEventManagerTests + public class WeakEventTests { class EventSource { - public event EventHandler Event; + public event EventHandler Event; public void Fire() { Event?.Invoke(this, new EventArgs()); } - public static readonly WeakEvent WeakEvent = WeakEvent.Register( + public static readonly WeakEvent WeakEv = WeakEvent.Register( (t, s) => t.Event += s, (t, s) => t.Event -= s); } @@ -45,7 +45,7 @@ namespace Avalonia.Base.UnitTests bool handled = false; var subscriber = new Subscriber(() => handled = true); var source = new EventSource(); - EventSource.WeakEvent.Subscribe(source, subscriber); + EventSource.WeakEv.Subscribe(source, subscriber); source.Fire(); Assert.True(handled); @@ -69,7 +69,7 @@ namespace Avalonia.Base.UnitTests private void AddSubscriber(EventSource source, Action func) { - EventSource.WeakEvent.Subscribe(source, new Subscriber(func)); + EventSource.WeakEv.Subscribe(source, new Subscriber(func)); } } } From 059e367cbfa9026dcfdf8f239d28cbce44cf1ed0 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 3 Jan 2022 19:24:31 +0300 Subject: [PATCH 10/44] Schedule weak event list Compact to be called later with Background dispatcher priority --- src/Avalonia.Base/Utilities/WeakEvent.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index 430bc92838..c353e11263 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; +using Avalonia.Threading; namespace Avalonia.Utilities; @@ -53,6 +54,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event new WeakReference>[16]; private int _count; private readonly Action _unsubscribe; + private bool _compactScheduled; public Subscription(WeakEvent ev, TSender target) { @@ -99,12 +101,21 @@ public class WeakEvent : WeakEvent where TEventArgs : Event if (removed) { - Compact(); + ScheduleCompact(); } } + void ScheduleCompact() + { + if(_compactScheduled) + return; + _compactScheduled = true; + Dispatcher.UIThread.Post(Compact, DispatcherPriority.Background); + } + void Compact() { + _compactScheduled = false; int empty = -1; for (var c = 0; c < _count; c++) { @@ -140,7 +151,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event } if (needCompact) - Compact(); + ScheduleCompact(); } } From f010322a066b570eb1d979c4b1013934575149ed Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 3 Jan 2022 19:50:22 +0300 Subject: [PATCH 11/44] Force compacting WeakEvent subscriber list in before assertions in tests --- .../ExpressionObserverTests_DataValidation.cs | 3 +++ .../Core/ExpressionObserverTests_Indexer.cs | 12 +++++++++ .../ExpressionObserverTests_Observable.cs | 11 +++++++- .../Core/ExpressionObserverTests_Property.cs | 27 +++++++++++++++++-- .../Plugins/IndeiValidationPluginTests.cs | 3 +++ 5 files changed, 53 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs index ecc43aa3a5..43192584af 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs @@ -6,6 +6,7 @@ using System.Reactive.Linq; using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.Markup.Parsers; +using Avalonia.Threading; using Avalonia.UnitTests; using Xunit; @@ -67,6 +68,8 @@ namespace Avalonia.Base.UnitTests.Data.Core Assert.Equal(1, data.ErrorsChangedSubscriptionCount); sub.Dispose(); + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(0, data.ErrorsChangedSubscriptionCount); } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs index 6289ec46c7..20a4cb6d98 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs @@ -9,6 +9,7 @@ using Avalonia.Data.Core; using Avalonia.UnitTests; using Xunit; using Avalonia.Markup.Parsers; +using Avalonia.Threading; namespace Avalonia.Base.UnitTests.Data.Core { @@ -110,6 +111,9 @@ namespace Avalonia.Base.UnitTests.Data.Core data.Foo.Add("baz"); } + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "baz" }, result); Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); @@ -127,6 +131,8 @@ namespace Avalonia.Base.UnitTests.Data.Core { data.Foo.RemoveAt(0); } + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(new[] { "foo", "bar" }, result); Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); @@ -145,6 +151,9 @@ namespace Avalonia.Base.UnitTests.Data.Core { data.Foo[1] = "baz"; } + + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(new[] { "bar", "baz" }, result); Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); @@ -202,6 +211,9 @@ namespace Avalonia.Base.UnitTests.Data.Core data.Foo["foo"] = "bar2"; } + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + var expected = new[] { "bar", "bar2" }; Assert.Equal(expected, result); Assert.Equal(0, data.Foo.PropertyChangedSubscriptionCount); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs index a70d4574a6..4f88d2de7c 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs @@ -5,6 +5,7 @@ using System.Reactive.Subjects; using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.Markup.Parsers; +using Avalonia.Threading; using Avalonia.UnitTests; using Xunit; @@ -68,6 +69,8 @@ namespace Avalonia.Base.UnitTests.Data.Core Assert.Equal(new[] { "foo" }, result); sub.Dispose(); + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(0, data.PropertyChangedSubscriptionCount); GC.KeepAlive(data); @@ -109,10 +112,16 @@ namespace Avalonia.Base.UnitTests.Data.Core var sub = target.Subscribe(x => result.Add(x)); data1.Next.OnNext(data2); sync.ExecutePostedCallbacks(); - + + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(new[] { new BindingNotification("foo") }, result); sub.Dispose(); + + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(0, data1.PropertyChangedSubscriptionCount); GC.KeepAlive(data1); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs index 32cdd21e04..a9c62a3c4a 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs @@ -10,6 +10,7 @@ using Avalonia.UnitTests; using Xunit; using System.Threading.Tasks; using Avalonia.Markup.Parsers; +using Avalonia.Threading; namespace Avalonia.Base.UnitTests.Data.Core { @@ -182,6 +183,9 @@ namespace Avalonia.Base.UnitTests.Data.Core sub.Dispose(); + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(0, data.PropertyChangedSubscriptionCount); GC.KeepAlive(data); @@ -209,8 +213,11 @@ namespace Avalonia.Base.UnitTests.Data.Core data.RaisePropertyChanged(null); Assert.Equal(new[] { "foo", "bar", "bar" }, result); - + sub.Dispose(); + + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(0, data.PropertyChangedSubscriptionCount); @@ -231,7 +238,9 @@ namespace Avalonia.Base.UnitTests.Data.Core Assert.Equal(new[] { "bar", "baz", null }, result); sub.Dispose(); - + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(0, data.PropertyChangedSubscriptionCount); Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount); @@ -253,6 +262,9 @@ namespace Avalonia.Base.UnitTests.Data.Core Assert.Equal(new[] { "bar", "baz", null }, result); sub.Dispose(); + + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(0, data.PropertyChangedSubscriptionCount); Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount); @@ -297,6 +309,9 @@ namespace Avalonia.Base.UnitTests.Data.Core sub.Dispose(); + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(0, data.PropertyChangedSubscriptionCount); Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount); Assert.Equal(0, old.PropertyChangedSubscriptionCount); @@ -329,6 +344,9 @@ namespace Avalonia.Base.UnitTests.Data.Core result); sub.Dispose(); + + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(0, data.PropertyChangedSubscriptionCount); Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount); @@ -412,6 +430,9 @@ namespace Avalonia.Base.UnitTests.Data.Core sub1.Dispose(); sub2.Dispose(); + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(0, data.PropertyChangedSubscriptionCount); GC.KeepAlive(data); @@ -535,6 +556,8 @@ namespace Avalonia.Base.UnitTests.Data.Core }, result); + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(0, first.PropertyChangedSubscriptionCount); Assert.Equal(0, second.PropertyChangedSubscriptionCount); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs index d8eddf6330..e8f1f38b90 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using Avalonia.Data; using Avalonia.Data.Core.Plugins; +using Avalonia.Threading; using Xunit; namespace Avalonia.Base.UnitTests.Data.Core.Plugins @@ -57,6 +58,8 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins validator.Subscribe(_ => { }); Assert.Equal(1, data.ErrorsChangedSubscriptionCount); validator.Unsubscribe(); + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(0, data.ErrorsChangedSubscriptionCount); } From b6219db4ad844f7e461fbcabcfa07d00d39c0d92 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 3 Jan 2022 20:44:37 +0300 Subject: [PATCH 12/44] More Dispatcher.UIThread.RunJobs in tests --- .../Data/BindingTests.cs | 7 +++++++ .../ExpressionObserverBuilderTests_Indexer.cs | 15 ++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index a0dd565a87..055de999e2 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -12,6 +12,7 @@ using System.Runtime.CompilerServices; using Avalonia.UnitTests; using Avalonia.Data.Converters; using Avalonia.Data.Core; +using Avalonia.Threading; namespace Avalonia.Markup.UnitTests.Data { @@ -160,6 +161,9 @@ namespace Avalonia.Markup.UnitTests.Data target.Bind(TextBlock.TextProperty, new Binding("Foo", BindingMode.OneTime)); target.DataContext = source; + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(0, source.SubscriberCount); } @@ -608,6 +612,9 @@ namespace Avalonia.Markup.UnitTests.Data root.DataContext = source; } + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(0, source.SubscriberCount); } diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs index 39d6152b69..dbf6ef2ce9 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs @@ -10,6 +10,7 @@ using System.Collections.ObjectModel; using System.Reactive.Linq; using System.Text; using System.Threading.Tasks; +using Avalonia.Threading; using Xunit; namespace Avalonia.Markup.UnitTests.Parsers @@ -159,7 +160,10 @@ namespace Avalonia.Markup.UnitTests.Parsers { data.Foo.Add("baz"); } - + + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "baz" }, result); Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); @@ -178,6 +182,9 @@ namespace Avalonia.Markup.UnitTests.Parsers data.Foo.RemoveAt(0); } + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(new[] { "foo", "bar" }, result); Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); @@ -196,6 +203,9 @@ namespace Avalonia.Markup.UnitTests.Parsers data.Foo[1] = "baz"; } + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(new[] { "bar", "baz" }, result); Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); @@ -252,6 +262,9 @@ namespace Avalonia.Markup.UnitTests.Parsers data.Foo["foo"] = "bar2"; } + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + var expected = new[] { "bar", "bar2" }; Assert.Equal(expected, result); Assert.Equal(0, data.Foo.PropertyChangedSubscriptionCount); From e516fe2e6043cce99926981dfdfc52fe09379878 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 3 Jan 2022 20:45:55 +0300 Subject: [PATCH 13/44] More Dispatcher.UIThread.RunJobjs() in tests --- tests/Avalonia.LeakTests/AvaloniaObjectTests.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.LeakTests/AvaloniaObjectTests.cs b/tests/Avalonia.LeakTests/AvaloniaObjectTests.cs index 54f9a87f94..19208b15f3 100644 --- a/tests/Avalonia.LeakTests/AvaloniaObjectTests.cs +++ b/tests/Avalonia.LeakTests/AvaloniaObjectTests.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Subjects; +using Avalonia.Threading; using JetBrains.dotMemoryUnit; using Xunit; using Xunit.Abstractions; @@ -56,7 +57,9 @@ namespace Avalonia.LeakTests completeSource(); GC.Collect(); - + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + GC.Collect(); Assert.False(weakSource.IsAlive); } From a175fbbe3ad2ad82be446925760baf12b9c65ce7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 3 Jan 2022 19:06:39 +0100 Subject: [PATCH 14/44] Check for null visual parent. (#7298) It shouldn't be possible to come across a null visual parent here because the visual should be attached to the visual tree, but according to #6930, it is. Because we don't have a repro here, defensively return a null value instead of throwing if we hit this case. --- src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index cb916293ac..63c22efc3f 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -126,7 +126,12 @@ namespace Avalonia.Rendering.SceneGraph while (node == null && visual.IsVisible) { - visual = visual.VisualParent!; + var parent = visual.VisualParent; + + if (parent is null) + return null; + + visual = parent; node = scene.FindNode(visual); } From 32bf6160dfcb3c0a0550e627a51325b2f935625d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 3 Jan 2022 18:23:40 +0000 Subject: [PATCH 15/44] use IControlledApplicationLifetime --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 5015c92e46..57a8701202 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -131,7 +131,7 @@ namespace Avalonia.Native }; quitItem.Click += (sender, args) => { - (Application.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.Shutdown(); + (Application.Current.ApplicationLifetime as IControlledApplicationLifetime)?.Shutdown(); }; result.Add(quitItem); } From dfd0523e36e3f5227305a6da73cd61d6a0943b93 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 3 Jan 2022 22:12:51 +0300 Subject: [PATCH 16/44] Optimize Pen's subscriptions to IAffects render --- src/Avalonia.Visuals/ApiCompatBaseline.txt | 5 +- src/Avalonia.Visuals/Media/Pen.cs | 110 ++++++++------------- 2 files changed, 46 insertions(+), 69 deletions(-) diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt index dcb3246a63..68e3673cfe 100644 --- a/src/Avalonia.Visuals/ApiCompatBaseline.txt +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -7,6 +7,9 @@ InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Task MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.PageSlide.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.GlyphRun..ctor()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.GlyphRun.GlyphTypeface.set(Avalonia.Media.GlyphTypeface)' does not exist in the implementation but it does exist in the contract. +CannotSealType : Type 'Avalonia.Media.Pen' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +MembersMustExist : Member 'protected void Avalonia.Media.Pen.AffectsRender(Avalonia.AvaloniaProperty[])' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Media.Pen.RaiseInvalidated(System.EventArgs)' does not exist in the implementation but it does exist in the contract. TypeCannotChangeClassification : Type 'Avalonia.Media.Immutable.ImmutableSolidColorBrush' is a 'class' in the implementation but is a 'struct' in the contract. MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract. CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' is abstract in the implementation but is missing in the contract. @@ -83,4 +86,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Size Avaloni InterfacesShouldHaveSameMembers : Interface member 'public System.TimeSpan Avalonia.Platform.IPlatformSettings.TouchDoubleClickTime' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Size Avalonia.Platform.IPlatformSettings.TouchDoubleClickSize.get()' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.TimeSpan Avalonia.Platform.IPlatformSettings.TouchDoubleClickTime.get()' is present in the implementation but not in the contract. -Total Issues: 84 +Total Issues: 87 diff --git a/src/Avalonia.Visuals/Media/Pen.cs b/src/Avalonia.Visuals/Media/Pen.cs index 7c966a35cf..f4ae58eff3 100644 --- a/src/Avalonia.Visuals/Media/Pen.cs +++ b/src/Avalonia.Visuals/Media/Pen.cs @@ -7,7 +7,7 @@ namespace Avalonia.Media /// /// Describes how a stroke is drawn. /// - public class Pen : AvaloniaObject, IPen + public sealed class Pen : AvaloniaObject, IPen, IWeakEventSubscriber { /// /// Defines the property. @@ -45,6 +45,9 @@ namespace Avalonia.Media public static readonly StyledProperty MiterLimitProperty = AvaloniaProperty.Register(nameof(MiterLimit), 10.0); + private EventHandler? _invalidated; + private IAffectsRender? _subscribedTo; + /// /// Initializes a new instance of the class. /// @@ -96,17 +99,6 @@ namespace Avalonia.Media DashStyle = dashStyle; } - static Pen() - { - AffectsRender( - BrushProperty, - ThicknessProperty, - DashStyleProperty, - LineCapProperty, - LineJoinProperty, - MiterLimitProperty); - } - /// /// Gets or sets the brush used to draw the stroke. /// @@ -116,6 +108,11 @@ namespace Avalonia.Media set => SetValue(BrushProperty, value); } + private static readonly WeakEvent InvalidatedWeakEvent = + WeakEvent.Register( + (s, h) => s.Invalidated += h, + (s, h) => s.Invalidated -= h); + /// /// Gets or sets the stroke thickness. /// @@ -165,7 +162,19 @@ namespace Avalonia.Media /// /// Raised when the pen changes. /// - public event EventHandler? Invalidated; + public event EventHandler? Invalidated + { + add + { + _invalidated += value; + UpdateBrushSubscription(); + } + remove + { + _invalidated -= value; + UpdateBrushSubscription(); + } + } /// /// Creates an immutable clone of the brush. @@ -182,68 +191,33 @@ namespace Avalonia.Media MiterLimit); } - /// - /// Marks a property as affecting the pen's visual representation. - /// - /// The properties. - /// - /// After a call to this method in a pen's static constructor, any change to the - /// property will cause the event to be raised on the pen. - /// - protected static void AffectsRender(params AvaloniaProperty[] properties) - where T : Pen + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - static void Invalidate(AvaloniaPropertyChangedEventArgs e) - { - if (e.Sender is T sender) - { - sender.RaiseInvalidated(EventArgs.Empty); - } - } + _invalidated?.Invoke(this, EventArgs.Empty); + if(change.Property == BrushProperty) + UpdateBrushSubscription(); + base.OnPropertyChanged(change); + } - static void InvalidateAndSubscribe(AvaloniaPropertyChangedEventArgs e) + void UpdateBrushSubscription() + { + if ((_invalidated == null || _subscribedTo != Brush) && _subscribedTo != null) { - if (e.Sender is T sender) - { - if (e.OldValue is IAffectsRender oldValue) - { - WeakEventHandlerManager.Unsubscribe( - oldValue, - nameof(oldValue.Invalidated), - sender.AffectsRenderInvalidated); - } - - if (e.NewValue is IAffectsRender newValue) - { - WeakEventHandlerManager.Subscribe( - newValue, - nameof(newValue.Invalidated), - sender.AffectsRenderInvalidated); - } - - sender.RaiseInvalidated(EventArgs.Empty); - } + InvalidatedWeakEvent.Unsubscribe(_subscribedTo, this); + _subscribedTo = null; } - foreach (var property in properties) + if (_invalidated != null && _subscribedTo != Brush && Brush is IAffectsRender affectsRender) { - if (property.CanValueAffectRender()) - { - property.Changed.Subscribe(e => InvalidateAndSubscribe(e)); - } - else - { - property.Changed.Subscribe(e => Invalidate(e)); - } + InvalidatedWeakEvent.Subscribe(affectsRender, this); + _subscribedTo = affectsRender; } } - - /// - /// Raises the event. - /// - /// The event args. - protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e); - - private void AffectsRenderInvalidated(object? sender, EventArgs e) => RaiseInvalidated(EventArgs.Empty); + + void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, EventArgs e) + { + if (ev == InvalidatedWeakEvent) + _invalidated?.Invoke(this, EventArgs.Empty); + } } } From 7e45ee1e537e5f0b67c23cba159638dfcf5241a7 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 4 Jan 2022 01:19:14 +0300 Subject: [PATCH 17/44] Subscribe to DashStyle too --- src/Avalonia.Visuals/Media/Pen.cs | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Visuals/Media/Pen.cs b/src/Avalonia.Visuals/Media/Pen.cs index f4ae58eff3..65ba851100 100644 --- a/src/Avalonia.Visuals/Media/Pen.cs +++ b/src/Avalonia.Visuals/Media/Pen.cs @@ -46,7 +46,8 @@ namespace Avalonia.Media AvaloniaProperty.Register(nameof(MiterLimit), 10.0); private EventHandler? _invalidated; - private IAffectsRender? _subscribedTo; + private IAffectsRender? _subscribedToBrush; + private IAffectsRender? _subscribedToDashes; /// /// Initializes a new instance of the class. @@ -167,12 +168,12 @@ namespace Avalonia.Media add { _invalidated += value; - UpdateBrushSubscription(); + UpdateSubscriptions(); } remove { _invalidated -= value; - UpdateBrushSubscription(); + UpdateSubscriptions(); } } @@ -195,24 +196,33 @@ namespace Avalonia.Media { _invalidated?.Invoke(this, EventArgs.Empty); if(change.Property == BrushProperty) - UpdateBrushSubscription(); + UpdateSubscription(ref _subscribedToBrush, Brush); + if(change.Property == DashStyleProperty) + UpdateSubscription(ref _subscribedToDashes, DashStyle); base.OnPropertyChanged(change); } - void UpdateBrushSubscription() + + void UpdateSubscription(ref IAffectsRender? field, object? value) { - if ((_invalidated == null || _subscribedTo != Brush) && _subscribedTo != null) + if ((_invalidated == null || field != value) && field != null) { - InvalidatedWeakEvent.Unsubscribe(_subscribedTo, this); - _subscribedTo = null; + InvalidatedWeakEvent.Unsubscribe(field, this); + field = null; } - if (_invalidated != null && _subscribedTo != Brush && Brush is IAffectsRender affectsRender) + if (_invalidated != null && field != value && value is IAffectsRender affectsRender) { InvalidatedWeakEvent.Subscribe(affectsRender, this); - _subscribedTo = affectsRender; + field = affectsRender; } } + + void UpdateSubscriptions() + { + UpdateSubscription(ref _subscribedToBrush, Brush); + UpdateSubscription(ref _subscribedToDashes, DashStyle); + } void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, EventArgs e) { From a08c4888e0e2e1112275d228b42a0a3899e2a5f2 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 4 Jan 2022 18:01:32 +0300 Subject: [PATCH 18/44] WeakEvent: cache Compact() delegate --- src/Avalonia.Base/Utilities/WeakEvent.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index c353e11263..0b32015a8a 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -49,6 +49,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event { private readonly WeakEvent _ev; private readonly TSender _target; + private readonly Action _compact; private WeakReference>?[] _data = new WeakReference>[16]; @@ -60,7 +61,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event { _ev = ev; _target = target; - + _compact = Compact; _unsubscribe = ev._subscribe(target, OnEvent); } @@ -110,7 +111,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event if(_compactScheduled) return; _compactScheduled = true; - Dispatcher.UIThread.Post(Compact, DispatcherPriority.Background); + Dispatcher.UIThread.Post(_compact, DispatcherPriority.Background); } void Compact() From c73a6c86c0d0fba2b969a81dd9459ce0d0d02125 Mon Sep 17 00:00:00 2001 From: odalet Date: Tue, 4 Jan 2022 17:35:59 +0100 Subject: [PATCH 19/44] Fixes #7309 - Each time we retrieve a null *PlatformOptions from AvaloniaLocator, return a default instance --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 8 ++++---- src/Avalonia.Native/AvaloniaNativePlatform.cs | 4 ++-- src/Avalonia.X11/Glx/GlxDisplay.cs | 4 ++-- .../LinuxFramebufferPlatform.cs | 4 ++-- src/Windows/Avalonia.Win32/Win32GlManager.cs | 9 ++++----- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 57a8701202..40ffc31728 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -80,8 +80,8 @@ namespace Avalonia.Native }; result.Add(aboutItem); - var macOpts = AvaloniaLocator.Current.GetService(); - if (macOpts == null || !macOpts.DisableDefaultApplicationMenuItems) + var macOpts = AvaloniaLocator.Current.GetService() ?? new MacOSPlatformOptions(); + if (!macOpts.DisableDefaultApplicationMenuItems) { result.Add(new NativeMenuItemSeparator()); @@ -142,9 +142,9 @@ namespace Avalonia.Native private void DoLayoutReset(bool forceUpdate = false) { - var macOpts = AvaloniaLocator.Current.GetService(); + var macOpts = AvaloniaLocator.Current.GetService() ?? new MacOSPlatformOptions(); - if (macOpts != null && macOpts.DisableNativeMenus) + if (macOpts.DisableNativeMenus) { return; } diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 5fa50f0e7f..1eadf70b13 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -96,9 +96,9 @@ namespace Avalonia.Native _factory.Initialize(new GCHandleDeallocator(), applicationPlatform); if (_factory.MacOptions != null) { - var macOpts = AvaloniaLocator.Current.GetService(); + var macOpts = AvaloniaLocator.Current.GetService() ?? new MacOSPlatformOptions(); - _factory.MacOptions.SetShowInDock(macOpts?.ShowInDock != false ? 1 : 0); + _factory.MacOptions.SetShowInDock(macOpts.ShowInDock ? 1 : 0); } AvaloniaLocator.CurrentMutable diff --git a/src/Avalonia.X11/Glx/GlxDisplay.cs b/src/Avalonia.X11/Glx/GlxDisplay.cs index fa8c866c09..fcdc10e999 100644 --- a/src/Avalonia.X11/Glx/GlxDisplay.cs +++ b/src/Avalonia.X11/Glx/GlxDisplay.cs @@ -95,8 +95,8 @@ namespace Avalonia.X11.Glx if (Environment.GetEnvironmentVariable("AVALONIA_GLX_IGNORE_RENDERER_BLACKLIST") != "1") { - var blacklist = AvaloniaLocator.Current.GetService() - ?.GlxRendererBlacklist; + var opts = AvaloniaLocator.Current.GetService() ?? new X11PlatformOptions(); + var blacklist = opts.GlxRendererBlacklist; if (blacklist != null) foreach (var item in blacklist) if (glInterface.Renderer.Contains(item)) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index f4db6bf48a..4add4c423b 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -38,11 +38,11 @@ namespace Avalonia.LinuxFramebuffer if (_fb is IGlOutputBackend gl) AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl.PlatformOpenGlInterface); - var opts = AvaloniaLocator.Current.GetService(); + var opts = AvaloniaLocator.Current.GetService() ?? new LinuxFramebufferPlatformOptions(); AvaloniaLocator.CurrentMutable .Bind().ToConstant(Threading) - .Bind().ToConstant(new DefaultRenderTimer(opts?.Fps ?? 60)) + .Bind().ToConstant(new DefaultRenderTimer(opts.Fps)) .Bind().ToConstant(new RenderLoop()) .Bind().ToTransient() .Bind().ToConstant(new KeyboardDevice()) diff --git a/src/Windows/Avalonia.Win32/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs index 289c100d51..0376a41f8c 100644 --- a/src/Windows/Avalonia.Win32/Win32GlManager.cs +++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs @@ -13,19 +13,18 @@ namespace Avalonia.Win32 { AvaloniaLocator.CurrentMutable.Bind().ToLazy(() => { - var opts = AvaloniaLocator.Current.GetService(); - if (opts?.UseWgl == true) + var opts = AvaloniaLocator.Current.GetService() ?? new Win32PlatformOptions(); + if (opts.UseWgl) { var wgl = WglPlatformOpenGlInterface.TryCreate(); return wgl; } - if (opts?.AllowEglInitialization ?? Win32Platform.WindowsVersion > PlatformConstants.Windows7) + if (opts.AllowEglInitialization ?? Win32Platform.WindowsVersion > PlatformConstants.Windows7) { var egl = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay()); - if (egl != null && - opts?.UseWindowsUIComposition == true) + if (egl != null && opts.UseWindowsUIComposition) { WinUICompositorConnection.TryCreateAndRegister(egl, opts.CompositionBackdropCornerRadius); } From 1604be478470c2dffd1c41055ea854fc89fa6b87 Mon Sep 17 00:00:00 2001 From: Tim U Date: Wed, 5 Jan 2022 08:00:03 +0100 Subject: [PATCH 20/44] fix Validation for SelectedDateProperty not working --- src/Avalonia.Controls/Calendar/CalendarDatePicker.cs | 11 +++++------ .../Controls/CalendarDatePicker.xaml | 6 +++++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs b/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs index a856ee071c..cd9c80d3e0 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs @@ -185,7 +185,8 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterDirect( nameof(SelectedDate), o => o.SelectedDate, - (o, v) => o.SelectedDate = v); + (o, v) => o.SelectedDate = v, + enableDataValidation: true); public static readonly StyledProperty SelectedDateFormatProperty = AvaloniaProperty.Register( @@ -533,13 +534,11 @@ namespace Avalonia.Controls } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) { - base.OnPropertyChanged(change); - - if (change.Property == SelectedDateProperty) + if (property == SelectedDateProperty) { - DataValidationErrors.SetError(this, change.NewValue.Error); + DataValidationErrors.SetError(this, value.Error); } } diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml index 6c4e94caf1..26c3bbc19f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml @@ -33,6 +33,7 @@ + @@ -107,7 +108,6 @@ Padding="{TemplateBinding Padding}" Watermark="{TemplateBinding Watermark}" UseFloatingWatermark="{TemplateBinding UseFloatingWatermark}" - DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" Grid.Column="0"/> @@ -136,8 +136,12 @@ DisplayDateEnd="{TemplateBinding DisplayDateEnd}" /> + + From fe21e298afbff5ed03fa0b08c95dad5e2352dd25 Mon Sep 17 00:00:00 2001 From: Tim U Date: Wed, 5 Jan 2022 08:00:43 +0100 Subject: [PATCH 21/44] Show validation in Demo App --- .../Pages/CalendarDatePickerPage.xaml | 5 +++++ .../ViewModels/MainWindowViewModel.cs | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml b/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml index 3e50bf8a08..2fe16ba8e3 100644 --- a/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml +++ b/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml @@ -1,5 +1,7 @@ A control for selecting dates with a calendar drop-down @@ -39,6 +41,9 @@ + + + diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index 4b3cfa9c9d..2b0c30f311 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Notifications; using Avalonia.Dialogs; using Avalonia.Platform; using System; +using System.ComponentModel.DataAnnotations; using MiniMvvm; namespace ControlCatalog.ViewModels @@ -164,5 +165,17 @@ namespace ControlCatalog.ViewModels public MiniCommand ExitCommand { get; } public MiniCommand ToggleMenuItemCheckedCommand { get; } + + private DateTime? _validatedDateExample; + + /// + /// A required DateTime which should demonstrate validation for the DateTimePicker + /// + [Required] + public DateTime? ValidatedDateExample + { + get => _validatedDateExample; + set => this.RaiseAndSetIfChanged(ref _validatedDateExample, value); + } } } From 88db01532f70f209af9a1cb90f8bd13ad36fc6bf Mon Sep 17 00:00:00 2001 From: Tim U Date: Tue, 4 Jan 2022 16:34:41 +0100 Subject: [PATCH 22/44] Implement CellEditingTemplate --- .../DataGridTemplateColumn.cs | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs index 7e95dd100c..fbdad0a8ad 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs @@ -30,6 +30,20 @@ namespace Avalonia.Controls set { SetAndRaise(CellTemplateProperty, ref _cellTemplate, value); } } + private IDataTemplate _cellEditingCellTemplate; + + public static readonly DirectProperty CellEditingTemplateProperty = + AvaloniaProperty.RegisterDirect( + nameof(CellEditingTemplate), + o => o.CellEditingTemplate, + (o, v) => o.CellEditingTemplate = v); + + public IDataTemplate CellEditingTemplate + { + get => _cellEditingCellTemplate; + set => SetAndRaise(CellEditingTemplateProperty, ref _cellEditingCellTemplate, value); + } + private void OnCellTemplateChanged(AvaloniaPropertyChangedEventArgs e) { var oldValue = (IDataTemplate)e.OldValue; @@ -38,7 +52,7 @@ namespace Avalonia.Controls public DataGridTemplateColumn() { - IsReadOnly = true; + // IsReadOnly = true; } protected override IControl GenerateElement(DataGridCell cell, object dataItem) @@ -60,7 +74,18 @@ namespace Avalonia.Controls protected override IControl GenerateEditingElement(DataGridCell cell, object dataItem, out ICellEditBinding binding) { binding = null; - return GenerateElement(cell, dataItem); + if(CellEditingTemplate != null) + { + return CellEditingTemplate.Build(dataItem); + } + if (Design.IsDesignMode) + { + return null; + } + else + { + throw DataGridError.DataGridTemplateColumn.MissingTemplateForType(typeof(DataGridTemplateColumn)); + } } protected override object PrepareCellForEdit(IControl editingElement, RoutedEventArgs editingEventArgs) @@ -70,7 +95,8 @@ namespace Avalonia.Controls protected internal override void RefreshCellContent(IControl element, string propertyName) { - if(propertyName == nameof(CellTemplate) && element.Parent is DataGridCell cell) + var cell = element.Parent as DataGridCell; + if(propertyName == nameof(CellTemplate) && cell is not null) { cell.Content = GenerateElement(cell, cell.DataContext); } From 66a02a37d09b226127b3d7636e18f0d84fbd0f02 Mon Sep 17 00:00:00 2001 From: Tim U Date: Tue, 4 Jan 2022 17:27:28 +0100 Subject: [PATCH 23/44] Handle IsReadOnly correct for DataGridTemplateColumn --- .../DataGridColumn.cs | 2 +- .../DataGridTemplateColumn.cs | 25 +++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index 6b515503aa..8501ce3896 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -448,7 +448,7 @@ namespace Avalonia.Controls internal set; } - public bool IsReadOnly + public virtual bool IsReadOnly { get { diff --git a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs index fbdad0a8ad..e8ccb7df34 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs @@ -15,7 +15,7 @@ namespace Avalonia.Controls { public class DataGridTemplateColumn : DataGridColumn { - IDataTemplate _cellTemplate; + private IDataTemplate _cellTemplate; public static readonly DirectProperty CellTemplateProperty = AvaloniaProperty.RegisterDirect( @@ -54,7 +54,7 @@ namespace Avalonia.Controls { // IsReadOnly = true; } - + protected override IControl GenerateElement(DataGridCell cell, object dataItem) { if(CellTemplate != null) @@ -78,6 +78,10 @@ namespace Avalonia.Controls { return CellEditingTemplate.Build(dataItem); } + else if (CellTemplate != null) + { + return CellTemplate.Build(dataItem); + } if (Design.IsDesignMode) { return null; @@ -103,5 +107,22 @@ namespace Avalonia.Controls base.RefreshCellContent(element, propertyName); } + + public override bool IsReadOnly + { + get + { + if (CellEditingTemplate is null) + { + return true; + } + + return base.IsReadOnly; + } + set + { + base.IsReadOnly = value; + } + } } } From cf6c0991f8f52bcd683328dafa011550df184698 Mon Sep 17 00:00:00 2001 From: Tim U Date: Tue, 4 Jan 2022 17:27:57 +0100 Subject: [PATCH 24/44] Update Demo --- samples/ControlCatalog/Models/Person.cs | 15 +++++++++++++++ samples/ControlCatalog/Pages/DataGridPage.xaml | 12 ++++++++++++ samples/ControlCatalog/Pages/DataGridPage.xaml.cs | 6 +++--- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog/Models/Person.cs b/samples/ControlCatalog/Models/Person.cs index 47f41bc584..cd70fa3959 100644 --- a/samples/ControlCatalog/Models/Person.cs +++ b/samples/ControlCatalog/Models/Person.cs @@ -16,6 +16,7 @@ namespace ControlCatalog.Models string _firstName; string _lastName; bool _isBanned; + private int _age; public string FirstName { @@ -59,6 +60,20 @@ namespace ControlCatalog.Models } } + + /// + /// Gets or sets the age of the person + /// + public int Age + { + get => _age; + set + { + _age = value; + OnPropertyChanged(nameof(Age)); + } + } + Dictionary> _errorLookup = new Dictionary>(); void SetError(string propertyName, string error) diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml b/samples/ControlCatalog/Pages/DataGridPage.xaml index 63e873d9b5..451a774cb4 100644 --- a/samples/ControlCatalog/Pages/DataGridPage.xaml +++ b/samples/ControlCatalog/Pages/DataGridPage.xaml @@ -64,6 +64,18 @@ + + + + + + + + + + + +