diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 603308ef9a..accad63faa 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -34,9 +34,17 @@ jobs: pool: vmImage: 'macOS-10.14' steps: - - task: DotNetCoreInstaller@0 + - task: UseDotNet@2 + displayName: 'Use .NET Core SDK 3.0.x' inputs: - version: '2.1.403' + packageType: sdk + version: 3.0.x + + - task: UseDotNet@2 + displayName: 'Use .NET Core Runtime 2.1.x' + inputs: + packageType: runtime + version: 2.1.x - task: CmdLine@2 displayName: 'Install Mono 5.18' diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 958729e2e8..52a9591c94 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -1,6 +1,4 @@ -using System; using Avalonia; -using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; @@ -19,7 +17,7 @@ namespace ControlCatalog desktopLifetime.MainWindow = new MainWindow(); else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) singleViewLifetime.MainView = new MainView(); - + base.OnFrameworkInitializationCompleted(); } } diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index d718f5917c..01daeafc3a 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -173,12 +173,20 @@ namespace Avalonia Contract.Requires(type != null); Contract.Requires(name != null); - if (name.Contains('.')) + if (name.Contains(".")) { throw new InvalidOperationException("Attached properties not supported."); } - return GetRegistered(type).FirstOrDefault(x => x.Name == name); + foreach (AvaloniaProperty x in GetRegistered(type)) + { + if (x.Name == name) + { + return x; + } + } + + return null; } /// diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs index 44b47329ac..256de2f902 100644 --- a/src/Avalonia.Base/Data/BindingOperations.cs +++ b/src/Avalonia.Base/Data/BindingOperations.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; @@ -56,22 +55,34 @@ namespace Avalonia.Data if (source != null) { + // Perf: Avoid allocating closure in the outer scope. + var targetCopy = target; + var propertyCopy = property; + var bindingCopy = binding; + return source .Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue) .Take(1) - .Subscribe(x => target.SetValue(property, x, binding.Priority)); + .Subscribe(x => targetCopy.SetValue(propertyCopy, x, bindingCopy.Priority)); } else { target.SetValue(property, binding.Value, binding.Priority); return Disposable.Empty; } + case BindingMode.OneWayToSource: + { + // Perf: Avoid allocating closure in the outer scope. + var bindingCopy = binding; + return Observable.CombineLatest( binding.Observable, target.GetObservable(property), (_, v) => v) - .Subscribe(x => binding.Subject.OnNext(x)); + .Subscribe(x => bindingCopy.Subject.OnNext(x)); + } + default: throw new ArgumentException("Invalid binding mode."); } diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index 986e2cf012..9eec5d6b2b 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -21,6 +21,7 @@ namespace Avalonia.Data.Core private readonly ExpressionObserver _inner; private readonly Type _targetType; private readonly object _fallbackValue; + private readonly object _targetNullValue; private readonly BindingPriority _priority; InnerListener _innerListener; WeakReference _value; @@ -51,7 +52,7 @@ namespace Avalonia.Data.Core IValueConverter converter, object converterParameter = null, BindingPriority priority = BindingPriority.LocalValue) - : this(inner, targetType, AvaloniaProperty.UnsetValue, converter, converterParameter, priority) + : this(inner, targetType, AvaloniaProperty.UnsetValue, AvaloniaProperty.UnsetValue, converter, converterParameter, priority) { } @@ -63,6 +64,9 @@ namespace Avalonia.Data.Core /// /// The value to use when the binding is unable to produce a value. /// + /// + /// The value to use when the binding result is null. + /// /// The value converter to use. /// /// A parameter to pass to . @@ -72,6 +76,7 @@ namespace Avalonia.Data.Core ExpressionObserver inner, Type targetType, object fallbackValue, + object targetNullValue, IValueConverter converter, object converterParameter = null, BindingPriority priority = BindingPriority.LocalValue) @@ -85,6 +90,7 @@ namespace Avalonia.Data.Core Converter = converter; ConverterParameter = converterParameter; _fallbackValue = fallbackValue; + _targetNullValue = targetNullValue; _priority = priority; } @@ -196,6 +202,11 @@ namespace Avalonia.Data.Core /// private object ConvertValue(object value) { + if (value == null && _targetNullValue != AvaloniaProperty.UnsetValue) + { + return _targetNullValue; + } + if (value == BindingOperations.DoNothing) { return value; diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 7060fd3451..91a27be634 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -21,7 +21,7 @@ namespace Avalonia.Data.Core /// An ordered collection of property accessor plugins that can be used to customize /// the reading and subscription of property values on a type. /// - public static readonly IList PropertyAccessors = + public static readonly List PropertyAccessors = new List { new AvaloniaPropertyAccessorPlugin(), @@ -33,7 +33,7 @@ namespace Avalonia.Data.Core /// An ordered collection of validation checker plugins that can be used to customize /// the validation of view model and model data. /// - public static readonly IList DataValidators = + public static readonly List DataValidators = new List { new DataAnnotationsValidationPlugin(), @@ -45,7 +45,7 @@ namespace Avalonia.Data.Core /// An ordered collection of stream plugins that can be used to customize the behavior /// of the '^' stream binding operator. /// - public static readonly IList StreamHandlers = + public static readonly List StreamHandlers = new List { new TaskStreamPlugin(), diff --git a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs index ab4a109cc2..fa72235a89 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive.Linq; +using System.Runtime.ExceptionServices; namespace Avalonia.Data.Core.Plugins { @@ -76,7 +76,7 @@ namespace Avalonia.Data.Core.Plugins return false; } - private class Accessor : PropertyAccessorBase + private class Accessor : PropertyAccessorBase, IObserver { private readonly WeakReference _reference; private readonly AvaloniaProperty _property; @@ -117,7 +117,7 @@ namespace Avalonia.Data.Core.Plugins protected override void SubscribeCore() { - _subscription = Instance?.GetObservable(_property).Subscribe(PublishValue); + _subscription = Instance?.GetObservable(_property).Subscribe(this); } protected override void UnsubscribeCore() @@ -125,6 +125,20 @@ namespace Avalonia.Data.Core.Plugins _subscription?.Dispose(); _subscription = null; } + + void IObserver.OnCompleted() + { + } + + void IObserver.OnError(Exception error) + { + ExceptionDispatchInfo.Capture(error).Throw(); + } + + void IObserver.OnNext(object value) + { + PublishValue(value); + } } } } diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index 70f53b8b88..45b7207413 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -2,8 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Linq; -using System.Reactive.Linq; using Avalonia.Data.Core.Plugins; namespace Avalonia.Data.Core @@ -41,7 +39,17 @@ namespace Avalonia.Data.Core { reference.TryGetTarget(out object target); - var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(target, PropertyName)); + IPropertyAccessorPlugin plugin = null; + + foreach (IPropertyAccessorPlugin x in ExpressionObserver.PropertyAccessors) + { + if (x.Match(target, PropertyName)) + { + plugin = x; + break; + } + } + var accessor = plugin?.Start(reference, PropertyName); if (_enableValidation && Next == null) diff --git a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs index 41009e4cd3..f5052e5858 100644 --- a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs +++ b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs @@ -116,20 +116,33 @@ namespace Avalonia.Reactive { if (Volatile.Read(ref _observers) != null) { - IObserver[] observers; - + IObserver[] observers = null; + IObserver singleObserver = null; lock (this) { if (_observers == null) { return; } - observers = _observers.ToArray(); + if (_observers.Count == 1) + { + singleObserver = _observers[0]; + } + else + { + observers = _observers.ToArray(); + } } - - foreach (var observer in observers) + if (singleObserver != null) { - observer.OnNext(value); + singleObserver.OnNext(value); + } + else + { + foreach (var observer in observers) + { + observer.OnNext(value); + } } } } diff --git a/src/Avalonia.Base/Utilities/IdentifierParser.cs b/src/Avalonia.Base/Utilities/IdentifierParser.cs index a57a2b7ba5..973b0aa641 100644 --- a/src/Avalonia.Base/Utilities/IdentifierParser.cs +++ b/src/Avalonia.Base/Utilities/IdentifierParser.cs @@ -15,7 +15,7 @@ namespace Avalonia.Utilities { if (IsValidIdentifierStart(r.Peek)) { - return r.TakeWhile(IsValidIdentifierChar); + return r.TakeWhile(c => IsValidIdentifierChar(c)); } else { diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 59c6c47ed9..9158ac7038 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -32,7 +32,7 @@ namespace Avalonia /// method. /// - Tracks the lifetime of the application. /// - public class Application : AvaloniaObject, IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode + public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode { /// /// The application-global data templates. @@ -45,6 +45,12 @@ namespace Avalonia private Styles _styles; private IResourceDictionary _resources; + /// + /// Defines the property. + /// + public static readonly StyledProperty DataContextProperty = + StyledElement.DataContextProperty.AddOwner(); + /// public event EventHandler ResourcesChanged; @@ -56,6 +62,19 @@ namespace Avalonia Name = "Avalonia Application"; } + /// + /// Gets or sets the Applications's data context. + /// + /// + /// The data context property specifies the default object that will + /// be used for data binding. + /// + public object DataContext + { + get { return GetValue(DataContextProperty); } + set { SetValue(DataContextProperty, value); } + } + /// /// Gets the current instance of the class. /// diff --git a/src/Avalonia.Controls/Primitives/RangeBase.cs b/src/Avalonia.Controls/Primitives/RangeBase.cs index f1ee7c0e1a..baa51f92ec 100644 --- a/src/Avalonia.Controls/Primitives/RangeBase.cs +++ b/src/Avalonia.Controls/Primitives/RangeBase.cs @@ -75,7 +75,10 @@ namespace Avalonia.Controls.Primitives set { - ValidateDouble(value, "Minimum"); + if (!ValidateDouble(value)) + { + return; + } if (IsInitialized) { @@ -102,7 +105,10 @@ namespace Avalonia.Controls.Primitives set { - ValidateDouble(value, "Maximum"); + if (!ValidateDouble(value)) + { + return; + } if (IsInitialized) { @@ -129,7 +135,10 @@ namespace Avalonia.Controls.Primitives set { - ValidateDouble(value, "Value"); + if (!ValidateDouble(value)) + { + return; + } if (IsInitialized) { @@ -164,16 +173,12 @@ namespace Avalonia.Controls.Primitives } /// - /// Throws an exception if the double value is NaN or Inf. + /// Checks if the double value is not inifinity nor NaN. /// /// The value. - /// The name of the property being set. - private static void ValidateDouble(double value, string property) + private static bool ValidateDouble(double value) { - if (double.IsInfinity(value) || double.IsNaN(value)) - { - throw new ArgumentException($"{value} is not a valid value for {property}."); - } + return !double.IsInfinity(value) || !double.IsNaN(value); } /// diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 293809bf51..a0df186eb7 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -269,6 +269,12 @@ namespace Avalonia.Controls /// protected virtual void HandleClosed() { + var logicalArgs = new LogicalTreeAttachmentEventArgs(this); + ((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs); + + var visualArgs = new VisualTreeAttachmentEventArgs(this, this); + OnDetachedFromVisualTreeCore(visualArgs); + (this as IInputRoot).MouseDevice?.TopLevelClosed(this); PlatformImpl = null; OnClosed(EventArgs.Empty); diff --git a/src/Avalonia.Styling/IDataContextProvider.cs b/src/Avalonia.Styling/IDataContextProvider.cs new file mode 100644 index 0000000000..31639c5784 --- /dev/null +++ b/src/Avalonia.Styling/IDataContextProvider.cs @@ -0,0 +1,13 @@ +namespace Avalonia +{ + /// + /// Defines an element with a data context that can be used for binding. + /// + public interface IDataContextProvider : IAvaloniaObject + { + /// + /// Gets or sets the element's data context. + /// + object DataContext { get; set; } + } +} diff --git a/src/Avalonia.Styling/IStyledElement.cs b/src/Avalonia.Styling/IStyledElement.cs index bcf1898c4c..d4d0f179c3 100644 --- a/src/Avalonia.Styling/IStyledElement.cs +++ b/src/Avalonia.Styling/IStyledElement.cs @@ -10,7 +10,8 @@ namespace Avalonia IStyleHost, ILogical, IResourceProvider, - IResourceNode + IResourceNode, + IDataContextProvider { /// /// Occurs when the control has finished initialization. @@ -27,11 +28,6 @@ namespace Avalonia /// new Classes Classes { get; set; } - /// - /// Gets or sets the control's data context. - /// - object DataContext { get; set; } - /// /// Gets the control's logical parent. /// diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index de8093c048..cf3c4dc855 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -24,7 +24,7 @@ namespace Avalonia /// - Implements to form part of a logical tree. /// - A collection of class strings for custom styling. /// - public class StyledElement : Animatable, IStyledElement, ISetLogicalParent, ISetInheritanceParent + public class StyledElement : Animatable, IDataContextProvider, IStyledElement, ISetLogicalParent, ISetInheritanceParent { /// /// Defines the property. diff --git a/src/Avalonia.Visuals/Media/FontFamily.cs b/src/Avalonia.Visuals/Media/FontFamily.cs index b57b4a0ca8..771de524d9 100644 --- a/src/Avalonia.Visuals/Media/FontFamily.cs +++ b/src/Avalonia.Visuals/Media/FontFamily.cs @@ -184,36 +184,25 @@ namespace Avalonia.Media { unchecked { - var hash = (int)2186146271; - - if (Key != null) - { - hash = (hash * 15768619) ^ Key.GetHashCode(); - } - else - { - hash = (hash * 15768619) ^ FamilyNames.GetHashCode(); - } - - if (Key != null) - { - hash = (hash * 15768619) ^ Key.GetHashCode(); - } - - return hash; + return ((FamilyNames != null ? FamilyNames.GetHashCode() : 0) * 397) ^ (Key != null ? Key.GetHashCode() : 0); } } public override bool Equals(object obj) { + if (ReferenceEquals(this, obj)) + { + return true; + } + if (!(obj is FontFamily other)) { return false; } - if (Key != null) + if (!Equals(Key, other.Key)) { - return other.FamilyNames.Equals(FamilyNames) && other.Key.Equals(Key); + return false; } return other.FamilyNames.Equals(FamilyNames); diff --git a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs index e777d93315..eb0faf4187 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs @@ -111,7 +111,24 @@ namespace Avalonia.Media.Fonts /// public override int GetHashCode() { - return ToString().GetHashCode(); + if (Count == 0) + { + return 0; + } + + unchecked + { + int hash = 17; + + for (var i = 0; i < Names.Count; i++) + { + string name = Names[i]; + + hash = hash * 23 + name.GetHashCode(); + } + + return hash; + } } /// @@ -128,7 +145,20 @@ namespace Avalonia.Media.Fonts return false; } - return other.ToString().Equals(ToString()); + if (other.Count != Count) + { + return false; + } + + for (int i = 0; i < Count; i++) + { + if (Names[i] != other.Names[i]) + { + return false; + } + } + + return true; } public int Count => Names.Count; diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs index a466714136..20f68df820 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -52,6 +52,13 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions // the context. object anchor = context.GetFirstParent(); + if(anchor is null) + { + // Try to find IDataContextProvider, this was added to allow us to find + // a datacontext for Application class when using NativeMenuItems. + anchor = context.GetFirstParent(); + } + // If a control was not found, then try to find the highest-level style as the XAML // file could be a XAML file containing only styles. return anchor ?? diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index dbe5800e55..b4545f792e 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -26,6 +26,7 @@ namespace Avalonia.Data public Binding() { FallbackValue = AvaloniaProperty.UnsetValue; + TargetNullValue = AvaloniaProperty.UnsetValue; } /// @@ -60,6 +61,11 @@ namespace Avalonia.Data /// public object FallbackValue { get; set; } + /// + /// Gets or sets the value to use when the binding result is null. + /// + public object TargetNullValue { get; set; } + /// /// Gets or sets the binding mode. /// @@ -209,6 +215,7 @@ namespace Avalonia.Data observer, targetType, fallback, + TargetNullValue, converter ?? DefaultValueConverter.Instance, ConverterParameter, Priority); @@ -224,9 +231,9 @@ namespace Avalonia.Data { Contract.Requires(target != null); - if (!(target is IStyledElement)) + if (!(target is IDataContextProvider)) { - target = anchor as IStyledElement; + target = anchor as IDataContextProvider; if (target == null) { diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index 29945e25c3..4325ad8a74 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -37,6 +37,11 @@ namespace Avalonia.Data /// public object FallbackValue { get; set; } + /// + /// Gets or sets the value to use when the binding result is null. + /// + public object TargetNullValue { get; set; } + /// /// Gets or sets the binding mode. /// @@ -57,6 +62,12 @@ namespace Avalonia.Data /// public string StringFormat { get; set; } + public MultiBinding() + { + FallbackValue = AvaloniaProperty.UnsetValue; + TargetNullValue = AvaloniaProperty.UnsetValue; + } + /// public InstancedBinding Initiate( IAvaloniaObject target, @@ -102,6 +113,11 @@ namespace Avalonia.Data var culture = CultureInfo.CurrentCulture; var converted = converter.Convert(values, targetType, ConverterParameter, culture); + if (converted == null) + { + converted = TargetNullValue; + } + if (converted == AvaloniaProperty.UnsetValue) { converted = FallbackValue; diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs index d51f56f558..8eb8d1b5db 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs @@ -139,6 +139,7 @@ namespace Avalonia.Base.UnitTests.Data.Core ExpressionObserver.Create(data, o => o.StringValue), typeof(int), 42, + AvaloniaProperty.UnsetValue, DefaultValueConverter.Instance); var result = await target.Take(1); @@ -160,6 +161,7 @@ namespace Avalonia.Base.UnitTests.Data.Core ExpressionObserver.Create(data, o => o.StringValue, true), typeof(int), 42, + AvaloniaProperty.UnsetValue, DefaultValueConverter.Instance); var result = await target.Take(1); @@ -181,6 +183,7 @@ namespace Avalonia.Base.UnitTests.Data.Core ExpressionObserver.Create(data, o => o.StringValue), typeof(int), "bar", + AvaloniaProperty.UnsetValue, DefaultValueConverter.Instance); var result = await target.Take(1); @@ -203,6 +206,7 @@ namespace Avalonia.Base.UnitTests.Data.Core ExpressionObserver.Create(data, o => o.StringValue, true), typeof(int), "bar", + AvaloniaProperty.UnsetValue, DefaultValueConverter.Instance); var result = await target.Take(1); @@ -238,6 +242,7 @@ namespace Avalonia.Base.UnitTests.Data.Core ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string), "9.8", + AvaloniaProperty.UnsetValue, DefaultValueConverter.Instance); target.OnNext("foo"); @@ -353,6 +358,29 @@ namespace Avalonia.Base.UnitTests.Data.Core GC.KeepAlive(data); } + [Fact] + public async Task Null_Value_Should_Use_TargetNullValue() + { + var data = new Class1 { StringValue = "foo" }; + + var target = new BindingExpression( + ExpressionObserver.Create(data, o => o.StringValue), + typeof(string), + AvaloniaProperty.UnsetValue, + "bar", + DefaultValueConverter.Instance); + + object result = null; + target.Subscribe(x => result = x); + + Assert.Equal("foo", result); + + data.StringValue = null; + Assert.Equal("bar", result); + + GC.KeepAlive(data); + } + private class Class1 : NotifyingBase { private string _stringValue; diff --git a/tests/Avalonia.Benchmarks/Data/BindingsBenchmark.cs b/tests/Avalonia.Benchmarks/Data/BindingsBenchmark.cs new file mode 100644 index 0000000000..a320f07063 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Data/BindingsBenchmark.cs @@ -0,0 +1,59 @@ +using Avalonia.Data; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Data +{ + [MemoryDiagnoser, InProcess] + public class BindingsBenchmark + { + [Benchmark] + public void TwoWayBinding_Via_Binding() + { + var instance = new TestClass(); + + var binding = new Binding(nameof(TestClass.BoundValue), BindingMode.TwoWay) + { + Source = instance + }; + + instance.Bind(TestClass.IntValueProperty, binding); + } + + [Benchmark] + public void UpdateTwoWayBinding_Via_Binding() + { + var instance = new TestClass(); + + var binding = new Binding(nameof(TestClass.BoundValue), BindingMode.TwoWay) + { + Source = instance + }; + + instance.Bind(TestClass.IntValueProperty, binding); + for (int i = 0; i < 60; i++) + { + instance.IntValue = i; + } + } + private class TestClass : AvaloniaObject + { + public static readonly StyledProperty IntValueProperty = + AvaloniaProperty.Register(nameof(IntValue)); + + public static readonly StyledProperty BoundValueProperty = + AvaloniaProperty.Register(nameof(BoundValue)); + + public int IntValue + { + get => GetValue(IntValueProperty); + set => SetValue(IntValueProperty, value); + } + + public int BoundValue + { + get => GetValue(BoundValueProperty); + set => SetValue(BoundValueProperty, value); + } + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs index e2eb628512..34e6b228d0 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs @@ -82,22 +82,6 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(50, target.Value); } - [Fact] - public void Properties_Should_Not_Accept_Nan_And_Inifinity() - { - var target = new TestRange(); - - Assert.Throws(() => target.Minimum = double.NaN); - Assert.Throws(() => target.Minimum = double.PositiveInfinity); - Assert.Throws(() => target.Minimum = double.NegativeInfinity); - Assert.Throws(() => target.Maximum = double.NaN); - Assert.Throws(() => target.Maximum = double.PositiveInfinity); - Assert.Throws(() => target.Maximum = double.NegativeInfinity); - Assert.Throws(() => target.Value = double.NaN); - Assert.Throws(() => target.Value = double.PositiveInfinity); - Assert.Throws(() => target.Value = double.NegativeInfinity); - } - [Theory] [InlineData(true)] [InlineData(false)] diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index 7e053392c7..cd33fae6f3 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -405,6 +405,24 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(42, target.Value); } + [Fact] + public void Should_Return_TargetNullValue_When_Value_Is_Null() + { + var target = new TextBlock(); + var source = new Source { Foo = null }; + + var binding = new Binding + { + Source = source, + Path = "Foo", + TargetNullValue = "(null)", + }; + + target.Bind(TextBlock.TextProperty, binding); + + Assert.Equal("(null)", target.Text); + } + [Fact] public void Null_Path_Should_Bind_To_DataContext() { diff --git a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs index 78c538d99d..f3e8ff9248 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs @@ -94,6 +94,28 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal("fallback", target.Text); } + [Fact] + public void Should_Return_TargetNullValue_When_Value_Is_Null() + { + var target = new TextBlock(); + + var binding = new MultiBinding + { + Converter = new NullValueConverter(), + Bindings = new[] + { + new Binding { Path = "A" }, + new Binding { Path = "B" }, + new Binding { Path = "C" }, + }, + TargetNullValue = "(null)", + }; + + target.Bind(TextBlock.TextProperty, binding); + + Assert.Equal("(null)", target.Text); + } + private class ConcatConverter : IMultiValueConverter { public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) @@ -109,5 +131,13 @@ namespace Avalonia.Markup.UnitTests.Data return AvaloniaProperty.UnsetValue; } } + + private class NullValueConverter : IMultiValueConverter + { + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + return null; + } + } } } diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs index c6eeb1ec0e..320dbfeec3 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs @@ -81,89 +81,12 @@ namespace Avalonia.Styling.UnitTests Assert.Equal("TestLogical1 > TestLogical3", selector.ToString()); } - public abstract class TestLogical : ILogical, IStyleable + public abstract class TestLogical : Control { - public TestLogical() + public ILogical LogicalParent { - Classes = new Classes(); - } - - public event EventHandler PropertyChanged; - public event EventHandler InheritablePropertyChanged; - public event EventHandler AttachedToLogicalTree; - public event EventHandler DetachedFromLogicalTree; - - public Classes Classes { get; } - - public string Name { get; set; } - - public bool IsAttachedToLogicalTree { get; } - - public IAvaloniaReadOnlyList LogicalChildren { get; set; } - - public ILogical LogicalParent { get; set; } - - public Type StyleKey { get; } - - public ITemplatedControl TemplatedParent { get; } - - IObservable IStyleable.StyleDetach { get; } - - IAvaloniaReadOnlyList IStyleable.Classes => Classes; - - public object GetValue(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public T GetValue(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public void SetValue(AvaloniaProperty property, object value, BindingPriority priority) - { - throw new NotImplementedException(); - } - - public void SetValue(AvaloniaProperty property, T value, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - - public IDisposable Bind(AvaloniaProperty property, IObservable source, BindingPriority priority) - { - throw new NotImplementedException(); - } - - public bool IsAnimating(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public bool IsSet(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public IDisposable Bind(AvaloniaProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - - public void NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) - { - throw new NotImplementedException(); - } - - public void NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) - { - throw new NotImplementedException(); - } - - public void NotifyResourcesChanged(ResourcesChangedEventArgs e) - { - throw new NotImplementedException(); + get => Parent; + set => ((ISetLogicalParent)this).SetParent(value); } } diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs index 496998ecd9..fd25b17ba4 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs @@ -144,7 +144,7 @@ namespace Avalonia.Styling.UnitTests Assert.Equal(new[] { true, false }, result); } - public class Control1 : TestControlBase + public class Control1 : Control { } } diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs index aef539becd..940b2b18ef 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs @@ -111,89 +111,12 @@ namespace Avalonia.Styling.UnitTests Assert.Equal("TestLogical1.foo TestLogical3", selector.ToString()); } - public abstract class TestLogical : ILogical, IStyleable + public abstract class TestLogical : Control { - public TestLogical() + public ILogical LogicalParent { - Classes = new Classes(); - } - - public event EventHandler PropertyChanged; - public event EventHandler InheritablePropertyChanged; - public event EventHandler AttachedToLogicalTree; - public event EventHandler DetachedFromLogicalTree; - - public Classes Classes { get; } - - public string Name { get; set; } - - public bool IsAttachedToLogicalTree { get; } - - public IAvaloniaReadOnlyList LogicalChildren { get; set; } - - public ILogical LogicalParent { get; set; } - - public Type StyleKey { get; } - - public ITemplatedControl TemplatedParent { get; } - - IAvaloniaReadOnlyList IStyleable.Classes => Classes; - - IObservable IStyleable.StyleDetach { get; } - - public object GetValue(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public T GetValue(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public void SetValue(AvaloniaProperty property, object value, BindingPriority priority) - { - throw new NotImplementedException(); - } - - public void SetValue(AvaloniaProperty property, T value, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - - public IDisposable Bind(AvaloniaProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - - public bool IsAnimating(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public bool IsSet(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public IDisposable Bind(AvaloniaProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - - public void NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) - { - throw new NotImplementedException(); - } - - public void NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) - { - throw new NotImplementedException(); - } - - public void NotifyResourcesChanged(ResourcesChangedEventArgs e) - { - throw new NotImplementedException(); + get => Parent; + set => ((ISetLogicalParent)this).SetParent(value); } } diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Name.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Name.cs index 1c8a3a406e..26734a494b 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Name.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Name.cs @@ -1,6 +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 Avalonia.Controls; using Moq; using Xunit; @@ -52,7 +53,7 @@ namespace Avalonia.Styling.UnitTests Assert.Equal("Control1#foo", target.ToString()); } - public class Control1 : TestControlBase + public class Control1 : Control { } } diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs index 2f3e2b8f34..7c23cb4d2c 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs @@ -103,11 +103,11 @@ namespace Avalonia.Styling.UnitTests Assert.Equal(typeof(Control1), target.TargetType); } - public class Control1 : TestControlBase + public class Control1 : Control { } - public class Control2 : TestControlBase + public class Control2 : Control { } } diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_OfType.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_OfType.cs index 05f3544366..a254c73c03 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_OfType.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_OfType.cs @@ -1,6 +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 Avalonia.Controls; using Moq; using Xunit; @@ -44,11 +45,11 @@ namespace Avalonia.Styling.UnitTests Assert.Equal(SelectorMatchResult.AlwaysThisType, target.Match(control).Result); } - public class Control1 : TestControlBase + public class Control1 : Control { } - public class Control2 : TestControlBase + public class Control2 : Control { } } diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Or.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Or.cs index 521c73ce27..e9bc93c957 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Or.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Or.cs @@ -1,6 +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 Avalonia.Controls; using Xunit; namespace Avalonia.Styling.UnitTests @@ -78,7 +79,7 @@ namespace Avalonia.Styling.UnitTests default(Selector).OfType().Class("foo"), default(Selector).OfType().Class("bar")); - Assert.Equal(typeof(TestControlBase), target.TargetType); + Assert.Equal(typeof(Control), target.TargetType); } [Fact] @@ -91,15 +92,15 @@ namespace Avalonia.Styling.UnitTests Assert.Equal(null, target.TargetType); } - public class Control1 : TestControlBase + public class Control1 : Control { } - public class Control2 : TestControlBase + public class Control2 : Control { } - public class Control3 : TestControlBase + public class Control3 : Control { } } diff --git a/tests/Avalonia.Styling.UnitTests/TestControlBase.cs b/tests/Avalonia.Styling.UnitTests/TestControlBase.cs deleted file mode 100644 index d9fabf6f5d..0000000000 --- a/tests/Avalonia.Styling.UnitTests/TestControlBase.cs +++ /dev/null @@ -1,83 +0,0 @@ -// 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; -using Avalonia.Collections; -using Avalonia.Controls; -using Avalonia.Data; - -namespace Avalonia.Styling.UnitTests -{ - public class TestControlBase : IStyleable - { - public TestControlBase() - { - Classes = new Classes(); - SubscribeCheckObservable = new TestObservable(); - } - -#pragma warning disable CS0067 // Event not used - public event EventHandler PropertyChanged; - public event EventHandler InheritablePropertyChanged; -#pragma warning restore CS0067 - - public string Name { get; set; } - - public virtual Classes Classes { get; set; } - - public Type StyleKey => GetType(); - - public TestObservable SubscribeCheckObservable { get; private set; } - - public ITemplatedControl TemplatedParent - { - get; - set; - } - - IAvaloniaReadOnlyList IStyleable.Classes => Classes; - - IObservable IStyleable.StyleDetach { get; } - - public object GetValue(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public T GetValue(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public void SetValue(AvaloniaProperty property, object value, BindingPriority priority) - { - throw new NotImplementedException(); - } - - public void SetValue(AvaloniaProperty property, T value, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - - public bool IsAnimating(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public bool IsSet(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public IDisposable Bind(AvaloniaProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - - public IDisposable Bind(AvaloniaProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - } -} diff --git a/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs b/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs deleted file mode 100644 index e92ac36e8f..0000000000 --- a/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs +++ /dev/null @@ -1,81 +0,0 @@ -// 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; -using Avalonia.Collections; -using Avalonia.Controls; -using Avalonia.Data; - -namespace Avalonia.Styling.UnitTests -{ - public abstract class TestTemplatedControl : ITemplatedControl, IStyleable - { - public event EventHandler PropertyChanged; - public event EventHandler InheritablePropertyChanged; - - public abstract Classes Classes - { - get; - } - - public abstract string Name - { - get; - } - - public abstract Type StyleKey - { - get; - } - - public abstract ITemplatedControl TemplatedParent - { - get; - } - - IAvaloniaReadOnlyList IStyleable.Classes => Classes; - - IObservable IStyleable.StyleDetach { get; } - - public object GetValue(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public T GetValue(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public void SetValue(AvaloniaProperty property, object value, BindingPriority priority) - { - throw new NotImplementedException(); - } - - public void SetValue(AvaloniaProperty property, T value, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - - public IDisposable Bind(AvaloniaProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - - public IDisposable Bind(AvaloniaProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - - public bool IsAnimating(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public bool IsSet(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - } -}