diff --git a/src/ReactiveUI.XamForms.Tests/Activation/ActivatingReactivePagesTests.cs b/src/ReactiveUI.XamForms.Tests/Activation/ActivatingReactivePagesTests.cs
new file mode 100644
index 0000000000..6372b20e03
--- /dev/null
+++ b/src/ReactiveUI.XamForms.Tests/Activation/ActivatingReactivePagesTests.cs
@@ -0,0 +1,146 @@
+// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using ReactiveUI.XamForms;
+using ReactiveUI.XamForms.Tests.Activation;
+using ReactiveUI.XamForms.Tests.Activation.Mocks;
+using Splat;
+using Xamarin.Forms;
+using Xunit;
+
+namespace ReactiveUI.Tests
+{
+ ///
+ /// Tests for activating views.
+ ///
+ public class ActivatingReactivePagesTests : IClassFixture>
+ {
+ private ApplicationFixture _fixture;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The fixture.
+ public ActivatingReactivePagesTests(ApplicationFixture fixture)
+ {
+ _fixture = fixture;
+ Locator.CurrentMutable.Register(() => new ActivationForViewFetcher(), typeof(IActivationForViewFetcher));
+ Locator.CurrentMutable.Register>(() => new ShellView());
+ Locator.CurrentMutable.Register>(() => new ContentPageView());
+ Locator.CurrentMutable.Register>(() => new TabbedPageView());
+ Locator.CurrentMutable.Register>(() => new CarouselPageView());
+ Locator.CurrentMutable.Register>(() => new FlyoutPageView());
+ _fixture.ActivateApp(new App());
+ }
+
+ ///
+ /// Tests to make sure that views generally activate.
+ ///
+ [Fact]
+ public void ActivatingReactiveShellTest()
+ {
+ var main = _fixture.AppMock!.MainPage as AppShell;
+
+ Assert.Equal(1, main!.ViewModel!.IsActiveCount);
+ Assert.Equal(1, main.IsActiveCount);
+ }
+
+ ///
+ /// Tests to make sure that views generally activate.
+ ///
+ [Fact]
+ public void ActivatingReactiveContentPageTest()
+ {
+ var vm = new ContentPageViewModel();
+ var fixture = new ContentPageView
+ {
+ ViewModel = vm
+ };
+
+ // Activate
+ Shell.Current.Navigation.PushAsync(fixture);
+ Assert.Equal(1, fixture.ViewModel.IsActiveCount);
+ Assert.Equal(1, fixture.IsActiveCount);
+
+ // Deactivate
+ Shell.Current.GoToAsync("..");
+ fixture.ViewModel = null;
+ Assert.Equal(0, vm.IsActiveCount);
+ Assert.Equal(0, fixture.IsActiveCount);
+ }
+
+ ///
+ /// Tests to make sure that views generally activate.
+ ///
+ [Fact]
+ public void ActivatingReactiveTabbedPageTest()
+ {
+ var vm1 = new TabbedPageViewModel();
+ var fixture1 = new TabbedPageView
+ {
+ ViewModel = vm1
+ };
+
+ // Activate
+ Shell.Current.Navigation.PushAsync(fixture1);
+ Assert.Equal(1, fixture1.ViewModel.IsActiveCount);
+ Assert.Equal(1, fixture1.IsActiveCount);
+
+ // Deactivate
+ Shell.Current.GoToAsync("..");
+ fixture1.ViewModel = null;
+ Assert.Equal(0, vm1.IsActiveCount);
+ Assert.Equal(0, fixture1.IsActiveCount);
+ }
+
+ ///
+ /// Tests to make sure that views generally activate.
+ ///
+ [Fact]
+ public void ActivatingReactiveFlyoutPageTest()
+ {
+ var vm3 = new FlyOutPageViewModel();
+ var fixture3 = new FlyoutPageView
+ {
+ ViewModel = vm3
+ };
+
+ // Activate
+ Shell.Current.Navigation.PushAsync(fixture3);
+ Assert.Equal(1, fixture3.ViewModel!.IsActiveCount);
+ Assert.Equal(1, fixture3.IsActiveCount);
+
+ // Deactivate
+ Shell.Current.GoToAsync("..");
+ fixture3.ViewModel = null;
+ Assert.Equal(0, vm3.IsActiveCount);
+ Assert.Equal(0, fixture3.IsActiveCount);
+ }
+
+ ///
+ /// Tests to make sure that views generally activate.
+ ///
+ [Fact]
+ public void ActivatingReactiveCarouselPageTest()
+ {
+ var vm4 = new CarouselPageViewModel();
+ var fixture4 = new CarouselPageView
+ {
+ ViewModel = vm4
+ };
+
+ // Activate
+ Shell.Current.Navigation.PushAsync(fixture4);
+ Assert.Equal(1, fixture4.ViewModel!.IsActiveCount);
+ Assert.Equal(1, fixture4.IsActiveCount);
+
+ // Deactivate
+ Shell.Current.GoToAsync("..");
+ fixture4.ViewModel = null;
+ Assert.Equal(0, vm4.IsActiveCount);
+ Assert.Equal(0, fixture4.IsActiveCount);
+ }
+ }
+}
diff --git a/src/ReactiveUI.XamForms.Tests/Activation/Mocks/App.xaml b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/App.xaml
new file mode 100644
index 0000000000..393be06d9b
--- /dev/null
+++ b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/App.xaml
@@ -0,0 +1,34 @@
+
+
+
+
+
+ #2196F3
+
+
+
+
diff --git a/src/ReactiveUI.XamForms.Tests/Activation/Mocks/App.xaml.cs b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/App.xaml.cs
new file mode 100644
index 0000000000..e26216449e
--- /dev/null
+++ b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/App.xaml.cs
@@ -0,0 +1,57 @@
+// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using Xamarin.Forms;
+
+namespace ReactiveUI.XamForms.Tests.Activation
+{
+ ///
+ /// The App.
+ ///
+ ///
+ public partial class App
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The page.
+ public App()
+ {
+ InitializeComponent();
+
+ MainPage = new AppShell();
+ }
+
+ ///
+ /// Application developers override this method to perform actions when the application starts.
+ ///
+ ///
+ /// To be added.
+ ///
+ protected override void OnStart()
+ {
+ }
+
+ ///
+ /// Application developers override this method to perform actions when the application enters the sleeping state.
+ ///
+ ///
+ /// To be added.
+ ///
+ protected override void OnSleep()
+ {
+ }
+
+ ///
+ /// Application developers override this method to perform actions when the application resumes from a sleeping state.
+ ///
+ ///
+ /// To be added.
+ ///
+ protected override void OnResume()
+ {
+ }
+ }
+}
diff --git a/src/ReactiveUI.XamForms.Tests/Activation/Mocks/AppShell.xaml b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/AppShell.xaml
new file mode 100644
index 0000000000..8378f7896b
--- /dev/null
+++ b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/AppShell.xaml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ReactiveUI.XamForms.Tests/Activation/Mocks/AppShell.xaml.cs b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/AppShell.xaml.cs
new file mode 100644
index 0000000000..36899fd9cb
--- /dev/null
+++ b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/AppShell.xaml.cs
@@ -0,0 +1,35 @@
+// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Reactive.Disposables;
+
+namespace ReactiveUI.XamForms.Tests.Activation
+{
+ ///
+ /// App Shell.
+ ///
+ ///
+ public partial class AppShell
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public AppShell()
+ {
+ InitializeComponent();
+ ViewModel = new();
+ this.WhenActivated(d =>
+ {
+ IsActiveCount++;
+ d(Disposable.Create(() => IsActiveCount--));
+ });
+ }
+
+ ///
+ /// Gets or sets the active count.
+ ///
+ public int IsActiveCount { get; set; }
+ }
+}
diff --git a/src/ReactiveUI.XamForms.Tests/Activation/Mocks/AppShellViewModel.cs b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/AppShellViewModel.cs
new file mode 100644
index 0000000000..61cf138d8f
--- /dev/null
+++ b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/AppShellViewModel.cs
@@ -0,0 +1,41 @@
+// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Reactive.Disposables;
+
+namespace ReactiveUI.XamForms.Tests.Activation
+{
+ ///
+ /// Activating View Model2.
+ ///
+ ///
+ ///
+ public class AppShellViewModel : ReactiveObject, IActivatableViewModel
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public AppShellViewModel()
+ {
+ Activator = new ViewModelActivator();
+
+ this.WhenActivated(d =>
+ {
+ IsActiveCount++;
+ d(Disposable.Create(() => IsActiveCount--));
+ });
+ }
+
+ ///
+ /// Gets or sets the Activator which will be used by the View when Activation/Deactivation occurs.
+ ///
+ public ViewModelActivator Activator { get; protected set; }
+
+ ///
+ /// Gets or sets the active count.
+ ///
+ public int IsActiveCount { get; protected set; }
+ }
+}
diff --git a/src/ReactiveUI.XamForms.Tests/Activation/Mocks/ApplicationFixture.cs b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/ApplicationFixture.cs
new file mode 100644
index 0000000000..76d46cfcde
--- /dev/null
+++ b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/ApplicationFixture.cs
@@ -0,0 +1,73 @@
+// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System;
+using Splat;
+using Xamarin.Forms;
+using Xamarin.Forms.Mocks;
+
+namespace ReactiveUI.Tests
+{
+ ///
+ /// Application Fixture.
+ ///
+ /// The Application Type.
+ ///
+ public class ApplicationFixture : IDisposable
+ where T : Application
+ {
+ private bool _disposedValue;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ApplicationFixture()
+ {
+ MockForms.Init();
+ }
+
+ ///
+ /// Gets the application.
+ ///
+ ///
+ /// The application.
+ ///
+ public T? AppMock { get; private set; }
+
+ ///
+ /// Activates this instance.
+ ///
+ /// The application.
+ public void ActivateApp(T app) => AppMock = app;
+
+ ///
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ ///
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Releases unmanaged and - optionally - managed resources.
+ ///
+ /// true to release both managed and unmanaged resources; false to release only unmanaged resources.
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposedValue)
+ {
+ if (disposing)
+ {
+ AppMock?.Quit();
+ Application.Current = null;
+ }
+
+ _disposedValue = true;
+ }
+ }
+ }
+}
diff --git a/src/ReactiveUI.XamForms.Tests/Activation/Mocks/ApplicationMock.cs b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/ApplicationMock.cs
new file mode 100644
index 0000000000..a0a8dd1631
--- /dev/null
+++ b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/ApplicationMock.cs
@@ -0,0 +1,23 @@
+// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using Xamarin.Forms;
+
+namespace ReactiveUI.Tests
+{
+ ///
+ /// Application Mock.
+ ///
+ ///
+ public class ApplicationMock : Application
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ApplicationMock()
+ {
+ }
+ }
+}
diff --git a/src/ReactiveUI.XamForms.Tests/Activation/Mocks/CarouselPageView.cs b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/CarouselPageView.cs
new file mode 100644
index 0000000000..32f53cb7bf
--- /dev/null
+++ b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/CarouselPageView.cs
@@ -0,0 +1,30 @@
+// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Reactive.Disposables;
+
+namespace ReactiveUI.XamForms.Tests.Activation
+{
+ ///
+ /// Carousel Page View.
+ ///
+ public class CarouselPageView : ReactiveCarouselPage
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CarouselPageView() =>
+ this.WhenActivated(d =>
+ {
+ IsActiveCount++;
+ d(Disposable.Create(() => IsActiveCount--));
+ });
+
+ ///
+ /// Gets or sets the active count.
+ ///
+ public int IsActiveCount { get; set; }
+ }
+}
diff --git a/src/ReactiveUI.XamForms.Tests/Activation/Mocks/CarouselPageViewModel.cs b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/CarouselPageViewModel.cs
new file mode 100644
index 0000000000..1efdec19c9
--- /dev/null
+++ b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/CarouselPageViewModel.cs
@@ -0,0 +1,39 @@
+// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Reactive.Disposables;
+
+namespace ReactiveUI.XamForms.Tests.Activation
+{
+ ///
+ /// Simulates a activating view model.
+ ///
+ public class CarouselPageViewModel : ReactiveObject, IActivatableViewModel
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CarouselPageViewModel()
+ {
+ Activator = new ViewModelActivator();
+
+ this.WhenActivated(d =>
+ {
+ IsActiveCount++;
+ d(Disposable.Create(() => IsActiveCount--));
+ });
+ }
+
+ ///
+ /// Gets or sets the Activator which will be used by the View when Activation/Deactivation occurs.
+ ///
+ public ViewModelActivator Activator { get; protected set; }
+
+ ///
+ /// Gets or sets the active count.
+ ///
+ public int IsActiveCount { get; protected set; }
+ }
+}
diff --git a/src/ReactiveUI.XamForms.Tests/Activation/Mocks/ContentPageView.cs b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/ContentPageView.cs
new file mode 100644
index 0000000000..d2ae28d4bb
--- /dev/null
+++ b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/ContentPageView.cs
@@ -0,0 +1,30 @@
+// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Reactive.Disposables;
+
+namespace ReactiveUI.XamForms.Tests.Activation
+{
+ ///
+ /// Content Page View.
+ ///
+ public class ContentPageView : ReactiveContentPage
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ContentPageView() =>
+ this.WhenActivated(d =>
+ {
+ IsActiveCount++;
+ d(Disposable.Create(() => IsActiveCount--));
+ });
+
+ ///
+ /// Gets or sets the active count.
+ ///
+ public int IsActiveCount { get; set; }
+ }
+}
diff --git a/src/ReactiveUI.XamForms.Tests/Activation/Mocks/ContentPageViewModel.cs b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/ContentPageViewModel.cs
new file mode 100644
index 0000000000..63ba7a89ff
--- /dev/null
+++ b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/ContentPageViewModel.cs
@@ -0,0 +1,41 @@
+// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Reactive.Disposables;
+
+namespace ReactiveUI.XamForms.Tests.Activation
+{
+ ///
+ /// ContentPageViewModel.
+ ///
+ ///
+ ///
+ public class ContentPageViewModel : ReactiveObject, IActivatableViewModel
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ContentPageViewModel()
+ {
+ Activator = new ViewModelActivator();
+
+ this.WhenActivated(d =>
+ {
+ IsActiveCount++;
+ d(Disposable.Create(() => IsActiveCount--));
+ });
+ }
+
+ ///
+ /// Gets or sets the Activator which will be used by the View when Activation/Deactivation occurs.
+ ///
+ public ViewModelActivator Activator { get; protected set; }
+
+ ///
+ /// Gets or sets the active count.
+ ///
+ public int IsActiveCount { get; protected set; }
+ }
+}
diff --git a/src/ReactiveUI.XamForms.Tests/Activation/Mocks/FlyOutPageViewModel.cs b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/FlyOutPageViewModel.cs
new file mode 100644
index 0000000000..adb118cbfd
--- /dev/null
+++ b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/FlyOutPageViewModel.cs
@@ -0,0 +1,41 @@
+// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Reactive.Disposables;
+
+namespace ReactiveUI.XamForms.Tests.Activation
+{
+ ///
+ /// FlyOutPageViewModel.
+ ///
+ ///
+ ///
+ public class FlyOutPageViewModel : ReactiveObject, IActivatableViewModel
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public FlyOutPageViewModel()
+ {
+ Activator = new ViewModelActivator();
+
+ this.WhenActivated(d =>
+ {
+ IsActiveCount++;
+ d(Disposable.Create(() => IsActiveCount--));
+ });
+ }
+
+ ///
+ /// Gets or sets the Activator which will be used by the View when Activation/Deactivation occurs.
+ ///
+ public ViewModelActivator Activator { get; protected set; }
+
+ ///
+ /// Gets or sets the active count.
+ ///
+ public int IsActiveCount { get; protected set; }
+ }
+}
diff --git a/src/ReactiveUI.XamForms.Tests/Activation/Mocks/FlyoutPageView.xaml b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/FlyoutPageView.xaml
new file mode 100644
index 0000000000..1440ae0a35
--- /dev/null
+++ b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/FlyoutPageView.xaml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ReactiveUI.XamForms.Tests/Activation/Mocks/FlyoutPageView.xaml.cs b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/FlyoutPageView.xaml.cs
new file mode 100644
index 0000000000..74143c459f
--- /dev/null
+++ b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/FlyoutPageView.xaml.cs
@@ -0,0 +1,57 @@
+// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System;
+using System.Reactive.Disposables;
+using Xamarin.Forms;
+using Xamarin.Forms.Xaml;
+
+namespace ReactiveUI.XamForms.Tests.Activation.Mocks
+{
+ ///
+ /// Flyout Page View.
+ ///
+ [XamlCompilation(XamlCompilationOptions.Compile)]
+ public partial class FlyoutPageView
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public FlyoutPageView()
+ {
+ InitializeComponent();
+
+ this.WhenActivated(d =>
+ {
+ IsActiveCount++;
+ d(Disposable.Create(() => IsActiveCount--));
+ });
+
+ FlyoutPage.ListView.ItemSelected += ListView_ItemSelected;
+ }
+
+ ///
+ /// Gets or sets the active count.
+ ///
+ public int IsActiveCount { get; set; }
+
+ private void ListView_ItemSelected(object? sender, SelectedItemChangedEventArgs e)
+ {
+ var item = e.SelectedItem as FlyoutPageViewFlyoutMenuItem;
+ if (item == null)
+ {
+ return;
+ }
+
+ var page = (Page?)Activator.CreateInstance(item.TargetType);
+ page!.Title = item.Title;
+
+ Detail = new NavigationPage(page);
+ IsPresented = false;
+
+ FlyoutPage.ListView.SelectedItem = null;
+ }
+ }
+}
diff --git a/src/ReactiveUI.XamForms.Tests/Activation/Mocks/FlyoutPageViewDetail.xaml b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/FlyoutPageViewDetail.xaml
new file mode 100644
index 0000000000..33b7bacae3
--- /dev/null
+++ b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/FlyoutPageViewDetail.xaml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
diff --git a/src/ReactiveUI.XamForms.Tests/Activation/Mocks/FlyoutPageViewDetail.xaml.cs b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/FlyoutPageViewDetail.xaml.cs
new file mode 100644
index 0000000000..a434f978b4
--- /dev/null
+++ b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/FlyoutPageViewDetail.xaml.cs
@@ -0,0 +1,26 @@
+// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using Xamarin.Forms;
+using Xamarin.Forms.Xaml;
+
+namespace ReactiveUI.XamForms.Tests.Activation.Mocks
+{
+ ///
+ /// FlyoutPageView Detail.
+ ///
+ ///
+ [XamlCompilation(XamlCompilationOptions.Compile)]
+ public partial class FlyoutPageViewDetail : ContentPage
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public FlyoutPageViewDetail()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/src/ReactiveUI.XamForms.Tests/Activation/Mocks/FlyoutPageViewFlyout.xaml b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/FlyoutPageViewFlyout.xaml
new file mode 100644
index 0000000000..f20eb06d1f
--- /dev/null
+++ b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/FlyoutPageViewFlyout.xaml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ReactiveUI.XamForms.Tests/Activation/Mocks/FlyoutPageViewFlyout.xaml.cs b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/FlyoutPageViewFlyout.xaml.cs
new file mode 100644
index 0000000000..16e0d1581c
--- /dev/null
+++ b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/FlyoutPageViewFlyout.xaml.cs
@@ -0,0 +1,81 @@
+// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using Xamarin.Forms;
+using Xamarin.Forms.Xaml;
+
+namespace ReactiveUI.XamForms.Tests.Activation.Mocks
+{
+ ///
+ /// Flyout Page View Flyout.
+ ///
+ ///
+ [XamlCompilation(XamlCompilationOptions.Compile)]
+ public partial class FlyoutPageViewFlyout : ContentPage
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public FlyoutPageViewFlyout()
+ {
+ InitializeComponent();
+
+ BindingContext = new FlyoutPageView1FlyoutViewModel();
+ ListView = MenuItemsListView;
+ }
+
+ ///
+ /// Gets the ListView.
+ ///
+ ///
+ /// The ListView.
+ ///
+ public ListView ListView { get; }
+
+ private class FlyoutPageView1FlyoutViewModel : INotifyPropertyChanged
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public FlyoutPageView1FlyoutViewModel()
+ {
+ MenuItems = new ObservableCollection(new[]
+ {
+ new FlyoutPageViewFlyoutMenuItem { Id = 0, Title = "Page 1" },
+ new FlyoutPageViewFlyoutMenuItem { Id = 1, Title = "Page 2" },
+ new FlyoutPageViewFlyoutMenuItem { Id = 2, Title = "Page 3" },
+ new FlyoutPageViewFlyoutMenuItem { Id = 3, Title = "Page 4" },
+ new FlyoutPageViewFlyoutMenuItem { Id = 4, Title = "Page 5" },
+ });
+ }
+
+ ///
+ /// Occurs when a property value changes.
+ ///
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ ///
+ /// Gets or sets the menu items.
+ ///
+ ///
+ /// The menu items.
+ ///
+ public ObservableCollection MenuItems { get; set; }
+
+ private void OnPropertyChanged([CallerMemberName] string propertyName = "")
+ {
+ if (PropertyChanged == null)
+ {
+ return;
+ }
+
+ PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+ }
+}
diff --git a/src/ReactiveUI.XamForms.Tests/Activation/Mocks/FlyoutPageViewFlyoutMenuItem.cs b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/FlyoutPageViewFlyoutMenuItem.cs
new file mode 100644
index 0000000000..af3b879d3e
--- /dev/null
+++ b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/FlyoutPageViewFlyoutMenuItem.cs
@@ -0,0 +1,47 @@
+// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System;
+
+namespace ReactiveUI.XamForms.Tests.Activation.Mocks
+{
+ ///
+ /// FlyoutPageViewFlyoutMenuItem.
+ ///
+ public class FlyoutPageViewFlyoutMenuItem
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public FlyoutPageViewFlyoutMenuItem()
+ {
+ TargetType = typeof(FlyoutPageViewFlyoutMenuItem);
+ }
+
+ ///
+ /// Gets or sets the identifier.
+ ///
+ ///
+ /// The identifier.
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// Gets or sets the title.
+ ///
+ ///
+ /// The title.
+ ///
+ public string? Title { get; set; }
+
+ ///
+ /// Gets or sets the type of the target.
+ ///
+ ///
+ /// The type of the target.
+ ///
+ public Type TargetType { get; set; }
+ }
+}
diff --git a/src/ReactiveUI.XamForms.Tests/Activation/Mocks/ShellView.cs b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/ShellView.cs
new file mode 100644
index 0000000000..b360fdec91
--- /dev/null
+++ b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/ShellView.cs
@@ -0,0 +1,33 @@
+// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Reactive.Disposables;
+
+namespace ReactiveUI.XamForms.Tests.Activation
+{
+ ///
+ /// Shell View.
+ ///
+ public class ShellView : ReactiveShell
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ShellView()
+ {
+ ViewModel = new();
+ this.WhenActivated(d =>
+ {
+ IsActiveCount++;
+ d(Disposable.Create(() => IsActiveCount--));
+ });
+ }
+
+ ///
+ /// Gets or sets the active count.
+ ///
+ public int IsActiveCount { get; set; }
+ }
+}
diff --git a/src/ReactiveUI.XamForms.Tests/Activation/Mocks/ShellViewModel.cs b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/ShellViewModel.cs
new file mode 100644
index 0000000000..5778865dd3
--- /dev/null
+++ b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/ShellViewModel.cs
@@ -0,0 +1,41 @@
+// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Reactive.Disposables;
+
+namespace ReactiveUI.XamForms.Tests.Activation
+{
+ ///
+ /// ShellViewModel.
+ ///
+ ///
+ ///
+ public class ShellViewModel : ReactiveObject, IActivatableViewModel
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ShellViewModel()
+ {
+ Activator = new ViewModelActivator();
+
+ this.WhenActivated(d =>
+ {
+ IsActiveCount++;
+ d(Disposable.Create(() => IsActiveCount--));
+ });
+ }
+
+ ///
+ /// Gets or sets the Activator which will be used by the View when Activation/Deactivation occurs.
+ ///
+ public ViewModelActivator Activator { get; protected set; }
+
+ ///
+ /// Gets or sets the active count.
+ ///
+ public int IsActiveCount { get; protected set; }
+ }
+}
diff --git a/src/ReactiveUI.XamForms.Tests/Activation/Mocks/TabbedPageView.cs b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/TabbedPageView.cs
new file mode 100644
index 0000000000..05bd5fc0d4
--- /dev/null
+++ b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/TabbedPageView.cs
@@ -0,0 +1,30 @@
+// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Reactive.Disposables;
+
+namespace ReactiveUI.XamForms.Tests.Activation
+{
+ ///
+ /// Tabbed Page View.
+ ///
+ public class TabbedPageView : ReactiveTabbedPage
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TabbedPageView() =>
+ this.WhenActivated(d =>
+ {
+ IsActiveCount++;
+ d(Disposable.Create(() => IsActiveCount--));
+ });
+
+ ///
+ /// Gets or sets the active count.
+ ///
+ public int IsActiveCount { get; set; }
+ }
+}
diff --git a/src/ReactiveUI.XamForms.Tests/Activation/Mocks/TabbedPageViewModel.cs b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/TabbedPageViewModel.cs
new file mode 100644
index 0000000000..88c5b15441
--- /dev/null
+++ b/src/ReactiveUI.XamForms.Tests/Activation/Mocks/TabbedPageViewModel.cs
@@ -0,0 +1,41 @@
+// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Reactive.Disposables;
+
+namespace ReactiveUI.XamForms.Tests.Activation
+{
+ ///
+ /// TabbedPageViewModel.
+ ///
+ ///
+ ///
+ public class TabbedPageViewModel : ReactiveObject, IActivatableViewModel
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TabbedPageViewModel()
+ {
+ Activator = new ViewModelActivator();
+
+ this.WhenActivated(d =>
+ {
+ IsActiveCount++;
+ d(Disposable.Create(() => IsActiveCount--));
+ });
+ }
+
+ ///
+ /// Gets or sets the Activator which will be used by the View when Activation/Deactivation occurs.
+ ///
+ public ViewModelActivator Activator { get; protected set; }
+
+ ///
+ /// Gets or sets the active count.
+ ///
+ public int IsActiveCount { get; protected set; }
+ }
+}
diff --git a/src/ReactiveUI.XamForms.Tests/ReactiveUI.XamForms.Tests.csproj b/src/ReactiveUI.XamForms.Tests/ReactiveUI.XamForms.Tests.csproj
index 4a0985c33c..7bb2161d6d 100644
--- a/src/ReactiveUI.XamForms.Tests/ReactiveUI.XamForms.Tests.csproj
+++ b/src/ReactiveUI.XamForms.Tests/ReactiveUI.XamForms.Tests.csproj
@@ -1,12 +1,15 @@
-
netcoreapp3.1false
+ latest
-
+
+
+
+
diff --git a/src/ReactiveUI.XamForms/ActivationForViewFetcher.cs b/src/ReactiveUI.XamForms/ActivationForViewFetcher.cs
index fc3dc351b8..4fa5652b6e 100644
--- a/src/ReactiveUI.XamForms/ActivationForViewFetcher.cs
+++ b/src/ReactiveUI.XamForms/ActivationForViewFetcher.cs
@@ -19,6 +19,8 @@ public class ActivationForViewFetcher : IActivationForViewFetcher
{
///
public int GetAffinityForView(Type view) =>
+ typeof(Shell).GetTypeInfo().IsAssignableFrom(view.GetTypeInfo()) ||
+ typeof(BaseShellItem).GetTypeInfo().IsAssignableFrom(view.GetTypeInfo()) ||
typeof(Page).GetTypeInfo().IsAssignableFrom(view.GetTypeInfo()) ||
typeof(View).GetTypeInfo().IsAssignableFrom(view.GetTypeInfo()) ||
typeof(Cell).GetTypeInfo().IsAssignableFrom(view.GetTypeInfo())
@@ -29,6 +31,8 @@ public IObservable GetActivationForView(IActivatableView view)
{
var activation =
GetActivationFor(view as ICanActivate) ??
+ GetActivationFor(view as Shell) ??
+ GetActivationFor(view as BaseShellItem) ??
GetActivationFor(view as Page) ??
GetActivationFor(view as View) ??
GetActivationFor(view as Cell) ??
@@ -116,5 +120,57 @@ public IObservable GetActivationForView(IActivatableView view)
return appearing.Merge(disappearing);
}
+
+ private static IObservable? GetActivationFor(Shell? shell)
+ {
+ if (shell is null)
+ {
+ return null;
+ }
+
+ var appearing = shell.WhenAnyValue(x => x.Navigation.NavigationStack.Count)
+ .Where(x => x > 0)
+ .DistinctUntilChanged()
+ .Select(_ => true);
+
+ var disappearing = Observable.FromEvent(
+ eventHandler =>
+ {
+ void Handler(object? sender, EventArgs e) => eventHandler(false);
+ return Handler;
+ },
+ x => shell.Disappearing += x,
+ x => shell.Disappearing -= x);
+
+ return appearing.Merge(disappearing);
+ }
+
+ private static IObservable? GetActivationFor(BaseShellItem? page)
+ {
+ if (page is null)
+ {
+ return null;
+ }
+
+ var appearing = Observable.FromEvent(
+ eventHandler =>
+ {
+ void Handler(object? sender, EventArgs e) => eventHandler(true);
+ return Handler;
+ },
+ x => page.Appearing += x,
+ x => page.Appearing -= x);
+
+ var disappearing = Observable.FromEvent(
+ eventHandler =>
+ {
+ void Handler(object? sender, EventArgs e) => eventHandler(false);
+ return Handler;
+ },
+ x => page.Disappearing += x,
+ x => page.Disappearing -= x);
+
+ return appearing.Merge(disappearing);
+ }
}
}
diff --git a/src/ReactiveUI.XamForms/ReactiveFlyoutPage.cs b/src/ReactiveUI.XamForms/ReactiveFlyoutPage.cs
new file mode 100644
index 0000000000..78bb839b53
--- /dev/null
+++ b/src/ReactiveUI.XamForms/ReactiveFlyoutPage.cs
@@ -0,0 +1,55 @@
+// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using Xamarin.Forms;
+
+namespace ReactiveUI.XamForms
+{
+ ///
+ /// Reactive Flyout Page.
+ ///
+ /// The type of the view model.
+ ///
+ ///
+ public class ReactiveFlyoutPage : FlyoutPage, IViewFor
+ where TViewModel : class
+ {
+ ///
+ /// The view model bindable property.
+ ///
+ public static readonly BindableProperty ViewModelProperty = BindableProperty.Create(
+ nameof(ViewModel),
+ typeof(TViewModel),
+ typeof(ReactiveFlyoutPage),
+ default(TViewModel),
+ BindingMode.OneWay,
+ propertyChanged: OnViewModelChanged);
+
+ ///
+ /// Gets or sets the ViewModel to display.
+ ///
+ public TViewModel? ViewModel
+ {
+ get => (TViewModel)GetValue(ViewModelProperty);
+ set => SetValue(ViewModelProperty, value);
+ }
+
+ ///
+ object? IViewFor.ViewModel
+ {
+ get => ViewModel;
+ set => ViewModel = (TViewModel?)value;
+ }
+
+ ///
+ protected override void OnBindingContextChanged()
+ {
+ base.OnBindingContextChanged();
+ ViewModel = BindingContext as TViewModel;
+ }
+
+ private static void OnViewModelChanged(BindableObject bindableObject, object oldValue, object newValue) => bindableObject.BindingContext = newValue;
+ }
+}
diff --git a/src/ReactiveUI.XamForms/ReactiveMasterDetailPage.cs b/src/ReactiveUI.XamForms/ReactiveMasterDetailPage.cs
index 4baa4a5e03..09ffb1bdc7 100644
--- a/src/ReactiveUI.XamForms/ReactiveMasterDetailPage.cs
+++ b/src/ReactiveUI.XamForms/ReactiveMasterDetailPage.cs
@@ -3,6 +3,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.
+using System;
using Xamarin.Forms;
namespace ReactiveUI.XamForms
diff --git a/src/ReactiveUI.XamForms/ReactiveShell.cs b/src/ReactiveUI.XamForms/ReactiveShell.cs
new file mode 100644
index 0000000000..a78d8c48fe
--- /dev/null
+++ b/src/ReactiveUI.XamForms/ReactiveShell.cs
@@ -0,0 +1,55 @@
+// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using Xamarin.Forms;
+
+namespace ReactiveUI.XamForms
+{
+ ///
+ /// Reactive Shell.
+ ///
+ /// The type of the view model.
+ ///
+ ///
+ public class ReactiveShell : Shell, IViewFor
+ where TViewModel : class
+ {
+ ///
+ /// The view model bindable property.
+ ///
+ public static readonly BindableProperty ViewModelProperty = BindableProperty.Create(
+ nameof(ViewModel),
+ typeof(TViewModel),
+ typeof(ReactiveShell),
+ default(TViewModel),
+ BindingMode.OneWay,
+ propertyChanged: OnViewModelChanged);
+
+ ///
+ /// Gets or sets the ViewModel to display.
+ ///
+ public TViewModel? ViewModel
+ {
+ get => (TViewModel)GetValue(ViewModelProperty);
+ set => SetValue(ViewModelProperty, value);
+ }
+
+ ///
+ object? IViewFor.ViewModel
+ {
+ get => ViewModel;
+ set => ViewModel = (TViewModel?)value;
+ }
+
+ ///
+ protected override void OnBindingContextChanged()
+ {
+ base.OnBindingContextChanged();
+ ViewModel = BindingContext as TViewModel;
+ }
+
+ private static void OnViewModelChanged(BindableObject bindableObject, object oldValue, object newValue) => bindableObject.BindingContext = newValue;
+ }
+}
diff --git a/src/ReactiveUI.XamForms/ReactiveShellContent.cs b/src/ReactiveUI.XamForms/ReactiveShellContent.cs
new file mode 100644
index 0000000000..44cafac6d9
--- /dev/null
+++ b/src/ReactiveUI.XamForms/ReactiveShellContent.cs
@@ -0,0 +1,95 @@
+// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System;
+using Splat;
+using Xamarin.Forms;
+
+namespace ReactiveUI.XamForms
+{
+ ///
+ /// ShellViewModel.
+ ///
+ /// The type of the view model.
+ ///
+ public class ReactiveShellContent : ShellContent, IActivatableView
+ where TViewModel : class
+ {
+ ///
+ /// The contract property.
+ ///
+ public static readonly BindableProperty ContractProperty = BindableProperty.Create(
+ nameof(Contract),
+ typeof(string),
+ typeof(ReactiveShellContent),
+ null,
+ BindingMode.Default,
+ propertyChanged: ViewModelChanged);
+
+ ///
+ /// The view model property.
+ ///
+ public static readonly BindableProperty ViewModelProperty = BindableProperty.Create(
+ nameof(ViewModel),
+ typeof(TViewModel),
+ typeof(ReactiveShellContent),
+ default(TViewModel),
+ BindingMode.Default,
+ propertyChanged: ViewModelChanged);
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ReactiveShellContent()
+ {
+ var view = Locator.Current.GetService>(Contract);
+ if (view != null)
+ {
+ ContentTemplate = new DataTemplate(() => view);
+ }
+ }
+
+ ///
+ /// Gets or sets the view model.
+ ///
+ ///
+ /// The view model.
+ ///
+ public TViewModel? ViewModel
+ {
+ get => (TViewModel)GetValue(ViewModelProperty);
+ set => SetValue(ViewModelProperty, value);
+ }
+
+ ///
+ /// Gets or sets the contract for the view.
+ ///
+ ///
+ /// The contract.
+ ///
+ public string? Contract
+ {
+ get => (string?)GetValue(ContractProperty);
+ set => SetValue(ContractProperty, value);
+ }
+
+ private static void ViewModelChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ if (Locator.Current is null)
+ {
+ throw new NullReferenceException(nameof(Locator.Current));
+ }
+
+ if (bindable is ReactiveShellContent svm)
+ {
+ var view = Locator.Current.GetService>(svm.Contract);
+ if (view != null)
+ {
+ svm.ContentTemplate = new DataTemplate(() => view);
+ }
+ }
+ }
+ }
+}