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)
{
diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs
index 2a19f40ecb..7601b64ce9 100644
--- a/src/Avalonia.Base/AvaloniaObject.cs
+++ b/src/Avalonia.Base/AvaloniaObject.cs
@@ -26,6 +26,7 @@ namespace Avalonia
private List _directBindings;
private PropertyChangedEventHandler _inpcChanged;
private EventHandler _propertyChanged;
+ private EventHandler _inheritablePropertyChanged;
private ValueStore _values;
private ValueStore Values => _values ?? (_values = new ValueStore(this));
@@ -56,6 +57,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.
@@ -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
});
}
+ ///
+ /// 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.
@@ -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();
}
+ ///
+ /// 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/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/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 d520e2b80a..24f85ea6b1 100644
--- a/src/Avalonia.Base/ValueStore.cs
+++ b/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)
{
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/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
+}
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/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs
index f3540ea631..45efccc1fa 100644
--- a/src/Avalonia.Layout/LayoutManager.cs
+++ b/src/Avalonia.Layout/LayoutManager.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
-using System.Collections.Generic;
using Avalonia.Logging;
using Avalonia.Threading;
@@ -13,8 +12,8 @@ namespace Avalonia.Layout
///
public class LayoutManager : ILayoutManager
{
- private readonly Queue _toMeasure = new Queue();
- private readonly Queue _toArrange = new Queue();
+ 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 +79,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 +100,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 +117,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 +138,7 @@ namespace Avalonia.Layout
private void ExecuteArrangePass()
{
- while (_toArrange.Count > 0 && _toMeasure.Count == 0)
+ while (_toArrange.Count > 0)
{
var control = _toArrange.Dequeue();
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);
+ }
+ }
+ }
+ }
+}
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/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
index 2fb8e84a2e..19fb54e125 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
@@ -322,36 +322,51 @@ 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)
{
_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()
+ ///
+ /// 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)
{
- _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;
}
}
}
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.
///
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