From 5b186ce0ae3e8732aa5ac1e9828641ec7e3c3bc8 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Sat, 8 Jul 2023 04:11:37 +0100 Subject: [PATCH] Fix Maui Navigation RoutedViewHost (#3574) --- .../ActivationForViewFetcher.cs | 60 ++++++- .../Common/AutoDataTemplateBindingHook.cs | 81 +++++++++ .../Common/BooleanToVisibilityHint.cs | 31 ++++ .../BooleanToVisibilityTypeConverter.cs | 70 ++++++++ .../Common/PlatformOperations.cs | 16 ++ src/ReactiveUI.Maui/Common/ReactivePage.cs | 137 +++++++++++++++ .../Common/ReactiveUserControl.cs | 113 ++++++++++++ src/ReactiveUI.Maui/Common/RoutedViewHost.cs | 161 ++++++++++++++++++ .../Common/ViewModelViewHost.cs | 158 +++++++++++++++++ src/ReactiveUI.Maui/ReactiveUI.Maui.csproj | 50 +++--- src/ReactiveUI.Maui/Registrations.cs | 6 +- src/ReactiveUI.Maui/RoutedViewHost.cs | 62 ++++--- .../DependencyObjectObservableForProperty.cs | 6 +- .../WinUI}/DispatcherQueueScheduler.cs | 0 .../TransitioningContentControl.Empty.cs | 4 +- .../ActivationForViewFetcher.cs | 57 ------- src/ReactiveUI.WinUI/ReactiveUI.WinUI.csproj | 16 +- src/ReactiveUI.WinUI/Registrations.cs | 42 ----- 18 files changed, 899 insertions(+), 171 deletions(-) create mode 100644 src/ReactiveUI.Maui/Common/AutoDataTemplateBindingHook.cs create mode 100644 src/ReactiveUI.Maui/Common/BooleanToVisibilityHint.cs create mode 100644 src/ReactiveUI.Maui/Common/BooleanToVisibilityTypeConverter.cs create mode 100644 src/ReactiveUI.Maui/Common/PlatformOperations.cs create mode 100644 src/ReactiveUI.Maui/Common/ReactivePage.cs create mode 100644 src/ReactiveUI.Maui/Common/ReactiveUserControl.cs create mode 100644 src/ReactiveUI.Maui/Common/RoutedViewHost.cs create mode 100644 src/ReactiveUI.Maui/Common/ViewModelViewHost.cs rename src/{ReactiveUI.WinUI => ReactiveUI.Maui/WinUI}/DependencyObjectObservableForProperty.cs (98%) rename src/{ReactiveUI.WinUI => ReactiveUI.Maui/WinUI}/DispatcherQueueScheduler.cs (100%) rename src/{ReactiveUI.WinUI => ReactiveUI.Maui/WinUI}/TransitioningContentControl.Empty.cs (95%) delete mode 100644 src/ReactiveUI.WinUI/ActivationForViewFetcher.cs delete mode 100644 src/ReactiveUI.WinUI/Registrations.cs diff --git a/src/ReactiveUI.Maui/ActivationForViewFetcher.cs b/src/ReactiveUI.Maui/ActivationForViewFetcher.cs index afc2e61c8f..b0185bc4c0 100644 --- a/src/ReactiveUI.Maui/ActivationForViewFetcher.cs +++ b/src/ReactiveUI.Maui/ActivationForViewFetcher.cs @@ -4,12 +4,20 @@ // See the LICENSE file in the project root for full license information. using System; -using System.ComponentModel; using System.Reactive.Linq; using System.Reflection; +#if HAS_WINUI +using Microsoft.UI.Xaml; +using Windows.Foundation; + +namespace ReactiveUI.WinUI; +#endif +#if HAS_MAUI +using System.ComponentModel; using Microsoft.Maui.Controls; namespace ReactiveUI.Maui; +#endif /// /// This class is the default implementation that determines when views are Activated and Deactivated. @@ -19,9 +27,14 @@ public class ActivationForViewFetcher : IActivationForViewFetcher { /// public int GetAffinityForView(Type view) => +#if HAS_WINUI + typeof(FrameworkElement).GetTypeInfo().IsAssignableFrom(view.GetTypeInfo()) +#endif +#if HAS_MAUI typeof(Page).GetTypeInfo().IsAssignableFrom(view.GetTypeInfo()) || - typeof(View).GetTypeInfo().IsAssignableFrom(view.GetTypeInfo()) || + typeof(View).GetTypeInfo().IsAssignableFrom(view.GetTypeInfo()) || typeof(Cell).GetTypeInfo().IsAssignableFrom(view.GetTypeInfo()) +#endif ? 10 : 0; /// @@ -29,9 +42,14 @@ public IObservable GetActivationForView(IActivatableView view) { var activation = GetActivationFor(view as ICanActivate) ?? +#if HAS_WINUI + GetActivationFor(view as FrameworkElement) ?? +#endif +#if HAS_MAUI GetActivationFor(view as Page) ?? GetActivationFor(view as View) ?? GetActivationFor(view as Cell) ?? +#endif Observable.Never; return activation.DistinctUntilChanged(); @@ -39,6 +57,7 @@ public IObservable GetActivationForView(IActivatableView view) private static IObservable? GetActivationFor(ICanActivate? canActivate) => canActivate?.Activated.Select(_ => true).Merge(canActivate.Deactivated.Select(_ => false)); +#if HAS_MAUI private static IObservable? GetActivationFor(Page? page) { if (page is null) @@ -116,4 +135,39 @@ public IObservable GetActivationForView(IActivatableView view) return appearing.Merge(disappearing); } -} \ No newline at end of file +#endif + +#if HAS_WINUI + private static IObservable GetActivationFor(FrameworkElement? view) + { + if (view is null) + { + return Observable.Empty; + } + + var viewLoaded = Observable.FromEvent, bool>( + eventHandler => + { + void Handler(FrameworkElement sender, object e) => eventHandler(true); + return Handler; + }, + x => view.Loading += x, + x => view.Loading -= x); + + var viewUnloaded = Observable.FromEvent( + eventHandler => + { + void Handler(object sender, RoutedEventArgs e) => eventHandler(false); + return Handler; + }, + x => view.Unloaded += x, + x => view.Unloaded -= x); + + return viewLoaded + .Merge(viewUnloaded) + .Select(b => b ? view.WhenAnyValue(x => x.IsHitTestVisible).SkipWhile(x => !x) : Observables.False) + .Switch() + .DistinctUntilChanged(); + } +#endif +} diff --git a/src/ReactiveUI.Maui/Common/AutoDataTemplateBindingHook.cs b/src/ReactiveUI.Maui/Common/AutoDataTemplateBindingHook.cs new file mode 100644 index 0000000000..571c98fc60 --- /dev/null +++ b/src/ReactiveUI.Maui/Common/AutoDataTemplateBindingHook.cs @@ -0,0 +1,81 @@ +// Copyright (c) 2023 .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. + +#if HAS_WINUI +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Markup; + +namespace ReactiveUI +{ + /// + /// AutoDataTemplateBindingHook is a binding hook that checks ItemsControls + /// that don't have DataTemplates, and assigns a default DataTemplate that + /// loads the View associated with each ViewModel. + /// + public class AutoDataTemplateBindingHook : IPropertyBindingHook + { + /// + /// Gets the default item template. + /// + [SuppressMessage("Design", "CA1307: Use the currency locale settings", Justification = "Not available on all platforms.")] + public static Lazy DefaultItemTemplate { get; } = new(() => + { + const string template = " " + + "" + + ""; + + var assemblyName = typeof(AutoDataTemplateBindingHook).Assembly.FullName; + assemblyName = assemblyName?.Substring(0, assemblyName.IndexOf(',')); + + return (DataTemplate)XamlReader.Load(template.Replace("__ASSEMBLYNAME__", assemblyName)); + }); + + /// + public bool ExecuteHook(object? source, object target, Func[]> getCurrentViewModelProperties, Func[]> getCurrentViewProperties, BindingDirection direction) + { + if (getCurrentViewProperties is null) + { + throw new ArgumentNullException(nameof(getCurrentViewProperties)); + } + + var viewProperties = getCurrentViewProperties(); + var lastViewProperty = viewProperties.LastOrDefault(); + + if (lastViewProperty?.Sender is not ItemsControl itemsControl) + { + return true; + } + + if (!string.IsNullOrEmpty(itemsControl.DisplayMemberPath)) + { + return true; + } + + if (viewProperties.Last().GetPropertyName() != "ItemsSource") + { + return true; + } + + if (itemsControl.ItemTemplate is not null) + { + return true; + } + + if (itemsControl.ItemTemplateSelector is not null) + { + return true; + } + + itemsControl.ItemTemplate = DefaultItemTemplate.Value; + return true; + } + } +} +#endif diff --git a/src/ReactiveUI.Maui/Common/BooleanToVisibilityHint.cs b/src/ReactiveUI.Maui/Common/BooleanToVisibilityHint.cs new file mode 100644 index 0000000000..df0b9fee7d --- /dev/null +++ b/src/ReactiveUI.Maui/Common/BooleanToVisibilityHint.cs @@ -0,0 +1,31 @@ +// Copyright (c) 2023 .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 +{ + /// + /// Enum that hints at the visibility of a ui element. + /// + [Flags] + public enum BooleanToVisibilityHint + { + /// + /// Do not modify the boolean type conversion from it's default action of using the Visibility.Collapsed. + /// + None = 0, + + /// + /// Inverse the action of the boolean type conversion, when it's true collapse the visibility. + /// + Inverse = 1 << 1, + + /// + /// Use the hidden version rather than the Collapsed. + /// + UseHidden = 1 << 2, + } +} diff --git a/src/ReactiveUI.Maui/Common/BooleanToVisibilityTypeConverter.cs b/src/ReactiveUI.Maui/Common/BooleanToVisibilityTypeConverter.cs new file mode 100644 index 0000000000..c622b42298 --- /dev/null +++ b/src/ReactiveUI.Maui/Common/BooleanToVisibilityTypeConverter.cs @@ -0,0 +1,70 @@ +// Copyright (c) 2023 .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; +#if HAS_MAUI +using Microsoft.Maui; +#endif +#if HAS_WINUI +using Microsoft.UI.Xaml; +#endif + +namespace ReactiveUI +{ + /// + /// This type convert converts between Boolean and XAML Visibility - the + /// conversionHint is a BooleanToVisibilityHint. + /// + public class BooleanToVisibilityTypeConverter : IBindingTypeConverter + { + /// + public int GetAffinityForObjects(Type fromType, Type toType) + { + if (fromType == typeof(bool) && toType == typeof(Visibility)) + { + return 10; + } + + if (fromType == typeof(Visibility) && toType == typeof(bool)) + { + return 10; + } + + return 0; + } + + /// + public bool TryConvert(object? from, Type toType, object? conversionHint, out object result) + { + var hint = conversionHint is BooleanToVisibilityHint visibilityHint ? + visibilityHint : + BooleanToVisibilityHint.None; + + if (toType == typeof(Visibility) && from is bool fromBool) + { + var fromAsBool = (hint & BooleanToVisibilityHint.Inverse) != 0 ? !fromBool : fromBool; + +#if !NETFX_CORE && !HAS_UNO && !HAS_WINUI + var notVisible = (hint & BooleanToVisibilityHint.UseHidden) != 0 ? Visibility.Hidden : Visibility.Collapsed; +#else + var notVisible = Visibility.Collapsed; +#endif + result = fromAsBool ? Visibility.Visible : notVisible; + return true; + } + + if (from is Visibility fromAsVis) + { + result = fromAsVis == Visibility.Visible ^ (hint & BooleanToVisibilityHint.Inverse) == 0; + } + else + { + result = Visibility.Visible; + } + + return true; + } + } +} diff --git a/src/ReactiveUI.Maui/Common/PlatformOperations.cs b/src/ReactiveUI.Maui/Common/PlatformOperations.cs new file mode 100644 index 0000000000..24d3197c52 --- /dev/null +++ b/src/ReactiveUI.Maui/Common/PlatformOperations.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2023 .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. + +namespace ReactiveUI +{ + /// + /// Returns the current orientation of the device on Windows. + /// + public class PlatformOperations : IPlatformOperations + { + /// + public string? GetOrientation() => null; + } +} diff --git a/src/ReactiveUI.Maui/Common/ReactivePage.cs b/src/ReactiveUI.Maui/Common/ReactivePage.cs new file mode 100644 index 0000000000..7a257ff899 --- /dev/null +++ b/src/ReactiveUI.Maui/Common/ReactivePage.cs @@ -0,0 +1,137 @@ +// Copyright (c) 2023 .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. + +#if HAS_MAUI +using Microsoft.Maui.Controls; +#endif +#if HAS_WINUI +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +#endif + +namespace ReactiveUI +{ + /// + /// A that is reactive. + /// + /// + /// + /// This class is a that is also reactive. That is, it implements . + /// You can extend this class to get an implementation of rather than writing one yourself. + /// + /// + /// Note that the XAML for your control must specify the same base class, including the generic argument you provide for your view + /// model. To do this, use the TypeArguments attribute as follows: + /// + /// + /// + /// + /// ]]> + /// + /// + /// + /// Note that UWP and WinUI projects do not support the TypeArguments attribute. The XAML designer window in WPF projects also does not + /// support generic types. To use in XAML documents you need to create a base class + /// where you derive from with the type argument filled in. + /// + /// { /* No code needed here */ } + /// + /// public partial class YourView : YourViewBase + /// { + /// /* Your code */ + /// } + /// ]]> + /// + /// Then you can use this base class as root in your XAML document. + /// + /// + /// + /// + /// ]]> + /// + /// + /// + /// + /// The type of the view model backing the view. + /// + public + class ReactivePage : + Page, IViewFor + where TViewModel : class + { +#if HAS_WINUI + /// + /// The view model dependency property. + /// + public static readonly DependencyProperty ViewModelProperty = + DependencyProperty.Register( + "ViewModel", + typeof(TViewModel), + typeof(ReactivePage), + new PropertyMetadata(null)); +#else + /// + /// The view model bindable property. + /// + public static readonly BindableProperty ViewModelProperty = BindableProperty.Create( + nameof(ViewModel), + typeof(TViewModel), + typeof(ReactivePage), + default(TViewModel), + BindingMode.OneWay, + propertyChanged: OnViewModelChanged); +#endif + + /// + /// Gets the binding root view model. + /// + public TViewModel? BindingRoot => ViewModel; + + /// + public TViewModel? ViewModel + { + get => (TViewModel)GetValue(ViewModelProperty); + set => SetValue(ViewModelProperty, value); + } + + /// + object? IViewFor.ViewModel + { + get => ViewModel; + set => ViewModel = (TViewModel?)value; + } + +#if HAS_MAUI + /// + protected override void OnBindingContextChanged() + { + base.OnBindingContextChanged(); + ViewModel = BindingContext as TViewModel; + } + + private static void OnViewModelChanged(BindableObject bindableObject, object oldValue, object newValue) => bindableObject.BindingContext = newValue; +#endif + } +} diff --git a/src/ReactiveUI.Maui/Common/ReactiveUserControl.cs b/src/ReactiveUI.Maui/Common/ReactiveUserControl.cs new file mode 100644 index 0000000000..b8c907171b --- /dev/null +++ b/src/ReactiveUI.Maui/Common/ReactiveUserControl.cs @@ -0,0 +1,113 @@ +// Copyright (c) 2023 .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. + +#if HAS_WINUI +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace ReactiveUI +{ + /// + /// A that is reactive. + /// + /// + /// + /// This class is a that is also reactive. That is, it implements . + /// You can extend this class to get an implementation of rather than writing one yourself. + /// + /// + /// Note that the XAML for your control must specify the same base class, including the generic argument you provide for your view + /// model. To do this, use the TypeArguments attribute as follows: + /// + /// + /// + /// + /// ]]> + /// + /// + /// + /// Note that UWP and WinUI projects do not support the TypeArguments attribute. The XAML designer window in WPF projects also does not + /// support generic types. To use in XAML documents you need to create a base class + /// where you derive from with the type argument filled in. + /// + /// { /* No code needed here */ } + /// + /// public partial class YourView : YourViewBase + /// { + /// /* Your code */ + /// } + /// ]]> + /// + /// Then you can use this base class as root in your XAML document. + /// + /// + /// + /// + /// ]]> + /// + /// + /// + /// + /// The type of the view model backing the view. + /// + [SuppressMessage("Design", "CA1010:Collections should implement generic interface", Justification = "Deliberate usage")] + public + class ReactiveUserControl : + UserControl, IViewFor + where TViewModel : class + { + /// + /// The view model dependency property. + /// + public static readonly DependencyProperty ViewModelProperty = + DependencyProperty.Register( + "ViewModel", + typeof(TViewModel), + typeof(ReactiveUserControl), + new PropertyMetadata(null)); + + /// + /// Gets the binding root view model. + /// + public TViewModel? BindingRoot => ViewModel; + + /// + public TViewModel? ViewModel + { + get => (TViewModel)GetValue(ViewModelProperty); + set => SetValue(ViewModelProperty, value); + } + + /// + object? IViewFor.ViewModel + { + get => ViewModel; + set => ViewModel = (TViewModel?)value; + } + } +} +#endif diff --git a/src/ReactiveUI.Maui/Common/RoutedViewHost.cs b/src/ReactiveUI.Maui/Common/RoutedViewHost.cs new file mode 100644 index 0000000000..7faf999d5c --- /dev/null +++ b/src/ReactiveUI.Maui/Common/RoutedViewHost.cs @@ -0,0 +1,161 @@ +// Copyright (c) 2023 .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. + +#if HAS_WINUI +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reactive.Linq; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using ReactiveUI; +using Splat; + +namespace ReactiveUI +{ + /// + /// This control hosts the View associated with a Router, and will display + /// the View and wire up the ViewModel whenever a new ViewModel is + /// navigated to. Put this control as the only control in your Window. + /// + public + class RoutedViewHost : TransitioningContentControl, IActivatableView, IEnableLogger + { + /// + /// The router dependency property. + /// + public static readonly DependencyProperty RouterProperty = + DependencyProperty.Register("Router", typeof(RoutingState), typeof(RoutedViewHost), new PropertyMetadata(null)); + + /// + /// The default content property. + /// + public static readonly DependencyProperty DefaultContentProperty = + DependencyProperty.Register("DefaultContent", typeof(object), typeof(RoutedViewHost), new PropertyMetadata(null)); + + /// + /// The view contract observable property. + /// + public static readonly DependencyProperty ViewContractObservableProperty = + DependencyProperty.Register("ViewContractObservable", typeof(IObservable), typeof(RoutedViewHost), new PropertyMetadata(Observable.Default)); + + private string? _viewContract; + + /// + /// Initializes a new instance of the class. + /// + public RoutedViewHost() + { + HorizontalContentAlignment = HorizontalAlignment.Stretch; + VerticalContentAlignment = VerticalAlignment.Stretch; + + var platform = Locator.Current.GetService(); + Func platformGetter = () => default; + + if (platform is null) + { + // NB: This used to be an error but WPF design mode can't read + // good or do other stuff good. + this.Log().Error("Couldn't find an IPlatformOperations implementation. Please make sure you have installed the latest version of the ReactiveUI packages for your platform. See https://reactiveui.net/docs/getting-started/installation for guidance."); + } + else + { + platformGetter = () => platform.GetOrientation(); + } + + ViewContractObservable = ModeDetector.InUnitTestRunner() + ? Observable.Never + : Observable.FromEvent( + eventHandler => + { + void Handler(object sender, SizeChangedEventArgs e) => eventHandler(platformGetter()); + return Handler; + }, + x => SizeChanged += x, + x => SizeChanged -= x) + .StartWith(platformGetter()) + .DistinctUntilChanged(); + + IRoutableViewModel? currentViewModel = null; + var vmAndContract = this.WhenAnyObservable(x => x.Router.CurrentViewModel).Do(x => currentViewModel = x).StartWith(currentViewModel).CombineLatest( + this.WhenAnyObservable(x => x.ViewContractObservable).Do(x => _viewContract = x).StartWith(ViewContract), + (viewModel, contract) => (viewModel, contract)); + + this.WhenActivated(d => + { + // NB: The DistinctUntilChanged is useful because most views in + // WinRT will end up getting here twice - once for configuring + // the RoutedViewHost's ViewModel, and once on load via SizeChanged + d(vmAndContract.DistinctUntilChanged<(IRoutableViewModel? viewModel, string? contract)>().Subscribe( + ResolveViewForViewModel, + ex => RxApp.DefaultExceptionHandler.OnNext(ex))); + }); + } + + /// + /// Gets or sets the of the view model stack. + /// + public RoutingState Router + { + get => (RoutingState)GetValue(RouterProperty); + set => SetValue(RouterProperty, value); + } + + /// + /// Gets or sets the content displayed whenever there is no page currently + /// routed. + /// + public object DefaultContent + { + get => GetValue(DefaultContentProperty); + set => SetValue(DefaultContentProperty, value); + } + + /// + /// Gets or sets the view contract observable. + /// + /// + /// The view contract observable. + /// + public IObservable ViewContractObservable + { + get => (IObservable)GetValue(ViewContractObservableProperty); + set => SetValue(ViewContractObservableProperty, value); + } + + /// + /// Gets or sets the view contract. + /// + public string? ViewContract + { + get => _viewContract; + set => ViewContractObservable = Observable.Return(value); + } + + /// + /// Gets or sets the view locator. + /// + /// + /// The view locator. + /// + public IViewLocator? ViewLocator { get; set; } + + private void ResolveViewForViewModel((IRoutableViewModel? viewModel, string? contract) x) + { + if (x.viewModel is null) + { + Content = DefaultContent; + return; + } + + var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current; + var view = (viewLocator.ResolveView(x.viewModel, x.contract) ?? viewLocator.ResolveView(x.viewModel)) ?? throw new Exception($"Couldn't find view for '{x.viewModel}'."); + view.ViewModel = x.viewModel; + Content = view; + } + } +} +#endif diff --git a/src/ReactiveUI.Maui/Common/ViewModelViewHost.cs b/src/ReactiveUI.Maui/Common/ViewModelViewHost.cs new file mode 100644 index 0000000000..2d92165214 --- /dev/null +++ b/src/ReactiveUI.Maui/Common/ViewModelViewHost.cs @@ -0,0 +1,158 @@ +// Copyright (c) 2023 .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. + +#if HAS_WINUI +using System; +using System.Diagnostics.CodeAnalysis; +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Splat; + +namespace ReactiveUI +{ + /// + /// This content control will automatically load the View associated with + /// the ViewModel property and display it. This control is very useful + /// inside a DataTemplate to display the View associated with a ViewModel. + /// + public + class ViewModelViewHost : TransitioningContentControl, IViewFor, IEnableLogger + { + /// + /// The default content dependency property. + /// + public static readonly DependencyProperty DefaultContentProperty = + DependencyProperty.Register(nameof(DefaultContent), typeof(object), typeof(ViewModelViewHost), new PropertyMetadata(null)); + + /// + /// The view model dependency property. + /// + public static readonly DependencyProperty ViewModelProperty = + DependencyProperty.Register(nameof(ViewModel), typeof(object), typeof(ViewModelViewHost), new PropertyMetadata(null)); + + /// + /// The view contract observable dependency property. + /// + public static readonly DependencyProperty ViewContractObservableProperty = + DependencyProperty.Register(nameof(ViewContractObservable), typeof(IObservable), typeof(ViewModelViewHost), new PropertyMetadata(Observable.Default)); + + private string? _viewContract; + + /// + /// Initializes a new instance of the class. + /// + public ViewModelViewHost() + { + var platform = Locator.Current.GetService(); + Func platformGetter = () => default; + + if (platform is null) + { + // NB: This used to be an error but WPF design mode can't read + // good or do other stuff good. + this.Log().Error("Couldn't find an IPlatformOperations implementation. Please make sure you have installed the latest version of the ReactiveUI packages for your platform. See https://reactiveui.net/docs/getting-started/installation for guidance."); + } + else + { + platformGetter = () => platform.GetOrientation(); + } + + ViewContractObservable = ModeDetector.InUnitTestRunner() + ? Observable.Never + : Observable.FromEvent( + eventHandler => + { + void Handler(object? sender, SizeChangedEventArgs e) => eventHandler(platformGetter()!); + return Handler; + }, + x => SizeChanged += x, + x => SizeChanged -= x) + .StartWith(platformGetter()) + .DistinctUntilChanged(); + + var contractChanged = this.WhenAnyObservable(x => x.ViewContractObservable).Do(x => _viewContract = x).StartWith(ViewContract); + var viewModelChanged = this.WhenAnyValue(x => x.ViewModel).StartWith(ViewModel); + var vmAndContract = contractChanged + .CombineLatest(viewModelChanged, (contract, vm) => (ViewModel: vm, Contract: contract)); + + this.WhenActivated(d => + { + d(contractChanged + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(x => _viewContract = x ?? string.Empty)); + + d(vmAndContract.DistinctUntilChanged().Subscribe(x => ResolveViewForViewModel(x.ViewModel, x.Contract))); + }); + } + + /// + /// Gets or sets the view contract observable. + /// + public IObservable ViewContractObservable + { + get => (IObservable)GetValue(ViewContractObservableProperty); + set => SetValue(ViewContractObservableProperty, value); + } + + /// + /// Gets or sets the content displayed by default when no content is set. + /// + public object DefaultContent + { + get => GetValue(DefaultContentProperty); + set => SetValue(DefaultContentProperty, value); + } + + /// + /// Gets or sets the ViewModel to display. + /// + public object? ViewModel + { + get => GetValue(ViewModelProperty); + set => SetValue(ViewModelProperty, value); + } + + /// + /// Gets or sets the view contract. + /// + public string? ViewContract + { + get => _viewContract; + set => ViewContractObservable = Observable.Return(value); + } + + /// + /// Gets or sets the view locator. + /// + public IViewLocator? ViewLocator { get; set; } + + private void ResolveViewForViewModel(object? viewModel, string? contract) + { + if (viewModel is null) + { + Content = DefaultContent; + return; + } + + var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current; + var viewInstance = viewLocator.ResolveView(viewModel, contract) ?? viewLocator.ResolveView(viewModel); + + if (viewInstance is null) + { + Content = DefaultContent; + this.Log().Warn($"The {nameof(ViewModelViewHost)} could not find a valid view for the view model of type {viewModel.GetType()} and value {viewModel}."); + return; + } + + viewInstance.ViewModel = viewModel; + + Content = viewInstance; + } + } +} +#endif diff --git a/src/ReactiveUI.Maui/ReactiveUI.Maui.csproj b/src/ReactiveUI.Maui/ReactiveUI.Maui.csproj index c6bab6bdc5..9f6c332002 100644 --- a/src/ReactiveUI.Maui/ReactiveUI.Maui.csproj +++ b/src/ReactiveUI.Maui/ReactiveUI.Maui.csproj @@ -1,35 +1,29 @@ - + - - net6.0;net7.0 - $(TargetFrameworks);net6.0-windows10.0.19041.0;net7.0-windows10.0.19041.0 - Contains the ReactiveUI platform specific extensions for Microsoft Maui - mvvm;reactiveui;rx;reactive extensions;observable;LINQ;events;frp;maui;android;ios;mac;forms;net - true - + + net6.0;net7.0 + $(TargetFrameworks);net6.0-windows10.0.19041.0;net7.0-windows10.0.19041.0 + Contains the ReactiveUI platform specific extensions for Microsoft Maui + mvvm;reactiveui;rx;reactive extensions;observable;LINQ;events;frp;maui;android;ios;mac;forms;net + true + - - HAS_WINUI - - - HAS_MAUI - + + HAS_WINUI + + + HAS_MAUI + - - - + + - - - - - - - - - - - + + + + + + diff --git a/src/ReactiveUI.Maui/Registrations.cs b/src/ReactiveUI.Maui/Registrations.cs index ec4cc2f5b3..4e466557cc 100644 --- a/src/ReactiveUI.Maui/Registrations.cs +++ b/src/ReactiveUI.Maui/Registrations.cs @@ -8,9 +8,12 @@ #if HAS_WINUI using System.Reactive.Concurrency; using Splat; -#endif +namespace ReactiveUI.WinUI; +#endif +#if HAS_MAUI namespace ReactiveUI.Maui; +#endif /// /// The main registration for common classes for the Splat dependency injection. @@ -35,6 +38,7 @@ public void Register(Action, Type> registerFunction) registerFunction(() => new BooleanToVisibilityTypeConverter(), typeof(IBindingTypeConverter)); #if HAS_WINUI + registerFunction(() => new PlatformOperations(), typeof(IPlatformOperations)); registerFunction(() => new DependencyObjectObservableForProperty(), typeof(ICreatesObservableForProperty)); registerFunction(() => new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); registerFunction(() => new ComponentModelTypeConverter(), typeof(IBindingTypeConverter)); diff --git a/src/ReactiveUI.Maui/RoutedViewHost.cs b/src/ReactiveUI.Maui/RoutedViewHost.cs index e122cd95e2..7ee116a3ab 100644 --- a/src/ReactiveUI.Maui/RoutedViewHost.cs +++ b/src/ReactiveUI.Maui/RoutedViewHost.cs @@ -4,12 +4,12 @@ // See the LICENSE file in the project root for full license information. using System; -using System.Diagnostics.CodeAnalysis; using System.Reactive; using System.Reactive.Concurrency; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reflection; +using System.Threading.Tasks; using Microsoft.Maui.Controls; using Splat; @@ -20,7 +20,6 @@ namespace ReactiveUI.Maui; /// /// /// -[SuppressMessage("Readability", "RCS1090: Call 'ConfigureAwait(false)", Justification = "This class interacts with the UI thread.")] public class RoutedViewHost : NavigationPage, IActivatableView, IEnableLogger { /// @@ -47,7 +46,7 @@ public class RoutedViewHost : NavigationPage, IActivatableView, IEnableLogger /// You *must* register an IScreen class representing your App's main Screen. public RoutedViewHost() { - this.WhenActivated(disposable => + this.WhenActivated(async disposable => { var currentlyNavigating = false; @@ -66,7 +65,7 @@ public RoutedViewHost() } InvalidateCurrentViewModel(); - SyncNavigationStacks(); + await SyncNavigationStacksAsync(); }) .DisposeWith(disposable); @@ -93,7 +92,7 @@ public RoutedViewHost() currentlyNavigating = false; } - SyncNavigationStacks(); + await SyncNavigationStacksAsync(); return page; }) @@ -115,8 +114,10 @@ public RoutedViewHost() .Where(_ => !currentlyNavigating && Router is not null) .Subscribe(_ => { - - Router!.NavigationStack.RemoveAt(Router.NavigationStack.Count - 1); + if (Router?.NavigationStack.Count > 0) + { + Router.NavigationStack.RemoveAt(Router.NavigationStack.Count - 1); + } InvalidateCurrentViewModel(); }) @@ -137,22 +138,21 @@ public RoutedViewHost() .Where(_ => !currentlyNavigating && Router is not null) .Subscribe(_ => { - for (var i = Router!.NavigationStack.Count - 1; i > 0; i--) + for (var i = Router?.NavigationStack.Count - 1; i > 0; i--) { - Router.NavigationStack.RemoveAt(i); + if (i.HasValue) + { + Router?.NavigationStack.RemoveAt(i.Value); + } } InvalidateCurrentViewModel(); }) .DisposeWith(disposable); + await SyncNavigationStacksAsync(); }); - var screen = Locator.Current.GetService(); - if (screen is null) - { - throw new Exception("You *must* register an IScreen class representing your App's main Screen"); - } - + var screen = Locator.Current.GetService() ?? throw new Exception("You *must* register an IScreen class representing your App's main Screen"); Router = screen.Router; } @@ -179,7 +179,6 @@ public bool SetTitleOnNavigate /// /// The vm. /// An observable of the page associated to a . - [SuppressMessage("Design", "CA1822: Can be made static", Justification = "Might be used by implementors.")] protected virtual IObservable PagesForViewModel(IRoutableViewModel? vm) { if (vm is null) @@ -211,7 +210,6 @@ protected virtual IObservable PagesForViewModel(IRoutableViewModel? vm) /// /// The vm. /// An observable of the page associated to a . - [SuppressMessage("Design", "CA1822: Can be made static", Justification = "Might be used by implementors.")] protected virtual Page PageForViewModel(IRoutableViewModel vm) { if (vm is null) @@ -256,22 +254,38 @@ protected void InvalidateCurrentViewModel() /// Syncs page's navigation stack with /// to affect manipulations like Add or Clear. /// - protected void SyncNavigationStacks() + /// A representing the asynchronous operation. + protected async Task SyncNavigationStacksAsync() { if (Navigation.NavigationStack.Count != Router.NavigationStack.Count || StacksAreDifferent()) { - for (var i = Navigation.NavigationStack.Count - 2; i >= 0; i--) + if (Navigation.NavigationStack.Count > 2) { - Navigation.RemovePage(Navigation.NavigationStack[i]); + for (var i = Navigation.NavigationStack.Count - 2; i >= 0; i--) + { + Navigation.RemovePage(Navigation.NavigationStack[i]); + } } - var rootPage = Navigation.NavigationStack[0]; + Page? rootPage; + if (Navigation.NavigationStack.Count >= 1) + { + rootPage = Navigation.NavigationStack[0]; + } + else + { + rootPage = PageForViewModel(Router.NavigationStack[0]); + await Navigation.PushAsync(rootPage, false); + } - for (var i = 0; i < Router.NavigationStack.Count - 1; i++) + if (Router.NavigationStack.Count >= 1) { - var page = PageForViewModel(Router.NavigationStack[i]); - Navigation.InsertPageBefore(page, rootPage); + for (var i = 0; i < Router.NavigationStack.Count - 1; i++) + { + var page = PageForViewModel(Router.NavigationStack[i]); + Navigation.InsertPageBefore(page, rootPage); + } } } } diff --git a/src/ReactiveUI.WinUI/DependencyObjectObservableForProperty.cs b/src/ReactiveUI.Maui/WinUI/DependencyObjectObservableForProperty.cs similarity index 98% rename from src/ReactiveUI.WinUI/DependencyObjectObservableForProperty.cs rename to src/ReactiveUI.Maui/WinUI/DependencyObjectObservableForProperty.cs index 452334d436..ba9c6af040 100644 --- a/src/ReactiveUI.WinUI/DependencyObjectObservableForProperty.cs +++ b/src/ReactiveUI.Maui/WinUI/DependencyObjectObservableForProperty.cs @@ -3,7 +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. -#if !HAS_MAUI +#if HAS_WINUI using System; using System.Globalization; using System.Linq.Expressions; @@ -13,11 +13,7 @@ using Microsoft.UI.Xaml; using Splat; -#if HAS_UNO -namespace ReactiveUI.Uno -#else namespace ReactiveUI -#endif { /// /// Creates a observable for a property if available that is based on a DependencyProperty. diff --git a/src/ReactiveUI.WinUI/DispatcherQueueScheduler.cs b/src/ReactiveUI.Maui/WinUI/DispatcherQueueScheduler.cs similarity index 100% rename from src/ReactiveUI.WinUI/DispatcherQueueScheduler.cs rename to src/ReactiveUI.Maui/WinUI/DispatcherQueueScheduler.cs diff --git a/src/ReactiveUI.WinUI/TransitioningContentControl.Empty.cs b/src/ReactiveUI.Maui/WinUI/TransitioningContentControl.Empty.cs similarity index 95% rename from src/ReactiveUI.WinUI/TransitioningContentControl.Empty.cs rename to src/ReactiveUI.Maui/WinUI/TransitioningContentControl.Empty.cs index 76b6e13343..7034159adb 100644 --- a/src/ReactiveUI.WinUI/TransitioningContentControl.Empty.cs +++ b/src/ReactiveUI.Maui/WinUI/TransitioningContentControl.Empty.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. +#if HAS_WINUI using System.Diagnostics.CodeAnalysis; using Microsoft.UI.Xaml.Controls; @@ -13,4 +14,5 @@ namespace ReactiveUI; /// public class TransitioningContentControl : ContentControl { -} \ No newline at end of file +} +#endif diff --git a/src/ReactiveUI.WinUI/ActivationForViewFetcher.cs b/src/ReactiveUI.WinUI/ActivationForViewFetcher.cs deleted file mode 100644 index dc2ac0cf1f..0000000000 --- a/src/ReactiveUI.WinUI/ActivationForViewFetcher.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) 2023 .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.Linq; -using System.Reactive.Linq; -using System.Reflection; -using Microsoft.UI.Xaml; -using Windows.Foundation; - -namespace ReactiveUI.WinUI; - -/// -/// ActiveationForViewFetcher is how ReactiveUI determine when a -/// View is activated or deactivated. This is usually only used when porting -/// ReactiveUI to a new UI framework. -/// -public class ActivationForViewFetcher : IActivationForViewFetcher -{ - /// - public int GetAffinityForView(Type view) => typeof(FrameworkElement).GetTypeInfo().IsAssignableFrom(view.GetTypeInfo()) ? 10 : 0; - - /// - public IObservable GetActivationForView(IActivatableView view) - { - if (view is not FrameworkElement fe) - { - return Observable.Empty; - } - - var viewLoaded = Observable.FromEvent, bool>( - eventHandler => - { - void Handler(FrameworkElement sender, object e) => eventHandler(true); - return Handler; - }, - x => fe.Loading += x, - x => fe.Loading -= x); - - var viewUnloaded = Observable.FromEvent( - eventHandler => - { - void Handler(object sender, RoutedEventArgs e) => eventHandler(false); - return Handler; - }, - x => fe.Unloaded += x, - x => fe.Unloaded -= x); - - return viewLoaded - .Merge(viewUnloaded) - .Select(b => b ? fe.WhenAnyValue(x => x.IsHitTestVisible).SkipWhile(x => !x) : Observables.False) - .Switch() - .DistinctUntilChanged(); - } -} \ No newline at end of file diff --git a/src/ReactiveUI.WinUI/ReactiveUI.WinUI.csproj b/src/ReactiveUI.WinUI/ReactiveUI.WinUI.csproj index 82324a2619..a69bb5d727 100644 --- a/src/ReactiveUI.WinUI/ReactiveUI.WinUI.csproj +++ b/src/ReactiveUI.WinUI/ReactiveUI.WinUI.csproj @@ -1,4 +1,4 @@ - + net6.0-windows10.0.17763.0;net7.0-windows10.0.17763.0 10.0.17763.0 @@ -10,21 +10,17 @@ - + - - - + + + + diff --git a/src/ReactiveUI.WinUI/Registrations.cs b/src/ReactiveUI.WinUI/Registrations.cs deleted file mode 100644 index d47d741d88..0000000000 --- a/src/ReactiveUI.WinUI/Registrations.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) 2023 .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.Concurrency; -using ReactiveUI; -using Splat; - -namespace ReactiveUI.WinUI; - -/// -/// Registrations specific to the WPF platform. -/// -public class Registrations : IWantsToRegisterStuff -{ - /// - public void Register(Action, Type> registerFunction) - { - if (registerFunction is null) - { - throw new ArgumentNullException(nameof(registerFunction)); - } - - registerFunction(() => new PlatformOperations(), typeof(IPlatformOperations)); - registerFunction(() => new ActivationForViewFetcher(), typeof(IActivationForViewFetcher)); - registerFunction(() => new DependencyObjectObservableForProperty(), typeof(ICreatesObservableForProperty)); - registerFunction(() => new BooleanToVisibilityTypeConverter(), typeof(IBindingTypeConverter)); - registerFunction(() => new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); - registerFunction(() => new ComponentModelTypeConverter(), typeof(IBindingTypeConverter)); - - if (!ModeDetector.InUnitTestRunner()) - { - // NB: On .NET Core, trying to touch DispatcherScheduler blows up :cry: - RxApp.MainThreadScheduler = new WaitForDispatcherScheduler(() => DispatcherQueueScheduler.Current); - RxApp.TaskpoolScheduler = TaskPoolScheduler.Default; - } - - RxApp.SuppressViewCommandBindingMessage = true; - } -} \ No newline at end of file