From 6f45631036a39fe581b83467bc802893783954fb Mon Sep 17 00:00:00 2001 From: artyom Date: Thu, 22 Oct 2020 14:03:30 +0300 Subject: [PATCH 1/6] Remove the runtime XAML loader from the tests --- .../AvaloniaActivationForViewFetcherTest.cs | 22 ++----------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs index fda8503135..fdf8a858bd 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs @@ -74,36 +74,18 @@ namespace Avalonia.ReactiveUI.UnitTests { public ActivatableWindow() { - InitializeComponent(); - Assert.IsType(Content); + Content = new Border(); this.WhenActivated(disposables => { }); } - - private void InitializeComponent() - { - AvaloniaRuntimeXamlLoader.Load(@" - - -", null, this); - } } public class ActivatableUserControl : ReactiveUserControl { public ActivatableUserControl() { - InitializeComponent(); - Assert.IsType(Content); + Content = new Border(); this.WhenActivated(disposables => { }); } - - private void InitializeComponent() - { - AvaloniaRuntimeXamlLoader.Load(@" - - -", null, this); - } } public AvaloniaActivationForViewFetcherTest() From f2cb6d755718b6fa92e0bec13536d04b5dfaf834 Mon Sep 17 00:00:00 2001 From: artyom Date: Thu, 22 Oct 2020 15:17:02 +0300 Subject: [PATCH 2/6] TwoWay DataContext and ViewModel sync --- .../ReactiveUserControl.cs | 6 ++++- src/Avalonia.ReactiveUI/ReactiveWindow.cs | 6 ++++- .../AvaloniaActivationForViewFetcherTest.cs | 12 ++++----- .../ReactiveUserControlTest.cs | 21 ++++++++++++++-- .../ReactiveWindowTest.cs | 25 ++++++++++++++++--- 5 files changed, 56 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs index fc48a8853d..24aba6b650 100644 --- a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs +++ b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs @@ -1,3 +1,4 @@ +using System; using Avalonia; using Avalonia.VisualTree; using Avalonia.Controls; @@ -20,7 +21,10 @@ namespace Avalonia.ReactiveUI /// public ReactiveUserControl() { - DataContextChanged += (sender, args) => ViewModel = DataContext as TViewModel; + this.WhenAnyValue(x => x.DataContext) + .Subscribe(context => ViewModel = context as TViewModel); + this.WhenAnyValue(x => x.ViewModel) + .Subscribe(model => DataContext = model); } /// diff --git a/src/Avalonia.ReactiveUI/ReactiveWindow.cs b/src/Avalonia.ReactiveUI/ReactiveWindow.cs index 5695a727af..a412f8e383 100644 --- a/src/Avalonia.ReactiveUI/ReactiveWindow.cs +++ b/src/Avalonia.ReactiveUI/ReactiveWindow.cs @@ -1,3 +1,4 @@ +using System; using Avalonia; using Avalonia.VisualTree; using Avalonia.Controls; @@ -20,7 +21,10 @@ namespace Avalonia.ReactiveUI /// public ReactiveWindow() { - DataContextChanged += (sender, args) => ViewModel = DataContext as TViewModel; + this.WhenAnyValue(x => x.DataContext) + .Subscribe(context => ViewModel = context as TViewModel); + this.WhenAnyValue(x => x.ViewModel) + .Subscribe(model => DataContext = model); } /// diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs index fdf8a858bd..c66a3c4ba1 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs @@ -88,12 +88,12 @@ namespace Avalonia.ReactiveUI.UnitTests } } - public AvaloniaActivationForViewFetcherTest() - { - Locator.CurrentMutable.RegisterConstant( - new AvaloniaActivationForViewFetcher(), - typeof(IActivationForViewFetcher)); - } + public AvaloniaActivationForViewFetcherTest() => + Locator + .CurrentMutable + .RegisterConstant( + new AvaloniaActivationForViewFetcher(), + typeof(IActivationForViewFetcher)); [Fact] public void Visual_Element_Is_Activated_And_Deactivated() diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs index b57bb242bd..026a08874f 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs @@ -1,4 +1,3 @@ -using Avalonia.Controls; using Avalonia.UnitTests; using ReactiveUI; using Splat; @@ -12,10 +11,20 @@ namespace Avalonia.ReactiveUI.UnitTests public class ExampleView : ReactiveUserControl { } + public ReactiveUserControlTest() => + Locator + .CurrentMutable + .RegisterConstant( + new AvaloniaActivationForViewFetcher(), + typeof(IActivationForViewFetcher)); + [Fact] public void Data_Context_Should_Stay_In_Sync_With_Reactive_User_Control_View_Model() { + var root = new TestRoot(); var view = new ExampleView(); + root.Child = view; + var viewModel = new ExampleViewModel(); Assert.Null(view.ViewModel); @@ -26,6 +35,14 @@ namespace Avalonia.ReactiveUI.UnitTests view.DataContext = null; Assert.Null(view.ViewModel); Assert.Null(view.DataContext); + + view.ViewModel = viewModel; + Assert.Equal(viewModel, view.ViewModel); + Assert.Equal(viewModel, view.DataContext); + + view.ViewModel = null; + Assert.Null(view.ViewModel); + Assert.Null(view.DataContext); } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs index 3a5c562a59..7612c07aae 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs @@ -1,4 +1,3 @@ -using Avalonia.Controls; using Avalonia.UnitTests; using ReactiveUI; using Splat; @@ -12,6 +11,13 @@ namespace Avalonia.ReactiveUI.UnitTests public class ExampleWindow : ReactiveWindow { } + public ReactiveWindowTest() => + Locator + .CurrentMutable + .RegisterConstant( + new AvaloniaActivationForViewFetcher(), + typeof(IActivationForViewFetcher)); + [Fact] public void Data_Context_Should_Stay_In_Sync_With_Reactive_Window_View_Model() { @@ -19,16 +25,27 @@ namespace Avalonia.ReactiveUI.UnitTests { var view = new ExampleWindow(); var viewModel = new ExampleViewModel(); + view.Show(); + Assert.Null(view.ViewModel); + Assert.Null(view.DataContext); view.DataContext = viewModel; - Assert.Equal(view.ViewModel, viewModel); - Assert.Equal(view.DataContext, viewModel); + Assert.Equal(viewModel, view.ViewModel); + Assert.Equal(viewModel, view.DataContext); view.DataContext = null; Assert.Null(view.ViewModel); Assert.Null(view.DataContext); + + view.ViewModel = viewModel; + Assert.Equal(viewModel, view.ViewModel); + Assert.Equal(viewModel, view.DataContext); + + view.ViewModel = null; + Assert.Null(view.ViewModel); + Assert.Null(view.DataContext); } } } -} \ No newline at end of file +} From a6ebfefe7fbbce2941f1a9d2db4c1d5721903936 Mon Sep 17 00:00:00 2001 From: artyom Date: Thu, 22 Oct 2020 15:23:39 +0300 Subject: [PATCH 3/6] Document the new behavior --- src/Avalonia.ReactiveUI/ReactiveUserControl.cs | 5 +++-- src/Avalonia.ReactiveUI/ReactiveWindow.cs | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs index 24aba6b650..a401d62a06 100644 --- a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs +++ b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs @@ -7,8 +7,9 @@ using ReactiveUI; namespace Avalonia.ReactiveUI { /// - /// A ReactiveUI UserControl that implements - /// and will activate your ViewModel automatically if it supports activation. + /// A ReactiveUI UserControl that implements and will activate your ViewModel + /// automatically if it supports activation. When the DataContext property changes, this class will update the + /// ViewModel property with the new DataContext value, and vice versa. /// /// ViewModel type. public class ReactiveUserControl : UserControl, IViewFor where TViewModel : class diff --git a/src/Avalonia.ReactiveUI/ReactiveWindow.cs b/src/Avalonia.ReactiveUI/ReactiveWindow.cs index a412f8e383..896795f42d 100644 --- a/src/Avalonia.ReactiveUI/ReactiveWindow.cs +++ b/src/Avalonia.ReactiveUI/ReactiveWindow.cs @@ -7,8 +7,9 @@ using ReactiveUI; namespace Avalonia.ReactiveUI { /// - /// A ReactiveUI Window that implements - /// and will activate your ViewModel automatically if it supports activation. + /// A ReactiveUI Window that implements and will activate your ViewModel + /// automatically if it supports activation. When the DataContext property changes, this class will update the + /// ViewModel property with the new DataContext value, and vice versa. /// /// ViewModel type. public class ReactiveWindow : Window, IViewFor where TViewModel : class From 02bdbd5823ba5a8eeb176bcec081200f4c87fbba Mon Sep 17 00:00:00 2001 From: artyom Date: Thu, 22 Oct 2020 18:29:08 +0300 Subject: [PATCH 4/6] Properly handle context initialization --- .../ReactiveUserControl.cs | 9 ++- src/Avalonia.ReactiveUI/ReactiveWindow.cs | 9 ++- .../ReactiveUserControlTest.cs | 58 ++++++++++++++++- .../ReactiveWindowTest.cs | 62 ++++++++++++++++++- 4 files changed, 132 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs index a401d62a06..5430d288bc 100644 --- a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs +++ b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs @@ -1,4 +1,6 @@ using System; +using System.Reactive.Disposables; +using System.Reactive.Linq; using Avalonia; using Avalonia.VisualTree; using Avalonia.Controls; @@ -22,10 +24,13 @@ namespace Avalonia.ReactiveUI /// public ReactiveUserControl() { - this.WhenAnyValue(x => x.DataContext) - .Subscribe(context => ViewModel = context as TViewModel); + this.WhenActivated(disposables => { }); this.WhenAnyValue(x => x.ViewModel) + .Skip(1) .Subscribe(model => DataContext = model); + this.WhenAnyValue(x => x.DataContext) + .Skip(1) + .Subscribe(context => ViewModel = context as TViewModel); } /// diff --git a/src/Avalonia.ReactiveUI/ReactiveWindow.cs b/src/Avalonia.ReactiveUI/ReactiveWindow.cs index 896795f42d..0a25d66af8 100644 --- a/src/Avalonia.ReactiveUI/ReactiveWindow.cs +++ b/src/Avalonia.ReactiveUI/ReactiveWindow.cs @@ -1,4 +1,6 @@ using System; +using System.Reactive.Disposables; +using System.Reactive.Linq; using Avalonia; using Avalonia.VisualTree; using Avalonia.Controls; @@ -22,10 +24,13 @@ namespace Avalonia.ReactiveUI /// public ReactiveWindow() { - this.WhenAnyValue(x => x.DataContext) - .Subscribe(context => ViewModel = context as TViewModel); + this.WhenActivated(disposables => { }); this.WhenAnyValue(x => x.ViewModel) + .Skip(1) .Subscribe(model => DataContext = model); + this.WhenAnyValue(x => x.DataContext) + .Skip(1) + .Subscribe(context => ViewModel = context as TViewModel); } /// diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs index 026a08874f..5d257f75f2 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs @@ -1,3 +1,4 @@ +using System.Reactive.Disposables; using Avalonia.UnitTests; using ReactiveUI; using Splat; @@ -7,7 +8,20 @@ namespace Avalonia.ReactiveUI.UnitTests { public class ReactiveUserControlTest { - public class ExampleViewModel : ReactiveObject { } + public class ExampleViewModel : ReactiveObject, IActivatableViewModel + { + public bool IsActive { get; private set; } + + public ViewModelActivator Activator { get; } = new ViewModelActivator(); + + public ExampleViewModel() => this.WhenActivated(disposables => + { + IsActive = true; + Disposable + .Create(() => IsActive = false) + .DisposeWith(disposables); + }); + } public class ExampleView : ReactiveUserControl { } @@ -44,5 +58,47 @@ namespace Avalonia.ReactiveUI.UnitTests Assert.Null(view.ViewModel); Assert.Null(view.DataContext); } + + [Fact] + public void Should_Start_With_NotNull_Activated_ViewModel() + { + var root = new TestRoot(); + var view = new ExampleView {ViewModel = new ExampleViewModel()}; + + Assert.False(view.ViewModel.IsActive); + + root.Child = view; + + Assert.NotNull(view.ViewModel); + Assert.NotNull(view.DataContext); + Assert.True(view.ViewModel.IsActive); + + root.Child = null; + + Assert.NotNull(view.ViewModel); + Assert.NotNull(view.DataContext); + Assert.False(view.ViewModel.IsActive); + } + + [Fact] + public void Should_Start_With_NotNull_Activated_DataContext() + { + var root = new TestRoot(); + var view = new ExampleView {DataContext = new ExampleViewModel()}; + + Assert.False(view.ViewModel.IsActive); + + root.Child = view; + + Assert.NotNull(view.ViewModel); + Assert.NotNull(view.DataContext); + Assert.True(view.ViewModel.IsActive); + + root.Child = null; + + Assert.NotNull(view.ViewModel); + Assert.NotNull(view.DataContext); + Assert.False(view.ViewModel.IsActive); + } } } diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs index 7612c07aae..18a8a33f09 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs @@ -1,3 +1,4 @@ +using System.Reactive.Disposables; using Avalonia.UnitTests; using ReactiveUI; using Splat; @@ -7,7 +8,20 @@ namespace Avalonia.ReactiveUI.UnitTests { public class ReactiveWindowTest { - public class ExampleViewModel : ReactiveObject { } + public class ExampleViewModel : ReactiveObject, IActivatableViewModel + { + public bool IsActive { get; private set; } + + public ViewModelActivator Activator { get; } = new ViewModelActivator(); + + public ExampleViewModel() => this.WhenActivated(disposables => + { + IsActive = true; + Disposable + .Create(() => IsActive = false) + .DisposeWith(disposables); + }); + } public class ExampleWindow : ReactiveWindow { } @@ -47,5 +61,51 @@ namespace Avalonia.ReactiveUI.UnitTests Assert.Null(view.DataContext); } } + + [Fact] + public void Should_Start_With_NotNull_Activated_ViewModel() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var view = new ExampleWindow { ViewModel = new ExampleViewModel() }; + + Assert.False(view.ViewModel.IsActive); + + view.Show(); + + Assert.NotNull(view.ViewModel); + Assert.NotNull(view.DataContext); + Assert.True(view.ViewModel.IsActive); + + view.Close(); + + Assert.NotNull(view.ViewModel); + Assert.NotNull(view.DataContext); + Assert.False(view.ViewModel.IsActive); + } + } + + [Fact] + public void Should_Start_With_NotNull_Activated_DataContext() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var view = new ExampleWindow { DataContext = new ExampleViewModel() }; + + Assert.False(view.ViewModel.IsActive); + + view.Show(); + + Assert.NotNull(view.ViewModel); + Assert.NotNull(view.DataContext); + Assert.True(view.ViewModel.IsActive); + + view.Close(); + + Assert.NotNull(view.ViewModel); + Assert.NotNull(view.DataContext); + Assert.False(view.ViewModel.IsActive); + } + } } } From 40371ac89c5f93436b98ece524080691376b3921 Mon Sep 17 00:00:00 2001 From: artyom Date: Thu, 22 Oct 2020 18:31:58 +0300 Subject: [PATCH 5/6] Improve the XML documentation --- src/Avalonia.ReactiveUI/ReactiveUserControl.cs | 7 ++++--- src/Avalonia.ReactiveUI/ReactiveWindow.cs | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs index 5430d288bc..2cf1f1e678 100644 --- a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs +++ b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs @@ -9,9 +9,10 @@ using ReactiveUI; namespace Avalonia.ReactiveUI { /// - /// A ReactiveUI UserControl that implements and will activate your ViewModel - /// automatically if it supports activation. When the DataContext property changes, this class will update the - /// ViewModel property with the new DataContext value, and vice versa. + /// A ReactiveUI that implements the interface and + /// will activate your ViewModel automatically if the view model implements . + /// When the DataContext property changes, this class will update the ViewModel property with the new DataContext + /// value, and vice versa. /// /// ViewModel type. public class ReactiveUserControl : UserControl, IViewFor where TViewModel : class diff --git a/src/Avalonia.ReactiveUI/ReactiveWindow.cs b/src/Avalonia.ReactiveUI/ReactiveWindow.cs index 0a25d66af8..77ffe04c5d 100644 --- a/src/Avalonia.ReactiveUI/ReactiveWindow.cs +++ b/src/Avalonia.ReactiveUI/ReactiveWindow.cs @@ -9,9 +9,10 @@ using ReactiveUI; namespace Avalonia.ReactiveUI { /// - /// A ReactiveUI Window that implements and will activate your ViewModel - /// automatically if it supports activation. When the DataContext property changes, this class will update the - /// ViewModel property with the new DataContext value, and vice versa. + /// A ReactiveUI that implements the interface and will + /// activate your ViewModel automatically if the view model implements . When + /// the DataContext property changes, this class will update the ViewModel property with the new DataContext value, + /// and vice versa. /// /// ViewModel type. public class ReactiveWindow : Window, IViewFor where TViewModel : class From 2b6c74148eb252d1d21fd9c8fd819ceed8b368a5 Mon Sep 17 00:00:00 2001 From: artyom Date: Fri, 23 Oct 2020 12:11:18 +0300 Subject: [PATCH 6/6] Use ObservableForProperty() instead of WhenAny() with Skip() --- src/Avalonia.ReactiveUI/ReactiveUserControl.cs | 13 +++++++------ src/Avalonia.ReactiveUI/ReactiveWindow.cs | 13 +++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs index 2cf1f1e678..31f4691c90 100644 --- a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs +++ b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs @@ -25,13 +25,14 @@ namespace Avalonia.ReactiveUI /// public ReactiveUserControl() { + // This WhenActivated block calls ViewModel's WhenActivated + // block if the ViewModel implements IActivatableViewModel. this.WhenActivated(disposables => { }); - this.WhenAnyValue(x => x.ViewModel) - .Skip(1) - .Subscribe(model => DataContext = model); - this.WhenAnyValue(x => x.DataContext) - .Skip(1) - .Subscribe(context => ViewModel = context as TViewModel); + + this.ObservableForProperty(x => x.ViewModel, false, true) + .Subscribe(args => DataContext = args.Value); + this.ObservableForProperty(x => x.DataContext, false, true) + .Subscribe(args => ViewModel = args.Value as TViewModel); } /// diff --git a/src/Avalonia.ReactiveUI/ReactiveWindow.cs b/src/Avalonia.ReactiveUI/ReactiveWindow.cs index 77ffe04c5d..1204266b63 100644 --- a/src/Avalonia.ReactiveUI/ReactiveWindow.cs +++ b/src/Avalonia.ReactiveUI/ReactiveWindow.cs @@ -25,13 +25,14 @@ namespace Avalonia.ReactiveUI /// public ReactiveWindow() { + // This WhenActivated block calls ViewModel's WhenActivated + // block if the ViewModel implements IActivatableViewModel. this.WhenActivated(disposables => { }); - this.WhenAnyValue(x => x.ViewModel) - .Skip(1) - .Subscribe(model => DataContext = model); - this.WhenAnyValue(x => x.DataContext) - .Skip(1) - .Subscribe(context => ViewModel = context as TViewModel); + + this.ObservableForProperty(x => x.ViewModel, false, true) + .Subscribe(args => DataContext = args.Value); + this.ObservableForProperty(x => x.DataContext, false, true) + .Subscribe(args => ViewModel = args.Value as TViewModel); } ///