Browse Source

Merge branch 'master' into textbox-maxlength-respect-text-from-clipboard

pull/5630/head
Steven Kirk 5 years ago
committed by GitHub
parent
commit
0eacfc78df
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      native/Avalonia.Native/src/OSX/AvnString.h
  2. 15
      native/Avalonia.Native/src/OSX/AvnString.mm
  3. 28
      native/Avalonia.Native/src/OSX/app.mm
  4. 2
      native/Avalonia.Native/src/OSX/clipboard.mm
  5. 2
      native/Avalonia.Native/src/OSX/common.h
  6. 4
      native/Avalonia.Native/src/OSX/main.mm
  7. 7
      native/Avalonia.Native/src/OSX/window.mm
  8. 40
      src/Avalonia.Base/AvaloniaObject.cs
  9. 57
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  10. 30
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  11. 8
      src/Avalonia.Base/PropertyStore/IBatchUpdate.cs
  12. 9
      src/Avalonia.Base/PropertyStore/IValue.cs
  13. 16
      src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
  14. 121
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  15. 17
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  16. 264
      src/Avalonia.Base/ValueStore.cs
  17. 56
      src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs
  18. 3
      src/Avalonia.Controls/ApiCompatBaseline.txt
  19. 11
      src/Avalonia.Controls/Application.cs
  20. 14
      src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
  21. 2
      src/Avalonia.Controls/Control.cs
  22. 77
      src/Avalonia.Controls/DefinitionBase.cs
  23. 17
      src/Avalonia.Controls/Notifications/NotificationCard.cs
  24. 7
      src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs
  25. 14
      src/Avalonia.Controls/UrlOpenedEventArgs.cs
  26. 14
      src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs
  27. 7
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  28. 8
      src/Avalonia.Native/avn.idl
  29. 4
      src/Avalonia.Styling/IStyledElement.cs
  30. 28
      src/Avalonia.Styling/StyledElement.cs
  31. 18
      src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs
  32. 2
      src/Avalonia.Styling/Styling/PropertySetterInstance.cs
  33. 128
      src/Avalonia.Styling/Styling/PropertySetterLazyInstance.cs
  34. 24
      src/Avalonia.Styling/Styling/Setter.cs
  35. 14
      src/Windows/Avalonia.Win32/WindowImpl.cs
  36. 1
      tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj
  37. 494
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs
  38. 1
      tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
  39. 74
      tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs
  40. 1
      tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj
  41. 96
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs
  42. 1
      tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj
  43. 282
      tests/Avalonia.Styling.UnitTests/StyleTests.cs
  44. 2
      tests/Avalonia.UnitTests/UnitTestApplication.cs

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

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

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

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

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

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

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

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

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

@ -31,7 +31,7 @@ extern NSMenuItem* GetAppMenuItem ();
extern void SetAutoGenerateDefaultAppMenuItems (bool enabled); extern void SetAutoGenerateDefaultAppMenuItems (bool enabled);
extern bool GetAutoGenerateDefaultAppMenuItems (); extern bool GetAutoGenerateDefaultAppMenuItems ();
extern void InitializeAvnApp(); extern void InitializeAvnApp(IAvnApplicationEvents* events);
extern NSApplicationActivationPolicy AvnDesiredActivationPolicy; extern NSApplicationActivationPolicy AvnDesiredActivationPolicy;
extern NSPoint ToNSPoint (AvnPoint p); extern NSPoint ToNSPoint (AvnPoint p);
extern AvnPoint ToAvnPoint (NSPoint p); extern AvnPoint ToAvnPoint (NSPoint p);

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

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

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

@ -1877,7 +1877,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
for(int i = 0; i < numWindows; i++) for(int i = 0; i < numWindows; i++)
{ {
[[windows objectAtIndex:i] performClose:nil]; auto window = (AvnWindow*)[windows objectAtIndex:i];
if([window parentWindow] == nullptr) // Avalonia will handle the child windows.
{
[window performClose:nil];
}
} }
} }

40
src/Avalonia.Base/AvaloniaObject.cs

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

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

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

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

@ -1,4 +1,5 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data; using Avalonia.Data;
#nullable enable #nullable enable
@ -17,7 +18,7 @@ namespace Avalonia.PropertyStore
public ConstantValueEntry( public ConstantValueEntry(
StyledPropertyBase<T> property, StyledPropertyBase<T> property,
T value, [AllowNull] T value,
BindingPriority priority, BindingPriority priority,
IValueSink sink) IValueSink sink)
{ {
@ -28,7 +29,7 @@ namespace Avalonia.PropertyStore
} }
public StyledPropertyBase<T> Property { get; } public StyledPropertyBase<T> Property { get; }
public BindingPriority Priority { get; } public BindingPriority Priority { get; private set; }
Optional<object> IValue.GetValue() => _value.ToObject(); Optional<object> IValue.GetValue() => _value.ToObject();
public Optional<T> GetValue(BindingPriority maxPriority = BindingPriority.Animation) public Optional<T> GetValue(BindingPriority maxPriority = BindingPriority.Animation)
@ -36,7 +37,30 @@ namespace Avalonia.PropertyStore
return Priority >= maxPriority ? _value : Optional<T>.Empty; return Priority >= maxPriority ? _value : Optional<T>.Empty;
} }
public void Dispose() => _sink.Completed(Property, this, _value); public void Dispose()
{
var oldValue = _value;
_value = default;
Priority = BindingPriority.Unset;
_sink.Completed(Property, this, oldValue);
}
public void Reparent(IValueSink sink) => _sink = sink; public void Reparent(IValueSink sink) => _sink = sink;
public void Start() { }
public void RaiseValueChanged(
IValueSink sink,
IAvaloniaObject owner,
AvaloniaProperty property,
Optional<object> oldValue,
Optional<object> newValue)
{
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
newValue.GetValueOrDefault<T>(),
Priority));
}
} }
} }

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

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

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

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

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

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

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

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

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

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

264
src/Avalonia.Base/ValueStore.cs

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

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

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

3
src/Avalonia.Controls/ApiCompatBaseline.txt

@ -1,6 +1,7 @@
Compat issues with assembly Avalonia.Controls: Compat issues with assembly Avalonia.Controls:
MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
Total Issues: 4 Total Issues: 5

11
src/Avalonia.Controls/Application.cs

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

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

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

2
src/Avalonia.Controls/Control.cs

@ -20,7 +20,7 @@ namespace Avalonia.Controls
/// ///
/// - A <see cref="Tag"/> property to allow user-defined data to be attached to the control. /// - A <see cref="Tag"/> property to allow user-defined data to be attached to the control.
/// </remarks> /// </remarks>
public class Control : InputElement, IControl, INamed, ISupportInitialize, IVisualBrushInitialize, ISetterValue public class Control : InputElement, IControl, INamed, IVisualBrushInitialize, ISetterValue
{ {
/// <summary> /// <summary>
/// Defines the <see cref="FocusAdorner"/> property. /// Defines the <see cref="FocusAdorner"/> property.

77
src/Avalonia.Controls/DefinitionBase.cs

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

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

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

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

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

14
src/Avalonia.Controls/UrlOpenedEventArgs.cs

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

14
src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs

@ -0,0 +1,14 @@
using System;
using Avalonia.Native.Interop;
using Avalonia.Platform;
namespace Avalonia.Native
{
internal class AvaloniaNativeApplicationPlatform : CallbackBase, IAvnApplicationEvents
{
void IAvnApplicationEvents.FilesOpened(IAvnStringArray urls)
{
((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(urls.ToStringArray());
}
}
}

7
src/Avalonia.Native/AvaloniaNativePlatform.cs

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

8
src/Avalonia.Native/avn.idl

@ -403,7 +403,7 @@ enum AvnExtendClientAreaChromeHints
[uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)] [uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)]
interface IAvaloniaNativeFactory : IUnknown interface IAvaloniaNativeFactory : IUnknown
{ {
HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator); HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator, IAvnApplicationEvents* appCb);
IAvnMacOptions* GetMacOptions(); IAvnMacOptions* GetMacOptions();
HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnWindow** ppv); HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnWindow** ppv);
HRESULT CreatePopup(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnPopup** ppv); HRESULT CreatePopup(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnPopup** ppv);
@ -728,3 +728,9 @@ interface IAvnNativeControlHostTopLevelAttachment : IUnknown
void HideWithSize(float width, float height); void HideWithSize(float width, float height);
void ReleaseChild(); void ReleaseChild();
} }
[uuid(6575b5af-f27a-4609-866c-f1f014c20f79)]
interface IAvnApplicationEvents : IUnknown
{
void FilesOpened (IAvnStringArray* urls);
}

4
src/Avalonia.Styling/IStyledElement.cs

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

28
src/Avalonia.Styling/StyledElement.cs

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

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

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

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

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

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

@ -0,0 +1,128 @@
using System;
using Avalonia.Data;
using Avalonia.Reactive;
#nullable enable
namespace Avalonia.Styling
{
/// <summary>
/// A <see cref="Setter"/> which has been instanced on a control and whose value is lazily
/// evaluated.
/// </summary>
/// <typeparam name="T">The target property type.</typeparam>
internal class PropertySetterLazyInstance<T> : SingleSubscriberObservableBase<BindingValue<T>>,
ISetterInstance
{
private readonly IStyleable _target;
private readonly StyledPropertyBase<T>? _styledProperty;
private readonly DirectPropertyBase<T>? _directProperty;
private readonly Func<T> _valueFactory;
private BindingValue<T> _value;
private IDisposable? _subscription;
private bool _isActive;
public PropertySetterLazyInstance(
IStyleable target,
StyledPropertyBase<T> property,
Func<T> valueFactory)
{
_target = target;
_styledProperty = property;
_valueFactory = valueFactory;
}
public PropertySetterLazyInstance(
IStyleable target,
DirectPropertyBase<T> property,
Func<T> valueFactory)
{
_target = target;
_directProperty = property;
_valueFactory = valueFactory;
}
public void Start(bool hasActivator)
{
_isActive = !hasActivator;
if (_styledProperty is object)
{
var priority = hasActivator ? BindingPriority.StyleTrigger : BindingPriority.Style;
_subscription = _target.Bind(_styledProperty, this, priority);
}
else
{
_subscription = _target.Bind(_directProperty, this);
}
}
public void Activate()
{
if (!_isActive)
{
_isActive = true;
PublishNext();
}
}
public void Deactivate()
{
if (_isActive)
{
_isActive = false;
PublishNext();
}
}
public override void Dispose()
{
if (_subscription is object)
{
var sub = _subscription;
_subscription = null;
sub.Dispose();
}
else if (_isActive)
{
if (_styledProperty is object)
{
_target.ClearValue(_styledProperty);
}
else
{
_target.ClearValue(_directProperty);
}
}
base.Dispose();
}
protected override void Subscribed() => PublishNext();
protected override void Unsubscribed() { }
private T GetValue()
{
if (_value.HasValue)
{
return _value.Value;
}
_value = _valueFactory();
return _value.Value;
}
private void PublishNext()
{
if (_isActive)
{
GetValue();
PublishNext(_value);
}
else
{
PublishNext(default);
}
}
}
}

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

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

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

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

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

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

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

@ -0,0 +1,494 @@
using System;
using System.Collections.Generic;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Text;
using Avalonia.Data;
using Xunit;
namespace Avalonia.Base.UnitTests
{
public class AvaloniaObjectTests_BatchUpdate
{
[Fact]
public void SetValue_Should_Not_Raise_Property_Changes_During_Batch_Update()
{
var target = new TestClass();
var raised = new List<string>();
target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x));
target.BeginBatchUpdate();
target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue);
Assert.Empty(raised);
}
[Fact]
public void Binding_Should_Not_Raise_Property_Changes_During_Batch_Update()
{
var target = new TestClass();
var observable = new TestObservable<string>("foo");
var raised = new List<string>();
target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x));
target.BeginBatchUpdate();
target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
Assert.Empty(raised);
}
[Fact]
public void Binding_Completion_Should_Not_Raise_Property_Changes_During_Batch_Update()
{
var target = new TestClass();
var observable = new TestObservable<string>("foo");
var raised = new List<string>();
target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x));
target.BeginBatchUpdate();
observable.OnCompleted();
Assert.Empty(raised);
}
[Fact]
public void SetValue_Change_Should_Be_Raised_After_Batch_Update_1()
{
var target = new TestClass();
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue);
target.EndBatchUpdate();
Assert.Equal(1, raised.Count);
Assert.Equal("foo", target.Foo);
Assert.Null(raised[0].OldValue);
Assert.Equal("foo", raised[0].NewValue);
}
[Fact]
public void SetValue_Change_Should_Be_Raised_After_Batch_Update_2()
{
var target = new TestClass();
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue);
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.SetValue(TestClass.FooProperty, "bar", BindingPriority.LocalValue);
target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue);
target.EndBatchUpdate();
Assert.Equal(1, raised.Count);
Assert.Equal("baz", target.Foo);
}
[Fact]
public void SetValue_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update()
{
var target = new TestClass();
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue);
target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue);
target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue);
target.EndBatchUpdate();
Assert.Equal(2, raised.Count);
Assert.Equal(TestClass.BarProperty, raised[0].Property);
Assert.Equal(TestClass.FooProperty, raised[1].Property);
Assert.Equal("baz", target.Foo);
Assert.Equal("bar", target.Bar);
}
[Fact]
public void SetValue_And_Binding_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update_1()
{
var target = new TestClass();
var observable = new TestObservable<string>("baz");
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue);
target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue);
target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
target.EndBatchUpdate();
Assert.Equal(2, raised.Count);
Assert.Equal(TestClass.BarProperty, raised[0].Property);
Assert.Equal(TestClass.FooProperty, raised[1].Property);
Assert.Equal("baz", target.Foo);
Assert.Equal("bar", target.Bar);
}
[Fact]
public void SetValue_And_Binding_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update_2()
{
var target = new TestClass();
var observable = new TestObservable<string>("foo");
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue);
target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue);
target.EndBatchUpdate();
Assert.Equal(2, raised.Count);
Assert.Equal(TestClass.BarProperty, raised[0].Property);
Assert.Equal(TestClass.FooProperty, raised[1].Property);
Assert.Equal("baz", target.Foo);
Assert.Equal("bar", target.Bar);
}
[Fact]
public void SetValue_And_Binding_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update_3()
{
var target = new TestClass();
var observable1 = new TestObservable<string>("foo");
var observable2 = new TestObservable<string>("qux");
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue);
target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue);
target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue);
target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue);
target.EndBatchUpdate();
Assert.Equal(2, raised.Count);
Assert.Equal(TestClass.BarProperty, raised[0].Property);
Assert.Equal(TestClass.FooProperty, raised[1].Property);
Assert.Equal("baz", target.Foo);
Assert.Equal("bar", target.Bar);
}
[Fact]
public void Binding_Change_Should_Be_Raised_After_Batch_Update_1()
{
var target = new TestClass();
var observable = new TestObservable<string>("foo");
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
target.EndBatchUpdate();
Assert.Equal(1, raised.Count);
Assert.Equal("foo", target.Foo);
Assert.Null(raised[0].OldValue);
Assert.Equal("foo", raised[0].NewValue);
}
[Fact]
public void Binding_Change_Should_Be_Raised_After_Batch_Update_2()
{
var target = new TestClass();
var observable1 = new TestObservable<string>("bar");
var observable2 = new TestObservable<string>("baz");
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue);
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue);
target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue);
target.EndBatchUpdate();
Assert.Equal(1, raised.Count);
Assert.Equal("baz", target.Foo);
Assert.Equal("foo", raised[0].OldValue);
Assert.Equal("baz", raised[0].NewValue);
}
[Fact]
public void Binding_Completion_Should_Be_Raised_After_Batch_Update()
{
var target = new TestClass();
var observable = new TestObservable<string>("foo");
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
observable.OnCompleted();
target.EndBatchUpdate();
Assert.Equal(1, raised.Count);
Assert.Null(target.Foo);
Assert.Equal("foo", raised[0].OldValue);
Assert.Null(raised[0].NewValue);
Assert.Equal(BindingPriority.Unset, raised[0].Priority);
}
[Fact]
public void ClearValue_Change_Should_Be_Raised_After_Batch_Update_1()
{
var target = new TestClass();
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.Foo = "foo";
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.ClearValue(TestClass.FooProperty);
target.EndBatchUpdate();
Assert.Equal(1, raised.Count);
Assert.Null(target.Foo);
Assert.Equal("foo", raised[0].OldValue);
Assert.Null(raised[0].NewValue);
Assert.Equal(BindingPriority.Unset, raised[0].Priority);
}
[Fact]
public void Bindings_Should_Be_Subscribed_Before_Batch_Update()
{
var target = new TestClass();
var observable1 = new TestObservable<string>("foo");
var observable2 = new TestObservable<string>("bar");
target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue);
target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue);
Assert.Equal(1, observable1.SubscribeCount);
Assert.Equal(1, observable2.SubscribeCount);
}
[Fact]
public void Non_Active_Binding_Should_Not_Be_Subscribed_Before_Batch_Update()
{
var target = new TestClass();
var observable1 = new TestObservable<string>("foo");
var observable2 = new TestObservable<string>("bar");
target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue);
target.Bind(TestClass.FooProperty, observable2, BindingPriority.Style);
Assert.Equal(1, observable1.SubscribeCount);
Assert.Equal(0, observable2.SubscribeCount);
}
[Fact]
public void LocalValue_Bindings_Should_Be_Subscribed_During_Batch_Update()
{
var target = new TestClass();
var observable1 = new TestObservable<string>("foo");
var observable2 = new TestObservable<string>("bar");
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
// We need to subscribe to LocalValue bindings even if we've got a batch operation
// in progress because otherwise we don't know whether the binding or a subsequent
// SetValue with local priority will win. Notifications however shouldn't be sent.
target.BeginBatchUpdate();
target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue);
target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue);
Assert.Equal(1, observable1.SubscribeCount);
Assert.Equal(1, observable2.SubscribeCount);
Assert.Empty(raised);
}
[Fact]
public void Style_Bindings_Should_Not_Be_Subscribed_During_Batch_Update()
{
var target = new TestClass();
var observable1 = new TestObservable<string>("foo");
var observable2 = new TestObservable<string>("bar");
target.BeginBatchUpdate();
target.Bind(TestClass.FooProperty, observable1, BindingPriority.Style);
target.Bind(TestClass.FooProperty, observable2, BindingPriority.StyleTrigger);
Assert.Equal(0, observable1.SubscribeCount);
Assert.Equal(0, observable2.SubscribeCount);
}
[Fact]
public void Active_Style_Binding_Should_Be_Subscribed_After_Batch_Uppdate_1()
{
var target = new TestClass();
var observable1 = new TestObservable<string>("foo");
var observable2 = new TestObservable<string>("bar");
target.BeginBatchUpdate();
target.Bind(TestClass.FooProperty, observable1, BindingPriority.Style);
target.Bind(TestClass.FooProperty, observable2, BindingPriority.Style);
target.EndBatchUpdate();
Assert.Equal(0, observable1.SubscribeCount);
Assert.Equal(1, observable2.SubscribeCount);
}
[Fact]
public void Active_Style_Binding_Should_Be_Subscribed_After_Batch_Uppdate_2()
{
var target = new TestClass();
var observable1 = new TestObservable<string>("foo");
var observable2 = new TestObservable<string>("bar");
target.BeginBatchUpdate();
target.Bind(TestClass.FooProperty, observable1, BindingPriority.StyleTrigger);
target.Bind(TestClass.FooProperty, observable2, BindingPriority.Style);
target.EndBatchUpdate();
Assert.Equal(1, observable1.SubscribeCount);
Assert.Equal(0, observable2.SubscribeCount);
}
[Fact]
public void Change_Can_Be_Triggered_By_Ending_Batch_Update_1()
{
var target = new TestClass();
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.Foo = "foo";
target.PropertyChanged += (s, e) =>
{
if (e.Property == TestClass.FooProperty && (string)e.NewValue == "foo")
target.Bar = "bar";
};
target.EndBatchUpdate();
Assert.Equal("foo", target.Foo);
Assert.Equal("bar", target.Bar);
Assert.Equal(2, raised.Count);
Assert.Equal(TestClass.FooProperty, raised[0].Property);
Assert.Equal(TestClass.BarProperty, raised[1].Property);
}
[Fact]
public void Change_Can_Be_Triggered_By_Ending_Batch_Update_2()
{
var target = new TestClass();
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.Foo = "foo";
target.Bar = "baz";
target.PropertyChanged += (s, e) =>
{
if (e.Property == TestClass.FooProperty && (string)e.NewValue == "foo")
target.Bar = "bar";
};
target.EndBatchUpdate();
Assert.Equal("foo", target.Foo);
Assert.Equal("bar", target.Bar);
Assert.Equal(2, raised.Count);
}
[Fact]
public void Batch_Update_Can_Be_Triggered_By_Ending_Batch_Update()
{
var target = new TestClass();
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.Foo = "foo";
target.Bar = "baz";
// Simulates the following scenario:
// - A control is added to the logical tree
// - A batch update is started to apply styles
// - Ending the batch update triggers something which removes the control from the logical tree
// - A new batch update is started to detach styles
target.PropertyChanged += (s, e) =>
{
if (e.Property == TestClass.FooProperty && (string)e.NewValue == "foo")
{
target.BeginBatchUpdate();
target.ClearValue(TestClass.FooProperty);
target.ClearValue(TestClass.BarProperty);
target.EndBatchUpdate();
}
};
target.EndBatchUpdate();
Assert.Null(target.Foo);
Assert.Null(target.Bar);
Assert.Equal(2, raised.Count);
Assert.Equal(TestClass.FooProperty, raised[0].Property);
Assert.Null(raised[0].OldValue);
Assert.Equal("foo", raised[0].NewValue);
Assert.Equal(TestClass.FooProperty, raised[1].Property);
Assert.Equal("foo", raised[1].OldValue);
Assert.Null(raised[1].NewValue);
}
public class TestClass : AvaloniaObject
{
public static readonly StyledProperty<string> FooProperty =
AvaloniaProperty.Register<TestClass, string>(nameof(Foo));
public static readonly StyledProperty<string> BarProperty =
AvaloniaProperty.Register<TestClass, string>(nameof(Bar));
public string Foo
{
get => GetValue(FooProperty);
set => SetValue(FooProperty, value);
}
public string Bar
{
get => GetValue(BarProperty);
set => SetValue(BarProperty, value);
}
}
public class TestObservable<T> : ObservableBase<BindingValue<T>>
{
private readonly T _value;
private IObserver<BindingValue<T>> _observer;
public TestObservable(T value) => _value = value;
public int SubscribeCount { get; private set; }
public void OnCompleted() => _observer.OnCompleted();
public void OnError(Exception e) => _observer.OnError(e);
protected override IDisposable SubscribeCore(IObserver<BindingValue<T>> observer)
{
++SubscribeCount;
_observer = observer;
observer.OnNext(_value);
return Disposable.Empty;
}
}
}
}

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

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

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

@ -0,0 +1,74 @@
using System;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.Styling;
using Avalonia.Platform;
using Avalonia.Shared.PlatformSupport;
using Avalonia.Styling;
using Avalonia.UnitTests;
using BenchmarkDotNet.Attributes;
using Moq;
namespace Avalonia.Benchmarks.Themes
{
[MemoryDiagnoser]
public class FluentBenchmark
{
private readonly IDisposable _app;
private readonly TestRoot _root;
public FluentBenchmark()
{
_app = CreateApp();
_root = new TestRoot(true, null)
{
Renderer = new NullRenderer()
};
_root.LayoutManager.ExecuteInitialLayoutPass();
}
public void Dispose()
{
_app.Dispose();
}
[Benchmark]
public void RepeatButton()
{
var button = new RepeatButton();
_root.Child = button;
_root.LayoutManager.ExecuteLayoutPass();
}
private static IDisposable CreateApp()
{
var services = new TestServices(
assetLoader: new AssetLoader(),
globalClock: new MockGlobalClock(),
platform: new AppBuilder().RuntimePlatform,
renderInterface: new MockPlatformRenderInterface(),
standardCursorFactory: Mock.Of<ICursorFactory>(),
styler: new Styler(),
theme: () => LoadFluentTheme(),
threadingInterface: new NullThreadingPlatform(),
fontManagerImpl: new MockFontManagerImpl(),
textShaperImpl: new MockTextShaperImpl(),
windowingPlatform: new MockWindowingPlatform());
return UnitTestApplication.Start(services);
}
private static Styles LoadFluentTheme()
{
AssetLoader.RegisterResUriParsers();
return new Styles
{
new Avalonia.Themes.Fluent.FluentTheme(new Uri("avares://Avalonia.Benchmarks"))
{
}
};
}
}
}

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

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

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

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

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

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

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

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

2
tests/Avalonia.UnitTests/UnitTestApplication.cs

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

Loading…
Cancel
Save