Skip to content
This repository has been archived by the owner on May 1, 2024. It is now read-only.

[Handlers] Register handlers and use Microsoft.Extensions for Hosting and Dependency Injection #12460

Merged
merged 81 commits into from
Feb 23, 2021

Conversation

rmarinho
Copy link
Member

@rmarinho rmarinho commented Oct 12, 2020

Description of Change

Adding the Host concept similar to ASPNET , this will allow to use the containers and providers for our handlers as well for services during the application, will open possiblity to use as Configuration with appsettings file, or other features of IHost like a built-in lifecycle and services, logging extensions etc.

Adding the App abstract class that allows the basic for working with the AppBuilder. Also we would have MauiAppthat allows to work with Handlers and Windows.

IWindow
Windows must be provided by the user by implementing IWindow GetWindowFor(IActivationState state) on there own MauiApp class.

IAppHostBuilder
Users can register services and configure the default AppHostBuilder with there own services or configurations, this can be done by overiding `virtual IAppHostBuilder CreateBuilder()´

User must register Windows as IWindow , or override Windows on App class.

Example:

public class MyApp : MauiApp
{
   //override the default builder so we can configure and register our own services
   public override IAppHostBuilder CreateBuilder()
   {
   	var builder = base.CreateBuilder()
   		   .ConfigureLogging(logging =>
   		   {
   		   	logging.ClearProviders();
   		   	logging.AddConsole();
   		   })
   		   .ConfigureAppConfiguration((hostingContext, config) =>
   		   {
   			   config.AddInMemoryCollection(new Dictionary<string, string>
   								{
   								   {"MyKey", "Dictionary MyKey Value"},
   								   {":Title", "Dictionary_Title"},
   								   {"Position:Name", "Dictionary_Name" },
   								   {"Logging:LogLevel:Default", "Warning"}
   								});
   		   })
   		   .ConfigureServices(ConfigureServices);
   	return builder;
   }
   
   // we need to implement this so we can return new windows when they are requested by the OS 
   // IActivationState will be related to
   public override IWindow GetWindowFor(IActivationState state)
   {
   	return Services.GetService<IWindow>();
   }
       //user can configure extra services on the container
   void ConfigureServices(HostBuilderContext ctx, IServiceCollection services)
   {
   	services.AddLogging();
   	services.AddSingleton<ITextService, TextService>();
   	services.AddTransient<MainPageViewModel>();
   	services.AddTransient<MainPage>();
   	services.AddTransient<IWindow, MainWindow>();
   }
}

The AppBuilder might be hidden from the user , but we will allow the user to override it, ant the user can extend by registering native services, configure logging , register new handlers.

Example : Register a custom handler to override the default one, configure native services

var builder = App.CreateDefaultBuilder()
					.RegisterHandler<IButton, CustomHandlers.CustomPinkTextButtonHandler>()
					.ConfigureServices(ConfigureExtraServices)
					

Using the Microsoft Dependency Injection to register handlers in a our own ServiceCollection implementation based on a dictionary. We assume that we only want 1 handler for a specific type then the use of dictionary. We still allow users to override it by just registering there own factory container of IServiceProviderFactory<ServiceCollection> type.

Example to use the default ServiceCollection instead of the Xamarin.Forms one

public override IAppHostBuilder CreateBuilder()
{
	var builder = base.CreateBuilder()
					 .UseServiceProviderFactory<ServiceCollection>(new DIExtensionsServiceProviderFactory();
						 
	return builder;
}
   
public class DIExtensionsServiceProviderFactory : IServiceProviderFactory<ServiceCollection>
{
	public ServiceCollection CreateBuilder(IServiceCollection services)
		=> new ServiceCollection { services };

	public IServiceProvider CreateServiceProvider(ServiceCollection containerBuilder)
		=> containerBuilder.BuildServiceProvider();
}

This PR also adds a couple of things

  • Maui.Controls.Sample that is just the Handlers + Maui.Controls

  • Add benchmark project to better get a sense of improvements on MAUI non specific platform code.

  • Add profile-android.ps1 to help measure startup time on the Android application.
    usage:

//old registrar
cd  eng\scripts folder
./profile-android.ps1 -project ..\..\src\Platform.Handlers\samples\Sample.Droid\Sample.Droid.csproj  -configuration Release -package com.microsoft.sample_droid

//new builder
cd  eng\scripts folder
./profile-android.ps1 -project ..\..\src\Forms\samples\Maui.Controls.Sample.Droid\Maui.Controls.Sample.Droid.csproj  -configuration Release -package com.microsoft.maui

Current baseline to Register and Get Handlers via Dependency Injection system and a basic Dictionary

Method Job UnrollFactor Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
RegisterHandlerUsingRegistrar DefaultJob 16 2.279 ms 0.0497 ms 0.1409 ms - - - -
GetHandlerUsingDI DefaultJob 16 36.325 ms 0.7165 ms 1.5114 ms 7071.4286 - - 29606000 B
GetHandlerUsingRegistrar DefaultJob 16 35.231 ms 0.7023 ms 1.0725 ms 7071.4286 - - 29606014 B
RegisterHandlerUsingDI Job-LQJADY 1 2.946 ms 0.3277 ms 0.9662 ms - - - 2097504 B

Current StartupTime using Release/AOT using Google Pixel 2

Method Mean Error StdDev
Empty App 307 ms 5.22 ms 16.51 ms
Registrar 367.9 ms 6.53 ms 20.67 ms
AppBuilder+AppHost 428.231 ms 5.02 ms 15.88 ms

Issues Resolved

  • None

API Changes

Trying to map IApp/MauiApp to Application on Android and AppDelegate. on iOS while Windows will map to Acitivites and SceneDelegates.

public class MauiApplication<TApplication> : global::Android.App.Application where TApplication : MauiApp

public class MauiAppCompatActivity : AppCompatActivity

public class MauiUIApplicationDelegate<TApplication> : UIApplicationDelegate, IUIApplicationDelegate where TApplication : MauiApp

The user can register it's own IHandlerServiceProvider and override our handler system. This provider shouldn't validate that implementation implements the required service, since we Register IButton,ButtonHandler, but ButtonHandler doesn't implement IButton, this is just for mapping one type to the other.

public interface IMauiServiceProvider : IServiceProvider

Special ServiceCollection based on Dictionary

public interface IMauiServiceCollection : IServiceCollection, IDictionary<Type, Func<IServiceProvider, object?>?>

This might exist for all IFrameworkElements and can be platform specific, will consolidate all the info to needed to generate a renderer from a IView, we might also be adding the Dispatcher here too

public interface IMauiContext
{
	IServiceProvider Provider { get; }

	IMauiServiceProvider Handlers { get; }
#if __ANDROID__
	global::Android.Content.Context Context { get; }
#elif __IOS__

#endif
}

A point where we can get handlers our other services, HotReload service could also be retrived via App.Services

public interface IApp
{
	IServiceProvider Services { get; }
}
public abstract class App : IApp
{
	public static App? Current { get; private set; }

	public IServiceProvider? Services ;

	public virtual IAppHostBuilder CreateBuilder() => CreateDefaultBuilder();

	internal void SetServiceProvider(IServiceProvider provider)
	
	public static IAppHostBuilder CreateDefaultBuilder()
	{
		var builder = new AppBuilder();

		builder.UseMauiHandlers();

		return builder;
	}
}

The IMauiContext will move to IFrameworkElement

public abstract class MauiApp : App
{
	public IMauiContext? Context;
	public abstract IWindow GetWindowFor(IActivationState state);
	internal void SetHandlerContext(IHandlersContext? context)
}
public static class ServiceProviderExtensions
{
	internal static IServiceProvider BuildServiceProvider(this IMauiServiceCollection serviceCollection)
			=> new MauiServiceProvider(serviceCollection);


	public static IViewHandler? GetHandler(this IServiceProvider services, Type type)
			=> services.GetService(type) as IViewHandler;

	public static IViewHandler? GetHandler<T>(this IServiceProvider services) where T : IView
			=> GetHandler(services, typeof(T));
}
public interface IAppHostBuilder : IHostBuilder
{
	IHostBuilder ConfigureHandlers(Action<HostBuilderContext, IServiceCollection> configureDelegate);
	new IAppHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate);
	new IAppHostBuilder ConfigureContainer<TContainerBuilder>(Action<HostBuilderContext, TContainerBuilder> configureDelegate);
	new IAppHostBuilder ConfigureHostConfiguration(Action<IConfigurationBuilder> configureDelegate);
	new IAppHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate);
	new IAppHostBuilder UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory);
	new IAppHostBuilder UseServiceProviderFactory<TContainerBuilder>(Func<HostBuilderContext, IServiceProviderFactory<TContainerBuilder>> factory);
	IHost Build(IApp app);
}

The IAppHostBuilder implementation, when we call Build(app) we get an IAppHost

public class AppHostBuilder : IAppHostBuilder
{	
}
public static class AppHostBuilderExtensions
{
	public static IAppHostBuilder RegisterHandler<TType, TTypeRender>(this IAppHostBuilder builder)
			where TType : IFrameworkElement
			where TTypeRender : IViewHandler
	public static IAppHostBuilder RegisterHandlers(this IAppHostBuilder builder, Dictionary<Type, Type> handlers)
	public static IAppHostBuilder UseMauiHandlers(this IAppHostBuilder builder)
}
public interface IWindow
{
		public IPage Page { get; set; }
		public IHandlersContext HandlersContext { get; set; }
public interface IPage : IView
{
		public IView View { get; set; }
}

Platforms Affected

  • Core/XAML (all platforms)
  • iOS
  • Android

Behavioral/Visual Changes

None

Before/After Screenshots

Not applicable

Testing Procedure

Unit tests present

PR Checklist

  • Targets the correct branch
  • Tests are passing (or failures are unrelated)

@rmarinho rmarinho changed the title Add Handlers registering use Microsoft.Extensions for Hosting and Dependency Injection [Handlers] Register handlers and use Microsoft.Extensions for Hosting and Dependency Injection Oct 12, 2020
@rmarinho
Copy link
Member Author

/azp run

@azure-pipelines
Copy link

No pipelines are associated with this pull request.

@rmarinho
Copy link
Member Author

/azp run

@azure-pipelines
Copy link

No pipelines are associated with this pull request.

@rmarinho rmarinho force-pushed the handlers-di branch 2 times, most recently from b1e3fa0 to b6ff3f9 Compare October 22, 2020 12:20
@rmarinho
Copy link
Member Author

/azp run

@azure-pipelines
Copy link

Pull request contains merge conflicts.

@rmarinho
Copy link
Member Author

/azp run

@azure-pipelines
Copy link

No pipelines are associated with this pull request.

Copy link

@eerhardt eerhardt left a comment

Choose a reason for hiding this comment

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

I took a casual look through some of the major pieces. Just some initial thoughts.

if (_handlersServiceProvider != null)
servicesCollection.AddSingleton<IHandlerServiceProvider>(_handlersServiceProvider);

var app = (TApplication)Activator.CreateInstance(typeof(TApplication));

Choose a reason for hiding this comment

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

Why isn't the Application created through DI? That way it can participate in DI and get services itself.

if (_hostBuilderContext != null)
AppLoader.ConfigureAppServices<TApplication>(_hostBuilderContext, servicesCollection, app);

var services = servicesCollection.BuildServiceProvider();

Choose a reason for hiding this comment

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

Is it intentional to have 2 service containers? This one and the one created in CreateServiceProvider()?

Copy link
Member Author

Choose a reason for hiding this comment

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

yes our service container for handlers is registering using a different way, the service doesn't need to implement the interface

rmarinho and others added 7 commits February 23, 2021 00:47
Use the null-forgiving operator (!) to rather swear that we doin' good
Also split the benchmarks into separate classes for better comparison.
{
public abstract class MauiApp : App
{
public abstract IWindow GetWindowFor(IActivationState state);
Copy link
Contributor

Choose a reason for hiding this comment

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

Just interested to know what this state will do... How does it affect the place when I have multiple windows or is this just for that first window?

Copy link
Member Author

Choose a reason for hiding this comment

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

All Windows, state will be platform dependent and can have things like intent, url link, state to restore etc.. everything that will allow the user to return the window that is needed, a new one or a existing one after restoring the state.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe the confusion here is that it's called GetWindowFor and it should be called CreateNewWindowFor

Copy link
Contributor

Choose a reason for hiding this comment

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

But if it gets the windows that is already visible...

Copy link
Member Author

Choose a reason for hiding this comment

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

Right Create seems it's always new, could be a Window already exists ?!

Copy link
Member

Choose a reason for hiding this comment

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

It doesn't get already visible, it's supposed to be when the platform asks you to create a new window for the given state/args ... this is to deal with the fact that on android and iOS you aren't always in control of when a window instantiation is requested. On iOS, as a user, you can use the task manager to request that an app creates a new window for you.

src/Platform.Handlers/src/Xamarin.Platform.Handlers/App.cs Outdated Show resolved Hide resolved
Comment on lines +8 to +11
[Application]
public class MainApplication : MauiApplication<MyApp>
{
public MainApplication(IntPtr handle, JniHandleOwnership ownerShip) : base(handle, ownerShip)
Copy link
Member

Choose a reason for hiding this comment

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

@dellis1972 it looks like they are planning on using this for default Maui apps

Is there still some issue with Fast Deployment and a custom application class?

Choose a reason for hiding this comment

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

There is but only for Enhanced Fast Deployment. That is where we fast deploy the dex files. It just won't work since we need the app class (java) to be in the classes.dex in the apk. Which it won't be. There is a possible way around it but it would involve allot of work.

@rmarinho rmarinho merged commit 228b3a0 into main-handler Feb 23, 2021
@rmarinho rmarinho deleted the handlers-di branch February 23, 2021 20:05
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.