committed by
GitHub
95 changed files with 2967 additions and 579 deletions
@ -1,5 +1,5 @@ |
|||||
<?xml version="1.0" encoding="utf-8"?> |
<?xml version="1.0" encoding="utf-8"?> |
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="ControlCatalog.Android" android:versionCode="1" android:versionName="1.0" android:installLocation="auto"> |
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="ControlCatalog.Android" android:versionCode="1" android:versionName="1.0" android:installLocation="auto"> |
||||
<uses-sdk android:targetSdkVersion="29" /> |
<uses-sdk android:targetSdkVersion="30" /> |
||||
<application android:label="ControlCatalog.Android"></application> |
<application android:label="ControlCatalog.Android"></application> |
||||
</manifest> |
</manifest> |
||||
@ -1,47 +0,0 @@ |
|||||
using Android.App; |
|
||||
using Android.OS; |
|
||||
|
|
||||
namespace Avalonia.Android |
|
||||
{ |
|
||||
internal class ActivityTracker : Java.Lang.Object, global::Android.App.Application.IActivityLifecycleCallbacks |
|
||||
{ |
|
||||
public static Activity Current { get; private set; } |
|
||||
public void OnActivityCreated(Activity activity, Bundle savedInstanceState) |
|
||||
{ |
|
||||
Current = activity; |
|
||||
} |
|
||||
|
|
||||
public void OnActivityDestroyed(Activity activity) |
|
||||
{ |
|
||||
if (Current == activity) |
|
||||
Current = null; |
|
||||
} |
|
||||
|
|
||||
public void OnActivityPaused(Activity activity) |
|
||||
{ |
|
||||
if (Current == activity) |
|
||||
Current = null; |
|
||||
} |
|
||||
|
|
||||
public void OnActivityResumed(Activity activity) |
|
||||
{ |
|
||||
Current = activity; |
|
||||
} |
|
||||
|
|
||||
public void OnActivitySaveInstanceState(Activity activity, Bundle outState) |
|
||||
{ |
|
||||
Current = activity; |
|
||||
} |
|
||||
|
|
||||
public void OnActivityStarted(Activity activity) |
|
||||
{ |
|
||||
Current = activity; |
|
||||
} |
|
||||
|
|
||||
public void OnActivityStopped(Activity activity) |
|
||||
{ |
|
||||
if (Current == activity) |
|
||||
Current = null; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,101 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Reactive.Disposables; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
using Android.OS; |
||||
|
using Android.Views; |
||||
|
|
||||
|
using Avalonia.Rendering; |
||||
|
|
||||
|
using Java.Lang; |
||||
|
|
||||
|
namespace Avalonia.Android |
||||
|
{ |
||||
|
internal sealed class ChoreographerTimer : Java.Lang.Object, IRenderTimer, Choreographer.IFrameCallback |
||||
|
{ |
||||
|
private readonly object _lock = new object(); |
||||
|
|
||||
|
private readonly Thread _thread; |
||||
|
private readonly TaskCompletionSource<Choreographer> _choreographer = new TaskCompletionSource<Choreographer>(); |
||||
|
|
||||
|
private readonly ISet<AvaloniaView> _views = new HashSet<AvaloniaView>(); |
||||
|
|
||||
|
private Action<TimeSpan> _tick; |
||||
|
private int _count; |
||||
|
|
||||
|
public ChoreographerTimer() |
||||
|
{ |
||||
|
_thread = new Thread(Loop); |
||||
|
_thread.Start(); |
||||
|
} |
||||
|
|
||||
|
public event Action<TimeSpan> Tick |
||||
|
{ |
||||
|
add |
||||
|
{ |
||||
|
lock (_lock) |
||||
|
{ |
||||
|
_tick += value; |
||||
|
_count++; |
||||
|
|
||||
|
if (_count == 1) |
||||
|
{ |
||||
|
_choreographer.Task.Result.PostFrameCallback(this); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
remove |
||||
|
{ |
||||
|
lock (_lock) |
||||
|
{ |
||||
|
_tick -= value; |
||||
|
_count--; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal IDisposable SubscribeView(AvaloniaView view) |
||||
|
{ |
||||
|
lock (_lock) |
||||
|
{ |
||||
|
_views.Add(view); |
||||
|
|
||||
|
if (_views.Count == 1) |
||||
|
{ |
||||
|
_choreographer.Task.Result.PostFrameCallback(this); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return Disposable.Create( |
||||
|
() => |
||||
|
{ |
||||
|
lock (_lock) |
||||
|
{ |
||||
|
_views.Remove(view); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private void Loop() |
||||
|
{ |
||||
|
Looper.Prepare(); |
||||
|
_choreographer.SetResult(Choreographer.Instance); |
||||
|
Looper.Loop(); |
||||
|
} |
||||
|
|
||||
|
public void DoFrame(long frameTimeNanos) |
||||
|
{ |
||||
|
_tick?.Invoke(TimeSpan.FromTicks(frameTimeNanos / 100)); |
||||
|
|
||||
|
lock (_lock) |
||||
|
{ |
||||
|
if (_count > 0 && _views.Count > 0) |
||||
|
{ |
||||
|
Choreographer.Instance.PostFrameCallback(this); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,12 +1,21 @@ |
|||||
using System; |
|
||||
using Avalonia.Input; |
using Avalonia.Input; |
||||
using Avalonia.Platform; |
using Avalonia.Platform; |
||||
|
|
||||
namespace Avalonia.Android |
namespace Avalonia.Android |
||||
{ |
{ |
||||
internal class CursorFactory : IStandardCursorFactory |
internal class CursorFactory : ICursorFactory |
||||
{ |
{ |
||||
public IPlatformHandle GetCursor(StandardCursorType cursorType) |
public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => CursorImpl.ZeroCursor; |
||||
=> new PlatformHandle(IntPtr.Zero, "ZeroCursor"); |
|
||||
|
public ICursorImpl GetCursor(StandardCursorType cursorType) => CursorImpl.ZeroCursor; |
||||
|
|
||||
|
private sealed class CursorImpl : ICursorImpl |
||||
|
{ |
||||
|
public static CursorImpl ZeroCursor { get; } = new CursorImpl(); |
||||
|
|
||||
|
private CursorImpl() { } |
||||
|
|
||||
|
public void Dispose() { } |
||||
|
} |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -1,23 +1,30 @@ |
|||||
using Avalonia.OpenGL.Egl; |
using System; |
||||
|
|
||||
|
using Avalonia.OpenGL.Egl; |
||||
using Avalonia.OpenGL.Surfaces; |
using Avalonia.OpenGL.Surfaces; |
||||
|
|
||||
namespace Avalonia.Android.OpenGL |
namespace Avalonia.Android.OpenGL |
||||
{ |
{ |
||||
internal sealed class GlRenderTarget : EglPlatformSurfaceRenderTargetBase |
internal sealed class GlRenderTarget : EglPlatformSurfaceRenderTargetBase, IGlPlatformSurfaceRenderTargetWithCorruptionInfo |
||||
{ |
{ |
||||
private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info; |
private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info; |
||||
private readonly EglSurface _surface; |
private readonly EglSurface _surface; |
||||
|
private readonly IntPtr _handle; |
||||
|
|
||||
public GlRenderTarget( |
public GlRenderTarget( |
||||
EglPlatformOpenGlInterface egl, |
EglPlatformOpenGlInterface egl, |
||||
EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info, |
EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info, |
||||
EglSurface surface) |
EglSurface surface, |
||||
|
IntPtr handle) |
||||
: base(egl) |
: base(egl) |
||||
{ |
{ |
||||
_info = info; |
_info = info; |
||||
_surface = surface; |
_surface = surface; |
||||
|
_handle = handle; |
||||
} |
} |
||||
|
|
||||
|
public bool IsCorrupted => _handle != _info.Handle; |
||||
|
|
||||
public override IGlPlatformSurfaceRenderingSession BeginDraw() => BeginDraw(_surface, _info); |
public override IGlPlatformSurfaceRenderingSession BeginDraw() => BeginDraw(_surface, _info); |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -1,14 +0,0 @@ |
|||||
using Avalonia.Input; |
|
||||
|
|
||||
namespace Avalonia.Android.Platform.Input |
|
||||
{ |
|
||||
public class AndroidMouseDevice : MouseDevice |
|
||||
{ |
|
||||
public static AndroidMouseDevice Instance { get; } = new AndroidMouseDevice(); |
|
||||
|
|
||||
public AndroidMouseDevice() |
|
||||
{ |
|
||||
|
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,11 +0,0 @@ |
|||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||
<configuration> |
|
||||
<runtime> |
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> |
|
||||
<dependentAssembly> |
|
||||
<assemblyIdentity name="System.Runtime.InteropServices.WindowsRuntime" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" /> |
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0" /> |
|
||||
</dependentAssembly> |
|
||||
</assemblyBinding> |
|
||||
</runtime> |
|
||||
</configuration> |
|
||||
@ -1,6 +1,6 @@ |
|||||
<?xml version="1.0" encoding="utf-8"?> |
<?xml version="1.0" encoding="utf-8"?> |
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="Avalonia.AndroidTestApplication" android:versionCode="1" android:versionName="1.0" android:installLocation="auto"> |
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="Avalonia.AndroidTestApplication" android:versionCode="1" android:versionName="1.0" android:installLocation="auto"> |
||||
<uses-sdk android:targetSdkVersion="29" /> |
<uses-sdk android:targetSdkVersion="30" /> |
||||
<application android:label="Avalonia.AndroidTestApplication" android:icon="@drawable/Icon" android:hardwareAccelerated="true"></application> |
<application android:label="Avalonia.AndroidTestApplication" android:icon="@drawable/Icon" android:hardwareAccelerated="true"></application> |
||||
<uses-permission android:name="android.permission.INTERNET" /> |
<uses-permission android:name="android.permission.INTERNET" /> |
||||
</manifest> |
</manifest> |
||||
@ -0,0 +1,8 @@ |
|||||
|
namespace Avalonia.PropertyStore |
||||
|
{ |
||||
|
internal interface IBatchUpdate |
||||
|
{ |
||||
|
void BeginBatchUpdate(); |
||||
|
void EndBatchUpdate(); |
||||
|
} |
||||
|
} |
||||
@ -1,6 +1,9 @@ |
|||||
Compat issues with assembly Avalonia.Controls: |
Compat issues with assembly Avalonia.Controls: |
||||
|
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseClosed()' is present in the implementation but not in the contract. |
||||
|
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseOpening()' is present in the implementation but not 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 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.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract. |
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract. |
||||
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation. |
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation. |
||||
MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. |
MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. |
||||
Total Issues: 4 |
Total Issues: 7 |
||||
|
|||||
@ -0,0 +1,16 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia.Controls |
||||
|
{ |
||||
|
|
||||
|
[Obsolete("This class exists to maintain backwards compatiblity with existing code. Use NativeMenuItemSeparator instead")] |
||||
|
public class NativeMenuItemSeperator : NativeMenuItemSeparator |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public class NativeMenuItemSeparator : NativeMenuItemBase |
||||
|
{ |
||||
|
[Obsolete("This is a temporary hack to make our MenuItem recognize this as a separator, don't use", true)] |
||||
|
public string Header => "-"; |
||||
|
} |
||||
|
} |
||||
@ -1,10 +0,0 @@ |
|||||
using System; |
|
||||
|
|
||||
namespace Avalonia.Controls |
|
||||
{ |
|
||||
public class NativeMenuItemSeperator : NativeMenuItemBase |
|
||||
{ |
|
||||
[Obsolete("This is a temporary hack to make our MenuItem recognize this as a separator, don't use", true)] |
|
||||
public string Header => "-"; |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,7 @@ |
|||||
|
namespace Avalonia.Platform |
||||
|
{ |
||||
|
public interface IApplicationPlatformEvents |
||||
|
{ |
||||
|
void RaiseUrlsOpened(string[] urls); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia |
||||
|
{ |
||||
|
public class UrlOpenedEventArgs : EventArgs |
||||
|
{ |
||||
|
public UrlOpenedEventArgs(string[] urls) |
||||
|
{ |
||||
|
Urls = urls; |
||||
|
} |
||||
|
|
||||
|
public string[] Urls { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Native.Interop; |
||||
|
using Avalonia.Platform; |
||||
|
|
||||
|
namespace Avalonia.Native |
||||
|
{ |
||||
|
internal class AvaloniaNativeApplicationPlatform : CallbackBase, IAvnApplicationEvents |
||||
|
{ |
||||
|
void IAvnApplicationEvents.FilesOpened(IAvnStringArray urls) |
||||
|
{ |
||||
|
((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(urls.ToStringArray()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,128 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Data; |
||||
|
using Avalonia.Reactive; |
||||
|
|
||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Styling |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A <see cref="Setter"/> which has been instanced on a control and whose value is lazily
|
||||
|
/// evaluated.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="T">The target property type.</typeparam>
|
||||
|
internal class PropertySetterLazyInstance<T> : SingleSubscriberObservableBase<BindingValue<T>>, |
||||
|
ISetterInstance |
||||
|
{ |
||||
|
private readonly IStyleable _target; |
||||
|
private readonly StyledPropertyBase<T>? _styledProperty; |
||||
|
private readonly DirectPropertyBase<T>? _directProperty; |
||||
|
private readonly Func<T> _valueFactory; |
||||
|
private BindingValue<T> _value; |
||||
|
private IDisposable? _subscription; |
||||
|
private bool _isActive; |
||||
|
|
||||
|
public PropertySetterLazyInstance( |
||||
|
IStyleable target, |
||||
|
StyledPropertyBase<T> property, |
||||
|
Func<T> valueFactory) |
||||
|
{ |
||||
|
_target = target; |
||||
|
_styledProperty = property; |
||||
|
_valueFactory = valueFactory; |
||||
|
} |
||||
|
|
||||
|
public PropertySetterLazyInstance( |
||||
|
IStyleable target, |
||||
|
DirectPropertyBase<T> property, |
||||
|
Func<T> valueFactory) |
||||
|
{ |
||||
|
_target = target; |
||||
|
_directProperty = property; |
||||
|
_valueFactory = valueFactory; |
||||
|
} |
||||
|
|
||||
|
public void Start(bool hasActivator) |
||||
|
{ |
||||
|
_isActive = !hasActivator; |
||||
|
|
||||
|
if (_styledProperty is object) |
||||
|
{ |
||||
|
var priority = hasActivator ? BindingPriority.StyleTrigger : BindingPriority.Style; |
||||
|
_subscription = _target.Bind(_styledProperty, this, priority); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_subscription = _target.Bind(_directProperty, this); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Activate() |
||||
|
{ |
||||
|
if (!_isActive) |
||||
|
{ |
||||
|
_isActive = true; |
||||
|
PublishNext(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Deactivate() |
||||
|
{ |
||||
|
if (_isActive) |
||||
|
{ |
||||
|
_isActive = false; |
||||
|
PublishNext(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public override void Dispose() |
||||
|
{ |
||||
|
if (_subscription is object) |
||||
|
{ |
||||
|
var sub = _subscription; |
||||
|
_subscription = null; |
||||
|
sub.Dispose(); |
||||
|
} |
||||
|
else if (_isActive) |
||||
|
{ |
||||
|
if (_styledProperty is object) |
||||
|
{ |
||||
|
_target.ClearValue(_styledProperty); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_target.ClearValue(_directProperty); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
base.Dispose(); |
||||
|
} |
||||
|
|
||||
|
protected override void Subscribed() => PublishNext(); |
||||
|
protected override void Unsubscribed() { } |
||||
|
|
||||
|
private T GetValue() |
||||
|
{ |
||||
|
if (_value.HasValue) |
||||
|
{ |
||||
|
return _value.Value; |
||||
|
} |
||||
|
|
||||
|
_value = _valueFactory(); |
||||
|
return _value.Value; |
||||
|
} |
||||
|
|
||||
|
private void PublishNext() |
||||
|
{ |
||||
|
if (_isActive) |
||||
|
{ |
||||
|
GetValue(); |
||||
|
PublishNext(_value); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
PublishNext(default); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1 +1 @@ |
|||||
Subproject commit f3ca2028f4f64be3556a6afd22f192902de095e5 |
Subproject commit 9e90d34e97c766ba8dcb70128147fcded65d195a |
||||
@ -0,0 +1,616 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Reactive; |
||||
|
using System.Reactive.Disposables; |
||||
|
using System.Reactive.Linq; |
||||
|
using System.Text; |
||||
|
using Avalonia.Data; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Base.UnitTests |
||||
|
{ |
||||
|
public class AvaloniaObjectTests_BatchUpdate |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void SetValue_Should_Not_Raise_Property_Changes_During_Batch_Update() |
||||
|
{ |
||||
|
var target = new TestClass(); |
||||
|
var raised = new List<string>(); |
||||
|
|
||||
|
target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x)); |
||||
|
target.BeginBatchUpdate(); |
||||
|
target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue); |
||||
|
|
||||
|
Assert.Empty(raised); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Binding_Should_Not_Raise_Property_Changes_During_Batch_Update() |
||||
|
{ |
||||
|
var target = new TestClass(); |
||||
|
var observable = new TestObservable<string>("foo"); |
||||
|
var raised = new List<string>(); |
||||
|
|
||||
|
target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x)); |
||||
|
target.BeginBatchUpdate(); |
||||
|
target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue); |
||||
|
|
||||
|
Assert.Empty(raised); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Binding_Completion_Should_Not_Raise_Property_Changes_During_Batch_Update() |
||||
|
{ |
||||
|
var target = new TestClass(); |
||||
|
var observable = new TestObservable<string>("foo"); |
||||
|
var raised = new List<string>(); |
||||
|
|
||||
|
target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue); |
||||
|
target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x)); |
||||
|
target.BeginBatchUpdate(); |
||||
|
observable.OnCompleted(); |
||||
|
|
||||
|
Assert.Empty(raised); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Binding_Disposal_Should_Not_Raise_Property_Changes_During_Batch_Update() |
||||
|
{ |
||||
|
var target = new TestClass(); |
||||
|
var observable = new TestObservable<string>("foo"); |
||||
|
var raised = new List<string>(); |
||||
|
|
||||
|
var sub = target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue); |
||||
|
target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x)); |
||||
|
target.BeginBatchUpdate(); |
||||
|
sub.Dispose(); |
||||
|
|
||||
|
Assert.Empty(raised); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void SetValue_Change_Should_Be_Raised_After_Batch_Update_1() |
||||
|
{ |
||||
|
var target = new TestClass(); |
||||
|
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
||||
|
|
||||
|
target.PropertyChanged += (s, e) => raised.Add(e); |
||||
|
|
||||
|
target.BeginBatchUpdate(); |
||||
|
target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue); |
||||
|
target.EndBatchUpdate(); |
||||
|
|
||||
|
Assert.Equal(1, raised.Count); |
||||
|
Assert.Equal("foo", target.Foo); |
||||
|
Assert.Null(raised[0].OldValue); |
||||
|
Assert.Equal("foo", raised[0].NewValue); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void SetValue_Change_Should_Be_Raised_After_Batch_Update_2() |
||||
|
{ |
||||
|
var target = new TestClass(); |
||||
|
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
||||
|
|
||||
|
target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue); |
||||
|
target.PropertyChanged += (s, e) => raised.Add(e); |
||||
|
|
||||
|
target.BeginBatchUpdate(); |
||||
|
target.SetValue(TestClass.FooProperty, "bar", BindingPriority.LocalValue); |
||||
|
target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue); |
||||
|
target.EndBatchUpdate(); |
||||
|
|
||||
|
Assert.Equal(1, raised.Count); |
||||
|
Assert.Equal("baz", target.Foo); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void SetValue_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update() |
||||
|
{ |
||||
|
var target = new TestClass(); |
||||
|
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
||||
|
|
||||
|
target.PropertyChanged += (s, e) => raised.Add(e); |
||||
|
|
||||
|
target.BeginBatchUpdate(); |
||||
|
target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue); |
||||
|
target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue); |
||||
|
target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue); |
||||
|
target.EndBatchUpdate(); |
||||
|
|
||||
|
Assert.Equal(2, raised.Count); |
||||
|
Assert.Equal(TestClass.BarProperty, raised[0].Property); |
||||
|
Assert.Equal(TestClass.FooProperty, raised[1].Property); |
||||
|
Assert.Equal("baz", target.Foo); |
||||
|
Assert.Equal("bar", target.Bar); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void SetValue_And_Binding_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update_1() |
||||
|
{ |
||||
|
var target = new TestClass(); |
||||
|
var observable = new TestObservable<string>("baz"); |
||||
|
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
||||
|
|
||||
|
target.PropertyChanged += (s, e) => raised.Add(e); |
||||
|
|
||||
|
target.BeginBatchUpdate(); |
||||
|
target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue); |
||||
|
target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue); |
||||
|
target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue); |
||||
|
target.EndBatchUpdate(); |
||||
|
|
||||
|
Assert.Equal(2, raised.Count); |
||||
|
Assert.Equal(TestClass.BarProperty, raised[0].Property); |
||||
|
Assert.Equal(TestClass.FooProperty, raised[1].Property); |
||||
|
Assert.Equal("baz", target.Foo); |
||||
|
Assert.Equal("bar", target.Bar); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void SetValue_And_Binding_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update_2() |
||||
|
{ |
||||
|
var target = new TestClass(); |
||||
|
var observable = new TestObservable<string>("foo"); |
||||
|
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
||||
|
|
||||
|
target.PropertyChanged += (s, e) => raised.Add(e); |
||||
|
|
||||
|
target.BeginBatchUpdate(); |
||||
|
target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue); |
||||
|
target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue); |
||||
|
target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue); |
||||
|
target.EndBatchUpdate(); |
||||
|
|
||||
|
Assert.Equal(2, raised.Count); |
||||
|
Assert.Equal(TestClass.BarProperty, raised[0].Property); |
||||
|
Assert.Equal(TestClass.FooProperty, raised[1].Property); |
||||
|
Assert.Equal("baz", target.Foo); |
||||
|
Assert.Equal("bar", target.Bar); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void SetValue_And_Binding_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update_3() |
||||
|
{ |
||||
|
var target = new TestClass(); |
||||
|
var observable1 = new TestObservable<string>("foo"); |
||||
|
var observable2 = new TestObservable<string>("qux"); |
||||
|
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
||||
|
|
||||
|
target.PropertyChanged += (s, e) => raised.Add(e); |
||||
|
|
||||
|
target.BeginBatchUpdate(); |
||||
|
target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue); |
||||
|
target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue); |
||||
|
target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue); |
||||
|
target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue); |
||||
|
target.EndBatchUpdate(); |
||||
|
|
||||
|
Assert.Equal(2, raised.Count); |
||||
|
Assert.Equal(TestClass.BarProperty, raised[0].Property); |
||||
|
Assert.Equal(TestClass.FooProperty, raised[1].Property); |
||||
|
Assert.Equal("baz", target.Foo); |
||||
|
Assert.Equal("bar", target.Bar); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Binding_Change_Should_Be_Raised_After_Batch_Update_1() |
||||
|
{ |
||||
|
var target = new TestClass(); |
||||
|
var observable = new TestObservable<string>("foo"); |
||||
|
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
||||
|
|
||||
|
target.PropertyChanged += (s, e) => raised.Add(e); |
||||
|
|
||||
|
target.BeginBatchUpdate(); |
||||
|
target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue); |
||||
|
target.EndBatchUpdate(); |
||||
|
|
||||
|
Assert.Equal(1, raised.Count); |
||||
|
Assert.Equal("foo", target.Foo); |
||||
|
Assert.Null(raised[0].OldValue); |
||||
|
Assert.Equal("foo", raised[0].NewValue); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Binding_Change_Should_Be_Raised_After_Batch_Update_2() |
||||
|
{ |
||||
|
var target = new TestClass(); |
||||
|
var observable1 = new TestObservable<string>("bar"); |
||||
|
var observable2 = new TestObservable<string>("baz"); |
||||
|
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
||||
|
|
||||
|
target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue); |
||||
|
target.PropertyChanged += (s, e) => raised.Add(e); |
||||
|
|
||||
|
target.BeginBatchUpdate(); |
||||
|
target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue); |
||||
|
target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue); |
||||
|
target.EndBatchUpdate(); |
||||
|
|
||||
|
Assert.Equal(1, raised.Count); |
||||
|
Assert.Equal("baz", target.Foo); |
||||
|
Assert.Equal("foo", raised[0].OldValue); |
||||
|
Assert.Equal("baz", raised[0].NewValue); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Binding_Completion_Should_Be_Raised_After_Batch_Update() |
||||
|
{ |
||||
|
var target = new TestClass(); |
||||
|
var observable = new TestObservable<string>("foo"); |
||||
|
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
||||
|
|
||||
|
target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue); |
||||
|
target.PropertyChanged += (s, e) => raised.Add(e); |
||||
|
|
||||
|
target.BeginBatchUpdate(); |
||||
|
observable.OnCompleted(); |
||||
|
target.EndBatchUpdate(); |
||||
|
|
||||
|
Assert.Equal(1, raised.Count); |
||||
|
Assert.Null(target.Foo); |
||||
|
Assert.Equal("foo", raised[0].OldValue); |
||||
|
Assert.Null(raised[0].NewValue); |
||||
|
Assert.Equal(BindingPriority.Unset, raised[0].Priority); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Binding_Disposal_Should_Be_Raised_After_Batch_Update() |
||||
|
{ |
||||
|
var target = new TestClass(); |
||||
|
var observable = new TestObservable<string>("foo"); |
||||
|
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
||||
|
|
||||
|
var sub = target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue); |
||||
|
target.PropertyChanged += (s, e) => raised.Add(e); |
||||
|
|
||||
|
target.BeginBatchUpdate(); |
||||
|
sub.Dispose(); |
||||
|
target.EndBatchUpdate(); |
||||
|
|
||||
|
Assert.Equal(1, raised.Count); |
||||
|
Assert.Null(target.Foo); |
||||
|
Assert.Equal("foo", raised[0].OldValue); |
||||
|
Assert.Null(raised[0].NewValue); |
||||
|
Assert.Equal(BindingPriority.Unset, raised[0].Priority); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void ClearValue_Change_Should_Be_Raised_After_Batch_Update_1() |
||||
|
{ |
||||
|
var target = new TestClass(); |
||||
|
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
||||
|
|
||||
|
target.Foo = "foo"; |
||||
|
target.PropertyChanged += (s, e) => raised.Add(e); |
||||
|
|
||||
|
target.BeginBatchUpdate(); |
||||
|
target.ClearValue(TestClass.FooProperty); |
||||
|
target.EndBatchUpdate(); |
||||
|
|
||||
|
Assert.Equal(1, raised.Count); |
||||
|
Assert.Null(target.Foo); |
||||
|
Assert.Equal("foo", raised[0].OldValue); |
||||
|
Assert.Null(raised[0].NewValue); |
||||
|
Assert.Equal(BindingPriority.Unset, raised[0].Priority); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Bindings_Should_Be_Subscribed_Before_Batch_Update() |
||||
|
{ |
||||
|
var target = new TestClass(); |
||||
|
var observable1 = new TestObservable<string>("foo"); |
||||
|
var observable2 = new TestObservable<string>("bar"); |
||||
|
|
||||
|
target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue); |
||||
|
target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue); |
||||
|
|
||||
|
Assert.Equal(1, observable1.SubscribeCount); |
||||
|
Assert.Equal(1, observable2.SubscribeCount); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Non_Active_Binding_Should_Not_Be_Subscribed_Before_Batch_Update() |
||||
|
{ |
||||
|
var target = new TestClass(); |
||||
|
var observable1 = new TestObservable<string>("foo"); |
||||
|
var observable2 = new TestObservable<string>("bar"); |
||||
|
|
||||
|
target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue); |
||||
|
target.Bind(TestClass.FooProperty, observable2, BindingPriority.Style); |
||||
|
|
||||
|
Assert.Equal(1, observable1.SubscribeCount); |
||||
|
Assert.Equal(0, observable2.SubscribeCount); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void LocalValue_Bindings_Should_Be_Subscribed_During_Batch_Update() |
||||
|
{ |
||||
|
var target = new TestClass(); |
||||
|
var observable1 = new TestObservable<string>("foo"); |
||||
|
var observable2 = new TestObservable<string>("bar"); |
||||
|
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
||||
|
|
||||
|
target.PropertyChanged += (s, e) => raised.Add(e); |
||||
|
|
||||
|
// We need to subscribe to LocalValue bindings even if we've got a batch operation
|
||||
|
// in progress because otherwise we don't know whether the binding or a subsequent
|
||||
|
// SetValue with local priority will win. Notifications however shouldn't be sent.
|
||||
|
target.BeginBatchUpdate(); |
||||
|
target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue); |
||||
|
target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue); |
||||
|
|
||||
|
Assert.Equal(1, observable1.SubscribeCount); |
||||
|
Assert.Equal(1, observable2.SubscribeCount); |
||||
|
Assert.Empty(raised); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Style_Bindings_Should_Not_Be_Subscribed_During_Batch_Update() |
||||
|
{ |
||||
|
var target = new TestClass(); |
||||
|
var observable1 = new TestObservable<string>("foo"); |
||||
|
var observable2 = new TestObservable<string>("bar"); |
||||
|
|
||||
|
target.BeginBatchUpdate(); |
||||
|
target.Bind(TestClass.FooProperty, observable1, BindingPriority.Style); |
||||
|
target.Bind(TestClass.FooProperty, observable2, BindingPriority.StyleTrigger); |
||||
|
|
||||
|
Assert.Equal(0, observable1.SubscribeCount); |
||||
|
Assert.Equal(0, observable2.SubscribeCount); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Active_Style_Binding_Should_Be_Subscribed_After_Batch_Uppdate_1() |
||||
|
{ |
||||
|
var target = new TestClass(); |
||||
|
var observable1 = new TestObservable<string>("foo"); |
||||
|
var observable2 = new TestObservable<string>("bar"); |
||||
|
|
||||
|
target.BeginBatchUpdate(); |
||||
|
target.Bind(TestClass.FooProperty, observable1, BindingPriority.Style); |
||||
|
target.Bind(TestClass.FooProperty, observable2, BindingPriority.Style); |
||||
|
target.EndBatchUpdate(); |
||||
|
|
||||
|
Assert.Equal(0, observable1.SubscribeCount); |
||||
|
Assert.Equal(1, observable2.SubscribeCount); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Active_Style_Binding_Should_Be_Subscribed_After_Batch_Uppdate_2() |
||||
|
{ |
||||
|
var target = new TestClass(); |
||||
|
var observable1 = new TestObservable<string>("foo"); |
||||
|
var observable2 = new TestObservable<string>("bar"); |
||||
|
|
||||
|
target.BeginBatchUpdate(); |
||||
|
target.Bind(TestClass.FooProperty, observable1, BindingPriority.StyleTrigger); |
||||
|
target.Bind(TestClass.FooProperty, observable2, BindingPriority.Style); |
||||
|
target.EndBatchUpdate(); |
||||
|
|
||||
|
Assert.Equal(1, observable1.SubscribeCount); |
||||
|
Assert.Equal(0, observable2.SubscribeCount); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Change_Can_Be_Triggered_By_Ending_Batch_Update_1() |
||||
|
{ |
||||
|
var target = new TestClass(); |
||||
|
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
||||
|
|
||||
|
target.PropertyChanged += (s, e) => raised.Add(e); |
||||
|
|
||||
|
target.BeginBatchUpdate(); |
||||
|
target.Foo = "foo"; |
||||
|
|
||||
|
target.PropertyChanged += (s, e) => |
||||
|
{ |
||||
|
if (e.Property == TestClass.FooProperty && (string)e.NewValue == "foo") |
||||
|
target.Bar = "bar"; |
||||
|
}; |
||||
|
|
||||
|
target.EndBatchUpdate(); |
||||
|
|
||||
|
Assert.Equal("foo", target.Foo); |
||||
|
Assert.Equal("bar", target.Bar); |
||||
|
Assert.Equal(2, raised.Count); |
||||
|
Assert.Equal(TestClass.FooProperty, raised[0].Property); |
||||
|
Assert.Equal(TestClass.BarProperty, raised[1].Property); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Change_Can_Be_Triggered_By_Ending_Batch_Update_2() |
||||
|
{ |
||||
|
var target = new TestClass(); |
||||
|
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
||||
|
|
||||
|
target.PropertyChanged += (s, e) => raised.Add(e); |
||||
|
|
||||
|
target.BeginBatchUpdate(); |
||||
|
target.Foo = "foo"; |
||||
|
target.Bar = "baz"; |
||||
|
|
||||
|
target.PropertyChanged += (s, e) => |
||||
|
{ |
||||
|
if (e.Property == TestClass.FooProperty && (string)e.NewValue == "foo") |
||||
|
target.Bar = "bar"; |
||||
|
}; |
||||
|
|
||||
|
target.EndBatchUpdate(); |
||||
|
|
||||
|
Assert.Equal("foo", target.Foo); |
||||
|
Assert.Equal("bar", target.Bar); |
||||
|
Assert.Equal(2, raised.Count); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Batch_Update_Can_Be_Triggered_By_Ending_Batch_Update() |
||||
|
{ |
||||
|
var target = new TestClass(); |
||||
|
var raised = new List<AvaloniaPropertyChangedEventArgs>(); |
||||
|
|
||||
|
target.PropertyChanged += (s, e) => raised.Add(e); |
||||
|
|
||||
|
target.BeginBatchUpdate(); |
||||
|
target.Foo = "foo"; |
||||
|
target.Bar = "baz"; |
||||
|
|
||||
|
// Simulates the following scenario:
|
||||
|
// - A control is added to the logical tree
|
||||
|
// - A batch update is started to apply styles
|
||||
|
// - Ending the batch update triggers something which removes the control from the logical tree
|
||||
|
// - A new batch update is started to detach styles
|
||||
|
target.PropertyChanged += (s, e) => |
||||
|
{ |
||||
|
if (e.Property == TestClass.FooProperty && (string)e.NewValue == "foo") |
||||
|
{ |
||||
|
target.BeginBatchUpdate(); |
||||
|
target.ClearValue(TestClass.FooProperty); |
||||
|
target.ClearValue(TestClass.BarProperty); |
||||
|
target.EndBatchUpdate(); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
target.EndBatchUpdate(); |
||||
|
|
||||
|
Assert.Null(target.Foo); |
||||
|
Assert.Null(target.Bar); |
||||
|
Assert.Equal(2, raised.Count); |
||||
|
Assert.Equal(TestClass.FooProperty, raised[0].Property); |
||||
|
Assert.Null(raised[0].OldValue); |
||||
|
Assert.Equal("foo", raised[0].NewValue); |
||||
|
Assert.Equal(TestClass.FooProperty, raised[1].Property); |
||||
|
Assert.Equal("foo", raised[1].OldValue); |
||||
|
Assert.Null(raised[1].NewValue); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Can_Set_Cleared_Value_When_Ending_Batch_Update() |
||||
|
{ |
||||
|
var target = new TestClass(); |
||||
|
var raised = 0; |
||||
|
|
||||
|
target.Foo = "foo"; |
||||
|
|
||||
|
target.BeginBatchUpdate(); |
||||
|
target.ClearValue(TestClass.FooProperty); |
||||
|
target.PropertyChanged += (sender, e) => |
||||
|
{ |
||||
|
if (e.Property == TestClass.FooProperty && e.NewValue is null) |
||||
|
{ |
||||
|
target.Foo = "bar"; |
||||
|
++raised; |
||||
|
} |
||||
|
}; |
||||
|
target.EndBatchUpdate(); |
||||
|
|
||||
|
Assert.Equal("bar", target.Foo); |
||||
|
Assert.Equal(1, raised); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Can_Bind_Cleared_Value_When_Ending_Batch_Update() |
||||
|
{ |
||||
|
var target = new TestClass(); |
||||
|
var raised = 0; |
||||
|
var notifications = new List<AvaloniaPropertyChangedEventArgs>(); |
||||
|
|
||||
|
target.Foo = "foo"; |
||||
|
|
||||
|
target.BeginBatchUpdate(); |
||||
|
target.ClearValue(TestClass.FooProperty); |
||||
|
target.PropertyChanged += (sender, e) => |
||||
|
{ |
||||
|
if (e.Property == TestClass.FooProperty && e.NewValue is null) |
||||
|
{ |
||||
|
target.Bind(TestClass.FooProperty, new TestObservable<string>("bar")); |
||||
|
++raised; |
||||
|
} |
||||
|
|
||||
|
notifications.Add(e); |
||||
|
}; |
||||
|
target.EndBatchUpdate(); |
||||
|
|
||||
|
Assert.Equal("bar", target.Foo); |
||||
|
Assert.Equal(1, raised); |
||||
|
Assert.Equal(2, notifications.Count); |
||||
|
Assert.Equal(null, notifications[0].NewValue); |
||||
|
Assert.Equal("bar", notifications[1].NewValue); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Can_Bind_Completed_Binding_Back_To_Original_Value_When_Ending_Batch_Update() |
||||
|
{ |
||||
|
var target = new TestClass(); |
||||
|
var raised = 0; |
||||
|
var notifications = new List<AvaloniaPropertyChangedEventArgs>(); |
||||
|
var observable1 = new TestObservable<string>("foo"); |
||||
|
var observable2 = new TestObservable<string>("foo"); |
||||
|
|
||||
|
target.Bind(TestClass.FooProperty, observable1); |
||||
|
|
||||
|
target.BeginBatchUpdate(); |
||||
|
observable1.OnCompleted(); |
||||
|
target.PropertyChanged += (sender, e) => |
||||
|
{ |
||||
|
if (e.Property == TestClass.FooProperty && e.NewValue is null) |
||||
|
{ |
||||
|
target.Bind(TestClass.FooProperty, observable2); |
||||
|
++raised; |
||||
|
} |
||||
|
|
||||
|
notifications.Add(e); |
||||
|
}; |
||||
|
target.EndBatchUpdate(); |
||||
|
|
||||
|
Assert.Equal("foo", target.Foo); |
||||
|
Assert.Equal(1, raised); |
||||
|
Assert.Equal(2, notifications.Count); |
||||
|
Assert.Equal(null, notifications[0].NewValue); |
||||
|
Assert.Equal("foo", notifications[1].NewValue); |
||||
|
} |
||||
|
|
||||
|
public class TestClass : AvaloniaObject |
||||
|
{ |
||||
|
public static readonly StyledProperty<string> FooProperty = |
||||
|
AvaloniaProperty.Register<TestClass, string>(nameof(Foo)); |
||||
|
|
||||
|
public static readonly StyledProperty<string> BarProperty = |
||||
|
AvaloniaProperty.Register<TestClass, string>(nameof(Bar)); |
||||
|
|
||||
|
public string Foo |
||||
|
{ |
||||
|
get => GetValue(FooProperty); |
||||
|
set => SetValue(FooProperty, value); |
||||
|
} |
||||
|
|
||||
|
public string Bar |
||||
|
{ |
||||
|
get => GetValue(BarProperty); |
||||
|
set => SetValue(BarProperty, value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class TestObservable<T> : ObservableBase<BindingValue<T>> |
||||
|
{ |
||||
|
private readonly T _value; |
||||
|
private IObserver<BindingValue<T>> _observer; |
||||
|
|
||||
|
public TestObservable(T value) => _value = value; |
||||
|
|
||||
|
public int SubscribeCount { get; private set; } |
||||
|
|
||||
|
public void OnCompleted() => _observer.OnCompleted(); |
||||
|
public void OnError(Exception e) => _observer.OnError(e); |
||||
|
|
||||
|
protected override IDisposable SubscribeCore(IObserver<BindingValue<T>> observer) |
||||
|
{ |
||||
|
++SubscribeCount; |
||||
|
_observer = observer; |
||||
|
observer.OnNext(_value); |
||||
|
return Disposable.Empty; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,74 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
using Avalonia.Markup.Xaml.Styling; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Shared.PlatformSupport; |
||||
|
using Avalonia.Styling; |
||||
|
using Avalonia.UnitTests; |
||||
|
using BenchmarkDotNet.Attributes; |
||||
|
using Moq; |
||||
|
|
||||
|
namespace Avalonia.Benchmarks.Themes |
||||
|
{ |
||||
|
[MemoryDiagnoser] |
||||
|
public class FluentBenchmark |
||||
|
{ |
||||
|
private readonly IDisposable _app; |
||||
|
private readonly TestRoot _root; |
||||
|
|
||||
|
public FluentBenchmark() |
||||
|
{ |
||||
|
_app = CreateApp(); |
||||
|
_root = new TestRoot(true, null) |
||||
|
{ |
||||
|
Renderer = new NullRenderer() |
||||
|
}; |
||||
|
|
||||
|
_root.LayoutManager.ExecuteInitialLayoutPass(); |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
_app.Dispose(); |
||||
|
} |
||||
|
|
||||
|
[Benchmark] |
||||
|
public void RepeatButton() |
||||
|
{ |
||||
|
var button = new RepeatButton(); |
||||
|
_root.Child = button; |
||||
|
_root.LayoutManager.ExecuteLayoutPass(); |
||||
|
} |
||||
|
|
||||
|
private static IDisposable CreateApp() |
||||
|
{ |
||||
|
var services = new TestServices( |
||||
|
assetLoader: new AssetLoader(), |
||||
|
globalClock: new MockGlobalClock(), |
||||
|
platform: new AppBuilder().RuntimePlatform, |
||||
|
renderInterface: new MockPlatformRenderInterface(), |
||||
|
standardCursorFactory: Mock.Of<ICursorFactory>(), |
||||
|
styler: new Styler(), |
||||
|
theme: () => LoadFluentTheme(), |
||||
|
threadingInterface: new NullThreadingPlatform(), |
||||
|
fontManagerImpl: new MockFontManagerImpl(), |
||||
|
textShaperImpl: new MockTextShaperImpl(), |
||||
|
windowingPlatform: new MockWindowingPlatform()); |
||||
|
|
||||
|
return UnitTestApplication.Start(services); |
||||
|
} |
||||
|
|
||||
|
private static Styles LoadFluentTheme() |
||||
|
{ |
||||
|
AssetLoader.RegisterResUriParsers(); |
||||
|
return new Styles |
||||
|
{ |
||||
|
new Avalonia.Themes.Fluent.FluentTheme(new Uri("avares://Avalonia.Benchmarks")) |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,95 @@ |
|||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Reactive.Subjects; |
||||
|
using Avalonia.Controls.Templates; |
||||
|
using Avalonia.Data; |
||||
|
using Avalonia.Threading; |
||||
|
using Avalonia.UnitTests; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Controls.UnitTests |
||||
|
{ |
||||
|
public class NumericUpDownTests |
||||
|
{ |
||||
|
private static TestServices Services => TestServices.StyledWindow; |
||||
|
|
||||
|
[Fact] |
||||
|
public void Text_Validation() |
||||
|
{ |
||||
|
RunTest((control, textbox) => |
||||
|
{ |
||||
|
var exception = new InvalidCastException("failed validation"); |
||||
|
var textObservable = new BehaviorSubject<BindingNotification>(new BindingNotification(exception, BindingErrorType.DataValidationError)); |
||||
|
control.Bind(NumericUpDown.TextProperty, textObservable); |
||||
|
Dispatcher.UIThread.RunJobs(); |
||||
|
|
||||
|
Assert.True(DataValidationErrors.GetHasErrors(control)); |
||||
|
Assert.True(DataValidationErrors.GetErrors(control).SequenceEqual(new[] { exception })); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Value_Validation() |
||||
|
{ |
||||
|
RunTest((control, textbox) => |
||||
|
{ |
||||
|
var exception = new InvalidCastException("failed validation"); |
||||
|
var valueObservable = new BehaviorSubject<BindingNotification>(new BindingNotification(exception, BindingErrorType.DataValidationError)); |
||||
|
control.Bind(NumericUpDown.ValueProperty, valueObservable); |
||||
|
Dispatcher.UIThread.RunJobs(); |
||||
|
|
||||
|
Assert.True(DataValidationErrors.GetHasErrors(control)); |
||||
|
Assert.True(DataValidationErrors.GetErrors(control).SequenceEqual(new[] { exception })); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private void RunTest(Action<NumericUpDown, TextBox> test) |
||||
|
{ |
||||
|
using (UnitTestApplication.Start(Services)) |
||||
|
{ |
||||
|
var control = CreateControl(); |
||||
|
TextBox textBox = GetTextBox(control); |
||||
|
var window = new Window { Content = control }; |
||||
|
window.ApplyTemplate(); |
||||
|
window.Presenter.ApplyTemplate(); |
||||
|
Dispatcher.UIThread.RunJobs(); |
||||
|
test.Invoke(control, textBox); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private NumericUpDown CreateControl() |
||||
|
{ |
||||
|
var control = new NumericUpDown |
||||
|
{ |
||||
|
Template = CreateTemplate() |
||||
|
}; |
||||
|
|
||||
|
control.ApplyTemplate(); |
||||
|
return control; |
||||
|
} |
||||
|
private TextBox GetTextBox(NumericUpDown control) |
||||
|
{ |
||||
|
return control.GetTemplateChildren() |
||||
|
.OfType<ButtonSpinner>() |
||||
|
.Select(b => b.Content) |
||||
|
.OfType<TextBox>() |
||||
|
.First(); |
||||
|
} |
||||
|
private IControlTemplate CreateTemplate() |
||||
|
{ |
||||
|
return new FuncControlTemplate<NumericUpDown>((control, scope) => |
||||
|
{ |
||||
|
var textBox = |
||||
|
new TextBox |
||||
|
{ |
||||
|
Name = "PART_TextBox" |
||||
|
}.RegisterInNameScope(scope); |
||||
|
return new ButtonSpinner |
||||
|
{ |
||||
|
Name = "PART_Spinner", |
||||
|
Content = textBox, |
||||
|
}.RegisterInNameScope(scope); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue