diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj
index 20ca0576d4..1a68c4d732 100644
--- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj
+++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj
@@ -16,7 +16,7 @@
Resources\Resource.Designer.cs
Off
False
- v10.0
+ v11.0
Properties\AndroidManifest.xml
diff --git a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml
index 02e97f3065..9effda7e79 100644
--- a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml
+++ b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/src/Android/Avalonia.Android/ActivityTracker.cs b/src/Android/Avalonia.Android/ActivityTracker.cs
deleted file mode 100644
index 2ad1d9e361..0000000000
--- a/src/Android/Avalonia.Android/ActivityTracker.cs
+++ /dev/null
@@ -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;
- }
- }
-}
\ No newline at end of file
diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs
index e0ceb0c8b7..5e11d8eab2 100644
--- a/src/Android/Avalonia.Android/AndroidPlatform.cs
+++ b/src/Android/Avalonia.Android/AndroidPlatform.cs
@@ -29,60 +29,42 @@ namespace Avalonia
namespace Avalonia.Android
{
- class AndroidPlatform : IPlatformSettings, IWindowingPlatform
+ class AndroidPlatform : IPlatformSettings
{
public static readonly AndroidPlatform Instance = new AndroidPlatform();
+ public static AndroidPlatformOptions Options { get; private set; }
public Size DoubleClickSize => new Size(4, 4);
public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(200);
- public double RenderScalingFactor => _scalingFactor;
- public double LayoutScalingFactor => _scalingFactor;
-
- private readonly double _scalingFactor = 1;
-
- public AndroidPlatform()
- {
- _scalingFactor = global::Android.App.Application.Context.Resources.DisplayMetrics.ScaledDensity;
- }
public static void Initialize(Type appType, AndroidPlatformOptions options)
{
+ Options = options;
+
AvaloniaLocator.CurrentMutable
.Bind().ToTransient()
- .Bind().ToTransient()
+ .Bind().ToTransient()
.Bind().ToSingleton()
.Bind().ToConstant(Instance)
.Bind().ToConstant(new AndroidThreadingInterface())
.Bind().ToTransient()
- .Bind().ToConstant(Instance)
.Bind().ToSingleton()
- .Bind().ToConstant(new DefaultRenderTimer(60))
+ .Bind().ToConstant(new ChoreographerTimer())
.Bind().ToConstant(new RenderLoop())
.Bind().ToSingleton()
.Bind().ToConstant(new AssetLoader(appType.Assembly));
SkiaPlatform.Initialize();
- ((global::Android.App.Application) global::Android.App.Application.Context.ApplicationContext)
- .RegisterActivityLifecycleCallbacks(new ActivityTracker());
if (options.UseGpu)
{
EglPlatformOpenGlInterface.TryInitialize();
}
}
-
- public IWindowImpl CreateWindow()
- {
- throw new NotSupportedException();
- }
-
- public IWindowImpl CreateEmbeddableWindow()
- {
- throw new NotSupportedException();
- }
}
public sealed class AndroidPlatformOptions
{
+ public bool UseDeferredRendering { get; set; } = true;
public bool UseGpu { get; set; } = true;
}
}
diff --git a/src/Android/Avalonia.Android/AndroidThreadingInterface.cs b/src/Android/Avalonia.Android/AndroidThreadingInterface.cs
index 6fe77adca1..e72f0aed90 100644
--- a/src/Android/Avalonia.Android/AndroidThreadingInterface.cs
+++ b/src/Android/Avalonia.Android/AndroidThreadingInterface.cs
@@ -1,25 +1,26 @@
using System;
using System.Reactive.Disposables;
using System.Threading;
+
using Android.OS;
+
using Avalonia.Platform;
using Avalonia.Threading;
+using App = Android.App.Application;
+
namespace Avalonia.Android
{
- class AndroidThreadingInterface : IPlatformThreadingInterface
+ internal sealed class AndroidThreadingInterface : IPlatformThreadingInterface
{
private Handler _handler;
public AndroidThreadingInterface()
{
- _handler = new Handler(global::Android.App.Application.Context.MainLooper);
+ _handler = new Handler(App.Context.MainLooper);
}
- public void RunLoop(CancellationToken cancellationToken)
- {
- return;
- }
+ public void RunLoop(CancellationToken cancellationToken) => throw new NotSupportedException();
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
{
@@ -57,7 +58,7 @@ namespace Avalonia.Android
});
}
}, null, TimeSpan.Zero, interval);
-
+
return Disposable.Create(() =>
{
lock (l)
diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj
index c170e8449c..8c6775733f 100644
--- a/src/Android/Avalonia.Android/Avalonia.Android.csproj
+++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj
@@ -1,6 +1,6 @@
- monoandroid90
+ monoandroid11.0
true
diff --git a/src/Android/Avalonia.Android/AvaloniaActivity.cs b/src/Android/Avalonia.Android/AvaloniaActivity.cs
index d3696aa41d..b3a7585520 100644
--- a/src/Android/Avalonia.Android/AvaloniaActivity.cs
+++ b/src/Android/Avalonia.Android/AvaloniaActivity.cs
@@ -1,4 +1,3 @@
-
using Android.App;
using Android.OS;
using Android.Views;
@@ -7,15 +6,13 @@ namespace Avalonia.Android
{
public abstract class AvaloniaActivity : Activity
{
-
internal AvaloniaView View;
object _content;
protected override void OnCreate(Bundle savedInstanceState)
{
- RequestWindowFeature(WindowFeatures.NoTitle);
View = new AvaloniaView(this);
- if(_content != null)
+ if (_content != null)
View.Content = _content;
SetContentView(View);
TakeKeyEvents(true);
@@ -36,9 +33,7 @@ namespace Avalonia.Android
}
}
- public override bool DispatchKeyEvent(KeyEvent e)
- {
- return View.DispatchKeyEvent(e);
- }
+ public override bool DispatchKeyEvent(KeyEvent e) =>
+ View.DispatchKeyEvent(e) ? true : base.DispatchKeyEvent(e);
}
}
diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs
index 72732a1f95..8de3657283 100644
--- a/src/Android/Avalonia.Android/AvaloniaView.cs
+++ b/src/Android/Avalonia.Android/AvaloniaView.cs
@@ -1,11 +1,12 @@
using System;
using Android.Content;
+using Android.Runtime;
using Android.Views;
using Android.Widget;
using Avalonia.Android.Platform.SkiaPlatform;
using Avalonia.Controls;
using Avalonia.Controls.Embedding;
-using Avalonia.Platform;
+using Avalonia.Rendering;
namespace Avalonia.Android
{
@@ -14,6 +15,8 @@ namespace Avalonia.Android
private readonly EmbeddableControlRoot _root;
private readonly ViewImpl _view;
+ private IDisposable? _timerSubscription;
+
public AvaloniaView(Context context) : base(context)
{
_view = new ViewImpl(context);
@@ -33,6 +36,36 @@ namespace Avalonia.Android
return _view.View.DispatchKeyEvent(e);
}
+ public override void OnVisibilityAggregated(bool isVisible)
+ {
+ base.OnVisibilityAggregated(isVisible);
+ OnVisibilityChanged(isVisible);
+ }
+
+ protected override void OnVisibilityChanged(View changedView, [GeneratedEnum] ViewStates visibility)
+ {
+ base.OnVisibilityChanged(changedView, visibility);
+ OnVisibilityChanged(visibility == ViewStates.Visible);
+ }
+
+ private void OnVisibilityChanged(bool isVisible)
+ {
+ if (isVisible)
+ {
+ if (AvaloniaLocator.Current.GetService() is ChoreographerTimer timer)
+ {
+ _timerSubscription = timer.SubscribeView(this);
+ }
+
+ _root.Renderer.Start();
+ }
+ else
+ {
+ _root.Renderer.Stop();
+ _timerSubscription?.Dispose();
+ }
+ }
+
class ViewImpl : TopLevelImpl
{
public ViewImpl(Context context) : base(context)
diff --git a/src/Android/Avalonia.Android/ChoreographerTimer.cs b/src/Android/Avalonia.Android/ChoreographerTimer.cs
new file mode 100644
index 0000000000..1d898261a3
--- /dev/null
+++ b/src/Android/Avalonia.Android/ChoreographerTimer.cs
@@ -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 = new TaskCompletionSource();
+
+ private readonly ISet _views = new HashSet();
+
+ private Action _tick;
+ private int _count;
+
+ public ChoreographerTimer()
+ {
+ _thread = new Thread(Loop);
+ _thread.Start();
+ }
+
+ public event Action 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);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Android/Avalonia.Android/CursorFactory.cs b/src/Android/Avalonia.Android/CursorFactory.cs
index 9eb28c67f9..6293637d4e 100644
--- a/src/Android/Avalonia.Android/CursorFactory.cs
+++ b/src/Android/Avalonia.Android/CursorFactory.cs
@@ -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() { }
+ }
}
}
diff --git a/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs b/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs
index 4f4c03fe77..a9710039f8 100644
--- a/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs
+++ b/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs
@@ -1,6 +1,4 @@
-using System.Linq;
-
-using Avalonia.OpenGL.Egl;
+using Avalonia.OpenGL.Egl;
using Avalonia.OpenGL.Surfaces;
namespace Avalonia.Android.OpenGL
@@ -17,7 +15,7 @@ namespace Avalonia.Android.OpenGL
}
public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() =>
- new GlRenderTarget(_egl, _info, _egl.CreateWindowSurface(_info.Handle));
+ new GlRenderTarget(_egl, _info, _egl.CreateWindowSurface(_info.Handle), _info.Handle);
public static GlPlatformSurface TryCreate(IEglWindowGlPlatformSurfaceInfo info)
{
diff --git a/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs b/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs
index 75bbd15e3e..f9071d9b27 100644
--- a/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs
+++ b/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs
@@ -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);
}
}
diff --git a/src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs b/src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs
deleted file mode 100644
index d52eeb15e4..0000000000
--- a/src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs
+++ /dev/null
@@ -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()
- {
-
- }
- }
-}
\ No newline at end of file
diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs
index 2afa4e83f1..b115917622 100644
--- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs
+++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs
@@ -10,7 +10,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
{
private IntPtr _window;
- public AndroidFramebuffer(Surface surface)
+ public AndroidFramebuffer(Surface surface, double scaling)
{
if(surface == null)
throw new ArgumentNullException(nameof(surface));
@@ -31,6 +31,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
RowBytes = buffer.stride * (Format == PixelFormat.Rgb565 ? 2 : 4);
Address = buffer.bits;
+
+ Dpi = scaling * new Vector(96, 96);
}
public void Dispose()
@@ -44,7 +46,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public IntPtr Address { get; set; }
public PixelSize Size { get; }
public int RowBytes { get; }
- public Vector Dpi { get; } = new Vector(96, 96);
+ public Vector Dpi { get; }
public PixelFormat Format { get; }
[DllImport("android")]
diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs
index 18c4796fae..56a4eb22d4 100644
--- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs
+++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs
@@ -12,6 +12,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_topLevel = topLevel;
}
- public ILockedFramebuffer Lock() => new AndroidFramebuffer(_topLevel.InternalView.Holder.Surface);
+ public ILockedFramebuffer Lock() => new AndroidFramebuffer(_topLevel.InternalView.Holder.Surface, _topLevel.RenderScaling);
}
}
diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs
index 02ea702236..16c5bdae3d 100644
--- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs
+++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs
@@ -43,11 +43,13 @@ namespace Avalonia.Android
}
}
+ [Obsolete("deprecated")]
public override void Invalidate(global::Android.Graphics.Rect dirty)
{
Invalidate();
}
+ [Obsolete("deprecated")]
public override void Invalidate(int l, int t, int r, int b)
{
Invalidate();
diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
index a8c7f7af9b..fe237a1719 100644
--- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
+++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
@@ -6,7 +6,6 @@ using Android.Runtime;
using Android.Views;
using Avalonia.Android.OpenGL;
-using Avalonia.Android.Platform.Input;
using Avalonia.Android.Platform.Specific;
using Avalonia.Android.Platform.Specific.Helpers;
using Avalonia.Controls;
@@ -35,16 +34,16 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_view = new ViewImpl(context, this, placeOnTop);
_keyboardHelper = new AndroidKeyboardEventsHelper(this);
_touchHelper = new AndroidTouchEventsHelper(this, () => InputRoot,
- p => GetAvaloniaPointFromEvent(p));
+ GetAvaloniaPointFromEvent);
_gl = GlPlatformSurface.TryCreate(this);
_framebuffer = new FramebufferManager(this);
- MaxClientSize = new Size(_view.Resources.DisplayMetrics.WidthPixels,
- _view.Resources.DisplayMetrics.HeightPixels);
- }
-
+ RenderScaling = (int)_view.Resources.DisplayMetrics.Density;
+ MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels,
+ _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling);
+ }
private bool _handleEvents;
@@ -58,25 +57,14 @@ namespace Avalonia.Android.Platform.SkiaPlatform
}
}
- public virtual Point GetAvaloniaPointFromEvent(MotionEvent e) => new Point(e.GetX(), e.GetY());
+ public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) =>
+ new Point(e.GetX(pointerIndex), e.GetY(pointerIndex)) / RenderScaling;
public IInputRoot InputRoot { get; private set; }
- public virtual Size ClientSize
- {
- get
- {
- if (_view == null)
- return new Size(0, 0);
- return new Size(_view.Width, _view.Height);
- }
- set
- {
-
- }
- }
+ public virtual Size ClientSize => Size.ToSize(RenderScaling);
- public IMouseDevice MouseDevice => AndroidMouseDevice.Instance;
+ public IMouseDevice MouseDevice { get; } = new MouseDevice();
public Action Closed { get; set; }
@@ -98,10 +86,10 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public IEnumerable
@@ -150,4 +150,4 @@
-
\ No newline at end of file
+
diff --git a/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml b/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml
index e8e81da9de..57ee503005 100644
--- a/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml
+++ b/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs
index b59fd7abde..aab6a41890 100644
--- a/src/Avalonia.Controls/AutoCompleteBox.cs
+++ b/src/Avalonia.Controls/AutoCompleteBox.cs
@@ -483,7 +483,9 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterDirect(
nameof(SelectedItem),
o => o.SelectedItem,
- (o, v) => o.SelectedItem = v);
+ (o, v) => o.SelectedItem = v,
+ defaultBindingMode: BindingMode.TwoWay,
+ enableDataValidation: true);
///
/// Identifies the
@@ -1333,7 +1335,7 @@ namespace Avalonia.Controls
base.OnApplyTemplate(e);
}
-
+
///
/// Called to update the validation state for properties for which data validation is
/// enabled.
@@ -1342,7 +1344,7 @@ namespace Avalonia.Controls
/// The new binding value for the property.
protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value)
{
- if (property == TextProperty)
+ if (property == TextProperty || property == SelectedItemProperty)
{
DataValidationErrors.SetError(this, value.Error);
}
diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
index b6b71644a3..abfbc038eb 100644
--- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
+++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
@@ -91,14 +91,14 @@ namespace Avalonia.Controls
///
public static readonly DirectProperty TextProperty =
AvaloniaProperty.RegisterDirect(nameof(Text), o => o.Text, (o, v) => o.Text = v,
- defaultBindingMode: BindingMode.TwoWay);
+ defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
///
/// Defines the property.
///
public static readonly DirectProperty ValueProperty =
AvaloniaProperty.RegisterDirect(nameof(Value), updown => updown.Value,
- (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay);
+ (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
///
/// Defines the property.
@@ -370,6 +370,20 @@ namespace Avalonia.Controls
}
}
+ ///
+ /// Called to update the validation state for properties for which data validation is
+ /// enabled.
+ ///
+ /// The property.
+ /// The new binding value for the property.
+ protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value)
+ {
+ if (property == TextProperty || property == ValueProperty)
+ {
+ DataValidationErrors.SetError(this, value.Error);
+ }
+ }
+
///
/// Called when the property value changed.
///
diff --git a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs
index 764fc7b332..be2405efde 100644
--- a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs
+++ b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs
@@ -169,7 +169,7 @@ namespace Avalonia.DesignerSupport.Remote
if (entryPoint == null)
throw Die($"Assembly {args.AppPath} doesn't have an entry point");
var builderMethod = entryPoint.DeclaringType.GetMethod(BuilderMethodName,
- BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
+ BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, Array.Empty(), null);
if (builderMethod == null)
throw Die($"{entryPoint.DeclaringType.FullName} doesn't have a method named {BuilderMethodName}");
Design.IsDesignMode = true;
diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
index 3e78e951e2..b346fca330 100644
--- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
@@ -14,6 +14,8 @@ using Avalonia.UnitTests;
using Moq;
using Xunit;
using System.Collections.ObjectModel;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
namespace Avalonia.Controls.UnitTests
{
@@ -396,6 +398,36 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(control.Text, control.ItemSelector(input, selectedItem));
});
}
+
+ [Fact]
+ public void Text_Validation()
+ {
+ RunTest((control, textbox) =>
+ {
+ var exception = new InvalidCastException("failed validation");
+ var textObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError));
+ control.Bind(AutoCompleteBox.TextProperty, textObservable);
+ Dispatcher.UIThread.RunJobs();
+
+ Assert.Equal(DataValidationErrors.GetHasErrors(control), true);
+ Assert.Equal(DataValidationErrors.GetErrors(control).SequenceEqual(new[] { exception }), true);
+ });
+ }
+
+ [Fact]
+ public void SelectedItem_Validation()
+ {
+ RunTest((control, textbox) =>
+ {
+ var exception = new InvalidCastException("failed validation");
+ var itemObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError));
+ control.Bind(AutoCompleteBox.SelectedItemProperty, itemObservable);
+ Dispatcher.UIThread.RunJobs();
+
+ Assert.Equal(DataValidationErrors.GetHasErrors(control), true);
+ Assert.Equal(DataValidationErrors.GetErrors(control).SequenceEqual(new[] { exception }), true);
+ });
+ }
///
/// Retrieves a defined predicate filter through a new AutoCompleteBox
diff --git a/tests/Avalonia.Controls.UnitTests/NumericUpDownTests.cs b/tests/Avalonia.Controls.UnitTests/NumericUpDownTests.cs
new file mode 100644
index 0000000000..4cef7e4d05
--- /dev/null
+++ b/tests/Avalonia.Controls.UnitTests/NumericUpDownTests.cs
@@ -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(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(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 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()
+ .Select(b => b.Content)
+ .OfType()
+ .First();
+ }
+ private IControlTemplate CreateTemplate()
+ {
+ return new FuncControlTemplate((control, scope) =>
+ {
+ var textBox =
+ new TextBox
+ {
+ Name = "PART_TextBox"
+ }.RegisterInNameScope(scope);
+ return new ButtonSpinner
+ {
+ Name = "PART_Spinner",
+ Content = textBox,
+ }.RegisterInNameScope(scope);
+ });
+ }
+ }
+}