From eb6bfd3de86dc8e5c909c5eb51c86e998f7c76c4 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 6 Jun 2017 00:46:47 +0300 Subject: [PATCH 01/94] Moved layout manager from service locator to ILayoutRoot --- src/Avalonia.Controls/Application.cs | 1 - .../Embedding/EmbeddableControlRoot.cs | 2 +- .../Presenters/ItemVirtualizerSimple.cs | 2 +- src/Avalonia.Controls/TopLevel.cs | 15 +++++++++- src/Avalonia.Controls/Window.cs | 4 +-- src/Avalonia.Controls/WindowBase.cs | 4 +-- src/Avalonia.Layout/ILayoutRoot.cs | 5 ++++ src/Avalonia.Layout/LayoutManager.cs | 5 ---- src/Avalonia.Layout/Layoutable.cs | 13 +++++++-- .../ItemsPresenterTests_Virtualization.cs | 12 ++++++-- ...emsPresenterTests_Virtualization_Simple.cs | 10 +++---- .../Primitives/PopupTests.cs | 1 - .../TopLevelTests.cs | 17 +++++++---- .../WindowBaseTests.cs | 2 +- .../FullLayoutTests.cs | 7 ++--- .../LayoutManagerTests.cs | 11 +++----- .../TestLayoutRoot.cs | 19 ++++++++++++- tests/Avalonia.LeakTests/ControlTests.cs | 28 +++++++++---------- tests/Avalonia.UnitTests/TestRoot.cs | 2 +- tests/Avalonia.UnitTests/TestServices.cs | 11 +------- tests/Avalonia.UnitTests/TestTemplatedRoot.cs | 2 +- .../Avalonia.UnitTests/UnitTestApplication.cs | 1 - 22 files changed, 104 insertions(+), 70 deletions(-) diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 3d13608226..8122af36ce 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -175,7 +175,6 @@ namespace Avalonia .Bind().ToConstant(InputManager) .Bind().ToTransient() .Bind().ToConstant(_styler) - .Bind().ToSingleton() .Bind().ToConstant(this) .Bind().ToConstant(AvaloniaScheduler.Instance); } diff --git a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs index b8d54fa67b..d06172b4b3 100644 --- a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs +++ b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs @@ -26,7 +26,7 @@ namespace Avalonia.Controls.Embedding { EnsureInitialized(); ApplyTemplate(); - LayoutManager.Instance.ExecuteInitialLayoutPass(this); + LayoutManager.ExecuteInitialLayoutPass(this); } private void EnsureInitialized() diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 20602d5475..38e4d2c5cc 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -510,7 +510,7 @@ namespace Avalonia.Controls.Presenters } var container = generator.ContainerFromIndex(index); - var layoutManager = LayoutManager.Instance; + var layoutManager = (Owner.GetVisualRoot() as ILayoutRoot)?.LayoutManager; // We need to do a layout here because it's possible that the container we moved to // is only partially visible due to differing item sizes. If the container is only diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index a0a8f6b27e..516b6f7e7d 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -46,6 +46,7 @@ namespace Avalonia.Controls private readonly IApplicationLifecycle _applicationLifecycle; private readonly IPlatformRenderInterface _renderInterface; private Size _clientSize; + private ILayoutManager _layoutManager; /// /// Initializes static members of the class. @@ -133,6 +134,18 @@ namespace Avalonia.Controls protected set { SetAndRaise(ClientSizeProperty, ref _clientSize, value); } } + protected virtual ILayoutManager CreateLayoutManager() => new LayoutManager(); + + public ILayoutManager LayoutManager + { + get + { + if (_layoutManager == null) + _layoutManager = CreateLayoutManager(); + return _layoutManager; + } + } + /// /// Gets the platform-specific window implementation. /// @@ -245,7 +258,7 @@ namespace Avalonia.Controls ClientSize = clientSize; Width = clientSize.Width; Height = clientSize.Height; - LayoutManager.Instance.ExecuteLayoutPass(); + LayoutManager.ExecuteLayoutPass(); Renderer?.Resized(clientSize); } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 3802f2b6ea..3421bd3f32 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -247,7 +247,7 @@ namespace Avalonia.Controls EnsureInitialized(); IsVisible = true; - LayoutManager.Instance.ExecuteInitialLayoutPass(this); + LayoutManager.ExecuteInitialLayoutPass(this); using (BeginAutoSizing()) { @@ -286,7 +286,7 @@ namespace Avalonia.Controls EnsureInitialized(); IsVisible = true; - LayoutManager.Instance.ExecuteInitialLayoutPass(this); + LayoutManager.ExecuteInitialLayoutPass(this); using (BeginAutoSizing()) { diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 1f484fd6cb..990d4d201c 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -136,7 +136,7 @@ namespace Avalonia.Controls { EnsureInitialized(); IsVisible = true; - LayoutManager.Instance.ExecuteInitialLayoutPass(this); + LayoutManager.ExecuteInitialLayoutPass(this); PlatformImpl?.Show(); } finally @@ -215,7 +215,7 @@ namespace Avalonia.Controls Height = clientSize.Height; } ClientSize = clientSize; - LayoutManager.Instance.ExecuteLayoutPass(); + LayoutManager.ExecuteLayoutPass(); Renderer?.Resized(clientSize); } diff --git a/src/Avalonia.Layout/ILayoutRoot.cs b/src/Avalonia.Layout/ILayoutRoot.cs index 25a6331b38..700b6a8600 100644 --- a/src/Avalonia.Layout/ILayoutRoot.cs +++ b/src/Avalonia.Layout/ILayoutRoot.cs @@ -22,5 +22,10 @@ namespace Avalonia.Layout /// The scaling factor to use in layout. /// double LayoutScaling { get; } + + /// + /// Associated instance of layout manager + /// + ILayoutManager LayoutManager { get; } } } diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index b7b83bf852..ebf6411beb 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -19,11 +19,6 @@ namespace Avalonia.Layout private bool _queued; private bool _running; - /// - /// Gets the layout manager. - /// - public static ILayoutManager Instance => AvaloniaLocator.Current.GetService(); - /// public void InvalidateMeasure(ILayoutable control) { diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index 20050058bf..bea62efe50 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -378,7 +378,7 @@ namespace Avalonia.Layout IsMeasureValid = false; IsArrangeValid = false; - LayoutManager.Instance?.InvalidateMeasure(this); + (VisualRoot as ILayoutRoot)?.LayoutManager.InvalidateMeasure(this); InvalidateVisual(); } } @@ -393,7 +393,7 @@ namespace Avalonia.Layout Logger.Verbose(LogArea.Layout, this, "Invalidated arrange"); IsArrangeValid = false; - LayoutManager.Instance?.InvalidateArrange(this); + (VisualRoot as ILayoutRoot)?.LayoutManager?.InvalidateArrange(this); InvalidateVisual(); } } @@ -620,6 +620,15 @@ namespace Avalonia.Layout base.OnVisualParentChanged(oldParent, newParent); } + protected override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTreeCore(e); + if(!IsMeasureValid) + (VisualRoot as ILayoutRoot)?.LayoutManager.InvalidateMeasure(this); + else if (!IsArrangeValid) + (VisualRoot as ILayoutRoot)?.LayoutManager.InvalidateArrange(this); + } + /// /// Calls on the control on which a property changed. /// diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index 1ea64b915c..cb0fc705ce 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -12,6 +12,7 @@ using Avalonia.Layout; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.UnitTests; +using Avalonia.VisualTree; using Xunit; namespace Avalonia.Controls.UnitTests.Presenters @@ -219,7 +220,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Changing_VirtualizationMode_None_To_Simple_Should_Add_Correct_Number_Of_Controls() { - using (UnitTestApplication.Start(TestServices.RealLayoutManager)) + using (UnitTestApplication.Start(new TestServices())) { var target = CreateTarget(mode: ItemVirtualizationMode.None); var scroll = (ScrollContentPresenter)target.Parent; @@ -237,7 +238,7 @@ namespace Avalonia.Controls.UnitTests.Presenters }; target.VirtualizationMode = ItemVirtualizationMode.Simple; - LayoutManager.Instance.ExecuteLayoutPass(); + ((ILayoutRoot)scroll.GetVisualRoot()).LayoutManager.ExecuteLayoutPass(); Assert.Equal(10, target.Panel.Children.Count); } @@ -313,11 +314,16 @@ namespace Avalonia.Controls.UnitTests.Presenters }); } - private class TestScroller : ScrollContentPresenter, IRenderRoot + private class TestScroller : ScrollContentPresenter, IRenderRoot, ILayoutRoot { public IRenderer Renderer { get; } public Size ClientSize { get; } + public Size MaxClientSize => Size.Infinity; + + public double LayoutScaling => 1; + + public ILayoutManager LayoutManager { get; } = new LayoutManager(); public IRenderTarget CreateRenderTarget() { throw new NotImplementedException(); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 1da9cfce76..b18729160b 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -564,11 +564,10 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Scrolling_To_Item_In_Zero_Sized_Presenter_Doesnt_Throw() { - using (UnitTestApplication.Start(TestServices.RealLayoutManager)) + using (UnitTestApplication.Start(new TestServices())) { var target = CreateTarget(itemCount: 10); var items = (IList)target.Items; - target.ApplyTemplate(); target.Measure(Size.Empty); target.Arrange(Rect.Empty); @@ -757,7 +756,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void GetControlInDirection_Down_Should_Scroll_If_Partially_Visible() { - using (UnitTestApplication.Start(TestServices.RealLayoutManager)) + using (UnitTestApplication.Start(new TestServices())) { var target = CreateTarget(); var scroller = (ScrollContentPresenter)target.Parent; @@ -778,7 +777,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void GetControlInDirection_Up_Should_Scroll_If_Partially_Visible_Item_Is_Currently_Shown() { - using (UnitTestApplication.Start(TestServices.RealLayoutManager)) + using (UnitTestApplication.Start(new TestServices())) { var target = CreateTarget(); var scroller = (ScrollContentPresenter)target.Parent; @@ -869,7 +868,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void GetControlInDirection_Right_Should_Scroll_If_Partially_Visible() { - using (UnitTestApplication.Start(TestServices.RealLayoutManager)) + using (UnitTestApplication.Start(new TestServices())) { var target = CreateTarget(orientation: Orientation.Horizontal); var scroller = (ScrollContentPresenter)target.Parent; @@ -1008,6 +1007,7 @@ namespace Avalonia.Controls.UnitTests.Presenters }; scroller.UpdateChild(); + new TestRoot().Child = scroller; return result; } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index f192e87f08..6696258258 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -270,7 +270,6 @@ namespace Avalonia.Controls.UnitTests.Primitives var renderInterface = new Mock(); AvaloniaLocator.CurrentMutable - .Bind().ToTransient() .Bind().ToFunc(() => globalStyles.Object) .Bind().ToConstant(new WindowingPlatformMock()) .Bind().ToTransient() diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index 5cd3c57e2e..35b0c75a08 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -65,15 +65,16 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Layout_Pass_Should_Not_Be_Automatically_Scheduled() { - var services = TestServices.StyledWindow.With(layoutManager: Mock.Of()); + var services = TestServices.StyledWindow; using (UnitTestApplication.Start(services)) { var impl = new Mock(); - var target = new TestTopLevel(impl.Object); + + var target = new TestTopLevel(impl.Object, Mock.Of()); // The layout pass should be scheduled by the derived class. - var layoutManagerMock = Mock.Get(LayoutManager.Instance); + var layoutManagerMock = Mock.Get(target.LayoutManager); layoutManagerMock.Verify(x => x.ExecuteLayoutPass(), Times.Never); } } @@ -98,7 +99,7 @@ namespace Avalonia.Controls.UnitTests } }; - LayoutManager.Instance.ExecuteInitialLayoutPass(target); + target.LayoutManager.ExecuteInitialLayoutPass(target); Assert.Equal(new Rect(0, 0, 321, 432), target.Bounds); } @@ -113,7 +114,7 @@ namespace Avalonia.Controls.UnitTests impl.Setup(x => x.ClientSize).Returns(new Size(123, 456)); var target = new TestTopLevel(impl.Object); - LayoutManager.Instance.ExecuteLayoutPass(); + target.LayoutManager.ExecuteLayoutPass(); Assert.Equal(double.NaN, target.Width); Assert.Equal(double.NaN, target.Height); @@ -222,13 +223,17 @@ namespace Avalonia.Controls.UnitTests private class TestTopLevel : TopLevel { + private readonly ILayoutManager _layoutManager; public bool IsClosed { get; private set; } - public TestTopLevel(ITopLevelImpl impl) + public TestTopLevel(ITopLevelImpl impl, ILayoutManager layoutManager = null) : base(impl) { + _layoutManager = layoutManager ?? new LayoutManager(); } + protected override ILayoutManager CreateLayoutManager() => _layoutManager; + protected override void HandleApplicationExiting() { base.HandleApplicationExiting(); diff --git a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs index d6ffb32d1c..3c3070db22 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs @@ -38,7 +38,7 @@ namespace Avalonia.Controls.UnitTests IsVisible = true, }; - LayoutManager.Instance.ExecuteInitialLayoutPass(target); + target.LayoutManager.ExecuteInitialLayoutPass(target); Mock.Get(impl).Verify(x => x.Resize(new Size(321, 432))); } diff --git a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs index 6b7c73da2a..7d79d369cb 100644 --- a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs +++ b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs @@ -56,11 +56,11 @@ namespace Avalonia.Layout.UnitTests }; window.Show(); - LayoutManager.Instance.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(window); Assert.Equal(new Size(400, 400), border.Bounds.Size); textBlock.Width = 200; - LayoutManager.Instance.ExecuteLayoutPass(); + window.LayoutManager.ExecuteLayoutPass(); Assert.Equal(new Size(200, 400), border.Bounds.Size); } @@ -98,7 +98,7 @@ namespace Avalonia.Layout.UnitTests }; window.Show(); - LayoutManager.Instance.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(window); Assert.Equal(new Size(800, 600), window.Bounds.Size); Assert.Equal(new Size(200, 200), scrollViewer.Bounds.Size); @@ -186,7 +186,6 @@ namespace Avalonia.Layout.UnitTests .Bind().ToConstant(new AssetLoader()) .Bind().ToConstant(new Mock().Object) .Bind().ToConstant(globalStyles.Object) - .Bind().ToConstant(new LayoutManager()) .Bind().ToConstant(new AppBuilder().RuntimePlatform) .Bind().ToConstant(renderInterface.Object) .Bind().ToConstant(new Styler()) diff --git a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs index f67c5a353f..d76c9ea6bc 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs @@ -11,12 +11,9 @@ namespace Avalonia.Layout.UnitTests [Fact] public void Invalidating_Child_Should_Remeasure_Parent() { - var layoutManager = new LayoutManager(); - using (AvaloniaLocator.EnterScope()) { - AvaloniaLocator.CurrentMutable.Bind().ToConstant(layoutManager); - + Border border; StackPanel panel; @@ -30,14 +27,14 @@ namespace Avalonia.Layout.UnitTests } } }; - - layoutManager.ExecuteInitialLayoutPass(root); + + root.LayoutManager.ExecuteInitialLayoutPass(root); Assert.Equal(new Size(0, 0), root.DesiredSize); border.Width = 100; border.Height = 100; - layoutManager.ExecuteLayoutPass(); + root.LayoutManager.ExecuteLayoutPass(); Assert.Equal(new Size(100, 100), panel.DesiredSize); } } diff --git a/tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs b/tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs index fab1647c5d..8c4dfbb209 100644 --- a/tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs +++ b/tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs @@ -2,10 +2,12 @@ // 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 + internal class TestLayoutRoot : Decorator, ILayoutRoot, IRenderRoot { public TestLayoutRoot() { @@ -18,7 +20,22 @@ namespace Avalonia.Layout.UnitTests 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(); + } } diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index b9bdee09e6..fa004976c0 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -42,12 +42,12 @@ namespace Avalonia.LeakTests window.Show(); // Do a layout and make sure that Canvas gets added to visual tree. - LayoutManager.Instance.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(window); Assert.IsType(window.Presenter.Child); // Clear the content and ensure the Canvas is removed. window.Content = null; - LayoutManager.Instance.ExecuteLayoutPass(); + window.LayoutManager.ExecuteLayoutPass(); Assert.Null(window.Presenter.Child); return window; @@ -78,13 +78,13 @@ namespace Avalonia.LeakTests window.Show(); // Do a layout and make sure that Canvas gets added to visual tree. - LayoutManager.Instance.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(window); Assert.IsType(window.Find("foo")); Assert.IsType(window.Presenter.Child); // Clear the content and ensure the Canvas is removed. window.Content = null; - LayoutManager.Instance.ExecuteLayoutPass(); + window.LayoutManager.ExecuteLayoutPass(); Assert.Null(window.Presenter.Child); return window; @@ -116,13 +116,13 @@ namespace Avalonia.LeakTests // Do a layout and make sure that ScrollViewer gets added to visual tree and its // template applied. - LayoutManager.Instance.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(window); Assert.IsType(window.Presenter.Child); Assert.IsType(((ScrollViewer)window.Presenter.Child).Presenter.Child); // Clear the content and ensure the ScrollViewer is removed. window.Content = null; - LayoutManager.Instance.ExecuteLayoutPass(); + window.LayoutManager.ExecuteLayoutPass(); Assert.Null(window.Presenter.Child); return window; @@ -153,13 +153,13 @@ namespace Avalonia.LeakTests // Do a layout and make sure that TextBox gets added to visual tree and its // template applied. - LayoutManager.Instance.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(window); Assert.IsType(window.Presenter.Child); Assert.NotEqual(0, window.Presenter.Child.GetVisualChildren().Count()); // Clear the content and ensure the TextBox is removed. window.Content = null; - LayoutManager.Instance.ExecuteLayoutPass(); + window.LayoutManager.ExecuteLayoutPass(); Assert.Null(window.Presenter.Child); return window; @@ -197,14 +197,14 @@ namespace Avalonia.LeakTests // Do a layout and make sure that TextBox gets added to visual tree and its // Text property set. - LayoutManager.Instance.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(window); Assert.IsType(window.Presenter.Child); Assert.Equal("foo", ((TextBox)window.Presenter.Child).Text); // Clear the content and DataContext and ensure the TextBox is removed. window.Content = null; window.DataContext = null; - LayoutManager.Instance.ExecuteLayoutPass(); + window.LayoutManager.ExecuteLayoutPass(); Assert.Null(window.Presenter.Child); return window; @@ -235,7 +235,7 @@ namespace Avalonia.LeakTests // Do a layout and make sure that TextBox gets added to visual tree and its // template applied. - LayoutManager.Instance.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(window); Assert.Same(textBox, window.Presenter.Child); // Get the border from the TextBox template. @@ -247,7 +247,7 @@ namespace Avalonia.LeakTests // Clear the content and ensure the TextBox is removed. window.Content = null; - LayoutManager.Instance.ExecuteLayoutPass(); + window.LayoutManager.ExecuteLayoutPass(); Assert.Null(window.Presenter.Child); // Check that the TextBox has no subscriptions to its Classes collection. @@ -289,12 +289,12 @@ namespace Avalonia.LeakTests window.Show(); // Do a layout and make sure that TreeViewItems get realized. - LayoutManager.Instance.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(window); Assert.Equal(1, target.ItemContainerGenerator.Containers.Count()); // Clear the content and ensure the TreeView is removed. window.Content = null; - LayoutManager.Instance.ExecuteLayoutPass(); + window.LayoutManager.ExecuteLayoutPass(); Assert.Null(window.Presenter.Child); return window; diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index 8a711c415e..7387602213 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -47,7 +47,7 @@ namespace Avalonia.UnitTests public double LayoutScaling => 1; - public ILayoutManager LayoutManager => AvaloniaLocator.Current.GetService(); + public ILayoutManager LayoutManager { get; set; } = new LayoutManager(); public IRenderTarget RenderTarget => null; diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index 0cd8d4295b..6699c33be9 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -21,7 +21,6 @@ namespace Avalonia.UnitTests { public static readonly TestServices StyledWindow = new TestServices( assetLoader: new AssetLoader(), - layoutManager: new LayoutManager(), platform: new AppBuilder().RuntimePlatform, renderer: (_, __) => Mock.Of(), renderInterface: CreateRenderInterfaceMock(), @@ -51,10 +50,7 @@ namespace Avalonia.UnitTests focusManager: new FocusManager(), keyboardDevice: () => new KeyboardDevice(), inputManager: new InputManager()); - - public static readonly TestServices RealLayoutManager = new TestServices( - layoutManager: new LayoutManager()); - + public static readonly TestServices RealStyler = new TestServices( styler: new Styler()); @@ -63,7 +59,6 @@ namespace Avalonia.UnitTests IFocusManager focusManager = null, IInputManager inputManager = null, Func keyboardDevice = null, - ILayoutManager layoutManager = null, IRuntimePlatform platform = null, Func renderer = null, IPlatformRenderInterface renderInterface = null, @@ -79,7 +74,6 @@ namespace Avalonia.UnitTests FocusManager = focusManager; InputManager = inputManager; KeyboardDevice = keyboardDevice; - LayoutManager = layoutManager; Platform = platform; Renderer = renderer; RenderInterface = renderInterface; @@ -96,7 +90,6 @@ namespace Avalonia.UnitTests public IInputManager InputManager { get; } public IFocusManager FocusManager { get; } public Func KeyboardDevice { get; } - public ILayoutManager LayoutManager { get; } public IRuntimePlatform Platform { get; } public Func Renderer { get; } public IPlatformRenderInterface RenderInterface { get; } @@ -113,7 +106,6 @@ namespace Avalonia.UnitTests IFocusManager focusManager = null, IInputManager inputManager = null, Func keyboardDevice = null, - ILayoutManager layoutManager = null, IRuntimePlatform platform = null, Func renderer = null, IPlatformRenderInterface renderInterface = null, @@ -131,7 +123,6 @@ namespace Avalonia.UnitTests focusManager: focusManager ?? FocusManager, inputManager: inputManager ?? InputManager, keyboardDevice: keyboardDevice ?? KeyboardDevice, - layoutManager: layoutManager ?? LayoutManager, platform: platform ?? Platform, renderer: renderer ?? Renderer, renderInterface: renderInterface ?? RenderInterface, diff --git a/tests/Avalonia.UnitTests/TestTemplatedRoot.cs b/tests/Avalonia.UnitTests/TestTemplatedRoot.cs index 73c9f370d6..8bed09dd27 100644 --- a/tests/Avalonia.UnitTests/TestTemplatedRoot.cs +++ b/tests/Avalonia.UnitTests/TestTemplatedRoot.cs @@ -39,7 +39,7 @@ namespace Avalonia.UnitTests public double LayoutScaling => 1; - public ILayoutManager LayoutManager => AvaloniaLocator.Current.GetService(); + public ILayoutManager LayoutManager { get; set; } = new LayoutManager(); public IRenderTarget RenderTarget => null; diff --git a/tests/Avalonia.UnitTests/UnitTestApplication.cs b/tests/Avalonia.UnitTests/UnitTestApplication.cs index c5d533486b..22d0901a14 100644 --- a/tests/Avalonia.UnitTests/UnitTestApplication.cs +++ b/tests/Avalonia.UnitTests/UnitTestApplication.cs @@ -49,7 +49,6 @@ namespace Avalonia.UnitTests .BindToSelf(this) .Bind().ToConstant(Services.InputManager) .Bind().ToConstant(Services.KeyboardDevice?.Invoke()) - .Bind().ToConstant(Services.LayoutManager) .Bind().ToConstant(Services.Platform) .Bind().ToConstant(new RendererFactory(Services.Renderer)) .Bind().ToConstant(Services.RenderInterface) From 00fbc5cea70b6925b1fa121f3868cc1d300f6b2d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 6 Jun 2017 01:30:21 +0300 Subject: [PATCH 02/94] Use (0, 0) as default for non-existing PreviousMeasure --- src/Avalonia.Layout/LayoutManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index ebf6411beb..ed2b930114 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -133,7 +133,7 @@ namespace Avalonia.Layout if (!control.IsMeasureValid) { - control.Measure(control.PreviousMeasure.Value); + control.Measure(control.PreviousMeasure ?? default(Size)); } _toMeasure.Remove(control); From 48df92055ec66285e04683b22034a748d549fe43 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 11 Jun 2017 13:36:06 +0200 Subject: [PATCH 03/94] Fix ItemsPresenterSimple tests. There were two problems: - There were two root controls (`TestScroller` and `TestRoot`) - The root control needs to have a fixed size otherwise it will grow because `LayoutManager` passes `MaxClientSize` to its measure (which may be different to the initial measure that we were using to set its size). --- ...emsPresenterTests_Virtualization_Simple.cs | 121 +++++++++--------- 1 file changed, 63 insertions(+), 58 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index b18729160b..7a7f4ab4ec 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -12,6 +12,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.Layout; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.UnitTests; @@ -722,10 +723,10 @@ namespace Avalonia.Controls.UnitTests.Presenters public void GetControlInDirection_Down_Should_Return_Existing_Container_If_Materialized() { var target = CreateTarget(); + var scroller = (TestScroller)target.Parent; - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); + scroller.Width = scroller.Height = 100; + scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); var from = target.Panel.Children[5]; var result = ((ILogicalScrollable)target).GetControlInDirection( @@ -739,10 +740,10 @@ namespace Avalonia.Controls.UnitTests.Presenters public void GetControlInDirection_Down_Should_Scroll_If_Necessary() { var target = CreateTarget(); + var scroller = (TestScroller)target.Parent; - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); + scroller.Width = scroller.Height = 100; + scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); var from = target.Panel.Children[9]; var result = ((ILogicalScrollable)target).GetControlInDirection( @@ -756,44 +757,40 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void GetControlInDirection_Down_Should_Scroll_If_Partially_Visible() { - using (UnitTestApplication.Start(new TestServices())) - { - var target = CreateTarget(); - var scroller = (ScrollContentPresenter)target.Parent; + var target = CreateTarget(); + var scroller = (TestScroller)target.Parent; - scroller.Measure(new Size(100, 95)); - scroller.Arrange(new Rect(0, 0, 100, 95)); + scroller.Width = 100; + scroller.Height = 95; + scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); - var from = target.Panel.Children[8]; - var result = ((ILogicalScrollable)target).GetControlInDirection( - NavigationDirection.Down, - from); + var from = target.Panel.Children[8]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + NavigationDirection.Down, + from); - Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset); - Assert.Same(target.Panel.Children[8], result); - } + Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset); + Assert.Same(target.Panel.Children[8], result); } [Fact] public void GetControlInDirection_Up_Should_Scroll_If_Partially_Visible_Item_Is_Currently_Shown() { - using (UnitTestApplication.Start(new TestServices())) - { - var target = CreateTarget(); - var scroller = (ScrollContentPresenter)target.Parent; + var target = CreateTarget(); + var scroller = (TestScroller)target.Parent; - scroller.Measure(new Size(100, 95)); - scroller.Arrange(new Rect(0, 0, 100, 95)); - ((ILogicalScrollable)target).Offset = new Vector(0, 11); + scroller.Width = 100; + scroller.Height = 95; + scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); + ((ILogicalScrollable)target).Offset = new Vector(0, 11); - var from = target.Panel.Children[1]; - var result = ((ILogicalScrollable)target).GetControlInDirection( - NavigationDirection.Up, - from); + var from = target.Panel.Children[1]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + NavigationDirection.Up, + from); - Assert.Equal(new Vector(0, 10), ((ILogicalScrollable)target).Offset); - Assert.Same(target.Panel.Children[0], result); - } + Assert.Equal(new Vector(0, 10), ((ILogicalScrollable)target).Offset); + Assert.Same(target.Panel.Children[0], result); } [Fact] @@ -834,10 +831,10 @@ namespace Avalonia.Controls.UnitTests.Presenters public void GetControlInDirection_Right_Should_Return_Existing_Container_If_Materialized() { var target = CreateTarget(orientation: Orientation.Horizontal); + var scroller = (TestScroller)target.Parent; - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); + scroller.Width = scroller.Height = 100; + scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); var from = target.Panel.Children[5]; var result = ((ILogicalScrollable)target).GetControlInDirection( @@ -851,10 +848,10 @@ namespace Avalonia.Controls.UnitTests.Presenters public void GetControlInDirection_Right_Should_Scroll_If_Necessary() { var target = CreateTarget(orientation: Orientation.Horizontal); + var scroller = (TestScroller)target.Parent; - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); + scroller.Width = scroller.Height = 100; + scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); var from = target.Panel.Children[9]; var result = ((ILogicalScrollable)target).GetControlInDirection( @@ -868,32 +865,31 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void GetControlInDirection_Right_Should_Scroll_If_Partially_Visible() { - using (UnitTestApplication.Start(new TestServices())) - { - var target = CreateTarget(orientation: Orientation.Horizontal); - var scroller = (ScrollContentPresenter)target.Parent; + var target = CreateTarget(orientation: Orientation.Horizontal); + var scroller = (TestScroller)target.Parent; - scroller.Measure(new Size(95, 100)); - scroller.Arrange(new Rect(0, 0, 95, 100)); + scroller.Width = 95; + scroller.Height = 100; + scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); - var from = target.Panel.Children[8]; - var result = ((ILogicalScrollable)target).GetControlInDirection( - NavigationDirection.Right, - from); + var from = target.Panel.Children[8]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + NavigationDirection.Right, + from); - Assert.Equal(new Vector(1, 0), ((ILogicalScrollable)target).Offset); - Assert.Same(target.Panel.Children[8], result); - } + Assert.Equal(new Vector(1, 0), ((ILogicalScrollable)target).Offset); + Assert.Same(target.Panel.Children[8], result); } [Fact] public void GetControlInDirection_Left_Should_Scroll_If_Partially_Visible_Item_Is_Currently_Shown() { var target = CreateTarget(orientation: Orientation.Horizontal); + var scroller = (TestScroller)target.Parent; - target.ApplyTemplate(); - target.Measure(new Size(95, 100)); - target.Arrange(new Rect(0, 0, 95, 100)); + scroller.Width = 95; + scroller.Height = 100; + scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); ((ILogicalScrollable)target).Offset = new Vector(11, 0); var from = target.Panel.Children[1]; @@ -1007,8 +1003,6 @@ namespace Avalonia.Controls.UnitTests.Presenters }; scroller.UpdateChild(); - new TestRoot().Child = scroller; - return result; } @@ -1030,11 +1024,17 @@ namespace Avalonia.Controls.UnitTests.Presenters }); } - private class TestScroller : ScrollContentPresenter, IRenderRoot + private class TestScroller : ScrollContentPresenter, IRenderRoot, ILayoutRoot { public IRenderer Renderer { get; } public Size ClientSize { get; } + public Size MaxClientSize => Size.Infinity; + + public double LayoutScaling => 1; + + public ILayoutManager LayoutManager { get; } = new LayoutManager(); + public IRenderTarget CreateRenderTarget() { throw new NotImplementedException(); @@ -1054,6 +1054,11 @@ namespace Avalonia.Controls.UnitTests.Presenters { throw new NotImplementedException(); } + + protected override Size MeasureOverride(Size availableSize) + { + return base.MeasureOverride(availableSize); + } } private class TestItemsPresenter : ItemsPresenter From 4b8db11f0d237fd4e1b8bfbeaac1ab399514631f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 11 Jun 2017 13:38:16 +0200 Subject: [PATCH 04/94] FIx ItemsPresenterTests_Virtualization. The root control must have a fixed size, doing an initial measure isn't enough as the `LayoutManager` will remeasure with `MaxClientSize`. --- .../Presenters/ItemsPresenterTests_Virtualization.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index cb0fc705ce..c69eebf324 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -223,10 +223,10 @@ namespace Avalonia.Controls.UnitTests.Presenters using (UnitTestApplication.Start(new TestServices())) { var target = CreateTarget(mode: ItemVirtualizationMode.None); - var scroll = (ScrollContentPresenter)target.Parent; + var scroll = (TestScroller)target.Parent; - scroll.Measure(new Size(100, 100)); - scroll.Arrange(new Rect(0, 0, 100, 100)); + scroll.Width = scroll.Height = 100; + scroll.LayoutManager.ExecuteInitialLayoutPass(scroll); // Ensure than an intermediate measure pass doesn't add more controls than it // should. This can happen if target gets measured with Size.Infinity which From 188a0ce442d7b8fb073ebfb9c1f07b50d561c3c7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 14 Jun 2017 01:45:49 +0200 Subject: [PATCH 05/94] Fix compile errors. --- tests/Avalonia.Benchmarks/Layout/Measure.cs | 14 +++----------- .../LayoutManagerTests.cs | 2 -- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/tests/Avalonia.Benchmarks/Layout/Measure.cs b/tests/Avalonia.Benchmarks/Layout/Measure.cs index d1fdae9971..b0490d8a0f 100644 --- a/tests/Avalonia.Benchmarks/Layout/Measure.cs +++ b/tests/Avalonia.Benchmarks/Layout/Measure.cs @@ -8,26 +8,18 @@ using BenchmarkDotNet.Attributes; namespace Avalonia.Benchmarks.Layout { [MemoryDiagnoser] - public class Measure : IDisposable + public class Measure { - 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(); + root.LayoutManager.ExecuteInitialLayoutPass(root); } [Benchmark] @@ -43,7 +35,7 @@ namespace Avalonia.Benchmarks.Layout } } - LayoutManager.Instance.ExecuteLayoutPass(); + root.LayoutManager.ExecuteLayoutPass(); } private void CreateChildren(IPanel parent, int childCount, int iterations) diff --git a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs index c4c0f9a441..5f43a8d00e 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs @@ -264,8 +264,6 @@ namespace Avalonia.Layout.UnitTests { using (AvaloniaLocator.EnterScope()) { - AvaloniaLocator.CurrentMutable.Bind().ToConstant(layoutManager); - Border border; StackPanel panel; From 5e3f604308e514e3392160e307602288d9f1e53a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 14 Jun 2017 01:51:11 +0200 Subject: [PATCH 06/94] Removed unnecessary service locator code. `ILayoutManager` is not longer retrieved from `AvaloniaLocator` so can remove this stuff. --- .../LayoutManagerTests.cs | 314 ++++++++---------- .../LayoutableTests.cs | 87 +++-- 2 files changed, 174 insertions(+), 227 deletions(-) diff --git a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs index 5f43a8d00e..3526b29cb5 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs @@ -15,285 +15,239 @@ namespace Avalonia.Layout.UnitTests public void Measures_And_Arranges_InvalidateMeasured_Control() { var target = new LayoutManager(); + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; - using (Start(target)) - { - var control = new LayoutTestControl(); - var root = new LayoutTestRoot { Child = control }; - - target.ExecuteInitialLayoutPass(root); - control.Measured = control.Arranged = false; + target.ExecuteInitialLayoutPass(root); + control.Measured = control.Arranged = false; - control.InvalidateMeasure(); - target.ExecuteLayoutPass(); + control.InvalidateMeasure(); + target.ExecuteLayoutPass(); - Assert.True(control.Measured); - Assert.True(control.Arranged); - } + Assert.True(control.Measured); + Assert.True(control.Arranged); } [Fact] public void Arranges_InvalidateArranged_Control() { var target = new LayoutManager(); + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; - using (Start(target)) - { - var control = new LayoutTestControl(); - var root = new LayoutTestRoot { Child = control }; - - target.ExecuteInitialLayoutPass(root); - control.Measured = control.Arranged = false; + target.ExecuteInitialLayoutPass(root); + control.Measured = control.Arranged = false; - control.InvalidateArrange(); - target.ExecuteLayoutPass(); + control.InvalidateArrange(); + target.ExecuteLayoutPass(); - Assert.False(control.Measured); - Assert.True(control.Arranged); - } + Assert.False(control.Measured); + Assert.True(control.Arranged); } [Fact] public void Measures_Parent_Of_Newly_Added_Control() { var target = new LayoutManager(); + var control = new LayoutTestControl(); + var root = new LayoutTestRoot(); - using (Start(target)) - { - var control = new LayoutTestControl(); - var root = new LayoutTestRoot(); - - target.ExecuteInitialLayoutPass(root); - root.Child = control; - root.Measured = root.Arranged = false; + target.ExecuteInitialLayoutPass(root); + root.Child = control; + root.Measured = root.Arranged = false; - target.ExecuteLayoutPass(); + target.ExecuteLayoutPass(); - Assert.True(root.Measured); - Assert.True(root.Arranged); - Assert.True(control.Measured); - Assert.True(control.Arranged); - } + 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 { - LayoutTestControl control1; - LayoutTestControl control2; - var root = new LayoutTestRoot + Child = control1 = new LayoutTestControl { - Child = control1 = new LayoutTestControl - { - Child = control2 = new LayoutTestControl(), - } - }; + Child = control2 = new LayoutTestControl(), + } + }; - var order = new List(); - Size MeasureOverride(ILayoutable control, Size size) - { - order.Add(control); - return new Size(10, 10); - } + 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); + root.DoMeasureOverride = MeasureOverride; + control1.DoMeasureOverride = MeasureOverride; + control2.DoMeasureOverride = MeasureOverride; + target.ExecuteInitialLayoutPass(root); - control2.InvalidateMeasure(); - control1.InvalidateMeasure(); - root.InvalidateMeasure(); + control2.InvalidateMeasure(); + control1.InvalidateMeasure(); + root.InvalidateMeasure(); - order.Clear(); - target.ExecuteLayoutPass(); + order.Clear(); + target.ExecuteLayoutPass(); - Assert.Equal(new ILayoutable[] { root, control1, control2 }, order); - } + 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 { - LayoutTestControl control1; - LayoutTestControl control2; - var root = new LayoutTestRoot + Child = control1 = new LayoutTestControl { - Child = control1 = new LayoutTestControl - { - Child = control2 = new LayoutTestControl(), - } - }; + Child = control2 = new LayoutTestControl(), + } + }; - var order = new List(); - Size MeasureOverride(ILayoutable control, Size size) - { - order.Add(control); - return new Size(10, 10); - } + 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); + root.DoMeasureOverride = MeasureOverride; + control1.DoMeasureOverride = MeasureOverride; + control2.DoMeasureOverride = MeasureOverride; + target.ExecuteInitialLayoutPass(root); - control2.InvalidateMeasure(); - root.InvalidateMeasure(); + control2.InvalidateMeasure(); + root.InvalidateMeasure(); - order.Clear(); - target.ExecuteLayoutPass(); + order.Clear(); + target.ExecuteLayoutPass(); - Assert.Equal(new ILayoutable[] { root, control2 }, order); - } + Assert.Equal(new ILayoutable[] { root, control2 }, order); } [Fact] public void Doesnt_Measure_Non_Invalidated_Root() { var target = new LayoutManager(); + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; - 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; - target.ExecuteInitialLayoutPass(root); - root.Measured = root.Arranged = false; - control.Measured = control.Arranged = false; + control.InvalidateMeasure(); + target.ExecuteLayoutPass(); - control.InvalidateMeasure(); - target.ExecuteLayoutPass(); - - Assert.False(root.Measured); - Assert.False(root.Arranged); - Assert.True(control.Measured); - Assert.True(control.Arranged); - } + 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(); + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; - using (Start(target)) - { - var control = new LayoutTestControl(); - var root = new LayoutTestRoot { Child = control }; - - target.ExecuteInitialLayoutPass(root); - control.Measured = control.Arranged = false; + target.ExecuteInitialLayoutPass(root); + control.Measured = control.Arranged = false; - control.InvalidateMeasure(); - root.Child = null; - target.ExecuteLayoutPass(); + control.InvalidateMeasure(); + root.Child = null; + target.ExecuteLayoutPass(); - Assert.False(control.Measured); - Assert.False(control.Arranged); - } + Assert.False(control.Measured); + Assert.False(control.Arranged); } [Fact] public void Measures_Root_With_Infinity() { var target = new LayoutManager(); + var root = new LayoutTestRoot(); + var availableSize = default(Size); - using (Start(target)) - { - var root = new LayoutTestRoot(); - var availableSize = default(Size); - - // Should not measure with this size. - root.MaxClientSize = new Size(123, 456); + // Should not measure with this size. + root.MaxClientSize = new Size(123, 456); - root.DoMeasureOverride = (_, s) => - { - availableSize = s; - return new Size(100, 100); - }; + root.DoMeasureOverride = (_, s) => + { + availableSize = s; + return new Size(100, 100); + }; - target.ExecuteInitialLayoutPass(root); + target.ExecuteInitialLayoutPass(root); - Assert.Equal(Size.Infinity, availableSize); - } + Assert.Equal(Size.Infinity, availableSize); } [Fact] public void Arranges_Root_With_DesiredSize() { var target = new LayoutManager(); - - using (Start(target)) + var root = new LayoutTestRoot { - var root = new LayoutTestRoot - { - Width = 100, - Height = 100, - }; + Width = 100, + Height = 100, + }; - var arrangeSize = default(Size); + var arrangeSize = default(Size); - root.DoArrangeOverride = (_, s) => - { - arrangeSize = s; - return s; - }; + root.DoArrangeOverride = (_, s) => + { + arrangeSize = s; + return s; + }; - target.ExecuteInitialLayoutPass(root); - Assert.Equal(new Size(100, 100), arrangeSize); + target.ExecuteInitialLayoutPass(root); + Assert.Equal(new Size(100, 100), arrangeSize); - root.Width = 120; + root.Width = 120; - target.ExecuteLayoutPass(); - Assert.Equal(new Size(120, 100), arrangeSize); - } + target.ExecuteLayoutPass(); + Assert.Equal(new Size(120, 100), arrangeSize); } [Fact] public void Invalidating_Child_Remeasures_Parent() { - using (AvaloniaLocator.EnterScope()) - { - Border border; - StackPanel panel; + Border border; + StackPanel panel; - var root = new LayoutTestRoot + var root = new LayoutTestRoot + { + Child = panel = new StackPanel + { + Children = new Controls.Controls { - Child = panel = new StackPanel - { - Children = new Controls.Controls - { - (border = new Border()) - } - } - }; + (border = new Border()) + } + } + }; - root.LayoutManager.ExecuteInitialLayoutPass(root); - Assert.Equal(new Size(0, 0), root.DesiredSize); + root.LayoutManager.ExecuteInitialLayoutPass(root); + Assert.Equal(new Size(0, 0), root.DesiredSize); - border.Width = 100; - border.Height = 100; + border.Width = 100; + border.Height = 100; - root.LayoutManager.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; + root.LayoutManager.ExecuteLayoutPass(); + Assert.Equal(new Size(100, 100), panel.DesiredSize); } } } diff --git a/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs index dcc65edc74..68cdaa1b12 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs @@ -11,80 +11,73 @@ namespace Avalonia.Layout.UnitTests public void Only_Calls_LayoutManager_InvalidateMeasure_Once() { var target = new Mock(); - - using (Start(target.Object)) + var control = new Decorator(); + var root = new LayoutTestRoot { - var control = new Decorator(); - var root = new LayoutTestRoot { Child = control }; + Child = control, + LayoutManager = target.Object, + }; - root.Measure(Size.Infinity); - root.Arrange(new Rect(root.DesiredSize)); - target.ResetCalls(); + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + target.ResetCalls(); - control.InvalidateMeasure(); - control.InvalidateMeasure(); + control.InvalidateMeasure(); + control.InvalidateMeasure(); - target.Verify(x => x.InvalidateMeasure(control), Times.Once()); - } + 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 { - var control = new Decorator(); - var root = new LayoutTestRoot { Child = control }; + Child = control, + LayoutManager = target.Object, + }; - root.Measure(Size.Infinity); - root.Arrange(new Rect(root.DesiredSize)); - target.ResetCalls(); + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + target.ResetCalls(); - control.InvalidateArrange(); - control.InvalidateArrange(); + control.InvalidateArrange(); + control.InvalidateArrange(); - target.Verify(x => x.InvalidateArrange(control), Times.Once()); - } + 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 { - var control = new Decorator(); - var root = new LayoutTestRoot { Child = control }; + Child = control, + LayoutManager = target.Object, + }; - root.Measure(Size.Infinity); - root.Arrange(new Rect(root.DesiredSize)); - Assert.True(control.IsMeasureValid); + 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)); + root.Child = null; + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); - Assert.False(control.IsMeasureValid); - Assert.True(root.IsMeasureValid); + Assert.False(control.IsMeasureValid); + Assert.True(root.IsMeasureValid); - target.ResetCalls(); + target.ResetCalls(); - root.Child = control; + 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; + Assert.False(root.IsMeasureValid); + Assert.False(control.IsMeasureValid); + target.Verify(x => x.InvalidateMeasure(root), Times.Once()); } } } From 061a264ca4fc0d2ff237fb86500a7577f42b020e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 14 Jun 2017 01:55:46 +0200 Subject: [PATCH 07/94] Don't need to create LayoutManager target. There is already one on the root. --- .../LayoutManagerTests.cs | 47 ++++++++----------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs index 3526b29cb5..70b5d5a991 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs @@ -14,15 +14,14 @@ namespace Avalonia.Layout.UnitTests [Fact] public void Measures_And_Arranges_InvalidateMeasured_Control() { - var target = new LayoutManager(); var control = new LayoutTestControl(); var root = new LayoutTestRoot { Child = control }; - target.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(root); control.Measured = control.Arranged = false; control.InvalidateMeasure(); - target.ExecuteLayoutPass(); + root.LayoutManager.ExecuteLayoutPass(); Assert.True(control.Measured); Assert.True(control.Arranged); @@ -31,15 +30,14 @@ namespace Avalonia.Layout.UnitTests [Fact] public void Arranges_InvalidateArranged_Control() { - var target = new LayoutManager(); var control = new LayoutTestControl(); var root = new LayoutTestRoot { Child = control }; - target.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(root); control.Measured = control.Arranged = false; control.InvalidateArrange(); - target.ExecuteLayoutPass(); + root.LayoutManager.ExecuteLayoutPass(); Assert.False(control.Measured); Assert.True(control.Arranged); @@ -48,15 +46,14 @@ namespace Avalonia.Layout.UnitTests [Fact] public void Measures_Parent_Of_Newly_Added_Control() { - var target = new LayoutManager(); var control = new LayoutTestControl(); var root = new LayoutTestRoot(); - target.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(root); root.Child = control; root.Measured = root.Arranged = false; - target.ExecuteLayoutPass(); + root.LayoutManager.ExecuteLayoutPass(); Assert.True(root.Measured); Assert.True(root.Arranged); @@ -67,7 +64,6 @@ namespace Avalonia.Layout.UnitTests [Fact] public void Measures_In_Correct_Order() { - var target = new LayoutManager(); LayoutTestControl control1; LayoutTestControl control2; var root = new LayoutTestRoot @@ -89,14 +85,14 @@ namespace Avalonia.Layout.UnitTests root.DoMeasureOverride = MeasureOverride; control1.DoMeasureOverride = MeasureOverride; control2.DoMeasureOverride = MeasureOverride; - target.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(root); control2.InvalidateMeasure(); control1.InvalidateMeasure(); root.InvalidateMeasure(); order.Clear(); - target.ExecuteLayoutPass(); + root.LayoutManager.ExecuteLayoutPass(); Assert.Equal(new ILayoutable[] { root, control1, control2 }, order); } @@ -104,7 +100,6 @@ namespace Avalonia.Layout.UnitTests [Fact] public void Measures_Root_And_Grandparent_In_Correct_Order() { - var target = new LayoutManager(); LayoutTestControl control1; LayoutTestControl control2; var root = new LayoutTestRoot @@ -126,13 +121,13 @@ namespace Avalonia.Layout.UnitTests root.DoMeasureOverride = MeasureOverride; control1.DoMeasureOverride = MeasureOverride; control2.DoMeasureOverride = MeasureOverride; - target.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(root); control2.InvalidateMeasure(); root.InvalidateMeasure(); order.Clear(); - target.ExecuteLayoutPass(); + root.LayoutManager.ExecuteLayoutPass(); Assert.Equal(new ILayoutable[] { root, control2 }, order); } @@ -140,16 +135,15 @@ namespace Avalonia.Layout.UnitTests [Fact] public void Doesnt_Measure_Non_Invalidated_Root() { - var target = new LayoutManager(); var control = new LayoutTestControl(); var root = new LayoutTestRoot { Child = control }; - target.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(root); root.Measured = root.Arranged = false; control.Measured = control.Arranged = false; control.InvalidateMeasure(); - target.ExecuteLayoutPass(); + root.LayoutManager.ExecuteLayoutPass(); Assert.False(root.Measured); Assert.False(root.Arranged); @@ -160,16 +154,15 @@ namespace Avalonia.Layout.UnitTests [Fact] public void Doesnt_Measure_Removed_Control() { - var target = new LayoutManager(); var control = new LayoutTestControl(); var root = new LayoutTestRoot { Child = control }; - target.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(root); control.Measured = control.Arranged = false; control.InvalidateMeasure(); root.Child = null; - target.ExecuteLayoutPass(); + root.LayoutManager.ExecuteLayoutPass(); Assert.False(control.Measured); Assert.False(control.Arranged); @@ -178,7 +171,6 @@ namespace Avalonia.Layout.UnitTests [Fact] public void Measures_Root_With_Infinity() { - var target = new LayoutManager(); var root = new LayoutTestRoot(); var availableSize = default(Size); @@ -191,7 +183,7 @@ namespace Avalonia.Layout.UnitTests return new Size(100, 100); }; - target.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(root); Assert.Equal(Size.Infinity, availableSize); } @@ -199,7 +191,6 @@ namespace Avalonia.Layout.UnitTests [Fact] public void Arranges_Root_With_DesiredSize() { - var target = new LayoutManager(); var root = new LayoutTestRoot { Width = 100, @@ -213,13 +204,13 @@ namespace Avalonia.Layout.UnitTests arrangeSize = s; return s; }; - - target.ExecuteInitialLayoutPass(root); + + root.LayoutManager.ExecuteInitialLayoutPass(root); Assert.Equal(new Size(100, 100), arrangeSize); root.Width = 120; - - target.ExecuteLayoutPass(); + + root.LayoutManager.ExecuteLayoutPass(); Assert.Equal(new Size(120, 100), arrangeSize); } From 5a34acac2d4588d8fce104019a99a76cb2c57cee Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 14 Jun 2017 01:58:58 +0200 Subject: [PATCH 08/94] Don't need to do this. We don't need to invalidate newly added controls with the layout manager - this is already handed by the parent control. --- src/Avalonia.Layout/Layoutable.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index bea62efe50..f594450039 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -620,15 +620,6 @@ namespace Avalonia.Layout base.OnVisualParentChanged(oldParent, newParent); } - protected override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTreeCore(e); - if(!IsMeasureValid) - (VisualRoot as ILayoutRoot)?.LayoutManager.InvalidateMeasure(this); - else if (!IsArrangeValid) - (VisualRoot as ILayoutRoot)?.LayoutManager.InvalidateArrange(this); - } - /// /// Calls on the control on which a property changed. /// From ab30fd343b04382a34bc2e4e70b199f36df2792d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 14 Jun 2017 02:00:12 +0200 Subject: [PATCH 09/94] Handle no previous measure/arrange. This shouldn't happen, but if it does, don't crash. --- src/Avalonia.Layout/LayoutManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index f1f0749d41..73aadcd545 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -165,7 +165,7 @@ namespace Avalonia.Layout { root.Measure(Size.Infinity); } - else + else if (control.PreviousMeasure.HasValue) { control.Measure(control.PreviousMeasure.Value); } @@ -185,7 +185,7 @@ namespace Avalonia.Layout { root.Arrange(new Rect(control.DesiredSize)); } - else + else if (control.PreviousArrange.HasValue) { control.Arrange(control.PreviousArrange.Value); } From e8c32bf801a6f36270155657b4665821019f986a Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 17 May 2018 17:39:46 -0500 Subject: [PATCH 10/94] Preliminary support. Indexers require more work since the compiler doesn't generate IndexExpressions (they weren't in S.L.Expressions v1 so they aren't auto-genned). --- .../Data/ExpressionNodeBuilder.cs | 8 + .../Data/ExpressionObserver.cs | 92 ++++++++-- .../Data/ExpressionParseException.cs | 4 +- .../Data/IndexerExpressionNode.cs | 61 +++++++ .../Avalonia.Markup/Data/IndexerNode.cs | 80 ++------- .../Avalonia.Markup/Data/IndexerNodeBase.cs | 87 ++++++++++ .../Data/Parsers/ExpressionTreeParser.cs | 34 ++++ .../Parsers/ExpressionVisitorNodeBuilder.cs | 158 ++++++++++++++++++ .../ExpressionObserverTests_ExpressionTree.cs | 111 ++++++++++++ 9 files changed, 555 insertions(+), 80 deletions(-) create mode 100644 src/Markup/Avalonia.Markup/Data/IndexerExpressionNode.cs create mode 100644 src/Markup/Avalonia.Markup/Data/IndexerNodeBase.cs create mode 100644 src/Markup/Avalonia.Markup/Data/Parsers/ExpressionTreeParser.cs create mode 100644 src/Markup/Avalonia.Markup/Data/Parsers/ExpressionVisitorNodeBuilder.cs create mode 100644 tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_ExpressionTree.cs diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionNodeBuilder.cs b/src/Markup/Avalonia.Markup/Data/ExpressionNodeBuilder.cs index 013299c1d7..e19259c6ed 100644 --- a/src/Markup/Avalonia.Markup/Data/ExpressionNodeBuilder.cs +++ b/src/Markup/Avalonia.Markup/Data/ExpressionNodeBuilder.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.Linq.Expressions; using Avalonia.Markup.Data.Parsers; namespace Avalonia.Markup.Data @@ -26,5 +27,12 @@ namespace Avalonia.Markup.Data return node; } + + public static ExpressionNode Build(LambdaExpression expression, bool enableValidation = false) + { + var parser = new ExpressionTreeParser(enableValidation); + + return parser.Parse(expression); + } } } diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs b/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs index dd9718a0f6..127b338008 100644 --- a/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs +++ b/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq.Expressions; using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; @@ -72,20 +73,36 @@ namespace Avalonia.Markup.Data string expression, bool enableDataValidation = false, string description = null) + : this(root, Parse(expression, enableDataValidation), description ?? expression) { Contract.Requires(expression != null); + Expression = expression; + } + private ExpressionObserver( + object root, + ExpressionNode node, + string description = null) + { if (root == AvaloniaProperty.UnsetValue) { root = null; } - Expression = expression; - Description = description ?? expression; - _node = Parse(expression, enableDataValidation); + _node = node; + Description = description; _root = new WeakReference(root); } + public static ExpressionObserver CreateFromExpression( + T root, + Expression> expression, + bool enableDataValidation = false, + string description = null) + { + return new ExpressionObserver(root, Parse(expression, enableDataValidation), description ?? expression.ToString()); + } + /// /// Initializes a new instance of the class. /// @@ -100,15 +117,38 @@ namespace Avalonia.Markup.Data string expression, bool enableDataValidation = false, string description = null) + : this(rootObservable, Parse(expression, enableDataValidation), description ?? expression) { Contract.Requires(rootObservable != null); Contract.Requires(expression != null); Expression = expression; - Description = description ?? expression; - _node = Parse(expression, enableDataValidation); - _finished = new Subject(); + } + + private ExpressionObserver( + IObservable rootObservable, + ExpressionNode node, + string description) + { + Contract.Requires(rootObservable != null); + + _node = node; + Description = description; _root = rootObservable; + _finished = new Subject(); + } + + public static ExpressionObserver CreateFromExpression( + IObservable rootObservable, + Expression> expression, + bool enableDataValidation = false, + string description = null) + { + Contract.Requires(rootObservable != null); + return new ExpressionObserver( + rootObservable.Select(o => (object)o), + Parse(expression, enableDataValidation), + description ?? expression.ToString()); } /// @@ -127,19 +167,44 @@ namespace Avalonia.Markup.Data IObservable update, bool enableDataValidation = false, string description = null) + : this(rootGetter, Parse(expression, enableDataValidation), update, description ?? expression) { - Contract.Requires(rootGetter != null); Contract.Requires(expression != null); - Contract.Requires(update != null); Expression = expression; - Description = description ?? expression; - _node = Parse(expression, enableDataValidation); - _finished = new Subject(); + } + + private ExpressionObserver( + Func rootGetter, + ExpressionNode node, + IObservable update, + string description) + { + Contract.Requires(rootGetter != null); + Contract.Requires(update != null); + Description = description; + _node = node; + _finished = new Subject(); _node.Target = new WeakReference(rootGetter()); _root = update.Select(x => rootGetter()); } + + public static ExpressionObserver CreateFromExpression( + Func rootGetter, + Expression> expression, + IObservable update, + bool enableDataValidation = false, + string description = null) + { + Contract.Requires(rootGetter != null); + + return new ExpressionObserver( + () => rootGetter(), + Parse(expression, enableDataValidation), + update, + description ?? expression.ToString()); + } /// /// Attempts to set the value of a property expression. @@ -238,6 +303,11 @@ namespace Avalonia.Markup.Data } } + private static ExpressionNode Parse(LambdaExpression expression, bool enableDataValidation) + { + return ExpressionNodeBuilder.Build(expression, enableDataValidation); + } + private static object ToWeakReference(object o) { return o is BindingNotification ? o : new WeakReference(o); diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionParseException.cs b/src/Markup/Avalonia.Markup/Data/ExpressionParseException.cs index d06bdd1e52..3ef225e70a 100644 --- a/src/Markup/Avalonia.Markup/Data/ExpressionParseException.cs +++ b/src/Markup/Avalonia.Markup/Data/ExpressionParseException.cs @@ -17,8 +17,8 @@ namespace Avalonia.Markup.Data /// /// The column position of the error. /// The exception message. - public ExpressionParseException(int column, string message) - : base(message) + public ExpressionParseException(int column, string message, Exception innerException = null) + : base(message, innerException) { Column = column; } diff --git a/src/Markup/Avalonia.Markup/Data/IndexerExpressionNode.cs b/src/Markup/Avalonia.Markup/Data/IndexerExpressionNode.cs new file mode 100644 index 0000000000..b296badb86 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/IndexerExpressionNode.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq.Expressions; +using System.Text; +using Avalonia.Data; + +namespace Avalonia.Markup.Data +{ + class IndexerExpressionNode : IndexerNodeBase + { + private readonly ParameterExpression parameter; + private readonly IndexExpression expression; + private readonly Delegate setDelegate; + private readonly Delegate getDelegate; + private readonly Delegate firstArgumentDelegate; + + public IndexerExpressionNode(IndexExpression expression) + { + parameter = Expression.Parameter(expression.Object.Type); + this.expression = expression.Update(parameter, expression.Arguments); + + getDelegate = Expression.Lambda(this.expression, parameter).Compile(); + + var valueParameter = Expression.Parameter(expression.Type); + + setDelegate = Expression.Lambda(Expression.Assign(this.expression, valueParameter), parameter, valueParameter).Compile(); + + firstArgumentDelegate = Expression.Lambda(this.expression.Arguments[0], parameter).Compile(); + } + + public override Type PropertyType => expression.Type; + + public override string Description => expression.ToString(); + + public override bool SetTargetValue(object value, BindingPriority priority) + { + try + { + setDelegate.DynamicInvoke(Target.Target, value); + return true; + } + catch (Exception) + { + return false; + } + } + + protected override object GetValue(object target) + { + return getDelegate.DynamicInvoke(target); + } + + protected override bool ShouldUpdate(object sender, PropertyChangedEventArgs e) + { + return expression.Indexer.Name == e.PropertyName; + } + + protected override int? TryGetFirstArgumentAsInt() => firstArgumentDelegate.DynamicInvoke(Target.Target) as int?; + } +} diff --git a/src/Markup/Avalonia.Markup/Data/IndexerNode.cs b/src/Markup/Avalonia.Markup/Data/IndexerNode.cs index 4e2914a148..09ce2b85e9 100644 --- a/src/Markup/Avalonia.Markup/Data/IndexerNode.cs +++ b/src/Markup/Avalonia.Markup/Data/IndexerNode.cs @@ -15,7 +15,7 @@ using Avalonia.Data; namespace Avalonia.Markup.Data { - internal class IndexerNode : ExpressionNode, ISettableNode + internal class IndexerNode : IndexerNodeBase { public IndexerNode(IList arguments) { @@ -24,35 +24,7 @@ namespace Avalonia.Markup.Data public override string Description => "[" + string.Join(",", Arguments) + "]"; - protected override IObservable StartListeningCore(WeakReference reference) - { - var target = reference.Target; - var incc = target as INotifyCollectionChanged; - var inpc = target as INotifyPropertyChanged; - var inputs = new List>(); - - if (incc != null) - { - inputs.Add(WeakObservable.FromEventPattern( - incc, - nameof(incc.CollectionChanged)) - .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) - .Select(_ => GetValue(target))); - } - - if (inpc != null) - { - inputs.Add(WeakObservable.FromEventPattern( - inpc, - nameof(inpc.PropertyChanged)) - .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) - .Select(_ => GetValue(target))); - } - - return Observable.Merge(inputs).StartWith(GetValue(target)); - } - - public bool SetTargetValue(object value, BindingPriority priority) + public override bool SetTargetValue(object value, BindingPriority priority) { var typeInfo = Target.Target.GetType().GetTypeInfo(); var list = Target.Target as IList; @@ -154,9 +126,9 @@ namespace Avalonia.Markup.Data public IList Arguments { get; } - public Type PropertyType => GetIndexer(Target.Target.GetType().GetTypeInfo())?.PropertyType; + public override Type PropertyType => GetIndexer(Target.Target.GetType().GetTypeInfo())?.PropertyType; - private object GetValue(object target) + protected override object GetValue(object target) { var typeInfo = target.GetType().GetTypeInfo(); var list = target as IList; @@ -309,45 +281,19 @@ namespace Avalonia.Markup.Data } } - private bool ShouldUpdate(object sender, NotifyCollectionChangedEventArgs e) + protected override bool ShouldUpdate(object sender, PropertyChangedEventArgs e) { - if (sender is IList) - { - object indexObject; - - if (!TypeUtilities.TryConvert(typeof(int), Arguments[0], CultureInfo.InvariantCulture, out indexObject)) - { - return false; - } - - var index = (int)indexObject; - - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - return index >= e.NewStartingIndex; - case NotifyCollectionChangedAction.Remove: - return index >= e.OldStartingIndex; - case NotifyCollectionChangedAction.Replace: - return index >= e.NewStartingIndex && - index < e.NewStartingIndex + e.NewItems.Count; - case NotifyCollectionChangedAction.Move: - return (index >= e.NewStartingIndex && - index < e.NewStartingIndex + e.NewItems.Count) || - (index >= e.OldStartingIndex && - index < e.OldStartingIndex + e.OldItems.Count); - case NotifyCollectionChangedAction.Reset: - return true; - } - } - - return true; // Implementation defined meaning for the index, so just try to update anyway + var typeInfo = sender.GetType().GetTypeInfo(); + return typeInfo.GetDeclaredProperty(e.PropertyName)?.GetIndexParameters().Any() ?? false; } - private bool ShouldUpdate(object sender, PropertyChangedEventArgs e) + protected override int? TryGetFirstArgumentAsInt() { - var typeInfo = sender.GetType().GetTypeInfo(); - return typeInfo.GetDeclaredProperty(e.PropertyName)?.GetIndexParameters().Any() ?? false; + if (TypeUtilities.TryConvert(typeof(int), Arguments[0], CultureInfo.InvariantCulture, out var value)) + { + return (int?)value; + } + return null; } } } diff --git a/src/Markup/Avalonia.Markup/Data/IndexerNodeBase.cs b/src/Markup/Avalonia.Markup/Data/IndexerNodeBase.cs new file mode 100644 index 0000000000..d3a4a818fe --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/IndexerNodeBase.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Reactive.Linq; +using System.Reflection; +using System.Text; +using Avalonia.Data; +using Avalonia.Utilities; + +namespace Avalonia.Markup.Data +{ + abstract class IndexerNodeBase : ExpressionNode, ISettableNode + { + protected override IObservable StartListeningCore(WeakReference reference) + { + var target = reference.Target; + var inputs = new List>(); + + if (target is INotifyCollectionChanged incc) + { + inputs.Add(WeakObservable.FromEventPattern( + incc, + nameof(incc.CollectionChanged)) + .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) + .Select(_ => GetValue(target))); + } + + if (target is INotifyPropertyChanged inpc) + { + inputs.Add(WeakObservable.FromEventPattern( + inpc, + nameof(inpc.PropertyChanged)) + .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) + .Select(_ => GetValue(target))); + } + + return inputs.Merge().StartWith(GetValue(target)); + } + + public abstract bool SetTargetValue(object value, BindingPriority priority); + + public abstract Type PropertyType { get; } + + protected abstract object GetValue(object target); + + protected abstract int? TryGetFirstArgumentAsInt(); + + private bool ShouldUpdate(object sender, NotifyCollectionChangedEventArgs e) + { + if (sender is IList) + { + var index = TryGetFirstArgumentAsInt(); + + if (index == null) + { + return false; + } + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + return index >= e.NewStartingIndex; + case NotifyCollectionChangedAction.Remove: + return index >= e.OldStartingIndex; + case NotifyCollectionChangedAction.Replace: + return index >= e.NewStartingIndex && + index < e.NewStartingIndex + e.NewItems.Count; + case NotifyCollectionChangedAction.Move: + return (index >= e.NewStartingIndex && + index < e.NewStartingIndex + e.NewItems.Count) || + (index >= e.OldStartingIndex && + index < e.OldStartingIndex + e.OldItems.Count); + case NotifyCollectionChangedAction.Reset: + return true; + } + } + + return true; // Implementation defined meaning for the index, so just try to update anyway + } + + protected abstract bool ShouldUpdate(object sender, PropertyChangedEventArgs e); + } +} diff --git a/src/Markup/Avalonia.Markup/Data/Parsers/ExpressionTreeParser.cs b/src/Markup/Avalonia.Markup/Data/Parsers/ExpressionTreeParser.cs new file mode 100644 index 0000000000..9e225ffcc5 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/Parsers/ExpressionTreeParser.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; + +namespace Avalonia.Markup.Data.Parsers +{ + class ExpressionTreeParser + { + private readonly bool enableDataValidation; + + public ExpressionTreeParser(bool enableDataValidation) + { + this.enableDataValidation = enableDataValidation; + } + + public ExpressionNode Parse(Expression expr) + { + var visitor = new ExpressionVisitorNodeBuilder(enableDataValidation); + + visitor.Visit(expr); + + var nodes = visitor.Nodes; + + for (int n = 0; n < nodes.Count - 1; ++n) + { + nodes[n].Next = nodes[n + 1]; + } + + return nodes.FirstOrDefault() ?? new EmptyExpressionNode(); + } + } +} diff --git a/src/Markup/Avalonia.Markup/Data/Parsers/ExpressionVisitorNodeBuilder.cs b/src/Markup/Avalonia.Markup/Data/Parsers/ExpressionVisitorNodeBuilder.cs new file mode 100644 index 0000000000..0126d31098 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/Parsers/ExpressionVisitorNodeBuilder.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; + +namespace Avalonia.Markup.Data.Parsers +{ + class ExpressionVisitorNodeBuilder : ExpressionVisitor + { + private static PropertyInfo AvaloniaObjectIndexer; + + private readonly bool enableDataValidation; + + static ExpressionVisitorNodeBuilder() + { + AvaloniaObjectIndexer = typeof(AvaloniaObject).GetProperty("Item", new[] { typeof(AvaloniaProperty) }); + } + + public List Nodes { get; } + + public ExpressionVisitorNodeBuilder(bool enableDataValidation) + { + this.enableDataValidation = enableDataValidation; + Nodes = new List(); + } + + protected override Expression VisitUnary(UnaryExpression node) + { + if (node.NodeType != ExpressionType.Not || node.Type != typeof(bool)) + { + throw new ExpressionParseException(0, $"Invalid unary operation {node.NodeType} in binding expression"); + } + + Nodes.Add(new LogicalNotNode()); + + return base.VisitUnary(node); + } + + protected override Expression VisitMember(MemberExpression node) + { + Nodes.Add(new PropertyAccessorNode(node.Member.Name, enableDataValidation)); + return base.VisitMember(node); + } + + protected override Expression VisitIndex(IndexExpression node) + { + if (node.Indexer == AvaloniaObjectIndexer) + { + var property = GetArgumentExpressionValue(node.Arguments[0]); + Nodes.Add(new PropertyAccessorNode($"{property.OwnerType.Name}.{property.Name}", enableDataValidation)); + } + else + { + Nodes.Add(new IndexerExpressionNode(node)); + } + + return node; + } + + private T GetArgumentExpressionValue(Expression expr) + { + try + { + return Expression.Lambda>(expr).Compile(preferInterpretation: true)(); + } + catch (InvalidOperationException ex) + { + throw new ExpressionParseException(0, "Unable to parse indexer value.", ex); + } + } + + protected override Expression VisitBinary(BinaryExpression node) + { + if (node.NodeType == ExpressionType.ArrayIndex) + { + return base.VisitBinary(node); + } + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitBlock(BlockExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override CatchBlock VisitCatchBlock(CatchBlock node) + { + throw new ExpressionParseException(0, $"Catch blocks are not allowed in binding expressions."); + } + + protected override Expression VisitConditional(ConditionalExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitDynamic(DynamicExpression node) + { + throw new ExpressionParseException(0, $"Dynamic expressions are not allowed in binding expressions."); + } + + protected override ElementInit VisitElementInit(ElementInit node) + { + throw new ExpressionParseException(0, $"Element init expressions are not valid in a binding expression."); + } + + protected override Expression VisitGoto(GotoExpression node) + { + throw new ExpressionParseException(0, $"Goto expressions not supported in binding expressions."); + } + + protected override Expression VisitInvocation(InvocationExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitLabel(LabelExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitListInit(ListInitExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitLoop(LoopExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override MemberAssignment VisitMemberAssignment(MemberAssignment node) + { + throw new ExpressionParseException(0, $"Member assignments not supported in binding expressions."); + } + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitSwitch(SwitchExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitTry(TryExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitTypeBinary(TypeBinaryExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + } +} diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_ExpressionTree.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_ExpressionTree.cs new file mode 100644 index 0000000000..3fdd9ffc10 --- /dev/null +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_ExpressionTree.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Markup.Data; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.UnitTests.Data +{ + public class ExpressionObserverTests_ExpressionTree + { + [Fact] + public async Task IdentityExpression_Creates_IdentityObserver() + { + var target = new object(); + + var observer = ExpressionObserver.CreateFromExpression(target, o => o); + + Assert.Equal(target, await observer.Take(1)); + } + + [Fact] + public async Task Property_Access_Expression_Observes_Property() + { + var target = new Class1(); + + var observer = ExpressionObserver.CreateFromExpression(target, o => o.Foo); + + Assert.Null(await observer.Take(1)); + + using (observer.Subscribe(_ => {})) + { + target.Foo = "Test"; + } + + Assert.Equal("Test", await observer.Take(1)); + + GC.KeepAlive(target); + } + + [Fact] + public void Property_Acccess_Expression_Can_Set_Property() + { + var data = new Class1(); + var target = ExpressionObserver.CreateFromExpression(data, o => o.Foo); + + using (target.Subscribe(_ => { })) + { + Assert.True(target.SetValue("baz")); + } + + GC.KeepAlive(data); + } + + [Fact] + public async Task Indexer_Accessor_Can_Read_Value() + { + var data = new[] { 1, 2, 3, 4 }; + + var target = ExpressionObserver.CreateFromExpression(data, o => o[0]); + + Assert.Equal(data[0], await target.Take(1)); + } + + [Fact] + public async Task Indexer_Accessor_Can_Read_Complex_Index() + { + var data = new Dictionary(); + + var key = new object(); + + data.Add(key, new object()); + + var target = ExpressionObserver.CreateFromExpression(data, o => o[key]); + + Assert.Equal(data[key], await target.Take(1)); + } + + [Fact] + public void Indexer_Can_Set_Value() + { + var data = new[] { 1, 2, 3, 4 }; + + var target = ExpressionObserver.CreateFromExpression(data, o => o[0]); + + using (target.Subscribe(_ => { })) + { + Assert.True(target.SetValue(2)); + } + + GC.KeepAlive(data); + } + + private class Class1 : NotifyingBase + { + private string _foo; + + public string Foo + { + get { return _foo; } + set + { + _foo = value; + RaisePropertyChanged(nameof(Foo)); + } + } + } + } +} From bf6375fe266f3825e59eaf5f020c9bfbe955f45a Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 29 May 2018 16:49:08 -0500 Subject: [PATCH 11/94] Fix indexer and casting expressions. --- .../Parsers/ExpressionVisitorNodeBuilder.cs | 49 ++++++++++-- .../ExpressionObserverTests_ExpressionTree.cs | 80 +++++++++++++++++++ 2 files changed, 122 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs index 8564bf5111..5affe227e1 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs +++ b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -28,24 +29,43 @@ namespace Avalonia.Data.Core.Parsers protected override Expression VisitUnary(UnaryExpression node) { - if (node.NodeType != ExpressionType.Not || node.Type != typeof(bool)) + if (node.NodeType == ExpressionType.Not && node.Type == typeof(bool)) { - throw new ExpressionParseException(0, $"Invalid unary operation {node.NodeType} in binding expression"); + Nodes.Add(new LogicalNotNode()); + } + else if (node.NodeType == ExpressionType.Convert) + { + if (node.Operand.Type.IsAssignableFrom(node.Type)) + { + // Ignore inheritance casts + } + else + { + throw new ExpressionParseException(0, $"Cannot parse non-inheritance casts in a binding expression."); + } + } + else if (node.NodeType == ExpressionType.TypeAs) + { + // Ignore as operator. + } + else + { + throw new ExpressionParseException(0, $"Unable to parse unary operator {node.NodeType} in a binding expression"); } - - Nodes.Add(new LogicalNotNode()); return base.VisitUnary(node); } protected override Expression VisitMember(MemberExpression node) { + var visited = base.VisitMember(node); Nodes.Add(new PropertyAccessorNode(node.Member.Name, enableDataValidation)); - return base.VisitMember(node); + return visited; } protected override Expression VisitIndex(IndexExpression node) { + var visited = base.VisitIndex(node); if (node.Indexer == AvaloniaObjectIndexer) { var property = GetArgumentExpressionValue(node.Arguments[0]); @@ -56,7 +76,7 @@ namespace Avalonia.Data.Core.Parsers Nodes.Add(new IndexerExpressionNode(node)); } - return node; + return visited; } private T GetArgumentExpressionValue(Expression expr) @@ -75,7 +95,8 @@ namespace Avalonia.Data.Core.Parsers { if (node.NodeType == ExpressionType.ArrayIndex) { - return base.VisitBinary(node); + base.VisitBinary(node); + return Visit(Expression.MakeIndex(node.Left, null, new[] { node.Right })); } throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); } @@ -137,9 +158,23 @@ namespace Avalonia.Data.Core.Parsers protected override Expression VisitMethodCall(MethodCallExpression node) { + base.VisitMethodCall(node); + var property = TryGetPropertyFromMethod(node.Method); + + if (property != null) + { + return Visit(Expression.MakeIndex(node.Object, property, node.Arguments)); + } + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); } + private PropertyInfo TryGetPropertyFromMethod(MethodInfo method) + { + var type = method.DeclaringType; + return type.GetRuntimeProperties().FirstOrDefault(prop => prop.GetMethod == method); + } + protected override Expression VisitSwitch(SwitchExpression node) { throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs index 1ec4bdb4f5..ebf3ca2a49 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs @@ -19,6 +19,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var observer = ExpressionObserver.CreateFromExpression(target, o => o); Assert.Equal(target, await observer.Take(1)); + GC.KeepAlive(target); } [Fact] @@ -62,6 +63,18 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = ExpressionObserver.CreateFromExpression(data, o => o[0]); Assert.Equal(data[0], await target.Take(1)); + GC.KeepAlive(data); + } + + [Fact] + public async Task Indexer_List_Accessor_Can_Read_Value() + { + var data = new List { 1, 2, 3, 4 }; + + var target = ExpressionObserver.CreateFromExpression(data, o => o[0]); + + Assert.Equal(data[0], await target.Take(1)); + GC.KeepAlive(data); } [Fact] @@ -76,6 +89,8 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = ExpressionObserver.CreateFromExpression(data, o => o[key]); Assert.Equal(data[key], await target.Take(1)); + + GC.KeepAlive(data); } [Fact] @@ -93,6 +108,62 @@ namespace Avalonia.Base.UnitTests.Data.Core GC.KeepAlive(data); } + [Fact] + public async Task Inheritance_Casts_Should_Be_Ignored() + { + NotifyingBase test = new Class1 { Foo = "Test" }; + + var target = ExpressionObserver.CreateFromExpression(test, o => ((Class1)o).Foo); + + Assert.Equal("Test", await target.Take(1)); + + GC.KeepAlive(test); + } + + [Fact] + public void Convert_Casts_Should_Error() + { + var test = 1; + + Assert.Throws(() => ExpressionObserver.CreateFromExpression(test, o => (double)o)); + } + + [Fact] + public async Task As_Operator_Should_Be_Ignored() + { + NotifyingBase test = new Class1 { Foo = "Test" }; + + var target = ExpressionObserver.CreateFromExpression(test, o => (o as Class1).Foo); + + Assert.Equal("Test", await target.Take(1)); + + GC.KeepAlive(test); + } + + [Fact] + public async Task Avalonia_Property_Indexer_Reads_Avalonia_Property_Value() + { + var test = new Class2(); + + var target = ExpressionObserver.CreateFromExpression(test, o => o[Class2.FooProperty]); + + Assert.Equal("foo", await target.Take(1)); + + GC.KeepAlive(test); + } + + [Fact] + public async Task Complex_Expression_Correctly_Parsed() + { + var test = new Class1 { Foo = "Test" }; + + var target = ExpressionObserver.CreateFromExpression(test, o => o.Foo.Length); + + Assert.Equal(test.Foo.Length, await target.Take(1)); + + GC.KeepAlive(test); + } + private class Class1 : NotifyingBase { private string _foo; @@ -107,5 +178,14 @@ namespace Avalonia.Base.UnitTests.Data.Core } } } + + + private class Class2 : AvaloniaObject + { + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register("Foo", defaultValue: "foo"); + + public string ClrProperty { get; } = "clr-property"; + } } } From d2823110318865693625463f92eaf42c64ed98c4 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 31 May 2018 18:39:03 -0500 Subject: [PATCH 12/94] Fix bug in ExpressionVisitorNodeBuilder. --- .../Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs index 5affe227e1..433cfd1889 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs +++ b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs @@ -65,7 +65,8 @@ namespace Avalonia.Data.Core.Parsers protected override Expression VisitIndex(IndexExpression node) { - var visited = base.VisitIndex(node); + Visit(node.Object); + if (node.Indexer == AvaloniaObjectIndexer) { var property = GetArgumentExpressionValue(node.Arguments[0]); @@ -76,7 +77,7 @@ namespace Avalonia.Data.Core.Parsers Nodes.Add(new IndexerExpressionNode(node)); } - return visited; + return node; } private T GetArgumentExpressionValue(Expression expr) @@ -158,7 +159,6 @@ namespace Avalonia.Data.Core.Parsers protected override Expression VisitMethodCall(MethodCallExpression node) { - base.VisitMethodCall(node); var property = TryGetPropertyFromMethod(node.Method); if (property != null) From e576ec178c95ccbd8a5772754ad1c30d87465d26 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 6 Jun 2018 21:06:05 +0200 Subject: [PATCH 13/94] Initial --- src/Avalonia.Controls/AppBuilderBase.cs | 45 +++--- src/Avalonia.Controls/Application.cs | 118 ++++++++++++++- src/Avalonia.Controls/ExitMode.cs | 12 ++ src/Avalonia.Controls/Window.cs | 51 ++++--- src/Avalonia.Controls/WindowCollection.cs | 134 ++++++++++++++++++ .../ApplicationTests.cs | 107 ++++++++++++++ .../WindowTests.cs | 10 +- 7 files changed, 428 insertions(+), 49 deletions(-) create mode 100644 src/Avalonia.Controls/ExitMode.cs create mode 100644 src/Avalonia.Controls/WindowCollection.cs create mode 100644 tests/Avalonia.Controls.UnitTests/ApplicationTests.cs diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index 7af3deef34..875f5263c2 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -15,7 +15,7 @@ namespace Avalonia.Controls public abstract class AppBuilderBase where TAppBuilder : AppBuilderBase, new() { private static bool s_setupWasAlreadyCalled; - + /// /// Gets or sets the instance. /// @@ -92,7 +92,7 @@ namespace Avalonia.Controls }; } - protected TAppBuilder Self => (TAppBuilder) this; + protected TAppBuilder Self => (TAppBuilder)this; /// /// Registers a callback to call before Start is called on the . @@ -125,7 +125,6 @@ namespace Avalonia.Controls var window = new TMainWindow(); if (dataContextProvider != null) window.DataContext = dataContextProvider(); - window.Show(); Instance.Run(window); } @@ -143,7 +142,6 @@ namespace Avalonia.Controls if (dataContextProvider != null) mainWindow.DataContext = dataContextProvider(); - mainWindow.Show(); Instance.Run(mainWindow); } @@ -209,6 +207,17 @@ namespace Avalonia.Controls public TAppBuilder UseAvaloniaModules() => AfterSetup(builder => SetupAvaloniaModules()); + /// + /// Sets the shutdown mode of the application. + /// + /// The shutdown mode. + /// + public TAppBuilder SetExitMode(ExitMode exitMode) + { + Instance.ExitMode = exitMode; + return Self; + } + private bool CheckSetup { get; set; } = true; /// @@ -223,20 +232,20 @@ namespace Avalonia.Controls private void SetupAvaloniaModules() { var moduleInitializers = from assembly in AvaloniaLocator.Current.GetService().GetLoadedAssemblies() - from attribute in assembly.GetCustomAttributes() - where attribute.ForWindowingSubsystem == "" - || attribute.ForWindowingSubsystem == WindowingSubsystemName - where attribute.ForRenderingSubsystem == "" - || attribute.ForRenderingSubsystem == RenderingSubsystemName - group attribute by attribute.Name into exports - select (from export in exports - orderby export.ForWindowingSubsystem.Length descending - orderby export.ForRenderingSubsystem.Length descending - select export).First().ModuleType into moduleType - select (from constructor in moduleType.GetTypeInfo().DeclaredConstructors - where constructor.GetParameters().Length == 0 && !constructor.IsStatic - select constructor).Single() into constructor - select (Action)(() => constructor.Invoke(new object[0])); + from attribute in assembly.GetCustomAttributes() + where attribute.ForWindowingSubsystem == "" + || attribute.ForWindowingSubsystem == WindowingSubsystemName + where attribute.ForRenderingSubsystem == "" + || attribute.ForRenderingSubsystem == RenderingSubsystemName + group attribute by attribute.Name into exports + select (from export in exports + orderby export.ForWindowingSubsystem.Length descending + orderby export.ForRenderingSubsystem.Length descending + select export).First().ModuleType into moduleType + select (from constructor in moduleType.GetTypeInfo().DeclaredConstructors + where constructor.GetParameters().Length == 0 && !constructor.IsStatic + select constructor).Single() into constructor + select (Action)(() => constructor.Invoke(new object[0])); Delegate.Combine(moduleInitializers.ToArray()).DynamicInvoke(); } diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 6fdca557eb..ffe4a9c513 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -43,11 +43,15 @@ namespace Avalonia private Styles _styles; private IResourceDictionary _resources; + private CancellationTokenSource _mainLoopCancellationTokenSource; + /// /// Initializes a new instance of the class. /// public Application() { + Windows = new WindowCollection(this); + OnExit += OnExiting; } @@ -158,6 +162,40 @@ namespace Avalonia /// IResourceNode IResourceNode.ResourceParent => null; + /// + /// Gets or sets the . This property indicates whether the application exits explicitly or implicitly. + /// If is set to OnExplicitExit the application is only closes if Exit is called. + /// The default is OnLastWindowClose + /// + /// + /// The shutdown mode. + /// + public ExitMode ExitMode { get; set; } + + /// + /// Gets or sets the main window of the application. + /// + /// + /// The main window. + /// + public Window MainWindow { get; set; } + + /// + /// Gets the open windows of the application. + /// + /// + /// The windows. + /// + public WindowCollection Windows { get; } + + /// + /// Gets or sets a value indicating whether this instance is existing. + /// + /// + /// true if this instance is existing; otherwise, false. + /// + internal bool IsExiting { get; set; } + /// /// Initializes the application by loading XAML etc. /// @@ -171,19 +209,81 @@ namespace Avalonia /// The closable to track public void Run(ICloseable closable) { - var source = new CancellationTokenSource(); - closable.Closed += OnExiting; - closable.Closed += (s, e) => source.Cancel(); - Dispatcher.UIThread.MainLoop(source.Token); + if (_mainLoopCancellationTokenSource != null) + { + throw new Exception("Run should only called once"); + } + + closable.Closed += (s, e) => Exit(); + + _mainLoopCancellationTokenSource = new CancellationTokenSource(); + + Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token); + + // Make sure we call OnExit in case an error happened and Exit() wasn't called explicitly + if (!IsExiting) + { + OnExit?.Invoke(this, EventArgs.Empty); + } + } + + /// + /// Runs the application's main loop until some condition occurs that is specified by ExitMode. + /// + /// The main window + public void Run(Window mainWindow) + { + if (_mainLoopCancellationTokenSource != null) + { + throw new Exception("Run should only called once"); + } + + _mainLoopCancellationTokenSource = new CancellationTokenSource(); + + Dispatcher.UIThread.InvokeAsync( + () => + { + if (mainWindow == null) + { + return; + } + + if (MainWindow != null) + { + return; + } + + if (!mainWindow.IsVisible) + { + mainWindow.Show(); + } + + MainWindow = mainWindow; + }, + DispatcherPriority.Send); + + Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token); + + // Make sure we call OnExit in case an error happened and Exit() wasn't called explicitly + if (!IsExiting) + { + OnExit?.Invoke(this, EventArgs.Empty); + } } - + /// - /// Runs the application's main loop until the is cancelled. + /// Runs the application's main loop until the is canceled. /// /// The token to track public void Run(CancellationToken token) { Dispatcher.UIThread.MainLoop(token); + + // Make sure we call OnExit in case an error happened and Exit() wasn't called explicitly + if (!IsExiting) + { + OnExit?.Invoke(this, EventArgs.Empty); + } } /// @@ -191,7 +291,13 @@ namespace Avalonia /// public void Exit() { + IsExiting = true; + + Windows.Clear(); + OnExit?.Invoke(this, EventArgs.Empty); + + _mainLoopCancellationTokenSource?.Cancel(); } /// diff --git a/src/Avalonia.Controls/ExitMode.cs b/src/Avalonia.Controls/ExitMode.cs new file mode 100644 index 0000000000..0c5ecd7171 --- /dev/null +++ b/src/Avalonia.Controls/ExitMode.cs @@ -0,0 +1,12 @@ +// 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. + +namespace Avalonia +{ + public enum ExitMode + { + OnLastWindowClose, + OnMainWindowClose, + OnExplicitExit + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 3cbfdbd657..c19c69ce73 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -49,14 +49,6 @@ namespace Avalonia.Controls /// public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot, INameScope { - private static List s_windows = new List(); - - /// - /// Retrieves an enumeration of all Windows in the currently running application. - /// - public static IReadOnlyList OpenWindows => s_windows; - - /// /// Defines the property. /// public static readonly StyledProperty SizeToContentProperty = @@ -75,7 +67,7 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(ShowInTaskbar), true); /// - /// Enables or disables the taskbar icon + /// Represents the current window state (normal, minimized, maximized) /// public static readonly StyledProperty WindowStateProperty = AvaloniaProperty.Register(nameof(WindowState)); @@ -117,7 +109,7 @@ namespace Avalonia.Controls BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White); TitleProperty.Changed.AddClassHandler((s, e) => s.PlatformImpl?.SetTitle((string)e.NewValue)); HasSystemDecorationsProperty.Changed.AddClassHandler( - (s, e) => s.PlatformImpl?.SetSystemDecorations((bool) e.NewValue)); + (s, e) => s.PlatformImpl?.SetSystemDecorations((bool)e.NewValue)); ShowInTaskbarProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.ShowTaskbarIcon((bool)e.NewValue)); @@ -149,7 +141,7 @@ namespace Avalonia.Controls _maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size); Screens = new Screens(PlatformImpl?.Screen); } - + /// event EventHandler INameScope.Registered { @@ -199,7 +191,7 @@ namespace Avalonia.Controls get { return GetValue(HasSystemDecorationsProperty); } set { SetValue(HasSystemDecorationsProperty, value); } } - + /// /// Enables or disables the taskbar icon /// @@ -259,6 +251,26 @@ namespace Avalonia.Controls /// public event EventHandler Closing; + private static void AddWindow(Window window) + { + if (Application.Current == null) + { + return; + } + + Application.Current.Windows.Add(window); + } + + private static void RemoveWindow(Window window) + { + if (Application.Current == null) + { + return; + } + + Application.Current.Windows.Remove(window); + } + /// /// Closes the window. /// @@ -298,10 +310,9 @@ namespace Avalonia.Controls finally { if (ignoreCancel || !cancelClosing) - { - s_windows.Remove(this); + { PlatformImpl?.Dispose(); - IsVisible = false; + HandleClosed(); } } } @@ -359,7 +370,7 @@ namespace Avalonia.Controls return; } - s_windows.Add(this); + AddWindow(this); EnsureInitialized(); SetWindowStartupLocation(); @@ -400,7 +411,7 @@ namespace Avalonia.Controls throw new InvalidOperationException("The window is already being shown."); } - s_windows.Add(this); + AddWindow(this); EnsureInitialized(); SetWindowStartupLocation(); @@ -409,7 +420,7 @@ namespace Avalonia.Controls using (BeginAutoSizing()) { - var affectedWindows = s_windows.Where(w => w.IsEnabled && w != this).ToList(); + var affectedWindows = Application.Current.Windows.Where(w => w.IsEnabled && w != this).ToList(); var activated = affectedWindows.Where(w => w.IsActive).FirstOrDefault(); SetIsEnabled(affectedWindows, false); @@ -513,8 +524,8 @@ namespace Avalonia.Controls protected override void HandleClosed() { - IsVisible = false; - s_windows.Remove(this); + RemoveWindow(this); + base.HandleClosed(); } diff --git a/src/Avalonia.Controls/WindowCollection.cs b/src/Avalonia.Controls/WindowCollection.cs new file mode 100644 index 0000000000..c21a12f05b --- /dev/null +++ b/src/Avalonia.Controls/WindowCollection.cs @@ -0,0 +1,134 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Collections; +using System.Collections.Generic; + +using Avalonia.Controls; + +namespace Avalonia +{ + public class WindowCollection : IReadOnlyList + { + private readonly Application _application; + private readonly List _windows = new List(); + + public WindowCollection(Application application) + { + _application = application; + } + + /// + /// + /// Gets the number of elements in the collection. + /// + public int Count => _windows.Count; + + /// + /// + /// Gets the at the specified index. + /// + /// + /// The . + /// + /// The index. + /// + public Window this[int index] => _windows[index]; + + /// + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// An enumerator that can be used to iterate through the collection. + /// + public IEnumerator GetEnumerator() + { + return _windows.GetEnumerator(); + } + + /// + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Adds the specified window. + /// + /// The window. + internal void Add(Window window) + { + if (window == null) + { + return; + } + + _windows.Add(window); + } + + /// + /// Removes the specified window. + /// + /// The window. + internal void Remove(Window window) + { + if (window == null) + { + return; + } + + _windows.Remove(window); + + OnRemoveWindow(window); + } + + /// + /// Closes all windows and removes them from the underlying collection. + /// + internal void Clear() + { + while (_windows.Count > 0) + { + _windows[0].Close(); + } + } + + private void OnRemoveWindow(Window window) + { + if (window == null) + { + return; + } + + if (_application.IsExiting) + { + return; + } + + switch (_application.ExitMode) + { + case ExitMode.OnLastWindowClose: + if (Count == 0) + { + _application.Exit(); + } + + break; + case ExitMode.OnMainWindowClose: + if (window == _application.MainWindow) + { + _application.Exit(); + } + + break; + } + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs b/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs new file mode 100644 index 0000000000..85f95b2b5c --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs @@ -0,0 +1,107 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Collections.Generic; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class ApplicationTests + { + [Fact] + public void Should_Exit_After_MainWindow_Closed() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + Application.Current.ExitMode = ExitMode.OnMainWindowClose; + + var mainWindow = new Window(); + + mainWindow.Show(); + + Application.Current.MainWindow = mainWindow; + + var window = new Window(); + + window.Show(); + + mainWindow.Close(); + + Assert.True(Application.Current.IsExiting); + } + } + + [Fact] + public void Should_Exit_After_Last_Window_Closed() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + Application.Current.ExitMode = ExitMode.OnLastWindowClose; + + var windowA = new Window(); + + windowA.Show(); + + var windowB = new Window(); + + windowB.Show(); + + windowA.Close(); + + Assert.False(Application.Current.IsExiting); + + windowB.Close(); + + Assert.True(Application.Current.IsExiting); + } + } + + [Fact] + public void Should_Only_Exit_On_Explicit_Exit() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + Application.Current.ExitMode = ExitMode.OnExplicitExit; + + var windowA = new Window(); + + windowA.Show(); + + var windowB = new Window(); + + windowB.Show(); + + windowA.Close(); + + Assert.False(Application.Current.IsExiting); + + windowB.Close(); + + Assert.False(Application.Current.IsExiting); + + Application.Current.Exit(); + + Assert.True(Application.Current.IsExiting); + } + } + + [Fact] + public void Should_Close_All_Remaining_Open_Windows_After_Explicit_Exit_Call() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var windows = new List { new Window(), new Window(), new Window(), new Window() }; + + foreach (var window in windows) + { + window.Show(); + } + + Application.Current.Exit(); + + Assert.Empty(Application.Current.Windows); + } + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index a85c4df8af..e80ffd97cd 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -129,7 +129,7 @@ namespace Avalonia.Controls.UnitTests window.Show(); - Assert.Equal(new[] { window }, Window.OpenWindows); + Assert.Equal(new[] { window }, Application.Current.Windows); } } @@ -145,7 +145,7 @@ namespace Avalonia.Controls.UnitTests window.Show(); window.IsVisible = true; - Assert.Equal(new[] { window }, Window.OpenWindows); + Assert.Equal(new[] { window }, Application.Current.Windows); window.Close(); } @@ -162,7 +162,7 @@ namespace Avalonia.Controls.UnitTests window.Show(); window.Close(); - Assert.Empty(Window.OpenWindows); + Assert.Empty(Application.Current.Windows); } } @@ -184,7 +184,7 @@ namespace Avalonia.Controls.UnitTests window.Show(); windowImpl.Object.Closed(); - Assert.Empty(Window.OpenWindows); + Assert.Empty(Application.Current.Windows); } } @@ -339,7 +339,7 @@ namespace Avalonia.Controls.UnitTests { // HACK: We really need a decent way to have "statics" that can be scoped to // AvaloniaLocator scopes. - ((IList)Window.OpenWindows).Clear(); + Application.Current.Windows.Clear(); } } } From 4f26d4fc0865c71a346f526dd13b300df546aa63 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 7 Jun 2018 15:32:57 +0100 Subject: [PATCH 14/94] dont highlight selected menuitems. --- src/Avalonia.Themes.Default/MenuItem.xaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index efb31175fa..319ff189f0 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -122,11 +122,6 @@ - -