Browse Source

Merge branch 'master' into fixes/2137-popuproot-parent

pull/2323/head
Steven Kirk 8 years ago
committed by GitHub
parent
commit
42649a5612
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      src/Avalonia.Animation/Cue.cs
  2. 63
      src/Avalonia.Base/AvaloniaObject.cs
  3. 7
      src/Avalonia.Base/IAvaloniaObject.cs
  4. 8
      src/Avalonia.Base/IPriorityValueOwner.cs
  5. 53
      src/Avalonia.Base/Logging/LoggerExtensions.cs
  6. 2
      src/Avalonia.Base/PriorityValue.cs
  7. 4
      src/Avalonia.Base/ValueStore.cs
  8. 14
      src/Avalonia.Controls/ContentControl.cs
  9. 29
      src/Avalonia.Controls/Mixins/ContentControlMixin.cs
  10. 42
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  11. 15
      src/Avalonia.Controls/Presenters/IContentPresenter.cs
  12. 37
      src/Avalonia.Controls/Primitives/HeaderedContentControl.cs
  13. 21
      src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs
  14. 21
      src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs
  15. 15
      src/Avalonia.Layout/LayoutManager.cs
  16. 79
      src/Avalonia.Layout/LayoutQueue.cs
  17. 4
      src/Avalonia.Layout/Properties/AssemblyInfo.cs
  18. 37
      src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
  19. 29
      src/Avalonia.Visuals/Visual.cs
  20. 16
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs
  21. 2
      tests/Avalonia.Controls.UnitTests/HeaderedItemsControlTests .cs
  22. 68
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  23. 28
      tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs
  24. 32
      tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs
  25. 119
      tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs
  26. 196
      tests/Avalonia.Layout.UnitTests/LayoutQueueTests.cs
  27. 1
      tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs
  28. 1
      tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs
  29. 1
      tests/Avalonia.Styling.UnitTests/TestControlBase.cs
  30. 1
      tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs
  31. 5
      tests/Avalonia.UnitTests/TestTemplatedRoot.cs
  32. 46
      tests/Avalonia.Visuals.UnitTests/VisualTests.cs

4
src/Avalonia.Animation/Cue.cs

@ -30,7 +30,7 @@ namespace Avalonia.Animation
/// <summary>
/// Parses a string to a <see cref="Cue"/> object.
/// </summary>
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)
{

63
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>

7
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
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>

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

2
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
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)
{

14
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;
}
}
}
}

29
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,

42
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);

15
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;
}
}
}

37
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;
}
}
}
}

21
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;
}
}
}
}

21
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;
}
}
}
}

15
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
/// </summary>
public class LayoutManager : ILayoutManager
{
private readonly Queue<ILayoutable> _toMeasure = new Queue<ILayoutable>();
private readonly Queue<ILayoutable> _toArrange = new Queue<ILayoutable>();
private readonly LayoutQueue<ILayoutable> _toMeasure = new LayoutQueue<ILayoutable>(v => !v.IsMeasureValid);
private readonly LayoutQueue<ILayoutable> _toArrange = new LayoutQueue<ILayoutable>(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();

79
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<T> : IReadOnlyCollection<T>
{
private struct Info
{
public bool Active;
public int Count;
}
public LayoutQueue(Func<T, bool> shouldEnqueue)
{
_shouldEnqueue = shouldEnqueue;
}
private Func<T, bool> _shouldEnqueue;
private Queue<T> _inner = new Queue<T>();
private Dictionary<T, Info> _loopQueueInfo = new Dictionary<T, Info>();
private int _maxEnqueueCountPerLoop = 1;
public int Count => _inner.Count;
public IEnumerator<T> GetEnumerator() => (_inner as IEnumerable<T>).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);
}
}
}
}
}

4
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")]

37
src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs

@ -322,36 +322,51 @@ namespace Avalonia.Rendering.SceneGraph
}
}
/// <summary>
/// Ensures that this node draw operations have been created and are mutable (in case we are using cloned operations).
/// </summary>
private void EnsureDrawOperationsCreated()
{
if (_drawOperations == null)
{
_drawOperations = new List<IRef<IDrawOperation>>();
_drawOperationsRefCounter = RefCountable.Create(Disposable.Create(DisposeDrawOperations));
_drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations));
_drawOperationsCloned = false;
}
else if (_drawOperationsCloned)
{
_drawOperations = new List<IRef<IDrawOperation>>(_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()
/// <summary>
/// Creates disposable that will dispose all items in passed draw operations after being disposed.
/// It is crucial that we don't capture current <see cref="VisualNode"/> instance
/// as draw operations can be cloned and may persist across subsequent scenes.
/// </summary>
/// <param name="drawOperations">Draw operations that need to be disposed.</param>
/// <returns>Disposable for given draw operations.</returns>
private static IDisposable CreateDisposeDrawOperations(List<IRef<IDrawOperation>> 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;
}
}
}

29
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
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 =

2
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;

68
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@ -267,6 +267,74 @@ 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<string>(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<object>(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();
//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;
var toMeasure = lm.GetType().GetField("_toMeasure", flags).GetValue(lm) as System.Collections.Generic.IEnumerable<Layout.ILayoutable>;
var toArrange = lm.GetType().GetField("_toArrange", flags).GetValue(lm) as System.Collections.Generic.IEnumerable<Layout.ILayoutable>;
Assert.Equal(0, toMeasure.Count());
Assert.Equal(0, toArrange.Count());
}
}
private FuncControlTemplate ListBoxTemplate()
{
return new FuncControlTemplate<ListBox>(parent =>

28
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; }
}
}
}
}

32
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;
}
}
}

119
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<ILayoutable>();
Size MeasureOverride(ILayoutable control, Size size)
{
@ -110,7 +108,6 @@ namespace Avalonia.Layout.UnitTests
}
};
var order = new List<ILayoutable>();
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_Infinite_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 an infinite 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_Infinite_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_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();
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<LayoutTestControl>())
{
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));
}
}
}

196
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<string>(_ => true);
var refQueue = new Queue<string>();
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<string>(_ => true);
var refQueue = new Queue<string>();
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<string>(_ => 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<string>(_ => 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<string>(_ => 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<string>(_ => 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<string>(_ => 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);
}
}
}

1
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
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
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
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
{

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

Loading…
Cancel
Save