Browse Source

Merge branch 'master' into platform-screen-api

pull/7165/head
Steven Kirk 5 years ago
committed by GitHub
parent
commit
abfc213de4
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      samples/ControlCatalog/App.xaml
  2. 15
      samples/ControlCatalog/Models/Person.cs
  3. 5
      samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml
  4. 12
      samples/ControlCatalog/Pages/DataGridPage.xaml
  5. 22
      samples/ControlCatalog/Pages/DataGridPage.xaml.cs
  6. 13
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  7. 16
      src/Avalonia.Animation/Animatable.cs
  8. 46
      src/Avalonia.Animation/Animation.cs
  9. 33
      src/Avalonia.Animation/AnimationInstance`1.cs
  10. 20
      src/Avalonia.Animation/AnimatorKeyFrame.cs
  11. 9
      src/Avalonia.Animation/Animators/Animator`1.cs
  12. 4
      src/Avalonia.Animation/Avalonia.Animation.csproj
  13. 2
      src/Avalonia.Animation/Clock.cs
  14. 8
      src/Avalonia.Animation/Cue.cs
  15. 8
      src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs
  16. 4
      src/Avalonia.Animation/Easing/Easing.cs
  17. 4
      src/Avalonia.Animation/Easing/EasingTypeConverter.cs
  18. 2
      src/Avalonia.Animation/IAnimation.cs
  19. 4
      src/Avalonia.Animation/IAnimationSetter.cs
  20. 4
      src/Avalonia.Animation/IAnimator.cs
  21. 2
      src/Avalonia.Animation/ITransition.cs
  22. 2
      src/Avalonia.Animation/IterationCount.cs
  23. 4
      src/Avalonia.Animation/IterationCountTypeConverter.cs
  24. 4
      src/Avalonia.Animation/KeyFrame.cs
  25. 4
      src/Avalonia.Animation/KeySplineTypeConverter.cs
  26. 21
      src/Avalonia.Animation/Transition.cs
  27. 6
      src/Avalonia.Animation/TransitionInstance.cs
  28. 21
      src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs
  29. 10
      src/Avalonia.Base/Data/Core/IndexerNodeBase.cs
  30. 20
      src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs
  31. 24
      src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
  32. 12
      src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs
  33. 187
      src/Avalonia.Base/Utilities/WeakEvent.cs
  34. 40
      src/Avalonia.Base/Utilities/WeakEvents.cs
  35. 35
      src/Avalonia.Base/Utilities/WeakObservable.cs
  36. 1
      src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs
  37. 1
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  38. 2
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  39. 72
      src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs
  40. 7
      src/Avalonia.Controls/ApiCompatBaseline.txt
  41. 11
      src/Avalonia.Controls/Calendar/CalendarDatePicker.cs
  42. 10
      src/Avalonia.Controls/NativeMenuItem.cs
  43. 32
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  44. 15
      src/Avalonia.Controls/TopLevel.cs
  45. 15
      src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs
  46. 2
      src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml
  47. 3
      src/Avalonia.Dialogs/ApiCompatBaseline.txt
  48. 1
      src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
  49. 6
      src/Avalonia.Input/KeyBinding.cs
  50. 17
      src/Avalonia.Layout/AttachedLayout.cs
  51. 115
      src/Avalonia.Native/AvaloniaNativeMenuExporter.cs
  52. 4
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  53. 28
      src/Avalonia.Native/WindowImplBase.cs
  54. 6
      src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml
  55. 1
      src/Avalonia.Themes.Fluent/Controls/ListBox.xaml
  56. 7
      src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs
  57. 5
      src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs
  58. 5
      src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs
  59. 7
      src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs
  60. 5
      src/Avalonia.Visuals/ApiCompatBaseline.txt
  61. 118
      src/Avalonia.Visuals/Media/Pen.cs
  62. 7
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
  63. 4
      src/Avalonia.X11/Glx/GlxDisplay.cs
  64. 4
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  65. 36
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs
  66. 6
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs
  67. 8
      src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs
  68. 9
      src/Windows/Avalonia.Win32/Win32GlManager.cs
  69. 25
      tests/Avalonia.Animation.UnitTests/AnimatableTests.cs
  70. 3
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs
  71. 12
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs
  72. 11
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs
  73. 27
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs
  74. 3
      tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs
  75. 75
      tests/Avalonia.Base.UnitTests/WeakEventTests.cs
  76. 5
      tests/Avalonia.LeakTests/AvaloniaObjectTests.cs
  77. 7
      tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs
  78. 15
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs

1
samples/ControlCatalog/App.xaml

@ -3,6 +3,7 @@
xmlns:vm="using:ControlCatalog.ViewModels"
x:DataType="vm:ApplicationViewModel"
x:CompileBindings="True"
Name="Avalonia ControlCatalog"
x:Class="ControlCatalog.App">
<Application.Styles>
<Style Selector="TextBlock.h1, TextBlock.h2, TextBlock.h3">

15
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
}
}
/// <summary>
/// Gets or sets the age of the person
/// </summary>
public int Age
{
get => _age;
set
{
_age = value;
OnPropertyChanged(nameof(Age));
}
}
Dictionary<string, List<string>> _errorLookup = new Dictionary<string, List<string>>();
void SetError(string propertyName, string error)

5
samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml

@ -1,5 +1,7 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:ControlCatalog.ViewModels"
x:DataType="vm:MainWindowViewModel"
x:Class="ControlCatalog.Pages.CalendarDatePickerPage">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h2">A control for selecting dates with a calendar drop-down</TextBlock>
@ -39,6 +41,9 @@
<TextBlock Text="Disabled"/>
<CalendarDatePicker IsEnabled="False"/>
<TextBlock Text="Validation Example"/>
<CalendarDatePicker SelectedDate="{CompiledBinding ValidatedDateExample, Mode=TwoWay}"/>
</StackPanel>
</StackPanel>

12
samples/ControlCatalog/Pages/DataGridPage.xaml

@ -64,6 +64,18 @@
<DataGridTextColumn Header="First Name" Binding="{Binding FirstName}" Width="2*" FontSize="{Binding #FontSizeSlider.Value, Mode=OneWay}" />
<DataGridTextColumn Header="Last" Binding="{Binding LastName}" Width="2*" FontSize="{Binding #FontSizeSlider.Value, Mode=OneWay}" />
<DataGridCheckBoxColumn Header="Is Banned" Binding="{Binding IsBanned}" Width="*" IsThreeState="{Binding #IsThreeStateCheckBox.IsChecked, Mode=OneWay}" />
<DataGridTemplateColumn Header="Age" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="local:Person">
<TextBlock Text="{Binding Age, StringFormat='{}{0} years'}" VerticalAlignment="Center" HorizontalAlignment="Center" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate DataType="local:Person">
<NumericUpDown Value="{Binding Age}" FormatString="N0" HorizontalAlignment="Stretch" Minimum="0" Maximum="120" TemplateApplied="NumericUpDown_OnTemplateApplied" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<Button Grid.Row="1" Name="btnAdd" Margin="12,0,12,12" Content="Add" HorizontalAlignment="Right" />

22
samples/ControlCatalog/Pages/DataGridPage.xaml.cs

@ -6,7 +6,9 @@ using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ControlCatalog.Models;
using Avalonia.Collections;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Threading;
namespace ControlCatalog.Pages
{
@ -48,9 +50,9 @@ namespace ControlCatalog.Pages
var items = new List<Person>
{
new Person { FirstName = "John", LastName = "Doe" },
new Person { FirstName = "Elizabeth", LastName = "Thomas", IsBanned = true },
new Person { FirstName = "Zack", LastName = "Ward" }
new Person { FirstName = "John", LastName = "Doe" , Age = 30},
new Person { FirstName = "Elizabeth", LastName = "Thomas", IsBanned = true , Age = 40 },
new Person { FirstName = "Zack", LastName = "Ward" , Age = 50 }
};
var collectionView3 = new DataGridCollectionView(items);
@ -84,5 +86,19 @@ namespace ControlCatalog.Pages
return Comparer.Default.Compare(x, y);
}
}
private void NumericUpDown_OnTemplateApplied(object sender, TemplateAppliedEventArgs e)
{
// We want to focus the TextBox of the NumericUpDown. To do so we search for this control when the template
// is applied, but we postpone the action until the control is actually loaded.
if (e.NameScope.Find<TextBox>("PART_TextBox") is {} textBox)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
textBox.Focus();
textBox.SelectAll();
}, DispatcherPriority.Loaded);
}
}
}
}

13
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;
/// <summary>
/// A required DateTime which should demonstrate validation for the DateTimePicker
/// </summary>
[Required]
public DateTime? ValidatedDateExample
{
get => _validatedDateExample;
set => this.RaiseAndSetIfChanged(ref _validatedDateExample, value);
}
}
}

16
src/Avalonia.Animation/Animatable.cs

@ -157,7 +157,7 @@ namespace Avalonia.Animation
state.Instance?.Dispose();
state.Instance = transition.Apply(
this,
Clock ?? AvaloniaLocator.Current.GetService<IGlobalClock>(),
Clock ?? AvaloniaLocator.Current.GetRequiredService<IGlobalClock>(),
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))
{

46
src/Avalonia.Animation/Animation.cs

@ -203,7 +203,7 @@ namespace Avalonia.Animation
/// </summary>
/// <param name="setter">The animation setter.</param>
/// <returns>The property animator type.</returns>
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
}
/// <inheritdoc/>
public IDisposable Apply(Animatable control, IClock clock, IObservable<bool> match, Action onComplete)
public IDisposable Apply(Animatable control, IClock? clock, IObservable<bool> 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<Task>() : null;
foreach (IAnimator animator in animators)
{
Action animatorOnComplete = null;
Action? animatorOnComplete = null;
if (onComplete != null)
{
var tcs = new TaskCompletionSource<object>();
var tcs = new TaskCompletionSource<object?>();
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
}
/// <inheritdoc/>
public Task RunAsync(Animatable control, IClock clock = null)
public Task RunAsync(Animatable control, IClock? clock = null)
{
return RunAsync(control, clock, default);
}
/// <inheritdoc/>
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<object>();
var run = new TaskCompletionSource<object?>();
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);

33
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<double, T, T> _interpolator;
private IDisposable _timerSub;
private IDisposable? _timerSub;
private readonly IClock _baseClock;
private IClock _clock;
private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChangedDelegate;
private IClock? _clock;
private EventHandler<AvaloniaPropertyChangedEventArgs>? _propertyChangedDelegate;
public AnimationInstance(Animation animation, Animatable control, Animator<T> animator, IClock baseClock, Action OnComplete, Func<double, T, T> Interpolator)
public AnimationInstance(Animation animation, Animatable control, Animator<T> animator, IClock baseClock, Action? OnComplete, Func<double, T, T> 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)
{

20
src/Avalonia.Animation/AnimatorKeyFrame.cs

@ -12,22 +12,22 @@ namespace Avalonia.Animation
/// </summary>
public class AnimatorKeyFrame : AvaloniaObject
{
public static readonly DirectProperty<AnimatorKeyFrame, object> ValueProperty =
AvaloniaProperty.RegisterDirect<AnimatorKeyFrame, object>(nameof(Value), k => k.Value, (k, v) => k.Value = v);
public static readonly DirectProperty<AnimatorKeyFrame, object?> ValueProperty =
AvaloniaProperty.RegisterDirect<AnimatorKeyFrame, object?>(nameof(Value), k => k.Value, (k, v) => k.Value = v);
public AnimatorKeyFrame()
{
}
public AnimatorKeyFrame(Type animatorType, Cue cue)
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))!;
}
}
}

9
src/Avalonia.Animation/Animators/Animator`1.cs

@ -24,7 +24,7 @@ namespace Avalonia.Animation.Animators
/// <summary>
/// Gets or sets the target property for the keyframe.
/// </summary>
public AvaloniaProperty Property { get; set; }
public AvaloniaProperty? Property { get; set; }
public Animator()
{
@ -33,7 +33,7 @@ namespace Avalonia.Animation.Animators
}
/// <inheritdoc/>
public virtual IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable<bool> match, Action onComplete)
public virtual IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable<bool> match, Action? onComplete)
{
if (!_isVerifiedAndConverted)
VerifyConvertKeyFrames();
@ -106,13 +106,16 @@ namespace Avalonia.Animation.Animators
public virtual IDisposable BindAnimation(Animatable control, IObservable<T> instance)
{
if (Property is null)
throw new InvalidOperationException("Animator has no property specified.");
return control.Bind((AvaloniaProperty<T>)Property, instance, BindingPriority.Animation);
}
/// <summary>
/// Runs the KeyFrames Animation.
/// </summary>
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<T>(
animation,

4
src/Avalonia.Animation/Avalonia.Animation.csproj

@ -2,9 +2,13 @@
<PropertyGroup>
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
</ItemGroup>
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\NullableEnable.props" />
</Project>

2
src/Avalonia.Animation/Clock.cs

@ -4,7 +4,7 @@ namespace Avalonia.Animation
{
public class Clock : ClockBase
{
public static IClock GlobalClock => AvaloniaLocator.Current.GetService<IGlobalClock>();
public static IClock GlobalClock => AvaloniaLocator.Current.GetRequiredService<IGlobalClock>();
private readonly IDisposable _parentSubscription;

8
src/Avalonia.Animation/Cue.cs

@ -30,7 +30,7 @@ namespace Avalonia.Animation
/// <summary>
/// Parses a string to a <see cref="Cue"/> object.
/// </summary>
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);
}
}
}
}

8
src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs

@ -8,15 +8,15 @@ namespace Avalonia.Animation
/// </summary>
internal class DisposeAnimationInstanceSubject<T> : IObserver<bool>, IDisposable
{
private IDisposable _lastInstance;
private IDisposable? _lastInstance;
private bool _lastMatch;
private Animator<T> _animator;
private Animation _animation;
private Animatable _control;
private Action _onComplete;
private IClock _clock;
private Action? _onComplete;
private IClock? _clock;
public DisposeAnimationInstanceSubject(Animator<T> animator, Animation animation, Animatable control, IClock clock, Action onComplete)
public DisposeAnimationInstanceSubject(Animator<T> animator, Animation animation, Animatable control, IClock? clock, Action? onComplete)
{
this._animator = animator;
this._animation = animation;

4
src/Avalonia.Animation/Easing/Easing.cs

@ -15,7 +15,7 @@ namespace Avalonia.Animation.Easings
/// <inheritdoc/>
public abstract double Ease(double progress);
static Dictionary<string, Type> _easingTypes;
static Dictionary<string, Type>? _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
{

4
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);
}

2
src/Avalonia.Animation/IAnimation.cs

@ -12,7 +12,7 @@ namespace Avalonia.Animation
/// <summary>
/// Apply the animation to the specified control and run it when <paramref name="match" /> produces <c>true</c>.
/// </summary>
IDisposable Apply(Animatable control, IClock clock, IObservable<bool> match, Action onComplete = null);
IDisposable Apply(Animatable control, IClock? clock, IObservable<bool> match, Action? onComplete = null);
/// <summary>
/// Run the animation on the specified control.

4
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; }
}
}

4
src/Avalonia.Animation/IAnimator.cs

@ -11,11 +11,11 @@ namespace Avalonia.Animation
/// <summary>
/// The target property.
/// </summary>
AvaloniaProperty Property {get; set;}
AvaloniaProperty? Property {get; set;}
/// <summary>
/// Applies the current KeyFrame group to the specified control.
/// </summary>
IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable<bool> match, Action onComplete);
IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable<bool> match, Action? onComplete);
}
}

2
src/Avalonia.Animation/ITransition.cs

@ -10,7 +10,7 @@ namespace Avalonia.Animation
/// <summary>
/// Applies the transition to the specified <see cref="Animatable"/>.
/// </summary>
IDisposable Apply(Animatable control, IClock clock, object oldValue, object newValue);
IDisposable Apply(Animatable control, IClock clock, object? oldValue, object? newValue);
/// <summary>
/// Gets the property to be animated.

2
src/Avalonia.Animation/IterationCount.cs

@ -97,7 +97,7 @@ namespace Avalonia.Animation
/// </summary>
/// <param name="o">The object with which to test equality.</param>
/// <returns>True if the objects are equal, otherwise false.</returns>
public override bool Equals(object o)
public override bool Equals(object? o)
{
if (o == null)
{

4
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);
}

4
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 <see cref="KeyFrame"/>.
/// </summary>
/// <value>The key spline.</value>
public KeySpline KeySpline
public KeySpline? KeySpline
{
get
{

4
src/Avalonia.Animation/KeySplineTypeConverter.cs

@ -12,12 +12,12 @@ namespace Avalonia.Animation
/// </summary>
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);
}

21
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
/// </summary>
public abstract class Transition<T> : AvaloniaObject, ITransition
{
private AvaloniaProperty _prop;
private AvaloniaProperty? _prop;
/// <summary>
/// Gets or sets the duration of the transition.
@ -26,7 +27,8 @@ namespace Avalonia.Animation
public Easing Easing { get; set; } = new LinearEasing();
/// <inheritdocs/>
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;
}
/// <summary>
/// Apply interpolation to the property.
/// </summary>
public abstract IObservable<T> DoTransition(IObservable<double> progress, T oldValue, T newValue);
/// <inheritdocs/>
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<T>((AvaloniaProperty<T>)Property, transition, Data.BindingPriority.Animation);
}
}
}
}

6
src/Avalonia.Animation/TransitionInstance.cs

@ -10,11 +10,11 @@ namespace Avalonia.Animation
/// </summary>
internal class TransitionInstance : SingleSubscriberObservableBase<double>, IObserver<TimeSpan>
{
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()

21
src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs

@ -59,7 +59,7 @@ namespace Avalonia.Collections
}
private class WeakCollectionChangedObservable : LightweightObservableBase<NotifyCollectionChangedEventArgs>,
IWeakSubscriber<NotifyCollectionChangedEventArgs>
IWeakEventSubscriber<NotifyCollectionChangedEventArgs>
{
private WeakReference<INotifyCollectionChanged> _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);
}
}
}

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

@ -23,18 +23,16 @@ namespace Avalonia.Data.Core
if (incc != null)
{
inputs.Add(WeakObservable.FromEventPattern<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>(
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<INotifyPropertyChanged, PropertyChangedEventArgs>(
inpc,
nameof(inpc.PropertyChanged))
inputs.Add(WeakObservable.FromEventPattern(
inpc, WeakEvents.PropertyChanged)
.Where(x => ShouldUpdate(x.Sender, x.EventArgs))
.Select(_ => GetValue(target)));
}

20
src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs

@ -11,6 +11,12 @@ namespace Avalonia.Data.Core.Plugins
/// </summary>
public class IndeiValidationPlugin : IDataValidationPlugin
{
private static readonly WeakEvent<INotifyDataErrorInfo, DataErrorsChangedEventArgs>
ErrorsChangedWeakEvent = WeakEvent.Register<INotifyDataErrorInfo, DataErrorsChangedEventArgs>(
(s, h) => s.ErrorsChanged += h,
(s, h) => s.ErrorsChanged -= h
);
/// <inheritdoc/>
public bool Match(WeakReference<object?> reference, string memberName)
{
@ -25,7 +31,7 @@ namespace Avalonia.Data.Core.Plugins
return new Validator(reference, name, accessor);
}
private class Validator : DataValidationBase, IWeakSubscriber<DataErrorsChangedEventArgs>
private class Validator : DataValidationBase, IWeakEventSubscriber<DataErrorsChangedEventArgs>
{
private readonly WeakReference<object?> _reference;
private readonly string _name;
@ -37,7 +43,7 @@ namespace Avalonia.Data.Core.Plugins
_name = name;
}
void IWeakSubscriber<DataErrorsChangedEventArgs>.OnEvent(object? sender, DataErrorsChangedEventArgs e)
void IWeakEventSubscriber<DataErrorsChangedEventArgs>.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();

24
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<PropertyChangedEventArgs>
private class Accessor : PropertyAccessorBase, IWeakEventSubscriber<PropertyChangedEventArgs>
{
private readonly WeakReference<object?> _reference;
private readonly PropertyInfo _property;
@ -129,7 +130,8 @@ namespace Avalonia.Data.Core.Plugins
return false;
}
void IWeakSubscriber<PropertyChangedEventArgs>.OnEvent(object? sender, PropertyChangedEventArgs e)
void IWeakEventSubscriber<PropertyChangedEventArgs>.
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);
}
}
}

12
src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs

@ -0,0 +1,12 @@
using System;
namespace Avalonia.Utilities;
/// <summary>
/// Defines a listener to a event subscribed vis the <see cref="WeakEvent{TTarget, TEventArgs}"/>.
/// </summary>
/// <typeparam name="TEventArgs">The type of the event arguments.</typeparam>
public interface IWeakEventSubscriber<in TEventArgs> where TEventArgs : EventArgs
{
void OnEvent(object? sender, WeakEvent ev, TEventArgs e);
}

187
src/Avalonia.Base/Utilities/WeakEvent.cs

@ -0,0 +1,187 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using Avalonia.Threading;
namespace Avalonia.Utilities;
/// <summary>
/// Manages subscriptions to events using weak listeners.
/// </summary>
public class WeakEvent<TSender, TEventArgs> : WeakEvent where TEventArgs : EventArgs where TSender : class
{
private readonly Func<TSender, EventHandler<TEventArgs>, Action> _subscribe;
readonly ConditionalWeakTable<object, Subscription> _subscriptions = new();
internal WeakEvent(
Action<TSender, EventHandler<TEventArgs>> subscribe,
Action<TSender, EventHandler<TEventArgs>> unsubscribe)
{
_subscribe = (t, s) =>
{
subscribe(t, s);
return () => unsubscribe(t, s);
};
}
internal WeakEvent(Func<TSender, EventHandler<TEventArgs>, Action> subscribe)
{
_subscribe = subscribe;
}
public void Subscribe(TSender target, IWeakEventSubscriber<TEventArgs> subscriber)
{
if (!_subscriptions.TryGetValue(target, out var subscription))
_subscriptions.Add(target, subscription = new Subscription(this, target));
subscription.Add(new WeakReference<IWeakEventSubscriber<TEventArgs>>(subscriber));
}
public void Unsubscribe(TSender target, IWeakEventSubscriber<TEventArgs> subscriber)
{
if (_subscriptions.TryGetValue(target, out var subscription))
subscription.Remove(subscriber);
}
private class Subscription
{
private readonly WeakEvent<TSender, TEventArgs> _ev;
private readonly TSender _target;
private readonly Action _compact;
private WeakReference<IWeakEventSubscriber<TEventArgs>>?[] _data =
new WeakReference<IWeakEventSubscriber<TEventArgs>>[16];
private int _count;
private readonly Action _unsubscribe;
private bool _compactScheduled;
public Subscription(WeakEvent<TSender, TEventArgs> ev, TSender target)
{
_ev = ev;
_target = target;
_compact = Compact;
_unsubscribe = ev._subscribe(target, OnEvent);
}
void Destroy()
{
_unsubscribe();
_ev._subscriptions.Remove(_target);
}
public void Add(WeakReference<IWeakEventSubscriber<TEventArgs>> s)
{
if (_count == _data.Length)
{
//Extend capacity
var extendedData = new WeakReference<IWeakEventSubscriber<TEventArgs>>?[_data.Length * 2];
Array.Copy(_data, extendedData, _data.Length);
_data = extendedData;
}
_data[_count] = s;
_count++;
}
public void Remove(IWeakEventSubscriber<TEventArgs> 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)
{
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++)
{
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)
ScheduleCompact();
}
}
}
public class WeakEvent
{
public static WeakEvent<TSender, TEventArgs> Register<TSender, TEventArgs>(
Action<TSender, EventHandler<TEventArgs>> subscribe,
Action<TSender, EventHandler<TEventArgs>> unsubscribe) where TSender : class where TEventArgs : EventArgs
{
return new WeakEvent<TSender, TEventArgs>(subscribe, unsubscribe);
}
public static WeakEvent<TSender, TEventArgs> Register<TSender, TEventArgs>(
Func<TSender, EventHandler<TEventArgs>, Action> subscribe) where TSender : class where TEventArgs : EventArgs
{
return new WeakEvent<TSender, TEventArgs>(subscribe);
}
public static WeakEvent<TSender, EventArgs> Register<TSender>(
Action<TSender, EventHandler> subscribe,
Action<TSender, EventHandler> unsubscribe) where TSender : class
{
return Register<TSender, EventArgs>((s, h) =>
{
EventHandler handler = (_, e) => h(s, e);
subscribe(s, handler);
return () => unsubscribe(s, handler);
});
}
}

40
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
{
/// <summary>
/// Represents CollectionChanged event from <see cref="INotifyCollectionChanged"/>
/// </summary>
public static readonly WeakEvent<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>
CollectionChanged = WeakEvent.Register<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>(
(c, s) =>
{
NotifyCollectionChangedEventHandler handler = (_, e) => s(c, e);
c.CollectionChanged += handler;
return () => c.CollectionChanged -= handler;
});
/// <summary>
/// Represents PropertyChanged event from <see cref="INotifyPropertyChanged"/>
/// </summary>
public static readonly WeakEvent<INotifyPropertyChanged, PropertyChangedEventArgs>
PropertyChanged = WeakEvent.Register<INotifyPropertyChanged, PropertyChangedEventArgs>(
(s, h) =>
{
PropertyChangedEventHandler handler = (_, e) => h(s, e);
s.PropertyChanged += handler;
return () => s.PropertyChanged -= handler;
});
/// <summary>
/// Represents CanExecuteChanged event from <see cref="ICommand"/>
/// </summary>
public static readonly WeakEvent<ICommand, EventArgs> CommandCanExecuteChanged =
WeakEvent.Register<ICommand>((s, h) => s.CanExecuteChanged += h,
(s, h) => s.CanExecuteChanged -= h);
}

35
src/Avalonia.Base/Utilities/WeakObservable.cs

@ -18,6 +18,7 @@ namespace Avalonia.Utilities
/// <param name="target">Object instance that exposes the event to convert.</param>
/// <param name="eventName">Name of the event to convert.</param>
/// <returns></returns>
[Obsolete("Use WeakEvent-based overload")]
public static IObservable<EventPattern<object, TEventArgs>> FromEventPattern<TTarget, TEventArgs>(
TTarget target,
string eventName)
@ -34,7 +35,9 @@ namespace Avalonia.Utilities
}).Publish().RefCount();
}
private class Handler<TEventArgs> : IWeakSubscriber<TEventArgs> where TEventArgs : EventArgs
private class Handler<TEventArgs>
: IWeakSubscriber<TEventArgs>,
IWeakEventSubscriber<TEventArgs> where TEventArgs : EventArgs
{
private IObserver<EventPattern<object, TEventArgs>> _observer;
@ -47,6 +50,36 @@ namespace Avalonia.Utilities
{
_observer.OnNext(new EventPattern<object, TEventArgs>(sender, e));
}
public void OnEvent(object? sender, WeakEvent ev, TEventArgs e)
{
_observer.OnNext(new EventPattern<object, TEventArgs>(sender, e));
}
}
/// <summary>
/// Converts a WeakEvent conforming to the standard .NET event pattern into an observable
/// sequence, subscribing weakly.
/// </summary>
/// <typeparam name="TTarget">The type of target.</typeparam>
/// <typeparam name="TEventArgs">The type of the event args.</typeparam>
/// <param name="target">Object instance that exposes the event to convert.</param>
/// <param name="ev">The weak event to convert.</param>
/// <returns></returns>
public static IObservable<EventPattern<object, TEventArgs>> FromEventPattern<TTarget, TEventArgs>(
TTarget target, WeakEvent<TTarget, TEventArgs> ev)
where TEventArgs : EventArgs where TTarget : class
{
_ = target ?? throw new ArgumentNullException(nameof(target));
_ = ev ?? throw new ArgumentNullException(nameof(ev));
return Observable.Create<EventPattern<object, TEventArgs>>(observer =>
{
var handler = new Handler<TEventArgs>(observer);
ev.Subscribe(target, handler);
return () => ev.Unsubscribe(target, handler);
}).Publish().RefCount();
}
}
}

1
src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs

@ -19,6 +19,7 @@ namespace Avalonia.Utilities
/// <param name="target">The event source.</param>
/// <param name="eventName">The name of the event.</param>
/// <param name="subscriber">The subscriber.</param>
[Obsolete("Use WeakEvent")]
public static void Subscribe<TTarget, TEventArgs>(TTarget target, string eventName, IWeakSubscriber<TEventArgs> subscriber)
where TEventArgs : EventArgs
{

1
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);

2
src/Avalonia.Controls.DataGrid/DataGridColumn.cs

@ -448,7 +448,7 @@ namespace Avalonia.Controls
internal set;
}
public bool IsReadOnly
public virtual bool IsReadOnly
{
get
{

72
src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs

@ -1,4 +1,4 @@
// (c) Copyright Microsoft Corporation.
// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
@ -15,7 +15,7 @@ namespace Avalonia.Controls
{
public class DataGridTemplateColumn : DataGridColumn
{
IDataTemplate _cellTemplate;
private IDataTemplate _cellTemplate;
public static readonly DirectProperty<DataGridTemplateColumn, IDataTemplate> CellTemplateProperty =
AvaloniaProperty.RegisterDirect<DataGridTemplateColumn, IDataTemplate>(
@ -30,17 +30,38 @@ namespace Avalonia.Controls
set { SetAndRaise(CellTemplateProperty, ref _cellTemplate, value); }
}
private IDataTemplate _cellEditingCellTemplate;
/// <summary>
/// Defines the <see cref="CellEditingTemplate"/> property.
/// </summary>
public static readonly DirectProperty<DataGridTemplateColumn, IDataTemplate> CellEditingTemplateProperty =
AvaloniaProperty.RegisterDirect<DataGridTemplateColumn, IDataTemplate>(
nameof(CellEditingTemplate),
o => o.CellEditingTemplate,
(o, v) => o.CellEditingTemplate = v);
/// <summary>
/// Gets or sets the <see cref="IDataTemplate"/> which is used for the editing mode of the current <see cref="DataGridCell"/>
/// </summary>
/// <value>
/// An <see cref="IDataTemplate"/> for the editing mode of the current <see cref="DataGridCell"/>
/// </value>
/// <remarks>
/// If this property is <see langword="null"/> the column is read-only.
/// </remarks>
public IDataTemplate CellEditingTemplate
{
get => _cellEditingCellTemplate;
set => SetAndRaise(CellEditingTemplateProperty, ref _cellEditingCellTemplate, value);
}
private void OnCellTemplateChanged(AvaloniaPropertyChangedEventArgs e)
{
var oldValue = (IDataTemplate)e.OldValue;
var value = (IDataTemplate)e.NewValue;
}
public DataGridTemplateColumn()
{
IsReadOnly = true;
}
protected override IControl GenerateElement(DataGridCell cell, object dataItem)
{
if(CellTemplate != null)
@ -60,7 +81,22 @@ 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);
}
else if (CellTemplate != null)
{
return CellTemplate.Build(dataItem);
}
if (Design.IsDesignMode)
{
return null;
}
else
{
throw DataGridError.DataGridTemplateColumn.MissingTemplateForType(typeof(DataGridTemplateColumn));
}
}
protected override object PrepareCellForEdit(IControl editingElement, RoutedEventArgs editingEventArgs)
@ -70,12 +106,30 @@ 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);
}
base.RefreshCellContent(element, propertyName);
}
public override bool IsReadOnly
{
get
{
if (CellEditingTemplate is null)
{
return true;
}
return base.IsReadOnly;
}
set
{
base.IsReadOnly = value;
}
}
}
}

7
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<System.Boolean> Avalonia.StyledProperty<System.Boolean> 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<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
MembersMustExist : Member 'public Avalonia.AvaloniaProperty<Avalonia.Media.Stretch> Avalonia.AvaloniaProperty<Avalonia.Media.Stretch> 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<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.WindowBase' does not implement interface 'Avalonia.Utilities.IWeakSubscriber<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs> 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<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs>)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.remove_ShutdownRequested(System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs>)' is present in the implementation but not in the contract.
CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Embedding.EmbeddableControlRoot' does not implement interface 'Avalonia.Utilities.IWeakSubscriber<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
MembersMustExist : Member 'public System.Action<Avalonia.Size> 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<Avalonia.Size>)' 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<Avalonia.Controls.ResourcesChangedEventArgs>' 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.Size> Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable<Avalonia.Size> 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

11
src/Avalonia.Controls/Calendar/CalendarDatePicker.cs

@ -185,7 +185,8 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterDirect<CalendarDatePicker, DateTime?>(
nameof(SelectedDate),
o => o.SelectedDate,
(o, v) => o.SelectedDate = v);
(o, v) => o.SelectedDate = v,
enableDataValidation: true);
public static readonly StyledProperty<CalendarDatePickerFormat> SelectedDateFormatProperty =
AvaloniaProperty.Register<CalendarDatePicker, CalendarDatePickerFormat>(
@ -533,13 +534,11 @@ namespace Avalonia.Controls
}
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{
base.OnPropertyChanged(change);
if (change.Property == SelectedDateProperty)
if (property == SelectedDateProperty)
{
DataValidationErrors.SetError(this, change.NewValue.Error);
DataValidationErrors.SetError(this, value.Error);
}
}

10
src/Avalonia.Controls/NativeMenuItem.cs

@ -33,7 +33,7 @@ namespace Avalonia.Controls
}
class CanExecuteChangedSubscriber : IWeakSubscriber<EventArgs>
class CanExecuteChangedSubscriber : IWeakEventSubscriber<EventArgs>
{
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();
}

32
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.
/// </summary>
public class ItemsRepeater : Panel, IChildIndexProvider
public class ItemsRepeater : Panel, IChildIndexProvider, IWeakEventSubscriber<EventArgs>
{
/// <summary>
/// Defines the <see cref="HorizontalCacheLength"/> property.
@ -723,14 +723,8 @@ namespace Avalonia.Controls
{
oldValue.UninitializeForContext(LayoutContext);
WeakEventHandlerManager.Unsubscribe<EventArgs, ItemsRepeater>(
oldValue,
nameof(AttachedLayout.MeasureInvalidated),
InvalidateMeasureForLayout);
WeakEventHandlerManager.Unsubscribe<EventArgs, ItemsRepeater>(
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<AttachedLayout, EventArgs, ItemsRepeater>(
newValue,
nameof(AttachedLayout.MeasureInvalidated),
InvalidateMeasureForLayout);
WeakEventHandlerManager.Subscribe<AttachedLayout, EventArgs, ItemsRepeater>(
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<EventArgs>.OnEvent(object? sender, WeakEvent ev, EventArgs e)
{
if(ev == AttachedLayout.ArrangeInvalidatedWeakEvent)
InvalidateArrange();
else if (ev == AttachedLayout.MeasureInvalidatedWeakEvent)
InvalidateMeasure();
}
private VirtualizingLayoutContext GetLayoutContext()
{

15
src/Avalonia.Controls/TopLevel.cs

@ -34,7 +34,7 @@ namespace Avalonia.Controls
IStyleHost,
ILogicalRoot,
ITextInputMethodRoot,
IWeakSubscriber<ResourcesChangedEventArgs>
IWeakEventSubscriber<ResourcesChangedEventArgs>
{
/// <summary>
/// Defines the <see cref="ClientSize"/> property.
@ -74,6 +74,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IBrush> TransparencyBackgroundFallbackProperty =
AvaloniaProperty.Register<TopLevel, IBrush>(nameof(TransparencyBackgroundFallback), Brushes.White);
private static readonly WeakEvent<IResourceHost, ResourcesChangedEventArgs>
ResourcesChangedWeakEvent = WeakEvent.Register<IResourceHost, ResourcesChangedEventArgs>(
(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
/// <inheritdoc/>
IMouseDevice IInputRoot.MouseDevice => PlatformImpl?.MouseDevice;
void IWeakSubscriber<ResourcesChangedEventArgs>.OnEvent(object sender, ResourcesChangedEventArgs e)
void IWeakEventSubscriber<ResourcesChangedEventArgs>.OnEvent(object sender, WeakEvent ev, ResourcesChangedEventArgs e)
{
((ILogical)this).NotifyResourcesChanged(e);
}

15
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<NotifyCollectionChangedEventArgs>, IDisposable
private class Entry : IWeakEventSubscriber<NotifyCollectionChangedEventArgs>, IDisposable
{
private INotifyCollectionChanged _collection;
@ -91,23 +91,18 @@ namespace Avalonia.Controls.Utils
{
_collection = collection;
Listeners = new List<WeakReference<ICollectionChangedListener>>();
WeakSubscriptionManager.Subscribe(
_collection,
nameof(INotifyCollectionChanged.CollectionChanged),
this);
WeakEvents.CollectionChanged.Subscribe(_collection, this);
}
public List<WeakReference<ICollectionChangedListener>> Listeners { get; }
public void Dispose()
{
WeakSubscriptionManager.Unsubscribe(
_collection,
nameof(INotifyCollectionChanged.CollectionChanged),
this);
WeakEvents.CollectionChanged.Unsubscribe(_collection, this);
}
void IWeakSubscriber<NotifyCollectionChangedEventArgs>.OnEvent(object? sender, NotifyCollectionChangedEventArgs e)
void IWeakEventSubscriber<NotifyCollectionChangedEventArgs>.
OnEvent(object? notifyCollectionChanged, WeakEvent ev, NotifyCollectionChangedEventArgs e)
{
static void Notify(
INotifyCollectionChanged incc,

2
src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml

@ -95,7 +95,7 @@
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" >
<TextBlock Text="Chat Room | " />
<Button Classes="Hyperlink" CommandParameter="https://gitter.im/AvaloniaUI/Avalonia/" />
<Button Classes="Hyperlink" CommandParameter="https://t.me/Avalonia" />
</StackPanel>
</StackPanel>
<StackPanel VerticalAlignment="Bottom" Margin="10">

3
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<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
Total Issues: 1

1
src/Avalonia.Dialogs/Avalonia.Dialogs.csproj

@ -5,6 +5,7 @@
<ItemGroup>
<AvaloniaResource Include="Assets\*" />
<AvaloniaResource Include="**/*.xaml" />
</ItemGroup>
<Import Project="..\..\build\BuildTargets.targets" />

6
src/Avalonia.Input/KeyBinding.cs

@ -35,9 +35,11 @@ namespace Avalonia.Input
{
if (Gesture?.Matches(args) == true)
{
args.Handled = true;
if (Command?.CanExecute(CommandParameter) == true)
if (Command?.CanExecute(CommandParameter) == true)
{
args.Handled = true;
Command.Execute(CommandParameter);
}
}
}
}

17
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
/// </summary>
public event EventHandler? MeasureInvalidated;
/// <summary>
/// Occurs when the measurement state (layout) has been invalidated.
/// </summary>
public static readonly WeakEvent<AttachedLayout, EventArgs> MeasureInvalidatedWeakEvent =
WeakEvent.Register<AttachedLayout>(
(s, h) => s.MeasureInvalidated += h,
(s, h) => s.MeasureInvalidated -= h);
/// <summary>
/// Occurs when the arrange state (layout) has been invalidated.
/// </summary>
public event EventHandler? ArrangeInvalidated;
/// <summary>
/// Occurs when the arrange state (layout) has been invalidated.
/// </summary>
public static readonly WeakEvent<AttachedLayout, EventArgs> ArrangeInvalidatedWeakEvent =
WeakEvent.Register<AttachedLayout>(
(s, h) => s.ArrangeInvalidated += h,
(s, h) => s.ArrangeInvalidated -= h);
/// <summary>
/// Initializes any per-container state the layout requires when it is attached to an

115
src/Avalonia.Native/AvaloniaNativeMenuExporter.cs

@ -70,81 +70,83 @@ namespace Avalonia.Native
var result = new NativeMenu();
var aboutItem = new NativeMenuItem("About Avalonia");
aboutItem.Click += async (sender, e) =>
aboutItem.Click += async (_, _) =>
{
var dialog = new AboutAvaloniaDialog();
var mainWindow = (Application.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow;
await dialog.ShowDialog(mainWindow);
if (Application.Current is
{ ApplicationLifetime: IClassicDesktopStyleApplicationLifetime { MainWindow: { } mainWindow } })
{
await dialog.ShowDialog(mainWindow);
}
};
result.Add(aboutItem);
var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>();
if (macOpts == null || !macOpts.DisableDefaultApplicationMenuItems)
{
result.Add(new NativeMenuItemSeparator());
return result;
}
var servicesMenu = new NativeMenuItem("Services");
servicesMenu.Menu = new NativeMenu
{
[MacOSNativeMenuCommands.IsServicesSubmenuProperty] = true
};
result.Add(servicesMenu);
private void PopulateStandardOSXMenuItems(NativeMenu appMenu)
{
appMenu.Add(new NativeMenuItemSeparator());
result.Add(new NativeMenuItemSeparator());
var servicesMenu = new NativeMenuItem("Services");
servicesMenu.Menu = new NativeMenu { [MacOSNativeMenuCommands.IsServicesSubmenuProperty] = true };
var hideItem = new NativeMenuItem("Hide " + Application.Current.Name)
{
Gesture = new KeyGesture(Key.H, KeyModifiers.Meta)
};
hideItem.Click += (sender, args) =>
{
_applicationCommands.HideApp();
};
result.Add(hideItem);
appMenu.Add(servicesMenu);
appMenu.Add(new NativeMenuItemSeparator());
var hideOthersItem = new NativeMenuItem("Hide Others")
{
Gesture = new KeyGesture(Key.Q, KeyModifiers.Meta | KeyModifiers.Alt)
};
hideOthersItem.Click += (sender, args) =>
{
_applicationCommands.HideOthers();
};
result.Add(hideOthersItem);
var hideItem = new NativeMenuItem("Hide " + (Application.Current?.Name ?? "Application"))
{
Gesture = new KeyGesture(Key.H, KeyModifiers.Meta)
};
hideItem.Click += (_, _) =>
{
_applicationCommands.HideApp();
};
var showAllItem = new NativeMenuItem("Show All");
showAllItem.Click += (sender, args) =>
{
_applicationCommands.ShowAll();
};
result.Add(showAllItem);
appMenu.Add(hideItem);
result.Add(new NativeMenuItemSeparator());
var hideOthersItem = new NativeMenuItem("Hide Others")
{
Gesture = new KeyGesture(Key.Q, KeyModifiers.Meta | KeyModifiers.Alt)
};
hideOthersItem.Click += (_, _) =>
{
_applicationCommands.HideOthers();
};
appMenu.Add(hideOthersItem);
var quitItem = new NativeMenuItem("Quit")
{
Gesture = new KeyGesture(Key.Q, KeyModifiers.Meta)
};
quitItem.Click += (sender, args) =>
{
_applicationCommands.ShowAll();
};
result.Add(quitItem);
}
var showAllItem = new NativeMenuItem("Show All");
showAllItem.Click += (_, _) =>
{
_applicationCommands.ShowAll();
};
appMenu.Add(showAllItem);
return result;
appMenu.Add(new NativeMenuItemSeparator());
var quitItem = new NativeMenuItem("Quit") { Gesture = new KeyGesture(Key.Q, KeyModifiers.Meta) };
quitItem.Click += (_, _) =>
{
if (Application.Current is { ApplicationLifetime: IControlledApplicationLifetime lifetime })
{
lifetime.Shutdown();
}
};
appMenu.Add(quitItem);
}
private void DoLayoutReset(bool forceUpdate = false)
{
var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>();
var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>() ?? new MacOSPlatformOptions();
if (macOpts != null && macOpts.DisableNativeMenus)
if (macOpts.DisableNativeMenus)
{
return;
}
@ -220,6 +222,13 @@ namespace Avalonia.Native
_nativeMenu.Initialize(this, appMenuHolder, "");
var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>();
if (macOpts == null || !macOpts.DisableDefaultApplicationMenuItems)
{
PopulateStandardOSXMenuItems(menu);
}
setMenu = true;
}

4
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<MacOSPlatformOptions>();
var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>() ?? new MacOSPlatformOptions();
_factory.MacOptions.SetShowInDock(macOpts?.ShowInDock != false ? 1 : 0);
_factory.MacOptions.SetShowInDock(macOpts.ShowInDock ? 1 : 0);
}
AvaloniaLocator.CurrentMutable

28
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)

6
src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml

@ -33,6 +33,7 @@
<Setter Property="Template">
<ControlTemplate>
<DataValidationErrors>
<Grid ColumnDefinitions="*,Auto">
<Grid.Styles>
@ -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}" />
</Popup>
</Grid>
</DataValidationErrors>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="CalendarDatePicker:error TextBox /template/ Border#PART_BorderElement">
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlErrorTextForegroundBrush}"/>
</Style>
</Styles>

1
src/Avalonia.Themes.Fluent/Controls/ListBox.xaml

@ -20,6 +20,7 @@
<Setter Property="Template">
<ControlTemplate>
<Border Name="border"
ClipToBounds="{TemplateBinding ClipToBounds}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"

7
src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs

@ -38,8 +38,8 @@ namespace Avalonia.Animation.Animators
}
/// <inheritdoc/>
public override IDisposable Apply(Animation animation, Animatable control, IClock clock,
IObservable<bool> match, Action onComplete)
public override IDisposable? Apply(Animation animation, Animatable control, IClock? clock,
IObservable<bool> 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))

5
src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs

@ -58,6 +58,11 @@ namespace Avalonia.Animation.Animators
public override IDisposable BindAnimation(Animatable control, IObservable<IGradientBrush?> instance)
{
if (Property is null)
{
throw new InvalidOperationException("Animator has no property specified.");
}
return control.Bind((AvaloniaProperty<IBrush?>)Property, instance, BindingPriority.Animation);
}

5
src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs

@ -24,6 +24,11 @@ namespace Avalonia.Animation.Animators
public override IDisposable BindAnimation(Animatable control, IObservable<ISolidColorBrush?> instance)
{
if (Property is null)
{
throw new InvalidOperationException("Animator has no property specified.");
}
return control.Bind((AvaloniaProperty<IBrush?>)Property, instance, BindingPriority.Animation);
}
}

7
src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs

@ -14,10 +14,15 @@ namespace Avalonia.Animation.Animators
DoubleAnimator? _doubleAnimator;
/// <inheritdoc/>
public override IDisposable? Apply(Animation animation, Animatable control, IClock clock, IObservable<bool> obsMatch, Action onComplete)
public override IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable<bool> 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))
{

5
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<T>(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

118
src/Avalonia.Visuals/Media/Pen.cs

@ -7,7 +7,7 @@ namespace Avalonia.Media
/// <summary>
/// Describes how a stroke is drawn.
/// </summary>
public class Pen : AvaloniaObject, IPen
public sealed class Pen : AvaloniaObject, IPen, IWeakEventSubscriber<EventArgs>
{
/// <summary>
/// Defines the <see cref="Brush"/> property.
@ -45,6 +45,10 @@ namespace Avalonia.Media
public static readonly StyledProperty<double> MiterLimitProperty =
AvaloniaProperty.Register<Pen, double>(nameof(MiterLimit), 10.0);
private EventHandler? _invalidated;
private IAffectsRender? _subscribedToBrush;
private IAffectsRender? _subscribedToDashes;
/// <summary>
/// Initializes a new instance of the <see cref="Pen"/> class.
/// </summary>
@ -96,17 +100,6 @@ namespace Avalonia.Media
DashStyle = dashStyle;
}
static Pen()
{
AffectsRender<Pen>(
BrushProperty,
ThicknessProperty,
DashStyleProperty,
LineCapProperty,
LineJoinProperty,
MiterLimitProperty);
}
/// <summary>
/// Gets or sets the brush used to draw the stroke.
/// </summary>
@ -116,6 +109,11 @@ namespace Avalonia.Media
set => SetValue(BrushProperty, value);
}
private static readonly WeakEvent<IAffectsRender, EventArgs> InvalidatedWeakEvent =
WeakEvent.Register<IAffectsRender>(
(s, h) => s.Invalidated += h,
(s, h) => s.Invalidated -= h);
/// <summary>
/// Gets or sets the stroke thickness.
/// </summary>
@ -165,7 +163,19 @@ namespace Avalonia.Media
/// <summary>
/// Raised when the pen changes.
/// </summary>
public event EventHandler? Invalidated;
public event EventHandler? Invalidated
{
add
{
_invalidated += value;
UpdateSubscriptions();
}
remove
{
_invalidated -= value;
UpdateSubscriptions();
}
}
/// <summary>
/// Creates an immutable clone of the brush.
@ -182,68 +192,42 @@ namespace Avalonia.Media
MiterLimit);
}
/// <summary>
/// Marks a property as affecting the pen's visual representation.
/// </summary>
/// <param name="properties">The properties.</param>
/// <remarks>
/// After a call to this method in a pen's static constructor, any change to the
/// property will cause the <see cref="Invalidated"/> event to be raised on the pen.
/// </remarks>
protected static void AffectsRender<T>(params AvaloniaProperty[] properties)
where T : Pen
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> 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)
UpdateSubscription(ref _subscribedToBrush, Brush);
if(change.Property == DashStyleProperty)
UpdateSubscription(ref _subscribedToDashes, DashStyle);
base.OnPropertyChanged(change);
}
static void InvalidateAndSubscribe(AvaloniaPropertyChangedEventArgs e)
void UpdateSubscription(ref IAffectsRender? field, object? value)
{
if ((_invalidated == null || field != value) && field != null)
{
if (e.Sender is T sender)
{
if (e.OldValue is IAffectsRender oldValue)
{
WeakEventHandlerManager.Unsubscribe<EventArgs, T>(
oldValue,
nameof(oldValue.Invalidated),
sender.AffectsRenderInvalidated);
}
if (e.NewValue is IAffectsRender newValue)
{
WeakEventHandlerManager.Subscribe<IAffectsRender, EventArgs, T>(
newValue,
nameof(newValue.Invalidated),
sender.AffectsRenderInvalidated);
}
sender.RaiseInvalidated(EventArgs.Empty);
}
InvalidatedWeakEvent.Unsubscribe(field, this);
field = null;
}
foreach (var property in properties)
if (_invalidated != null && field != value && value is IAffectsRender affectsRender)
{
if (property.CanValueAffectRender())
{
property.Changed.Subscribe(e => InvalidateAndSubscribe(e));
}
else
{
property.Changed.Subscribe(e => Invalidate(e));
}
InvalidatedWeakEvent.Subscribe(affectsRender, this);
field = affectsRender;
}
}
/// <summary>
/// Raises the <see cref="Invalidated"/> event.
/// </summary>
/// <param name="e">The event args.</param>
protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e);
private void AffectsRenderInvalidated(object? sender, EventArgs e) => RaiseInvalidated(EventArgs.Empty);
void UpdateSubscriptions()
{
UpdateSubscription(ref _subscribedToBrush, Brush);
UpdateSubscription(ref _subscribedToDashes, DashStyle);
}
void IWeakEventSubscriber<EventArgs>.OnEvent(object? sender, WeakEvent ev, EventArgs e)
{
if (ev == InvalidatedWeakEvent)
_invalidated?.Invoke(this, EventArgs.Empty);
}
}
}

7
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);
}

4
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<X11PlatformOptions>()
?.GlxRendererBlacklist;
var opts = AvaloniaLocator.Current.GetService<X11PlatformOptions>() ?? new X11PlatformOptions();
var blacklist = opts.GlxRendererBlacklist;
if (blacklist != null)
foreach (var item in blacklist)
if (glInterface.Renderer.Contains(item))

4
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs

@ -38,11 +38,11 @@ namespace Avalonia.LinuxFramebuffer
if (_fb is IGlOutputBackend gl)
AvaloniaLocator.CurrentMutable.Bind<IPlatformOpenGlInterface>().ToConstant(gl.PlatformOpenGlInterface);
var opts = AvaloniaLocator.Current.GetService<LinuxFramebufferPlatformOptions>();
var opts = AvaloniaLocator.Current.GetService<LinuxFramebufferPlatformOptions>() ?? new LinuxFramebufferPlatformOptions();
AvaloniaLocator.CurrentMutable
.Bind<IPlatformThreadingInterface>().ToConstant(Threading)
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(opts?.Fps ?? 60))
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(opts.Fps))
.Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<ICursorFactory>().ToTransient<CursorFactoryStub>()
.Bind<IKeyboardDevice>().ToConstant(new KeyboardDevice())

36
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<PropertyChangedEventArgs>
{
protected readonly WeakReference<object> _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<PropertyChangedEventArgs, InpcPropertyAccessor>(
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<INotifyPropertyChanged, PropertyChangedEventArgs, InpcPropertyAccessor>(
inpc,
nameof(INotifyPropertyChanged.PropertyChanged),
OnNotifyPropertyChanged);
}
WeakEvents.PropertyChanged.Subscribe(inpc, this);
}
}
internal class IndexerAccessor : InpcPropertyAccessor
internal class IndexerAccessor : InpcPropertyAccessor, IWeakEventSubscriber<NotifyCollectionChangedEventArgs>
{
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<INotifyCollectionChanged, NotifyCollectionChangedEventArgs, IndexerAccessor>(
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<NotifyCollectionChangedEventArgs, IndexerAccessor>(
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))
{

6
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);

8
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;
}
}

9
src/Windows/Avalonia.Win32/Win32GlManager.cs

@ -13,19 +13,18 @@ namespace Avalonia.Win32
{
AvaloniaLocator.CurrentMutable.Bind<IPlatformOpenGlInterface>().ToLazy<IPlatformOpenGlInterface>(() =>
{
var opts = AvaloniaLocator.Current.GetService<Win32PlatformOptions>();
if (opts?.UseWgl == true)
var opts = AvaloniaLocator.Current.GetService<Win32PlatformOptions>() ?? 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);
}

25
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<IDisposable>();
@ -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<IDisposable>();
@ -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<Clock>(),
It.IsAny<IClock>(),
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<Clock>(),
It.IsAny<IClock>(),
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<ITransition> CreateTarget()
{
return CreateTransition(Visual.OpacityProperty);

3
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);
}

12
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);

11
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);

27
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);

3
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);
}

75
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 WeakEventTests
{
class EventSource
{
public event EventHandler Event;
public void Fire()
{
Event?.Invoke(this, new EventArgs());
}
public static readonly WeakEvent<EventSource, EventArgs> WeakEv = WeakEvent.Register<EventSource>(
(t, s) => t.Event += s,
(t, s) => t.Event -= s);
}
class Subscriber : IWeakEventSubscriber<EventArgs>
{
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.WeakEv.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.WeakEv.Subscribe(source, new Subscriber(func));
}
}
}

5
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);
}

7
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);
}

15
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);

Loading…
Cancel
Save