Browse Source

Refactor Browser and Blazor startup code

pull/10343/head
Max Katz 3 years ago
parent
commit
bd1928efac
  1. 11
      samples/ControlCatalog.Browser.Blazor/App.razor.cs
  2. 12
      samples/ControlCatalog.Browser.Blazor/Program.cs
  3. 20
      samples/ControlCatalog.Browser/Program.cs
  4. 3
      samples/ControlCatalog.Browser/main.js
  5. 2
      src/Avalonia.Base/Platform/DefaultPlatformSettings.cs
  6. 14
      src/Browser/Avalonia.Browser.Blazor/AvaloniaView.cs
  7. 37
      src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs
  8. 5
      src/Browser/Avalonia.Browser/AvaloniaView.cs
  9. 83
      src/Browser/Avalonia.Browser/BrowserAppBuilder.cs
  10. 40
      src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs
  11. 51
      src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs
  12. 4
      src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs
  13. 18
      src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts

11
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<ControlCatalog.App>()
.UseBlazor()
// .With(new SkiaOptions { CustomGpuFactory = null }) // uncomment to disable GPU/GL rendering
.SetupWithSingleViewLifetime();
base.OnParametersSet();
}
}

12
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<ControlCatalog.App>()
.StartBlazorApp();
}
public static WebAssemblyHostBuilder CreateHostBuilder(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);

20
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<App>();
}

3
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!"]);

2
src/Avalonia.Base/Platform/DefaultPlatformSettings.cs

@ -37,7 +37,7 @@ namespace Avalonia.Platform
};
}
public event EventHandler<PlatformColorValues>? ColorValuesChanged;
public virtual event EventHandler<PlatformColorValues>? ColorValuesChanged;
protected void OnColorValuesChanged(PlatformColorValues colorValues)
{

14
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");
}
}
}

37
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)
/// <summary>
/// Configures blazor backend, loads avalonia javascript modules and creates a single view lifetime.
/// </summary>
/// <param name="builder">Application builder.</param>
/// <param name="options">Browser backend specific options.</param>
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<TApp>()
where TApp : Application, new()
{
return AppBuilder.Configure<TApp>()
.UseBlazor();
builder.SetupWithLifetime(new BlazorSingleViewLifetime());
}
internal class BlazorSingleViewLifetime : ISingleViewApplicationLifetime

5
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<RawPointerPoint> s_intermediatePointsPooledList = new(ClearMode.Never);
private readonly BrowserTopLevelImpl _topLevelImpl;
@ -43,8 +43,9 @@ namespace Avalonia.Browser
private bool _useGL;
private ITextInputMethodClient? _client;
/// <param name="divId">ID of the html element where avalonia content should be rendered.</param>
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."))
{
}

83
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
{
/// <summary>
/// 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.
/// </summary>
public Func<string, string>? FrameworkAssetPathResolver { get; set; }
}
public static class BrowserAppBuilder
{
/// <summary>
/// Configures browser backend, loads avalonia javascript modules and creates a single view lifetime from the passed <see cref="mainDivId"/> parameter.
/// </summary>
/// <param name="builder">Application builder.</param>
/// <param name="mainDivId">ID of the html element where avalonia content should be rendered.</param>
/// <param name="options">Browser backend specific options.</param>
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);
}
/// <summary>
/// Loads avalonia javascript modules and configures browser backend.
/// </summary>
/// <param name="builder">Application builder.</param>
/// <param name="options">Browser backend specific options.</param>
/// <remarks>
/// This method doesn't creates any avalonia views to be rendered. To do so create an <see cref="AvaloniaView"/> object.
/// Alternatively, you can call <see cref="StartBrowserApp"/> method instead of <see cref="SetupBrowserApp"/>.
/// </remarks>
public static async Task SetupBrowserApp(this AppBuilder builder, BrowserPlatformOptions? options = null)
{
builder = await PreSetupBrowser(builder, options);
builder
.SetupWithoutStarting();
}
internal static async Task<AppBuilder> PreSetupBrowser(AppBuilder builder, BrowserPlatformOptions? options)
{
options ??= new BrowserPlatformOptions();
options.FrameworkAssetPathResolver ??= fileName => $"./{fileName}";
AvaloniaLocator.CurrentMutable.Bind<BrowserPlatformOptions>().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();
}
}

40
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<PlatformColorValues>? 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");
}
}
}

51
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<string, string> 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.");
}
}
}

4
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<BrowserPlatformOptions>() ?? 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<BrowserPlatformOptions>() ?? new BrowserPlatformOptions();
return JSHost.ImportAsync(StorageModuleName, options.FrameworkAssetPathResolver("storage.js"));
return JSHost.ImportAsync(StorageModuleName, options.FrameworkAssetPathResolver!("storage.js"));
}
[JSImport("Caniuse.isMobile", AvaloniaModule.MainModuleName)]

18
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<void> {
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
};

Loading…
Cancel
Save