From f8e83db6b0367c0ace69d95bcc789bf10e6f1c5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Tue, 24 Sep 2019 03:18:44 +0100 Subject: [PATCH 01/27] Update .NET Core SDK to version 3.0.x. --- azure-pipelines.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 92e4afdca8..389f343c54 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' From 1ba4c68a3583c268c2e4c009aa15eab5934514c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Fri, 15 Nov 2019 21:50:17 +0000 Subject: [PATCH 02/27] Implemented TargetNullValue for bindings. --- .../Data/Core/BindingExpression.cs | 13 +++++++- src/Markup/Avalonia.Markup/Data/Binding.cs | 7 +++++ .../Avalonia.Markup/Data/MultiBinding.cs | 16 ++++++++++ .../Data/Core/BindingExpressionTests.cs | 28 +++++++++++++++++ .../Data/BindingTests.cs | 18 +++++++++++ .../Data/MultiBindingTests.cs | 30 +++++++++++++++++++ 6 files changed, 111 insertions(+), 1 deletion(-) 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/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index dbe5800e55..61d0f7c83b 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); 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.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; + } + } } } From 24f11bba18d963757a1e8d7bb71ffb55de59ac70 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 16 Nov 2019 11:33:01 +0100 Subject: [PATCH 03/27] Simplify test controls in styling unit tests. Making a change to interfaces was becoming a pain because of this. --- .../SelectorTests_Child.cs | 85 +------------------ .../SelectorTests_Class.cs | 2 +- .../SelectorTests_Descendent.cs | 85 +------------------ .../SelectorTests_Name.cs | 3 +- .../SelectorTests_Not.cs | 4 +- .../SelectorTests_OfType.cs | 5 +- .../SelectorTests_Or.cs | 9 +- .../TestControlBase.cs | 83 ------------------ .../TestTemplatedControl.cs | 81 ------------------ 9 files changed, 21 insertions(+), 336 deletions(-) delete mode 100644 tests/Avalonia.Styling.UnitTests/TestControlBase.cs delete mode 100644 tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs 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(); - } - } -} From 8e60e83d4ca78caabcf2c7dfebcb41f6d7e2b59a Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 16 Nov 2019 16:21:31 +0100 Subject: [PATCH 04/27] Reduce memory usage of binding operations. --- src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 12 +++++- src/Avalonia.Base/Data/BindingOperations.cs | 16 ++++++- .../Data/Core/ExpressionObserver.cs | 6 +-- .../Plugins/AvaloniaPropertyAccessorPlugin.cs | 19 +++++++- .../Data/Core/PropertyAccessorNode.cs | 12 +++++- .../Utilities/IdentifierParser.cs | 2 +- .../Data/BindingOperations.cs | 43 +++++++++++++++++++ 7 files changed, 99 insertions(+), 11 deletions(-) create mode 100644 tests/Avalonia.Benchmarks/Data/BindingOperations.cs 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..bcef896fb7 100644 --- a/src/Avalonia.Base/Data/BindingOperations.cs +++ b/src/Avalonia.Base/Data/BindingOperations.cs @@ -56,22 +56,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/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..4ee8e30631 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs @@ -3,6 +3,7 @@ using System; using System.Reactive.Linq; +using System.Runtime.ExceptionServices; namespace Avalonia.Data.Core.Plugins { @@ -76,7 +77,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 +118,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 +126,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..7255fb93b3 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -41,7 +41,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/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/tests/Avalonia.Benchmarks/Data/BindingOperations.cs b/tests/Avalonia.Benchmarks/Data/BindingOperations.cs new file mode 100644 index 0000000000..d1ca77c980 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Data/BindingOperations.cs @@ -0,0 +1,43 @@ +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); + } + + 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); + } + } + } +} From ef8cbf07f72307d44e78d36864d34f14255198c9 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 16 Nov 2019 16:23:14 +0100 Subject: [PATCH 05/27] Rename benchmark file. --- .../Data/{BindingOperations.cs => BindingsBenchmark.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/Avalonia.Benchmarks/Data/{BindingOperations.cs => BindingsBenchmark.cs} (100%) diff --git a/tests/Avalonia.Benchmarks/Data/BindingOperations.cs b/tests/Avalonia.Benchmarks/Data/BindingsBenchmark.cs similarity index 100% rename from tests/Avalonia.Benchmarks/Data/BindingOperations.cs rename to tests/Avalonia.Benchmarks/Data/BindingsBenchmark.cs From 710240b2ad4240fbdd45bd86412bd12f9e302666 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 16 Nov 2019 16:25:08 +0100 Subject: [PATCH 06/27] Cleanup using. --- src/Avalonia.Base/Data/BindingOperations.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs index bcef896fb7..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; From 7158f660dcc6f05d9c9ac655cc300e8d60300c5d Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 16 Nov 2019 16:26:20 +0100 Subject: [PATCH 07/27] Cleanup more usings. --- .../Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs | 1 - src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs index 4ee8e30631..fa72235a89 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.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.Reactive.Linq; using System.Runtime.ExceptionServices; namespace Avalonia.Data.Core.Plugins diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index 7255fb93b3..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 From 678038470e0a20f2702603f72a347cb66f4e817a Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 16 Nov 2019 21:53:05 +0100 Subject: [PATCH 08/27] Optimize FontFamily and members hashcode/equality. --- src/Avalonia.Visuals/Media/FontFamily.cs | 27 +++++---------- .../Media/Fonts/FamilyNameCollection.cs | 34 +++++++++++++++++-- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/src/Avalonia.Visuals/Media/FontFamily.cs b/src/Avalonia.Visuals/Media/FontFamily.cs index b57b4a0ca8..efb3c99143 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 (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; From 364d6697cc5bef0a7fe2f8a8b5e938f0fa9644d9 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 16 Nov 2019 22:33:32 +0100 Subject: [PATCH 09/27] Fix wrath of equality operator. --- src/Avalonia.Visuals/Media/FontFamily.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/FontFamily.cs b/src/Avalonia.Visuals/Media/FontFamily.cs index efb3c99143..771de524d9 100644 --- a/src/Avalonia.Visuals/Media/FontFamily.cs +++ b/src/Avalonia.Visuals/Media/FontFamily.cs @@ -200,7 +200,7 @@ namespace Avalonia.Media return false; } - if (Key != other.Key) + if (!Equals(Key, other.Key)) { return false; } From 35ee4f79762f933ca12ad27c9a3bb7201cc24dcd Mon Sep 17 00:00:00 2001 From: ahopper Date: Wed, 20 Nov 2019 11:04:20 +0000 Subject: [PATCH 10/27] Reduce calls to ToArray in PublishNext --- .../Reactive/LightweightObservableBase.cs | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs index 41009e4cd3..b6c7cecfdc 100644 --- a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs +++ b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs @@ -111,25 +111,38 @@ namespace Avalonia.Reactive protected abstract void Initialize(); protected abstract void Deinitialize(); - + protected void PublishNext(T value) { 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); + } } } } From f7c644eb7567a865b95c3ba9dc95f9398d5969a7 Mon Sep 17 00:00:00 2001 From: ahopper Date: Wed, 20 Nov 2019 11:14:48 +0000 Subject: [PATCH 11/27] whitespace --- src/Avalonia.Base/Reactive/LightweightObservableBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs index b6c7cecfdc..f5052e5858 100644 --- a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs +++ b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs @@ -111,7 +111,7 @@ namespace Avalonia.Reactive protected abstract void Initialize(); protected abstract void Deinitialize(); - + protected void PublishNext(T value) { if (Volatile.Read(ref _observers) != null) From 8879d4c92d97dac93b9e659950f47f87da005c9e Mon Sep 17 00:00:00 2001 From: ahopper Date: Wed, 20 Nov 2019 16:12:09 +0000 Subject: [PATCH 12/27] added benchmark --- .../Data/BindingsBenchmark.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/Avalonia.Benchmarks/Data/BindingsBenchmark.cs b/tests/Avalonia.Benchmarks/Data/BindingsBenchmark.cs index d1ca77c980..a320f07063 100644 --- a/tests/Avalonia.Benchmarks/Data/BindingsBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Data/BindingsBenchmark.cs @@ -19,6 +19,22 @@ namespace Avalonia.Benchmarks.Data 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 = From 138ea74b88f64e2977482583b9e4042d68567b24 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 22 Nov 2019 19:32:09 +0000 Subject: [PATCH 13/27] Add code to show desired syntax. --- samples/ControlCatalog/App.xaml | 5 +++++ samples/ControlCatalog/App.xaml.cs | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index e40509dfda..813a043b01 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -17,4 +17,9 @@ + + + + + diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 958729e2e8..b713c3dfd6 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -1,18 +1,32 @@ using System; +using System.Reactive; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; +using ReactiveUI; namespace ControlCatalog { public class App : Application { + public App() + { + DataContext = this; + + AboutCommand = ReactiveCommand.Create(() => + { + + }); + } + public override void Initialize() { AvaloniaXamlLoader.Load(this); } + public ReactiveCommand AboutCommand { get; } + public override void OnFrameworkInitializationCompleted() { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) From 11a67d5b1088433ed9b3e951f216f127e79553a4 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Fri, 22 Nov 2019 23:51:50 +0100 Subject: [PATCH 14/27] Make sure window and its children get detached from visual and logical trees when closing. --- src/Avalonia.Controls/TopLevel.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 293809bf51..81f132660d 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -269,6 +269,18 @@ namespace Avalonia.Controls /// protected virtual void HandleClosed() { + { + var e = new LogicalTreeAttachmentEventArgs(this); + + ((ILogical)this).NotifyDetachedFromLogicalTree(e); + } + + { + var e = new VisualTreeAttachmentEventArgs(this, this); + + OnDetachedFromVisualTreeCore(e); + } + (this as IInputRoot).MouseDevice?.TopLevelClosed(this); PlatformImpl = null; OnClosed(EventArgs.Empty); From 1212e2444769f76c60c75b8b06e4988fcdae7ff3 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Sun, 24 Nov 2019 15:45:31 +0800 Subject: [PATCH 15/27] Hackfix for #3284 --- src/Avalonia.Controls/Primitives/RangeBase.cs | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/RangeBase.cs b/src/Avalonia.Controls/Primitives/RangeBase.cs index f1ee7c0e1a..39d00b6953 100644 --- a/src/Avalonia.Controls/Primitives/RangeBase.cs +++ b/src/Avalonia.Controls/Primitives/RangeBase.cs @@ -75,8 +75,11 @@ namespace Avalonia.Controls.Primitives set { - ValidateDouble(value, "Minimum"); - + if (!ValidateDouble(value)) + { + value = _minimum; + } + if (IsInitialized) { SetAndRaise(MinimumProperty, ref _minimum, value); @@ -102,7 +105,10 @@ namespace Avalonia.Controls.Primitives set { - ValidateDouble(value, "Maximum"); + if (!ValidateDouble(value)) + { + value = _maximum; + } if (IsInitialized) { @@ -129,7 +135,10 @@ namespace Avalonia.Controls.Primitives set { - ValidateDouble(value, "Value"); + if (!ValidateDouble(value)) + { + value = default; + } if (IsInitialized) { @@ -167,13 +176,9 @@ namespace Avalonia.Controls.Primitives /// Throws an exception if the double value is NaN or Inf. /// /// 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)); } /// From 7c7f6022cba12ad59c69fe2b614da27e2786c725 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 24 Nov 2019 15:46:00 +0100 Subject: [PATCH 16/27] Cleanup event args locals. --- src/Avalonia.Controls/TopLevel.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 81f132660d..a0df186eb7 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -269,17 +269,11 @@ namespace Avalonia.Controls /// protected virtual void HandleClosed() { - { - var e = new LogicalTreeAttachmentEventArgs(this); - - ((ILogical)this).NotifyDetachedFromLogicalTree(e); - } + var logicalArgs = new LogicalTreeAttachmentEventArgs(this); + ((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs); - { - var e = new VisualTreeAttachmentEventArgs(this, this); - - OnDetachedFromVisualTreeCore(e); - } + var visualArgs = new VisualTreeAttachmentEventArgs(this, this); + OnDetachedFromVisualTreeCore(visualArgs); (this as IInputRoot).MouseDevice?.TopLevelClosed(this); PlatformImpl = null; From 6a74ab560c96078aa15e268900df342c47d6f50d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 25 Nov 2019 10:05:45 +0100 Subject: [PATCH 17/27] Added IDataContextProvider. So that non-`IStyledElement`s can take part in binding. Apply the interface to `Application`. --- src/Avalonia.Controls/Application.cs | 22 +++++++++++++++++++- src/Avalonia.Styling/IDataContextProvider.cs | 13 ++++++++++++ src/Avalonia.Styling/IStyledElement.cs | 8 ++----- src/Avalonia.Styling/StyledElement.cs | 2 +- src/Markup/Avalonia.Markup/Data/Binding.cs | 4 ++-- 5 files changed, 39 insertions(+), 10 deletions(-) create mode 100644 src/Avalonia.Styling/IDataContextProvider.cs diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 59c6c47ed9..9df92a49bc 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,13 @@ namespace Avalonia private Styles _styles; private IResourceDictionary _resources; + /// + /// Defines the property. + /// + public static readonly StyledProperty DataContextProperty = + AvaloniaProperty.Register( + nameof(DataContext)); + /// public event EventHandler ResourcesChanged; @@ -56,6 +63,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.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/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index 61d0f7c83b..b4545f792e 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -231,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) { From 7b11d7674aecbd31e8c15bee1c2715ffd798105e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 25 Nov 2019 09:28:42 +0000 Subject: [PATCH 18/27] implement IDataContextProvider on NativeMenuItem --- src/Avalonia.Controls/NativeMenuItemBase.cs | 24 ++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/NativeMenuItemBase.cs b/src/Avalonia.Controls/NativeMenuItemBase.cs index 47eb86cdc3..fa2ecd609a 100644 --- a/src/Avalonia.Controls/NativeMenuItemBase.cs +++ b/src/Avalonia.Controls/NativeMenuItemBase.cs @@ -2,7 +2,7 @@ namespace Avalonia.Controls { - public class NativeMenuItemBase : AvaloniaObject + public class NativeMenuItemBase : AvaloniaObject, IDataContextProvider { private NativeMenu _parent; @@ -11,6 +11,7 @@ namespace Avalonia.Controls } + public static readonly DirectProperty ParentProperty = AvaloniaProperty.RegisterDirect("Parent", o => o.Parent, (o, v) => o.Parent = v); @@ -19,5 +20,26 @@ namespace Avalonia.Controls get => _parent; set => SetAndRaise(ParentProperty, ref _parent, value); } + + /// + /// Defines the property. + /// + public static readonly StyledProperty DataContextProperty = + AvaloniaProperty.Register( + nameof(DataContext)); + + /// + /// Gets or sets the controls'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); } + } } } From 7eea7f65c65fca0b7f4e35b3b8a518e31afc687b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 25 Nov 2019 10:07:18 +0000 Subject: [PATCH 19/27] dont implement datacontext on menuitems --- src/Avalonia.Controls/NativeMenuItemBase.cs | 24 +-------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/src/Avalonia.Controls/NativeMenuItemBase.cs b/src/Avalonia.Controls/NativeMenuItemBase.cs index fa2ecd609a..47eb86cdc3 100644 --- a/src/Avalonia.Controls/NativeMenuItemBase.cs +++ b/src/Avalonia.Controls/NativeMenuItemBase.cs @@ -2,7 +2,7 @@ namespace Avalonia.Controls { - public class NativeMenuItemBase : AvaloniaObject, IDataContextProvider + public class NativeMenuItemBase : AvaloniaObject { private NativeMenu _parent; @@ -11,7 +11,6 @@ namespace Avalonia.Controls } - public static readonly DirectProperty ParentProperty = AvaloniaProperty.RegisterDirect("Parent", o => o.Parent, (o, v) => o.Parent = v); @@ -20,26 +19,5 @@ namespace Avalonia.Controls get => _parent; set => SetAndRaise(ParentProperty, ref _parent, value); } - - /// - /// Defines the property. - /// - public static readonly StyledProperty DataContextProperty = - AvaloniaProperty.Register( - nameof(DataContext)); - - /// - /// Gets or sets the controls'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); } - } } } From eb6f3e620d802b495f0120204cc4b9c3590a9a4a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 25 Nov 2019 10:07:45 +0000 Subject: [PATCH 20/27] GetDefaultAnchor will looks for IDataContextProviders --- .../MarkupExtensions/BindingExtension.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs index a466714136..7532e01f55 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -52,6 +52,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions // the context. object anchor = context.GetFirstParent(); + if(anchor is null) + { + 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 ?? From 2ffb1af3bc0fbbcd6f666e4cf62d63111e9f03ef Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 25 Nov 2019 10:08:55 +0000 Subject: [PATCH 21/27] add comment --- .../Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs index 7532e01f55..20f68df820 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -54,6 +54,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions 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(); } From 055115415c4aaad19f755aa525e778014444ec67 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 25 Nov 2019 12:11:29 +0000 Subject: [PATCH 22/27] Application class uses AddOwner instead of making its own version of datacontext property. --- src/Avalonia.Controls/Application.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 9df92a49bc..9158ac7038 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -49,8 +49,7 @@ namespace Avalonia /// Defines the property. /// public static readonly StyledProperty DataContextProperty = - AvaloniaProperty.Register( - nameof(DataContext)); + StyledElement.DataContextProperty.AddOwner(); /// public event EventHandler ResourcesChanged; From c37562971734e5bbad55055ec47410235c48e3ad Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 25 Nov 2019 12:31:12 +0000 Subject: [PATCH 23/27] restore app.xaml /cs --- samples/ControlCatalog/App.xaml | 5 ----- samples/ControlCatalog/App.xaml.cs | 6 ------ 2 files changed, 11 deletions(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 813a043b01..e40509dfda 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -17,9 +17,4 @@ - - - - - diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index b713c3dfd6..ea5ae0a75b 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -12,12 +12,6 @@ namespace ControlCatalog { public App() { - DataContext = this; - - AboutCommand = ReactiveCommand.Create(() => - { - - }); } public override void Initialize() From 79527e6ab9e195437fb533e1e12f2ab435682e45 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 25 Nov 2019 12:35:15 +0000 Subject: [PATCH 24/27] restore app.xaml.cs --- samples/ControlCatalog/App.xaml.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index ea5ae0a75b..52a9591c94 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -1,33 +1,23 @@ -using System; -using System.Reactive; using Avalonia; -using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; -using ReactiveUI; namespace ControlCatalog { public class App : Application { - public App() - { - } - public override void Initialize() { AvaloniaXamlLoader.Load(this); } - public ReactiveCommand AboutCommand { get; } - public override void OnFrameworkInitializationCompleted() { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) desktopLifetime.MainWindow = new MainWindow(); else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) singleViewLifetime.MainView = new MainView(); - + base.OnFrameworkInitializationCompleted(); } } From 73e1224ac33de0e100bbbd3be8333569b4553777 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 25 Nov 2019 21:55:26 +0800 Subject: [PATCH 25/27] Ignore changes if isnt a valid double value. --- src/Avalonia.Controls/Primitives/RangeBase.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/RangeBase.cs b/src/Avalonia.Controls/Primitives/RangeBase.cs index 39d00b6953..7155cceb8b 100644 --- a/src/Avalonia.Controls/Primitives/RangeBase.cs +++ b/src/Avalonia.Controls/Primitives/RangeBase.cs @@ -77,9 +77,9 @@ namespace Avalonia.Controls.Primitives { if (!ValidateDouble(value)) { - value = _minimum; + return; } - + if (IsInitialized) { SetAndRaise(MinimumProperty, ref _minimum, value); @@ -107,7 +107,7 @@ namespace Avalonia.Controls.Primitives { if (!ValidateDouble(value)) { - value = _maximum; + return; } if (IsInitialized) @@ -137,7 +137,7 @@ namespace Avalonia.Controls.Primitives { if (!ValidateDouble(value)) { - value = default; + return; } if (IsInitialized) @@ -178,7 +178,7 @@ namespace Avalonia.Controls.Primitives /// The value. private static bool ValidateDouble(double value) { - return (!double.IsInfinity(value) || !double.IsNaN(value)); + return !double.IsInfinity(value) || !double.IsNaN(value); } /// From f569287998ee37b90f5604683c11dd5704765e0e Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 25 Nov 2019 21:56:19 +0800 Subject: [PATCH 26/27] Update xml comment. --- src/Avalonia.Controls/Primitives/RangeBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/RangeBase.cs b/src/Avalonia.Controls/Primitives/RangeBase.cs index 7155cceb8b..baa51f92ec 100644 --- a/src/Avalonia.Controls/Primitives/RangeBase.cs +++ b/src/Avalonia.Controls/Primitives/RangeBase.cs @@ -173,7 +173,7 @@ 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. private static bool ValidateDouble(double value) From e23da2873111e7fabdf12c2e56abddca6d180aa0 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 25 Nov 2019 23:13:01 +0800 Subject: [PATCH 27/27] Remove obsolete unit test. --- .../Primitives/RangeBaseTests.cs | 16 ---------------- 1 file changed, 16 deletions(-) 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)]