Browse Source

Merge branch 'master' into fixed-can-execute-changed

pull/5720/head
Nikita Tsukanov 5 years ago
committed by GitHub
parent
commit
dd0fac2dc8
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      native/Avalonia.Native/src/OSX/AvnString.h
  2. 15
      native/Avalonia.Native/src/OSX/AvnString.mm
  3. 28
      native/Avalonia.Native/src/OSX/app.mm
  4. 2
      native/Avalonia.Native/src/OSX/clipboard.mm
  5. 4
      native/Avalonia.Native/src/OSX/common.h
  6. 8
      native/Avalonia.Native/src/OSX/main.mm
  7. 9
      native/Avalonia.Native/src/OSX/menu.h
  8. 34
      native/Avalonia.Native/src/OSX/menu.mm
  9. 14
      native/Avalonia.Native/src/OSX/window.mm
  10. 4
      nukebuild/Build.cs
  11. 2
      samples/ControlCatalog.Android/ControlCatalog.Android.csproj
  12. 2
      samples/ControlCatalog.Android/Properties/AndroidManifest.xml
  13. 4
      samples/ControlCatalog/MainWindow.xaml
  14. 47
      src/Android/Avalonia.Android/ActivityTracker.cs
  15. 32
      src/Android/Avalonia.Android/AndroidPlatform.cs
  16. 15
      src/Android/Avalonia.Android/AndroidThreadingInterface.cs
  17. 2
      src/Android/Avalonia.Android/Avalonia.Android.csproj
  18. 11
      src/Android/Avalonia.Android/AvaloniaActivity.cs
  19. 35
      src/Android/Avalonia.Android/AvaloniaView.cs
  20. 101
      src/Android/Avalonia.Android/ChoreographerTimer.cs
  21. 17
      src/Android/Avalonia.Android/CursorFactory.cs
  22. 6
      src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs
  23. 13
      src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs
  24. 14
      src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs
  25. 6
      src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs
  26. 2
      src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs
  27. 2
      src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs
  28. 48
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  29. 91
      src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs
  30. 11
      src/Android/Avalonia.Android/app.config
  31. 4
      src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj
  32. 2
      src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml
  33. 28
      src/Avalonia.Animation/Animatable.cs
  34. 40
      src/Avalonia.Base/AvaloniaObject.cs
  35. 60
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  36. 39
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  37. 8
      src/Avalonia.Base/PropertyStore/IBatchUpdate.cs
  38. 9
      src/Avalonia.Base/PropertyStore/IValue.cs
  39. 16
      src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
  40. 121
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  41. 19
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  42. 301
      src/Avalonia.Base/ValueStore.cs
  43. 11
      src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs
  44. 56
      src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs
  45. 5
      src/Avalonia.Controls/ApiCompatBaseline.txt
  46. 11
      src/Avalonia.Controls/Application.cs
  47. 14
      src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
  48. 8
      src/Avalonia.Controls/AutoCompleteBox.cs
  49. 69
      src/Avalonia.Controls/Control.cs
  50. 77
      src/Avalonia.Controls/DefinitionBase.cs
  51. 2
      src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs
  52. 33
      src/Avalonia.Controls/NativeMenu.cs
  53. 16
      src/Avalonia.Controls/NativeMenuItemSeparator.cs
  54. 10
      src/Avalonia.Controls/NativeMenuItemSeperator.cs
  55. 17
      src/Avalonia.Controls/Notifications/NotificationCard.cs
  56. 18
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  57. 7
      src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs
  58. 22
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  59. 43
      src/Avalonia.Controls/TextBox.cs
  60. 14
      src/Avalonia.Controls/UrlOpenedEventArgs.cs
  61. 45
      src/Avalonia.Controls/Window.cs
  62. 2
      src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs
  63. 2
      src/Avalonia.FreeDesktop/DBusMenuExporter.cs
  64. 2
      src/Avalonia.Layout/ElementManager.cs
  65. 14
      src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs
  66. 7
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  67. 26
      src/Avalonia.Native/IAvnMenu.cs
  68. 15
      src/Avalonia.Native/avn.idl
  69. 4
      src/Avalonia.Styling/IStyledElement.cs
  70. 28
      src/Avalonia.Styling/StyledElement.cs
  71. 18
      src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs
  72. 2
      src/Avalonia.Styling/Styling/PropertySetterInstance.cs
  73. 128
      src/Avalonia.Styling/Styling/PropertySetterLazyInstance.cs
  74. 24
      src/Avalonia.Styling/Styling/Setter.cs
  75. 2
      src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml
  76. 14
      src/Avalonia.Visuals/Visual.cs
  77. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
  78. 14
      src/Windows/Avalonia.Win32/WindowImpl.cs
  79. 31
      tests/Avalonia.Animation.UnitTests/AnimatableTests.cs
  80. 1
      tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj
  81. 616
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs
  82. 1
      tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
  83. 74
      tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs
  84. 32
      tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
  85. 64
      tests/Avalonia.Controls.UnitTests/CarouselTests.cs
  86. 29
      tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
  87. 26
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  88. 95
      tests/Avalonia.Controls.UnitTests/NumericUpDownTests.cs
  89. 62
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  90. 124
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  91. 1
      tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj
  92. 96
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs
  93. 1
      tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj
  94. 282
      tests/Avalonia.Styling.UnitTests/StyleTests.cs
  95. 2
      tests/Avalonia.UnitTests/UnitTestApplication.cs

1
native/Avalonia.Native/src/OSX/AvnString.h

@ -11,6 +11,7 @@
extern IAvnString* CreateAvnString(NSString* string);
extern IAvnStringArray* CreateAvnStringArray(NSArray<NSString*>* array);
extern IAvnStringArray* CreateAvnStringArray(NSArray<NSURL*>* array);
extern IAvnStringArray* CreateAvnStringArray(NSString* string);
extern IAvnString* CreateByteArray(void* data, int len);
#endif /* AvnString_h */

15
native/Avalonia.Native/src/OSX/AvnString.mm

@ -85,6 +85,16 @@ public:
}
}
AvnStringArrayImpl(NSArray<NSURL*>* array)
{
for(int c = 0; c < [array count]; c++)
{
ComPtr<IAvnString> s;
*s.getPPV() = new AvnStringImpl([array objectAtIndex:c].absoluteString);
_list.push_back(s);
}
}
AvnStringArrayImpl(NSString* string)
{
ComPtr<IAvnString> s;
@ -117,6 +127,11 @@ IAvnStringArray* CreateAvnStringArray(NSArray<NSString*> * array)
return new AvnStringArrayImpl(array);
}
IAvnStringArray* CreateAvnStringArray(NSArray<NSURL*> * array)
{
return new AvnStringArrayImpl(array);
}
IAvnStringArray* CreateAvnStringArray(NSString* string)
{
return new AvnStringArrayImpl(string);

28
native/Avalonia.Native/src/OSX/app.mm

@ -1,10 +1,20 @@
#include "common.h"
#include "AvnString.h"
@interface AvnAppDelegate : NSObject<NSApplicationDelegate>
-(AvnAppDelegate* _Nonnull) initWithEvents: (IAvnApplicationEvents* _Nonnull) events;
@end
NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular;
@implementation AvnAppDelegate
ComPtr<IAvnApplicationEvents> _events;
- (AvnAppDelegate *)initWithEvents:(IAvnApplicationEvents *)events
{
_events = events;
return self;
}
- (void)applicationWillFinishLaunching:(NSNotification *)notification
{
if([[NSApplication sharedApplication] activationPolicy] != AvnDesiredActivationPolicy)
@ -27,11 +37,23 @@ NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivati
[[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateIgnoringOtherApps];
}
- (void)application:(NSApplication *)sender openFiles:(NSArray<NSString *> *)filenames
{
auto array = CreateAvnStringArray(filenames);
_events->FilesOpened(array);
}
- (void)application:(NSApplication *)application openURLs:(NSArray<NSURL *> *)urls
{
auto array = CreateAvnStringArray(urls);
_events->FilesOpened(array);
}
@end
@interface AvnApplication : NSApplication
@end
@implementation AvnApplication
@ -63,9 +85,9 @@ NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivati
@end
extern void InitializeAvnApp()
extern void InitializeAvnApp(IAvnApplicationEvents* events)
{
NSApplication* app = [AvnApplication sharedApplication];
id delegate = [AvnAppDelegate new];
id delegate = [[AvnAppDelegate alloc] initWithEvents:events];
[app setDelegate:delegate];
}

2
native/Avalonia.Native/src/OSX/clipboard.mm

@ -56,7 +56,7 @@ public:
return S_OK;
}
NSArray* arr = (NSArray*)data;
NSArray<NSString*>* arr = (NSArray*)data;
for(int c = 0; c < [arr count]; c++)
if(![[arr objectAtIndex:c] isKindOfClass:[NSString class]])

4
native/Avalonia.Native/src/OSX/common.h

@ -23,7 +23,7 @@ extern IAvnCursorFactory* CreateCursorFactory();
extern IAvnGlDisplay* GetGlDisplay();
extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events);
extern IAvnMenuItem* CreateAppMenuItem();
extern IAvnMenuItem* CreateAppMenuItemSeperator();
extern IAvnMenuItem* CreateAppMenuItemSeparator();
extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent);
extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu);
extern IAvnMenu* GetAppMenu ();
@ -31,7 +31,7 @@ extern NSMenuItem* GetAppMenuItem ();
extern void SetAutoGenerateDefaultAppMenuItems (bool enabled);
extern bool GetAutoGenerateDefaultAppMenuItems ();
extern void InitializeAvnApp();
extern void InitializeAvnApp(IAvnApplicationEvents* events);
extern NSApplicationActivationPolicy AvnDesiredActivationPolicy;
extern NSPoint ToNSPoint (AvnPoint p);
extern AvnPoint ToAvnPoint (NSPoint p);

8
native/Avalonia.Native/src/OSX/main.mm

@ -163,13 +163,13 @@ class AvaloniaNative : public ComSingleObject<IAvaloniaNativeFactory, &IID_IAval
public:
FORWARD_IUNKNOWN()
virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator) override
virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator, IAvnApplicationEvents* events) override
{
_deallocator = deallocator;
@autoreleasepool{
[[ThreadingInitializer new] do];
}
InitializeAvnApp();
InitializeAvnApp(events);
return S_OK;
};
@ -253,9 +253,9 @@ public:
return S_OK;
}
virtual HRESULT CreateMenuItemSeperator (IAvnMenuItem** ppv) override
virtual HRESULT CreateMenuItemSeparator (IAvnMenuItem** ppv) override
{
*ppv = ::CreateAppMenuItemSeperator();
*ppv = ::CreateAppMenuItemSeparator();
return S_OK;
}

9
native/Avalonia.Native/src/OSX/menu.h

@ -31,13 +31,13 @@ private:
NSMenuItem* _native; // here we hold a pointer to an AvnMenuItem
IAvnActionCallback* _callback;
IAvnPredicateCallback* _predicate;
bool _isSeperator;
bool _isSeparator;
bool _isCheckable;
public:
FORWARD_IUNKNOWN()
AvnAppMenuItem(bool isSeperator);
AvnAppMenuItem(bool isSeparator);
NSMenuItem* GetNative();
@ -60,7 +60,6 @@ public:
void RaiseOnClicked();
};
class AvnAppMenu : public ComSingleObject<IAvnMenu, &IID_IAvnMenu>
{
private:
@ -71,10 +70,12 @@ public:
FORWARD_IUNKNOWN()
AvnAppMenu(IAvnMenuEvents* events);
AvnMenu* GetNative();
void RaiseNeedsUpdate ();
void RaiseOpening();
void RaiseClosed();
virtual HRESULT InsertItem (int index, IAvnMenuItem* item) override;

34
native/Avalonia.Native/src/OSX/menu.mm

@ -71,12 +71,12 @@
}
@end
AvnAppMenuItem::AvnAppMenuItem(bool isSeperator)
AvnAppMenuItem::AvnAppMenuItem(bool isSeparator)
{
_isCheckable = false;
_isSeperator = isSeperator;
_isSeparator = isSeparator;
if(isSeperator)
if(isSeparator)
{
_native = [NSMenuItem separatorItem];
}
@ -298,6 +298,23 @@ void AvnAppMenu::RaiseNeedsUpdate()
}
}
void AvnAppMenu::RaiseOpening()
{
if(_baseEvents != nullptr)
{
_baseEvents->Opening();
}
}
void AvnAppMenu::RaiseClosed()
{
if(_baseEvents != nullptr)
{
_baseEvents->Closed();
}
}
HRESULT AvnAppMenu::InsertItem(int index, IAvnMenuItem *item)
{
@autoreleasepool
@ -382,6 +399,15 @@ HRESULT AvnAppMenu::Clear()
_parent->RaiseNeedsUpdate();
}
- (void)menuWillOpen:(NSMenu *)menu
{
_parent->RaiseOpening();
}
- (void)menuDidClose:(NSMenu *)menu
{
_parent->RaiseClosed();
}
@end
@ -401,7 +427,7 @@ extern IAvnMenuItem* CreateAppMenuItem()
}
}
extern IAvnMenuItem* CreateAppMenuItemSeperator()
extern IAvnMenuItem* CreateAppMenuItemSeparator()
{
@autoreleasepool
{

14
native/Avalonia.Native/src/OSX/window.mm

@ -1877,7 +1877,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
for(int i = 0; i < numWindows; i++)
{
[[windows objectAtIndex:i] performClose:nil];
auto window = (AvnWindow*)[windows objectAtIndex:i];
if([window parentWindow] == nullptr) // Avalonia will handle the child windows.
{
[window performClose:nil];
}
}
}
@ -2226,9 +2231,12 @@ protected:
{
@autoreleasepool
{
[Window setContentSize:NSSize{x, y}];
if (Window != nullptr)
{
[Window setContentSize:NSSize{x, y}];
[Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))];
[Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))];
}
return S_OK;
}

4
nukebuild/Build.cs

@ -89,10 +89,6 @@ partial class Build : NukeBuild
Process.Start(new ProcessStartInfo(command, args) {UseShellExecute = false}).WaitForExit();
}
ExecWait("dotnet version:", "dotnet", "--version");
if (Parameters.IsRunningOnUnix)
ExecWait("Mono version:", "mono", "--version");
}
IReadOnlyCollection<Output> MsBuildCommon(

2
samples/ControlCatalog.Android/ControlCatalog.Android.csproj

@ -16,7 +16,7 @@
<AndroidResgenFile>Resources\Resource.Designer.cs</AndroidResgenFile>
<GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
<AndroidUseLatestPlatformSdk>False</AndroidUseLatestPlatformSdk>
<TargetFrameworkVersion>v10.0</TargetFrameworkVersion>
<TargetFrameworkVersion>v11.0</TargetFrameworkVersion>
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">

2
samples/ControlCatalog.Android/Properties/AndroidManifest.xml

@ -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>

4
samples/ControlCatalog/MainWindow.xaml

@ -18,11 +18,11 @@
<NativeMenuItem Header="File">
<NativeMenu>
<NativeMenuItem Icon="/Assets/test_icon.ico" Header="Open" Clicked="OnOpenClicked" Gesture="Ctrl+O"/>
<NativeMenuItemSeperator/>
<NativeMenuItemSeperator/><!-- Uses incorrect spelling to demonstrate backwards compatibility -->
<NativeMenuItem Icon="/Assets/github_icon.png" Header="Recent">
<NativeMenu/>
</NativeMenuItem>
<NativeMenuItemSeperator/>
<NativeMenuItemSeparator/>
<NativeMenuItem Header="{x:Static local:MainWindow.MenuQuitHeader}"
Gesture="{x:Static local:MainWindow.MenuQuitGesture}"
Clicked="OnCloseClicked" />

47
src/Android/Avalonia.Android/ActivityTracker.cs

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

32
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<IClipboard>().ToTransient<ClipboardImpl>()
.Bind<IStandardCursorFactory>().ToTransient<CursorFactory>()
.Bind<ICursorFactory>().ToTransient<CursorFactory>()
.Bind<IKeyboardDevice>().ToSingleton<AndroidKeyboardDevice>()
.Bind<IPlatformSettings>().ToConstant(Instance)
.Bind<IPlatformThreadingInterface>().ToConstant(new AndroidThreadingInterface())
.Bind<ISystemDialogImpl>().ToTransient<SystemDialogImpl>()
.Bind<IWindowingPlatform>().ToConstant(Instance)
.Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoader>()
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
.Bind<IRenderTimer>().ToConstant(new ChoreographerTimer())
.Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
.Bind<IAssetLoader>().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;
}
}

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

2
src/Android/Avalonia.Android/Avalonia.Android.csproj

@ -1,6 +1,6 @@
<Project Sdk="MSBuild.Sdk.Extras">
<PropertyGroup>
<TargetFramework>monoandroid90</TargetFramework>
<TargetFramework>monoandroid11.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>

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

35
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<IRenderTimer>() 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)

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

17
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() { }
}
}
}

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

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

14
src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs

@ -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()
{
}
}
}

6
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")]

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

2
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();

48
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<TopLevelImpl>(this);
_touchHelper = new AndroidTouchEventsHelper<TopLevelImpl>(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<object> Surfaces => new object[] { _gl, _framebuffer };
public IRenderer CreateRenderer(IRenderRoot root)
{
return new ImmediateRenderer(root);
}
public IRenderer CreateRenderer(IRenderRoot root) =>
AndroidPlatform.Options.UseDeferredRendering
? new DeferredRenderer(root, AvaloniaLocator.Current.GetService<IRenderLoop>()) { RenderOnlyOnRenderThread = true }
: new ImmediateRenderer(root);
public virtual void Hide()
{
@ -115,15 +103,15 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public Point PointToClient(PixelPoint point)
{
return point.ToPoint(1);
return point.ToPoint(RenderScaling);
}
public PixelPoint PointToScreen(Point point)
{
return PixelPoint.FromPoint(point, 1);
return PixelPoint.FromPoint(point, RenderScaling);
}
public void SetCursor(IPlatformHandle cursor)
public void SetCursor(ICursorImpl cursor)
{
//still not implemented
}
@ -138,7 +126,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_view.Visibility = ViewStates.Visible;
}
public double RenderScaling => 1;
public double RenderScaling { get; }
void Draw()
{
@ -193,7 +181,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
void ISurfaceHolderCallback.SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height)
{
var newSize = new Size(width, height);
var newSize = new PixelSize(width, height).ToSize(_tl.RenderScaling);
if (newSize != _oldSize)
{

91
src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs

@ -11,7 +11,7 @@ namespace Avalonia.Android.Platform.Specific.Helpers
private TView _view;
public bool HandleEvents { get; set; }
public AndroidTouchEventsHelper(TView view, Func<IInputRoot> getInputRoot, Func<MotionEvent, Point> getPointfunc)
public AndroidTouchEventsHelper(TView view, Func<IInputRoot> getInputRoot, Func<MotionEvent, int, Point> getPointfunc)
{
this._view = view;
HandleEvents = true;
@ -19,11 +19,9 @@ namespace Avalonia.Android.Platform.Specific.Helpers
_getInputRoot = getInputRoot;
}
private DateTime _lastTouchMoveEventTime = DateTime.Now;
private Point? _lastTouchMovePoint;
private Func<MotionEvent, Point> _getPointFunc;
private TouchDevice _touchDevice = new TouchDevice();
private Func<MotionEvent, int, Point> _getPointFunc;
private Func<IInputRoot> _getInputRoot;
private Point _point;
public bool? DispatchTouchEvent(MotionEvent e, out bool callBase)
{
@ -33,89 +31,44 @@ namespace Avalonia.Android.Platform.Specific.Helpers
return null;
}
RawPointerEventType? mouseEventType = null;
var eventTime = DateTime.Now;
//Basic touch support
switch (e.Action)
var pointerEventType = e.Action switch
{
case MotionEventActions.Move:
//may be bot flood the evnt system with too many event especially on not so powerfull mobile devices
if ((eventTime - _lastTouchMoveEventTime).TotalMilliseconds > 10)
{
mouseEventType = RawPointerEventType.Move;
}
break;
case MotionEventActions.Down:
mouseEventType = RawPointerEventType.LeftButtonDown;
MotionEventActions.Down => RawPointerEventType.TouchBegin,
MotionEventActions.Up => RawPointerEventType.TouchEnd,
MotionEventActions.Cancel => RawPointerEventType.TouchCancel,
_ => RawPointerEventType.TouchUpdate
};
break;
if (e.Action.HasFlag(MotionEventActions.PointerDown))
{
pointerEventType = RawPointerEventType.TouchBegin;
}
case MotionEventActions.Up:
mouseEventType = RawPointerEventType.LeftButtonUp;
break;
if (e.Action.HasFlag(MotionEventActions.PointerUp))
{
pointerEventType = RawPointerEventType.TouchEnd;
}
if (mouseEventType != null)
for (int i = 0; i < e.PointerCount; i++)
{
//if point is in view otherwise it's possible avalonia not to find the proper window to dispatch the event
_point = _getPointFunc(e);
var point = _getPointFunc(e, i);
double x = _view.View.GetX();
double y = _view.View.GetY();
double r = x + _view.View.Width;
double b = y + _view.View.Height;
if (x <= _point.X && r >= _point.X && y <= _point.Y && b >= _point.Y)
if (x <= point.X && r >= point.X && y <= point.Y && b >= point.Y)
{
var inputRoot = _getInputRoot();
var mouseDevice = Avalonia.Android.Platform.Input.AndroidMouseDevice.Instance;
//in order the controls to work in a predictable way
//we need to generate mouse move before first mouse down event
//as this is the way buttons are working every time
//otherwise there is a problem sometimes
if (mouseEventType == RawPointerEventType.LeftButtonDown)
{
var me = new RawPointerEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot,
RawPointerEventType.Move, _point, RawInputModifiers.None);
_view.Input(me);
}
var mouseEvent = new RawPointerEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot,
mouseEventType.Value, _point, RawInputModifiers.LeftMouseButton);
var mouseEvent = new RawTouchEventArgs(_touchDevice, (uint)eventTime.Ticks, inputRoot,
i == e.ActionIndex ? pointerEventType : RawPointerEventType.TouchUpdate, point, RawInputModifiers.None, e.GetPointerId(i));
_view.Input(mouseEvent);
if (e.Action == MotionEventActions.Move && mouseDevice.Captured == null)
{
if (_lastTouchMovePoint != null)
{
//raise mouse scroll event so the scrollers
//are moving with the cursor
double vectorX = _point.X - _lastTouchMovePoint.Value.X;
double vectorY = _point.Y - _lastTouchMovePoint.Value.Y;
//based on test correction of 0.02 is working perfect
double correction = 0.02;
var ps = AndroidPlatform.Instance.LayoutScalingFactor;
var mouseWheelEvent = new RawMouseWheelEventArgs(
mouseDevice,
(uint)eventTime.Ticks,
inputRoot,
_point,
new Vector(vectorX * correction / ps, vectorY * correction / ps), RawInputModifiers.LeftMouseButton);
_view.Input(mouseWheelEvent);
}
_lastTouchMovePoint = _point;
_lastTouchMoveEventTime = eventTime;
}
else if (e.Action == MotionEventActions.Down)
{
_lastTouchMovePoint = _point;
}
else
{
_lastTouchMovePoint = null;
}
}
}

11
src/Android/Avalonia.Android/app.config

@ -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>

4
src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj

@ -16,7 +16,7 @@
<AndroidResgenFile>Resources\Resource.Designer.cs</AndroidResgenFile>
<GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
<AndroidUseLatestPlatformSdk>False</AndroidUseLatestPlatformSdk>
<TargetFrameworkVersion>v10.0</TargetFrameworkVersion>
<TargetFrameworkVersion>v11.0</TargetFrameworkVersion>
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
@ -150,4 +150,4 @@
<Import Project="..\..\..\build\System.Memory.props" />
<Import Project="..\..\..\build\AndroidWorkarounds.props" />
<Import Project="..\..\..\build\LegacyProject.targets" />
</Project>
</Project>

2
src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml

@ -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>

28
src/Avalonia.Animation/Animatable.cs

@ -2,6 +2,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Data;
#nullable enable
@ -93,16 +94,35 @@ namespace Avalonia.Animation
var oldTransitions = change.OldValue.GetValueOrDefault<Transitions>();
var newTransitions = change.NewValue.GetValueOrDefault<Transitions>();
// When transitions are replaced, we add the new transitions before removing the old
// transitions, so that when the old transition being disposed causes the value to
// change, there is a corresponding entry in `_transitionStates`. This means that we
// need to account for any transitions present in both the old and new transitions
// collections.
if (newTransitions is object)
{
var toAdd = (IList)newTransitions;
if (newTransitions.Count > 0 && oldTransitions?.Count > 0)
{
toAdd = newTransitions.Except(oldTransitions).ToList();
}
newTransitions.CollectionChanged += TransitionsCollectionChanged;
AddTransitions(newTransitions);
AddTransitions(toAdd);
}
if (oldTransitions is object)
{
var toRemove = (IList)oldTransitions;
if (oldTransitions.Count > 0 && newTransitions?.Count > 0)
{
toRemove = oldTransitions.Except(newTransitions).ToList();
}
oldTransitions.CollectionChanged -= TransitionsCollectionChanged;
RemoveTransitions(oldTransitions);
RemoveTransitions(toRemove);
}
}
else if (_transitionsEnabled &&
@ -115,9 +135,9 @@ namespace Avalonia.Animation
{
var transition = Transitions[i];
if (transition.Property == change.Property)
if (transition.Property == change.Property &&
_transitionState.TryGetValue(transition, out var state))
{
var state = _transitionState[transition];
var oldValue = state.BaseValue;
var newValue = GetAnimationBaseValue(transition.Property);

40
src/Avalonia.Base/AvaloniaObject.cs

@ -23,7 +23,7 @@ namespace Avalonia
private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged;
private List<IAvaloniaObject> _inheritanceChildren;
private ValueStore _values;
private ValueStore Values => _values ?? (_values = new ValueStore(this));
private bool _batchUpdate;
/// <summary>
/// Initializes a new instance of the <see cref="AvaloniaObject"/> class.
@ -117,6 +117,22 @@ namespace Avalonia
set { this.Bind(binding.Property, value); }
}
private ValueStore Values
{
get
{
if (_values is null)
{
_values = new ValueStore(this);
if (_batchUpdate)
_values.BeginBatchUpdate();
}
return _values;
}
}
public bool CheckAccess() => Dispatcher.UIThread.CheckAccess();
public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess();
@ -434,6 +450,28 @@ namespace Avalonia
_values?.CoerceValue(property);
}
public void BeginBatchUpdate()
{
if (_batchUpdate)
{
throw new InvalidOperationException("Batch update already in progress.");
}
_batchUpdate = true;
_values?.BeginBatchUpdate();
}
public void EndBatchUpdate()
{
if (!_batchUpdate)
{
throw new InvalidOperationException("No batch update in progress.");
}
_batchUpdate = false;
_values?.EndBatchUpdate();
}
/// <inheritdoc/>
void IAvaloniaObject.AddInheritanceChild(IAvaloniaObject child)
{

60
src/Avalonia.Base/PropertyStore/BindingEntry.cs

@ -9,8 +9,9 @@ namespace Avalonia.PropertyStore
/// <summary>
/// Represents an untyped interface to <see cref="BindingEntry{T}"/>.
/// </summary>
internal interface IBindingEntry : IPriorityValueEntry, IDisposable
internal interface IBindingEntry : IBatchUpdate, IPriorityValueEntry, IDisposable
{
void Start(bool ignoreBatchUpdate);
}
/// <summary>
@ -22,6 +23,8 @@ namespace Avalonia.PropertyStore
private readonly IAvaloniaObject _owner;
private IValueSink _sink;
private IDisposable? _subscription;
private bool _isSubscribed;
private bool _batchUpdate;
private Optional<T> _value;
public BindingEntry(
@ -39,10 +42,20 @@ namespace Avalonia.PropertyStore
}
public StyledPropertyBase<T> Property { get; }
public BindingPriority Priority { get; }
public BindingPriority Priority { get; private set; }
public IObservable<BindingValue<T>> Source { get; }
Optional<object> IValue.GetValue() => _value.ToObject();
public void BeginBatchUpdate() => _batchUpdate = true;
public void EndBatchUpdate()
{
_batchUpdate = false;
if (_sink is ValueStore)
Start();
}
public Optional<T> GetValue(BindingPriority maxPriority)
{
return Priority >= maxPriority ? _value : Optional<T>.Empty;
@ -52,10 +65,17 @@ namespace Avalonia.PropertyStore
{
_subscription?.Dispose();
_subscription = null;
_sink.Completed(Property, this, _value);
OnCompleted();
}
public void OnCompleted() => _sink.Completed(Property, this, _value);
public void OnCompleted()
{
var oldValue = _value;
_value = default;
Priority = BindingPriority.Unset;
_isSubscribed = false;
_sink.Completed(Property, this, oldValue);
}
public void OnError(Exception error)
{
@ -79,13 +99,39 @@ namespace Avalonia.PropertyStore
}
}
public void Start()
public void Start() => Start(false);
public void Start(bool ignoreBatchUpdate)
{
_subscription = Source.Subscribe(this);
// We can't use _subscription to check whether we're subscribed because it won't be set
// until Subscribe has finished, which will be too late to prevent reentrancy. In addition
// don't re-subscribe to completed/disposed bindings (indicated by Unset priority).
if (!_isSubscribed &&
Priority != BindingPriority.Unset &&
(!_batchUpdate || ignoreBatchUpdate))
{
_isSubscribed = true;
_subscription = Source.Subscribe(this);
}
}
public void Reparent(IValueSink sink) => _sink = sink;
public void RaiseValueChanged(
IValueSink sink,
IAvaloniaObject owner,
AvaloniaProperty property,
Optional<object> oldValue,
Optional<object> newValue)
{
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
newValue.GetValueOrDefault<T>(),
Priority));
}
private void UpdateValue(BindingValue<T> value)
{
if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false)

39
src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs

@ -1,23 +1,31 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
#nullable enable
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents an untyped interface to <see cref="ConstantValueEntry{T}"/>.
/// </summary>
internal interface IConstantValueEntry : IPriorityValueEntry, IDisposable
{
}
/// <summary>
/// Stores a value with a priority in a <see cref="ValueStore"/> or
/// <see cref="PriorityValue{T}"/>.
/// </summary>
/// <typeparam name="T">The property type.</typeparam>
internal class ConstantValueEntry<T> : IPriorityValueEntry<T>, IDisposable
internal class ConstantValueEntry<T> : IPriorityValueEntry<T>, IConstantValueEntry
{
private IValueSink _sink;
private Optional<T> _value;
public ConstantValueEntry(
StyledPropertyBase<T> property,
T value,
[AllowNull] T value,
BindingPriority priority,
IValueSink sink)
{
@ -28,7 +36,7 @@ namespace Avalonia.PropertyStore
}
public StyledPropertyBase<T> Property { get; }
public BindingPriority Priority { get; }
public BindingPriority Priority { get; private set; }
Optional<object> IValue.GetValue() => _value.ToObject();
public Optional<T> GetValue(BindingPriority maxPriority = BindingPriority.Animation)
@ -36,7 +44,30 @@ namespace Avalonia.PropertyStore
return Priority >= maxPriority ? _value : Optional<T>.Empty;
}
public void Dispose() => _sink.Completed(Property, this, _value);
public void Dispose()
{
var oldValue = _value;
_value = default;
Priority = BindingPriority.Unset;
_sink.Completed(Property, this, oldValue);
}
public void Reparent(IValueSink sink) => _sink = sink;
public void Start() { }
public void RaiseValueChanged(
IValueSink sink,
IAvaloniaObject owner,
AvaloniaProperty property,
Optional<object> oldValue,
Optional<object> newValue)
{
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
newValue.GetValueOrDefault<T>(),
Priority));
}
}
}

8
src/Avalonia.Base/PropertyStore/IBatchUpdate.cs

@ -0,0 +1,8 @@
namespace Avalonia.PropertyStore
{
internal interface IBatchUpdate
{
void BeginBatchUpdate();
void EndBatchUpdate();
}
}

9
src/Avalonia.Base/PropertyStore/IValue.cs

@ -9,8 +9,15 @@ namespace Avalonia.PropertyStore
/// </summary>
internal interface IValue
{
Optional<object> GetValue();
BindingPriority Priority { get; }
Optional<object> GetValue();
void Start();
void RaiseValueChanged(
IValueSink sink,
IAvaloniaObject owner,
AvaloniaProperty property,
Optional<object> oldValue,
Optional<object> newValue);
}
/// <summary>

16
src/Avalonia.Base/PropertyStore/LocalValueEntry.cs

@ -24,5 +24,21 @@ namespace Avalonia.PropertyStore
}
public void SetValue(T value) => _value = value;
public void Start() { }
public void RaiseValueChanged(
IValueSink sink,
IAvaloniaObject owner,
AvaloniaProperty property,
Optional<object> oldValue,
Optional<object> newValue)
{
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
newValue.GetValueOrDefault<T>(),
BindingPriority.LocalValue));
}
}
}

121
src/Avalonia.Base/PropertyStore/PriorityValue.cs

@ -18,7 +18,7 @@ namespace Avalonia.PropertyStore
/// <see cref="IPriorityValueEntry{T}"/> entries (sorted first by priority and then in the order
/// they were added) plus a local value.
/// </remarks>
internal class PriorityValue<T> : IValue<T>, IValueSink
internal class PriorityValue<T> : IValue<T>, IValueSink, IBatchUpdate
{
private readonly IAvaloniaObject _owner;
private readonly IValueSink _sink;
@ -26,6 +26,8 @@ namespace Avalonia.PropertyStore
private readonly Func<IAvaloniaObject, T, T>? _coerceValue;
private Optional<T> _localValue;
private Optional<T> _value;
private bool _isCalculatingValue;
private bool _batchUpdate;
public PriorityValue(
IAvaloniaObject owner,
@ -53,6 +55,18 @@ namespace Avalonia.PropertyStore
existing.Reparent(this);
_entries.Add(existing);
if (existing is IBindingEntry binding &&
existing.Priority == BindingPriority.LocalValue)
{
// Bit of a special case here: if we have a local value binding that is being
// promoted to a priority value we need to make sure the binding is subscribed
// 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. A
// notification won't be sent during batch update anyway because it will be
// caught and stored for later by the ValueStore.
binding.Start(ignoreBatchUpdate: true);
}
var v = existing.GetValue();
if (v.HasValue)
@ -78,6 +92,28 @@ namespace Avalonia.PropertyStore
public IReadOnlyList<IPriorityValueEntry<T>> Entries => _entries;
Optional<object> IValue.GetValue() => _value.ToObject();
public void BeginBatchUpdate()
{
_batchUpdate = true;
foreach (var entry in _entries)
{
(entry as IBatchUpdate)?.BeginBatchUpdate();
}
}
public void EndBatchUpdate()
{
_batchUpdate = false;
foreach (var entry in _entries)
{
(entry as IBatchUpdate)?.EndBatchUpdate();
}
UpdateEffectiveValue(null);
}
public void ClearLocalValue()
{
UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>(
@ -134,10 +170,37 @@ namespace Avalonia.PropertyStore
var binding = new BindingEntry<T>(_owner, Property, source, priority, this);
var insert = FindInsertPoint(binding.Priority);
_entries.Insert(insert, binding);
if (_batchUpdate)
{
binding.BeginBatchUpdate();
if (priority == BindingPriority.LocalValue)
{
binding.Start(ignoreBatchUpdate: true);
}
}
return binding;
}
public void CoerceValue() => UpdateEffectiveValue(null);
public void UpdateEffectiveValue() => UpdateEffectiveValue(null);
public void Start() => UpdateEffectiveValue(null);
public void RaiseValueChanged(
IValueSink sink,
IAvaloniaObject owner,
AvaloniaProperty property,
Optional<object> oldValue,
Optional<object> newValue)
{
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
newValue.GetValueOrDefault<T>(),
Priority));
}
void IValueSink.ValueChanged<TValue>(AvaloniaPropertyChangedEventArgs<TValue> change)
{
@ -146,7 +209,7 @@ namespace Avalonia.PropertyStore
_localValue = default;
}
if (change is AvaloniaPropertyChangedEventArgs<T> c)
if (!_isCalculatingValue && change is AvaloniaPropertyChangedEventArgs<T> c)
{
UpdateEffectiveValue(c);
}
@ -188,41 +251,47 @@ namespace Avalonia.PropertyStore
public (Optional<T>, BindingPriority) CalculateValue(BindingPriority maxPriority)
{
var reachedLocalValues = false;
_isCalculatingValue = true;
for (var i = _entries.Count - 1; i >= 0; --i)
try
{
var entry = _entries[i];
if (entry.Priority < maxPriority)
for (var i = _entries.Count - 1; i >= 0; --i)
{
continue;
var entry = _entries[i];
if (entry.Priority < maxPriority)
{
continue;
}
entry.Start();
if (entry.Priority >= BindingPriority.LocalValue &&
maxPriority <= BindingPriority.LocalValue &&
_localValue.HasValue)
{
return (_localValue, BindingPriority.LocalValue);
}
var entryValue = entry.GetValue();
if (entryValue.HasValue)
{
return (entryValue, entry.Priority);
}
}
if (!reachedLocalValues &&
entry.Priority >= BindingPriority.LocalValue &&
maxPriority <= BindingPriority.LocalValue &&
_localValue.HasValue)
if (maxPriority <= BindingPriority.LocalValue && _localValue.HasValue)
{
return (_localValue, BindingPriority.LocalValue);
}
var entryValue = entry.GetValue();
if (entryValue.HasValue)
{
return (entryValue, entry.Priority);
}
return (default, BindingPriority.Unset);
}
if (!reachedLocalValues &&
maxPriority <= BindingPriority.LocalValue &&
_localValue.HasValue)
finally
{
return (_localValue, BindingPriority.LocalValue);
_isCalculatingValue = false;
}
return (default, BindingPriority.Unset);
}
private void UpdateEffectiveValue(AvaloniaPropertyChangedEventArgs<T>? change)

19
src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@ -22,6 +22,9 @@ namespace Avalonia.Utilities
_entries = s_emptyEntries;
}
public int Count => _entries.Length - 1;
public TValue this[int index] => _entries[index].Value;
private (int, bool) TryFindEntry(int propertyId)
{
if (_entries.Length <= 12)
@ -91,7 +94,7 @@ namespace Avalonia.Utilities
return (0, false);
}
public bool TryGetValue(AvaloniaProperty property, [MaybeNull] out TValue value)
public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value)
{
(int index, bool found) = TryFindEntry(property.Id);
if (!found)
@ -163,18 +166,6 @@ namespace Avalonia.Utilities
}
}
public Dictionary<AvaloniaProperty, TValue> ToDictionary()
{
var dict = new Dictionary<AvaloniaProperty, TValue>(_entries.Length - 1);
for (int i = 0; i < _entries.Length - 1; ++i)
{
dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value);
}
return dict;
}
private struct Entry
{
internal int PropertyId;

301
src/Avalonia.Base/ValueStore.cs

@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using Avalonia.PropertyStore;
using Avalonia.Utilities;
@ -26,6 +28,7 @@ namespace Avalonia
private readonly AvaloniaObject _owner;
private readonly IValueSink _sink;
private readonly AvaloniaPropertyValueStore<IValue> _values;
private BatchUpdate? _batchUpdate;
public ValueStore(AvaloniaObject owner)
{
@ -33,9 +36,28 @@ namespace Avalonia
_values = new AvaloniaPropertyValueStore<IValue>();
}
public void BeginBatchUpdate()
{
_batchUpdate ??= new BatchUpdate(this);
_batchUpdate.Begin();
}
public void EndBatchUpdate()
{
if (_batchUpdate is null)
{
throw new InvalidOperationException("No batch update in progress.");
}
if (_batchUpdate.End())
{
_batchUpdate = null;
}
}
public bool IsAnimating(AvaloniaProperty property)
{
if (_values.TryGetValue(property, out var slot))
if (TryGetValue(property, out var slot))
{
return slot.Priority < BindingPriority.LocalValue;
}
@ -45,7 +67,7 @@ namespace Avalonia
public bool IsSet(AvaloniaProperty property)
{
if (_values.TryGetValue(property, out var slot))
if (TryGetValue(property, out var slot))
{
return slot.GetValue().HasValue;
}
@ -58,7 +80,7 @@ namespace Avalonia
BindingPriority maxPriority,
out T value)
{
if (_values.TryGetValue(property, out var slot))
if (TryGetValue(property, out var slot))
{
var v = ((IValue<T>)slot).GetValue(maxPriority);
@ -82,7 +104,7 @@ namespace Avalonia
IDisposable? result = null;
if (_values.TryGetValue(property, out var slot))
if (TryGetValue(property, out var slot))
{
result = SetExisting(slot, property, value, priority);
}
@ -90,23 +112,21 @@ namespace Avalonia
{
// If the property has any coercion callbacks then always create a PriorityValue.
var entry = new PriorityValue<T>(_owner, property, this);
_values.AddValue(property, entry);
AddValue(property, entry);
result = entry.SetValue(value, priority);
}
else
{
var change = new AvaloniaPropertyChangedEventArgs<T>(_owner, property, default, value, priority);
if (priority == BindingPriority.LocalValue)
{
_values.AddValue(property, new LocalValueEntry<T>(value));
_sink.ValueChanged(change);
AddValue(property, new LocalValueEntry<T>(value));
NotifyValueChanged<T>(property, default, value, priority);
}
else
{
var entry = new ConstantValueEntry<T>(property, value, priority, this);
_values.AddValue(property, entry);
_sink.ValueChanged(change);
AddValue(property, entry);
NotifyValueChanged<T>(property, default, value, priority);
result = entry;
}
}
@ -119,7 +139,7 @@ namespace Avalonia
IObservable<BindingValue<T>> source,
BindingPriority priority)
{
if (_values.TryGetValue(property, out var slot))
if (TryGetValue(property, out var slot))
{
return BindExisting(slot, property, source, priority);
}
@ -128,62 +148,69 @@ namespace Avalonia
// If the property has any coercion callbacks then always create a PriorityValue.
var entry = new PriorityValue<T>(_owner, property, this);
var binding = entry.AddBinding(source, priority);
_values.AddValue(property, entry);
binding.Start();
AddValue(property, entry);
return binding;
}
else
{
var entry = new BindingEntry<T>(_owner, property, source, priority, this);
_values.AddValue(property, entry);
entry.Start();
AddValue(property, entry);
return entry;
}
}
public void ClearLocalValue<T>(StyledPropertyBase<T> property)
{
if (_values.TryGetValue(property, out var slot))
if (TryGetValue(property, out var slot))
{
if (slot is PriorityValue<T> p)
{
p.ClearLocalValue();
}
else
else if (slot.Priority == BindingPriority.LocalValue)
{
var remove = slot is ConstantValueEntry<T> c ?
c.Priority == BindingPriority.LocalValue :
!(slot is IPriorityValueEntry<T>);
var old = TryGetValue(property, BindingPriority.LocalValue, out var value) ? value : default;
if (remove)
// During batch update values can't be removed immediately because they're needed to raise
// a correctly-typed _sink.ValueChanged notification. They instead mark themselves for removal
// by setting their priority to Unset.
if (!IsBatchUpdating())
{
var old = TryGetValue(property, BindingPriority.LocalValue, out var value) ? value : default;
_values.Remove(property);
_sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
property,
new Optional<T>(old),
default,
BindingPriority.Unset));
}
else if (slot is IDisposable d)
{
d.Dispose();
}
else
{
// Local value entries are optimized and contain only a single value field to save space,
// so there's no way to mark them for removal at the end of a batch update. Instead convert
// them to a constant value entry with Unset priority in the event of a local value being
// cleared during a batch update.
var sentinel = new ConstantValueEntry<T>(property, default, BindingPriority.Unset, _sink);
_values.SetValue(property, sentinel);
}
NotifyValueChanged<T>(property, old, default, BindingPriority.Unset);
}
}
}
public void CoerceValue<T>(StyledPropertyBase<T> property)
{
if (_values.TryGetValue(property, out var slot))
if (TryGetValue(property, out var slot))
{
if (slot is PriorityValue<T> p)
{
p.CoerceValue();
p.UpdateEffectiveValue();
}
}
}
public Diagnostics.AvaloniaPropertyValue? GetDiagnostic(AvaloniaProperty property)
{
if (_values.TryGetValue(property, out var slot))
if (TryGetValue(property, out var slot))
{
var slotValue = slot.GetValue();
return new Diagnostics.AvaloniaPropertyValue(
@ -198,7 +225,17 @@ namespace Avalonia
void IValueSink.ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
_sink.ValueChanged(change);
if (_batchUpdate is object)
{
if (change.IsEffectiveValueChange)
{
NotifyValueChanged<T>(change.Property, change.OldValue, change.NewValue, change.Priority);
}
}
else
{
_sink.ValueChanged(change);
}
}
void IValueSink.Completed<T>(
@ -206,13 +243,18 @@ namespace Avalonia
IPriorityValueEntry entry,
Optional<T> oldValue)
{
if (_values.TryGetValue(property, out var slot))
// We need to include remove sentinels here so call `_values.TryGetValue` directly.
if (_values.TryGetValue(property, out var slot) && slot == entry)
{
if (slot == entry)
if (_batchUpdate is null)
{
_values.Remove(property);
_sink.Completed(property, entry, oldValue);
}
else
{
_batchUpdate.ValueChanged(property, oldValue.ToObject());
}
}
}
@ -240,16 +282,13 @@ namespace Avalonia
{
var old = l.GetValue(BindingPriority.LocalValue);
l.SetValue(value);
_sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
property,
old,
value,
priority));
NotifyValueChanged<T>(property, old, value, priority);
}
else
{
var priorityValue = new PriorityValue<T>(_owner, property, this, l);
if (IsBatchUpdating())
priorityValue.BeginBatchUpdate();
result = priorityValue.SetValue(value, priority);
_values.SetValue(property, priorityValue);
}
@ -273,6 +312,11 @@ namespace Avalonia
if (slot is IPriorityValueEntry<T> e)
{
priorityValue = new PriorityValue<T>(_owner, property, this, e);
if (IsBatchUpdating())
{
priorityValue.BeginBatchUpdate();
}
}
else if (slot is PriorityValue<T> p)
{
@ -289,8 +333,181 @@ namespace Avalonia
var binding = priorityValue.AddBinding(source, priority);
_values.SetValue(property, priorityValue);
binding.Start();
priorityValue.UpdateEffectiveValue();
return binding;
}
private void AddValue(AvaloniaProperty property, IValue value)
{
_values.AddValue(property, value);
if (IsBatchUpdating() && value is IBatchUpdate batch)
batch.BeginBatchUpdate();
value.Start();
}
private void NotifyValueChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{
if (_batchUpdate is null)
{
_sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
property,
oldValue,
newValue,
priority));
}
else
{
_batchUpdate.ValueChanged(property, oldValue.ToObject());
}
}
private bool IsBatchUpdating() => _batchUpdate?.IsBatchUpdating == true;
private bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out IValue value)
{
return _values.TryGetValue(property, out value) && !IsRemoveSentinel(value);
}
private static bool IsRemoveSentinel(IValue value)
{
// Local value entries are optimized and contain only a single value field to save space,
// so there's no way to mark them for removal at the end of a batch update. Instead a
// ConstantValueEntry with a priority of Unset is used as a sentinel value.
return value is IConstantValueEntry t && t.Priority == BindingPriority.Unset;
}
private class BatchUpdate
{
private ValueStore _owner;
private List<Notification>? _notifications;
private int _batchUpdateCount;
private int _iterator = -1;
public BatchUpdate(ValueStore owner) => _owner = owner;
public bool IsBatchUpdating => _batchUpdateCount > 0;
public void Begin()
{
if (_batchUpdateCount++ == 0)
{
var values = _owner._values;
for (var i = 0; i < values.Count; ++i)
{
(values[i] as IBatchUpdate)?.BeginBatchUpdate();
}
}
}
public bool End()
{
if (--_batchUpdateCount > 0)
return false;
var values = _owner._values;
// First call EndBatchUpdate on all bindings. This should cause the active binding to be subscribed
// but notifications will still not be raised because the owner ValueStore will still have a reference
// to this batch update object.
for (var i = 0; i < values.Count; ++i)
{
(values[i] as IBatchUpdate)?.EndBatchUpdate();
// Somehow subscribing to a binding caused a new batch update. This shouldn't happen but in case it
// does, abort and continue batch updating.
if (_batchUpdateCount > 0)
return false;
}
if (_notifications is object)
{
// Raise all batched notifications. Doing this can cause other notifications to be added and even
// cause a new batch update to start, so we need to handle _notifications being modified by storing
// the index in field.
_iterator = 0;
for (; _iterator < _notifications.Count; ++_iterator)
{
var entry = _notifications[_iterator];
if (values.TryGetValue(entry.property, out var slot))
{
var oldValue = entry.oldValue;
var newValue = slot.GetValue();
// Raising this notification can cause a new batch update to be started, which in turn
// results in another change to the property. In this case we need to update the old value
// so that the *next* notification has an oldValue which follows on from the newValue
// raised here.
_notifications[_iterator] = new Notification
{
property = entry.property,
oldValue = newValue,
};
// Call _sink.ValueChanged with an appropriately typed AvaloniaPropertyChangedEventArgs<T>.
slot.RaiseValueChanged(_owner._sink, _owner._owner, entry.property, oldValue, newValue);
// During batch update values can't be removed immediately because they're needed to raise
// the _sink.ValueChanged notification. They instead mark themselves for removal by setting
// their priority to Unset. We need to re-read the slot here because raising ValueChanged
// could have caused it to be updated.
if (values.TryGetValue(entry.property, out var updatedSlot) &&
updatedSlot.Priority == BindingPriority.Unset)
{
values.Remove(entry.property);
}
}
else
{
throw new AvaloniaInternalException("Value could not be found at the end of batch update.");
}
// If a new batch update was started while ending this one, abort.
if (_batchUpdateCount > 0)
return false;
}
}
_iterator = int.MaxValue - 1;
return true;
}
public void ValueChanged(AvaloniaProperty property, Optional<object> oldValue)
{
_notifications ??= new List<Notification>();
for (var i = 0; i < _notifications.Count; ++i)
{
if (_notifications[i].property == property)
{
oldValue = _notifications[i].oldValue;
_notifications.RemoveAt(i);
if (i <= _iterator)
--_iterator;
break;
}
}
_notifications.Add(new Notification
{
property = property,
oldValue = oldValue,
});
}
private struct Notification
{
public AvaloniaProperty property;
public Optional<object> oldValue;
}
}
}
}

11
src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs

@ -3275,7 +3275,7 @@ namespace Avalonia.Collections
addIndex);
// next check if we need to add an item into the current group
// bool needsGrouping = false;
bool needsGrouping = false;
if (Count == 1 && GroupDescriptions.Count > 0)
{
// if this is the first item being added
@ -3302,7 +3302,7 @@ namespace Avalonia.Collections
// otherwise, we need to validate that it is within the current page.
if (PageSize == 0 || (PageIndex + 1) * PageSize > leafIndex)
{
//needsGrouping = true;
needsGrouping = true;
int pageStartIndex = PageIndex * PageSize;
@ -3340,6 +3340,13 @@ namespace Avalonia.Collections
}
}
// if we need to add the item into the current group
// that will be displayed
if (needsGrouping)
{
this._group.AddToSubgroups(addedItem, false /*loading*/);
}
int addedIndex = IndexOf(addedItem);
// if the item is within the current page

56
src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs

@ -17,7 +17,6 @@ namespace Avalonia.Controls
/// </summary>
public class DataGridCheckBoxColumn : DataGridBoundColumn
{
private bool _beganEditWithKeyboard;
private CheckBox _currentCheckBox;
private DataGrid _owningGrid;
@ -153,23 +152,7 @@ namespace Avalonia.Controls
{
if (editingElement is CheckBox editingCheckBox)
{
bool? uneditedValue = editingCheckBox.IsChecked;
bool editValue = false;
if(editingEventArgs is PointerPressedEventArgs args)
{
// Editing was triggered by a mouse click
Point position = args.GetPosition(editingCheckBox);
Rect rect = new Rect(0, 0, editingCheckBox.Bounds.Width, editingCheckBox.Bounds.Height);
editValue = rect.Contains(position);
}
else if (_beganEditWithKeyboard)
{
// Editing began by a user pressing spacebar
editValue = true;
_beganEditWithKeyboard = false;
}
if (editValue)
void EditValue()
{
// User clicked the checkbox itself or pressed space, let's toggle the IsChecked value
if (editingCheckBox.IsThreeState)
@ -192,6 +175,40 @@ namespace Avalonia.Controls
editingCheckBox.IsChecked = !editingCheckBox.IsChecked;
}
}
bool? uneditedValue = editingCheckBox.IsChecked;
if(editingEventArgs is PointerPressedEventArgs args)
{
void ProcessPointerArgs()
{
// Editing was triggered by a mouse click
Point position = args.GetPosition(editingCheckBox);
Rect rect = new Rect(0, 0, editingCheckBox.Bounds.Width, editingCheckBox.Bounds.Height);
if(rect.Contains(position))
{
EditValue();
}
}
void OnLayoutUpdated(object sender, EventArgs e)
{
if(!editingCheckBox.Bounds.IsEmpty)
{
editingCheckBox.LayoutUpdated -= OnLayoutUpdated;
ProcessPointerArgs();
}
}
if(editingCheckBox.Bounds.IsEmpty)
{
editingCheckBox.LayoutUpdated += OnLayoutUpdated;
}
else
{
ProcessPointerArgs();
}
}
return uneditedValue;
}
return false;
@ -284,13 +301,10 @@ namespace Avalonia.Controls
CheckBox checkBox = GetCellContent(row) as CheckBox;
if (checkBox == _currentCheckBox)
{
_beganEditWithKeyboard = true;
OwningGrid.BeginEdit();
return;
}
}
}
_beganEditWithKeyboard = false;
}
private void OwningGrid_LoadingRow(object sender, DataGridRowEventArgs e)

5
src/Avalonia.Controls/ApiCompatBaseline.txt

@ -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

11
src/Avalonia.Controls/Application.cs

@ -30,7 +30,7 @@ namespace Avalonia
/// method.
/// - Tracks the lifetime of the application.
/// </remarks>
public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceHost
public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceHost, IApplicationPlatformEvents
{
/// <summary>
/// The application-global data templates.
@ -55,6 +55,8 @@ namespace Avalonia
/// <inheritdoc/>
public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
public event EventHandler<UrlOpenedEventArgs> UrlsOpened;
/// <summary>
/// Creates an instance of the <see cref="Application"/> class.
/// </summary>
@ -247,7 +249,11 @@ namespace Avalonia
public virtual void OnFrameworkInitializationCompleted()
{
}
void IApplicationPlatformEvents.RaiseUrlsOpened(string[] urls)
{
UrlsOpened?.Invoke(this, new UrlOpenedEventArgs (urls));
}
private void NotifyResourcesChanged(ResourcesChangedEventArgs e)
@ -288,5 +294,6 @@ namespace Avalonia
get => _name;
set => SetAndRaise(NameProperty, ref _name, value);
}
}
}

14
src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs

@ -5,6 +5,7 @@ using System.Threading;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Interactivity;
using Avalonia.Platform;
using Avalonia.Threading;
namespace Avalonia.Controls.ApplicationLifetimes
@ -102,6 +103,14 @@ namespace Avalonia.Controls.ApplicationLifetimes
public int Start(string[] args)
{
Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args));
var options = AvaloniaLocator.Current.GetService<ClassicDesktopStyleApplicationLifetimeOptions>();
if(options != null && options.ProcessUrlActivationCommandLine && args.Length > 0)
{
((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(args);
}
_cts = new CancellationTokenSource();
MainWindow?.Show();
Dispatcher.UIThread.MainLoop(_cts.Token);
@ -115,6 +124,11 @@ namespace Avalonia.Controls.ApplicationLifetimes
_activeLifetime = null;
}
}
public class ClassicDesktopStyleApplicationLifetimeOptions
{
public bool ProcessUrlActivationCommandLine { get; set; }
}
}
namespace Avalonia

8
src/Avalonia.Controls/AutoCompleteBox.cs

@ -483,7 +483,9 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterDirect<AutoCompleteBox, object>(
nameof(SelectedItem),
o => o.SelectedItem,
(o, v) => o.SelectedItem = v);
(o, v) => o.SelectedItem = v,
defaultBindingMode: BindingMode.TwoWay,
enableDataValidation: true);
/// <summary>
/// Identifies the
@ -1333,7 +1335,7 @@ namespace Avalonia.Controls
base.OnApplyTemplate(e);
}
/// <summary>
/// Called to update the validation state for properties for which data validation is
/// enabled.
@ -1342,7 +1344,7 @@ namespace Avalonia.Controls
/// <param name="value">The new binding value for the property.</param>
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{
if (property == TextProperty)
if (property == TextProperty || property == SelectedItemProperty)
{
DataValidationErrors.SetError(this, value.Error);
}

69
src/Avalonia.Controls/Control.cs

@ -8,6 +8,8 @@ using Avalonia.Rendering;
using Avalonia.Styling;
using Avalonia.VisualTree;
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
@ -18,25 +20,25 @@ namespace Avalonia.Controls
///
/// - A <see cref="Tag"/> property to allow user-defined data to be attached to the control.
/// </remarks>
public class Control : InputElement, IControl, INamed, ISupportInitialize, IVisualBrushInitialize, ISetterValue
public class Control : InputElement, IControl, INamed, IVisualBrushInitialize, ISetterValue
{
/// <summary>
/// Defines the <see cref="FocusAdorner"/> property.
/// </summary>
public static readonly StyledProperty<ITemplate<IControl>> FocusAdornerProperty =
AvaloniaProperty.Register<Control, ITemplate<IControl>>(nameof(FocusAdorner));
public static readonly StyledProperty<ITemplate<IControl>?> FocusAdornerProperty =
AvaloniaProperty.Register<Control, ITemplate<IControl>?>(nameof(FocusAdorner));
/// <summary>
/// Defines the <see cref="Tag"/> property.
/// </summary>
public static readonly StyledProperty<object> TagProperty =
AvaloniaProperty.Register<Control, object>(nameof(Tag));
public static readonly StyledProperty<object?> TagProperty =
AvaloniaProperty.Register<Control, object?>(nameof(Tag));
/// <summary>
/// Defines the <see cref="ContextMenu"/> property.
/// </summary>
public static readonly StyledProperty<ContextMenu> ContextMenuProperty =
AvaloniaProperty.Register<Control, ContextMenu>(nameof(ContextMenu));
public static readonly StyledProperty<ContextMenu?> ContextMenuProperty =
AvaloniaProperty.Register<Control, ContextMenu?>(nameof(ContextMenu));
/// <summary>
/// Event raised when an element wishes to be scrolled into view.
@ -44,16 +46,16 @@ namespace Avalonia.Controls
public static readonly RoutedEvent<RequestBringIntoViewEventArgs> RequestBringIntoViewEvent =
RoutedEvent.Register<Control, RequestBringIntoViewEventArgs>("RequestBringIntoView", RoutingStrategies.Bubble);
private DataTemplates _dataTemplates;
private IControl _focusAdorner;
private DataTemplates? _dataTemplates;
private IControl? _focusAdorner;
/// <summary>
/// Gets or sets the control's focus adorner.
/// </summary>
public ITemplate<IControl> FocusAdorner
public ITemplate<IControl>? FocusAdorner
{
get { return GetValue(FocusAdornerProperty); }
set { SetValue(FocusAdornerProperty, value); }
get => GetValue(FocusAdornerProperty);
set => SetValue(FocusAdornerProperty, value);
}
/// <summary>
@ -63,27 +65,27 @@ namespace Avalonia.Controls
/// Each control may define data templates which are applied to the control itself and its
/// children.
/// </remarks>
public DataTemplates DataTemplates => _dataTemplates ?? (_dataTemplates = new DataTemplates());
public DataTemplates DataTemplates => _dataTemplates ??= new DataTemplates();
/// <summary>
/// Gets or sets a context menu to the control.
/// </summary>
public ContextMenu ContextMenu
public ContextMenu? ContextMenu
{
get { return GetValue(ContextMenuProperty); }
set { SetValue(ContextMenuProperty, value); }
get => GetValue(ContextMenuProperty);
set => SetValue(ContextMenuProperty, value);
}
/// <summary>
/// Gets or sets a user-defined object attached to the control.
/// </summary>
public object Tag
public object? Tag
{
get { return GetValue(TagProperty); }
set { SetValue(TagProperty, value); }
get => GetValue(TagProperty);
set => SetValue(TagProperty, value);
}
public new IControl Parent => (IControl)base.Parent;
public new IControl? Parent => (IControl?)base.Parent;
/// <inheritdoc/>
bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null;
@ -106,15 +108,10 @@ namespace Avalonia.Controls
{
var c = i as IControl;
if (c?.IsInitialized == false)
if (c?.IsInitialized == false && c is ISupportInitialize init)
{
var init = c as ISupportInitialize;
if (init != null)
{
init.BeginInit();
init.EndInit();
}
init.BeginInit();
init.EndInit();
}
}
}
@ -131,10 +128,7 @@ namespace Avalonia.Controls
/// Gets the element that receives the focus adorner.
/// </summary>
/// <returns>The control that receives the focus adorner.</returns>
protected virtual IControl GetTemplateFocusTarget()
{
return this;
}
protected virtual IControl? GetTemplateFocusTarget() => this;
/// <inheritdoc/>
protected sealed override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e)
@ -173,15 +167,10 @@ namespace Avalonia.Controls
}
}
if (_focusAdorner != null)
if (_focusAdorner != null && GetTemplateFocusTarget() is Visual target)
{
var target = (Visual)GetTemplateFocusTarget();
if (target != null)
{
AdornerLayer.SetAdornedElement((Visual)_focusAdorner, target);
adornerLayer.Children.Add(_focusAdorner);
}
AdornerLayer.SetAdornedElement((Visual)_focusAdorner, target);
adornerLayer.Children.Add(_focusAdorner);
}
}
}

77
src/Avalonia.Controls/DefinitionBase.cs

@ -662,31 +662,64 @@ namespace Avalonia.Controls
{
DefinitionBase definitionBase = _registry[i];
if (sharedMinSizeChanged || definitionBase.LayoutWasUpdated)
// we'll set d.UseSharedMinimum to maintain the invariant:
// d.UseSharedMinimum iff d._minSize < this.MinSize
// i.e. iff d is not a "long-pole" definition.
//
// Measure/Arrange of d's Grid uses d._minSize for long-pole
// definitions, and max(d._minSize, shared size) for
// short-pole definitions. This distinction allows us to react
// to changes in "long-pole-ness" more efficiently and correctly,
// by avoiding remeasures when a long-pole definition changes.
bool useSharedMinimum = !MathUtilities.AreClose(definitionBase._minSize, sharedMinSize);
// before doing that, determine whether d's Grid needs to be remeasured.
// It's important _not_ to remeasure if the last measure is still
// valid, otherwise infinite loops are possible
bool measureIsValid;
if(!definitionBase.UseSharedMinimum)
{
// if definition's min size is different, then need to re-measure
if (!MathUtilities.AreClose(sharedMinSize, definitionBase.MinSize))
{
Grid parentGrid = (Grid)definitionBase.Parent;
parentGrid.InvalidateMeasure();
definitionBase.UseSharedMinimum = true;
}
else
{
definitionBase.UseSharedMinimum = false;
// if measure is valid then also need to check arrange.
// Note: definitionBase.SizeCache is volatile but at this point
// it contains up-to-date final size
if (!MathUtilities.AreClose(sharedMinSize, definitionBase.SizeCache))
{
Grid parentGrid = (Grid)definitionBase.Parent;
parentGrid.InvalidateArrange();
}
}
// d was a long-pole. measure is valid iff it's still a long-pole,
// since previous measure didn't use shared size.
measureIsValid = !useSharedMinimum;
}
else if(useSharedMinimum)
{
// d was a short-pole, and still is. measure is valid
// iff the shared size didn't change
measureIsValid = !sharedMinSizeChanged;
}
else
{
// d was a short-pole, but is now a long-pole. This can
// happen in several ways:
// a. d's minSize increased to or past the old shared size
// b. other long-pole definitions decreased, leaving
// d as the new winner
// In the former case, the measure is valid - it used
// d's new larger minSize. In the latter case, the
// measure is invalid - it used the old shared size,
// which is larger than d's (possibly changed) minSize
measureIsValid = (definitionBase.LayoutWasUpdated &&
MathUtilities.GreaterThanOrClose(definitionBase._minSize, this.MinSize));
}
definitionBase.LayoutWasUpdated = false;
if(!measureIsValid)
{
definitionBase.Parent.InvalidateMeasure();
}
else if (!MathUtilities.AreClose(sharedMinSize, definitionBase.SizeCache))
{
// if measure is valid then also need to check arrange.
// Note: definitionBase.SizeCache is volatile but at this point
// it contains up-to-date final size
definitionBase.Parent.InvalidateArrange();
}
// now we can restore the invariant, and clear the layout flag
definitionBase.UseSharedMinimum = useSharedMinimum;
definitionBase.LayoutWasUpdated = false;
}
_minSize = sharedMinSize;

2
src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs

@ -3,5 +3,7 @@ namespace Avalonia.Controls
public interface INativeMenuExporterEventsImplBridge
{
void RaiseNeedsUpdate ();
void RaiseOpening();
void RaiseClosed();
}
}

33
src/Avalonia.Controls/NativeMenu.cs

@ -12,13 +12,34 @@ namespace Avalonia.Controls
private readonly AvaloniaList<NativeMenuItemBase> _items =
new AvaloniaList<NativeMenuItemBase> { ResetBehavior = ResetBehavior.Remove };
private NativeMenuItem _parent;
[Content]
public IList<NativeMenuItemBase> Items => _items;
/// <summary>
/// Raised when the user clicks the menu and before its opened. Use this event to update the menu dynamically.
/// Raised when the menu requests an update.
/// </summary>
/// <remarks>
/// Use this event to add, remove or modify menu items before a menu is
/// shown or a hotkey is pressed.
/// </remarks>
public event EventHandler<EventArgs> NeedsUpdate;
/// <summary>
/// Raised before the menu is opened.
/// </summary>
/// <remarks>
/// Do not update the menu in this event; use <see cref="NeedsUpdate"/>.
/// </remarks>
public event EventHandler<EventArgs> Opening;
/// <summary>
/// Raised after the menu is closed.
/// </summary>
/// <remarks>
/// Do not update the menu in this event; use <see cref="NeedsUpdate"/>.
/// </remarks>
public event EventHandler<EventArgs> Closed;
public NativeMenu()
{
@ -27,10 +48,20 @@ namespace Avalonia.Controls
}
void INativeMenuExporterEventsImplBridge.RaiseNeedsUpdate()
{
NeedsUpdate?.Invoke(this, EventArgs.Empty);
}
void INativeMenuExporterEventsImplBridge.RaiseOpening()
{
Opening?.Invoke(this, EventArgs.Empty);
}
void INativeMenuExporterEventsImplBridge.RaiseClosed()
{
Closed?.Invoke(this, EventArgs.Empty);
}
private void Validator(NativeMenuItemBase obj)
{
if (obj.Parent != null)

16
src/Avalonia.Controls/NativeMenuItemSeparator.cs

@ -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 => "-";
}
}

10
src/Avalonia.Controls/NativeMenuItemSeperator.cs

@ -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 => "-";
}
}

17
src/Avalonia.Controls/Notifications/NotificationCard.cs

@ -16,6 +16,11 @@ namespace Avalonia.Controls.Notifications
private bool _isClosed;
private bool _isClosing;
static NotificationCard()
{
CloseOnClickProperty.Changed.AddClassHandler<Button>(OnCloseOnClickPropertyChanged);
}
/// <summary>
/// Initializes a new instance of the <see cref="NotificationCard"/> class.
/// </summary>
@ -105,22 +110,26 @@ namespace Avalonia.Controls.Notifications
public static bool GetCloseOnClick(Button obj)
{
Contract.Requires<ArgumentNullException>(obj != null);
return (bool)obj.GetValue(CloseOnClickProperty);
}
public static void SetCloseOnClick(Button obj, bool value)
{
Contract.Requires<ArgumentNullException>(obj != null);
obj.SetValue(CloseOnClickProperty, value);
}
/// <summary>
/// Defines the CloseOnClick property.
/// </summary>
public static readonly AvaloniaProperty CloseOnClickProperty =
AvaloniaProperty.RegisterAttached<Button, bool>("CloseOnClick", typeof(NotificationCard)/*, validate: CloseOnClickChanged*/);
public static readonly AttachedProperty<bool> CloseOnClickProperty =
AvaloniaProperty.RegisterAttached<NotificationCard, Button, bool>("CloseOnClick", defaultValue: false);
private static bool CloseOnClickChanged(Button button, bool value)
private static void OnCloseOnClickPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e)
{
var button = (Button)d;
var value = (bool)e.NewValue;
if (value)
{
button.Click += Button_Click;
@ -129,8 +138,6 @@ namespace Avalonia.Controls.Notifications
{
button.Click -= Button_Click;
}
return true;
}
/// <summary>

18
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

@ -91,14 +91,14 @@ namespace Avalonia.Controls
/// </summary>
public static readonly DirectProperty<NumericUpDown, string> TextProperty =
AvaloniaProperty.RegisterDirect<NumericUpDown, string>(nameof(Text), o => o.Text, (o, v) => o.Text = v,
defaultBindingMode: BindingMode.TwoWay);
defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
/// <summary>
/// Defines the <see cref="Value"/> property.
/// </summary>
public static readonly DirectProperty<NumericUpDown, double> ValueProperty =
AvaloniaProperty.RegisterDirect<NumericUpDown, double>(nameof(Value), updown => updown.Value,
(updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay);
(updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
/// <summary>
/// Defines the <see cref="Watermark"/> property.
@ -370,6 +370,20 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Called to update the validation state for properties for which data validation is
/// enabled.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="value">The new binding value for the property.</param>
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{
if (property == TextProperty || property == ValueProperty)
{
DataValidationErrors.SetError(this, value.Error);
}
}
/// <summary>
/// Called when the <see cref="CultureInfo"/> property value changed.
/// </summary>

7
src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs

@ -0,0 +1,7 @@
namespace Avalonia.Platform
{
public interface IApplicationPlatformEvents
{
void RaiseUrlsOpened(string[] urls);
}
}

22
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -64,7 +64,7 @@ namespace Avalonia.Controls.Primitives
nameof(SelectedItem),
o => o.SelectedItem,
(o, v) => o.SelectedItem = v,
defaultBindingMode: BindingMode.TwoWay);
defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
/// <summary>
/// Defines the <see cref="SelectedItems"/> property.
@ -466,6 +466,20 @@ namespace Avalonia.Controls.Primitives
EndUpdating();
}
/// <summary>
/// Called to update the validation state for properties for which data validation is
/// enabled.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="value">The new binding value for the property.</param>
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{
if (property == SelectedItemProperty)
{
DataValidationErrors.SetError(this, value.Error);
}
}
protected override void OnInitialized()
{
base.OnInitialized();
@ -707,7 +721,7 @@ namespace Avalonia.Controls.Primitives
_oldSelectedItem = SelectedItem;
}
else if (e.PropertyName == nameof(InternalSelectionModel.WritableSelectedItems) &&
_oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems)
_oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems)
{
RaisePropertyChanged(
SelectedItemsProperty,
@ -977,7 +991,7 @@ namespace Avalonia.Controls.Primitives
public Optional<ISelectionModel> Selection { get; set; }
public Optional<IList?> SelectedItems { get; set; }
public Optional<int> SelectedIndex
public Optional<int> SelectedIndex
{
get => _selectedIndex;
set
@ -996,6 +1010,6 @@ namespace Avalonia.Controls.Primitives
_selectedIndex = default;
}
}
}
}
}
}

43
src/Avalonia.Controls/TextBox.cs

@ -514,21 +514,36 @@ namespace Avalonia.Controls
private void HandleTextInput(string input)
{
if (!IsReadOnly)
if (IsReadOnly)
{
input = RemoveInvalidCharacters(input);
string text = Text ?? string.Empty;
int caretIndex = CaretIndex;
if (!string.IsNullOrEmpty(input) && (MaxLength == 0 || input.Length + text.Length - (Math.Abs(SelectionStart - SelectionEnd)) <= MaxLength))
{
DeleteSelection();
caretIndex = CaretIndex;
text = Text ?? string.Empty;
SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
CaretIndex += input.Length;
ClearSelection();
_undoRedoHelper.DiscardRedo();
}
return;
}
input = RemoveInvalidCharacters(input);
if (string.IsNullOrEmpty(input))
{
return;
}
string text = Text ?? string.Empty;
int caretIndex = CaretIndex;
int newLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd);
if (MaxLength > 0 && newLength > MaxLength)
{
input = input.Remove(Math.Max(0, input.Length - (newLength - MaxLength)));
}
if (!string.IsNullOrEmpty(input))
{
DeleteSelection();
caretIndex = CaretIndex;
text = Text ?? string.Empty;
SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
CaretIndex += input.Length;
ClearSelection();
_undoRedoHelper.DiscardRedo();
}
}

14
src/Avalonia.Controls/UrlOpenedEventArgs.cs

@ -0,0 +1,14 @@
using System;
namespace Avalonia
{
public class UrlOpenedEventArgs : EventArgs
{
public UrlOpenedEventArgs(string[] urls)
{
Urls = urls;
}
public string[] Urls { get; }
}
}

45
src/Avalonia.Controls/Window.cs

@ -4,10 +4,7 @@ using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Controls.Chrome;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
@ -482,10 +479,9 @@ namespace Avalonia.Controls
try
{
if (!ignoreCancel && HandleClosing())
if (!ignoreCancel && ShouldCancelClose())
{
close = false;
return;
}
}
finally
@ -497,11 +493,25 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Handles a closing notification from <see cref="IWindowImpl.Closing"/>.
/// <returns>true if closing is cancelled. Otherwise false.</returns>
/// </summary>
protected virtual bool HandleClosing()
{
if (!ShouldCancelClose())
{
CloseInternal();
return false;
}
return true;
}
private void CloseInternal()
{
foreach (var (child, _) in _children.ToList())
{
// if we HandleClosing() before then there will be no children.
child.CloseInternal();
}
@ -515,20 +525,18 @@ namespace Avalonia.Controls
PlatformImpl?.Dispose();
}
/// <summary>
/// Handles a closing notification from <see cref="IWindowImpl.Closing"/>.
/// </summary>
protected virtual bool HandleClosing()
private bool ShouldCancelClose(CancelEventArgs args = null)
{
if (args is null)
{
args = new CancelEventArgs();
}
bool canClose = true;
foreach (var (child, _) in _children.ToList())
{
if (!child.HandleClosing())
{
child.CloseInternal();
}
else
if (child.ShouldCancelClose(args))
{
canClose = false;
}
@ -536,15 +544,12 @@ namespace Avalonia.Controls
if (canClose)
{
var args = new CancelEventArgs();
OnClosing(args);
return args.Cancel;
}
else
{
return !canClose;
}
return true;
}
protected virtual void HandleWindowStateChanged(WindowState state)

2
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<Type>(), null);
if (builderMethod == null)
throw Die($"{entryPoint.DeclaringType.FullName} doesn't have a method named {BuilderMethodName}");
Design.IsDesignMode = true;

2
src/Avalonia.FreeDesktop/DBusMenuExporter.cs

@ -192,7 +192,7 @@ namespace Avalonia.FreeDesktop
{
var (it, menu) = i;
if (it is NativeMenuItemSeperator)
if (it is NativeMenuItemSeparator)
{
if (name == "type")
return "separator";

2
src/Avalonia.Layout/ElementManager.cs

@ -207,7 +207,7 @@ namespace Avalonia.Layout
}
}
public bool IsIndexValidInData(int currentIndex) => currentIndex >= 0 && currentIndex < _context.ItemCount;
public bool IsIndexValidInData(int currentIndex) => (uint)currentIndex < _context.ItemCount;
public ILayoutable GetRealizedElement(int dataIndex)
{

14
src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs

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

7
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -1,6 +1,5 @@
using System;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Platform;
@ -9,7 +8,6 @@ using Avalonia.Native.Interop;
using Avalonia.OpenGL;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Platform.Interop;
namespace Avalonia.Native
{
@ -86,7 +84,10 @@ namespace Avalonia.Native
void DoInitialize(AvaloniaNativePlatformOptions options)
{
_options = options;
_factory.Initialize(new GCHandleDeallocator());
var applicationPlatform = new AvaloniaNativeApplicationPlatform();
_factory.Initialize(new GCHandleDeallocator(), applicationPlatform);
if (_factory.MacOptions != null)
{
var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>();

26
src/Avalonia.Native/IAvnMenu.cs

@ -20,11 +20,23 @@ namespace Avalonia.Native.Interop
{
_parent?.RaiseNeedsUpdate();
}
public void Opening()
{
_parent?.RaiseOpening();
}
public void Closed()
{
_parent?.RaiseClosed();
}
}
partial interface IAvnMenu
{
void RaiseNeedsUpdate();
void RaiseOpening();
void RaiseClosed();
void Deinitialise();
}
}
@ -45,6 +57,16 @@ namespace Avalonia.Native.Interop.Impl
_exporter.UpdateIfNeeded();
}
public void RaiseOpening()
{
(ManagedMenu as INativeMenuExporterEventsImplBridge).RaiseOpening();
}
public void RaiseClosed()
{
(ManagedMenu as INativeMenuExporterEventsImplBridge).RaiseClosed();
}
internal NativeMenu ManagedMenu { get; private set; }
public static __MicroComIAvnMenuProxy Create(IAvaloniaNativeFactory factory)
@ -103,8 +125,8 @@ namespace Avalonia.Native.Interop.Impl
private __MicroComIAvnMenuItemProxy CreateNew(IAvaloniaNativeFactory factory, NativeMenuItemBase item)
{
var nativeItem = (__MicroComIAvnMenuItemProxy)(item is NativeMenuItemSeperator ?
factory.CreateMenuItemSeperator() :
var nativeItem = (__MicroComIAvnMenuItemProxy)(item is NativeMenuItemSeparator ?
factory.CreateMenuItemSeparator() :
factory.CreateMenuItem());
nativeItem.ManagedMenuItem = item;

15
src/Avalonia.Native/avn.idl

@ -403,7 +403,7 @@ enum AvnExtendClientAreaChromeHints
[uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)]
interface IAvaloniaNativeFactory : IUnknown
{
HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator);
HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator, IAvnApplicationEvents* appCb);
IAvnMacOptions* GetMacOptions();
HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnWindow** ppv);
HRESULT CreatePopup(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnPopup** ppv);
@ -417,7 +417,7 @@ interface IAvaloniaNativeFactory : IUnknown
HRESULT SetAppMenu(IAvnMenu* menu);
HRESULT CreateMenu(IAvnMenuEvents* cb, IAvnMenu** ppv);
HRESULT CreateMenuItem(IAvnMenuItem** ppv);
HRESULT CreateMenuItemSeperator(IAvnMenuItem** ppv);
HRESULT CreateMenuItemSeparator(IAvnMenuItem** ppv);
}
[uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)]
@ -685,10 +685,9 @@ interface IAvnMenuItem : IUnknown
[uuid(0af7df53-7632-42f4-a650-0992c361b477)]
interface IAvnMenuEvents : IUnknown
{
/**
* NeedsUpdate
*/
void NeedsUpdate();
void Opening();
void Closed();
}
[uuid(5142bb41-66ab-49e7-bb37-cd079c000f27)]
@ -728,3 +727,9 @@ interface IAvnNativeControlHostTopLevelAttachment : IUnknown
void HideWithSize(float width, float height);
void ReleaseChild();
}
[uuid(6575b5af-f27a-4609-866c-f1f014c20f79)]
interface IAvnApplicationEvents : IUnknown
{
void FilesOpened (IAvnStringArray* urls);
}

4
src/Avalonia.Styling/IStyledElement.cs

@ -1,4 +1,5 @@
using System;
using System.ComponentModel;
using Avalonia.Controls;
using Avalonia.LogicalTree;
using Avalonia.Styling;
@ -10,7 +11,8 @@ namespace Avalonia
IStyleHost,
ILogical,
IResourceHost,
IDataContextProvider
IDataContextProvider,
ISupportInitialize
{
/// <summary>
/// Occurs when the control has finished initialization.

28
src/Avalonia.Styling/StyledElement.cs

@ -334,7 +334,16 @@ namespace Avalonia
{
if (_initCount == 0 && !_styled)
{
AvaloniaLocator.Current.GetService<IStyler>()?.ApplyStyles(this);
try
{
BeginBatchUpdate();
AvaloniaLocator.Current.GetService<IStyler>()?.ApplyStyles(this);
}
finally
{
EndBatchUpdate();
}
_styled = true;
}
@ -748,12 +757,21 @@ namespace Avalonia
{
if (_appliedStyles is object)
{
foreach (var i in _appliedStyles)
BeginBatchUpdate();
try
{
i.Dispose();
}
foreach (var i in _appliedStyles)
{
i.Dispose();
}
_appliedStyles.Clear();
_appliedStyles.Clear();
}
finally
{
EndBatchUpdate();
}
}
_styled = false;

18
src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs

@ -92,6 +92,7 @@ namespace Avalonia.Styling
{
if (!_isActive)
{
_innerSubscription ??= _binding.Observable.Subscribe(_inner);
_isActive = true;
PublishNext();
}
@ -102,6 +103,8 @@ namespace Avalonia.Styling
if (_isActive)
{
_isActive = false;
_innerSubscription?.Dispose();
_innerSubscription = null;
PublishNext();
}
}
@ -122,9 +125,6 @@ namespace Avalonia.Styling
sub.Dispose();
}
_innerSubscription?.Dispose();
_innerSubscription = null;
base.Dispose();
}
@ -148,7 +148,17 @@ namespace Avalonia.Styling
protected override void Subscribed()
{
_innerSubscription = _binding.Observable.Subscribe(_inner);
if (_isActive)
{
if (_innerSubscription is null)
{
_innerSubscription ??= _binding.Observable.Subscribe(_inner);
}
else
{
PublishNext();
}
}
}
protected override void Unsubscribed()

2
src/Avalonia.Styling/Styling/PropertySetterInstance.cs

@ -7,7 +7,7 @@ using Avalonia.Reactive;
namespace Avalonia.Styling
{
/// <summary>
/// A <see cref="Setter"/> which has been instance on a control.
/// A <see cref="Setter"/> which has been instanced on a control.
/// </summary>
/// <typeparam name="T">The target property type.</typeparam>
internal class PropertySetterInstance<T> : SingleSubscriberObservableBase<BindingValue<T>>,

128
src/Avalonia.Styling/Styling/PropertySetterLazyInstance.cs

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

24
src/Avalonia.Styling/Styling/Setter.cs

@ -68,18 +68,10 @@ namespace Avalonia.Styling
throw new InvalidOperationException("Setter.Property must be set.");
}
var value = Value;
if (value is ITemplate template &&
!typeof(ITemplate).IsAssignableFrom(Property.PropertyType))
{
value = template.Build();
}
var data = new SetterVisitorData
{
target = target,
value = value,
value = Value,
};
Property.Accept(this, ref data);
@ -97,6 +89,13 @@ namespace Avalonia.Styling
property,
binding);
}
else if (data.value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(property.PropertyType))
{
data.result = new PropertySetterLazyInstance<T>(
data.target,
property,
() => (T)template.Build());
}
else
{
data.result = new PropertySetterInstance<T>(
@ -117,6 +116,13 @@ namespace Avalonia.Styling
property,
binding);
}
else if (data.value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(property.PropertyType))
{
data.result = new PropertySetterLazyInstance<T>(
data.target,
property,
() => (T)template.Build());
}
else
{
data.result = new PropertySetterInstance<T>(

2
src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml

@ -35,7 +35,7 @@
<ItemsControl Items="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Message}" Foreground="{DynamicResource SystemControlErrorTextForegroundBrush}" TextWrapping="Wrap" />
<TextBlock Text="{Binding }" Foreground="{DynamicResource SystemControlErrorTextForegroundBrush}" TextWrapping="Wrap" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

14
src/Avalonia.Visuals/Visual.cs

@ -150,7 +150,7 @@ namespace Avalonia
public TransformedBounds? TransformedBounds => _transformedBounds;
/// <summary>
/// Gets a value indicating whether the control should be clipped to its bounds.
/// Gets or sets a value indicating whether the control should be clipped to its bounds.
/// </summary>
public bool ClipToBounds
{
@ -191,7 +191,7 @@ namespace Avalonia
}
/// <summary>
/// Gets a value indicating whether this control is visible.
/// Gets or sets a value indicating whether this control is visible.
/// </summary>
public bool IsVisible
{
@ -200,7 +200,7 @@ namespace Avalonia
}
/// <summary>
/// Gets the opacity of the control.
/// Gets or sets the opacity of the control.
/// </summary>
public double Opacity
{
@ -209,7 +209,7 @@ namespace Avalonia
}
/// <summary>
/// Gets the opacity mask of the control.
/// Gets or sets the opacity mask of the control.
/// </summary>
public IBrush OpacityMask
{
@ -218,7 +218,7 @@ namespace Avalonia
}
/// <summary>
/// Gets the render transform of the control.
/// Gets or sets the render transform of the control.
/// </summary>
public ITransform RenderTransform
{
@ -227,7 +227,7 @@ namespace Avalonia
}
/// <summary>
/// Gets the transform origin of the control.
/// Gets or sets the transform origin of the control.
/// </summary>
public RelativePoint RenderTransformOrigin
{
@ -236,7 +236,7 @@ namespace Avalonia
}
/// <summary>
/// Gets the Z index of the control.
/// Gets or sets the Z index of the control.
/// </summary>
/// <remarks>
/// Controls with a higher <see cref="ZIndex"/> will appear in front of controls with

2
src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github

@ -1 +1 @@
Subproject commit f3ca2028f4f64be3556a6afd22f192902de095e5
Subproject commit 9e90d34e97c766ba8dcb70128147fcded65d195a

14
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -84,6 +84,7 @@ namespace Avalonia.Win32
private WindowImpl _parent;
private ExtendClientAreaChromeHints _extendChromeHints = ExtendClientAreaChromeHints.Default;
private bool _isCloseRequested;
private bool _shown;
public WindowImpl()
{
@ -565,6 +566,7 @@ namespace Avalonia.Win32
public void Hide()
{
UnmanagedMethods.ShowWindow(_hwnd, ShowWindowCommand.Hide);
_shown = false;
}
public virtual void Show(bool activate)
@ -871,6 +873,11 @@ namespace Avalonia.Win32
private void ExtendClientArea()
{
if (!_shown)
{
return;
}
if (DwmIsCompositionEnabled(out bool compositionEnabled) < 0 || !compositionEnabled)
{
_isClientAreaExtended = false;
@ -916,6 +923,13 @@ namespace Avalonia.Win32
private void ShowWindow(WindowState state, bool activate)
{
_shown = true;
if (_isClientAreaExtended)
{
ExtendClientArea();
}
ShowWindowCommand? command;
var newWindowProperties = _windowProperties;

31
tests/Avalonia.Animation.UnitTests/AnimatableTests.cs

@ -330,6 +330,37 @@ namespace Avalonia.Animation.UnitTests
}
}
[Fact]
public void Transitions_Can_Be_Changed_To_Collection_That_Contains_The_Same_Transitions()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
control.Transitions = new Transitions { target.Object };
}
[Fact]
public void Transitions_Can_Re_Set_During_Batch_Update()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
// Assigning and then clearing Transitions ensures we have a transition state
// collection created.
control.Transitions = null;
control.BeginBatchUpdate();
// Setting opacity then Transitions means that we receive the Transitions change
// after the Opacity change when EndBatchUpdate is called.
control.Opacity = 0.5;
control.Transitions = new Transitions { target.Object };
// Which means that the transition state hasn't been initialized with the new
// Transitions when the Opacity change notification gets raised here.
control.EndBatchUpdate();
}
private static Mock<ITransition> CreateTarget()
{
return CreateTransition(Visual.OpacityProperty);

1
tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj

@ -3,6 +3,7 @@
<TargetFrameworks>netcoreapp3.1;net47</TargetFrameworks>
<OutputType>Library</OutputType>
<IsTestProject>true</IsTestProject>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\UnitTests.NetFX.props" />

616
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs

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

1
tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj

@ -12,6 +12,7 @@
<ProjectReference Include="..\..\src\Avalonia.Input\Avalonia.Input.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Interactivity\Avalonia.Interactivity.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Layout\Avalonia.Layout.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Visuals\Avalonia.Visuals.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Styling\Avalonia.Styling.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj" />

74
tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs

@ -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"))
{
}
};
}
}
}

32
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<BindingNotification>(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<BindingNotification>(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);
});
}
/// <summary>
/// Retrieves a defined predicate filter through a new AutoCompleteBox

64
tests/Avalonia.Controls.UnitTests/CarouselTests.cs

@ -1,10 +1,14 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Subjects;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.LogicalTree;
using Avalonia.Threading;
using Avalonia.VisualTree;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Controls.UnitTests
@ -77,9 +81,9 @@ namespace Avalonia.Controls.UnitTests
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"FooBar"
"Foo",
"Bar",
"FooBar"
};
var target = new Carousel
@ -113,9 +117,9 @@ namespace Avalonia.Controls.UnitTests
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"FooBar"
"Foo",
"Bar",
"FooBar"
};
var target = new Carousel
@ -172,9 +176,9 @@ namespace Avalonia.Controls.UnitTests
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"FooBar"
"Foo",
"Bar",
"FooBar"
};
var target = new Carousel
@ -206,9 +210,9 @@ namespace Avalonia.Controls.UnitTests
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"FooBar"
"Foo",
"Bar",
"FooBar"
};
var target = new Carousel
@ -235,9 +239,9 @@ namespace Avalonia.Controls.UnitTests
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"FooBar"
"Foo",
"Bar",
"FooBar"
};
var target = new Carousel
@ -269,9 +273,9 @@ namespace Avalonia.Controls.UnitTests
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"FooBar"
"Foo",
"Bar",
"FooBar"
};
var target = new Carousel
@ -311,5 +315,29 @@ namespace Avalonia.Controls.UnitTests
contentPresenter.UpdateChild();
return Assert.IsType<TextBlock>(contentPresenter.Child);
}
[Fact]
public void SelectedItem_Validation()
{
using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
{
var target = new Carousel
{
Template = new FuncControlTemplate<Carousel>(CreateTemplate), IsVirtualized = false
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
var exception = new System.InvalidCastException("failed validation");
var textObservable =
new BehaviorSubject<BindingNotification>(new BindingNotification(exception,
BindingErrorType.DataValidationError));
target.Bind(ComboBox.SelectedItemProperty, textObservable);
Assert.True(DataValidationErrors.GetHasErrors(target));
Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception }));
}
}
}
}

29
tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs

@ -1,11 +1,14 @@
using System.Linq;
using System.Reactive.Subjects;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Xunit;
@ -173,5 +176,31 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(expectedSelectedIndex, target.SelectedIndex);
}
}
[Fact]
public void SelectedItem_Validation()
{
using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
{
var target = new ComboBox
{
Template = GetTemplate(),
VirtualizationMode = ItemVirtualizationMode.None
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
var exception = new System.InvalidCastException("failed validation");
var textObservable = new BehaviorSubject<BindingNotification>(new BindingNotification(exception, BindingErrorType.DataValidationError));
target.Bind(ComboBox.SelectedItemProperty, textObservable);
Assert.True(DataValidationErrors.GetHasErrors(target));
Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception }));
}
}
}
}

26
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@ -1,11 +1,14 @@
using System.Linq;
using System.Reactive.Subjects;
using Avalonia.Collections;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Styling;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
@ -559,5 +562,28 @@ namespace Avalonia.Controls.UnitTests
public string Value { get; }
}
[Fact]
public void SelectedItem_Validation()
{
var target = new ListBox
{
Template = ListBoxTemplate(),
Items = new[] { "Foo" },
ItemTemplate = new FuncDataTemplate<string>((_, __) => new Canvas()),
SelectionMode = SelectionMode.AlwaysSelected,
VirtualizationMode = ItemVirtualizationMode.None
};
Prepare(target);
var exception = new System.InvalidCastException("failed validation");
var textObservable = new BehaviorSubject<BindingNotification>(new BindingNotification(exception, BindingErrorType.DataValidationError));
target.Bind(ComboBox.SelectedItemProperty, textObservable);
Assert.True(DataValidationErrors.GetHasErrors(target));
Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception }));
}
}
}

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

62
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@ -646,22 +646,49 @@ namespace Avalonia.Controls.UnitTests
Assert.Null(target.Text);
}
}
[Fact]
public void Text_Box_MaxLength_Work_Properly()
[Theory]
[InlineData("abc", "d", 3, 0, 0, false, "abc")]
[InlineData("abc", "dd", 4, 3, 3, false, "abcd")]
[InlineData("abc", "ddd", 3, 0, 2, true, "ddc")]
[InlineData("abc", "dddd", 4, 1, 3, true, "addd")]
[InlineData("abc", "ddddd", 5, 3, 3, true, "abcdd")]
public void MaxLength_Works_Properly(
string initalText,
string textInput,
int maxLength,
int selectionStart,
int selectionEnd,
bool fromClipboard,
string expected)
{
using (UnitTestApplication.Start(Services))
{
var target = new TextBox
{
Template = CreateTemplate(),
Text = "abc",
MaxLength = 3,
Text = initalText,
MaxLength = maxLength,
SelectionStart = selectionStart,
SelectionEnd = selectionEnd
};
RaiseKeyEvent(target, Key.D, KeyModifiers.None);
Assert.Equal("abc", target.Text);
if (fromClipboard)
{
AvaloniaLocator.CurrentMutable.Bind<IClipboard>().ToSingleton<ClipboardStub>();
var clipboard = AvaloniaLocator.CurrentMutable.GetService<IClipboard>();
clipboard.SetTextAsync(textInput).GetAwaiter().GetResult();
RaiseKeyEvent(target, Key.V, KeyModifiers.Control);
clipboard.ClearAsync().GetAwaiter().GetResult();
}
else
{
RaiseTextEvent(target, textInput);
}
Assert.Equal(expected, target.Text);
}
}
@ -758,11 +785,22 @@ namespace Avalonia.Controls.UnitTests
private class ClipboardStub : IClipboard // in order to get tests working that use the clipboard
{
public Task<string> GetTextAsync() => Task.FromResult("");
private string _text;
public Task<string> GetTextAsync() => Task.FromResult(_text);
public Task SetTextAsync(string text) => Task.CompletedTask;
public Task SetTextAsync(string text)
{
_text = text;
return Task.CompletedTask;
}
public Task ClearAsync() => Task.CompletedTask;
public Task ClearAsync()
{
_text = null;
return Task.CompletedTask;
}
public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask;
public Task<string[]> GetFormatsAsync() => Task.FromResult(Array.Empty<string>());

124
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Layout;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.UnitTests;
@ -137,6 +136,129 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(1, count);
}
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Child_windows_should_be_closed_before_parent(bool programaticClose)
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = new Window();
var child = new Window();
int count = 0;
int windowClosing = 0;
int childClosing = 0;
int windowClosed = 0;
int childClosed = 0;
window.Closing += (sender, e) =>
{
count++;
windowClosing = count;
};
child.Closing += (sender, e) =>
{
count++;
childClosing = count;
};
window.Closed += (sender, e) =>
{
count++;
windowClosed = count;
};
child.Closed += (sender, e) =>
{
count++;
childClosed = count;
};
window.Show();
child.Show(window);
if (programaticClose)
{
window.Close();
}
else
{
var cancel = window.PlatformImpl.Closing();
Assert.Equal(false, cancel);
}
Assert.Equal(2, windowClosing);
Assert.Equal(1, childClosing);
Assert.Equal(4, windowClosed);
Assert.Equal(3, childClosed);
}
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Child_windows_must_not_close_before_parent_has_chance_to_Cancel_OSCloseButton(bool programaticClose)
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = new Window();
var child = new Window();
int count = 0;
int windowClosing = 0;
int childClosing = 0;
int windowClosed = 0;
int childClosed = 0;
window.Closing += (sender, e) =>
{
count++;
windowClosing = count;
e.Cancel = true;
};
child.Closing += (sender, e) =>
{
count++;
childClosing = count;
};
window.Closed += (sender, e) =>
{
count++;
windowClosed = count;
};
child.Closed += (sender, e) =>
{
count++;
childClosed = count;
};
window.Show();
child.Show(window);
if (programaticClose)
{
window.Close();
}
else
{
var cancel = window.PlatformImpl.Closing();
Assert.Equal(true, cancel);
}
Assert.Equal(2, windowClosing);
Assert.Equal(1, childClosing);
Assert.Equal(0, windowClosed);
Assert.Equal(0, childClosed);
}
}
[Fact]
public void Showing_Should_Start_Renderer()

1
tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj

@ -3,6 +3,7 @@
<TargetFrameworks>netcoreapp3.1;net47</TargetFrameworks>
<OutputType>Library</OutputType>
<IsTestProject>true</IsTestProject>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\UnitTests.NetFX.props" />

96
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
@ -809,6 +810,82 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
Assert.Equal(0xff506070, brush.Color.ToUint32());
}
[Fact]
public void Resource_In_Non_Matching_Style_Is_Not_Resolved()
{
using var app = StyledWindow();
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions'>
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<local:TrackingResourceProvider/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Window.Styles>
<Style Selector='Border.nomatch'>
<Setter Property='Tag' Value='{DynamicResource foo}'/>
</Style>
<Style Selector='Border'>
<Setter Property='Tag' Value='{DynamicResource bar}'/>
</Style>
</Window.Styles>
<Border Name='border'/>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var border = window.FindControl<Border>("border");
Assert.Equal("bar", border.Tag);
var resourceProvider = (TrackingResourceProvider)window.Resources.MergedDictionaries[0];
Assert.Equal(new[] { "bar" }, resourceProvider.RequestedResources);
}
[Fact]
public void Resource_In_Non_Active_Style_Is_Not_Resolved()
{
using var app = StyledWindow();
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions'>
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<local:TrackingResourceProvider/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Window.Styles>
<Style Selector='Border'>
<Setter Property='Tag' Value='{DynamicResource foo}'/>
</Style>
<Style Selector='Border'>
<Setter Property='Tag' Value='{DynamicResource bar}'/>
</Style>
</Window.Styles>
<Border Name='border'/>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var border = window.FindControl<Border>("border");
Assert.Equal("bar", border.Tag);
var resourceProvider = (TrackingResourceProvider)window.Resources.MergedDictionaries[0];
Assert.Equal(new[] { "bar" }, resourceProvider.RequestedResources);
}
private IDisposable StyledWindow(params (string, string)[] assets)
{
var services = TestServices.StyledWindow.With(
@ -839,4 +916,23 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
};
}
}
public class TrackingResourceProvider : IResourceProvider
{
public IResourceHost Owner { get; private set; }
public bool HasResources => true;
public List<object> RequestedResources { get; } = new List<object>();
public event EventHandler OwnerChanged;
public void AddOwner(IResourceHost owner) => Owner = owner;
public void RemoveOwner(IResourceHost owner) => Owner = null;
public bool TryGetResource(object key, out object value)
{
RequestedResources.Add(key);
value = key;
return true;
}
}
}

1
tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj

@ -4,6 +4,7 @@
<OutputType>Library</OutputType>
<NoWarn>CS0067</NoWarn>
<IsTestProject>true</IsTestProject>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\UnitTests.NetFX.props" />

282
tests/Avalonia.Styling.UnitTests/StyleTests.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.UnitTests;
using Moq;
@ -217,6 +218,278 @@ namespace Avalonia.Styling.UnitTests
Assert.Equal(new[] { "foodefault", "Bar" }, values);
}
[Fact]
public void Inactive_Values_Should_Not_Be_Made_Active_During_Style_Attach()
{
using var app = UnitTestApplication.Start(TestServices.RealStyler);
var root = new TestRoot
{
Styles =
{
new Style(x => x.OfType<Class1>())
{
Setters =
{
new Setter(Class1.FooProperty, "Foo"),
},
},
new Style(x => x.OfType<Class1>())
{
Setters =
{
new Setter(Class1.FooProperty, "Bar"),
},
}
}
};
var values = new List<string>();
var target = new Class1();
target.GetObservable(Class1.FooProperty).Subscribe(x => values.Add(x));
root.Child = target;
Assert.Equal(new[] { "foodefault", "Bar" }, values);
}
[Fact]
public void Inactive_Bindings_Should_Not_Be_Made_Active_During_Style_Attach()
{
using var app = UnitTestApplication.Start(TestServices.RealStyler);
var root = new TestRoot
{
Styles =
{
new Style(x => x.OfType<Class1>())
{
Setters =
{
new Setter(Class1.FooProperty, new Binding("Foo")),
},
},
new Style(x => x.OfType<Class1>())
{
Setters =
{
new Setter(Class1.FooProperty, new Binding("Bar")),
},
}
}
};
var values = new List<string>();
var target = new Class1
{
DataContext = new
{
Foo = "Foo",
Bar = "Bar",
}
};
target.GetObservable(Class1.FooProperty).Subscribe(x => values.Add(x));
root.Child = target;
Assert.Equal(new[] { "foodefault", "Bar" }, values);
}
[Fact]
public void Inactive_Values_Should_Not_Be_Made_Active_During_Style_Detach()
{
using var app = UnitTestApplication.Start(TestServices.RealStyler);
var root = new TestRoot
{
Styles =
{
new Style(x => x.OfType<Class1>())
{
Setters =
{
new Setter(Class1.FooProperty, "Foo"),
},
},
new Style(x => x.OfType<Class1>())
{
Setters =
{
new Setter(Class1.FooProperty, "Bar"),
},
}
}
};
var target = new Class1();
root.Child = target;
var values = new List<string>();
target.GetObservable(Class1.FooProperty).Subscribe(x => values.Add(x));
root.Child = null;
Assert.Equal(new[] { "Bar", "foodefault" }, values);
}
[Fact]
public void Inactive_Values_Should_Not_Be_Made_Active_During_Style_Detach_2()
{
using var app = UnitTestApplication.Start(TestServices.RealStyler);
var root = new TestRoot
{
Styles =
{
new Style(x => x.OfType<Class1>().Class("foo"))
{
Setters =
{
new Setter(Class1.FooProperty, "Foo"),
},
},
new Style(x => x.OfType<Class1>())
{
Setters =
{
new Setter(Class1.FooProperty, "Bar"),
},
}
}
};
var target = new Class1 { Classes = { "foo" } };
root.Child = target;
var values = new List<string>();
target.GetObservable(Class1.FooProperty).Subscribe(x => values.Add(x));
root.Child = null;
Assert.Equal(new[] { "Foo", "foodefault" }, values);
}
[Fact]
public void Inactive_Bindings_Should_Not_Be_Made_Active_During_Style_Detach()
{
using var app = UnitTestApplication.Start(TestServices.RealStyler);
var root = new TestRoot
{
Styles =
{
new Style(x => x.OfType<Class1>())
{
Setters =
{
new Setter(Class1.FooProperty, new Binding("Foo")),
},
},
new Style(x => x.OfType<Class1>())
{
Setters =
{
new Setter(Class1.FooProperty, new Binding("Bar")),
},
}
}
};
var target = new Class1
{
DataContext = new
{
Foo = "Foo",
Bar = "Bar",
}
};
root.Child = target;
var values = new List<string>();
target.GetObservable(Class1.FooProperty).Subscribe(x => values.Add(x));
root.Child = null;
Assert.Equal(new[] { "Bar", "foodefault" }, values);
}
[Fact]
public void Template_In_Non_Matching_Style_Is_Not_Built()
{
var instantiationCount = 0;
var template = new FuncTemplate<Class1>(() =>
{
++instantiationCount;
return new Class1();
});
Styles styles = new Styles
{
new Style(x => x.OfType<Class1>().Class("foo"))
{
Setters =
{
new Setter(Class1.ChildProperty, template),
},
},
new Style(x => x.OfType<Class1>())
{
Setters =
{
new Setter(Class1.ChildProperty, template),
},
}
};
var target = new Class1();
styles.TryAttach(target, null);
Assert.NotNull(target.Child);
Assert.Equal(1, instantiationCount);
}
[Fact]
public void Template_In_Inactive_Style_Is_Not_Built()
{
var instantiationCount = 0;
var template = new FuncTemplate<Class1>(() =>
{
++instantiationCount;
return new Class1();
});
Styles styles = new Styles
{
new Style(x => x.OfType<Class1>())
{
Setters =
{
new Setter(Class1.ChildProperty, template),
},
},
new Style(x => x.OfType<Class1>())
{
Setters =
{
new Setter(Class1.ChildProperty, template),
},
}
};
var target = new Class1();
target.BeginBatchUpdate();
styles.TryAttach(target, null);
target.EndBatchUpdate();
Assert.NotNull(target.Child);
Assert.Equal(1, instantiationCount);
}
[Fact]
public void Style_Should_Detach_When_Control_Removed_From_Logical_Tree()
{
@ -453,12 +726,21 @@ namespace Avalonia.Styling.UnitTests
public static readonly StyledProperty<string> FooProperty =
AvaloniaProperty.Register<Class1, string>(nameof(Foo), "foodefault");
public static readonly StyledProperty<Class1> ChildProperty =
AvaloniaProperty.Register<Class1, Class1>(nameof(Child));
public string Foo
{
get { return GetValue(FooProperty); }
set { SetValue(FooProperty, value); }
}
public Class1 Child
{
get => GetValue(ChildProperty);
set => SetValue(ChildProperty, value);
}
protected override Size MeasureOverride(Size availableSize)
{
throw new NotImplementedException();

2
tests/Avalonia.UnitTests/UnitTestApplication.cs

@ -25,6 +25,7 @@ namespace Avalonia.UnitTests
public UnitTestApplication(TestServices services)
{
_services = services ?? new TestServices();
AvaloniaLocator.CurrentMutable.BindToSelf<Application>(this);
RegisterServices();
}
@ -36,7 +37,6 @@ namespace Avalonia.UnitTests
{
var scope = AvaloniaLocator.EnterScope();
var app = new UnitTestApplication(services);
AvaloniaLocator.CurrentMutable.BindToSelf<Application>(app);
Dispatcher.UIThread.UpdateServices();
return Disposable.Create(() =>
{

Loading…
Cancel
Save