Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Maui Navigation RoutedViewHost #3574

Merged
merged 2 commits into from
Jul 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 57 additions & 3 deletions src/ReactiveUI.Maui/ActivationForViewFetcher.cs
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading
Loading