ソースを参照

Merge pull request #2264 from AvaloniaUI/fixes/1099-inherited-propertychanged-order

Fix binding errors when switching ContentControl content
Steven Kirk 6 年 前
コミット
6e7e90b658
24 ファイル変更389 行追加89 行削除
  1. 60 3
      src/Avalonia.Base/AvaloniaObject.cs
  2. 6 1
      src/Avalonia.Base/IAvaloniaObject.cs
  3. 8 0
      src/Avalonia.Base/IPriorityValueOwner.cs
  4. 0 53
      src/Avalonia.Base/Logging/LoggerExtensions.cs
  5. 1 1
      src/Avalonia.Base/PriorityValue.cs
  6. 4 0
      src/Avalonia.Base/ValueStore.cs
  7. 13 1
      src/Avalonia.Controls/ContentControl.cs
  8. 23 6
      src/Avalonia.Controls/Mixins/ContentControlMixin.cs
  9. 35 7
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  10. 14 1
      src/Avalonia.Controls/Presenters/IContentPresenter.cs
  11. 35 2
      src/Avalonia.Controls/Primitives/HeaderedContentControl.cs
  12. 16 5
      src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs
  13. 16 5
      src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs
  14. 29 0
      src/Avalonia.Visuals/Visual.cs
  15. 16 0
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs
  16. 1 1
      tests/Avalonia.Controls.UnitTests/HeaderedItemsControlTests .cs
  17. 27 1
      tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs
  18. 31 1
      tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs
  19. 1 0
      tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs
  20. 1 0
      tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs
  21. 1 0
      tests/Avalonia.Styling.UnitTests/TestControlBase.cs
  22. 1 0
      tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs
  23. 4 1
      tests/Avalonia.UnitTests/TestTemplatedRoot.cs
  24. 46 0
      tests/Avalonia.Visuals.UnitTests/VisualTests.cs

+ 60 - 3
src/Avalonia.Base/AvaloniaObject.cs

@@ -26,6 +26,7 @@ namespace Avalonia
         private List<DirectBindingSubscription> _directBindings;
         private PropertyChangedEventHandler _inpcChanged;
         private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged;
+        private EventHandler<AvaloniaPropertyChangedEventArgs> _inheritablePropertyChanged;
         private ValueStore _values;
         private ValueStore Values => _values ?? (_values = new ValueStore(this));
 
@@ -56,6 +57,15 @@ namespace Avalonia
             remove { _inpcChanged -= value; }
         }
 
+        /// <summary>
+        /// Raised when an inheritable <see cref="AvaloniaProperty"/> value changes on this object.
+        /// </summary>
+        event EventHandler<AvaloniaPropertyChangedEventArgs> IAvaloniaObject.InheritablePropertyChanged
+        {
+            add { _inheritablePropertyChanged += value; }
+            remove { _inheritablePropertyChanged -= value; }
+        }
+
         /// <summary>
         /// Gets or sets the parent object that inherited <see cref="AvaloniaProperty"/> values
         /// are inherited from.
@@ -76,8 +86,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
@@ -102,7 +113,7 @@ namespace Avalonia
 
                     if (_inheritanceParent != null)
                     {
-                        _inheritanceParent.PropertyChanged += ParentPropertyChanged;
+                        _inheritanceParent.InheritablePropertyChanged += ParentPropertyChanged;
                     }
                 }
             }
@@ -379,6 +390,7 @@ namespace Avalonia
         
         internal void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
         {
+            LogIfError(property, notification);
             UpdateDataValidation(property, notification);
         }
 
@@ -410,6 +422,23 @@ namespace Avalonia
             });
         }
 
+        /// <summary>
+        /// Logs a binding error for a property.
+        /// </summary>
+        /// <param name="property">The property that the error occurred on.</param>
+        /// <param name="e">The binding error.</param>
+        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);
+        }
+
         /// <summary>
         /// Called to update the validation state for properties for which data validation is
         /// enabled.
@@ -467,6 +496,11 @@ namespace Avalonia
                     PropertyChangedEventArgs e2 = new PropertyChangedEventArgs(property.Name);
                     _inpcChanged(this, e2);
                 }
+
+                if (property.Inherits)
+                {
+                    _inheritablePropertyChanged?.Invoke(this, e);
+                }
             }
             finally
             {
@@ -606,7 +640,7 @@ namespace Avalonia
 
                 if (notification != null)
                 {
-                    notification.LogIfError(this, property);
+                    LogIfError(property, notification);
                     value = notification.Value;
                 }
 
@@ -738,6 +772,29 @@ namespace Avalonia
             return description?.Description ?? o.ToString();
         }
 
+        /// <summary>
+        /// Logs a mesage if the notification represents a binding error.
+        /// </summary>
+        /// <param name="property">The property being bound.</param>
+        /// <param name="notification">The binding notification.</param>
+        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);
+                }
+            }
+        }
+
         /// <summary>
         /// Logs a property set message.
         /// </summary>

+ 6 - 1
src/Avalonia.Base/IAvaloniaObject.cs

@@ -16,6 +16,11 @@ namespace Avalonia
         /// </summary>
         event EventHandler<AvaloniaPropertyChangedEventArgs> PropertyChanged;
 
+        /// <summary>
+        /// Raised when an inheritable <see cref="AvaloniaProperty"/> value changes on this object.
+        /// </summary>
+        event EventHandler<AvaloniaPropertyChangedEventArgs> InheritablePropertyChanged;
+
         /// <summary>
         /// Gets a <see cref="AvaloniaProperty"/> value.
         /// </summary>
@@ -97,4 +102,4 @@ namespace Avalonia
             IObservable<T> source,
             BindingPriority priority = BindingPriority.LocalValue);
     }
-}
+}

+ 8 - 0
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
         /// <param name="notification">The notification.</param>
         void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification);
 
+        /// <summary>
+        /// Logs a binding error.
+        /// </summary>
+        /// <param name="property">The property the error occurred on.</param>
+        /// <param name="e">The binding error.</param>
+        void LogError(AvaloniaProperty property, Exception e);
+
         /// <summary>
         /// Ensures that the current thread is the UI thread.
         /// </summary>

+ 0 - 53
src/Avalonia.Base/Logging/LoggerExtensions.cs

@@ -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);
-        }
-    }
-}

+ 1 - 1
src/Avalonia.Base/PriorityValue.cs

@@ -197,7 +197,7 @@ namespace Avalonia
         /// <param name="error">The binding error.</param>
         public void LevelError(PriorityLevel level, BindingNotification error)
         {
-            error.LogIfError(Owner, Property);
+            Owner.LogError(Property, error.Error);
         }
 
         /// <summary>

+ 4 - 0
src/Avalonia.Base/ValueStore.cs

@@ -118,6 +118,10 @@ namespace Avalonia
 
             return dict;
         }
+        public void LogError(AvaloniaProperty property, Exception e)
+        {
+            _owner.LogBindingError(property, e);
+        }
 
         public object GetValue(AvaloniaProperty property)
         {

+ 13 - 1
src/Avalonia.Controls/ContentControl.cs

@@ -97,7 +97,19 @@ namespace Avalonia.Controls
         /// <inheritdoc/>
         void IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter)
         {
-            Presenter = presenter;
+            RegisterContentPresenter(presenter);
+        }
+
+        /// <summary>
+        /// Called when an <see cref="IContentPresenter"/> is registered with the control.
+        /// </summary>
+        /// <param name="presenter">The presenter.</param>
+        protected virtual void RegisterContentPresenter(IContentPresenter presenter)
+        {
+            if (presenter.Name == "PART_ContentPresenter")
+            {
+                Presenter = presenter;
+            }
         }
     }
 }

+ 23 - 6
src/Avalonia.Controls/Mixins/ContentControlMixin.cs

@@ -19,8 +19,8 @@ namespace Avalonia.Controls.Mixins
     /// <para>
     /// The <see cref="ContentControlMixin"/> adds behavior to a control which acts as a content
     /// control such as <see cref="ContentControl"/> and <see cref="HeaderedItemsControl"/>. 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.
     /// </para>
     public class ContentControlMixin
     {
@@ -49,25 +49,42 @@ namespace Avalonia.Controls.Mixins
             Contract.Requires<ArgumentNullException>(content != null);
             Contract.Requires<ArgumentNullException>(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,

+ 35 - 7
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<AvaloniaPropertyChangedEventArgs> _childChanging;
         private IDataTemplate _dataTemplate;
         private readonly BorderRenderHelper _borderRenderer = new BorderRenderHelper();
 
@@ -188,6 +190,13 @@ namespace Avalonia.Controls.Presenters
             set { SetValue(PaddingProperty, value); }
         }
 
+        /// <inheritdoc/>
+        event EventHandler<AvaloniaPropertyChangedEventArgs> IContentPresenter.ChildChanging
+        {
+            add => _childChanging += value;
+            remove => _childChanging -= value;
+        }
+
         /// <inheritdoc/>
         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);

+ 14 - 1
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.
         /// </summary>
         object Content { get; set; }
+
+        /// <summary>
+        /// Raised when <see cref="Child"/> property is about to change.
+        /// </summary>
+        /// <remarks>
+        /// This event should be raised after the child has been removed from the visual tree,
+        /// but before the <see cref="Child"/> property has changed. It is intended for consumption
+        /// by <see cref="ContentControlMixin"/> in order to update the host control's logical
+        /// children.
+        /// </remarks>
+        event EventHandler<AvaloniaPropertyChangedEventArgs> ChildChanging;
     }
-}
+}

+ 35 - 2
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 <see cref="HeaderTemplate"/> property.
         /// </summary>
         public static readonly StyledProperty<IDataTemplate> HeaderTemplateProperty =
-            AvaloniaProperty.Register<HeaderedContentControl, IDataTemplate>(nameof(HeaderTemplate));          
+            AvaloniaProperty.Register<HeaderedContentControl, IDataTemplate>(nameof(HeaderTemplate));
+
+        /// <summary>
+        /// Initializes static members of the <see cref="ContentControl"/> class.
+        /// </summary>
+        static HeaderedContentControl()
+        {
+            ContentControlMixin.Attach<HeaderedContentControl>(
+                HeaderProperty,
+                x => x.LogicalChildren,
+                "PART_HeaderPresenter");
+        }
 
         /// <summary>
         /// Gets or sets the header content.
@@ -29,7 +42,16 @@ namespace Avalonia.Controls.Primitives
         {
             get { return GetValue(HeaderProperty); }
             set { SetValue(HeaderProperty, value); }
-        }     
+        }
+
+        /// <summary>
+        /// Gets the header presenter from the control's template.
+        /// </summary>
+        public IContentPresenter HeaderPresenter
+        {
+            get;
+            private set;
+        }
 
         /// <summary>
         /// 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); }
         }
+
+        /// <inheritdoc/>
+        protected override void RegisterContentPresenter(IContentPresenter presenter)
+        {
+            base.RegisterContentPresenter(presenter);
+
+            if (presenter.Name == "PART_HeaderPresenter")
+            {
+                HeaderPresenter = presenter;
+            }
+        }
     }
 }

+ 16 - 5
src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs

@@ -9,7 +9,7 @@ namespace Avalonia.Controls.Primitives
     /// <summary>
     /// Represents an <see cref="ItemsControl"/> with a related header.
     /// </summary>
-    public class HeaderedItemsControl : ItemsControl
+    public class HeaderedItemsControl : ItemsControl, IContentPresenterHost
     {
         /// <summary>
         /// Defines the <see cref="Header"/> property.
@@ -40,17 +40,28 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Gets the header presenter from the control's template.
         /// </summary>
-        public ContentPresenter HeaderPresenter
+        public IContentPresenter HeaderPresenter
         {
             get;
             private set;
         }
 
         /// <inheritdoc/>
-        protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
+        void IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter)
         {
-            HeaderPresenter = e.NameScope.Find<ContentPresenter>("PART_HeaderPresenter");
-            base.OnTemplateApplied(e);
+            RegisterContentPresenter(presenter);
+        }
+
+        /// <summary>
+        /// Called when an <see cref="IContentPresenter"/> is registered with the control.
+        /// </summary>
+        /// <param name="presenter">The presenter.</param>
+        protected virtual void RegisterContentPresenter(IContentPresenter presenter)
+        {
+            if (presenter.Name == "PART_HeaderPresenter")
+            {
+                HeaderPresenter = presenter;
+            }
         }
     }
 }

+ 16 - 5
src/Avalonia.Controls/Primitives/HeaderedSelectingControl.cs → src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs

@@ -9,7 +9,7 @@ namespace Avalonia.Controls.Primitives
     /// <summary>
     /// Represents a <see cref="SelectingItemsControl"/> with a related header.
     /// </summary>
-    public class HeaderedSelectingItemsControl : SelectingItemsControl
+    public class HeaderedSelectingItemsControl : SelectingItemsControl, IContentPresenterHost
     {
         /// <summary>
         /// Defines the <see cref="Header"/> property.
@@ -40,17 +40,28 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Gets the header presenter from the control's template.
         /// </summary>
-        public ContentPresenter HeaderPresenter
+        public IContentPresenter HeaderPresenter
         {
             get;
             private set;
         }
 
         /// <inheritdoc/>
-        protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
+        void IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter)
         {
-            base.OnTemplateApplied(e);
-            HeaderPresenter = e.NameScope.Find<ContentPresenter>("PART_HeaderPresenter");
+            RegisterContentPresenter(presenter);
+        }
+
+        /// <summary>
+        /// Called when an <see cref="IContentPresenter"/> is registered with the control.
+        /// </summary>
+        /// <param name="presenter">The presenter.</param>
+        protected virtual void RegisterContentPresenter(IContentPresenter presenter)
+        {
+            if (presenter.Name == "PART_HeaderPresenter")
+            {
+                HeaderPresenter = presenter;
+            }
         }
     }
 }

+ 29 - 0
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);
+            }
+        }
+
         /// <summary>
         /// Gets the visual offset from the specified ancestor.
         /// </summary>

+ 16 - 0
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<object>();
+
+            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<string> FooProperty =

+ 1 - 1
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;
 

+ 27 - 1
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<Canvas>(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<string>(x => textBlock));
+            host.DataTemplates.Add(new FuncDataTemplate<int>(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; }
         }
     }
-}
+}

+ 31 - 1
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<string>(x => textBlock),
+                    new FuncDataTemplate<int>(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;
+        }
     }
 }

+ 1 - 0
tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs

@@ -89,6 +89,7 @@ namespace Avalonia.Styling.UnitTests
             }
 
             public event EventHandler<AvaloniaPropertyChangedEventArgs> PropertyChanged;
+            public event EventHandler<AvaloniaPropertyChangedEventArgs> InheritablePropertyChanged;
             public event EventHandler<LogicalTreeAttachmentEventArgs> AttachedToLogicalTree;
             public event EventHandler<LogicalTreeAttachmentEventArgs> DetachedFromLogicalTree;
 

+ 1 - 0
tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs

@@ -119,6 +119,7 @@ namespace Avalonia.Styling.UnitTests
             }
 
             public event EventHandler<AvaloniaPropertyChangedEventArgs> PropertyChanged;
+            public event EventHandler<AvaloniaPropertyChangedEventArgs> InheritablePropertyChanged;
             public event EventHandler<LogicalTreeAttachmentEventArgs> AttachedToLogicalTree;
             public event EventHandler<LogicalTreeAttachmentEventArgs> DetachedFromLogicalTree;
 

+ 1 - 0
tests/Avalonia.Styling.UnitTests/TestControlBase.cs

@@ -19,6 +19,7 @@ namespace Avalonia.Styling.UnitTests
 
 #pragma warning disable CS0067 // Event not used
         public event EventHandler<AvaloniaPropertyChangedEventArgs> PropertyChanged;
+        public event EventHandler<AvaloniaPropertyChangedEventArgs> InheritablePropertyChanged;
 #pragma warning restore CS0067
 
         public string Name { get; set; }

+ 1 - 0
tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs

@@ -12,6 +12,7 @@ namespace Avalonia.Styling.UnitTests
     public abstract class TestTemplatedControl : ITemplatedControl, IStyleable
     {
         public event EventHandler<AvaloniaPropertyChangedEventArgs> PropertyChanged;
+        public event EventHandler<AvaloniaPropertyChangedEventArgs> InheritablePropertyChanged;
 
         public abstract Classes Classes
         {

+ 4 - 1
tests/Avalonia.UnitTests/TestTemplatedRoot.cs

@@ -18,7 +18,10 @@ namespace Avalonia.UnitTests
 
         public TestTemplatedRoot()
         {
-            Template = new FuncControlTemplate<TestTemplatedRoot>(x => new ContentPresenter());
+            Template = new FuncControlTemplate<TestTemplatedRoot>(x => new ContentPresenter
+            {
+                Name = "PART_ContentPresenter",
+            });
         }
 
         public event EventHandler<NameScopeEventArgs> Registered

+ 46 - 0
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);
+        }
     }
 }