diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs
index e7ef6dcbaa..d741e5b977 100644
--- a/src/Avalonia.Controls/AppBuilderBase.cs
+++ b/src/Avalonia.Controls/AppBuilderBase.cs
@@ -3,6 +3,7 @@
using System;
using System.Reflection;
+using System.Linq;
using Avalonia.Platform;
namespace Avalonia.Controls
@@ -16,22 +17,22 @@ namespace Avalonia.Controls
///
/// Gets or sets the instance.
///
- public IRuntimePlatform RuntimePlatform { get; set; }
+ public IRuntimePlatform RuntimePlatform { get; private set; }
///
/// Gets or sets a method to call the initialize the runtime platform services (e. g. AssetLoader)
///
- public Action RuntimePlatformServicesInitializer { get; set; }
+ public Action RuntimePlatformServicesInitializer { get; private set; }
///
/// Gets or sets the instance being initialized.
///
- public Application Instance { get; set; }
+ public Application Instance { get; protected set; }
///
/// Gets or sets a method to call the initialize the windowing subsystem.
///
- public Action WindowingSubsystemInitializer { get; set; }
+ public Action WindowingSubsystemInitializer { get; private set; }
///
/// Gets or sets the name of the windowing subsystem to use.
@@ -41,18 +42,23 @@ namespace Avalonia.Controls
///
/// Gets or sets a method to call the initialize the windowing subsystem.
///
- public Action RenderingSubsystemInitializer { get; set; }
+ public Action RenderingSubsystemInitializer { get; private set; }
///
/// Gets or sets the name of the rendering subsystem to use.
///
public string RenderingSubsystemName { get; set; }
+ ///
+ /// Gets or sets a method to call after the is setup.
+ ///
+ public Action AfterSetupCallback { get; private set; } = builder => { };
+
///
/// Gets or sets a method to call before is called on the
/// .
///
- public Action BeforeStartCallback { get; set; }
+ public Action BeforeStartCallback { get; private set; } = builder => { };
protected AppBuilderBase(IRuntimePlatform platform, Action platformSevices)
{
@@ -95,12 +101,13 @@ namespace Avalonia.Controls
/// An instance.
public TAppBuilder BeforeStarting(Action callback)
{
- var oldCallback = BeforeStartCallback;
- BeforeStartCallback = builder =>
- {
- oldCallback?.Invoke(builder);
- callback(builder);
- };
+ BeforeStartCallback = (Action)Delegate.Combine(BeforeStartCallback, callback);
+ return Self;
+ }
+
+ public AppBuilder AfterSetup(Action callback)
+ {
+ AfterSetupCallback = (Action)Delegate.Combine(AfterSetupCallback, callback);
return Self;
}
@@ -112,7 +119,7 @@ namespace Avalonia.Controls
where TMainWindow : Window, new()
{
Setup();
- BeforeStartCallback?.Invoke(Self);
+ BeforeStartCallback(Self);
var window = new TMainWindow();
window.Show();
@@ -175,6 +182,28 @@ namespace Avalonia.Controls
init.Invoke(null, null);
};
+ public TAppBuilder UseAvaloniaModules() => AfterSetup(builder => SetupAvaloniaModules());
+
+ private void SetupAvaloniaModules()
+ {
+ var moduleInitializers = from assembly in AvaloniaLocator.Current.GetService().GetLoadedAssemblies()
+ from attribute in assembly.GetCustomAttributes()
+ where attribute.RequiredWindowingSubsystem == ""
+ || attribute.RequiredWindowingSubsystem == WindowingSubsystemName
+ where attribute.RequiredRenderingSubsystem == ""
+ || attribute.RequiredRenderingSubsystem == RenderingSubsystemName
+ group attribute by attribute.Name into exports
+ select (from export in exports
+ orderby export.RequiredWindowingSubsystem.Length descending
+ orderby export.RequiredRenderingSubsystem.Length descending
+ select export).First().ModuleType into moduleType
+ select (from constructor in moduleType.GetTypeInfo().DeclaredConstructors
+ where constructor.GetParameters().Length == 0 && !constructor.IsStatic
+ select constructor).Single() into constructor
+ select (Action)(() => constructor.Invoke(new object[0]));
+ Delegate.Combine(moduleInitializers.ToArray()).DynamicInvoke();
+ }
+
///
/// Sets up the platform-speciic services for the .
///
@@ -205,6 +234,7 @@ namespace Avalonia.Controls
WindowingSubsystemInitializer();
RenderingSubsystemInitializer();
Instance.Initialize();
+ AfterSetupCallback(Self);
}
}
}
diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj
index 7a04e0747c..1f5fd7c449 100644
--- a/src/Avalonia.Controls/Avalonia.Controls.csproj
+++ b/src/Avalonia.Controls/Avalonia.Controls.csproj
@@ -58,6 +58,7 @@
+
diff --git a/src/Avalonia.Controls/Platform/ExportAvaloniaModuleAttribute.cs b/src/Avalonia.Controls/Platform/ExportAvaloniaModuleAttribute.cs
new file mode 100644
index 0000000000..50e39252e4
--- /dev/null
+++ b/src/Avalonia.Controls/Platform/ExportAvaloniaModuleAttribute.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace Avalonia.Platform
+{
+ [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
+ public class ExportAvaloniaModuleAttribute : Attribute
+ {
+ public ExportAvaloniaModuleAttribute(string name, Type moduleType)
+ {
+ Name = name;
+ ModuleType = moduleType;
+ }
+
+ public string Name { get; private set; }
+ public Type ModuleType { get; private set; }
+
+ public string RequiredWindowingSubsystem { get; set; } = "";
+ public string RequiredRenderingSubsystem { get; set; } = "";
+ }
+}
diff --git a/tests/Avalonia.Controls.UnitTests/AppBuilderTests.cs b/tests/Avalonia.Controls.UnitTests/AppBuilderTests.cs
new file mode 100644
index 0000000000..0838052ffc
--- /dev/null
+++ b/tests/Avalonia.Controls.UnitTests/AppBuilderTests.cs
@@ -0,0 +1,120 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+using Avalonia.Controls.UnitTests;
+using Avalonia.Platform;
+
+[assembly: ExportAvaloniaModule("DefaultModule", typeof(AppBuilderTests.DefaultModule))]
+[assembly: ExportAvaloniaModule("RenderingModule", typeof(AppBuilderTests.Direct2DModule), RequiredRenderingSubsystem = "Direct2D1")]
+[assembly: ExportAvaloniaModule("RenderingModule", typeof(AppBuilderTests.SkiaModule), RequiredRenderingSubsystem = "Skia")]
+[assembly: ExportAvaloniaModule("RenderingModule", typeof(AppBuilderTests.DefaultRenderingModule))]
+
+
+namespace Avalonia.Controls.UnitTests
+{
+
+ public class AppBuilderTests
+ {
+ class App : Application
+ {
+ }
+
+ public class DefaultModule
+ {
+ public static bool IsLoaded = false;
+ public DefaultModule()
+ {
+ IsLoaded = true;
+ }
+ }
+
+ public class DefaultRenderingModule
+ {
+ public static bool IsLoaded = false;
+ public DefaultRenderingModule()
+ {
+ IsLoaded = true;
+ }
+ }
+
+ public class Direct2DModule
+ {
+ public static bool IsLoaded = false;
+ public Direct2DModule()
+ {
+ IsLoaded = true;
+ }
+ }
+
+ public class SkiaModule
+ {
+ public static bool IsLoaded = false;
+ public SkiaModule()
+ {
+ IsLoaded = true;
+ }
+ }
+
+ [Fact]
+ public void LoadsDefaultModule()
+ {
+ ResetModuleLoadStates();
+ AppBuilder.Configure()
+ .UseWindowingSubsystem(() => { })
+ .UseRenderingSubsystem(() => { })
+ .UseAvaloniaModules()
+ .SetupWithoutStarting();
+
+ Assert.True(DefaultModule.IsLoaded);
+ }
+
+ [Fact]
+ public void LoadsRenderingModuleWithMatchingRenderingSubsystem()
+ {
+ ResetModuleLoadStates();
+ var builder = AppBuilder.Configure()
+ .UseWindowingSubsystem(() => { })
+ .UseRenderingSubsystem(() => { });
+ builder.RenderingSubsystemName = "Direct2D1";
+ builder.UseAvaloniaModules().SetupWithoutStarting();
+ Assert.False(DefaultRenderingModule.IsLoaded);
+ Assert.True(Direct2DModule.IsLoaded);
+ Assert.False(SkiaModule.IsLoaded);
+
+ ResetModuleLoadStates();
+ builder = AppBuilder.Configure()
+ .UseWindowingSubsystem(() => { })
+ .UseRenderingSubsystem(() => { });
+ builder.RenderingSubsystemName = "Skia";
+ builder.UseAvaloniaModules().SetupWithoutStarting();
+ Assert.False(DefaultRenderingModule.IsLoaded);
+ Assert.False(Direct2DModule.IsLoaded);
+ Assert.True(SkiaModule.IsLoaded);
+ }
+
+ [Fact]
+ public void LoadsRenderingModuleWithoutDependenciesWhenNoModuleMatches()
+ {
+ ResetModuleLoadStates();
+ var builder = AppBuilder.Configure()
+ .UseWindowingSubsystem(() => { })
+ .UseRenderingSubsystem(() => { });
+ builder.RenderingSubsystemName = "Cairo";
+ builder.UseAvaloniaModules().SetupWithoutStarting();
+ Assert.True(DefaultRenderingModule.IsLoaded);
+ Assert.False(Direct2DModule.IsLoaded);
+ Assert.False(SkiaModule.IsLoaded);
+ }
+
+ private static void ResetModuleLoadStates()
+ {
+ DefaultModule.IsLoaded = false;
+ DefaultRenderingModule.IsLoaded = false;
+ Direct2DModule.IsLoaded = false;
+ SkiaModule.IsLoaded = false;
+ }
+ }
+}
diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj
index e46b43d90e..f9b3b4de96 100644
--- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj
+++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj
@@ -94,6 +94,7 @@
+
@@ -152,6 +153,10 @@
+
+ {4a1abb09-9047-4bd5-a4ad-a055e52c5ee0}
+ Avalonia.DotNetFrameworkRuntime
+
{3e53a01a-b331-47f3-b828-4a5717e77a24}
Avalonia.Markup.Xaml