diff --git a/src/ReactiveMarbles.Navigation.sln b/src/ReactiveMarbles.Navigation.sln index 4df079e..8b62846 100644 --- a/src/ReactiveMarbles.Navigation.sln +++ b/src/ReactiveMarbles.Navigation.sln @@ -33,6 +33,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveMarbles.ViewModel.M EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ViewModel.MAUI.Example", "ViewModel.MAUI.Example\ViewModel.MAUI.Example.csproj", "{DE39BF42-E055-4797-9807-6AAA44089FE5}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ViewModelNav", "ViewModelNav", "{8EE96502-4F77-460E-89F5-5CD222CE0896}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ViewNav", "ViewNav", "{C11FB3F2-E114-448F-84AD-491EF1E0E7E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactiveMarbles.View.Core", "ReactiveMarbles.View.Core\ReactiveMarbles.View.Core.csproj", "{F715B429-EF30-4716-ADDB-24BC99892336}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactiveMarbles.View.Wpf", "ReactiveMarbles.View.Wpf\ReactiveMarbles.View.Wpf.csproj", "{E4F89A0E-71D0-48D9-9865-1BCB98ADB1FD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -85,14 +93,32 @@ Global {DE39BF42-E055-4797-9807-6AAA44089FE5}.Release|Any CPU.ActiveCfg = Release|Any CPU {DE39BF42-E055-4797-9807-6AAA44089FE5}.Release|Any CPU.Build.0 = Release|Any CPU {DE39BF42-E055-4797-9807-6AAA44089FE5}.Release|Any CPU.Deploy.0 = Release|Any CPU + {F715B429-EF30-4716-ADDB-24BC99892336}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F715B429-EF30-4716-ADDB-24BC99892336}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F715B429-EF30-4716-ADDB-24BC99892336}.Design|Any CPU.ActiveCfg = Design|Any CPU + {F715B429-EF30-4716-ADDB-24BC99892336}.Design|Any CPU.Build.0 = Design|Any CPU + {F715B429-EF30-4716-ADDB-24BC99892336}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F715B429-EF30-4716-ADDB-24BC99892336}.Release|Any CPU.Build.0 = Release|Any CPU + {E4F89A0E-71D0-48D9-9865-1BCB98ADB1FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E4F89A0E-71D0-48D9-9865-1BCB98ADB1FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E4F89A0E-71D0-48D9-9865-1BCB98ADB1FD}.Design|Any CPU.ActiveCfg = Design|Any CPU + {E4F89A0E-71D0-48D9-9865-1BCB98ADB1FD}.Design|Any CPU.Build.0 = Design|Any CPU + {E4F89A0E-71D0-48D9-9865-1BCB98ADB1FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E4F89A0E-71D0-48D9-9865-1BCB98ADB1FD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution + {6B5A20B7-CAA7-4CBF-B48F-6EFFE9414610} = {8EE96502-4F77-460E-89F5-5CD222CE0896} + {990C0E6D-0C09-482D-8AFA-F135EC206C43} = {8EE96502-4F77-460E-89F5-5CD222CE0896} + {CD830D09-4558-403B-9504-C0CFD90FDC3B} = {8EE96502-4F77-460E-89F5-5CD222CE0896} {5E67BC1D-9AD7-4DDA-B095-6EFE99AD2C2E} = {6E654C37-7F3A-4E17-83C1-CA8BC9B54476} {67688407-5509-4158-B9C3-5C2076E4946C} = {6E654C37-7F3A-4E17-83C1-CA8BC9B54476} + {267783AB-7239-42A1-9596-22F53B8884D1} = {8EE96502-4F77-460E-89F5-5CD222CE0896} {DE39BF42-E055-4797-9807-6AAA44089FE5} = {6E654C37-7F3A-4E17-83C1-CA8BC9B54476} + {F715B429-EF30-4716-ADDB-24BC99892336} = {C11FB3F2-E114-448F-84AD-491EF1E0E7E8} + {E4F89A0E-71D0-48D9-9865-1BCB98ADB1FD} = {C11FB3F2-E114-448F-84AD-491EF1E0E7E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {489B52FF-BC1B-435B-8412-301C8A46D473} diff --git a/src/ReactiveMarbles.View.Core/INotifiyRoutableView.cs b/src/ReactiveMarbles.View.Core/INotifiyRoutableView.cs new file mode 100644 index 0000000..b8dbb1a --- /dev/null +++ b/src/ReactiveMarbles.View.Core/INotifiyRoutableView.cs @@ -0,0 +1,48 @@ +// Copyright (c) 2019-2023 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated 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; +using ReactiveMarbles.ViewModel.Core; + +namespace ReactiveMarbles.View.Core; + +/// +/// INotifiy Routable ViewModel. +/// +/// +public interface INotifiyRoutableView : Mvvm.IRxObject, IUseHostedNavigation +{ + /// + /// Gets the name. + /// + /// + /// The name. + /// + string? Name { get; } + + /// + /// Raises the event. + /// + /// + /// The instance containing the event data. + /// + void WhenNavigatedFrom(IViewNavigationEventArgs e); + + /// + /// Raises the event. + /// + /// + /// The instance containing the event data. + /// + /// The disposables. + void WhenNavigatedTo(IViewNavigationEventArgs e, CompositeDisposable disposables); + + /// + /// Raises the event. + /// + /// + /// The instance containing the event data. + /// + void WhenNavigating(IViewNavigatingEventArgs e); +} diff --git a/src/ReactiveMarbles.View.Core/IRxNavBase.cs b/src/ReactiveMarbles.View.Core/IRxNavBase.cs new file mode 100644 index 0000000..841cb44 --- /dev/null +++ b/src/ReactiveMarbles.View.Core/IRxNavBase.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2019-2023 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated 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; +using ReactiveMarbles.ViewModel.Core; + +namespace ReactiveMarbles.View.Core; + +/// +/// interface for RxBase. +/// +/// +public interface IRxNavBase : INotifiyRoutableView, ICancelable, IAmBuilt +{ +} diff --git a/src/ReactiveMarbles.View.Core/IViewRoutedViewModelHost.cs b/src/ReactiveMarbles.View.Core/IViewRoutedViewModelHost.cs new file mode 100644 index 0000000..7710bf6 --- /dev/null +++ b/src/ReactiveMarbles.View.Core/IViewRoutedViewModelHost.cs @@ -0,0 +1,123 @@ +// Copyright (c) 2019-2023 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated 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 ReactiveMarbles.ViewModel.Core; + +namespace ReactiveMarbles.View.Core; + +/// +/// IViewModel Routed ViewHost. +/// +public interface IViewRoutedViewModelHost +{ + /// + /// Gets the navigation stack. + /// + /// + /// The navigation stack. + /// + ObservableCollection NavigationStack { get; } + + /// + /// Gets the current view model. + /// + /// + /// The current view model. + /// + IObservable CurrentView { get; } + + /// + /// Gets or sets a value indicating whether [navigate back is enabled]. + /// + /// + /// true if [navigate back is enabled]; otherwise, false. + /// + bool CanNavigateBack { get; set; } + + /// + /// Gets the can navigate back observable. + /// + /// + /// The can navigate back observable. + /// + IObservable CanNavigateBackObservable { get; } + + /// + /// Gets or sets a value indicating whether [navigate back is enabled]. + /// + /// + /// true if [navigate back is enabled]; otherwise, false. + /// + bool NavigateBackIsEnabled { get; set; } + + /// + /// Gets or sets the name of the host. + /// + /// + /// The name of the host. + /// + string Name { get; set; } + + /// + /// Gets a value indicating whether [requires setup]. + /// + /// + /// true if [requires setup]; otherwise, false. + /// + bool RequiresSetup { get; } + + /// + /// Clears the history. + /// + void ClearHistory(); + + /// + /// Setups this instance. + /// + void Setup(); + + /// + /// Navigates the specified contract. + /// + /// The Type. + /// The contract. + /// The parameter. + void Navigate(string? contract = null, object? parameter = null) + where T : class, IAmViewFor; + + /// + /// Navigates the specified contract. + /// + /// The view model. + /// The parameter. + void Navigate(IAmViewFor viewModel, object? parameter = null); + + /// + /// Navigates the and reset. + /// + /// The Type. + /// The contract. + /// The parameter. + void NavigateAndReset(string? contract = null, object? parameter = null) + where T : class, IAmViewFor; + + /// + /// Navigates the and reset. + /// + /// The view model. + /// The parameter. + void NavigateAndReset(IAmViewFor viewModel, object? parameter = null); + + /// + /// Navigates the back. + /// + /// The parameter. + void NavigateBack(object? parameter = null); + + /// + /// Refreshes this instance. + /// + void Refresh(); +} diff --git a/src/ReactiveMarbles.View.Core/NavigationEvents/IViewNavigatingEventArgs.cs b/src/ReactiveMarbles.View.Core/NavigationEvents/IViewNavigatingEventArgs.cs new file mode 100644 index 0000000..7e0d022 --- /dev/null +++ b/src/ReactiveMarbles.View.Core/NavigationEvents/IViewNavigatingEventArgs.cs @@ -0,0 +1,19 @@ +// Copyright (c) 2019-2023 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +namespace ReactiveMarbles.View.Core; + +/// +/// IView Model Navigating EventArgs. +/// +public interface IViewNavigatingEventArgs : IViewNavigationEventArgs +{ + /// + /// Gets or sets a value indicating whether this is cancel. + /// + /// + /// true if cancel; otherwise, false. + /// + bool Cancel { get; set; } +} diff --git a/src/ReactiveMarbles.View.Core/NavigationEvents/IViewNavigationBaseEventArgs.cs b/src/ReactiveMarbles.View.Core/NavigationEvents/IViewNavigationBaseEventArgs.cs new file mode 100644 index 0000000..7d7d0be --- /dev/null +++ b/src/ReactiveMarbles.View.Core/NavigationEvents/IViewNavigationBaseEventArgs.cs @@ -0,0 +1,37 @@ +// Copyright (c) 2019-2023 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using ReactiveMarbles.ViewModel.Core; + +namespace ReactiveMarbles.View.Core; + +/// +/// IView Model Navigation Base Event Args. +/// +public interface IViewNavigationBaseEventArgs +{ + /// + /// Gets from. + /// + /// + /// From. + /// + IRxNavBase? From { get; } + + /// + /// Gets the navigation parameter. + /// + /// + /// The navigation parameter. + /// + object? NavigationParameter { get; } + + /// + /// Gets to. + /// + /// + /// To. + /// + IRxNavBase? To { get; } +} diff --git a/src/ReactiveMarbles.View.Core/NavigationEvents/IViewNavigationEventArgs.cs b/src/ReactiveMarbles.View.Core/NavigationEvents/IViewNavigationEventArgs.cs new file mode 100644 index 0000000..3e3610e --- /dev/null +++ b/src/ReactiveMarbles.View.Core/NavigationEvents/IViewNavigationEventArgs.cs @@ -0,0 +1,37 @@ +// Copyright (c) 2019-2023 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using ReactiveMarbles.ViewModel.Core; + +namespace ReactiveMarbles.View.Core; + +/// +/// I View Model Navigation EventArgs. +/// +public interface IViewNavigationEventArgs : IViewNavigationBaseEventArgs +{ + /// + /// Gets or sets the name of the host. + /// + /// + /// The name of the host. + /// + string HostName { get; set; } + + /// + /// Gets the type of the navigation. + /// + /// + /// The type of the navigation. + /// + NavigationType NavigationType { get; } + + /// + /// Gets or sets the view. + /// + /// + /// The view. + /// + IAmViewFor? View { get; set; } +} diff --git a/src/ReactiveMarbles.View.Core/NavigationEvents/ViewNavigatingEventArgs.cs b/src/ReactiveMarbles.View.Core/NavigationEvents/ViewNavigatingEventArgs.cs new file mode 100644 index 0000000..13d7d11 --- /dev/null +++ b/src/ReactiveMarbles.View.Core/NavigationEvents/ViewNavigatingEventArgs.cs @@ -0,0 +1,33 @@ +// Copyright (c) 2019-2023 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Runtime.Serialization; +using ReactiveMarbles.ViewModel.Core; + +namespace ReactiveMarbles.View.Core; + +/// +/// View Model Navigating Event Args. +/// +/// +/// Initializes a new instance of the class. +/// +/// From. +/// To. +/// Type of the nav. +/// The view. +/// The hostName. +/// The parmeter. +[DataContract] +public class ViewNavigatingEventArgs(IRxNavBase? from, IRxNavBase? to, NavigationType navType, IAmViewFor? view, string hostName, object? parmeter = null) + : ViewNavigationEventArgs(from, to, navType, view, hostName, parmeter), IViewNavigatingEventArgs +{ + /// + /// Gets or sets a value indicating whether this + /// is canceled. + /// + /// true if cancel; otherwise, false. + [DataMember] + public bool Cancel { get; set; } +} diff --git a/src/ReactiveMarbles.View.Core/NavigationEvents/ViewNavigationBaseEventArgs.cs b/src/ReactiveMarbles.View.Core/NavigationEvents/ViewNavigationBaseEventArgs.cs new file mode 100644 index 0000000..3862fca --- /dev/null +++ b/src/ReactiveMarbles.View.Core/NavigationEvents/ViewNavigationBaseEventArgs.cs @@ -0,0 +1,38 @@ +// Copyright (c) 2019-2023 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Runtime.Serialization; +using ReactiveMarbles.ViewModel.Core; + +namespace ReactiveMarbles.View.Core; + +/// +/// View Model Navigation Base Event Args. +/// +/// +[DataContract] +public abstract class ViewNavigationBaseEventArgs + : EventArgs, IViewNavigationBaseEventArgs +{ + /// + /// Gets or sets where is Navigating from. + /// + /// From. + [DataMember] + public IRxNavBase? From { get; protected set; } + + /// + /// Gets or sets the navigation parameter. + /// + /// The navigation parameter. + [DataMember] + public object? NavigationParameter { get; protected set; } + + /// + /// Gets or sets where is Navigating to. + /// + /// To. + [DataMember] + public IRxNavBase? To { get; protected set; } +} diff --git a/src/ReactiveMarbles.View.Core/NavigationEvents/ViewNavigationEventArgs.cs b/src/ReactiveMarbles.View.Core/NavigationEvents/ViewNavigationEventArgs.cs new file mode 100644 index 0000000..06714e7 --- /dev/null +++ b/src/ReactiveMarbles.View.Core/NavigationEvents/ViewNavigationEventArgs.cs @@ -0,0 +1,55 @@ +// Copyright (c) 2019-2023 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Runtime.Serialization; +using ReactiveMarbles.ViewModel.Core; + +namespace ReactiveMarbles.View.Core; + +/// +/// View Model Navigation EventArgs. +/// +[DataContract] +public class ViewNavigationEventArgs : ViewNavigationBaseEventArgs, IViewNavigationEventArgs +{ + /// Initializes a new instance of the class. + /// From. + /// To. + /// Type of the nav. + /// The view. + /// The Hostname. + /// The parmeter. + public ViewNavigationEventArgs(IRxNavBase? from, IRxNavBase? to, NavigationType navType, IAmViewFor? view, string hostName, object? parmeter = null) + { + From = from; + To = to; + View = view; + NavigationType = navType; + NavigationParameter = parmeter; + HostName = hostName; + } + + /// + /// Gets or sets the name of the host. + /// + /// + /// The name of the host. + /// + [DataMember] + public string HostName { get; set; } + + /// + /// Gets or sets the type of the navigation. + /// + /// The type of the navigation. + [DataMember] + public NavigationType NavigationType { get; protected set; } + + /// + /// Gets or sets the view. + /// + /// The view. + [DataMember] + public IAmViewFor? View { get; set; } +} diff --git a/src/ReactiveMarbles.View.Core/ReactiveMarbles.View.Core.csproj b/src/ReactiveMarbles.View.Core/ReactiveMarbles.View.Core.csproj new file mode 100644 index 0000000..333da19 --- /dev/null +++ b/src/ReactiveMarbles.View.Core/ReactiveMarbles.View.Core.csproj @@ -0,0 +1,22 @@ + + + + netstandard2.0;net6.0;net7.0;net8.0;net7.0-macos;net8.0-macos;net7.0-tvos;net8.0-tvos;$(MauiTargetFrameworks) + $(TargetFrameworks);net462;net472;net6.0-windows10.0.19041.0;$(MauiWinTargetFrameworks) + enable + false + + + + + + + + + + + + + + + diff --git a/src/ReactiveMarbles.View.Core/RxNavBase.cs b/src/ReactiveMarbles.View.Core/RxNavBase.cs new file mode 100644 index 0000000..e30e906 --- /dev/null +++ b/src/ReactiveMarbles.View.Core/RxNavBase.cs @@ -0,0 +1,84 @@ +// Copyright (c) 2019-2023 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated 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 ReactiveMarbles.View.Core; + +/// +/// Rx Object. +/// +/// +/// +public abstract class RxNavBase : Mvvm.RxObject, IRxNavBase +{ + /// + /// Initializes a new instance of the class. + /// + protected RxNavBase() + { + } + + /// + /// Gets the URL path segment. + /// + /// + /// The URL path segment. + /// + public string? Name => GetType().FullName; + + /// + /// Gets a value indicating whether this instance is disposed. + /// + /// true if this instance is disposed; otherwise, false. + public bool IsDisposed => Disposables?.IsDisposed == true; + + /// + /// Gets the disposables. + /// + /// + /// The disposables. + /// + protected CompositeDisposable Disposables { get; } = new CompositeDisposable(); + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting + /// unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + public virtual void WhenNavigatedFrom(IViewNavigationEventArgs e) + { + } + + /// + public virtual void WhenNavigatedTo(IViewNavigationEventArgs e, CompositeDisposable disposables) + { + } + + /// + public virtual void WhenNavigating(IViewNavigatingEventArgs e) + { + } + + /// + /// 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 (!IsDisposed && disposing) + { + Disposables?.Dispose(); + } + } +} diff --git a/src/ReactiveMarbles.View.Core/ServiceLocatorMixins.cs b/src/ReactiveMarbles.View.Core/ServiceLocatorMixins.cs new file mode 100644 index 0000000..e139dbb --- /dev/null +++ b/src/ReactiveMarbles.View.Core/ServiceLocatorMixins.cs @@ -0,0 +1,124 @@ +// Copyright (c) 2019-2023 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using ReactiveMarbles.View.Core; +using ReactiveMarbles.ViewModel.Core; + +namespace ReactiveMarbles.Locator; + +/// +/// ServiceLocatorMixins. +/// +public static class ServiceLocatorMixins +{ + /// + /// Adds the navigation view. + /// + /// The type of the view model. + /// The type of the view. + /// The service locator. + public static void AddNavigationView(this IServiceLocator serviceLocator) + where TView : class, IAmViewFor, new() + where TViewModel : class, IRxNavBase + { + if (serviceLocator == null) + { + throw new ArgumentNullException(nameof(serviceLocator)); + } + + serviceLocator.AddService(() => new TView(), typeof(TViewModel).FullName!); + serviceLocator.AddService>(() => new TView()); + serviceLocator.AddService(() => ServiceLocator.Current().GetService>() as TView); + } + + /// + /// Adds the navigation view. + /// + /// The type of the view model. + /// The type of the view. + /// The service locator. + /// The contract. + /// serviceLocator. + public static void AddNavigationView(this IServiceLocator serviceLocator, string contract) + where TView : class, IAmViewFor, new() + where TViewModel : class, IRxNavBase + { + if (serviceLocator == null) + { + throw new ArgumentNullException(nameof(serviceLocator)); + } + + serviceLocator.AddService(() => new TView(), typeof(TViewModel).FullName!); + serviceLocator.AddService>(() => new TView(), contract); + serviceLocator.AddService(() => ServiceLocator.Current().GetService>() as TView, contract); + } + + /// + /// Gets the service. + /// + /// The Type. + /// The service locator. + /// The contract. + /// + /// An instance. + /// + /// serviceLocator. + public static IAmViewFor? GetView(this IServiceLocator serviceLocator, string? contract = null) + where T : class, IRxNavBase + { + if (serviceLocator == null) + { + throw new ArgumentNullException(nameof(serviceLocator)); + } + + return serviceLocator.GetServiceWithContract>(contract); + } + + /// + /// Gets the service. + /// + /// The service locator. + /// The view model. + /// + /// An instance. + /// + /// serviceLocator. + public static IAmViewFor? GetView(this IServiceLocator serviceLocator, IRxNavBase viewModel) + { + if (serviceLocator == null) + { + throw new ArgumentNullException(nameof(serviceLocator)); + } + + if (viewModel == null) + { + throw new ArgumentNullException(nameof(viewModel)); + } + + return (IAmViewFor)serviceLocator.GetServiceWithContract(viewModel.GetType().FullName); + } + + /// + /// Gets the service. + /// + /// The type. + /// The service locator. + /// The contract. + /// An instance of T. + /// serviceLocator. + public static T GetServiceWithContract(this IServiceLocator serviceLocator, string? contract = null) + { + if (serviceLocator == null) + { + throw new ArgumentNullException(nameof(serviceLocator)); + } + + if (string.IsNullOrWhiteSpace(contract)) + { + return serviceLocator.GetService(); + } + + return serviceLocator.GetService(contract!); + } +} diff --git a/src/ReactiveMarbles.View.Core/ViewRoutedViewModelHostMixins.cs b/src/ReactiveMarbles.View.Core/ViewRoutedViewModelHostMixins.cs new file mode 100644 index 0000000..c66c179 --- /dev/null +++ b/src/ReactiveMarbles.View.Core/ViewRoutedViewModelHostMixins.cs @@ -0,0 +1,615 @@ +// Copyright (c) 2019-2023 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Runtime.CompilerServices; +using ReactiveMarbles.Extensions; +using ReactiveMarbles.ViewModel.Core; + +[assembly: InternalsVisibleTo("ReactiveMarbles.View.Wpf")] +[assembly: InternalsVisibleTo("ReactiveMarbles.View.WinForms")] +[assembly: InternalsVisibleTo("ReactiveMarbles.View.MAUI")] +[assembly: InternalsVisibleTo("ReactiveMarbles.View.Avalonia")] + +namespace ReactiveMarbles.View.Core; + +/// +/// View Model Routed View Host Mixins. +/// +public static class ViewRoutedViewModelHostMixins +{ +#pragma warning disable RCS1175 // Unused 'this' parameter. + + internal static ReplaySubject ASetupCompleted { get; } = new(1); + + internal static Dictionary CurrentViewDisposable { get; } = []; + + internal static Dictionary NavigationHost { get; } = []; + + internal static Dictionary> ResultNavigating { get; } = []; + + internal static Subject SetWhenNavigated { get; } = new(); + + internal static Subject SetWhenNavigating { get; } = new(); + + internal static Dictionary> WhenSetupSubjects { get; } = []; + + /// + /// Determines whether this instance [can navigate back] the specified this. + /// + /// The this. + /// + /// A bool. + /// + /// this. + public static IObservable CanNavigateBack(this IUseNavigation @this) + { + if (@this == null) + { + throw new ArgumentNullException(nameof(@this)); + } + + return Observable.Create(obs => + { + var dis = new CompositeDisposable(); + @this.WhenSetup().Subscribe(_ => + { + if (NavigationHost.Count > 0 && @this.Name != null) + { + if (@this.Name.Length == 0) + { + NavigationHost.First().Value.CanNavigateBackObservable + .Subscribe(x => obs.OnNext(x)) + .DisposeWith(dis); + } + + if (NavigationHost.TryGetValue(@this.Name, out var value)) + { + value.CanNavigateBackObservable + .Subscribe(x => obs.OnNext(x)) + .DisposeWith(dis); + } + } + }); + + obs.OnNext(false); + + return dis; + }); + } + + /// + /// Determines whether this instance [can navigate back] the specified host name. + /// + /// The navigation host. + /// Name of the host. + /// + /// A bool. + /// + /// this. + public static IObservable CanNavigateBack(this IUseHostedNavigation @this, string hostName = "") + { + if (@this == null) + { + throw new ArgumentNullException(nameof(@this)); + } + + return Observable.Create(obs => + { + var dis = new CompositeDisposable(); + @this.WhenSetup(hostName).Subscribe(_ => + { + if (NavigationHost.Count > 0 && hostName != null) + { + if (hostName.Length == 0) + { + NavigationHost.First().Value.CanNavigateBackObservable + .DistinctUntilChanged() + .Subscribe(x => obs.OnNext(x)) + .DisposeWith(dis); + } + else if (NavigationHost.TryGetValue(hostName, out var value)) + { + value.CanNavigateBackObservable + .DistinctUntilChanged() + .Subscribe(x => obs.OnNext(x)) + .DisposeWith(dis); + } + } + }).DisposeWith(dis); + + obs.OnNext(false); + + return dis; + }); + } + + /// + /// Clears the history. + /// + /// The dummy. + /// + /// Chainable host. + /// + /// this. + /// No navigation host registered, please ensure that the NavigationShell has a Name. + public static IUseNavigation ClearHistory(this IUseNavigation @this) + { + if (@this == null) + { + throw new ArgumentNullException(nameof(@this)); + } + + if (NavigationHost.Count == 0) + { + throw new InvalidOperationException("No navigation host registered, please ensure that the NavigationShell has a Name."); + } + + if (NavigationHost.Count > 0 && @this.Name != null) + { + switch (@this.Name.Length) + { + case 0: + NavigationHost.First().Value.ClearHistory(); + break; + default: + NavigationHost[@this.Name].ClearHistory(); + break; + } + } + + return @this; + } + + /// + /// Clears the history. + /// + /// The this. + /// Name of the host. + /// Chainable host. + /// No navigation host registered, please ensure that the NavigationShell has a Name. + public static IUseHostedNavigation ClearHistory(this IUseHostedNavigation @this, string hostName = "") + { + if (NavigationHost.Count == 0) + { + throw new InvalidOperationException("No navigation host registered, please ensure that the NavigationShell has a Name."); + } + + if (NavigationHost.Count > 0 && hostName != null) + { + switch (hostName.Length) + { + case 0: + NavigationHost.First().Value.ClearHistory(); + break; + default: + NavigationHost[hostName].ClearHistory(); + break; + } + } + + return @this; + } + + /// + /// Navigates the back. + /// + /// The this. + /// The parameter. + /// + /// Chainable host. + /// + /// this. + /// No navigation host registered, please ensure that the NavigationShell has a Name. + public static IUseNavigation NavigateBack(this IUseNavigation @this, object? parameter = null) + { + if (@this == null) + { + throw new ArgumentNullException(nameof(@this)); + } + + if (NavigationHost.Count == 0) + { + throw new InvalidOperationException("No navigation host registered, please ensure that the NavigationShell has a Name."); + } + + if (NavigationHost.Count > 0 && @this.Name != null) + { + switch (@this.Name.Length) + { + case 0: + NavigationHost.First().Value.NavigateBack(parameter); + break; + default: + if (NavigationHost.TryGetValue(@this.Name, out var value)) + { + value.NavigateBack(parameter); + } + + break; + } + } + + return @this; + } + + /// + /// Navigates backwards. + /// + /// The this. + /// Name of the host. + /// The parameter. + /// Chainable host. + /// No navigation host registered, please ensure that the NavigationShell has a Name. + public static IUseHostedNavigation NavigateBack(this IUseHostedNavigation @this, string hostName = "", object? parameter = null) + { + if (NavigationHost.Count == 0) + { + throw new InvalidOperationException("No navigation host registered, please ensure that the NavigationShell has a Name."); + } + + if (NavigationHost.Count > 0 && hostName != null) + { + switch (hostName.Length) + { + case 0: + NavigationHost.First().Value.NavigateBack(parameter); + break; + default: + if (NavigationHost.TryGetValue(hostName, out var value)) + { + value.NavigateBack(parameter); + } + + break; + } + } + + return @this; + } + + /// + /// Navigates the specified contract. + /// + /// The Type. + /// The this. + /// The contract. + /// The parameter. + /// Chainable host. + /// this. + /// No navigation host registered, please ensure that the NavigationShell has a Name. + public static IUseNavigation NavigateToView(this IUseNavigation @this, string? contract = null, object? parameter = null) + where T : class, IRxNavBase + { + if (@this == null) + { + throw new ArgumentNullException(nameof(@this)); + } + + if (NavigationHost.Count == 0) + { + throw new InvalidOperationException("No navigation host registered, please ensure that the NavigationShell has a Name."); + } + + if (NavigationHost.Count > 0 && @this.Name != null) + { + switch (@this.Name.Length) + { + case 0: + NavigationHost.First().Value.Navigate>(contract, parameter); + break; + default: + NavigationHost[@this.Name].Navigate>(contract, parameter); + break; + } + } + + return @this; + } + + /// + /// Navigates to view. + /// + /// The Type. + /// The this. + /// Name of the host. + /// The contract. + /// The parameter. + /// Chainable host. + /// No navigation host registered, please ensure that the NavigationShell has a Name. + public static IUseHostedNavigation NavigateToView(this IUseHostedNavigation @this, string? hostName = "", string? contract = null, object? parameter = null) + where T : class, IRxNavBase + { + if (NavigationHost.Count == 0) + { + throw new InvalidOperationException("No navigation host registered, please ensure that the NavigationShell has a Name."); + } + + if (NavigationHost.Count > 0 && hostName != null) + { + switch (hostName.Length) + { + case 0: + NavigationHost.First().Value.Navigate>(contract, parameter); + break; + default: + if (NavigationHost.TryGetValue(hostName, out var value)) + { + value.Navigate>(contract, parameter); + } + + break; + } + } + + return @this; + } + + /// + /// Navigates the and reset. + /// + /// The Type. + /// The this. + /// The contract. + /// The parameter. + /// Chainable host. + /// this. + /// No navigation host registered, please ensure that the NavigationShell has a Name. + public static IUseNavigation NavigateToViewAndClearHistory(this IUseNavigation @this, string? contract = null, object? parameter = null) + where T : class, IRxNavBase + { + if (@this == null) + { + throw new ArgumentNullException(nameof(@this)); + } + + if (NavigationHost.Count == 0) + { + throw new InvalidOperationException("No navigation host registered, please ensure that the NavigationShell has a Name."); + } + + if (NavigationHost.Count > 0 && @this.Name != null) + { + switch (@this.Name.Length) + { + case 0: + NavigationHost.First().Value.NavigateAndReset>(contract, parameter); + break; + default: + if (NavigationHost.TryGetValue(@this.Name, out var value)) + { + value.NavigateAndReset>(contract, parameter); + } + + break; + } + } + + return @this; + } + + /// + /// Navigates to view and clear history. + /// + /// The Type. + /// The this. + /// Name of the host. + /// The contract. + /// The parameter. + /// Chainable host. + /// No navigation host registered, please ensure that the NavigationShell has a Name. + public static IUseHostedNavigation NavigateToViewAndClearHistory(this IUseHostedNavigation @this, string hostName = "", string? contract = null, object? parameter = null) + where T : class, IRxNavBase + { + if (NavigationHost.Count == 0) + { + throw new InvalidOperationException("No navigation host registered, please ensure that the NavigationShell has a Name."); + } + + if (NavigationHost.Count > 0 && hostName != null) + { + switch (hostName.Length) + { + case 0: + NavigationHost.First().Value.NavigateAndReset>(contract, parameter); + break; + default: + if (NavigationHost.TryGetValue(hostName, out var value)) + { + value.NavigateAndReset>(contract, parameter); + } + + break; + } + } + + return @this; + } + + /// + /// Sets the main navigation host. + /// + /// The dummy. + /// The view host. + public static void SetMainNavigationHost(this ISetNavigation @this, IViewRoutedViewModelHost viewHost) + { + if (@this == null) + { + throw new ArgumentNullException(nameof(@this)); + } + + if (viewHost == null) + { + throw new ArgumentNullException(nameof(viewHost)); + } + + if (NavigationHost.ContainsKey(@this.Name)) + { + return; + } + + WhenSetupSubjects.Add(@this.Name, new(1)); + NavigationHost.Add(@this.Name, viewHost); + CurrentViewDisposable.Add(@this.Name, []); + ResultNavigating.Add(@this.Name, new Subject()); + + if (viewHost.RequiresSetup) + { + viewHost.Setup(); + } + + ASetupCompleted.OnNext(Unit.Default); + WhenSetupSubjects[@this.Name].OnNext(true); + } + + /// + /// Whens the navigated from. + /// + /// The this. + /// The e. + public static void WhenNavigatedFrom(this INotifiyNavigation @this, Action e) + { + if (@this == null) + { + throw new ArgumentNullException(nameof(@this)); + } + + @this.ISetupNavigatedFrom = true; + var vm = (@this as IAmViewFor)?.ViewModel as INotifiyRoutableView; + SetWhenNavigated.Where(x => x.From != null && x.From.Name == vm?.Name).Subscribe(ea => + { + e(ea); + ea.From?.WhenNavigatedFrom(ea); + }).DisposeWith(@this.CleanUp); + } + + /// + /// Whens the navigated to. + /// + /// The this. + /// The e. + public static void WhenNavigatedTo(this INotifiyNavigation @this, Action e) + { + if (@this == null) + { + throw new ArgumentNullException(nameof(@this)); + } + + @this.ISetupNavigatedTo = true; + var vm = (@this as IAmViewFor)?.ViewModel as INotifiyRoutableView; + SetWhenNavigated.Where(x => x?.To?.Name == vm?.Name).Subscribe(ea => + { + if (ea.NavigationType == NavigationType.New) + { + CurrentViewDisposable[ea.HostName]?.Dispose(); + CurrentViewDisposable[ea.HostName] = []; + } + + e(ea, CurrentViewDisposable[ea.HostName]); + ea?.To?.WhenNavigatedTo(ea, CurrentViewDisposable[ea.HostName]); + }).DisposeWith(@this.CleanUp); + } + + /// + /// Called when [navigating]. + /// + /// The this. + /// + /// The instance containing the event data. + /// + public static void WhenNavigating(this INotifiyNavigation @this, Func e) + { + if (@this == null) + { + throw new ArgumentNullException(nameof(@this)); + } + + @this.ISetupNavigating = true; + var vm = (@this as IAmViewFor)?.ViewModel as INotifiyRoutableView; + SetWhenNavigating.Where(x => x?.From == null || x.From.Name == vm?.Name).Subscribe(ea => + { + if (ea != null) + { + if (ea.From != null) + { + e(ea); + } + + ea.From?.WhenNavigating(ea); + + ResultNavigating[ea.HostName].OnNext(ea); + } + }).DisposeWith(@this.CleanUp); + } + + /// + /// Whens the activated. + /// + /// The this. + /// A Bool. + public static IObservable WhenSetup(this IUseNavigation @this) => + Observable.Create(obs => + { + var dis = new CompositeDisposable(); + ASetupCompleted.Subscribe(_ => + { + if (WhenSetupSubjects.Count > 0 && @this.Name != null) + { + switch (@this.Name.Length) + { + case 0: + { + WhenSetupSubjects.First().Value.Where(x => x).Subscribe(obs).DisposeWith(dis); + break; + } + + default: + if (NavigationHost.ContainsKey(@this.Name)) + { + WhenSetupSubjects[@this.Name].Where(x => x).Subscribe(obs).DisposeWith(dis); + } + + break; + } + } + }).DisposeWith(dis); + return dis; + }); + + /// + /// Whens the activated. + /// + /// The dummy. + /// Name of the host. + /// + /// A Bool. + /// + public static IObservable WhenSetup(this IUseHostedNavigation dummy, string? hostName = "") => + Observable.Create(obs => + { + var dis = new CompositeDisposable(); + ASetupCompleted.Subscribe(_ => + { + if (WhenSetupSubjects.Count > 0) + { + if (hostName?.Length > 0) + { + if (NavigationHost.ContainsKey(hostName)) + { + WhenSetupSubjects[hostName].Where(x => x).Subscribe(obs).DisposeWith(dis); + } + } + else + { + WhenSetupSubjects.First().Value.Where(x => x).Subscribe(obs).DisposeWith(dis); + } + } + }).DisposeWith(dis); + return dis; + }); +#pragma warning restore RCS1175 // Unused 'this' parameter. +} diff --git a/src/ReactiveMarbles.View.Wpf/AssemblyInfo.cs b/src/ReactiveMarbles.View.Wpf/AssemblyInfo.cs new file mode 100644 index 0000000..729cf17 --- /dev/null +++ b/src/ReactiveMarbles.View.Wpf/AssemblyInfo.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2019-2023 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Windows; +using System.Windows.Markup; + +[assembly: XmlnsDefinition("https://github.com/reactivemarbles/Navigation", "ReactiveMarbles.ViewModel.Core")] +[assembly: XmlnsDefinition("https://github.com/reactivemarbles/Navigation", "ReactiveMarbles.View.Core")] +[assembly: XmlnsDefinition("https://github.com/reactivemarbles/Navigation", "ReactiveMarbles.View.Wpf")] +[assembly: XmlnsPrefix("https://github.com/reactivemarbles/Navigation", "rxNav")] +[assembly: ThemeInfo(ResourceDictionaryLocation.SourceAssembly, ResourceDictionaryLocation.SourceAssembly)] diff --git a/src/ReactiveMarbles.View.Wpf/ReactiveMarbles.View.Wpf.csproj b/src/ReactiveMarbles.View.Wpf/ReactiveMarbles.View.Wpf.csproj new file mode 100644 index 0000000..fae4c63 --- /dev/null +++ b/src/ReactiveMarbles.View.Wpf/ReactiveMarbles.View.Wpf.csproj @@ -0,0 +1,23 @@ + + + + net472;net48;net6.0-windows10.0.19041.0;$(MauiWinTargetFrameworks) + true + enable + false + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/src/ReactiveMarbles.View.Wpf/ViewRoutedViewModelHost.cs b/src/ReactiveMarbles.View.Wpf/ViewRoutedViewModelHost.cs new file mode 100644 index 0000000..2cdc379 --- /dev/null +++ b/src/ReactiveMarbles.View.Wpf/ViewRoutedViewModelHost.cs @@ -0,0 +1,159 @@ +// Copyright (c) 2019-2023 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated 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.Reactive.Linq; +using System.Reactive.Subjects; +using System.Windows; +using System.Windows.Controls; +using ReactiveMarbles.Locator; +using ReactiveMarbles.Mvvm; +using ReactiveMarbles.View.Core; +using ReactiveMarbles.ViewModel.Core; + +namespace ReactiveMarbles.View.Wpf +{ + /// + /// View Routed ViewModel Host. + /// + /// + /// + public class ViewRoutedViewModelHost : ContentControl, IViewRoutedViewModelHost + { + /// + /// The navigate back is enabled property. + /// + public static readonly DependencyProperty CanNavigateBackProperty = DependencyProperty.Register(nameof(CanNavigateBack), typeof(bool), typeof(ViewRoutedViewModelHost), new PropertyMetadata(false)); + + /// + /// The host name property. + /// + public static readonly DependencyProperty HostNameProperty = DependencyProperty.Register(nameof(HostName), typeof(string), typeof(ViewRoutedViewModelHost), new PropertyMetadata(string.Empty)); + + /// + /// The navigate back is enabled property. + /// + public static readonly DependencyProperty NavigateBackIsEnabledProperty = DependencyProperty.Register(nameof(NavigateBackIsEnabled), typeof(bool), typeof(ViewRoutedViewModelHost), new PropertyMetadata(true)); + + private readonly ISubject _canNavigateBackSubject = new Subject(); + private readonly ISubject _currentViewModelSubject = new Subject(); + private IRxNavBase? _currentViewModel; + private IAmViewFor? _currentView; + private IAmViewFor? _lastView; + private bool _navigateBack; + private bool _resetStack; + private IRxNavBase? _toViewModel; + private ICoreRegistration? _coreRegistration; + + /// + /// Gets the navigation stack. + /// + /// + /// The navigation stack. + /// + public ObservableCollection NavigationStack { get; } = []; + + /// + /// Gets the current view model. + /// + /// + /// The current view model. + /// + public IObservable CurrentView => _currentViewModelSubject; + + /// + /// Gets or sets a value indicating whether [navigate back is enabled]. + /// + /// true if [navigate back is enabled]; otherwise, false. + public bool CanNavigateBack + { + get => (bool)GetValue(CanNavigateBackProperty); + set => SetValue(CanNavigateBackProperty, value); + } + + /// + /// Gets the can navigate back observable. + /// + /// + /// The can navigate back observable. + /// + public IObservable CanNavigateBackObservable => _canNavigateBackSubject; + + /// + /// Gets or sets the name of the host. + /// + /// + /// The name of the host. + /// + public string HostName + { + get => (string)GetValue(HostNameProperty); + set => SetValue(HostNameProperty, value); + } + + /// + /// Gets or sets a value indicating whether [navigate back is enabled]. + /// + /// + /// true if [navigate back is enabled]; otherwise, false. + /// + public bool NavigateBackIsEnabled + { + get => (bool)GetValue(NavigateBackIsEnabledProperty); + set => SetValue(NavigateBackIsEnabledProperty, value); + } + + /// + /// Gets a value indicating whether [requires setup]. + /// + /// + /// true if [requires setup]; otherwise, false. + /// + public bool RequiresSetup => false; + + /// + /// Clears the history. + /// + public void ClearHistory() => throw new NotImplementedException(); + /// + /// Navigates the specified contract. + /// + /// The view model. + /// The parameter. + public void Navigate(IAmViewFor viewModel, object? parameter = null) => throw new NotImplementedException(); + /// + /// Navigates the and reset. + /// + /// The view model. + /// The parameter. + public void NavigateAndReset(IAmViewFor viewModel, object? parameter = null) => throw new NotImplementedException(); + /// + /// Navigates the back. + /// + /// The parameter. + public void NavigateBack(object? parameter = null) => throw new NotImplementedException(); + /// + /// Refreshes this instance. + /// + public void Refresh() => throw new NotImplementedException(); + /// + /// Setups this instance. + /// + public void Setup() => throw new NotImplementedException(); + /// + /// Navigates the specified contract. + /// + /// The Type. + /// The contract. + /// The parameter. + void IViewRoutedViewModelHost.Navigate(string? contract, object? parameter) => throw new NotImplementedException(); + /// + /// Navigates the and reset. + /// + /// The Type. + /// The contract. + /// The parameter. + void IViewRoutedViewModelHost.NavigateAndReset(string? contract, object? parameter) => throw new NotImplementedException(); + } +}