From 9a18ef0afaa03b423982f104813d1032a819b9a6 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 30 Aug 2016 00:12:42 -0500 Subject: [PATCH] Added support for auto-loading "Avalonia modules", which are classes that are auto-initialized via the instance constructor. --- src/Avalonia.Controls/AppBuilderBase.cs | 56 ++++++-- .../Avalonia.Controls.csproj | 1 + .../Platform/ExportAvaloniaModuleAttribute.cs | 20 +++ .../AppBuilderTests.cs | 120 ++++++++++++++++++ .../Avalonia.Controls.UnitTests.csproj | 5 + 5 files changed, 189 insertions(+), 13 deletions(-) create mode 100644 src/Avalonia.Controls/Platform/ExportAvaloniaModuleAttribute.cs create mode 100644 tests/Avalonia.Controls.UnitTests/AppBuilderTests.cs 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