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/Direct3DInteropSample/Direct3DInteropSample.csproj b/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj index 54a816b0a9..d90a251173 100644 --- a/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj +++ b/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj @@ -22,6 +22,7 @@ + diff --git a/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj b/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj index 28e5e274d0..ac7d25a91e 100644 --- a/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj +++ b/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj @@ -181,5 +181,6 @@ + \ No newline at end of file diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index c779031e6c..e9b4ad0a6d 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -51,7 +51,6 @@ namespace Avalonia.Android .Bind().ToTransient() .Bind().ToTransient() .Bind().ToSingleton() - .Bind().ToSingleton() .Bind().ToConstant(Instance) .Bind().ToConstant(ImmediateRenderer.Factory) .Bind().ToConstant(new AndroidThreadingInterface()) diff --git a/src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs b/src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs index 9ae79efb48..d52eeb15e4 100644 --- a/src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs +++ b/src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs @@ -4,6 +4,8 @@ namespace Avalonia.Android.Platform.Input { public class AndroidMouseDevice : MouseDevice { + public static AndroidMouseDevice Instance { get; } = new AndroidMouseDevice(); + public AndroidMouseDevice() { diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs index 64dbeb89cc..051a058363 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs @@ -44,7 +44,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform public int Width { get; } public int Height { get; } public int RowBytes { get; } - public Size Dpi { get; } = new Size(96, 96); + public Vector Dpi { get; } = new Vector(96, 96); public PixelFormat Format { get; } [DllImport("android")] diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 75772be171..0c62eb9060 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -10,6 +10,7 @@ using Avalonia.Platform; using System; using System.Collections.Generic; using System.Reactive.Disposables; +using Avalonia.Android.Platform.Input; using Avalonia.Controls; using Avalonia.Controls.Platform.Surfaces; @@ -65,6 +66,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform } } + public IMouseDevice MouseDevice => AndroidMouseDevice.Instance; + public Action Closed { get; set; } public Action Input { get; set; } diff --git a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs index 702829b91c..0f90472bd0 100644 --- a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs +++ b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs @@ -71,7 +71,7 @@ namespace Avalonia.Android.Platform.Specific.Helpers if (x <= _point.X && r >= _point.X && y <= _point.Y && b >= _point.Y) { var inputRoot = _getInputRoot(); - var mouseDevice = MouseDevice.Instance; + var mouseDevice = Avalonia.Android.Platform.Input.AndroidMouseDevice.Instance; //in order the controls to work in a predictable way //we need to generate mouse move before first mouse down event diff --git a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs index ffb7199619..54cd132b95 100644 --- a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs +++ b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; @@ -59,7 +60,8 @@ namespace Avalonia.Collections /// the index in the collection and the item. /// /// - /// An action called when the collection is reset. + /// An action called when the collection is reset. This will be followed by calls to + /// for each item present in the collection after the reset. /// /// A disposable used to terminate the subscription. public static IDisposable ForEachItem( @@ -68,112 +70,38 @@ namespace Avalonia.Collections Action removed, Action reset) { - int index; - - NotifyCollectionChangedEventHandler handler = (_, e) => + void Add(int index, IList items) { - switch (e.Action) + foreach (T item in items) { - case NotifyCollectionChangedAction.Add: - index = e.NewStartingIndex; - - foreach (T item in e.NewItems) - { - added(index++, item); - } - - break; - - case NotifyCollectionChangedAction.Replace: - index = e.OldStartingIndex; - - foreach (T item in e.OldItems) - { - removed(index++, item); - } - - index = e.NewStartingIndex; - - foreach (T item in e.NewItems) - { - added(index++, item); - } - - break; - - case NotifyCollectionChangedAction.Remove: - index = e.OldStartingIndex; - - foreach (T item in e.OldItems) - { - removed(index++, item); - } - - break; - - case NotifyCollectionChangedAction.Reset: - if (reset == null) - { - throw new InvalidOperationException( - "Reset called on collection without reset handler."); - } - - reset(); - break; + added(index++, item); } - }; + } - index = 0; - foreach (T i in collection) + void Remove(int index, IList items) { - added(index++, i); + for (var i = items.Count - 1; i >= 0; --i) + { + removed(index + i, (T)items[i]); + } } - collection.CollectionChanged += handler; - - return Disposable.Create(() => collection.CollectionChanged -= handler); - } - - /// - /// Invokes an action for each item in a collection and subsequently each item added or - /// removed from the collection. - /// - /// The type of the collection items. - /// The collection. - /// - /// An action called initially with all items in the collection and subsequently with a - /// list of items added to the collection. The parameters passed are the index of the - /// first item added to the collection and the items added. - /// - /// - /// An action called with all items removed from the collection. The parameters passed - /// are the index of the first item removed from the collection and the items removed. - /// - /// - /// An action called when the collection is reset. - /// - /// A disposable used to terminate the subscription. - public static IDisposable ForEachItem( - this IAvaloniaReadOnlyList collection, - Action> added, - Action> removed, - Action reset) - { NotifyCollectionChangedEventHandler handler = (_, e) => { switch (e.Action) { case NotifyCollectionChangedAction.Add: - added(e.NewStartingIndex, e.NewItems.Cast()); + Add(e.NewStartingIndex, e.NewItems); break; + case NotifyCollectionChangedAction.Move: case NotifyCollectionChangedAction.Replace: - removed(e.OldStartingIndex, e.OldItems.Cast()); - added(e.NewStartingIndex, e.NewItems.Cast()); + Remove(e.OldStartingIndex, e.OldItems); + Add(e.NewStartingIndex, e.NewItems); break; case NotifyCollectionChangedAction.Remove: - removed(e.OldStartingIndex, e.OldItems.Cast()); + Remove(e.OldStartingIndex, e.OldItems); break; case NotifyCollectionChangedAction.Reset: @@ -184,16 +112,31 @@ namespace Avalonia.Collections } reset(); + Add(0, (IList)collection); break; } }; - added(0, collection); + Add(0, (IList)collection); collection.CollectionChanged += handler; return Disposable.Create(() => collection.CollectionChanged -= handler); } + public static IAvaloniaReadOnlyList CreateDerivedList( + this IAvaloniaReadOnlyList collection, + Func select) + { + var result = new AvaloniaList(); + + collection.ForEachItem( + (i, item) => result.Insert(i, select(item)), + (i, item) => result.RemoveAt(i), + () => result.Clear()); + + return result; + } + /// /// Listens for property changed events from all items in a collection. /// diff --git a/src/Avalonia.Base/Data/BindingNotification.cs b/src/Avalonia.Base/Data/BindingNotification.cs index 3394bc4f1a..5510a73b91 100644 --- a/src/Avalonia.Base/Data/BindingNotification.cs +++ b/src/Avalonia.Base/Data/BindingNotification.cs @@ -45,11 +45,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. @@ -57,7 +53,7 @@ namespace Avalonia.Data /// The binding value. public BindingNotification(object value) { - _value = new WeakReference(value ?? NullValue); + _value = value; } /// @@ -74,6 +70,7 @@ namespace Avalonia.Data Error = error; ErrorType = errorType; + _value = AvaloniaProperty.UnsetValue; } /// @@ -85,7 +82,7 @@ namespace Avalonia.Data public BindingNotification(Exception error, BindingErrorType errorType, object fallbackValue) : this(error, errorType) { - _value = new WeakReference(fallbackValue ?? NullValue); + _value = fallbackValue; } /// @@ -96,31 +93,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. @@ -249,7 +227,7 @@ namespace Avalonia.Data /// public void ClearValue() { - _value = null; + _value = AvaloniaProperty.UnsetValue; } /// @@ -257,7 +235,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 b728655863..b02b588d44 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -238,7 +238,7 @@ namespace Avalonia.Controls PseudoClasses.Remove(":pressed"); e.Handled = true; - if (ClickMode == ClickMode.Release && Classes.Contains(":pointerover")) + 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 27b046cf47..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); @@ -469,7 +476,7 @@ namespace Avalonia.Controls { if (!IsInitialized) { - foreach (var i in this.GetSelfAndVisualDescendents()) + foreach (var i in this.GetSelfAndVisualDescendants()) { var c = i as IControl; diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index 16f436fd45..ba3ecb99c5 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using Avalonia.Input; using Avalonia.Input.Raw; +using JetBrains.Annotations; namespace Avalonia.Platform { @@ -93,5 +94,11 @@ namespace Avalonia.Platform /// Gets or sets a method called when the underlying implementation is destroyed. /// Action Closed { get; set; } + + /// + /// Gets a mouse device associated with toplevel + /// + [CanBeNull] + IMouseDevice MouseDevice { get; } } } 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/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index c217290226..e41c4e1e28 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -111,7 +111,7 @@ namespace Avalonia.Controls.Presenters /// The target visual. /// The portion of the target visual to bring into view. /// True if the scroll offset was changed; otherwise false. - public bool BringDescendentIntoView(IVisual target, Rect targetRect) + public bool BringDescendantIntoView(IVisual target, Rect targetRect) { if (Child == null) { @@ -262,7 +262,7 @@ namespace Avalonia.Controls.Presenters private void BringIntoViewRequested(object sender, RequestBringIntoViewEventArgs e) { - e.Handled = BringDescendentIntoView(e.TargetObject, e.TargetRect); + e.Handled = BringDescendantIntoView(e.TargetObject, e.TargetRect); } private void ChildChanged(AvaloniaPropertyChangedEventArgs e) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index bb2a61c024..daea187a69 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -340,11 +340,11 @@ namespace Avalonia.Controls.Primitives switch (mode) { case PlacementMode.Pointer: - if (MouseDevice.Instance != null) + if(PopupRoot != null) { // Scales the Horizontal and Vertical offset to screen co-ordinates. var screenOffset = new Point(HorizontalOffset * (PopupRoot as ILayoutRoot).LayoutScaling, VerticalOffset * (PopupRoot as ILayoutRoot).LayoutScaling); - return MouseDevice.Instance.Position + screenOffset; + return (((IInputRoot)PopupRoot)?.MouseDevice?.Position ?? default(Point)) + screenOffset; } return default(Point); 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/Templates/TemplateExtensions.cs b/src/Avalonia.Controls/Templates/TemplateExtensions.cs index 535be23b23..09da737836 100644 --- a/src/Avalonia.Controls/Templates/TemplateExtensions.cs +++ b/src/Avalonia.Controls/Templates/TemplateExtensions.cs @@ -31,9 +31,9 @@ namespace Avalonia.Controls.Templates if (child.TemplatedParent != null) { - foreach (var descendent in GetTemplateChildren(child, templatedParent)) + foreach (var descendant in GetTemplateChildren(child, templatedParent)) { - yield return descendent; + yield return descendant; } } } diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index cff3dab150..22bc589a36 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -105,17 +105,21 @@ namespace Avalonia.Controls { if (control != null && control.IsVisible && control.GetVisualRoot() != null) { - if (s_popup != null) + var cp = (control.GetVisualRoot() as IInputRoot)?.MouseDevice?.GetPosition(control); + var position = control.PointToScreen(cp ?? new Point(0, 0)) + new Vector(0, 22); + + if (s_popup == null) { - throw new AvaloniaInternalException("Previous ToolTip not disposed."); + s_popup = new PopupRoot(); + s_popup.Content = new ToolTip(); + } + else + { + ((ISetLogicalParent)s_popup).SetParent(null); } - var cp = MouseDevice.Instance?.GetPosition(control); - var position = control.PointToScreen(cp ?? new Point(0, 0)) + new Vector(0, 22); - - s_popup = new PopupRoot(); ((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(); @@ -147,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/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 567ca80d6e..a0a8f6b27e 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -163,6 +163,9 @@ namespace Avalonia.Controls set { SetValue(PointerOverElementProperty, value); } } + /// + IMouseDevice IInputRoot.MouseDevice => PlatformImpl?.MouseDevice; + /// /// Gets or sets a value indicating whether access keys are shown in the window. /// @@ -253,7 +256,7 @@ namespace Avalonia.Controls /// The window scaling. protected virtual void HandleScalingChanged(double scaling) { - foreach (ILayoutable control in this.GetSelfAndVisualDescendents()) + foreach (ILayoutable control in this.GetSelfAndVisualDescendants()) { control.InvalidateMeasure(); } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 32eaf499f0..3802f2b6ea 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -47,12 +47,12 @@ namespace Avalonia.Controls /// public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot, INameScope { - private static IList s_windows = new List(); + private static List s_windows = new List(); /// /// Retrieves an enumeration of all Windows in the currently running application. /// - public static IList OpenWindows => s_windows; + public static IReadOnlyList OpenWindows => s_windows; /// /// Defines the property. @@ -238,6 +238,11 @@ namespace Avalonia.Controls /// public override void Show() { + if (IsVisible) + { + return; + } + s_windows.Add(this); EnsureInitialized(); @@ -272,6 +277,11 @@ namespace Avalonia.Controls /// public Task ShowDialog() { + if (IsVisible) + { + throw new InvalidOperationException("The window is already being shown."); + } + s_windows.Add(this); EnsureInitialized(); @@ -360,6 +370,7 @@ namespace Avalonia.Controls protected override void HandleClosed() { IsVisible = false; + s_windows.Remove(this); base.HandleClosed(); } diff --git a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj index a060827f27..e4752f6662 100644 --- a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj +++ b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj @@ -34,7 +34,6 @@ - diff --git a/src/Avalonia.Diagnostics/DevTools.xaml.cs b/src/Avalonia.Diagnostics/DevTools.xaml.cs index 5be252c5c3..b735372b59 100644 --- a/src/Avalonia.Diagnostics/DevTools.xaml.cs +++ b/src/Avalonia.Diagnostics/DevTools.xaml.cs @@ -11,7 +11,6 @@ using Avalonia.Input.Raw; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using Avalonia.VisualTree; -using ReactiveUI; namespace Avalonia { @@ -74,7 +73,7 @@ namespace Avalonia.Diagnostics Content = devTools, DataTemplates = new DataTemplates { - new ViewLocator(), + new ViewLocator(), } }; @@ -107,7 +106,8 @@ namespace Avalonia.Diagnostics if ((e.Modifiers) == modifiers) { - var point = MouseDevice.Instance.GetPosition(Root); + + var point = (Root.VisualRoot as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default(Point); var control = Root.GetVisualsAt(point, x => (!(x is AdornerLayer) && x.IsVisible)) .FirstOrDefault(); diff --git a/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs index 0339d724f7..d723890196 100644 --- a/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -4,11 +4,10 @@ using System.Collections.Generic; using System.Linq; using Avalonia.VisualTree; -using ReactiveUI; namespace Avalonia.Diagnostics.ViewModels { - internal class ControlDetailsViewModel : ReactiveObject + internal class ControlDetailsViewModel : ViewModelBase { public ControlDetailsViewModel(IVisual control) { diff --git a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs index dbee9ad624..2d3f978462 100644 --- a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs @@ -5,32 +5,50 @@ using System; using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Input; -using ReactiveUI; namespace Avalonia.Diagnostics.ViewModels { - internal class DevToolsViewModel : ReactiveObject + internal class DevToolsViewModel : ViewModelBase { - private ReactiveObject _content; - + private ViewModelBase _content; private int _selectedTab; - private TreePageViewModel _logicalTree; - private TreePageViewModel _visualTree; - - private readonly ObservableAsPropertyHelper _focusedControl; - - private readonly ObservableAsPropertyHelper _pointerOverElement; + private string _focusedControl; + private string _pointerOverElement; public DevToolsViewModel(IControl root) { _logicalTree = new TreePageViewModel(LogicalTreeNode.Create(root)); _visualTree = new TreePageViewModel(VisualTreeNode.Create(root)); - this.WhenAnyValue(x => x.SelectedTab).Subscribe(index => + UpdateFocusedControl(); + KeyboardDevice.Instance.PropertyChanged += (s, e) => { - switch (index) + if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement)) + { + UpdateFocusedControl(); + } + }; + + root.GetObservable(TopLevel.PointerOverElementProperty) + .Subscribe(x => PointerOverElement = x?.GetType().Name); + } + + public ViewModelBase Content + { + get { return _content; } + private set { RaiseAndSetIfChanged(ref _content, value); } + } + + public int SelectedTab + { + get { return _selectedTab; } + set + { + _selectedTab = value; + + switch (value) { case 0: Content = _logicalTree; @@ -39,34 +57,23 @@ namespace Avalonia.Diagnostics.ViewModels Content = _visualTree; break; } - }); - _focusedControl = KeyboardDevice.Instance - .WhenAnyValue(x => x.FocusedElement) - .Select(x => x?.GetType().Name) - .ToProperty(this, x => x.FocusedControl); - - _pointerOverElement = root.GetObservable(TopLevel.PointerOverElementProperty) - .Select(x => x?.GetType().Name) - .ToProperty(this, x => x.PointerOverElement); + RaisePropertyChanged(); + } } - public ReactiveObject Content + public string FocusedControl { - get { return _content; } - private set { this.RaiseAndSetIfChanged(ref _content, value); } + get { return _focusedControl; } + private set { RaiseAndSetIfChanged(ref _focusedControl, value); } } - public int SelectedTab + public string PointerOverElement { - get { return _selectedTab; } - set { this.RaiseAndSetIfChanged(ref _selectedTab, value); } + get { return _pointerOverElement; } + private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); } } - public string FocusedControl => _focusedControl.Value; - - public string PointerOverElement => _pointerOverElement.Value; - public void SelectControl(IControl control) { var tree = Content as TreePageViewModel; @@ -76,5 +83,10 @@ namespace Avalonia.Diagnostics.ViewModels tree.SelectControl(control); } } + + private void UpdateFocusedControl() + { + _focusedControl = KeyboardDevice.Instance.FocusedElement?.GetType().Name; + } } } diff --git a/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs index dcb4e79402..e8a8951f0d 100644 --- a/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs +++ b/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs @@ -2,9 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Avalonia.Collections; using Avalonia.Controls; using Avalonia.LogicalTree; -using ReactiveUI; namespace Avalonia.Diagnostics.ViewModels { @@ -13,7 +13,7 @@ namespace Avalonia.Diagnostics.ViewModels public LogicalTreeNode(ILogical logical, TreeNode parent) : base((Control)logical, parent) { - Children = logical.LogicalChildren.CreateDerivedCollection(x => new LogicalTreeNode(x, this)); + Children = logical.LogicalChildren.CreateDerivedList(x => new LogicalTreeNode(x, this)); } public static LogicalTreeNode[] Create(object control) diff --git a/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs b/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs index c06f2415cf..2609b74ce0 100644 --- a/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs +++ b/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs @@ -3,16 +3,13 @@ using System; using Avalonia.Data; -using ReactiveUI; namespace Avalonia.Diagnostics.ViewModels { - internal class PropertyDetails : ReactiveObject + internal class PropertyDetails : ViewModelBase { private object _value; - private string _priority; - private string _diagnostic; public PropertyDetails(AvaloniaObject o, AvaloniaProperty property) @@ -41,19 +38,19 @@ namespace Avalonia.Diagnostics.ViewModels public string Priority { get { return _priority; } - private set { this.RaiseAndSetIfChanged(ref _priority, value); } + private set { RaiseAndSetIfChanged(ref _priority, value); } } public string Diagnostic { get { return _diagnostic; } - private set { this.RaiseAndSetIfChanged(ref _diagnostic, value); } + private set { RaiseAndSetIfChanged(ref _diagnostic, value); } } public object Value { get { return _value; } - private set { this.RaiseAndSetIfChanged(ref _value, value); } + private set { RaiseAndSetIfChanged(ref _value, value); } } } } diff --git a/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs index 0b0b7a94fe..7c403e1b04 100644 --- a/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs @@ -5,13 +5,13 @@ using System; using System.Collections.Specialized; using System.Reactive; using System.Reactive.Linq; +using Avalonia.Collections; using Avalonia.Styling; using Avalonia.VisualTree; -using ReactiveUI; namespace Avalonia.Diagnostics.ViewModels { - internal class TreeNode : ReactiveObject + internal class TreeNode : ViewModelBase { private string _classes; private bool _isExpanded; @@ -47,7 +47,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - public IReadOnlyReactiveList Children + public IAvaloniaReadOnlyList Children { get; protected set; @@ -56,7 +56,7 @@ namespace Avalonia.Diagnostics.ViewModels public string Classes { get { return _classes; } - private set { this.RaiseAndSetIfChanged(ref _classes, value); } + private set { RaiseAndSetIfChanged(ref _classes, value); } } public IVisual Visual @@ -67,7 +67,7 @@ namespace Avalonia.Diagnostics.ViewModels public bool IsExpanded { get { return _isExpanded; } - set { this.RaiseAndSetIfChanged(ref _isExpanded, value); } + set { RaiseAndSetIfChanged(ref _isExpanded, value); } } public TreeNode Parent diff --git a/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs index 97bd971e74..dba44c5d0c 100644 --- a/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs @@ -1,25 +1,19 @@ // 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.Reactive.Linq; using Avalonia.Controls; using Avalonia.VisualTree; -using ReactiveUI; namespace Avalonia.Diagnostics.ViewModels { - internal class TreePageViewModel : ReactiveObject + internal class TreePageViewModel : ViewModelBase { private TreeNode _selected; - - private readonly ObservableAsPropertyHelper _details; + private ControlDetailsViewModel _details; public TreePageViewModel(TreeNode[] nodes) { Nodes = nodes; - _details = this.WhenAnyValue(x => x.SelectedNode) - .Select(x => x != null ? new ControlDetailsViewModel(x.Visual) : null) - .ToProperty(this, x => x.Details); } public TreeNode[] Nodes { get; protected set; } @@ -27,10 +21,20 @@ namespace Avalonia.Diagnostics.ViewModels public TreeNode SelectedNode { get { return _selected; } - set { this.RaiseAndSetIfChanged(ref _selected, value); } + set + { + if (RaiseAndSetIfChanged(ref _selected, value)) + { + Details = value != null ? new ControlDetailsViewModel(value.Visual) : null; + } + } } - public ControlDetailsViewModel Details => _details.Value; + public ControlDetailsViewModel Details + { + get { return _details; } + private set { RaiseAndSetIfChanged(ref _details, value); } + } public TreeNode FindNode(IControl control) { diff --git a/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs b/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000000..349404603a --- /dev/null +++ b/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; + +namespace Avalonia.Diagnostics.ViewModels +{ + public class ViewModelBase : INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + + [NotifyPropertyChangedInvocator] + protected bool RaiseAndSetIfChanged(ref T field, T value, [CallerMemberName] string propertyName = null) + { + if (!EqualityComparer.Default.Equals(field, value)) + { + field = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + return true; + } + + return false; + } + + [NotifyPropertyChangedInvocator] + protected void RaisePropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs index f1b02a61c9..8c070261d9 100644 --- a/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs @@ -1,10 +1,9 @@ // 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 Avalonia.Collections; using Avalonia.Styling; using Avalonia.VisualTree; -using ReactiveUI; namespace Avalonia.Diagnostics.ViewModels { @@ -17,11 +16,11 @@ namespace Avalonia.Diagnostics.ViewModels if (host?.Root == null) { - Children = visual.VisualChildren.CreateDerivedCollection(x => new VisualTreeNode(x, this)); + Children = visual.VisualChildren.CreateDerivedList(x => new VisualTreeNode(x, this)); } else { - Children = new ReactiveList(new[] { new VisualTreeNode(host.Root, this) }); + Children = new AvaloniaList(new[] { new VisualTreeNode(host.Root, this) }); } if ((Visual is IStyleable styleable)) diff --git a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs b/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs index 7cb74ebb33..d7bd6fd128 100644 --- a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs +++ b/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs @@ -8,7 +8,6 @@ using Avalonia.Controls; using Avalonia.Diagnostics.ViewModels; using Avalonia.Media; using Avalonia.Styling; -using ReactiveUI; namespace Avalonia.Diagnostics.Views { @@ -16,6 +15,7 @@ namespace Avalonia.Diagnostics.Views { private static readonly StyledProperty ViewModelProperty = AvaloniaProperty.Register("ViewModel"); + private SimpleGrid _grid; public ControlDetailsView() { @@ -27,7 +27,11 @@ namespace Avalonia.Diagnostics.Views public ControlDetailsViewModel ViewModel { get { return GetValue(ViewModelProperty); } - private set { SetValue(ViewModelProperty, value); } + private set + { + SetValue(ViewModelProperty, value); + _grid[GridRepeater.ItemsProperty] = value?.Properties; + } } private void InitializeComponent() @@ -36,7 +40,7 @@ namespace Avalonia.Diagnostics.Views Content = new ScrollViewer { - Content = new SimpleGrid + Content = _grid = new SimpleGrid { Styles = new Styles { @@ -49,7 +53,6 @@ namespace Avalonia.Diagnostics.Views }, }, [GridRepeater.TemplateProperty] = pt, - [!GridRepeater.ItemsProperty] = this.WhenAnyValue(x => x.ViewModel.Properties).ToBinding(), } }; } @@ -62,16 +65,13 @@ namespace Avalonia.Diagnostics.Views { Text = property.Name, TextWrapping = TextWrapping.NoWrap, - [!ToolTip.TipProperty] = property - .WhenAnyValue(x => x.Diagnostic) - .ToBinding(), + [!ToolTip.TipProperty] = property.GetObservable(nameof(property.Diagnostic)).ToBinding(), }; yield return new TextBlock { TextWrapping = TextWrapping.NoWrap, - [!TextBlock.TextProperty] = property - .WhenAnyValue(v => v.Value) + [!TextBlock.TextProperty] = property.GetObservable(nameof(property.Value)) .Select(v => v?.ToString()) .ToBinding(), }; @@ -79,7 +79,7 @@ namespace Avalonia.Diagnostics.Views yield return new TextBlock { TextWrapping = TextWrapping.NoWrap, - [!TextBlock.TextProperty] = property.WhenAnyValue(x => x.Priority).ToBinding(), + [!TextBlock.TextProperty] = property.GetObservable((nameof(property.Priority))).ToBinding(), }; } } diff --git a/src/Avalonia.Diagnostics/Views/PropertyChangedExtenions.cs b/src/Avalonia.Diagnostics/Views/PropertyChangedExtenions.cs new file mode 100644 index 0000000000..1d4ad24fd0 --- /dev/null +++ b/src/Avalonia.Diagnostics/Views/PropertyChangedExtenions.cs @@ -0,0 +1,30 @@ +using System; +using System.ComponentModel; +using System.Reactive.Linq; +using System.Reflection; + +namespace Avalonia.Diagnostics.Views +{ + internal static class PropertyChangedExtenions + { + public static IObservable GetObservable(this INotifyPropertyChanged source, string propertyName) + { + Contract.Requires(source != null); + Contract.Requires(propertyName != null); + + var property = source.GetType().GetTypeInfo().GetDeclaredProperty(propertyName); + + if (property == null) + { + throw new ArgumentException($"Property '{propertyName}' not found on '{source}."); + } + + return Observable.FromEventPattern( + e => source.PropertyChanged += e, + e => source.PropertyChanged -= e) + .Where(e => e.EventArgs.PropertyName == propertyName) + .Select(_ => (T)property.GetValue(source)) + .StartWith((T)property.GetValue(source)); + } + } +} diff --git a/src/Avalonia.HtmlRenderer/Adapters/ControlAdapter.cs b/src/Avalonia.HtmlRenderer/Adapters/ControlAdapter.cs index 523da3508e..9a55768f6a 100644 --- a/src/Avalonia.HtmlRenderer/Adapters/ControlAdapter.cs +++ b/src/Avalonia.HtmlRenderer/Adapters/ControlAdapter.cs @@ -10,9 +10,11 @@ // - Sun Tsu, // "The Art of War" +using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Html; using Avalonia.Input; +using Avalonia.VisualTree; using TheArtOfDev.HtmlRenderer.Adapters; using TheArtOfDev.HtmlRenderer.Adapters.Entities; using TheArtOfDev.HtmlRenderer.Core.Utils; @@ -54,7 +56,8 @@ namespace TheArtOfDev.HtmlRenderer.Avalonia.Adapters { get { - return Util.Convert(MouseDevice.Instance.GetPosition(_control)); + var pos = (_control.GetVisualRoot() as IInputRoot)?.MouseDevice?.Position ?? default(Point); + return Util.Convert(pos); } } diff --git a/src/Avalonia.HtmlRenderer/HtmlControl.cs b/src/Avalonia.HtmlRenderer/HtmlControl.cs index 88a6e5fda4..0051f6427b 100644 --- a/src/Avalonia.HtmlRenderer/HtmlControl.cs +++ b/src/Avalonia.HtmlRenderer/HtmlControl.cs @@ -17,6 +17,7 @@ using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Threading; +using Avalonia.VisualTree; using TheArtOfDev.HtmlRenderer.Core; using TheArtOfDev.HtmlRenderer.Core.Entities; using TheArtOfDev.HtmlRenderer.Avalonia; @@ -512,7 +513,7 @@ namespace Avalonia.Controls.Html protected virtual void InvokeMouseMove() { - _htmlContainer.HandleMouseMove(this, MouseDevice.Instance?.GetPosition(this) ?? default(Point)); + _htmlContainer.HandleMouseMove(this, (this.GetVisualRoot() as IInputRoot)?.MouseDevice?.GetPosition(this) ?? default(Point)); } /// diff --git a/src/Avalonia.Input/IInputDevice.cs b/src/Avalonia.Input/IInputDevice.cs index 916f29376a..72fa2ab9bf 100644 --- a/src/Avalonia.Input/IInputDevice.cs +++ b/src/Avalonia.Input/IInputDevice.cs @@ -1,9 +1,16 @@ // 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.Input.Raw; + namespace Avalonia.Input { public interface IInputDevice { + /// + /// Processes raw event. Is called after preprocessing by InputManager + /// + /// + void ProcessRawEvent(RawInputEventArgs ev); } } diff --git a/src/Avalonia.Input/IInputRoot.cs b/src/Avalonia.Input/IInputRoot.cs index 6c01bb9c0e..6b3e1e6bc5 100644 --- a/src/Avalonia.Input/IInputRoot.cs +++ b/src/Avalonia.Input/IInputRoot.cs @@ -1,6 +1,8 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using JetBrains.Annotations; + namespace Avalonia.Input { /// @@ -27,5 +29,11 @@ namespace Avalonia.Input /// Gets or sets a value indicating whether access keys are shown in the window. /// bool ShowAccessKeys { get; set; } + + /// + /// Gets associated mouse device + /// + [CanBeNull] + IMouseDevice MouseDevice { get; } } } diff --git a/src/Avalonia.Input/IKeyboardDevice.cs b/src/Avalonia.Input/IKeyboardDevice.cs index f27fec6907..1410476267 100644 --- a/src/Avalonia.Input/IKeyboardDevice.cs +++ b/src/Avalonia.Input/IKeyboardDevice.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.ComponentModel; namespace Avalonia.Input { @@ -26,7 +27,7 @@ namespace Avalonia.Input Toggled = 2, } - public interface IKeyboardDevice : IInputDevice + public interface IKeyboardDevice : IInputDevice, INotifyPropertyChanged { IInputElement FocusedElement { get; } diff --git a/src/Avalonia.Input/InputManager.cs b/src/Avalonia.Input/InputManager.cs index 7aa609e65c..9e2d5ffea3 100644 --- a/src/Avalonia.Input/InputManager.cs +++ b/src/Avalonia.Input/InputManager.cs @@ -35,6 +35,7 @@ namespace Avalonia.Input public void ProcessInput(RawInputEventArgs e) { _preProcess.OnNext(e); + e.Device?.ProcessRawEvent(e); _process.OnNext(e); _postProcess.OnNext(e); } diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs index 51af01f69b..d815f8082b 100644 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ b/src/Avalonia.Input/KeyboardDevice.cs @@ -16,14 +16,6 @@ namespace Avalonia.Input { private IInputElement _focusedElement; - public KeyboardDevice() - { - InputManager.Process - .OfType() - .Where(e => e.Device == this && !e.Handled) - .Subscribe(ProcessRawEvent); - } - public event PropertyChangedEventHandler PropertyChanged; public static IKeyboardDevice Instance => AvaloniaLocator.Current.GetService(); @@ -77,8 +69,10 @@ namespace Avalonia.Input PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } - private void ProcessRawEvent(RawInputEventArgs e) + public void ProcessRawEvent(RawInputEventArgs e) { + if(e.Handled) + return; IInputElement element = FocusedElement; if (element != null) diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index cbcb4382ec..875a5ebaee 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -20,23 +20,7 @@ namespace Avalonia.Input private int _clickCount; private Rect _lastClickRect; private uint _lastClickTime; - - /// - /// Intializes a new instance of . - /// - public MouseDevice() - { - InputManager.Process - .OfType() - .Where(e => e.Device == this && !e.Handled) - .Subscribe(ProcessRawEvent); - } - - /// - /// Gets the current mouse device instance. - /// - public static IMouseDevice Instance => AvaloniaLocator.Current.GetService(); - + /// /// Gets the control that is currently capturing by the mouse, if any. /// @@ -50,12 +34,7 @@ namespace Avalonia.Input get; protected set; } - - /// - /// Gets the application's input manager. - /// - public IInputManager InputManager => AvaloniaLocator.Current.GetService(); - + /// /// Gets the mouse position, in screen coordinates. /// @@ -102,6 +81,12 @@ namespace Avalonia.Input return root.PointToClient(Position) - p; } + public void ProcessRawEvent(RawInputEventArgs e) + { + if (!e.Handled && e is RawMouseEventArgs margs) + ProcessRawEvent(margs); + } + private void ProcessRawEvent(RawMouseEventArgs e) { Contract.Requires(e != null); diff --git a/src/Avalonia.Input/Navigation/DirectionalNavigation.cs b/src/Avalonia.Input/Navigation/DirectionalNavigation.cs index b2f7a35799..a88ed1e8aa 100644 --- a/src/Avalonia.Input/Navigation/DirectionalNavigation.cs +++ b/src/Avalonia.Input/Navigation/DirectionalNavigation.cs @@ -44,7 +44,7 @@ namespace Avalonia.Input.Navigation GetFirstInNextContainer(element, direction); case KeyboardNavigationMode.Cycle: return GetNextInContainer(element, container, direction) ?? - GetFocusableDescendent(container, direction); + GetFocusableDescendant(container, direction); case KeyboardNavigationMode.Contained: return GetNextInContainer(element, container, direction); default: @@ -53,7 +53,7 @@ namespace Avalonia.Input.Navigation } else { - return GetFocusableDescendents(element).FirstOrDefault(); + return GetFocusableDescendants(element).FirstOrDefault(); } } @@ -71,24 +71,24 @@ namespace Avalonia.Input.Navigation } /// - /// Gets the first or last focusable descendent of the specified element. + /// Gets the first or last focusable descendant of the specified element. /// /// The element. /// The direction to search. /// The element or null if not found.## - private static IInputElement GetFocusableDescendent(IInputElement container, NavigationDirection direction) + private static IInputElement GetFocusableDescendant(IInputElement container, NavigationDirection direction) { return IsForward(direction) ? - GetFocusableDescendents(container).FirstOrDefault() : - GetFocusableDescendents(container).LastOrDefault(); + GetFocusableDescendants(container).FirstOrDefault() : + GetFocusableDescendants(container).LastOrDefault(); } /// - /// Gets the focusable descendents of the specified element. + /// Gets the focusable descendants of the specified element. /// /// The element. - /// The element's focusable descendents. - private static IEnumerable GetFocusableDescendents(IInputElement element) + /// The element's focusable descendants. + private static IEnumerable GetFocusableDescendants(IInputElement element) { var children = element.GetVisualChildren().OfType(); @@ -99,11 +99,11 @@ namespace Avalonia.Input.Navigation yield return child; } - if (child.CanFocusDescendents()) + if (child.CanFocusDescendants()) { - foreach (var descendent in GetFocusableDescendents(child)) + foreach (var descendant in GetFocusableDescendants(child)) { - yield return descendent; + yield return descendant; } } } @@ -123,11 +123,11 @@ namespace Avalonia.Input.Navigation { if (direction == NavigationDirection.Down) { - var descendent = GetFocusableDescendents(element).FirstOrDefault(); + var descendant = GetFocusableDescendants(element).FirstOrDefault(); - if (descendent != null) + if (descendant != null) { - return descendent; + return descendant; } } @@ -156,11 +156,11 @@ namespace Avalonia.Input.Navigation if (element != null && direction == NavigationDirection.Up) { - var descendent = GetFocusableDescendents(element).LastOrDefault(); + var descendant = GetFocusableDescendants(element).LastOrDefault(); - if (descendent != null) + if (descendant != null) { - return descendent; + return descendant; } } @@ -193,7 +193,7 @@ namespace Avalonia.Input.Navigation var siblings = parent.GetVisualChildren() .OfType() - .Where(FocusExtensions.CanFocusDescendents); + .Where(FocusExtensions.CanFocusDescendants); var sibling = isForward ? siblings.SkipWhile(x => x != container).Skip(1).FirstOrDefault() : siblings.TakeWhile(x => x != container).LastOrDefault(); @@ -207,8 +207,8 @@ namespace Avalonia.Input.Navigation else { next = isForward ? - GetFocusableDescendents(sibling).FirstOrDefault() : - GetFocusableDescendents(sibling).LastOrDefault(); + GetFocusableDescendants(sibling).FirstOrDefault() : + GetFocusableDescendants(sibling).LastOrDefault(); } } @@ -220,8 +220,8 @@ namespace Avalonia.Input.Navigation else { next = isForward ? - GetFocusableDescendents(container).FirstOrDefault() : - GetFocusableDescendents(container).LastOrDefault(); + GetFocusableDescendants(container).FirstOrDefault() : + GetFocusableDescendants(container).LastOrDefault(); } return next; diff --git a/src/Avalonia.Input/Navigation/FocusExtensions.cs b/src/Avalonia.Input/Navigation/FocusExtensions.cs index 36fda1abb1..41e7c4cd7b 100644 --- a/src/Avalonia.Input/Navigation/FocusExtensions.cs +++ b/src/Avalonia.Input/Navigation/FocusExtensions.cs @@ -16,10 +16,10 @@ namespace Avalonia.Input.Navigation public static bool CanFocus(this IInputElement e) => e.Focusable && e.IsEnabledCore && e.IsVisible; /// - /// Checks if descendents of the specified element can be focused. + /// Checks if descendants of the specified element can be focused. /// /// The element. - /// True if descendents of the element can be focused. - public static bool CanFocusDescendents(this IInputElement e) => e.IsEnabledCore && e.IsVisible; + /// True if descendants of the element can be focused. + public static bool CanFocusDescendants(this IInputElement e) => e.IsEnabledCore && e.IsVisible; } } diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs index bc3826d90e..6ba7ab1a0c 100644 --- a/src/Avalonia.Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Input/Navigation/TabNavigation.cs @@ -44,7 +44,7 @@ namespace Avalonia.Input.Navigation GetFirstInNextContainer(element, direction); case KeyboardNavigationMode.Cycle: return GetNextInContainer(element, container, direction) ?? - GetFocusableDescendent(container, direction); + GetFocusableDescendant(container, direction); case KeyboardNavigationMode.Contained: return GetNextInContainer(element, container, direction); default: @@ -53,29 +53,29 @@ namespace Avalonia.Input.Navigation } else { - return GetFocusableDescendents(element).FirstOrDefault(); + return GetFocusableDescendants(element).FirstOrDefault(); } } /// - /// Gets the first or last focusable descendent of the specified element. + /// Gets the first or last focusable descendant of the specified element. /// /// The element. /// The direction to search. /// The element or null if not found.## - private static IInputElement GetFocusableDescendent(IInputElement container, NavigationDirection direction) + private static IInputElement GetFocusableDescendant(IInputElement container, NavigationDirection direction) { return direction == NavigationDirection.Next ? - GetFocusableDescendents(container).FirstOrDefault() : - GetFocusableDescendents(container).LastOrDefault(); + GetFocusableDescendants(container).FirstOrDefault() : + GetFocusableDescendants(container).LastOrDefault(); } /// - /// Gets the focusable descendents of the specified element. + /// Gets the focusable descendants of the specified element. /// /// The element. - /// The element's focusable descendents. - private static IEnumerable GetFocusableDescendents(IInputElement element) + /// The element's focusable descendants. + private static IEnumerable GetFocusableDescendants(IInputElement element) { var mode = KeyboardNavigation.GetTabNavigation((InputElement)element); @@ -108,11 +108,11 @@ namespace Avalonia.Input.Navigation yield return child; } - if (child.CanFocusDescendents()) + if (child.CanFocusDescendants()) { - foreach (var descendent in GetFocusableDescendents(child)) + foreach (var descendant in GetFocusableDescendants(child)) { - yield return descendent; + yield return descendant; } } } @@ -132,11 +132,11 @@ namespace Avalonia.Input.Navigation { if (direction == NavigationDirection.Next) { - var descendent = GetFocusableDescendents(element).FirstOrDefault(); + var descendant = GetFocusableDescendants(element).FirstOrDefault(); - if (descendent != null) + if (descendant != null) { - return descendent; + return descendant; } } @@ -167,11 +167,11 @@ namespace Avalonia.Input.Navigation if (element != null && direction == NavigationDirection.Previous) { - var descendent = GetFocusableDescendents(element).LastOrDefault(); + var descendant = GetFocusableDescendants(element).LastOrDefault(); - if (descendent != null) + if (descendant != null) { - return descendent; + return descendant; } } @@ -203,7 +203,7 @@ namespace Avalonia.Input.Navigation var siblings = parent.GetVisualChildren() .OfType() - .Where(FocusExtensions.CanFocusDescendents); + .Where(FocusExtensions.CanFocusDescendants); var sibling = direction == NavigationDirection.Next ? siblings.SkipWhile(x => x != container).Skip(1).FirstOrDefault() : siblings.TakeWhile(x => x != container).LastOrDefault(); @@ -217,8 +217,8 @@ namespace Avalonia.Input.Navigation else { next = direction == NavigationDirection.Next ? - GetFocusableDescendents(sibling).FirstOrDefault() : - GetFocusableDescendents(sibling).LastOrDefault(); + GetFocusableDescendants(sibling).FirstOrDefault() : + GetFocusableDescendants(sibling).LastOrDefault(); } } @@ -230,8 +230,8 @@ namespace Avalonia.Input.Navigation else { next = direction == NavigationDirection.Next ? - GetFocusableDescendents(container).FirstOrDefault() : - GetFocusableDescendents(container).LastOrDefault(); + GetFocusableDescendants(container).FirstOrDefault() : + GetFocusableDescendants(container).LastOrDefault(); } return next; diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index b7b83bf852..965ab3eee6 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.DesiredSize)); - } - 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(control.DesiredSize)); + } + 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 95a0dd625c..dad00d93d4 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(); + } } } @@ -612,7 +620,7 @@ namespace Avalonia.Layout /// protected override sealed void OnVisualParentChanged(IVisual oldParent, IVisual newParent) { - foreach (ILayoutable i in this.GetSelfAndVisualDescendents()) + foreach (ILayoutable i in this.GetSelfAndVisualDescendants()) { i.InvalidateMeasure(); } 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/LogicalTree/LogicalExtensions.cs b/src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs index 0d9c0f6daa..a72a558258 100644 --- a/src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs +++ b/src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs @@ -37,15 +37,15 @@ namespace Avalonia.LogicalTree return logical.LogicalChildren; } - public static IEnumerable GetLogicalDescendents(this ILogical logical) + public static IEnumerable GetLogicalDescendants(this ILogical logical) { foreach (ILogical child in logical.LogicalChildren) { yield return child; - foreach (ILogical descendent in child.GetLogicalDescendents()) + foreach (ILogical descendant in child.GetLogicalDescendants()) { - yield return descendent; + yield return descendant; } } } diff --git a/src/Avalonia.Styling/Styling/DescendentSelector.cs b/src/Avalonia.Styling/Styling/DescendentSelector.cs index 525f31cbf1..943b3f0161 100644 --- a/src/Avalonia.Styling/Styling/DescendentSelector.cs +++ b/src/Avalonia.Styling/Styling/DescendentSelector.cs @@ -7,16 +7,16 @@ using Avalonia.LogicalTree; namespace Avalonia.Styling { - internal class DescendentSelector : Selector + internal class DescendantSelector : Selector { private readonly Selector _parent; private string _selectorString; - public DescendentSelector(Selector parent) + public DescendantSelector(Selector parent) { if (parent == null) { - throw new InvalidOperationException("Descendent selector must be preceeded by a selector."); + throw new InvalidOperationException("Descendant selector must be preceeded by a selector."); } _parent = parent; @@ -41,7 +41,7 @@ namespace Avalonia.Styling protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) { ILogical c = (ILogical)control; - List> descendentMatches = new List>(); + List> descendantMatches = new List>(); while (c != null) { @@ -60,14 +60,14 @@ namespace Avalonia.Styling } else { - descendentMatches.Add(match.ObservableResult); + descendantMatches.Add(match.ObservableResult); } } } - if (descendentMatches.Count > 0) + if (descendantMatches.Count > 0) { - return new SelectorMatch(StyleActivator.Or(descendentMatches)); + return new SelectorMatch(StyleActivator.Or(descendantMatches)); } else { diff --git a/src/Avalonia.Styling/Styling/Selectors.cs b/src/Avalonia.Styling/Styling/Selectors.cs index 1b4c876f81..c91cc7af04 100644 --- a/src/Avalonia.Styling/Styling/Selectors.cs +++ b/src/Avalonia.Styling/Styling/Selectors.cs @@ -42,13 +42,13 @@ namespace Avalonia.Styling } /// - /// Returns a selector which matches a descendent of a previous selector. + /// Returns a selector which matches a descendant of a previous selector. /// /// The previous selector. /// The selector. - public static Selector Descendent(this Selector previous) + public static Selector Descendant(this Selector previous) { - return new DescendentSelector(previous); + return new DescendantSelector(previous); } /// diff --git a/src/Avalonia.Visuals/Platform/ILockedFramebuffer.cs b/src/Avalonia.Visuals/Platform/ILockedFramebuffer.cs index 92ec2877ab..45ca1a5a99 100644 --- a/src/Avalonia.Visuals/Platform/ILockedFramebuffer.cs +++ b/src/Avalonia.Visuals/Platform/ILockedFramebuffer.cs @@ -27,7 +27,7 @@ namespace Avalonia.Platform /// /// DPI of underling screen /// - Size Dpi { get; } + Vector Dpi { get; } /// /// Pixel format diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs index 4879532d6e..7d2222e449 100644 --- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs @@ -164,7 +164,7 @@ namespace Avalonia.Rendering private static void ClearTransformedBounds(IVisual visual) { - foreach (var e in visual.GetSelfAndVisualDescendents()) + foreach (var e in visual.GetSelfAndVisualDescendants()) { BoundsTracker.SetTransformedBounds((Visual)visual, null); } diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index cc1c700690..69cbfd9592 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -52,8 +52,6 @@ namespace Avalonia return new Point(a._x, a._y); } - - /// /// Calculates the dot product of two vectors /// @@ -65,6 +63,17 @@ namespace Avalonia return a.X*b.X + a.Y*b.Y; } + /// + /// Scales a vector. + /// + /// The vector + /// The scaling factor. + /// The scaled vector. + public static Vector operator *(Vector vector, double scale) + { + return new Vector(vector._x * scale, vector._y * scale); + } + /// /// Length of the vector /// diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index 20772d8f0d..cfe6bce7e0 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -314,7 +314,7 @@ namespace Avalonia /// /// Calls the method - /// for this control and all of its visual descendents. + /// for this control and all of its visual descendants. /// /// The event args. protected virtual void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) @@ -342,7 +342,7 @@ namespace Avalonia /// /// Calls the method - /// for this control and all of its visual descendents. + /// for this control and all of its visual descendants. /// /// The event args. protected virtual void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) @@ -422,7 +422,7 @@ namespace Avalonia if (visual == null) { - throw new ArgumentException("'visual' is not a descendent of 'ancestor'."); + throw new ArgumentException("'visual' is not a descendant of 'ancestor'."); } } diff --git a/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs b/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs index 11fbae4321..289c4134d1 100644 --- a/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs +++ b/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs @@ -123,33 +123,33 @@ namespace Avalonia.VisualTree } /// - /// Enumerates the descendents of an in the visual tree. + /// Enumerates the descendants of an in the visual tree. /// /// The visual. /// The visual's ancestors. - public static IEnumerable GetVisualDescendents(this IVisual visual) + public static IEnumerable GetVisualDescendants(this IVisual visual) { foreach (IVisual child in visual.VisualChildren) { yield return child; - foreach (IVisual descendent in child.GetVisualDescendents()) + foreach (IVisual descendant in child.GetVisualDescendants()) { - yield return descendent; + yield return descendant; } } } /// - /// Enumerates an and its descendents in the visual tree. + /// Enumerates an and its descendants in the visual tree. /// /// The visual. /// The visual and its ancestors. - public static IEnumerable GetSelfAndVisualDescendents(this IVisual visual) + public static IEnumerable GetSelfAndVisualDescendants(this IVisual visual) { yield return visual; - foreach (var ancestor in visual.GetVisualDescendents()) + foreach (var ancestor in visual.GetVisualDescendants()) { yield return ancestor; } @@ -196,7 +196,7 @@ namespace Avalonia.VisualTree /// Tests whether an is an ancestor of another visual. /// /// The visual. - /// The potential descendent. + /// The potential descendant. /// /// True if is an ancestor of ; /// otherwise false. diff --git a/src/Gtk/Avalonia.Gtk/GtkPlatform.cs b/src/Gtk/Avalonia.Gtk/GtkPlatform.cs index d387ed0320..ef7e4d3362 100644 --- a/src/Gtk/Avalonia.Gtk/GtkPlatform.cs +++ b/src/Gtk/Avalonia.Gtk/GtkPlatform.cs @@ -51,7 +51,6 @@ namespace Avalonia.Gtk .Bind().ToSingleton() .Bind().ToConstant(CursorFactory.Instance) .Bind().ToConstant(GtkKeyboardDevice.Instance) - .Bind().ToConstant(GtkMouseDevice.Instance) .Bind().ToConstant(s_instance) .Bind().ToConstant(s_instance) .Bind().ToConstant(s_instance) diff --git a/src/Gtk/Avalonia.Gtk/SurfaceFramebuffer.cs b/src/Gtk/Avalonia.Gtk/SurfaceFramebuffer.cs index 7e6da0e76a..29f4ce1d15 100644 --- a/src/Gtk/Avalonia.Gtk/SurfaceFramebuffer.cs +++ b/src/Gtk/Avalonia.Gtk/SurfaceFramebuffer.cs @@ -48,7 +48,7 @@ namespace Avalonia.Gtk public int Height => _surface.Height; public int RowBytes => _surface.Stride; //TODO: Proper DPI detect - public Size Dpi => new Size(96, 96); + public Vector Dpi => new Vector(96, 96); public PixelFormat Format => PixelFormat.Bgra8888; } } diff --git a/src/Gtk/Avalonia.Gtk/TopLevelImpl.cs b/src/Gtk/Avalonia.Gtk/TopLevelImpl.cs index a1bb3f847e..ce1f50ac0f 100644 --- a/src/Gtk/Avalonia.Gtk/TopLevelImpl.cs +++ b/src/Gtk/Avalonia.Gtk/TopLevelImpl.cs @@ -75,6 +75,8 @@ namespace Avalonia.Gtk } } + public IMouseDevice MouseDevice => GtkMouseDevice.Instance; + public Avalonia.Controls.WindowState WindowState { get @@ -114,6 +116,7 @@ namespace Avalonia.Gtk public Action Closed { get; set; } + public Action Deactivated { get; set; } public Action Input { get; set; } diff --git a/src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs b/src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs index a2913b4066..a3db0def74 100644 --- a/src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs +++ b/src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs @@ -34,7 +34,6 @@ namespace Avalonia.Gtk3 .Bind().ToSingleton() .Bind().ToConstant(new CursorFactory()) .Bind().ToConstant(Keyboard) - .Bind().ToConstant(Mouse) .Bind().ToConstant(Instance) .Bind().ToConstant(Instance) .Bind().ToSingleton() diff --git a/src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs b/src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs index 3263018a17..61b1e69aa2 100644 --- a/src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs +++ b/src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs @@ -52,12 +52,11 @@ namespace Avalonia.Gtk3 public int RowBytes { get; } - public Size Dpi + public Vector Dpi { get { - - return new Size(96, 96) * _factor; + return new Vector(96, 96) * _factor; } } 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/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs index 478580e65e..00130346e8 100644 --- a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs +++ b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs @@ -233,6 +233,7 @@ namespace Avalonia.Gtk3 } } + public IMouseDevice MouseDevice => Gtk3Platform.Mouse; public double Scaling => (double) 1 / (Native.GtkWidgetGetScaleFactor?.Invoke(GtkWidget) ?? 1); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 193d2c1d05..0854f9acf4 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -56,6 +56,7 @@ namespace Avalonia.LinuxFramebuffer } public Size ClientSize => _fb.PixelSize; + public IMouseDevice MouseDevice => LinuxFramebufferPlatform.MouseDevice; public double Scaling => 1; public IEnumerable Surfaces => new object[] {_fb}; public Action Input { get; set; } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs index 5aec5408a4..8d04360edf 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs @@ -13,16 +13,16 @@ namespace Avalonia.LinuxFramebuffer { public sealed unsafe class LinuxFramebuffer : IFramebufferPlatformSurface, IDisposable { - private readonly Size _dpi; + private readonly Vector _dpi; private int _fd; private fb_fix_screeninfo _fixedInfo; private fb_var_screeninfo _varInfo; private IntPtr _mappedLength; private IntPtr _mappedAddress; - public LinuxFramebuffer(string fileName = null, Size? dpi = null) + public LinuxFramebuffer(string fileName = null, Vector? dpi = null) { - _dpi = dpi ?? new Size(96, 96); + _dpi = dpi ?? new Vector(96, 96); fileName = fileName ?? Environment.GetEnvironmentVariable("FRAMEBUFFER") ?? "/dev/fb0"; _fd = NativeUnsafeMethods.open(fileName, 2, 0); if (_fd <= 0) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index a65a91d6b0..5f1c55fd5d 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -33,7 +33,6 @@ namespace Avalonia.LinuxFramebuffer AvaloniaLocator.CurrentMutable .Bind().ToTransient() .Bind().ToConstant(KeyboardDevice) - .Bind().ToConstant(MouseDevice) .Bind().ToSingleton() .Bind().ToConstant(ImmediateRenderer.Factory) .Bind().ToConstant(PlatformThreadingInterface.Instance) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs b/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs index d8330fcb70..795d9648ea 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs @@ -11,7 +11,7 @@ namespace Avalonia.LinuxFramebuffer private fb_var_screeninfo _varInfo; private readonly IntPtr _address; - public LockedFramebuffer(int fb, fb_fix_screeninfo fixedInfo, fb_var_screeninfo varInfo, IntPtr address, Size dpi) + public LockedFramebuffer(int fb, fb_fix_screeninfo fixedInfo, fb_var_screeninfo varInfo, IntPtr address, Vector dpi) { _fb = fb; _fixedInfo = fixedInfo; @@ -41,7 +41,7 @@ namespace Avalonia.LinuxFramebuffer public int Width => (int)_varInfo.xres; public int Height => (int) _varInfo.yres; public int RowBytes => (int) _fixedInfo.line_length; - public Size Dpi { get; } + public Vector Dpi { get; } public PixelFormat Format => _varInfo.blue.offset == 16 ? PixelFormat.Rgba8888 : PixelFormat.Bgra8888; } } \ No newline at end of file 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.Xaml/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup.Xaml/Parsers/SelectorGrammar.cs index d11df1a9e9..671fdfff30 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Parsers/SelectorGrammar.cs @@ -93,9 +93,9 @@ namespace Avalonia.Markup.Xaml.Parsers public static readonly Parser Child = Parse.Char('>').Token().Return(new ChildSyntax()); - public static readonly Parser Descendent = + public static readonly Parser Descendant = from child in Parse.WhiteSpace.Many() - select new DescendentSyntax(); + select new DescendantSyntax(); public static readonly Parser Template = from template in Parse.String("/template/").Token() @@ -115,7 +115,7 @@ namespace Avalonia.Markup.Xaml.Parsers .Or(Property) .Or(Child) .Or(Template) - .Or(Descendent); + .Or(Descendant); public static readonly Parser> Selector = SingleSelector.Many().End(); @@ -191,11 +191,11 @@ namespace Avalonia.Markup.Xaml.Parsers } } - public class DescendentSyntax : ISyntax + public class DescendantSyntax : ISyntax { public override bool Equals(object obj) { - return obj is DescendentSyntax; + return obj is DescendantSyntax; } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Parsers/SelectorParser.cs b/src/Markup/Avalonia.Markup.Xaml/Parsers/SelectorParser.cs index c4a7e188c6..1cecb21f17 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Parsers/SelectorParser.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Parsers/SelectorParser.cs @@ -47,7 +47,7 @@ namespace Avalonia.Markup.Xaml.Parsers var name = i as SelectorGrammar.NameSyntax; var property = i as SelectorGrammar.PropertySyntax; var child = i as SelectorGrammar.ChildSyntax; - var descendent = i as SelectorGrammar.DescendentSyntax; + var descendant = i as SelectorGrammar.DescendantSyntax; var template = i as SelectorGrammar.TemplateSyntax; if (ofType != null) @@ -102,9 +102,9 @@ namespace Avalonia.Markup.Xaml.Parsers { result = result.Child(); } - else if (descendent != null) + else if (descendant != null) { - result = result.Descendent(); + result = result.Descendant(); } else if (template != 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/src/Skia/Avalonia.Skia/BitmapImpl.cs b/src/Skia/Avalonia.Skia/BitmapImpl.cs index e9c241b848..e5e8faec5f 100644 --- a/src/Skia/Avalonia.Skia/BitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/BitmapImpl.cs @@ -131,7 +131,7 @@ namespace Avalonia.Skia public int Width => _bmp.Width; public int Height => _bmp.Height; public int RowBytes => _bmp.RowBytes; - public Size Dpi { get; } = new Size(96, 96); + public Vector Dpi { get; } = new Vector(96, 96); public PixelFormat Format => _bmp.ColorType.ToPixelFormat(); } diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs index 0eacdf41ac..ae8e653e55 100644 --- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs @@ -76,7 +76,7 @@ namespace Avalonia.Skia canvas.RestoreToCount(0); canvas.Save(); canvas.ResetMatrix(); - var scale = Matrix.CreateScale(fb.Dpi.Width / 96, fb.Dpi.Height / 96); + var scale = Matrix.CreateScale(fb.Dpi.X / 96, fb.Dpi.Y / 96); return new DrawingContextImpl(canvas, visualBrushRenderer, scale, canvas, surface, shim, fb); } } diff --git a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj index ab62f5ac75..cf5a055df8 100644 --- a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj +++ b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj @@ -50,7 +50,9 @@ + + diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 469c29d626..3176dee7a4 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -135,12 +135,17 @@ namespace Avalonia.Direct2D1 public IRenderTarget CreateRenderTarget(IEnumerable surfaces) { - var nativeWindow = surfaces?.OfType().FirstOrDefault(); - if (nativeWindow != null) + foreach (var s in surfaces) { - if(nativeWindow.HandleDescriptor != "HWND") - throw new NotSupportedException("Don't know how to create a Direct2D1 renderer from " + nativeWindow.HandleDescriptor); - return new HwndRenderTarget(nativeWindow); + if (s is IPlatformHandle nativeWindow) + { + if (nativeWindow.HandleDescriptor != "HWND") + throw new NotSupportedException("Don't know how to create a Direct2D1 renderer from " + + nativeWindow.HandleDescriptor); + return new HwndRenderTarget(nativeWindow); + } + if (s is IExternalDirect2DRenderTargetSurface external) + return new ExternalRenderTarget(external, s_dwfactory); } throw new NotSupportedException("Don't know how to create a Direct2D1 renderer from any of provided surfaces"); } diff --git a/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs new file mode 100644 index 0000000000..b1c0e7e30a --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Direct2D1.Media; +using Avalonia.Platform; +using Avalonia.Rendering; +using SharpDX; +using DirectWriteFactory = SharpDX.DirectWrite.Factory; + +namespace Avalonia.Direct2D1 +{ + class ExternalRenderTarget : IRenderTarget + { + private readonly IExternalDirect2DRenderTargetSurface _externalRenderTargetProvider; + private readonly DirectWriteFactory _dwFactory; + private SharpDX.Direct2D1.RenderTarget _target; + public ExternalRenderTarget(IExternalDirect2DRenderTargetSurface externalRenderTargetProvider, + DirectWriteFactory dwFactory) + { + _externalRenderTargetProvider = externalRenderTargetProvider; + _dwFactory = dwFactory; + } + + public void Dispose() + { + _target?.Dispose(); + _target = null; + } + + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + { + _target = _target ?? _externalRenderTargetProvider.CreateRenderTarget(); + _externalRenderTargetProvider.BeforeDrawing(); + return new DrawingContextImpl(visualBrushRenderer, _target, _dwFactory, null, () => + { + try + { + _externalRenderTargetProvider.AfterDrawing(); + } + catch (SharpDXException ex) when ((uint) ex.HResult == 0x8899000C) // D2DERR_RECREATE_TARGET + { + _target?.Dispose(); + _target = null; + } + }); + } + } +} diff --git a/src/Windows/Avalonia.Direct2D1/IExternalDirect2DRenderTargetSurface.cs b/src/Windows/Avalonia.Direct2D1/IExternalDirect2DRenderTargetSurface.cs new file mode 100644 index 0000000000..0774c25937 --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/IExternalDirect2DRenderTargetSurface.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia.Direct2D1 +{ + public interface IExternalDirect2DRenderTargetSurface + { + SharpDX.Direct2D1.RenderTarget CreateRenderTarget(); + void BeforeDrawing(); + void AfterDrawing(); + } +} diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 535ca900c2..0b46ba1c47 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -23,6 +23,7 @@ namespace Avalonia.Direct2D1.Media private readonly IVisualBrushRenderer _visualBrushRenderer; private readonly SharpDX.Direct2D1.RenderTarget _renderTarget; private readonly SharpDX.DXGI.SwapChain1 _swapChain; + private readonly Action _finishedCallback; private SharpDX.DirectWrite.Factory _directWriteFactory; /// @@ -32,15 +33,18 @@ namespace Avalonia.Direct2D1.Media /// The render target to draw to. /// The DirectWrite factory. /// An optional swap chain associated with this drawing context. + /// An optional delegate to be called when context is disposed. public DrawingContextImpl( IVisualBrushRenderer visualBrushRenderer, SharpDX.Direct2D1.RenderTarget renderTarget, SharpDX.DirectWrite.Factory directWriteFactory, - SharpDX.DXGI.SwapChain1 swapChain = null) + SharpDX.DXGI.SwapChain1 swapChain = null, + Action finishedCallback = null) { _visualBrushRenderer = visualBrushRenderer; _renderTarget = renderTarget; _swapChain = swapChain; + _finishedCallback = finishedCallback; _directWriteFactory = directWriteFactory; _swapChain = swapChain; _renderTarget.BeginDraw(); @@ -73,6 +77,7 @@ namespace Avalonia.Direct2D1.Media _renderTarget.EndDraw(); _swapChain?.Present(1, SharpDX.DXGI.PresentFlags.None); + _finishedCallback?.Invoke(); } catch (SharpDXException ex) when ((uint)ex.HResult == 0x8899000C) // D2DERR_RECREATE_TARGET { diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WritableWicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WritableWicBitmapImpl.cs index 06eb26b407..5dc07e06c4 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WritableWicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WritableWicBitmapImpl.cs @@ -37,7 +37,7 @@ namespace Avalonia.Direct2D1.Media.Imaging public int Width => _lock.Size.Width; public int Height => _lock.Size.Height; public int RowBytes => _lock.Stride; - public Size Dpi { get; } = new Size(96, 96); + public Vector Dpi { get; } = new Vector(96, 96); public PixelFormat Format => _format; } diff --git a/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs b/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs index a55c808415..3d8d04d6cc 100644 --- a/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs +++ b/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs @@ -10,7 +10,7 @@ namespace Avalonia.Win32.Input { class WindowsMouseDevice : MouseDevice { - public new static WindowsMouseDevice Instance { get; } = new WindowsMouseDevice(); + public static WindowsMouseDevice Instance { get; } = new WindowsMouseDevice(); public WindowImpl CurrentWindow { diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index b6cfb03221..e02c67c0e6 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -66,7 +66,6 @@ namespace Avalonia.Win32 .Bind().ToSingleton() .Bind().ToConstant(CursorFactory.Instance) .Bind().ToConstant(WindowsKeyboardDevice.Instance) - .Bind().ToConstant(WindowsMouseDevice.Instance) .Bind().ToConstant(s_instance) .Bind().ToConstant(s_instance) .Bind().ToConstant(new RenderLoop(60)) diff --git a/src/Windows/Avalonia.Win32/WindowFramebuffer.cs b/src/Windows/Avalonia.Win32/WindowFramebuffer.cs index fe4fe5c668..df238c919e 100644 --- a/src/Windows/Avalonia.Win32/WindowFramebuffer.cs +++ b/src/Windows/Avalonia.Win32/WindowFramebuffer.cs @@ -39,7 +39,7 @@ namespace Avalonia.Win32 public int RowBytes => Width * 4; public PixelFormat Format => PixelFormat.Bgra8888; - public Size Dpi + public Vector Dpi { get { @@ -56,10 +56,10 @@ namespace Avalonia.Win32 out dpix, out dpiy) == 0) { - return new Size(dpix, dpiy); + return new Vector(dpix, dpiy); } } - return new Size(96, 96); + return new Vector(96, 96); } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index f2d7e0e043..f1537e53f5 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -133,6 +133,8 @@ namespace Avalonia.Win32 } } + public IMouseDevice MouseDevice => WindowsMouseDevice.Instance; + public WindowState WindowState { get diff --git a/src/iOS/Avalonia.iOS/EmulatedFramebuffer.cs b/src/iOS/Avalonia.iOS/EmulatedFramebuffer.cs index f3fc90a2ab..58cf6edd78 100644 --- a/src/iOS/Avalonia.iOS/EmulatedFramebuffer.cs +++ b/src/iOS/Avalonia.iOS/EmulatedFramebuffer.cs @@ -24,7 +24,7 @@ namespace Avalonia.iOS Width = (int) frame.Width * factor; Height = (int) frame.Height * factor; RowBytes = Width * 4; - Dpi = new Size(96, 96) * factor; + Dpi = new Vector(96, 96) * factor; Format = PixelFormat.Rgba8888; Address = Marshal.AllocHGlobal(Height * RowBytes); } @@ -53,7 +53,7 @@ namespace Avalonia.iOS public int Width { get; } public int Height { get; } public int RowBytes { get; } - public Size Dpi { get; } + public Vector Dpi { get; } public PixelFormat Format { get; } } } diff --git a/src/iOS/Avalonia.iOS/TopLevelImpl.cs b/src/iOS/Avalonia.iOS/TopLevelImpl.cs index 7949e331fe..cf4801dbb7 100644 --- a/src/iOS/Avalonia.iOS/TopLevelImpl.cs +++ b/src/iOS/Avalonia.iOS/TopLevelImpl.cs @@ -61,6 +61,8 @@ namespace Avalonia.iOS public Size ClientSize => Bounds.Size.ToAvalonia(); + public IMouseDevice MouseDevice => iOSPlatform.MouseDevice; + public override void Draw(CGRect rect) { Paint?.Invoke(new Rect(rect.X, rect.Y, rect.Width, rect.Height)); diff --git a/src/iOS/Avalonia.iOS/iOSPlatform.cs b/src/iOS/Avalonia.iOS/iOSPlatform.cs index 6d6e1fab03..4c4e497cd7 100644 --- a/src/iOS/Avalonia.iOS/iOSPlatform.cs +++ b/src/iOS/Avalonia.iOS/iOSPlatform.cs @@ -57,7 +57,6 @@ namespace Avalonia.iOS //.Bind().ToTransient() .Bind().ToTransient() .Bind().ToConstant(KeyboardDevice) - .Bind().ToConstant(MouseDevice) .Bind().ToConstant(ImmediateRenderer.Factory) .Bind().ToSingleton() .Bind().ToConstant(PlatformThreadingInterface.Instance) 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/Collections/AvaloniaListExtenionsTests.cs b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListExtenionsTests.cs new file mode 100644 index 0000000000..b996db8d48 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListExtenionsTests.cs @@ -0,0 +1,134 @@ +using System; +using System.Linq; +using Avalonia.Collections; +using Xunit; + +namespace Avalonia.Base.UnitTests.Collections +{ + public class AvaloniaListExtenionsTests + { + [Fact] + public void CreateDerivedList_Creates_Initial_Items() + { + var source = new AvaloniaList(new[] { 0, 1, 2, 3 }); + var target = source.CreateDerivedList(x => new Wrapper(x)); + var result = target.Select(x => x.Value).ToList(); + + Assert.Equal(source, result); + } + + [Fact] + public void CreateDerivedList_Handles_Add() + { + var source = new AvaloniaList(new[] { 0, 1, 2, 3 }); + var target = source.CreateDerivedList(x => new Wrapper(x)); + + source.Add(4); + + var result = target.Select(x => x.Value).ToList(); + + Assert.Equal(source, result); + } + + [Fact] + public void CreateDerivedList_Handles_Insert() + { + var source = new AvaloniaList(new[] { 0, 1, 2, 3 }); + var target = source.CreateDerivedList(x => new Wrapper(x)); + + source.Insert(1, 4); + + var result = target.Select(x => x.Value).ToList(); + + Assert.Equal(source, result); + } + + [Fact] + public void CreateDerivedList_Handles_Remove() + { + var source = new AvaloniaList(new[] { 0, 1, 2, 3 }); + var target = source.CreateDerivedList(x => new Wrapper(x)); + + source.Remove(2); + + var result = target.Select(x => x.Value).ToList(); + + Assert.Equal(source, result); + } + + [Fact] + public void CreateDerivedList_Handles_RemoveRange() + { + var source = new AvaloniaList(new[] { 0, 1, 2, 3 }); + var target = source.CreateDerivedList(x => new Wrapper(x)); + + source.RemoveRange(1, 2); + + var result = target.Select(x => x.Value).ToList(); + + Assert.Equal(source, result); + } + + [Fact] + public void CreateDerivedList_Handles_Move() + { + var source = new AvaloniaList(new[] { 0, 1, 2, 3 }); + var target = source.CreateDerivedList(x => new Wrapper(x)); + + source.Move(2, 0); + + var result = target.Select(x => x.Value).ToList(); + + Assert.Equal(source, result); + } + + [Fact] + public void CreateDerivedList_Handles_MoveRange() + { + var source = new AvaloniaList(new[] { 0, 1, 2, 3 }); + var target = source.CreateDerivedList(x => new Wrapper(x)); + + source.MoveRange(1, 2, 0); + + var result = target.Select(x => x.Value).ToList(); + + Assert.Equal(source, result); + } + + [Fact] + public void CreateDerivedList_Handles_Replace() + { + var source = new AvaloniaList(new[] { 0, 1, 2, 3 }); + var target = source.CreateDerivedList(x => new Wrapper(x)); + + source[1] = 4; + + var result = target.Select(x => x.Value).ToList(); + + Assert.Equal(source, result); + } + + [Fact] + public void CreateDerivedList_Handles_Clear() + { + var source = new AvaloniaList(new[] { 0, 1, 2, 3 }); + var target = source.CreateDerivedList(x => new Wrapper(x)); + + source.Clear(); + + var result = target.Select(x => x.Value).ToList(); + + Assert.Equal(source, result); + } + + private class Wrapper + { + public Wrapper(int value) + { + Value = value; + } + + public int Value { get; } + } + } +} 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/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index a588e88eb2..e58542bfb4 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -245,7 +245,7 @@ namespace Avalonia.Controls.UnitTests // The items were created before the template was applied, so now we need to go back // and re-arrange everything. - foreach (IControl i in target.GetSelfAndVisualDescendents()) + foreach (IControl i in target.GetSelfAndVisualDescendants()) { i.InvalidateMeasure(); } 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/Presenters/ScrollContentPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs index 33ed26d6d8..ab16552e12 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs @@ -219,7 +219,7 @@ namespace Avalonia.Controls.UnitTests.Presenters } [Fact] - public void BringDescendentIntoView_Should_Update_Offset() + public void BringDescendantIntoView_Should_Update_Offset() { var target = new ScrollContentPresenter { @@ -235,13 +235,13 @@ namespace Avalonia.Controls.UnitTests.Presenters target.UpdateChild(); target.Measure(Size.Infinity); target.Arrange(new Rect(0, 0, 100, 100)); - target.BringDescendentIntoView(target.Child, new Rect(200, 200, 0, 0)); + target.BringDescendantIntoView(target.Child, new Rect(200, 200, 0, 0)); Assert.Equal(new Vector(100, 100), target.Offset); } [Fact] - public void BringDescendentIntoView_Should_Handle_Child_Margin() + public void BringDescendantIntoView_Should_Handle_Child_Margin() { Border border; var target = new ScrollContentPresenter @@ -262,7 +262,7 @@ namespace Avalonia.Controls.UnitTests.Presenters target.UpdateChild(); target.Measure(Size.Infinity); target.Arrange(new Rect(0, 0, 100, 100)); - target.BringDescendentIntoView(border, new Rect(200, 200, 0, 0)); + target.BringDescendantIntoView(border, new Rect(200, 200, 0, 0)); Assert.Equal(new Vector(150, 150), target.Offset); } 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/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 367070b37a..f192e87f08 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -222,7 +222,7 @@ namespace Avalonia.Controls.UnitTests.Primitives popup.Open(); var popupRoot = popup.PopupRoot; - var children = popupRoot.GetVisualDescendents().ToList(); + var children = popupRoot.GetVisualDescendants().ToList(); var types = children.Select(x => x.GetType().Name).ToList(); Assert.Equal( diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs index 3c2f2e4f5c..72c8073f21 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs @@ -78,7 +78,7 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); - var types = target.GetVisualDescendents().Select(x => x.GetType()).ToList(); + var types = target.GetVisualDescendants().Select(x => x.GetType()).ToList(); Assert.Equal( new[] @@ -135,7 +135,7 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); - var templatedParents = target.GetVisualDescendents() + var templatedParents = target.GetVisualDescendants() .OfType() .Select(x => x.TemplatedParent) .ToList(); @@ -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 eb0ee5a231..52d36a33fa 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -80,7 +80,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Root_TreeContainerFromItem_Should_Return_Descendent_Item() + public void Root_TreeContainerFromItem_Should_Return_Descendant_Item() { var tree = CreateTestTreeData(); var target = new TreeView diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 96afecc966..e0dd908bbb 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -4,6 +4,7 @@ // // ----------------------------------------------------------------------- +using System.Collections.Generic; using Avalonia.Platform; using Avalonia.UnitTests; using Moq; @@ -114,5 +115,81 @@ namespace Avalonia.Controls.UnitTests Assert.False(window.IsVisible); } } + + [Fact] + public void Show_Should_Add_Window_To_OpenWindows() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + ClearOpenWindows(); + var window = new Window(); + + window.Show(); + + Assert.Equal(new[] { window }, Window.OpenWindows); + } + } + + [Fact] + public void Window_Should_Be_Added_To_OpenWindows_Only_Once() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + ClearOpenWindows(); + var window = new Window(); + + window.Show(); + window.Show(); + window.IsVisible = true; + + Assert.Equal(new[] { window }, Window.OpenWindows); + + window.Close(); + } + } + + [Fact] + public void Close_Should_Remove_Window_From_OpenWindows() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + ClearOpenWindows(); + var window = new Window(); + + window.Show(); + window.Close(); + + Assert.Empty(Window.OpenWindows); + } + } + + [Fact] + public void Impl_Closing_Should_Remove_Window_From_OpenWindows() + { + var windowImpl = new Mock(); + windowImpl.SetupProperty(x => x.Closed); + windowImpl.Setup(x => x.Scaling).Returns(1); + + var services = TestServices.StyledWindow.With( + windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object)); + + using (UnitTestApplication.Start(services)) + { + ClearOpenWindows(); + var window = new Window(); + + window.Show(); + windowImpl.Object.Closed(); + + Assert.Empty(Window.OpenWindows); + } + } + + private void ClearOpenWindows() + { + // HACK: We really need a decent way to have "statics" that can be scoped to + // AvaloniaLocator scopes. + ((IList)Window.OpenWindows).Clear(); + } } } 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.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.Interactivity.UnitTests/InteractiveTests.cs b/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs index dd0974fad0..067cf85c3c 100644 --- a/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs +++ b/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs @@ -391,7 +391,7 @@ namespace Avalonia.Interactivity.UnitTests if (handler != null) { - foreach (var i in tree.GetSelfAndVisualDescendents().Cast()) + foreach (var i in tree.GetSelfAndVisualDescendants().Cast()) { i.AddHandler(ev, handler, handlerRoutes, handledEventsToo); } 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/MeasureTests.cs b/tests/Avalonia.Layout.UnitTests/MeasureTests.cs index 570628b5b7..99983fd19b 100644 --- a/tests/Avalonia.Layout.UnitTests/MeasureTests.cs +++ b/tests/Avalonia.Layout.UnitTests/MeasureTests.cs @@ -45,7 +45,7 @@ namespace Avalonia.Layout.UnitTests } [Fact] - public void Removing_From_Parent_Should_Invalidate_Measure_Of_Control_And_Descendents() + public void Removing_From_Parent_Should_Invalidate_Measure_Of_Control_And_Descendants() { var panel = new StackPanel(); var child2 = new Border(); 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 959dce7181..60174e65e6 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,30 +38,36 @@ 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_BindingNotification_Error_For_Root_Null() + public async Task Should_Return_BindingNotification_Error_For_Root_Null() { var data = new Class3 { Foo = "foo" }; var target = new ExpressionObserver(default(object), "Foo"); @@ -70,10 +79,12 @@ namespace Avalonia.Markup.UnitTests.Data BindingErrorType.Error, AvaloniaProperty.UnsetValue), result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Return_BindingNotification_Error_For_Root_UnsetValue() + public async Task Should_Return_BindingNotification_Error_For_Root_UnsetValue() { var data = new Class3 { Foo = "foo" }; var target = new ExpressionObserver(AvaloniaProperty.UnsetValue, "Foo"); @@ -85,10 +96,12 @@ namespace Avalonia.Markup.UnitTests.Data BindingErrorType.Error, AvaloniaProperty.UnsetValue), result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Return_BindingNotification_Error_For_Observable_Root_Null() + public async Task Should_Return_BindingNotification_Error_For_Observable_Root_Null() { var data = new Class3 { Foo = "foo" }; var target = new ExpressionObserver(Observable.Return(default(object)), "Foo"); @@ -100,6 +113,8 @@ namespace Avalonia.Markup.UnitTests.Data BindingErrorType.Error, AvaloniaProperty.UnsetValue), result); + + GC.KeepAlive(data); } [Fact] @@ -115,16 +130,20 @@ namespace Avalonia.Markup.UnitTests.Data BindingErrorType.Error, 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 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] @@ -136,10 +155,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"); @@ -151,6 +172,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] @@ -171,6 +194,8 @@ namespace Avalonia.Markup.UnitTests.Data AvaloniaProperty.UnsetValue), }, result); + + GC.KeepAlive(data); } [Fact] @@ -180,6 +205,8 @@ namespace Avalonia.Markup.UnitTests.Data var target = new ExpressionObserver(data, "Foo.Bar.Baz"); Assert.Null(target.ResultType); + + GC.KeepAlive(data); } [Fact] @@ -197,6 +224,8 @@ namespace Avalonia.Markup.UnitTests.Data sub.Dispose(); Assert.Equal(0, data.PropertyChangedSubscriptionCount); + + GC.KeepAlive(data); } [Fact] @@ -206,7 +235,7 @@ namespace Avalonia.Markup.UnitTests.Data var target = new ExpressionObserver(data, "Bar"); var result = new List(); - var sub = target.Subscribe(x => result.Add(x)); + var sub = target.Subscribe(x => result.Add(x)); Assert.Equal(new[] { "foo" }, result); @@ -225,6 +254,8 @@ namespace Avalonia.Markup.UnitTests.Data sub.Dispose(); Assert.Equal(0, data.PropertyChangedSubscriptionCount); + + GC.KeepAlive(data); } [Fact] @@ -244,6 +275,8 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(0, data.PropertyChangedSubscriptionCount); Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount); + + GC.KeepAlive(data); } [Fact] @@ -265,6 +298,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] @@ -290,7 +325,7 @@ namespace Avalonia.Markup.UnitTests.Data data.Next = old; Assert.Equal( - new object[] + new object[] { "bar", new BindingNotification( @@ -298,7 +333,7 @@ namespace Avalonia.Markup.UnitTests.Data BindingErrorType.Error, AvaloniaProperty.UnsetValue), "bar" - }, + }, result); sub.Dispose(); @@ -306,6 +341,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] @@ -338,6 +375,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] @@ -354,6 +393,8 @@ namespace Avalonia.Markup.UnitTests.Data update.OnNext(Unit.Default); Assert.Equal(new[] { "foo", "bar" }, result); + + GC.KeepAlive(data); } [Fact] @@ -394,6 +435,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] @@ -411,6 +454,8 @@ namespace Avalonia.Markup.UnitTests.Data sub2.Dispose(); Assert.Equal(0, data.PropertyChangedSubscriptionCount); + + GC.KeepAlive(data); } [Fact] @@ -425,6 +470,8 @@ namespace Avalonia.Markup.UnitTests.Data } Assert.Equal("bar", data.Foo); + + GC.KeepAlive(data); } [Fact] @@ -439,18 +486,22 @@ namespace Avalonia.Markup.UnitTests.Data } Assert.Equal("baz", ((Class2)data.Next).Bar); + + GC.KeepAlive(data); } [Fact] public void SetValue_Should_Return_False_For_Missing_Property() { - var data = new Class1 { Next = new WithoutBar()}; + var data = new Class1 { Next = new WithoutBar() }; var target = new ExpressionObserver(data, "Next.Bar"); using (target.Subscribe(_ => { })) { Assert.False(target.SetValue("baz")); } + + GC.KeepAlive(data); } [Fact] @@ -464,6 +515,8 @@ namespace Avalonia.Markup.UnitTests.Data target.SetValue("bar"); Assert.Equal(new[] { null, "bar" }, result); + + GC.KeepAlive(data); } [Fact] @@ -477,6 +530,8 @@ namespace Avalonia.Markup.UnitTests.Data target.SetValue("bar"); Assert.Equal(new[] { null, "bar" }, result); + + GC.KeepAlive(data); } [Fact] @@ -489,6 +544,8 @@ namespace Avalonia.Markup.UnitTests.Data { Assert.False(target.SetValue("baz")); } + + GC.KeepAlive(data); } [Fact] @@ -508,7 +565,7 @@ namespace Avalonia.Markup.UnitTests.Data update.OnNext(Unit.Default); Assert.Equal( - new object[] + new object[] { "foo", "bar", @@ -516,11 +573,14 @@ namespace Avalonia.Markup.UnitTests.Data new MarkupBindingChainException("Null value", "Foo", string.Empty), BindingErrorType.Error, AvaloniaProperty.UnsetValue) - }, + }, result); 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/Parsers/SelectorGrammarTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/SelectorGrammarTests.cs index 676d6288d6..ad2c1bf8d3 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/SelectorGrammarTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/SelectorGrammarTests.cs @@ -157,7 +157,7 @@ namespace Avalonia.Xaml.Base.UnitTest.Parsers } [Fact] - public void OfType_Descendent_Class() + public void OfType_Descendant_Class() { var result = SelectorGrammar.Selector.Parse("Button .foo").ToList(); @@ -165,7 +165,7 @@ namespace Avalonia.Xaml.Base.UnitTest.Parsers new SelectorGrammar.ISyntax[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, - new SelectorGrammar.DescendentSyntax { }, + new SelectorGrammar.DescendantSyntax { }, new SelectorGrammar.ClassSyntax { Class = "foo" }, }, result); 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.RenderTests/Media/BitmapTests.cs b/tests/Avalonia.RenderTests/Media/BitmapTests.cs index 2e6daa8554..9e0ac5cf14 100644 --- a/tests/Avalonia.RenderTests/Media/BitmapTests.cs +++ b/tests/Avalonia.RenderTests/Media/BitmapTests.cs @@ -43,7 +43,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media public IntPtr Address { get; } - public Size Dpi { get; } = new Size(96, 96); + public Vector Dpi { get; } = new Vector(96, 96); public PixelFormat Format { get; } 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 38491f9164..7cf8c3dd1c 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs @@ -14,23 +14,23 @@ using Xunit; namespace Avalonia.Styling.UnitTests { - public class SelectorTests_Descendent + public class SelectorTests_Descendant { [Fact] - public void Descendent_Matches_Control_When_It_Is_Child_OfType() + public void Descendant_Matches_Control_When_It_Is_Child_OfType() { var parent = new TestLogical1(); var child = new TestLogical2(); child.LogicalParent = parent; - var selector = default(Selector).OfType().Descendent().OfType(); + var selector = default(Selector).OfType().Descendant().OfType(); Assert.True(selector.Match(child).ImmediateResult); } [Fact] - public void Descendent_Matches_Control_When_It_Is_Descendent_OfType() + public void Descendant_Matches_Control_When_It_Is_Descendant_OfType() { var grandparent = new TestLogical1(); var parent = new TestLogical2(); @@ -39,13 +39,13 @@ namespace Avalonia.Styling.UnitTests parent.LogicalParent = grandparent; child.LogicalParent = parent; - var selector = default(Selector).OfType().Descendent().OfType(); + var selector = default(Selector).OfType().Descendant().OfType(); Assert.True(selector.Match(child).ImmediateResult); } [Fact] - public async Task Descendent_Matches_Control_When_It_Is_Descendent_OfType_And_Class() + public async Task Descendant_Matches_Control_When_It_Is_Descendant_OfType_And_Class() { var grandparent = new TestLogical1(); var parent = new TestLogical2(); @@ -55,14 +55,14 @@ namespace Avalonia.Styling.UnitTests parent.LogicalParent = grandparent; child.LogicalParent = parent; - var selector = default(Selector).OfType().Class("foo").Descendent().OfType(); + var selector = default(Selector).OfType().Class("foo").Descendant().OfType(); var activator = selector.Match(child).ObservableResult; Assert.True(await activator.Take(1)); } [Fact] - public async Task Descendent_Doesnt_Match_Control_When_It_Is_Descendent_OfType_But_Wrong_Class() + public async Task Descendant_Doesnt_Match_Control_When_It_Is_Descendant_OfType_But_Wrong_Class() { var grandparent = new TestLogical1(); var parent = new TestLogical2(); @@ -73,14 +73,14 @@ namespace Avalonia.Styling.UnitTests parent.Classes.Add("foo"); child.LogicalParent = parent; - var selector = default(Selector).OfType().Class("foo").Descendent().OfType(); + var selector = default(Selector).OfType().Class("foo").Descendant().OfType(); var activator = selector.Match(child).ObservableResult; Assert.False(await activator.Take(1)); } [Fact] - public async Task Descendent_Matches_Any_Ancestor() + public async Task Descendant_Matches_Any_Ancestor() { var grandparent = new TestLogical1(); var parent = new TestLogical1(); @@ -89,7 +89,7 @@ namespace Avalonia.Styling.UnitTests parent.LogicalParent = grandparent; child.LogicalParent = parent; - var selector = default(Selector).OfType().Class("foo").Descendent().OfType(); + var selector = default(Selector).OfType().Class("foo").Descendant().OfType(); var activator = selector.Match(child).ObservableResult; Assert.False(await activator.Take(1)); @@ -104,9 +104,9 @@ namespace Avalonia.Styling.UnitTests } [Fact] - public void Descendent_Selector_Should_Have_Correct_String_Representation() + public void Descendant_Selector_Should_Have_Correct_String_Representation() { - var selector = default(Selector).OfType().Class("foo").Descendent().OfType(); + var selector = default(Selector).OfType().Class("foo").Descendant().OfType(); Assert.Equal("TestLogical1.foo TestLogical3", selector.ToString()); } @@ -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.Styling.UnitTests/SelectorTests_Multiple.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Multiple.cs index 5a06734819..067b08b296 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Multiple.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Multiple.cs @@ -91,11 +91,11 @@ namespace Avalonia.Styling.UnitTests } [Fact] - public void TargetType_Descendent() + public void TargetType_Descendant() { var selector = default(Selector) .OfType