Browse Source

Merge branch 'master' into patch-1

pull/2879/head
danwalmsley 7 years ago
committed by GitHub
parent
commit
f56db79a76
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      build/AndroidWorkarounds.props
  2. 9
      build/SharedVersion.props
  3. 2
      native/Avalonia.Native/inc/avalonia-native.h
  4. 4
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  5. 24
      native/Avalonia.Native/src/OSX/app.mm
  6. 4
      native/Avalonia.Native/src/OSX/common.h
  7. 14
      native/Avalonia.Native/src/OSX/main.mm
  8. 59
      native/Avalonia.Native/src/OSX/platformthreading.mm
  9. 8
      samples/ControlCatalog.NetCore/Program.cs
  10. 1
      scripts/ReplaceNugetCache.sh
  11. 17
      src/Avalonia.Animation/Animatable.cs
  12. 88
      src/Avalonia.Base/AvaloniaObject.cs
  13. 5
      src/Avalonia.Base/AvaloniaProperty.cs
  14. 118
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  15. 28
      src/Avalonia.Base/BoxedValue.cs
  16. 229
      src/Avalonia.Base/Collections/AvaloniaList.cs
  17. 6
      src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs
  18. 26
      src/Avalonia.Base/Data/Core/ExpressionNode.cs
  19. 8
      src/Avalonia.Base/Data/Core/ExpressionObserver.cs
  20. 11
      src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs
  21. 5
      src/Avalonia.Base/Data/Core/IndexerNodeBase.cs
  22. 4
      src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs
  23. 23
      src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs
  24. 8
      src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs
  25. 5
      src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs
  26. 3
      src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessorPlugin.cs
  27. 4
      src/Avalonia.Base/Data/Core/Plugins/IStreamPlugin.cs
  28. 39
      src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs
  29. 23
      src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
  30. 21
      src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs
  31. 12
      src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs
  32. 13
      src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs
  33. 6
      src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs
  34. 18
      src/Avalonia.Base/Data/Core/SettableNode.cs
  35. 2
      src/Avalonia.Base/Data/Core/StreamNode.cs
  36. 21
      src/Avalonia.Base/Data/DataValidationException.cs
  37. 16
      src/Avalonia.Base/PriorityBindingEntry.cs
  38. 14
      src/Avalonia.Base/PriorityValue.cs
  39. 11
      src/Avalonia.Base/StyledPropertyBase.cs
  40. 14
      src/Avalonia.Base/StyledPropertyMetadata`1.cs
  41. 30
      src/Avalonia.Base/Utilities/DeferredSetter.cs
  42. 4
      src/Avalonia.Controls.DataGrid/Themes/Default.xaml
  43. 15
      src/Avalonia.Controls/ControlExtensions.cs
  44. 24
      src/Avalonia.Controls/DataValidationErrors.cs
  45. 6
      src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
  46. 2
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  47. 5
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  48. 11
      src/Avalonia.Controls/TextBox.cs
  49. 2
      src/Avalonia.Controls/TopLevel.cs
  50. 4
      src/Avalonia.Controls/TreeView.cs
  51. 23
      src/Avalonia.Controls/TreeViewItem.cs
  52. 7
      src/Avalonia.Diagnostics/DevTools.xaml.cs
  53. 1
      src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
  54. 42
      src/Avalonia.Input/Gestures.cs
  55. 2
      src/Avalonia.Input/IMouseDevice.cs
  56. 4
      src/Avalonia.Input/InputExtensions.cs
  57. 5
      src/Avalonia.Input/MouseDevice.cs
  58. 4
      src/Avalonia.Interactivity/EventSubscription.cs
  59. 101
      src/Avalonia.Interactivity/Interactive.cs
  60. 50
      src/Avalonia.Interactivity/RoutedEvent.cs
  61. 23
      src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs
  62. 19
      src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs
  63. 4
      src/Avalonia.Themes.Default/TreeViewItem.xaml
  64. 4
      src/Avalonia.Visuals/Rect.cs
  65. 30
      src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs
  66. 160
      src/Avalonia.Visuals/Vector.cs
  67. 57
      src/Avalonia.X11/X11KeyTransform.cs
  68. 2
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs
  69. 2
      src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github
  70. 12
      src/Markup/Avalonia.Markup/Data/MultiBinding.cs
  71. 2
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs
  72. 4
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs
  73. 26
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs
  74. 9
      src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs
  75. 17
      tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs
  76. 4
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs
  77. 2
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs
  78. 14
      tests/Avalonia.Base.UnitTests/Data/Core/Plugins/DataAnnotationsValidationPluginTests.cs
  79. 4
      tests/Avalonia.Base.UnitTests/Data/Core/Plugins/ExceptionValidationPluginTests.cs
  80. 12
      tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs
  81. 15
      tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs
  82. 19
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs
  83. 18
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  84. 2
      tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs
  85. 18
      tests/Avalonia.Controls.UnitTests/TopLevelTests.cs
  86. 132
      tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs
  87. 76
      tests/Avalonia.Markup.Xaml.UnitTests/Converters/MultiValueConverterTests.cs
  88. 20
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
  89. 46
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/EventTests.cs
  90. 112
      tests/Avalonia.Visuals.UnitTests/VectorTests.cs

8
build/AndroidWorkarounds.props

@ -5,4 +5,12 @@
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.5.1" />
<PackageReference Include="System.Buffers" Version="4.5.0" />
</ItemGroup>
<Target Name="_RemoveNonExistingResgenFile" BeforeTargets="CoreCompile" Condition="'$(_SdkSetAndroidResgenFile)' == 'true' And '$(AndroidResgenFile)' != '' And !Exists('$(AndroidResgenFile)')">
<ItemGroup>
<Compile Remove="$(AndroidResgenFile)"/>
</ItemGroup>
</Target>
<PropertyGroup>
<DesignTimeBuild>false</DesignTimeBuild>
</PropertyGroup>
</Project>

9
build/SharedVersion.props

@ -4,11 +4,16 @@
<Product>Avalonia</Product>
<Version>0.8.999</Version>
<Copyright>Copyright 2019 &#169; The AvaloniaUI Project</Copyright>
<PackageLicenseUrl>https://github.com/AvaloniaUI/Avalonia/blob/master/licence.md</PackageLicenseUrl>
<PackageProjectUrl>https://github.com/AvaloniaUI/Avalonia/</PackageProjectUrl>
<PackageProjectUrl>https://avaloniaui.net</PackageProjectUrl>
<RepositoryUrl>https://github.com/AvaloniaUI/Avalonia/</RepositoryUrl>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>CS1591</NoWarn>
<LangVersion>latest</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIconUrl>https://avatars2.githubusercontent.com/u/14075148?s=200</PackageIconUrl>
<PackageDescription>Avalonia is a WPF/UWP-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), MacOS and with experimental support for Android and iOS.</PackageDescription>
<PackageTags>avalonia;avaloniaui;mvvm;rx;reactive extensions;android;ios;mac;forms;wpf;net;netstandard;net461;uwp;xamarin</PackageTags>
<PackageReleaseNotes>https://github.com/AvaloniaUI/Avalonia/releases</PackageReleaseNotes>
<RepositoryType>git</RepositoryType>
</PropertyGroup>
</Project>

2
native/Avalonia.Native/inc/avalonia-native.h

@ -280,7 +280,7 @@ AVNCOM(IAvnPlatformThreadingInterface, 0b) : IUnknown
virtual bool GetCurrentThreadIsLoopThread() = 0;
virtual void SetSignaledCallback(IAvnSignaledCallback* cb) = 0;
virtual IAvnLoopCancellation* CreateLoopCancellation() = 0;
virtual void RunLoop(IAvnLoopCancellation* cancel) = 0;
virtual HRESULT RunLoop(IAvnLoopCancellation* cancel) = 0;
// Can't pass int* to sharpgentools for some reason
virtual void Signal(int priority) = 0;
virtual IUnknown* StartTimer(int priority, int ms, IAvnActionCallback* callback) = 0;

4
native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj

@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
1A002B9E232135EE00021753 /* app.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A002B9D232135EE00021753 /* app.mm */; };
37A517B32159597E00FBA241 /* Screens.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37A517B22159597E00FBA241 /* Screens.mm */; };
37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* SystemDialogs.mm */; };
37DDA9B0219330F8002E132B /* AvnString.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37DDA9AF219330F8002E132B /* AvnString.mm */; };
@ -22,6 +23,7 @@
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
1A002B9D232135EE00021753 /* app.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = app.mm; sourceTree = "<group>"; };
379860FE214DA0C000CD0246 /* KeyTransform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyTransform.h; sourceTree = "<group>"; };
37A4E71A2178846A00EACBCD /* headers */ = {isa = PBXFileReference; lastKnownFileType = folder; name = headers; path = ../../inc; sourceTree = "<group>"; };
37A517B22159597E00FBA241 /* Screens.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = Screens.mm; sourceTree = "<group>"; };
@ -68,6 +70,7 @@
AB7A61E62147C814003C5833 = {
isa = PBXGroup;
children = (
1A002B9D232135EE00021753 /* app.mm */,
37DDA9B121933371002E132B /* AvnString.h */,
37DDA9AF219330F8002E132B /* AvnString.mm */,
37A4E71A2178846A00EACBCD /* headers */,
@ -164,6 +167,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
1A002B9E232135EE00021753 /* app.mm in Sources */,
5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */,
5B21A982216530F500CEE36E /* cursor.mm in Sources */,
37DDA9B0219330F8002E132B /* AvnString.mm in Sources */,

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

@ -0,0 +1,24 @@
#include "common.h"
@interface AvnAppDelegate : NSObject<NSApplicationDelegate>
@end
extern NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular;
@implementation AvnAppDelegate
- (void)applicationWillFinishLaunching:(NSNotification *)notification
{
[[NSApplication sharedApplication] setActivationPolicy: AvnDesiredActivationPolicy];
}
- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
[NSApp activateIgnoringOtherApps:true];
}
@end
extern void InitializeAvnApp()
{
NSApplication* app = [NSApplication sharedApplication];
id delegate = [AvnAppDelegate new];
[app setDelegate:delegate];
}

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

@ -19,12 +19,12 @@ extern IAvnClipboard* CreateClipboard();
extern IAvnCursorFactory* CreateCursorFactory();
extern IAvnGlFeature* GetGlFeature();
extern IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(NSWindow* window, NSView* view);
extern void InitializeAvnApp();
extern NSApplicationActivationPolicy AvnDesiredActivationPolicy;
extern NSPoint ToNSPoint (AvnPoint p);
extern AvnPoint ToAvnPoint (NSPoint p);
extern AvnPoint ConvertPointY (AvnPoint p);
extern NSSize ToNSSize (AvnSize s);
#ifdef DEBUG
#define NSDebugLog(...) NSLog(__VA_ARGS__)
#else

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

@ -5,21 +5,14 @@
#define COM_GUIDS_MATERIALIZE
#include "common.h"
static BOOL ShowInDock = 1;
static void SetActivationPolicy()
{
[[NSApplication sharedApplication] setActivationPolicy: (ShowInDock ? NSApplicationActivationPolicyRegular : NSApplicationActivationPolicyAccessory)];
}
class MacOptions : public ComSingleObject<IAvnMacOptions, &IID_IAvnMacOptions>
{
public:
FORWARD_IUNKNOWN()
virtual HRESULT SetShowInDock(int show) override
{
ShowInDock = show;
SetActivationPolicy();
AvnDesiredActivationPolicy = show
? NSApplicationActivationPolicyRegular : NSApplicationActivationPolicyAccessory;
return S_OK;
}
};
@ -64,8 +57,9 @@ public:
{
@autoreleasepool{
[[ThreadingInitializer new] do];
return S_OK;
}
InitializeAvnApp();
return S_OK;
};
virtual IAvnMacOptions* GetMacOptions() override

59
native/Avalonia.Native/src/OSX/platformthreading.mm

@ -57,16 +57,36 @@ class PlatformThreadingInterface : public ComSingleObject<IAvnPlatformThreadingI
{
private:
Signaler* _signaler;
bool _wasRunningAtLeastOnce = false;
class LoopCancellation : public ComSingleObject<IAvnLoopCancellation, &IID_IAvnLoopCancellation>
{
public:
FORWARD_IUNKNOWN()
bool Cancelled = 0;
virtual void Cancel() override
bool Running = false;
bool Cancelled = false;
virtual void Cancel()
{
Cancelled = 1;
Cancelled = true;
if(Running)
{
Running = false;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSApplication sharedApplication] stop:nil];
NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
location:NSMakePoint(0, 0)
modifierFlags:0
timestamp:0
windowNumber:0
context:nil
subtype:0
data1:0
data2:0];
[NSApp postEvent:event atStart:YES];
});
}
}
};
public:
@ -99,30 +119,17 @@ public:
return new LoopCancellation();
}
virtual void RunLoop(IAvnLoopCancellation* cancel) override
virtual HRESULT RunLoop(IAvnLoopCancellation* cancel) override
{
@autoreleasepool {
auto can = dynamic_cast<LoopCancellation*>(cancel);
[[NSApplication sharedApplication] activateIgnoringOtherApps:true];
while(true)
{
@autoreleasepool
{
if(can != NULL && can->Cancelled)
return;
NSEvent* ev = [[NSApplication sharedApplication]
nextEventMatchingMask:NSEventMaskAny
untilDate: [NSDate dateWithTimeIntervalSinceNow:1]
inMode:NSDefaultRunLoopMode
dequeue:true];
if(can != NULL && can->Cancelled)
return;
if(ev != NULL)
[[NSApplication sharedApplication] sendEvent:ev];
}
}
NSDebugLog(@"RunLoop exited");
}
auto can = dynamic_cast<LoopCancellation*>(cancel);
if(can->Cancelled)
return S_OK;
if(_wasRunningAtLeastOnce)
return E_FAIL;
can->Running = true;
_wasRunningAtLeastOnce = true;
[NSApp run];
return S_OK;
}
virtual void Signal(int priority) override

8
samples/ControlCatalog.NetCore/Program.cs

@ -4,22 +4,16 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using Avalonia;
using Avalonia.Controls;
using Avalonia.LinuxFramebuffer.Output;
using Avalonia.Skia;
using Avalonia.ReactiveUI;
using Avalonia.Dialogs;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ControlCatalog.NetCore
{
static class Program
{
[STAThread]
static int Main(string[] args)
{
Thread.CurrentThread.TrySetApartmentState(ApartmentState.STA);
if (args.Contains("--wait-for-attach"))
{
Console.WriteLine("Attach debugger and use 'Set next statement'");

1
scripts/ReplaceNugetCache.sh

@ -2,7 +2,6 @@
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netcoreapp2.0/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netstandard2.0/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.gtk3/$1/lib/netstandard2.0/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.skia/$1/lib/netstandard2.0/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.native/$1/lib/netstandard2.0/

17
src/Avalonia.Animation/Animatable.cs

@ -45,16 +45,17 @@ namespace Avalonia.Animation
{
get
{
if (_transitions == null)
if (_transitions is null)
_transitions = new Transitions();
if (_previousTransitions == null)
if (_previousTransitions is null)
_previousTransitions = new Dictionary<AvaloniaProperty, IDisposable>();
return _transitions;
}
set
{
SetAndRaise(TransitionsProperty, ref _transitions, value);
}
}
@ -66,18 +67,20 @@ namespace Avalonia.Animation
/// <param name="e">The event args.</param>
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Priority != BindingPriority.Animation && Transitions != null && _previousTransitions != null)
{
var match = Transitions.FirstOrDefault(x => x.Property == e.Property);
if (_transitions is null || _previousTransitions is null || e.Priority == BindingPriority.Animation) return;
if (match != null)
// PERF-SENSITIVE: Called on every property change. Don't use LINQ here (too many allocations).
foreach (var transition in Transitions)
{
if (transition.Property == e.Property)
{
if (_previousTransitions.TryGetValue(e.Property, out var dispose))
dispose.Dispose();
var instance = match.Apply(this, Clock ?? Avalonia.Animation.Clock.GlobalClock, e.OldValue, e.NewValue);
var instance = transition.Apply(this, Clock ?? Avalonia.Animation.Clock.GlobalClock, e.OldValue, e.NewValue);
_previousTransitions[e.Property] = instance;
return;
}
}
}

88
src/Avalonia.Base/AvaloniaObject.cs

@ -82,6 +82,7 @@ namespace Avalonia
set
{
VerifyAccess();
if (_inheritanceParent != value)
{
if (_inheritanceParent != null)
@ -89,25 +90,33 @@ namespace Avalonia
_inheritanceParent.InheritablePropertyChanged -= ParentPropertyChanged;
}
var properties = AvaloniaPropertyRegistry.Instance.GetRegistered(this)
.Concat(AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(this.GetType()));
var inherited = (from property in properties
where property.Inherits
select new
{
Property = property,
Value = GetValue(property),
}).ToList();
var oldInheritanceParent = _inheritanceParent;
_inheritanceParent = value;
var valuestore = _values;
foreach (var i in inherited)
foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType()))
{
object newValue = GetValue(i.Property);
if (valuestore != null && valuestore.GetValue(property) != AvaloniaProperty.UnsetValue)
{
// if local value set there can be no change
continue;
}
// get the value as it would have been with the previous InheritanceParent
object oldValue;
if (oldInheritanceParent is AvaloniaObject aobj)
{
oldValue = aobj.GetValueOrDefaultUnchecked(property);
}
else
{
oldValue = ((IStyledPropertyAccessor)property).GetDefaultValue(GetType());
}
if (!Equals(i.Value, newValue))
object newValue = GetDefaultValue(property);
if (!Equals(oldValue, newValue))
{
RaisePropertyChanged(i.Property, i.Value, newValue, BindingPriority.LocalValue);
RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue);
}
}
@ -208,20 +217,9 @@ namespace Avalonia
{
return ((IDirectPropertyAccessor)GetRegistered(property)).GetValue(this);
}
else if (_values != null)
{
var result = Values.GetValue(property);
if (result == AvaloniaProperty.UnsetValue)
{
result = GetDefaultValue(property);
}
return result;
}
else
{
return GetDefaultValue(property);
return GetValueOrDefaultUnchecked(property);
}
}
@ -598,10 +596,46 @@ namespace Avalonia
private object GetDefaultValue(AvaloniaProperty property)
{
if (property.Inherits && InheritanceParent is AvaloniaObject aobj)
return aobj.GetValue(property);
return aobj.GetValueOrDefaultUnchecked(property);
return ((IStyledPropertyAccessor) property).GetDefaultValue(GetType());
}
/// <summary>
/// Gets the value or default value for a property.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The default value.</returns>
private object GetValueOrDefaultUnchecked(AvaloniaProperty property)
{
var aobj = this;
var valuestore = aobj._values;
if (valuestore != null)
{
var result = valuestore.GetValue(property);
if (result != AvaloniaProperty.UnsetValue)
{
return result;
}
}
if (property.Inherits)
{
while (aobj.InheritanceParent is AvaloniaObject parent)
{
aobj = parent;
valuestore = aobj._values;
if (valuestore != null)
{
var result = valuestore.GetValue(property);
if (result != AvaloniaProperty.UnsetValue)
{
return result;
}
}
}
}
return ((IStyledPropertyAccessor)property).GetDefaultValue(GetType());
}
/// <summary>
/// Sets the value of a direct property.
/// </summary>

5
src/Avalonia.Base/AvaloniaProperty.cs

@ -492,6 +492,11 @@ namespace Avalonia
return Name;
}
/// <summary>
/// True if <see cref="Initialized"/> has any observers.
/// </summary>
internal bool HasNotifyInitializedObservers => _initialized.HasObservers;
/// <summary>
/// Notifies the <see cref="Initialized"/> observable.
/// </summary>

118
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@ -24,8 +24,10 @@ namespace Avalonia
new Dictionary<Type, List<AvaloniaProperty>>();
private readonly Dictionary<Type, List<AvaloniaProperty>> _attachedCache =
new Dictionary<Type, List<AvaloniaProperty>>();
private readonly Dictionary<Type, List<KeyValuePair<AvaloniaProperty, object>>> _initializedCache =
new Dictionary<Type, List<KeyValuePair<AvaloniaProperty, object>>>();
private readonly Dictionary<Type, List<PropertyInitializationData>> _initializedCache =
new Dictionary<Type, List<PropertyInitializationData>>();
private readonly Dictionary<Type, List<AvaloniaProperty>> _inheritedCache =
new Dictionary<Type, List<AvaloniaProperty>>();
/// <summary>
/// Gets the <see cref="AvaloniaPropertyRegistry"/> instance
@ -103,6 +105,46 @@ namespace Avalonia
return result;
}
/// <summary>
/// Gets all inherited <see cref="AvaloniaProperty"/>s registered on a type.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
public IEnumerable<AvaloniaProperty> GetRegisteredInherited(Type type)
{
Contract.Requires<ArgumentNullException>(type != null);
if (_inheritedCache.TryGetValue(type, out var result))
{
return result;
}
result = new List<AvaloniaProperty>();
var visited = new HashSet<AvaloniaProperty>();
foreach (var property in GetRegistered(type))
{
if (property.Inherits)
{
result.Add(property);
visited.Add(property);
}
}
foreach (var property in GetRegisteredAttached(type))
{
if (property.Inherits)
{
if (!visited.Contains(property))
{
result.Add(property);
}
}
}
_inheritedCache.Add(type, result);
return result;
}
/// <summary>
/// Gets all <see cref="AvaloniaProperty"/>s registered on a object.
/// </summary>
@ -230,6 +272,7 @@ namespace Avalonia
_registeredCache.Clear();
_initializedCache.Clear();
_inheritedCache.Clear();
}
/// <summary>
@ -266,6 +309,7 @@ namespace Avalonia
_attachedCache.Clear();
_initializedCache.Clear();
_inheritedCache.Clear();
}
internal void NotifyInitialized(AvaloniaObject o)
@ -286,35 +330,73 @@ namespace Avalonia
property.NotifyInitialized(e);
}
if (!_initializedCache.TryGetValue(type, out var items))
if (!_initializedCache.TryGetValue(type, out var initializationData))
{
var build = new Dictionary<AvaloniaProperty, object>();
var visited = new HashSet<AvaloniaProperty>();
initializationData = new List<PropertyInitializationData>();
foreach (var property in GetRegistered(type))
foreach (AvaloniaProperty property in GetRegistered(type))
{
var value = !property.IsDirect ?
((IStyledPropertyAccessor)property).GetDefaultValue(type) :
null;
build.Add(property, value);
if (property.IsDirect)
{
initializationData.Add(new PropertyInitializationData(property, (IDirectPropertyAccessor)property));
}
else
{
initializationData.Add(new PropertyInitializationData(property, (IStyledPropertyAccessor)property, type));
}
visited.Add(property);
}
foreach (var property in GetRegisteredAttached(type))
foreach (AvaloniaProperty property in GetRegisteredAttached(type))
{
if (!build.ContainsKey(property))
if (!visited.Contains(property))
{
var value = ((IStyledPropertyAccessor)property).GetDefaultValue(type);
build.Add(property, value);
initializationData.Add(new PropertyInitializationData(property, (IStyledPropertyAccessor)property, type));
visited.Add(property);
}
}
items = build.ToList();
_initializedCache.Add(type, items);
_initializedCache.Add(type, initializationData);
}
foreach (PropertyInitializationData data in initializationData)
{
if (!data.Property.HasNotifyInitializedObservers)
{
continue;
}
object value = data.IsDirect ? data.DirectAccessor.GetValue(o) : data.Value;
Notify(data.Property, value);
}
}
private readonly struct PropertyInitializationData
{
public AvaloniaProperty Property { get; }
public object Value { get; }
public bool IsDirect { get; }
public IDirectPropertyAccessor DirectAccessor { get; }
public PropertyInitializationData(AvaloniaProperty property, IDirectPropertyAccessor directAccessor)
{
Property = property;
Value = null;
IsDirect = true;
DirectAccessor = directAccessor;
}
foreach (var i in items)
public PropertyInitializationData(AvaloniaProperty property, IStyledPropertyAccessor styledAccessor, Type type)
{
var value = i.Key.IsDirect ? o.GetValue(i.Key) : i.Value;
Notify(i.Key, value);
Property = property;
Value = styledAccessor.GetDefaultValue(type);
IsDirect = false;
DirectAccessor = null;
}
}
}

28
src/Avalonia.Base/BoxedValue.cs

@ -0,0 +1,28 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia
{
/// <summary>
/// Represents boxed value of type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">Type of stored value.</typeparam>
internal readonly struct BoxedValue<T>
{
public BoxedValue(T value)
{
Boxed = value;
Typed = value;
}
/// <summary>
/// Boxed value.
/// </summary>
public object Boxed { get; }
/// <summary>
/// Typed value.
/// </summary>
public T Typed { get; }
}
}

229
src/Avalonia.Base/Collections/AvaloniaList.cs

@ -55,15 +55,15 @@ namespace Avalonia.Collections
/// </remarks>
public class AvaloniaList<T> : IAvaloniaList<T>, IList, INotifyCollectionChangedDebug
{
private List<T> _inner;
private readonly List<T> _inner;
private NotifyCollectionChangedEventHandler _collectionChanged;
/// <summary>
/// Initializes a new instance of the <see cref="AvaloniaList{T}"/> class.
/// </summary>
public AvaloniaList()
: this(Enumerable.Empty<T>())
{
_inner = new List<T>();
}
/// <summary>
@ -89,8 +89,8 @@ namespace Avalonia.Collections
/// </summary>
public event NotifyCollectionChangedEventHandler CollectionChanged
{
add { _collectionChanged += value; }
remove { _collectionChanged -= value; }
add => _collectionChanged += value;
remove => _collectionChanged -= value;
}
/// <summary>
@ -150,7 +150,7 @@ namespace Avalonia.Collections
T old = _inner[index];
if (!object.Equals(old, value))
if (!EqualityComparer<T>.Default.Equals(old, value))
{
_inner[index] = value;
@ -187,45 +187,38 @@ namespace Avalonia.Collections
Validate?.Invoke(item);
int index = _inner.Count;
_inner.Add(item);
NotifyAdd(new[] { item }, index);
NotifyAdd(item, index);
}
/// <summary>
/// Adds multiple items to the collection.
/// </summary>
/// <param name="items">The items.</param>
public virtual void AddRange(IEnumerable<T> items)
{
Contract.Requires<ArgumentNullException>(items != null);
var list = (items as IList) ?? items.ToList();
if (list.Count > 0)
{
if (Validate != null)
{
foreach (var item in list)
{
Validate((T)item);
}
}
int index = _inner.Count;
_inner.AddRange(items);
NotifyAdd(list, index);
}
}
public virtual void AddRange(IEnumerable<T> items) => InsertRange(_inner.Count, items);
/// <summary>
/// Removes all items from the collection.
/// </summary>
public virtual void Clear()
{
if (this.Count > 0)
if (Count > 0)
{
var old = _inner;
_inner = new List<T>();
NotifyReset(old);
if (_collectionChanged != null)
{
var e = ResetBehavior == ResetBehavior.Reset ?
EventArgsCache.ResetCollectionChanged :
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, _inner.ToList(), 0);
_inner.Clear();
_collectionChanged(this, e);
}
else
{
_inner.Clear();
}
NotifyCountChanged();
}
}
@ -253,9 +246,20 @@ namespace Avalonia.Collections
/// Returns an enumerator that enumerates the items in the collection.
/// </summary>
/// <returns>An <see cref="IEnumerator{T}"/>.</returns>
public IEnumerator<T> GetEnumerator()
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return _inner.GetEnumerator();
return new Enumerator(_inner);
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
{
return new Enumerator(_inner);
}
public Enumerator GetEnumerator()
{
return new Enumerator(_inner);
}
/// <summary>
@ -289,7 +293,7 @@ namespace Avalonia.Collections
{
Validate?.Invoke(item);
_inner.Insert(index, item);
NotifyAdd(new[] { item }, index);
NotifyAdd(item, index);
}
/// <summary>
@ -301,20 +305,83 @@ namespace Avalonia.Collections
{
Contract.Requires<ArgumentNullException>(items != null);
var list = (items as IList) ?? items.ToList();
bool willRaiseCollectionChanged = _collectionChanged != null;
bool hasValidation = Validate != null;
if (list.Count > 0)
if (items is IList list)
{
if (Validate != null)
if (list.Count > 0)
{
foreach (var item in list)
if (list is ICollection<T> collection)
{
Validate((T)item);
if (hasValidation)
{
foreach (T item in collection)
{
Validate(item);
}
}
_inner.InsertRange(index, collection);
NotifyAdd(list, index);
}
else
{
using (IEnumerator<T> en = items.GetEnumerator())
{
int insertIndex = index;
while (en.MoveNext())
{
T item = en.Current;
if (hasValidation)
{
Validate(item);
}
_inner.Insert(insertIndex++, item);
}
}
NotifyAdd(list, index);
}
}
}
else
{
using (IEnumerator<T> en = items.GetEnumerator())
{
if (en.MoveNext())
{
// Avoid allocating list for collection notification if there is no event subscriptions.
List<T> notificationItems = willRaiseCollectionChanged ?
new List<T>() :
null;
int insertIndex = index;
do
{
T item = en.Current;
if (hasValidation)
{
Validate(item);
}
_inner.InsertRange(index, items);
NotifyAdd((items as IList) ?? items.ToList(), index);
_inner.Insert(insertIndex++, item);
if (willRaiseCollectionChanged)
{
notificationItems.Add(item);
}
} while (en.MoveNext());
NotifyAdd(notificationItems, index);
}
}
}
}
@ -382,7 +449,7 @@ namespace Avalonia.Collections
if (index != -1)
{
_inner.RemoveAt(index);
NotifyRemove(new[] { item }, index);
NotifyRemove(item , index);
return true;
}
@ -412,7 +479,7 @@ namespace Avalonia.Collections
{
T item = _inner[index];
_inner.RemoveAt(index);
NotifyRemove(new[] { item }, index);
NotifyRemove(item , index);
}
/// <summary>
@ -480,12 +547,6 @@ namespace Avalonia.Collections
_inner.CopyTo((T[])array, index);
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
{
return _inner.GetEnumerator();
}
/// <inheritdoc/>
Delegate[] INotifyCollectionChangedDebug.GetCollectionChangedSubscribers() => _collectionChanged?.GetInvocationList();
@ -505,13 +566,29 @@ namespace Avalonia.Collections
NotifyCountChanged();
}
/// <summary>
/// Raises the <see cref="CollectionChanged"/> event with a add action.
/// </summary>
/// <param name="item">The item that was added.</param>
/// <param name="index">The starting index.</param>
private void NotifyAdd(T item, int index)
{
if (_collectionChanged != null)
{
var e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new[] { item }, index);
_collectionChanged(this, e);
}
NotifyCountChanged();
}
/// <summary>
/// Raises the <see cref="PropertyChanged"/> event when the <see cref="Count"/> property
/// changes.
/// </summary>
private void NotifyCountChanged()
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count)));
PropertyChanged?.Invoke(this, EventArgsCache.CountPropertyChanged);
}
/// <summary>
@ -531,23 +608,57 @@ namespace Avalonia.Collections
}
/// <summary>
/// Raises the <see cref="CollectionChanged"/> event with a reset action.
/// Raises the <see cref="CollectionChanged"/> event with a remove action.
/// </summary>
/// <param name="t">The items that were removed.</param>
private void NotifyReset(IList t)
/// <param name="item">The item that was removed.</param>
/// <param name="index">The starting index.</param>
private void NotifyRemove(T item, int index)
{
if (_collectionChanged != null)
{
NotifyCollectionChangedEventArgs e;
e = ResetBehavior == ResetBehavior.Reset ?
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset) :
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, t, 0);
var e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { item }, index);
_collectionChanged(this, e);
}
NotifyCountChanged();
}
/// <summary>
/// Enumerates the elements of a <see cref="AvaloniaList{T}"/>.
/// </summary>
public struct Enumerator : IEnumerator<T>
{
private List<T>.Enumerator _innerEnumerator;
public Enumerator(List<T> inner)
{
_innerEnumerator = inner.GetEnumerator();
}
public bool MoveNext()
{
return _innerEnumerator.MoveNext();
}
void IEnumerator.Reset()
{
((IEnumerator)_innerEnumerator).Reset();
}
public T Current => _innerEnumerator.Current;
object IEnumerator.Current => Current;
public void Dispose()
{
_innerEnumerator.Dispose();
}
}
}
internal static class EventArgsCache
{
internal static readonly PropertyChangedEventArgs CountPropertyChanged = new PropertyChangedEventArgs(nameof(AvaloniaList<object>.Count));
internal static readonly NotifyCollectionChangedEventArgs ResetCollectionChanged = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
}
}

6
src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs

@ -24,7 +24,7 @@ namespace Avalonia.Data.Core
{
try
{
if (Target.IsAlive && Target.Target is IAvaloniaObject obj)
if (Target.TryGetTarget(out object target) && target is IAvaloniaObject obj)
{
obj.SetValue(_property, value, priority);
return true;
@ -37,9 +37,9 @@ namespace Avalonia.Data.Core
}
}
protected override void StartListeningCore(WeakReference reference)
protected override void StartListeningCore(WeakReference<object> reference)
{
if (reference.Target is IAvaloniaObject obj)
if (reference.TryGetTarget(out object target) && target is IAvaloniaObject obj)
{
_subscription = new AvaloniaPropertyObservable<object>(obj, _property).Subscribe(ValueChanged);
}

26
src/Avalonia.Base/Data/Core/ExpressionNode.cs

@ -8,27 +8,27 @@ namespace Avalonia.Data.Core
public abstract class ExpressionNode
{
private static readonly object CacheInvalid = new object();
protected static readonly WeakReference UnsetReference =
new WeakReference(AvaloniaProperty.UnsetValue);
protected static readonly WeakReference<object> UnsetReference =
new WeakReference<object>(AvaloniaProperty.UnsetValue);
private WeakReference _target = UnsetReference;
private WeakReference<object> _target = UnsetReference;
private Action<object> _subscriber;
private bool _listening;
protected WeakReference LastValue { get; private set; }
protected WeakReference<object> LastValue { get; private set; }
public abstract string Description { get; }
public ExpressionNode Next { get; set; }
public WeakReference Target
public WeakReference<object> Target
{
get { return _target; }
set
{
Contract.Requires<ArgumentNullException>(value != null);
var oldTarget = _target?.Target;
var newTarget = value.Target;
_target.TryGetTarget(out var oldTarget);
value.TryGetTarget(out object newTarget);
if (!ReferenceEquals(oldTarget, newTarget))
{
@ -72,9 +72,11 @@ namespace Avalonia.Data.Core
_subscriber = null;
}
protected virtual void StartListeningCore(WeakReference reference)
protected virtual void StartListeningCore(WeakReference<object> reference)
{
ValueChanged(reference.Target);
reference.TryGetTarget(out object target);
ValueChanged(target);
}
protected virtual void StopListeningCore()
@ -96,7 +98,7 @@ namespace Avalonia.Data.Core
if (notification == null)
{
LastValue = new WeakReference(value);
LastValue = new WeakReference<object>(value);
if (Next != null)
{
@ -109,7 +111,7 @@ namespace Avalonia.Data.Core
}
else
{
LastValue = new WeakReference(notification.Value);
LastValue = new WeakReference<object>(notification.Value);
if (Next != null)
{
@ -125,7 +127,7 @@ namespace Avalonia.Data.Core
private void StartListening()
{
var target = _target.Target;
_target.TryGetTarget(out object target);
if (target == null)
{

8
src/Avalonia.Base/Data/Core/ExpressionObserver.cs

@ -78,7 +78,7 @@ namespace Avalonia.Data.Core
_node = node;
Description = description;
_root = new WeakReference(root);
_root = new WeakReference<object>(root);
}
/// <summary>
@ -120,7 +120,7 @@ namespace Avalonia.Data.Core
Contract.Requires<ArgumentNullException>(update != null);
Description = description;
_node = node;
_node.Target = new WeakReference(rootGetter());
_node.Target = new WeakReference<object>(rootGetter());
_root = update.Select(x => rootGetter());
}
@ -285,13 +285,13 @@ namespace Avalonia.Data.Core
if (_root is IObservable<object> observable)
{
_rootSubscription = observable.Subscribe(
x => _node.Target = new WeakReference(x != AvaloniaProperty.UnsetValue ? x : null),
x => _node.Target = new WeakReference<object>(x != AvaloniaProperty.UnsetValue ? x : null),
x => PublishCompleted(),
() => PublishCompleted());
}
else
{
_node.Target = (WeakReference)_root;
_node.Target = (WeakReference<object>)_root;
}
}

11
src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs

@ -36,7 +36,9 @@ namespace Avalonia.Data.Core
{
try
{
_setDelegate.DynamicInvoke(Target.Target, value);
Target.TryGetTarget(out object target);
_setDelegate.DynamicInvoke(target, value);
return true;
}
catch (Exception)
@ -64,6 +66,11 @@ namespace Avalonia.Data.Core
return _expression.Indexer == null || _expression.Indexer.Name == e.PropertyName;
}
protected override int? TryGetFirstArgumentAsInt() => _firstArgumentDelegate.DynamicInvoke(Target.Target) as int?;
protected override int? TryGetFirstArgumentAsInt()
{
Target.TryGetTarget(out object target);
return _firstArgumentDelegate.DynamicInvoke(target) as int?;
}
}
}

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

@ -13,9 +13,10 @@ namespace Avalonia.Data.Core
{
private IDisposable _subscription;
protected override void StartListeningCore(WeakReference reference)
protected override void StartListeningCore(WeakReference<object> reference)
{
var target = reference.Target;
reference.TryGetTarget(out object target);
var incc = target as INotifyCollectionChanged;
var inpc = target as INotifyPropertyChanged;
var inputs = new List<IObservable<object>>();

4
src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs

@ -31,12 +31,12 @@ namespace Avalonia.Data.Core.Plugins
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made.
/// </returns>
public IPropertyAccessor Start(WeakReference reference, string propertyName)
public IPropertyAccessor Start(WeakReference<object> reference, string propertyName)
{
Contract.Requires<ArgumentNullException>(reference != null);
Contract.Requires<ArgumentNullException>(propertyName != null);
var instance = reference.Target;
reference.TryGetTarget(out object instance);
var o = (AvaloniaObject)instance;
var p = LookupProperty(o, propertyName);

23
src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs

@ -15,9 +15,11 @@ namespace Avalonia.Data.Core.Plugins
public class DataAnnotationsValidationPlugin : IDataValidationPlugin
{
/// <inheritdoc/>
public bool Match(WeakReference reference, string memberName)
public bool Match(WeakReference<object> reference, string memberName)
{
return reference.Target?
reference.TryGetTarget(out object target);
return target?
.GetType()
.GetRuntimeProperty(memberName)?
.GetCustomAttributes<ValidationAttribute>()
@ -25,25 +27,22 @@ namespace Avalonia.Data.Core.Plugins
}
/// <inheritdoc/>
public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor inner)
public IPropertyAccessor Start(WeakReference<object> reference, string name, IPropertyAccessor inner)
{
return new Accessor(reference, name, inner);
}
private class Accessor : DataValidationBase
private sealed class Accessor : DataValidationBase
{
private ValidationContext _context;
private readonly ValidationContext _context;
public Accessor(WeakReference reference, string name, IPropertyAccessor inner)
public Accessor(WeakReference<object> reference, string name, IPropertyAccessor inner)
: base(inner)
{
_context = new ValidationContext(reference.Target);
_context.MemberName = name;
}
reference.TryGetTarget(out object target);
public override bool SetValue(object value, BindingPriority priority)
{
return base.SetValue(value, priority);
_context = new ValidationContext(target);
_context.MemberName = name;
}
protected override void InnerValueChanged(object value)

8
src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs

@ -12,17 +12,17 @@ namespace Avalonia.Data.Core.Plugins
public class ExceptionValidationPlugin : IDataValidationPlugin
{
/// <inheritdoc/>
public bool Match(WeakReference reference, string memberName) => true;
public bool Match(WeakReference<object> reference, string memberName) => true;
/// <inheritdoc/>
public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor inner)
public IPropertyAccessor Start(WeakReference<object> reference, string name, IPropertyAccessor inner)
{
return new Validator(reference, name, inner);
}
private class Validator : DataValidationBase
private sealed class Validator : DataValidationBase
{
public Validator(WeakReference reference, string name, IPropertyAccessor inner)
public Validator(WeakReference<object> reference, string name, IPropertyAccessor inner)
: base(inner)
{
}

5
src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs

@ -16,7 +16,7 @@ namespace Avalonia.Data.Core.Plugins
/// <param name="reference">A weak reference to the object.</param>
/// <param name="memberName">The name of the member to validate.</param>
/// <returns>True if the plugin can handle the object; otherwise false.</returns>
bool Match(WeakReference reference, string memberName);
bool Match(WeakReference<object> reference, string memberName);
/// <summary>
/// Starts monitoring the data validation state of a property on an object.
@ -28,8 +28,7 @@ namespace Avalonia.Data.Core.Plugins
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made.
/// </returns>
IPropertyAccessor Start(
WeakReference reference,
IPropertyAccessor Start(WeakReference<object> reference,
string propertyName,
IPropertyAccessor inner);
}

3
src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessorPlugin.cs

@ -28,8 +28,7 @@ namespace Avalonia.Data.Core.Plugins
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made.
/// </returns>
IPropertyAccessor Start(
WeakReference reference,
IPropertyAccessor Start(WeakReference<object> reference,
string propertyName);
}
}

4
src/Avalonia.Base/Data/Core/Plugins/IStreamPlugin.cs

@ -15,7 +15,7 @@ namespace Avalonia.Data.Core.Plugins
/// </summary>
/// <param name="reference">A weak reference to the value.</param>
/// <returns>True if the plugin can handle the value; otherwise false.</returns>
bool Match(WeakReference reference);
bool Match(WeakReference<object> reference);
/// <summary>
/// Starts producing output based on the specified value.
@ -24,6 +24,6 @@ namespace Avalonia.Data.Core.Plugins
/// <returns>
/// An observable that produces the output for the value.
/// </returns>
IObservable<object> Start(WeakReference reference);
IObservable<object> Start(WeakReference<object> reference);
}
}

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

@ -15,20 +15,25 @@ namespace Avalonia.Data.Core.Plugins
public class IndeiValidationPlugin : IDataValidationPlugin
{
/// <inheritdoc/>
public bool Match(WeakReference reference, string memberName) => reference.Target is INotifyDataErrorInfo;
public bool Match(WeakReference<object> reference, string memberName)
{
reference.TryGetTarget(out object target);
return target is INotifyDataErrorInfo;
}
/// <inheritdoc/>
public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor)
public IPropertyAccessor Start(WeakReference<object> reference, string name, IPropertyAccessor accessor)
{
return new Validator(reference, name, accessor);
}
private class Validator : DataValidationBase, IWeakSubscriber<DataErrorsChangedEventArgs>
{
WeakReference _reference;
string _name;
private readonly WeakReference<object> _reference;
private readonly string _name;
public Validator(WeakReference reference, string name, IPropertyAccessor inner)
public Validator(WeakReference<object> reference, string name, IPropertyAccessor inner)
: base(inner)
{
_reference = reference;
@ -45,7 +50,7 @@ namespace Avalonia.Data.Core.Plugins
protected override void SubscribeCore()
{
var target = _reference.Target as INotifyDataErrorInfo;
var target = GetReferenceTarget() as INotifyDataErrorInfo;
if (target != null)
{
@ -60,7 +65,7 @@ namespace Avalonia.Data.Core.Plugins
protected override void UnsubscribeCore()
{
var target = _reference.Target as INotifyDataErrorInfo;
var target = GetReferenceTarget() as INotifyDataErrorInfo;
if (target != null)
{
@ -80,13 +85,14 @@ namespace Avalonia.Data.Core.Plugins
private BindingNotification CreateBindingNotification(object value)
{
var target = (INotifyDataErrorInfo)_reference.Target;
var target = (INotifyDataErrorInfo)GetReferenceTarget();
if (target != null)
{
var errors = target.GetErrors(_name)?
.Cast<String>()
.Where(x => x != null).ToList();
.Cast<object>()
.Where(x => x != null)
.ToList();
if (errors?.Count > 0)
{
@ -100,16 +106,23 @@ namespace Avalonia.Data.Core.Plugins
return new BindingNotification(value);
}
private Exception GenerateException(IList<string> errors)
private object GetReferenceTarget()
{
_reference.TryGetTarget(out object target);
return target;
}
private Exception GenerateException(IList<object> errors)
{
if (errors.Count == 1)
{
return new Exception(errors[0]);
return new DataValidationException(errors[0]);
}
else
{
return new AggregateException(
errors.Select(x => new Exception(x)));
errors.Select(x => new DataValidationException(x)));
}
}
}

23
src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs

@ -28,12 +28,12 @@ namespace Avalonia.Data.Core.Plugins
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made.
/// </returns>
public IPropertyAccessor Start(WeakReference reference, string propertyName)
public IPropertyAccessor Start(WeakReference<object> reference, string propertyName)
{
Contract.Requires<ArgumentNullException>(reference != null);
Contract.Requires<ArgumentNullException>(propertyName != null);
var instance = reference.Target;
reference.TryGetTarget(out object instance);
var p = instance.GetType().GetRuntimeProperties().FirstOrDefault(x => x.Name == propertyName);
if (p != null)
@ -50,11 +50,11 @@ namespace Avalonia.Data.Core.Plugins
private class Accessor : PropertyAccessorBase, IWeakSubscriber<PropertyChangedEventArgs>
{
private readonly WeakReference _reference;
private readonly WeakReference<object> _reference;
private readonly PropertyInfo _property;
private bool _eventRaised;
public Accessor(WeakReference reference, PropertyInfo property)
public Accessor(WeakReference<object> reference, PropertyInfo property)
{
Contract.Requires<ArgumentNullException>(reference != null);
Contract.Requires<ArgumentNullException>(property != null);
@ -69,7 +69,7 @@ namespace Avalonia.Data.Core.Plugins
{
get
{
var o = _reference.Target;
var o = GetReferenceTarget();
return (o != null) ? _property.GetValue(o) : null;
}
}
@ -79,7 +79,7 @@ namespace Avalonia.Data.Core.Plugins
if (_property.CanWrite)
{
_eventRaised = false;
_property.SetValue(_reference.Target, value);
_property.SetValue(GetReferenceTarget(), value);
if (!_eventRaised)
{
@ -109,7 +109,7 @@ namespace Avalonia.Data.Core.Plugins
protected override void UnsubscribeCore()
{
var inpc = _reference.Target as INotifyPropertyChanged;
var inpc = GetReferenceTarget() as INotifyPropertyChanged;
if (inpc != null)
{
@ -120,6 +120,13 @@ namespace Avalonia.Data.Core.Plugins
}
}
private object GetReferenceTarget()
{
_reference.TryGetTarget(out object target);
return target;
}
private void SendCurrentValue()
{
try
@ -132,7 +139,7 @@ namespace Avalonia.Data.Core.Plugins
private void SubscribeToChanges()
{
var inpc = _reference.Target as INotifyPropertyChanged;
var inpc = GetReferenceTarget() as INotifyPropertyChanged;
if (inpc != null)
{

21
src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs

@ -9,12 +9,12 @@ namespace Avalonia.Data.Core.Plugins
public bool Match(object obj, string methodName)
=> obj.GetType().GetRuntimeMethods().Any(x => x.Name == methodName);
public IPropertyAccessor Start(WeakReference reference, string methodName)
public IPropertyAccessor Start(WeakReference<object> reference, string methodName)
{
Contract.Requires<ArgumentNullException>(reference != null);
Contract.Requires<ArgumentNullException>(methodName != null);
var instance = reference.Target;
reference.TryGetTarget(out object instance);
var method = instance.GetType().GetRuntimeMethods().FirstOrDefault(x => x.Name == methodName);
if (method != null)
@ -35,9 +35,9 @@ namespace Avalonia.Data.Core.Plugins
}
}
private class Accessor : PropertyAccessorBase
private sealed class Accessor : PropertyAccessorBase
{
public Accessor(WeakReference reference, MethodInfo method)
public Accessor(WeakReference<object> reference, MethodInfo method)
{
Contract.Requires<ArgumentNullException>(reference != null);
Contract.Requires<ArgumentNullException>(method != null);
@ -61,8 +61,17 @@ namespace Avalonia.Data.Core.Plugins
var genericTypeParameters = paramTypes.Concat(new[] { returnType }).ToArray();
PropertyType = Type.GetType($"System.Func`{genericTypeParameters.Length}").MakeGenericType(genericTypeParameters);
}
Value = method.IsStatic ? method.CreateDelegate(PropertyType) : method.CreateDelegate(PropertyType, reference.Target);
if (method.IsStatic)
{
Value = method.CreateDelegate(PropertyType);
}
else
{
reference.TryGetTarget(out object target);
Value = method.CreateDelegate(PropertyType, target);
}
}
public override Type PropertyType { get; }

12
src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs

@ -20,9 +20,11 @@ namespace Avalonia.Data.Core.Plugins
/// </summary>
/// <param name="reference">A weak reference to the value.</param>
/// <returns>True if the plugin can handle the value; otherwise false.</returns>
public virtual bool Match(WeakReference reference)
public virtual bool Match(WeakReference<object> reference)
{
return reference.Target.GetType().GetInterfaces().Any(x =>
reference.TryGetTarget(out object target);
return target != null && target.GetType().GetInterfaces().Any(x =>
x.IsGenericType &&
x.GetGenericTypeDefinition() == typeof(IObservable<>));
}
@ -34,9 +36,9 @@ namespace Avalonia.Data.Core.Plugins
/// <returns>
/// An observable that produces the output for the value.
/// </returns>
public virtual IObservable<object> Start(WeakReference reference)
public virtual IObservable<object> Start(WeakReference<object> reference)
{
var target = reference.Target;
reference.TryGetTarget(out object target);
// If the observable returns a reference type then we can cast it.
if (target is IObservable<object> result)
@ -46,7 +48,7 @@ namespace Avalonia.Data.Core.Plugins
// If the observable returns a value type then we need to call Observable.Select on it.
// First get the type of T in `IObservable<T>`.
var sourceType = reference.Target.GetType().GetInterfaces().First(x =>
var sourceType = target.GetType().GetInterfaces().First(x =>
x.IsGenericType &&
x.GetGenericTypeDefinition() == typeof(IObservable<>)).GetGenericArguments()[0];

13
src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs

@ -19,7 +19,12 @@ namespace Avalonia.Data.Core.Plugins
/// </summary>
/// <param name="reference">A weak reference to the value.</param>
/// <returns>True if the plugin can handle the value; otherwise false.</returns>
public virtual bool Match(WeakReference reference) => reference.Target is Task;
public virtual bool Match(WeakReference<object> reference)
{
reference.TryGetTarget(out object target);
return target is Task;
}
/// <summary>
/// Starts producing output based on the specified value.
@ -28,11 +33,11 @@ namespace Avalonia.Data.Core.Plugins
/// <returns>
/// An observable that produces the output for the value.
/// </returns>
public virtual IObservable<object> Start(WeakReference reference)
public virtual IObservable<object> Start(WeakReference<object> reference)
{
var task = reference.Target as Task;
reference.TryGetTarget(out object target);
if (task != null)
if (target is Task task)
{
var resultProperty = task.GetType().GetRuntimeProperty("Result");

6
src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs

@ -37,9 +37,11 @@ namespace Avalonia.Data.Core
return false;
}
protected override void StartListeningCore(WeakReference reference)
protected override void StartListeningCore(WeakReference<object> reference)
{
var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference.Target, PropertyName));
reference.TryGetTarget(out object target);
var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(target, PropertyName));
var accessor = plugin?.Start(reference, PropertyName);
if (_enableValidation && Next == null)

18
src/Avalonia.Base/Data/Core/SettableNode.cs

@ -19,11 +19,25 @@ namespace Avalonia.Data.Core
{
return false;
}
if (LastValue == null)
{
return false;
}
bool isLastValueAlive = LastValue.TryGetTarget(out object lastValue);
if (!isLastValueAlive)
{
return false;
}
if (PropertyType.IsValueType)
{
return LastValue?.Target != null && LastValue.Target.Equals(value);
return lastValue.Equals(value);
}
return LastValue != null && Object.ReferenceEquals(LastValue?.Target, value);
return ReferenceEquals(lastValue, value);
}
protected abstract bool SetTargetValueCore(object value, BindingPriority priority);

2
src/Avalonia.Base/Data/Core/StreamNode.cs

@ -12,7 +12,7 @@ namespace Avalonia.Data.Core
public override string Description => "^";
protected override void StartListeningCore(WeakReference reference)
protected override void StartListeningCore(WeakReference<object> reference)
{
foreach (var plugin in ExpressionObserver.StreamHandlers)
{

21
src/Avalonia.Base/Data/DataValidationException.cs

@ -0,0 +1,21 @@
using System;
namespace Avalonia.Data
{
/// <summary>
/// Exception, which wrap validation errors.
/// </summary>
public class DataValidationException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="DataValidationException"/> class.
/// </summary>
/// <param name="errorData">Data of validation error.</param>
public DataValidationException(object errorData) : base(errorData?.ToString())
{
ErrorData = errorData;
}
public object ErrorData { get; }
}
}

16
src/Avalonia.Base/PriorityBindingEntry.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Runtime.ExceptionServices;
using Avalonia.Data;
using Avalonia.Threading;
@ -10,9 +11,9 @@ namespace Avalonia
/// <summary>
/// A registered binding in a <see cref="PriorityValue"/>.
/// </summary>
internal class PriorityBindingEntry : IDisposable
internal class PriorityBindingEntry : IDisposable, IObserver<object>
{
private PriorityLevel _owner;
private readonly PriorityLevel _owner;
private IDisposable _subscription;
/// <summary>
@ -85,7 +86,7 @@ namespace Avalonia
Description = ((IDescription)binding).Description;
}
_subscription = binding.Subscribe(ValueChanged, Completed);
_subscription = binding.Subscribe(this);
}
/// <summary>
@ -96,7 +97,7 @@ namespace Avalonia
_subscription?.Dispose();
}
private void ValueChanged(object value)
void IObserver<object>.OnNext(object value)
{
void Signal()
{
@ -132,7 +133,7 @@ namespace Avalonia
}
}
private void Completed()
void IObserver<object>.OnCompleted()
{
HasCompleted = true;
@ -145,5 +146,10 @@ namespace Avalonia
Dispatcher.UIThread.Post(() => _owner.Completed(this));
}
}
void IObserver<object>.OnError(Exception error)
{
ExceptionDispatchInfo.Capture(error).Throw();
}
}
}

14
src/Avalonia.Base/PriorityValue.cs

@ -24,13 +24,11 @@ namespace Avalonia
/// <see cref="IPriorityValueOwner.Changed"/> method on the
/// owner object is fired with the old and new values.
/// </remarks>
internal class PriorityValue
internal sealed class PriorityValue : ISetAndNotifyHandler<(object,int)>
{
private readonly Type _valueType;
private readonly SingleOrDictionary<int, PriorityLevel> _levels = new SingleOrDictionary<int, PriorityLevel>();
private readonly Func<object, object> _validate;
private readonly SetAndNotifyCallback<(object, int)> _setAndNotifyCallback;
private (object value, int priority) _value;
private DeferredSetter<object> _setter;
@ -52,7 +50,6 @@ namespace Avalonia
_valueType = valueType;
_value = (AvaloniaProperty.UnsetValue, int.MaxValue);
_validate = validate;
_setAndNotifyCallback = SetAndNotify;
}
/// <summary>
@ -257,10 +254,15 @@ namespace Avalonia
_setter = Owner.GetNonDirectDeferredSetter(Property);
}
_setter.SetAndNotifyCallback(Property, _setAndNotifyCallback, ref _value, newValue);
_setter.SetAndNotifyCallback(Property, this, ref _value, newValue);
}
void ISetAndNotifyHandler<(object, int)>.HandleSetAndNotify(AvaloniaProperty property, ref (object, int) backing, (object, int) value)
{
SetAndNotify(ref backing, value);
}
private void SetAndNotify(AvaloniaProperty property, ref (object value, int priority) backing, (object value, int priority) update)
private void SetAndNotify(ref (object value, int priority) backing, (object value, int priority) update)
{
var val = update.value;
var notification = val as BindingNotification;

11
src/Avalonia.Base/StyledPropertyBase.cs

@ -68,7 +68,7 @@ namespace Avalonia
{
Contract.Requires<ArgumentNullException>(type != null);
return GetMetadata(type).DefaultValue;
return GetMetadata(type).DefaultValue.Typed;
}
/// <summary>
@ -164,7 +164,14 @@ namespace Avalonia
}
/// <inheritdoc/>
object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultValue(type);
object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type);
private object GetDefaultBoxedValue(Type type)
{
Contract.Requires<ArgumentNullException>(type != null);
return GetMetadata(type).DefaultValue.Boxed;
}
[DebuggerHidden]
private Func<IAvaloniaObject, TValue, TValue> Cast<THost>(Func<THost, TValue, TValue> validate)

14
src/Avalonia.Base/StyledPropertyMetadata`1.cs

@ -19,26 +19,26 @@ namespace Avalonia
/// <param name="validate">A validation function.</param>
/// <param name="defaultBindingMode">The default binding mode.</param>
public StyledPropertyMetadata(
TValue defaultValue = default(TValue),
TValue defaultValue = default,
Func<IAvaloniaObject, TValue, TValue> validate = null,
BindingMode defaultBindingMode = BindingMode.Default)
: base(defaultBindingMode)
{
DefaultValue = defaultValue;
DefaultValue = new BoxedValue<TValue>(defaultValue);
Validate = validate;
}
/// <summary>
/// Gets the default value for the property.
/// </summary>
public TValue DefaultValue { get; private set; }
internal BoxedValue<TValue> DefaultValue { get; private set; }
/// <summary>
/// Gets the validation callback.
/// </summary>
public Func<IAvaloniaObject, TValue, TValue> Validate { get; private set; }
object IStyledPropertyMetadata.DefaultValue => DefaultValue;
object IStyledPropertyMetadata.DefaultValue => DefaultValue.Boxed;
Func<IAvaloniaObject, object, object> IStyledPropertyMetadata.Validate => Cast(Validate);
@ -47,11 +47,9 @@ namespace Avalonia
{
base.Merge(baseMetadata, property);
var src = baseMetadata as StyledPropertyMetadata<TValue>;
if (src != null)
if (baseMetadata is StyledPropertyMetadata<TValue> src)
{
if (DefaultValue == null)
if (DefaultValue.Boxed == null)
{
DefaultValue = src.DefaultValue;
}

30
src/Avalonia.Base/Utilities/DeferredSetter.cs

@ -5,15 +5,6 @@ using System;
namespace Avalonia.Utilities
{
/// <summary>
/// Callback invoked when deferred setter wants to set a value.
/// </summary>
/// <typeparam name="TValue">Value type.</typeparam>
/// <param name="property">Property being set.</param>
/// <param name="backing">Backing field reference.</param>
/// <param name="value">New value.</param>
internal delegate void SetAndNotifyCallback<TValue>(AvaloniaProperty property, ref TValue backing, TValue value);
/// <summary>
/// A utility class to enable deferring assignment until after property-changed notifications are sent.
/// Used to fix #855.
@ -70,14 +61,14 @@ namespace Avalonia.Utilities
return false;
}
public bool SetAndNotifyCallback<TValue>(AvaloniaProperty property, SetAndNotifyCallback<TValue> setAndNotifyCallback, ref TValue backing, TValue value)
public bool SetAndNotifyCallback<TValue>(AvaloniaProperty property, ISetAndNotifyHandler<TValue> setAndNotifyHandler, ref TValue backing, TValue value)
where TValue : TSetRecord
{
if (!_isNotifying)
{
using (new NotifyDisposable(this))
{
setAndNotifyCallback(property, ref backing, value);
setAndNotifyHandler.HandleSetAndNotify(property, ref backing, value);
}
if (!_pendingValues.Empty)
@ -86,7 +77,7 @@ namespace Avalonia.Utilities
{
while (!_pendingValues.Empty)
{
setAndNotifyCallback(property, ref backing, (TValue) _pendingValues.Dequeue());
setAndNotifyHandler.HandleSetAndNotify(property, ref backing, (TValue)_pendingValues.Dequeue());
}
}
}
@ -119,4 +110,19 @@ namespace Avalonia.Utilities
}
}
}
/// <summary>
/// Handler for set and notify requests.
/// </summary>
/// <typeparam name="TValue">Value type.</typeparam>
internal interface ISetAndNotifyHandler<TValue>
{
/// <summary>
/// Handles deferred setter requests to set a value.
/// </summary>
/// <param name="property">Property being set.</param>
/// <param name="backing">Backing field reference.</param>
/// <param name="value">New value.</param>
void HandleSetAndNotify(AvaloniaProperty property, ref TValue backing, TValue value);
}
}

4
src/Avalonia.Controls.DataGrid/Themes/Default.xaml

@ -217,12 +217,12 @@
<DataGridRowsPresenter Name="PART_RowsPresenter" Grid.ColumnSpan="2" Grid.Row="1" />
<Rectangle Name="BottomRightCorner" Fill="#FFE9EEF4" Grid.Column="2" Grid.Row="2" />
<Rectangle Name="BottomLeftCorner" Fill="#FFE9EEF4" Grid.Row="2" Grid.ColumnSpan="2" />
<ScrollBar Name="PART_VerticalScrollbar" Orientation="Vertical" Grid.Column="2" Grid.Row="1" Width="18" Margin="0,-1,-1,-1"/>
<ScrollBar Name="PART_VerticalScrollbar" Orientation="Vertical" Grid.Column="2" Grid.Row="1" Width="{DynamicResource ScrollBarThickness}" Margin="0,-1,-1,-1"/>
<Grid Grid.Column="1" Grid.Row="2"
ColumnDefinitions="Auto,*">
<Rectangle Name="PART_FrozenColumnScrollBarSpacer" />
<ScrollBar Name="PART_HorizontalScrollbar" Grid.Column="1" Orientation="Horizontal" Height="18" Margin="-1,0,-1,-1"/>
<ScrollBar Name="PART_HorizontalScrollbar" Grid.Column="1" Orientation="Horizontal" Height="{DynamicResource ScrollBarThickness}" Margin="-1,0,-1,-1"/>
</Grid>
</Grid>
</Border>

15
src/Avalonia.Controls/ControlExtensions.cs

@ -34,14 +34,17 @@ namespace Avalonia.Controls
{
Contract.Requires<ArgumentNullException>(control != null);
var ev = new RequestBringIntoViewEventArgs
if (control.IsEffectivelyVisible)
{
RoutedEvent = Control.RequestBringIntoViewEvent,
TargetObject = control,
TargetRect = rect,
};
var ev = new RequestBringIntoViewEventArgs
{
RoutedEvent = Control.RequestBringIntoViewEvent,
TargetObject = control,
TargetRect = rect,
};
control.RaiseEvent(ev);
control.RaiseEvent(ev);
}
}
/// <summary>

24
src/Avalonia.Controls/DataValidationErrors.cs

@ -22,8 +22,8 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the DataValidationErrors.Errors attached property.
/// </summary>
public static readonly AttachedProperty<IEnumerable<Exception>> ErrorsProperty =
AvaloniaProperty.RegisterAttached<DataValidationErrors, Control, IEnumerable<Exception>>("Errors");
public static readonly AttachedProperty<IEnumerable<object>> ErrorsProperty =
AvaloniaProperty.RegisterAttached<DataValidationErrors, Control, IEnumerable<object>>("Errors");
/// <summary>
/// Defines the DataValidationErrors.HasErrors attached property.
@ -76,7 +76,7 @@ namespace Avalonia.Controls
private static void ErrorsChanged(AvaloniaPropertyChangedEventArgs e)
{
var control = (Control)e.Sender;
var errors = (IEnumerable<Exception>)e.NewValue;
var errors = (IEnumerable<object>)e.NewValue;
var hasErrors = false;
if (errors != null && errors.Any())
@ -91,11 +91,11 @@ namespace Avalonia.Controls
classes.Set(":error", (bool)e.NewValue);
}
public static IEnumerable<Exception> GetErrors(Control control)
public static IEnumerable<object> GetErrors(Control control)
{
return control.GetValue(ErrorsProperty);
}
public static void SetErrors(Control control, IEnumerable<Exception> errors)
public static void SetErrors(Control control, IEnumerable<object> errors)
{
control.SetValue(ErrorsProperty, errors);
}
@ -112,14 +112,14 @@ namespace Avalonia.Controls
return control.GetValue(HasErrorsProperty);
}
private static IEnumerable<Exception> UnpackException(Exception exception)
private static IEnumerable<object> UnpackException(Exception exception)
{
if (exception != null)
{
var aggregate = exception as AggregateException;
var exceptions = aggregate == null ?
(IEnumerable<Exception>)new[] { exception } :
aggregate.InnerExceptions;
new[] { GetExceptionData(exception) } :
aggregate.InnerExceptions.Select(GetExceptionData).ToArray();
var filtered = exceptions.Where(x => !(x is BindingChainException)).ToList();
if (filtered.Count > 0)
@ -130,5 +130,13 @@ namespace Avalonia.Controls
return null;
}
private static object GetExceptionData(Exception exception)
{
if (exception is DataValidationException dataValidationException)
return dataValidationException.ErrorData;
return exception;
}
}
}

6
src/Avalonia.Controls/Generators/ItemContainerGenerator.cs

@ -128,6 +128,12 @@ namespace Avalonia.Controls.Generators
}
Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result));
if (toMove.Count > 0)
{
var containers = toMove.Select(x => x.Value).ToList();
Recycled?.Invoke(this, new ItemContainerEventArgs(containers[0].Index, containers));
}
}
return result;

2
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@ -141,7 +141,7 @@ namespace Avalonia.Controls.Presenters
/// <returns>True if the scroll offset was changed; otherwise false.</returns>
public bool BringDescendantIntoView(IVisual target, Rect targetRect)
{
if (Child == null)
if (Child?.IsEffectivelyVisible != true)
{
return false;
}

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

@ -979,13 +979,14 @@ namespace Avalonia.Controls.Primitives
}
var item = ElementAt(Items, index);
var itemChanged = !Equals(item, oldItem);
var added = -1;
HashSet<int> removed = null;
_selectedIndex = index;
_selectedItem = item;
if (oldIndex != index || _selection.HasMultiple)
if (oldIndex != index || itemChanged || _selection.HasMultiple)
{
if (clear)
{
@ -1022,7 +1023,7 @@ namespace Avalonia.Controls.Primitives
index);
}
if (!Equals(item, oldItem))
if (itemChanged)
{
RaisePropertyChanged(
SelectedItemProperty,

11
src/Avalonia.Controls/TextBox.cs

@ -59,6 +59,9 @@ namespace Avalonia.Controls
o => o.SelectionEnd,
(o, v) => o.SelectionEnd = v);
public static readonly StyledProperty<int> MaxLengthProperty =
AvaloniaProperty.Register<TextBox, int>(nameof(MaxLength), defaultValue: 0);
public static readonly DirectProperty<TextBox, string> TextProperty =
TextBlock.TextProperty.AddOwner<TextBox>(
o => o.Text,
@ -232,6 +235,12 @@ namespace Avalonia.Controls
}
}
public int MaxLength
{
get { return GetValue(MaxLengthProperty); }
set { SetValue(MaxLengthProperty, value); }
}
[Content]
public string Text
{
@ -345,7 +354,7 @@ namespace Avalonia.Controls
input = RemoveInvalidCharacters(input);
string text = Text ?? string.Empty;
int caretIndex = CaretIndex;
if (!string.IsNullOrEmpty(input))
if (!string.IsNullOrEmpty(input) && (MaxLength == 0 || input.Length + text.Length - (Math.Abs(SelectionStart - SelectionEnd)) <= MaxLength))
{
DeleteSelection();
caretIndex = CaretIndex;

2
src/Avalonia.Controls/TopLevel.cs

@ -269,8 +269,8 @@ namespace Avalonia.Controls
/// </summary>
protected virtual void HandleClosed()
{
(this as IInputRoot).MouseDevice?.TopLevelClosed(this);
PlatformImpl = null;
Closed?.Invoke(this, EventArgs.Empty);
Renderer?.Dispose();
Renderer = null;

4
src/Avalonia.Controls/TreeView.cs

@ -105,11 +105,13 @@ namespace Avalonia.Controls
get => _selectedItem;
set
{
var selectedItems = SelectedItems;
SetAndRaise(SelectedItemProperty, ref _selectedItem, value);
if (value != null)
{
if (SelectedItems.Count != 1 || SelectedItems[0] != value)
if (selectedItems.Count != 1 || selectedItems[0] != value)
{
_syncingSelectedItems = true;
SelectSingleItem(value);

23
src/Avalonia.Controls/TreeViewItem.cs

@ -42,6 +42,7 @@ namespace Avalonia.Controls
new FuncTemplate<IPanel>(() => new StackPanel());
private TreeView _treeView;
private IControl _header;
private bool _isExpanded;
private int _level;
@ -53,6 +54,7 @@ namespace Avalonia.Controls
SelectableMixin.Attach<TreeViewItem>(IsSelectedProperty);
FocusableProperty.OverrideDefaultValue<TreeViewItem>(true);
ItemsPanelProperty.OverrideDefaultValue<TreeViewItem>(DefaultPanel);
RequestBringIntoViewEvent.AddClassHandler<TreeViewItem>(x => x.OnRequestBringIntoView);
}
/// <summary>
@ -120,6 +122,21 @@ namespace Avalonia.Controls
ItemContainerGenerator.Clear();
}
protected virtual void OnRequestBringIntoView(RequestBringIntoViewEventArgs e)
{
if (e.TargetObject == this && _header != null)
{
var m = _header.TransformToVisual(this);
if (m.HasValue)
{
var bounds = new Rect(_header.Bounds.Size);
var rect = bounds.TransformToAABB(m.Value);
e.TargetRect = rect;
}
}
}
/// <inheritdoc/>
protected override void OnKeyDown(KeyEventArgs e)
{
@ -146,6 +163,12 @@ namespace Avalonia.Controls
// Don't call base.OnKeyDown - let events bubble up to containing TreeView.
}
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
_header = e.NameScope.Find<IControl>("PART_Header");
}
private static int CalculateDistanceFromLogicalParent<T>(ILogical logical, int @default = -1) where T : class
{
var result = 0;

7
src/Avalonia.Diagnostics/DevTools.xaml.cs

@ -28,6 +28,11 @@ namespace Avalonia
{
Diagnostics.DevTools.Attach(control, gesture);
}
public static void OpenDevTools(this TopLevel control)
{
Diagnostics.DevTools.OpenDevTools(control);
}
}
}
@ -73,7 +78,7 @@ namespace Avalonia.Diagnostics
RoutingStrategies.Tunnel);
}
private static void OpenDevTools(TopLevel control)
internal static void OpenDevTools(TopLevel control)
{
if (s_open.TryGetValue(control, out var devToolsWindow))
{

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

@ -1,7 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>

42
src/Avalonia.Input/Gestures.cs

@ -31,7 +31,7 @@ namespace Avalonia.Input
RoutedEvent.Register<ScrollGestureEventArgs>(
"ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures));
private static WeakReference s_lastPress;
private static WeakReference<IInteractive> s_lastPress = new WeakReference<IInteractive>(null);
static Gestures()
{
@ -39,6 +39,36 @@ namespace Avalonia.Input
InputElement.PointerReleasedEvent.RouteFinished.Subscribe(PointerReleased);
}
public static void AddTappedHandler(IInteractive element, EventHandler<RoutedEventArgs> handler)
{
element.AddHandler(TappedEvent, handler);
}
public static void AddDoubleTappedHandler(IInteractive element, EventHandler<RoutedEventArgs> handler)
{
element.AddHandler(DoubleTappedEvent, handler);
}
public static void AddRightTappedHandler(IInteractive element, EventHandler<RoutedEventArgs> handler)
{
element.AddHandler(RightTappedEvent, handler);
}
public static void RemoveTappedHandler(IInteractive element, EventHandler<RoutedEventArgs> handler)
{
element.RemoveHandler(TappedEvent, handler);
}
public static void RemoveDoubleTappedHandler(IInteractive element, EventHandler<RoutedEventArgs> handler)
{
element.RemoveHandler(DoubleTappedEvent, handler);
}
public static void RemoveRightTappedHandler(IInteractive element, EventHandler<RoutedEventArgs> handler)
{
element.RemoveHandler(RightTappedEvent, handler);
}
private static void PointerPressed(RoutedEventArgs ev)
{
if (ev.Route == RoutingStrategies.Bubble)
@ -47,11 +77,11 @@ namespace Avalonia.Input
if (e.ClickCount <= 1)
{
s_lastPress = new WeakReference(e.Source);
s_lastPress = new WeakReference<IInteractive>(e.Source);
}
else if (s_lastPress?.IsAlive == true && e.ClickCount == 2 && s_lastPress.Target == e.Source)
else if (s_lastPress != null && e.ClickCount == 2 && e.MouseButton != MouseButton.Right)
{
if (e.MouseButton != MouseButton.Right)
if (s_lastPress.TryGetTarget(out var target) && target == e.Source)
{
e.Source.RaiseEvent(new RoutedEventArgs(DoubleTappedEvent));
}
@ -65,10 +95,10 @@ namespace Avalonia.Input
{
var e = (PointerReleasedEventArgs)ev;
if (s_lastPress?.IsAlive == true && s_lastPress.Target == e.Source)
if (s_lastPress.TryGetTarget(out var target) && target == e.Source)
{
var et = e.MouseButton != MouseButton.Right ? TappedEvent : RightTappedEvent;
((IInteractive)s_lastPress.Target).RaiseEvent(new RoutedEventArgs(et));
e.Source.RaiseEvent(new RoutedEventArgs(et));
}
}
}

2
src/Avalonia.Input/IMouseDevice.cs

@ -16,6 +16,8 @@ namespace Avalonia.Input
[Obsolete("Use PointerEventArgs.GetPosition")]
PixelPoint Position { get; }
void TopLevelClosed(IInputRoot root);
void SceneInvalidated(IInputRoot root, Rect rect);
}
}

4
src/Avalonia.Input/InputExtensions.cs

@ -13,6 +13,8 @@ namespace Avalonia.Input
/// </summary>
public static class InputExtensions
{
private static readonly Func<IVisual, bool> s_hitTestDelegate = IsHitTestVisible;
/// <summary>
/// Returns the active input elements at a point on an <see cref="IInputElement"/>.
/// </summary>
@ -25,7 +27,7 @@ namespace Avalonia.Input
{
Contract.Requires<ArgumentNullException>(element != null);
return element.GetVisualsAt(p, IsHitTestVisible).Cast<IInputElement>();
return element.GetVisualsAt(p, s_hitTestDelegate).Cast<IInputElement>();
}
/// <summary>

5
src/Avalonia.Input/MouseDevice.cs

@ -86,6 +86,11 @@ namespace Avalonia.Input
ProcessRawEvent(margs);
}
public void TopLevelClosed(IInputRoot root)
{
ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None);
}
public void SceneInvalidated(IInputRoot root, Rect rect)
{
var clientPoint = root.PointToClient(Position);

4
src/Avalonia.Interactivity/EventSubscription.cs

@ -5,8 +5,12 @@ using System;
namespace Avalonia.Interactivity
{
internal delegate void HandlerInvokeSignature(Delegate baseHandler, object sender, RoutedEventArgs args);
internal class EventSubscription
{
public HandlerInvokeSignature InvokeAdapter { get; set; }
public Delegate Handler { get; set; }
public RoutingStrategies Routes { get; set; }

101
src/Avalonia.Interactivity/Interactive.cs

@ -4,8 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Layout;
using Avalonia.VisualTree;
@ -18,15 +16,14 @@ namespace Avalonia.Interactivity
{
private Dictionary<RoutedEvent, List<EventSubscription>> _eventHandlers;
private static readonly Dictionary<Type, HandlerInvokeSignature> s_invokeHandlerCache = new Dictionary<Type, HandlerInvokeSignature>();
/// <summary>
/// Gets the interactive parent of the object for bubbling and tunneling events.
/// </summary>
IInteractive IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive;
private Dictionary<RoutedEvent, List<EventSubscription>> EventHandlers
{
get { return _eventHandlers ?? (_eventHandlers = new Dictionary<RoutedEvent, List<EventSubscription>>()); }
}
private Dictionary<RoutedEvent, List<EventSubscription>> EventHandlers => _eventHandlers ?? (_eventHandlers = new Dictionary<RoutedEvent, List<EventSubscription>>());
/// <summary>
/// Adds a handler for the specified routed event.
@ -45,24 +42,14 @@ namespace Avalonia.Interactivity
Contract.Requires<ArgumentNullException>(routedEvent != null);
Contract.Requires<ArgumentNullException>(handler != null);
List<EventSubscription> subscriptions;
if (!EventHandlers.TryGetValue(routedEvent, out subscriptions))
{
subscriptions = new List<EventSubscription>();
EventHandlers.Add(routedEvent, subscriptions);
}
var sub = new EventSubscription
var subscription = new EventSubscription
{
Handler = handler,
Routes = routes,
AlsoIfHandled = handledEventsToo,
};
subscriptions.Add(sub);
return Disposable.Create(() => subscriptions.Remove(sub));
return AddEventSubscription(routedEvent, subscription);
}
/// <summary>
@ -80,7 +67,37 @@ namespace Avalonia.Interactivity
RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
bool handledEventsToo = false) where TEventArgs : RoutedEventArgs
{
return AddHandler(routedEvent, (Delegate)handler, routes, handledEventsToo);
Contract.Requires<ArgumentNullException>(routedEvent != null);
Contract.Requires<ArgumentNullException>(handler != null);
// EventHandler delegate is not covariant, this forces us to create small wrapper
// that will cast our type erased instance and invoke it.
Type eventArgsType = routedEvent.EventArgsType;
if (!s_invokeHandlerCache.TryGetValue(eventArgsType, out var invokeAdapter))
{
void InvokeAdapter(Delegate baseHandler, object sender, RoutedEventArgs args)
{
var typedHandler = (EventHandler<TEventArgs>)baseHandler;
var typedArgs = (TEventArgs)args;
typedHandler(sender, typedArgs);
}
invokeAdapter = InvokeAdapter;
s_invokeHandlerCache.Add(eventArgsType, invokeAdapter);
}
var subscription = new EventSubscription
{
InvokeAdapter = invokeAdapter,
Handler = handler,
Routes = routes,
AlsoIfHandled = handledEventsToo,
};
return AddEventSubscription(routedEvent, subscription);
}
/// <summary>
@ -196,10 +213,54 @@ namespace Avalonia.Interactivity
if (correctRoute && notFinished)
{
sub.Handler.DynamicInvoke(this, e);
if (sub.InvokeAdapter != null)
{
sub.InvokeAdapter(sub.Handler, this, e);
}
else
{
sub.Handler.DynamicInvoke(this, e);
}
}
}
}
}
private List<EventSubscription> GetEventSubscriptions(RoutedEvent routedEvent)
{
if (!EventHandlers.TryGetValue(routedEvent, out var subscriptions))
{
subscriptions = new List<EventSubscription>();
EventHandlers.Add(routedEvent, subscriptions);
}
return subscriptions;
}
private IDisposable AddEventSubscription(RoutedEvent routedEvent, EventSubscription subscription)
{
List<EventSubscription> subscriptions = GetEventSubscriptions(routedEvent);
subscriptions.Add(subscription);
return new UnsubscribeDisposable(subscriptions, subscription);
}
private sealed class UnsubscribeDisposable : IDisposable
{
private readonly List<EventSubscription> _subscriptions;
private readonly EventSubscription _subscription;
public UnsubscribeDisposable(List<EventSubscription> subscriptions, EventSubscription subscription)
{
_subscriptions = subscriptions;
_subscription = subscription;
}
public void Dispose()
{
_subscriptions.Remove(_subscription);
}
}
}
}

50
src/Avalonia.Interactivity/RoutedEvent.cs

@ -4,7 +4,6 @@
using System;
using System.Reactive.Subjects;
using System.Reflection;
using System.Runtime.ExceptionServices;
namespace Avalonia.Interactivity
{
@ -18,8 +17,8 @@ namespace Avalonia.Interactivity
public class RoutedEvent
{
private Subject<Tuple<object, RoutedEventArgs>> _raised = new Subject<Tuple<object, RoutedEventArgs>>();
private Subject<RoutedEventArgs> _routeFinished = new Subject<RoutedEventArgs>();
private readonly Subject<(object, RoutedEventArgs)> _raised = new Subject<(object, RoutedEventArgs)>();
private readonly Subject<RoutedEventArgs> _routeFinished = new Subject<RoutedEventArgs>();
public RoutedEvent(
string name,
@ -38,31 +37,15 @@ namespace Avalonia.Interactivity
RoutingStrategies = routingStrategies;
}
public Type EventArgsType
{
get;
private set;
}
public Type EventArgsType { get; }
public string Name
{
get;
private set;
}
public string Name { get; }
public Type OwnerType
{
get;
private set;
}
public Type OwnerType { get; }
public RoutingStrategies RoutingStrategies
{
get;
private set;
}
public RoutingStrategies RoutingStrategies { get; }
public IObservable<Tuple<object, RoutedEventArgs>> Raised => _raised;
public IObservable<(object, RoutedEventArgs)> Raised => _raised;
public IObservable<RoutedEventArgs> RouteFinished => _routeFinished;
public static RoutedEvent<TEventArgs> Register<TOwner, TEventArgs>(
@ -98,29 +81,20 @@ namespace Avalonia.Interactivity
{
return Raised.Subscribe(args =>
{
var sender = args.Item1;
var e = args.Item2;
(object sender, RoutedEventArgs e) = args;
if (targetType.GetTypeInfo().IsAssignableFrom(sender.GetType().GetTypeInfo()) &&
((e.Route == RoutingStrategies.Direct) || (e.Route & routes) != 0) &&
if (targetType.IsInstanceOfType(sender) &&
(e.Route == RoutingStrategies.Direct || (e.Route & routes) != 0) &&
(!e.Handled || handledEventsToo))
{
try
{
handler.DynamicInvoke(sender, e);
}
catch (TargetInvocationException ex)
{
// Unwrap the inner exception.
ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
}
handler(sender, e);
}
});
}
internal void InvokeRaised(object sender, RoutedEventArgs e)
{
_raised.OnNext(Tuple.Create(sender, e));
_raised.OnNext((sender, e));
}
internal void InvokeRouteFinished(RoutedEventArgs e)

23
src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs

@ -1,5 +1,8 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Disposables;
using System.Threading;
using Avalonia.Native.Interop;
using Avalonia.Rendering;
@ -13,11 +16,27 @@ namespace Avalonia.Native
{
_window = window;
}
public IDisposable TryLock()
{
if (_window.TryLock())
return Disposable.Create(() => _window.Unlock());
return new UnlockDisposable(_window);
return null;
}
private sealed class UnlockDisposable : IDisposable
{
private IAvnWindowBase _window;
public UnlockDisposable(IAvnWindowBase window)
{
_window = window;
}
public void Dispose()
{
Interlocked.Exchange(ref _window, null)?.Unlock();
}
}
}
}

19
src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs

@ -8,16 +8,16 @@ using Avalonia.Controls.Platform;
namespace Avalonia.Native
{
internal class WindowsMountedVolumeInfoListener : IDisposable
internal class MacOSMountedVolumeInfoListener : IDisposable
{
private readonly CompositeDisposable _disposables;
private readonly ObservableCollection<MountedVolumeInfo> _targetObs;
private bool _beenDisposed = false;
private ObservableCollection<MountedVolumeInfo> mountedDrives;
public WindowsMountedVolumeInfoListener(ObservableCollection<MountedVolumeInfo> mountedDrives)
public MacOSMountedVolumeInfoListener(ObservableCollection<MountedVolumeInfo> mountedDrives)
{
this.mountedDrives = mountedDrives;
_disposables = new CompositeDisposable();
var pollTimer = Observable.Interval(TimeSpan.FromSeconds(1))
@ -30,7 +30,8 @@ namespace Avalonia.Native
private void Poll(long _)
{
var mountVolInfos = Directory.GetDirectories("/Volumes")
var mountVolInfos = Directory.GetDirectories("/Volumes/")
.Where(p=> p != null)
.Select(p => new MountedVolumeInfo()
{
VolumeLabel = Path.GetFileName(p),
@ -38,15 +39,15 @@ namespace Avalonia.Native
VolumeSizeBytes = 0
})
.ToArray();
if (_targetObs.SequenceEqual(mountVolInfos))
if (mountedDrives.SequenceEqual(mountVolInfos))
return;
else
{
_targetObs.Clear();
mountedDrives.Clear();
foreach (var i in mountVolInfos)
_targetObs.Add(i);
mountedDrives.Add(i);
}
}
@ -72,7 +73,7 @@ namespace Avalonia.Native
public IDisposable Listen(ObservableCollection<MountedVolumeInfo> mountedDrives)
{
Contract.Requires<ArgumentNullException>(mountedDrives != null);
return new WindowsMountedVolumeInfoListener(mountedDrives);
return new MacOSMountedVolumeInfoListener(mountedDrives);
}
}
}

4
src/Avalonia.Themes.Default/TreeViewItem.xaml

@ -16,7 +16,9 @@
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
TemplatedControl.IsTemplateFocusTarget="True">
<Grid ColumnDefinitions="16, *"
<Grid Name="PART_Header"
ColumnDefinitions="16, Auto"
HorizontalAlignment="Left"
Margin="{TemplateBinding Level, Mode=OneWay, Converter={StaticResource LeftMarginConverter}}" >
<ToggleButton Name="expander"
Focusable="False"

4
src/Avalonia.Visuals/Rect.cs

@ -371,12 +371,12 @@ namespace Avalonia
/// <returns>The bounding box</returns>
public Rect TransformToAABB(Matrix matrix)
{
var points = new[]
ReadOnlySpan<Point> points = stackalloc Point[4]
{
TopLeft.Transform(matrix),
TopRight.Transform(matrix),
BottomRight.Transform(matrix),
BottomLeft.Transform(matrix),
BottomLeft.Transform(matrix)
};
var left = double.MaxValue;

30
src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs

@ -1,5 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Disposables;
using System.Threading;
namespace Avalonia.Rendering
@ -7,7 +9,7 @@ namespace Avalonia.Rendering
public class ManagedDeferredRendererLock : IDeferredRendererLock
{
private readonly object _lock = new object();
/// <summary>
/// Tries to lock the target surface or window
/// </summary>
@ -15,7 +17,7 @@ namespace Avalonia.Rendering
public IDisposable TryLock()
{
if (Monitor.TryEnter(_lock))
return Disposable.Create(() => Monitor.Exit(_lock));
return new UnlockDisposable(_lock);
return null;
}
@ -25,7 +27,27 @@ namespace Avalonia.Rendering
public IDisposable Lock()
{
Monitor.Enter(_lock);
return Disposable.Create(() => Monitor.Exit(_lock));
return new UnlockDisposable(_lock);
}
private sealed class UnlockDisposable : IDisposable
{
private object _lock;
public UnlockDisposable(object @lock)
{
_lock = @lock;
}
public void Dispose()
{
object @lock = Interlocked.Exchange(ref _lock, null);
if (@lock != null)
{
Monitor.Exit(@lock);
}
}
}
}
}

160
src/Avalonia.Visuals/Vector.cs

@ -65,9 +65,7 @@ namespace Avalonia
/// <param name="b">Second vector</param>
/// <returns>The dot product</returns>
public static double operator *(Vector a, Vector b)
{
return a.X * b.X + a.Y * b.Y;
}
=> Dot(a, b);
/// <summary>
/// Scales a vector.
@ -76,9 +74,7 @@ namespace Avalonia
/// <param name="scale">The scaling factor.</param>
/// <returns>The scaled vector.</returns>
public static Vector operator *(Vector vector, double scale)
{
return new Vector(vector._x * scale, vector._y * scale);
}
=> Multiply(vector, scale);
/// <summary>
/// Scales a vector.
@ -87,14 +83,17 @@ namespace Avalonia
/// <param name="scale">The divisor.</param>
/// <returns>The scaled vector.</returns>
public static Vector operator /(Vector vector, double scale)
{
return new Vector(vector._x / scale, vector._y / scale);
}
=> Divide(vector, scale);
/// <summary>
/// Length of the vector
/// </summary>
public double Length => Math.Sqrt(X * X + Y * Y);
public double Length => Math.Sqrt(SquaredLength);
/// <summary>
/// Squared Length of the vector
/// </summary>
public double SquaredLength => _x * _x + _y * _y;
/// <summary>
/// Negates a vector.
@ -102,9 +101,7 @@ namespace Avalonia
/// <param name="a">The vector.</param>
/// <returns>The negated vector.</returns>
public static Vector operator -(Vector a)
{
return new Vector(-a._x, -a._y);
}
=> Negate(a);
/// <summary>
/// Adds two vectors.
@ -113,9 +110,7 @@ namespace Avalonia
/// <param name="b">The second vector.</param>
/// <returns>A vector that is the result of the addition.</returns>
public static Vector operator +(Vector a, Vector b)
{
return new Vector(a._x + b._x, a._y + b._y);
}
=> Add(a, b);
/// <summary>
/// Subtracts two vectors.
@ -124,9 +119,7 @@ namespace Avalonia
/// <param name="b">The second vector.</param>
/// <returns>A vector that is the result of the subtraction.</returns>
public static Vector operator -(Vector a, Vector b)
{
return new Vector(a._x - b._x, a._y - b._y);
}
=> Subtract(a, b);
/// <summary>
/// Check if two vectors are equal (bitwise).
@ -155,7 +148,8 @@ namespace Avalonia
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(null, obj))
return false;
return obj is Vector vector && Equals(vector);
}
@ -206,5 +200,131 @@ namespace Avalonia
{
return new Vector(_x, y);
}
/// <summary>
/// Returns a normalized version of this vector.
/// </summary>
/// <returns>The normalized vector.</returns>
public Vector Normalize()
=> Normalize(this);
/// <summary>
/// Returns a negated version of this vector.
/// </summary>
/// <returns>The negated vector.</returns>
public Vector Negate()
=> Negate(this);
/// <summary>
/// Returns the dot product of two vectors.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The dot product.</returns>
public static double Dot(Vector a, Vector b)
=> a._x * b._x + a._y * b._y;
/// <summary>
/// Returns the cross product of two vectors.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The cross product.</returns>
public static double Cross(Vector a, Vector b)
=> a._x * b._y - a._y * b._x;
/// <summary>
/// Normalizes the given vector.
/// </summary>
/// <param name="vector">The vector</param>
/// <returns>The normalized vector.</returns>
public static Vector Normalize(Vector vector)
=> Divide(vector, vector.Length);
/// <summary>
/// Divides the first vector by the second.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The scaled vector.</returns>
public static Vector Divide(Vector a, Vector b)
=> new Vector(a._x / b._x, a._y / b._y);
/// <summary>
/// Divides the vector by the given scalar.
/// </summary>
/// <param name="vector">The vector</param>
/// <param name="scalar">The scalar value</param>
/// <returns>The scaled vector.</returns>
public static Vector Divide(Vector vector, double scalar)
=> new Vector(vector._x / scalar, vector._y / scalar);
/// <summary>
/// Multiplies the first vector by the second.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The scaled vector.</returns>
public static Vector Multiply(Vector a, Vector b)
=> new Vector(a._x * b._x, a._y * b._y);
/// <summary>
/// Multiplies the vector by the given scalar.
/// </summary>
/// <param name="vector">The vector</param>
/// <param name="scalar">The scalar value</param>
/// <returns>The scaled vector.</returns>
public static Vector Multiply(Vector vector, double scalar)
=> new Vector(vector._x * scalar, vector._y * scalar);
/// <summary>
/// Adds the second to the first vector
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The summed vector.</returns>
public static Vector Add(Vector a, Vector b)
=> new Vector(a._x + b._x, a._y + b._y);
/// <summary>
/// Subtracts the second from the first vector
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The difference vector.</returns>
public static Vector Subtract(Vector a, Vector b)
=> new Vector(a._x - b._x, a._y - b._y);
/// <summary>
/// Negates the vector
/// </summary>
/// <param name="vector">The vector to negate.</param>
/// <returns>The scaled vector.</returns>
public static Vector Negate(Vector vector)
=> new Vector(-vector._x, -vector._y);
/// <summary>
/// Returnes the vector (0.0, 0.0)
/// </summary>
public static Vector Zero
=> new Vector(0, 0);
/// <summary>
/// Returnes the vector (1.0, 1.0)
/// </summary>
public static Vector One
=> new Vector(1, 1);
/// <summary>
/// Returnes the vector (1.0, 0.0)
/// </summary>
public static Vector UnitX
=> new Vector(1, 0);
/// <summary>
/// Returnes the vector (0.0, 1.0)
/// </summary>
public static Vector UnitY
=> new Vector(0, 1);
}
}

57
src/Avalonia.X11/X11KeyTransform.cs

@ -52,16 +52,6 @@ namespace Avalonia.X11
{X11Key.Delete, Key.Delete},
{X11Key.KP_Delete, Key.Delete},
{X11Key.Help, Key.Help},
{X11Key.XK_0, Key.D0},
{X11Key.XK_1, Key.D1},
{X11Key.XK_2, Key.D2},
{X11Key.XK_3, Key.D3},
{X11Key.XK_4, Key.D4},
{X11Key.XK_5, Key.D5},
{X11Key.XK_6, Key.D6},
{X11Key.XK_7, Key.D7},
{X11Key.XK_8, Key.D8},
{X11Key.XK_9, Key.D9},
{X11Key.A, Key.A},
{X11Key.B, Key.B},
{X11Key.C, Key.C},
@ -114,8 +104,8 @@ namespace Avalonia.X11
{X11Key.x, Key.X},
{X11Key.y, Key.Y},
{X11Key.z, Key.Z},
//{ X11Key.?, Key.LWin }
//{ X11Key.?, Key.RWin }
{X11Key.Meta_L, Key.LWin },
{X11Key.Meta_R, Key.RWin },
{X11Key.Menu, Key.Apps},
//{ X11Key.?, Key.Sleep }
{X11Key.KP_0, Key.NumPad0},
@ -185,20 +175,51 @@ namespace Avalonia.X11
//{ X11Key.?, Key.SelectMedia }
//{ X11Key.?, Key.LaunchApplication1 }
//{ X11Key.?, Key.LaunchApplication2 }
{X11Key.semicolon, Key.OemSemicolon},
{X11Key.minus, Key.OemMinus},
{X11Key.underscore, Key.OemMinus},
{X11Key.plus, Key.OemPlus},
{X11Key.equal, Key.OemPlus},
{X11Key.bracketleft, Key.OemOpenBrackets},
{X11Key.braceleft, Key.OemOpenBrackets},
{X11Key.bracketright, Key.OemCloseBrackets},
{X11Key.braceright, Key.OemCloseBrackets},
{X11Key.backslash, Key.OemPipe},
{X11Key.bar, Key.OemPipe},
{X11Key.semicolon, Key.OemSemicolon},
{X11Key.colon, Key.OemSemicolon},
{X11Key.apostrophe, Key.OemQuotes},
{X11Key.quotedbl, Key.OemQuotes},
{X11Key.comma, Key.OemComma},
{X11Key.minus, Key.OemMinus},
{X11Key.less, Key.OemComma},
{X11Key.period, Key.OemPeriod},
{X11Key.greater, Key.OemPeriod},
{X11Key.slash, Key.Oem2},
{X11Key.question, Key.Oem2},
{X11Key.grave, Key.OemTilde},
{X11Key.asciitilde, Key.OemTilde},
{X11Key.XK_1, Key.D1},
{X11Key.exclam, Key.D1},
{X11Key.XK_2, Key.D2},
{X11Key.at, Key.D2},
{X11Key.XK_3, Key.D3},
{X11Key.numbersign, Key.D3},
{X11Key.XK_4, Key.D4},
{X11Key.dollar, Key.D4},
{X11Key.XK_5, Key.D5},
{X11Key.percent, Key.D5},
{X11Key.XK_6, Key.D6},
{X11Key.asciicircum, Key.D6},
{X11Key.XK_7, Key.D7},
{X11Key.ampersand, Key.D7},
{X11Key.XK_8, Key.D8},
{X11Key.asterisk, Key.D8},
{X11Key.XK_9, Key.D9},
{X11Key.parenleft, Key.D9},
{X11Key.XK_0, Key.D0},
{X11Key.parenright, Key.D0},
//{ X11Key.?, Key.AbntC1 }
//{ X11Key.?, Key.AbntC2 }
{X11Key.bracketleft, Key.OemOpenBrackets},
{X11Key.backslash, Key.OemPipe},
{X11Key.bracketright, Key.OemCloseBrackets},
{X11Key.apostrophe, Key.OemQuotes},
//{ X11Key.?, Key.Oem8 }
//{ X11Key.?, Key.Oem102 }
//{ X11Key.?, Key.ImeProcessed }

2
src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs

@ -164,6 +164,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public bool IsStatic => true;
public string Name { get; protected set; }
public IXamlIlType DeclaringType { get; }
public IXamlIlMethod MakeGenericMethod(IReadOnlyList<IXamlIlType> typeArguments)
=> throw new System.NotSupportedException();
public bool Equals(IXamlIlMethod other) =>

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

@ -1 +1 @@
Subproject commit c2ec091f79fb4e1eea629bc823c9c24da7050022
Subproject commit c7155c5f6c1a5153ee2d8cd78e5d1524dd6744cf

12
src/Markup/Avalonia.Markup/Data/MultiBinding.cs

@ -76,7 +76,12 @@ namespace Avalonia.Data
}
var children = Bindings.Select(x => x.Initiate(target, null));
var input = children.Select(x => x.Observable).CombineLatest().Select(x => ConvertValue(x, targetType, converter));
var input = children.Select(x => x.Observable)
.CombineLatest()
.Select(x => ConvertValue(x, targetType, converter))
.Where(x => x != BindingOperations.DoNothing);
var mode = Mode == BindingMode.Default ?
targetProperty?.GetMetadata(target.GetType()).DefaultBindingMode : Mode;
@ -97,11 +102,6 @@ namespace Avalonia.Data
var culture = CultureInfo.CurrentCulture;
var converted = converter.Convert(values, targetType, ConverterParameter, culture);
if (converted == BindingOperations.DoNothing)
{
return converted;
}
if (converted == AvaloniaProperty.UnsetValue)
{
converted = FallbackValue;

2
src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs

@ -19,7 +19,7 @@ namespace Avalonia.Markup.Parsers.Nodes
public override string Description => $"#{_name}";
protected override void StartListeningCore(WeakReference reference)
protected override void StartListeningCore(WeakReference<object> reference)
{
if (_nameScope.TryGetTarget(out var scope))
_subscription = NameScopeLocator.Track(scope, _name).Subscribe(ValueChanged);

4
src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs

@ -31,9 +31,9 @@ namespace Avalonia.Markup.Parsers.Nodes
}
}
protected override void StartListeningCore(WeakReference reference)
protected override void StartListeningCore(WeakReference<object> reference)
{
if (reference.Target is ILogical logical)
if (reference.TryGetTarget(out object target) && target is ILogical logical)
{
_subscription = ControlLocator.Track(logical, _level, _ancestorType).Subscribe(ValueChanged);
}

26
src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs

@ -26,9 +26,11 @@ namespace Avalonia.Markup.Parsers.Nodes
protected override bool SetTargetValueCore(object value, BindingPriority priority)
{
var typeInfo = Target.Target.GetType().GetTypeInfo();
var list = Target.Target as IList;
var dictionary = Target.Target as IDictionary;
Target.TryGetTarget(out object target);
var typeInfo = target.GetType().GetTypeInfo();
var list = target as IList;
var dictionary = target as IDictionary;
var indexerProperty = GetIndexer(typeInfo);
var indexerParameters = indexerProperty?.GetIndexParameters();
@ -53,7 +55,7 @@ namespace Avalonia.Markup.Parsers.Nodes
// Try special cases where we can validate indices
if (typeInfo.IsArray)
{
return SetValueInArray((Array)Target.Target, intArgs, value);
return SetValueInArray((Array)target, intArgs, value);
}
else if (Arguments.Count == 1)
{
@ -83,14 +85,14 @@ namespace Avalonia.Markup.Parsers.Nodes
else
{
// Fallback to unchecked access
indexerProperty.SetValue(Target.Target, value, convertedObjectArray);
indexerProperty.SetValue(target, value, convertedObjectArray);
return true;
}
}
else
{
// Fallback to unchecked access
indexerProperty.SetValue(Target.Target, value, convertedObjectArray);
indexerProperty.SetValue(target, value, convertedObjectArray);
return true;
}
}
@ -98,7 +100,7 @@ namespace Avalonia.Markup.Parsers.Nodes
// multidimensional indexer, which doesn't take the same number of arguments
else if (typeInfo.IsArray)
{
SetValueInArray((Array)Target.Target, value);
SetValueInArray((Array)target, value);
return true;
}
return false;
@ -126,7 +128,15 @@ namespace Avalonia.Markup.Parsers.Nodes
public IList<string> Arguments { get; }
public override Type PropertyType => GetIndexer(Target.Target.GetType().GetTypeInfo())?.PropertyType;
public override Type PropertyType
{
get
{
Target.TryGetTarget(out object target);
return GetIndexer(target.GetType().GetTypeInfo())?.PropertyType;
}
}
protected override object GetValue(object target)
{

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

@ -10,8 +10,7 @@ namespace Avalonia.Win32
{
internal class WindowsMountedVolumeInfoListener : IDisposable
{
private readonly CompositeDisposable _disposables;
private readonly ObservableCollection<MountedVolumeInfo> _targetObs = new ObservableCollection<MountedVolumeInfo>();
private readonly CompositeDisposable _disposables;
private bool _beenDisposed = false;
private ObservableCollection<MountedVolumeInfo> mountedDrives;
@ -41,14 +40,14 @@ namespace Avalonia.Win32
})
.ToArray();
if (_targetObs.SequenceEqual(mountVolInfos))
if (mountedDrives.SequenceEqual(mountVolInfos))
return;
else
{
_targetObs.Clear();
mountedDrives.Clear();
foreach (var i in mountVolInfos)
_targetObs.Add(i);
mountedDrives.Add(i);
}
}

17
tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs

@ -148,6 +148,23 @@ namespace Avalonia.Base.UnitTests.Collections
Assert.True(raised);
}
[Fact]
public void AddRange_Items_Should_Raise_Correct_CollectionChanged()
{
var target = new AvaloniaList<object>();
var eventItems = new List<object>();
target.CollectionChanged += (sender, args) =>
{
eventItems.AddRange(args.NewItems.Cast<object>());
};
target.AddRange(Enumerable.Range(0,10).Select(i => new object()));
Assert.Equal(eventItems, target);
}
[Fact]
public void Replacing_Item_Should_Raise_CollectionChanged()
{

4
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs

@ -100,7 +100,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
// Value is first signalled without an error as validation hasn't been updated.
new BindingNotification(-5),
new BindingNotification(new Exception("Must be positive"), BindingErrorType.DataValidationError, -5),
new BindingNotification(new DataValidationException("Must be positive"), BindingErrorType.DataValidationError, -5),
// Exception is thrown by trying to set value to "foo".
new BindingNotification(
@ -108,7 +108,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
BindingErrorType.DataValidationError),
// Value is set then validation is updated.
new BindingNotification(new Exception("Must be positive"), BindingErrorType.DataValidationError, 5),
new BindingNotification(new DataValidationException("Must be positive"), BindingErrorType.DataValidationError, 5),
new BindingNotification(5),
}, result);

2
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs

@ -574,7 +574,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
var source = new Class1 { Foo = "foo" };
var target = new PropertyAccessorNode("Foo", false);
Assert.NotNull(target);
target.Target = new WeakReference(source);
target.Target = new WeakReference<object>(source);
target.Subscribe(_ => { });
target.Unsubscribe();
target.Unsubscribe();

14
tests/Avalonia.Base.UnitTests/Data/Core/Plugins/DataAnnotationsValidationPluginTests.cs

@ -20,7 +20,7 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins
var target = new DataAnnotationsValidationPlugin();
var data = new Data();
Assert.True(target.Match(new WeakReference(data), nameof(Data.Between5And10)));
Assert.True(target.Match(new WeakReference<object>(data), nameof(Data.Between5And10)));
}
[Fact]
@ -29,7 +29,7 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins
var target = new DataAnnotationsValidationPlugin();
var data = new Data();
Assert.True(target.Match(new WeakReference(data), nameof(Data.PhoneNumber)));
Assert.True(target.Match(new WeakReference<object>(data), nameof(Data.PhoneNumber)));
}
[Fact]
@ -38,7 +38,7 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins
var target = new DataAnnotationsValidationPlugin();
var data = new Data();
Assert.False(target.Match(new WeakReference(data), nameof(Data.Unvalidated)));
Assert.False(target.Match(new WeakReference<object>(data), nameof(Data.Unvalidated)));
}
[Fact]
@ -47,8 +47,8 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins
var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
var validatorPlugin = new DataAnnotationsValidationPlugin();
var data = new Data();
var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Between5And10));
var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Between5And10), accessor);
var accessor = inpcAccessorPlugin.Start(new WeakReference<object>(data), nameof(data.Between5And10));
var validator = validatorPlugin.Start(new WeakReference<object>(data), nameof(data.Between5And10), accessor);
var result = new List<object>();
var errmsg = new RangeAttribute(5, 10).FormatErrorMessage(nameof(Data.Between5And10));
@ -79,8 +79,8 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins
var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
var validatorPlugin = new DataAnnotationsValidationPlugin();
var data = new Data();
var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.PhoneNumber));
var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.PhoneNumber), accessor);
var accessor = inpcAccessorPlugin.Start(new WeakReference<object>(data), nameof(data.PhoneNumber));
var validator = validatorPlugin.Start(new WeakReference<object>(data), nameof(data.PhoneNumber), accessor);
var result = new List<object>();
validator.Subscribe(x => result.Add(x));

4
tests/Avalonia.Base.UnitTests/Data/Core/Plugins/ExceptionValidationPluginTests.cs

@ -19,8 +19,8 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins
var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
var validatorPlugin = new ExceptionValidationPlugin();
var data = new Data();
var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive));
var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor);
var accessor = inpcAccessorPlugin.Start(new WeakReference<object>(data), nameof(data.MustBePositive));
var validator = validatorPlugin.Start(new WeakReference<object>(data), nameof(data.MustBePositive), accessor);
var result = new List<object>();
validator.Subscribe(x => result.Add(x));

12
tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs

@ -18,8 +18,8 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins
var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
var validatorPlugin = new IndeiValidationPlugin();
var data = new Data { Maximum = 5 };
var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Value));
var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor);
var accessor = inpcAccessorPlugin.Start(new WeakReference<object>(data), nameof(data.Value));
var validator = validatorPlugin.Start(new WeakReference<object>(data), nameof(data.Value), accessor);
var result = new List<object>();
validator.Subscribe(x => result.Add(x));
@ -37,13 +37,13 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins
new BindingNotification(6),
// Then the ErrorsChanged event is fired.
new BindingNotification(new Exception("Must be less than Maximum"), BindingErrorType.DataValidationError, 6),
new BindingNotification(new DataValidationException("Must be less than Maximum"), BindingErrorType.DataValidationError, 6),
// Maximum is changed to 10 so value is now valid.
new BindingNotification(6),
// And Maximum is changed back to 5.
new BindingNotification(new Exception("Must be less than Maximum"), BindingErrorType.DataValidationError, 6),
new BindingNotification(new DataValidationException("Must be less than Maximum"), BindingErrorType.DataValidationError, 6),
}, result);
}
@ -53,8 +53,8 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins
var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
var validatorPlugin = new IndeiValidationPlugin();
var data = new Data { Maximum = 5 };
var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Value));
var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor);
var accessor = inpcAccessorPlugin.Start(new WeakReference<object>(data), nameof(data.Value));
var validator = validatorPlugin.Start(new WeakReference<object>(data), nameof(data.Value), accessor);
Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
validator.Subscribe(_ => { });

15
tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs

@ -0,0 +1,15 @@
using Avalonia.Controls;
using BenchmarkDotNet.Attributes;
namespace Avalonia.Benchmarks.Base
{
[MemoryDiagnoser]
public class AvaloniaObjectInitializationBenchmark
{
[Benchmark(OperationsPerInvoke = 1000)]
public Button InitializeButton()
{
return new Button();
}
}
}

19
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs

@ -102,6 +102,25 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Null(target.SelectedItem);
}
[Fact]
public void Removing_Selected_First_Item_Should_Select_Next_Item()
{
var items = new AvaloniaList<string>(new[] { "foo", "bar" });
var target = new TestSelector
{
Items = items,
Template = Template(),
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
items.RemoveAt(0);
Assert.Equal(0, target.SelectedIndex);
Assert.Equal("bar", target.SelectedItem);
Assert.Equal(new[] { ":selected" }, target.Presenter.Panel.Children[0].Classes);
}
private FuncControlTemplate Template()
{
return new FuncControlTemplate<SelectingItemsControl>((control, scope) =>

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

@ -460,6 +460,24 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Text_Box_MaxLength_Work_Properly()
{
using (UnitTestApplication.Start(Services))
{
var target = new TextBox
{
Template = CreateTemplate(),
Text = "abc",
MaxLength = 3,
};
RaiseKeyEvent(target, Key.D, KeyModifiers.None);
Assert.Equal("abc", target.Text);
}
}
private static TestServices FocusServices => TestServices.MockThreadingInterface.With(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(),

2
tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs

@ -58,7 +58,7 @@ namespace Avalonia.Controls.UnitTests
Assert.Null(DataValidationErrors.GetErrors(target));
target.Text = "20";
IEnumerable<Exception> errors = DataValidationErrors.GetErrors(target);
IEnumerable<object> errors = DataValidationErrors.GetErrors(target);
Assert.Single(errors);
Assert.IsType<InvalidOperationException>(errors.Single());
target.Text = "1";

18
tests/Avalonia.Controls.UnitTests/TopLevelTests.cs

@ -224,6 +224,24 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Close_Should_Notify_MouseDevice()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var impl = new Mock<ITopLevelImpl>();
var mouseDevice = new Mock<IMouseDevice>();
impl.SetupAllProperties();
impl.Setup(x => x.MouseDevice).Returns(mouseDevice.Object);
var target = new TestTopLevel(impl.Object);
impl.Object.Closed();
mouseDevice.Verify(x => x.TopLevelClosed(target));
}
}
private FuncControlTemplate<TestTopLevel> CreateTemplate()
{
return new FuncControlTemplate<TestTopLevel>((x, scope) =>

132
tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs

@ -60,6 +60,80 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal("baz", source.Foo);
}
[Fact]
public void TwoWay_Binding_Should_Be_Set_Up_GC_Collect()
{
var source = new WeakRefSource { Foo = null };
var target = new TestControl { DataContext = source };
var binding = new Binding
{
Path = "Foo",
Mode = BindingMode.TwoWay
};
target.Bind(TestControl.ValueProperty, binding);
var ref1 = AssignValue(target, "ref1");
Assert.Equal(ref1.Target, source.Foo);
GC.Collect();
GC.WaitForPendingFinalizers();
var ref2 = AssignValue(target, "ref2");
GC.Collect();
GC.WaitForPendingFinalizers();
target.Value = null;
Assert.Null(source.Foo);
}
private class DummyObject : ICloneable
{
private readonly string _val;
public DummyObject(string val)
{
_val = val;
}
public object Clone()
{
return new DummyObject(_val);
}
protected bool Equals(DummyObject other)
{
return string.Equals(_val, other._val);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((DummyObject) obj);
}
public override int GetHashCode()
{
return (_val != null ? _val.GetHashCode() : 0);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private WeakReference AssignValue(TestControl source, string val)
{
var obj = new DummyObject(val);
source.Value = obj;
return new WeakReference(obj);
}
[Fact]
public void OneTime_Binding_Should_Be_Set_Up()
{
@ -568,12 +642,70 @@ namespace Avalonia.Markup.UnitTests.Data
}
}
public class WeakRefSource : INotifyPropertyChanged
{
private WeakReference<object> _foo;
public object Foo
{
get
{
if (_foo == null)
{
return null;
}
if (_foo.TryGetTarget(out object target))
{
if (target is ICloneable cloneable)
{
return cloneable.Clone();
}
return target;
}
return null;
}
set
{
_foo = new WeakReference<object>(value);
RaisePropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged([CallerMemberName] string prop = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
}
}
private class OldDataContextViewModel
{
public int Foo { get; set; } = 1;
public int Bar { get; set; } = 2;
}
private class TestControl : Control
{
public static readonly DirectProperty<TestControl, object> ValueProperty =
AvaloniaProperty.RegisterDirect<TestControl, object>(
nameof(Value),
o => o.Value,
(o, v) => o.Value = v);
private object _value;
public object Value
{
get => _value;
set => SetAndRaise(ValueProperty, ref _value, value);
}
}
private class OldDataContextTest : Control
{
public static readonly StyledProperty<int> FooProperty =

76
tests/Avalonia.Markup.Xaml.UnitTests/Converters/MultiValueConverterTests.cs

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Converters
{
public class MultiValueConverterTests : XamlTestBase
{
[Fact]
public void MultiValueConverter_Special_Values_Work()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:c='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Converters;assembly=Avalonia.Markup.Xaml.UnitTests'>
<TextBlock Name='textBlock'>
<TextBlock.Text>
<MultiBinding Converter='{x:Static c:TestMultiValueConverter.Instance}' FallbackValue='bar'>
<Binding Path='Item1' />
<Binding Path='Item2' />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var textBlock = window.FindControl<TextBlock>("textBlock");
window.ApplyTemplate();
window.DataContext = Tuple.Create(2, 2);
Assert.Equal("foo", textBlock.Text);
window.DataContext = Tuple.Create(-3, 3);
Assert.Equal("foo", textBlock.Text);
window.DataContext = Tuple.Create(0, 2);
Assert.Equal("bar", textBlock.Text);
}
}
}
public class TestMultiValueConverter : IMultiValueConverter
{
public static readonly TestMultiValueConverter Instance = new TestMultiValueConverter();
public object Convert(IList<object> values, Type targetType, object parameter, CultureInfo culture)
{
if (values[0] is int i && values[1] is int j)
{
var p = i * j;
if (p > 0)
{
return "foo";
}
if (p == 0)
{
return AvaloniaProperty.UnsetValue;
}
return BindingOperations.DoNothing;
}
return "(default)";
}
}
}

20
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs

@ -309,8 +309,12 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
}
}
[Fact]
public void Binding_To_TextBlock_Text_With_StringConverter_Works()
[Theory,
InlineData(@"Hello \{0\}"),
InlineData(@"'Hello {0}'"),
InlineData(@"Hello {0}")]
public void Binding_To_TextBlock_Text_With_StringConverter_Works(string fmt)
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
@ -318,8 +322,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<TextBlock Name='textBlock' Text='{Binding Foo, StringFormat=Hello \{0\}}'/>
</Window>";
<TextBlock Name='textBlock' Text=""{Binding Foo, StringFormat=" + fmt + @"}""/>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var textBlock = window.FindControl<TextBlock>("textBlock");
@ -331,8 +335,10 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
}
}
[Fact(Skip="Issue #2592")]
public void MultiBinding_To_TextBlock_Text_With_StringConverter_Works()
[Theory,
InlineData("{}{0} {1}!"),
InlineData(@"\{0\} \{1\}!")]
public void MultiBinding_To_TextBlock_Text_With_StringConverter_Works(string fmt)
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
@ -342,7 +348,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<TextBlock Name='textBlock'>
<TextBlock.Text>
<MultiBinding StringFormat='\{0\} \{1\}!'>
<MultiBinding StringFormat='" + fmt + @"'>
<Binding Path='Greeting1'/>
<Binding Path='Greeting2'/>
</MultiBinding>

46
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/EventTests.cs

@ -1,7 +1,6 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
@ -12,45 +11,56 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
public class EventTests : XamlTestBase
{
[Fact]
public void Event_Is_Attached()
public void Event_Is_Assigned()
{
var xaml = @"<Button xmlns='https://github.com/avaloniaui' Click='OnClick'/>";
var loader = new AvaloniaXamlLoader();
var target = new MyButton();
loader.Load(xaml, rootInstance: target);
RaiseClick(target);
Assert.True(target.Clicked);
target.RaiseEvent(new RoutedEventArgs
{
RoutedEvent = Button.ClickEvent,
});
Assert.True(target.WasClicked);
}
[Fact]
public void Exception_Is_Thrown_If_Event_Not_Found()
public void Attached_Event_Is_Assigned()
{
var xaml = @"<Button xmlns='https://github.com/avaloniaui' Click='NotFound'/>";
var xaml = @"<Button xmlns='https://github.com/avaloniaui' Gestures.Tapped='OnTapped'/>";
var loader = new AvaloniaXamlLoader();
var target = new MyButton();
XamlTestHelpers.AssertThrowsXamlException(() => loader.Load(xaml, rootInstance: target));
}
loader.Load(xaml, rootInstance: target);
private void RaiseClick(MyButton target)
{
target.RaiseEvent(new KeyEventArgs
target.RaiseEvent(new RoutedEventArgs
{
RoutedEvent = Button.KeyDownEvent,
Key = Key.Enter,
RoutedEvent = Gestures.TappedEvent,
});
Assert.True(target.WasTapped);
}
[Fact]
public void Exception_Is_Thrown_If_Event_Not_Found()
{
var xaml = @"<Button xmlns='https://github.com/avaloniaui' Click='NotFound'/>";
var loader = new AvaloniaXamlLoader();
var target = new MyButton();
XamlTestHelpers.AssertThrowsXamlException(() => loader.Load(xaml, rootInstance: target));
}
public class MyButton : Button
{
public bool Clicked { get; private set; }
public bool WasClicked { get; private set; }
public bool WasTapped { get; private set; }
public void OnClick(object sender, RoutedEventArgs e)
{
Clicked = true;
}
public void OnClick(object sender, RoutedEventArgs e) => WasClicked = true;
public void OnTapped(object sender, RoutedEventArgs e) => WasTapped = true;
}
}
}

112
tests/Avalonia.Visuals.UnitTests/VectorTests.cs

@ -0,0 +1,112 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Xunit;
using Avalonia;
using System;
namespace Avalonia.Visuals.UnitTests
{
public class VectorTests
{
[Fact]
public void Length_Should_Return_Correct_Length_Of_Vector()
{
var vector = new Vector(2, 4);
var length = Math.Sqrt(2 * 2 + 4 * 4);
Assert.Equal(length, vector.Length);
}
[Fact]
public void Length_Squared_Should_Return_Correct_Length_Of_Vector()
{
var vectorA = new Vector(2, 4);
var squaredLengthA = 2 * 2 + 4 * 4;
Assert.Equal(squaredLengthA, vectorA.SquaredLength);
}
[Fact]
public void Normalize_Should_Return_Normalized_Vector()
{
// the length of a normalized vector must be 1
var vectorA = new Vector(13, 84);
var vectorB = new Vector(-34, 345);
var vectorC = new Vector(-34, -84);
Assert.Equal(1.0, vectorA.Normalize().Length);
Assert.Equal(1.0, vectorB.Normalize().Length);
Assert.Equal(1.0, vectorC.Normalize().Length);
}
[Fact]
public void Negate_Should_Return_Negated_Vector()
{
var vector = new Vector(2, 4);
var negated = new Vector(-2, -4);
Assert.Equal(negated, vector.Negate());
}
[Fact]
public void Dot_Should_Return_Correct_Value()
{
var a = new Vector(-6, 8.0);
var b = new Vector(5, 12.0);
Assert.Equal(66.0, Vector.Dot(a, b));
}
[Fact]
public void Cross_Should_Return_Correct_Value()
{
var a = new Vector(-6, 8.0);
var b = new Vector(5, 12.0);
Assert.Equal(-112.0, Vector.Cross(a, b));
}
[Fact]
public void Divied_By_Vector_Should_Return_Correct_Value()
{
var a = new Vector(10, 2);
var b = new Vector(5, 2);
var expected = new Vector(2, 1);
Assert.Equal(expected, Vector.Divide(a, b));
}
[Fact]
public void Divied_Should_Return_Correct_Value()
{
var vector = new Vector(10, 2);
var expected = new Vector(5, 1);
Assert.Equal(expected, Vector.Divide(vector, 2));
}
[Fact]
public void Multiply_By_Vector_Should_Return_Correct_Value()
{
var a = new Vector(10, 2);
var b = new Vector(2, 2);
var expected = new Vector(20, 4);
Assert.Equal(expected, Vector.Multiply(a, b));
}
[Fact]
public void Multiply_Should_Return_Correct_Value()
{
var vector = new Vector(10, 2);
var expected = new Vector(20, 4);
Assert.Equal(expected, Vector.Multiply(vector, 2));
}
}
}
Loading…
Cancel
Save