diff --git a/.gitignore b/.gitignore index c36f64e5de..640725fa26 100644 --- a/.gitignore +++ b/.gitignore @@ -162,7 +162,8 @@ $RECYCLE.BIN/ ################# ## Cake ################# -tools/ +tools/* +!tools/packages.config .nuget artifacts/ nuget diff --git a/build.cake b/build.cake index 6518431959..b3822271d4 100644 --- a/build.cake +++ b/build.cake @@ -11,7 +11,7 @@ // TOOLS /////////////////////////////////////////////////////////////////////////////// -#tool "nuget:?package=xunit.runner.console&version=2.1.0" +#tool "nuget:?package=xunit.runner.console&version=2.2.0" #tool "nuget:?package=OpenCover" /////////////////////////////////////////////////////////////////////////////// @@ -98,7 +98,6 @@ Task("Clean") CleanDirectory(parameters.TestsRoot); }); - Task("Restore-NuGet-Packages") .IsDependentOn("Clean") .WithCriteria(parameters.IsRunningOnWindows) @@ -171,23 +170,25 @@ void RunCoreTest(string dir, Parameters parameters, bool net461Only) continue; Information("Running for " + fw); DotNetCoreTest(System.IO.Path.Combine(dir, System.IO.Path.GetFileName(dir)+".csproj"), - new DotNetCoreTestSettings{Framework = fw}); + new DotNetCoreTestSettings { + Configuration = parameters.Configuration, + Framework = fw + }); } } - Task("Run-Net-Core-Unit-Tests") .IsDependentOn("Clean") .Does(() => { RunCoreTest("./tests/Avalonia.Base.UnitTests", parameters, false); - RunCoreTest("./tests/Avalonia.Controls.UnitTests", parameters, true); - RunCoreTest("./tests/Avalonia.Input.UnitTests", parameters, true); - RunCoreTest("./tests/Avalonia.Interactivity.UnitTests", parameters, true); - RunCoreTest("./tests/Avalonia.Layout.UnitTests", parameters, true); - //RunCoreTest("./tests/Avalonia.Markup.UnitTests", parameters, true); - //RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", parameters, true); - RunCoreTest("./tests/Avalonia.Styling.UnitTests", parameters, true); - RunCoreTest("./tests/Avalonia.Visuals.UnitTests", parameters, true); + RunCoreTest("./tests/Avalonia.Controls.UnitTests", parameters, false); + RunCoreTest("./tests/Avalonia.Input.UnitTests", parameters, false); + RunCoreTest("./tests/Avalonia.Interactivity.UnitTests", parameters, false); + RunCoreTest("./tests/Avalonia.Layout.UnitTests", parameters, false); + RunCoreTest("./tests/Avalonia.Markup.UnitTests", parameters, false); + RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", parameters, false); + RunCoreTest("./tests/Avalonia.Styling.UnitTests", parameters, false); + RunCoreTest("./tests/Avalonia.Visuals.UnitTests", parameters, false); }); Task("Run-Unit-Tests") diff --git a/build/Moq.props b/build/Moq.props index c8544b8309..55242d922e 100644 --- a/build/Moq.props +++ b/build/Moq.props @@ -1,5 +1,5 @@  - + diff --git a/build/XUnit.props b/build/XUnit.props index 58df7e8d3c..27e0afc987 100644 --- a/build/XUnit.props +++ b/build/XUnit.props @@ -7,7 +7,9 @@ - - + + + + diff --git a/docs/tutorial/from-wpf.md b/docs/tutorial/from-wpf.md index aa7a9bc13a..2db40cfd86 100644 --- a/docs/tutorial/from-wpf.md +++ b/docs/tutorial/from-wpf.md @@ -33,7 +33,7 @@ placed in a `DataTemplates` collection on each control (and on `Application`): - + diff --git a/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj b/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj index d0405a3af8..1aad43a0ea 100644 --- a/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj +++ b/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj @@ -185,5 +185,6 @@ + \ No newline at end of file diff --git a/src/Avalonia.Base/Data/BindingNotification.cs b/src/Avalonia.Base/Data/BindingNotification.cs index ecaf59e174..125c29b21b 100644 --- a/src/Avalonia.Base/Data/BindingNotification.cs +++ b/src/Avalonia.Base/Data/BindingNotification.cs @@ -44,11 +44,7 @@ namespace Avalonia.Data public static readonly BindingNotification UnsetValue = new BindingNotification(AvaloniaProperty.UnsetValue); - // Null cannot be held in WeakReference as it's indistinguishable from an expired value so - // use this value in its place. - private static readonly object NullValue = new object(); - - private WeakReference _value; + private object _value; /// /// Initializes a new instance of the class. @@ -56,7 +52,7 @@ namespace Avalonia.Data /// The binding value. public BindingNotification(object value) { - _value = new WeakReference(value ?? NullValue); + _value = value; } /// @@ -73,6 +69,7 @@ namespace Avalonia.Data Error = error; ErrorType = errorType; + _value = AvaloniaProperty.UnsetValue; } /// @@ -84,7 +81,7 @@ namespace Avalonia.Data public BindingNotification(Exception error, BindingErrorType errorType, object fallbackValue) : this(error, errorType) { - _value = new WeakReference(fallbackValue ?? NullValue); + _value = fallbackValue; } /// @@ -95,31 +92,12 @@ namespace Avalonia.Data /// If this property is read when is false then it will return /// . /// - public object Value - { - get - { - if (_value != null) - { - object result; - - if (_value.TryGetTarget(out result)) - { - return result == NullValue ? null : result; - } - } - - // There's the possibility of a race condition in that HasValue can return true, - // and then the value is GC'd before Value is read. We should be ok though as - // we return UnsetValue which should be a safe alternative. - return AvaloniaProperty.UnsetValue; - } - } + public object Value => _value; /// /// Gets a value indicating whether should be pushed to the target. /// - public bool HasValue => _value != null; + public bool HasValue => _value != AvaloniaProperty.UnsetValue; /// /// Gets the error that occurred on the source, if any. @@ -248,7 +226,7 @@ namespace Avalonia.Data /// public void ClearValue() { - _value = null; + _value = AvaloniaProperty.UnsetValue; } /// @@ -256,7 +234,7 @@ namespace Avalonia.Data /// public void SetValue(object value) { - _value = new WeakReference(value ?? NullValue); + _value = value; } /// diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 7ed1c7fd8c..78bbd836be 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -207,7 +207,11 @@ namespace Avalonia.Controls /// The event args. protected virtual void OnClick(RoutedEventArgs e) { - Command?.Execute(CommandParameter); + if (Command != null) + { + Command.Execute(CommandParameter); + e.Handled = true; + } } /// @@ -215,13 +219,16 @@ namespace Avalonia.Controls { base.OnPointerPressed(e); - PseudoClasses.Add(":pressed"); - e.Device.Capture(this); - e.Handled = true; - - if (ClickMode == ClickMode.Press) + if (e.MouseButton == MouseButton.Left) { - RaiseClickEvent(); + PseudoClasses.Add(":pressed"); + e.Device.Capture(this); + e.Handled = true; + + if (ClickMode == ClickMode.Press) + { + RaiseClickEvent(); + } } } @@ -230,13 +237,16 @@ namespace Avalonia.Controls { base.OnPointerReleased(e); - e.Device.Capture(null); - PseudoClasses.Remove(":pressed"); - e.Handled = true; - - if (ClickMode == ClickMode.Release && Classes.Contains(":pointerover")) + if (e.MouseButton == MouseButton.Left) { - RaiseClickEvent(); + e.Device.Capture(null); + PseudoClasses.Remove(":pressed"); + e.Handled = true; + + if (ClickMode == ClickMode.Release && new Rect(Bounds.Size).Contains(e.GetPosition(this))) + { + RaiseClickEvent(); + } } } diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 758f7bbf55..83a76cb1a7 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -118,6 +118,7 @@ namespace Avalonia.Controls public Control() { _nameScope = this as INameScope; + _isAttachedToLogicalTree = this is IStyleRoot; } /// @@ -369,6 +370,12 @@ namespace Avalonia.Controls } } + /// + void ILogical.NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + this.OnAttachedToLogicalTreeCore(e); + } + /// void ILogical.NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { @@ -418,7 +425,7 @@ namespace Avalonia.Controls if (_isAttachedToLogicalTree) { - var oldRoot = FindStyleRoot(old); + var oldRoot = FindStyleRoot(old) ?? this as IStyleRoot; if (oldRoot == null) { @@ -436,7 +443,7 @@ namespace Avalonia.Controls _parent = (IControl)parent; - if (_parent is IStyleRoot || _parent?.IsAttachedToLogicalTree == true) + if (_parent is IStyleRoot || _parent?.IsAttachedToLogicalTree == true || this is IStyleRoot) { var newRoot = FindStyleRoot(this); diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index e919275d4f..994af9dab8 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -47,7 +47,7 @@ namespace Avalonia.Controls static Menu() { ItemsPanelProperty.OverrideDefaultValue(typeof(Menu), DefaultPanel); - MenuItem.ClickEvent.AddClassHandler(x => x.OnMenuClick); + MenuItem.ClickEvent.AddClassHandler(x => x.OnMenuClick, handledEventsToo: true); MenuItem.SubmenuOpenedEvent.AddClassHandler(x => x.OnSubmenuOpened); } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 3d15ed99e7..3d66fbc51b 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -102,6 +102,11 @@ namespace Avalonia.Controls AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler(x => x.AccessKeyPressed); } + public MenuItem() + { + + } + /// /// Occurs when a without a submenu is clicked. /// @@ -192,7 +197,11 @@ namespace Avalonia.Controls /// The click event args. protected virtual void OnClick(RoutedEventArgs e) { - Command?.Execute(CommandParameter); + if (Command != null) + { + Command.Execute(CommandParameter); + e.Handled = true; + } } /// diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 40fc2f302c..c1adff402a 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.Templates; using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Media; +using Avalonia.VisualTree; namespace Avalonia.Controls.Presenters { @@ -88,6 +89,7 @@ namespace Avalonia.Controls.Presenters static ContentPresenter() { ContentProperty.Changed.AddClassHandler(x => x.ContentChanged); + ContentTemplateProperty.Changed.AddClassHandler(x => x.ContentChanged); TemplatedParentProperty.Changed.AddClassHandler(x => x.TemplatedParentChanged); } @@ -313,27 +315,22 @@ namespace Avalonia.Controls.Presenters if (content != null && newChild == null) { - // We have content and it isn't a control, so first try to recycle the existing - // child control to display the new data by querying if the template that created - // the child can recycle items and that it also matches the new data. - if (oldChild != null && - _dataTemplate != null && - _dataTemplate.SupportsRecycling && - _dataTemplate.Match(content)) + var dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? FuncDataTemplate.Default; + + // We have content and it isn't a control, so if the new data template is the same + // as the old data template, try to recycle the existing child control to display + // the new data. + if (dataTemplate == _dataTemplate && dataTemplate.SupportsRecycling) { newChild = oldChild; } else { - // We couldn't recycle an existing control so find a data template for the data - // and use it to create a control. - _dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? FuncDataTemplate.Default; + _dataTemplate = dataTemplate; newChild = _dataTemplate.Build(content); - // Try to give the new control its own name scope. - var controlResult = newChild as Control; - - if (controlResult != null) + // Give the new control its own name scope. + if (newChild is Control controlResult) { NameScope.SetNameScope(controlResult, new NameScope()); } @@ -424,6 +421,19 @@ namespace Avalonia.Controls.Presenters private void ContentChanged(AvaloniaPropertyChangedEventArgs e) { _createdChild = false; + + if (((ILogical)this).IsAttachedToLogicalTree) + { + UpdateChild(); + } + else if (Child != null) + { + VisualChildren.Remove(Child); + LogicalChildren.Remove(Child); + Child = null; + _dataTemplate = null; + } + InvalidateMeasure(); } diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 7a42c48053..1ddfb97c14 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -285,6 +285,17 @@ namespace Avalonia.Controls.Primitives return this; } + /// + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + if (VisualChildren.Count > 0) + { + ((ILogical)VisualChildren[0]).NotifyAttachedToLogicalTree(e); + } + + base.OnAttachedToLogicalTree(e); + } + /// protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index b8896a3acf..22bc589a36 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -105,16 +105,21 @@ namespace Avalonia.Controls { if (control != null && control.IsVisible && control.GetVisualRoot() != null) { - if (s_popup != null) - { - throw new AvaloniaInternalException("Previous ToolTip not disposed."); - } var cp = (control.GetVisualRoot() as IInputRoot)?.MouseDevice?.GetPosition(control); var position = control.PointToScreen(cp ?? new Point(0, 0)) + new Vector(0, 22); - s_popup = new PopupRoot(); + if (s_popup == null) + { + s_popup = new PopupRoot(); + s_popup.Content = new ToolTip(); + } + else + { + ((ISetLogicalParent)s_popup).SetParent(null); + } + ((ISetLogicalParent)s_popup).SetParent(control); - s_popup.Content = new ToolTip { Content = GetTip(control) }; + ((ToolTip)s_popup.Content).Content = GetTip(control); s_popup.Position = position; s_popup.Show(); @@ -146,16 +151,23 @@ namespace Avalonia.Controls { if (s_popup != null) { - // Clear the ToolTip's Content in case it has control content: this will - // reset its visual parent allowing it to be used again. - ((ToolTip)s_popup.Content).Content = null; - - // Dispose of the popup. - s_popup.Dispose(); - s_popup = null; + DisposeTooltip(); + s_show.OnNext(null); } + } + } + + private static void DisposeTooltip() + { + if (s_popup != null) + { + // Clear the ToolTip's Content in case it has control content: this will + // reset its visual parent allowing it to be used again. + ((ToolTip)s_popup.Content).Content = null; - s_show.OnNext(null); + // Dispose of the popup. + s_popup.Dispose(); + s_popup = null; } } } diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index b966d09b1f..5d1b9a1462 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -16,7 +16,7 @@ namespace Avalonia.Controls /// /// Displays a hierachical tree of data. /// - public class TreeView : ItemsControl + public class TreeView : ItemsControl, ICustomKeyboardNavigation { /// /// Defines the property. @@ -90,6 +90,26 @@ namespace Avalonia.Controls } } + (bool handled, IInputElement next) ICustomKeyboardNavigation.GetNext(IInputElement element, NavigationDirection direction) + { + if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous) + { + if (!this.IsVisualAncestorOf(element)) + { + IControl result = _selectedItem != null ? + ItemContainerGenerator.Index.ContainerFromItem(_selectedItem) : + ItemContainerGenerator.ContainerFromIndex(0); + return (true, result); + } + else + { + return (true, null); + } + } + + return (false, null); + } + /// protected override IItemContainerGenerator CreateItemContainerGenerator() { diff --git a/src/Avalonia.Input/Avalonia.Input.csproj b/src/Avalonia.Input/Avalonia.Input.csproj index e9e74e24fe..0411cf77a5 100644 --- a/src/Avalonia.Input/Avalonia.Input.csproj +++ b/src/Avalonia.Input/Avalonia.Input.csproj @@ -37,5 +37,8 @@ Properties\SharedAssemblyInfo.cs + + + \ No newline at end of file diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs index e5cc5a8557..102da6efc4 100644 --- a/src/Avalonia.Input/FocusManager.cs +++ b/src/Avalonia.Input/FocusManager.cs @@ -176,9 +176,10 @@ namespace Avalonia.Input /// The event args. private void OnPreviewPointerPressed(object sender, RoutedEventArgs e) { - if (sender == e.Source) + var ev = (PointerPressedEventArgs)e; + + if (sender == e.Source && ev.MouseButton == MouseButton.Left) { - var ev = (PointerPressedEventArgs)e; var element = (ev.Device?.Captured as IInputElement) ?? (e.Source as IInputElement); if (element == null || !CanFocus(element)) diff --git a/src/Avalonia.Input/ICustomKeyboardNavigation.cs b/src/Avalonia.Input/ICustomKeyboardNavigation.cs new file mode 100644 index 0000000000..de5f98e04b --- /dev/null +++ b/src/Avalonia.Input/ICustomKeyboardNavigation.cs @@ -0,0 +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; + +namespace Avalonia.Input +{ + /// + /// Designates a control as handling its own keyboard navigation. + /// + public interface ICustomKeyboardNavigation + { + (bool handled, IInputElement next) GetNext(IInputElement element, NavigationDirection direction); + } +} diff --git a/src/Avalonia.Input/KeyboardNavigationHandler.cs b/src/Avalonia.Input/KeyboardNavigationHandler.cs index 57da49fa03..bf2b61d08b 100644 --- a/src/Avalonia.Input/KeyboardNavigationHandler.cs +++ b/src/Avalonia.Input/KeyboardNavigationHandler.cs @@ -2,7 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Linq; using Avalonia.Input.Navigation; +using Avalonia.VisualTree; namespace Avalonia.Input { @@ -52,6 +54,31 @@ namespace Avalonia.Input { Contract.Requires(element != null); + var customHandler = element.GetSelfAndVisualAncestors() + .OfType() + .FirstOrDefault(); + + if (customHandler != null) + { + var (handled, next) = customHandler.GetNext(element, direction); + + if (handled) + { + if (next != null) + { + return next; + } + else if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous) + { + return TabNavigation.GetNextInTabOrder((IInputElement)customHandler, direction, true); + } + else + { + return null; + } + } + } + if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous) { return TabNavigation.GetNextInTabOrder(element, direction); diff --git a/src/Avalonia.Input/Navigation/DirectionalNavigation.cs b/src/Avalonia.Input/Navigation/DirectionalNavigation.cs index a88ed1e8aa..75cb3a39e8 100644 --- a/src/Avalonia.Input/Navigation/DirectionalNavigation.cs +++ b/src/Avalonia.Input/Navigation/DirectionalNavigation.cs @@ -41,7 +41,7 @@ namespace Avalonia.Input.Navigation { case KeyboardNavigationMode.Continue: return GetNextInContainer(element, container, direction) ?? - GetFirstInNextContainer(element, direction); + GetFirstInNextContainer(element, element, direction); case KeyboardNavigationMode.Cycle: return GetNextInContainer(element, container, direction) ?? GetFocusableDescendant(container, direction); @@ -173,10 +173,12 @@ namespace Avalonia.Input.Navigation /// /// Gets the first item that should be focused in the next container. /// + /// The element being navigated away from. /// The container. /// The direction of the search. /// The first element, or null if there are no more elements. private static IInputElement GetFirstInNextContainer( + IInputElement element, IInputElement container, NavigationDirection direction) { @@ -200,6 +202,16 @@ namespace Avalonia.Input.Navigation if (sibling != null) { + if (sibling is ICustomKeyboardNavigation custom) + { + var (handled, customNext) = custom.GetNext(element, direction); + + if (handled) + { + return customNext; + } + } + if (sibling.CanFocus()) { next = sibling; @@ -214,7 +226,7 @@ namespace Avalonia.Input.Navigation if (next == null) { - next = GetFirstInNextContainer(parent, direction); + next = GetFirstInNextContainer(element, parent, direction); } } else diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs index 6ba7ab1a0c..6e077e887f 100644 --- a/src/Avalonia.Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Input/Navigation/TabNavigation.cs @@ -18,13 +18,17 @@ namespace Avalonia.Input.Navigation /// /// The element. /// The tab direction. Must be Next or Previous. + /// + /// If true will not descend into to find next control. + /// /// /// The next element in the specified direction, or null if /// was the last in the requested direction. /// public static IInputElement GetNextInTabOrder( IInputElement element, - NavigationDirection direction) + NavigationDirection direction, + bool outsideElement = false) { Contract.Requires(element != null); Contract.Requires( @@ -40,20 +44,20 @@ namespace Avalonia.Input.Navigation switch (mode) { case KeyboardNavigationMode.Continue: - return GetNextInContainer(element, container, direction) ?? - GetFirstInNextContainer(element, direction); + return GetNextInContainer(element, container, direction, outsideElement) ?? + GetFirstInNextContainer(element, element, direction); case KeyboardNavigationMode.Cycle: - return GetNextInContainer(element, container, direction) ?? + return GetNextInContainer(element, container, direction, outsideElement) ?? GetFocusableDescendant(container, direction); case KeyboardNavigationMode.Contained: - return GetNextInContainer(element, container, direction); + return GetNextInContainer(element, container, direction, outsideElement); default: - return GetFirstInNextContainer(container, direction); + return GetFirstInNextContainer(element, container, direction); } } else { - return GetFocusableDescendants(element).FirstOrDefault(); + return GetFocusableDescendants(element, direction).FirstOrDefault(); } } @@ -66,16 +70,17 @@ namespace Avalonia.Input.Navigation private static IInputElement GetFocusableDescendant(IInputElement container, NavigationDirection direction) { return direction == NavigationDirection.Next ? - GetFocusableDescendants(container).FirstOrDefault() : - GetFocusableDescendants(container).LastOrDefault(); + GetFocusableDescendants(container, direction).FirstOrDefault() : + GetFocusableDescendants(container, direction).LastOrDefault(); } /// /// Gets the focusable descendants of the specified element. /// /// The element. + /// The tab direction. Must be Next or Previous. /// The element's focusable descendants. - private static IEnumerable GetFocusableDescendants(IInputElement element) + private static IEnumerable GetFocusableDescendants(IInputElement element, NavigationDirection direction) { var mode = KeyboardNavigation.GetTabNavigation((InputElement)element); @@ -103,16 +108,25 @@ namespace Avalonia.Input.Navigation foreach (var child in children) { - if (child.CanFocus()) + var customNext = GetCustomNext(child, direction); + + if (customNext.handled) { - yield return child; + yield return customNext.next; } - - if (child.CanFocusDescendants()) + else { - foreach (var descendant in GetFocusableDescendants(child)) + if (child.CanFocus()) { - yield return descendant; + yield return child; + } + + if (child.CanFocusDescendants()) + { + foreach (var descendant in GetFocusableDescendants(child, direction)) + { + yield return descendant; + } } } } @@ -124,15 +138,19 @@ namespace Avalonia.Input.Navigation /// The starting element/ /// The container. /// The direction. + /// + /// If true will not descend into to find next control. + /// /// The next element, or null if the element is the last. private static IInputElement GetNextInContainer( IInputElement element, IInputElement container, - NavigationDirection direction) + NavigationDirection direction, + bool outsideElement) { - if (direction == NavigationDirection.Next) + if (direction == NavigationDirection.Next && !outsideElement) { - var descendant = GetFocusableDescendants(element).FirstOrDefault(); + var descendant = GetFocusableDescendants(element, direction).FirstOrDefault(); if (descendant != null) { @@ -167,7 +185,7 @@ namespace Avalonia.Input.Navigation if (element != null && direction == NavigationDirection.Previous) { - var descendant = GetFocusableDescendants(element).LastOrDefault(); + var descendant = GetFocusableDescendants(element, direction).LastOrDefault(); if (descendant != null) { @@ -184,10 +202,12 @@ namespace Avalonia.Input.Navigation /// /// Gets the first item that should be focused in the next container. /// + /// The element being navigated away from. /// The container. /// The direction of the search. /// The first element, or null if there are no more elements. private static IInputElement GetFirstInNextContainer( + IInputElement element, IInputElement container, NavigationDirection direction) { @@ -210,6 +230,13 @@ namespace Avalonia.Input.Navigation if (sibling != null) { + var customNext = GetCustomNext(sibling, direction); + + if (customNext.handled) + { + return customNext.next; + } + if (sibling.CanFocus()) { next = sibling; @@ -217,24 +244,34 @@ namespace Avalonia.Input.Navigation else { next = direction == NavigationDirection.Next ? - GetFocusableDescendants(sibling).FirstOrDefault() : - GetFocusableDescendants(sibling).LastOrDefault(); + GetFocusableDescendants(sibling, direction).FirstOrDefault() : + GetFocusableDescendants(sibling, direction).LastOrDefault(); } } if (next == null) { - next = GetFirstInNextContainer(parent, direction); + next = GetFirstInNextContainer(element, parent, direction); } } else { next = direction == NavigationDirection.Next ? - GetFocusableDescendants(container).FirstOrDefault() : - GetFocusableDescendants(container).LastOrDefault(); + GetFocusableDescendants(container, direction).FirstOrDefault() : + GetFocusableDescendants(container, direction).LastOrDefault(); } return next; } + + private static (bool handled, IInputElement next) GetCustomNext(IInputElement element, NavigationDirection direction) + { + if (element is ICustomKeyboardNavigation custom) + { + return custom.GetNext(element, direction); + } + + return (false, null); + } } } diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index 93c2227cab..0ff417e791 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -14,8 +14,8 @@ namespace Avalonia.Layout /// public class LayoutManager : ILayoutManager { - private readonly HashSet _toMeasure = new HashSet(); - private readonly HashSet _toArrange = new HashSet(); + private readonly Queue _toMeasure = new Queue(); + private readonly Queue _toArrange = new Queue(); private bool _queued; private bool _running; @@ -30,8 +30,18 @@ namespace Avalonia.Layout Contract.Requires(control != null); Dispatcher.UIThread.VerifyAccess(); - _toMeasure.Add(control); - _toArrange.Add(control); + if (!control.IsAttachedToVisualTree) + { +#if DEBUG + throw new AvaloniaInternalException( + "LayoutManager.InvalidateMeasure called on a control that is detached from the visual tree."); +#else + return; +#endif + } + + _toMeasure.Enqueue(control); + _toArrange.Enqueue(control); QueueLayoutPass(); } @@ -41,7 +51,17 @@ namespace Avalonia.Layout Contract.Requires(control != null); Dispatcher.UIThread.VerifyAccess(); - _toArrange.Add(control); + if (!control.IsAttachedToVisualTree) + { +#if DEBUG + throw new AvaloniaInternalException( + "LayoutManager.InvalidateArrange called on a control that is detached from the visual tree."); +#else + return; +#endif + } + + _toArrange.Enqueue(control); QueueLayoutPass(); } @@ -108,8 +128,12 @@ namespace Avalonia.Layout { while (_toMeasure.Count > 0) { - var next = _toMeasure.First(); - Measure(next); + var control = _toMeasure.Dequeue(); + + if (!control.IsMeasureValid && control.IsAttachedToVisualTree) + { + Measure(control); + } } } @@ -117,53 +141,60 @@ namespace Avalonia.Layout { while (_toArrange.Count > 0 && _toMeasure.Count == 0) { - var next = _toArrange.First(); - Arrange(next); + var control = _toArrange.Dequeue(); + + if (!control.IsArrangeValid && control.IsAttachedToVisualTree) + { + Arrange(control); + } } } private void Measure(ILayoutable control) { - var root = control as ILayoutRoot; - var parent = control.VisualParent as ILayoutable; - - if (root != null) - { - root.Measure(root.MaxClientSize); - } - else if (parent != null) + // Controls closest to the visual root need to be arranged first. We don't try to store + // ordered invalidation lists, instead we traverse the tree upwards, measuring the + // controls closest to the root first. This has been shown by benchmarks to be the + // fastest and most memory-efficent algorithm. + if (control.VisualParent is ILayoutable parent) { Measure(parent); } - if (!control.IsMeasureValid) + // If the control being measured has IsMeasureValid == true here then its measure was + // handed by an ancestor and can be ignored. The measure may have also caused the + // control to be removed. + if (!control.IsMeasureValid && control.IsAttachedToVisualTree) { - control.Measure(control.PreviousMeasure.Value); + if (control is ILayoutRoot root) + { + root.Measure(Size.Infinity); + } + else + { + control.Measure(control.PreviousMeasure.Value); + } } - - _toMeasure.Remove(control); } private void Arrange(ILayoutable control) { - var root = control as ILayoutRoot; - var parent = control.VisualParent as ILayoutable; - - if (root != null) - { - root.Arrange(new Rect(root.ClientSize)); - } - else if (parent != null) + if (control.VisualParent is ILayoutable parent) { Arrange(parent); } - if (control.PreviousArrange.HasValue) + if (!control.IsArrangeValid && control.IsAttachedToVisualTree) { - control.Arrange(control.PreviousArrange.Value); + if (control is ILayoutRoot root) + { + root.Arrange(new Rect(root.ClientSize)); + } + else + { + control.Arrange(control.PreviousArrange.Value); + } } - - _toArrange.Remove(control); } private void QueueLayoutPass() diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index 3f6d789877..a08ab77d70 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -378,8 +378,12 @@ namespace Avalonia.Layout IsMeasureValid = false; IsArrangeValid = false; - LayoutManager.Instance?.InvalidateMeasure(this); - InvalidateVisual(); + + if (((ILayoutable)this).IsAttachedToVisualTree) + { + LayoutManager.Instance?.InvalidateMeasure(this); + InvalidateVisual(); + } } } @@ -393,8 +397,12 @@ namespace Avalonia.Layout Logger.Verbose(LogArea.Layout, this, "Invalidated arrange"); IsArrangeValid = false; - LayoutManager.Instance?.InvalidateArrange(this); - InvalidateVisual(); + + if (((ILayoutable)this).IsAttachedToVisualTree) + { + LayoutManager.Instance?.InvalidateArrange(this); + InvalidateVisual(); + } } } diff --git a/src/Avalonia.Styling/LogicalTree/ILogical.cs b/src/Avalonia.Styling/LogicalTree/ILogical.cs index f2291b42e9..006a9f5cc1 100644 --- a/src/Avalonia.Styling/LogicalTree/ILogical.cs +++ b/src/Avalonia.Styling/LogicalTree/ILogical.cs @@ -36,6 +36,16 @@ namespace Avalonia.LogicalTree /// IAvaloniaReadOnlyList LogicalChildren { get; } + /// + /// Notifies the control that it is being attached to a rooted logical tree. + /// + /// The event args. + /// + /// This method will be called automatically by the framework, you should not need to call + /// this method yourself. + /// + void NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e); + /// /// Notifies the control that it is being detached from a rooted logical tree. /// diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index be4282cdc0..c050ff0e75 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -61,12 +61,12 @@ namespace Avalonia.Styling } /// - /// Gets or sets style's selector. + /// Gets or sets the style's selector. /// public Selector Selector { get; set; } /// - /// Gets or sets style's setters. + /// Gets or sets the style's setters. /// [Content] public IEnumerable Setters { get; set; } = new List(); diff --git a/src/Gtk/Avalonia.Gtk3/Interop/GObject.cs b/src/Gtk/Avalonia.Gtk3/Interop/GObject.cs index 9ead1d2cb3..9766e8ca36 100644 --- a/src/Gtk/Avalonia.Gtk3/Interop/GObject.cs +++ b/src/Gtk/Avalonia.Gtk3/Interop/GObject.cs @@ -41,7 +41,7 @@ namespace Avalonia.Gtk3.Interop class GtkWindow : GtkWidget { - + public static GtkWindow Null { get; } = new GtkWindow(); } class GtkImContext : GObject diff --git a/src/Gtk/Avalonia.Gtk3/Interop/Utf8Buffer.cs b/src/Gtk/Avalonia.Gtk3/Interop/Utf8Buffer.cs index f108c291b8..fc76fefd1a 100644 --- a/src/Gtk/Avalonia.Gtk3/Interop/Utf8Buffer.cs +++ b/src/Gtk/Avalonia.Gtk3/Interop/Utf8Buffer.cs @@ -11,6 +11,8 @@ namespace Avalonia.Gtk3.Interop public Utf8Buffer(string s) : base(IntPtr.Zero, true) { + if (s == null) + return; _data = Encoding.UTF8.GetBytes(s); _gchandle = GCHandle.Alloc(_data, GCHandleType.Pinned); handle = _gchandle.AddrOfPinnedObject(); diff --git a/src/Gtk/Avalonia.Gtk3/SystemDialogs.cs b/src/Gtk/Avalonia.Gtk3/SystemDialogs.cs index f6232ac68e..fb8af02d5d 100644 --- a/src/Gtk/Avalonia.Gtk3/SystemDialogs.cs +++ b/src/Gtk/Avalonia.Gtk3/SystemDialogs.cs @@ -18,7 +18,8 @@ namespace Avalonia.Gtk3 bool multiselect, string initialFileName) { GtkFileChooser dlg; - using (var name = title != null ? new Utf8Buffer(title) : null) + parent = parent ?? GtkWindow.Null; + using (var name = new Utf8Buffer(title)) dlg = Native.GtkFileChooserDialogNew(name, parent, action, IntPtr.Zero); if (multiselect) Native.GtkFileChooserSetSelectMultiple(dlg, true); diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs index 69190be220..621e06efba 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs @@ -102,7 +102,7 @@ namespace Avalonia.Markup.Xaml.Data private object ConvertValue(IList values, Type targetType) { - var converted = Converter.Convert(values, targetType, null, CultureInfo.CurrentUICulture); + var converted = Converter.Convert(values, targetType, null, CultureInfo.CurrentCulture); if (converted == AvaloniaProperty.UnsetValue && FallbackValue != null) { diff --git a/src/Markup/Avalonia.Markup/Data/BindingExpression.cs b/src/Markup/Avalonia.Markup/Data/BindingExpression.cs index 0f4c091bff..5b9959e42e 100644 --- a/src/Markup/Avalonia.Markup/Data/BindingExpression.cs +++ b/src/Markup/Avalonia.Markup/Data/BindingExpression.cs @@ -122,7 +122,7 @@ namespace Avalonia.Markup.Data value, type, ConverterParameter, - CultureInfo.CurrentUICulture); + CultureInfo.CurrentCulture); if (converted == AvaloniaProperty.UnsetValue) { @@ -186,7 +186,7 @@ namespace Avalonia.Markup.Data value, _targetType, ConverterParameter, - CultureInfo.CurrentUICulture); + CultureInfo.CurrentCulture); notification = converted as BindingNotification; diff --git a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj index d55dc8d544..c656801d90 100644 --- a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj +++ b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj @@ -1,6 +1,7 @@  net461;netcoreapp1.1 + Library diff --git a/tests/Avalonia.Base.UnitTests/Properties/AssemblyInfo.cs b/tests/Avalonia.Base.UnitTests/Properties/AssemblyInfo.cs index 8e5e3a305b..4b93ea8400 100644 --- a/tests/Avalonia.Base.UnitTests/Properties/AssemblyInfo.cs +++ b/tests/Avalonia.Base.UnitTests/Properties/AssemblyInfo.cs @@ -7,4 +7,4 @@ using Xunit; [assembly: AssemblyTitle("Avalonia.UnitTests")] // Don't run tests in parallel. -[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file +[assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj index 1f5ebac203..21d7b186b4 100644 --- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj +++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj @@ -49,6 +49,7 @@ + @@ -100,7 +101,7 @@ - + \ No newline at end of file diff --git a/tests/Avalonia.Benchmarks/Layout/Measure.cs b/tests/Avalonia.Benchmarks/Layout/Measure.cs new file mode 100644 index 0000000000..d1fdae9971 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Layout/Measure.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.UnitTests; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Layout +{ + [MemoryDiagnoser] + public class Measure : IDisposable + { + private IDisposable _app; + private TestRoot root; + private List controls = new List(); + + public Measure() + { + _app = UnitTestApplication.Start(TestServices.RealLayoutManager); + + var panel = new StackPanel(); + root = new TestRoot { Child = panel }; + controls.Add(panel); + CreateChildren(panel, 3, 5); + LayoutManager.Instance.ExecuteInitialLayoutPass(root); + } + + public void Dispose() + { + _app.Dispose(); + } + + [Benchmark] + public void Remeasure_Half() + { + var random = new Random(1); + + foreach (var control in controls) + { + if (random.Next(2) == 0) + { + control.InvalidateMeasure(); + } + } + + LayoutManager.Instance.ExecuteLayoutPass(); + } + + private void CreateChildren(IPanel parent, int childCount, int iterations) + { + for (var i = 0; i < childCount; ++i) + { + var control = new StackPanel(); + parent.Children.Add(control); + + if (iterations > 0) + { + CreateChildren(control, childCount, iterations - 1); + } + + controls.Add(control); + } + } + } +} diff --git a/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs b/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs index 0af451efd2..33af55fdf9 100644 --- a/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs +++ b/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs @@ -11,6 +11,7 @@ using Avalonia.VisualTree; namespace Avalonia.Benchmarks.Styling { + [MemoryDiagnoser] public class ApplyStyling : IDisposable { private IDisposable _app; diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj index f8235f7d68..957cdd7036 100644 --- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj +++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj @@ -1,12 +1,16 @@  net461;netcoreapp1.1 + Library + + + diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs new file mode 100644 index 0000000000..e32c703409 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs @@ -0,0 +1,291 @@ +// 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.Linq; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.LogicalTree; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using Moq; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Presenters +{ + /// + /// Tests for ContentControls that are hosted in a control template. + /// + public class ContentPresenterTests_InTemplate + { + [Fact] + public void Should_Register_With_Host_When_TemplatedParent_Set() + { + var host = new Mock(); + var target = new ContentPresenter(); + + target.SetValue(Control.TemplatedParentProperty, host.Object); + + host.Verify(x => x.RegisterContentPresenter(target)); + } + + [Fact] + public void Setting_Content_To_Control_Should_Set_Child() + { + var (target, _) = CreateTarget(); + var child = new Border(); + + target.Content = child; + + Assert.Equal(child, target.Child); + } + + [Fact] + public void Setting_Content_To_Control_Should_Update_Logical_Tree() + { + var (target, parent) = CreateTarget(); + var child = new Border(); + + target.Content = child; + + Assert.Equal(parent, child.GetLogicalParent()); + Assert.Equal(new[] { child }, parent.GetLogicalChildren()); + } + + [Fact] + public void Setting_Content_To_Control_Should_Update_Visual_Tree() + { + var (target, _) = CreateTarget(); + var child = new Border(); + + target.Content = child; + + Assert.Equal(target, child.GetVisualParent()); + Assert.Equal(new[] { child }, target.GetVisualChildren()); + } + + [Fact] + public void Setting_Content_To_String_Should_Create_TextBlock() + { + var (target, _) = CreateTarget(); + + target.Content = "Foo"; + + Assert.IsType(target.Child); + Assert.Equal("Foo", ((TextBlock)target.Child).Text); + } + + [Fact] + public void Setting_Content_To_String_Should_Update_Logical_Tree() + { + var (target, parent) = CreateTarget(); + + target.Content = "Foo"; + + var child = target.Child; + Assert.Equal(parent, child.GetLogicalParent()); + Assert.Equal(new[] { child }, parent.GetLogicalChildren()); + } + + [Fact] + public void Setting_Content_To_String_Should_Update_Visual_Tree() + { + var (target, _) = CreateTarget(); + + target.Content = "Foo"; + + var child = target.Child; + Assert.Equal(target, child.GetVisualParent()); + Assert.Equal(new[] { child }, target.GetVisualChildren()); + } + + [Fact] + public void Clearing_Control_Content_Should_Update_Logical_Tree() + { + var (target, _) = CreateTarget(); + var child = new Border(); + + target.Content = child; + target.Content = null; + + Assert.Equal(null, child.GetLogicalParent()); + Assert.Empty(target.GetLogicalChildren()); + } + + [Fact] + public void Clearing_Control_Content_Should_Update_Visual_Tree() + { + var (target, _) = CreateTarget(); + var child = new Border(); + + target.Content = child; + target.Content = null; + + Assert.Equal(null, child.GetVisualParent()); + Assert.Empty(target.GetVisualChildren()); + } + + [Fact] + public void Control_Content_Should_Not_Be_NameScope() + { + var (target, _) = CreateTarget(); + + target.Content = new TextBlock(); + + Assert.IsType(target.Child); + Assert.Null(NameScope.GetNameScope((Control)target.Child)); + } + + [Fact] + public void DataTemplate_Created_Control_Should_Be_NameScope() + { + var (target, _) = CreateTarget(); + + target.Content = "Foo"; + + Assert.IsType(target.Child); + Assert.NotNull(NameScope.GetNameScope((Control)target.Child)); + } + + [Fact] + public void Assigning_Control_To_Content_Should_Not_Set_DataContext() + { + var (target, _) = CreateTarget(); + target.Content = new Border(); + + Assert.False(target.IsSet(Control.DataContextProperty)); + } + + [Fact] + public void Assigning_NonControl_To_Content_Should_Set_DataContext_On_UpdateChild() + { + var (target, _) = CreateTarget(); + target.Content = "foo"; + + Assert.Equal("foo", target.DataContext); + } + + [Fact] + public void Should_Use_ContentTemplate_If_Specified() + { + var (target, _) = CreateTarget(); + + target.ContentTemplate = new FuncDataTemplate(_ => new Canvas()); + target.Content = "Foo"; + + Assert.IsType(target.Child); + } + + [Fact] + public void Should_Update_If_ContentTemplate_Changed() + { + var (target, _) = CreateTarget(); + + target.Content = "Foo"; + Assert.IsType(target.Child); + + target.ContentTemplate = new FuncDataTemplate(_ => new Canvas()); + Assert.IsType(target.Child); + + target.ContentTemplate = null; + Assert.IsType(target.Child); + } + + [Fact] + public void Assigning_Control_To_Content_After_NonControl_Should_Clear_DataContext() + { + var (target, _) = CreateTarget(); + + target.Content = "foo"; + + Assert.True(target.IsSet(Control.DataContextProperty)); + + target.Content = new Border(); + + Assert.False(target.IsSet(Control.DataContextProperty)); + } + + [Fact] + public void Recycles_DataTemplate() + { + var (target, _) = CreateTarget(); + target.DataTemplates.Add(new FuncDataTemplate(_ => new Border(), true)); + + target.Content = "foo"; + + var control = target.Child; + Assert.IsType(control); + + target.Content = "bar"; + Assert.Same(control, target.Child); + } + + [Fact] + public void Detects_DataTemplate_Doesnt_Match_And_Doesnt_Recycle() + { + var (target, _) = CreateTarget(); + target.DataTemplates.Add(new FuncDataTemplate(x => x == "foo", _ => new Border(), true)); + + target.Content = "foo"; + + var control = target.Child; + Assert.IsType(control); + + target.Content = "bar"; + Assert.IsType(target.Child); + } + + [Fact] + public void Detects_DataTemplate_Doesnt_Support_Recycling() + { + var (target, _) = CreateTarget(); + target.DataTemplates.Add(new FuncDataTemplate(_ => new Border(), false)); + + target.Content = "foo"; + + var control = target.Child; + Assert.IsType(control); + + target.Content = "bar"; + Assert.NotSame(control, target.Child); + } + + [Fact] + public void Reevaluates_DataTemplates_When_Recycling() + { + var (target, _) = CreateTarget(); + + target.DataTemplates.Add(new FuncDataTemplate(x => x == "bar", _ => new Canvas(), true)); + target.DataTemplates.Add(new FuncDataTemplate(_ => new Border(), true)); + + target.Content = "foo"; + + var control = target.Child; + Assert.IsType(control); + + target.Content = "bar"; + Assert.IsType(target.Child); + } + + (ContentPresenter presenter, ContentControl templatedParent) CreateTarget() + { + var templatedParent = new ContentControl + { + Template = new FuncControlTemplate(x => + new ContentPresenter + { + Name = "PART_ContentPresenter", + }), + }; + var root = new TestRoot { Child = templatedParent }; + + templatedParent.ApplyTemplate(); + + return ((ContentPresenter)templatedParent.Presenter, templatedParent); + } + + private class TestContentControl : ContentControl + { + public IControl Child { get; set; } + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs similarity index 52% rename from tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs rename to tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs index 88d26334ed..589b1d67d2 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs @@ -15,91 +15,13 @@ using Xunit; namespace Avalonia.Controls.UnitTests.Presenters { - public class ContentPresenterTests + /// + /// Tests for ContentControls that aren't hosted in a control template. + /// + public class ContentPresenterTests_Standalone { [Fact] - public void Should_Register_With_Host_When_TemplatedParent_Set() - { - var host = new Mock(); - var target = new ContentPresenter(); - - target.SetValue(Control.TemplatedParentProperty, host.Object); - - host.Verify(x => x.RegisterContentPresenter(target)); - } - - [Fact] - public void Setting_Content_To_Control_Should_Set_Child() - { - var target = new ContentPresenter(); - var child = new Border(); - - target.Content = child; - - Assert.Null(target.Child); - target.UpdateChild(); - Assert.Equal(child, target.Child); - } - - [Fact] - public void Setting_Content_To_String_Should_Create_TextBlock() - { - var target = new ContentPresenter(); - - target.Content = "Foo"; - - Assert.Null(target.Child); - target.UpdateChild(); - Assert.IsType(target.Child); - Assert.Equal("Foo", ((TextBlock)target.Child).Text); - } - - [Fact] - public void Control_Content_Should_Not_Be_NameScope() - { - var target = new ContentPresenter(); - - target.Content = new TextBlock(); - - Assert.Null(target.Child); - target.UpdateChild(); - Assert.IsType(target.Child); - Assert.Null(NameScope.GetNameScope((Control)target.Child)); - } - - [Fact] - public void DataTemplate_Created_Control_Should_Be_NameScope() - { - var target = new ContentPresenter(); - - target.Content = "Foo"; - - Assert.Null(target.Child); - target.UpdateChild(); - Assert.IsType(target.Child); - Assert.NotNull(NameScope.GetNameScope((Control)target.Child)); - } - - [Fact] - public void Should_Set_Childs_Parent_To_TemplatedParent() - { - var content = new Border(); - var target = new TestContentControl - { - Template = new FuncControlTemplate(parent => - new ContentPresenter { Content = parent.Child }), - Child = content, - }; - - target.ApplyTemplate(); - var presenter = ((ContentPresenter)target.GetVisualChildren().Single()); - presenter.UpdateChild(); - - Assert.Same(target, content.Parent); - } - - [Fact] - public void Should_Set_Childs_Parent_To_Itself_Outside_Template() + public void Should_Set_Childs_Parent_To_Itself_Standalone() { var content = new Border(); var target = new ContentPresenter { Content = content }; @@ -110,7 +32,7 @@ namespace Avalonia.Controls.UnitTests.Presenters } [Fact] - public void Should_Add_Child_To_Own_LogicalChildren_Outside_Template() + public void Should_Add_Child_To_Own_LogicalChildren_Standalone() { var content = new Border(); var target = new ContentPresenter { Content = content }; @@ -124,94 +46,7 @@ namespace Avalonia.Controls.UnitTests.Presenters } [Fact] - public void Adding_To_Logical_Tree_Should_Reevaluate_DataTemplates() - { - var target = new ContentPresenter - { - Content = "Foo", - }; - - target.UpdateChild(); - Assert.IsType(target.Child); - - var root = new TestRoot - { - DataTemplates = new DataTemplates - { - new FuncDataTemplate(x => new Decorator()), - }, - }; - - root.Child = target; - target.ApplyTemplate(); - Assert.IsType(target.Child); - } - - [Fact] - public void Assigning_Control_To_Content_Should_Not_Set_DataContext() - { - var target = new ContentPresenter - { - Content = new Border(), - }; - - Assert.False(target.IsSet(Control.DataContextProperty)); - } - - [Fact] - public void Assigning_NonControl_To_Content_Should_Set_DataContext_On_UpdateChild() - { - var target = new ContentPresenter - { - Content = "foo", - }; - - target.UpdateChild(); - - Assert.Equal("foo", target.DataContext); - } - - [Fact] - public void Assigning_Control_To_Content_After_NonControl_Should_Clear_DataContext() - { - var target = new ContentPresenter(); - - target.Content = "foo"; - target.UpdateChild(); - - Assert.True(target.IsSet(Control.DataContextProperty)); - - target.Content = new Border(); - target.UpdateChild(); - - Assert.False(target.IsSet(Control.DataContextProperty)); - } - - [Fact] - public void Tries_To_Recycle_DataTemplate() - { - var target = new ContentPresenter - { - DataTemplates = new DataTemplates - { - new FuncDataTemplate(_ => new Border(), true), - }, - Content = "foo", - }; - - target.UpdateChild(); - var control = target.Child; - - Assert.IsType(control); - - target.Content = "bar"; - target.UpdateChild(); - - Assert.Same(control, target.Child); - } - - [Fact] - public void Should_Raise_DetachedFromLogicalTree_On_Content_Changed_OutsideTemplate() + public void Should_Raise_DetachedFromLogicalTree_On_Content_Changed_Standalone() { var target = new ContentPresenter { @@ -250,7 +85,7 @@ namespace Avalonia.Controls.UnitTests.Presenters } [Fact] - public void Should_Raise_DetachedFromLogicalTree_In_ContentControl_On_Content_Changed_OutsideTemplate() + public void Should_Raise_DetachedFromLogicalTree_In_ContentControl_On_Content_Changed_Standalone() { var contentControl = new ContentControl { @@ -292,13 +127,14 @@ namespace Avalonia.Controls.UnitTests.Presenters var tbbar = target.Child as ContentControl; Assert.NotNull(tbbar); + Assert.True(tbbar != tbfoo); Assert.False((tbfoo as IControl).IsAttachedToLogicalTree); Assert.True(foodetached); } [Fact] - public void Should_Raise_DetachedFromLogicalTree_On_Detached_OutsideTemplate() + public void Should_Raise_DetachedFromLogicalTree_On_Detached_Standalone() { var target = new ContentPresenter { @@ -332,7 +168,7 @@ namespace Avalonia.Controls.UnitTests.Presenters } [Fact] - public void Should_Remove_Old_Child_From_LogicalChildren_On_ContentChanged_OutsideTemplate() + public void Should_Remove_Old_Child_From_LogicalChildren_On_ContentChanged_Standalone() { var target = new ContentPresenter { @@ -363,9 +199,5 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.NotEqual(foo, logicalChildren.First()); } - private class TestContentControl : ContentControl - { - public IControl Child { get; set; } - } } } \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Unrooted.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Unrooted.cs new file mode 100644 index 0000000000..3585109dee --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Unrooted.cs @@ -0,0 +1,102 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Presenters +{ + /// + /// Tests for ContentControls that are not attached to a logical tree. + /// + public class ContentPresenterTests_Unrooted + { + [Fact] + public void Setting_Content_To_Control_Should_Not_Set_Child_Unless_UpdateChild_Called() + { + var target = new ContentPresenter(); + var child = new Border(); + + target.Content = child; + Assert.Null(target.Child); + + target.ApplyTemplate(); + Assert.Null(target.Child); + + target.UpdateChild(); + Assert.Equal(child, target.Child); + } + + [Fact] + public void Setting_Content_To_String_Should_Not_Create_TextBlock_Unless_UpdateChild_Called() + { + var target = new ContentPresenter(); + + target.Content = "Foo"; + Assert.Null(target.Child); + + target.ApplyTemplate(); + Assert.Null(target.Child); + + target.UpdateChild(); + Assert.IsType(target.Child); + Assert.Equal("Foo", ((TextBlock)target.Child).Text); + } + + [Fact] + public void Clearing_Control_Content_Should_Remove_Child_Immediately() + { + var target = new ContentPresenter(); + var child = new Border(); + + target.Content = child; + target.UpdateChild(); + Assert.Equal(child, target.Child); + + target.Content = null; + Assert.Null(target.Child); + } + + [Fact] + public void Clearing_String_Content_Should_Remove_Child_Immediately() + { + var target = new ContentPresenter(); + + target.Content = "Foo"; + target.UpdateChild(); + Assert.IsType(target.Child); + + target.Content = null; + Assert.Null(target.Child); + } + + [Fact] + public void Adding_To_Logical_Tree_Should_Reevaluate_DataTemplates() + { + var root = new TestRoot(); + var target = new ContentPresenter(); + + target.Content = "Foo"; + Assert.Null(target.Child); + + root.Child = target; + target.ApplyTemplate(); + Assert.IsType(target.Child); + + root.Child = null; + root = new TestRoot + { + DataTemplates = new DataTemplates + { + new FuncDataTemplate(x => new Decorator()), + }, + }; + + root.Child = target; + target.ApplyTemplate(); + Assert.IsType(target.Child); + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs new file mode 100644 index 0000000000..64344a1584 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs @@ -0,0 +1,109 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.LogicalTree; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Primitives +{ + public class PopupRootTests + { + [Fact] + public void PopupRoot_IsAttachedToLogicalTree_Is_True() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = CreateTarget(); + + Assert.True(((ILogical)target).IsAttachedToLogicalTree); + } + } + + [Fact] + public void Templated_Child_IsAttachedToLogicalTree_Is_True() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = CreateTarget(); + + Assert.True(target.Presenter.IsAttachedToLogicalTree); + } + } + + [Fact] + public void Attaching_PopupRoot_To_Parent_Logical_Tree_Raises_DetachedFromLogicalTree_And_AttachedToLogicalTree() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var child = new Decorator(); + var target = CreateTarget(); + var window = new Window(); + var detachedCount = 0; + var attachedCount = 0; + + target.Content = child; + + target.DetachedFromLogicalTree += (s, e) => ++detachedCount; + child.DetachedFromLogicalTree += (s, e) => ++detachedCount; + target.AttachedToLogicalTree += (s, e) => ++attachedCount; + child.AttachedToLogicalTree += (s, e) => ++attachedCount; + + ((ISetLogicalParent)target).SetParent(window); + + Assert.Equal(2, detachedCount); + Assert.Equal(2, attachedCount); + } + } + + [Fact] + public void Detaching_PopupRoot_From_Parent_Logical_Tree_Raises_DetachedFromLogicalTree_And_AttachedToLogicalTree() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var child = new Decorator(); + var target = CreateTarget(); + var window = new Window(); + var detachedCount = 0; + var attachedCount = 0; + + target.Content = child; + ((ISetLogicalParent)target).SetParent(window); + + target.DetachedFromLogicalTree += (s, e) => ++detachedCount; + child.DetachedFromLogicalTree += (s, e) => ++detachedCount; + target.AttachedToLogicalTree += (s, e) => ++attachedCount; + child.AttachedToLogicalTree += (s, e) => ++attachedCount; + + ((ISetLogicalParent)target).SetParent(null); + + // Despite being detached from the parent logical tree, we're still attached to a + // logical tree as PopupRoot itself is a logical tree root. + Assert.True(((ILogical)target).IsAttachedToLogicalTree); + Assert.True(((ILogical)child).IsAttachedToLogicalTree); + Assert.Equal(2, detachedCount); + Assert.Equal(2, attachedCount); + } + } + + private PopupRoot CreateTarget() + { + var result = new PopupRoot + { + Template = new FuncControlTemplate(_ => + new ContentPresenter + { + Name = "PART_ContentPresenter", + }), + }; + + result.ApplyTemplate(); + + return result; + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs index 636492ed1c..72c8073f21 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs @@ -527,6 +527,34 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void Moving_To_New_LogicalTree_Should_Detach_Attach_Template_Child() + { + using (UnitTestApplication.Start(TestServices.RealStyler)) + { + TestTemplatedControl target; + var root = new TestRoot + { + Child = target = new TestTemplatedControl + { + Template = new FuncControlTemplate(_ => new Decorator()), + } + }; + + Assert.NotNull(target.Template); + target.ApplyTemplate(); + + var templateChild = (ILogical)target.GetVisualChildren().Single(); + Assert.True(templateChild.IsAttachedToLogicalTree); + + root.Child = null; + Assert.False(templateChild.IsAttachedToLogicalTree); + + var newRoot = new TestRoot { Child = target }; + Assert.True(templateChild.IsAttachedToLogicalTree); + } + } + private static IControl ScrollingContentControlTemplate(ContentControl control) { return new Border diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index 5cd3c57e2e..da30336be6 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -2,24 +2,33 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive; -using System.Reactive.Subjects; -using Moq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Layout; +using Avalonia.LogicalTree; using Avalonia.Platform; -using Avalonia.Rendering; -using Avalonia.Styling; using Avalonia.UnitTests; +using Moq; using Xunit; namespace Avalonia.Controls.UnitTests { public class TopLevelTests { + [Fact] + public void IsAttachedToLogicalTree_Is_True() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var impl = new Mock(); + var target = new TestTopLevel(impl.Object); + + Assert.True(((ILogical)target).IsAttachedToLogicalTree); + } + } + [Fact] public void ClientSize_Should_Be_Set_On_Construction() { diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 52d36a33fa..44ef7192ff 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -315,6 +315,50 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new[] { "NewChild1" }, ExtractItemHeader(target, 1)); } + [Fact] + public void Keyboard_Navigation_Should_Move_To_Last_Selected_Node() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var focus = FocusManager.Instance; + var navigation = AvaloniaLocator.Current.GetService(); + var data = CreateTestTreeData(); + + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = data, + DataTemplates = CreateNodeDataTemplate(), + }; + + var button = new Button(); + + var root = new TestRoot + { + Child = new StackPanel + { + Children = { target, button }, + } + }; + + ApplyTemplates(target); + + var item = data[0].Children[0]; + var node = target.ItemContainerGenerator.Index.ContainerFromItem(item); + Assert.NotNull(node); + + target.SelectedItem = item; + node.Focus(); + Assert.Same(node, focus.Current); + + navigation.Move(focus.Current, NavigationDirection.Next); + Assert.Same(button, focus.Current); + + navigation.Move(focus.Current, NavigationDirection.Next); + Assert.Same(node, focus.Current); + } + } + private void ApplyTemplates(TreeView tree) { tree.ApplyTemplate(); diff --git a/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj b/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj index d35542b51f..8dd8faf9db 100644 --- a/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj +++ b/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj @@ -1,6 +1,7 @@  net461;netcoreapp1.1 + Library diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs new file mode 100644 index 0000000000..a090dcd18d --- /dev/null +++ b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs @@ -0,0 +1,214 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Controls; +using Xunit; + +namespace Avalonia.Input.UnitTests +{ + public class KeyboardNavigationTests_Custom + { + [Fact] + public void Tab_Should_Custom_Navigate_Within_Children() + { + Button current; + Button next; + var target = new CustomNavigatingStackPanel + { + Children = + { + (current = new Button { Content = "Button 1" }), + new Button { Content = "Button 2" }, + (next = new Button { Content = "Button 3" }), + }, + NextControl = next, + }; + + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); + + Assert.Same(next, result); + } + + [Fact] + public void Right_Should_Custom_Navigate_Within_Children() + { + Button current; + Button next; + var target = new CustomNavigatingStackPanel + { + Children = + { + (current = new Button { Content = "Button 1" }), + new Button { Content = "Button 2" }, + (next = new Button { Content = "Button 3" }), + }, + NextControl = next, + }; + + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Right); + + Assert.Same(next, result); + } + + [Fact] + public void Tab_Should_Custom_Navigate_From_Outside() + { + Button current; + Button next; + var target = new CustomNavigatingStackPanel + { + Children = + { + new Button { Content = "Button 1" }, + new Button { Content = "Button 2" }, + (next = new Button { Content = "Button 3" }), + }, + NextControl = next, + }; + + var root = new StackPanel + { + Children = + { + (current = new Button { Content = "Outside" }), + target, + } + }; + + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); + + Assert.Same(next, result); + } + + [Fact] + public void Tab_Should_Custom_Navigate_From_Outside_When_Wrapping() + { + Button current; + Button next; + var target = new CustomNavigatingStackPanel + { + Children = + { + new Button { Content = "Button 1" }, + new Button { Content = "Button 2" }, + (next = new Button { Content = "Button 3" }), + }, + NextControl = next, + }; + + var root = new StackPanel + { + Children = + { + target, + (current = new Button { Content = "Outside" }), + } + }; + + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); + + Assert.Same(next, result); + } + + [Fact] + public void ShiftTab_Should_Custom_Navigate_From_Outside() + { + Button current; + Button next; + var target = new CustomNavigatingStackPanel + { + Children = + { + new Button { Content = "Button 1" }, + new Button { Content = "Button 2" }, + (next = new Button { Content = "Button 3" }), + }, + NextControl = next, + }; + + var root = new StackPanel + { + Children = + { + (current = new Button { Content = "Outside" }), + target, + } + }; + + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); + + Assert.Same(next, result); + } + + [Fact] + public void Right_Should_Custom_Navigate_From_Outside() + { + Button current; + Button next; + var target = new CustomNavigatingStackPanel + { + Children = + { + new Button { Content = "Button 1" }, + new Button { Content = "Button 2" }, + (next = new Button { Content = "Button 3" }), + }, + NextControl = next, + }; + + var root = new StackPanel + { + Children = + { + (current = new Button { Content = "Outside" }), + target, + }, + [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, + }; + + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Right); + + Assert.Same(next, result); + } + + [Fact] + public void Tab_Should_Navigate_Outside_When_Null_Returned_As_Next() + { + Button current; + Button next; + var target = new CustomNavigatingStackPanel + { + Children = + { + new Button { Content = "Button 1" }, + (current = new Button { Content = "Button 2" }), + new Button { Content = "Button 3" }, + }, + }; + + var root = new StackPanel + { + Children = + { + target, + (next = new Button { Content = "Outside" }), + } + }; + + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); + + Assert.Same(next, result); + } + + private class CustomNavigatingStackPanel : StackPanel, ICustomKeyboardNavigation + { + public bool CustomNavigates { get; set; } = true; + public IInputElement NextControl { get; set; } + + public (bool handled, IInputElement next) GetNext(IInputElement element, NavigationDirection direction) + { + return (CustomNavigates, NextControl); + } + } + } +} diff --git a/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj b/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj index 8f9607fe67..86c9cf0617 100644 --- a/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj +++ b/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj @@ -1,6 +1,7 @@  net461;netcoreapp1.1 + Library diff --git a/tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj b/tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj index af33c80352..0950856dca 100644 --- a/tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj +++ b/tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj @@ -1,6 +1,7 @@  net461;netcoreapp1.1 + Library diff --git a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs index f67c5a353f..361e7678be 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs @@ -2,25 +2,276 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using Avalonia.Controls; +using Avalonia.UnitTests; +using System; using Xunit; +using System.Collections.Generic; namespace Avalonia.Layout.UnitTests { public class LayoutManagerTests { [Fact] - public void Invalidating_Child_Should_Remeasure_Parent() + public void Measures_And_Arranges_InvalidateMeasured_Control() { - var layoutManager = new LayoutManager(); + var target = new LayoutManager(); - using (AvaloniaLocator.EnterScope()) + using (Start(target)) { - AvaloniaLocator.CurrentMutable.Bind().ToConstant(layoutManager); + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; + + target.ExecuteInitialLayoutPass(root); + control.Measured = control.Arranged = false; + + control.InvalidateMeasure(); + target.ExecuteLayoutPass(); + + Assert.True(control.Measured); + Assert.True(control.Arranged); + } + } + + [Fact] + public void Arranges_InvalidateArranged_Control() + { + var target = new LayoutManager(); + + using (Start(target)) + { + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; + + target.ExecuteInitialLayoutPass(root); + control.Measured = control.Arranged = false; + + control.InvalidateArrange(); + target.ExecuteLayoutPass(); + + Assert.False(control.Measured); + Assert.True(control.Arranged); + } + } + + [Fact] + public void Measures_Parent_Of_Newly_Added_Control() + { + var target = new LayoutManager(); + + using (Start(target)) + { + var control = new LayoutTestControl(); + var root = new LayoutTestRoot(); + + target.ExecuteInitialLayoutPass(root); + root.Child = control; + root.Measured = root.Arranged = false; + + target.ExecuteLayoutPass(); + + Assert.True(root.Measured); + Assert.True(root.Arranged); + Assert.True(control.Measured); + Assert.True(control.Arranged); + } + } + + [Fact] + public void Measures_In_Correct_Order() + { + var target = new LayoutManager(); + + using (Start(target)) + { + LayoutTestControl control1; + LayoutTestControl control2; + var root = new LayoutTestRoot + { + Child = control1 = new LayoutTestControl + { + Child = control2 = new LayoutTestControl(), + } + }; + + + var order = new List(); + Size MeasureOverride(ILayoutable control, Size size) + { + order.Add(control); + return new Size(10, 10); + } + + root.DoMeasureOverride = MeasureOverride; + control1.DoMeasureOverride = MeasureOverride; + control2.DoMeasureOverride = MeasureOverride; + target.ExecuteInitialLayoutPass(root); + + control2.InvalidateMeasure(); + control1.InvalidateMeasure(); + root.InvalidateMeasure(); + + order.Clear(); + target.ExecuteLayoutPass(); + + Assert.Equal(new ILayoutable[] { root, control1, control2 }, order); + } + } + + [Fact] + public void Measures_Root_And_Grandparent_In_Correct_Order() + { + var target = new LayoutManager(); + + using (Start(target)) + { + LayoutTestControl control1; + LayoutTestControl control2; + var root = new LayoutTestRoot + { + Child = control1 = new LayoutTestControl + { + Child = control2 = new LayoutTestControl(), + } + }; + + + var order = new List(); + Size MeasureOverride(ILayoutable control, Size size) + { + order.Add(control); + return new Size(10, 10); + } + + root.DoMeasureOverride = MeasureOverride; + control1.DoMeasureOverride = MeasureOverride; + control2.DoMeasureOverride = MeasureOverride; + target.ExecuteInitialLayoutPass(root); + + control2.InvalidateMeasure(); + root.InvalidateMeasure(); + + order.Clear(); + target.ExecuteLayoutPass(); + + Assert.Equal(new ILayoutable[] { root, control2 }, order); + } + } + + [Fact] + public void Doesnt_Measure_Non_Invalidated_Root() + { + var target = new LayoutManager(); + + using (Start(target)) + { + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; + + target.ExecuteInitialLayoutPass(root); + root.Measured = root.Arranged = false; + control.Measured = control.Arranged = false; + + control.InvalidateMeasure(); + target.ExecuteLayoutPass(); + + Assert.False(root.Measured); + Assert.False(root.Arranged); + Assert.True(control.Measured); + Assert.True(control.Arranged); + } + } + + [Fact] + public void Doesnt_Measure_Removed_Control() + { + var target = new LayoutManager(); + + using (Start(target)) + { + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; + + target.ExecuteInitialLayoutPass(root); + control.Measured = control.Arranged = false; + + control.InvalidateMeasure(); + root.Child = null; + target.ExecuteLayoutPass(); + + Assert.False(control.Measured); + Assert.False(control.Arranged); + } + } + + [Fact] + public void Measures_Root_With_Infinity() + { + var target = new LayoutManager(); + + using (Start(target)) + { + var root = new LayoutTestRoot(); + var availableSize = default(Size); + + // Should not measure with this size. + root.MaxClientSize = new Size(123, 456); + + root.DoMeasureOverride = (_, s) => + { + availableSize = s; + return new Size(100, 100); + }; + + target.ExecuteInitialLayoutPass(root); + + Assert.Equal(Size.Infinity, availableSize); + } + } + + [Fact] + public void Arranges_Root_With_DesiredSize() + { + var target = new LayoutManager(); + + using (Start(target)) + { + var root = new LayoutTestRoot + { + Width = 100, + Height = 100, + }; + + var arrangeSize = default(Size); + + root.DoArrangeOverride = (_, s) => + { + arrangeSize = s; + return s; + }; + + target.ExecuteInitialLayoutPass(root); + Assert.Equal(new Size(100, 100), arrangeSize); + + root.Width = 120; + + target.ExecuteLayoutPass(); + Assert.Equal(new Size(120, 100), arrangeSize); + } + } + + [Fact] + public void Invalidating_Child_Remeasures_Parent() + { + var target = new LayoutManager(); + + using (Start(target)) + { + AvaloniaLocator.CurrentMutable.Bind().ToConstant(target); Border border; StackPanel panel; - var root = new TestLayoutRoot + var root = new LayoutTestRoot { Child = panel = new StackPanel { @@ -31,15 +282,22 @@ namespace Avalonia.Layout.UnitTests } }; - layoutManager.ExecuteInitialLayoutPass(root); + target.ExecuteInitialLayoutPass(root); Assert.Equal(new Size(0, 0), root.DesiredSize); border.Width = 100; border.Height = 100; - layoutManager.ExecuteLayoutPass(); + target.ExecuteLayoutPass(); Assert.Equal(new Size(100, 100), panel.DesiredSize); } } + + private IDisposable Start(LayoutManager layoutManager) + { + var result = AvaloniaLocator.EnterScope(); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(layoutManager); + return result; + } } } diff --git a/tests/Avalonia.Layout.UnitTests/LayoutTestControl.cs b/tests/Avalonia.Layout.UnitTests/LayoutTestControl.cs new file mode 100644 index 0000000000..f7f072eb1e --- /dev/null +++ b/tests/Avalonia.Layout.UnitTests/LayoutTestControl.cs @@ -0,0 +1,29 @@ +using System; +using Avalonia.Controls; + +namespace Avalonia.Layout.UnitTests +{ + internal class LayoutTestControl : Decorator + { + public bool Measured { get; set; } + public bool Arranged { get; set; } + public Func DoMeasureOverride { get; set; } + public Func DoArrangeOverride { get; set; } + + protected override Size MeasureOverride(Size availableSize) + { + Measured = true; + return DoMeasureOverride != null ? + DoMeasureOverride(this, availableSize) : + base.MeasureOverride(availableSize); + } + + protected override Size ArrangeOverride(Size finalSize) + { + Arranged = true; + return DoArrangeOverride != null ? + DoArrangeOverride(this, finalSize) : + base.ArrangeOverride(finalSize); + } + } +} diff --git a/tests/Avalonia.Layout.UnitTests/LayoutTestRoot.cs b/tests/Avalonia.Layout.UnitTests/LayoutTestRoot.cs new file mode 100644 index 0000000000..07476844e0 --- /dev/null +++ b/tests/Avalonia.Layout.UnitTests/LayoutTestRoot.cs @@ -0,0 +1,43 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.UnitTests; + +namespace Avalonia.Layout.UnitTests +{ + internal class LayoutTestRoot : TestRoot, ILayoutable + { + public bool Measured { get; set; } + public bool Arranged { get; set; } + public Func DoMeasureOverride { get; set; } + public Func DoArrangeOverride { get; set; } + + void ILayoutable.Measure(Size availableSize) + { + Measured = true; + Measure(availableSize); + } + + void ILayoutable.Arrange(Rect rect) + { + Arranged = true; + Arrange(rect); + } + + protected override Size MeasureOverride(Size availableSize) + { + return DoMeasureOverride != null ? + DoMeasureOverride(this, availableSize) : + base.MeasureOverride(availableSize); + } + + protected override Size ArrangeOverride(Size finalSize) + { + Arranged = true; + return DoArrangeOverride != null ? + DoArrangeOverride(this, finalSize) : + base.ArrangeOverride(finalSize); + } + } +} diff --git a/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs new file mode 100644 index 0000000000..dcc65edc74 --- /dev/null +++ b/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs @@ -0,0 +1,90 @@ +using System; +using Avalonia.Controls; +using Moq; +using Xunit; + +namespace Avalonia.Layout.UnitTests +{ + public class LayoutableTests + { + [Fact] + public void Only_Calls_LayoutManager_InvalidateMeasure_Once() + { + var target = new Mock(); + + using (Start(target.Object)) + { + var control = new Decorator(); + var root = new LayoutTestRoot { Child = control }; + + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + target.ResetCalls(); + + control.InvalidateMeasure(); + control.InvalidateMeasure(); + + target.Verify(x => x.InvalidateMeasure(control), Times.Once()); + } + } + + [Fact] + public void Only_Calls_LayoutManager_InvalidateArrange_Once() + { + var target = new Mock(); + + using (Start(target.Object)) + { + var control = new Decorator(); + var root = new LayoutTestRoot { Child = control }; + + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + target.ResetCalls(); + + control.InvalidateArrange(); + control.InvalidateArrange(); + + target.Verify(x => x.InvalidateArrange(control), Times.Once()); + } + } + + [Fact] + public void Attaching_Control_To_Tree_Invalidates_Parent_Measure() + { + var target = new Mock(); + + using (Start(target.Object)) + { + var control = new Decorator(); + var root = new LayoutTestRoot { Child = control }; + + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + Assert.True(control.IsMeasureValid); + + root.Child = null; + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + + Assert.False(control.IsMeasureValid); + Assert.True(root.IsMeasureValid); + + target.ResetCalls(); + + root.Child = control; + + Assert.False(root.IsMeasureValid); + Assert.False(control.IsMeasureValid); + target.Verify(x => x.InvalidateMeasure(root), Times.Once()); + } + } + + private IDisposable Start(ILayoutManager layoutManager) + { + var result = AvaloniaLocator.EnterScope(); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(layoutManager); + return result; + } + } +} diff --git a/tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs b/tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs deleted file mode 100644 index fab1647c5d..0000000000 --- a/tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs +++ /dev/null @@ -1,24 +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 Avalonia.Controls; - -namespace Avalonia.Layout.UnitTests -{ - internal class TestLayoutRoot : Decorator, ILayoutRoot - { - public TestLayoutRoot() - { - ClientSize = new Size(500, 500); - } - - public Size ClientSize - { - get; - set; - } - - public Size MaxClientSize => Size.Infinity; - public double LayoutScaling => 1; - } -} diff --git a/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj b/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj index b7c4811495..3ccd3da044 100644 --- a/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj +++ b/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj @@ -1,6 +1,7 @@  net461;netcoreapp1.1 + Library diff --git a/tests/Avalonia.Markup.UnitTests/ControlLocatorTests.cs b/tests/Avalonia.Markup.UnitTests/ControlLocatorTests.cs index b9e1cb353a..a5414f1e8c 100644 --- a/tests/Avalonia.Markup.UnitTests/ControlLocatorTests.cs +++ b/tests/Avalonia.Markup.UnitTests/ControlLocatorTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Reactive.Linq; +using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.UnitTests; using Xunit; @@ -13,7 +14,7 @@ namespace Avalonia.Markup.UnitTests public class ControlLocatorTests { [Fact] - public async void Track_By_Name_Should_Find_Control_Added_Earlier() + public async Task Track_By_Name_Should_Find_Control_Added_Earlier() { TextBlock target; TextBlock relativeTo; diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs index 062402d465..f5c0c6ec15 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Globalization; using System.Reactive.Linq; using System.Threading; +using System.Threading.Tasks; using Avalonia.Data; using Avalonia.Markup.Data; using Avalonia.UnitTests; @@ -17,13 +18,15 @@ namespace Avalonia.Markup.UnitTests.Data public class BindingExpressionTests : IClassFixture { [Fact] - public async void Should_Get_Simple_Property_Value() + public async Task Should_Get_Simple_Property_Value() { var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(string)); var result = await target.Take(1); Assert.Equal("foo", result); + + GC.KeepAlive(data); } [Fact] @@ -35,6 +38,8 @@ namespace Avalonia.Markup.UnitTests.Data target.OnNext("bar"); Assert.Equal("bar", data.StringValue); + + GC.KeepAlive(data); } [Fact] @@ -46,102 +51,87 @@ namespace Avalonia.Markup.UnitTests.Data target.OnNext("bar"); Assert.Equal("bar", data.Foo[0]); + + GC.KeepAlive(data); } [Fact] - public async void Should_Convert_Get_String_To_Double() + public async Task Should_Convert_Get_String_To_Double() { -#if NET461 - Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; -#else - CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture; -#endif var data = new Class1 { StringValue = "5.6" }; var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double)); var result = await target.Take(1); Assert.Equal(5.6, result); + + GC.KeepAlive(data); } [Fact] - public async void Getting_Invalid_Double_String_Should_Return_BindingError() + public async Task Getting_Invalid_Double_String_Should_Return_BindingError() { var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double)); var result = await target.Take(1); Assert.IsType(result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Coerce_Get_Null_Double_String_To_UnsetValue() + public async Task Should_Coerce_Get_Null_Double_String_To_UnsetValue() { var data = new Class1 { StringValue = null }; var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double)); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); } [Fact] public void Should_Convert_Set_String_To_Double() { -#if NET461 - Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; -#else - CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture; -#endif - var data = new Class1 { StringValue = (5.6).ToString() }; var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double)); target.OnNext(6.7); Assert.Equal((6.7).ToString(), data.StringValue); + + GC.KeepAlive(data); } [Fact] - public async void Should_Convert_Get_Double_To_String() + public async Task Should_Convert_Get_Double_To_String() { -#if NET461 - Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; -#else - CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture; -#endif - var data = new Class1 { DoubleValue = 5.6 }; var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string)); var result = await target.Take(1); Assert.Equal((5.6).ToString(), result); + + GC.KeepAlive(data); } [Fact] public void Should_Convert_Set_Double_To_String() { -#if NET461 - Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; -#else - CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture; -#endif - var data = new Class1 { DoubleValue = 5.6 }; var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string)); target.OnNext("6.7"); Assert.Equal(6.7, data.DoubleValue); + + GC.KeepAlive(data); } [Fact] - public async void Should_Return_BindingNotification_With_FallbackValue_For_NonConvertibe_Target_Value() + public async Task Should_Return_BindingNotification_With_FallbackValue_For_NonConvertibe_Target_Value() { -#if NET461 - Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; -#else - CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture; -#endif - var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression( new ExpressionObserver(data, "StringValue"), @@ -156,17 +146,13 @@ namespace Avalonia.Markup.UnitTests.Data BindingErrorType.Error, 42), result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Return_BindingNotification_With_FallbackValue_For_NonConvertibe_Target_Value_With_Data_Validation() + public async Task Should_Return_BindingNotification_With_FallbackValue_For_NonConvertibe_Target_Value_With_Data_Validation() { -#if NET461 - Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; -#else - CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture; -#endif - var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression( new ExpressionObserver(data, "StringValue", true), @@ -181,17 +167,13 @@ namespace Avalonia.Markup.UnitTests.Data BindingErrorType.Error, 42), result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Return_BindingNotification_For_Invalid_FallbackValue() + public async Task Should_Return_BindingNotification_For_Invalid_FallbackValue() { -#if NET461 - Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; -#else - CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture; -#endif - var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression( new ExpressionObserver(data, "StringValue"), @@ -203,21 +185,17 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal( new BindingNotification( new AggregateException( - new InvalidCastException("Could not convert 'foo' to 'System.Int32'"), + new InvalidCastException("'foo' is not a valid number."), new InvalidCastException("Could not convert FallbackValue 'bar' to 'System.Int32'")), BindingErrorType.Error), result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Return_BindingNotification_For_Invalid_FallbackValue_With_Data_Validation() + public async Task Should_Return_BindingNotification_For_Invalid_FallbackValue_With_Data_Validation() { -#if NET461 - Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; -#else - CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture; -#endif - var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression( new ExpressionObserver(data, "StringValue", true), @@ -229,10 +207,12 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal( new BindingNotification( new AggregateException( - new InvalidCastException("Could not convert 'foo' to 'System.Int32'"), + new InvalidCastException("'foo' is not a valid number."), new InvalidCastException("Could not convert FallbackValue 'bar' to 'System.Int32'")), BindingErrorType.Error), result); + + GC.KeepAlive(data); } [Fact] @@ -244,6 +224,8 @@ namespace Avalonia.Markup.UnitTests.Data target.OnNext("foo"); Assert.Equal(5.6, data.DoubleValue); + + GC.KeepAlive(data); } [Fact] @@ -259,6 +241,8 @@ namespace Avalonia.Markup.UnitTests.Data target.OnNext("foo"); Assert.Equal(9.8, data.DoubleValue); + + GC.KeepAlive(data); } [Fact] @@ -270,6 +254,8 @@ namespace Avalonia.Markup.UnitTests.Data target.OnNext(null); Assert.Equal(0, data.DoubleValue); + + GC.KeepAlive(data); } [Fact] @@ -281,6 +267,8 @@ namespace Avalonia.Markup.UnitTests.Data target.OnNext(AvaloniaProperty.UnsetValue); Assert.Equal(0, data.DoubleValue); + + GC.KeepAlive(data); } [Fact] @@ -288,6 +276,7 @@ namespace Avalonia.Markup.UnitTests.Data { var data = new Class1 { DoubleValue = 5.6 }; var converter = new Mock(); + var target = new BindingExpression( new ExpressionObserver(data, "DoubleValue"), typeof(string), @@ -296,7 +285,9 @@ namespace Avalonia.Markup.UnitTests.Data target.Subscribe(_ => { }); - converter.Verify(x => x.Convert(5.6, typeof(string), "foo", CultureInfo.CurrentUICulture)); + converter.Verify(x => x.Convert(5.6, typeof(string), "foo", CultureInfo.CurrentCulture)); + + GC.KeepAlive(data); } [Fact] @@ -312,7 +303,9 @@ namespace Avalonia.Markup.UnitTests.Data target.OnNext("bar"); - converter.Verify(x => x.ConvertBack("bar", typeof(double), "foo", CultureInfo.CurrentUICulture)); + converter.Verify(x => x.ConvertBack("bar", typeof(double), "foo", CultureInfo.CurrentCulture)); + + GC.KeepAlive(data); } [Fact] @@ -339,6 +332,8 @@ namespace Avalonia.Markup.UnitTests.Data BindingErrorType.Error) }, result); + + GC.KeepAlive(data); } private class Class1 : NotifyingBase diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_AttachedProperty.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_AttachedProperty.cs index 349a09da2c..a8069cb75c 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_AttachedProperty.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_AttachedProperty.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Reactive.Linq; +using System.Threading.Tasks; using Avalonia.Diagnostics; using Avalonia.Markup.Data; using Xunit; @@ -18,7 +19,7 @@ namespace Avalonia.Markup.UnitTests.Data } [Fact] - public async void Should_Get_Attached_Property_Value() + public async Task Should_Get_Attached_Property_Value() { var data = new Class1(); var target = new ExpressionObserver(data, "(Owner.Foo)"); @@ -30,7 +31,7 @@ namespace Avalonia.Markup.UnitTests.Data } [Fact] - public async void Should_Get_Chained_Attached_Property_Value() + public async Task Should_Get_Chained_Attached_Property_Value() { var data = new Class1 { diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_AvaloniaProperty.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_AvaloniaProperty.cs index ece9437308..cd691daaf9 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_AvaloniaProperty.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_AvaloniaProperty.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Reactive.Linq; +using System.Threading.Tasks; using Avalonia.Diagnostics; using Avalonia.Markup.Data; using Xunit; @@ -18,7 +19,7 @@ namespace Avalonia.Markup.UnitTests.Data } [Fact] - public async void Should_Get_Simple_Property_Value() + public async Task Should_Get_Simple_Property_Value() { var data = new Class1(); var target = new ExpressionObserver(data, "Foo"); diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs index 3b5ca26db1..125bd84f3d 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs @@ -28,6 +28,8 @@ namespace Avalonia.Markup.UnitTests.Data observer.SetValue(-5); Assert.False(validationMessageFound); + + GC.KeepAlive(data); } [Fact] @@ -43,6 +45,8 @@ namespace Avalonia.Markup.UnitTests.Data observer.SetValue(-5); Assert.True(validationMessageFound); + + GC.KeepAlive(data); } [Fact] @@ -102,6 +106,8 @@ namespace Avalonia.Markup.UnitTests.Data new BindingNotification(new Exception("Must be positive"), BindingErrorType.DataValidationError, 5), new BindingNotification(5), }, result); + + GC.KeepAlive(data); } [Fact] @@ -147,6 +153,9 @@ namespace Avalonia.Markup.UnitTests.Data BindingErrorType.Error, AvaloniaProperty.UnsetValue), }, result); + + GC.KeepAlive(container); + GC.KeepAlive(inner); } public class ExceptionTest : NotifyingBase diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs index 9cc843381c..a68213baee 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Reactive.Linq; +using System.Threading.Tasks; using Avalonia.Collections; using Avalonia.Diagnostics; using Avalonia.Markup.Data; @@ -16,113 +17,135 @@ namespace Avalonia.Markup.UnitTests.Data public class ExpressionObserverTests_Indexer { [Fact] - public async void Should_Get_Array_Value() + public async Task Should_Get_Array_Value() { var data = new { Foo = new [] { "foo", "bar" } }; var target = new ExpressionObserver(data, "Foo[1]"); var result = await target.Take(1); Assert.Equal("bar", result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Get_UnsetValue_For_Invalid_Array_Index() + public async Task Should_Get_UnsetValue_For_Invalid_Array_Index() { var data = new { Foo = new[] { "foo", "bar" } }; var target = new ExpressionObserver(data, "Foo[invalid]"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Get_UnsetValue_For_Invalid_Dictionary_Index() + public async Task Should_Get_UnsetValue_For_Invalid_Dictionary_Index() { var data = new { Foo = new Dictionary { { 1, "foo" } } }; var target = new ExpressionObserver(data, "Foo[invalid]"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Get_UnsetValue_For_Object_Without_Indexer() + public async Task Should_Get_UnsetValue_For_Object_Without_Indexer() { var data = new { Foo = 5 }; var target = new ExpressionObserver(data, "Foo[noindexer]"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Get_MultiDimensional_Array_Value() + public async Task Should_Get_MultiDimensional_Array_Value() { var data = new { Foo = new[,] { { "foo", "bar" }, { "baz", "qux" } } }; var target = new ExpressionObserver(data, "Foo[1, 1]"); var result = await target.Take(1); Assert.Equal("qux", result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Get_Value_For_String_Indexer() + public async Task Should_Get_Value_For_String_Indexer() { var data = new { Foo = new Dictionary { { "foo", "bar" }, { "baz", "qux" } } }; var target = new ExpressionObserver(data, "Foo[foo]"); var result = await target.Take(1); Assert.Equal("bar", result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Get_Value_For_Non_String_Indexer() + public async Task Should_Get_Value_For_Non_String_Indexer() { var data = new { Foo = new Dictionary { { 1.0, "bar" }, { 2.0, "qux" } } }; var target = new ExpressionObserver(data, "Foo[1.0]"); var result = await target.Take(1); Assert.Equal("bar", result); + + GC.KeepAlive(data); } [Fact] - public async void Array_Out_Of_Bounds_Should_Return_UnsetValue() + public async Task Array_Out_Of_Bounds_Should_Return_UnsetValue() { var data = new { Foo = new[] { "foo", "bar" } }; var target = new ExpressionObserver(data, "Foo[2]"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); } [Fact] - public async void Array_With_Wrong_Dimensions_Should_Return_UnsetValue() + public async Task Array_With_Wrong_Dimensions_Should_Return_UnsetValue() { var data = new { Foo = new[] { "foo", "bar" } }; var target = new ExpressionObserver(data, "Foo[1,2]"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); } [Fact] - public async void List_Out_Of_Bounds_Should_Return_UnsetValue() + public async Task List_Out_Of_Bounds_Should_Return_UnsetValue() { var data = new { Foo = new List { "foo", "bar" } }; var target = new ExpressionObserver(data, "Foo[2]"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Get_List_Value() + public async Task Should_Get_List_Value() { var data = new { Foo = new List { "foo", "bar" } }; var target = new ExpressionObserver(data, "Foo[1]"); var result = await target.Take(1); Assert.Equal("bar", result); + + GC.KeepAlive(data); } [Fact] @@ -139,6 +162,8 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "baz" }, result); Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); + + GC.KeepAlive(data); } [Fact] @@ -155,6 +180,8 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(new[] { "foo", "bar" }, result); Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); + + GC.KeepAlive(data); } [Fact] @@ -171,6 +198,8 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(new[] { "bar", "baz" }, result); Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); + + GC.KeepAlive(data); } [Fact] @@ -187,6 +216,9 @@ namespace Avalonia.Markup.UnitTests.Data data.Foo.Move(0, 1); Assert.Equal(new[] { "bar", "foo" }, result); + + GC.KeepAlive(sub); + GC.KeepAlive(data); } [Fact] @@ -200,6 +232,9 @@ namespace Avalonia.Markup.UnitTests.Data data.Foo.Clear(); Assert.Equal(new[] { "bar", AvaloniaProperty.UnsetValue }, result); + + GC.KeepAlive(sub); + GC.KeepAlive(data); } [Fact] @@ -220,6 +255,8 @@ namespace Avalonia.Markup.UnitTests.Data var expected = new[] { "bar", "bar2" }; Assert.Equal(expected, result); Assert.Equal(0, data.Foo.PropertyChangedSubscriptionCount); + + GC.KeepAlive(data); } [Fact] @@ -234,6 +271,8 @@ namespace Avalonia.Markup.UnitTests.Data } Assert.Equal("baz", data.Foo[1]); + + GC.KeepAlive(data); } [Fact] @@ -254,6 +293,8 @@ namespace Avalonia.Markup.UnitTests.Data } Assert.Equal(4, data.Foo["foo"]); + + GC.KeepAlive(data); } [Fact] @@ -274,6 +315,8 @@ namespace Avalonia.Markup.UnitTests.Data } Assert.Equal(4, data.Foo["bar"]); + + GC.KeepAlive(data); } [Fact] @@ -291,6 +334,8 @@ namespace Avalonia.Markup.UnitTests.Data } Assert.Equal("bar2", data.Foo["foo"]); + + GC.KeepAlive(data); } private class NonIntegerIndexer : NotifyingBase diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs index 2a2bf06bf1..04a8e30d16 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs @@ -90,7 +90,8 @@ namespace Avalonia.Markup.UnitTests.Data { var scheduler = new TestScheduler(); var update = scheduler.CreateColdObservable(); - var target = new ExpressionObserver(() => new { Foo = "foo" }, "Foo", update); + var data = new { Foo = "foo" }; + var target = new ExpressionObserver(() => data, "Foo", update); var result = new List(); using (target.Subscribe(x => result.Add(x))) @@ -101,6 +102,8 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(new[] { "foo" }, result); Assert.All(update.Subscriptions, x => Assert.NotEqual(Subscription.Infinite, x.Unsubscribe)); + + GC.KeepAlive(data); } private Recorded> OnNext(long time, object value) diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs index 6bee0d10f4..d8dc2de847 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs @@ -3,6 +3,7 @@ using System; using System.Reactive.Linq; +using System.Threading.Tasks; using Avalonia.Data; using Avalonia.Markup.Data; using Xunit; @@ -12,57 +13,67 @@ namespace Avalonia.Markup.UnitTests.Data public class ExpressionObserverTests_Negation { [Fact] - public async void Should_Negate_Boolean_Value() + public async Task Should_Negate_Boolean_Value() { var data = new { Foo = true }; var target = new ExpressionObserver(data, "!Foo"); var result = await target.Take(1); Assert.Equal(false, result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Negate_0() + public async Task Should_Negate_0() { var data = new { Foo = 0 }; var target = new ExpressionObserver(data, "!Foo"); var result = await target.Take(1); Assert.Equal(true, result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Negate_1() + public async Task Should_Negate_1() { var data = new { Foo = 1 }; var target = new ExpressionObserver(data, "!Foo"); var result = await target.Take(1); Assert.Equal(false, result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Negate_False_String() + public async Task Should_Negate_False_String() { var data = new { Foo = "false" }; var target = new ExpressionObserver(data, "!Foo"); var result = await target.Take(1); Assert.Equal(true, result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Negate_True_String() + public async Task Should_Negate_True_String() { var data = new { Foo = "True" }; var target = new ExpressionObserver(data, "!Foo"); var result = await target.Take(1); Assert.Equal(false, result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Return_BindingNotification_For_String_Not_Convertible_To_Boolean() + public async Task Should_Return_BindingNotification_For_String_Not_Convertible_To_Boolean() { var data = new { Foo = "foo" }; var target = new ExpressionObserver(data, "!Foo"); @@ -73,10 +84,12 @@ namespace Avalonia.Markup.UnitTests.Data new InvalidCastException($"Unable to convert 'foo' to bool."), BindingErrorType.Error), result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Return_BindingNotification_For_Value_Not_Convertible_To_Boolean() + public async Task Should_Return_BindingNotification_For_Value_Not_Convertible_To_Boolean() { var data = new { Foo = new object() }; var target = new ExpressionObserver(data, "!Foo"); @@ -87,6 +100,8 @@ namespace Avalonia.Markup.UnitTests.Data new InvalidCastException($"Unable to convert 'System.Object' to bool."), BindingErrorType.Error), result); + + GC.KeepAlive(data); } [Fact] @@ -96,6 +111,8 @@ namespace Avalonia.Markup.UnitTests.Data var target = new ExpressionObserver(data, "!Foo"); Assert.False(target.SetValue("bar")); + + GC.KeepAlive(data); } } } diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs index 640d82fa19..62d5c28f49 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs @@ -29,6 +29,8 @@ namespace Avalonia.Markup.UnitTests.Data sync.ExecutePostedCallbacks(); Assert.Equal(new[] { source }, result); + + GC.KeepAlive(data); } } @@ -47,6 +49,8 @@ namespace Avalonia.Markup.UnitTests.Data sync.ExecutePostedCallbacks(); Assert.Equal(new[] { "foo", "bar" }, result); + + GC.KeepAlive(data); } } @@ -67,6 +71,8 @@ namespace Avalonia.Markup.UnitTests.Data sub.Dispose(); Assert.Equal(0, data.PropertyChangedSubscriptionCount); + + GC.KeepAlive(data); } } @@ -87,6 +93,8 @@ namespace Avalonia.Markup.UnitTests.Data // What does it mean to have data validation on an observable? Without a use-case // it's hard to know what to do here so for the moment the value is returned. Assert.Equal(new[] { "foo", "bar" }, result); + + GC.KeepAlive(data); } } @@ -107,6 +115,8 @@ namespace Avalonia.Markup.UnitTests.Data sub.Dispose(); Assert.Equal(0, data.PropertyChangedSubscriptionCount); + + GC.KeepAlive(data); } } @@ -132,6 +142,8 @@ namespace Avalonia.Markup.UnitTests.Data result); sub.Dispose(); + + GC.KeepAlive(data); } } diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs index bdcd39d997..4cb2061c9e 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs @@ -11,19 +11,22 @@ using Avalonia.Data; using Avalonia.Markup.Data; using Avalonia.UnitTests; using Xunit; +using System.Threading.Tasks; namespace Avalonia.Markup.UnitTests.Data { public class ExpressionObserverTests_Property { [Fact] - public async void Should_Get_Simple_Property_Value() + public async Task Should_Get_Simple_Property_Value() { var data = new { Foo = "foo" }; var target = new ExpressionObserver(data, "Foo"); var result = await target.Take(1); Assert.Equal("foo", result); + + GC.KeepAlive(data); } [Fact] @@ -35,76 +38,92 @@ namespace Avalonia.Markup.UnitTests.Data target.Subscribe(_ => { }); Assert.Equal(typeof(string), target.ResultType); + + GC.KeepAlive(data); } [Fact] - public async void Should_Get_Simple_Property_Value_Null() + public async Task Should_Get_Simple_Property_Value_Null() { var data = new { Foo = (string)null }; var target = new ExpressionObserver(data, "Foo"); var result = await target.Take(1); Assert.Null(result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Get_Simple_Property_From_Base_Class() + public async Task Should_Get_Simple_Property_From_Base_Class() { var data = new Class3 { Foo = "foo" }; var target = new ExpressionObserver(data, "Foo"); var result = await target.Take(1); Assert.Equal("foo", result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Return_UnsetValue_For_Root_Null() + public async Task Should_Return_UnsetValue_For_Root_Null() { var data = new Class3 { Foo = "foo" }; var target = new ExpressionObserver(default(object), "Foo"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Return_UnsetValue_For_Root_UnsetValue() + public async Task Should_Return_UnsetValue_For_Root_UnsetValue() { var data = new Class3 { Foo = "foo" }; var target = new ExpressionObserver(AvaloniaProperty.UnsetValue, "Foo"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Return_UnsetValue_For_Observable_Root_Null() + public async Task Should_Return_UnsetValue_For_Observable_Root_Null() { var data = new Class3 { Foo = "foo" }; var target = new ExpressionObserver(Observable.Return(default(object)), "Foo"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Return_UnsetValue_For_Observable_Root_UnsetValue() + public async Task Should_Return_UnsetValue_For_Observable_Root_UnsetValue() { var data = new Class3 { Foo = "foo" }; var target = new ExpressionObserver(Observable.Return(AvaloniaProperty.UnsetValue), "Foo"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Get_Simple_Property_Chain() + public async Task Should_Get_Simple_Property_Chain() { var data = new { Foo = new { Bar = new { Baz = "baz" } } }; var target = new ExpressionObserver(data, "Foo.Bar.Baz"); var result = await target.Take(1); Assert.Equal("baz", result); + + GC.KeepAlive(data); } [Fact] @@ -116,10 +135,12 @@ namespace Avalonia.Markup.UnitTests.Data target.Subscribe(_ => { }); Assert.Equal(typeof(string), target.ResultType); + + GC.KeepAlive(data); } [Fact] - public async void Should_Return_BindingNotification_Error_For_Broken_Chain() + public async Task Should_Return_BindingNotification_Error_For_Broken_Chain() { var data = new { Foo = new { Bar = 1 } }; var target = new ExpressionObserver(data, "Foo.Bar.Baz"); @@ -131,6 +152,8 @@ namespace Avalonia.Markup.UnitTests.Data new BindingNotification( new MissingMemberException("Could not find CLR property 'Baz' on '1'"), BindingErrorType.Error), result); + + GC.KeepAlive(data); } [Fact] @@ -151,6 +174,8 @@ namespace Avalonia.Markup.UnitTests.Data AvaloniaProperty.UnsetValue), }, result); + + GC.KeepAlive(data); } [Fact] @@ -160,6 +185,8 @@ namespace Avalonia.Markup.UnitTests.Data var target = new ExpressionObserver(data, "Foo.Bar.Baz"); Assert.Null(target.ResultType); + + GC.KeepAlive(data); } [Fact] @@ -177,6 +204,8 @@ namespace Avalonia.Markup.UnitTests.Data sub.Dispose(); Assert.Equal(0, data.PropertyChangedSubscriptionCount); + + GC.KeepAlive(data); } [Fact] @@ -205,6 +234,8 @@ namespace Avalonia.Markup.UnitTests.Data sub.Dispose(); Assert.Equal(0, data.PropertyChangedSubscriptionCount); + + GC.KeepAlive(data); } [Fact] @@ -224,6 +255,8 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(0, data.PropertyChangedSubscriptionCount); Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount); + + GC.KeepAlive(data); } [Fact] @@ -245,6 +278,8 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(0, data.PropertyChangedSubscriptionCount); Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount); Assert.Equal(0, old.PropertyChangedSubscriptionCount); + + GC.KeepAlive(data); } [Fact] @@ -286,6 +321,8 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(0, data.PropertyChangedSubscriptionCount); Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount); Assert.Equal(0, old.PropertyChangedSubscriptionCount); + + GC.KeepAlive(data); } [Fact] @@ -318,6 +355,8 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount); Assert.Equal(0, breaking.PropertyChangedSubscriptionCount); Assert.Equal(0, old.PropertyChangedSubscriptionCount); + + GC.KeepAlive(data); } [Fact] @@ -334,6 +373,8 @@ namespace Avalonia.Markup.UnitTests.Data update.OnNext(Unit.Default); Assert.Equal(new[] { "foo", "bar" }, result); + + GC.KeepAlive(data); } [Fact] @@ -374,6 +415,8 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(new[] { "foo", "bar" }, result1); Assert.Equal(new[] { "foo", "bar" }, result2); Assert.Equal(new[] { "bar" }, result3); + + GC.KeepAlive(data); } [Fact] @@ -391,6 +434,8 @@ namespace Avalonia.Markup.UnitTests.Data sub2.Dispose(); Assert.Equal(0, data.PropertyChangedSubscriptionCount); + + GC.KeepAlive(data); } [Fact] @@ -405,6 +450,8 @@ namespace Avalonia.Markup.UnitTests.Data } Assert.Equal("bar", data.Foo); + + GC.KeepAlive(data); } [Fact] @@ -419,6 +466,8 @@ namespace Avalonia.Markup.UnitTests.Data } Assert.Equal("baz", ((Class2)data.Next).Bar); + + GC.KeepAlive(data); } [Fact] @@ -431,6 +480,8 @@ namespace Avalonia.Markup.UnitTests.Data { Assert.False(target.SetValue("baz")); } + + GC.KeepAlive(data); } [Fact] @@ -444,6 +495,8 @@ namespace Avalonia.Markup.UnitTests.Data target.SetValue("bar"); Assert.Equal(new[] { null, "bar" }, result); + + GC.KeepAlive(data); } [Fact] @@ -457,6 +510,8 @@ namespace Avalonia.Markup.UnitTests.Data target.SetValue("bar"); Assert.Equal(new[] { null, "bar" }, result); + + GC.KeepAlive(data); } [Fact] @@ -469,6 +524,8 @@ namespace Avalonia.Markup.UnitTests.Data { Assert.False(target.SetValue("baz")); } + + GC.KeepAlive(data); } [Fact] @@ -498,6 +555,9 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(0, first.PropertyChangedSubscriptionCount); Assert.Equal(0, second.PropertyChangedSubscriptionCount); + + GC.KeepAlive(first); + GC.KeepAlive(second); } [Fact] diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs index 61e6dcb833..c251f4398a 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs @@ -30,6 +30,8 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(1, result.Count); Assert.IsType>(result[0]); + + GC.KeepAlive(data); } } @@ -45,6 +47,8 @@ namespace Avalonia.Markup.UnitTests.Data var sub = target.Subscribe(x => result.Add(x)); Assert.Equal(new[] { "foo" }, result); + + GC.KeepAlive(data); } } @@ -63,6 +67,8 @@ namespace Avalonia.Markup.UnitTests.Data sync.ExecutePostedCallbacks(); Assert.Equal(new[] { "foo" }, result); + + GC.KeepAlive(data); } } @@ -88,6 +94,8 @@ namespace Avalonia.Markup.UnitTests.Data BindingErrorType.Error) }, result); + + GC.KeepAlive(data); } } @@ -110,6 +118,8 @@ namespace Avalonia.Markup.UnitTests.Data BindingErrorType.Error) }, result); + + GC.KeepAlive(data); } } @@ -130,6 +140,8 @@ namespace Avalonia.Markup.UnitTests.Data // What does it mean to have data validation on a Task? Without a use-case it's // hard to know what to do here so for the moment the value is returned. Assert.Equal(new [] { "foo" }, result); + + GC.KeepAlive(data); } } diff --git a/tests/Avalonia.Markup.UnitTests/Data/Plugins/ExceptionValidationPluginTests.cs b/tests/Avalonia.Markup.UnitTests/Data/Plugins/ExceptionValidationPluginTests.cs index 4a34791008..eb529a3b13 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/Plugins/ExceptionValidationPluginTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/Plugins/ExceptionValidationPluginTests.cs @@ -35,6 +35,8 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins new BindingNotification(new ArgumentOutOfRangeException("value"), BindingErrorType.DataValidationError), new BindingNotification(6), }, result); + + GC.KeepAlive(data); } public class Data : NotifyingBase diff --git a/tests/Avalonia.Markup.UnitTests/Properties/AssemblyInfo.cs b/tests/Avalonia.Markup.UnitTests/Properties/AssemblyInfo.cs index d1567d46be..4c3825ed44 100644 --- a/tests/Avalonia.Markup.UnitTests/Properties/AssemblyInfo.cs +++ b/tests/Avalonia.Markup.UnitTests/Properties/AssemblyInfo.cs @@ -37,4 +37,4 @@ using Xunit; [assembly: AssemblyFileVersion("1.0.0.0")] // Don't run tests in parallel. -[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file +[assembly: CollectionBehavior(MaxParallelThreads = 1)] diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj index 0cbdc142eb..f6f8f6bcb0 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj @@ -1,6 +1,7 @@  net461;netcoreapp1.1 + Library diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/MultiBindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/MultiBindingTests.cs index 5d67151992..874dc18552 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/MultiBindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/MultiBindingTests.cs @@ -10,13 +10,14 @@ using Moq; using Avalonia.Controls; using Avalonia.Markup.Xaml.Data; using Xunit; +using System.Threading.Tasks; namespace Avalonia.Markup.Xaml.UnitTests.Data { public class MultiBindingTests { [Fact] - public async void OneWay_Binding_Should_Be_Set_Up() + public async Task OneWay_Binding_Should_Be_Set_Up() { var source = new { A = 1, B = 2, C = 3 }; var binding = new MultiBinding diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Properties/AssemblyInfo.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Properties/AssemblyInfo.cs index 034e9f74ce..24cc853318 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Properties/AssemblyInfo.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Properties/AssemblyInfo.cs @@ -7,4 +7,4 @@ using Xunit; [assembly: AssemblyTitle("Avalonia.Markup.Xaml.UnitTests")] // Don't run tests in parallel. -[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file +[assembly: CollectionBehavior(MaxParallelThreads = 1)] diff --git a/tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj b/tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj index d35542b51f..8dd8faf9db 100644 --- a/tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj +++ b/tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj @@ -1,6 +1,7 @@  net461;netcoreapp1.1 + Library diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs index d97cc74c95..50b4828e73 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive; using System.Reactive.Linq; +using System.Threading.Tasks; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Data; @@ -45,7 +46,7 @@ namespace Avalonia.Styling.UnitTests } [Fact] - public async void Child_Matches_Control_When_It_Is_Child_OfType_And_Class() + public async Task Child_Matches_Control_When_It_Is_Child_OfType_And_Class() { var parent = new TestLogical1(); var child = new TestLogical2(); @@ -144,6 +145,11 @@ namespace Avalonia.Styling.UnitTests throw new NotImplementedException(); } + public void NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + throw new NotImplementedException(); + } + public void NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { throw new NotImplementedException(); diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs index b4c284e7c9..7cf8c3dd1c 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs @@ -175,6 +175,11 @@ namespace Avalonia.Styling.UnitTests throw new NotImplementedException(); } + public void NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + throw new NotImplementedException(); + } + public void NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { throw new NotImplementedException(); diff --git a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj index 628ccb2a1f..938fca8b4a 100644 --- a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj +++ b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj @@ -2,6 +2,7 @@ net461;netcoreapp1.1 false + Library true @@ -51,8 +52,5 @@ - - - \ No newline at end of file diff --git a/tests/Avalonia.UnitTests/InvariantCultureFixture.cs b/tests/Avalonia.UnitTests/InvariantCultureFixture.cs index f27df8f599..b00b16e2bb 100644 --- a/tests/Avalonia.UnitTests/InvariantCultureFixture.cs +++ b/tests/Avalonia.UnitTests/InvariantCultureFixture.cs @@ -20,13 +20,22 @@ namespace Avalonia.UnitTests public InvariantCultureFixture() { - _restore = CultureInfo.CurrentUICulture; - CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; +#if NET461 + _restore = Thread.CurrentThread.CurrentCulture; + Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; +#else + _restore = CultureInfo.CurrentCulture; + CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; +#endif } public void Dispose() { - CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture = _restore; +#if NET461 + Thread.CurrentThread.CurrentCulture = _restore; +#else + CultureInfo.CurrentCulture = _restore; +#endif } } } diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index 8a711c415e..399870aef9 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -43,7 +43,7 @@ namespace Avalonia.UnitTests public Size ClientSize => new Size(100, 100); - public Size MaxClientSize => Size.Infinity; + public Size MaxClientSize { get; set; } = Size.Infinity; public double LayoutScaling => 1; diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index 0cd8d4295b..f66adec1eb 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -50,6 +50,7 @@ namespace Avalonia.UnitTests public static readonly TestServices RealFocus = new TestServices( focusManager: new FocusManager(), keyboardDevice: () => new KeyboardDevice(), + keyboardNavigation: new KeyboardNavigationHandler(), inputManager: new InputManager()); public static readonly TestServices RealLayoutManager = new TestServices( @@ -63,6 +64,7 @@ namespace Avalonia.UnitTests IFocusManager focusManager = null, IInputManager inputManager = null, Func keyboardDevice = null, + IKeyboardNavigationHandler keyboardNavigation = null, ILayoutManager layoutManager = null, IRuntimePlatform platform = null, Func renderer = null, @@ -79,6 +81,7 @@ namespace Avalonia.UnitTests FocusManager = focusManager; InputManager = inputManager; KeyboardDevice = keyboardDevice; + KeyboardNavigation = keyboardNavigation; LayoutManager = layoutManager; Platform = platform; Renderer = renderer; @@ -96,6 +99,7 @@ namespace Avalonia.UnitTests public IInputManager InputManager { get; } public IFocusManager FocusManager { get; } public Func KeyboardDevice { get; } + public IKeyboardNavigationHandler KeyboardNavigation { get; } public ILayoutManager LayoutManager { get; } public IRuntimePlatform Platform { get; } public Func Renderer { get; } @@ -113,6 +117,7 @@ namespace Avalonia.UnitTests IFocusManager focusManager = null, IInputManager inputManager = null, Func keyboardDevice = null, + IKeyboardNavigationHandler keyboardNavigation = null, ILayoutManager layoutManager = null, IRuntimePlatform platform = null, Func renderer = null, @@ -131,6 +136,7 @@ namespace Avalonia.UnitTests focusManager: focusManager ?? FocusManager, inputManager: inputManager ?? InputManager, keyboardDevice: keyboardDevice ?? KeyboardDevice, + keyboardNavigation: keyboardNavigation ?? KeyboardNavigation, layoutManager: layoutManager ?? LayoutManager, platform: platform ?? Platform, renderer: renderer ?? Renderer, diff --git a/tests/Avalonia.UnitTests/UnitTestApplication.cs b/tests/Avalonia.UnitTests/UnitTestApplication.cs index c5d533486b..28577e9670 100644 --- a/tests/Avalonia.UnitTests/UnitTestApplication.cs +++ b/tests/Avalonia.UnitTests/UnitTestApplication.cs @@ -49,6 +49,7 @@ namespace Avalonia.UnitTests .BindToSelf(this) .Bind().ToConstant(Services.InputManager) .Bind().ToConstant(Services.KeyboardDevice?.Invoke()) + .Bind().ToConstant(Services.KeyboardNavigation) .Bind().ToConstant(Services.LayoutManager) .Bind().ToConstant(Services.Platform) .Bind().ToConstant(new RendererFactory(Services.Renderer)) diff --git a/tools/packages.config b/tools/packages.config new file mode 100644 index 0000000000..5657d953fc --- /dev/null +++ b/tools/packages.config @@ -0,0 +1,4 @@ + + + +