Skip to content

Commit

Permalink
feat: Use CreateWindow instead of MainPage (#159)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dreamescaper authored Nov 8, 2024
1 parent 3b6e4ee commit 7a459ef
Show file tree
Hide file tree
Showing 10 changed files with 105 additions and 91 deletions.
18 changes: 16 additions & 2 deletions src/BlazorBindings.Maui/BlazorBindingsApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,19 @@ namespace BlazorBindings.Maui;
public class BlazorBindingsApplication<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>
: Application where T : IComponent
{
public BlazorBindingsApplication(IServiceProvider services)
public BlazorBindingsApplication()
{
#pragma warning disable CS0618 // Type or member is obsolete
Configure();
#pragma warning restore CS0618 // Type or member is obsolete
}

[Obsolete("Use parameterless constructor instead.")]
public BlazorBindingsApplication(IServiceProvider _) { }

protected override Window CreateWindow(Microsoft.Maui.IActivationState activationState)
{
var services = activationState?.Context?.Services ?? Handler.MauiContext.Services;
var renderer = services.GetRequiredService<MauiBlazorBindingsRenderer>();

if (WrapperComponentType != null)
Expand All @@ -18,18 +27,23 @@ public BlazorBindingsApplication(IServiceProvider services)
}

var (componentType, parameters) = GetComponentToRender();
var task = renderer.AddComponent(componentType, this, parameters);
var window = new Window();
var task = renderer.AddComponent(componentType, window, parameters);
AwaitVoid(task);

return window;

static async void AwaitVoid(Task task) => await task;
}

[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public virtual Type WrapperComponentType { get; }


/// <summary>
/// This method is executed before the rendering. It can be used to set resources, for example.
/// </summary>
[Obsolete("Configure the application in the constructor instead.")]
protected virtual void Configure() { }

private (Type ComponentType, Dictionary<string, object> Parameters) GetComponentToRender()
Expand Down
20 changes: 0 additions & 20 deletions src/BlazorBindings.Maui/Elements/Handlers/ApplicationHandler.cs

This file was deleted.

25 changes: 25 additions & 0 deletions src/BlazorBindings.Maui/Elements/Handlers/WindowHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using BlazorBindings.Maui.Extensions;
using Microsoft.Maui.Controls;
using MC = Microsoft.Maui.Controls;

namespace BlazorBindings.Maui.Elements.Handlers;

internal class WindowHandler(Window window) : IContainerElementHandler
{
public void AddChild(object child, int physicalSiblingIndex)
{
window.Page = child.Cast<MC.Page>();
}

public void ReplaceChild(int physicalSiblingIndex, object oldChild, object newChild)
{
window.Page = newChild.Cast<MC.Page>();
}

public void RemoveChild(object child, int physicalSiblingIndex)
{
window.Page = null;
}

public object TargetElement => window;
}
21 changes: 4 additions & 17 deletions src/BlazorBindings.Maui/MauiBlazorBindingsRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ internal MauiBlazorBindingsRenderer(MauiBlazorBindingsServiceProvider servicePro

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

public Task AddComponent(
internal Task AddComponent(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type componentType,
MC.Application parent,
MC.Window parent,
Dictionary<string, object> parameters = null)
{
var handler = new ApplicationHandler(parent);
var handler = new WindowHandler(parent);
var addComponentTask = AddComponent(componentType, handler, parameters);

if (addComponentTask.Exception != null)
Expand All @@ -36,30 +36,17 @@ public Task AddComponent(
ExceptionDispatchInfo.Throw(addComponentTask.Exception.InnerException);
}

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 AddComponent<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(
MC.Application parent, Dictionary<string, object> parameters = null)
{
return AddComponent(typeof(T), parent, parameters);
}

protected override void HandleException(Exception exception)
{
ExceptionDispatchInfo.Throw(exception);
}

// It tries to return the Element as soon as it is available, therefore Component task might still be in progress.
internal async Task<(object Element, Task<IComponent> Component)> GetElementFromRenderedComponent(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type componentType,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type componentType,
Dictionary<string, object> parameters = null)
{
var (elements, addComponentTask) = await GetElementsFromRenderedComponent(componentType, parameters);
Expand Down
16 changes: 11 additions & 5 deletions src/BlazorBindings.UnitTests/BlazorBindingsApplicationTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using BlazorBindings.Maui;
using BlazorBindings.UnitTests.Components;
using Microsoft.Maui;
using Microsoft.Maui.Controls;

namespace BlazorBindings.UnitTests;

Expand All @@ -19,19 +21,23 @@ public void SetsTheMainPage_WithRootWrapper()
PageContentWithCascadingParameter.ValidateContent(application.MainPage, WrapperWithCascadingValue.Value);
}

private static BlazorBindingsApplication<T> CreateApplication<T>() where T : IComponent
private static Application CreateApplication<T>() where T : IComponent
{
return new BlazorBindingsApplication<T>(TestServiceProvider.Create());
var application = TestApplication.Create<BlazorBindingsApplication<T>>();
((IApplication)application).CreateWindow(new ActivationState(application.Handler.MauiContext));
return application;
}

private static BlazorBindingsApplication<TMain> CreateApplicationWithWrapper<TMain, TWrapper>()
private static Application CreateApplicationWithWrapper<TMain, TWrapper>()
where TMain : IComponent
where TWrapper : IComponent
{
return new BlazorBindingsApplicationWithWrapper<TMain, TWrapper>(TestServiceProvider.Create());
var application = TestApplication.Create<BlazorBindingsApplicationWithWrapper<TMain, TWrapper>>();
((IApplication)application).CreateWindow(new ActivationState(application.Handler.MauiContext));
return application;
}

class BlazorBindingsApplicationWithWrapper<TMain, TWrapper>(IServiceProvider services) : BlazorBindingsApplication<TMain>(services)
class BlazorBindingsApplicationWithWrapper<TMain, TWrapper> : BlazorBindingsApplication<TMain>
where TMain : IComponent
where TWrapper : IComponent
{
Expand Down
7 changes: 5 additions & 2 deletions src/BlazorBindings.UnitTests/Elements/ElementTestBase.razor
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@
@code {
public ElementTestBase()
{
MC.Application.Current = new TestApplication();
_application = TestApplication.Create();
_renderer = (TestBlazorBindingsRenderer)_application.Handler.MauiContext.Services.GetRequiredService<MauiBlazorBindingsRenderer>();
MC.Application.Current = _application;
}

private TestBlazorBindingsRenderer _renderer = (TestBlazorBindingsRenderer)TestBlazorBindingsRenderer.Create();
private readonly MC.Application _application;
private readonly TestBlazorBindingsRenderer _renderer;
private RenderFragmentComponent _renderedComponent;

protected async Task<T> Render<T>(RenderFragment renderFragment)
Expand Down
37 changes: 15 additions & 22 deletions src/BlazorBindings.UnitTests/MauiBlazorBindingsRendererTests.cs
Original file line number Diff line number Diff line change
@@ -1,30 +1,23 @@
using BlazorBindings.UnitTests.Components;
using BlazorBindings.Maui;
using BlazorBindings.UnitTests.Components;

namespace BlazorBindings.UnitTests;

public class MauiBlazorBindingsRendererTests
{
private readonly TestBlazorBindingsRenderer _renderer = (TestBlazorBindingsRenderer)TestBlazorBindingsRenderer.Create();
private readonly MC.Application _application = new TestApplication();
private readonly TestBlazorBindingsRenderer _renderer;
private readonly MC.Window _window = new();

public MauiBlazorBindingsRendererTests()
{
MC.Application.Current = _application;
}

[Test]
public async Task RenderToApplication_PageContent()
{
await _renderer.AddComponent<PageContent>(_application);

var content = _application.MainPage;
PageContent.ValidateContent(content);
var services = TestServiceProvider.CreateMauiAppBuilder().Build().Services;
_renderer = (TestBlazorBindingsRenderer)services.GetRequiredService<MauiBlazorBindingsRenderer>();
}

[Test]
public void ShouldThrowExceptionIfHappenedDuringSyncRender()
{
void action() => _ = _renderer.AddComponent<ComponentWithException>(_application);
void action() => _ = _renderer.AddComponent(typeof(ComponentWithException), _window);

Assert.That(action, Throws.InvalidOperationException.With.Message.EqualTo("Should fail here."));
}
Expand All @@ -34,8 +27,8 @@ public async Task RendererShouldHandleAsyncExceptions()
{
_renderer.ThrowExceptions = false;

await _renderer.AddComponent<PageWithButtonWithExceptionOnClick>(_application);
var button = (MC.Button)((MC.ContentPage)_application.MainPage).Content;
await _renderer.AddComponent(typeof(PageWithButtonWithExceptionOnClick), _window);
var button = (MC.Button)((MC.ContentPage)_window.Page).Content;
button.SendClicked();

Assert.That(() => _renderer.Exceptions, Is.Not.Empty.After(1000, 10));
Expand All @@ -45,18 +38,18 @@ public async Task RendererShouldHandleAsyncExceptions()
[Test]
public async Task RenderedComponentShouldBeAbleToReplaceMainPage()
{
await _renderer.AddComponent(typeof(SwitchablePages), _application);
await _renderer.AddComponent(typeof(SwitchablePages), _window);

Assert.That(_application.MainPage.Title, Is.EqualTo("Page1"));
Assert.That(_window.Page.Title, Is.EqualTo("Page1"));

var switchButton = (MC.Button)((MC.ContentPage)_application.MainPage).Content;
var switchButton = (MC.Button)((MC.ContentPage)_window.Page).Content;
switchButton.SendClicked();

Assert.That(_application.MainPage.Title, Is.EqualTo("Page2"));
Assert.That(_window.Page.Title, Is.EqualTo("Page2"));

switchButton = (MC.Button)((MC.ContentPage)_application.MainPage).Content;
switchButton = (MC.Button)((MC.ContentPage)_window.Page).Content;
switchButton.SendClicked();

Assert.That(_application.MainPage.Title, Is.EqualTo("Page1"));
Assert.That(_window.Page.Title, Is.EqualTo("Page1"));
}
}
11 changes: 4 additions & 7 deletions src/BlazorBindings.UnitTests/Navigation/NonUriNavigationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Licensed under the MIT license.

using BlazorBindings.UnitTests.Components;
using Microsoft.Maui.Dispatching;

namespace BlazorBindings.UnitTests.Navigation;

Expand All @@ -20,13 +19,11 @@ public NonUriNavigationTests(string root)
? (MC.Page)new MC.Shell { Items = { new MC.ContentPage { Title = "Root" } } }
: new MC.NavigationPage(new MC.ContentPage { Title = "Root" });

var sp = TestServiceProvider.Create();
MC.Application.Current = new TestApplication(sp) { MainPage = mainPage };
var application = TestApplication.Create();
application.MainPage = mainPage;
MC.Application.Current = application;

var ctx = MC.Application.Current.Handler.MauiContext;
var dsp = ctx.Services.GetService<IDispatcher>();

_navigationService = sp.GetRequiredService<Maui.Navigation>();
_navigationService = application.Handler.MauiContext.Services.GetRequiredService<Maui.Navigation>();
_mauiNavigation = mainPage.Navigation;
_rootPage = _mauiNavigation.NavigationStack[0];
}
Expand Down
7 changes: 4 additions & 3 deletions src/BlazorBindings.UnitTests/Navigation/UriNavigationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ public class UriNavigationTests
public UriNavigationTests()
{
var shell = new MC.Shell { Items = { new MC.ContentPage { Title = "Root" } } };
var sp = TestServiceProvider.Create();
MC.Application.Current = new TestApplication(sp) { MainPage = shell };
_navigationService = sp.GetRequiredService<Maui.Navigation>();
var application = TestApplication.Create();
application.MainPage = shell;
MC.Application.Current = application;
_navigationService = application.Handler.MauiContext.Services.GetRequiredService<Maui.Navigation>();
_mauiNavigation = shell.Navigation;
}

Expand Down
34 changes: 21 additions & 13 deletions src/BlazorBindings.UnitTests/TestTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Maui;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Hosting;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Hosting;
using System.Runtime.ExceptionServices;
Expand All @@ -10,18 +11,30 @@

namespace BlazorBindings.UnitTests;

class TestApplication : MC.Application
static class TestApplication
{
public TestApplication(IServiceProvider serviceProvider = null)
class ApplicationFromTestAssembly : MC.Application;

public static MC.Application Create() => Create<ApplicationFromTestAssembly>();

public static T Create<T>() where T : MC.Application, new()
{
serviceProvider ??= TestServiceProvider.Create();
Handler = new TestHandler
var mauiApp = TestServiceProvider
.CreateMauiAppBuilder()
.UseMauiApp<T>()
.Build();

var application = mauiApp.Services.GetRequiredService<IApplication>();

application.Handler = new TestHandler
{
MauiContext = new MauiContext(serviceProvider),
VirtualView = this
MauiContext = new MauiContext(mauiApp.Services),
VirtualView = application
};

DependencyService.RegisterSingleton(new TestSystemResources());

return (T)application;
}

class TestHandler : IElementHandler
Expand All @@ -46,7 +59,7 @@ class TestSystemResources : ISystemResourcesProvider

public static class TestServiceProvider
{
public static IServiceProvider Create()
public static MauiAppBuilder CreateMauiAppBuilder()
{
var builder = MauiApp.CreateBuilder();

Expand All @@ -56,7 +69,7 @@ public static IServiceProvider Create()
builder.UseMauiBlazorBindings();
builder.Services.AddSingleton<MauiBlazorBindingsRenderer, TestBlazorBindingsRenderer>();
builder.Services.AddSingleton<MauiDispatching.IDispatcher, TestDispatcher>();
return builder.Build().Services;
return builder;
}

class TestDispatcher : MauiDispatching.IDispatcher
Expand Down Expand Up @@ -96,11 +109,6 @@ protected override void HandleException(Exception exception)

public override Dispatcher Dispatcher => NullDispatcher.Instance;

public static MauiBlazorBindingsRenderer Create()
{
return TestServiceProvider.Create().GetRequiredService<MauiBlazorBindingsRenderer>();
}

sealed class NullDispatcher : Dispatcher
{
public static readonly Dispatcher Instance = new NullDispatcher();
Expand Down

0 comments on commit 7a459ef

Please sign in to comment.