From 97db5ec6578a811837d7b28acbab66cdb7063d52 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 25 Oct 2018 16:34:50 +0200 Subject: [PATCH 01/37] Cache AvaloniaObject initialization notifications. When an `AvaloniaObject` is created, it notifies each of the `AvaloniaProperties` registered on it that they have been initialized on a new object. Instead of calling `GetDefaultValue` each time, cache the default values. --- src/Avalonia.Base/AvaloniaObject.cs | 46 +--------------- src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 55 +++++++++++++++++++ 2 files changed, 57 insertions(+), 44 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 7e8d733f1b..2a19f40ecb 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -22,27 +22,10 @@ namespace Avalonia /// public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged { - /// - /// The parent object that inherited values are inherited from. - /// private IAvaloniaObject _inheritanceParent; - - /// - /// Maintains a list of direct property binding subscriptions so that the binding source - /// doesn't get collected. - /// private List _directBindings; - - /// - /// Event handler for implementation. - /// private PropertyChangedEventHandler _inpcChanged; - - /// - /// Event handler for implementation. - /// private EventHandler _propertyChanged; - private ValueStore _values; private ValueStore Values => _values ?? (_values = new ValueStore(this)); @@ -52,32 +35,7 @@ namespace Avalonia public AvaloniaObject() { VerifyAccess(); - - void Notify(AvaloniaProperty property) - { - object value = property.IsDirect ? - ((IDirectPropertyAccessor)property).GetValue(this) : - ((IStyledPropertyAccessor)property).GetDefaultValue(GetType()); - - var e = new AvaloniaPropertyChangedEventArgs( - this, - property, - AvaloniaProperty.UnsetValue, - value, - BindingPriority.Unset); - - property.NotifyInitialized(e); - } - - foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegistered(this)) - { - Notify(property); - } - - foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(this.GetType())) - { - Notify(property); - } + AvaloniaPropertyRegistry.Instance.NotifyInitialized(this); } /// @@ -628,7 +586,7 @@ namespace Avalonia /// /// The property. /// The default value. - internal object GetDefaultValue(AvaloniaProperty property) + private object GetDefaultValue(AvaloniaProperty property) { if (property.Inherits && InheritanceParent is AvaloniaObject aobj) return aobj.GetValue(property); diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index e29e7339ae..af587ea1af 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; +using Avalonia.Data; namespace Avalonia { @@ -21,6 +22,8 @@ namespace Avalonia new Dictionary>(); private readonly Dictionary> _attachedCache = new Dictionary>(); + private readonly Dictionary>> _initializedCache = + new Dictionary>>(); /// /// Gets the instance @@ -204,6 +207,7 @@ namespace Avalonia } _registeredCache.Clear(); + _initializedCache.Clear(); } /// @@ -239,6 +243,57 @@ namespace Avalonia } _attachedCache.Clear(); + _initializedCache.Clear(); + } + + internal void NotifyInitialized(AvaloniaObject o) + { + Contract.Requires(o != null); + + var type = o.GetType(); + + void Notify(AvaloniaProperty property, object value) + { + var e = new AvaloniaPropertyChangedEventArgs( + o, + property, + AvaloniaProperty.UnsetValue, + value, + BindingPriority.Unset); + + property.NotifyInitialized(e); + } + + if (!_initializedCache.TryGetValue(type, out var items)) + { + var build = new Dictionary(); + + foreach (var property in GetRegistered(type)) + { + var value = !property.IsDirect ? + ((IStyledPropertyAccessor)property).GetDefaultValue(type) : + null; + build.Add(property, value); + } + + foreach (var property in GetRegisteredAttached(type)) + { + if (!build.ContainsKey(property)) + { + var value = ((IStyledPropertyAccessor)property).GetDefaultValue(type); + build.Add(property, value); + } + } + + items = build.ToList(); + _initializedCache.Add(type, items); + } + + foreach (var i in items) + { + var value = i.Key.IsDirect ? o.GetValue(i.Key) : i.Value; + Notify(i.Key, value); + } } } } From 9501da85fa4da89dc0d7e11eaff9316d42da523c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 23 Jan 2019 09:19:27 +0100 Subject: [PATCH 02/37] Added failing tests for #2260. --- .../Avalonia.Controls.UnitTests/ImageTests.cs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ImageTests.cs b/tests/Avalonia.Controls.UnitTests/ImageTests.cs index e92fc572b4..71d0d1e328 100644 --- a/tests/Avalonia.Controls.UnitTests/ImageTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ImageTests.cs @@ -61,5 +61,61 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Size(50, 50), target.DesiredSize); } + + [Fact] + public void Arrange_Should_Return_Correct_Size_For_No_Stretch() + { + var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var target = new Image(); + target.Stretch = Stretch.None; + target.Source = bitmap; + + target.Measure(new Size(50, 50)); + target.Arrange(new Rect(0, 0, 100, 400)); + + Assert.Equal(new Size(50, 100), target.Bounds.Size); + } + + [Fact] + public void Arrange_Should_Return_Correct_Size_For_Fill_Stretch() + { + var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var target = new Image(); + target.Stretch = Stretch.Fill; + target.Source = bitmap; + + target.Measure(new Size(50, 50)); + target.Arrange(new Rect(0, 0, 25, 100)); + + Assert.Equal(new Size(25, 100), target.Bounds.Size); + } + + [Fact] + public void Arrange_Should_Return_Correct_Size_For_Uniform_Stretch() + { + var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var target = new Image(); + target.Stretch = Stretch.Uniform; + target.Source = bitmap; + + target.Measure(new Size(50, 50)); + target.Arrange(new Rect(0, 0, 25, 100)); + + Assert.Equal(new Size(25, 50), target.Bounds.Size); + } + + [Fact] + public void Arrange_Should_Return_Correct_Size_For_UniformToFill_Stretch() + { + var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var target = new Image(); + target.Stretch = Stretch.UniformToFill; + target.Source = bitmap; + + target.Measure(new Size(50, 50)); + target.Arrange(new Rect(0, 0, 25, 100)); + + Assert.Equal(new Size(25, 100), target.Bounds.Size); + } } } From 3ee48b25e4ecd967227694f96a85a89aa36b4217 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 23 Jan 2019 09:09:44 +0100 Subject: [PATCH 03/37] Fix image arrange. `Image` was calculating its desired size correctly according to the value of `Stretch` but then was not applying that calculation to the arrange pass. Fixes #2260 --- src/Avalonia.Controls/Image.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index 72379e7b53..c696fe7975 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -99,5 +99,22 @@ namespace Avalonia.Controls return new Size(); } } + + /// + protected override Size ArrangeOverride(Size finalSize) + { + var source = Source; + + if (source != null) + { + var sourceSize = new Size(source.PixelSize.Width, source.PixelSize.Height); + var result = Stretch.CalculateSize(finalSize, sourceSize); + return result; + } + else + { + return new Size(); + } + } } } From 302bf55b8a8827350795cef4945b41ffd980eb30 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 20 Jan 2019 01:15:53 +0100 Subject: [PATCH 04/37] Added failing tests for #1099. --- .../AvaloniaObjectTests_Inheritance.cs | 16 ++++++++++ .../ContentPresenterTests_InTemplate.cs | 28 +++++++++++++++- .../ContentPresenterTests_Standalone.cs | 32 ++++++++++++++++++- 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs index ef6e03a60b..c465db50fb 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.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 System.Collections.Generic; using Xunit; namespace Avalonia.Base.UnitTests @@ -115,6 +116,21 @@ namespace Avalonia.Base.UnitTests Assert.True(raised); } + [Fact] + public void PropertyChanged_Is_Raised_In_Parent_Before_Child() + { + var parent = new Class1(); + var child = new Class2 { Parent = parent }; + var result = new List(); + + parent.PropertyChanged += (s, e) => result.Add(parent); + child.PropertyChanged += (s, e) => result.Add(child); + + parent.SetValue(Class1.BazProperty, "changed"); + + Assert.Equal(new[] { parent, child }, result); + } + private class Class1 : AvaloniaObject { public static readonly StyledProperty FooProperty = diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs index a524ca3e89..708e934214 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs @@ -4,6 +4,7 @@ using System.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.LogicalTree; using Avalonia.UnitTests; using Avalonia.VisualTree; @@ -266,6 +267,31 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.IsType(target.Child); } + + [Fact] + public void Should_Not_Bind_Old_Child_To_New_DataContext() + { + // Test for issue #1099. + var textBlock = new TextBlock + { + [!TextBlock.TextProperty] = new Binding(), + }; + + var (target, host) = CreateTarget(); + host.DataTemplates.Add(new FuncDataTemplate(x => textBlock)); + host.DataTemplates.Add(new FuncDataTemplate(x => new Canvas())); + + target.Content = "foo"; + Assert.Same(textBlock, target.Child); + + textBlock.PropertyChanged += (s, e) => + { + Assert.NotEqual(e.NewValue, "42"); + }; + + target.Content = 42; + } + (ContentPresenter presenter, ContentControl templatedParent) CreateTarget() { var templatedParent = new ContentControl @@ -288,4 +314,4 @@ namespace Avalonia.Controls.UnitTests.Presenters public IControl Child { get; set; } } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs index 9d65f2cba7..2facee16b7 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs @@ -14,6 +14,7 @@ using System.Linq; using Xunit; using Avalonia.Rendering; using Avalonia.Media; +using Avalonia.Data; namespace Avalonia.Controls.UnitTests.Presenters { @@ -204,7 +205,6 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.NotEqual(foo, logicalChildren.First()); } - [Fact] public void Changing_Background_Brush_Color_Should_Invalidate_Visual() { @@ -221,5 +221,35 @@ namespace Avalonia.Controls.UnitTests.Presenters renderer.Verify(x => x.AddDirty(target), Times.Once); } + + [Fact] + public void Should_Not_Bind_Old_Child_To_New_DataContext() + { + // Test for issue #1099. + var textBlock = new TextBlock + { + [!TextBlock.TextProperty] = new Binding(), + }; + + var target = new ContentPresenter() + { + DataTemplates = + { + new FuncDataTemplate(x => textBlock), + new FuncDataTemplate(x => new Canvas()), + }, + }; + + var root = new TestRoot(target); + target.Content = "foo"; + Assert.Same(textBlock, target.Child); + + textBlock.PropertyChanged += (s, e) => + { + Assert.NotEqual(e.NewValue, "42"); + }; + + target.Content = 42; + } } } From f75f85af2bd6d81b2deb9f571a2abd8e823441a3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 20 Jan 2019 01:19:25 +0100 Subject: [PATCH 05/37] Fix order of inherited property changed events. Previously `PropertyChanged` was raised on the child before the parent because the inherited value change was being notified by listening to the parent `PropertyChanged` event, and this event handler was added first. Added an `InheritablePropertyChanged` event which will be called only after all other property changed events have been raised and only for inheritable properties. --- src/Avalonia.Base/AvaloniaObject.cs | 37 +++++++++---------- src/Avalonia.Base/IAvaloniaObject.cs | 7 +++- .../SelectorTests_Child.cs | 1 + .../SelectorTests_Descendent.cs | 1 + .../TestControlBase.cs | 1 + .../TestTemplatedControl.cs | 1 + 6 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 7e8d733f1b..28148f6568 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -22,27 +22,11 @@ namespace Avalonia /// public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged { - /// - /// The parent object that inherited values are inherited from. - /// private IAvaloniaObject _inheritanceParent; - - /// - /// Maintains a list of direct property binding subscriptions so that the binding source - /// doesn't get collected. - /// private List _directBindings; - - /// - /// Event handler for implementation. - /// private PropertyChangedEventHandler _inpcChanged; - - /// - /// Event handler for implementation. - /// private EventHandler _propertyChanged; - + private EventHandler _inheritablePropertyChanged; private ValueStore _values; private ValueStore Values => _values ?? (_values = new ValueStore(this)); @@ -98,6 +82,15 @@ namespace Avalonia remove { _inpcChanged -= value; } } + /// + /// Raised when an inheritable value changes on this object. + /// + event EventHandler IAvaloniaObject.InheritablePropertyChanged + { + add { _inheritablePropertyChanged += value; } + remove { _inheritablePropertyChanged -= value; } + } + /// /// Gets or sets the parent object that inherited values /// are inherited from. @@ -118,8 +111,9 @@ namespace Avalonia { if (_inheritanceParent != null) { - _inheritanceParent.PropertyChanged -= ParentPropertyChanged; + _inheritanceParent.InheritablePropertyChanged -= ParentPropertyChanged; } + var properties = AvaloniaPropertyRegistry.Instance.GetRegistered(this) .Concat(AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(this.GetType())); var inherited = (from property in properties @@ -144,7 +138,7 @@ namespace Avalonia if (_inheritanceParent != null) { - _inheritanceParent.PropertyChanged += ParentPropertyChanged; + _inheritanceParent.InheritablePropertyChanged += ParentPropertyChanged; } } } @@ -509,6 +503,11 @@ namespace Avalonia PropertyChangedEventArgs e2 = new PropertyChangedEventArgs(property.Name); _inpcChanged(this, e2); } + + if (property.Inherits) + { + _inheritablePropertyChanged?.Invoke(this, e); + } } finally { diff --git a/src/Avalonia.Base/IAvaloniaObject.cs b/src/Avalonia.Base/IAvaloniaObject.cs index c11f8ada7e..5a3829167a 100644 --- a/src/Avalonia.Base/IAvaloniaObject.cs +++ b/src/Avalonia.Base/IAvaloniaObject.cs @@ -16,6 +16,11 @@ namespace Avalonia /// event EventHandler PropertyChanged; + /// + /// Raised when an inheritable value changes on this object. + /// + event EventHandler InheritablePropertyChanged; + /// /// Gets a value. /// @@ -97,4 +102,4 @@ namespace Avalonia IObservable source, BindingPriority priority = BindingPriority.LocalValue); } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs index 0561837ad1..c6eeb1ec0e 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs @@ -89,6 +89,7 @@ namespace Avalonia.Styling.UnitTests } public event EventHandler PropertyChanged; + public event EventHandler InheritablePropertyChanged; public event EventHandler AttachedToLogicalTree; public event EventHandler DetachedFromLogicalTree; diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs index 56dad13186..aef539becd 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs @@ -119,6 +119,7 @@ namespace Avalonia.Styling.UnitTests } public event EventHandler PropertyChanged; + public event EventHandler InheritablePropertyChanged; public event EventHandler AttachedToLogicalTree; public event EventHandler DetachedFromLogicalTree; diff --git a/tests/Avalonia.Styling.UnitTests/TestControlBase.cs b/tests/Avalonia.Styling.UnitTests/TestControlBase.cs index 82be755a39..d9fabf6f5d 100644 --- a/tests/Avalonia.Styling.UnitTests/TestControlBase.cs +++ b/tests/Avalonia.Styling.UnitTests/TestControlBase.cs @@ -19,6 +19,7 @@ namespace Avalonia.Styling.UnitTests #pragma warning disable CS0067 // Event not used public event EventHandler PropertyChanged; + public event EventHandler InheritablePropertyChanged; #pragma warning restore CS0067 public string Name { get; set; } diff --git a/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs b/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs index 03b2f03bf2..e92ac36e8f 100644 --- a/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs +++ b/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs @@ -12,6 +12,7 @@ namespace Avalonia.Styling.UnitTests public abstract class TestTemplatedControl : ITemplatedControl, IStyleable { public event EventHandler PropertyChanged; + public event EventHandler InheritablePropertyChanged; public abstract Classes Classes { From f54e48d9a2027ec491c2aeab0a59465f0c525ea8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 22 Jan 2019 18:44:32 +0100 Subject: [PATCH 06/37] Added ContentPresenter.ChildChanging. And listen for this event in `ContentControlMixin` in order to remove the logical child before setting the `DataContext`. Fixes #1099 --- .../Mixins/ContentControlMixin.cs | 29 ++++++++++--- .../Presenters/ContentPresenter.cs | 42 +++++++++++++++---- .../Presenters/IContentPresenter.cs | 15 ++++++- 3 files changed, 72 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs b/src/Avalonia.Controls/Mixins/ContentControlMixin.cs index c4da00f5d0..25b29e37e6 100644 --- a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs +++ b/src/Avalonia.Controls/Mixins/ContentControlMixin.cs @@ -19,8 +19,8 @@ namespace Avalonia.Controls.Mixins /// /// The adds behavior to a control which acts as a content /// control such as and . It - /// updates keeps the control's logical children in sync with the content being displayed by - /// the control. + /// keeps the control's logical children in sync with the content being displayed by the + /// control. /// public class ContentControlMixin { @@ -49,25 +49,42 @@ namespace Avalonia.Controls.Mixins Contract.Requires(content != null); Contract.Requires(logicalChildrenSelector != null); + void ChildChanging(object s, AvaloniaPropertyChangedEventArgs e) + { + if (s is IControl sender && sender?.TemplatedParent is TControl parent) + { + UpdateLogicalChild( + sender, + logicalChildrenSelector(parent), + e.OldValue, + null); + } + } + void TemplateApplied(object s, RoutedEventArgs ev) { if (s is TControl sender) { var e = (TemplateAppliedEventArgs)ev; - var presenter = (IControl)e.NameScope.Find(presenterName); + var presenter = e.NameScope.Find(presenterName) as IContentPresenter; if (presenter != null) { presenter.ApplyTemplate(); var logicalChildren = logicalChildrenSelector(sender); - var subscription = presenter + var subscription = new CompositeDisposable(); + + presenter.ChildChanging += ChildChanging; + subscription.Add(Disposable.Create(() => presenter.ChildChanging -= ChildChanging)); + + subscription.Add(presenter .GetPropertyChangedObservable(ContentPresenter.ChildProperty) .Subscribe(c => UpdateLogicalChild( sender, logicalChildren, - c.OldValue, - c.NewValue)); + null, + c.NewValue))); UpdateLogicalChild( sender, diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 83d8616e90..49f268c128 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -5,6 +5,7 @@ using System; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; +using Avalonia.Data; using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Media; @@ -82,6 +83,7 @@ namespace Avalonia.Controls.Presenters private IControl _child; private bool _createdChild; + EventHandler _childChanging; private IDataTemplate _dataTemplate; private readonly BorderRenderHelper _borderRenderer = new BorderRenderHelper(); @@ -188,6 +190,13 @@ namespace Avalonia.Controls.Presenters set { SetValue(PaddingProperty, value); } } + /// + event EventHandler IContentPresenter.ChildChanging + { + add => _childChanging += value; + remove => _childChanging -= value; + } + /// public sealed override void ApplyTemplate() { @@ -215,9 +224,30 @@ namespace Avalonia.Controls.Presenters var newChild = CreateChild(); // Remove the old child if we're not recycling it. - if (oldChild != null && newChild != oldChild) + if (newChild != oldChild) { - VisualChildren.Remove(oldChild); + if (oldChild != null) + { + VisualChildren.Remove(oldChild); + } + + if (oldChild?.Parent == this) + { + // If we're the child's parent then the presenter isn't in a ContentControl's + // template. + LogicalChildren.Remove(oldChild); + } + else + { + // If we're in a ContentControl's template then invoke ChildChanging to let + // ContentControlMixin handle removing the logical child. + _childChanging?.Invoke(this, new AvaloniaPropertyChangedEventArgs( + this, + ChildProperty, + oldChild, + newChild, + BindingPriority.LocalValue)); + } } // Set the DataContext if the data isn't a control. @@ -241,11 +271,9 @@ namespace Avalonia.Controls.Presenters Child = newChild; - if (oldChild?.Parent == this) - { - LogicalChildren.Remove(oldChild); - } - + // If we're in a ContentControl's template then the child's parent will have been + // set by ContentControlMixin in response to Child changing. If not, then we're + // standalone and should make the control our own logical child. if (newChild.Parent == null && TemplatedParent == null) { LogicalChildren.Add(newChild); diff --git a/src/Avalonia.Controls/Presenters/IContentPresenter.cs b/src/Avalonia.Controls/Presenters/IContentPresenter.cs index 3b8039f33c..78bffec93b 100644 --- a/src/Avalonia.Controls/Presenters/IContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/IContentPresenter.cs @@ -1,6 +1,8 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; +using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; namespace Avalonia.Controls.Presenters @@ -20,5 +22,16 @@ namespace Avalonia.Controls.Presenters /// Gets or sets the content to be displayed by the presenter. /// object Content { get; set; } + + /// + /// Raised when property is about to change. + /// + /// + /// This event should be raised after the child has been removed from the visual tree, + /// but before the property has changed. It is intended for consumption + /// by in order to update the host control's logical + /// children. + /// + event EventHandler ChildChanging; } -} \ No newline at end of file +} From fafa6952570d264ad41d181d2dde6b527519f29c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 22 Jan 2019 19:13:16 +0100 Subject: [PATCH 07/37] Use recommended ContentControl pattern. --- src/Avalonia.Controls/ContentControl.cs | 14 ++++++- .../Primitives/HeaderedContentControl.cs | 37 ++++++++++++++++++- .../Primitives/HeaderedItemsControl.cs | 21 ++++++++--- ...ol.cs => HeaderedSelectingItemsControl.cs} | 21 ++++++++--- .../HeaderedItemsControlTests .cs | 2 +- tests/Avalonia.UnitTests/TestTemplatedRoot.cs | 5 ++- 6 files changed, 85 insertions(+), 15 deletions(-) rename src/Avalonia.Controls/Primitives/{HeaderedSelectingControl.cs => HeaderedSelectingItemsControl.cs} (71%) diff --git a/src/Avalonia.Controls/ContentControl.cs b/src/Avalonia.Controls/ContentControl.cs index 6da6da54a5..16f17ae1bd 100644 --- a/src/Avalonia.Controls/ContentControl.cs +++ b/src/Avalonia.Controls/ContentControl.cs @@ -97,7 +97,19 @@ namespace Avalonia.Controls /// void IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) { - Presenter = presenter; + RegisterContentPresenter(presenter); + } + + /// + /// Called when an is registered with the control. + /// + /// The presenter. + protected virtual void RegisterContentPresenter(IContentPresenter presenter) + { + if (presenter.Name == "PART_ContentPresenter") + { + Presenter = presenter; + } } } } diff --git a/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs b/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs index 7a46e0f776..98476c9c94 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs @@ -1,6 +1,8 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Avalonia.Controls.Mixins; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; namespace Avalonia.Controls.Primitives @@ -20,7 +22,18 @@ namespace Avalonia.Controls.Primitives /// Defines the property. /// public static readonly StyledProperty HeaderTemplateProperty = - AvaloniaProperty.Register(nameof(HeaderTemplate)); + AvaloniaProperty.Register(nameof(HeaderTemplate)); + + /// + /// Initializes static members of the class. + /// + static HeaderedContentControl() + { + ContentControlMixin.Attach( + HeaderProperty, + x => x.LogicalChildren, + "PART_HeaderPresenter"); + } /// /// Gets or sets the header content. @@ -29,7 +42,16 @@ namespace Avalonia.Controls.Primitives { get { return GetValue(HeaderProperty); } set { SetValue(HeaderProperty, value); } - } + } + + /// + /// Gets the header presenter from the control's template. + /// + public IContentPresenter HeaderPresenter + { + get; + private set; + } /// /// Gets or sets the data template used to display the header content of the control. @@ -39,5 +61,16 @@ namespace Avalonia.Controls.Primitives get { return GetValue(HeaderTemplateProperty); } set { SetValue(HeaderTemplateProperty, value); } } + + /// + protected override void RegisterContentPresenter(IContentPresenter presenter) + { + base.RegisterContentPresenter(presenter); + + if (presenter.Name == "PART_HeaderPresenter") + { + HeaderPresenter = presenter; + } + } } } diff --git a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs index c5aa73e56a..bda426c23b 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs @@ -9,7 +9,7 @@ namespace Avalonia.Controls.Primitives /// /// Represents an with a related header. /// - public class HeaderedItemsControl : ItemsControl + public class HeaderedItemsControl : ItemsControl, IContentPresenterHost { /// /// Defines the property. @@ -40,17 +40,28 @@ namespace Avalonia.Controls.Primitives /// /// Gets the header presenter from the control's template. /// - public ContentPresenter HeaderPresenter + public IContentPresenter HeaderPresenter { get; private set; } /// - protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + void IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) { - HeaderPresenter = e.NameScope.Find("PART_HeaderPresenter"); - base.OnTemplateApplied(e); + RegisterContentPresenter(presenter); + } + + /// + /// Called when an is registered with the control. + /// + /// The presenter. + protected virtual void RegisterContentPresenter(IContentPresenter presenter) + { + if (presenter.Name == "PART_HeaderPresenter") + { + HeaderPresenter = presenter; + } } } } diff --git a/src/Avalonia.Controls/Primitives/HeaderedSelectingControl.cs b/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs similarity index 71% rename from src/Avalonia.Controls/Primitives/HeaderedSelectingControl.cs rename to src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs index 87bb079ae7..d59be66b2b 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedSelectingControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs @@ -9,7 +9,7 @@ namespace Avalonia.Controls.Primitives /// /// Represents a with a related header. /// - public class HeaderedSelectingItemsControl : SelectingItemsControl + public class HeaderedSelectingItemsControl : SelectingItemsControl, IContentPresenterHost { /// /// Defines the property. @@ -40,17 +40,28 @@ namespace Avalonia.Controls.Primitives /// /// Gets the header presenter from the control's template. /// - public ContentPresenter HeaderPresenter + public IContentPresenter HeaderPresenter { get; private set; } /// - protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + void IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) { - base.OnTemplateApplied(e); - HeaderPresenter = e.NameScope.Find("PART_HeaderPresenter"); + RegisterContentPresenter(presenter); + } + + /// + /// Called when an is registered with the control. + /// + /// The presenter. + protected virtual void RegisterContentPresenter(IContentPresenter presenter) + { + if (presenter.Name == "PART_HeaderPresenter") + { + HeaderPresenter = presenter; + } } } } diff --git a/tests/Avalonia.Controls.UnitTests/HeaderedItemsControlTests .cs b/tests/Avalonia.Controls.UnitTests/HeaderedItemsControlTests .cs index 570d619963..66789ef874 100644 --- a/tests/Avalonia.Controls.UnitTests/HeaderedItemsControlTests .cs +++ b/tests/Avalonia.Controls.UnitTests/HeaderedItemsControlTests .cs @@ -37,7 +37,7 @@ namespace Avalonia.Controls.UnitTests target.Header = "Foo"; target.ApplyTemplate(); - target.HeaderPresenter.UpdateChild(); + ((ContentPresenter)target.HeaderPresenter).UpdateChild(); var child = target.HeaderPresenter.Child; diff --git a/tests/Avalonia.UnitTests/TestTemplatedRoot.cs b/tests/Avalonia.UnitTests/TestTemplatedRoot.cs index 5d42699d3f..ef49bd2f5c 100644 --- a/tests/Avalonia.UnitTests/TestTemplatedRoot.cs +++ b/tests/Avalonia.UnitTests/TestTemplatedRoot.cs @@ -18,7 +18,10 @@ namespace Avalonia.UnitTests public TestTemplatedRoot() { - Template = new FuncControlTemplate(x => new ContentPresenter()); + Template = new FuncControlTemplate(x => new ContentPresenter + { + Name = "PART_ContentPresenter", + }); } public event EventHandler Registered From af83673f918373f904f91b7abc43c683669602ea Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 7 Feb 2019 16:46:59 +0200 Subject: [PATCH 08/37] make DropDown support mouse wheel --- src/Avalonia.Controls/DropDown.cs | 53 ++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/DropDown.cs b/src/Avalonia.Controls/DropDown.cs index 93b33e0589..684279a3cd 100644 --- a/src/Avalonia.Controls/DropDown.cs +++ b/src/Avalonia.Controls/DropDown.cs @@ -56,6 +56,7 @@ namespace Avalonia.Controls private bool _isDropDownOpen; private Popup _popup; private object _selectionBoxItem; + private IDisposable _subscriptionsOnOpen; /// /// Initializes static members of the class. @@ -174,6 +175,37 @@ namespace Avalonia.Controls } } + /// + protected override void OnPointerWheelChanged(PointerWheelEventArgs e) + { + base.OnPointerWheelChanged(e); + + if (!e.Handled) + { + if (!IsDropDownOpen) + { + if (IsFocused) + { + if (e.Delta.Y < 0) + { + if (++SelectedIndex >= ItemCount) + SelectedIndex = 0; + } + else + { + if (--SelectedIndex < 0) + SelectedIndex = ItemCount - 1; + } + e.Handled = true; + } + } + else + { + e.Handled = true; + } + } + } + /// protected override void OnPointerPressed(PointerPressedEventArgs e) { @@ -223,6 +255,9 @@ namespace Avalonia.Controls private void PopupClosed(object sender, EventArgs e) { + _subscriptionsOnOpen?.Dispose(); + _subscriptionsOnOpen = null; + if (CanFocus(this)) { Focus(); @@ -232,6 +267,22 @@ namespace Avalonia.Controls private void PopupOpened(object sender, EventArgs e) { TryFocusSelectedItem(); + + _subscriptionsOnOpen?.Dispose(); + _subscriptionsOnOpen = null; + + var toplevel = this.GetVisualRoot() as TopLevel; + if (toplevel != null) + { + _subscriptionsOnOpen = toplevel.AddHandler(PointerWheelChangedEvent, (s, ev) => + { + //eat wheel scroll event outside dropdown popup while it's open + if (IsDropDownOpen && (ev.Source as IVisual).GetVisualRoot() == toplevel) + { + ev.Handled = true; + } + }, Interactivity.RoutingStrategies.Tunnel); + } } private void SelectedItemChanged(AvaloniaPropertyChangedEventArgs e) @@ -247,7 +298,7 @@ namespace Avalonia.Controls { var container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); - if(container == null && SelectedItems.Count > 0) + if (container == null && SelectedItems.Count > 0) { ScrollIntoView(SelectedItems[0]); container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); From f285c38d2766368767f2fee844743fd7365af821 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 7 Feb 2019 17:22:56 +0200 Subject: [PATCH 09/37] improve dropdown key up/down and mouse wheel auto selections so they don't go through unselected state --- src/Avalonia.Controls/DropDown.cs | 39 ++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/DropDown.cs b/src/Avalonia.Controls/DropDown.cs index 684279a3cd..4f65a0aed4 100644 --- a/src/Avalonia.Controls/DropDown.cs +++ b/src/Avalonia.Controls/DropDown.cs @@ -150,16 +150,12 @@ namespace Avalonia.Controls { if (e.Key == Key.Down) { - if (++SelectedIndex >= ItemCount) - SelectedIndex = 0; - + SelectNext(); e.Handled = true; } else if (e.Key == Key.Up) { - if (--SelectedIndex < 0) - SelectedIndex = ItemCount - 1; - + SelectPrev(); e.Handled = true; } } @@ -187,15 +183,10 @@ namespace Avalonia.Controls if (IsFocused) { if (e.Delta.Y < 0) - { - if (++SelectedIndex >= ItemCount) - SelectedIndex = 0; - } + SelectNext(); else - { - if (--SelectedIndex < 0) - SelectedIndex = ItemCount - 1; - } + SelectPrev(); + e.Handled = true; } } @@ -358,5 +349,25 @@ namespace Avalonia.Controls } } } + + private void SelectNext() + { + int next = SelectedIndex + 1; + + if (next >= ItemCount) + next = 0; + + SelectedIndex = next; + } + + private void SelectPrev() + { + int prev = SelectedIndex - 1; + + if (prev < 0) + prev = ItemCount - 1; + + SelectedIndex = prev; + } } } From 990bc26d7b42c9437b128c0955cc79639a36f4d9 Mon Sep 17 00:00:00 2001 From: artyom Date: Sun, 10 Feb 2019 12:42:53 +0300 Subject: [PATCH 10/37] Add RoutedViewHost control implementation --- src/Avalonia.ReactiveUI/RoutedViewHost.cs | 157 ++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 src/Avalonia.ReactiveUI/RoutedViewHost.cs diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs new file mode 100644 index 0000000000..8006ef9aa6 --- /dev/null +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -0,0 +1,157 @@ +using System; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Styling; +using ReactiveUI; +using Splat; + +namespace Avalonia +{ + /// + /// This control hosts the View associated with a Router, and will display + /// the View and wire up the ViewModel whenever a new ViewModel is navigated to. + /// + public class RoutedViewHost : UserControl, IActivatable, IEnableLogger + { + /// + /// The router dependency property. + /// + public static readonly AvaloniaProperty RouterProperty = + AvaloniaProperty.Register(nameof(Router)); + + /// + /// The default content property. + /// + public static readonly AvaloniaProperty DefaultContentProperty = + AvaloniaProperty.Register(nameof(DefaultContent)); + + private readonly IAnimation _fadeOutAnimation = CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25)); + private readonly IAnimation _fadeInAnimation = CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25)); + + /// + /// Initializes a new instance of the class. + /// + public RoutedViewHost() + { + this.WhenActivated(disposables => + { + this.WhenAnyObservable(x => x.Router.CurrentViewModel) + .DistinctUntilChanged() + .Subscribe(HandleViewModelChange) + .DisposeWith(disposables); + }); + } + + /// + /// Gets or sets the ReactiveUI view locator used by this router. + /// + public IViewLocator ViewLocator { get; set; } + + /// + /// Gets or sets the of the view model stack. + /// + public RoutingState Router + { + get => GetValue(RouterProperty); + set => SetValue(RouterProperty, value); + } + + /// + /// Gets or sets the content displayed whenever there is no page currently routed. + /// + public object DefaultContent + { + get => GetValue(DefaultContentProperty); + set => SetValue(DefaultContentProperty, value); + } + + /// + /// Duplicates the Content property with a private setter. + /// + public new object Content + { + get => base.Content; + private set => base.Content = value; + } + + /// + /// Invoked when ReactiveUI router navigates to a view model. + /// + /// ViewModel to which the user navigates. + /// + /// Thrown when ViewLocator is unable to find the appropriate view. + /// + private void HandleViewModelChange(IRoutableViewModel viewModel) + { + if (viewModel == null) + { + this.Log().Info("ViewModel is null, falling back to default content."); + UpdateContent(DefaultContent); + return; + } + + var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current; + var view = viewLocator.ResolveView(viewModel); + if (view == null) throw new Exception($"Couldn't find view for '{viewModel}'. Is it registered?"); + + this.Log().Info($"Ready to show {view} with autowired {viewModel}."); + view.ViewModel = viewModel; + UpdateContent(view); + } + + /// + /// Updates the content with transitions. + /// + /// New content to set. + private async void UpdateContent(object newContent) + { + await _fadeOutAnimation.RunAsync(this, null); + Content = newContent; + await _fadeInAnimation.RunAsync(this, null); + } + + /// + /// Creates opacity animation for this routed view host. + /// + /// Opacity to start from. + /// Opacity to finish with. + /// Duration of the animation. + /// Animation object instance. + private static IAnimation CreateOpacityAnimation(double from, double to, TimeSpan duration) + { + return new Avalonia.Animation.Animation + { + Duration = duration, + Children = + { + new KeyFrame + { + Setters = + { + new Setter + { + Property = OpacityProperty, + Value = from + } + }, + Cue = new Cue(0d) + }, + new KeyFrame + { + Setters = + { + new Setter + { + Property = OpacityProperty, + Value = to + } + }, + Cue = new Cue(1d) + } + } + }; + } + } +} From 6e4b9a23c248a33961a501ef614a753c4b9b805f Mon Sep 17 00:00:00 2001 From: artyom Date: Sun, 10 Feb 2019 16:28:22 +0300 Subject: [PATCH 11/37] Add unit tests for RoutedViewHost --- src/Avalonia.ReactiveUI/RoutedViewHost.cs | 59 +++++++--- .../RoutedViewHostTest.cs | 104 ++++++++++++++++++ 2 files changed, 149 insertions(+), 14 deletions(-) create mode 100644 tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs index 8006ef9aa6..3613d3d259 100644 --- a/src/Avalonia.ReactiveUI/RoutedViewHost.cs +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -26,9 +26,20 @@ namespace Avalonia /// public static readonly AvaloniaProperty DefaultContentProperty = AvaloniaProperty.Register(nameof(DefaultContent)); - - private readonly IAnimation _fadeOutAnimation = CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25)); - private readonly IAnimation _fadeInAnimation = CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25)); + + /// + /// Fade in animation property. + /// + public static readonly AvaloniaProperty FadeInAnimationProperty = + AvaloniaProperty.Register(nameof(DefaultContent), + CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25))); + + /// + /// Fade out animation property. + /// + public static readonly AvaloniaProperty FadeOutAnimationProperty = + AvaloniaProperty.Register(nameof(DefaultContent), + CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25))); /// /// Initializes a new instance of the class. @@ -39,16 +50,11 @@ namespace Avalonia { this.WhenAnyObservable(x => x.Router.CurrentViewModel) .DistinctUntilChanged() - .Subscribe(HandleViewModelChange) + .Subscribe(NavigateToViewModel) .DisposeWith(disposables); }); } - /// - /// Gets or sets the ReactiveUI view locator used by this router. - /// - public IViewLocator ViewLocator { get; set; } - /// /// Gets or sets the of the view model stack. /// @@ -66,15 +72,38 @@ namespace Avalonia get => GetValue(DefaultContentProperty); set => SetValue(DefaultContentProperty, value); } + + /// + /// Gets or sets the animation played when page appears. + /// + public IAnimation FadeInAnimation + { + get => GetValue(FadeInAnimationProperty); + set => SetValue(FadeInAnimationProperty, value); + } + + /// + /// Gets or sets the animation played when page disappears. + /// + public IAnimation FadeOutAnimation + { + get => GetValue(FadeOutAnimationProperty); + set => SetValue(FadeOutAnimationProperty, value); + } /// /// Duplicates the Content property with a private setter. /// public new object Content { - get => base.Content; + get => base.Content ?? DefaultContent; private set => base.Content = value; } + + /// + /// Gets or sets the ReactiveUI view locator used by this router. + /// + public IViewLocator ViewLocator { get; set; } /// /// Invoked when ReactiveUI router navigates to a view model. @@ -83,12 +112,12 @@ namespace Avalonia /// /// Thrown when ViewLocator is unable to find the appropriate view. /// - private void HandleViewModelChange(IRoutableViewModel viewModel) + private void NavigateToViewModel(IRoutableViewModel viewModel) { if (viewModel == null) { this.Log().Info("ViewModel is null, falling back to default content."); - UpdateContent(DefaultContent); + UpdateContent(null); return; } @@ -107,9 +136,11 @@ namespace Avalonia /// New content to set. private async void UpdateContent(object newContent) { - await _fadeOutAnimation.RunAsync(this, null); + if (FadeOutAnimation != null) + await FadeOutAnimation.RunAsync(this, Clock); Content = newContent; - await _fadeInAnimation.RunAsync(this, null); + if (FadeInAnimation != null) + await FadeInAnimation.RunAsync(this, Clock); } /// diff --git a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs new file mode 100644 index 0000000000..c85b999af2 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs @@ -0,0 +1,104 @@ +using System; +using System.Reactive.Concurrency; +using System.Reactive.Disposables; +using Avalonia.Controls; +using Avalonia.Rendering; +using Avalonia.Platform; +using Avalonia.UnitTests; +using Avalonia; +using ReactiveUI; +using DynamicData; +using Xunit; +using Splat; +using Avalonia.Markup.Xaml; +using System.ComponentModel; +using System.Threading.Tasks; +using System.Reactive; + +namespace Avalonia +{ + public class RoutedViewHostTest + { + public class FirstRoutableViewModel : ReactiveObject, IRoutableViewModel + { + public string UrlPathSegment => "first"; + + public IScreen HostScreen { get; set; } + } + + public class FirstRoutableView : ReactiveUserControl { } + + public class SecondRoutableViewModel : ReactiveObject, IRoutableViewModel + { + public string UrlPathSegment => "second"; + + public IScreen HostScreen { get; set; } + } + + public class SecondRoutableView : ReactiveUserControl { } + + public class ScreenViewModel : ReactiveObject, IScreen + { + public RoutingState Router { get; } = new RoutingState(); + } + + public RoutedViewHostTest() + { + Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); + Locator.CurrentMutable.Register(() => new FirstRoutableView(), typeof(IViewFor)); + Locator.CurrentMutable.Register(() => new SecondRoutableView(), typeof(IViewFor)); + } + + [Fact] + public void RoutedViewHostShouldStayInSyncWithRoutingState() + { + var screen = new ScreenViewModel(); + var defaultContent = new TextBlock(); + var host = new RoutedViewHost + { + Router = screen.Router, + DefaultContent = defaultContent, + FadeOutAnimation = null, + FadeInAnimation = null + }; + + var root = new TestRoot + { + Child = host + }; + + Assert.NotNull(host.Content); + Assert.Equal(typeof(TextBlock), host.Content.GetType()); + Assert.Equal(defaultContent, host.Content); + + screen.Router.Navigate + .Execute(new FirstRoutableViewModel()) + .Subscribe(); + + Assert.NotNull(host.Content); + Assert.Equal(typeof(FirstRoutableView), host.Content.GetType()); + + screen.Router.Navigate + .Execute(new SecondRoutableViewModel()) + .Subscribe(); + + Assert.NotNull(host.Content); + Assert.Equal(typeof(SecondRoutableView), host.Content.GetType()); + + screen.Router.NavigateBack + .Execute(Unit.Default) + .Subscribe(); + + Assert.NotNull(host.Content); + Assert.Equal(typeof(FirstRoutableView), host.Content.GetType()); + + screen.Router.NavigateBack + .Execute(Unit.Default) + .Subscribe(); + + Assert.NotNull(host.Content); + Assert.Equal(typeof(TextBlock), host.Content.GetType()); + Assert.Equal(defaultContent, host.Content); + } + } +} \ No newline at end of file From 724ec9b8bb0ee923632e7e43d764a957c6f7543e Mon Sep 17 00:00:00 2001 From: artyom Date: Sun, 10 Feb 2019 18:21:51 +0300 Subject: [PATCH 12/37] Add xml docs and usage examples --- src/Avalonia.ReactiveUI/RoutedViewHost.cs | 44 ++++++++++++++++++++--- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs index 3613d3d259..726e086d9c 100644 --- a/src/Avalonia.ReactiveUI/RoutedViewHost.cs +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -10,9 +10,45 @@ using Splat; namespace Avalonia { /// - /// This control hosts the View associated with a Router, and will display - /// the View and wire up the ViewModel whenever a new ViewModel is navigated to. + /// This control hosts the View associated with ReactiveUI RoutingState, + /// and will display the View and wire up the ViewModel whenever a new + /// ViewModel is navigated to. Nested routing is also supported. /// + /// + /// + /// ReactiveUI routing consists of an IScreen that contains current + /// RoutingState, several IRoutableViewModels, and a platform-specific + /// XAML control called RoutedViewHost. + /// + /// + /// RoutingState manages the ViewModel navigation stack and allows + /// ViewModels to navigate to other ViewModels. IScreen is the root of + /// a navigation stack; despite the name, its views don't have to occupy + /// the whole screen. RoutedViewHost monitors an instance of RoutingState, + /// responding to any changes in the navigation stack by creating and + /// embedding the appropriate view. + /// + /// + /// Place this control to a view containing your ViewModel that implements + /// IScreen, and bind IScreen.Router property to RoutedViewHost.Router property. + /// + /// + /// + /// + /// + /// + /// ]]> + /// + /// + /// + /// See + /// ReactiveUI routing documentation website for more info. + /// + /// public class RoutedViewHost : UserControl, IActivatable, IEnableLogger { /// @@ -96,7 +132,7 @@ namespace Avalonia /// public new object Content { - get => base.Content ?? DefaultContent; + get => base.Content; private set => base.Content = value; } @@ -117,7 +153,7 @@ namespace Avalonia if (viewModel == null) { this.Log().Info("ViewModel is null, falling back to default content."); - UpdateContent(null); + UpdateContent(DefaultContent); return; } From 870a2f365f6409a8940616ccad5baa36dc067886 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Tue, 12 Feb 2019 16:45:04 +0200 Subject: [PATCH 13/37] layoutmanager/listbox/vistualization/scroll issue unit test --- .../ListBoxTests.cs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index b6f7c9ec96..6939c6a081 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -267,6 +267,71 @@ namespace Avalonia.Controls.UnitTests Assert.True(true); } + [Fact] + public void LayoutManager_Should_Measure_Arrange_All() + { + var virtualizationMode = ItemVirtualizationMode.Simple; + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var items = new AvaloniaList(Enumerable.Range(1, 7).Select(v => v.ToString())); + + var wnd = new Window() { SizeToContent = SizeToContent.WidthAndHeight }; + + wnd.IsVisible = true; + + var target = new ListBox(); + + wnd.Content = target; + + var lm = wnd.LayoutManager; + + target.Height = 110; + target.Width = 50; + target.DataContext = items; + target.VirtualizationMode = virtualizationMode; + + target.ItemTemplate = new FuncDataTemplate(c => + { + var tb = new TextBlock() { Height = 10, Width = 30 }; + tb.Bind(TextBlock.TextProperty, new Data.Binding()); + return tb; + }, true); + + lm.ExecuteInitialLayoutPass(wnd); + + target.Items = items; + + lm.ExecuteLayoutPass(); + + items.Insert(3, "3+"); + lm.ExecuteLayoutPass(); + + items.Insert(4, "4+"); + lm.ExecuteLayoutPass(); + + //RESET + items.Clear(); + foreach (var i in Enumerable.Range(1, 7)) + { + items.Add(i.ToString()); + } + + //working bit better with this line no outof memory or remaining to arrange/measure ??? + //lm.ExecuteLayoutPass(); + + items.Insert(2, "2+"); + + lm.ExecuteLayoutPass(); + + var flags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic; + var toMeasure = lm.GetType().GetField("_toMeasure", flags).GetValue(lm) as System.Collections.Generic.IEnumerable; + var toArrange = lm.GetType().GetField("_toArrange", flags).GetValue(lm) as System.Collections.Generic.IEnumerable; + + Assert.Equal(0, toMeasure.Count()); + Assert.Equal(0, toArrange.Count()); + } + } + private FuncControlTemplate ListBoxTemplate() { return new FuncControlTemplate(parent => From 91e678baf858054b39ee716c0a6a4d17f23f3bb3 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Tue, 12 Feb 2019 11:19:06 +0200 Subject: [PATCH 14/37] call canexecute before execute for button/menu command --- src/Avalonia.Controls/Button.cs | 2 +- src/Avalonia.Controls/MenuItem.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index d485924885..f572c67284 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -217,7 +217,7 @@ namespace Avalonia.Controls var e = new RoutedEventArgs(ClickEvent); RaiseEvent(e); - if (Command != null) + if (!e.Handled && Command?.CanExecute(CommandParameter) == true) { Command.Execute(CommandParameter); e.Handled = true; diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 055d49fb0b..aa20ee0595 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -287,7 +287,7 @@ namespace Avalonia.Controls /// The click event args. protected virtual void OnClick(RoutedEventArgs e) { - if (Command != null) + if (!e.Handled && Command?.CanExecute(CommandParameter) == true) { Command.Execute(CommandParameter); e.Handled = true; From 131a4d90eff1a65d4484a55e1a10280d4dfa90d5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 15 Feb 2019 11:09:34 +0100 Subject: [PATCH 15/37] Remove Avalonia.ISupportInitialize. This is a relic from when we were targeting a PCL profile that didn't have `System.ComponentModel.ISupportInitialize`. Now that we have that, use it instead. --- src/Avalonia.Base/ISupportInitialize.cs | 22 ------------ src/Avalonia.Controls/Control.cs | 1 + .../Embedding/EmbeddableControlRoot.cs | 1 + .../Embedding/Offscreen/OffscreenTopLevel.cs | 1 + src/Avalonia.Controls/WindowBase.cs | 1 + .../AvaloniaXamlLoader.cs | 1 + .../PortableXaml/AvaloniaXamlObjectWriter.cs | 35 ------------------- .../Primitives/SelectingItemsControlTests.cs | 1 + .../Xaml/InitializationOrderTracker.cs | 3 +- .../StyledElementTests.cs | 1 + 10 files changed, 9 insertions(+), 58 deletions(-) delete mode 100644 src/Avalonia.Base/ISupportInitialize.cs diff --git a/src/Avalonia.Base/ISupportInitialize.cs b/src/Avalonia.Base/ISupportInitialize.cs deleted file mode 100644 index 04e3d72e6c..0000000000 --- a/src/Avalonia.Base/ISupportInitialize.cs +++ /dev/null @@ -1,22 +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. - -namespace Avalonia -{ - /// - /// Specifies that this object supports a simple, transacted notification for batch - /// initialization. - /// - public interface ISupportInitialize - { - /// - /// Signals the object that initialization is starting. - /// - void BeginInit(); - - /// - /// Signals the object that initialization is complete. - /// - void EndInit(); - } -} diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index a00d586233..a7ee027e70 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.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 System.ComponentModel; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; diff --git a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs index 224af979ab..43beb923e5 100644 --- a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs +++ b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Platform; diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs index 8b39cc03b8..c4f83ffd54 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using Avalonia.Styling; namespace Avalonia.Controls.Embedding.Offscreen diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 56ffd315f1..363af05a0b 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; diff --git a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs index 2720e674cc..b99864b050 100644 --- a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs +++ b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs @@ -8,6 +8,7 @@ using Avalonia.Platform; using Portable.Xaml; using System; using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Reflection; using System.Runtime.Serialization; diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs index 5d1a98f6f8..9fa6c26c35 100644 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs @@ -77,40 +77,15 @@ namespace Avalonia.Markup.Xaml.PortableXaml _delayedValuesHelper.ApplyAll(); } - protected internal override void OnAfterBeginInit(object value) - { - //not called for avalonia objects - //as it's called inly for - //Portable.Xaml.ComponentModel.ISupportInitialize - base.OnAfterBeginInit(value); - } - - protected internal override void OnAfterEndInit(object value) - { - //not called for avalonia objects - //as it's called inly for - //Portable.Xaml.ComponentModel.ISupportInitialize - base.OnAfterEndInit(value); - } - protected internal override void OnAfterProperties(object value) { _delayedValuesHelper.EndInit(value); base.OnAfterProperties(value); - - //AfterEndInit is not called as it supports only - //Portable.Xaml.ComponentModel.ISupportInitialize - //and we have Avalonia.ISupportInitialize so we need some hacks - HandleEndEdit(value); } protected internal override void OnBeforeProperties(object value) { - //OnAfterBeginInit is not called as it supports only - //Portable.Xaml.ComponentModel.ISupportInitialize - //and we have Avalonia.ISupportInitialize so we need some hacks - HandleBeginInit(value); if (value != null) _delayedValuesHelper.BeginInit(value); @@ -127,16 +102,6 @@ namespace Avalonia.Markup.Xaml.PortableXaml return base.OnSetValue(target, member, value); } - private void HandleBeginInit(object value) - { - (value as Avalonia.ISupportInitialize)?.BeginInit(); - } - - private void HandleEndEdit(object value) - { - (value as Avalonia.ISupportInitialize)?.EndInit(); - } - public override void WriteStartMember(XamlMember property) { foreach(var d in DesignDirectives) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index bbe1d85acb..2df925301f 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; +using System.ComponentModel; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Presenters; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs index 3ecb2d9f37..104f46cbac 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs @@ -4,6 +4,7 @@ using Avalonia.Controls; using Avalonia.LogicalTree; using System.Collections.Generic; +using System.ComponentModel; namespace Avalonia.Markup.Xaml.UnitTests.Xaml { @@ -39,4 +40,4 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Order.Add($"EndInit {InitState}"); } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs b/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs index 4096dcf380..4970addd81 100644 --- a/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs +++ b/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs @@ -10,6 +10,7 @@ using Avalonia.UnitTests; using Xunit; using Avalonia.LogicalTree; using Avalonia.Controls; +using System.ComponentModel; namespace Avalonia.Styling.UnitTests { From a88c1473da0f05a409bb9c7160ff1a1800791702 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 18 Feb 2019 00:16:02 +0000 Subject: [PATCH 16/37] [OSX/Avalonia.Native] fix NRE in double dispose of ScreenImpl --- src/Avalonia.Native/ScreenImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Native/ScreenImpl.cs b/src/Avalonia.Native/ScreenImpl.cs index c1edd6c846..0729de9b8e 100644 --- a/src/Avalonia.Native/ScreenImpl.cs +++ b/src/Avalonia.Native/ScreenImpl.cs @@ -41,7 +41,7 @@ namespace Avalonia.Native public void Dispose () { - _native.Dispose(); + _native?.Dispose(); _native = null; } } From 71cee6ae5bb3442da56fe0c0cc3dfc19f5a1a589 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Tue, 19 Feb 2019 11:11:42 +0100 Subject: [PATCH 17/37] Fix ItemTemplateProperty being subscribed to on every ItemsControl or derived class construction. --- src/Avalonia.Controls/ItemsControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index d74078c712..3dfeae52a4 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -64,6 +64,7 @@ namespace Avalonia.Controls static ItemsControl() { ItemsProperty.Changed.AddClassHandler(x => x.ItemsChanged); + ItemTemplateProperty.Changed.AddClassHandler(x => x.ItemTemplateChanged); } /// @@ -73,7 +74,6 @@ namespace Avalonia.Controls { PseudoClasses.Add(":empty"); SubscribeToItems(_items); - ItemTemplateProperty.Changed.AddClassHandler(x => x.ItemTemplateChanged); } /// From 2a4cb2c3f6399146f03469745ebd3c70136b6546 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Tue, 19 Feb 2019 11:45:40 +0100 Subject: [PATCH 18/37] Fix Style subscriptions not being removed at all. --- src/Avalonia.Styling/Styling/Style.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index 27fad58346..d799df7ac9 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -143,6 +143,7 @@ namespace Avalonia.Styling } controlSubscriptions.Add(subs); + controlSubscriptions.Add(Disposable.Create(() => Subscriptions.Remove(subs))); Subscriptions.Add(subs); } @@ -159,8 +160,9 @@ namespace Avalonia.Styling var sub = setter.Apply(this, control, null); subs.Add(sub); } - + controlSubscriptions.Add(subs); + controlSubscriptions.Add(Disposable.Create(() => Subscriptions.Remove(subs))); Subscriptions.Add(subs); return true; } @@ -223,7 +225,7 @@ namespace Avalonia.Styling { if (!_applied.TryGetValue(control, out var subscriptions)) { - subscriptions = new CompositeDisposable(2); + subscriptions = new CompositeDisposable(3); subscriptions.Add(control.StyleDetach.Subscribe(ControlDetach)); _applied.Add(control, subscriptions); } From d53dbe9416775f46f62b3e8a0dff832cc5f7c148 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Tue, 19 Feb 2019 14:29:44 +0200 Subject: [PATCH 19/37] add some more layout manager tests for potential issues with infinity loop and proper work --- .../LayoutManagerTests.cs | 119 ++++++++++++++++-- 1 file changed, 110 insertions(+), 9 deletions(-) diff --git a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs index b0e8a3780e..10006609aa 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs @@ -1,11 +1,10 @@ // 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.Collections.Generic; +using System.Linq; using Avalonia.Controls; -using Avalonia.UnitTests; -using System; using Xunit; -using System.Collections.Generic; namespace Avalonia.Layout.UnitTests { @@ -74,7 +73,6 @@ namespace Avalonia.Layout.UnitTests } }; - var order = new List(); Size MeasureOverride(ILayoutable control, Size size) { @@ -110,7 +108,6 @@ namespace Avalonia.Layout.UnitTests } }; - var order = new List(); Size MeasureOverride(ILayoutable control, Size size) { @@ -196,9 +193,9 @@ namespace Avalonia.Layout.UnitTests Width = 100, Height = 100, }; - + var arrangeSize = default(Size); - + root.DoArrangeOverride = (_, s) => { arrangeSize = s; @@ -207,7 +204,7 @@ namespace Avalonia.Layout.UnitTests root.LayoutManager.ExecuteInitialLayoutPass(root); Assert.Equal(new Size(100, 100), arrangeSize); - + root.Width = 120; root.LayoutManager.ExecuteLayoutPass(); @@ -238,7 +235,111 @@ namespace Avalonia.Layout.UnitTests border.Height = 100; root.LayoutManager.ExecuteLayoutPass(); - Assert.Equal(new Size(100, 100), panel.DesiredSize); + Assert.Equal(new Size(100, 100), panel.DesiredSize); + } + + [Fact] + public void LayoutManager_Should_Prevent_Infinity_Loop_On_Measure() + { + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; + + root.LayoutManager.ExecuteInitialLayoutPass(root); + control.Measured = false; + + int cnt = 0; + int maxcnt = 100; + control.DoMeasureOverride = (l, s) => + { + //emulate a problem in the logic of a control that triggers + //invalidate measure during measure + //it can lead to infinity loop in layoutmanager + if (++cnt < maxcnt) + { + control.InvalidateMeasure(); + } + + return new Size(100, 100); + }; + + control.InvalidateMeasure(); + + root.LayoutManager.ExecuteLayoutPass(); + + Assert.True(cnt < 100); + } + + [Fact] + public void LayoutManager_Should_Prevent_Infinity_Loop_On_Arrange() + { + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; + + root.LayoutManager.ExecuteInitialLayoutPass(root); + control.Arranged = false; + + int cnt = 0; + int maxcnt = 100; + control.DoArrangeOverride = (l, s) => + { + //emulate a problem in the logic of a control that triggers + //invalidate measure during arrange + //it can lead to infinity loop in layoutmanager + if (++cnt < maxcnt) + { + control.InvalidateArrange(); + } + + return new Size(100, 100); + }; + + control.InvalidateArrange(); + + root.LayoutManager.ExecuteLayoutPass(); + + Assert.True(cnt < 100); + } + + [Fact] + public void LayoutManager_Should_Properly_Arrange_Visuals_Even_There_Are_Issues_With_Previous_Arranged() + { + var nonArrageableTargets = Enumerable.Range(1, 10).Select(_ => new LayoutTestControl()).ToArray(); + var targets = Enumerable.Range(1, 10).Select(_ => new LayoutTestControl()).ToArray(); + + StackPanel panel; + + var root = new LayoutTestRoot + { + Child = panel = new StackPanel() + }; + + panel.Children.AddRange(nonArrageableTargets); + panel.Children.AddRange(targets); + + root.LayoutManager.ExecuteInitialLayoutPass(root); + + foreach (var c in panel.Children.OfType()) + { + c.Measured = c.Arranged = false; + c.InvalidateMeasure(); + } + + foreach (var c in nonArrageableTargets) + { + c.DoArrangeOverride = (l, s) => + { + //emulate a problem in the logic of a control that triggers + //invalidate measure during arrange + c.InvalidateMeasure(); + return new Size(100, 100); + }; + } + + root.LayoutManager.ExecuteLayoutPass(); + + //altough nonArrageableTargets has rubbish logic and can't be measured/arranged properly + //layoutmanager should process properly other visuals + Assert.All(targets, c => Assert.True(c.Arranged)); } } } From 48ad6babdabfd02a9338aa16fde1195ce36d3ef8 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Tue, 19 Feb 2019 14:31:01 +0200 Subject: [PATCH 20/37] improve test --- tests/Avalonia.Controls.UnitTests/ListBoxTests.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 6939c6a081..343d8d41f3 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -321,6 +321,9 @@ namespace Avalonia.Controls.UnitTests items.Insert(2, "2+"); + lm.ExecuteLayoutPass(); + //after few more layout cycles layoutmanager shouldn't hold any more visual for measure/arrange + lm.ExecuteLayoutPass(); lm.ExecuteLayoutPass(); var flags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic; From 8c0746132349abf93caa8a479967603535e7e255 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Mon, 18 Feb 2019 17:19:12 +0200 Subject: [PATCH 21/37] LayoutManager InfinityLoop protection and safety improvements --- src/Avalonia.Layout/LayoutManager.cs | 93 ++++++++++++++++++++++++++-- 1 file changed, 89 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index f3540ea631..3d390f46e6 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -2,7 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections; using System.Collections.Generic; +using System.Linq; using Avalonia.Logging; using Avalonia.Threading; @@ -13,8 +15,85 @@ namespace Avalonia.Layout /// public class LayoutManager : ILayoutManager { - private readonly Queue _toMeasure = new Queue(); - private readonly Queue _toArrange = new Queue(); + private class LayoutQueue : IReadOnlyCollection + { + private class Info + { + public bool Active; + public int Count; + } + + public LayoutQueue(Func shouldEnqueue) + { + _shouldEnqueue = shouldEnqueue; + } + + private Func _shouldEnqueue; + private Queue _inner = new Queue(); + private Dictionary _loopQueueInfo = new Dictionary(); + private int _maxEnqueueCountPerLoop = 1; + + public int Count => _inner.Count; + + public IEnumerator GetEnumerator() => (_inner as IEnumerable).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator(); + + public T Dequeue() + { + var result = _inner.Dequeue(); + + if (_loopQueueInfo.TryGetValue(result, out var info)) + { + info.Active = false; + } + + return result; + } + + public void Enqueue(T item) + { + if (!_loopQueueInfo.TryGetValue(item, out var info)) + { + _loopQueueInfo[item] = info = new Info(); + } + + if (!info.Active && info.Count < _maxEnqueueCountPerLoop) + { + _inner.Enqueue(item); + info.Active = true; + info.Count++; + } + } + + public void BeginLoop(int maxEnqueueCountPerLoop) + { + _maxEnqueueCountPerLoop = maxEnqueueCountPerLoop; + } + + public void EndLoop() + { + var notfinalized = _loopQueueInfo.Where(v => v.Value.Count == _maxEnqueueCountPerLoop).ToArray(); + + _loopQueueInfo.Clear(); + + //prevent layout cycle but add to next layout the non arranged/measured items that might have caused cycle + //one more time as a final attempt + foreach (var item in notfinalized) + { + if (_shouldEnqueue(item.Key)) + { + item.Value.Active = true; + item.Value.Count++; + _loopQueueInfo[item.Key] = item.Value; + _inner.Enqueue(item.Key); + } + } + } + } + + private readonly LayoutQueue _toMeasure = new LayoutQueue(v => !v.IsMeasureValid); + private readonly LayoutQueue _toArrange = new LayoutQueue(v => !v.IsArrangeValid); private bool _queued; private bool _running; @@ -80,6 +159,9 @@ namespace Avalonia.Layout var stopwatch = new System.Diagnostics.Stopwatch(); stopwatch.Start(); + _toMeasure.BeginLoop(MaxPasses); + _toArrange.BeginLoop(MaxPasses); + try { for (var pass = 0; pass < MaxPasses; ++pass) @@ -98,6 +180,9 @@ namespace Avalonia.Layout _running = false; } + _toMeasure.EndLoop(); + _toArrange.EndLoop(); + stopwatch.Stop(); Logger.Information(LogArea.Layout, this, "Layout pass finished in {Time}", stopwatch.Elapsed); } @@ -112,7 +197,7 @@ namespace Avalonia.Layout Arrange(root); // Running the initial layout pass may have caused some control to be invalidated - // so run a full layout pass now (this usually due to scrollbars; its not known + // so run a full layout pass now (this usually due to scrollbars; its not known // whether they will need to be shown until the layout pass has run and if the // first guess was incorrect the layout will need to be updated). ExecuteLayoutPass(); @@ -133,7 +218,7 @@ namespace Avalonia.Layout private void ExecuteArrangePass() { - while (_toArrange.Count > 0 && _toMeasure.Count == 0) + while (_toArrange.Count > 0) { var control = _toArrange.Dequeue(); From b97b3c59353dd4af4765a1077d8cfc79a731c8ee Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Tue, 19 Feb 2019 17:55:33 +0200 Subject: [PATCH 22/37] make internal stored Info a strcut to optimize memory (pr note) --- src/Avalonia.Layout/LayoutManager.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index 3d390f46e6..ea84b58cbd 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -17,7 +17,7 @@ namespace Avalonia.Layout { private class LayoutQueue : IReadOnlyCollection { - private class Info + private struct Info { public bool Active; public int Count; @@ -46,6 +46,7 @@ namespace Avalonia.Layout if (_loopQueueInfo.TryGetValue(result, out var info)) { info.Active = false; + _loopQueueInfo[result] = info; } return result; @@ -53,16 +54,12 @@ namespace Avalonia.Layout public void Enqueue(T item) { - if (!_loopQueueInfo.TryGetValue(item, out var info)) - { - _loopQueueInfo[item] = info = new Info(); - } + _loopQueueInfo.TryGetValue(item, out var info); if (!info.Active && info.Count < _maxEnqueueCountPerLoop) { _inner.Enqueue(item); - info.Active = true; - info.Count++; + _loopQueueInfo[item] = new Info() { Active = true, Count = info.Count + 1 }; } } @@ -83,9 +80,7 @@ namespace Avalonia.Layout { if (_shouldEnqueue(item.Key)) { - item.Value.Active = true; - item.Value.Count++; - _loopQueueInfo[item.Key] = item.Value; + _loopQueueInfo[item.Key] = new Info() { Active = true, Count = item.Value.Count + 1 }; _inner.Enqueue(item.Key); } } From 6d7d051f3e717d46dde6bce80dd480190df0994c Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Tue, 19 Feb 2019 18:31:06 +0200 Subject: [PATCH 23/37] extract layoutqueue to separate file --- src/Avalonia.Layout/LayoutManager.cs | 75 -------------------------- src/Avalonia.Layout/LayoutQueue.cs | 79 ++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 75 deletions(-) create mode 100644 src/Avalonia.Layout/LayoutQueue.cs diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index ea84b58cbd..45efccc1fa 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -2,9 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; using Avalonia.Logging; using Avalonia.Threading; @@ -15,78 +12,6 @@ namespace Avalonia.Layout /// public class LayoutManager : ILayoutManager { - private class LayoutQueue : IReadOnlyCollection - { - private struct Info - { - public bool Active; - public int Count; - } - - public LayoutQueue(Func shouldEnqueue) - { - _shouldEnqueue = shouldEnqueue; - } - - private Func _shouldEnqueue; - private Queue _inner = new Queue(); - private Dictionary _loopQueueInfo = new Dictionary(); - private int _maxEnqueueCountPerLoop = 1; - - public int Count => _inner.Count; - - public IEnumerator GetEnumerator() => (_inner as IEnumerable).GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator(); - - public T Dequeue() - { - var result = _inner.Dequeue(); - - if (_loopQueueInfo.TryGetValue(result, out var info)) - { - info.Active = false; - _loopQueueInfo[result] = info; - } - - return result; - } - - public void Enqueue(T item) - { - _loopQueueInfo.TryGetValue(item, out var info); - - if (!info.Active && info.Count < _maxEnqueueCountPerLoop) - { - _inner.Enqueue(item); - _loopQueueInfo[item] = new Info() { Active = true, Count = info.Count + 1 }; - } - } - - public void BeginLoop(int maxEnqueueCountPerLoop) - { - _maxEnqueueCountPerLoop = maxEnqueueCountPerLoop; - } - - public void EndLoop() - { - var notfinalized = _loopQueueInfo.Where(v => v.Value.Count == _maxEnqueueCountPerLoop).ToArray(); - - _loopQueueInfo.Clear(); - - //prevent layout cycle but add to next layout the non arranged/measured items that might have caused cycle - //one more time as a final attempt - foreach (var item in notfinalized) - { - if (_shouldEnqueue(item.Key)) - { - _loopQueueInfo[item.Key] = new Info() { Active = true, Count = item.Value.Count + 1 }; - _inner.Enqueue(item.Key); - } - } - } - } - private readonly LayoutQueue _toMeasure = new LayoutQueue(v => !v.IsMeasureValid); private readonly LayoutQueue _toArrange = new LayoutQueue(v => !v.IsArrangeValid); private bool _queued; diff --git a/src/Avalonia.Layout/LayoutQueue.cs b/src/Avalonia.Layout/LayoutQueue.cs new file mode 100644 index 0000000000..ce40fdde49 --- /dev/null +++ b/src/Avalonia.Layout/LayoutQueue.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Avalonia.Layout +{ + internal class LayoutQueue : IReadOnlyCollection + { + private struct Info + { + public bool Active; + public int Count; + } + + public LayoutQueue(Func shouldEnqueue) + { + _shouldEnqueue = shouldEnqueue; + } + + private Func _shouldEnqueue; + private Queue _inner = new Queue(); + private Dictionary _loopQueueInfo = new Dictionary(); + private int _maxEnqueueCountPerLoop = 1; + + public int Count => _inner.Count; + + public IEnumerator GetEnumerator() => (_inner as IEnumerable).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator(); + + public T Dequeue() + { + var result = _inner.Dequeue(); + + if (_loopQueueInfo.TryGetValue(result, out var info)) + { + info.Active = false; + _loopQueueInfo[result] = info; + } + + return result; + } + + public void Enqueue(T item) + { + _loopQueueInfo.TryGetValue(item, out var info); + + if (!info.Active && info.Count < _maxEnqueueCountPerLoop) + { + _inner.Enqueue(item); + _loopQueueInfo[item] = new Info() { Active = true, Count = info.Count + 1 }; + } + } + + public void BeginLoop(int maxEnqueueCountPerLoop) + { + _maxEnqueueCountPerLoop = maxEnqueueCountPerLoop; + } + + public void EndLoop() + { + var notfinalized = _loopQueueInfo.Where(v => v.Value.Count == _maxEnqueueCountPerLoop).ToArray(); + + _loopQueueInfo.Clear(); + + //prevent layout cycle but add to next layout the non arranged/measured items that might have caused cycle + //one more time as a final attempt + foreach (var item in notfinalized) + { + if (_shouldEnqueue(item.Key)) + { + _loopQueueInfo[item.Key] = new Info() { Active = true, Count = item.Value.Count + 1 }; + _inner.Enqueue(item.Key); + } + } + } + } +} From a4132890bf6a7003cc7faac4090cc90896881488 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Tue, 19 Feb 2019 18:32:15 +0200 Subject: [PATCH 24/37] add unit tests for layout queue --- .../Properties/AssemblyInfo.cs | 4 + .../LayoutQueueTests.cs | 196 ++++++++++++++++++ 2 files changed, 200 insertions(+) create mode 100644 tests/Avalonia.Layout.UnitTests/LayoutQueueTests.cs diff --git a/src/Avalonia.Layout/Properties/AssemblyInfo.cs b/src/Avalonia.Layout/Properties/AssemblyInfo.cs index 70fc1e9330..392ad323e5 100644 --- a/src/Avalonia.Layout/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Layout/Properties/AssemblyInfo.cs @@ -1,6 +1,10 @@ // 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.Runtime.CompilerServices; using Avalonia.Metadata; +[assembly: InternalsVisibleTo("Avalonia.Layout.UnitTests")] + [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Layout")] + diff --git a/tests/Avalonia.Layout.UnitTests/LayoutQueueTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutQueueTests.cs new file mode 100644 index 0000000000..f047677a8a --- /dev/null +++ b/tests/Avalonia.Layout.UnitTests/LayoutQueueTests.cs @@ -0,0 +1,196 @@ +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Avalonia.Layout.UnitTests +{ + public class LayoutQueueTests + { + [Fact] + public void Should_Enqueue() + { + var target = new LayoutQueue(_ => true); + var refQueue = new Queue(); + var items = new[] { "1", "2", "3" }; + + foreach (var item in items) + { + target.Enqueue(item); + refQueue.Enqueue(item); + } + + Assert.Equal(refQueue, target); + } + + [Fact] + public void Should_Dequeue() + { + var target = new LayoutQueue(_ => true); + var refQueue = new Queue(); + var items = new[] { "1", "2", "3" }; + + foreach (var item in items) + { + target.Enqueue(item); + refQueue.Enqueue(item); + } + + while (refQueue.Count > 0) + { + Assert.Equal(refQueue.Dequeue(), target.Dequeue()); + } + } + + [Fact] + public void Should_Enqueue_UniqueElements() + { + var target = new LayoutQueue(_ => true); + + var items = new[] { "1", "2", "3", "1" }; + + foreach (var item in items) + { + target.Enqueue(item); + } + + Assert.Equal(3, target.Count); + Assert.Equal(items.Take(3), target); + } + + [Fact] + public void Shouldnt_Enqueue_More_Than_Limit_In_Loop() + { + var target = new LayoutQueue(_ => true); + + //1 + target.Enqueue("Foo"); + + Assert.Equal(1, target.Count); + + target.BeginLoop(3); + + target.Dequeue(); + + //2 + target.Enqueue("Foo"); + target.Dequeue(); + + //3 + target.Enqueue("Foo"); + + Assert.Equal(1, target.Count); + + target.Dequeue(); + + //4 more than limit shouldn't be added + target.Enqueue("Foo"); + + Assert.Equal(0, target.Count); + } + + [Fact] + public void Shouldnt_Count_Unique_Enqueue_For_Limit_In_Loop() + { + var target = new LayoutQueue(_ => true); + + //1 + target.Enqueue("Foo"); + + Assert.Equal(1, target.Count); + + target.BeginLoop(3); + + target.Dequeue(); + + //2 + target.Enqueue("Foo"); + target.Enqueue("Foo"); + target.Dequeue(); + + //3 + target.Enqueue("Foo"); + target.Enqueue("Foo"); + + Assert.Equal(1, target.Count); + + target.Dequeue(); + + //4 more than limit shouldn't be added + target.Enqueue("Foo"); + + Assert.Equal(0, target.Count); + } + + [Fact] + public void Should_Enqueue_When_Condition_True_After_Loop_When_Limit_Met() + { + var target = new LayoutQueue(_ => true); + + //1 + target.Enqueue("Foo"); + + Assert.Equal(1, target.Count); + + target.BeginLoop(3); + + target.Dequeue(); + + //2 + target.Enqueue("Foo"); + target.Dequeue(); + + //3 + target.Enqueue("Foo"); + + Assert.Equal(1, target.Count); + + target.Dequeue(); + + //4 more than limit shouldn't be added to queue + target.Enqueue("Foo"); + + Assert.Equal(0, target.Count); + + target.EndLoop(); + + //after loop should be added once + Assert.Equal(1, target.Count); + Assert.Equal("Foo", target.First()); + } + + [Fact] + public void Shouldnt_Enqueue_When_Condition_False_After_Loop_When_Limit_Met() + { + var target = new LayoutQueue(_ => false); + + //1 + target.Enqueue("Foo"); + + Assert.Equal(1, target.Count); + + target.BeginLoop(3); + + target.Dequeue(); + + //2 + target.Enqueue("Foo"); + target.Dequeue(); + + //3 + target.Enqueue("Foo"); + + Assert.Equal(1, target.Count); + + target.Dequeue(); + + //4 more than limit shouldn't be added + target.Enqueue("Foo"); + + Assert.Equal(0, target.Count); + + target.EndLoop(); + + Assert.Equal(0, target.Count); + } + } +} From 372f0f266ec4c6700ec156bebf30e80ffcc28459 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 20 Feb 2019 15:27:07 +0100 Subject: [PATCH 25/37] Add another failing test relating to #1099. --- .../Avalonia.Visuals.UnitTests/VisualTests.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTests.cs b/tests/Avalonia.Visuals.UnitTests/VisualTests.cs index 0414ac4c74..504f0ada86 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTests.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using Avalonia.Controls; +using Avalonia.Data; using Avalonia.Media; using Avalonia.Rendering; using Avalonia.UnitTests; @@ -236,5 +237,50 @@ namespace Avalonia.Visuals.UnitTests //child is centered (400 - 100*2 scale)/2 Assert.Equal(new Point(100, 100), point); } + + [Fact] + public void Should_Not_Log_Binding_Error_When_Not_Attached_To_Logical_Tree() + { + var target = new Decorator { DataContext = "foo" }; + var called = false; + + LogCallback checkLogMessage = (level, area, src, mt, pv) => + { + if (level >= Logging.LogEventLevel.Warning) + { + called = true; + } + }; + + using (TestLogSink.Start(checkLogMessage)) + { + target.Bind(Decorator.TagProperty, new Binding("Foo")); + } + + Assert.False(called); + } + + [Fact] + public void Should_Log_Binding_Error_When_Attached_To_Logical_Tree() + { + var target = new Decorator(); + var root = new TestRoot { Child = target, DataContext = "foo" }; + var called = false; + + LogCallback checkLogMessage = (level, area, src, mt, pv) => + { + if (level >= Logging.LogEventLevel.Warning) + { + called = true; + } + }; + + using (TestLogSink.Start(checkLogMessage)) + { + target.Bind(Decorator.TagProperty, new Binding("Foo")); + } + + Assert.True(called); + } } } From 6961d55e7a33883f7c229c6a45f1335bac646fb3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 20 Feb 2019 15:30:05 +0100 Subject: [PATCH 26/37] Only log binding errors when attached to a tree. --- src/Avalonia.Base/AvaloniaObject.cs | 43 ++++++++++++++- src/Avalonia.Base/IPriorityValueOwner.cs | 8 +++ src/Avalonia.Base/Logging/LoggerExtensions.cs | 53 ------------------- src/Avalonia.Base/PriorityValue.cs | 2 +- src/Avalonia.Base/ValueStore.cs | 5 ++ src/Avalonia.Visuals/Visual.cs | 29 ++++++++++ 6 files changed, 85 insertions(+), 55 deletions(-) delete mode 100644 src/Avalonia.Base/Logging/LoggerExtensions.cs diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 28148f6568..90b6aca505 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -415,6 +415,7 @@ namespace Avalonia internal void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification) { + LogIfError(property, notification); UpdateDataValidation(property, notification); } @@ -446,6 +447,23 @@ namespace Avalonia }); } + /// + /// Logs a binding error for a property. + /// + /// The property that the error occurred on. + /// The binding error. + protected internal virtual void LogBindingError(AvaloniaProperty property, Exception e) + { + Logger.Log( + LogEventLevel.Warning, + LogArea.Binding, + this, + "Error in binding to {Target}.{Property}: {Message}", + this, + property, + e.Message); + } + /// /// Called to update the validation state for properties for which data validation is /// enabled. @@ -647,7 +665,7 @@ namespace Avalonia if (notification != null) { - notification.LogIfError(this, property); + LogIfError(property, notification); value = notification.Value; } @@ -779,6 +797,29 @@ namespace Avalonia return description?.Description ?? o.ToString(); } + /// + /// Logs a mesage if the notification represents a binding error. + /// + /// The property being bound. + /// The binding notification. + private void LogIfError(AvaloniaProperty property, BindingNotification notification) + { + if (notification.ErrorType == BindingErrorType.Error) + { + if (notification.Error is AggregateException aggregate) + { + foreach (var inner in aggregate.InnerExceptions) + { + LogBindingError(property, inner); + } + } + else + { + LogBindingError(property, notification.Error); + } + } + } + /// /// Logs a property set message. /// diff --git a/src/Avalonia.Base/IPriorityValueOwner.cs b/src/Avalonia.Base/IPriorityValueOwner.cs index 8cbf212381..540b1bf19b 100644 --- a/src/Avalonia.Base/IPriorityValueOwner.cs +++ b/src/Avalonia.Base/IPriorityValueOwner.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 System; using Avalonia.Data; using Avalonia.Utilities; @@ -28,6 +29,13 @@ namespace Avalonia /// The notification. void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification); + /// + /// Logs a binding error. + /// + /// The property the error occurred on. + /// The binding error. + void LogError(AvaloniaProperty property, Exception e); + /// /// Ensures that the current thread is the UI thread. /// diff --git a/src/Avalonia.Base/Logging/LoggerExtensions.cs b/src/Avalonia.Base/Logging/LoggerExtensions.cs deleted file mode 100644 index 24e44bf9de..0000000000 --- a/src/Avalonia.Base/Logging/LoggerExtensions.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using Avalonia.Data; - -namespace Avalonia.Logging -{ - internal static class LoggerExtensions - { - public static void LogIfError( - this BindingNotification notification, - object source, - AvaloniaProperty property) - { - if (notification.ErrorType == BindingErrorType.Error) - { - if (notification.Error is AggregateException aggregate) - { - foreach (var inner in aggregate.InnerExceptions) - { - LogError(source, property, inner); - } - } - else - { - LogError(source, property, notification.Error); - } - } - } - - private static void LogError(object source, AvaloniaProperty property, Exception e) - { - var level = LogEventLevel.Warning; - - if (e is BindingChainException b && - !string.IsNullOrEmpty(b.Expression) && - string.IsNullOrEmpty(b.ExpressionErrorPoint)) - { - // The error occurred at the root of the binding chain: it's possible that the - // DataContext isn't set up yet, so log at Information level instead of Warning - // to prevent spewing hundreds of errors. - level = LogEventLevel.Information; - } - - Logger.Log( - level, - LogArea.Binding, - source, - "Error in binding to {Target}.{Property}: {Message}", - source, - property, - e.Message); - } - } -} diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs index c8b434c6f9..89a893577f 100644 --- a/src/Avalonia.Base/PriorityValue.cs +++ b/src/Avalonia.Base/PriorityValue.cs @@ -197,7 +197,7 @@ namespace Avalonia /// The binding error. public void LevelError(PriorityLevel level, BindingNotification error) { - error.LogIfError(Owner, Property); + Owner.LogError(Property, error.Error); } /// diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index adbe89aceb..c5bf6ba0a8 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -100,6 +100,11 @@ namespace Avalonia _owner.PriorityValueChanged(property, priority, oldValue, newValue); } + public void LogError(AvaloniaProperty property, Exception e) + { + _owner.LogBindingError(property, e); + } + public IDictionary GetSetValues() => _values; public object GetValue(AvaloniaProperty property) diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index f26c21d1b6..bf282db72f 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -8,6 +8,7 @@ using System.Reactive.Linq; using Avalonia.Collections; using Avalonia.Data; using Avalonia.Logging; +using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Rendering; using Avalonia.VisualTree; @@ -448,6 +449,34 @@ namespace Avalonia RaisePropertyChanged(VisualParentProperty, oldParent, newParent, BindingPriority.LocalValue); } + protected override sealed void LogBindingError(AvaloniaProperty property, Exception e) + { + // Don't log a binding error unless the control is attached to a logical or visual tree. + // In theory this should only need to check for logical tree attachment, but in practise + // due to ContentControlMixin only taking effect when the template has finished being + // applied, some controls are attached to the visual tree before the logical tree. + if (((ILogical)this).IsAttachedToLogicalTree || ((IVisual)this).IsAttachedToVisualTree) + { + if (e is BindingChainException b && + string.IsNullOrEmpty(b.ExpressionErrorPoint) && + DataContext == null) + { + // The error occurred at the root of the binding chain and DataContext is null; + // don't log this - the DataContext probably hasn't been set up yet. + return; + } + + Logger.Log( + LogEventLevel.Warning, + LogArea.Binding, + this, + "Error in binding to {Target}.{Property}: {Message}", + this, + property, + e.Message); + } + } + /// /// Gets the visual offset from the specified ancestor. /// From 6dcd634a87a317b772205d8b4257f18bcbce4e59 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 20 Feb 2019 15:42:40 +0100 Subject: [PATCH 27/37] Missed change from merge. --- src/Avalonia.Base/ValueStore.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 22961a7568..24f85ea6b1 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -123,8 +123,6 @@ namespace Avalonia _owner.LogBindingError(property, e); } - public IDictionary GetSetValues() => _values; - public object GetValue(AvaloniaProperty property) { var result = AvaloniaProperty.UnsetValue; From 3d6b4a875bb6c5adb24c49dd3dea159cd0f376e1 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 21 Feb 2019 14:34:24 +0100 Subject: [PATCH 28/37] Do not capture this when creating _drawOperationsRefCounter. --- .../Rendering/SceneGraph/VisualNode.cs | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index 2fb8e84a2e..71e364ffc9 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -327,31 +327,36 @@ namespace Avalonia.Rendering.SceneGraph if (_drawOperations == null) { _drawOperations = new List>(); - _drawOperationsRefCounter = RefCountable.Create(Disposable.Create(DisposeDrawOperations)); + _drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations)); _drawOperationsCloned = false; } else if (_drawOperationsCloned) { _drawOperations = new List>(_drawOperations.Select(op => op.Clone())); _drawOperationsRefCounter.Dispose(); - _drawOperationsRefCounter = RefCountable.Create(Disposable.Create(DisposeDrawOperations)); + _drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations)); _drawOperationsCloned = false; } } - public bool Disposed { get; } - - public void Dispose() + private static IDisposable CreateDisposeDrawOperations(List> drawOperations) { - _drawOperationsRefCounter?.Dispose(); + return Disposable.Create(() => + { + foreach (var operation in drawOperations) + { + operation.Dispose(); + } + }); } - private void DisposeDrawOperations() + public bool Disposed { get; private set; } + + public void Dispose() { - foreach (var operation in DrawOperations) - { - operation.Dispose(); - } + _drawOperationsRefCounter?.Dispose(); + + Disposed = true; } } } From 61be769d61ecf869d13f528a78fc9082a8b00697 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 21 Feb 2019 15:50:48 +0200 Subject: [PATCH 29/37] Update tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs Co-Authored-By: donandren --- tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs index 10006609aa..b0e2f9b393 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs @@ -239,7 +239,7 @@ namespace Avalonia.Layout.UnitTests } [Fact] - public void LayoutManager_Should_Prevent_Infinity_Loop_On_Measure() + public void LayoutManager_Should_Prevent_Infinite_Loop_On_Measure() { var control = new LayoutTestControl(); var root = new LayoutTestRoot { Child = control }; From 260789c42506ca90384f658d06c5edcbb842d295 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 21 Feb 2019 15:50:56 +0200 Subject: [PATCH 30/37] Update tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs Co-Authored-By: donandren --- tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs index b0e2f9b393..6ecf5a7b8c 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs @@ -253,7 +253,7 @@ namespace Avalonia.Layout.UnitTests { //emulate a problem in the logic of a control that triggers //invalidate measure during measure - //it can lead to infinity loop in layoutmanager + //it can lead to an infinite loop in layoutmanager if (++cnt < maxcnt) { control.InvalidateMeasure(); From 9b4bd8d62124f552cae798f6ceed79de5048d0f5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 21 Feb 2019 15:51:07 +0200 Subject: [PATCH 31/37] Update tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs Co-Authored-By: donandren --- tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs index 6ecf5a7b8c..4d7e0ee259 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs @@ -270,7 +270,7 @@ namespace Avalonia.Layout.UnitTests } [Fact] - public void LayoutManager_Should_Prevent_Infinity_Loop_On_Arrange() + public void LayoutManager_Should_Prevent_Infinite_Loop_On_Arrange() { var control = new LayoutTestControl(); var root = new LayoutTestRoot { Child = control }; From b32a10ceda9b418cea7ff25d0593cfdae8362dfe Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 21 Feb 2019 15:51:34 +0200 Subject: [PATCH 32/37] Update tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs Co-Authored-By: donandren --- tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs index 4d7e0ee259..4c288b2702 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs @@ -301,7 +301,7 @@ namespace Avalonia.Layout.UnitTests } [Fact] - public void LayoutManager_Should_Properly_Arrange_Visuals_Even_There_Are_Issues_With_Previous_Arranged() + public void LayoutManager_Should_Properly_Arrange_Visuals_Even_When_There_Are_Issues_With_Previous_Arranged() { var nonArrageableTargets = Enumerable.Range(1, 10).Select(_ => new LayoutTestControl()).ToArray(); var targets = Enumerable.Range(1, 10).Select(_ => new LayoutTestControl()).ToArray(); From 823b58b3351a4984b785bf7298b7d34ffbaf71d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Komosi=C5=84ski?= Date: Thu, 21 Feb 2019 22:16:47 +0100 Subject: [PATCH 33/37] Add doc comment to related methods. --- .../Rendering/SceneGraph/VisualNode.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index 71e364ffc9..19fb54e125 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -322,6 +322,9 @@ namespace Avalonia.Rendering.SceneGraph } } + /// + /// Ensures that this node draw operations have been created and are mutable (in case we are using cloned operations). + /// private void EnsureDrawOperationsCreated() { if (_drawOperations == null) @@ -339,6 +342,13 @@ namespace Avalonia.Rendering.SceneGraph } } + /// + /// Creates disposable that will dispose all items in passed draw operations after being disposed. + /// It is crucial that we don't capture current instance + /// as draw operations can be cloned and may persist across subsequent scenes. + /// + /// Draw operations that need to be disposed. + /// Disposable for given draw operations. private static IDisposable CreateDisposeDrawOperations(List> drawOperations) { return Disposable.Create(() => From 40b0bbd46c348fc83be2fe2d04f3c8f398f691fb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 22 Feb 2019 10:51:03 +0100 Subject: [PATCH 34/37] Make sure PopupRoot.Parent is a Popup. Fixes #2137. --- src/Avalonia.Controls/Primitives/Popup.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index d9070197b6..f349bcf059 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -252,9 +252,9 @@ namespace Avalonia.Controls.Primitives else { var parentPopuproot = _topLevel as PopupRoot; - if (parentPopuproot != null && parentPopuproot.Parent != null) + if (parentPopuproot?.Parent is Popup popup) { - ((Popup)(parentPopuproot.Parent)).Closed += ParentClosed; + popup.Closed += ParentClosed; } } _topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel); @@ -293,9 +293,9 @@ namespace Avalonia.Controls.Primitives else { var parentPopuproot = _topLevel as PopupRoot; - if (parentPopuproot != null && parentPopuproot.Parent != null) + if (parentPopuproot?.Parent is Popup popup) { - ((Popup)parentPopuproot.Parent).Closed -= ParentClosed; + popup.Closed -= ParentClosed; } } _nonClientListener?.Dispose(); From 2da756aa5304d91fbcfb7469968b2fd6fca20fe3 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Fri, 22 Feb 2019 19:17:49 +0800 Subject: [PATCH 35/37] Make parse function in Cue.cs explicitly typed in order for Xaml-IL tocorrectly compile and avoid unnecessary boxing. --- src/Avalonia.Animation/Cue.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Animation/Cue.cs b/src/Avalonia.Animation/Cue.cs index 52d1609cf9..7da7a9382b 100644 --- a/src/Avalonia.Animation/Cue.cs +++ b/src/Avalonia.Animation/Cue.cs @@ -30,7 +30,7 @@ namespace Avalonia.Animation /// /// Parses a string to a object. /// - public static object Parse(string value, CultureInfo culture) + public static Cue Parse(string value, CultureInfo culture) { string v = value; @@ -70,7 +70,7 @@ namespace Avalonia.Animation } } - public class CueTypeConverter : TypeConverter + public class CueTypeConverter : TypeConverter { public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { From af2431e53984e41342328b27839910eee6a31c34 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Sat, 23 Feb 2019 11:48:16 +0800 Subject: [PATCH 36/37] Fix renderdemo sidebar template. --- samples/RenderDemo/SideBar.xaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/RenderDemo/SideBar.xaml b/samples/RenderDemo/SideBar.xaml index 624c1a7b28..3af90f1844 100644 --- a/samples/RenderDemo/SideBar.xaml +++ b/samples/RenderDemo/SideBar.xaml @@ -1,5 +1,5 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> - + \ No newline at end of file From 64a0ae0921ec0625081b8a976f8df22a0e589c6a Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 25 Feb 2019 14:04:07 +0300 Subject: [PATCH 37/37] [X11] Fixed window size hints handling --- src/Avalonia.X11/X11Window.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index cbff4e38cf..3af2d5f3fe 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -557,8 +557,14 @@ namespace Avalonia.X11 private bool _systemDecorations = true; private bool _canResize = true; - private (Size minSize, Size maxSize) _scaledMinMaxSize; - private (PixelSize minSize, PixelSize maxSize) _minMaxSize; + private const int MaxWindowDimension = 100000; + + private (Size minSize, Size maxSize) _scaledMinMaxSize = + (new Size(1, 1), new Size(double.PositiveInfinity, double.PositiveInfinity)); + + private (PixelSize minSize, PixelSize maxSize) _minMaxSize = (new PixelSize(1, 1), + new PixelSize(MaxWindowDimension, MaxWindowDimension)); + private double _scaling = 1; void ScheduleInput(RawInputEventArgs args, ref XEvent xev) @@ -874,10 +880,10 @@ namespace Avalonia.X11 (int)(minSize.Width < 1 ? 1 : minSize.Width * Scaling), (int)(minSize.Height < 1 ? 1 : minSize.Height * Scaling)); - const int maxDim = 100000; + const int maxDim = MaxWindowDimension; var max = new PixelSize( - (int)(maxSize.Width > maxDim ? maxDim : Math.Max(min.Width, minSize.Width * Scaling)), - (int)(maxSize.Height > maxDim ? maxDim : Math.Max(min.Height, minSize.Height * Scaling))); + (int)(maxSize.Width > maxDim ? maxDim : Math.Max(min.Width, maxSize.Width * Scaling)), + (int)(maxSize.Height > maxDim ? maxDim : Math.Max(min.Height, maxSize.Height * Scaling))); _minMaxSize = (min, max); UpdateSizeHints(null);