diff --git a/build.cake b/build.cake index 24f529ee4a..56653109ae 100644 --- a/build.cake +++ b/build.cake @@ -170,6 +170,7 @@ Task("Run-Unit-Tests-Impl") RunCoreTest("./tests/Avalonia.Styling.UnitTests", data.Parameters, false); RunCoreTest("./tests/Avalonia.Visuals.UnitTests", data.Parameters, false); RunCoreTest("./tests/Avalonia.Skia.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.ReactiveUI.UnitTests", data.Parameters, false); if (data.Parameters.IsRunningOnWindows) { RunCoreTest("./tests/Avalonia.Direct2D1.UnitTests", data.Parameters, false); diff --git a/build/ReactiveUI.props b/build/ReactiveUI.props index acdfdd215a..1208be34b8 100644 --- a/build/ReactiveUI.props +++ b/build/ReactiveUI.props @@ -1,5 +1,5 @@ - + diff --git a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs index eb08ef9656..eba17f92e4 100644 --- a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs @@ -7,6 +7,7 @@ using System.Linq; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Primitives; +using ReactiveUI.Legacy; using ReactiveUI; namespace VirtualizationDemo.ViewModels diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index 882d2f4ddd..f1a1f94a01 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -15,7 +15,7 @@ namespace Avalonia.Controls.Generators /// public class ItemContainerGenerator : IItemContainerGenerator { - private Dictionary _containers = new Dictionary(); + private SortedDictionary _containers = new SortedDictionary(); /// /// Initializes a new instance of the class. @@ -246,4 +246,4 @@ namespace Avalonia.Controls.Generators Recycled?.Invoke(this, e); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index d7db04f369..f8440aac47 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -289,12 +289,12 @@ namespace Avalonia.Controls.Primitives /// public override void EndInit() { - base.EndInit(); - if (--_updateCount == 0) { UpdateFinished(); } + + base.EndInit(); } /// diff --git a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs index 3eab54115a..d763febdf3 100644 --- a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs +++ b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs @@ -4,6 +4,7 @@ using Avalonia.Controls; using Avalonia.Threading; using ReactiveUI; +using Splat; namespace Avalonia { @@ -15,6 +16,9 @@ namespace Avalonia return builder.AfterSetup(_ => { RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; + Locator.CurrentMutable.Register( + () => new AvaloniaActivationForViewFetcher(), + typeof(IActivationForViewFetcher)); }); } } diff --git a/src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs b/src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs new file mode 100644 index 0000000000..828d8024e6 --- /dev/null +++ b/src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs @@ -0,0 +1,38 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Reflection; +using System.Reactive.Linq; +using Avalonia; +using Avalonia.VisualTree; +using ReactiveUI; + +namespace Avalonia +{ + public class AvaloniaActivationForViewFetcher : IActivationForViewFetcher + { + public int GetAffinityForView(Type view) + { + return typeof(IVisual).GetTypeInfo().IsAssignableFrom(view.GetTypeInfo()) ? 10 : 0; + } + + public IObservable GetActivationForView(IActivatable view) + { + if (!(view is IVisual visual)) return Observable.Return(false); + var viewLoaded = Observable + .FromEventPattern( + x => visual.AttachedToVisualTree += x, + x => visual.DetachedFromVisualTree -= x) + .Select(args => true); + var viewUnloaded = Observable + .FromEventPattern( + x => visual.DetachedFromVisualTree += x, + x => visual.DetachedFromVisualTree -= x) + .Select(args => false); + return viewLoaded + .Merge(viewUnloaded) + .DistinctUntilChanged(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Visuals/Rendering/RenderLoop.cs b/src/Avalonia.Visuals/Rendering/RenderLoop.cs index d0d5b2250d..df32d9eec1 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLoop.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLoop.cs @@ -19,7 +19,8 @@ namespace Avalonia.Rendering private readonly IDispatcher _dispatcher; private List _items = new List(); private IRenderTimer _timer; - private int inTick; + private int _inTick; + private int _inUpdate; /// /// Initializes a new instance of the class. @@ -84,21 +85,36 @@ namespace Avalonia.Rendering } } - private async void TimerTick(TimeSpan time) + private void TimerTick(TimeSpan time) { - if (Interlocked.CompareExchange(ref inTick, 1, 0) == 0) + if (Interlocked.CompareExchange(ref _inTick, 1, 0) == 0) { try { - if (_items.Any(item => item.NeedsUpdate)) + if (_items.Any(item => item.NeedsUpdate) && + Interlocked.CompareExchange(ref _inUpdate, 1, 0) == 0) { - await _dispatcher.InvokeAsync(() => + _dispatcher.Post(() => { - foreach (var i in _items) + for (var i = 0; i < _items.Count; ++i) { - i.Update(time); + var item = _items[i]; + + if (item.NeedsUpdate) + { + try + { + item.Update(time); + } + catch (Exception ex) + { + Logger.Error(LogArea.Visual, this, "Exception in render update: {Error}", ex); + } + } } - }, DispatcherPriority.Render).ConfigureAwait(false); + + Interlocked.Exchange(ref _inUpdate, 0); + }, DispatcherPriority.Render); } foreach (var i in _items) @@ -112,7 +128,7 @@ namespace Avalonia.Rendering } finally { - Interlocked.Exchange(ref inTick, 0); + Interlocked.Exchange(ref _inTick, 0); } } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index d6b95bc7b0..c3c867d3fb 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -976,6 +976,9 @@ namespace Avalonia.Win32.Interop [DllImport("shcore.dll")] public static extern void GetScaleFactorForMonitor(IntPtr hMon, out uint pScale); + [DllImport("user32.dll", SetLastError = true)] + public static extern bool SetProcessDPIAware(); + [DllImport("user32.dll")] public static extern IntPtr MonitorFromPoint(POINT pt, MONITOR dwFlags); @@ -987,7 +990,7 @@ namespace Avalonia.Win32.Interop [DllImport("user32", EntryPoint = "GetMonitorInfoW", ExactSpelling = true, CharSet = CharSet.Unicode)] [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool GetMonitorInfo([In] IntPtr hMonitor, [Out] MONITORINFO lpmi); + public static extern bool GetMonitorInfo([In] IntPtr hMonitor, ref MONITORINFO lpmi); [return: MarshalAs(UnmanagedType.Bool)] [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "PostMessageW")] @@ -1055,12 +1058,17 @@ namespace Avalonia.Win32.Interop } [StructLayout(LayoutKind.Sequential)] - internal class MONITORINFO + internal struct MONITORINFO { - public int cbSize = Marshal.SizeOf(); - public RECT rcMonitor = new RECT(); - public RECT rcWork = new RECT(); - public int dwFlags = 0; + public int cbSize; + public RECT rcMonitor; + public RECT rcWork; + public int dwFlags; + + public static MONITORINFO Create() + { + return new MONITORINFO() { cbSize = Marshal.SizeOf() }; + } public enum MonitorOptions : uint { diff --git a/src/Windows/Avalonia.Win32/ScreenImpl.cs b/src/Windows/Avalonia.Win32/ScreenImpl.cs index e1df24151d..5cfceb68b7 100644 --- a/src/Windows/Avalonia.Win32/ScreenImpl.cs +++ b/src/Windows/Avalonia.Win32/ScreenImpl.cs @@ -26,8 +26,8 @@ namespace Avalonia.Win32 EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, (IntPtr monitor, IntPtr hdcMonitor, ref Rect lprcMonitor, IntPtr data) => { - MONITORINFO monitorInfo = new MONITORINFO(); - if (GetMonitorInfo(monitor, monitorInfo)) + MONITORINFO monitorInfo = MONITORINFO.Create(); + if (GetMonitorInfo(monitor,ref monitorInfo)) { RECT bounds = monitorInfo.rcMonitor; RECT workingArea = monitorInfo.rcWork; diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 89943b5b0a..5041942c63 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -77,6 +77,8 @@ namespace Avalonia.Win32 public static void Initialize(bool deferredRendering = true) { + UnmanagedMethods.SetProcessDPIAware(); + AvaloniaLocator.CurrentMutable .Bind().ToSingleton() .Bind().ToConstant(CursorFactory.Instance) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 65580ff9c8..45120fa21b 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -837,9 +837,9 @@ namespace Avalonia.Win32 if (monitor != IntPtr.Zero) { - MONITORINFO monitorInfo = new MONITORINFO(); + MONITORINFO monitorInfo = MONITORINFO.Create(); - if (GetMonitorInfo(monitor, monitorInfo)) + if (GetMonitorInfo(monitor,ref monitorInfo)) { RECT rcMonitorArea = monitorInfo.rcMonitor; diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index a5f5f8d328..1debccd3c5 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -1,16 +1,18 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; +using System.Collections.ObjectModel; using System.Linq; +using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; -using Avalonia.Input; +using Avalonia.Data; using Avalonia.LogicalTree; using Avalonia.Styling; using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; -using Avalonia.Collections; namespace Avalonia.Controls.UnitTests { @@ -170,9 +172,33 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Size(100, 10), target.Scroll.Viewport); } + [Fact] + public void Containers_Correct_After_Clear_Add_Remove() + { + // Issue #1936 + var items = new AvaloniaList(Enumerable.Range(0, 11).Select(x => $"Item {x}")); + var target = new ListBox + { + Template = ListBoxTemplate(), + Items = items, + ItemTemplate = new FuncDataTemplate(x => new TextBlock { Width = 20, Height = 10 }), + SelectedIndex = 0, + }; + + Prepare(target); + + items.Clear(); + items.AddRange(Enumerable.Range(0, 11).Select(x => $"Item {x}")); + items.Remove("Item 2"); + + Assert.Equal( + items, + target.Presenter.Panel.Children.Cast().Select(x => (string)x.Content)); + } + private FuncControlTemplate ListBoxTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate(parent => new ScrollViewer { Name = "PART_ScrollViewer", @@ -189,7 +215,7 @@ namespace Avalonia.Controls.UnitTests private FuncControlTemplate ListBoxItemTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate(parent => new ContentPresenter { Name = "PART_ContentPresenter", diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 14e1b15ebc..bbe1d85acb 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -707,6 +707,26 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.True(target.SelectedIndex == 1); } + [Fact] + public void Binding_With_DelayedBinding_And_Initialization_Where_DataContext_Is_Root_Works() + { + // Test for #1932. + var root = new RootWithItems(); + + root.BeginInit(); + root.DataContext = root; + + var target = new ListBox(); + target.BeginInit(); + root.Child = target; + + DelayedBinding.Add(target, ItemsControl.ItemsProperty, new Binding(nameof(RootWithItems.Items))); + DelayedBinding.Add(target, ListBox.SelectedItemProperty, new Binding(nameof(RootWithItems.Selected))); + target.EndInit(); + root.EndInit(); + + Assert.Equal("b", target.SelectedItem); + } private FuncControlTemplate Template() { @@ -745,5 +765,11 @@ namespace Avalonia.Controls.UnitTests.Primitives public IList Items { get; set; } public Item SelectedItem { get; set; } } + + private class RootWithItems : TestRoot + { + public List Items { get; set; } = new List() { "a", "b", "c", "d", "e" }; + public string Selected { get; set; } = "b"; + } } } diff --git a/tests/Avalonia.ReactiveUI.UnitTests/Avalonia.ReactiveUI.UnitTests.csproj b/tests/Avalonia.ReactiveUI.UnitTests/Avalonia.ReactiveUI.UnitTests.csproj new file mode 100644 index 0000000000..7c0ff79183 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/Avalonia.ReactiveUI.UnitTests.csproj @@ -0,0 +1,14 @@ + + + netcoreapp2.0 + + + + + + + + + + + diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs new file mode 100644 index 0000000000..97701f8437 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs @@ -0,0 +1,89 @@ +using System; +using System.Reactive.Concurrency; +using System.Reactive.Disposables; +using Avalonia.Controls; +using Avalonia.Rendering; +using Avalonia.Platform; +using Avalonia.UnitTests; +using Avalonia; +using ReactiveUI; +using DynamicData; +using Xunit; +using Splat; + +namespace Avalonia +{ + public class AvaloniaActivationForViewFetcherTest + { + public class TestUserControl : UserControl, IActivatable { } + + public class TestUserControlWithWhenActivated : UserControl, IActivatable + { + public bool Active { get; private set; } + + public TestUserControlWithWhenActivated() + { + this.WhenActivated(disposables => { + Active = true; + Disposable + .Create(() => Active = false) + .DisposeWith(disposables); + }); + } + } + + [Fact] + public void Visual_Element_Is_Activated_And_Deactivated() + { + var userControl = new TestUserControl(); + var activationForViewFetcher = new AvaloniaActivationForViewFetcher(); + + activationForViewFetcher + .GetActivationForView(userControl) + .ToObservableChangeSet(scheduler: ImmediateScheduler.Instance) + .Bind(out var activated) + .Subscribe(); + + var fakeRenderedDecorator = new TestRoot(); + fakeRenderedDecorator.Child = userControl; + Assert.True(activated[0]); + Assert.Equal(1, activated.Count); + + fakeRenderedDecorator.Child = null; + Assert.True(activated[0]); + Assert.False(activated[1]); + Assert.Equal(2, activated.Count); + } + + [Fact] + public void Get_Affinity_For_View_Should_Return_Non_Zero_For_Visual_Elements() + { + var userControl = new TestUserControl(); + var activationForViewFetcher = new AvaloniaActivationForViewFetcher(); + + var forUserControl = activationForViewFetcher.GetAffinityForView(userControl.GetType()); + var forNonUserControl = activationForViewFetcher.GetAffinityForView(typeof(object)); + + Assert.NotEqual(0, forUserControl); + Assert.Equal(0, forNonUserControl); + } + + [Fact] + public void Activation_For_View_Fetcher_Should_Support_When_Activated() + { + Locator.CurrentMutable.RegisterConstant( + new AvaloniaActivationForViewFetcher(), + typeof(IActivationForViewFetcher)); + + var userControl = new TestUserControlWithWhenActivated(); + Assert.False(userControl.Active); + + var fakeRenderedDecorator = new TestRoot(); + fakeRenderedDecorator.Child = userControl; + Assert.True(userControl.Active); + + fakeRenderedDecorator.Child = null; + Assert.False(userControl.Active); + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/RenderLoopTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/RenderLoopTests.cs index 16c2d3ee18..e9aefc46e6 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/RenderLoopTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/RenderLoopTests.cs @@ -19,14 +19,13 @@ namespace Avalonia.Visuals.UnitTests.Rendering bool inDispatcher = false; dispatcher.Setup( - d => d.InvokeAsync(It.IsAny(), DispatcherPriority.Render)) + d => d.Post(It.IsAny(), DispatcherPriority.Render)) .Callback((Action a, DispatcherPriority _) => { inDispatcher = true; a(); inDispatcher = false; - }) - .Returns(Task.CompletedTask); + }); var timer = new Mock(); @@ -71,14 +70,13 @@ namespace Avalonia.Visuals.UnitTests.Rendering var dispatcher = new Mock(); bool inDispatcher = false; dispatcher.Setup( - d => d.InvokeAsync(It.IsAny(), DispatcherPriority.Render)) + d => d.Post(It.IsAny(), DispatcherPriority.Render)) .Callback((Action a, DispatcherPriority _) => { inDispatcher = true; a(); inDispatcher = false; - }) - .Returns(Task.CompletedTask); + }); var timer = new Mock(); var loop = new RenderLoop(timer.Object, dispatcher.Object); @@ -100,9 +98,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering { var dispatcher = new Mock(); dispatcher.Setup( - d => d.InvokeAsync(It.IsAny(), DispatcherPriority.Render)) - .Callback((Action a, DispatcherPriority _) => a()) - .Returns(Task.CompletedTask); + d => d.Post(It.IsAny(), DispatcherPriority.Render)) + .Callback((Action a, DispatcherPriority _) => a()); var timer = new Mock(); var loop = new RenderLoop(timer.Object, dispatcher.Object);