Browse Source

Merge branch 'master' into fixes/1297-track-arrange-bug

pull/1298/head
Steven Kirk 8 years ago
parent
commit
53283aa36e
  1. 3
      .ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject
  2. 2
      src/Android/Avalonia.AndroidTestApplication/MainActivity.cs
  3. 104
      src/Avalonia.Base/AvaloniaObject.cs
  4. 24
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  5. 5
      src/Avalonia.Base/Collections/AvaloniaList.cs
  6. 15
      src/Avalonia.Base/Collections/IAvaloniaList.cs
  7. 7
      src/Avalonia.Base/IAvaloniaObject.cs
  8. 63
      src/Avalonia.Base/PriorityValue.cs
  9. 16
      src/Avalonia.Base/Reactive/AnonymousSubject`1.cs
  10. 49
      src/Avalonia.Base/Reactive/AnonymousSubject`2.cs
  11. 156
      src/Avalonia.Base/Utilities/DeferredSetter.cs
  12. 2
      src/Avalonia.Controls/Calendar/Calendar.cs
  13. 1
      src/Avalonia.Controls/Control.cs
  14. 33
      src/Avalonia.Controls/DropDown.cs
  15. 4
      src/Avalonia.Controls/IPanel.cs
  16. 11
      src/Avalonia.Controls/ItemsControl.cs
  17. 43
      src/Avalonia.Controls/Panel.cs
  18. 72
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  19. 5
      src/Avalonia.Controls/TreeView.cs
  20. 8
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  21. 6
      src/Avalonia.Diagnostics/Views/TreePage.xaml.cs
  22. 2
      src/Avalonia.Visuals/Media/PathMarkupParser.cs
  23. 50
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  24. 2
      src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
  25. 17
      src/Avalonia.Visuals/Rendering/RendererBase.cs
  26. 6
      src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs
  27. 17
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
  28. 30
      src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
  29. 13
      src/Avalonia.Visuals/Visual.cs
  30. 109
      src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs
  31. 2
      src/Markup/Avalonia.Markup.Xaml/Data/RelativeSource.cs
  32. 153
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  33. 2
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs
  34. 91
      src/Markup/Avalonia.Markup/ControlLocator.cs
  35. 10
      src/Markup/Avalonia.Markup/Data/Parsers/ArgumentListParser.cs
  36. 1
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  37. 139
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
  38. 103
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs
  39. 22
      tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs
  40. 12
      tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs
  41. 1
      tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
  42. 43
      tests/Avalonia.Benchmarks/Base/Properties.cs
  43. 4
      tests/Avalonia.Controls.UnitTests/DockPanelTests.cs
  44. 2
      tests/Avalonia.Controls.UnitTests/DropDownTests.cs
  45. 84
      tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs
  46. 2
      tests/Avalonia.Controls.UnitTests/GridTests.cs
  47. 71
      tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs
  48. 60
      tests/Avalonia.Controls.UnitTests/PanelTests.cs
  49. 108
      tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs
  50. 8
      tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs
  51. 2
      tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
  52. 12
      tests/Avalonia.Controls.UnitTests/StackPanelTests.cs
  53. 2
      tests/Avalonia.Controls.UnitTests/TabControlTests.cs
  54. 2
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  55. 52
      tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs
  56. 124
      tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Arrows.cs
  57. 168
      tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs
  58. 8
      tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs
  59. 10
      tests/Avalonia.Markup.UnitTests/ControlLocatorTests.cs
  60. 1
      tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj
  61. 162
      tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs
  62. 8
      tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_ElementName.cs
  63. 32
      tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs
  64. 174
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs
  65. 2
      tests/Avalonia.RenderTests/GeometryClippingTests.cs
  66. 2
      tests/Avalonia.RenderTests/Media/VisualBrushTests.cs
  67. 2
      tests/Avalonia.RenderTests/OpacityMaskTests.cs
  68. 2
      tests/Avalonia.RenderTests/SVGPathTests.cs
  69. 5
      tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs
  70. 5
      tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs
  71. 5
      tests/Avalonia.Styling.UnitTests/TestControlBase.cs
  72. 5
      tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs
  73. 18
      tests/Avalonia.UnitTests/TestRoot.cs
  74. 12
      tests/Avalonia.Visuals.UnitTests/RenderTests_Culling.cs
  75. 273
      tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs
  76. 14
      tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs
  77. 14
      tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests_HitTesting.cs
  78. 6
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs
  79. 75
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs

3
.ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject

@ -1,7 +1,6 @@
<ProjectConfiguration>
<Settings>
<DefaultTestTimeout>1000</DefaultTestTimeout>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
<DefaultTestTimeout>3000</DefaultTestTimeout>
<PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully>
</Settings>
</ProjectConfiguration>

2
src/Android/Avalonia.AndroidTestApplication/MainActivity.cs

@ -56,7 +56,7 @@ namespace Avalonia.AndroidTestApplication
{
Margin = new Thickness(30),
Background = Brushes.Yellow,
Children = new Avalonia.Controls.Controls
Children =
{
new TextBlock
{

104
src/Avalonia.Base/AvaloniaObject.cs

@ -51,6 +51,21 @@ namespace Avalonia
/// </summary>
private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged;
private DeferredSetter<AvaloniaProperty, object> _directDeferredSetter;
/// <summary>
/// Delayed setter helper for direct properties. Used to fix #855.
/// </summary>
private DeferredSetter<AvaloniaProperty, object> DirectPropertyDeferredSetter
{
get
{
return _directDeferredSetter ??
(_directDeferredSetter = new DeferredSetter<AvaloniaProperty, object>());
}
}
/// <summary>
/// Initializes a new instance of the <see cref="AvaloniaObject"/> class.
/// </summary>
@ -225,6 +240,19 @@ namespace Avalonia
return (T)GetValue((AvaloniaProperty)property);
}
/// <summary>
/// Checks whether a <see cref="AvaloniaProperty"/> is animating.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>True if the property is animating, otherwise false.</returns>
public bool IsAnimating(AvaloniaProperty property)
{
Contract.Requires<ArgumentNullException>(property != null);
VerifyAccess();
return _values.TryGetValue(property, out PriorityValue value) ? value.IsAnimating : false;
}
/// <summary>
/// Checks whether a <see cref="AvaloniaProperty"/> is set on this object.
/// </summary>
@ -539,6 +567,45 @@ namespace Avalonia
}
}
/// <summary>
/// A callback type for encapsulating complex logic for setting direct properties.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="value">The value to which to set the property.</param>
/// <param name="field">The backing field for the property.</param>
/// <param name="notifyWrapper">A wrapper for the property-changed notification.</param>
protected delegate void SetAndRaiseCallback<T>(T value, ref T field, Action<Action> notifyWrapper);
/// <summary>
/// Sets the backing field for a direct avalonia property, raising the
/// <see cref="PropertyChanged"/> event if the value has changed.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="field">The backing field.</param>
/// <param name="setterCallback">A callback called to actually set the value to the backing field.</param>
/// <param name="value">The value.</param>
/// <returns>
/// True if the value changed, otherwise false.
/// </returns>
protected bool SetAndRaise<T>(
AvaloniaProperty<T> property,
ref T field,
SetAndRaiseCallback<T> setterCallback,
T value)
{
Contract.Requires<ArgumentNullException>(setterCallback != null);
return DirectPropertyDeferredSetter.SetAndNotify(
property,
ref field,
(object val, ref T backing, Action<Action> notify) =>
{
setterCallback((T)val, ref backing, notify);
return true;
},
value);
}
/// <summary>
/// Sets the backing field for a direct avalonia property, raising the
/// <see cref="PropertyChanged"/> event if the value has changed.
@ -553,17 +620,32 @@ namespace Avalonia
protected bool SetAndRaise<T>(AvaloniaProperty<T> property, ref T field, T value)
{
VerifyAccess();
if (!object.Equals(field, value))
{
var old = field;
field = value;
RaisePropertyChanged(property, old, value, BindingPriority.LocalValue);
return true;
}
else
{
return false;
}
return SetAndRaise(
property,
ref field,
(T val, ref T backing, Action<Action> notifyWrapper)
=> SetAndRaiseCore(property, ref backing, val, notifyWrapper),
value);
}
/// <summary>
/// Default assignment logic for SetAndRaise.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="field">The backing field.</param>
/// <param name="value">The value.</param>
/// <param name="notifyWrapper">A wrapper for the property-changed notification.</param>
/// <returns>
/// True if the value changed, otherwise false.
/// </returns>
private bool SetAndRaiseCore<T>(AvaloniaProperty property, ref T field, T value, Action<Action> notifyWrapper)
{
var old = field;
field = value;
notifyWrapper(() => RaisePropertyChanged(property, old, value, BindingPriority.LocalValue));
return true;
}
/// <summary>

24
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -138,17 +138,9 @@ namespace Avalonia
AvaloniaProperty property,
BindingPriority priority = BindingPriority.LocalValue)
{
// TODO: Subject.Create<T> is not yet in stable Rx : once it is, remove the
// AnonymousSubject classes and use Subject.Create<T>.
var output = new Subject<object>();
var result = new AnonymousSubject<object>(
Observer.Create<object>(
x => output.OnNext(x),
e => output.OnError(e),
() => output.OnCompleted()),
return Subject.Create<object>(
Observer.Create<object>(x => o.SetValue(property, x, priority)),
o.GetObservable(property));
o.Bind(property, output, priority);
return result;
}
/// <summary>
@ -169,17 +161,9 @@ namespace Avalonia
AvaloniaProperty<T> property,
BindingPriority priority = BindingPriority.LocalValue)
{
// TODO: Subject.Create<T> is not yet in stable Rx : once it is, remove the
// AnonymousSubject classes from this file and use Subject.Create<T>.
var output = new Subject<T>();
var result = new AnonymousSubject<T>(
Observer.Create<T>(
x => output.OnNext(x),
e => output.OnError(e),
() => output.OnCompleted()),
return Subject.Create<T>(
Observer.Create<T>(x => o.SetValue(property, x, priority)),
o.GetObservable(property));
o.Bind(property, output, priority);
return result;
}
/// <summary>

5
src/Avalonia.Base/Collections/AvaloniaList.cs

@ -350,14 +350,15 @@ namespace Avalonia.Collections
public void MoveRange(int oldIndex, int count, int newIndex)
{
var items = _inner.GetRange(oldIndex, count);
var modifiedNewIndex = newIndex;
_inner.RemoveRange(oldIndex, count);
if (newIndex > oldIndex)
{
newIndex -= count;
modifiedNewIndex -= count;
}
_inner.InsertRange(newIndex, items);
_inner.InsertRange(modifiedNewIndex, items);
if (_collectionChanged != null)
{

15
src/Avalonia.Base/Collections/IAvaloniaList.cs

@ -36,6 +36,21 @@ namespace Avalonia.Collections
/// <param name="items">The items.</param>
void InsertRange(int index, IEnumerable<T> items);
/// <summary>
/// Moves an item to a new index.
/// </summary>
/// <param name="oldIndex">The index of the item to move.</param>
/// <param name="newIndex">The index to move the item to.</param>
void Move(int oldIndex, int newIndex);
/// <summary>
/// Moves multiple items to a new index.
/// </summary>
/// <param name="oldIndex">The first index of the items to move.</param>
/// <param name="count">The number of items to move.</param>
/// <param name="newIndex">The index to move the items to.</param>
void MoveRange(int oldIndex, int count, int newIndex);
/// <summary>
/// Removes multiple items from the collection.
/// </summary>

7
src/Avalonia.Base/IAvaloniaObject.cs

@ -31,6 +31,13 @@ namespace Avalonia
/// <returns>The value.</returns>
T GetValue<T>(AvaloniaProperty<T> property);
/// <summary>
/// Checks whether a <see cref="AvaloniaProperty"/> is animating.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>True if the property is animating, otherwise false.</returns>
bool IsAnimating(AvaloniaProperty property);
/// <summary>
/// Checks whether a <see cref="AvaloniaProperty"/> is set on this object.
/// </summary>

63
src/Avalonia.Base/PriorityValue.cs

@ -28,8 +28,10 @@ namespace Avalonia
{
private readonly Type _valueType;
private readonly SingleOrDictionary<int, PriorityLevel> _levels = new SingleOrDictionary<int, PriorityLevel>();
private object _value;
private readonly Func<object, object> _validate;
private static readonly DeferredSetter<PriorityValue, (object value, int priority)> delayedSetter = new DeferredSetter<PriorityValue, (object, int)>();
private (object value, int priority) _value;
/// <summary>
/// Initializes a new instance of the <see cref="PriorityValue"/> class.
@ -47,11 +49,22 @@ namespace Avalonia
Owner = owner;
Property = property;
_valueType = valueType;
_value = AvaloniaProperty.UnsetValue;
ValuePriority = int.MaxValue;
_value = (AvaloniaProperty.UnsetValue, int.MaxValue);
_validate = validate;
}
/// <summary>
/// Gets a value indicating whether the property is animating.
/// </summary>
public bool IsAnimating
{
get
{
return ValuePriority <= (int)BindingPriority.Animation &&
GetLevel(ValuePriority).ActiveBindingIndex != -1;
}
}
/// <summary>
/// Gets the owner of the value.
/// </summary>
@ -65,16 +78,12 @@ namespace Avalonia
/// <summary>
/// Gets the current value.
/// </summary>
public object Value => _value;
public object Value => _value.value;
/// <summary>
/// Gets the priority of the binding that is currently active.
/// </summary>
public int ValuePriority
{
get;
private set;
}
public int ValuePriority => _value.priority;
/// <summary>
/// Adds a new binding.
@ -234,25 +243,36 @@ namespace Avalonia
/// <param name="priority">The priority level that the value came from.</param>
private void UpdateValue(object value, int priority)
{
var notification = value as BindingNotification;
delayedSetter.SetAndNotify(this,
ref _value,
UpdateCore,
(value, priority));
}
private bool UpdateCore(
(object value, int priority) update,
ref (object value, int priority) backing,
Action<Action> notify)
{
var val = update.value;
var notification = val as BindingNotification;
object castValue;
if (notification != null)
{
value = (notification.HasValue) ? notification.Value : null;
val = (notification.HasValue) ? notification.Value : null;
}
if (TypeUtilities.TryConvertImplicit(_valueType, value, out castValue))
if (TypeUtilities.TryConvertImplicit(_valueType, val, out castValue))
{
var old = _value;
var old = backing.value;
if (_validate != null && castValue != AvaloniaProperty.UnsetValue)
{
castValue = _validate(castValue);
}
ValuePriority = priority;
_value = castValue;
backing = (castValue, update.priority);
if (notification?.HasValue == true)
{
@ -261,7 +281,7 @@ namespace Avalonia
if (notification == null || notification.HasValue)
{
Owner?.Changed(this, old, _value);
notify(() => Owner?.Changed(this, old, Value));
}
if (notification != null)
@ -272,14 +292,15 @@ namespace Avalonia
else
{
Logger.Error(
LogArea.Binding,
LogArea.Binding,
Owner,
"Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})",
Property.Name,
_valueType,
value,
value?.GetType());
Property.Name,
_valueType,
val,
val?.GetType());
}
return true;
}
}
}

16
src/Avalonia.Base/Reactive/AnonymousSubject`1.cs

@ -1,16 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Subjects;
namespace Avalonia.Reactive
{
public class AnonymousSubject<T> : AnonymousSubject<T, T>, ISubject<T>
{
public AnonymousSubject(IObserver<T> observer, IObservable<T> observable)
: base(observer, observable)
{
}
}
}

49
src/Avalonia.Base/Reactive/AnonymousSubject`2.cs

@ -1,49 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Subjects;
namespace Avalonia.Reactive
{
public class AnonymousSubject<T, U> : ISubject<T, U>
{
private readonly IObserver<T> _observer;
private readonly IObservable<U> _observable;
public AnonymousSubject(IObserver<T> observer, IObservable<U> observable)
{
_observer = observer;
_observable = observable;
}
public void OnCompleted()
{
_observer.OnCompleted();
}
public void OnError(Exception error)
{
if (error == null)
throw new ArgumentNullException("error");
_observer.OnError(error);
}
public void OnNext(T value)
{
_observer.OnNext(value);
}
public IDisposable Subscribe(IObserver<U> observer)
{
if (observer == null)
throw new ArgumentNullException("observer");
//
// [OK] Use of unsafe Subscribe: non-pretentious wrapping of an observable sequence.
//
return _observable.Subscribe/*Unsafe*/(observer);
}
}
}

156
src/Avalonia.Base/Utilities/DeferredSetter.cs

@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using System.Runtime.CompilerServices;
using System.Text;
namespace Avalonia.Utilities
{
/// <summary>
/// A utility class to enable deferring assignment until after property-changed notifications are sent.
/// </summary>
/// <typeparam name="TProperty">The type of the object that represents the property.</typeparam>
/// <typeparam name="TSetRecord">The type of value with which to track the delayed assignment.</typeparam>
class DeferredSetter<TProperty, TSetRecord>
where TProperty: class
{
private struct NotifyDisposable : IDisposable
{
private readonly SettingStatus status;
internal NotifyDisposable(SettingStatus status)
{
this.status = status;
status.Notifying = true;
}
public void Dispose()
{
status.Notifying = false;
}
}
/// <summary>
/// Information on current setting/notification status of a property.
/// </summary>
private class SettingStatus
{
public bool Notifying { get; set; }
private Queue<TSetRecord> pendingValues;
public Queue<TSetRecord> PendingValues
{
get
{
return pendingValues ?? (pendingValues = new Queue<TSetRecord>());
}
}
}
private readonly ConditionalWeakTable<TProperty, SettingStatus> setRecords = new ConditionalWeakTable<TProperty, SettingStatus>();
/// <summary>
/// Mark the property as currently notifying.
/// </summary>
/// <param name="property">The property to mark as notifying.</param>
/// <returns>Returns a disposable that when disposed, marks the property as done notifying.</returns>
private NotifyDisposable MarkNotifying(TProperty property)
{
Contract.Requires<InvalidOperationException>(!IsNotifying(property));
return new NotifyDisposable(setRecords.GetOrCreateValue(property));
}
/// <summary>
/// Check if the property is currently notifying listeners.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>If the property is currently notifying listeners.</returns>
private bool IsNotifying(TProperty property)
=> setRecords.TryGetValue(property, out var value) && value.Notifying;
/// <summary>
/// Add a pending assignment for the property.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="value">The value to assign.</param>
private void AddPendingSet(TProperty property, TSetRecord value)
{
Contract.Requires<InvalidOperationException>(IsNotifying(property));
setRecords.GetOrCreateValue(property).PendingValues.Enqueue(value);
}
/// <summary>
/// Checks if there are any pending assignments for the property.
/// </summary>
/// <param name="property">The property to check.</param>
/// <returns>If the property has any pending assignments.</returns>
private bool HasPendingSet(TProperty property)
{
return setRecords.TryGetValue(property, out var status) && status.PendingValues.Count != 0;
}
/// <summary>
/// Gets the first pending assignment for the property.
/// </summary>
/// <param name="property">The property to check.</param>
/// <returns>The first pending assignment for the property.</returns>
private TSetRecord GetFirstPendingSet(TProperty property)
{
return setRecords.GetOrCreateValue(property).PendingValues.Dequeue();
}
public delegate bool SetterDelegate<TValue>(TSetRecord record, ref TValue backing, Action<Action> notifyCallback);
/// <summary>
/// Set the property and notify listeners while ensuring we don't get into a stack overflow as happens with #855 and #824
/// </summary>
/// <param name="property">The property to set.</param>
/// <param name="backing">The backing field for the property</param>
/// <param name="setterCallback">
/// A callback that actually sets the property.
/// The first parameter is the value to set, and the second is a wrapper that takes a callback that sends the property-changed notification.
/// </param>
/// <param name="value">The value to try to set.</param>
public bool SetAndNotify<TValue>(
TProperty property,
ref TValue backing,
SetterDelegate<TValue> setterCallback,
TSetRecord value)
{
Contract.Requires<ArgumentNullException>(setterCallback != null);
if (!IsNotifying(property))
{
bool updated = false;
if (!object.Equals(value, backing))
{
updated = setterCallback(value, ref backing, notification =>
{
using (MarkNotifying(property))
{
notification();
}
});
}
while (HasPendingSet(property))
{
updated |= setterCallback(GetFirstPendingSet(property), ref backing, notification =>
{
using (MarkNotifying(property))
{
notification();
}
});
}
return updated;
}
else if(!object.Equals(value, backing))
{
AddPendingSet(property, value);
}
return false;
}
}
}

2
src/Avalonia.Controls/Calendar/Calendar.cs

@ -549,7 +549,7 @@ namespace Avalonia.Controls
}
else
{
if (addedDate.HasValue && !(SelectedDates.Count > 0 && SelectedDates[0] == addedDate.Value))
if (!(SelectedDates.Count > 0 && SelectedDates[0] == addedDate.Value))
{
foreach (DateTime item in SelectedDates)
{

1
src/Avalonia.Controls/Control.cs

@ -621,7 +621,6 @@ namespace Avalonia.Controls
Contract.Requires<ArgumentNullException>(property != null);
Contract.Requires<ArgumentNullException>(selector != null);
Contract.Requires<ArgumentNullException>(className != null);
Contract.Requires<ArgumentNullException>(property != null);
if (string.IsNullOrWhiteSpace(className))
{

33
src/Avalonia.Controls/DropDown.cs

@ -96,6 +96,16 @@ namespace Avalonia.Controls
this.UpdateSelectionBoxItem(this.SelectedItem);
}
protected override void OnGotFocus(GotFocusEventArgs e)
{
base.OnGotFocus(e);
if (!e.Handled && e.NavigationMethod == NavigationMethod.Directional)
{
e.Handled = UpdateSelectionFromEventSource(e.Source);
}
}
/// <inheritdoc/>
protected override void OnKeyDown(KeyEventArgs e)
{
@ -104,7 +114,7 @@ namespace Avalonia.Controls
if (!e.Handled)
{
if (e.Key == Key.F4 ||
(e.Key == Key.Down && ((e.Modifiers & InputModifiers.Alt) != 0)))
((e.Key == Key.Down || e.Key == Key.Up) && ((e.Modifiers & InputModifiers.Alt) != 0)))
{
IsDropDownOpen = !IsDropDownOpen;
e.Handled = true;
@ -114,6 +124,27 @@ namespace Avalonia.Controls
IsDropDownOpen = false;
e.Handled = true;
}
if (!IsDropDownOpen)
{
if (e.Key == Key.Down)
{
if (SelectedIndex == -1)
SelectedIndex = 0;
if (++SelectedIndex >= ItemCount)
SelectedIndex = 0;
e.Handled = true;
}
else if (e.Key == Key.Up)
{
if (--SelectedIndex < 0)
SelectedIndex = ItemCount - 1;
e.Handled = true;
}
}
}
}

4
src/Avalonia.Controls/IPanel.cs

@ -9,8 +9,8 @@ namespace Avalonia.Controls
public interface IPanel : IControl
{
/// <summary>
/// Gets or sets the children of the <see cref="Panel"/>.
/// Gets the children of the <see cref="Panel"/>.
/// </summary>
Controls Children { get; set; }
Controls Children { get; }
}
}

11
src/Avalonia.Controls/ItemsControl.cs

@ -11,6 +11,7 @@ using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
@ -106,6 +107,12 @@ namespace Avalonia.Controls
set { SetAndRaise(ItemsProperty, ref _items, value); }
}
public int ItemCount
{
get;
private set;
}
/// <summary>
/// Gets or sets the panel used to display the items.
/// </summary>
@ -352,6 +359,10 @@ namespace Avalonia.Controls
RemoveControlItemsFromLogicalChildren(e.OldItems);
break;
}
int? count = (Items as IList)?.Count;
if (count != null)
ItemCount = (int)count;
var collection = sender as ICollection;
PseudoClasses.Set(":empty", collection == null || collection.Count == 0);

43
src/Avalonia.Controls/Panel.cs

@ -25,8 +25,6 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IBrush> BackgroundProperty =
Border.BackgroundProperty.AddOwner<Panel>();
private readonly Controls _children = new Controls();
/// <summary>
/// Initializes static members of the <see cref="Panel"/> class.
/// </summary>
@ -40,38 +38,14 @@ namespace Avalonia.Controls
/// </summary>
public Panel()
{
_children.CollectionChanged += ChildrenChanged;
Children.CollectionChanged += ChildrenChanged;
}
/// <summary>
/// Gets or sets the children of the <see cref="Panel"/>.
/// Gets the children of the <see cref="Panel"/>.
/// </summary>
/// <remarks>
/// Even though this property can be set, the setter is only intended for use in object
/// initializers. Assigning to this property does not change the underlying collection,
/// it simply clears the existing collection and adds the contents of the assigned
/// collection.
/// </remarks>
[Content]
public Controls Children
{
get
{
return _children;
}
set
{
Contract.Requires<ArgumentNullException>(value != null);
if (_children != value)
{
VisualChildren.Clear();
_children.Clear();
_children.AddRange(value);
}
}
}
public Controls Children { get; } = new Controls();
/// <summary>
/// Gets or Sets Panel background brush.
@ -115,6 +89,11 @@ namespace Avalonia.Controls
VisualChildren.AddRange(e.NewItems.OfType<Visual>());
break;
case NotifyCollectionChangedAction.Move:
LogicalChildren.MoveRange(e.OldStartingIndex, e.OldItems.Count, e.NewStartingIndex);
VisualChildren.MoveRange(e.OldStartingIndex, e.OldItems.Count, e.NewStartingIndex);
break;
case NotifyCollectionChangedAction.Remove:
controls = e.OldItems.OfType<Control>().ToList();
LogicalChildren.RemoveAll(controls);
@ -132,11 +111,7 @@ namespace Avalonia.Controls
break;
case NotifyCollectionChangedAction.Reset:
controls = e.OldItems.OfType<Control>().ToList();
LogicalChildren.Clear();
VisualChildren.Clear();
VisualChildren.AddRange(_children);
break;
throw new NotSupportedException();
}
InvalidateMeasure();

72
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -151,15 +151,23 @@ namespace Avalonia.Controls.Primitives
{
if (_updateCount == 0)
{
var old = SelectedIndex;
var effective = (value >= 0 && value < Items?.Cast<object>().Count()) ? value : -1;
if (old != effective)
SetAndRaise(SelectedIndexProperty, ref _selectedIndex, (int val, ref int backing, Action<Action> notifyWrapper) =>
{
_selectedIndex = effective;
RaisePropertyChanged(SelectedIndexProperty, old, effective, BindingPriority.LocalValue);
SelectedItem = ElementAt(Items, effective);
}
var old = backing;
var effective = (val >= 0 && val < Items?.Cast<object>().Count()) ? val : -1;
if (old != effective)
{
backing = effective;
notifyWrapper(() =>
RaisePropertyChanged(
SelectedIndexProperty,
old,
effective,
BindingPriority.LocalValue));
SelectedItem = ElementAt(Items, effective);
}
}, value);
}
else
{
@ -183,31 +191,41 @@ namespace Avalonia.Controls.Primitives
{
if (_updateCount == 0)
{
var old = SelectedItem;
var index = IndexOf(Items, value);
var effective = index != -1 ? value : null;
if (!object.Equals(effective, old))
SetAndRaise(SelectedItemProperty, ref _selectedItem, (object val, ref object backing, Action<Action> notifyWrapper) =>
{
_selectedItem = effective;
RaisePropertyChanged(SelectedItemProperty, old, effective, BindingPriority.LocalValue);
SelectedIndex = index;
var old = backing;
var index = IndexOf(Items, val);
var effective = index != -1 ? val : null;
if (effective != null)
if (!object.Equals(effective, old))
{
if (SelectedItems.Count != 1 || SelectedItems[0] != effective)
backing = effective;
notifyWrapper(() =>
RaisePropertyChanged(
SelectedItemProperty,
old,
effective,
BindingPriority.LocalValue));
SelectedIndex = index;
if (effective != null)
{
if (SelectedItems.Count != 1 || SelectedItems[0] != effective)
{
_syncingSelectedItems = true;
SelectedItems.Clear();
SelectedItems.Add(effective);
_syncingSelectedItems = false;
}
}
else if (SelectedItems.Count > 0)
{
_syncingSelectedItems = true;
SelectedItems.Clear();
SelectedItems.Add(effective);
_syncingSelectedItems = false;
}
}
else if (SelectedItems.Count > 0)
{
SelectedItems.Clear();
}
}
}, value);
}
else
{
@ -297,7 +315,7 @@ namespace Avalonia.Controls.Primitives
.OfType<IControl>()
.FirstOrDefault(x => x.LogicalParent == this && ItemContainerGenerator?.IndexFromContainer(x) != -1);
return item as IControl;
return item;
}
/// <inheritdoc/>

5
src/Avalonia.Controls/TreeView.cs

@ -176,10 +176,7 @@ namespace Avalonia.Controls
SelectedItem = item;
if (SelectedItem != null)
{
MarkContainerSelected(container, true);
}
MarkContainerSelected(container, true);
}
}

8
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -134,12 +134,14 @@ namespace Avalonia.Controls
protected override IInputElement GetControlInDirection(NavigationDirection direction, IControl from)
{
if (from == null)
return null;
var logicalScrollable = Parent as ILogicalScrollable;
var fromControl = from as IControl;
if (logicalScrollable?.IsLogicalScrollEnabled == true && fromControl != null)
if (logicalScrollable?.IsLogicalScrollEnabled == true)
{
return logicalScrollable.GetControlInDirection(direction, fromControl);
return logicalScrollable.GetControlInDirection(direction, from);
}
else
{

6
src/Avalonia.Diagnostics/Views/TreePage.xaml.cs

@ -27,6 +27,12 @@ namespace Avalonia.Diagnostics.Views
if (layer != null)
{
if (_adorner != null)
{
((Panel)_adorner.Parent).Children.Remove(_adorner);
_adorner = null;
}
_adorner = new Rectangle
{
Fill = new SolidColorBrush(0x80a0c5e8),

2
src/Avalonia.Visuals/Media/PathMarkupParser.cs

@ -320,7 +320,7 @@ namespace Avalonia.Media
if (c == 'E')
{
readSign = false;
readExponent = c == 'E';
readExponent = true;
}
}
else

50
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@ -25,11 +25,9 @@ namespace Avalonia.Rendering
private readonly IRenderLoop _renderLoop;
private readonly IVisual _root;
private readonly ISceneBuilder _sceneBuilder;
private readonly RenderLayers _layers;
private bool _running;
private Scene _scene;
private IRenderTarget _renderTarget;
private DirtyVisuals _dirty;
private IRenderTargetBitmapImpl _overlay;
private bool _updateQueued;
@ -56,7 +54,7 @@ namespace Avalonia.Rendering
_dispatcher = dispatcher ?? Dispatcher.UIThread;
_root = root;
_sceneBuilder = sceneBuilder ?? new SceneBuilder();
_layers = new RenderLayers();
Layers = new RenderLayers();
_renderLoop = renderLoop;
}
@ -78,9 +76,9 @@ namespace Avalonia.Rendering
Contract.Requires<ArgumentNullException>(renderTarget != null);
_root = root;
_renderTarget = renderTarget;
RenderTarget = renderTarget;
_sceneBuilder = sceneBuilder ?? new SceneBuilder();
_layers = new RenderLayers();
Layers = new RenderLayers();
}
/// <inheritdoc/>
@ -94,6 +92,16 @@ namespace Avalonia.Rendering
/// </summary>
public string DebugFramesPath { get; set; }
/// <summary>
/// Gets the render layers.
/// </summary>
internal RenderLayers Layers { get; }
/// <summary>
/// Gets the current render target.
/// </summary>
internal IRenderTarget RenderTarget { get; private set; }
/// <inheritdoc/>
public void AddDirty(IVisual visual)
{
@ -173,9 +181,9 @@ namespace Avalonia.Rendering
bool renderOverlay = DrawDirtyRects || DrawFps;
bool composite = false;
if (_renderTarget == null)
if (RenderTarget == null)
{
_renderTarget = ((IRenderRoot)_root).CreateRenderTarget();
RenderTarget = ((IRenderRoot)_root).CreateRenderTarget();
}
if (renderOverlay)
@ -191,8 +199,8 @@ namespace Avalonia.Rendering
if (scene.Generation != _lastSceneId)
{
context = _renderTarget.CreateDrawingContext(this);
_layers.Update(scene, context);
context = RenderTarget.CreateDrawingContext(this);
Layers.Update(scene, context);
RenderToLayers(scene);
@ -208,13 +216,13 @@ namespace Avalonia.Rendering
if (renderOverlay)
{
context = context ?? _renderTarget.CreateDrawingContext(this);
context = context ?? RenderTarget.CreateDrawingContext(this);
RenderOverlay(scene, context);
RenderComposite(scene, context);
}
else if (composite)
{
context = context ?? _renderTarget.CreateDrawingContext(this);
context = context ?? RenderTarget.CreateDrawingContext(this);
RenderComposite(scene, context);
}
@ -224,8 +232,8 @@ namespace Avalonia.Rendering
catch (RenderTargetCorruptedException ex)
{
Logging.Logger.Information("Renderer", this, "Render target was corrupted. Exception: {0}", ex);
_renderTarget?.Dispose();
_renderTarget = null;
RenderTarget?.Dispose();
RenderTarget = null;
}
}
@ -235,9 +243,11 @@ namespace Avalonia.Rendering
{
clipBounds = node.ClipBounds.Intersect(clipBounds);
if (!clipBounds.IsEmpty)
if (!clipBounds.IsEmpty && node.Opacity > 0)
{
node.BeginRender(context);
var isLayerRoot = node.Visual == layer;
node.BeginRender(context, isLayerRoot);
foreach (var operation in node.DrawOperations)
{
@ -251,7 +261,7 @@ namespace Avalonia.Rendering
Render(context, (VisualNode)child, layer, clipBounds);
}
node.EndRender(context);
node.EndRender(context, isLayerRoot);
}
}
}
@ -262,7 +272,7 @@ namespace Avalonia.Rendering
{
foreach (var layer in scene.Layers)
{
var renderTarget = _layers[layer.LayerRoot].Bitmap;
var renderTarget = Layers[layer.LayerRoot].Bitmap;
var node = (VisualNode)scene.FindNode(layer.LayerRoot);
if (node != null)
@ -322,7 +332,7 @@ namespace Avalonia.Rendering
foreach (var layer in scene.Layers)
{
var bitmap = _layers[layer.LayerRoot].Bitmap;
var bitmap = Layers[layer.LayerRoot].Bitmap;
var sourceRect = new Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight);
if (layer.GeometryClip != null)
@ -353,7 +363,7 @@ namespace Avalonia.Rendering
if (DrawFps)
{
RenderFps(context, clientRect, true);
RenderFps(context, clientRect, scene.Layers.Count);
}
}
@ -442,7 +452,7 @@ namespace Avalonia.Rendering
{
var index = 0;
foreach (var layer in _layers)
foreach (var layer in Layers)
{
var fileName = Path.Combine(DebugFramesPath, $"frame-{id}-layer-{index++}.png");
layer.Bitmap.Save(fileName);

2
src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs

@ -69,7 +69,7 @@ namespace Avalonia.Rendering
if (DrawFps)
{
RenderFps(context.PlatformImpl, _root.Bounds, true);
RenderFps(context.PlatformImpl, _root.Bounds, null);
}
}
}

17
src/Avalonia.Visuals/Rendering/RendererBase.cs

@ -22,15 +22,12 @@ namespace Avalonia.Rendering
};
}
protected void RenderFps(IDrawingContextImpl context, Rect clientRect, bool incrementFrameCount)
protected void RenderFps(IDrawingContextImpl context, Rect clientRect, int? layerCount)
{
var now = _stopwatch.Elapsed;
var elapsed = now - _lastFpsUpdate;
if (incrementFrameCount)
{
++_framesThisSecond;
}
++_framesThisSecond;
if (elapsed.TotalSeconds > 1)
{
@ -39,7 +36,15 @@ namespace Avalonia.Rendering
_lastFpsUpdate = now;
}
_fpsText.Text = string.Format("FPS: {0:000}", _fps);
if (layerCount.HasValue)
{
_fpsText.Text = string.Format("Layers: {0} FPS: {1:000}", layerCount, _fps);
}
else
{
_fpsText.Text = string.Format("FPS: {0:000}", _fps);
}
var size = _fpsText.Measure();
var rect = new Rect(clientRect.Right - size.Width, 0, size.Width, size.Height);

6
src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs

@ -72,13 +72,15 @@ namespace Avalonia.Rendering.SceneGraph
/// Sets up the drawing context for rendering the node's geometry.
/// </summary>
/// <param name="context">The drawing context.</param>
void BeginRender(IDrawingContextImpl context);
/// <param name="skipOpacity">Whether to skip pushing the control's opacity.</param>
void BeginRender(IDrawingContextImpl context, bool skipOpacity);
/// <summary>
/// Resets the drawing context after rendering the node's geometry.
/// </summary>
/// <param name="context">The drawing context.</param>
void EndRender(IDrawingContextImpl context);
/// <param name="skipOpacity">Whether to skip popping the control's opacity.</param>
void EndRender(IDrawingContextImpl context, bool skipOpacity);
/// <summary>
/// Hit test the geometry in this node.

17
src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs

@ -167,7 +167,6 @@ namespace Avalonia.Rendering.SceneGraph
using (context.PushPostTransform(m))
using (context.PushTransformContainer())
{
var startLayer = opacity < 1 || visual.OpacityMask != null;
var clipBounds = bounds.TransformToAABB(contextImpl.Transform).Intersect(clip);
forceRecurse = forceRecurse ||
@ -179,9 +178,11 @@ namespace Avalonia.Rendering.SceneGraph
node.ClipToBounds = clipToBounds;
node.GeometryClip = visual.Clip?.PlatformImpl;
node.Opacity = opacity;
node.OpacityMask = visual.OpacityMask;
if (startLayer)
// TODO: Check equality between node.OpacityMask and visual.OpacityMask before assigning.
node.OpacityMask = visual.OpacityMask?.ToImmutable();
if (ShouldStartLayer(visual))
{
if (node.LayerRoot != visual)
{
@ -192,7 +193,7 @@ namespace Avalonia.Rendering.SceneGraph
UpdateLayer(node, scene.Layers[node.LayerRoot]);
}
}
else if (!startLayer && node.LayerRoot == node.Visual && node.Parent != null)
else if (node.LayerRoot == node.Visual && node.Parent != null)
{
ClearLayer(scene, node);
}
@ -366,6 +367,14 @@ namespace Avalonia.Rendering.SceneGraph
}
}
private static bool ShouldStartLayer(IVisual visual)
{
var o = visual as IAvaloniaObject;
return visual.VisualChildren.Count > 0 &&
o != null &&
o.IsAnimating(Visual.OpacityProperty);
}
private static IGeometryImpl CreateLayerGeometryClip(VisualNode node)
{
IGeometryImpl result = null;

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

@ -22,6 +22,7 @@ namespace Avalonia.Rendering.SceneGraph
private List<IVisualNode> _children;
private List<IDrawOperation> _drawOperations;
private bool _drawOperationsCloned;
private Matrix transformRestore;
/// <summary>
/// Initializes a new instance of the <see cref="VisualNode"/> class.
@ -218,8 +219,10 @@ namespace Avalonia.Rendering.SceneGraph
}
/// <inheritdoc/>
public void BeginRender(IDrawingContextImpl context)
public void BeginRender(IDrawingContextImpl context, bool skipOpacity)
{
transformRestore = context.Transform;
if (ClipToBounds)
{
context.Transform = Matrix.Identity;
@ -228,24 +231,47 @@ namespace Avalonia.Rendering.SceneGraph
context.Transform = Transform;
if (Opacity != 1 && !skipOpacity)
{
context.PushOpacity(Opacity);
}
if (GeometryClip != null)
{
context.PushGeometryClip(GeometryClip);
}
if (OpacityMask != null)
{
context.PushOpacityMask(OpacityMask, ClipBounds);
}
}
/// <inheritdoc/>
public void EndRender(IDrawingContextImpl context)
public void EndRender(IDrawingContextImpl context, bool skipOpacity)
{
if (OpacityMask != null)
{
context.PopOpacityMask();
}
if (GeometryClip != null)
{
context.PopGeometryClip();
}
if (Opacity != 1 && !skipOpacity)
{
context.PopOpacity();
}
if (ClipToBounds)
{
context.Transform = Matrix.Identity;
context.PopClip();
}
context.Transform = transformRestore;
}
private Rect CalculateBounds()

13
src/Avalonia.Visuals/Visual.cs

@ -537,6 +537,19 @@ namespace Avalonia
v.SetVisualParent(null);
}
break;
case NotifyCollectionChangedAction.Replace:
foreach (Visual v in e.OldItems)
{
v.SetVisualParent(null);
}
foreach (Visual v in e.NewItems)
{
v.SetVisualParent(this);
}
break;
}
}

109
src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs

@ -66,7 +66,7 @@ namespace Avalonia.Markup.Xaml.Data
/// <summary>
/// Gets or sets the binding path.
/// </summary>
public string Path { get; set; }
public string Path { get; set; } = "";
/// <summary>
/// Gets or sets the binding priority.
@ -93,53 +93,53 @@ namespace Avalonia.Markup.Xaml.Data
bool enableDataValidation = false)
{
Contract.Requires<ArgumentNullException>(target != null);
anchor = anchor ?? DefaultAnchor?.Target;
var pathInfo = ParsePath(Path);
ValidateState(pathInfo);
enableDataValidation = enableDataValidation && Priority == BindingPriority.LocalValue;
ExpressionObserver observer;
if (pathInfo.ElementName != null || ElementName != null)
if (ElementName != null)
{
observer = CreateElementObserver(
(target as IControl) ?? (anchor as IControl),
pathInfo.ElementName ?? ElementName,
pathInfo.Path);
ElementName,
Path,
enableDataValidation);
}
else if (Source != null)
{
observer = CreateSourceObserver(Source, pathInfo.Path, enableDataValidation);
observer = CreateSourceObserver(Source, Path, enableDataValidation);
}
else if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext)
{
observer = CreateDataContexObserver(
target,
pathInfo.Path,
Path,
targetProperty == Control.DataContextProperty,
anchor,
enableDataValidation);
}
else if (RelativeSource.Mode == RelativeSourceMode.Self)
{
observer = CreateSourceObserver(target, pathInfo.Path, enableDataValidation);
observer = CreateSourceObserver(target, Path, enableDataValidation);
}
else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent)
{
observer = CreateTemplatedParentObserver(target, pathInfo.Path);
observer = CreateTemplatedParentObserver(target, Path, enableDataValidation);
}
else if (RelativeSource.Mode == RelativeSourceMode.FindAncestor)
{
if (RelativeSource.AncestorType == null)
if (RelativeSource.Tree == TreeType.Visual && RelativeSource.AncestorType == null)
{
throw new InvalidOperationException("AncestorType must be set for RelativeSourceModel.FindAncestor.");
throw new InvalidOperationException("AncestorType must be set for RelativeSourceMode.FindAncestor when searching the visual tree.");
}
observer = CreateFindAncestorObserver(
(target as IControl) ?? (anchor as IControl),
pathInfo.Path);
RelativeSource,
Path,
enableDataValidation);
}
else
{
@ -168,53 +168,6 @@ namespace Avalonia.Markup.Xaml.Data
return new InstancedBinding(subject, Mode, Priority);
}
private static PathInfo ParsePath(string path)
{
var result = new PathInfo();
if (string.IsNullOrWhiteSpace(path) || path == ".")
{
result.Path = string.Empty;
}
else if (path.StartsWith("#"))
{
var dot = path.IndexOf('.');
if (dot != -1)
{
result.Path = path.Substring(dot + 1);
result.ElementName = path.Substring(1, dot - 1);
}
else
{
result.Path = string.Empty;
result.ElementName = path.Substring(1);
}
}
else
{
result.Path = path;
}
return result;
}
private void ValidateState(PathInfo pathInfo)
{
if (pathInfo.ElementName != null && ElementName != null)
{
throw new InvalidOperationException(
"ElementName property cannot be set when an #elementName path is provided.");
}
if ((pathInfo.ElementName != null || ElementName != null) &&
RelativeSource != null)
{
throw new InvalidOperationException(
"ElementName property cannot be set with a RelativeSource.");
}
}
private ExpressionObserver CreateDataContexObserver(
IAvaloniaObject target,
string path,
@ -256,7 +209,11 @@ namespace Avalonia.Markup.Xaml.Data
}
}
private ExpressionObserver CreateElementObserver(IControl target, string elementName, string path)
private ExpressionObserver CreateElementObserver(
IControl target,
string elementName,
string path,
bool enableDataValidation)
{
Contract.Requires<ArgumentNullException>(target != null);
@ -264,35 +221,39 @@ namespace Avalonia.Markup.Xaml.Data
var result = new ExpressionObserver(
ControlLocator.Track(target, elementName),
path,
false,
enableDataValidation,
description);
return result;
}
private ExpressionObserver CreateFindAncestorObserver(
IControl target,
string path)
RelativeSource relativeSource,
string path,
bool enableDataValidation)
{
Contract.Requires<ArgumentNullException>(target != null);
return new ExpressionObserver(
ControlLocator.Track(target, RelativeSource.AncestorType, RelativeSource.AncestorLevel -1),
path);
ControlLocator.Track(target, relativeSource.Tree, relativeSource.AncestorLevel - 1, relativeSource.AncestorType),
path,
enableDataValidation);
}
private ExpressionObserver CreateSourceObserver(
object source,
string path,
bool enabledDataValidation)
bool enableDataValidation)
{
Contract.Requires<ArgumentNullException>(source != null);
return new ExpressionObserver(source, path, enabledDataValidation);
return new ExpressionObserver(source, path, enableDataValidation);
}
private ExpressionObserver CreateTemplatedParentObserver(
IAvaloniaObject target,
string path)
string path,
bool enableDataValidation)
{
Contract.Requires<ArgumentNullException>(target != null);
@ -303,7 +264,8 @@ namespace Avalonia.Markup.Xaml.Data
var result = new ExpressionObserver(
() => target.GetValue(Control.TemplatedParentProperty),
path,
update);
update,
enableDataValidation);
return result;
}
@ -328,6 +290,7 @@ namespace Avalonia.Markup.Xaml.Data
{
public string Path { get; set; }
public string ElementName { get; set; }
public RelativeSource RelativeSource { get; set; }
}
}
}
}

2
src/Markup/Avalonia.Markup.Xaml/Data/RelativeSource.cs

@ -87,5 +87,7 @@ namespace Avalonia.Markup.Xaml.Data
/// Gets or sets a value that describes the type of relative source lookup.
/// </summary>
public RelativeSourceMode Mode { get; set; }
public TreeType Tree { get; set; } = TreeType.Visual;
}
}

153
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs

@ -29,20 +29,167 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
public override object ProvideValue(IServiceProvider serviceProvider)
{
var descriptorContext = (ITypeDescriptorContext)serviceProvider;
var pathInfo = ParsePath(Path, descriptorContext);
ValidateState(pathInfo);
return new Binding
{
Converter = Converter,
ConverterParameter = ConverterParameter,
ElementName = ElementName,
ElementName = pathInfo.ElementName ?? ElementName,
FallbackValue = FallbackValue,
Mode = Mode,
Path = Path,
Path = pathInfo.Path,
Priority = Priority,
RelativeSource = RelativeSource,
RelativeSource = pathInfo.RelativeSource ?? RelativeSource,
DefaultAnchor = new WeakReference(GetDefaultAnchor((ITypeDescriptorContext)serviceProvider))
};
}
private class PathInfo
{
public string Path { get; set; }
public string ElementName { get; set; }
public RelativeSource RelativeSource { get; set; }
}
private void ValidateState(PathInfo pathInfo)
{
if (pathInfo.ElementName != null && ElementName != null)
{
throw new InvalidOperationException(
"ElementName property cannot be set when an #elementName path is provided.");
}
if (pathInfo.RelativeSource != null && RelativeSource != null)
{
throw new InvalidOperationException(
"ElementName property cannot be set when a $self or $parent path is provided.");
}
if ((pathInfo.ElementName != null || ElementName != null) &&
(pathInfo.RelativeSource != null || RelativeSource != null))
{
throw new InvalidOperationException(
"ElementName property cannot be set with a RelativeSource.");
}
}
private static PathInfo ParsePath(string path, ITypeDescriptorContext context)
{
var result = new PathInfo();
if (string.IsNullOrWhiteSpace(path) || path == ".")
{
result.Path = string.Empty;
}
else if (path.StartsWith("#"))
{
var dot = path.IndexOf('.');
if (dot != -1)
{
result.Path = path.Substring(dot + 1);
result.ElementName = path.Substring(1, dot - 1);
}
else
{
result.Path = string.Empty;
result.ElementName = path.Substring(1);
}
}
else if (path.StartsWith("$"))
{
var relativeSource = new RelativeSource
{
Tree = TreeType.Logical
};
result.RelativeSource = relativeSource;
var dot = path.IndexOf('.');
string relativeSourceMode;
if (dot != -1)
{
result.Path = path.Substring(dot + 1);
relativeSourceMode = path.Substring(1, dot - 1);
}
else
{
result.Path = string.Empty;
relativeSourceMode = path.Substring(1);
}
if (relativeSourceMode == "self")
{
relativeSource.Mode = RelativeSourceMode.Self;
}
else if (relativeSourceMode == "parent")
{
relativeSource.Mode = RelativeSourceMode.FindAncestor;
relativeSource.AncestorLevel = 1;
}
else if (relativeSourceMode.StartsWith("parent["))
{
relativeSource.Mode = RelativeSourceMode.FindAncestor;
var parentConfigStart = relativeSourceMode.IndexOf('[');
if (!relativeSourceMode.EndsWith("]"))
{
throw new InvalidOperationException("Invalid RelativeSource binding syntax. Expected matching ']' for '['.");
}
var parentConfigParams = relativeSourceMode.Substring(parentConfigStart + 1).TrimEnd(']').Split(';');
if (parentConfigParams.Length > 2 || parentConfigParams.Length == 0)
{
throw new InvalidOperationException("Expected either 1 or 2 parameters for RelativeSource binding syntax");
}
else if (parentConfigParams.Length == 1)
{
if (int.TryParse(parentConfigParams[0], out int level))
{
relativeSource.AncestorType = null;
relativeSource.AncestorLevel = level + 1;
}
else
{
relativeSource.AncestorType = LookupAncestorType(parentConfigParams[0], context);
}
}
else
{
relativeSource.AncestorType = LookupAncestorType(parentConfigParams[0], context);
relativeSource.AncestorLevel = int.Parse(parentConfigParams[1]) + 1;
}
}
else
{
throw new InvalidOperationException($"Invalid RelativeSource binding syntax: {relativeSourceMode}");
}
}
else
{
result.Path = path;
}
return result;
}
private static Type LookupAncestorType(string ancestorTypeName, ITypeDescriptorContext context)
{
var parts = ancestorTypeName.Split(':');
if (parts.Length == 0 || parts.Length > 2)
{
throw new InvalidOperationException("Invalid type name");
}
if (parts.Length == 1)
{
return context.ResolveType(string.Empty, parts[0]);
}
else
{
return context.ResolveType(parts[0], parts[1]);
}
}
private static object GetDefaultAnchor(ITypeDescriptorContext context)
{

2
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs

@ -29,7 +29,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
ElementName = ElementName,
Mode = Mode,
RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
Path = Path,
Path = Path ?? string.Empty,
Priority = Priority,
};
}

91
src/Markup/Avalonia.Markup/ControlLocator.cs

@ -11,6 +11,21 @@ using Avalonia.VisualTree;
namespace Avalonia.Markup
{
/// <summary>
/// The type of tree via which to track a control.
/// </summary>
public enum TreeType
{
/// <summary>
/// The visual tree.
/// </summary>
Visual,
/// <summary>
/// The logical tree.
/// </summary>
Logical,
}
/// <summary>
/// Locates controls relative to other controls.
/// </summary>
@ -27,13 +42,13 @@ namespace Avalonia.Markup
{
var attached = Observable.FromEventPattern<LogicalTreeAttachmentEventArgs>(
x => relativeTo.AttachedToLogicalTree += x,
x => relativeTo.DetachedFromLogicalTree += x)
x => relativeTo.AttachedToLogicalTree -= x)
.Select(x => ((IControl)x.Sender).FindNameScope())
.StartWith(relativeTo.FindNameScope());
var detached = Observable.FromEventPattern<LogicalTreeAttachmentEventArgs>(
x => relativeTo.DetachedFromLogicalTree += x,
x => relativeTo.DetachedFromLogicalTree += x)
x => relativeTo.DetachedFromLogicalTree -= x)
.Select(x => (INameScope)null);
return attached.Merge(detached).Select(nameScope =>
@ -68,37 +83,75 @@ namespace Avalonia.Markup
/// <param name="relativeTo">
/// The control relative from which the other control should be found.
/// </param>
/// <param name="ancestorType">The type of the ancestor to find.</param>
/// <param name="tree">The tree via which to track the control.</param>
/// <param name="ancestorLevel">
/// The level of ancestor control to look for. Use 0 for the first ancestor of the
/// requested type.
/// </param>
public static IObservable<IControl> Track(IControl relativeTo, Type ancestorType, int ancestorLevel)
/// <param name="ancestorType">The type of the ancestor to find.</param>
public static IObservable<IControl> Track(IControl relativeTo, TreeType tree, int ancestorLevel, Type ancestorType = null)
{
return TrackAttachmentToTree(relativeTo, tree).Select(isAttachedToTree =>
{
if (isAttachedToTree)
{
if (tree == TreeType.Visual)
{
return relativeTo.GetVisualAncestors()
.Where(x => ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true)
.ElementAtOrDefault(ancestorLevel) as IControl;
}
else
{
return relativeTo.GetLogicalAncestors()
.Where(x => ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true)
.ElementAtOrDefault(ancestorLevel) as IControl;
}
}
else
{
return null;
}
});
}
private static IObservable<bool> TrackAttachmentToTree(IControl relativeTo, TreeType tree)
{
return tree == TreeType.Visual ? TrackAttachmentToVisualTree(relativeTo) : TrackAttachmentToLogicalTree(relativeTo);
}
private static IObservable<bool> TrackAttachmentToVisualTree(IControl relativeTo)
{
var attached = Observable.FromEventPattern<VisualTreeAttachmentEventArgs>(
x => relativeTo.AttachedToVisualTree += x,
x => relativeTo.DetachedFromVisualTree += x)
x => relativeTo.AttachedToVisualTree -= x)
.Select(x => true)
.StartWith(relativeTo.IsAttachedToVisualTree);
var detached = Observable.FromEventPattern<VisualTreeAttachmentEventArgs>(
x => relativeTo.DetachedFromVisualTree += x,
x => relativeTo.DetachedFromVisualTree += x)
x => relativeTo.DetachedFromVisualTree -= x)
.Select(x => false);
return attached.Merge(detached).Select(isAttachedToVisualTree =>
{
if (isAttachedToVisualTree)
{
return relativeTo.GetVisualAncestors()
.Where(x => ancestorType.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()))
.ElementAtOrDefault(ancestorLevel) as IControl;
}
else
{
return null;
}
});
var attachmentStatus = attached.Merge(detached);
return attachmentStatus;
}
private static IObservable<bool> TrackAttachmentToLogicalTree(IControl relativeTo)
{
var attached = Observable.FromEventPattern<LogicalTreeAttachmentEventArgs>(
x => relativeTo.AttachedToLogicalTree += x,
x => relativeTo.AttachedToLogicalTree -= x)
.Select(x => true)
.StartWith(relativeTo.IsAttachedToLogicalTree);
var detached = Observable.FromEventPattern<LogicalTreeAttachmentEventArgs>(
x => relativeTo.DetachedFromLogicalTree += x,
x => relativeTo.DetachedFromLogicalTree -= x)
.Select(x => false);
var attachmentStatus = attached.Merge(detached);
return attachmentStatus;
}
}
}

10
src/Markup/Avalonia.Markup/Data/Parsers/ArgumentListParser.cs

@ -51,15 +51,7 @@ namespace Avalonia.Markup.Data.Parsers
}
}
if (!r.End)
{
r.Take();
return result;
}
else
{
throw new ExpressionParseException(r.Position, "Expected ']'.");
}
throw new ExpressionParseException(r.Position, "Expected ']'.");
}
return null;

1
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@ -54,7 +54,6 @@ namespace Avalonia.Direct2D1.Media
_finishedCallback = finishedCallback;
_directWriteFactory = directWriteFactory;
_imagingFactory = imagingFactory;
_swapChain = swapChain;
_renderTarget.BeginDraw();
}

139
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs

@ -2,22 +2,22 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Microsoft.Reactive.Testing;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Data;
using Avalonia.Logging;
using Avalonia.UnitTests;
using Xunit;
using System.Threading.Tasks;
using Avalonia.Markup.Xaml.Data;
using Avalonia.Platform;
using System.Threading;
using Moq;
using System.Reactive.Disposables;
using System.Reactive.Concurrency;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Avalonia.Diagnostics;
using Microsoft.Reactive.Testing;
using Moq;
using Xunit;
namespace Avalonia.Base.UnitTests
{
@ -363,7 +363,7 @@ namespace Avalonia.Base.UnitTests
Assert.True(called);
}
}
[Fact]
public async Task Bind_With_Scheduler_Executes_On_Scheduler()
{
@ -387,6 +387,77 @@ namespace Avalonia.Base.UnitTests
}
}
[Fact]
public void SetValue_Should_Not_Cause_StackOverflow_And_Have_Correct_Values()
{
var viewModel = new TestStackOverflowViewModel()
{
Value = 50
};
var target = new Class1();
target.Bind(Class1.DoubleValueProperty,
new Binding("Value") { Mode = BindingMode.TwoWay, Source = viewModel });
var child = new Class1();
child[!!Class1.DoubleValueProperty] = target[!!Class1.DoubleValueProperty];
Assert.Equal(1, viewModel.SetterInvokedCount);
// Issues #855 and #824 were causing a StackOverflowException at this point.
target.DoubleValue = 51.001;
Assert.Equal(2, viewModel.SetterInvokedCount);
double expected = 51;
Assert.Equal(expected, viewModel.Value);
Assert.Equal(expected, target.DoubleValue);
Assert.Equal(expected, child.DoubleValue);
}
[Fact]
public void IsAnimating_On_Property_With_No_Value_Returns_False()
{
var target = new Class1();
Assert.False(target.IsAnimating(Class1.FooProperty));
}
[Fact]
public void IsAnimating_On_Property_With_Animation_Value_Returns_False()
{
var target = new Class1();
target.SetValue(Class1.FooProperty, "foo", BindingPriority.Animation);
Assert.False(target.IsAnimating(Class1.FooProperty));
}
[Fact]
public void IsAnimating_On_Property_With_Non_Animation_Binding_Returns_False()
{
var target = new Class1();
var source = new Subject<string>();
target.Bind(Class1.FooProperty, source, BindingPriority.LocalValue);
Assert.False(target.IsAnimating(Class1.FooProperty));
}
[Fact]
public void IsAnimating_On_Property_With_Animation_Binding_Returns_True()
{
var target = new Class1();
var source = new BehaviorSubject<string>("foo");
target.Bind(Class1.FooProperty, source, BindingPriority.Animation);
Assert.True(target.IsAnimating(Class1.FooProperty));
}
/// <summary>
/// Returns an observable that returns a single value but does not complete.
/// </summary>
@ -405,6 +476,15 @@ namespace Avalonia.Base.UnitTests
public static readonly StyledProperty<double> QuxProperty =
AvaloniaProperty.Register<Class1, double>("Qux", 5.6);
public static readonly StyledProperty<double> DoubleValueProperty =
AvaloniaProperty.Register<Class1, double>(nameof(DoubleValue));
public double DoubleValue
{
get { return GetValue(DoubleValueProperty); }
set { SetValue(DoubleValueProperty, value); }
}
}
private class Class2 : Class1
@ -431,5 +511,40 @@ namespace Avalonia.Base.UnitTests
return InstancedBinding.OneTime(_source);
}
}
private class TestStackOverflowViewModel : INotifyPropertyChanged
{
public int SetterInvokedCount { get; private set; }
public const int MaxInvokedCount = 1000;
private double _value;
public event PropertyChangedEventHandler PropertyChanged;
public double Value
{
get { return _value; }
set
{
if (_value != value)
{
SetterInvokedCount++;
if (SetterInvokedCount < MaxInvokedCount)
{
_value = (int)value;
if (_value > 75) _value = 75;
if (_value < 25) _value = 25;
}
else
{
_value = value;
}
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
}
}
}
}
}
}
}

103
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs

@ -3,10 +3,11 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reactive.Subjects;
using Avalonia;
using Avalonia.Data;
using Avalonia.Logging;
using Avalonia.Markup.Xaml.Data;
using Avalonia.UnitTests;
using Xunit;
@ -208,7 +209,7 @@ namespace Avalonia.Base.UnitTests
{
var target = new Class1();
Assert.Throws<ArgumentException>(() =>
Assert.Throws<ArgumentException>(() =>
target.SetValue(Class1.BarProperty, "newvalue"));
}
@ -217,7 +218,7 @@ namespace Avalonia.Base.UnitTests
{
var target = new Class1();
Assert.Throws<ArgumentException>(() =>
Assert.Throws<ArgumentException>(() =>
target.SetValue((AvaloniaProperty)Class1.BarProperty, "newvalue"));
}
@ -227,7 +228,7 @@ namespace Avalonia.Base.UnitTests
var target = new Class1();
var source = new Subject<string>();
Assert.Throws<ArgumentException>(() =>
Assert.Throws<ArgumentException>(() =>
target.Bind(Class1.BarProperty, source));
}
@ -439,12 +440,46 @@ namespace Avalonia.Base.UnitTests
Assert.Equal(BindingMode.OneWayToSource, bar.GetMetadata<Class2>().DefaultBindingMode);
}
[Fact]
public void SetValue_Should_Not_Cause_StackOverflow_And_Have_Correct_Values()
{
var viewModel = new TestStackOverflowViewModel()
{
Value = 50
};
var target = new Class1();
target.Bind(Class1.DoubleValueProperty, new Binding("Value")
{
Mode = BindingMode.TwoWay,
Source = viewModel
});
var child = new Class1();
child[!!Class1.DoubleValueProperty] = target[!!Class1.DoubleValueProperty];
Assert.Equal(1, viewModel.SetterInvokedCount);
// Issues #855 and #824 were causing a StackOverflowException at this point.
target.DoubleValue = 51.001;
Assert.Equal(2, viewModel.SetterInvokedCount);
double expected = 51;
Assert.Equal(expected, viewModel.Value);
Assert.Equal(expected, target.DoubleValue);
Assert.Equal(expected, child.DoubleValue);
}
private class Class1 : AvaloniaObject
{
public static readonly DirectProperty<Class1, string> FooProperty =
AvaloniaProperty.RegisterDirect<Class1, string>(
"Foo",
o => o.Foo,
"Foo",
o => o.Foo,
(o, v) => o.Foo = v,
unsetValue: "unset");
@ -453,14 +488,21 @@ namespace Avalonia.Base.UnitTests
public static readonly DirectProperty<Class1, int> BazProperty =
AvaloniaProperty.RegisterDirect<Class1, int>(
"Bar",
o => o.Baz,
(o,v) => o.Baz = v,
"Bar",
o => o.Baz,
(o, v) => o.Baz = v,
unsetValue: -1);
public static readonly DirectProperty<Class1, double> DoubleValueProperty =
AvaloniaProperty.RegisterDirect<Class1, double>(
nameof(DoubleValue),
o => o.DoubleValue,
(o, v) => o.DoubleValue = v);
private string _foo = "initial";
private readonly string _bar = "bar";
private int _baz = 5;
private double _doubleValue;
public string Foo
{
@ -478,6 +520,12 @@ namespace Avalonia.Base.UnitTests
get { return _baz; }
set { SetAndRaise(BazProperty, ref _baz, value); }
}
public double DoubleValue
{
get { return _doubleValue; }
set { SetAndRaise(DoubleValueProperty, ref _doubleValue, value); }
}
}
private class Class2 : AvaloniaObject
@ -497,5 +545,40 @@ namespace Avalonia.Base.UnitTests
set { SetAndRaise(FooProperty, ref _foo, value); }
}
}
private class TestStackOverflowViewModel : INotifyPropertyChanged
{
public int SetterInvokedCount { get; private set; }
public const int MaxInvokedCount = 1000;
private double _value;
public event PropertyChangedEventHandler PropertyChanged;
public double Value
{
get { return _value; }
set
{
if (_value != value)
{
SetterInvokedCount++;
if (SetterInvokedCount < MaxInvokedCount)
{
_value = (int)value;
if (_value > 75) _value = 75;
if (_value < 25) _value = 25;
}
else
{
_value = value;
}
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
}
}
}
}
}
}
}

22
tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs

@ -83,6 +83,28 @@ namespace Avalonia.Base.UnitTests.Collections
Assert.Equal(new[] { 6, 7, 8, 9, 10, 1, 2, 3, 4, 5 }, target);
}
[Fact]
public void MoveRange_Raises_Correct_CollectionChanged_Event()
{
var target = new AvaloniaList<int>(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
var raised = false;
target.CollectionChanged += (s, e) =>
{
Assert.Equal(NotifyCollectionChangedAction.Move, e.Action);
Assert.Equal(0, e.OldStartingIndex);
Assert.Equal(10, e.NewStartingIndex);
Assert.Equal(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, e.OldItems);
Assert.Equal(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, e.NewItems);
raised = true;
};
target.MoveRange(0, 9, 10);
Assert.True(raised);
Assert.Equal(new[] { 10, 1, 2, 3, 4, 5, 6, 7, 8, 9 }, target);
}
[Fact]
public void Adding_Item_Should_Raise_CollectionChanged()
{

12
tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Subjects;
using Avalonia.Data;
using Xunit;
@ -70,6 +71,17 @@ namespace Avalonia.Base.UnitTests
Assert.Same(p1.Initialized, p2.Initialized);
}
[Fact]
public void IsAnimating_On_DirectProperty_With_Binding_Returns_False()
{
var target = new Class1();
var source = new BehaviorSubject<string>("foo");
target.Bind(Class1.FooProperty, source, BindingPriority.Animation);
Assert.False(target.IsAnimating(Class1.FooProperty));
}
private class Class1 : AvaloniaObject
{
public static readonly DirectProperty<Class1, string> FooProperty =

1
tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj

@ -49,6 +49,7 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Base\Properties.cs" />
<Compile Include="Layout\Measure.cs" />
<Compile Include="Styling\ApplyStyling.cs" />
<Compile Include="Program.cs" />

43
tests/Avalonia.Benchmarks/Base/Properties.cs

@ -0,0 +1,43 @@
using System;
using System.Reactive.Subjects;
using BenchmarkDotNet.Attributes;
namespace Avalonia.Benchmarks.Base
{
[MemoryDiagnoser]
public class AvaloniaObjectBenchmark
{
private Class1 target = new Class1();
private Subject<int> intBinding = new Subject<int>();
public AvaloniaObjectBenchmark()
{
target.SetValue(Class1.IntProperty, 123);
}
[Benchmark]
public void ClearAndSetIntProperty()
{
target.ClearValue(Class1.IntProperty);
target.SetValue(Class1.IntProperty, 123);
}
[Benchmark]
public void BindIntProperty()
{
using (target.Bind(Class1.IntProperty, intBinding))
{
for (var i = 0; i < 100; ++i)
{
intBinding.OnNext(i);
}
}
}
class Class1 : AvaloniaObject
{
public static readonly AvaloniaProperty<int> IntProperty =
AvaloniaProperty.Register<Class1, int>("Int");
}
}
}

4
tests/Avalonia.Controls.UnitTests/DockPanelTests.cs

@ -12,7 +12,7 @@ namespace Avalonia.Controls.UnitTests
{
var target = new DockPanel
{
Children = new Controls
Children =
{
new Border { Width = 500, Height = 50, [DockPanel.DockProperty] = Dock.Top },
new Border { Width = 500, Height = 50, [DockPanel.DockProperty] = Dock.Bottom },
@ -38,7 +38,7 @@ namespace Avalonia.Controls.UnitTests
{
var target = new DockPanel
{
Children = new Controls
Children =
{
new Border { Width = 50, Height = 400, [DockPanel.DockProperty] = Dock.Left },
new Border { Width = 50, Height = 400, [DockPanel.DockProperty] = Dock.Right },

2
tests/Avalonia.Controls.UnitTests/DropDownTests.cs

@ -89,7 +89,7 @@ namespace Avalonia.Controls.UnitTests
return new Panel
{
Name = "container",
Children = new Controls
Children =
{
new ContentControl
{

84
tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs

@ -22,14 +22,14 @@ namespace Avalonia.Controls.UnitTests
{
var grid = new Grid()
{
RowDefinitions = new RowDefinitions("*,Auto,*"),
ColumnDefinitions = new ColumnDefinitions("*,*"),
Children = new Controls()
{
new Border { [Grid.RowProperty] = 0 },
new GridSplitter { [Grid.RowProperty] = 1, Name = "splitter" },
new Border { [Grid.RowProperty] = 2 }
}
RowDefinitions = new RowDefinitions("*,Auto,*"),
ColumnDefinitions = new ColumnDefinitions("*,*"),
Children =
{
new Border { [Grid.RowProperty] = 0 },
new GridSplitter { [Grid.RowProperty] = 1, Name = "splitter" },
new Border { [Grid.RowProperty] = 2 }
}
};
var root = new TestRoot { Child = grid };
@ -43,14 +43,14 @@ namespace Avalonia.Controls.UnitTests
{
var grid = new Grid()
{
ColumnDefinitions = new ColumnDefinitions("*,Auto,*"),
RowDefinitions = new RowDefinitions("*,*"),
Children = new Controls()
{
new Border { [Grid.ColumnProperty] = 0 },
new GridSplitter { [Grid.ColumnProperty] = 1, Name = "splitter" },
new Border { [Grid.ColumnProperty] = 2 },
}
ColumnDefinitions = new ColumnDefinitions("*,Auto,*"),
RowDefinitions = new RowDefinitions("*,*"),
Children =
{
new Border { [Grid.ColumnProperty] = 0 },
new GridSplitter { [Grid.ColumnProperty] = 1, Name = "splitter" },
new Border { [Grid.ColumnProperty] = 2 },
}
};
var root = new TestRoot { Child = grid };
@ -64,14 +64,14 @@ namespace Avalonia.Controls.UnitTests
{
var grid = new Grid()
{
ColumnDefinitions = new ColumnDefinitions("Auto,Auto,Auto"),
RowDefinitions = new RowDefinitions("Auto,Auto"),
Children = new Controls()
{
new Border { [Grid.ColumnProperty] = 0 },
new GridSplitter { [Grid.ColumnProperty] = 1, Name = "splitter" },
new Border { [Grid.ColumnProperty] = 2 },
}
ColumnDefinitions = new ColumnDefinitions("Auto,Auto,Auto"),
RowDefinitions = new RowDefinitions("Auto,Auto"),
Children =
{
new Border { [Grid.ColumnProperty] = 0 },
new GridSplitter { [Grid.ColumnProperty] = 1, Name = "splitter" },
new Border { [Grid.ColumnProperty] = 2 },
}
};
var root = new TestRoot { Child = grid };
@ -99,11 +99,11 @@ namespace Avalonia.Controls.UnitTests
var grid = new Grid()
{
RowDefinitions = rowDefinitions,
Children = new Controls()
{
control1, splitter, control2
}
RowDefinitions = rowDefinitions,
Children =
{
control1, splitter, control2
}
};
var root = new TestRoot { Child = grid };
@ -131,14 +131,14 @@ namespace Avalonia.Controls.UnitTests
{
var grid = new Grid()
{
ColumnDefinitions = new ColumnDefinitions("Auto,*,*"),
RowDefinitions = new RowDefinitions("*,*"),
Children = new Controls()
{
new GridSplitter { [Grid.ColumnProperty] = 0, Name = "splitter" },
new Border { [Grid.ColumnProperty] = 1 },
new Border { [Grid.ColumnProperty] = 2 },
}
ColumnDefinitions = new ColumnDefinitions("Auto,*,*"),
RowDefinitions = new RowDefinitions("*,*"),
Children =
{
new GridSplitter { [Grid.ColumnProperty] = 0, Name = "splitter" },
new Border { [Grid.ColumnProperty] = 1 },
new Border { [Grid.ColumnProperty] = 2 },
}
};
var root = new TestRoot { Child = grid };
@ -171,11 +171,11 @@ namespace Avalonia.Controls.UnitTests
var grid = new Grid()
{
ColumnDefinitions = columnDefinitions,
Children = new Controls()
{
control1, splitter, control2
}
ColumnDefinitions = columnDefinitions,
Children =
{
control1, splitter, control2
}
};
var root = new TestRoot { Child = grid };

2
tests/Avalonia.Controls.UnitTests/GridTests.cs

@ -24,7 +24,7 @@ namespace Avalonia.Controls.UnitTests
new RowDefinition(GridLength.Auto),
new RowDefinition(GridLength.Auto),
},
Children = new Controls
Children =
{
new Border
{

71
tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs

@ -1,11 +1,15 @@
// 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.ComponentModel;
using System.Linq;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml.Data;
using Avalonia.Styling;
using Avalonia.VisualTree;
using Xunit;
@ -199,6 +203,71 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(1, target.SelectedIndex);
}
[Fact]
public void SelectedItem_Should_Not_Cause_StackOverflow()
{
var viewModel = new TestStackOverflowViewModel()
{
Items = new List<string> { "foo", "bar", "baz" }
};
var target = new ListBox
{
Template = new FuncControlTemplate(CreateListBoxTemplate),
DataContext = viewModel,
Items = viewModel.Items
};
target.Bind(ListBox.SelectedItemProperty,
new Binding("SelectedItem") { Mode = BindingMode.TwoWay });
Assert.Equal(0, viewModel.SetterInvokedCount);
// In Issue #855, a Stackoverflow occured here.
target.SelectedItem = viewModel.Items[2];
Assert.Equal(viewModel.Items[1], target.SelectedItem);
Assert.Equal(1, viewModel.SetterInvokedCount);
}
private class TestStackOverflowViewModel : INotifyPropertyChanged
{
public List<string> Items { get; set; }
public int SetterInvokedCount { get; private set; }
public const int MaxInvokedCount = 1000;
private string _selectedItem;
public event PropertyChangedEventHandler PropertyChanged;
public string SelectedItem
{
get { return _selectedItem; }
set
{
if (_selectedItem != value)
{
SetterInvokedCount++;
int index = Items.IndexOf(value);
if (MaxInvokedCount > SetterInvokedCount && index > 0)
{
_selectedItem = Items[index - 1];
}
else
{
_selectedItem = value;
}
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedItem)));
}
}
}
}
private Control CreateListBoxTemplate(ITemplatedControl parent)
{
return new ScrollViewer
@ -237,4 +306,4 @@ namespace Avalonia.Controls.UnitTests
target.Presenter.ApplyTemplate();
}
}
}
}

60
tests/Avalonia.Controls.UnitTests/PanelTests.cs

@ -2,8 +2,8 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Linq;
using Avalonia.Collections;
using Avalonia.LogicalTree;
using Avalonia.VisualTree;
using Xunit;
namespace Avalonia.Controls.UnitTests
@ -18,20 +18,9 @@ namespace Avalonia.Controls.UnitTests
panel.Children.Add(child);
Assert.Equal(child.Parent, panel);
Assert.Equal(child.GetLogicalParent(), panel);
}
[Fact]
public void Setting_Controls_Should_Set_Child_Controls_Parent()
{
var panel = new Panel();
var child = new Control();
panel.Children = new Controls { child };
Assert.Equal(child.Parent, panel);
Assert.Equal(child.GetLogicalParent(), panel);
Assert.Same(child.Parent, panel);
Assert.Same(child.GetLogicalParent(), panel);
Assert.Same(child.GetVisualParent(), panel);
}
[Fact]
@ -45,6 +34,7 @@ namespace Avalonia.Controls.UnitTests
Assert.Null(child.Parent);
Assert.Null(child.GetLogicalParent());
Assert.Null(child.GetVisualParent());
}
[Fact]
@ -60,62 +50,70 @@ namespace Avalonia.Controls.UnitTests
Assert.Null(child1.Parent);
Assert.Null(child1.GetLogicalParent());
Assert.Null(child1.GetVisualParent());
Assert.Null(child2.Parent);
Assert.Null(child2.GetLogicalParent());
Assert.Null(child2.GetVisualParent());
}
[Fact]
public void Resetting_Panel_Children_Should_Clear_Child_Controls_Parent()
public void Replacing_Panel_Children_Should_Clear_And_Set_Control_Parent()
{
var panel = new Panel();
var child1 = new Control();
var child2 = new Control();
panel.Children.Add(child1);
panel.Children.Add(child2);
panel.Children = new Controls();
panel.Children[0] = child2;
Assert.Null(child1.Parent);
Assert.Null(child1.GetLogicalParent());
Assert.Null(child2.Parent);
Assert.Null(child2.GetLogicalParent());
Assert.Null(child1.GetVisualParent());
Assert.Same(child2.Parent, panel);
Assert.Same(child2.GetLogicalParent(), panel);
Assert.Same(child2.GetVisualParent(), panel);
}
[Fact]
public void Setting_Children_Should_Make_Controls_Appear_In_Panel_Children()
public void Child_Control_Should_Appear_In_Panel_Logical_And_Visual_Children()
{
var panel = new Panel();
var child = new Control();
panel.Children = new Controls { child };
panel.Children.Add(child);
Assert.Equal(new[] { child }, panel.Children);
Assert.Equal(new[] { child }, panel.GetLogicalChildren());
Assert.Equal(new[] { child }, panel.GetVisualChildren());
}
[Fact]
public void Child_Control_Should_Appear_In_Panel_Children()
public void Removing_Child_Control_Should_Remove_From_Panel_Logical_And_Visual_Children()
{
var panel = new Panel();
var child = new Control();
panel.Children.Add(child);
panel.Children.Remove(child);
Assert.Equal(new[] { child }, panel.Children);
Assert.Equal(new[] { child }, panel.GetLogicalChildren());
Assert.Equal(new Control[0], panel.Children);
Assert.Empty(panel.GetLogicalChildren());
Assert.Empty(panel.GetVisualChildren());
}
[Fact]
public void Removing_Child_Control_Should_Remove_From_Panel_Children()
public void Moving_Panel_Children_Should_Reoder_Logical_And_Visual_Children()
{
var panel = new Panel();
var child = new Control();
var child1 = new Control();
var child2 = new Control();
panel.Children.Add(child);
panel.Children.Remove(child);
panel.Children.Add(child1);
panel.Children.Add(child2);
panel.Children.Move(1, 0);
Assert.Equal(new Control[0], panel.Children);
Assert.Equal(new ILogical[0], panel.GetLogicalChildren());
Assert.Equal(new[] { child2, child1 }, panel.GetLogicalChildren());
Assert.Equal(new[] { child2, child1 }, panel.GetVisualChildren());
}
}
}

108
tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs

@ -2,7 +2,12 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.ComponentModel;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Markup.Xaml.Data;
using Avalonia.Styling;
using Xunit;
namespace Avalonia.Controls.UnitTests.Primitives
@ -87,8 +92,111 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Throws<ArgumentException>(() => target.Value = double.NegativeInfinity);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void SetValue_Should_Not_Cause_StackOverflow(bool useXamlBinding)
{
var viewModel = new TestStackOverflowViewModel()
{
Value = 50
};
Track track = null;
var target = new TestRange()
{
Template = new FuncControlTemplate<RangeBase>(c =>
{
track = new Track()
{
Width = 100,
Orientation = Orientation.Horizontal,
[~~Track.MinimumProperty] = c[~~RangeBase.MinimumProperty],
[~~Track.MaximumProperty] = c[~~RangeBase.MaximumProperty],
Name = "PART_Track",
Thumb = new Thumb()
};
if (useXamlBinding)
{
track.Bind(Track.ValueProperty, new Binding("Value")
{
Mode = BindingMode.TwoWay,
Source = c,
Priority = BindingPriority.Style
});
}
else
{
track[~~Track.ValueProperty] = c[~~RangeBase.ValueProperty];
}
return track;
}),
Minimum = 0,
Maximum = 100,
DataContext = viewModel
};
target.Bind(TestRange.ValueProperty, new Binding("Value") { Mode = BindingMode.TwoWay });
target.ApplyTemplate();
track.Measure(new Size(100, 0));
track.Arrange(new Rect(0, 0, 100, 0));
Assert.Equal(1, viewModel.SetterInvokedCount);
// Issues #855 and #824 were causing a StackOverflowException at this point.
target.Value = 51.001;
Assert.Equal(2, viewModel.SetterInvokedCount);
double expected = 51;
Assert.Equal(expected, viewModel.Value);
Assert.Equal(expected, target.Value);
Assert.Equal(expected, track.Value);
}
private class TestRange : RangeBase
{
}
private class TestStackOverflowViewModel : INotifyPropertyChanged
{
public int SetterInvokedCount { get; private set; }
public const int MaxInvokedCount = 1000;
private double _value;
public event PropertyChangedEventHandler PropertyChanged;
public double Value
{
get { return _value; }
set
{
if (_value != value)
{
SetterInvokedCount++;
if (SetterInvokedCount < MaxInvokedCount)
{
_value = (int)value;
if (_value > 75) _value = 75;
if (_value < 25) _value = 25;
}
else
{
_value = value;
}
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
}
}
}
}
}
}

8
tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs

@ -67,7 +67,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
Child = new Panel
{
Children = new Controls
Children =
{
new TextBlock(),
new Border(),
@ -101,7 +101,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
Child = new Panel
{
Children = new Controls
Children =
{
new TextBlock(),
new Border(),
@ -124,7 +124,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
Child = new Panel
{
Children = new Controls
Children =
{
new TextBlock(),
new Border(),
@ -189,7 +189,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
return new StackPanel
{
Children = new Controls
Children =
{
new TextBlock
{

2
tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs

@ -50,7 +50,7 @@ namespace Avalonia.Controls.UnitTests
new RowDefinition(1, GridUnitType.Star),
new RowDefinition(GridLength.Auto),
},
Children = new Controls
Children =
{
new ScrollContentPresenter
{

12
tests/Avalonia.Controls.UnitTests/StackPanelTests.cs

@ -13,7 +13,7 @@ namespace Avalonia.Controls.UnitTests
{
var target = new StackPanel
{
Children = new Controls
Children =
{
new Border { Height = 20, Width = 120 },
new Border { Height = 30 },
@ -36,7 +36,7 @@ namespace Avalonia.Controls.UnitTests
var target = new StackPanel
{
Orientation = Orientation.Horizontal,
Children = new Controls
Children =
{
new Border { Width = 20, Height = 120 },
new Border { Width = 30 },
@ -59,7 +59,7 @@ namespace Avalonia.Controls.UnitTests
var target = new StackPanel
{
Gap = 10,
Children = new Controls
Children =
{
new Border { Height = 20, Width = 120 },
new Border { Height = 30 },
@ -83,7 +83,7 @@ namespace Avalonia.Controls.UnitTests
{
Gap = 10,
Orientation = Orientation.Horizontal,
Children = new Controls
Children =
{
new Border { Width = 20, Height = 120 },
new Border { Width = 30 },
@ -106,7 +106,7 @@ namespace Avalonia.Controls.UnitTests
var target = new StackPanel
{
Height = 60,
Children = new Controls
Children =
{
new Border { Height = 20, Width = 120 },
new Border { Height = 30 },
@ -130,7 +130,7 @@ namespace Avalonia.Controls.UnitTests
{
Width = 60,
Orientation = Orientation.Horizontal,
Children = new Controls
Children =
{
new Border { Width = 20, Height = 120 },
new Border { Width = 30 },

2
tests/Avalonia.Controls.UnitTests/TabControlTests.cs

@ -272,7 +272,7 @@ namespace Avalonia.Controls.UnitTests
{
return new StackPanel
{
Children = new Controls
Children =
{
new TabStrip
{

2
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@ -432,7 +432,7 @@ namespace Avalonia.Controls.UnitTests
{
return new FuncControlTemplate<TreeViewItem>(parent => new Panel
{
Children = new Controls
Children =
{
new ContentPresenter
{

52
tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs

@ -12,12 +12,12 @@ namespace Avalonia.Controls.UnitTests
{
var target = new WrapPanel()
{
Width = 100,
Children = new Controls
{
new Border { Height = 50, Width = 100 },
new Border { Height = 50, Width = 100 },
}
Width = 100,
Children =
{
new Border { Height = 50, Width = 100 },
new Border { Height = 50, Width = 100 },
}
};
target.Measure(Size.Infinity);
@ -33,12 +33,12 @@ namespace Avalonia.Controls.UnitTests
{
var target = new WrapPanel()
{
Width = 200,
Children = new Controls
{
new Border { Height = 50, Width = 100 },
new Border { Height = 50, Width = 100 },
}
Width = 200,
Children =
{
new Border { Height = 50, Width = 100 },
new Border { Height = 50, Width = 100 },
}
};
target.Measure(Size.Infinity);
@ -54,13 +54,13 @@ namespace Avalonia.Controls.UnitTests
{
var target = new WrapPanel()
{
Orientation = Orientation.Vertical,
Height = 120,
Children = new Controls
{
new Border { Height = 50, Width = 100 },
new Border { Height = 50, Width = 100 },
}
Orientation = Orientation.Vertical,
Height = 120,
Children =
{
new Border { Height = 50, Width = 100 },
new Border { Height = 50, Width = 100 },
}
};
target.Measure(Size.Infinity);
@ -76,13 +76,13 @@ namespace Avalonia.Controls.UnitTests
{
var target = new WrapPanel()
{
Orientation = Orientation.Vertical,
Height = 60,
Children = new Controls
{
new Border { Height = 50, Width = 100 },
new Border { Height = 50, Width = 100 },
}
Orientation = Orientation.Vertical,
Height = 60,
Children =
{
new Border { Height = 50, Width = 100 },
new Border { Height = 50, Width = 100 },
}
};
target.Measure(Size.Infinity);

124
tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Arrows.cs

@ -6,8 +6,6 @@ using Xunit;
namespace Avalonia.Input.UnitTests
{
using Controls = Controls.Controls;
public class KeyboardNavigationTests_Arrows
{
[Fact]
@ -18,12 +16,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children = new Controls
Children =
{
new Button { Name = "Button1" },
(current = new Button { Name = "Button2" }),
@ -33,7 +31,7 @@ namespace Avalonia.Input.UnitTests
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children = new Controls
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
@ -56,12 +54,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children = new Controls
Children =
{
new Button { Name = "Button1" },
new Button { Name = "Button2" },
@ -71,7 +69,7 @@ namespace Avalonia.Input.UnitTests
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children = new Controls
Children =
{
(next = new Button { Name = "Button4" }),
new Button { Name = "Button5" },
@ -94,12 +92,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children = new Controls
Children =
{
new Button { Name = "Button1" },
new Button { Name = "Button2" },
@ -123,16 +121,16 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children = new Controls
Children =
{
new Button { Name = "Button1" },
new Button { Name = "Button2" },
@ -144,7 +142,7 @@ namespace Avalonia.Input.UnitTests
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children = new Controls
Children =
{
(next = new Button { Name = "Button4" }),
new Button { Name = "Button5" },
@ -167,7 +165,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children = new Controls
Children =
{
(next = new Button { Name = "Button1" }),
}
@ -187,17 +185,17 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children = new Controls
Children =
{
(next = new Button { Name = "Button1" }),
new Button { Name = "Button2" },
@ -209,7 +207,7 @@ namespace Avalonia.Input.UnitTests
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children = new Controls
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
@ -232,12 +230,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle,
Children = new Controls
Children =
{
new Button { Name = "Button1" },
(current = new Button { Name = "Button2" }),
@ -246,7 +244,7 @@ namespace Avalonia.Input.UnitTests
},
new StackPanel
{
Children = new Controls
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
@ -269,12 +267,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle,
Children = new Controls
Children =
{
(next = new Button { Name = "Button1" }),
new Button { Name = "Button2" },
@ -283,7 +281,7 @@ namespace Avalonia.Input.UnitTests
},
new StackPanel
{
Children = new Controls
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
@ -306,12 +304,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained,
Children = new Controls
Children =
{
new Button { Name = "Button1" },
(current = new Button { Name = "Button2" }),
@ -321,7 +319,7 @@ namespace Avalonia.Input.UnitTests
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained,
Children = new Controls
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
@ -343,12 +341,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained,
Children = new Controls
Children =
{
new Button { Name = "Button1" },
new Button { Name = "Button2" },
@ -358,7 +356,7 @@ namespace Avalonia.Input.UnitTests
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained,
Children = new Controls
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
@ -380,12 +378,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.None,
Children = new Controls
Children =
{
new Button { Name = "Button1" },
(current = new Button { Name = "Button2" }),
@ -395,7 +393,7 @@ namespace Avalonia.Input.UnitTests
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained,
Children = new Controls
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
@ -418,12 +416,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children = new Controls
Children =
{
new Button { Name = "Button1" },
(next = new Button { Name = "Button2" }),
@ -433,7 +431,7 @@ namespace Avalonia.Input.UnitTests
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained,
Children = new Controls
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
@ -456,12 +454,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children = new Controls
Children =
{
new Button { Name = "Button1" },
new Button { Name = "Button2" },
@ -471,7 +469,7 @@ namespace Avalonia.Input.UnitTests
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children = new Controls
Children =
{
(current = new Button { Name = "Button4" }),
new Button { Name = "Button5" },
@ -495,12 +493,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children = new Controls
Children =
{
new Button { Name = "Button1" },
new Button { Name = "Button2" },
@ -524,16 +522,16 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children = new Controls
Children =
{
new Button { Name = "Button1" },
new Button { Name = "Button2" },
@ -545,7 +543,7 @@ namespace Avalonia.Input.UnitTests
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children = new Controls
Children =
{
(current = new Button { Name = "Button4" }),
new Button { Name = "Button5" },
@ -568,16 +566,16 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children = new Controls
Children =
{
(current = new Button { Name = "Button1" }),
new Button { Name = "Button2" },
@ -589,7 +587,7 @@ namespace Avalonia.Input.UnitTests
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children = new Controls
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
@ -632,12 +630,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle,
Children = new Controls
Children =
{
(next = new Button { Name = "Button1" }),
(current = new Button { Name = "Button2" }),
@ -647,7 +645,7 @@ namespace Avalonia.Input.UnitTests
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle,
Children = new Controls
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
@ -670,12 +668,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle,
Children = new Controls
Children =
{
(current = new Button { Name = "Button1" }),
new Button { Name = "Button2" },
@ -685,7 +683,7 @@ namespace Avalonia.Input.UnitTests
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle,
Children = new Controls
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
@ -708,12 +706,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained,
Children = new Controls
Children =
{
(next = new Button { Name = "Button1" }),
(current = new Button { Name = "Button2" }),
@ -723,7 +721,7 @@ namespace Avalonia.Input.UnitTests
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained,
Children = new Controls
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
@ -745,12 +743,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained,
Children = new Controls
Children =
{
(current = new Button { Name = "Button1" }),
new Button { Name = "Button2" },
@ -760,7 +758,7 @@ namespace Avalonia.Input.UnitTests
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained,
Children = new Controls
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
@ -783,7 +781,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained,
Children = new Controls
Children =
{
(current = new Decorator
{

168
tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs

@ -6,8 +6,6 @@ using Xunit;
namespace Avalonia.Input.UnitTests
{
using Controls = Controls.Controls;
public class KeyboardNavigationTests_Tab
{
[Fact]
@ -18,11 +16,11 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
Children = new Controls
Children =
{
new Button { Name = "Button1" },
(current = new Button { Name = "Button2" }),
@ -31,7 +29,7 @@ namespace Avalonia.Input.UnitTests
},
new StackPanel
{
Children = new Controls
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
@ -54,11 +52,11 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
Children = new Controls
Children =
{
new Button { Name = "Button1" },
new Button { Name = "Button2" },
@ -67,7 +65,7 @@ namespace Avalonia.Input.UnitTests
},
new StackPanel
{
Children = new Controls
Children =
{
(next = new Button { Name = "Button4" }),
new Button { Name = "Button5" },
@ -90,11 +88,11 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
Children = new Controls
Children =
{
(next = new Button { Name = "Button1" }),
new Button { Name = "Button2" },
@ -104,11 +102,11 @@ namespace Avalonia.Input.UnitTests
new StackPanel
{
[KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.None,
Children = new Controls
Children =
{
new StackPanel
{
Children = new Controls
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
@ -133,11 +131,11 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
Children = new Controls
Children =
{
new Button { Name = "Button1" },
new Button { Name = "Button2" },
@ -161,15 +159,15 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
Children = new Controls
Children =
{
new Button { Name = "Button1" },
new Button { Name = "Button2" },
@ -180,7 +178,7 @@ namespace Avalonia.Input.UnitTests
},
new StackPanel
{
Children = new Controls
Children =
{
(next = new Button { Name = "Button4" }),
new Button { Name = "Button5" },
@ -202,7 +200,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
(next = new Button { Name = "Button1" }),
}
@ -221,15 +219,15 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
Children = new Controls
Children =
{
(next = new Button { Name = "Button1" }),
new Button { Name = "Button2" },
@ -240,7 +238,7 @@ namespace Avalonia.Input.UnitTests
},
new StackPanel
{
Children = new Controls
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
@ -263,12 +261,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children = new Controls
Children =
{
new Button { Name = "Button1" },
(current = new Button { Name = "Button2" }),
@ -277,7 +275,7 @@ namespace Avalonia.Input.UnitTests
},
new StackPanel
{
Children = new Controls
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
@ -300,12 +298,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children = new Controls
Children =
{
(next = new Button { Name = "Button1" }),
new Button { Name = "Button2" },
@ -314,7 +312,7 @@ namespace Avalonia.Input.UnitTests
},
new StackPanel
{
Children = new Controls
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
@ -337,12 +335,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Contained,
Children = new Controls
Children =
{
new Button { Name = "Button1" },
(current = new Button { Name = "Button2" }),
@ -351,7 +349,7 @@ namespace Avalonia.Input.UnitTests
},
new StackPanel
{
Children = new Controls
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
@ -373,12 +371,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Contained,
Children = new Controls
Children =
{
new Button { Name = "Button1" },
new Button { Name = "Button2" },
@ -387,7 +385,7 @@ namespace Avalonia.Input.UnitTests
},
new StackPanel
{
Children = new Controls
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
@ -410,12 +408,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Once,
Children = new Controls
Children =
{
new Button { Name = "Button1" },
(current = new Button { Name = "Button2" }),
@ -424,7 +422,7 @@ namespace Avalonia.Input.UnitTests
},
new StackPanel
{
Children = new Controls
Children =
{
(next = new Button { Name = "Button4" }),
new Button { Name = "Button5" },
@ -448,12 +446,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
(container = new StackPanel
{
[KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Once,
Children = new Controls
Children =
{
new Button { Name = "Button1" },
(next = new Button { Name = "Button2" }),
@ -462,7 +460,7 @@ namespace Avalonia.Input.UnitTests
}),
new StackPanel
{
Children = new Controls
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
@ -487,12 +485,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.None,
Children = new Controls
Children =
{
new Button { Name = "Button1" },
(current = new Button { Name = "Button2" }),
@ -501,7 +499,7 @@ namespace Avalonia.Input.UnitTests
},
new StackPanel
{
Children = new Controls
Children =
{
(next = new Button { Name = "Button4" }),
new Button { Name = "Button5" },
@ -525,12 +523,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
(container = new StackPanel
{
[KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.None,
Children = new Controls
Children =
{
new Button { Name = "Button1" },
new Button { Name = "Button2" },
@ -539,7 +537,7 @@ namespace Avalonia.Input.UnitTests
}),
new StackPanel
{
Children = new Controls
Children =
{
(next = new Button { Name = "Button4" }),
new Button { Name = "Button5" },
@ -564,11 +562,11 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
Children = new Controls
Children =
{
new Button { Name = "Button1" },
(next = new Button { Name = "Button2" }),
@ -577,7 +575,7 @@ namespace Avalonia.Input.UnitTests
},
new StackPanel
{
Children = new Controls
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
@ -600,11 +598,11 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
Children = new Controls
Children =
{
new Button { Name = "Button1" },
new Button { Name = "Button2" },
@ -613,7 +611,7 @@ namespace Avalonia.Input.UnitTests
},
new StackPanel
{
Children = new Controls
Children =
{
(current = new Button { Name = "Button4" }),
new Button { Name = "Button5" },
@ -636,11 +634,11 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
Children = new Controls
Children =
{
new Button { Name = "Button1" },
new Button { Name = "Button2" },
@ -664,15 +662,15 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
Children = new Controls
Children =
{
new Button { Name = "Button1" },
new Button { Name = "Button2" },
@ -683,7 +681,7 @@ namespace Avalonia.Input.UnitTests
},
new StackPanel
{
Children = new Controls
Children =
{
(current = new Button { Name = "Button4" }),
new Button { Name = "Button5" },
@ -706,15 +704,15 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
Children = new Controls
Children =
{
(current = new Button { Name = "Button1" }),
new Button { Name = "Button2" },
@ -725,7 +723,7 @@ namespace Avalonia.Input.UnitTests
},
new StackPanel
{
Children = new Controls
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
@ -767,12 +765,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children = new Controls
Children =
{
(next = new Button { Name = "Button1" }),
(current = new Button { Name = "Button2" }),
@ -781,7 +779,7 @@ namespace Avalonia.Input.UnitTests
},
new StackPanel
{
Children = new Controls
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
@ -804,12 +802,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children = new Controls
Children =
{
(current = new Button { Name = "Button1" }),
new Button { Name = "Button2" },
@ -818,7 +816,7 @@ namespace Avalonia.Input.UnitTests
},
new StackPanel
{
Children = new Controls
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
@ -841,12 +839,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Contained,
Children = new Controls
Children =
{
(next = new Button { Name = "Button1" }),
(current = new Button { Name = "Button2" }),
@ -855,7 +853,7 @@ namespace Avalonia.Input.UnitTests
},
new StackPanel
{
Children = new Controls
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
@ -877,12 +875,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Contained,
Children = new Controls
Children =
{
(current = new Button { Name = "Button1" }),
new Button { Name = "Button2" },
@ -891,7 +889,7 @@ namespace Avalonia.Input.UnitTests
},
new StackPanel
{
Children = new Controls
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
@ -914,11 +912,11 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
Children = new Controls
Children =
{
new Button { Name = "Button1" },
new Button { Name = "Button2" },
@ -928,7 +926,7 @@ namespace Avalonia.Input.UnitTests
new StackPanel
{
[KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Once,
Children = new Controls
Children =
{
new Button { Name = "Button4" },
(current = new Button { Name = "Button5" }),
@ -952,12 +950,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
(container = new StackPanel
{
[KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Once,
Children = new Controls
Children =
{
new Button { Name = "Button1" },
(next = new Button { Name = "Button2" }),
@ -966,7 +964,7 @@ namespace Avalonia.Input.UnitTests
}),
new StackPanel
{
Children = new Controls
Children =
{
(current = new Button { Name = "Button4" }),
new Button { Name = "Button5" },
@ -991,12 +989,12 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
Children = new Controls
Children =
{
new StackPanel
{
[KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Once,
Children = new Controls
Children =
{
(next = new Button { Name = "Button1" }),
new Button { Name = "Button2" },
@ -1005,7 +1003,7 @@ namespace Avalonia.Input.UnitTests
},
new StackPanel
{
Children = new Controls
Children =
{
(current = new Button { Name = "Button4" }),
new Button { Name = "Button5" },
@ -1028,7 +1026,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
[KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Contained,
Children = new Controls
Children =
{
(current = new Decorator
{

8
tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs

@ -275,10 +275,10 @@ namespace Avalonia.Layout.UnitTests
{
Child = panel = new StackPanel
{
Children = new Controls.Controls
{
(border = new Border())
}
Children =
{
(border = new Border())
}
}
};

10
tests/Avalonia.Markup.UnitTests/ControlLocatorTests.cs

@ -23,7 +23,7 @@ namespace Avalonia.Markup.UnitTests
{
Child = new StackPanel
{
Children = new Controls.Controls
Children =
{
(target = new TextBlock { Name = "target" }),
(relativeTo = new TextBlock { Name = "start" }),
@ -49,7 +49,7 @@ namespace Avalonia.Markup.UnitTests
{
Child = (panel = new StackPanel
{
Children = new Controls.Controls
Children =
{
(relativeTo = new TextBlock
{
@ -84,7 +84,7 @@ namespace Avalonia.Markup.UnitTests
{
Child = panel = new StackPanel
{
Children = new Controls.Controls
Children =
{
(target = new TextBlock { Name = "target" }),
(relativeTo = new TextBlock { Name = "start" }),
@ -114,7 +114,7 @@ namespace Avalonia.Markup.UnitTests
{
Child = new StackPanel
{
Children = new Controls.Controls
Children =
{
(relativeTo = new TextBlock
{
@ -129,7 +129,7 @@ namespace Avalonia.Markup.UnitTests
{
Child = new StackPanel
{
Children = new Controls.Controls
Children =
{
(target2 = new TextBlock { Name = "target" }),
}

1
tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj

@ -21,6 +21,7 @@
<ProjectReference Include="..\..\src\Avalonia.Visuals\Avalonia.Visuals.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Styling\Avalonia.Styling.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj" />
<ProjectReference Include="..\Avalonia.Base.UnitTests\Avalonia.Base.UnitTests.csproj" />
<ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
</ItemGroup>
<ItemGroup>

162
tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs

@ -13,6 +13,7 @@ using Moq;
using Xunit;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Avalonia.UnitTests;
namespace Avalonia.Markup.Xaml.UnitTests.Data
{
@ -337,6 +338,167 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
Assert.Equal("foo", target.Content);
}
[Fact]
public void StyledProperty_SetValue_Should_Not_Cause_StackOverflow_And_Have_Correct_Values()
{
var viewModel = new TestStackOverflowViewModel()
{
Value = 50
};
var target = new StyledPropertyClass();
target.Bind(StyledPropertyClass.DoubleValueProperty,
new Binding("Value") { Mode = BindingMode.TwoWay, Source = viewModel });
var child = new StyledPropertyClass();
child.Bind(StyledPropertyClass.DoubleValueProperty,
new Binding("DoubleValue")
{
Mode = BindingMode.TwoWay,
Source = target
});
Assert.Equal(1, viewModel.SetterInvokedCount);
//here in real life stack overflow exception is thrown issue #855 and #824
target.DoubleValue = 51.001;
Assert.Equal(2, viewModel.SetterInvokedCount);
double expected = 51;
Assert.Equal(expected, viewModel.Value);
Assert.Equal(expected, target.DoubleValue);
Assert.Equal(expected, child.DoubleValue);
}
[Fact]
public void SetValue_Should_Not_Cause_StackOverflow_And_Have_Correct_Values()
{
var viewModel = new TestStackOverflowViewModel()
{
Value = 50
};
var target = new DirectPropertyClass();
target.Bind(DirectPropertyClass.DoubleValueProperty, new Binding("Value")
{
Mode = BindingMode.TwoWay,
Source = viewModel
});
var child = new DirectPropertyClass();
child.Bind(DirectPropertyClass.DoubleValueProperty,
new Binding("DoubleValue")
{
Mode = BindingMode.TwoWay,
Source = target
});
Assert.Equal(1, viewModel.SetterInvokedCount);
//here in real life stack overflow exception is thrown issue #855 and #824
target.DoubleValue = 51.001;
Assert.Equal(2, viewModel.SetterInvokedCount);
double expected = 51;
Assert.Equal(expected, viewModel.Value);
Assert.Equal(expected, target.DoubleValue);
Assert.Equal(expected, child.DoubleValue);
}
private class StyledPropertyClass : AvaloniaObject
{
public static readonly StyledProperty<double> DoubleValueProperty =
AvaloniaProperty.Register<StyledPropertyClass, double>(nameof(DoubleValue));
public double DoubleValue
{
get { return GetValue(DoubleValueProperty); }
set { SetValue(DoubleValueProperty, value); }
}
}
private class DirectPropertyClass : AvaloniaObject
{
public static readonly DirectProperty<DirectPropertyClass, double> DoubleValueProperty =
AvaloniaProperty.RegisterDirect<DirectPropertyClass, double>(
nameof(DoubleValue),
o => o.DoubleValue,
(o, v) => o.DoubleValue = v);
private double _doubleValue;
public double DoubleValue
{
get { return _doubleValue; }
set { SetAndRaise(DoubleValueProperty, ref _doubleValue, value); }
}
}
private class TestStackOverflowViewModel : INotifyPropertyChanged
{
public int SetterInvokedCount { get; private set; }
public const int MaxInvokedCount = 1000;
private double _value;
public event PropertyChangedEventHandler PropertyChanged;
public double Value
{
get { return _value; }
set
{
if (_value != value)
{
SetterInvokedCount++;
if (SetterInvokedCount < MaxInvokedCount)
{
_value = (int)value;
if (_value > 75) _value = 75;
if (_value < 25) _value = 25;
}
else
{
_value = value;
}
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
}
}
}
}
[Fact]
public void Binding_With_Null_Path_Works()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<TextBlock Name='textBlock' Text='{Binding}'/>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var textBlock = window.FindControl<TextBlock>("textBlock");
window.DataContext = "foo";
window.ApplyTemplate();
Assert.Equal("foo", textBlock.Text);
}
}
private class TwoWayBindingTest : Control
{
public static readonly StyledProperty<string> TwoWayProperty =

8
tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_ElementName.cs

@ -18,7 +18,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
{
Child = new StackPanel
{
Children = new Controls.Controls
Children =
{
new TextBlock
{
@ -54,7 +54,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
{
Child = new StackPanel
{
Children = new Controls.Controls
Children =
{
(source = new TextBlock
{
@ -89,7 +89,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
{
Child = stackPanel = new StackPanel
{
Children = new Controls.Controls
Children =
{
(target = new TextBlock
{
@ -126,7 +126,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
{
Child = stackPanel = new StackPanel
{
Children = new Controls.Controls
Children =
{
(target = new ContentControl
{

32
tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs

@ -11,6 +11,9 @@ using Avalonia.Markup.Xaml.Data;
using Avalonia.Styling;
using Xunit;
using System.Reactive.Disposables;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using System.Linq;
namespace Avalonia.Markup.Xaml.UnitTests.Data
{
@ -56,6 +59,35 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
BindingPriority.TemplatedParent));
}
[Fact]
public void TemplateBinding_With_Null_Path_Works()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Button Name='button'>
<Button.Template>
<ControlTemplate>
<TextBlock Text='{TemplateBinding}'/>
</ControlTemplate>
</Button.Template>
</Button>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var button = window.FindControl<Button>("button");
window.ApplyTemplate();
button.ApplyTemplate();
var textBlock = (TextBlock)button.GetVisualChildren().Single();
Assert.Equal("Avalonia.Controls.Button", textBlock.Text);
}
}
private Mock<IControl> CreateTarget(
ITemplatedControl templatedParent = null,
string text = null)

174
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs

@ -3,6 +3,7 @@
using Avalonia.Controls;
using Avalonia.UnitTests;
using System;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Xaml
@ -77,6 +78,77 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
}
}
[Fact]
public void Binding_To_First_Ancestor_Without_AncestorType_Throws_Exception()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Border Name='border1'>
<ContentControl Name='contentControl'>
<Button Name='button' Content='{Binding Name, RelativeSource={RelativeSource AncestorLevel=1}}'/>
</ContentControl>
</Border>
</Window>";
var loader = new AvaloniaXamlLoader();
Assert.Throws<InvalidOperationException>( () => loader.Load(xaml));
}
}
[Fact]
public void Binding_To_First_Ancestor_With_Shorthand_Works()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Border Name='border1'>
<Border Name='border2'>
<Button Name='button' Content='{Binding $parent.Name}'/>
</Border>
</Border>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var button = window.FindControl<Button>("button");
window.ApplyTemplate();
Assert.Equal("border2", button.Content);
}
}
[Fact]
public void Binding_To_First_Ancestor_With_Shorthand_Uses_LogicalTree()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Border Name='border'>
<ContentControl Name='contentControl'>
<Button Name='button' Content='{Binding $parent.Name}'/>
</ContentControl>
</Border>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var contentControl = window.FindControl<ContentControl>("contentControl");
var button = window.FindControl<Button>("button");
window.ApplyTemplate();
Assert.Equal("contentControl", button.Content);
}
}
[Fact]
public void Binding_To_Second_Ancestor_Works()
{
@ -102,6 +174,108 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
}
}
[Fact]
public void Binding_To_Second_Ancestor_With_Shorthand_Uses_LogicalTree()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<ContentControl Name='contentControl1'>
<ContentControl Name='contentControl2'>
<Button Name='button' Content='{Binding $parent[1].Name}'/>
</ContentControl>
</ContentControl>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var contentControl1 = window.FindControl<ContentControl>("contentControl1");
var contentControl2 = window.FindControl<ContentControl>("contentControl2");
var button = window.FindControl<Button>("button");
window.ApplyTemplate();
Assert.Equal("contentControl1", button.Content);
}
}
[Fact]
public void Binding_To_Ancestor_Of_Type_With_Shorthand_Works()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Border Name='border1'>
<Border Name='border2'>
<Button Name='button' Content='{Binding $parent[Border].Name}'/>
</Border>
</Border>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var button = window.FindControl<Button>("button");
window.ApplyTemplate();
Assert.Equal("border2", button.Content);
}
}
[Fact]
public void Binding_To_Second_Ancestor_With_Shorthand_And_Type_Works()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Border Name='border1'>
<Border Name='border2'>
<Button Name='button' Content='{Binding $parent[Border; 1].Name}'/>
</Border>
</Border>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var button = window.FindControl<Button>("button");
window.ApplyTemplate();
Assert.Equal("border1", button.Content);
}
}
[Fact]
public void Binding_To_Second_Ancestor_With_Shorthand_Works()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Border Name='border1'>
<Border Name='border2'>
<Button Name='button' Content='{Binding $parent[1].Name}'/>
</Border>
</Border>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var button = window.FindControl<Button>("button");
window.ApplyTemplate();
Assert.Equal("border1", button.Content);
}
}
[Fact]
public void Binding_To_Ancestor_With_Namespace_Works()
{

2
tests/Avalonia.RenderTests/GeometryClippingTests.cs

@ -29,7 +29,7 @@ namespace Avalonia.Direct2D1.RenderTests
Clip = StreamGeometry.Parse("F1 M 0,0 H 76 V 76 Z"),
Width = 76,
Height = 76,
Children = new Avalonia.Controls.Controls
Children =
{
new Path
{

2
tests/Avalonia.RenderTests/Media/VisualBrushTests.cs

@ -33,7 +33,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{
return new Panel
{
Children = new Avalonia.Controls.Controls
Children =
{
new Image
{

2
tests/Avalonia.RenderTests/OpacityMaskTests.cs

@ -37,7 +37,7 @@ namespace Avalonia.Direct2D1.RenderTests
},
Width = 76,
Height = 76,
Children = new Avalonia.Controls.Controls
Children =
{
new Path
{

2
tests/Avalonia.RenderTests/SVGPathTests.cs

@ -30,7 +30,7 @@ namespace Avalonia.Direct2D1.RenderTests
Background = Brushes.Yellow,
Width = 76,
Height = 76,
Children = new Avalonia.Controls.Controls
Children =
{
new Path
{

5
tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs

@ -135,6 +135,11 @@ namespace Avalonia.Styling.UnitTests
throw new NotImplementedException();
}
public bool IsAnimating(AvaloniaProperty property)
{
throw new NotImplementedException();
}
public bool IsSet(AvaloniaProperty property)
{
throw new NotImplementedException();

5
tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs

@ -165,6 +165,11 @@ namespace Avalonia.Styling.UnitTests
throw new NotImplementedException();
}
public bool IsAnimating(AvaloniaProperty property)
{
throw new NotImplementedException();
}
public bool IsSet(AvaloniaProperty property)
{
throw new NotImplementedException();

5
tests/Avalonia.Styling.UnitTests/TestControlBase.cs

@ -59,6 +59,11 @@ namespace Avalonia.Styling.UnitTests
throw new NotImplementedException();
}
public bool IsAnimating(AvaloniaProperty property)
{
throw new NotImplementedException();
}
public bool IsSet(AvaloniaProperty property)
{
throw new NotImplementedException();

5
tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs

@ -67,6 +67,11 @@ namespace Avalonia.Styling.UnitTests
throw new NotImplementedException();
}
public bool IsAnimating(AvaloniaProperty property)
{
throw new NotImplementedException();
}
public bool IsSet(AvaloniaProperty property)
{
throw new NotImplementedException();

18
tests/Avalonia.UnitTests/TestRoot.cs

@ -16,8 +16,6 @@ namespace Avalonia.UnitTests
public class TestRoot : Decorator, IFocusScope, ILayoutRoot, IInputRoot, INameScope, IRenderRoot, IStyleRoot
{
private readonly NameScope _nameScope = new NameScope();
private readonly IRenderTarget _renderTarget = Mock.Of<IRenderTarget>(
x => x.CreateDrawingContext(It.IsAny<IVisualBrushRenderer>()) == Mock.Of<IDrawingContextImpl>());
public TestRoot()
{
@ -65,7 +63,21 @@ namespace Avalonia.UnitTests
IStyleHost IStyleHost.StylingParent => StylingParent;
public IRenderTarget CreateRenderTarget() => _renderTarget;
public IRenderTarget CreateRenderTarget()
{
var dc = new Mock<IDrawingContextImpl>();
dc.Setup(x => x.CreateLayer(It.IsAny<Size>())).Returns(() =>
{
var layerDc = new Mock<IDrawingContextImpl>();
var layer = new Mock<IRenderTargetBitmapImpl>();
layer.Setup(x => x.CreateDrawingContext(It.IsAny<IVisualBrushRenderer>())).Returns(layerDc.Object);
return layer.Object;
});
var result = new Mock<IRenderTarget>();
result.Setup(x => x.CreateDrawingContext(It.IsAny<IVisualBrushRenderer>())).Returns(dc.Object);
return result.Object;
}
public void Invalidate(Rect rect)
{

12
tests/Avalonia.Visuals.UnitTests/RenderTests_Culling.cs

@ -21,7 +21,7 @@ namespace Avalonia.Visuals.UnitTests
Width = 100,
Height = 100,
ClipToBounds = true,
Children = new Controls.Controls
Children =
{
(target = new TestControl
{
@ -47,7 +47,7 @@ namespace Avalonia.Visuals.UnitTests
Width = 100,
Height = 100,
ClipToBounds = true,
Children = new Controls.Controls
Children =
{
(target = new TestControl
{
@ -74,7 +74,7 @@ namespace Avalonia.Visuals.UnitTests
Width = 100,
Height = 100,
ClipToBounds = true,
Children = new Controls.Controls
Children =
{
new Canvas
{
@ -82,7 +82,7 @@ namespace Avalonia.Visuals.UnitTests
Height = 100,
[Canvas.LeftProperty] = 50,
[Canvas.TopProperty] = 50,
Children = new Controls.Controls
Children =
{
(target = new TestControl
{
@ -111,7 +111,7 @@ namespace Avalonia.Visuals.UnitTests
Width = 100,
Height = 100,
ClipToBounds = true,
Children = new Controls.Controls
Children =
{
(target = new TestControl
{
@ -138,7 +138,7 @@ namespace Avalonia.Visuals.UnitTests
Width = 100,
Height = 100,
ClipToBounds = true,
Children = new Controls.Controls
Children =
{
new Border
{

273
tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs

@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Subjects;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
@ -19,28 +21,18 @@ namespace Avalonia.Visuals.UnitTests.Rendering
[Fact]
public void First_Frame_Calls_UpdateScene_On_Dispatcher()
{
var loop = new Mock<IRenderLoop>();
var root = new TestRoot();
var dispatcher = new Mock<IDispatcher>();
dispatcher.Setup(x => x.InvokeAsync(It.IsAny<Action>(), DispatcherPriority.Render))
.Callback<Action, DispatcherPriority>((a, p) => a());
var target = new DeferredRenderer(
root,
loop.Object,
sceneBuilder: MockSceneBuilder(root).Object,
dispatcher: dispatcher.Object);
CreateTargetAndRunFrame(root, dispatcher: dispatcher.Object);
target.Start();
RunFrame(loop);
#if !NETCOREAPP1_1 // Delegate.Method is not available in netcoreapp1.1
dispatcher.Verify(x =>
x.InvokeAsync(
It.Is<Action>(a => a.Method.Name == "UpdateScene"),
DispatcherPriority.Render));
#endif
}
[Fact]
@ -49,15 +41,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering
var loop = new Mock<IRenderLoop>();
var root = new TestRoot();
var sceneBuilder = MockSceneBuilder(root);
var dispatcher = new ImmediateDispatcher();
var target = new DeferredRenderer(
root,
loop.Object,
sceneBuilder: sceneBuilder.Object,
dispatcher: dispatcher);
target.Start();
RunFrame(loop);
CreateTargetAndRunFrame(root, sceneBuilder: sceneBuilder.Object);
sceneBuilder.Verify(x => x.UpdateAll(It.IsAny<Scene>()));
}
@ -68,12 +53,10 @@ namespace Avalonia.Visuals.UnitTests.Rendering
var loop = new Mock<IRenderLoop>();
var root = new TestRoot();
var sceneBuilder = MockSceneBuilder(root);
var dispatcher = new ImmediateDispatcher();
var target = new DeferredRenderer(
root,
loop.Object,
sceneBuilder: sceneBuilder.Object,
dispatcher: dispatcher);
sceneBuilder: sceneBuilder.Object);
target.Start();
IgnoreFirstFrame(loop, sceneBuilder);
@ -127,12 +110,93 @@ namespace Avalonia.Visuals.UnitTests.Rendering
}
[Fact]
public void Frame_Should_Create_Layer_For_Root()
public void Should_Push_Opacity_For_Controls_With_Less_Than_1_Opacity()
{
var root = new TestRoot
{
Width = 100,
Height = 100,
Child = new Border
{
Background = Brushes.Red,
Opacity = 0.5,
}
};
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
var target = CreateTargetAndRunFrame(root);
var context = GetLayerContext(target, root);
var animation = new BehaviorSubject<double>(0.5);
context.Verify(x => x.PushOpacity(0.5), Times.Once);
context.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once);
context.Verify(x => x.PopOpacity(), Times.Once);
}
[Fact]
public void Should_Not_Draw_Controls_With_0_Opacity()
{
var root = new TestRoot
{
Width = 100,
Height = 100,
Child = new Border
{
Background = Brushes.Red,
Opacity = 0,
Child = new Border
{
Background = Brushes.Green,
}
}
};
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
var target = CreateTargetAndRunFrame(root);
var context = GetLayerContext(target, root);
var animation = new BehaviorSubject<double>(0.5);
context.Verify(x => x.PushOpacity(0.5), Times.Never);
context.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Never);
context.Verify(x => x.PopOpacity(), Times.Never);
}
[Fact]
public void Should_Push_Opacity_Mask()
{
var root = new TestRoot
{
Width = 100,
Height = 100,
Child = new Border
{
Background = Brushes.Red,
OpacityMask = Brushes.Green,
}
};
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
var target = CreateTargetAndRunFrame(root);
var context = GetLayerContext(target, root);
var animation = new BehaviorSubject<double>(0.5);
context.Verify(x => x.PushOpacityMask(Brushes.Green, new Rect(0, 0, 100, 100)), Times.Once);
context.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once);
context.Verify(x => x.PopOpacityMask(), Times.Once);
}
[Fact]
public void Should_Create_Layer_For_Root()
{
var loop = new Mock<IRenderLoop>();
var root = new TestRoot();
var rootLayer = new Mock<IRenderTargetBitmapImpl>();
var dispatcher = new ImmediateDispatcher();
var sceneBuilder = new Mock<ISceneBuilder>();
sceneBuilder.Setup(x => x.UpdateAll(It.IsAny<Scene>()))
@ -143,23 +207,53 @@ namespace Avalonia.Visuals.UnitTests.Rendering
});
var renderInterface = new Mock<IPlatformRenderInterface>();
var target = CreateTargetAndRunFrame(root, sceneBuilder: sceneBuilder.Object);
var target = new DeferredRenderer(
root,
loop.Object,
sceneBuilder: sceneBuilder.Object,
//layerFactory: layers.Object,
dispatcher: dispatcher);
Assert.Single(target.Layers);
}
target.Start();
[Fact]
public void Should_Create_And_Delete_Layers_For_Controls_With_Animated_Opacity()
{
Border border;
var root = new TestRoot
{
Width = 100,
Height = 100,
Child = new Border
{
Background = Brushes.Red,
Child = border = new Border
{
Background = Brushes.Green,
Child = new Canvas(),
Opacity = 0.9,
}
}
};
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
var loop = new Mock<IRenderLoop>();
var target = CreateTargetAndRunFrame(root, loop: loop);
Assert.Equal(new[] { root }, target.Layers.Select(x => x.LayerRoot));
var animation = new BehaviorSubject<double>(0.5);
border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
RunFrame(loop);
var context = Mock.Get(root.CreateRenderTarget().CreateDrawingContext(null));
context.Verify(x => x.CreateLayer(root.ClientSize));
Assert.Equal(new IVisual[] { root, border }, target.Layers.Select(x => x.LayerRoot));
animation.OnCompleted();
RunFrame(loop);
Assert.Equal(new[] { root }, target.Layers.Select(x => x.LayerRoot));
}
[Fact]
public void Should_Create_And_Delete_Layers_For_Transparent_Controls()
public void Should_Not_Create_Layer_For_Childless_Control_With_Animated_Opacity()
{
Border border;
var root = new TestRoot
@ -176,51 +270,96 @@ namespace Avalonia.Visuals.UnitTests.Rendering
}
};
var animation = new BehaviorSubject<double>(0.5);
border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
var rootLayer = CreateLayer();
var borderLayer = CreateLayer();
var renderTargetContext = Mock.Get(root.CreateRenderTarget().CreateDrawingContext(null));
renderTargetContext.SetupSequence(x => x.CreateLayer(It.IsAny<Size>()))
.Returns(rootLayer)
.Returns(borderLayer);
var loop = new Mock<IRenderLoop>();
var target = new DeferredRenderer(
root,
loop.Object,
dispatcher: new ImmediateDispatcher());
root.Renderer = target;
var target = CreateTargetAndRunFrame(root, loop: loop);
target.Start();
RunFrame(loop);
Assert.Single(target.Layers);
}
[Fact]
public void Should_Not_Push_Opacity_For_Transparent_Layer_Root_Control()
{
Border border;
var root = new TestRoot
{
Width = 100,
Height = 100,
Child = border = new Border
{
Background = Brushes.Red,
Child = new Canvas(),
}
};
var animation = new BehaviorSubject<double>(0.5);
border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
var rootContext = Mock.Get(rootLayer.CreateDrawingContext(null));
var borderContext = Mock.Get(borderLayer.CreateDrawingContext(null));
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
rootContext.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once);
rootContext.Verify(x => x.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100), 0), Times.Once);
borderContext.Verify(x => x.FillRectangle(It.IsAny<IBrush>(), It.IsAny<Rect>(), It.IsAny<float>()), Times.Never);
var target = CreateTargetAndRunFrame(root);
var context = GetLayerContext(target, border);
rootContext.ResetCalls();
borderContext.ResetCalls();
border.Opacity = 0.5;
RunFrame(loop);
context.Verify(x => x.PushOpacity(0.5), Times.Never);
context.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once);
context.Verify(x => x.PopOpacity(), Times.Never);
}
[Fact]
public void Should_Draw_Transparent_Layer_With_Correct_Opacity()
{
Border border;
var root = new TestRoot
{
Width = 100,
Height = 100,
Child = border = new Border
{
Background = Brushes.Red,
Child = new Canvas(),
}
};
var animation = new BehaviorSubject<double>(0.5);
border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
rootContext.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once);
rootContext.Verify(x => x.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100), 0), Times.Never);
borderContext.Verify(x => x.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100), 0), Times.Once);
var target = CreateTargetAndRunFrame(root);
var context = Mock.Get(target.RenderTarget.CreateDrawingContext(null));
var borderLayer = target.Layers[border].Bitmap;
rootContext.ResetCalls();
borderContext.ResetCalls();
border.Opacity = 1;
context.Verify(x => x.DrawImage(borderLayer, 0.5, It.IsAny<Rect>(), It.IsAny<Rect>()));
}
private DeferredRenderer CreateTargetAndRunFrame(
TestRoot root,
Mock<IRenderLoop> loop = null,
ISceneBuilder sceneBuilder = null,
IDispatcher dispatcher = null)
{
loop = loop ?? new Mock<IRenderLoop>();
var target = new DeferredRenderer(
root,
loop.Object,
sceneBuilder: sceneBuilder,
dispatcher: dispatcher ?? new ImmediateDispatcher());
root.Renderer = target;
target.Start();
RunFrame(loop);
return target;
}
Mock.Get(borderLayer).Verify(x => x.Dispose());
rootContext.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once);
rootContext.Verify(x => x.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100), 0), Times.Once);
borderContext.Verify(x => x.FillRectangle(It.IsAny<IBrush>(), It.IsAny<Rect>(), It.IsAny<float>()), Times.Never);
private Mock<IDrawingContextImpl> GetLayerContext(DeferredRenderer renderer, IControl layerRoot)
{
return Mock.Get(renderer.Layers[layerRoot].Bitmap.CreateDrawingContext(null));
}
private void IgnoreFirstFrame(Mock<IRenderLoop> loop, Mock<ISceneBuilder> sceneBuilder)

14
tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs

@ -154,7 +154,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
{
Width = 200,
Height = 200,
Children = new Controls.Controls
Children =
{
new Border
{
@ -198,7 +198,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
{
Width = 200,
Height = 200,
Children = new Controls.Controls
Children =
{
new Border
{
@ -255,7 +255,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
Height = 200,
Background = Brushes.Red,
ClipToBounds = false,
Children = new Controls.Controls
Children =
{
new Border
{
@ -303,7 +303,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
Width = 100,
Height = 200,
Background = Brushes.Red,
Children = new Controls.Controls
Children =
{
new Panel()
{
@ -312,7 +312,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
Background = Brushes.Red,
Margin = new Thickness(0, 100, 0, 0),
ClipToBounds = true,
Children = new Controls.Controls
Children =
{
(target = new Border()
{
@ -354,7 +354,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
Width = 100,
Height = 200,
Background = Brushes.Red,
Children = new Controls.Controls
Children =
{
(target = new Border()
{
@ -374,7 +374,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
{
Content = new StackPanel()
{
Children = new Controls.Controls
Children =
{
(item1 = new Border()
{

14
tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests_HitTesting.cs

@ -126,7 +126,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
{
Width = 200,
Height = 200,
Children = new Controls.Controls
Children =
{
new Border
{
@ -171,7 +171,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
{
Width = 200,
Height = 200,
Children = new Controls.Controls
Children =
{
new Border
{
@ -238,7 +238,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
Height = 200,
Background = Brushes.Red,
ClipToBounds = false,
Children = new Controls.Controls
Children =
{
new Border
{
@ -287,7 +287,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
Width = 100,
Height = 200,
Background = Brushes.Red,
Children = new Controls.Controls
Children =
{
new Panel()
{
@ -296,7 +296,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
Background = Brushes.Red,
Margin = new Thickness(0, 100, 0, 0),
ClipToBounds = true,
Children = new Controls.Controls
Children =
{
(target = new Border()
{
@ -339,7 +339,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
Width = 100,
Height = 200,
Background = Brushes.Red,
Children = new Controls.Controls
Children =
{
(target = new Border()
{
@ -359,7 +359,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
{
Content = new StackPanel()
{
Children = new Controls.Controls
Children =
{
(item1 = new Border()
{

6
tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs

@ -9,6 +9,8 @@ using Xunit;
using Avalonia.Layout;
using Moq;
using Avalonia.Platform;
using System.Reactive.Subjects;
using Avalonia.Data;
namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
{
@ -620,13 +622,15 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
Margin = new Thickness(0, 10, 0, 0),
Child = border = new Border
{
Opacity = 0.5,
Background = Brushes.Red,
Child = canvas = new Canvas(),
}
}
};
var animation = new BehaviorSubject<double>(0.5);
border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
var scene = new Scene(tree);
var sceneBuilder = new SceneBuilder();
sceneBuilder.UpdateAll(scene);

75
tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs

@ -7,14 +7,15 @@ using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
using Avalonia.Layout;
using Avalonia.Rendering;
using System.Reactive.Subjects;
using Avalonia.Data;
namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
{
public partial class SceneBuilderTests
{
[Fact]
public void Control_With_Transparency_Should_Start_New_Layer()
public void Control_With_Animated_Opacity_And_Children_Should_Start_New_Layer()
{
using (TestApplication())
{
@ -31,10 +32,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
Padding = new Thickness(11),
Child = border = new Border
{
Opacity = 0.5,
Background = Brushes.Red,
Padding = new Thickness(12),
Child = canvas = new Canvas(),
Child = canvas = new Canvas()
}
}
};
@ -42,6 +42,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
var layout = AvaloniaLocator.Current.GetService<ILayoutManager>();
layout.ExecuteInitialLayoutPass(tree);
var animation = new BehaviorSubject<double>(0.5);
border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
var scene = new Scene(tree);
var sceneBuilder = new SceneBuilder();
sceneBuilder.UpdateAll(scene);
@ -58,7 +61,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
Assert.Equal(2, scene.Layers.Count());
Assert.Empty(scene.Layers.Select(x => x.LayerRoot).Except(new IVisual[] { tree, border }));
border.Opacity = 1;
animation.OnCompleted();
scene = scene.Clone();
sceneBuilder.Update(scene, border);
@ -80,13 +83,12 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
}
[Fact]
public void Control_With_OpacityMask_Should_Start_New_Layer()
public void Control_With_Animated_Opacity_And_No_Children_Should_Not_Start_New_Layer()
{
using (TestApplication())
{
Decorator decorator;
Border border;
Canvas canvas;
var tree = new TestRoot
{
Padding = new Thickness(10),
@ -97,10 +99,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
Padding = new Thickness(11),
Child = border = new Border
{
OpacityMask = Brushes.Red,
Background = Brushes.Red,
Padding = new Thickness(12),
Child = canvas = new Canvas(),
}
}
};
@ -108,45 +107,19 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
var layout = AvaloniaLocator.Current.GetService<ILayoutManager>();
layout.ExecuteInitialLayoutPass(tree);
var animation = new BehaviorSubject<double>(0.5);
border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
var scene = new Scene(tree);
var sceneBuilder = new SceneBuilder();
sceneBuilder.UpdateAll(scene);
var rootNode = (VisualNode)scene.Root;
var borderNode = (VisualNode)scene.FindNode(border);
var canvasNode = (VisualNode)scene.FindNode(canvas);
Assert.Same(tree, rootNode.LayerRoot);
Assert.Same(border, borderNode.LayerRoot);
Assert.Same(border, canvasNode.LayerRoot);
Assert.Equal(Brushes.Red, scene.Layers[border].OpacityMask);
Assert.Equal(2, scene.Layers.Count());
Assert.Empty(scene.Layers.Select(x => x.LayerRoot).Except(new IVisual[] { tree, border }));
border.OpacityMask = null;
scene = scene.Clone();
sceneBuilder.Update(scene, border);
rootNode = (VisualNode)scene.Root;
borderNode = (VisualNode)scene.FindNode(border);
canvasNode = (VisualNode)scene.FindNode(canvas);
Assert.Same(tree, rootNode.LayerRoot);
Assert.Same(tree, borderNode.LayerRoot);
Assert.Same(tree, canvasNode.LayerRoot);
Assert.Single(scene.Layers);
var rootDirty = scene.Layers[tree].Dirty;
Assert.Single(rootDirty);
Assert.Equal(new Rect(21, 21, 58, 78), rootDirty.Single());
}
}
[Fact]
public void Removing_Transparent_Control_Should_Remove_Layers()
public void Removing_Control_With_Animated_Opacity_Should_Remove_Layers()
{
using (TestApplication())
{
@ -163,13 +136,12 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
Padding = new Thickness(11),
Child = border = new Border
{
Opacity = 0.5,
Background = Brushes.Red,
Padding = new Thickness(12),
Child = canvas = new Canvas
{
Opacity = 0.75,
},
Children = { new TextBlock() },
}
}
}
};
@ -177,6 +149,10 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
var layout = AvaloniaLocator.Current.GetService<ILayoutManager>();
layout.ExecuteInitialLayoutPass(tree);
var animation = new BehaviorSubject<double>(0.5);
border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
canvas.Bind(Canvas.OpacityProperty, animation, BindingPriority.Animation);
var scene = new Scene(tree);
var sceneBuilder = new SceneBuilder();
sceneBuilder.UpdateAll(scene);
@ -210,13 +186,12 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
Padding = new Thickness(11),
Child = border = new Border
{
Opacity = 0.5,
Background = Brushes.Red,
Padding = new Thickness(12),
Child = canvas = new Canvas
{
Opacity = 0.75,
},
Children = { new TextBlock() },
}
}
}
};
@ -224,6 +199,10 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
var layout = AvaloniaLocator.Current.GetService<ILayoutManager>();
layout.ExecuteInitialLayoutPass(tree);
var animation = new BehaviorSubject<double>(0.5);
border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
canvas.Bind(Canvas.OpacityProperty, animation, BindingPriority.Animation);
var scene = new Scene(tree);
var sceneBuilder = new SceneBuilder();
sceneBuilder.UpdateAll(scene);
@ -256,6 +235,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
Child = border = new Border
{
Opacity = 0.5,
Child = new Canvas(),
}
}
};
@ -263,6 +243,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
var layout = AvaloniaLocator.Current.GetService<ILayoutManager>();
layout.ExecuteInitialLayoutPass(tree);
var animation = new BehaviorSubject<double>(0.5);
border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
var scene = new Scene(tree);
var sceneBuilder = new SceneBuilder();
sceneBuilder.UpdateAll(scene);

Loading…
Cancel
Save