Bewerken

Delen via


Native embedding

Browse sample. Browse the sample

Typically, a .NET Multi-platform App UI (.NET MAUI) app includes pages that contain layouts, such as Grid, and layouts that contain views, such as Button. Pages, layouts, and views all derive from Element. Native embedding enables any .NET MAUI controls that derive from Element to be consumed in .NET for Android, .NET for iOS, .NET for Mac Catalyst, and WinUI native apps.

The process for consuming a .NET MAUI control in a native app is as follows:

  1. Create extension methods to bootstrap your native embedded app. For more information, see Create extension methods.
  2. Create a .NET MAUI single project that contains your .NET MAUI UI and any dependencies. For more information, see Create a .NET MAUI single project.
  3. Create a native app and enable .NET MAUI support in it. For more information, see Enable .NET MAUI support.
  4. Initialize .NET MAUI in your native app project. For more information, see Initialize .NET MAUI.
  5. Create the .NET MAUI UI and convert it to the appropriate native type with the ToPlatformEmbedding extension method. For more information, see Consume .NET MAUI controls.
  1. Create a .NET MAUI single project that contains your .NET MAUI UI and any dependencies. For more information, see Create a .NET MAUI single project.
  2. Create a native app and enable .NET MAUI support in it. For more information, see Enable .NET MAUI support.
  3. Initialize .NET MAUI in your native app project. For more information, see Initialize .NET MAUI.
  4. Create the .NET MAUI UI and convert it to the appropriate native type with the ToPlatformEmbedding extension method. For more information, see Consume .NET MAUI controls.

Note

When using native embedding, .NET MAUI's data binding engine still works. However, page navigation must be performed using the native navigation API.

Create extension methods

Before creating a native app that consumes .NET MAUI controls, you should first create a .NET MAUI class library project and delete the Platforms folder and the Class1 class from it. Then, add a class to it named EmbeddedExtensions that contains the following code:

using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Maui.Platform;

#if ANDROID
using PlatformView = Android.Views.View;
using PlatformWindow = Android.App.Activity;
using PlatformApplication = Android.App.Application;
#elif IOS || MACCATALYST
using PlatformView = UIKit.UIView;
using PlatformWindow = UIKit.UIWindow;
using PlatformApplication = UIKit.IUIApplicationDelegate;
#elif WINDOWS
using PlatformView = Microsoft.UI.Xaml.FrameworkElement;
using PlatformWindow = Microsoft.UI.Xaml.Window;
using PlatformApplication = Microsoft.UI.Xaml.Application;
#endif

namespace Microsoft.Maui.Controls;

public static class EmbeddedExtensions
{
    public static MauiAppBuilder UseMauiEmbedding(this MauiAppBuilder builder, PlatformApplication? platformApplication = null)
    {
#if ANDROID
        platformApplication ??= (Android.App.Application)Android.App.Application.Context;
#elif IOS || MACCATALYST
        platformApplication ??= UIKit.UIApplication.SharedApplication.Delegate;
#elif WINDOWS
        platformApplication ??= Microsoft.UI.Xaml.Application.Current;
#endif

        builder.Services.AddSingleton(platformApplication);
        builder.Services.AddSingleton<EmbeddedPlatformApplication>();
        builder.Services.AddScoped<EmbeddedWindowProvider>();

        // Returning null is acceptable here as the platform window is optional - but we don't know until we resolve it
        builder.Services.AddScoped<PlatformWindow>(svc => svc.GetRequiredService<EmbeddedWindowProvider>().PlatformWindow!);
        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IMauiInitializeService, EmbeddedInitializeService>());
        builder.ConfigureMauiHandlers(handlers =>
        {
            handlers.AddHandler(typeof(Window), typeof(EmbeddedWindowHandler));
        });

        return builder;
    }

    public static IMauiContext CreateEmbeddedWindowContext(this MauiApp mauiApp, PlatformWindow platformWindow, Window? window = null)
    {
        var windowScope = mauiApp.Services.CreateScope();

#if ANDROID
        var windowContext = new MauiContext(windowScope.ServiceProvider, platformWindow);
#else
        var windowContext = new MauiContext(windowScope.ServiceProvider);
#endif

        window ??= new Window();

        var wndProvider = windowContext.Services.GetRequiredService<EmbeddedWindowProvider>();
        wndProvider.SetWindow(platformWindow, window);
        window.ToHandler(windowContext);

        return windowContext;
    }

    public static PlatformView ToPlatformEmbedded(this IElement element, IMauiContext context)
    {
        var wndProvider = context.Services.GetService<EmbeddedWindowProvider>();
        if (wndProvider is not null && wndProvider.Window is Window wnd && element is VisualElement visual)
            wnd.AddLogicalChild(visual);

        return element.ToPlatform(context);
    }

    private class EmbeddedInitializeService : IMauiInitializeService
    {
        public void Initialize(IServiceProvider services) =>
            services.GetRequiredService<EmbeddedPlatformApplication>();
    }
}

These extension methods are in the Microsoft.Maui.Controls namespace, and are used to bootstrap your native embedded app on each platform. The extension methods reference EmbeddedPlatformApplication, EmbeddedWindowHandler, and EmbeddedWindowProvider types that you must also add to the .NET MAUI library project.

The following code shows the EmbeddedPlatformApplication class, which should be added to the same .NET MAUI library project as the EmbeddedExtensions class:

#if ANDROID
using PlatformApplication = Android.App.Application;
#elif IOS || MACCATALYST
using PlatformApplication = UIKit.IUIApplicationDelegate;
#elif WINDOWS
using PlatformApplication = Microsoft.UI.Xaml.Application;
#endif

namespace Microsoft.Maui.Controls;

internal class EmbeddedPlatformApplication : IPlatformApplication
{
    private readonly MauiContext rootContext;
    private readonly IMauiContext applicationContext;

    public IServiceProvider Services { get; }
    public IApplication Application { get; }

    public EmbeddedPlatformApplication(IServiceProvider services)
    {
        IPlatformApplication.Current = this;

#if ANDROID
        var platformApp = services.GetRequiredService<PlatformApplication>();
        rootContext = new MauiContext(services, platformApp);
#else
        rootContext = new MauiContext(services);
#endif

        applicationContext = MakeApplicationScope(rootContext);
        Services = applicationContext.Services;
        Application = Services.GetRequiredService<IApplication>();
    }

    private static IMauiContext MakeApplicationScope(IMauiContext rootContext)
    {
        var scopedContext = new MauiContext(rootContext.Services);
        InitializeScopedServices(scopedContext);
        return scopedContext;
    }

    private static void InitializeScopedServices(IMauiContext scopedContext)
    {
        var scopedServices = scopedContext.Services.GetServices<IMauiInitializeScopedService>();

        foreach (var service in scopedServices)
            service.Initialize(scopedContext.Services);
    }
}

The following code shows the EmbeddedWindowHandler class, which should be added to the same .NET MAUI library project as the EmbeddedExtensions class:

using Microsoft.Maui.Handlers;

#if ANDROID
using PlatformWindow = Android.App.Activity;
#elif IOS || MACCATALYST
using PlatformWindow = UIKit.UIWindow;
#elif WINDOWS
using PlatformWindow = Microsoft.UI.Xaml.Window;
#endif

namespace Microsoft.Maui.Controls;

internal class EmbeddedWindowHandler : ElementHandler<IWindow, PlatformWindow>, IWindowHandler
{
    public static IPropertyMapper<IWindow, IWindowHandler> Mapper =
        new PropertyMapper<IWindow, IWindowHandler>(ElementHandler.ElementMapper)
        {
        };

    public static CommandMapper<IWindow, IWindowHandler> CommandMapper =
        new CommandMapper<IWindow, IWindowHandler>(ElementHandler.ElementCommandMapper)
        {
        };

    public EmbeddedWindowHandler() : base(Mapper)
    {
    }

    protected override PlatformWindow CreatePlatformElement() =>
        MauiContext!.Services.GetRequiredService<PlatformWindow>() ??
        throw new InvalidOperationException("EmbeddedWindowHandler could not locate a platform window.");
}

The following code shows the EmbeddedWindowProvider class, which should be added to the same .NET MAUI library project as the EmbeddedExtensions class:

#if ANDROID
using PlatformWindow = Android.App.Activity;
#elif IOS || MACCATALYST
using PlatformWindow = UIKit.UIWindow;
#elif WINDOWS
using PlatformWindow = Microsoft.UI.Xaml.Window;
#endif

namespace Microsoft.Maui.Controls;

public class EmbeddedWindowProvider
{
    WeakReference<PlatformWindow?>? platformWindow;
    WeakReference<Window?>? window;

    public PlatformWindow? PlatformWindow => Get(platformWindow);
    public Window? Window => Get(window);

    public void SetWindow(PlatformWindow? platformWindow, Window? window)
    {
        this.platformWindow = new WeakReference<PlatformWindow?>(platformWindow);
        this.window = new WeakReference<Window?>(window);
    }

    private static T? Get<T>(WeakReference<T?>? weak) where T : class =>
        weak is not null && weak.TryGetTarget(out var target) ? target : null;
}

Create a .NET MAUI single project

Before creating a native app that consumes .NET MAUI controls, you should add a .NET MAUI app project to the same solution as the .NET MAUI class library project you created previously. The .NET MAUI app project will store the UI you intend you re-use in your native embedded app. After adding a new .NET MAUI app project to the solution, perform the following steps:

  1. Delete the Properties folder from the project.

  2. Delete the Platforms folder from the project.

  3. Delete the Resources/AppIcon folder from the project.

  4. Delete the Resources/raw folder from the project.

  5. Delete the Resources/Splash folder from the project.

  6. Delete the AppShell class from the project.

  7. Ensure the App class doesn't set the MainPage property or override the CreateWindow method:

    public partial class App : Application
    {
        public App()
        {
            InitializeComponent();
        }
    }
    
  8. Delete the MainPage class from the project.

  9. Modify the project file so that the $(TargetFramework) build property is set to net8.0, and the $(OutputType) build property is removed:

    <PropertyGroup>
      <TargetFramework>net8.0</TargetFramework>
    
      <RootNamespace>MyMauiApp</RootNamespace>
      <UseMaui>true</UseMaui>
      <SingleProject>true</SingleProject>
      <ImplicitUsings>enable</ImplicitUsings>
      <Nullable>enable</Nullable>
    
      ...
    </PropertyGroup>
    

    Important

    Ensure you set the $(TargetFramework) build property, not the$(TargetFrameworks) build property.

  10. Modify the CreateMauiApp method in the MauiProgram class so that it accepts an optional Action<MauiAppBuilder> argument that's invoked before the method returns:

    public static MauiApp CreateMauiApp(Action<MauiAppBuilder>? additional = null)
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            });
    
        #if DEBUG
            builder.Logging.AddDebug();
        #endif
    
        additional?.Invoke(builder);
        return builder.Build();
    }
    

At this point you should add your required .NET MAUI UI to the project, including any dependencies and resources, and ensure that the project builds correctly.

Create a .NET MAUI single project

Before creating a native app that consumes .NET MAUI controls, you should add a .NET MAUI app project to the same solution as the .NET MAUI class library project you created previously. The .NET MAUI app project will store the UI you intend you re-use in your native embedded app. After adding a new .NET MAUI app project to the solution, perform the following steps:

  1. Delete the Properties folder from the project.

  2. Delete the Platforms folder from the project.

  3. Delete the Resources/AppIcon folder from the project.

  4. Delete the Resources/raw folder from the project.

  5. Delete the Resources/Splash folder from the project.

  6. Delete the AppShell class from the project.

  7. Ensure the App class doesn't set the MainPage property or override the CreateWindow method:

    public partial class App : Application
    {
        public App()
        {
            InitializeComponent();
        }
    }
    
  8. Delete the MainPage class from the project.

  9. Modify the project file so that the $(TargetFramework) build property is set to net9.0, and the $(OutputType) build property is removed:

    <PropertyGroup>
      <TargetFramework>net9.0</TargetFramework>
    
      <RootNamespace>MyMauiApp</RootNamespace>
      <UseMaui>true</UseMaui>
      <SingleProject>true</SingleProject>
      <ImplicitUsings>enable</ImplicitUsings>
      <Nullable>enable</Nullable>
    
      ...
    </PropertyGroup>
    

    Important

    Ensure you set the $(TargetFramework) build property, not the$(TargetFrameworks) build property.

  10. In the MauiProgram class, modify the CreateMauiApp method to accept a TApp generic argument, and accept an optional Action<MauiAppBuilder> argument that's invoked before the method returns. In addition, change the call from UseMauiApp<App> to UseMauiEmbeddedApp<TApp>:

    public static class MauiProgram
    {
        // Create a MauiApp using the specified application.
        public static MauiApp CreateMauiApp<TApp>(Action<MauiAppBuilder>? additional = null) where TApp : App
        {
            var builder = MauiApp.CreateBuilder();
            builder
                .UseMauiEmbeddedApp<TApp>()
                .ConfigureFonts(fonts =>
                {
                    fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                    fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
                });
    
            #if DEBUG
                builder.Logging.AddDebug();
            #endif
    
            additional?.Invoke(builder);
    
            return builder.Build();
        }
    }
    
  11. In the MauiProgram class, add a CreateMauiApp overload that accepts an optional Action<MauiAppBuilder> argument:

    public static class MauiProgram
    {
        ...
    
        // Create a MauiApp using the default application.
        public static MauiApp CreateMauiApp(Action<MauiAppBuilder>? additional = null) =>
            CreateMauiApp<App>(additional);
    }
    

You should then add your required .NET MAUI UI to the project, including any dependencies and resources, and ensure that the project builds correctly.

Enable .NET MAUI support

To consume .NET MAUI controls that derive from Element in a .NET for Android, .NET for iOS, .NET for Mac Catalyst, or WinUI app, you should add your native app project to the same solution as the .NET MAUI class library project you created previously. Then you should enable .NET MAUI support in your native app's project file by setting the $(UseMaui) and $(MauiEnablePlatformUsings) build properties to true in the first <PropertyGroup> node in the project file:

<PropertyGroup>
    ...
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>

    <UseMaui>true</UseMaui>
    <MauiEnablePlatformUsings>true</MauiEnablePlatformUsings>  
</PropertyGroup>

For .NET for Mac Catalyst apps, you'll also need to set the $(SupportedOSPlatformVersion) build property to a minimum of 14.0:

<PropertyGroup>
    ...
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>

    <SupportedOSPlatformVersion>14.2</SupportedOSPlatformVersion>
    <UseMaui>true</UseMaui>
    <MauiEnablePlatformUsings>true</MauiEnablePlatformUsings>  
</PropertyGroup>

For .NET for Mac Catalyst apps, you'll also need to set the $(SupportedOSPlatformVersion) build property to a minimum of 15.0:

<PropertyGroup>
    ...
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>

    <SupportedOSPlatformVersion>15.0</SupportedOSPlatformVersion>
    <UseMaui>true</UseMaui>
    <MauiEnablePlatformUsings>true</MauiEnablePlatformUsings>  
</PropertyGroup>

For WinUI apps, you'll also need to set the $(EnableDefaultXamlItems) build property to false:

<PropertyGroup>
    ...
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>

    <UseMaui>true</UseMaui>
    <MauiEnablePlatformUsings>true</MauiEnablePlatformUsings>    
    <EnableDefaultXamlItems>false</EnableDefaultXamlItems>
</PropertyGroup>

This will stop you receiving build errors about the InitializeComponent method already being defined.

Then, add $(PackageReference) build items to the project file for the Microsoft.Maui.Controls and Microsoft.Maui.Controls.Compatiblity NuGet packages:

<ItemGroup>
    <PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
    <PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" />
</ItemGroup>

Then, add $(PackageReference) build items to the project file for the Microsoft.Maui.Controls NuGet package:

<ItemGroup>
    <PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
</ItemGroup>

Initialize .NET MAUI

.NET MAUI must be initialized before a native app project can construct a .NET MAUI control. Choosing when to initialize it primarily depends on when it's most convenient in your app flow - it could be performed at startup or just before a .NET MAUI control is constructed. The approach outlined here is to initialize .NET MAUI when the app's initial UI is created.

Typically, the pattern for initializing .NET MAUI in a native app project is as follows:

On Android, the OnCreate override in the MainActivity class is typically the place to perform app startup related tasks. The following code example shows .NET MAUI being initialized in the MainActivity class:

namespace MyNativeEmbeddedApp.Droid;

[Activity(Label = "@string/app_name", MainLauncher = true, Theme = "@style/AppTheme")]
public class MainActivity : Activity
{
    public static readonly Lazy<MauiApp> MauiApp = new(() =>
    {
        var mauiApp = MauiProgram.CreateMauiApp(builder =>
        {
            builder.UseMauiEmbedding();
        });
        return mauiApp;
    });

    public static bool UseWindowContext = true;

    protected override void OnCreate(Bundle? savedInstanceState)
    {
        base.OnCreate(savedInstanceState);

        // Ensure .NET MAUI app is built before creating .NET MAUI views
        var mauiApp = MainActivity.MauiApp.Value;

        // Create .NET MAUI context
        var context = UseWindowContext
            ? mauiApp.CreateEmbeddedWindowContext(this) // Create window context
            : new MauiContext(mauiApp.Services, this);  // Create app context

        ...              
    }
}

On iOS and Mac Catalyst, the AppDelegate class should be modified to return true for the FinishedLaunching override:

namespace MyNativeEmbeddedApp.iOS;

[Register("AppDelegate")]
public class AppDelegate : UIApplicationDelegate
{
    public override UIWindow? Window { get; set; }

    public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions) => true;
}

The WillConnect method in the SceneDelegate class should then be modified to create your main view controller and set it as the view of the UINavigationController:

namespace MyNativeEmbeddedApp.iOS;

[Register("SceneDelegate")]
public class SceneDelegate : UIResponder, IUIWindowSceneDelegate
{
    [Export("window")]
    public UIWindow? Window { get; set; }

    [Export("scene:willConnectToSession:options:")]
    public void WillConnect(UIScene scene, UISceneSession session, UISceneConnectionOptions connectionOptions)
    {
        if (scene is not UIWindowScene windowScene)
            return;

        Window = new UIWindow(windowScene);

        var mainVC = new MainViewController();
        var navigationController = new UINavigationController(mainVC);
        navigationController.NavigationBar.PrefersLargeTitles = true;

        Window.RootViewController = navigationController;
        Window.MakeKeyAndVisible();
    }

    ...
}

Then, in the XML editor, open the Info.plist file and add the following XML to the end of the file:

<key>UIApplicationSceneManifest</key>
<dict>
  <key>UIApplicationSupportsMultipleScenes</key>
  <true/>
  <key>UISceneConfigurations</key>
  <dict>
    <key>UIWindowSceneSessionRoleApplication</key>
    <array>
      <dict>
        <key>UISceneConfigurationName</key>
        <string>Default Configuration</string>
        <key>UISceneDelegateClassName</key>
        <string>SceneDelegate</string>
      </dict>
    </array>
  </dict>
</dict>

.NET MAUI can then be initialized in the ViewDidLoad method in your main view controller:

using Microsoft.Maui.Platform;

namespace MyNativeEmbeddedApp.iOS;

public class MainViewController : UIViewController
{
    UIWindow GetWindow() =>
        View?.Window ??
        ParentViewController?.View?.Window ??
        MainViewController.MauiApp.Value.Services.GetRequiredService<IUIApplicationDelegate>().GetWindow() ??
        UIApplication.SharedApplication.Delegate.GetWindow();

    public static readonly Lazy<MauiApp> MauiApp = new(() =>
    {
        var mauiApp = MauiProgram.CreateMauiApp(builder =>
        {
            builder.UseMauiEmbedding();
        });
        return mauiApp;
    });

    public static bool UseWindowContext = true;

    public override void ViewDidLoad()
    {
        base.ViewDidLoad();

        // Ensure app is built before creating .NET MAUI views
        var mauiApp = MainViewController.MauiApp.Value;

        // Create .NET MAUI context
        var context = UseWindowContext
            ? mauiApp.CreateEmbeddedWindowContext(GetWindow()) // Create window context
            : new MauiContext(mauiApp.Services);               // Create app context

        ...
    }
}

On Windows, the MainWindow class is typically the place to perform UI related app startup tasks:

namespace MyNativeEmbeddedApp.WinUI;

public sealed partial class MainWindow : Microsoft.UI.Xaml.Window
{
    public static readonly Lazy<MauiApp> MauiApp = new(() =>
    {
        var mauiApp = MauiProgram.CreateMauiApp(builder =>
        {
            builder.UseMauiEmbedding();
        });
        return mauiApp;
    });

    public static bool UseWindowContext = true;

    public MainWindow()
    {
        this.InitializeComponent();

        // Ensure .NET MAUI app is built before creating .NET MAUI views
        var mauiApp = MainWindow.MauiApp.Value;

        // Create .NET MAUI context
        var context = UseWindowContext
            ? mauiApp.CreateEmbeddedWindowContext(this) // Create window context
            : new MauiContext(mauiApp.Services);        // Create app context

        ...
    }
}

In this example, the MauiApp object is created using lazy initialization. The UseMauiEmbedding extension method is invoked on the MauiAppBuilder object. Therefore, your native app project should include a reference to the .NET MAUI class library project you created that contains this extension method. A MauiContext object is then created from the MauiApp object, with a bool determining where the context is scoped from. The MauiContext object will be used when converting .NET MAUI controls to native types.

Embedding can be performed in an app context or a window context, but for maximum .NET MAUI compatibility it should be performed in a window context.

App context

Native embedding can be performed in an app context, where the native app has no knowledge of a window. With this approach, native embedding initialization requires you to:

The following example shows this approach:

var mauiApp = MauiProgram.CreateMauiApp();
var context = new MauiContext(mauiApp.Services); // Activity also needs passing on Android

A .NET MAUI view can then be created and converted to a native view with the ToPlatformEmbedded extension method, which requires the MauiContext object as an argument.

This approach is suitable for scenarios where a native app needs to embed a simple .NET MAUI UI, but doesn't require access to all .NET MAUI features. The disadvantage of this approach is that tooling such as hot reload, and some .NET MAUI features, won't work.

Tip

Creating a MauiApp object each time a .NET MAUI view is embedded as a native view isn't recommended. This can be problematic if embedded views access the Application.Current property. Instead, the MauiApp object can be created as a shared, static instance:

public static class MyEmbeddedMauiApp
{
    static MauiApp? _shared;
    public static MauiApp Shared => _shared ??= MauiProgram.CreateMauiApp();
}

With this approach, you can instantiate the MauiApp object early in your app lifecycle to avoid having a small delay the first time you embed a .NET MAUI view in your app.

On Android, a fragment represents a portion of the UI within an activity. The following code example shows .NET MAUI being initialized in a fragment:

using Android.Runtime;
using Android.Views;
using AndroidX.Navigation.Fragment;
using Microsoft.Maui.Controls.Embedding;
using Fragment = AndroidX.Fragment.App.Fragment;
using View = Android.Views.View;

namespace MyNativeEmbeddedApp.Droid;

[Register("com.companyname.nativeembeddingdemo." + nameof(FirstFragment))]
public class FirstFragment : Fragment
{
    public override View? OnCreateView(LayoutInflater inflater, ViewGroup? container, Bundle? savedInstanceState) =>
        inflater.Inflate(Resource.Layout.fragment_first, container, false);

    public override void OnViewCreated(View view, Bundle? savedInstanceState)
    {
        base.OnViewCreated(view, savedInstanceState);

        // Ensure .NET MAUI app is built before creating .NET MAUI views
        var mauiApp = MyEmbeddedMauiApp.Shared;

        // Create .NET MAUI context
        var context = new MauiContext(mauiApp.Services, Activity);

        ...
    }
}

On iOS and Mac Catalyst, the AppDelegate class should be modified to return true for the FinishedLaunching override:

namespace MyNativeEmbeddedApp.iOS;

[Register("AppDelegate")]
public class AppDelegate : UIApplicationDelegate
{
    public override UIWindow? Window { get; set; }

    public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions) => true;
}

The WillConnect method in the SceneDelegate class should then be modified to create your main view controller and set it as the view of the UINavigationController:

namespace MyNativeEmbeddedApp.iOS;

[Register("SceneDelegate")]
public class SceneDelegate : UIResponder, IUIWindowSceneDelegate
{
    [Export("window")]
    public UIWindow? Window { get; set; }

    [Export("scene:willConnectToSession:options:")]
    public void WillConnect(UIScene scene, UISceneSession session, UISceneConnectionOptions connectionOptions)
    {
        if (scene is not UIWindowScene windowScene)
            return;

        Window = new UIWindow(windowScene);

        var mainVC = new MainViewController();
        var navigationController = new UINavigationController(mainVC);
        navigationController.NavigationBar.PrefersLargeTitles = true;

        Window.RootViewController = navigationController;
        Window.MakeKeyAndVisible();
    }

    ...
}

Then, in the XML editor, open the Info.plist file and add the following XML to the end of the file:

<key>UIApplicationSceneManifest</key>
<dict>
  <key>UIApplicationSupportsMultipleScenes</key>
  <true/>
  <key>UISceneConfigurations</key>
  <dict>
    <key>UIWindowSceneSessionRoleApplication</key>
    <array>
      <dict>
        <key>UISceneConfigurationName</key>
        <string>Default Configuration</string>
        <key>UISceneDelegateClassName</key>
        <string>SceneDelegate</string>
      </dict>
    </array>
  </dict>
</dict>

.NET MAUI can then be initialized in the ViewDidLoad method in your main view controller:

using Microsoft.Maui.Controls.Embedding;

namespace MyNativeEmbeddedApp.iOS;

public class MainViewController : UIViewController
{
    public override void ViewDidLoad()
    {
        base.ViewDidLoad();

        // Ensure .NET MAUI app is built before creating .NET MAUI views
        var mauiApp = MyEmbeddedMauiApp.Shared;

        // Create .NET MAUI context
        var context = new MauiContext(mauiApp.Services);

        ...
    }
}

On Windows, the MainWindow class is typically the place to perform UI related app startup tasks:

using Microsoft.Maui.Controls.Embedding;
using Microsoft.UI.Xaml;

namespace MyNativeEmbeddedApp.WinUI;

public sealed partial class MainWindow : Microsoft.UI.Xaml.Window
{
    public MainWindow()
    {
        this.InitializeComponent();
    }

    private async void OnRootLayoutLoaded(object? sender, RoutedEventArgs e)
    {
        await Task.Yield();

        // Ensure .NET MAUI app is built before creating .NET MAUI views
        var mauiApp = MyEmbeddedMauiApp.Shared;

        // Create .NET MAUI context
        var context = new MauiContext(mauiApp.Services);

        ...
    }
}

In this example, the MauiApp object is created as a shared, static instance. When this object is created, MauiProgram.CreateMauiApp is called which in turn calls the UseMauiEmbedding extension method on the MauiAppBuilder object. Therefore, your native app project should include a reference to the .NET MAUI class library project you created that contains your MauiProgram class and your .NET MAUI UI. A MauiContext object is then created from the MauiApp object, that's scoped to the MauiApp object. The MauiContext object will be used when converting .NET MAUI controls to native types.

Window context

Native embedding can be performed in a window context, where the native app has knowledge of a window. In some scenarios, .NET MAUI views require access to a window to work correctly. For example, adaptive triggers require access to a view's window, and if there is no window they don't work.

With this approach, native embedding initialization requires you to:

  • Create a MauiApp object.
  • Create a MauiContext object with the CreateEmbeddedWindowContext method. The MauiContext object will be used to obtain a native view from the .NET MAUI view.

The CreateEmbeddedWindowContext method creates a window context that relates a native window to a .NET MAUI window:

var mauiApp = MauiProgram.CreateMauiApp();
var context = mauiApp.CreateEmbeddedWindowContext(this);

A .NET MAUI view can then be created and converted to a native view with the ToPlatformEmbedded extension method, which requires the MauiContext object as an argument.

Note

The ToPlatformEmbedded extension method has an overload that adds a .NET MAUI view to an embedded window.

The advantage of this approach is that there's a single .NET MAUI window for each native window, window-related APIs will work correctly, and tooling such as hot reload works correctly.

Tip

Creating a MauiApp object each time a .NET MAUI view is embedded as a native view isn't recommended. This can be problematic if embedded views access the Application.Current property. Instead, the MauiApp object can be created as a shared, static instance:

public static class MyEmbeddedMauiApp
{
    static MauiApp? _shared;
    public static MauiApp Shared => _shared ??= MauiProgram.CreateMauiApp();
}

With this approach, you can instantiate the MauiApp object early in your app lifecycle to avoid having a small delay the first time you embed a .NET MAUI view in your app.

On Android, a fragment represents a portion of the UI within an activity. The following code example shows .NET MAUI being initialized in a fragment:

using Android.Runtime;
using Android.Views;
using AndroidX.Navigation.Fragment;
using Microsoft.Maui.Controls.Embedding;
using Fragment = AndroidX.Fragment.App.Fragment;
using View = Android.Views.View;

namespace MyNativeEmbeddedApp.Droid;

[Register("com.companyname.nativeembeddingdemo." + nameof(FirstFragment))]
public class FirstFragment : Fragment
{
    Activity? _window;
    IMauiContext? _windowContext;

    public IMauiContext WindowContext =>
        _windowContext ??= MyEmbeddedMauiApp.Shared.CreateEmbeddedWindowContext(_window ?? throw new InvalidOperationException());

    public override View? OnCreateView(LayoutInflater inflater, ViewGroup? container, Bundle? savedInstanceState) =>
        inflater.Inflate(Resource.Layout.fragment_first, container, false);

    public override void OnViewCreated(View view, Bundle? savedInstanceState)
    {
        base.OnViewCreated(view, savedInstanceState);

        _window ??= Activity;

        // Create MAUI embedded window context
        var context = WindowContext;

        ...
    }
}

On iOS and Mac Catalyst, the AppDelegate class should be modified to return true for the FinishedLaunching override:

namespace MyNativeEmbeddedApp.iOS;

[Register("AppDelegate")]
public class AppDelegate : UIApplicationDelegate
{
    public override UIWindow? Window { get; set; }

    public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions) => true;
}

The WillConnect method in the SceneDelegate class should then be modified to create your main view controller and set it as the view of the UINavigationController:

namespace MyNativeEmbeddedApp.iOS;

[Register("SceneDelegate")]
public class SceneDelegate : UIResponder, IUIWindowSceneDelegate
{
    [Export("window")]
    public UIWindow? Window { get; set; }

    [Export("scene:willConnectToSession:options:")]
    public void WillConnect(UIScene scene, UISceneSession session, UISceneConnectionOptions connectionOptions)
    {
        if (scene is not UIWindowScene windowScene)
            return;

        Window = new UIWindow(windowScene);

        var mainVC = new MainViewController();
        var navigationController = new UINavigationController(mainVC);
        navigationController.NavigationBar.PrefersLargeTitles = true;

        Window.RootViewController = navigationController;
        Window.MakeKeyAndVisible();
    }

    ...
}

Then, in the XML editor, open the Info.plist file and add the following XML to the end of the file:

<key>UIApplicationSceneManifest</key>
<dict>
  <key>UIApplicationSupportsMultipleScenes</key>
  <true/>
  <key>UISceneConfigurations</key>
  <dict>
    <key>UIWindowSceneSessionRoleApplication</key>
    <array>
      <dict>
        <key>UISceneConfigurationName</key>
        <string>Default Configuration</string>
        <key>UISceneDelegateClassName</key>
        <string>SceneDelegate</string>
      </dict>
    </array>
  </dict>
</dict>

.NET MAUI can then be initialized in the ViewDidLoad method in your main view controller:

using Microsoft.Maui.Controls.Embedding;

namespace MyNativeEmbeddedApp.iOS;

public class MainViewController : UIViewController
{
    UIKit.UIWindow? _window;
    IMauiContext? _windowContext;

    public IMauiContext WindowContext =>
        _windowContext ??= MyEmbeddedMauiApp.Shared.CreateEmbeddedWindowContext(_window ?? throw new InvalidOperationException());

    public override void ViewDidLoad()
    {
        base.ViewDidLoad();

        _window ??= ParentViewController!.View!.Window;

        // Create MAUI embedded window context
        var context = WindowContext;

        ...
    }
}

On Windows, the MainWindow class is typically the place to perform UI related app startup tasks:

using Microsoft.Maui.Controls.Embedding;
using Microsoft.UI.Xaml;

namespace MyNativeEmbeddedApp.WinUI;

public sealed partial class MainWindow : Microsoft.UI.Xaml.Window
{
    Microsoft.UI.Xaml.Window? _window;
    IMauiContext? _windowContext;

    public IMauiContext WindowContext =>
        _windowContext ??= MyEmbeddedMauiApp.Shared.CreateEmbeddedWindowContext(_window ?? throw new InvalidOperationException());

    public MainWindow()
    {
        this.InitializeComponent();
        _window ??= this;
    }

    private async void OnRootLayoutLoaded(object? sender, RoutedEventArgs e)
    {
        await Task.Yield();

        // Create MAUI embedded window context
        var context = WindowContext;

        ...
    }
}

In this example, the MauiApp object is created as a shared, static instance. When this object is created, MauiProgram.CreateMauiApp is called which in turn calls the UseMauiEmbedding extension method on the MauiAppBuilder object. Therefore, your native app project should include a reference to the .NET MAUI class library project you created that contains your MauiProgram class and your .NET MAUI UI. A MauiContext object is then created with the CreateEmbeddedWindowContext method, that's scoped to the window. The MauiContext object will be used when converting .NET MAUI controls to native types.

Consume .NET MAUI controls

After .NET MAUI has been initialized in your native app, you can add your .NET MAUI UI to your native app's layout. This can be achieved by creating an instance of the UI and converting it to the appropriate native type with the ToPlatformEmbedded extension method.

On Android, the ToPlatformEmbedded extension method converts the .NET MAUI control to an Android View object:

var mauiView = new MyMauiContent();
Android.Views.View nativeView = mauiView.ToPlatformEmbedded(context);

In this example, a ContentView-derived object is converted to an Android View object.

Note

The ToPlatformEmbedded extension method is in the .NET MAUI class library you created earlier. Therefore your native app project should include a reference to that project.

Note

The ToPlatformEmbedded extension method is in the Microsoft.Maui.Controls.Embedding namespace. Therefore your native app project should include a using statement for that namespace.

The View object can then be added to a layout in your native app:

rootLayout.AddView(nativeView, new LinearLayout.LayoutParams(MatchParent, WrapContent));

On iOS and Mac Catalyst, the ToPlatformEmbedded extension method converts the .NET MAUI control to a UIView object:

var mauiView = new MyMauiContent();
UIView nativeView = mauiView.ToPlatformEmbedded(context);
nativeView.WidthAnchor.ConstraintEqualTo(View.Frame.Width).Active = true;
nativeView.HeightAnchor.ConstraintEqualTo(500).Active = true;

In this example, a ContentView-derived object is converted to a UIView object and then width and height constraints are set on it to allow interaction.

Note

The ToPlatformEmbedded extension method is in the .NET MAUI class library you created earlier. Therefore your native app project should include a reference to that project.

The UIView object can then be added to a view in your view controller:

stackView.AddArrangedSubView(nativeView);
var mauiView = new MyMauiContent();
UIView nativeView = mauiView.ToPlatformEmbedded(context);

In this example, a ContentView-derived object is converted to a UIView object.

Note

The ToPlatformEmbedded extension method is in the Microsoft.Maui.Controls.Embedding namespace. Therefore your native app project should include a using statement for that namespace.

The UIView object can then be added to a view in your view controller:

stackView.AddArrangedSubView(new ContainerView(nativeView));

ContainerView is a custom type that wraps the .NET MAUI view to ensure that it's sized correctly. This is achieved by redirecting IntrinsicContentSize to the .NET MAUI view's SizeThatFits:

class ContainerView : UIView
{
    public ContainerView(UIView view)
    {
        AddSubview(view);
    }

    public override CGSize IntrinsicContentSize =>
        SizeThatFits(new CGSize(nfloat.MaxValue, nfloat.MaxValue));

    public override CGSize SizeThatFits(CGSize size) =>
        Subviews?.FirstOrDefault()?.SizeThatFits(size) ?? CGSize.Empty;

    public override void LayoutSubviews()
    {
        if (Subviews?.FirstOrDefault() is { } view)
            view.Frame = Bounds;
    }

    public override void SetNeedsLayout()
    {
        base.SetNeedsLayout();
          InvalidateIntrinsicContentSize();
    }
}

In addition, a ToUIViewController extension method in .NET MAUI can be used to convert a .NET MAUI page to a UIViewController:

MyMauiPage myMauiPage = new MyMauiPage();
UIViewController myPageController = myMauiPage.ToUIViewController(context);

In this example, a ContentPage-derived object is converted to a UIViewController.

On Windows, the ToPlatformEmbedded extension method converts the .NET MAUI control to a FrameworkElement object:

var mauiView = new MyMauiContent();
FrameworkElement nativeView = myMauiPage.ToPlatformEmbedded(context);

In this example, a ContentView-derived object is converted to a FrameworkElement object. The FrameworkElement object can then be set as the content of a WinUI page.

Note

The ToPlatformEmbedded extension method is in the .NET MAUI class library you created earlier. Therefore your native app project should include a reference to that project.

Note

The ToPlatformEmbedded extension method is in the Microsoft.Maui.Controls.Embedding namespace. Therefore your native app project should include a using statement for that namespace.

The FrameworkElement object can then be added to a layout in your native app:

stackPanel.Children.Add(nativeView);

Important

To avoid an error occurring, XAML hot reload should be disabled before running a native embedded app in debug configuration.

Support XAML hot reload

XAML hot reload isn't supported in native embedded apps. However, you can still use XAML hot reload to quickly iterate on your .NET MAUI UI by creating a .NET MAUI app that consumes the .NET MAUI UI.

To view your .NET MAUI UI with XAML hot reload:

  1. In the project containing your .NET MAUI UI, update the MauiProgram class to add a CreateMauiApp overload, and modify the existing CreateMauiApp method to accept a generic argument:

    public static class MauiProgram
    {
        public static MauiApp CreateMauiApp(Action<MauiAppBuilder>? additional = null) =>
            CreateMauiApp<App>(additional);
    
        public static MauiApp CreateMauiApp<TApp>(Action<MauiAppBuilder>? additional = null) where TApp : App
        {
            var builder = MauiApp.CreateBuilder();
            builder
                .UseMauiApp<TApp>()
                .ConfigureFonts(fonts =>
                {
                    fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                    fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
                });
    
    #if DEBUG
            builder.Logging.AddDebug();
    #endif
            additional?.Invoke(builder);
    
            return builder.Build();
        }
    }
    
  2. In the project containing your .NET MAUI UI, convert each resource dictionary from a stand-alone XAML file to a resource dictionary that's backed by a code-behind file.

  3. In the project containing your .NET MAUI UI, update your resource dictionary instantiation, typically in App.xaml, so that the Source property also specifies the assembly that contains the resource dictionary:

    <ResourceDictionary Source="Resources/Styles/Colors.xaml;assembly=NativeEmbeddingDemo" />
    <ResourceDictionary Source="Resources/Styles/Styles.xaml;assembly=NativeEmbeddingDemo" />
    
  4. Create a new .NET MAUI app and add it to the solution containing your .NET MAUI UI project and native embedded apps.

  5. In your .NET MAUI app project, add a reference to the project that contains your .NET MAUI UI.

  6. In your .NET MAUI app project, delete any Resource child folders where the resource is provided by your .NET MAUI UI project. For example, if your .NET MAUI UI project contains Resources > Fonts, Resources > Images, and Resources > Styles folders, these folders should be deleted from the .NET MAUI app you've just created. This enables your .NET MAUI app to consume the resources from the project containing your .NET MAUI UI.

  7. In your .NET MAUI app, update your App class so that it derives from the App class in your .NET MAUI UI project:

    <myMauiUIProject:App xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                         xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                         xmlns:myMauiUIProject="clr-namespace:NativeEmbeddingDemo;assembly=NativeEmbeddingDemo"
                         x:Class="TestHarnessApp.TestApp">
        <myMauiUIProject:App.Resources>
            <!-- App specific resources go here -->
        </myMauiUIProject:App.Resources>
    </myMauiUIProject:App>
    

    Then update the code-behind file for the App class so that it derives from the App class in your .NET MAUI UI project, and loads any XAML resources from this project:

    public partial class TestApp : myMauiUIProject.App
    {
        public TestApp()
        {
            var baseResources = Resources;
            InitializeComponent();
            Resources.MergedDictionaries.Add(baseResources);
            MainPage = new HostPage();
        }
    }
    
  8. In your .NET MAUI app, add a page that displays the UI from the project containing your .NET MAUI UI:

    <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 xmlns:myMauiUIProject="clr-namespace:NativeEmbeddingDemo;assembly=NativeEmbeddingDemo"
                 x:Class="TestHarnessApp.HostPage"
                 Title="HostPage">
        <myMauiUIProject:MyMauiContent />
    </ContentPage>
    
  9. In your .NET MAUI app, update the MauiProgram class to call the CreateMauiApp method in the project containing your .NET MAUI UI:

    public static class MauiProgram
    {
        public static MauiApp CreateMauiApp() =>
            NativeEmbeddingDemo.MauiProgram.CreateMauiApp<TestApp>(builder =>
            {
                // Add any test harness configuration such as service stubs or mocks.
            });
    }
    

You should now be able to run your .NET MAUI app project on each platform and use XAML hot reload to iterate on your .NET MAUI UI.

For an example of this approach, see the sample app.