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

Add embedding sample and code #23642

Merged
merged 7 commits into from
Jul 26, 2024
Merged

Add embedding sample and code #23642

merged 7 commits into from
Jul 26, 2024

Conversation

mattleibow
Copy link
Member

@mattleibow mattleibow commented Jul 16, 2024

Description of Change

This PR adds some overloads for MAUI to embed MAUI views in a platform app.

There are a few ways to embed things after this PR, each with increasing levels of steps, but also increasing levels of developer enjoyment.

With each case here, the MauiProgram is the same as before, except that UseMauiApp is replaced with UseMauiEmbeddedApp to enable the embedding infrastructure. This is needed because unlike a traditional MAUI app, the platform application (eg: UIApplication) and IPlatformApplication needs to be registered in the service provider for the rest of MAUI to be rooted. There may be cases where this is not needed, but this will probably break tooling and other things because everything expects there to be a current and running MAUI application. In a typical MAUI app, this is done in the platform application setup code. But, in an embedded scenario, the platform application is not controlled by MAUI and is instead a user instance.

public static class MauiProgram
{
    public static MauiApp CreateMauiApp() =>
        MauiApp
            .CreateBuilder()
            .UseMauiEmbeddedApp<App>()
            .Build();
}

Simplest

For the very simplest of cases, embedding can be done without any additional code besides the construction of the app and then creating a context to get the native view:

// 1. Ensure app is built before creating MAUI views (should be a shared static)
var mauiApp = MauiProgram.CreateMauiApp();
// 2. Create MAUI embedded window context
var mauiContext = new MauiContext(mauiApp.Services);
// 3. Create MAUI views
var mauiView = new MyMauiContent();
// 4. Create platform view
var nativeView = mauiView.ToPlatform(mauiContext);
// 5. Add to the UI
RootLayout.Children.Add(nativeView);

This will work because many situations may not care if it is attached to anything in the MAUI world. This is effectively a floating view in the MAUI universe that just has access to app-specific things. Some things, such as Hot Reload or MAUI features may not work because the view has no idea how to handle a world in which it is detached from the app.

In addition to the potentially missing features, we are also creating a brand new app each time we want to embed a control. This is fine for a single case and maybe if none of the components are using Application.Current. However, this is fairly common in apps and libraries, so the better way to do this is to create a shared, static instance of the MAUI app:

// 1. The MauiApp instance should be static for the entire app to use.
static MauiApp? _sharedMauiApp;
static MauiApp SharedMauiApp => _sharedMauiApp ??= MauiProgram.CreateMauiApp();

This will also allow you to instantiate the MauiApp early in your app lifecycle and not have a small delay when instantiating the actual MAUI views.

Window Scopes

.NET MAUI uses scoped services to manage the services and instances that are needed for each window. In some cases, a window may have a different dispatcher than the app or other windows. There may also be a case where a control needs to have access to a Window to do things. For example, adaptive triggers require access to a view's window, and if there is no window, it cannot work.

In order to wrap the view in a window, there is a ToPlatformEmbedded extension method that will do the same thing as the existing ToPlatform, but also make sure that it attaches to the application correctly. This means that it creates an embedded window that is attached to the application, and then the control is attached to that. This results in the window-related APIs working as well as tooling (Hot Reload) now starting to function.

// 1. Ensure app is built before creating MAUI views (should be a shared static)
var mauiApp = MauiProgram.CreateMauiApp();
// 2. Create MAUI views
var mauiView = new MyMauiContent();
// 3. Create platform view
var nativeView = mauiView.ToPlatformEmbedded(mauiApp, this);
// 4. Add to the UI
RootLayout.Children.Add(nativeView);

Correct Window Scopes

One downside of using the ToPlatformEmbedded extension method that accepts a platform window is that it will create a new embedded window for each invocation. This means, if you have 3 embedded views, you will also have 3 embedded windows attached to the app.

To correctly relate a single native window with a single MAUI window, we can use the CreateEmbeddedWindowContext to first create a window context and then use that (instead of the app context) to attach windows:

// 1. Ensure app is built before creating MAUI views (should be a shared static)
var mauiApp = MauiProgram.CreateMauiApp();
// 2. Create MAUI embedded window context
var windowContext = mauiApp.CreateEmbeddedWindowContext(this);
// 3. Create MAUI views
var mauiView = new MyMauiContent();
// 4. Create platform view
var nativeView = mauiView.ToPlatformEmbedded(windowContext);
// 5. Add to the UI
RootLayout.Children.Add(nativeView);

Final

If we put all this together, we can have a single, shared instance of our MAUI app, a single MAUI window for each native window, and all the tooling works correctly. For example, if we want to embed two different MAUI views on to a single WinUI window:

// 1. Create a shared/static/reusable instance of the MauiApp that will form the root of the MAUI component.
//
// A separate class just to provide shared access to the MAUI app from anywhere.
// This property could be anywhere because it is static, most likely on some native application type.
// But for platforms like Android, applications are not as common so a new type to host the property is also fine.
// If you have a single window app, then this static property could also just reside in the window class.
public static class MyEmbeddedMauiApp
{
    private static MauiApp? _shared;
    public static MauiApp Shared =>
        _shared ??= MauiProgram.CreateMauiApp();
}

// The type "MainWindow" is a native WinUI window that is not controlled by MAUI.
// For other platforms, this may not be a "window" type, but some component of it.
public sealed partial class MainWindow : Microsoft.UI.Xaml.Window
{
    // 2. A window-scoped IMauiContext that represents the native window.
    private IMauiContext? windowContext;
    private IMauiContext WindowContext =>
        windowContext ??= MyEmbeddedMauiApp.Shared.CreateEmbeddedWindowContext(this);

    // This method can be called multiple times to embed multiple MAUI views.
    private void EmbedMauiView()
    {
        // 2. Make sure the MAUI app and window context is ready because this example uses lazy loading.
        // If the implementation is not lazy, then this will be the actual call to CreateEmbeddedWindowContext.
        var context = WindowContext;

        // 3. Create the MAUI views to be embedded. There could be multiple in a single window.
        var mauiView = new MyMauiContent();

        // 4. Create the platform view from the MAUI view using the window context.
        // Access to the platform view from the MAUI view is via the mauiView.Handler.PlatformView property.
        var nativeView = mauiView.ToPlatformEmbedded(context);

        // 5. Add the new native view to the UI.
        RootLayout.Children.Add(nativeView);
    }
}

Issues Fixed

Fixes #

@mattleibow mattleibow force-pushed the dev/embedded branch 5 times, most recently from 844b484 to 27f27b2 Compare July 18, 2024 18:34
@mattleibow mattleibow changed the base branch from main to dev/embedded-sample July 19, 2024 03:56
Base automatically changed from dev/embedded-sample to main July 19, 2024 15:11
/// <summary>
/// A set of extension methods that allow for embedding a MAUI view within a native application.
/// </summary>
internal static class EmbeddingExtensions
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class is internal for now since this is a new API and we cannot add APIs to net8.0. However, we can make this public in net9.0.

This API is added here to make development easier as well as to allow for testing/usage in the Community Toolkit (if they want). Instead of trying to do complex things from the outside, we can make it internal and the [InternalsVisibleTo] attributes effectively expose all these to them.


namespace Microsoft.Maui.Controls.Hosting;

public static partial class AppHostBuilderExtensions
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class used to be in the Controls.Xaml assembly, however all of this was meant to be in Controls.Core. There is 2 lines in the old file that is referencing XAML types, so we rather move this logic into Controls.Core and call it from Controls.Xaml.

/// <summary>
/// A set of extension methods that allow for embedding a MAUI view within a native application.
/// </summary>
internal static class EmbeddingExtensions
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also is internal for net8.0 and should be public for net9.0. This is also visible to the Community Toolkit.

Comment on lines +44 to +45
DependencyService.Register<Xaml.ResourcesLoader>();
DependencyService.Register<Xaml.ValueConverterProvider>();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is the one I moved into Controls.Core - except for these 2 lines. The UseMauiApp API is still here, but now just calls into Controls.Core and then calls this one (SetupXamlDefaults).

@mattleibow mattleibow marked this pull request as ready for review July 19, 2024 17:35
@mattleibow mattleibow requested a review from a team as a code owner July 19, 2024 17:35
@mattleibow
Copy link
Member Author

/rebase

@PureWeen PureWeen merged commit a2fdea0 into main Jul 26, 2024
97 checks passed
@PureWeen PureWeen deleted the dev/embedded branch July 26, 2024 12:19
@samhouts samhouts added the fixed-in-net9.0-nightly This may be available in a nightly release! label Aug 2, 2024
@samhouts samhouts added fixed-in-8.0.80 and removed fixed-in-net9.0-nightly This may be available in a nightly release! labels Aug 8, 2024
@samhouts samhouts added the fixed-in-net9.0-nightly This may be available in a nightly release! label Aug 8, 2024
@github-actions github-actions bot locked and limited conversation to collaborators Sep 8, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
fixed-in-8.0.80 fixed-in-net9.0-nightly This may be available in a nightly release!
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants