目录
创建生成依赖项并使用生成后事件复制_framework文件夹
介绍
C#程序(以及用许多其他软件语言编写的程序)现在可以通过WebAssembly(简称Wasm)在几乎任何浏览器中运行。
除了用更好的语言编写之外,WebAssembly程序通常比JavaScript/TypeScript程序运行得更快,因为它们是编译的(至少是中级语言,有些甚至编译为本机汇编语言)。此外,作为强类型语言,它们具有更小、更快的内置类型。
Avalonia
Avalonia是一个出色的多平台框架,用于构建UI应用程序。Avalonia可用于构建适用于Windows、MacOS、Linux的桌面应用程序、浏览器WebAssembly应用程序以及适用于iOS、Android和Tizen的移动应用程序。
最重要的是,Avalonia允许在不同平台之间重用大部分代码。
这里有一个网站,演示了作为Avalonia浏览器内应用程序构建的Avalonia控件: Avalonia演示。
在本文中,我们将仅讨论通过Wasm的浏览器内Avalonia。
为什么“不使用Blazor”
Initial Microsoft’s WebAssembly支持产品称为Blazor(客户端版本),并作为ASP.NET的一部分发布。Blazor可用于执行非可视化操作,也可用于修改C#中的HTML树。
但是,在与HTML/JavaScript交互时,我看到了一些关于Blazor稳定性和性能的抱怨。这是网络上提到 Blazor稳定性和性能问题的一个地方。我记得在其他网站上也看到过类似的投诉。
由于这些问题,本文重点介绍如何使用System.Runtime.InteropServices.JavaScript包,该包已成为.NET的WebAssembly SDK。据报道,该软件包提供了更好的性能和更稳定的与JavaScript的交互。
最新版本的 Avalonia 也使用此库。
WebAssembly的主要问题——缺乏好的样本和好的文档
虽然通过Wasm的C#已经准备好迎接黄金时期,但主要问题是它是一项相对较新的技术,它几乎没有好的样本和良好的在线文档。
本文的主要目的是提供易于理解的示例和良好的文档,涵盖所有或大部分C#浏览器功能。
文章大纲
以下是本文涵盖的主题:
- 创建Wasm项目并将其嵌入到浏览器代码中。
- 从Wasm C#调用浏览器JavaScript方法,反之亦然——从浏览器内JavaScript调用Wasm C#方法。
- 从JavaScript调用C# Program.Main方法。
- 在浏览器中运行Avalonia可视化应用程序。
将ASP.NET Core用于示例
基于浏览器的编程总是意味着一个服务器——在这里的所有示例中,我都使用ASP.NET,因为ASP.NET是Microsoft的一项功能强大、经过充分测试、经过验证的技术,可以与我最喜欢的Visual Studio 2022很好地配合使用,并允许将HTML/JavaScript客户端代码与服务器代码放在同一个项目中。
为了速度和清晰起见,我尽量避免ASP.NET代码生成;相反,我使用ASP.NET作为Web和数据服务器以及中间层。
虽然ASP.NET是我选择的服务器,但部署和运行WebAssembly的完全相同的方法可以应用于任何其他服务器技术。
示例源代码位置
示例的源代码位于Web Assembly示例中。
JavaScript调用C#方法示例
重要提示
我将详细介绍第一个示例,详细解释与WebAssembly相关的几乎所有内容。对于其余的示例,将不会保持这种详细程度(因为到那时你已经了解了WebAssembly是如何工作的)。因此,阅读本节很重要,而其余的示例相关部分您可以根据需要有选择地阅读。
示例位置
此示例位于JSCallingDotNetSample文件夹(其解决方案文件具有相同的名称)中。
示例代码概述
JSCallingDotNetSample解决方案中有两个项目:
- JSCallingDotNetSample——一个ASP.NET 8.0项目。请确保这是您的启动项目。
- Greeter——一个C# .NET 8.0库。
解决方案和项目的创建方式
为了创建解决方案和主ASP.NET项目,我启动了Visual Studio 2022,单击“创建新项目”选项,然后选择“ASP.NET Core Web APP(Razor Pages)”:
然后,我输入了解决方案的名称(“JSCallingDotNetSample”),并通过取消选中“将解决方案和项目放在同一目录中”复选框,确保在项目上方的一个目录(而不是在同一目录中)创建解决方案。
然后,若要创建包含C#代码的项目,我在解决方案资源管理器中右键单击解决方案,选择“添加>新建项目”,然后选择“类库”模板:
请注意,虽然在下面的所有示例中,ASP.NET项目都是以相同的方式创建的,但某些仅限C#的项目将以不同的方式创建。有时我们不得不选择C# 控制台项目(而不是类库)模板,对于Avalonia示例,它会更有趣,当我们进入主题时,我将在下面详细介绍如何创建Avalonia Wasm项目。
请注意,没有项目依赖项——ASP.NET主项目不依赖C#项目。不过稍后我将展示如何在项目之间引入构建依赖关系。
运行项目
为了成功运行项目——首先构建Greeter项目。在项目的bin/Debug/net8.0-browser/wwwroot文件夹下,将创建一个子文件夹_framework:
将此_framework文件夹复制到JSCollingDotNetSample ASP.NET项目的wwwroot文件夹下。
请注意,该文件夹不应成为源代码的一部分(即使它已移动到源代码文件夹下)。如果你使用的是git,你必须将这个文件夹及其内容添加到git .ignore文件中(这是文件夹前面的红色小STOP图标的意思)。
生成并运行主JSCallingDotNetSample项目——首先,在一两秒钟内,您将看到一条消息“请等待Web Assembly正在加载!”,然后它将被C#代码生成的消息覆盖:
仅限C#的Greeter项目
C# Greeter项目包含由JavaScript调用的C#代码(来自ASP.NET项目)。它只有一个静态类JSInteropCallsContainerGreet,并且该类具有一个方法,该方法采用字符串(名称)数组并返回问候语字符串“Hello <name1>,<name2>,...<name_N>!!!“:
public static partial class JSInteropCallsContainer
{
// this simple static method is exported to JavaScript
// via WebAssembly
[JSExport]
public static string Greet(params string[] names)
{
var resultStr = string.Join(", ", names);
// return a string greeting comma separated names passed to it
// e.g. if the array of names contains two names "Joe" and "Jack"
// then the resulting string will be "Hello Joe, Jack!!!".
return $"Hello {resultStr}!!!";
}
}
请注意,该类是静态的和部分的,并且该Greet(...)方法具有JSExport属性——这允许从JavaScript调用该方法。
查看项目文件——Greeter.csproj:
<Project Sdk="Microsoft.NET.Sdk.WebAssembly">
<PropertyGroup>
<TargetFramework>net8.0-browser</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<OutputType>Library</OutputType>
</PropertyGroup>
...
</Project>
请注意与通常的C#库项目突出显示的差异:
- Sdk设置为Microsoft.NET.Sdk.WebAssembly。
- TargetFramework设置为 net8.0-browser。
- <AllowUnsafeBlocks>true</AllowUnsafeBlocks>。
这三项更改允许项目生成(作为构建的结果)包含.wasm和部署所需的其他文件_framework文件夹。
JSCallingDotNet示例代码
在这里,我将介绍使用“ASP.NET Core Web App(Razor Pages)”模板创建项目后对JSCallingDotNetSample ASP.NET项目中的文件所做的更改。
小改动
为了简化起见,我删除了wwwroot/lib文件夹——因为我不打算使用引导程序或jQuery。
我还大大简化了位于Pages/Shared文件夹下的_Layout.cshtml文件,删除了其页脚、页眉和CSS类。
对Program.cs文件的修改
我从文件中删除了一些不需要Progam.cs行,并添加了WebAssembly所需的MIME类型
using Microsoft.AspNetCore.StaticFiles;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
// create a dictionary of mime types to add
var provider = new FileExtensionContentTypeProvider();
var dict = new Dictionary<string, string>
{
{".pdb" , "application/octet-stream" },
{".blat", "application/octet-stream" },
{".dll" , "application/octet-stream" },
{".dat" , "application/octet-stream" },
{".json", "application/json" },
{".wasm", "application/wasm" },
{".symbols", "application/octet-stream" }
};
// add the dictionary entries to the provider
foreach (var kvp in dict)
{
provider.Mappings[kvp.Key] = kvp.Value;
}
// set the provider to contain the added
// mime types
app.UseStaticFiles(new StaticFileOptions
{
ContentTypeProvider = provider
});
app.MapRazorPages();
app.Run();
MIME类型将添加到FileExtensionContentTypeProvider对象中,然后将该对象指定为静态文件的提供程序:
var provider = new FileExtensionContentTypeProvider();
// create a dictionary of mime types to add
var dict = new Dictionary<string, string>
{
{".pdb" , "application/octet-stream" },
{".blat", "application/octet-stream" },
{".dll" , "application/octet-stream" },
{".dat" , "application/octet-stream" },
{".json", "application/json" },
{".wasm", "application/wasm" },
{".symbols", "application/octet-stream" }
};
// add the dictionary entries to the provider
foreach (var kvp in dict)
{
provider.Mappings[kvp.Key] = kvp.Value;
}
// set the provider to contain the added
// mime types
app.UseStaticFiles(new StaticFileOptions
{
ContentTypeProvider = provider
});
添加wasmRunner.js文件
我将wasmRunner.js文件添加到wwwroot文件夹(包含复制的_framework文件夹的同一文件夹)。
以下是文件的内容:
// note that it expects to load dotnet.js
// (and wasm files) from _framework folder
import { dotnet } from './_framework/dotnet.js'
const is_browser = typeof window != "undefined";
if (!is_browser) throw new Error(`Expected to be running in a browser`);
// get the objects needed to run exported C# code
const { getAssemblyExports, getConfig } =
await dotnet.create();
// config contains the web-site configurations
const config = getConfig();
// exports contain the methods exported by C#
const exports = await getAssemblyExports(config.mainAssemblyName);
// we call the exported C# method Greeter.JSInteropCallsContainer.Greet
// passing to it an array of names
const text = exports.Greeter.JSInteropCallsContainer.Greet(['Nick', 'Joe', 'Bob']);
// logging the result of Greet method call
console.log(text);
// adding the result of Greet method call to the inner text of
// an element out id "out"
document.getElementById("out").innerText = text;
该文件在代码中有很好的记录。以下是该文件中最重要的行:
1、我们从./_framework/dotnet.js文件加载dotnet对象。
import { dotnet } from './_framework/dotnet.js'
2、我们从dotnet中创建两个方法——一个返回一个包含所有C#导出到JavaScript的对象,另一个返回WebAssembly项目和网站的配置。
const { getAssemblyExports, getConfig } = await dotnet.create();
3、我们调用getConfig()方法来获取config配置对象:
const config = getConfig();
4、我们使用getAssemblyExport(...)方法获取包含所有 C# 导出的export对象:
const exports = await getAssemblyExports(config.mainAssemblyName);
5、我们调用导出的C# Greeter.JSInteropCallsContainer.Greet(...)方法并将结果保存在text变量中:
const text = exports.Greeter.JSInteropCallsContainer.Greet(['Nick', 'Joe', 'Bob']);
6、最后,我们用id="out"将得到的文本复制为div元素的内部文本,并带有:
document.getElementById("out").innerText = text;
修改Index.cshtml文件
我更改的最后一个文件是Pages/Index.cshtml文件。我将其内容更改为:
<div id="out"
style="font-size:50px">
<div style="font-size:30px">
Please wait while Web Assembly is Loading!
</div>
</div>
<script type="module" src="~/wasmRunner.js"></script>
请注意,它有一个id等于“out”的div元素,其内容将替换为文本。
另请注意,它从wwwroot文件夹加载模块wamsRunner.js(这就是~/的意思)。
提高浏览器内C#的性能
可以通过多种方式提高C#代码的浏览器内性能。最重要的是编译C# AOT(提前)。这可能会增加.wasm文件的大小,但会大大提高性能。
我们的示例演示了如何在发布配置中使用AOT编译。
您可以通过转到命令行上的Greeter项目文件夹并执行以下命令来创建Greeter的发布AOT版本:
dotnet publish -c Release
它将在bin\Release\net8.0-browser\publish\wwwroot文件夹下创建_framework文件夹。
此文件夹_framework将包含.wasm文件的优化版本。以与以前完全相同的方式移动或复制JSCallingDotNetSample/wwwroot文件夹下的此文件夹,现在当您运行项目时,它将加载优化的.wasm文件。
重要提示:项目的AOT编译可能需要大量时间——如果项目足够大,甚至需要10-15分钟。然而,在我们的案例中,我们的项目非常小,AOT生成应该只需要几秒钟。
再看一下Greeter.csproj项目文件。AOT指令包含在以发布配置为条件的PropertyGroup中:
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<RunAOTCompilation>true</RunAOTCompilation>
</PropertyGroup>
创建生成依赖项并使用生成后事件复制_framework文件夹
请注意,在Debug配置中,我们可以将项目Greeter设置为在主JSCallingDotNetSample项目之前生成,而无需设置项目依赖项。
为了正确执行此操作,请在解决方案资源管理器中单击解决方案JSCallingDotNetSample,然后选择“项目依赖项...”菜单项:
在打开的对话框中,选择“项目”下的“JSColldingDotNetSample”,然后在“Depends on”面板中确保选中该复选框。然后按“确定”按钮。
这将确保Greeter项目在主ASP.NET JSCallingDotNetSample项目之前生成。
现在,若要在调试模式下自动复制_framework文件夹,我们将以下行添加到JSColldingDotNetSample.csproj文件的末尾:
<Project Sdk="Microsoft.NET.Sdk.Web">
...
<Target Condition="'$(Configuration)'=='Debug'"
Name="PostBuild"
AfterTargets="PostBuildEvent">
<Exec Command="xcopy "$(SolutionDir)Greeter\bin\$(Configuration)\net8.0-browser\wwwroot\_framework" "$(ProjectDir)wwwroot\_framework" /E /R /Y /I" />
</Target>
</Project>
请注意,这只能对Debug选项执行,因为Release需要对Greeter项目执行发布步骤。当然,也应该可以自动化它,但是在这一点上,我不想花时间弄清楚它。
C#调用JavaScript程序的示例
此示例位于DotNetCallingJSSample文件夹(其解决方案与该文件夹同名)中。它建立在上一个示例之上。
它修改了我们导出的方法
[JSExport]
public static string Greet(params string[] names)
{
...
}
依赖另一种方法
[JSImport("getGreetingWord", "CSharpMethodsJSImplementationsModule")]
public static partial string GetGreetingWord();
其实现是在JavaScript中提供的。
Greeter项目中的JSInteropCallsContainer类有两个方法,而不是一个。
Greeter.GetGreetingWord()是未实现的额外方法。它被标记为partial,并且具有以下JSImport("getGreetingWord", "CSharpMethodsJSImplementationsModule")属性:
public static partial class JSInteropCallsContainer
{
[JSImport("getGreetingWord", "CSharpMethodsJSImplementationsModule")]
public static partial string GetGreetingWord();
...
}
属性参数表示程序期望GetGreetingWord()方法由名为“CSharpMethodsJSImplementationsModule”的JavaScript模块中的JavaScript getGreetingWord()方法实现。有点前瞻性参考,但不是世界末日。
该Greet(params string[] names)方法已稍作修改,以从GetGreetingWord()方法中获取问候词,而不是硬编码为“Hello”:
public static partial class JSInteropCallsContainer
{
[JSImport("getGreetingWord", "CSharpMethodsJSImplementationsModule")]
public static partial string GetGreetingWord();
// this simple static method is exported to JavaScript
// via WebAssembly
[JSExport]
public static string Greet(params string[] names)
{
var resultStr = string.Join(", ", names);
// return a string greeting comma separated names passed to it
// e.g. if the array of names contains two names "Joe" and "Jack"
// then the resulting string will be "Hello Joe, Jack!!!".
return $"{GetGreetingWord()} {resultStr}!!!";
}
}
在主DotNetCallingJSSample项目中更改的唯一文件是wwwroot/wasmRunner.js。它修改了一行,并插入了一个方法调用:
// get the objects needed to run exported C# code
const { getAssemblyExports, getConfig, setModuleImports } =
await dotnet.create();
// we set the module import
setModuleImports("CSharpMethodsJSImplementationsModule", {
getGreetingWord: () => { return "Hi"; }
});
请注意,除了方法getAssemblyExport(...)和getConfig(...)(我们已经在上一个示例中使用过)之外,我们还从await dotnet.create()调用中获取setModuleImports(...)方法。
然后,我们使用setModuleImports(...)方法在“CSharpMethodsJSImplementationsModule”模块中设置getGreetingWord()方法,以始终返回“Hi”。
现在,重新生成主项目DotNetCallingJSSample(强制重建Greeter项目并复制_framework文件夹)并运行它。我们将看到“嗨,尼克,乔,鲍勃!!”——问候语是“嗨”而不是“你好”:
在Web Assembly示例中运行C# Main方法
接下来,我将展示如何从JavaScript运行C# Program.Main方法。相应的示例位于JSCallingCSharpMainMethodSample/JSCallingCSharpMainMethodSample.sln解决方案下。
首先,重新生成并尝试运行主项目JSCallingCSharpMainMethodSample。按F12在浏览器中打开devtools。单击“控制台”选项卡。您将看到打印到控制台的任何内容:
行“Welcome to WebAssembly Program.Main(string[] args)!!”,然后是“Here are the arguments passed to Program.Main:”,最后是 “arg1”、“arg2”和“arg3”都打印在自己的行上。
该解决方案中的Greeter项目只有一个简单的文件C# Program.cs:
namespace Greeter;
public static partial class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Welcome to WebAssembly Program.Main(string[] args)!!!");
if (args.Length > 0)
{
Console.WriteLine();
Console.WriteLine("Here are the arguments passed to Program.Main:");
foreach(string arg in args)
{
Console.WriteLine($"\t{arg}");
}
}
}
}
它将打印到控制台“Welcome to WebAssembly Program.Main(string[] args)!!”,然后如果有一些参数传递给main,它将打印行“Here are the arguments passed to Program.Main:”,然后它将在自己的行上打印每个参数。
现在看一下Greeter.csproj文件:
<Project Sdk="Microsoft.NET.Sdk.WebAssembly">
<PropertyGroup>
<TargetFramework>net8.0-browser</TargetFramework>
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<OutputType>Exe</OutputType>
<StartupObject>Greeter.Program</StartupObject>
</PropertyGroup>
...
</Project>
注意——我们添加了<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>,我们将OutputType更改为“Exe”,并添加了StartupObject行
在JSCallingCSharpMainMethodSample(main)项目中,唯一更改的文件是wasmRunner.js:
// note that it expects to load dotnet.js
// (and wasm files) from _framework folder
import { dotnet } from './_framework/dotnet.js'
const is_browser = typeof window != "undefined";
if (!is_browser) throw new Error(`Expected to be running in a browser`);
// get the dotnetRuntime containing all the methods
// and objects for invoking C# from JavaScript and
// vice versa.
const dotnetRuntime = await dotnet.create();
// config contains the web-site configurations
const config = dotnetRuntime.getConfig();
// call Program.Main(string[] args) method from JavaScript
// passing to it an array of arguments "arg1", "arg2" and "arg3"
我们从await dotnet.create()获得dotnetRuntime并且然后调用它的dotnetRuntime.runMain(...)方法来调用C#的Program.Main(...)方法:
...
const dotnetRuntime = await dotnet.create();
...
await dotnetRuntime.runMain(config.mainAssemblyName, ["arg1", "arg2", "arg3"]);
请注意,传递给dotnetRuntime.runMain(...)的第二个参数应为字符串数组。这些是将传递给Program.Main(string[] args)作为args的字符串。这就是程序将“arg1”、“arg2”和“arg3”打印到控制台的原因。如果更改该数组中的参数,则会在程序输出中看到相应的更改。
通过WebAssembly在浏览器中运行Avalonia
浏览器中Avalonia的小介绍
Avalonia可以在许多平台上运行,包括通过WebAssembly在浏览器内运行。
浏览器示例项目中的Avalonia位于AvaInBrowserSample/AvaInBrowserSample.sln解决方案下。
打开解决方案,使AvaInBrowserSample ASP.NET项目成为启动项目。
运行项目
尝试重新生成AvaInBrowserSample项目,然后在调试器中运行它。几秒钟后,Avalonia应用程序将出现在浏览器中:
当您按下“更改文本”按钮时,上述短语的第一个单词在“您好”和“嗨”之间切换,而文本的其余部分保持不变。
Avalonia代码非常简单——自定义代码仅位于MainView.xaml和MainView.xaml.cs文件中。
按钮的回调会触发TextBox内的文本的更改:
private void ChangeTextButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (GreetingTextBlock.Text?.StartsWith("Hello") == true)
{
GreetingTextBlock.Text = "Hi from Avalonia in-Browser!!!";
}
else
{
GreetingTextBlock.Text = "Hello from Avalonia in-Browser!!!";
}
}
创建客户端C# Avalonia项目
在这里,我将展示如何使用Avalonia模板创建Avalonia-in-Browser项目。
请注意,我假设我们位于解决方案文件夹——AvaInBrowserSample中。
要创建一个Avalonia WebAssembly项目,我使用创建Avalonia Web Assembly项目中的说明:
1、我通过运行来安装wasm-tools(或确保它们已安装并且是最新的)
dotnet workload install wasm-tools
从命令行。
2、我通过运行命令更新到最新的Avalonia dotnet模板:
dotnet new install avalonia.templates
3、我为Avalonia项目创建文件夹AvaCode,并使用命令行将其cd到它。
4、在该文件夹中,我从命令行运行:
dotnet new avalonia.xplat
5、这将创建共享项目AvaCode(在同名文件夹中)和许多特定于平台的项目。
6、我删除了大多数特定于平台的项目,只留下AvaCode.Browser(用于构建Avalonia WebAssembly包)和AvaCode.Display(用于调试和更快的原型设计,如果需要)。
然后,我使用Visual Studio将这三个项目添加到我的AvaInBrowserSample解决方案中。我将这些项目放在一个单独的解决方案文件夹AvaCode中:
请注意,3个Avalonia项目位于AvaCode Solution文件夹中图像的顶部。
现在,我可以构建我的Avalonia功能(在AvaCode项目中),并通过从AvaCode.Desktop项目运行它来测试它。
我还可以通过将目录更改为AvaCode.Browser项目并在命令行上执行命令“dotnet run”来在浏览器中对其进行测试。
对主项目的更改
为了使我的主ASP.NET项目显示Avalonia的MainView,我将app.css文件从AvaCode.Browser/wwwroot文件夹复制到ASP.NET项目的AvaInBrowserSample/wwwroot/css/文件夹中。
然后,我修改了_Layout.cshtml文件,使其具有指向此app.css文件的链接,然后我添加了包含一些魔术词的样式:style="margin: 0px; overflow: hidden"是<body>标记,并简化了<body>标记内的区域,以确保@RenderBody()调用直接位于标记下方:
<body style="margin: 0px; overflow: hidden">
@RenderBody()
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
现在我们需要修改wasmRunner.js文件。它看起来与上一节中的几乎相同,但在dotnet.和.create()方法之间会有一些额外的调用:
import { dotnet } from './_framework/dotnet.js'
const is_browser = typeof window != "undefined";
if (!is_browser) throw new Error(`Expected to be running in a browser`);
const dotnetRuntime = await dotnet
.withDiagnosticTracing(false) // some extra methods
.withApplicationArgumentsFromQuery() // some extra methods
.create();
const config = dotnetRuntime.getConfig();
await dotnetRuntime.runMain(config.mainAssemblyName, [window.location.search]);
要更改的最后一个文件是Index.cshtml。此文件使用我们从AvaCode.Browser项目复制app.css文件中定义的一些CSS类。如果没有这些CSS类,Avalonia不会占用浏览器的正确空间(所有空间):
<div id="out">
<div id="out">
<div id="avalonia-splash">
<div class="center">
<h2 class="purple">
Please wait while the Avalonia Application Loads
</h2>
</div>
</div>
</div>
</div>
<script type="module" src="~/wasmRunner.js"></script>
结论
本文介绍如何将C# .NET代码嵌入到浏览器中,并提供一些易于理解的示例。
https://www.codeproject.com/Articles/5382292/Csharp-in-Browser-via-WebAssembly-without-Blazor