committed by
GitHub
95 changed files with 2967 additions and 579 deletions
@ -1,5 +1,5 @@ |
|||
<?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"> |
|||
<uses-sdk android:targetSdkVersion="29" /> |
|||
<uses-sdk android:targetSdkVersion="30" /> |
|||
<application android:label="ControlCatalog.Android"></application> |
|||
</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.Platform; |
|||
|
|||
namespace Avalonia.Android |
|||
{ |
|||
internal class CursorFactory : IStandardCursorFactory |
|||
internal class CursorFactory : ICursorFactory |
|||
{ |
|||
public IPlatformHandle GetCursor(StandardCursorType cursorType) |
|||
=> new PlatformHandle(IntPtr.Zero, "ZeroCursor"); |
|||
public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => CursorImpl.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; |
|||
|
|||
namespace Avalonia.Android.OpenGL |
|||
{ |
|||
internal sealed class GlRenderTarget : EglPlatformSurfaceRenderTargetBase |
|||
internal sealed class GlRenderTarget : EglPlatformSurfaceRenderTargetBase, IGlPlatformSurfaceRenderTargetWithCorruptionInfo |
|||
{ |
|||
private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info; |
|||
private readonly EglSurface _surface; |
|||
private readonly IntPtr _handle; |
|||
|
|||
public GlRenderTarget( |
|||
EglPlatformOpenGlInterface egl, |
|||
EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info, |
|||
EglSurface surface) |
|||
EglSurface surface, |
|||
IntPtr handle) |
|||
: base(egl) |
|||
{ |
|||
_info = info; |
|||
_surface = surface; |
|||
_handle = handle; |
|||
} |
|||
|
|||
public bool IsCorrupted => _handle != _info.Handle; |
|||
|
|||
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"?> |
|||
<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> |
|||
<uses-permission android:name="android.permission.INTERNET" /> |
|||
</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: |
|||
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 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.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. |
|||
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