diff --git a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs index fc48a8853d..31f4691c90 100644 --- a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs +++ b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs @@ -1,3 +1,6 @@ +using System; +using System.Reactive.Disposables; +using System.Reactive.Linq; using Avalonia; using Avalonia.VisualTree; using Avalonia.Controls; @@ -6,8 +9,10 @@ using ReactiveUI; namespace Avalonia.ReactiveUI { /// - /// A ReactiveUI UserControl that implements - /// and will activate your ViewModel automatically if it supports activation. + /// 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 @@ -20,7 +25,14 @@ namespace Avalonia.ReactiveUI /// public ReactiveUserControl() { - DataContextChanged += (sender, args) => ViewModel = DataContext as TViewModel; + // This WhenActivated block calls ViewModel's WhenActivated + // block if the ViewModel implements IActivatableViewModel. + this.WhenActivated(disposables => { }); + + 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 5695a727af..1204266b63 100644 --- a/src/Avalonia.ReactiveUI/ReactiveWindow.cs +++ b/src/Avalonia.ReactiveUI/ReactiveWindow.cs @@ -1,3 +1,6 @@ +using System; +using System.Reactive.Disposables; +using System.Reactive.Linq; using Avalonia; using Avalonia.VisualTree; using Avalonia.Controls; @@ -6,8 +9,10 @@ using ReactiveUI; namespace Avalonia.ReactiveUI { /// - /// A ReactiveUI Window that implements - /// and will activate your ViewModel automatically if it supports activation. + /// 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 @@ -20,7 +25,14 @@ namespace Avalonia.ReactiveUI /// public ReactiveWindow() { - DataContextChanged += (sender, args) => ViewModel = DataContext as TViewModel; + // This WhenActivated block calls ViewModel's WhenActivated + // block if the ViewModel implements IActivatableViewModel. + this.WhenActivated(disposables => { }); + + 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/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs index fda8503135..c66a3c4ba1 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs @@ -74,44 +74,26 @@ 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() - { - 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..5d257f75f2 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs @@ -1,4 +1,4 @@ -using Avalonia.Controls; +using System.Reactive.Disposables; using Avalonia.UnitTests; using ReactiveUI; using Splat; @@ -8,14 +8,37 @@ 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 { } + 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 +49,56 @@ 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); + } + + [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); } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs index 3a5c562a59..18a8a33f09 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs @@ -1,4 +1,4 @@ -using Avalonia.Controls; +using System.Reactive.Disposables; using Avalonia.UnitTests; using ReactiveUI; using Splat; @@ -8,10 +8,30 @@ 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 { } + 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 +39,73 @@ 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); + } + } + + [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); } } } -} \ No newline at end of file +}