Browse Source

Merge branch 'master' into extract-layout-manager

 Conflicts:
	src/Avalonia.Layout/LayoutManager.cs
	src/Avalonia.Layout/Layoutable.cs
	tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs
	tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs
Steven Kirk 8 years ago
parent
commit
c14fe7f2b7
29 changed files with 618 additions and 107 deletions
  1. 2 1
      .gitignore
  2. 13 12
      build.cake
  3. 1 1
      build/Moq.props
  4. 4 2
      build/XUnit.props
  5. 1 0
      samples/interop/WindowsInteropTest/WindowsInteropTest.csproj
  6. 64 33
      src/Avalonia.Layout/LayoutManager.cs
  7. 1 0
      tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj
  8. 1 1
      tests/Avalonia.Base.UnitTests/Properties/AssemblyInfo.cs
  9. 2 1
      tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
  10. 65 0
      tests/Avalonia.Benchmarks/Layout/Measure.cs
  11. 1 0
      tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs
  12. 1 0
      tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj
  13. 1 0
      tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj
  14. 1 0
      tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj
  15. 1 0
      tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj
  16. 262 3
      tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs
  17. 29 0
      tests/Avalonia.Layout.UnitTests/LayoutTestControl.cs
  18. 43 0
      tests/Avalonia.Layout.UnitTests/LayoutTestRoot.cs
  19. 90 0
      tests/Avalonia.Layout.UnitTests/LayoutableTests.cs
  20. 0 41
      tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs
  21. 1 0
      tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj
  22. 24 6
      tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs
  23. 1 1
      tests/Avalonia.Markup.UnitTests/Properties/AssemblyInfo.cs
  24. 1 0
      tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj
  25. 1 1
      tests/Avalonia.Markup.Xaml.UnitTests/Properties/AssemblyInfo.cs
  26. 1 0
      tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj
  27. 1 3
      tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
  28. 1 1
      tests/Avalonia.UnitTests/TestRoot.cs
  29. 4 0
      tools/packages.config

+ 2 - 1
.gitignore

@@ -162,7 +162,8 @@ $RECYCLE.BIN/
 #################
 ## Cake
 #################
-tools/
+tools/*
+!tools/packages.config
 .nuget
 artifacts/
 nuget

+ 13 - 12
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")

+ 1 - 1
build/Moq.props

@@ -1,5 +1,5 @@
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <ItemGroup>
-    <PackageReference Include="Moq" Version="4.7.1" />
+    <PackageReference Include="Moq" Version="4.7.25" />
   </ItemGroup>
 </Project>

+ 4 - 2
build/XUnit.props

@@ -7,7 +7,9 @@
     <PackageReference Include="xunit.extensibility.core" Version="2.2.0" />
     <PackageReference Include="xunit.extensibility.execution" Version="2.2.0" />
     <PackageReference Include="xunit.runner.console" Version="2.2.0" />
-    <PackageReference Condition="'$(TargetFramework)' == 'net461'" Include="xunit.runner.visualstudio" Version="2.2.0" />
-    <PackageReference Condition="'$(TargetFramework)' == 'netcoreapp1.1'" Include="Microsoft.NET.Test.Sdk" Version="15.0.0" />
+    <PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
+  </ItemGroup>
+  <ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp1.1'">
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" />
   </ItemGroup>
 </Project>

+ 1 - 0
samples/interop/WindowsInteropTest/WindowsInteropTest.csproj

@@ -181,5 +181,6 @@
   </ItemGroup>
   <Import Project="..\..\..\build\Rx.props" />
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+  <Import Project="..\..\..\build\SkiaSharp.props" />
   <Import Project="$(MSBuildThisFileDirectory)..\..\..\src\Shared\nuget.workaround.targets" />
 </Project>

+ 64 - 33
src/Avalonia.Layout/LayoutManager.cs

@@ -14,8 +14,8 @@ namespace Avalonia.Layout
     /// </summary>
     public class LayoutManager : ILayoutManager
     {
-        private readonly HashSet<ILayoutable> _toMeasure = new HashSet<ILayoutable>();
-        private readonly HashSet<ILayoutable> _toArrange = new HashSet<ILayoutable>();
+        private readonly Queue<ILayoutable> _toMeasure = new Queue<ILayoutable>();
+        private readonly Queue<ILayoutable> _toArrange = new Queue<ILayoutable>();
         private bool _queued;
         private bool _running;
 
@@ -25,8 +25,18 @@ namespace Avalonia.Layout
             Contract.Requires<ArgumentNullException>(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();
         }
 
@@ -36,7 +46,17 @@ namespace Avalonia.Layout
             Contract.Requires<ArgumentNullException>(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();
         }
 
@@ -103,8 +123,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);
+                }
             }
         }
 
@@ -112,53 +136,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 ?? default(Size));
+                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()

+ 1 - 0
tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj

@@ -1,6 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
   <PropertyGroup>
     <TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
+    <OutputType>Library</OutputType>
   </PropertyGroup>
   <Import Project="..\..\build\UnitTests.NetCore.targets" />
   <Import Project="..\..\build\Moq.props" />

+ 1 - 1
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)]
+[assembly: CollectionBehavior(MaxParallelThreads = 1)]

+ 2 - 1
tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj

@@ -49,6 +49,7 @@
     <Reference Include="System.Xml" />
   </ItemGroup>
   <ItemGroup>
+    <Compile Include="Layout\Measure.cs" />
     <Compile Include="Styling\ApplyStyling.cs" />
     <Compile Include="Program.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
@@ -100,7 +101,7 @@
   </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <ItemGroup>
-    <PackageReference Include="BenchmarkDotNet" Version="0.9.2" />
+    <PackageReference Include="BenchmarkDotNet" Version="0.10.8" />
   </ItemGroup>
   <Import Project="$(MSBuildThisFileDirectory)..\..\src\Shared\nuget.workaround.targets" />
 </Project>

+ 65 - 0
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<Control> controls = new List<Control>();
+
+        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);
+            }
+        }
+    }
+}

+ 1 - 0
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;

+ 1 - 0
tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj

@@ -1,6 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
   <PropertyGroup>
     <TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
+    <OutputType>Library</OutputType>
   </PropertyGroup>
   <Import Project="..\..\build\UnitTests.NetCore.targets" />
   <Import Project="..\..\build\Moq.props" />

+ 1 - 0
tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj

@@ -1,6 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
   <PropertyGroup>
     <TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
+    <OutputType>Library</OutputType>
   </PropertyGroup>
   <Import Project="..\..\build\UnitTests.NetCore.targets" />
   <Import Project="..\..\build\Moq.props" />

+ 1 - 0
tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj

@@ -1,6 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
   <PropertyGroup>
     <TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
+    <OutputType>Library</OutputType>
   </PropertyGroup>
   <Import Project="..\..\build\UnitTests.NetCore.targets" />
   <Import Project="..\..\build\XUnit.props" />

+ 1 - 0
tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj

@@ -1,6 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
   <PropertyGroup>
     <TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
+    <OutputType>Library</OutputType>
   </PropertyGroup>
   <Import Project="..\..\build\UnitTests.NetCore.targets" />
   <Import Project="..\..\build\Moq.props" />

+ 262 - 3
tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs

@@ -2,22 +2,274 @@
 // 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 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();
+                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<ILayoutable>();
+                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<ILayoutable>();
+                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()
         {
             using (AvaloniaLocator.EnterScope())
             {
-                
+                AvaloniaLocator.CurrentMutable.Bind<ILayoutManager>().ToConstant(layoutManager);
+
                 Border border;
                 StackPanel panel;
 
-                var root = new TestLayoutRoot
+                var root = new LayoutTestRoot
                 {
                     Child = panel = new StackPanel
                     {
@@ -38,5 +290,12 @@ namespace Avalonia.Layout.UnitTests
                 Assert.Equal(new Size(100, 100), panel.DesiredSize);
             }                
         }
+
+        private IDisposable Start(LayoutManager layoutManager)
+        {
+            var result = AvaloniaLocator.EnterScope();
+            AvaloniaLocator.CurrentMutable.Bind<ILayoutManager>().ToConstant(layoutManager);
+            return result;
+        }
     }
 }

+ 29 - 0
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<ILayoutable, Size, Size> DoMeasureOverride { get; set; }
+        public Func<ILayoutable, Size, Size> 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);
+        }
+    }
+}

+ 43 - 0
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<ILayoutable, Size, Size> DoMeasureOverride { get; set; }
+        public Func<ILayoutable, Size, Size> 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);
+        }
+    }
+}

+ 90 - 0
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<ILayoutManager>();
+
+            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<ILayoutManager>();
+
+            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<ILayoutManager>();
+
+            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<ILayoutManager>().ToConstant(layoutManager);
+            return result;
+        }
+    }
+}

+ 0 - 41
tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs

@@ -1,41 +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;
-using Avalonia.Platform;
-using Avalonia.Rendering;
-
-namespace Avalonia.Layout.UnitTests
-{
-    internal class TestLayoutRoot : Decorator, ILayoutRoot, IRenderRoot
-    {
-        public TestLayoutRoot()
-        {
-            ClientSize = new Size(500, 500);
-        }
-
-        public Size ClientSize
-        {
-            get;
-            set;
-        }
-
-        public IRenderer Renderer => null;
-
-        public IRenderTarget CreateRenderTarget() => null;
-
-        public void Invalidate(Rect rect)
-        {
-        }
-
-        public Point PointToClient(Point point) => point;
-
-        public Point PointToScreen(Point point) => point;
-
-        public Size MaxClientSize => Size.Infinity;
-        public double LayoutScaling => 1;
-
-        public ILayoutManager LayoutManager { get; set; } = new LayoutManager();
-        
-    }
-}

+ 1 - 0
tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj

@@ -1,6 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
   <PropertyGroup>
     <TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
+    <OutputType>Library</OutputType>
   </PropertyGroup>
   <Import Project="..\..\build\UnitTests.NetCore.targets" />
   <Import Project="..\..\build\Moq.props" />

+ 24 - 6
tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs

@@ -183,7 +183,7 @@ namespace Avalonia.Markup.UnitTests.Data
                 result);
         }
 
-        [Fact]
+        [Fact(Skip="Result is not always AggregateException.")]
         public async void Should_Return_BindingNotification_For_Invalid_FallbackValue()
         {
 #if NET461
@@ -203,13 +203,13 @@ 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);
         }
 
-        [Fact]
+        [Fact(Skip="Result is not always AggregateException.")]
         public async void Should_Return_BindingNotification_For_Invalid_FallbackValue_With_Data_Validation()
         {
 #if NET461
@@ -229,7 +229,7 @@ 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);
@@ -286,6 +286,12 @@ namespace Avalonia.Markup.UnitTests.Data
         [Fact]
         public void Should_Pass_ConverterParameter_To_Convert()
         {
+#if NET461
+            Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
+#else
+            CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture;
+#endif
+
             var data = new Class1 { DoubleValue = 5.6 };
             var converter = new Mock<IValueConverter>();
             var target = new BindingExpression(
@@ -296,12 +302,18 @@ 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.InvariantCulture));
         }
 
         [Fact]
         public void Should_Pass_ConverterParameter_To_ConvertBack()
         {
+#if NET461
+            Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
+#else
+            CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture;
+#endif
+
             var data = new Class1 { DoubleValue = 5.6 };
             var converter = new Mock<IValueConverter>();
             var target = new BindingExpression(
@@ -312,12 +324,18 @@ 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.InvariantCulture));
         }
 
         [Fact]
         public void Should_Handle_DataValidation()
         {
+#if NET461
+            Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
+#else
+            CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture;
+#endif
+
             var data = new Class1 { DoubleValue = 5.6 };
             var converter = new Mock<IValueConverter>();
             var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue", true), typeof(string));

+ 1 - 1
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)]
+[assembly: CollectionBehavior(MaxParallelThreads = 1)]

+ 1 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj

@@ -1,6 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
   <PropertyGroup>
     <TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
+    <OutputType>Library</OutputType>
   </PropertyGroup>
   <Import Project="..\..\build\UnitTests.NetCore.targets" />
   <Import Project="..\..\build\Moq.props" />

+ 1 - 1
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)]
+[assembly: CollectionBehavior(MaxParallelThreads = 1)]

+ 1 - 0
tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj

@@ -1,6 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
   <PropertyGroup>
     <TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
+    <OutputType>Library</OutputType>
   </PropertyGroup>
   <Import Project="..\..\build\UnitTests.NetCore.targets" />
   <Import Project="..\..\build\Moq.props" />

+ 1 - 3
tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj

@@ -2,6 +2,7 @@
   <PropertyGroup>
     <TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+    <OutputType>Library</OutputType>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
     <DebugSymbols>true</DebugSymbols>
@@ -51,8 +52,5 @@
   <Import Project="..\..\build\Moq.props" />
   <Import Project="..\..\build\Rx.props" />
   <Import Project="..\..\build\XUnit.props" />
-  <ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp1.1'">
-      <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" />
-  </ItemGroup>
   <Import Condition="'$(TargetFramework)' == 'net461'" Project="$(MSBuildThisFileDirectory)..\..\src\Shared\nuget.workaround.targets" />
 </Project>

+ 1 - 1
tests/Avalonia.UnitTests/TestRoot.cs

@@ -43,7 +43,7 @@ namespace Avalonia.UnitTests
 
         public Size ClientSize => new Size(100, 100);
 
-        public Size MaxClientSize => Size.Infinity;
+        public Size MaxClientSize { get; set; } = Size.Infinity;
 
         public double LayoutScaling => 1;
 

+ 4 - 0
tools/packages.config

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+    <package id="Cake" version="0.18.0" />
+</packages>