Skip to content

Commit

Permalink
Support replacing root for MainPage, non-Shell Navigation, modals (#104)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dreamescaper authored Feb 7, 2023
1 parent 42b2146 commit f1fb4ca
Show file tree
Hide file tree
Showing 13 changed files with 339 additions and 52 deletions.
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

0 comments on commit f1fb4ca

Please sign in to comment.