diff --git a/samples/ControlCatalog.Browser.Blazor/App.razor.cs b/samples/ControlCatalog.Browser.Blazor/App.razor.cs index f38db2b055..c331625664 100644 --- a/samples/ControlCatalog.Browser.Blazor/App.razor.cs +++ b/samples/ControlCatalog.Browser.Blazor/App.razor.cs @@ -1,3 +1,5 @@ +using System; +using System.Threading.Tasks; using Avalonia; using Avalonia.Browser.Blazor; @@ -5,13 +7,4 @@ namespace ControlCatalog.Browser.Blazor; public partial class App { - protected override void OnParametersSet() - { - AppBuilder.Configure() - .UseBlazor() - // .With(new SkiaOptions { CustomGpuFactory = null }) // uncomment to disable GPU/GL rendering - .SetupWithSingleViewLifetime(); - - base.OnParametersSet(); - } } diff --git a/samples/ControlCatalog.Browser.Blazor/Program.cs b/samples/ControlCatalog.Browser.Blazor/Program.cs index eb99ca518e..500055b405 100644 --- a/samples/ControlCatalog.Browser.Blazor/Program.cs +++ b/samples/ControlCatalog.Browser.Blazor/Program.cs @@ -1,6 +1,8 @@ using System; using System.Net.Http; using System.Threading.Tasks; +using Avalonia; +using Avalonia.Browser.Blazor; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.Extensions.DependencyInjection; using ControlCatalog.Browser.Blazor; @@ -9,9 +11,17 @@ public class Program { public static async Task Main(string[] args) { - await CreateHostBuilder(args).Build().RunAsync(); + var host = CreateHostBuilder(args).Build(); + await StartAvaloniaApp(); + await host.RunAsync(); } + public static async Task StartAvaloniaApp() + { + await AppBuilder.Configure() + .StartBlazorApp(); + } + public static WebAssemblyHostBuilder CreateHostBuilder(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); diff --git a/samples/ControlCatalog.Browser/Program.cs b/samples/ControlCatalog.Browser/Program.cs index 53b7c60a6f..4a4d8c7bb8 100644 --- a/samples/ControlCatalog.Browser/Program.cs +++ b/samples/ControlCatalog.Browser/Program.cs @@ -1,6 +1,8 @@ using System.Runtime.Versioning; +using System.Threading.Tasks; using Avalonia; using Avalonia.Browser; +using Avalonia.Controls; using ControlCatalog; using ControlCatalog.Browser; @@ -8,15 +10,27 @@ using ControlCatalog.Browser; internal partial class Program { - private static void Main(string[] args) + public static async Task Main(string[] args) { - BuildAvaloniaApp() + await BuildAvaloniaApp() .AfterSetup(_ => { ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb(); - }).SetupBrowserApp("out"); + }) + .StartBrowserApp("out"); } + // Example without a ISingleViewApplicationLifetime + // private static AvaloniaView _avaloniaView; + // public static async Task Main(string[] args) + // { + // await BuildAvaloniaApp() + // .SetupBrowserApp(); + // + // _avaloniaView = new AvaloniaView("out"); + // _avaloniaView.Content = new TextBlock { Text = "Hello world" }; + // } + public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure(); } diff --git a/samples/ControlCatalog.Browser/main.js b/samples/ControlCatalog.Browser/main.js index 87f8a4f943..9d90db8bd2 100644 --- a/samples/ControlCatalog.Browser/main.js +++ b/samples/ControlCatalog.Browser/main.js @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. import { dotnet } from './dotnet.js' -import { registerAvaloniaModule } from './avalonia.js'; const is_browser = typeof window != "undefined"; if (!is_browser) throw new Error(`Expected to be running in a browser`); @@ -12,8 +11,6 @@ const dotnetRuntime = await dotnet .withApplicationArgumentsFromQuery() .create(); -await registerAvaloniaModule(dotnetRuntime); - const config = dotnetRuntime.getConfig(); await dotnetRuntime.runMainAndExit(config.mainAssemblyName, ["dotnet", "is", "great!"]); diff --git a/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs index b5e7298b7e..08fcdb50aa 100644 --- a/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs +++ b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs @@ -37,7 +37,7 @@ namespace Avalonia.Platform }; } - public event EventHandler? ColorValuesChanged; + public virtual event EventHandler? ColorValuesChanged; protected void OnColorValuesChanged(PlatformColorValues colorValues) { diff --git a/src/Browser/Avalonia.Browser.Blazor/AvaloniaView.cs b/src/Browser/Avalonia.Browser.Blazor/AvaloniaView.cs index 68efea31d6..1fc87fed2f 100644 --- a/src/Browser/Avalonia.Browser.Blazor/AvaloniaView.cs +++ b/src/Browser/Avalonia.Browser.Blazor/AvaloniaView.cs @@ -30,12 +30,10 @@ public class AvaloniaView : ComponentBase builder.CloseElement(); } - protected override async Task OnInitializedAsync() + protected override void OnAfterRender(bool firstRender) { - if (OperatingSystem.IsBrowser()) + if (firstRender) { - await AvaloniaModule.ImportMain(); - _browserView = new Browser.AvaloniaView(_containerId); if (Application.Current?.ApplicationLifetime is ISingleViewApplicationLifetime lifetime) { @@ -43,4 +41,12 @@ public class AvaloniaView : ComponentBase } } } + + protected override void OnInitialized() + { + if (!OperatingSystem.IsBrowser()) + { + throw new NotSupportedException("Avalonia doesn't support server-side Blazor"); + } + } } diff --git a/src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs b/src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs index a7bb5a62df..1f62690aff 100644 --- a/src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs +++ b/src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs @@ -1,33 +1,28 @@ -using System.Runtime.Versioning; - +using System; +using System.Runtime.Versioning; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Browser.Interop; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; namespace Avalonia.Browser.Blazor; -public static class WebAppBuilder +public static class BlazorAppBuilder { - public static AppBuilder SetupWithSingleViewLifetime( - this AppBuilder builder) + /// + /// Configures blazor backend, loads avalonia javascript modules and creates a single view lifetime. + /// + /// Application builder. + /// Browser backend specific options. + public static async Task StartBlazorApp(this AppBuilder builder, BrowserPlatformOptions? options = null) { - return builder.SetupWithLifetime(new BlazorSingleViewLifetime()); - } + options ??= new BrowserPlatformOptions(); + options.FrameworkAssetPathResolver ??= filePath => $"/_content/Avalonia.Browser.Blazor/{filePath}"; - public static AppBuilder UseBlazor(this AppBuilder builder) - { - return builder - .UseBrowser() - .With(new BrowserPlatformOptions - { - FrameworkAssetPathResolver = new(filePath => $"/_content/Avalonia.Browser.Blazor/{filePath}") - }); - } + builder = await BrowserAppBuilder.PreSetupBrowser(builder, options); - public static AppBuilder Configure() - where TApp : Application, new() - { - return AppBuilder.Configure() - .UseBlazor(); + builder.SetupWithLifetime(new BlazorSingleViewLifetime()); } internal class BlazorSingleViewLifetime : ISingleViewApplicationLifetime diff --git a/src/Browser/Avalonia.Browser/AvaloniaView.cs b/src/Browser/Avalonia.Browser/AvaloniaView.cs index 3f4aa0d0ba..775100d76b 100644 --- a/src/Browser/Avalonia.Browser/AvaloniaView.cs +++ b/src/Browser/Avalonia.Browser/AvaloniaView.cs @@ -20,7 +20,7 @@ using static System.Runtime.CompilerServices.RuntimeHelpers; namespace Avalonia.Browser { - public partial class AvaloniaView : ITextInputMethodImpl + public class AvaloniaView : ITextInputMethodImpl { private static readonly PooledList s_intermediatePointsPooledList = new(ClearMode.Never); private readonly BrowserTopLevelImpl _topLevelImpl; @@ -43,8 +43,9 @@ namespace Avalonia.Browser private bool _useGL; private ITextInputMethodClient? _client; + /// ID of the html element where avalonia content should be rendered. public AvaloniaView(string divId) - : this(DomHelper.GetElementById(divId) ?? throw new Exception($"Element with id {divId} was not found in the html document.")) + : this(DomHelper.GetElementById(divId) ?? throw new Exception($"Element with id '{divId}' was not found in the html document.")) { } diff --git a/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs b/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs new file mode 100644 index 0000000000..866c8ceca4 --- /dev/null +++ b/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs @@ -0,0 +1,83 @@ +using System; +using System.Threading.Tasks; +using Avalonia.Browser.Interop; + +namespace Avalonia.Browser; + +public class BrowserPlatformOptions +{ + /// + /// Defines paths where avalonia modules and service locator should be resolved. + /// If null, default path resolved depending on the backend (browser or blazor) is used. + /// + public Func? FrameworkAssetPathResolver { get; set; } +} + +public static class BrowserAppBuilder +{ + /// + /// Configures browser backend, loads avalonia javascript modules and creates a single view lifetime from the passed parameter. + /// + /// Application builder. + /// ID of the html element where avalonia content should be rendered. + /// Browser backend specific options. + public static async Task StartBrowserApp(this AppBuilder builder, string mainDivId, BrowserPlatformOptions? options = null) + { + if (mainDivId is null) + { + throw new ArgumentNullException(nameof(mainDivId)); + } + + builder = await PreSetupBrowser(builder, options); + + var lifetime = new BrowserSingleViewLifetime(); + builder + .AfterSetup(_ => + { + lifetime.View = new AvaloniaView(mainDivId); + }) + .SetupWithLifetime(lifetime); + } + + /// + /// Loads avalonia javascript modules and configures browser backend. + /// + /// Application builder. + /// Browser backend specific options. + /// + /// This method doesn't creates any avalonia views to be rendered. To do so create an object. + /// Alternatively, you can call method instead of . + /// + public static async Task SetupBrowserApp(this AppBuilder builder, BrowserPlatformOptions? options = null) + { + builder = await PreSetupBrowser(builder, options); + + builder + .SetupWithoutStarting(); + } + + internal static async Task PreSetupBrowser(AppBuilder builder, BrowserPlatformOptions? options) + { + options ??= new BrowserPlatformOptions(); + options.FrameworkAssetPathResolver ??= fileName => $"./{fileName}"; + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(options); + + await AvaloniaModule.ImportMain(); + + if (builder.WindowingSubsystemInitializer is null) + { + builder = builder.UseBrowser(); + } + + return builder; + } + + public static AppBuilder UseBrowser( + this AppBuilder builder) + { + return builder + .UseWindowingSubsystem(BrowserWindowingPlatform.Register) + .UseSkia(); + } +} diff --git a/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs b/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs index 6084c5c7de..fa647d31b7 100644 --- a/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs +++ b/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs @@ -1,4 +1,5 @@ -using Avalonia.Browser.Interop; +using System; +using Avalonia.Browser.Interop; using Avalonia.Platform; namespace Avalonia.Browser; @@ -7,25 +8,44 @@ internal class BrowserPlatformSettings : DefaultPlatformSettings { private bool _isDarkMode; private bool _isHighContrast; - - public BrowserPlatformSettings() + private bool _isInitialized; + + public override event EventHandler? ColorValuesChanged { - var obj = DomHelper.ObserveDarkMode((isDarkMode, isHighContrast) => + add { - _isDarkMode = isDarkMode; - _isHighContrast = isHighContrast; - OnColorValuesChanged(GetColorValues()); - }); - _isDarkMode = obj.GetPropertyAsBoolean("isDarkMode"); - _isHighContrast = obj.GetPropertyAsBoolean("isHighContrast"); + EnsureBackend(); + base.ColorValuesChanged += value; + } + remove => base.ColorValuesChanged -= value; } public override PlatformColorValues GetColorValues() { + EnsureBackend(); + return base.GetColorValues() with { ThemeVariant = _isDarkMode ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light, ContrastPreference = _isHighContrast ? ColorContrastPreference.High : ColorContrastPreference.NoPreference }; } + + private void EnsureBackend() + { + if (!_isInitialized) + { + // WASM module has async nature of initialization. We can't native code right away during components registration. + _isInitialized = true; + + var obj = DomHelper.ObserveDarkMode((isDarkMode, isHighContrast) => + { + _isDarkMode = isDarkMode; + _isHighContrast = isHighContrast; + OnColorValuesChanged(GetColorValues()); + }); + _isDarkMode = obj.GetPropertyAsBoolean("isDarkMode"); + _isHighContrast = obj.GetPropertyAsBoolean("isHighContrast"); + } + } } diff --git a/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs b/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs index add69760ee..6fa79f6f54 100644 --- a/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs +++ b/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs @@ -1,47 +1,36 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using System.Runtime.Versioning; +using Avalonia.Browser; -namespace Avalonia.Browser; +namespace Avalonia; -public class BrowserSingleViewLifetime : ISingleViewApplicationLifetime +internal class BrowserSingleViewLifetime : ISingleViewApplicationLifetime { public AvaloniaView? View; public Control? MainView { - get => View!.Content; - set => View!.Content = value; - } -} - -public class BrowserPlatformOptions -{ - public Func FrameworkAssetPathResolver { get; set; } = new(fileName => $"./{fileName}"); -} - -public static class WebAppBuilder -{ - public static AppBuilder SetupBrowserApp( - this AppBuilder builder, string mainDivId) - { - var lifetime = new BrowserSingleViewLifetime(); - - return builder - .UseBrowser() - .AfterSetup(b => - { - lifetime.View = new AvaloniaView(mainDivId); - }) - .SetupWithLifetime(lifetime); + get + { + EnsureView(); + return View.Content; + } + set + { + EnsureView(); + View.Content = value; + } } - public static AppBuilder UseBrowser( - this AppBuilder builder) + [MemberNotNull(nameof(View))] + private void EnsureView() { - return builder - .UseWindowingSubsystem(BrowserWindowingPlatform.Register) - .UseSkia(); + if (View is null) + { + throw new InvalidOperationException("Browser lifetime was not initialized. Make sure AppBuilder.StartBrowserApp was called."); + } } } diff --git a/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs b/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs index b283fbaa56..f1936a8d97 100644 --- a/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs +++ b/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs @@ -11,13 +11,13 @@ internal static partial class AvaloniaModule public static Task ImportMain() { var options = AvaloniaLocator.Current.GetService() ?? new BrowserPlatformOptions(); - return JSHost.ImportAsync(MainModuleName, options.FrameworkAssetPathResolver("avalonia.js")); + return JSHost.ImportAsync(MainModuleName, options.FrameworkAssetPathResolver!("avalonia.js")); } public static Task ImportStorage() { var options = AvaloniaLocator.Current.GetService() ?? new BrowserPlatformOptions(); - return JSHost.ImportAsync(StorageModuleName, options.FrameworkAssetPathResolver("storage.js")); + return JSHost.ImportAsync(StorageModuleName, options.FrameworkAssetPathResolver!("storage.js")); } [JSImport("Caniuse.isMobile", AvaloniaModule.MainModuleName)] diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts index ab0c85eaa2..3fb4124c96 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts @@ -1,4 +1,3 @@ -import { RuntimeAPI } from "../types/dotnet"; import { SizeWatcher, DpiWatcher, Canvas } from "./avalonia/canvas"; import { InputHelper } from "./avalonia/input"; import { AvaloniaDOM } from "./avalonia/dom"; @@ -7,19 +6,6 @@ import { StreamHelper } from "./avalonia/stream"; import { NativeControlHost } from "./avalonia/nativeControlHost"; import { NavigationHelper } from "./avalonia/navigationHelper"; -async function registerAvaloniaModule(api: RuntimeAPI): Promise { - api.setModuleImports("avalonia", { - Caniuse, - Canvas, - InputHelper, - SizeWatcher, - DpiWatcher, - AvaloniaDOM, - StreamHelper, - NativeControlHost, - NavigationHelper - }); -} export { Caniuse, Canvas, @@ -29,7 +15,5 @@ export { AvaloniaDOM, StreamHelper, NativeControlHost, - NavigationHelper, - - registerAvaloniaModule + NavigationHelper };