Skip to content

Commit

Permalink
Fix Maui Navigation RoutedViewHost (#3574)
Browse files Browse the repository at this point in the history
  • Loading branch information
ChrisPulman committed Jul 8, 2023
1 parent 36c40d7 commit 5b186ce
Show file tree
Hide file tree
Showing 18 changed files with 899 additions and 171 deletions.
60 changes: 57 additions & 3 deletions src/ReactiveUI.Maui/ActivationForViewFetcher.cs
Expand Up @@ -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

/// <summary>
/// This class is the default implementation that determines when views are Activated and Deactivated.
Expand All @@ -19,26 +27,37 @@ public class ActivationForViewFetcher : IActivationForViewFetcher
{
/// <inheritdoc/>
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;

/// <inheritdoc/>
public IObservable<bool> 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<bool>.Never;

return activation.DistinctUntilChanged();
}

private static IObservable<bool>? GetActivationFor(ICanActivate? canActivate) => canActivate?.Activated.Select(_ => true).Merge(canActivate.Deactivated.Select(_ => false));

#if HAS_MAUI
private static IObservable<bool>? GetActivationFor(Page? page)
{
if (page is null)
Expand Down Expand Up @@ -116,4 +135,39 @@ public IObservable<bool> GetActivationForView(IActivatableView view)

return appearing.Merge(disappearing);
}
}
#endif

#if HAS_WINUI
private static IObservable<bool> GetActivationFor(FrameworkElement? view)
{
if (view is null)
{
return Observable<bool>.Empty;
}

var viewLoaded = Observable.FromEvent<TypedEventHandler<FrameworkElement, object>, bool>(
eventHandler =>
{
void Handler(FrameworkElement sender, object e) => eventHandler(true);
return Handler;
},
x => view.Loading += x,
x => view.Loading -= x);

var viewUnloaded = Observable.FromEvent<RoutedEventHandler, bool>(
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
}
81 changes: 81 additions & 0 deletions 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
{
/// <summary>
/// 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.
/// </summary>
public class AutoDataTemplateBindingHook : IPropertyBindingHook
{
/// <summary>
/// Gets the default item template.
/// </summary>
[SuppressMessage("Design", "CA1307: Use the currency locale settings", Justification = "Not available on all platforms.")]
public static Lazy<DataTemplate> DefaultItemTemplate { get; } = new(() =>
{
const string template = "<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' " +
"xmlns:xaml='clr-namespace:ReactiveUI;assembly=__ASSEMBLYNAME__'> " +
"<xaml:ViewModelViewHost ViewModel=\"{Binding Mode=OneWay}\" VerticalContentAlignment=\"Stretch\" HorizontalContentAlignment=\"Stretch\" IsTabStop=\"False\" />" +
"</DataTemplate>";
var assemblyName = typeof(AutoDataTemplateBindingHook).Assembly.FullName;
assemblyName = assemblyName?.Substring(0, assemblyName.IndexOf(','));
return (DataTemplate)XamlReader.Load(template.Replace("__ASSEMBLYNAME__", assemblyName));
});

/// <inheritdoc/>
public bool ExecuteHook(object? source, object target, Func<IObservedChange<object, object>[]> getCurrentViewModelProperties, Func<IObservedChange<object, object>[]> 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
31 changes: 31 additions & 0 deletions 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
{
/// <summary>
/// Enum that hints at the visibility of a ui element.
/// </summary>
[Flags]
public enum BooleanToVisibilityHint
{
/// <summary>
/// Do not modify the boolean type conversion from it's default action of using the Visibility.Collapsed.
/// </summary>
None = 0,

/// <summary>
/// Inverse the action of the boolean type conversion, when it's true collapse the visibility.
/// </summary>
Inverse = 1 << 1,

/// <summary>
/// Use the hidden version rather than the Collapsed.
/// </summary>
UseHidden = 1 << 2,
}
}
70 changes: 70 additions & 0 deletions 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
{
/// <summary>
/// This type convert converts between Boolean and XAML Visibility - the
/// conversionHint is a BooleanToVisibilityHint.
/// </summary>
public class BooleanToVisibilityTypeConverter : IBindingTypeConverter
{
/// <inheritdoc/>
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;
}

/// <inheritdoc/>
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;
}
}
}
16 changes: 16 additions & 0 deletions 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
{
/// <summary>
/// Returns the current orientation of the device on Windows.
/// </summary>
public class PlatformOperations : IPlatformOperations
{
/// <inheritdoc/>
public string? GetOrientation() => null;
}
}

0 comments on commit 5b186ce

Please sign in to comment.