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

Support replacing root for MainPage, non-Shell Navigation, modals #104

Merged
merged 4 commits into from
Feb 7, 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
6 changes: 5 additions & 1 deletion src/BlazorBindings.Maui/BlazorBindingsApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui.Controls;
using System;
using System.Threading.Tasks;

namespace BlazorBindings.Maui
{
Expand All @@ -10,7 +11,10 @@ public class BlazorBindingsApplication<T> : Application where T : IComponent
public BlazorBindingsApplication(IServiceProvider services)
{
var renderer = services.GetRequiredService<MauiBlazorBindingsRenderer>();
_ = renderer.AddComponent<T>(this);
var task = renderer.AddComponent(typeof(T), this);
AwaitVoid(task);

static async void AwaitVoid(Task task) => await task;
}
}
}
34 changes: 34 additions & 0 deletions src/BlazorBindings.Maui/Elements/Handlers/ApplicationHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Microsoft.Maui.Controls;
using MC = Microsoft.Maui.Controls;

namespace BlazorBindings.Maui.Elements.Handlers
{
internal class ApplicationHandler : IMauiContainerElementHandler
{
private readonly Application _application;

public ApplicationHandler(Application application)
{
_application = application;
}

public void AddChild(MC.BindableObject child, int physicalSiblingIndex)
{
_application.MainPage = (MC.Page)child;
}

public int GetChildIndex(MC.BindableObject child)
{
return Equals(_application.MainPage, child) ? 0 : -1;
}

public void RemoveChild(MC.BindableObject child)
{
// It is not allowed to have no MainPage.
}

public MC.BindableObject ElementControl => _application;
public object TargetElement => _application;
public void ApplyAttribute(ulong attributeEventHandlerId, string attributeName, object attributeValue, string attributeEventUpdatesAttributeName) { }
}
}
15 changes: 15 additions & 0 deletions src/BlazorBindings.Maui/MauiBlazorBindingsRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@ public MauiBlazorBindingsRenderer(IServiceProvider serviceProvider, ILoggerFacto

public override Dispatcher Dispatcher { get; } = new MauiDeviceDispatcher();

public Task AddComponent(Type componentType, MC.Application parent, Dictionary<string, object> parameters = null)
{
var handler = new ApplicationHandler(parent);
var addComponentTask = AddComponent(componentType, handler, parameters);

if (!addComponentTask.IsCompleted && parent is MC.Application app)
{
// MAUI requires the Application to have the MainPage. If rendering task is not completed synchronously,
// we need to set MainPage to something.
app.MainPage ??= new MC.ContentPage();
}

return addComponentTask;
}

public Task<TComponent> AddComponent<TComponent>(MC.Element parent, Dictionary<string, object> parameters = null) where TComponent : IComponent
{
var componentTask = AddComponentLocal();
Expand Down
76 changes: 35 additions & 41 deletions src/BlazorBindings.Maui/Navigation/Navigation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,62 +14,46 @@ namespace BlazorBindings.Maui
public partial class Navigation : INavigation
{
protected readonly IServiceProvider _services;
private readonly MauiBlazorBindingsRenderer _renderer;

public Navigation(IServiceProvider services)
{
_services = services;
_renderer = services.GetRequiredService<MauiBlazorBindingsRenderer>();
}

protected MC.INavigation MauiNavigation => Application.Current.MainPage.Navigation;

/// <summary>
/// Push page component <typeparamref name="T"/> to the Navigation Stack.
/// </summary>
public async Task PushAsync<T>(Dictionary<string, object> arguments = null, bool animated = true) where T : IComponent
public Task PushAsync<T>(Dictionary<string, object> arguments = null, bool animated = true) where T : IComponent
{
await NavigationAction(async () =>
{
var page = await BuildElement<Page>(typeof(T), arguments);
await MauiNavigation.PushAsync(page, animated);
});
return Navigate<T>(arguments, NavigationTarget.Navigation, animated);
}

/// <summary>
/// Push page component <typeparamref name="T"/> to the Modal Stack.
/// </summary>
public async Task PushModalAsync<T>(Dictionary<string, object> arguments = null, bool animated = true) where T : IComponent
public Task PushModalAsync<T>(Dictionary<string, object> arguments = null, bool animated = true) where T : IComponent
{
await NavigationAction(async () =>
{
var page = await BuildElement<Page>(typeof(T), arguments);
await MauiNavigation.PushModalAsync(page, animated);
});
return Navigate<T>(arguments, NavigationTarget.Modal, animated);
}

/// <summary>
/// Push page component from the <paramref name="renderFragment"/> to the Modal Stack.
/// </summary>
/// <remarks>Experimental API, subject to change.</remarks>
public async Task PushModalAsync(RenderFragment renderFragment, bool animated = true)
public Task PushModalAsync(RenderFragment renderFragment, bool animated = true)
{
await NavigationAction(async () =>
{
var page = await BuildElement<Page>(renderFragment);
await MauiNavigation.PushModalAsync(page, animated);
});
return Navigate(renderFragment, NavigationTarget.Modal, animated);
}

/// <summary>
/// Push page component from the <paramref name="renderFragment"/> to the Navigation Stack.
/// </summary>
/// <remarks>Experimental API, subject to change.</remarks>
public async Task PushAsync(RenderFragment renderFragment, bool animated = true)
public Task PushAsync(RenderFragment renderFragment, bool animated = true)
{
await NavigationAction(async () =>
{
var page = await BuildElement<Page>(renderFragment);
await MauiNavigation.PushAsync(page, animated);
});
return Navigate(renderFragment, NavigationTarget.Navigation, animated);
}

public async Task PopModalAsync(bool animated = true)
Expand All @@ -84,21 +68,7 @@ public async Task PopAsync(bool animated = true)

public async Task PopToRootAsync(bool animated = true)
{
await NavigationAction(() => MauiNavigation.PopToRootAsync(animated));
}

/// <summary>
/// Returns rendered MAUI element from <paramref name="renderFragment"/>.
/// This method is exposed for extensibility purposes, and shouldn't be used directly.
/// </summary>
/// <remarks>Experimental API, subject to change.</remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
public Task<T> BuildElement<T>(RenderFragment renderFragment) where T : Element
{
return BuildElement<T>(typeof(RenderFragmentComponent), new()
{
[nameof(RenderFragmentComponent.RenderFragment)] = renderFragment
});
await MauiNavigation.PopToRootAsync(animated);
}

/// <summary>
Expand Down Expand Up @@ -131,6 +101,30 @@ async void DisposeScopeWhenParentRemoved(object _, EventArgs __)
}
}

private Task Navigate(RenderFragment renderFragment, NavigationTarget target, bool animated)
{
return Navigate<RenderFragmentComponent>(new()
{
[nameof(RenderFragmentComponent.RenderFragment)] = renderFragment
}, target, animated);
}

private async Task Navigate<T>(Dictionary<string, object> arguments, NavigationTarget target, bool animated) where T : IComponent
{
await NavigationAction(() =>
{
var navigationHandler = new NavigationHandler(MauiNavigation, target, animated);
var renderTask = _renderer.AddComponent(typeof(T), navigationHandler, arguments);

navigationHandler.PageClosed += async () =>
{
_renderer.RemoveRootComponent(await renderTask);
};

return Task.WhenAny(renderTask, navigationHandler.WaitForNavigation());
});
}

static bool _navigationInProgress;
static async Task NavigationAction(Func<Task> action)
{
Expand Down
94 changes: 94 additions & 0 deletions src/BlazorBindings.Maui/Navigation/NavigationHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using MC = Microsoft.Maui.Controls;

namespace BlazorBindings.Maui
{
internal class NavigationHandler : IMauiContainerElementHandler
{
private readonly NavigationTarget _target;
private readonly MC.INavigation _navigation;
private readonly bool _animated;
private readonly TaskCompletionSource _taskCompletionSource = new();
private MC.Page _currentPage;
private bool _firstAdd = true;

public NavigationHandler(MC.INavigation navigation, NavigationTarget target, bool animated)
{
_target = target;
_navigation = navigation;
_animated = animated;
}

public Task WaitForNavigation() => _taskCompletionSource.Task;
public event Action PageClosed;

public async Task AddChildAsync(MC.Page child)
{
// The order of AddChild and RemoveChild is undetermined. We need to make sure that the previous page is removed.
if (_currentPage != null)
await RemoveChildAsync(_currentPage);

_currentPage = child;

if (_target == NavigationTarget.Modal)
{
await _navigation.PushModalAsync(child, _firstAdd && _animated);
}
else
{
await _navigation.PushAsync(child, _firstAdd && _animated);
}

_taskCompletionSource.TrySetResult();
_firstAdd = false;

child.ParentChanged += ParentChanged;
}

public async Task RemoveChildAsync(MC.Page child)
{
child.ParentChanged -= ParentChanged;
if (_target == NavigationTarget.Modal)
{
if (_navigation.ModalStack.LastOrDefault() == child)
{
await _navigation.PopModalAsync(animated: false);
}
}
else
{
if (_navigation.NavigationStack.Contains(child))
_navigation.RemovePage(child);
}
}

private void ParentChanged(object sender, EventArgs e)
{
var page = sender as MC.Page;

if (page == _currentPage && page.Parent == null)
{
PageClosed?.Invoke();
}

page.ParentChanged -= ParentChanged;
}

public async void RemoveChild(MC.BindableObject child)
{
await RemoveChildAsync((MC.Page)child);
}

public async void AddChild(MC.BindableObject child, int physicalSiblingIndex)
{
await AddChildAsync((MC.Page)child);
}

public int GetChildIndex(MC.BindableObject child) => -1;
public void ApplyAttribute(ulong attributeEventHandlerId, string attributeName, object attributeValue, string attributeEventUpdatesAttributeName) { }
public MC.BindableObject ElementControl => null;
public object TargetElement => null;
}
}
7 changes: 7 additions & 0 deletions src/BlazorBindings.Maui/Navigation/NavigationTarget.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace BlazorBindings.Maui
{
internal enum NavigationTarget
{
Navigation, Modal
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
@implements IDisposable

<ContentPage Title="Test">
<ContentPage @ref="_page" Title="Test">
<NonPageContent />
</ContentPage>

@code {
public static event Action OnDispose;
ContentPage _page;

public event Action OnDispose;

protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
// This is needed to be able to get component from tests.
_page.NativeControl.SetValue(TestProperties.ComponentProperty, this);
}
}

public void Dispose()
{
Expand Down
23 changes: 21 additions & 2 deletions src/BlazorBindings.UnitTests/Components/PageWithUrl.razor
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
@page "/test/path/{Title}"
@implements IDisposable
@page "/test/path/{Title}"

<ContentPage Title="@Title">
<ContentPage @ref="_page" Title="@Title">
<NonPageContent />
</ContentPage>

@code {
ContentPage _page;

[Parameter] public string Title { get; set; }
public event Action OnDispose;

protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
// This is needed to be able to get component from tests.
_page.NativeControl.SetValue(TestProperties.ComponentProperty, this);
}
}

public void Dispose()
{
OnDispose?.Invoke();
OnDispose = null;
}

public static void ValidateContent(MC.Element content)
{
Expand Down
17 changes: 17 additions & 0 deletions src/BlazorBindings.UnitTests/Components/SwitchablePages.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@if (_switch)
{
<ContentPage Title="Page1">
<Button OnClick="Switch" />
</ContentPage>
}
else
{
<ContentPage Title="Page2">
<Button OnClick="Switch" />
</ContentPage>
}

@code {
bool _switch = true;
void Switch() => _switch = !_switch;
}
Loading